
1. 为什么 WebApi 接 MCP 后Key 管理会变成第一个坑MCPModel Context Protocol这两年从 IDE 插件一路铺到服务端很多团队的第一反应是我手上已经有一堆 ASP.NET Core WebApi能不能直接让它们被 AI 客户端当成工具调用答案是可以ModelContextProtocol.AspNetCore这个包就是干这个的。但真正动手之后最先卡住人的往往不是协议本身而是鉴权。原因很直接。传统 WebApi 的调用方是你自己写的前端或另一个后端Key 放在配置文件里、写死在环境变量里都行反正只有你知道。可一旦接入 MCP调用方变成了 Claude Desktop、Cursor、Kiro 这类 AI 客户端它们会拿着你的工具描述去自动决定调不调、怎么调。这时候如果每个工具背后都挂一个不同的模型厂商 Key配置会迅速失控OpenAI 一个、Anthropic 一个、国内模型又一个轮换、限额、审计全散在各处。我试过的做法是WebApi 里只保留一套统一 Key所有模型调用都走同一个 API 通道MCP 工具本身不直接持有厂商密钥。这样客户端只需要认一个 Token后端换模型、调额度都不用动客户端配置。这篇就聚焦这个鉴权配置环节给出appsettings.json和Program.cs里可复制的骨架再附一次 MCP 工具调用的验证请求和预期响应帮你确认整条链路是通的。适合谁看正在把现有 ASP.NET Core WebApi 改造成 MCP Server 的后端同学需要在多个 AI 客户端之间共享同一套模型调用凭证的团队以及被「每个工具一个 Key」搞烦了、想收敛配置的人。2. 前置准备TaoToken 统一 Key 与 API 通道在写代码之前先把「统一 Key」这件事落地。TaoToken 提供的是一个聚合式的模型调用入口你可以在它的控制台里生成一把 API Key然后用这一个 Key 去访问不同模型不用在代码里为每家厂商单独维护凭证。对 MCP 场景来说这正好解决了上面说的配置发散问题。具体操作路径是这样先到官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录进控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 创建 API Key。创建完记得立刻复制页面刷新后就看不全了。如果你只是想先验证模型通不通可以直接在模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 里试一句确认 Key 有效再往代码里塞。API 的基础地址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 BaseUrl 用。Key 的管理入口在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 后续轮换、吊销都在这里。接入细节如果拿不准接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有各语言的示例照着改 BaseUrl 和 Key 就行。这里要区分两个概念很多人第一次会混概念作用存放位置TaoToken API Key后端调用模型的凭证服务端配置绝不下发客户端MCP 访问 TokenAI 客户端访问你 WebApi 的凭证客户端配置可轮换也就是说AI 客户端拿的是 MCP Token它调你的 WebApi你的 WebApi 再拿 TaoToken Key 去调模型。两层分离客户端永远看不到模型 Key。这个设计是后面所有配置的基础。3. 可复制配置appsettings.json 与 Program.cs 骨架先装包。MCP 的 ASP.NET Core 支持还在预览阶段版本号要写清楚dotnet add package ModelContextProtocol.AspNetCore --version 0.4.0-preview.3然后是配置文件。把两层凭证分开写TaoToken 的 Key 放TaoToken节点MCP 的访问 Token 放McpAuth节点{ TaoToken: { BaseUrl: https://taotoken.net/api, ApiKey: sk-your-taotoken-key, DefaultModel: claude-sonnet-4-5 }, McpAuth: { Enabled: true, ValidTokens: [ mcp-token-please-replace-with-32-chars ] } }开发环境单独一份把鉴权关掉方便调试但注意别把这份提交到生产{ McpAuth: { Enabled: false } }接下来是Program.cs。核心是三件事注册 MCP Server、注册一个带统一 Key 的 HttpClient、挂上鉴权中间件。using ModelContextProtocol.Server; var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // 统一模型调用通道所有 MCP 工具共用这一个 HttpClient builder.Services.AddHttpClient(TaoToken, client { client.BaseAddress new Uri(builder.Configuration[TaoToken:BaseUrl]!); client.DefaultRequestHeaders.Add( Authorization, $Bearer {builder.Configuration[TaoToken:ApiKey]}); client.DefaultRequestHeaders.Add(Accept, application/json); }); // 注册 MCP Server builder.Services .AddMcpServer(options { options.ServerInfo new ModelContextProtocol.Protocol.Implementation { Name UnifiedKeyApi, Version 1.0.0 }; }) .WithHttpTransport() .WithToolsFromAssembly(); var app builder.Build(); // 鉴权中间件必须在 MapMcp 之前 app.UseMiddlewareMcpAuthenticationMiddleware(); app.UseAuthorization(); app.MapControllers(); app.MapMcp(/mcp); app.Run();鉴权中间件只拦/mcp路径其他接口不受影响这样你原有的 WebApi 路由完全不用改public class McpAuthenticationMiddleware { private readonly RequestDelegate _next; private readonly IConfiguration _configuration; public McpAuthenticationMiddleware(RequestDelegate next, IConfiguration configuration) { _next next; _configuration configuration; } public async Task InvokeAsync(HttpContext context) { if (!context.Request.Path.StartsWithSegments(/mcp)) { await _next(context); return; } if (!_configuration.GetValuebool(McpAuth:Enabled)) { await _next(context); return; } var header context.Request.Headers[Authorization].FirstOrDefault(); if (string.IsNullOrEmpty(header) || !header.StartsWith(Bearer )) { context.Response.StatusCode 401; await context.Response.WriteAsJsonAsync(new { error missing_token }); return; } var token header[Bearer .Length..].Trim(); var valid _configuration.GetSection(McpAuth:ValidTokens).Getstring[](); if (valid is null || !valid.Contains(token)) { context.Response.StatusCode 401; await context.Response.WriteAsJsonAsync(new { error invalid_token }); return; } await _next(context); } }工具类里注入IHttpClientFactory拿命名客户端去调模型Key 完全不经过工具方法using System.ComponentModel; using ModelContextProtocol.Server; [McpServerToolType] public static class ModelTools { [McpServerTool] [Description(Ask the unified model channel a question and return the answer text.)] public static async Taskstring AskModel( IHttpClientFactory factory, [Description(The question to send to the model)] string prompt) { var client factory.CreateClient(TaoToken); var payload new { model claude-sonnet-4-5, messages new[] { new { role user, content prompt } } }; var resp await client.PostAsJsonAsync(/v1/messages, payload); resp.EnsureSuccessStatusCode(); return await resp.Content.ReadAsStringAsync(); } }到这里配置骨架就齐了。注意WithToolsFromAssembly()会扫描当前程序集里所有带[McpServerToolType]的类工具方法必须是static参数要么是基础类型要么能被 DI 解析。4. 验证请求一次 MCP 工具调用的完整往返配置写完别急着接客户端先用 curl 打一发确认链路通。启动服务dotnet run假设监听在http://localhost:5000先列工具这一步不需要 Token如果你的中间件对tools/list也放行的话如果没放行就带上curl -X POST http://localhost:5000/mcp \ -H Content-Type: application/json \ -H Authorization: Bearer mcp-token-please-replace-with-32-chars \ -d {jsonrpc:2.0,id:1,method:tools/list}预期返回里能看到AskModel这个工具带name、description和inputSchema{ jsonrpc: 2.0, id: 1, result: { tools: [ { name: AskModel, description: Ask the unified model channel a question and return the answer text., inputSchema: { type: object, properties: { prompt: { type: string, description: The question to send to the model } }, required: [prompt] } } ] } }然后真正调一次工具这一步会触发后端用 TaoToken Key 去请求模型curl -X POST http://localhost:5000/mcp \ -H Content-Type: application/json \ -H Authorization: Bearer mcp-token-please-replace-with-32-chars \ -d { jsonrpc:2.0, id:2, method:tools/call, params:{ name:AskModel, arguments:{prompt:用一句话说明 MCP 是什么} } }预期响应结构大致是这样content数组里是工具返回的文本{ jsonrpc: 2.0, id: 2, result: { content: [ { type: text, text: {\id\:\msg_...\,\content\:[{\type\:\text\,\text\:\MCP 是一套让 AI 应用与外部工具、数据源标准化通信的开放协议。\}]} } ], isError: false } }看到isError: false且text里有模型返回内容说明整条链路通了客户端 Token 校验通过 → 工具执行 → 后端用统一 Key 调模型 → 结果回传。如果text里是错误信息往下看排查部分。5. 本篇常见错排查401 missing_token / invalid_token。先确认请求头是Authorization: Bearer xxx中间有个空格很多人写成Bearerxxx。再确认McpAuth:Enabled在当前环境是true开发环境那份配置如果被加载了会直接放行反而让你以为鉴权生效了。Token 本身别带首尾空格appsettings.json里复制粘贴很容易带进去。工具列表为空。WithToolsFromAssembly()扫的是入口程序集如果你的工具类在另一个类库项目里得显式指定程序集或者把工具类挪到主项目。另外工具方法必须是public static类上必须有[McpServerToolType]少一个都扫不到。调用工具返回 500日志里是模型侧报错。大概率是 TaoToken 的 Key 或 BaseUrl 写错了。检查TaoToken:BaseUrl是不是https://taotoken.net/api注意结尾不要多加斜杠否则拼接路径会变成双斜杠。Key 是否过期可以在 API Keys 页面确认。如果报模型不存在把DefaultModel换成你账号下可用的模型名。参数绑定失败提示缺少 prompt。MCP 的参数名是大小写敏感的客户端传的arguments里的键必须和 C# 方法参数名一致。如果你在[Description]里写了中文说明但参数名用了缩写客户端可能按描述去猜结果对不上。保持参数名语义清晰别用p、q这种。CORS 报错浏览器客户端调不通。AI 客户端如果是桌面应用不受影响但网页版客户端会撞 CORS。在Program.cs里加builder.Services.AddCors(options { options.AddPolicy(McpClients, policy policy.WithOrigins(http://localhost:3000) .AllowAnyHeader() .AllowAnyMethod()); }); app.UseCors(McpClients);注意UseCors要放在UseMiddlewareMcpAuthenticationMiddleware()之前否则预检请求会被鉴权拦掉。改了配置不生效。appsettings.Development.json会覆盖appsettings.json的同名节点但数组是整体替换不是合并。如果你在开发配置里只写了Enabled: false没写ValidTokens那ValidTokens会变成 null鉴权逻辑里valid is null直接判失败。要么两份都写全要么用环境变量覆盖。6. 把统一 Key 用起来后续接入与长期编码链路验证通过之后接下来就是把它接到真实客户端。Claude Desktop 或 Cursor 这类工具配置里填你的 MCP 地址和 Token 即可模型 Key 完全不用出现在客户端。如果你要长期跑编码类 Agent反复调模型、跑工具建议用 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite 额度模型更贴合这种高频场景比按次调用省心。接入过程中如果遇到鉴权或参数绑定的问题先去 API Keys https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite 确认 Key 状态再对照接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 检查请求格式。想快速验证某个模型在当前 Key 下是否可用直接在模型对话 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite 里发一句最快。最后提醒一个实际踩过的坑MCP Token 和 TaoToken Key 一定要分开轮换。我见过有人图省事把两者设成同一个值结果客户端配置泄露等于模型 Key 泄露限额被刷爆才发现。两层分离不是形式主义是出问题时能把损失控制在一层内的保险。