mcpField
M
MedRecPro Drug Label Server

MedRecPro Drug Label Server

远程服务
medrecpro.com健康医疗412 周前更新

关于 MedRecPro Drug Label Server

MedRecPro Drug Label Server 是连接 MCP 客户端与 MedRecPro Web API 的 OAuth 2.1 网关,供访问 MedRecPro 应用的客户端使用。提供的 README 描述了传输和认证架构,但未说明药品标签搜索工具。

主要功能

  1. 01OAuth 2.1 认证网关
  2. 02Streamable HTTP MCP 传输
  3. 03支持 Google 和 Microsoft 登录
  4. 04提供健康检查和技术文档端点

适用场景

  1. 01将 MCP 客户端连接到 MedRecPro Web API
  2. 02通过 Google 或 Microsoft 验证客户端
  3. 03检查服务状态并查阅端点文档

MCP 相关

它在 `/mcp` 提供 MCP Streamable HTTP 端点,作为 MCP 客户端与 MedRecPro Web API 之间的 OAuth 2.1 网关。提供的 README 未列出其公开的 MCP 工具。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://www.medrecpro.com/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;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\wwwrootMedRecProStatic界面 / 静态内容
/apisite\wwwroot\apiMedRecProWeb API
/mcpsite\wwwroot\mcpMedRecProMCPMCP 服务(本项目)

端点

所有端点均由 IIS 虚拟应用添加 /mcp 外部前缀。

外部 URL方法说明
/mcpPOSTMCP Streamable HTTP 传输(JSON-RPC)
/mcp/healthGET健康检查(返回 JSON 状态)
/mcp/docsGETHTML 技术文档页面
/mcp/getting-startedGETHTML 入门页面(面向用户,供目录列表使用)
/mcp/docs/images/{filename}GET提供嵌入的截图图片(PNG)
/mcp/.well-known/oauth-protected-resourceGET受保护资源元数据(RFC 9728)
/mcp/.well-known/oauth-authorization-serverGET授权服务器元数据(RFC 8414)
/mcp/oauth/authorizeGETOAuth 授权端点
/mcp/oauth/tokenPOST令牌交换端点
/mcp/oauth/registerPOST动态客户端注册(RFC 7591)
/mcp/oauth/callback/googleGETGoogle OAuth 回调
/mcp/oauth/callback/microsoftGETMicrosoft 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。

工作原理:

  1. OAuthEndpoints.cs 从上游 IdP claims 中提取电子邮件、显示名称和提供方
  2. UserResolutionService 生成临时 MCP JWT,并调用 POST /api/users/resolve-mcp
  3. API 的 UsersController.ResolveMcpUser() 按电子邮件查找用户:
    • **用户已存在:**返回其加密的数据库用户 ID
    • **用户不存在:**自动创建新的用户记录(采用 AuthController.createUserForExternalLogin() 的默认值:EmailConfirmed=true、Timezone="UTC"、Locale="en-US",无密码),然后返回加密后的 ID
  4. UserResolutionService 使用共享的 Security:DB:PKSecret 通过 StringCipher 解密加密后的 ID
  5. OAuthEndpoints.cs 将上游 NameIdentifier claim 替换为数据库中的数字用户 ID
  6. McpTokenService.normalizeClaimType() 在创建令牌期间将 ClaimTypes.NameIdentifier 转换为标准 JWT sub claim,并清除 OutboundClaimTypeMap 以防止重复映射
  7. MCP JWT 使用数字用户 ID 作为 sub claim 签发,从而确保所有 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 在创建令牌之前,将 .NET ClaimTypes URI 规范化为标准 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/docsGET /docs
入门指南GET /mcp/getting-startedGET /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 前缀:

2. Azure App Service 设置
添加虚拟应用程序

在 Azure Portal 中:

  1. 导航到 App Service "MedRecPro" > Configuration > Path mappings
  2. 点击 "+ Add virtual application or directory"
  3. 配置:
    • 虚拟路径: /mcp
    • 物理路径: site\wwwroot\mcp
    • 类型: 勾选 Application
  4. 点击 Save
Key Vault 机密

确保 Azure Key Vault(medrecprovault)中存在以下机密。这些机密与 MedRecPro API 共用,因为两个应用使用同一个 Key Vault:

