接入 McpFramework 让任意 .NET 程序被 Agent 遥控
2026 年,MCP 已是 Agent 连接外部系统的默认协议。.NET 侧却缺一个"引用即开放"的通用框架。McpFramework 补上这一层:引用库、标特性、一行启动,控制台、Web、WinForms、WPF 都能被 Agent 通过 MCP 实时控制。
这篇文章先把"MCP 是什么"讲清楚,再给出接入方式、三种传输的差异、四类项目的改法,以及一个真实的 SSE 踩坑。所有代码都可运行,文末给出 Agent 侧配置。
一、先说清楚:MCP 是什么
MCP(Model Context Protocol,模型上下文协议),由 Claude 母公司 Anthropic 推出,是一个连接大模型与外部工具、数据源、应用系统的开放协议。它要解决一个具体问题:大模型以前只能靠提示词和静态上下文,碰不到你的文件、数据库、API。MCP 给出一条标准化通道,让模型按需访问这些资源、执行操作。
最贴切的比喻是 USB-C。USB-C 统一了设备与外设的接口;MCP 统一了模型与外部系统的接口。有了它,同一个工具写一次,就能被任何支持 MCP 的宿主复用。

图:MCP 像一根标准的 USB-C 线,一端接 MCP 客户端,另一端接远程服务与本地数据源。
三部分:Host、Client、Server
MCP 采用客户端—服务器架构,拆成三层:
- MCP Host:发起连接的宿主应用,比如 Claude Desktop、Cursor、Cline。
- MCP Client:宿主里的通信层,负责建连、发现能力(Tools/Resources/Prompts)、把模型的调用请求转成协议消息。
- MCP Server:能力提供方,把文件系统、数据库、API 封装成工具,收到请求就执行具体操作。

图:一个 Host 里可以挂多个 Client,各自连一个 Server;Server 再对接本地文件、数据库或互联网 API。
三个核心组件:Tools、Resources、Prompts
Server 对外只暴露三类东西:
- Tools(工具):可执行的能力,会改变系统状态。比如运行命令、调用 API、改文件。模型推理时决定调哪个、传什么参数。
- Resources(资源):只读的数据,给模型补上下文。比如文件内容、数据库记录、代码仓库。它不执行操作,只提供信息。
- Prompts(提示词):标准化的任务模板,规定模型"怎么做"。带输入输出 schema,可复用。
一句话区分:Tools 动手,Resources 给料,Prompts 定规矩。
工作流程
一次完整调用分七步:建连 → 协商规则 → 发现能力 → 模型思考选工具 → 客户端转标准格式发出 → 服务端调外部系统执行 → 结果回流并更新上下文。

图:从左到右是用户、MCP Client、MCP Server、以及被调用的外部工具;虚线表示请求与结果的双向流转。
二、三种传输:STDIO、SSE、Streamable HTTP
协议统一用 JSON-RPC,但底层怎么传,有三种方式。

图:从上到下是 LLM、MCP Client 层、传输通道(STDIO / HTTP / WebSocket)、MCP Server,再落到外部系统。
| 传输 | 适用场景 | 端点 | 特点 |
|---|---|---|---|
| STDIO | CLI、本地进程 | 进程 stdout | 父子进程直连,无网络、低延迟、强隔离 |
| SSE | 远程服务(旧) | /sse + /message |
单向流,服务端推送;需配额外 POST 通道 |
| Streamable HTTP | 远程服务(新) | /mcp |
POST 请求,可一次性返回也可流式;易过网关、认证 |
STDIO 是默认方式。宿主作为父进程拉起 Server 子进程,通过 stdin 发请求、stdout 收响应。整个过程无网络,像 USB 直插。代价是 stdout 必须是纯净 JSON,日志混进去就会破坏协议帧。
SSE 基于 HTTP 长连接,服务端单向推事件。实现简单、天然流式,但通信是单向的,得再配一个 POST 接口才能完成请求—响应闭环。官方已在最新规范中弃用它。
Streamable HTTP 是替代方案,也是现在推荐的方式。客户端 POST 请求,服务端可以一次返回,也可以按需流式返回中间结果。它不依赖单一长连接,更容易接认证、网关、负载均衡。

