让 AI 直接调你的 .NET 接口:MCP 服务端与客户端落地实战

0 阅读11分钟

让 AI 直接调你的 .NET 接口:MCP 服务端与客户端落地实战

本文以 NetCoreKevin 项目中真实落地的两条链路为例,讲清楚在 .NET 9 环境下:

  • 服务端:如何把一个已有的 ASP.NET Core WebApi(带 Swagger 文档)零改造变成一个 MCP Server,让 Claude Desktop、Cursor、通义灵码等 MCP 客户端直接调用。
  • 客户端:如何在 AI 智能体运行时动态连接远程 MCP Server,把它暴露的工具(Tools)与本地 AIFunctionFactory 注册的 C# 函数一起喂给大模型。

NetCoreKevin 是基于 .NET 9 构建的企业级 AI 中台智能体 SaaS 前后端分离架构,开箱即用集成 AI 知识库、多智能体协同、Skill 技能管理、本地离线大模型、AI 联网搜索、智能体权限管控、一库多租户、分布式微服务等核心能力,是国产 .NET 阵营中少有的“开箱即用级”企业 AI 中台底座。本文完整源码可查看:

对应实现可以参考 NetCoreKevin 中以下代码类(路径基于仓库根目录):

  • 服务端:App.WebApi.Mcp 项目的 Program 类 → App/WebApiMcp/App.WebApi.Mcp/Program.cs
  • 客户端:AIAgentToolSkillService 类 → Kevin/Application/Services/AI/AIAgentToolSkillService.cs
  • 前端管理后台:SkillToolManagement.vue 页面 → vue/kevin.web.vue/src/pages/ai/SkillToolManagement.vue

一、先说结论:技术选型

角色使用的库版本关键点
MCP 服务端Summerdawn.Mcpifier.AspNetCore1.2.1直接把 Swagger/OpenAPI 文档"翻译"成 MCP Tools,业务 WebApi 一行不改
MCP 客户端ModelContextProtocol(微软官方 SDK).NET 9支持 Stdio / SSE / StreamableHttp 三种传输,返回的 McpClientTool 天然实现了 AITool,可直接接入 Microsoft.Extensions.AI
工具装配Microsoft.Extensions.AI.Abstractions 中的 AIFunctionFactory-用来把普通 C# 方法包装成 AI 可调用的 Tool

选型理由:服务端用 Mcpifier 而不是自己实现 MCP 协议,是因为企业里绝大多数场景是"把已有 REST API 开放给 AI",与其手写 JSON-RPC 传输层,不如让 Swagger 文档当"单一事实源",接口一改,MCP 工具自动跟着改。客户端用官方 SDK,是因为它和 Microsoft.Extensions.AI 深度打通,ListToolsAsync() 拿到的对象可以直接塞进 ChatClienttools 参数。


二、MCP 服务端:把 REST API 一键"网关化"

2.1 目录结构

App/WebApiMcp/App.WebApi.Mcp/
├── Program.cs                     // 服务入口,注册 Mcpifier
├── HttpInterToMcpSetting.cs       // 配置模型(对应 appsettings 节点)
├── EnvironmentConfigHelper.cs     // 环境切换辅助
├── appsettings.json               // 正式环境配置
├── appsettings.Development.json
├── appsettings.Test.json
└── App.WebApi.Mcp.csproj

这是一个独立的网关项目,不承载任何业务逻辑,只做两件事:读 Swagger、暴露 MCP。业务 WebApi 该跑在哪跑在哪,二者通过 HTTP 解耦。

2.2 csproj:只依赖一个包

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Summerdawn.Mcpifier.AspNetCore" Version="1.2.1" />
  </ItemGroup>
</Project>

2.3 appsettings.json:把可变的部分全部外置

{
  "AllowedHosts": "*",
  "HttpInterToMcpService": {
    "ServerName": "NetCoreKevinMcpServer",
    "ForwardedHeaders": "Authorization,token",
    "BaseAddress": "https://localhost:9901/",
    "Mcpifier": "NetCoreKevinMcp",
    "SwaggerJsonUrl": "https://localhost:9001/swagger/v1/swagger.json"
  }
}

