XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

MCP ​

这一页讲 MCP(Model Context Protocol)适配器:怎样把操作发布成大模型可调用的 tool,tools/list 里的名字、Schema、标题描述与注解从哪里来,tools/call 怎样经过完整管道并区分业务失败与协议错误,HTTP 与 stdio 两种传输各自的规则,以及可配置的选项。读完你能让 Claude、Cursor 这类 MCP 客户端直接调用你的业务操作,而不另写一个 MCP 服务端。前置知识是操作契约与上一页 gRPC 开头的「一份契约,多个载体」。

为什么需要 ​

要让大模型调用业务系统,通常得为每个功能手写一个 tool:名字、描述、参数 Schema、调用代码,再处理认证与错误。这些信息业务系统其实都有,只是分散在控制器、DTO、注释与权限配置里,手写一遍就多一份会过期的拷贝,而且 tool 的实现往往绕过了业务层的授权与校验。

框架把 MCP 当作操作契约的又一个载体:tool 名由操作 ID 推出,inputSchema / outputSchema 就是契约的 JSON Schema,标题与描述来自消息目录并按调用方语言翻译,注解按动词推导。每个 tools/call 在自己的服务作用域里经完整内核管道执行,认证、按契约授权、校验与事务和 HTTP 上完全一样,大模型拿不到比当前用户更多的权限。

tool 名与 tools/list ​

tool 名按 {resource}_{verb} 推导(orders.create → orders_create),显式覆盖写在配置 XiHan:Protocols:Mcp:Tools:<操作 ID>。名字必须匹配 ^[A-Za-z0-9_-]{1,64}$,不合法时以 PROTOCOLS_VALIDATION_004、两个操作同名时以 PROTOCOLS_CONFLICT_001 启动失败。tools/list 不分页,每个 tool 的字段:

字段来源
nametool 名
title / description消息目录 <操作 ID>.title / <操作 ID>.description(操作 ID 里的 - 换成 _),按调用方区域翻译;没有就不给
inputSchema请求 Schema;Schema 的 type 不是 object 或操作没有 Schema 时为 {"type":"object"}
outputSchema响应 Schema,仅当其 type 是 object
annotations按契约推导,见下表
契约readOnlyHintdestructiveHintidempotentHint
只读(get、list,或 ReadOnly = true 的非标准动词)truefalsetrue
createfalsefalsefalse
updatefalsefalsetrue
deletefalsetruetrue
其他动词falsetruefalse

只读必须在 [Operation] 上显式声明:没有声明的查询类自定义动词(如 orders.search)按 MCP 的默认标成可能破坏,客户端会要求用户确认。描述里不写授权要求,由协议适配器统一呈现。

最小示例 ​

我们给 SimpleApp 加上 MCP。三步:引用 XiHan.Framework.Protocols.Mcp 并装配 McpProtocolFeature;在消息目录里给要暴露的操作写标题与描述;UseXiHan() 之后调用 MapXiHanMcp()。

bash
dotnet add package XiHan.Framework.Protocols.Mcp --prerelease

操作在 Expose 里带上 Mcp(上一页的 CreateOrder 已经带了)。标题与描述放在应用的语言文件里,每种语言一个文件,写法见全球化和本地化。这是 Localization/en-US.json:

json
{
  "orders.create.title": "Place an order",
  "orders.create.description": "Place an order with a name and an amount.",
  "orders.list.title": "List orders",
  "orders.list.description": "List the orders of the current tenant in creation order."
}
csharp
using SimpleApp;
using SimpleApp.Orders;
using XiHan.Framework.Hosting.AspNetCore;
using XiHan.Framework.Kernel.Hosting;
using XiHan.Framework.Localization.Abstractions;
using XiHan.Framework.Localization.Abstractions.Messages;
using XiHan.Framework.Profiles.Standard;
using XiHan.Framework.Protocols.Http;
using XiHan.Framework.Protocols.Http.Routing;
using XiHan.Framework.Protocols.Mcp;
using XiHan.Framework.Protocols.Mcp.Transports;