图:客户端 POST 一个请求,服务端返回 200 后持续推送 chunk,最后以 DONE 结束流。
三、官方 SDK 给了原语,没给"一键开放"
先看硬数据。Microsoft 的 ModelContextProtocol 2.3.0 与 ModelContextProtocol.AspNetCore 2.3.0,对应 2026-07-28 协议修订版。这版协议里,SEP-2567 取消了 Streamable HTTP 的会话机制(Mcp-Session-Id 被移除)。
这两个包只含服务端类型。我们的自测客户端想调用 McpClientFactory,在 ModelContextProtocol 2.3.0 里翻遍了都找不到,最后只能用裸 JSON-RPC 手搓。这不是 SDK 的错,它本来就只负责协议原语。
社区现状也不算空白。MCPSharp 走属性式 .NET 服务;Runiq.AI.Mcp 绑定 ASP.NET Core 管线;Python 那边有 FastMCP。但 .NET 侧缺一个"嵌入即开放"的通用 lib——你引用它,就能把应用里的方法变成 MCP 工具,不用读协议、不用写传输。
McpFramework 的定位很窄:对官方 SDK 做薄封装,把"开放能力"从读协议降到标特性。它不造协议,只造体验。
四、安装与接入三步
准备:安装 McpFramework
包已发布到 NuGet,包名 McpFramework。一条命令装上:
dotnet add package McpFramework
或者手写 PackageReference:
<PackageReference Include="McpFramework" Version="1.0.0" />
包在 nuspec 里声明了 Microsoft.AspNetCore.App 框架引用,消费端会自动带上,不用手动加。它依赖官方 ModelContextProtocol 2.3.0 与 ModelContextProtocol.AspNetCore 2.3.0,dotnet restore 时一并拉取。
还没上传到 nuget.org 的话,可以先用本地源安装:先把
McpFramework.1.0.0.nupkg放进一个目录,再执行dotnet add package McpFramework --source <该目录>。
第一步:把方法标成工具
using System.ComponentModel;
using ModelContextProtocol.Server;
[McpServerToolType]
public static class MyTools
{
[McpServerTool, Description("设置设备状态文本")]
public static string SetStatus([Description("新文本")] string text)
{
// 这里改你自己的运行时状态
return "ok";
}
[McpServerTool, Description("读取当前状态")]
public static string GetStatus() => "running";
}
[McpServerToolType] 标在类上,[McpServerTool] 标在方法上,[Description] 是写给 Agent 看的说明书。方法有几个参数、参数什么类型,SDK 会自动生成 JSON Schema,Agent 照着 schema 传参。
第二步:一行启动
Stdio 适合 CLI,由 Agent 当子进程拉起:
using McpFramework;
await McpServer.StartAsync(new McpFrameworkOptions
{
Transport = McpTransport.Stdio,
ServerName = "ConsoleApp",
Version = "1.0.0"
});
Http 适合常驻程序,MCP 端点随应用一起跑:
await McpServer.StartAsync(new McpFrameworkOptions
{
Transport = McpTransport.Http,
HttpPort = 5200,
ServerName = "WinFormsApp",
Version = "1.0.0",
EnableLegacySse = true // 同时暴露 SSE 端点,见第七节
});
HttpHost 默认 http://localhost,改端口一行搞定。Version 会出现在 initialize 的 serverInfo 里,方便 Agent 区分是谁。
第三步:让 Agent 连上
在 mcp.json 里写两条即可,Stdio 与 Http 各一条:
{
"mcpServers": {
"webapp": { "url": "http://localhost:5000/mcp" },
"winforms": { "url": "http://localhost:5200/mcp" },
"wpf": { "url": "http://localhost:5300/mcp" },
"console": {
"command": "dotnet",
"args": ["src/ConsoleApp/bin/Debug/net8.0/ConsoleApp.dll"]
}
}
}
要点:HTTP 类走 url,Stdio 类走 command。如果客户端只支持 SSE(第七节专讲),把 url 换成 http://localhost:5200/mcp/sse。
五、四类项目怎么改(带完整代码)
Web(Razor Pages)
using McpFramework;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddSingleton<WebAppState>();
builder.Services.AddMcpFramework(new McpFrameworkOptions
{
Transport = McpTransport.Http,
ServerName = "WebApp",
Version = "1.0.0",
EnableLegacySse = true
});
var app = builder.Build();
app.MapRazorPages();
app.MapMcpFramework(new McpFrameworkOptions { Transport = McpTransport.Http });
app.Run("http://localhost:5000");
AddMcpFramework 负责注册,MapMcpFramework 负责挂端点,两者与 Razor Pages 共用一条请求管线。工具类支持构造函数依赖注入,直接从容器拿状态:
[McpServerToolType]
public sealed class WebTools
{
private readonly WebAppState _state;
public WebTools(WebAppState state) => _state = state;
[McpServerTool, Description("设置站点标题,并返回确认。")]
public string SetTitle([Description("新标题")] string title)
{
_state.Title = title;
return $"title set: {title}";
}
[McpServerTool, Description("读取当前站点标题。")]
public string GetTitle() => _state.Title;
[McpServerTool, Description("返回已注册的页面列表。")]
public string[] GetPageList() => new[] { "/Index", "/Privacy", "/Error" };
}
这样 Agent 改标题,Razor 页面渲染时就能读到新值——状态是同一个单例。
WinForms:Control.Invoke 编组
MCP 工具跑在 Kestrel 的后台线程,直接改控件会抛跨线程异常。必须切回 UI 线程:
[McpServerToolType]
public static class WinFormsTools
{
[McpServerTool, Description("设置主窗体标签文本。")]
public static string SetLabel([Description("新标签文本")] string text)
{
var form = AppContextHolder.Form;
if (form is null) return "error: form not ready";
form.Invoke((Action)(() => form.SetLabel(text)));
return "label updated";
}
[McpServerTool, Description("读取当前标签与可见状态。")]
public static object GetFormState()
{
var form = AppContextHolder.Form;
if (form is null) return new { status = "not ready" };
return form.Invoke((Func<object>)(() =>
(object)new { label = form.LabelText, visible = form.Visible }))!;
}
}
启动时让 Kestrel 与 UI 线程并行,窗体关闭再取消令牌:
var cts = new CancellationTokenSource();
var serverTask = McpServer.StartAsync(new McpFrameworkOptions
{
Transport = McpTransport.Http,
HttpPort = 5200,
ServerName = "WinFormsApp",
EnableLegacySse = true
}, cts.Token);
ApplicationConfiguration.Initialize();
var form = new MainForm();
AppContextHolder.Form = form;
form.FormClosed += (_, _) => cts.Cancel();
Application.Run(form);
await serverTask;
WPF:Dispatcher.Invoke 编组
WPF 同理,只是换成 Dispatcher:
[McpServerToolType]
public static class WpfTools
{
[McpServerTool, Description("设置主窗口状态文本。")]
public static string SetText([Description("新文本")] string text)
{
var w = WpfContext.Window;
if (w is null) return "error: window not ready";
w.Dispatcher.Invoke(() => w.SetStatus(text));
return "text updated";
}
[McpServerTool, Description("读取主窗口当前状态文本。")]
public static object GetViewModel()
{
var w = WpfContext.Window;
if (w is null) return new { status = "not ready" };
return w.Dispatcher.Invoke(() => (object)new { text = w.Status })!;
}
}
WPF 的启动放在 App.OnStartup 里,OnExit 时取消令牌:
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var window = new MainWindow();
WpfContext.Window = window;
_cts = new CancellationTokenSource();
_ = McpServer.StartAsync(new McpFrameworkOptions
{
Transport = McpTransport.Http,
HttpPort = 5300,
ServerName = "WpfApp",
EnableLegacySse = true
}, _cts.Token);
MainWindow = window;
window.Show();
}
Console:Stdio 默认,--http 切 SSE
一个程序两种跑法。Stdio 给 Agent 拉子进程,--http 给浏览器或 SSE 客户端直连调试:
using McpFramework;
var useHttp = args.Contains("--http", StringComparer.OrdinalIgnoreCase);
if (useHttp)
{
Console.Error.WriteLine("ConsoleApp (HTTP+SSE): http://localhost:5400/mcp | /mcp/sse");
await McpServer.StartAsync(new McpFrameworkOptions
{
Transport = McpTransport.Http,
HttpPort = 5400,
ServerName = "ConsoleApp",
EnableLegacySse = true
});
}
else
{
Console.Error.WriteLine("ConsoleApp (Stdio) 启动,等待 Agent 接入。");
await McpServer.StartAsync(new McpFrameworkOptions
{
Transport = McpTransport.Stdio,
ServerName = "ConsoleApp"
});
}
Stdio 模式下框架会自动 Logging.ClearProviders()——因为 stdout 是 JSON-RPC 通道,任何日志写进去都会破坏协议帧。这是框架替开发者挡掉的一个坑。
六、端到端实测
四个项目全部接入并验证:
| 项目 | 传输 | 端点 | 暴露的工具 |
|---|---|---|---|
| ConsoleApp | Stdio / HTTP+SSE | 子进程 / :5400/mcp |
echo / add / get_time / set_message / get_message |
| WebApp | HTTP | :5000/mcp + /mcp/sse |
set_title / get_title / get_page_list / get_visitors |
| WinFormsApp | HTTP | :5200/mcp + /mcp/sse |
show_message / set_label / get_form_state |
| WpfApp | HTTP | :5300/mcp + /mcp/sse |
show_message / set_text / get_view_model |
自测客户端 McpSmokeTest(Stdio + HTTP 混合)输出 ALL TESTS PASSED;dotnet build TestMCP.sln 0 警告 0 错误。三个 HTTP 程序的 /mcp/sse 都返回 200 + Content-Type: text/event-stream。
七、SSE 握手实录
SSE 传输分两条通道:GET 建流,POST 送消息。用 curl 完整走一遍:
# 1) 建立 SSE 流。连上后服务端先推一个 endpoint 事件,告知 POST 地址
curl -N http://localhost:5200/mcp/sse -H "Accept: text/event-stream"
# 服务端立即回:
# HTTP/1.1 200 OK
# Content-Type: text/event-stream
#
# event: endpoint
# data: /mcp/message?sessionId=_rqTQHN_kGpN58zfkhIZuA
# 2) 把 JSON-RPC 消息 POST 到那个地址。返回 202 Accepted,响应走 SSE 流回传
curl -X POST "http://localhost:5200/mcp/message?sessionId=_rqTQHN_kGpN58zfkhIZuA" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call",
"params":{"name":"set_label","arguments":{"text":"SSE 控制 WinForms"}}}'
# 3) SSE 流里收到结果
# event: message
# data: {"result":{"content":[{"type":"text","text":"label updated"}]},"id":2,"jsonrpc":"2.0"}
再 POST 一次 get_form_state,流里回传 {"label":"SSE 控制 WinForms","visible":true}。这说明 SSE 传输与 UI 线程编组同时成立——标签真被改了,状态也真被读到了。
八、一个真实坑:SSE 与 Streamable 不匹配
把 WebApp 接进 WorkBuddy 时,报错 SSE error: Invalid content type, expected "text/event-stream"。
原因不复杂:客户端走遗留 SSE,发 GET 并期望 text/event-stream;服务端只开 Streamable HTTP,POST-only,且 Stateless 模式下 GET 被禁用。实测 GET /mcp 返回 405 Method Not Allowed,响应里连 Content-Type 都没有。客户端拿到一个非 SSE 响应,解析器直接报错。
修复只做两件事:开 EnableLegacySse,再把客户端 URL 从 /mcp 换成 /mcp/sse。改完对比:
| 请求 | 改前 | 改后 |
|---|---|---|
GET /mcp |
405 Method Not Allowed |
— |
GET /mcp/sse |
— | 200,Content-Type: text/event-stream,先收到 event: endpoint |
重连后 initialize 与 tools/list 完整走通,四个工具全部可用。这个坑值得写出来,因为"SSE 还是 Streamable"是 2026 年 MCP 接入最高频的错配。
九、Agent 侧的配置
四种 server 在 mcp.json 里的写法,注意 SSE 客户端要指到 /mcp/sse:
{
"mcpServers": {
"webapp": { "url": "http://localhost:5000/mcp" },
"webapp-sse": { "url": "http://localhost:5000/mcp/sse" },
"winforms": { "url": "http://localhost:5200/mcp" },
"wpf-sse": { "url": "http://localhost:5300/mcp/sse" },
"console-sse": { "url": "http://localhost:5400/mcp/sse" },
"console": {
"command": "dotnet",
"args": ["src/ConsoleApp/bin/Debug/net8.0/ConsoleApp.dll"]
}
}
}
同一个程序可以同时挂两条:要 Streamable 的客户端走 /mcp,只认 SSE 的客户端走 /mcp/sse,互不干扰。
结论
McpFramework 把”开放能力给 Agent”从读协议降到标特性。4 类 .NET 程序、3 种传输、2 个目标框架都已实测可行:net8.0 拿到 Http 与 SSE,netstandard2.0 拿到 Stdio,.NET Framework 4.8 的老项目同样能接。它不解决所有问题,但解决了最常见的那一步:让一个普通 .NET 程序在几行代码内变成 Agent 可控的端点。
分发这一步已经完成。包在 http://nuget.org 上,包 ID McpFramework,最新版 1.1.0,一行 dotnet add package McpFramework 就能用。接下来两件事:补官方客户端封装,让自测和二次开发不必再手搓 JSON-RPC;把 WinForms/WPF 的 UI 线程编组抽成基类,省掉每个项目重复写 Invoke 的样板。
包地址:https://www.nuget.org/packages/McpFramework/ 。装完跑不通,把 GET /mcp 的返回码和 Content-Type 贴出来,基本能定位是不是第八节那个传输错配。
配图来源:腾讯云开发者社区《MCP 到底是什么?一文带你彻底搞懂》(作者 Al1ex),仅作技术示意。

浙公网安备 33010602011771号