字段含义:

  • ServerName:MCP 握手时告诉客户端"我是谁",会显示在 Claude Desktop / Cursor 的服务端列表里。
  • BaseAddress:真正的业务 WebApi 根地址,Mcpifier 会把 MCP 工具调用翻译成对它的 HTTP 请求。
  • ForwardedHeaders:需要透传到业务后端的请求头,逗号分隔。JWT 场景一定要写 Authorization,否则业务侧拿不到用户身份。
  • Mcpifier:MCP 端点挂载路径,最终对外地址是 https://<host>:<port>/NetCoreKevinMcp
  • SwaggerJsonUrl:Swagger 文档地址,Mcpifier 启动时读取它,把每个 Operation 变成一个 Tool。

对应的强类型配置类:

public class HttpInterToMcpSetting
{
    public string ServerName { get; set; } = "NetCoreKevinMcpServer";
    public string ForwardedHeaders { get; set; } = "Authorization";
    public string BaseAddress { get; set; } = "";
    public string Mcpifier { get; set; } = "NetCoreKevinMcp";
    public string SwaggerJsonUrl { get; set; } = "";
}

小建议:配置类放在离它最近的项目里(这里就是 App.WebApi.Mcp),不要下沉到通用类库。这是本项目一贯的做法——appsettings 的强类型绑定类跟着消费方走,谁用谁维护。

2.4 Program.cs:完整启动流程

using Summerdawn.Mcpifier.DependencyInjection;

namespace App.WebApi.Mcp
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var builder = WebApplication.CreateBuilder(args);

            // 手动切换环境(也可以完全交给 ASPNETCORE_ENVIRONMENT)
            Kevin.Common.Helper.EnvironmentConfigHelper.SetEnvironment(
                Kevin.Common.Helper.EnvironmentConfigHelper.GetEnvironment());

            // 读取配置节点
            var setting = builder.Configuration
                .GetRequiredSection("HttpInterToMcpService")
                .Get<HttpInterToMcpSetting>()
                ?? throw new InvalidOperationException("缺少 HttpInterToMcpService 配置节点");

            builder.Services.AddControllers();

            // 关键三步:AddMcpifier -> AddAspNetCore -> AddToolsFromSwagger
            builder.Services
                .AddMcpifier(options =>
                {
                    options.Rest.BaseAddress = setting.BaseAddress;
                    options.Rest.ForwardedHeaders = setting.ForwardedHeaders
                        .Split(",", StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                        .ToDictionary(h => h, _ => true);
                    options.ServerInfo = new() { Name = setting.ServerName };
                })
                .AddAspNetCore()
                .AddToolsFromSwagger(setting.SwaggerJsonUrl);

            var app = builder.Build();

            // 诊断中间件:打印每次到达 /mcp 的请求 Authorization 头
            app.Use(async (context, next) =>
            {
                if (context.Request.Path.StartsWithSegments("/" + setting.Mcpifier))
                {
                    var auth = context.Request.Headers.Authorization.ToString();
                    Console.WriteLine($"[诊断] {context.Request.Method} {context.Request.Path} | Authorization: " +
                        (string.IsNullOrEmpty(auth) ? "<未携带>" : auth[..Math.Min(auth.Length, 40)] + "..."));
                }
                await next();
            });

            // 把 MCP 端点挂到自定义路径
            app.MapMcpifier("/" + setting.Mcpifier);

            app.UseHttpsRedirection();
            app.UseAuthorization();
            app.Run();
        }
    }
}

三个 API 的作用一目了然:

  1. AddMcpifier(...):配置 MCP 服务器本身(名字、转发目标、要透传哪些 Header)。
  2. AddAspNetCore():把 MCP 服务集成到 ASP.NET Core 请求管道。
  3. AddToolsFromSwagger(url):从 Swagger JSON 拉取 Operation 列表,自动生成 MCP Tool 定义。

MapMcpifier("/NetCoreKevinMcp") 最终暴露的 MCP 端点就是:

https://localhost:7198/NetCoreKevinMcp

2.5 那个"诊断中间件"为什么值得留

MCP 客户端连接问题里,十有八九是 Authorization 没带过来——尤其是走 StreamableHttp 时,某些客户端只在握手时带头,后续请求默认不带。加这么一段 Console.WriteLine 就能立刻判断:

  • 输出 <未携带> → 客户端配置漏了 Header,去检查客户端;
  • 输出 Bearer eyJ... → 网关没问题,去业务后端排查 JWT 中间件。

比抓包快得多。