var builder = WebApplication.CreateBuilder(args);

builder.AddXiHan(xihan => xihan
    .UseStandardProfile()
    .UseFeature<HttpProtocolFeature>()
    .UseFeature<McpProtocolFeature>()
    .UseFeature<OrdersFeature>());

// 应用自己的消息目录:tool 的 title 与 description 从这里取
builder.Services.AddMessageCatalog(MessageCatalog.FromEmbeddedResources("samples", typeof(SampleMessages)));

var app = builder.Build();

app.UseXiHan();
app.MapXiHanOperations();
app.MapXiHanMcp();   // POST /mcp:initialize、ping、tools/list、tools/call

app.Run();

运行后先列 tool,再以 bob 的凭据下单:

bash
curl -s -X POST localhost:5000/mcp -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
curl -s -X POST localhost:5000/mcp -H "Content-Type: application/json" -H "Authorization: ApiKey dev-bob-key" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"orders_create","arguments":{"name":"widget","amount":3}}}'

第一个请求返回 orders_create 与 orders_list 两个 tool,带英文标题与描述(加 Accept-Language: zh-CN 头就是中文)。bob 只有 orders.read,第二个请求的答复不是协议错误,而是业务失败:

json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "content": [{ "type": "text", "text": "SECURITY_DENIED_004: Permission 'orders.write' is missing." }],
    "isError": true
  }
}

换成 alice(dev-alice-key,持有 orders.write)再调一次,content[0].text 是新订单的 JSON,同时给出 structuredContent。

tools/call 的语义 ​

arguments(缺省 {},必须是对象)就是操作的载荷。每个 tools/call 是一次独立调用:以承载请求的头部建新的调用上下文,在自己的服务作用域里经完整管道执行,工作单元与作用域服务不与外层 HTTP 请求共享;调用来源记协议 mcp 与 tool 名。

成功时 content[0].text 是响应 JSON,响应是对象时同时给 structuredContent。操作失败(校验、未授权、冲突)不是协议错误,而是 isError: true 的结果,文本是 <错误码>: <文案>,文案按调用方区域翻译。这样区分的原因:大模型需要读到业务失败的原因来调整下一步(改参数、换 tool、告诉用户没权限),而协议错误只说明请求本身不合法。

Streamable HTTP 传输与 JSON-RPC 错误 ​

消息为 JSON-RPC 2.0,HTTP 上是 Streamable HTTP 传输:POST 一条消息一个答复(application/json);通知(没有 id,如 notifications/initialized)与客户端发来的响应不答复,回 202;不开 SSE 流、不建会话,GET 与 DELETE 为 405。请求带的 MCP-Protocol-Version 头与服务端版本不同时回 400,缺失按当前版本处理。

/mcp 本身是经过 UseXiHan() 的普通端点:请求凭据在外层认证,凭据无效时整个请求以 401 Problem Details 拒绝、带 WWW-Authenticate 质询,不进入 JSON-RPC;应用未就绪时 503。没有凭据的请求照常进入,按契约的授权在每个 tools/call 的管道里判定,所以匿名客户端能 tools/list,调需要权限的 tool 时得到 isError。

情形error.codeerror.data.codeHTTP
请求体不是 JSON-32700PROTOCOLS_VALIDATION_001400
不是 JSON-RPC 2.0 请求(非对象、jsonrpc 不是 2.0、缺 method、id 为 null、params 不是对象)-32600PROTOCOLS_VALIDATION_001400
MCP-Protocol-Version 不受支持-32600PROTOCOLS_VALIDATION_002400
方法不受支持-32601PROTOCOLS_NOTFOUND_001200
tools/call 缺 name-32602PROTOCOLS_VALIDATION_001200
tool 未登记-32602PROTOCOLS_NOTFOUND_002200
arguments 不是对象-32602PROTOCOLS_VALIDATION_003200

stdio 传输 ​

