关于 MedRecPro Drug Label Server
MedRecPro Drug Label Server 是连接 MCP 客户端与 MedRecPro Web API 的 OAuth 2.1 网关,供访问 MedRecPro 应用的客户端使用。提供的 README 描述了传输和认证架构,但未说明药品标签搜索工具。
主要功能
- 01OAuth 2.1 认证网关
- 02Streamable HTTP MCP 传输
- 03支持 Google 和 Microsoft 登录
- 04提供健康检查和技术文档端点
适用场景
- 01将 MCP 客户端连接到 MedRecPro Web API
- 02通过 Google 或 Microsoft 验证客户端
- 03检查服务状态并查阅端点文档
MCP 相关
它在 `/mcp` 提供 MCP Streamable HTTP 端点,作为 MCP 客户端与 MedRecPro Web API 之间的 OAuth 2.1 网关。提供的 README 未列出其公开的 MCP 工具。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://www.medrecpro.com/mcpclaude mcp add --transport http com-medrecpro-drug-label-server https://www.medrecpro.com/mcp{"mcpServers":{"com-medrecpro-drug-label-server":{"type":"http","url":"https://www.medrecpro.com/mcp"}}}在 Cursor 中打开{"servers":{"com-medrecpro-drug-label-server":{"type":"http","url":"https://www.medrecpro.com/mcp"}}}在 VS Code 中打开{"mcpServers":{"com-medrecpro-drug-label-server":{"type":"http","url":"https://www.medrecpro.com/mcp"}}}{"mcpServers":{"com-medrecpro-drug-label-server":{"serverUrl":"https://www.medrecpro.com/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
MedRecPro MCP 服务
MedRecPro 应用的 MCP(模型上下文协议)服务。作为 OAuth 2.1 网关,连接 MCP 客户端(Claude 等)与 MedRecPro Web API。
版本: 1.0.0 运行时: ASP.NET Core 8.0 生产环境 URL: https://www.medrecpro.com/mcp
架构
Azure App Service: "MedRecPro" (Windows, IIS)
+--------------------------------------------------+
| |
+------------------+ | / site\wwwroot MedRecProStatic |
| Claude / MCP | HTTPS | /api site\wwwroot\api MedRecPro API |
| Client |------->| /mcp site\wwwroot\mcp MedRecProMCP |
+------------------+ | |
| +--------------------------------------------------+
v |
+------------------+ v
| Cloudflare | +------------------+
| (CDN/WAF/DNS) | | Azure Key Vault |
+------------------+ | (medrecprovault)|
+------------------+
MCP 服务作为 IIS 虚拟应用运行在 /mcp,与 MedRecPro 主界面和 API 位于同一 Azure App Service 上。IIS 根据路径将请求路由到三个独立的 ASP.NET Core 进程:
| 虚拟路径 | 物理路径 | 项目 | 用途 |
|---|---|---|---|
/ | site\wwwroot | MedRecProStatic | 界面 / 静态内容 |
/api | site\wwwroot\api | MedRecPro | Web API |
/mcp | site\wwwroot\mcp | MedRecProMCP | MCP 服务(本项目) |
端点
所有端点均由 IIS 虚拟应用添加 /mcp 外部前缀。
| 外部 URL | 方法 | 说明 |
|---|---|---|
/mcp | POST | MCP Streamable HTTP 传输(JSON-RPC) |
/mcp/health | GET | 健康检查(返回 JSON 状态) |
/mcp/docs | GET | HTML 技术文档页面 |
/mcp/getting-started | GET | HTML 入门页面(面向用户,供目录列表使用) |
/mcp/docs/images/{filename} | GET | 提供嵌入的截图图片(PNG) |
/mcp/.well-known/oauth-protected-resource | GET | 受保护资源元数据(RFC 9728) |
/mcp/.well-known/oauth-authorization-server | GET | 授权服务器元数据(RFC 8414) |
/mcp/oauth/authorize | GET | OAuth 授权端点 |
/mcp/oauth/token | POST | 令牌交换端点 |
/mcp/oauth/register | POST | 动态客户端注册(RFC 7591) |
/mcp/oauth/callback/google | GET | Google OAuth 回调 |
/mcp/oauth/callback/microsoft | GET | Microsoft OAuth 回调 |
注意:
GET /mcp返回 HTTP 405(不允许的方法)。这是正确行为。MCP 传输仅接受 POST 请求。如需通过浏览器访问页面,请使用/mcp/docs(技术文档)或/mcp/getting-started(面向用户的指南)。
身份验证流程
1. Claude sends POST to /mcp (unauthenticated)
2. Server returns 401 with WWW-Authenticate header pointing to PRM
3. Claude fetches /mcp/.well-known/oauth-protected-resource
4. Claude fetches /mcp/.well-known/oauth-authorization-server
5. Claude registers via /mcp/oauth/register (Dynamic Client Registration)
6. Claude redirects user to /mcp/oauth/authorize?provider=google
7. User authenticates with Google/Microsoft
8. Provider redirects to /mcp/oauth/callback/{provider}
9. MCP server resolves upstream IdP email to numeric DB user ID via POST /api/users/resolve-mcp
(auto-provisions user if they don't exist in the database)
10. MCP server issues authorization code with numeric `sub` claim (standard JWT name), redirects to Claude
11. Claude exchanges code for tokens via /mcp/oauth/token
12. Claude sends authenticated POST to /mcp with Bearer MCP JWT
13. MCP server forwards MCP JWT to MedRecPro API for each tool call (McpBearer auth scheme)
用户解析与自动创建
在 OAuth 回调期间(第 9 步),MCP 服务必须将上游身份提供方的标识符(Google sub / Microsoft GUID)映射到 MedRecPro 数据库中的数字用户 ID。API 的 getEncryptedIdFromClaim() 方法要求 NameIdentifier claim 为数字;如果不进行此解析,所有 API 调用都会失败并返回 401。
工作原理:
OAuthEndpoints.cs从上游 IdP claims 中提取电子邮件、显示名称和提供方UserResolutionService生成临时 MCP JWT,并调用POST /api/users/resolve-mcp- API 的
UsersController.ResolveMcpUser()按电子邮件查找用户:- **用户已存在:**返回其加密的数据库用户 ID
- **用户不存在:**自动创建新的用户记录(采用
AuthController.createUserForExternalLogin()的默认值:EmailConfirmed=true、Timezone="UTC"、Locale="en-US",无密码),然后返回加密后的 ID
UserResolutionService使用共享的Security:DB:PKSecret通过StringCipher解密加密后的 IDOAuthEndpoints.cs将上游NameIdentifierclaim 替换为数据库中的数字用户 IDMcpTokenService.normalizeClaimType()在创建令牌期间将ClaimTypes.NameIdentifier转换为标准 JWTsubclaim,并清除OutboundClaimTypeMap以防止重复映射- MCP JWT 使用数字用户 ID 作为
subclaim 签发,从而确保所有 API 调用都能成功
关键架构:
TokenForwardingHandler将 MCP JWT 直接转发到 API(而非上游 Google/Microsoft 令牌)。API 的McpBearer身份验证方案会验证这些由 MCP 签名的 JWT。- MCP 服务和 API 都从 Azure Key Vault 共享
Security:DB:PKSecret,用于通过StringCipher加密/解密用户 ID。 UserResolutionService使用专用的"MedRecProApiDirect"命名 HttpClient(不带TokenForwardingHandler),以避免解析期间发生循环令牌转发。- Claim 规范化:
McpTokenService在创建令牌之前,将 .NETClaimTypesURI 规范化为标准 JWT 短名称(sub、name、email、given_name、family_name)。两个 JWT 处理程序都设置MapInboundClaims = false,因此读取 claims 时会保留其短名称。API 的ClaimHelper可从ClaimTypes.NameIdentifier(cookie 身份验证)和"sub"(MCP JWT 身份验证)解析用户 ID。
编译器指令(#if DEBUG)
路由路径使用编译器指令处理 IIS 虚拟应用程序路径剥离。这与 MedRecPro API 项目中 ApiControllerBase 使用的模式相同。
**问题:**IIS 会在将请求转发到应用程序之前剥离 /mcp 前缀。对 www.medrecpro.com/mcp/oauth/authorize 的请求到达 Kestrel 时会变为 /oauth/authorize。路由必须对此进行适配。
| 组件 | DEBUG(本地开发) | RELEASE(Azure/IIS) |
|---|---|---|
| MCP 传输 | MapMcp("/mcp") | MapMcp("/") |
| 健康检查 | GET / | GET /health |
| 文档 | GET /mcp/docs | GET /docs |
| 入门指南 | GET /mcp/getting-started | GET /getting-started |
| 图片服务 | GET /mcp/docs/images/{filename} | GET /docs/images/{filename} |
| OAuth 组 | /mcp/oauth | /oauth |
| 已知元数据 | /mcp/.well-known/* | /.well-known/* |
| PRM 重定向 | /mcp/.well-known/oauth-protected-resource 重定向到 SDK 路径 | 不需要(由 IIS 处理) |
包含编译器指令的文件:
Program.cs- MCP 传输、健康检查、文档、入门指南、图片服务、PRM 重定向Endpoints/OAuthEndpoints.cs- OAuth 路由组前缀Endpoints/OAuthMetadataEndpoints.cs- 已知元数据路径
由配置驱动(无需指令):
- OAuth 回调 URL - 运行时根据
McpServer:ServerUrl构建 - OAuth 元数据文档内容 - 运行时根据
McpServer:ServerUrl构建 - 受保护资源元数据 - 运行时根据
McpServer:ServerUrl构建
设置指南
先决条件
- .NET 8.0 SDK
- Azure 订阅,包含:
- App Service(Windows、IIS)——与 MedRecPro 共用
- Key Vault(
medrecprovault) - Application Insights(可选)
- Cloudflare 账户,包含
medrecpro.com域名 - Google Cloud Console 项目,包含 OAuth 2.0 凭据
- Microsoft Entra ID 应用注册
1. 本地开发设置
克隆并还原
git clone <repo-url>
cd MedRecProMCP
dotnet restore
配置用户机密
dotnet user-secrets init
dotnet user-secrets set "McpServer:JwtSigningKey" "<min-32-char-key>"
dotnet user-secrets set "Jwt:Key" "<min-32-char-key>"
dotnet user-secrets set "Jwt:Issuer" "http://localhost:5233/mcp"
dotnet user-secrets set "Jwt:Audience" "http://localhost:5233/mcp"
dotnet user-secrets set "Jwt:ExpirationMinutes" "60"
dotnet user-secrets set "Jwt:UpstreamTokenEncryptionKey" "<32-char-aes-key>"
dotnet user-secrets set "Authentication:Google:ClientId" "<google-client-id>"
dotnet user-secrets set "Authentication:Google:ClientSecret" "<google-client-secret>"
dotnet user-secrets set "Authentication:Microsoft:ClientId" "<microsoft-app-id>"
dotnet user-secrets set "Authentication:Microsoft:ClientSecret:Dev" "<microsoft-client-secret>"
dotnet user-secrets set "Authentication:Microsoft:TenantId" "<azure-tenant-id>"
dotnet user-secrets set "Security:DB:PKSecret" "<encryption-key-must-match-api>"
在本地运行
dotnet run
服务运行在 http://localhost:5233。在 DEBUG 模式下,端点包含 /mcp 前缀:
- 健康检查:http://localhost:5233/
- MCP 传输:http://localhost:5233/mcp(仅限 POST)
- 文档:http://localhost:5233/mcp/docs
- 入门指南:http://localhost:5233/mcp/getting-started
- 截图图片:http://localhost:5233/mcp/docs/images/{filename}
- OAuth 元数据:http://localhost:5233/mcp/.well-known/oauth-authorization-server
2. Azure App Service 设置
添加虚拟应用程序
在 Azure Portal 中:
- 导航到 App Service "MedRecPro" > Configuration > Path mappings
- 点击 "+ Add virtual application or directory"
- 配置:
- 虚拟路径:
/mcp - 物理路径:
site\wwwroot\mcp - 类型: 勾选 Application
- 虚拟路径:
- 点击 Save
Key Vault 机密
确保 Azure Key Vault(medrecprovault)中存在以下机密。这些机密与 MedRecPro API 共用,因为两个应用使用同一个 Key Vault:
| 机密名称 | 说明 |
|---|---|
McpServer--JwtSigningKey | MCP 令牌的 JWT 签名密钥(至少 32 个字符) |
Jwt--Key | JWT 签名密钥 |
Jwt--Issuer | JWT 颁发者声明 |
Jwt--Audience | JWT 受众声明 |
Jwt--ExpirationMinutes | 令牌过期时间(例如,60) |
Jwt--UpstreamTokenEncryptionKey | 上游令牌的 AES 加密密钥 |
Authentication--Google--ClientId | Google OAuth 客户端 ID |
Authentication--Google--ClientSecret | Google OAuth 客户端密钥 |
Authentication--Microsoft--ClientId | Microsoft Entra 应用 ID |
Authentication--Microsoft--ClientSecret--Prod | Microsoft 客户端密钥(生产环境) |
Authentication--Microsoft--TenantId | Azure AD 租户 ID |
Security--DB--PKSecret | 用于用户 ID 加密和解密的共享密钥(必须与 API 匹配) |
注意: Key Vault 使用
--作为分隔符。ASP.NET Core 会将其映射为配置中的:(例如,Jwt--Key会变为Jwt:Key)。
托管标识
App Service 必须具有系统分配的托管标识,并拥有对 Key Vault 机密的 Get 和 List 权限。由于 MCP 虚拟应用程序与 API 共用同一个 App Service,现有托管标识应该已经具有访问权限。
3. OAuth 提供程序配置
Google Cloud Console
- 前往 Google Cloud Console > APIs & Services > Credentials
- 编辑 OAuth 2.0 客户端 ID
- 添加已获授权的重定向 URI:
https://www.medrecpro.com/mcp/oauth/callback/google(生产环境)http://localhost:5233/mcp/oauth/callback/google(开发环境)
Microsoft Entra ID
- 前往 Azure Portal > Microsoft Entra ID > App registrations
- 选择应用 > Authentication
- 添加重定向 URI:
https://www.medrecpro.com/mcp/oauth/callback/microsoft(生产环境)http://localhost:5233/mcp/oauth/callback/microsoft(开发环境)
4. Cloudflare 配置
由于 MCP 服务运行在同一域名(www.medrecpro.com)下,无需更改 DNS。请配置以下规则:
缓存规则 — 对 /mcp 绕过缓存
Caching > Cache Rules > Create rule
- 规则名称:
Bypass cache for MCP server - 条件: 主机名等于
www.medrecpro.com,且 URI 路径以/mcp开头 - 操作: 缓存资格:Bypass cache
转换规则 — 禁用 SSE 响应缓冲
规则 > 转换规则 > 修改响应标头 > 创建规则
- 规则名称:
Disable buffering for MCP SSE - 条件: 主机名等于
www.medrecpro.com,且 URI 路径以/mcp开头 - 然后: 设置静态标头
X-Accel-Buffering=no
这可以防止 Cloudflare 缓冲 MCP Streamable HTTP 传输使用的服务器发送事件。
WAF 规则(可选)
仅当您已有可能会阻止发往 /mcp 或 /mcp/oauth/* 的 POST 请求的 WAF 规则时才需要。如果您的 WAF 没有自定义规则,可跳过此步骤。
5. 部署
从 Visual Studio 发布
- 在解决方案资源管理器中右键单击 MedRecProMCP > 发布
- 选择 "MedRecProMCP - Web Deploy" 配置文件
- 验证设置:
- 配置:Release
- 目标框架:net8.0
- 部署模式:Framework-dependent
- 单击 发布
发布配置文件通过 <DeployIisAppPath>MedRecPro/mcp</DeployIisAppPath> 设置部署到 site\wwwroot\mcp。
从 CLI 发布
dotnet publish -c Release -o ./publish
然后通过 Azure CLI 或 zip deploy 部署到 /mcp 虚拟应用程序路径。
6. 验证
部署后,验证每个端点:
# Health check — should return JSON with status "running"
curl https://www.medrecpro.com/mcp/health
# MCP transport — should return 405 (GET not allowed, POST only)
curl -i https://www.medrecpro.com/mcp
# OAuth Authorization Server Metadata — should return JSON
curl https://www.medrecpro.com/mcp/.well-known/oauth-authorization-server
# Protected Resource Metadata — should return JSON
curl https://www.medrecpro.com/mcp/.well-known/oauth-protected-resource
# Documentation page — should return HTML
curl https://www.medrecpro.com/mcp/docs
# Getting-started page — should return HTML with screenshots
curl https://www.medrecpro.com/mcp/getting-started
# Screenshot images — should return PNG image data
curl -I https://www.medrecpro.com/mcp/docs/images/MCP-Adverse-Example.PNG
# Main site still works
curl https://www.medrecpro.com
curl https://www.medrecpro.com/api
配置参考
appsettings.json(基础)
所有环境共用的非机密配置。
| 键 | 描述 | 默认值 |
|---|---|---|
McpServer:ServerUrl | MCP 服务的基本 URL(包含 /mcp) | https://www.medrecpro.com/mcp |
McpServer:ServerName | 显示名称 | MedRecPro MCP Server |
McpServer:ScopesSupported | OAuth 作用域 | openid, profile, email, mcp:tools, mcp:read, mcp:write |
McpServer:EnableDynamicClientRegistration | 允许 RFC 7591 注册 | true |
McpServer:MaxRegisteredClients | 动态客户端的最大数量 | 1000 |
McpServer:ClientRegistrationExpirationHours | 客户端 TTL | 24 |
MedRecProApi:BaseUrl | MedRecPro API URL | https://www.medrecpro.com/api |
MedRecProApi:TimeoutSeconds | HTTP 客户端超时 | 30 |
MedRecProApi:RetryCount | HTTP 重试次数 | 3 |
Security:DB:PKSecret | StringCipher 的共享加密密钥(必须与 API 一致) | (Key Vault) |
KeyVaultUrl | Azure Key Vault URL | (基础配置中为空,在 Production 中设置) |
appsettings.Development.json
| 键 | 覆盖值 |
|---|---|
McpServer:ServerUrl | http://localhost:5233/mcp |
MedRecProApi:BaseUrl | http://localhost:5093/api |
MedRecProApi:ValidateSslCertificate | false |
appsettings.Production.json
| 键 | 覆盖值 |
|---|---|
McpServer:ServerUrl | https://www.medrecpro.com/mcp |
MedRecProApi:BaseUrl | https://www.medrecpro.com/api |
KeyVaultUrl | https://medrecprovault.vault.azure.net/ |
项目结构
MedRecProMCP/
Program.cs # App startup, DI, middleware, endpoint mappings
MedRecProMCP.csproj # Project file (.NET 8.0)
MedRecProMCP.slnx # Solution file
MedRecProMCP.http # HTTP request test file
web.config # IIS configuration (OutOfProcess hosting)
server.json # MCP registry metadata (see MCP Registry below)
Documentation.md # Anthropic Connectors Directory submission doc
appsettings.json # Base configuration
appsettings.Development.json # Local dev overrides
appsettings.Production.json # Azure production overrides
Configuration/
McpServerSettings.cs # MCP server config model
MedRecProApiSettings.cs # API client config model
JwtSettings.cs # JWT config model
OAuthProviderSettings.cs # OAuth provider config model (Google, Microsoft)
Endpoints/
OAuthEndpoints.cs # OAuth authorize, token, register, callbacks
OAuthMetadataEndpoints.cs # .well-known metadata endpoints
Services/
IMcpTokenService.cs # Token service interface
McpTokenService.cs # JWT token generation and validation
IOAuthService.cs # OAuth service interface
OAuthService.cs # OAuth flow orchestration
IClientRegistrationService.cs # Client registration interface
ClientRegistrationService.cs # Dynamic Client Registration (RFC 7591)
IPkceService.cs # PKCE service interface
PkceService.cs # PKCE implementation
IPersistedCacheService.cs # Persisted cache interface
FilePersistedCacheService.cs # File-based persistent cache (PKCE, registrations)
MedRecProApiClient.cs # Typed HTTP client for MedRecPro API
IUserResolutionService.cs # User resolution service interface
UserResolutionService.cs # Resolves upstream IdP email to numeric DB user ID
Handlers/
TokenForwardingHandler.cs # Forwards MCP JWT to API (DelegatingHandler)
Helpers/
StringCipher.cs # AES encryption/decryption (copy from API for user ID handling)
Models/
AiAgentDtos.cs # AI/Claude integration models
WorkPlanModels.cs # Work plan and execution models
Tools/
DrugLabelTools.cs # MCP tools for drug labels, patent expiration (annotated)
UserTools.cs # MCP tools for user/account operations (annotated)
Templates/
McpDocumentation.html # Embedded HTML technical documentation template
McpGettingStarted.html # Embedded HTML getting-started page (user-facing)
Images/
MCP-Adverse-Example.PNG # Screenshot: warnings/adverse effects lookup
MCP-Authentication-Example.PNG # Screenshot: user profile/authentication status
MCP-Full-Label-Example.PNG # Screenshot: complete label export
MCP-Indication-Example.PNG # Screenshot: drug indications lookup
MCP-Starting-Dose-Example.PNG # Screenshot: dosage information lookup
PowerShell/
test-mcp.ps1 # MCP endpoint test script
test-mcp-detailed.ps1 # Detailed MCP test with verbose output
test-mcp-root.ps1 # Root endpoint test script
Properties/
launchSettings.json # Local development profiles
PublishProfiles/
MedRecProMCP - Web Deploy.pubxml # Azure deployment profile
Docs/
DeploymentGuide.md # Legacy deployment guide (subdomain approach)
Claude.ai 连接器集成
本节介绍如何在 Claude.ai 的“设置 > 连接器”中将 MCP 服务连接为自定义连接器。这项工作历经多次会话,期间遇到了许多与 IIS 虚拟应用程序、Cloudflare 和 Azure 架构相关的障碍。
Anthropic 连接器目录提交
Anthropic 连接器目录的提交材料:
Documentation.md— 按照 Anthropic 模板编写的提交文档(描述、功能、设置、带屏幕截图的示例)/mcp/getting-started— 面向用户的 HTML 页面,由目录列表链接至此- 工具注释 — 全部 5 个工具均包含
ReadOnly、Destructive和OpenWorld安全注释 - 身份验证 — 使用 PKCE 的 OAuth 2.1,支持动态客户端注册(RFC 7591)。无需特殊帐户 — 审核人员使用任意 Google 或 Microsoft 帐户进行身份验证后,系统会自动为其配置帐户。
连接状态
| 组件 | 状态 |
|---|---|
| OAuth 2.1 流程(PKCE S256) | 正常 |
MCP 会话建立(initialize) | 正常 |
工具列表(tools/list) | 正常 |
工具执行(tools/call) | 正常 |
| 下游 API 调用(MCP -> MedRecPro API) | 正常 |
| MCP 用户解析(邮箱 -> 数字数据库 ID) | 正常 |
| MCP 用户自动配置(新用户) | 正常 |
| JWT 声明规范化(sub、name、email) | 正常 |
| UsersController [Authorize(Policy = "ApiAccess")] | 正常 |
工具安全注解
全部 7 个工具都通过 [McpServerTool] 特性属性声明了 MCP 安全注解(ModelContextProtocol SDK v0.7.0-preview.1 支持):
| 工具 | 标题 | 只读 | 破坏性 | 开放世界 |
|---|---|---|---|---|
search_drug_labels | 搜索药品标签 | true | false | false |
export_drug_label_markdown | 导出药品标签 | true | false | false |
search_expiring_patents | 搜索即将到期的专利 | true | false | false |
search_by_pharmacologic_class | 按药理类别搜索 | true | false | false |
get_my_profile | 获取我的个人资料 | true | false | false |
get_my_activity | 获取我的活动 | true | false | false |
get_my_activity_by_date_range | 按日期范围获取活动 | true | false | false |
注解理由:
- 所有工具均为只读工具,不会修改任何数据
- 所有工具都使用
OpenWorld = false,因为所有数据都包含在 MedRecPro 数据库中,并通过控制台应用程序填充
遇到的问题及修复方法
集成过程中依次遇到了以下问题。必须先解决每个问题,下一个问题才会显现。
1. PKCE 验证失败
症状: OAuth 令牌交换因 PKCE 不匹配而失败。
根本原因: 在授权流程中,PKCE 验证器/质询对未能正确存储和匹配。
修复方法: 修正了 PkceService.cs 中的 PKCE 存储和验证逻辑。
2. 发现端点返回 302 重定向
症状: Claude 的 MCP SDK 收到 302 重定向,而非 /.well-known/oauth-protected-resource 和 /.well-known/oauth-authorization-server 的 JSON 响应。
根本原因: IIS 基于路径的路由以及 /mcp 下的虚拟应用程序导致发现 URL 解析出现问题。MCP SDK 预期 /.well-known/* 位于域名根目录,但 MCP 服务只处理 /mcp 下的请求。
修复方法: 在根站点 MedRecProStatic/Program.cs 中直接添加了 well-known 端点,以静态 JSON 响应提供 OAuth 元数据,从而完全避免重定向。
3. MCP 传输缺少 [Authorize]
症状: Claude 无需身份验证即可连接,未发送 401 质询。
根本原因: MapMcp() 端点缺少 .RequireAuthorization()。
修复方法: 在 Program.cs(RELEASE 配置)中的 MapMcp("/") 调用上添加了 .RequireAuthorization()。
4. MCP 传输上的 [AllowAnonymous]
症状: 即使添加了 .RequireAuthorization(),MCP 传输仍未触发 401。
根本原因: MCP SDK 的默认传输中间件在内部应用了 [AllowAnonymous]。
修复方法: 使用 McpAuthenticationHandler 质询方案,确保在未经身份验证的请求中返回包含 resource_metadata URI 的 WWW-Authenticate 标头。
5. IIS 将 401 替换为 HTML 错误页面
症状: Claude 收到的是 HTML 错误页面,而非包含 WWW-Authenticate 标头的 JSON 401 响应。
根本原因: IIS 会拦截 HTTP 错误响应,并将其替换为自己的 HTML 页面。
修复方法: 在 MedRecProStatic(根站点)和 MedRecProMCP 的 web.config 中都添加了 <httpErrors existingResponse="PassThrough" />。根站点的设置至关重要,因为它会继承到子虚拟应用程序。
6. 401 响应中的 resource_metadata URL 错误
症状: Claude 无法找到受保护资源元数据(PRM)端点。
根本原因: WWW-Authenticate 标头中的 resource_metadata URI 指向了错误的路径。
修复: 在 McpAuthenticationHandler 配置中显式设置 ResourceMetadataUri,值为 {ServerUrl}/.well-known/oauth-protected-resource。
7. PRM 端点返回 404
症状: 修正 URL 后,PRM 端点仍返回 404。
根本原因: 路由注册顺序和 IIS 路径剥离导致端点未被映射。
修复: 在 OAuthMetadataEndpoints.cs 中显式注册 PRM 端点,并设置正确的路由。
8. Cloudflare“管理 AI 机器人”WAF 规则阻止 Claude
症状: Claude 的请求从未到达服务端(Cloudflare 返回 403)。 根本原因: Cloudflare 托管的 WAF 规则“阻止 AI 机器人”阻止了来自 Claude-User(Anthropic 的爬虫 User-Agent)的所有请求。AI Crawl Control 仪表板显示有 149 个请求被阻止,0 个请求被允许。 修复(Cloudflare 仪表板):
- Security > Settings > Block AI Bots Scope — 改为“仅阻止包含广告的主机名”
- Security > AI Crawl Control > Crawlers — 将
Claude-User(Anthropic)设为允许
9. JWT 受众验证失败(IDX10214)
症状: IDX10214: Audience validation failed. Audiences: 'https://www.medrecpro.com/mcp'. Did not match: validationParameters.ValidAudience: 'MedRecUsers'
根本原因: McpTokenService 签发的令牌将受众设为 ServerUrl(https://www.medrecpro.com/mcp),但 JWT Bearer 处理程序只接受 MedRecUsers(来自 Jwt:Audience 配置)。
修复: 将单个 ValidAudience 改为数组 ValidAudiences,同时接受 jwtSettings.Audience 和 mcpSettings.ServerUrl.TrimEnd('/')。
10. JWT 签发者验证失败(IDX10205)
症状: IDX10205: Issuer validation failed. Issuer: 'https://www.medrecpro.com/mcp'. Did not match: validationParameters.ValidIssuer: 'MedRecPro'
根本原因: 与受众问题相同,这是双令牌问题——McpTokenService 使用 ServerUrl 作为签发者,但 JWT 处理程序只接受 MedRecPro。
修复: 将单个 ValidIssuer 改为数组 ValidIssuers,同时接受 jwtSettings.Issuer 和 mcpSettings.ServerUrl.TrimEnd('/')。
11. Cloudflare Bot Fight Mode 阻止下游 API 调用
症状: MCP 工具已执行,但返回 403。MCP 服务对 https://www.medrecpro.com/api/... 的 HTTP 调用被阻止。
根本原因: MCP 服务(运行于 Azure)向公共 API URL 发出出站 HTTP 请求。这些请求会经过 Cloudflare,而 Cloudflare 因请求来自带有空 User-Agent 标头的 Azure IP 地址,通过 Bot Fight Mode 将其标记为机器人流量。
修复(分两部分):
- 代码 — 在
Program.cs中的两个 HttpClient 注册项中添加User-Agent: MedRecProMCP/1.0 (Internal-API-Client)标头 - Cloudflare — 无法通过 WAF 自定义规则跳过 Bot Fight Mode,但如果请求匹配 IP Access Rule,则不会触发该模式。为全部 32 个 Azure App Service 出站 IP 添加 IP Access Rules(操作:Allow):
# Get all possible outbound IPs for the App Service az webapp show --resource-group <rg> --name <app> \ --query possibleOutboundIpAddresses --output tsv # Add each IP as a Cloudflare IP Access Rule (Allow) # Use Cloudflare API with Global API Key: for ip in $IPS; do curl -s -X POST \ "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/firewall/access_rules/rules" \ -H "X-Auth-Email: $EMAIL" \ -H "X-Auth-Key: $API_KEY" \ -H "Content-Type: application/json" \ --data "{\"mode\":\"whitelist\",\"configuration\":{\"target\":\"ip\",\"value\":\"$ip\"},\"notes\":\"Azure App Service outbound IP\"}" done
12. Azure SQL Database 冷启动超时
症状: 空闲一段时间后首次调用工具会失败,并显示 Database 'MedRecProDB' is not currently available 或 Execution Timeout Expired(30 秒命令超时)。
根本原因: Azure SQL Serverless 会在一段时间无活动后自动暂停。恢复需要 15–30 秒以上,超过 HttpClient 超时(30 秒)和 SQL 命令超时(30 秒)。
状态: 已知问题。数据库预热后,后续请求会成功。缓解方法包括延长自动暂停延迟、在 UseSqlServer 上启用 EnableRetryOnFailure,或增加 MedRecProApi 设置中的 TimeoutSeconds。
13. JWT 声明类型映射不匹配(工具调用返回 401)
症状: OAuth 登录成功并生成 MCP JWT 后,所有调用 API 的工具请求都返回 401。日志显示 [Auth] Token validated for user: unknown,且 getEncryptedIdFromClaim() 抛出 UnauthorizedAccessException: Unable to determine user ID from authentication context。
根本原因: JwtSecurityTokenHandler.OutboundClaimTypeMap 会在 JWT 序列化期间静默地将 ClaimTypes.NameIdentifier 转换为 "sub",并将 ClaimTypes.Name 转换为 "unique_name"。在 API 端,getEncryptedIdFromClaim() 搜索的是 c.Type.Contains("NameIdentifier"),这与 "sub" 不匹配。此外,NameClaimType = "name" 预期会有 "name" 声明,但 JWT 中包含的是 "unique_name",导致 Identity.Name 为 null。
修复(三部分):
- McpTokenService — 添加了
normalizeClaimType(),在创建令牌前显式地将ClaimTypes.*URI 映射为标准 JWT 短名称(sub、name、email、given_name、family_name)。清空了OutboundClaimTypeMap,以防止重复映射。 - 两个 JWT 处理程序 — 在 MCP 服务和 API 的
McpBearer处理程序中添加了MapInboundClaims = false,使声明读回ClaimsPrincipal时保留其原始短名称。 - API 声明读取器 — 创建了
ClaimHelper.cs,用于从ClaimTypes.NameIdentifier(Cookie 身份验证)和"sub"(MCP JWT)中解析用户 ID。将UsersController、AiController、AuthController、LabelController、RequireActorAttributeFilter、RequireUserRoleAttributeFilter、ActivityLogActionFilter和LogHelper中分散的 9 处 NameIdentifier 查找更新为使用共享帮助程序。
14. UsersController 缺少 [Authorize](MCP 工具调用返回 401)
症状: 修复 JWT 声明规范化问题(问题 #13)后,MCP 端日志现在正确显示 [Auth] Token validated for user: Christopher Barnes,但 API 工具调用仍返回 401。ClaimHelper.GetEncryptedUserIdOrThrow 因 User.Claims 为空而抛出异常。IIS 日志显示 Logon User: Anonymous。API 端没有 [MCP Auth] 日志。
根本原因: UsersController 在类级别和各个端点上都没有 [Authorize] 特性(resolve-mcp 除外)。如果没有 [Authorize],ASP.NET Core 的 JWT bearer 处理程序就不会对 McpBearer 令牌运行——请求会以匿名身份到达,且 User.Claims 为空。Cookie 身份验证无需 [Authorize] 也能运行,因为 Identity 中间件会全局运行,并在每个请求中从会话 Cookie 填充 User.Claims;但 JWT Bearer 方案需要显式添加 [Authorize] 才会触发验证。
修复: 在 UsersController 类上添加了 [Authorize(Policy = "ApiAccess")]。ApiAccess 策略(Program.cs)同时接受 IdentityConstants.ApplicationScheme(Cookie)和 "McpBearer"(JWT),因此会为 MCP 请求触发 JWT 处理程序,同时保留现有的 Cookie 身份验证行为。在必须保持公开访问的 signup 和 authenticate 端点上添加了 [AllowAnonymous]。还修复了 OnAuthenticationFailed 处理程序不输出 SecurityTokenException 日志的问题。
15. GetMyActivity / GetMyActivityByDateRange 静默失败
症状: 修复 Issue #14 后,GetMyProfile 成功返回 200,但 GetMyActivity 和 GetMyActivityByDateRange 很快就执行完毕,期间没有向活动端点发送第二个 HTTP 请求。MCP 工具向 Claude 返回 {"error":"Could not determine user ID"}。
根本原因(A — MCP 端): UserTools.cs 调用 profile.TryGetProperty("encryptedId", ...),但 API 使用 Newtonsoft.Json 和 CamelCasePropertyNamesContractResolver 进行序列化——实际 JSON 属性名是 "encryptedUserId"(驼峰命名,而且原查找键中缺少了“User”)。区分大小写的 TryGetProperty 查找失败,因此不会发出第二个 API 调用。
根本原因(B — API 端): UsersController.cs 中的 GetUserActivity(第 452 行)和 GetUserActivityByDateRange(第 630 行)使用了 if (!isSelf || !claimsUser.IsUserAdmin())——误用了 OR 而非 AND。这阻止了非管理员用户查看自己的活动日志。只有既是目标用户又是管理员的用户才能通过检查。
修复: 在 UserTools.cs 中将 "encryptedId"(两处)改为 "encryptedUserId",以匹配驼峰命名的 JSON 属性名。在 UsersController.cs 的两处授权检查中将 || 改为 &&,这样所有用户都可以访问自己的日志,管理员则可以访问任意用户的日志。
经验总结
-
IIS 虚拟应用会移除路径前缀。 对
/mcp/oauth/authorize的请求到达 Kestrel 时会变成/oauth/authorize。所有路由映射都必须在 RELEASE 构建中考虑到这一点。使用#if DEBUG指令。 -
OAuth 发现端点必须位于域名根路径。 MCP SDK 会相对于域名解析
/.well-known/*,而不是相对于 MCP 端点路径。应从根站点(MedRecProStatic)提供这些端点,而不是从 MCP 虚拟应用提供。 -
httpErrors existingResponse="PassThrough"至关重要。 如果没有它,IIS 会将 401 响应(OAuth 挑战所需)替换为 HTML 错误页面。必须在根站点的web.config中设置,因为子虚拟应用会继承此设置。 -
Cloudflare 有多种彼此独立的机器人拦截系统:
- “Block AI Bots”(WAF 托管规则) — 控制 AI 爬虫的访问。通过 AI Crawl Control 允许 Claude-User。
- “Bot Fight Mode” — 拦截来自托管服务提供商 IP 的自动化流量。无法通过 WAF 规则跳过。唯一的绕过方式是 IP Access Rules。
-
通过 Cloudflare 进行服务器到服务器的调用需要加入允许列表。 当服务器通过 HTTP 调用自己的公共域名时,这些请求会经过 Cloudflare,并被标记为机器人流量。始终为出站 HttpClients 设置
User-Agent标头,并在 Cloudflare IP Access Rules 中将服务器的出站 IP 加入允许列表。 -
Azure App Service 无法连接 localhost。 沙盒会阻止到
127.0.0.1:80的出站套接字连接。同一 App Service 内部的服务器到服务器调用必须通过公共主机名进行。 -
来自不同颁发者的 JWT 令牌需要
ValidIssuers/ValidAudiences数组。 当 MCP 服务签发的令牌具有与现有用户身份验证系统不同的颁发者/受众值时,JWT Bearer 处理程序必须同时接受两者。请使用复数数组属性,而不是单数属性。 -
Azure SQL Serverless 冷启动可能引发连锁问题。 已暂停的数据库需要 15-30 秒或更长时间才能恢复。如果 MCP HttpClient 的超时时间(30 秒)在数据库唤醒之前到期,工具调用就会失败。对于无服务器数据库,请考虑使用
EnableRetryOnFailure和较长的超时时间。 -
Cloudflare API 身份验证: Global API Key 使用
X-Auth-Email+X-Auth-Key标头,而不是Authorization: Bearer。API Tokens(有范围限制)使用Authorization: Bearer。使用错误的格式会返回Unable to authenticate request。 -
JwtSecurityTokenHandler会静默重命名声明类型。 在 JWT 序列化期间,OutboundClaimTypeMap会将ClaimTypes.NameIdentifier转换为"sub",并将ClaimTypes.Name转换为"unique_name"。在入站端,MapInboundClaims(默认值为true)是否会反向转换,取决于处理程序的实现(.NET 8 中的JsonWebTokenHandler不会反向转换)。创建令牌前,务必显式将声明规范化为标准 JWT 短名称,清空OutboundClaimTypeMap,并在 JWT 处理程序中设置MapInboundClaims = false,以确保声明类型从头到尾都可预测。 -
JWT Bearer 方案需要
[Authorize]才会触发验证。 与每次请求都会全局运行并从会话 Cookie 中填充User.Claims的 Cookie/Identity 中间件不同,JWT Bearer 处理程序只有在端点带有[Authorize](或引用该方案的策略)时才会验证令牌。若没有该特性,Authorization标头中的 Bearer 令牌会被忽略,请求会被视为匿名,且User.Claims为空。如果控制器同时包含公共端点和受保护端点,请使用类级别的[Authorize(Policy = "...")],并在公共端点上使用[AllowAnonymous]。 -
JSON 属性名称必须与 API 的序列化大小写一致。 MedRecPro API 使用 PascalCase 的 Newtonsoft.Json(不会转换为 camelCase)。在 MCP 项目中使用
System.Text.Json解析 API 响应时,TryGetProperty()区分大小写——"encryptedId"与"EncryptedUserId"不匹配。务必核实 API 返回的确切属性名称。 -
授权逻辑中的德摩根定律。
if (!isSelf || !isAdmin)等价于if (!(isSelf && isAdmin))——这要求两个条件都为真,而不是满足其中任意一个。若要实现“本人或管理员即可访问”,请使用if (!isSelf && !isAdmin)。
故障排除
502 Bad Gateway
MCP 应用在启动时崩溃。请检查:
- Kudu 控制台 — 访问
https://<app>.scm.azurewebsites.net,导航到LogFiles/并查找stdout-mcp*日志文件 - 启用 stdout 日志记录 — 在
site\wwwroot\mcp\web.config中,将stdoutLogEnabled="true",再次访问该 URL,然后检查日志 - 常见原因: 无法访问 Key Vault。确认
appsettings.Production.json中已设置KeyVaultUrl,且托管标识具有访问权限
IDX10703:密钥长度为零
JWT 签名密钥为空。这意味着 Key Vault 密钥未能加载:
- 确认
appsettings.Production.json中的KeyVaultUrl设置为https://medrecprovault.vault.azure.net/ - 确认 App Service 托管标识对 Key Vault 密钥具有
Get和List权限 - 确认 App Service 环境变量中的
ASPNETCORE_ENVIRONMENT设置为Production
IDX10214 / IDX10205:受众或颁发者验证失败
McpTokenService 颁发的 JWT 令牌使用 ServerUrl 作为颁发者和受众。JWT Bearer 处理程序必须同时接受 MCP 值和现有用户身份验证值:
ValidIssuers = new[] { jwtSettings.Issuer, mcpSettings.ServerUrl.TrimEnd('/') }
ValidAudiences = new[] { jwtSettings.Audience, mcpSettings.ServerUrl.TrimEnd('/') }
/mcp 返回 405 Method Not Allowed
这是预期行为。/mcp 上的 MCP Streamable HTTP 传输仅接受 POST 请求。浏览器发送 GET 请求,因此会返回 405。
下游 API 调用返回 403 Forbidden
MCP 服务向 https://www.medrecpro.com/api/... 发出的出站 HTTP 调用被 Cloudflare 拦截:
- 检查 Cloudflare 防火墙事件 — 查找来源为
botFight或managedChallenge的拦截记录 - 验证 IP 访问规则 — 必须在 Cloudflare 中将所有 Azure App Service 出站 IP 加入白名单(Security > WAF > Tools > IP Access Rules,操作:Allow)
- 验证 User-Agent 标头 — HttpClient 必须发送非空的 User-Agent。检查
Program.cs中的client.DefaultRequestHeaders.Add("User-Agent", ...) - 获取出站 IP:
az webapp show --resource-group <rg> --name <app> --query possibleOutboundIpAddresses --output tsv
OAuth 回调错误
- 确认已在 Google Cloud Console 和 Microsoft Entra ID 中注册重定向 URI
- 确认
McpServer:ServerUrl包含/mcp(例如https://www.medrecpro.com/mcp) - 回调 URL 按
{ServerUrl}/oauth/callback/{provider}的格式构建
SSE 连接中断
- 确认 Cloudflare 转换规则为
/mcp路径设置了X-Accel-Buffering: no - 确认 Cloudflare 缓存规则对
/mcp路径绕过缓存 - 确认已启用 Azure App Service 的始终开启功能
首次调用工具时数据库超时
Azure SQL Serverless 可能处于暂停状态。首次请求会触发恢复,耗时 15-30+ 秒:
- 重试 — 数据库预热后,第二次尝试通常会成功
- 延长自动暂停延迟 — 在 Azure Portal 中,将测试期间的无服务器自动暂停延迟设为 10-15 分钟
- 添加重试逻辑 — 考虑在
UseSqlServer()配置中使用EnableRetryOnFailure()
发布到 MCP Registry
该服务已注册到官方 MCP Registry,以便 MCP 客户端发现。
注册条目: com.medrecpro/drug-label-server
注册搜索: https://registry.modelcontextprotocol.io/v0.1/servers?search=medrecpro
前提条件
-
mcp-publisher CLI — 从 GitHub releases 安装:
# Windows (PowerShell) $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture -eq "Arm64") { "arm64" } else { "amd64" } Invoke-WebRequest -Uri "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_windows_$arch.tar.gz" -OutFile "mcp-publisher.tar.gz" tar xf mcp-publisher.tar.gz mcp-publisher.exe rm mcp-publisher.tar.gz -
Ed25519 密钥对 — 用于 DNS 域名验证(存储在本地,不在仓库中)
-
DNS TXT 记录 — 已在 Cloudflare 中配置:
medrecpro.com TXT "v=MCPv1; k=ed25519; p=<public-key>"
server.json
注册表元数据文件位于 MedRecProMCP/server.json:
{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "com.medrecpro/drug-label-server",
"title": "MedRecPro Drug Label Server",
"description": "Search and export FDA drug labels by brand name, generic ingredient, or UNII code.",
"version": "1.0.0",
"websiteUrl": "https://www.medrecpro.com",
"icons": [
{
"src": "https://www.medrecpro.com/favicon.svg",
"mimeType": "image/svg+xml",
"sizes": ["any"]
},
{
"src": "https://www.medrecpro.com/favicon-96x96.png",
"mimeType": "image/png",
"sizes": ["96x96"]
}
],
"repository": {
"url": "https://github.com/chriserikbarnes/MedRecPro",
"source": "github",
"subfolder": "MedRecProMCP"
},
"remotes": [
{
"type": "streamable-http",
"url": "https://www.medrecpro.com/mcp"
}
]
}
要点:
name使用com.medrecpro/命名空间(通过 DNS 验证)icons提供 SVG(可缩放)和 96x96 PNG,供客户端显示remotes声明这是托管服务(而非 npm/PyPI 包)- 未包含
packages,因为该服务不作为软件包分发
发布流程
-
通过 DNS 验证进行身份验证:
mcp-publisher.exe login dns --domain=medrecpro.com --private-key=<64-char-hex-private-key>⚠️ 私钥存储在仓库外的本地脚本中。请参阅
C:\Users\{user}\{folder}\{publisher-script}.ps1 -
发布到注册表:
mcp-publisher.exe publish server.json -
验证列表:
curl "https://registry.modelcontextprotocol.io/v0.1/servers?search=medrecpro"
何时发布更新
修改 server.json 中的 version 并重新发布的情况:
- 添加、移除或重命名工具
- 更改服务 URL
- 更新描述或元数据
注册表会分别存储各个版本。发布不会部署代码,只会更新目录条目。
保持向后兼容
由于部署的服务是单个实例(而非版本化软件包),请避免进行破坏性更改:
| ✅ 安全的更改 | ❌ 破坏性更改 |
|---|---|
| 添加新工具 | 移除现有工具 |
| 添加可选参数 | 移除或重命名参数 |
| 添加新的响应字段 | 更改响应结构 |
| 修复错误/改进逻辑 | 更改工具名称 |
| 改进描述 | 移除必需字段 |
DNS 验证参考
如需重新生成密钥对或在新机器上设置:
# Generate Ed25519 keypair (use Git Bash on Windows)
openssl genpkey -algorithm Ed25519 -out mcp-key.pem
# Extract public key (Base64) — goes in DNS TXT record
openssl pkey -in mcp-key.pem -pubout -outform DER | tail -c 32 | base64
# Extract private key (64-char hex) — used with CLI
openssl pkey -in mcp-key.pem -noout -text 2>&1 | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n'
DNS TXT 记录格式(在 Cloudflare 中):
- 类型: TXT
- 名称:
@(根域名) - 内容:
v=MCPv1; k=ed25519; p=<base64-public-key>
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。