2.6 在 Claude Desktop / Cursor 里接入

以 Claude Desktop 为例,编辑 claude_desktop_config.json

{
  "mcpServers": {
    "NetCoreKevinMcpServer": {
      "url": "https://localhost:7198/NetCoreKevinMcp",
      "headers": {
        "Authorization": "Bearer <你的 JWT>"
      }
    }
  }
}

重启客户端,业务 WebApi 里所有 Swagger 上可见的接口,就会作为工具出现在对话框的"可用工具"列表里。


三、MCP 客户端:在智能体运行时动态挂载远程工具

服务端解决"AI 怎么调我的接口",客户端解决"我怎么消费别人的 MCP Server"。NetCoreKevin 的 AI 智能体是多租户、可配置的:用户在管理后台勾选启用哪些 MCP 服务、勾选每个服务里的哪些工具,运行时按需连接。所以客户端逻辑必须做到:

  • 支持 Stdio / SSE / StreamableHttp 三种传输;
  • 连接时动态注入 Authorization(当前用户的 JWT);
  • 拉到的 Tool 要能与本地 AIFunctionFactory 注册的工具混合使用
  • 单个 MCP 失败不影响其他工具装载。

对应实现可以参考 NetCoreKevin 中 AIAgentToolSkillService 类的 GetMcpTools 方法。

下图是管理后台「编辑技能工具」弹窗中 MCP 类型的配置界面,表单字段与运行时代码一一对应: 外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • Mcp地址 / Mcp类型McpUrl + McpType,决定走 HttpClientTransport 还是 StdioClientTransport 分支(见 3.2);
  • McpHeadersAdditionalHeaders,未填 Authorization 时由系统自动补齐当前用户 JWT(见 3.4);
  • Mcp工具勾选列表McpSelectedTools,「测试连接」按钮本质就是一次 ListToolsAsync() 调用(见 4.2)。

3.1 依赖包

<PackageReference Include="ModelContextProtocol" Version="0.*" />
<PackageReference Include="Microsoft.Extensions.AI" Version="9.*" />

3.2 三种传输的写法

using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;

IClientTransport transport = null;

// McpHeaders 是用户在管理后台为该服务配置的自定义 Header(JSON)
var dic = !string.IsNullOrEmpty(item.McpHeaders)
    ? item.McpHeaders.ToObject<Dictionary<string, string>>()
    : new Dictionary<string, string>();

// 自动补 Authorization(透传当前登录用户的 JWT)
if (!dic.ContainsKey("Authorization"))
{
    dic.Add("Authorization", Authorization);
}

switch (item.McpType.ToLowerInvariant())
{
    case "http":
    case "https":
        // StreamableHttp —— 推荐,兼容新版协议
        transport = new HttpClientTransport(new HttpClientTransportOptions
        {
            Endpoint = new Uri(item.McpUrl),
            TransportMode = HttpTransportMode.StreamableHttp,
            AdditionalHeaders = dic
        });
        break;

    case "sse":
        // Server-Sent Events —— 兼容老版本 MCP Server
        transport = new HttpClientTransport(new HttpClientTransportOptions
        {
            Endpoint = new Uri(item.McpUrl),
            TransportMode = HttpTransportMode.Sse,
            AdditionalHeaders = dic
        });
        break;

    case "stdio":
        // 本地子进程(比如 npx、uvx 拉起来的官方 Server)
        transport = new StdioClientTransport(new StdioClientTransportOptions
        {
            Command = item.McpCommand,
            Arguments = item.McpArguments?.Split(",").ToList(),
            EnvironmentVariables = item.McpEnvironment?.ToObject<Dictionary<string, string?>>() ?? default
        });
        break;

    default:
        throw new NotSupportedException($"不支持的传输类型: {item.McpType}");
}

选型经验:

  • 远程服务优先 StreamableHttp,性能和协议前瞻性都更好;
  • 对端只支持 SSE 时再降级到 HttpTransportMode.Sse
  • Stdio 用来跑本地进程(比如 npx -y @modelcontextprotocol/server-filesystem),此时不要把它用在多租户 SaaS 里——每个请求 spawn 一个子进程会耗尽资源。

3.3 拿到工具、按用户勾选过滤