桌面 MCP 客户端常以子进程方式启动服务端,经 stdin / stdout 通信。装配了 McpProtocolFeature 与 AddXiHanCommandLine() 的命令行宿主提供 mcp 命令:stdin 与 stdout 每行一条 JSON-RPC 消息,日志只写 stderr,stdin 关闭即结束会话。stdin 的字节按 UTF-8 解码,与控制台代码页无关(Windows 上重定向的标准输入不按 936、437 等代码页解码,中文参数不会成乱码);答复里的非 ASCII 字符按 JSON 转义成 \uXXXX,只含 ASCII,按任何控制台输出代码页写出都是合法的 UTF-8。

bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | dotnet run --project framework/samples/SimpleCli -- mcp

mcp 命令接受与 invoke 相同的上下文选项,作用于整个会话里的每个 tools/call:

选项作用
--authorization '<scheme> <value>' 或环境变量 XIHAN_AUTHORIZATION凭据;选项优先。MCP 规范要求 stdio 服务端从运行环境取凭据,所以客户端配置里通常写环境变量
--tenant <id>、--correlation <id>租户与关联 ID
--locale <区域>tools/list 的标题与描述、失败文案按它翻译
--timeout <秒>每个 tools/call 的时限
bash
XIHAN_AUTHORIZATION="ApiKey dev-alice-key" dotnet run --project framework/samples/SimpleCli -- mcp --tenant acme --locale zh-CN

每个 tools/call 经完整管道,凭据在这次调用的 Authenticate 阶段认证;stdio 没有外层认证,凭据无效时需要认证的 tool 得到 isError(SECURITY_DENIED_003),会话继续。选项不合法(凭据不是 <scheme> <value>、时限不是正数)时不进入会话,stderr 写一行用法错误,退出码 64。调用来源只有协议 mcp 与 tool 名。宿主的搭建见命令行宿主。

选项 ​

配置节 XiHan:Protocols:Mcp(McpProtocolOptions):

键类型缺省说明
ServerNamestring宿主的应用名initialize 答复的 serverInfo.name
ServerVersionstring框架版本serverInfo.version
Instructionsstring空initialize 答复的 instructions,给大模型的使用说明,空时不写
Tools:<操作 ID>string—显式 tool 名

initialize 总是答复本服务端的协议版本与 capabilities.tools.listChanged = false,由客户端决定是否继续;ping 答复 {}。

常见问题 ​

tool 列表里没有我的操作 ​

依次检查:[Operation] 的 Expose 是否含 Mcp;Stability 是否为 Internal(内部操作不在任何协议上发布);操作所在的特性是否 UseFeature 了、Configure 里是否调用了 AddGeneratedOperations()。用命令行宿主的 operations 命令能直接看到每个操作暴露在哪些协议上。

标题与描述是空的 ​

tools/list 只在消息目录里有 <操作 ID>.title / <操作 ID>.description 键时才输出这两个字段。确认语言文件里有这两个键、文件以 LogicalName="Localization/%(Filename)%(Extension)" 嵌入、AddMessageCatalog 已注册。操作 ID 带连字符时键里要换成下划线:tenant-members.list 的键是 tenant_members.list.title。

客户端每次都要求确认我的查询 tool ​

MCP 客户端按 destructiveHint 决定是否确认。只有 get、list 两个动词自动算只读,search、export 这类自定义动词要在 [Operation] 上写 ReadOnly = true,否则按默认标成可能破坏。

能用 SSE 或长连接吗 ​

不能。这个实现只做 Streamable HTTP 的请求 / 响应部分:一条 POST 一个答复,不开 SSE 流、不建会话。服务端到客户端的通知与 listChanged 都不支持,tool 列表只在重启后变化。

大模型会不会拿到比用户更多的权限 ​

不会。每个 tools/call 带着这次 HTTP 请求(或 stdio 会话)的凭据经完整管道,Authorize 阶段按契约里的 Permission / Policy 判定,和 HTTP 上调同一个操作完全一样。凭据没有的权限,tool 也没有。

相关包与下一步 ​

Released under The MIT License