机密名称说明
McpServer--JwtSigningKeyMCP 令牌的 JWT 签名密钥(至少 32 个字符)
Jwt--KeyJWT 签名密钥
Jwt--IssuerJWT 颁发者声明
Jwt--AudienceJWT 受众声明
Jwt--ExpirationMinutes令牌过期时间(例如,60)
Jwt--UpstreamTokenEncryptionKey上游令牌的 AES 加密密钥
Authentication--Google--ClientIdGoogle OAuth 客户端 ID
Authentication--Google--ClientSecretGoogle OAuth 客户端密钥
Authentication--Microsoft--ClientIdMicrosoft Entra 应用 ID
Authentication--Microsoft--ClientSecret--ProdMicrosoft 客户端密钥(生产环境)
Authentication--Microsoft--TenantIdAzure 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
  1. 前往 Google Cloud Console > APIs & Services > Credentials
  2. 编辑 OAuth 2.0 客户端 ID
  3. 添加已获授权的重定向 URI:
    • https://www.medrecpro.com/mcp/oauth/callback/google(生产环境)
    • http://localhost:5233/mcp/oauth/callback/google(开发环境)
Microsoft Entra ID
  1. 前往 Azure Portal > Microsoft Entra ID > App registrations
  2. 选择应用 > Authentication
  3. 添加重定向 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 发布
  1. 在解决方案资源管理器中右键单击 MedRecProMCP > 发布
  2. 选择 "MedRecProMCP - Web Deploy" 配置文件
  3. 验证设置:
    • 配置:Release
    • 目标框架:net8.0
    • 部署模式:Framework-dependent
  4. 单击 发布

发布配置文件通过 <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:ServerUrlMCP 服务的基本 URL(包含 /mcp)https://www.medrecpro.com/mcp
McpServer:ServerName显示名称MedRecPro MCP Server
McpServer:ScopesSupportedOAuth 作用域openid, profile, email, mcp:tools, mcp:read, mcp:write
McpServer:EnableDynamicClientRegistration允许 RFC 7591 注册true
McpServer:MaxRegisteredClients动态客户端的最大数量1000
McpServer:ClientRegistrationExpirationHours客户端 TTL24
MedRecProApi:BaseUrlMedRecPro API URLhttps://www.medrecpro.com/api
MedRecProApi:TimeoutSecondsHTTP 客户端超时30
MedRecProApi:RetryCountHTTP 重试次数3
Security:DB:PKSecretStringCipher 的共享加密密钥(必须与 API 一致)(Key Vault)
KeyVaultUrlAzure Key Vault URL(基础配置中为空,在 Production 中设置)
appsettings.Development.json
键覆盖值
McpServer:ServerUrlhttp://localhost:5233/mcp
MedRecProApi:BaseUrlhttp://localhost:5093/api
MedRecProApi:ValidateSslCertificatefalse
appsettings.Production.json
键覆盖值
McpServer:ServerUrlhttps://www.medrecpro.com/mcp
MedRecProApi:BaseUrlhttps://www.medrecpro.com/api
KeyVaultUrlhttps://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搜索药品标签truefalsefalse
export_drug_label_markdown导出药品标签truefalsefalse
search_expiring_patents搜索即将到期的专利truefalsefalse
search_by_pharmacologic_class按药理类别搜索truefalsefalse
get_my_profile获取我的个人资料truefalsefalse
get_my_activity获取我的活动truefalsefalse
get_my_activity_by_date_range按日期范围获取活动truefalsefalse

注解理由:

  • 所有工具均为只读工具,不会修改任何数据
  • 所有工具都使用 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 仪表板):

  1. Security > Settings > Block AI Bots Scope — 改为“仅阻止包含广告的主机名”
  2. 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 将其标记为机器人流量。 修复(分两部分):

  1. 代码 — 在 Program.cs 中的两个 HttpClient 注册项中添加 User-Agent: MedRecProMCP/1.0 (Internal-API-Client) 标头
  2. 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。 修复(三部分):

  1. McpTokenService — 添加了 normalizeClaimType(),在创建令牌前显式地将 ClaimTypes.* URI 映射为标准 JWT 短名称(sub、name、email、given_name、family_name)。清空了 OutboundClaimTypeMap,以防止重复映射。
  2. 两个 JWT 处理程序 — 在 MCP 服务和 API 的 McpBearer 处理程序中添加了 MapInboundClaims = false,使声明读回 ClaimsPrincipal 时保留其原始短名称。
  3. 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 的两处授权检查中将 || 改为 &&,这样所有用户都可以访问自己的日志,管理员则可以访问任意用户的日志。