// 注意:返回的 McpClientTool 是"远程调用句柄",模型执行工具时还要通过同一个客户端连接发请求,
// 因此这里【不能】用 await using 立即释放客户端,否则 transport 被关闭,
// 工具调用会报 "A task was canceled"。
var mcpClient = await McpClient.CreateAsync(transport);
try
{
    var mcpTools = await mcpClient.ListToolsAsync();

    // 只加载用户勾选的工具;勾选列表为空(历史数据)时回退为加载全部,避免破坏既有智能体
    var selectedTools = ParseSelectedTools(item.McpSelectedTools);
    var toolsToAdd = mcpTools.Cast<AITool>()
        .Where(t => selectedTools.Count == 0 || selectedTools.Contains(t.Name))
        .ToList();
    aiTools.AddRange(toolsToAdd);
}
catch
{
    // 拉取工具失败时释放客户端,避免连接/子进程泄漏
    await mcpClient.DisposeAsync();
    throw;
}

这里有两个坑,务必记住:

  1. McpClient 不能提前 Dispose。 McpClientTool 只是一个句柄,真正的调用发生在大模型选中它、框架回调时。如果用 await using var mcpClient = ...,方法返回时连接就断了,之后模型执行工具全部报 A task was canceled。正确做法是把 mcpClient 的释放交给上层(跟随会话生命周期),或者只在拉取工具失败这种确定不会再用的路径上释放。

  2. 单个 MCP 失败要 catch 住,不 throw。 循环里包一层 try/catch,一个远程服务挂掉不能拖垮整个智能体的工具装载:

    catch (Exception ex)
    {
        Kevin.log4Net.LogHelper.logger.Error(ex +
            string.Format("获取 MCP 工具失败, 类型: {0}, URL: {1}", item.McpType, item.McpUrl));
        // 继续处理下一个
    }
    

3.4 Authorization 的两种来源

智能体的调用场景分两类,代码里都做了兜底:

var Authorization = "";
if (HttpContextAccessor != default && HttpContextAccessor.Current() != default)
{
    // 场景 A:当前是 HTTP 请求上下文,直接透传前端带的 JWT
    if (HttpContextAccessor.Current().Request.Headers.ContainsKey("Authorization"))
    {
        Authorization = HttpContextAccessor.Current().Request.Headers["Authorization"].ToString();
    }
    // 有些客户端把 token 放在 Query(比如 SSE 长连接场景)
    if (string.IsNullOrEmpty(Authorization) || !JwtToken.IsBearerValidJwt(Authorization))
    {
        if (HttpContextAccessor.Current().Request.Query.ContainsKey("Authorization"))
        {
            Authorization = HttpContextAccessor.Current().Request.Query["Authorization"].ToString();
        }
    }
}
else
{
    // 场景 B:Hangfire 后台任务、消息队列消费者——没有 HttpContext
    // 从共享上下文里拿 UserId + TenantId,重新签发一个 token
    if (_aIShareInfoService.GetData().UserId > 0 && _aIShareInfoService.GetData().TenantId > 0)
    {
        Authorization = "Bearer " + await _authorizeService.GetTokenById(
            _aIShareInfoService.GetData().UserId,
            _aIShareInfoService.GetData().TenantId);
    }
}

场景 B 是很多教程不会讲的关键点:智能体不一定跑在 HTTP 请求里,定时任务、事件回调都要能调 MCP,这时候必须能"根据 UserId 反签一个 JWT"。

3.5 与本地工具混编

GetMcpTools 返回的是 List<AITool>,本地 AIFunctionFactory.Create(...) 返回的也是 AITool,两者可以直接合并:

public async Task<List<AITool>> GetAIAgentMcpToolsAsync(string agentId)
{
    var aiTools = new List<AITool>();
    var agentBindIds = (await _iAISkillToolBindIdService.GetListById(agentId))
        .Select(t => t.AISkillToolManagementId).ToList();
    var mcps = (await _iAISkillToolManagementService.GetNotDataPerAllMcps())
        .Where(t => agentBindIds.Contains(t.Id)).ToList();
    aiTools.AddRange(await GetMcpTools(mcps));
    return aiTools;
}

上游 ChatClient 拿到这个混合列表后无差别地传给大模型——模型根本不知道某个工具是本地方法还是远程 MCP,它只看到 Name / Description / JSON Schema。这正是 Microsoft.Extensions.AI 抽象的价值。


四、前端管理后台:MCP 可视化配置与工具列表加载

