Docebo MCP OAuth2 代理服务器
多租户MCP(模型上下文协议)服务器,作为Docebo的OAuth2授权服务器代理。类似于 mcp.zapier.com这台服务器使MCP客户端能够通过单一终端点发现并与多个Docebo租户进行身份验证。
特点/功能
- OAuth2 代理作为授权服务器,代理Docebo租户
- 多租户支持使用独立凭据管理多个Docebo租户
- 符合RFC标准实现了RFC 8414(授权服务器元数据)和RFC 9728(受保护资源元数据)
- 自动发现MCP客户端自动发现OAuth2端点
- 无状态的没有会话存储,所有状态都在OAuth参数中
- MCP协议最新版本 2025-03-26
建筑学
MCP Client (Claude, ChatGPT, MCP Inspector, etc.)
↓
↓ 1. Discovery: GET /mcp/riccardo-lr-test/.well-known/oauth-authorization-server
↓
mcp.docebosaas.com (This Server)
↓ Returns: tenant-specific endpoints
↓ authorization_endpoint: /mcp/riccardo-lr-test/oauth2/authorize
↓ token_endpoint: /mcp/riccardo-lr-test/oauth2/token
↓
↓ 2. OAuth Flow: GET /mcp/riccardo-lr-test/oauth2/authorize
↓ Server extracts tenant from URL path
↓ Redirects to:
↓
riccardo-lr-test.docebosaas.com/oauth2/authorize
↓
↓ 3. User authorizes, callback with code
↓
↓ 4. Token Exchange: POST /mcp/riccardo-lr-test/oauth2/token
↓ Server extracts tenant from URL path
↓ Overrides redirect_uri with tenant's configured value
↓ Injects tenant credentials (client_id, client_secret)
↓ Proxies to:
↓
riccardo-lr-test.docebosaas.com/oauth2/token
↓ Returns access_token
↓
↓ 5. MCP Call: POST /mcp/riccardo-lr-test
↓ With: Authorization: Bearer
↓ Server calls:
↓
riccardo-lr-test.docebosaas.com/manage/v1/*
↓ Returns data to client先决条件
- Node.js 22.x 或更高版本
- 已配置OAuth2应用程序的Docebo学习管理系统(LMS)实例
- 每个租户的OAuth2客户端凭据
- (可选)使用 ngrok 进行本地测试
安装
npm install配置
1. 服务器配置
复制示例环境文件:
cp .env.example .env编辑 .env:
# Server public URL (use ngrok URL for local testing)
SERVER_PUBLIC_URL=https://mcp.docebosaas.com
# Or for local testing:
# SERVER_PUBLIC_URL=https://abc123.ngrok.io
PORT=3000
ALLOWED_ORIGINS=*
ALLOW_LOCAL_DEV=true2. 租户配置
为每个Docebo租户添加凭据:
# Format: TENANT_{UPPERCASE_TENANT_ID}_CLIENT_ID
# TENANT_{UPPERCASE_TENANT_ID}_CLIENT_SECRET
# TENANT_{UPPERCASE_TENANT_ID}_REDIRECT_URI
# Example: Tenant "riccardo-lr-test"
TENANT_RICCARDO_LR_TEST_CLIENT_ID=my-mcp-server
TENANT_RICCARDO_LR_TEST_CLIENT_SECRET=abc123secret...
TENANT_RICCARDO_LR_TEST_REDIRECT_URI=https://mcp.docebosaas.com/oauth/callback
# Example: Tenant "acme-corp"
TENANT_ACME_CORP_CLIENT_ID=acme-oauth-app
TENANT_ACME_CORP_CLIENT_SECRET=xyz789secret...
TENANT_ACME_CORP_REDIRECT_URI=https://mcp.docebosaas.com/oauth/callback重要的这个(或:那) REDIRECT_URI 必须与在Docebo OAuth2应用中注册的内容相匹配。在令牌交换过程中,服务器将用此值覆盖客户端的redirect_uri,以确保与MCP客户端的兼容性。
注租户ID格式转换:
- URL格式:
riccardo-lr-test - 环境变量格式:
RICCARDO_LR_TEST - 服务器自动进行格式转换
3. Docebo OAuth2 应用设置
为每个租户在Docebo中创建一个OAuth2应用程序:
- 登录到Docebo租户管理员界面
- 首选 管理菜单 → API与SSO → API 凭据
- 点击 添加OAuth2应用程序
- 配置:
- 客户端ID选择一个名称(例如,“mcp-server”) - 客户端密钥由Docebo自动生成 - 授权类型选择“授权码”和“刷新令牌” - 重定向URL: https://mcp.docebosaas.com/callback (或您的公开网址) - 范围(或作用域): api
- 保存并复制客户端ID和客户端密钥
- 添加到
.env使用上述格式保存文件
动态客户端注册(DCR)- 试点实施
⚠️ 警告:这是一个仅供测试的概念验证实现。不适用于生产环境,存在安全隐患。
这个服务器实现了 虚拟DCR层 这允许OAuth客户端(如MCP Inspector、ChatGPT、Claude Desktop)动态地“注册”自己,尽管Docebo本身并不原生支持RFC 7591动态客户端注册协议。
它是如何运作的
- 客户端注册MCP客户端调用
POST /mcp//oauth2/register带有客户端元数据 - 虚拟客户端创建服务器生成一个虚拟
client_id(通用唯一识别码) 并将映射存储在virtual-clients.txt - 凭证返回服务器返回虚拟(数据/结果)
client_id并且client_secret给客户 - OAuth 流程客户端在OAuth授权/令牌请求中使用虚拟凭证
- 凭证翻译服务器将虚拟凭据转换为真实租户凭据
- Docebo 代理所有请求均通过真实预配置的凭据代理到Docebo
示例 DCR 流程
# 1. Register a new client
curl -X POST 'https://abc123.ngrok.io/mcp/riccardo-lr-test/oauth2/register' \
-H 'Content-Type: application/json' \
-d '{
"client_name": "My MCP Client",
"redirect_uris": ["http://localhost:8080/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"]
}'
# Response:
{
"client_id": "550e8400-e29b-41d4-a716-446655440000",
"client_secret": "abc123...",
"client_id_issued_at": 1234567890,
...
}
# 2. Use virtual client_id in OAuth flow
# The server will automatically translate to real tenant credentials存储格式
虚拟客户端映射存储在 virtual-clients.txt (明文,已加入.gitignore忽略):
# Format: virtual_client_id|tenant_id|created_at|client_name|redirect_uris
550e8400-e29b-41d4-a716-446655440000|riccardo-lr-test|2025-10-22T10:00:00Z|My MCP Client|http://localhost:8080/callback安全限制(⚠️ 仅限概念验证)
不要在生产环境中使用此实现。 它存在严重的安全限制:
- ❌(表示错误或否定的符号) 明文存储虚拟客户端映射存储在未加密的文本文件中
- ❌(表示错误或否定) 无需认证任何人都可以注册客户端(无需API密钥,无需验证)
- ❌(这个符号在中文中通常表示“错误”或“取消”,没有直接的中文翻译,但根据上下文可以理解为“错误”或“取消”的意思。) 无速率限制易受滥用和拒绝服务(DoS)攻击
- ❌(这个符号在中文中通常表示“错误”或“不正确”,没有直接对应的中文文字,但在语境中可以理解为“错误”或“不对”的意思。) 无客户端撤销无法撤销或删除虚拟客户端
- ❌(这个符号在中文中通常表示“错误”或“不正确”,但直接翻译时,由于其是一个符号而非具体词汇,所以保持原样或根据上下文解释其含义) 无过期时间虚拟客户端永久存在
- ❌(这个符号在中文中通常表示错误或取消,没有直接的中文翻译,但可以根据上下文理解为“错误”或“取消”等意思) 基于文件的存储不适用于并发访问或多个服务器
- ❌(表示错误或否定的符号,无直接对应中文翻译,可理解为“错误”或“不正确”) 可预见的秘密从客户端ID派生的客户端密钥(使用HMAC-SHA256算法)
- ❌(表示错误或否定,无具体中文对应词汇,可理解为“错误”或根据上下文翻译为相应的否定含义) 无审计日志不追踪谁注册了什么
- ❌(这个符号在中文中通常表示“错误”或“取消”的意思,但直接翻译时保持原样,因为它是通用的符号) 无客户端管理没有API可用于列出、更新或删除客户端
用于生产
要在生产中使用DCR,您需要:
- 数据库存储 (PostgreSQL, MySQL, Redis)
- 认证 对于注册端点(API密钥、OAuth)
- 速率限制 以及预防虐待
- 客户撤销 以及管理API(应用程序编程接口)
- 妥善存储秘密 (已加密,未派生)
- 审计日志记录 所有DCR(数据变更请求/直接客户响应/数字内容资源等,具体含义需根据上下文确定)操作中
- 客户到期 以及清理机制
- 访问控制 (谁能注册客户)
- 监控和警报
为何存在这个概念验证(POC)
Docebo不支持RFC 7591动态客户端注册。某些MCP客户端(如ChatGPT、Claude Desktop)可能期望支持DCR。此概念验证(POC)允许您 测试 即使Docebo本身不原生支持DCR,也可以在本地使用这些客户端而无需进行修改。
对于生产环境部署,请考虑:
- 在Docebo中手动预注册OAuth应用程序
- 为MCP客户端提供静态凭据
- 如果确实需要,实施带有数据库后端的适当动态内容重写(DCR)
发展
使用 ngrok 进行本地开发
- 启动ngrok隧道:
ngrok http 3000- 更新
.env使用 ngrok URL:
SERVER_PUBLIC_URL=https://abc123.ngrok.io- 启动开发服务器:
npm run dev- 服务器将在以下地址可用:
- https://abc123.ngrok.io (公开) - http://localhost:3000 (本地)
MCP客户端配置
配置MCP客户端(例如,MCP Inspector)以指向您的服务器上特定于租户的端点:
# MCP Inspector
npx @modelcontextprotocol/inspector \
--transport http \
--server-url https://abc123.ngrok.io/mcp/riccardo-lr-test要点:
- URL路径中的租户:
/mcp/(非查询字符串) - MCP客户端将自动发现OAuth2终端点
/mcp//.well-known/oauth-authorization-server - 客户端将自动处理完整的OAuth2流程
- 每个租户都有其独立的MCP终端节点
注Claude Desktop 目前仅支持 stdio 传输(不支持 HTTP),因此还无法连接到此服务器。请使用 MCP Inspector 进行测试。
API 端点
所有终端点都是特定于租户的,并且在URL路径中包含租户ID。
发现端点
GET /mcp//.well-known/oauth-authorization-server (RFC 8414)
返回特定租户的OAuth2授权服务器元数据:
{
"issuer": "https://mcp.docebosaas.com/mcp/riccardo-lr-test",
"authorization_endpoint": "https://mcp.docebosaas.com/mcp/riccardo-lr-test/oauth2/authorize",
"token_endpoint": "https://mcp.docebosaas.com/mcp/riccardo-lr-test/oauth2/token",
"scopes_supported": ["api"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token", "password"],
"code_challenge_methods_supported": ["S256"]
}GET /mcp//.well-known/oauth-protected-resource (RFC 9728)
返回特定租户的受保护资源元数据:
{
"resource": "https://mcp.docebosaas.com/mcp/riccardo-lr-test",
"authorization_servers": ["https://mcp.docebosaas.com/mcp/riccardo-lr-test"]
}OAuth2 端点
GET /mcp//oauth2/authorize
向Docebo租户代理OAuth2授权请求。
参数:
- 标准OAuth2参数:
client_id,response_type,redirect_uri,scope,state,code_challenge等。 - 租户ID是从URL路径(而非查询字符串)中提取的
流动(或流畅):
- 服务器从URL路径中提取租户信息
- 重定向到
https://{tenant}.docebosaas.com/oauth2/authorize - 用户在Docebo中授权
- Docebo 重定向回来并附带授权信息
code
POST /mcp//oauth2/token
向Docebo租户代理OAuth2令牌请求。
身体参数 (application/x-www-form-urlencoded):
grant_type:authorization_code,refresh_token或者passwordcode授权码(用于authorization_code(授予)redirect_uri客户端的重定向URI(服务器会用租户配置的值覆盖)code_verifierPKCE 验证器(可选)- 其他特定于赠款的参数
流动;流程:
- 服务器从URL路径中提取租户信息
- 覆盖(或重写)
redirect_uri使用租户配置的值 - 注入租户的(信息/数据等,具体根据上下文确定)
client_id并且client_secret - 代理以
https://{tenant}.docebosaas.com/oauth2/token - 向客户端返回令牌响应
重要的服务器自动覆盖了 redirect_uri 与租户配置的参数 REDIRECT_URI 以确保它与Docebo中注册的内容一致。
MCP终端节点
POST /mcp/
特定租户的MCP JSON-RPC端点。
标题(或头部信息):
Authorization: BearerContent-Type: application/json
示例请求:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "docebo.list_users",
"arguments": {
"page_size": 10
}
}
}MCP 工具
docebo.list_users
列出Docebo学习管理系统中的用户。
参数:
| 参数 | 类型 | 描述 |
|---|---|---|
page | 序号 | 页码(从1开始计数) |
page_size | 编号 | 每页用户数(默认:200,最大:200) |
sort_attr | 字符串 | 排序属性(例如,“user_id”,“username”) |
sort_dir | 字符串 | 排序方向:“升序”或“降序” |
search_text | 字符串 | 用于搜索用户名或电子邮件的过滤器 |
测试
测试发现端点
curl https://abc123.ngrok.io/.well-known/oauth-authorization-server | jq .测试OAuth2流程(手动)
- 测试发现端点:
curl https://abc123.ngrok.io/mcp/riccardo-lr-test/.well-known/oauth-authorization-server | jq .- 开始授权 (将此网址粘贴到浏览器中):
https://abc123.ngrok.io/mcp/riccardo-lr-test/oauth2/authorize?client_id=test&response_type=code&redirect_uri=https://abc123.ngrok.io/oauth/callback&scope=api&code_challenge=CHALLENGE&code_challenge_method=S256- 用令牌兑换代码 (授权后):
curl -X POST https://abc123.ngrok.io/mcp/riccardo-lr-test/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d "grant_type=authorization_code" \
-d "code=" \
-d "code_verifier=" \
-d "redirect_uri=https://abc123.ngrok.io/oauth/callback"- 调用MCP端点:
TOKEN=""
curl -X POST "https://abc123.ngrok.io/mcp/riccardo-lr-test" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "docebo.list_users",
"arguments": {"page_size": 5}
}
}'生产部署
环境变量
SERVER_PUBLIC_URL=https://mcp.docebosaas.com
PORT=3000
ALLOWED_ORIGINS=*
ALLOW_LOCAL_DEV=false
# Add all tenant credentials
TENANT__CLIENT_ID=...
TENANT__CLIENT_SECRET=...构建并运行
npm run build
npm start部署检查清单
- \[ \] 设置
ALLOW_LOCAL_DEV=false - \[ \] 使用 HTTPS(OAuth2 必需)
- \[ \] 配置所有租户凭据
- \[ \] 设置正确
SERVER_PUBLIC_URL - \[ \] 在Docebo中更新OAuth2应用程序的重定向URI
- \[ \] 监控日志中的错误
- \[ \] 使用MCP客户端进行测试
安全
- 未泄露客户端密钥租户凭据仅存储在服务器端
- 需要HTTPSOAuth2 需要安全连接
- 状态参数防止CSRF攻击
- PKCE 支持增强公共客户端的安全性(S256)
- Bearer Tokens(承载令牌/持有者令牌)由Docebo在每次API调用时进行验证
- 无会话存储无状态架构
故障排除
“租户未配置”错误
- 检查环境变量中的租户ID格式
- 确保
TENANT_{UPPERCASE}_CLIENT_ID和_CLIENT_SECRET被设定 - 添加新租户后重启服务器
来自Docebo的“无效凭据”
- 验证client_id和client_secret是否正确
- 检查Docebo中的OAuth2应用程序是否处于激活状态
- 确保在Docebo应用程序中已启用授予类型
OAuth重定向问题
- 验证
SERVER_PUBLIC_URL是正确的 - 检查Docebo OAuth2应用中的重定向URI是否匹配
- 对于ngrok,在隧道重启后更新URL
CORS错误
- 设置
ALLOWED_ORIGINS=*用于测试 - 检查
ALLOW_LOCAL_DEV=true用于本地开发
建筑细节
多租户凭证存储
凭据存储在环境变量中,命名遵循以下规范:
TENANT_{TENANT_ID_UPPERCASE_WITH_UNDERSCORES}_CLIENT_ID
TENANT_{TENANT_ID_UPPERCASE_WITH_UNDERSCORES}_CLIENT_SECRET
TENANT_{TENANT_ID_UPPERCASE_WITH_UNDERSCORES}_REDIRECT_URI转换示例:
riccardo-lr-test→TENANT_RICCARDO_LR_TEST_CLIENT_IDacme-corp→TENANT_ACME_CORP_CLIENT_IDtest-123→TENANT_TEST_123_CLIENT_ID
重要的每个租户都必须配置这三个值:
CLIENT_ID- 来自Docebo的OAuth2客户端IDCLIENT_SECRET- 来自Docebo的OAuth2客户端密钥REDIRECT_URI- 在Docebo OAuth2应用中注册的重定向URI
这个(或“该”) REDIRECT_URI 在令牌交换过程中,会自动注入该值,覆盖MCP客户端发送的任何内容。这确保了与可能发送不同重定向URI的MCP客户端的兼容性(例如,MCP Inspector发送的……) http://localhost:6274/oauth/callback/debug)。
从URL路径中检测租户
服务器从所有端点的URL路径中提取租户ID:
/mcp/→ 从路径中提取的租户ID/mcp//oauth2/authorize→ 从路径中提取的租户ID/mcp//oauth2/token→ 从路径中提取的租户ID
无需查询字符串参数。租户已被添加至 req.body.tenant 在传递给OAuth代理之前,OAuth代理随后会:
- 加载租户配置
- 注入租户凭据
- 覆盖 redirect_uri 为租户配置的值
- 代理请求访问Docebo
文件结构
src/
├── config.ts # Server configuration
├── tenants.ts # Multi-tenant credential management
├── oauth-proxy.ts # OAuth2 authorize & token proxy
├── server.ts # Express app with all endpoints
├── mcp.ts # MCP JSON-RPC handler
└── docebo.ts # Docebo API client未来的改进/增强功能
见 增强功能.md 对于计划中的改进:
- 在代理之前进行租户验证
- 通过管理员API动态注册租户
- OAuth2令牌缓存
- 限速(或速率限制)
- 增强监控和日志记录
许可证
麻省理工学院(MIT)
支持
如遇到问题或有任何疑问,请在GitHub上提交一个议题。