经验总结
  1. IIS 虚拟应用会移除路径前缀。 对 /mcp/oauth/authorize 的请求到达 Kestrel 时会变成 /oauth/authorize。所有路由映射都必须在 RELEASE 构建中考虑到这一点。使用 #if DEBUG 指令。

  2. OAuth 发现端点必须位于域名根路径。 MCP SDK 会相对于域名解析 /.well-known/*,而不是相对于 MCP 端点路径。应从根站点(MedRecProStatic)提供这些端点,而不是从 MCP 虚拟应用提供。

  3. httpErrors existingResponse="PassThrough" 至关重要。 如果没有它,IIS 会将 401 响应(OAuth 挑战所需)替换为 HTML 错误页面。必须在根站点的 web.config 中设置,因为子虚拟应用会继承此设置。

  4. Cloudflare 有多种彼此独立的机器人拦截系统:

    • “Block AI Bots”(WAF 托管规则) — 控制 AI 爬虫的访问。通过 AI Crawl Control 允许 Claude-User。
    • “Bot Fight Mode” — 拦截来自托管服务提供商 IP 的自动化流量。无法通过 WAF 规则跳过。唯一的绕过方式是 IP Access Rules。
  5. 通过 Cloudflare 进行服务器到服务器的调用需要加入允许列表。 当服务器通过 HTTP 调用自己的公共域名时,这些请求会经过 Cloudflare,并被标记为机器人流量。始终为出站 HttpClients 设置 User-Agent 标头,并在 Cloudflare IP Access Rules 中将服务器的出站 IP 加入允许列表。

  6. Azure App Service 无法连接 localhost。 沙盒会阻止到 127.0.0.1:80 的出站套接字连接。同一 App Service 内部的服务器到服务器调用必须通过公共主机名进行。

  7. 来自不同颁发者的 JWT 令牌需要 ValidIssuers/ValidAudiences 数组。 当 MCP 服务签发的令牌具有与现有用户身份验证系统不同的颁发者/受众值时,JWT Bearer 处理程序必须同时接受两者。请使用复数数组属性,而不是单数属性。

  8. Azure SQL Serverless 冷启动可能引发连锁问题。 已暂停的数据库需要 15-30 秒或更长时间才能恢复。如果 MCP HttpClient 的超时时间(30 秒)在数据库唤醒之前到期,工具调用就会失败。对于无服务器数据库,请考虑使用 EnableRetryOnFailure 和较长的超时时间。

  9. Cloudflare API 身份验证: Global API Key 使用 X-Auth-Email + X-Auth-Key 标头,而不是 Authorization: Bearer。API Tokens(有范围限制)使用 Authorization: Bearer。使用错误的格式会返回 Unable to authenticate request。

  10. JwtSecurityTokenHandler 会静默重命名声明类型。 在 JWT 序列化期间,OutboundClaimTypeMap 会将 ClaimTypes.NameIdentifier 转换为 "sub",并将 ClaimTypes.Name 转换为 "unique_name"。在入站端,MapInboundClaims(默认值为 true)是否会反向转换,取决于处理程序的实现(.NET 8 中的 JsonWebTokenHandler 不会反向转换)。创建令牌前,务必显式将声明规范化为标准 JWT 短名称,清空 OutboundClaimTypeMap,并在 JWT 处理程序中设置 MapInboundClaims = false,以确保声明类型从头到尾都可预测。

  11. JWT Bearer 方案需要 [Authorize] 才会触发验证。 与每次请求都会全局运行并从会话 Cookie 中填充 User.Claims 的 Cookie/Identity 中间件不同,JWT Bearer 处理程序只有在端点带有 [Authorize](或引用该方案的策略)时才会验证令牌。若没有该特性,Authorization 标头中的 Bearer 令牌会被忽略,请求会被视为匿名,且 User.Claims 为空。如果控制器同时包含公共端点和受保护端点,请使用类级别的 [Authorize(Policy = "...")],并在公共端点上使用 [AllowAnonymous]。

  12. JSON 属性名称必须与 API 的序列化大小写一致。 MedRecPro API 使用 PascalCase 的 Newtonsoft.Json(不会转换为 camelCase)。在 MCP 项目中使用 System.Text.Json 解析 API 响应时,TryGetProperty() 区分大小写——"encryptedId" 与 "EncryptedUserId" 不匹配。务必核实 API 返回的确切属性名称。

  13. 授权逻辑中的德摩根定律。 if (!isSelf || !isAdmin) 等价于 if (!(isSelf && isAdmin))——这要求两个条件都为真,而不是满足其中任意一个。若要实现“本人或管理员即可访问”,请使用 if (!isSelf && !isAdmin)。

故障排除

502 Bad Gateway

MCP 应用在启动时崩溃。请检查:

  1. Kudu 控制台 — 访问 https://<app>.scm.azurewebsites.net,导航到 LogFiles/ 并查找 stdout-mcp* 日志文件
  2. 启用 stdout 日志记录 — 在 site\wwwroot\mcp\web.config 中,将 stdoutLogEnabled="true",再次访问该 URL,然后检查日志
  3. 常见原因: 无法访问 Key Vault。确认 appsettings.Production.json 中已设置 KeyVaultUrl,且托管标识具有访问权限
IDX10703:密钥长度为零

JWT 签名密钥为空。这意味着 Key Vault 密钥未能加载:

  1. 确认 appsettings.Production.json 中的 KeyVaultUrl 设置为 https://medrecprovault.vault.azure.net/
  2. 确认 App Service 托管标识对 Key Vault 密钥具有 Get 和 List 权限
  3. 确认 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 拦截:

  1. 检查 Cloudflare 防火墙事件 — 查找来源为 botFight 或 managedChallenge 的拦截记录
  2. 验证 IP 访问规则 — 必须在 Cloudflare 中将所有 Azure App Service 出站 IP 加入白名单(Security > WAF > Tools > IP Access Rules,操作:Allow)
  3. 验证 User-Agent 标头 — HttpClient 必须发送非空的 User-Agent。检查 Program.cs 中的 client.DefaultRequestHeaders.Add("User-Agent", ...)
  4. 获取出站 IP: az webapp show --resource-group <rg> --name <app> --query possibleOutboundIpAddresses --output tsv
OAuth 回调错误
  1. 确认已在 Google Cloud Console 和 Microsoft Entra ID 中注册重定向 URI
  2. 确认 McpServer:ServerUrl 包含 /mcp(例如 https://www.medrecpro.com/mcp)
  3. 回调 URL 按 {ServerUrl}/oauth/callback/{provider} 的格式构建
SSE 连接中断
  1. 确认 Cloudflare 转换规则为 /mcp 路径设置了 X-Accel-Buffering: no
  2. 确认 Cloudflare 缓存规则对 /mcp 路径绕过缓存
  3. 确认已启用 Azure App Service 的始终开启功能
首次调用工具时数据库超时

Azure SQL Serverless 可能处于暂停状态。首次请求会触发恢复,耗时 15-30+ 秒:

  1. 重试 — 数据库预热后,第二次尝试通常会成功
  2. 延长自动暂停延迟 — 在 Azure Portal 中,将测试期间的无服务器自动暂停延迟设为 10-15 分钟
  3. 添加重试逻辑 — 考虑在 UseSqlServer() 配置中使用 EnableRetryOnFailure()

发布到 MCP Registry

该服务已注册到官方 MCP Registry,以便 MCP 客户端发现。

注册条目: com.medrecpro/drug-label-server 注册搜索: https://registry.modelcontextprotocol.io/v0.1/servers?search=medrecpro

前提条件
  1. 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
    
  2. Ed25519 密钥对 — 用于 DNS 域名验证(存储在本地,不在仓库中)

  3. 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,因为该服务不作为软件包分发
发布流程
  1. 通过 DNS 验证进行身份验证:

    mcp-publisher.exe login dns --domain=medrecpro.com --private-key=<64-char-hex-private-key>
    

    ⚠️ 私钥存储在仓库外的本地脚本中。请参阅 C:\Users\{user}\{folder}\{publisher-script}.ps1

  2. 发布到注册表:

    mcp-publisher.exe publish server.json
    
  3. 验证列表:

    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>

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。