前面两节讲的都是"运行时"的事,但作为 SaaS 产品还有一个问题:这些 MCP 服务谁来配置? 答案是 Vue 管理后台的「AI 技能工具管理」页面——也就是第三节开头那张「编辑技能工具」弹窗。前端实现在 NetCoreKevin 的 SkillToolManagement.vue 页面,接口层在 src/api/ai/aiskilltoolManagement.js

4.1 一张表单覆盖三种传输

技能工具分三种类型:Tool(原生 C# 方法)、Skill(技能包)、Mcp(远程服务)。类型选 Mcp 后,表单动态出现以下字段,与后端 HttpClientTransportOptions / StdioClientTransportOptions 一一对应:

表单字段含义对应后端参数
Mcp地址远程 MCP 端点McpUrlEndpoint
Mcp类型https / sse / stdioMcpType → 决定 TransportMode 或 stdio 分支
McpHeadersJSON 键值对,如 {"Authorization": "Bearer xxx"}AdditionalHeaders
McpCommand / McpArguments / McpEnvironment仅 stdio 类型显示Command / Arguments / EnvironmentVariables
Mcp工具测试连接后的勾选列表McpSelectedTools

4.2 工具列表加载:点「测试连接」背后发生了什么

工具列表不是前端直连 MCP 服务器拉取的——浏览器里既没有 MCP 客户端,直连还有跨域和凭据泄露问题。正确姿势是前端调后端测试接口,由后端复用 3.3 同一套 MCP 客户端逻辑:

点击「测试连接」
   │  前端先校验:必选 Mcp类型;非 stdio 必填 Mcp地址;stdio 必填 McpCommand
   ▼
POST /api/AISkillToolManagement/TestMcpConnection
   │  报文:mcpUrl / mcpType / mcpHeaders / mcpCommand / mcpArguments / mcpEnvironment
   ▼
后端按类型创建 transport → McpClient.CreateAsyncListToolsAsync()
   ▼
返回 [{ name, description }, ...] → 前端渲染勾选列表 + 绿色标签「连接成功,共 N 个工具」

接口定义只有一行:

// 测试Mcp连接并返回该Mcp服务下的全部工具
export const testMcpConnection = (data) => {
  return http.post('/api/AISkillToolManagement/TestMcpConnection', data);
};

前端拿到响应后的核心处理(精简自 handleTestMcp):

const response = await testMcpConnection({
  mcpUrl: form.mcpUrl,
  mcpType: form.mcpType,
  mcpHeaders: form.mcpHeaders,
  mcpCommand: form.mcpCommand,
  mcpArguments: form.mcpArguments,
  mcpEnvironment: form.mcpEnvironment,
});
if (response && response.code === 200 && Array.isArray(response.data)) {
  mcpToolList.value = response.data.map((t) => ({
    name: t.name,
    description: t.description || "",
  }));
  // 保留仍然存在的已勾选工具,测试新返回的工具默认不勾选
  const names = mcpToolList.value.map((t) => t.name);
  form.mcpSelectedTools = (form.mcpSelectedTools || []).filter((n) => names.includes(n));
  mcpTested.value = true;
} else {
  mcpToolList.value = [];
  mcpTested.value = false;
  message.error(response?.err_msg || "测试连接失败");
}

三个值得借鉴的交互细节:

  1. 勾选增量保留:重新测试成功后,只剔除"已勾选但已不存在"的工具,新返回的工具默认不勾——避免远端新增工具悄悄进入 AI 的工具集;
  2. 搜索只过滤展示、不碰勾选:搜索框按名称/描述过滤 filteredMcpToolList,且全选只作用于当前过滤结果,不会误清过滤范围外已勾选的项(153 个工具里找几个,全靠这个);
  3. 硬校验兜底:提交规则要求"必须先测试连接成功且至少勾选一个工具",在表单层就杜绝"配了没连、勾选为空"的脏数据。

4.3 状态一致性:配置一变,测试结果必须作废

MCP 配置最容易出的错是:用户改了地址或 Headers,工具列表却还是旧的,保存下去就是"错配"组合。前端用一个 watch 强制保持一致:

// Mcp连接配置变更后,此前的测试结果与勾选失效,需重新测试连接
watch(
  () => [form.mcpUrl, form.mcpType, form.mcpHeaders, form.mcpCommand, form.mcpArguments, form.mcpEnvironment],
  () => {
    if (mcpRestoring.value) return;   // 编辑回显期间跳过
    mcpToolList.value = [];
    form.mcpSelectedTools = [];
    mcpTested.value = false;
  }
);

配套的 mcpRestoring 标志位也是个实战细节:编辑回显时给表单赋值会触发上面的 watch,刚回填的数据会被立即清空,所以回显期间用它抑制 watch、nextTick 后再放开。回显时先用后端保存的全量工具名(mcpTools)恢复列表(描述暂空)、按 mcpSelectedTools 恢复勾选,描述等再次点击「测试连接」才会带出。

4.4 保存的内容,决定 AI 加载什么

提交时前端把两个 JSON 数组随表单一起入库:

const mcpToolsPayload = JSON.stringify(mcpToolList.value.map((t) => t.name));   // 本次测试返回的全量工具名
const mcpSelectedPayload = JSON.stringify(form.mcpSelectedTools || []);        // 勾选启用的工具名

至此闭环接回 3.3:AI 运行时 GetMcpTools 读取 McpSelectedTools,只把勾选的工具 cast 成 AITool 喂给模型——管理后台勾了什么,模型就能调什么,一个不多一个不少


五、端到端串起来的一次调用

以"用户在 Vue 前端问:帮我查一下今天的销售订单"为例:

┌──────────────┐  1. HTTP + JWT   ┌──────────────────────┐
│  Vue 前端    │ ───────────────► │  App.WebApi (业务)   │
└──────────────┘                  │  AIAppsService       │
                                  └──────────┬───────────┘
                                             │ 2. 组装工具
                                             │   GetAITools() + GetMcpTools()
                                             ▼
                                  ┌──────────────────────┐
                                  │ AIAgentToolSkill     │
                                  │ Service              │
                                  └──────────┬───────────┘
                                             │ 3. MCP 握手
                                             │   HttpClientTransport
                                             │   Authorization 透传
                                             ▼
                                  ┌──────────────────────┐
                                  │ App.WebApi.Mcp       │
                                  │ /NetCoreKevinMcp     │
                                  └──────────┬───────────┘
                                             │ 4. Swagger 生成的 Tool
                                             │   翻译回 HTTP 请求
                                             ▼
                                  ┌──────────────────────┐
                                  │ 业务 REST API        │
                                  │ /api/v1/order/...    │
                                  └──────────────────────┘

四个环节里,业务开发者只需要维护最下面那一层 REST API(并写好 Swagger 注释),其余三层由框架自动打通。


六、常见坑清单

现象原因解决
客户端连上但列表为空Swagger 文档没暴露,或 SwaggerJsonUrl 打不通浏览器直接访问 swagger.json 验证
工具调用返回 401客户端没带 Authorization,或网关没转发看诊断中间件输出;检查 ForwardedHeaders 是否包含 Authorization
工具调用报 A task was canceledMcpClient 被提前 Dispose客户端生命周期跟随会话,不要在 ListToolsAsync 之后立即释放
Stdio 模式偶发失败生产环境没装 npx / uvx容器里预装 Node.js / Python,或者干脆禁用 Stdio,只走 HTTP
同一个 MCP 每次请求都重连每次都 new 一个 McpClient高频场景建议按 agentId + userId 做客户端池化
中文 Description 乱码Swagger JSON 编码问题业务 WebApi 输出 application/json; charset=utf-8

七、写在最后

MCP 在 .NET 生态里的成熟度比想象中高:微软官方 SDK 负责客户端,社区库 Mcpifier 负责服务端,两侧加起来不到 200 行代码就能把一套现成的 ASP.NET Core WebApi 接入到 AI 世界。

真正需要花心思的其实不是协议本身,而是三件工程化的事:

  1. 鉴权透传:MCP 只是通道,业务身份仍然靠 JWT / API Key;
  2. 生命周期McpClient 什么时候建、什么时候销毁,直接决定工具调用能不能成功;
  3. 可配置化:把 URL、传输模式、Header、勾选的工具列表都存到数据库,让用户在管理后台自助接入,才是 SaaS 场景下真正有价值的形态。

NetCoreKevin 的这套实现可以直接作为脚手架使用——把它抄过去,改改 appsettings.json,你的 .NET 项目就同时具备了"当 MCP 服务端"和"当 MCP 客户端"两种能力。