让 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 中台底座。本文完整源码可查看:
- Gitee:gitee.com/netkevin-li…
- GitHub:github.com/junkai-li/N…
对应实现可以参考 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.AspNetCore | 1.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() 拿到的对象可以直接塞进 ChatClient 的 tools 参数。
二、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 的作用一目了然:
AddMcpifier(...):配置 MCP 服务器本身(名字、转发目标、要透传哪些 Header)。AddAspNetCore():把 MCP 服务集成到 ASP.NET Core 请求管道。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); - McpHeaders →
AdditionalHeaders,未填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;
}
这里有两个坑,务必记住:
-
McpClient不能提前 Dispose。McpClientTool只是一个句柄,真正的调用发生在大模型选中它、框架回调时。如果用await using var mcpClient = ...,方法返回时连接就断了,之后模型执行工具全部报A task was canceled。正确做法是把mcpClient的释放交给上层(跟随会话生命周期),或者只在拉取工具失败这种确定不会再用的路径上释放。 -
单个 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 端点 | McpUrl → Endpoint |
| Mcp类型 | https / sse / stdio | McpType → 决定 TransportMode 或 stdio 分支 |
| McpHeaders | JSON 键值对,如 {"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.CreateAsync → ListToolsAsync()
▼
返回 [{ 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 || "测试连接失败");
}
三个值得借鉴的交互细节:
- 勾选增量保留:重新测试成功后,只剔除"已勾选但已不存在"的工具,新返回的工具默认不勾——避免远端新增工具悄悄进入 AI 的工具集;
- 搜索只过滤展示、不碰勾选:搜索框按名称/描述过滤
filteredMcpToolList,且全选只作用于当前过滤结果,不会误清过滤范围外已勾选的项(153 个工具里找几个,全靠这个); - 硬校验兜底:提交规则要求"必须先测试连接成功且至少勾选一个工具",在表单层就杜绝"配了没连、勾选为空"的脏数据。
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 canceled | McpClient 被提前 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 世界。
真正需要花心思的其实不是协议本身,而是三件工程化的事:
- 鉴权透传:MCP 只是通道,业务身份仍然靠 JWT / API Key;
- 生命周期:
McpClient什么时候建、什么时候销毁,直接决定工具调用能不能成功; - 可配置化:把 URL、传输模式、Header、勾选的工具列表都存到数据库,让用户在管理后台自助接入,才是 SaaS 场景下真正有价值的形态。
NetCoreKevin 的这套实现可以直接作为脚手架使用——把它抄过去,改改 appsettings.json,你的 .NET 项目就同时具备了"当 MCP 服务端"和"当 MCP 客户端"两种能力。