MCP服务器 用 MCP 让 AI 操控你的 WinForms 桌面应用
用 MCP 让 AI 操控你的 WinForms 桌面应用
为什么要做这件事?
大语言模型(LLM)很擅长处理文本,但它们无法直接操作你的桌面程序。想象一下这样的场景:
- 你正在写一个工控上位机软件,想让 AI 帮你自动填写参数、读取界面数据
- 你开发了一个内部管理工具,想让 AI 助手帮你批量操作界面上的表单
- 你做了一个数据可视化应用,想让 AI 根据用户对话内容动态更新界面
MCP(Model Context Protocol) 正是解决这个问题的协议。它定义了 AI 模型与外部工具之间的通信标准,让你可以把任何功能"暴露"给 AI 调用。
本教程演示如何搭建一个 WinForms 应用,通过 MCP 协议让 AI 可以:
- 读取文本框中的内容
- 设置文本框的内容
- 点击界面上的按钮
环境准备
| 工具 | 版本要求 | 用途 |
|---|---|---|
| .NET SDK | 8.0+ | 编译和运行项目 |
| Visual Studio 2022 | 17.8+ | 开发 IDE(也可用 VS Code + C# Dev Kit) |
第一步:创建项目
dotnet new winforms -n McpTestDemo1
cd McpTestDemo1
dotnet add package ModelContextProtocol.AspNetCore --version 1.4.1
关键点: 使用
ModelContextProtocol.AspNetCore包(不是ModelContextProtocol),它封装了 MCP 协议的全部实现,我们只需要关注业务逻辑。
项目文件 McpTestDemo1.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.4.1" />
</ItemGroup>
</Project>```
## 第二步:设计主窗体
修改 `Form1.Designer.cs`,添加一个 TextBox 控件作为演示:
```csharp
namespace McpTestDemo1
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
textBox1 = new TextBox();
SuspendLayout();
//
// textBox1
//
textBox1.Location = new Point(216, 169);
textBox1.Name = "textBox1";
textBox1.Size = new Size(282, 23);
textBox1.TabIndex = 0;
//
// Form1
//
AutoScaleDimensions = new SizeF(7F, 17F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(800, 450);
Controls.Add(textBox1);
Name = "Form1";
Text = "Form1";
ResumeLayout(false);
PerformLayout();
}
#endregion
public TextBox textBox1;
}
}
窗体代码 Form1.cs:
namespace McpTestDemo1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}
重要:
textBox1必须声明为public,因为 MCP 工具类需要在外部访问它。实际项目中也可以通过属性或方法来暴露控件。
第三步:编写 MCP 工具(核心)
创建 MyWinFormTools.cs 文件,定义 AI 可以调用的工具:
using ModelContextProtocol.Server;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
namespace McpTestDemo1
{
[McpServerToolType]
public class MyWinFormTools
{
private static Form1 _mainForm;
public static void Initialize(Form1 mainForm)
{
_mainForm = mainForm;
}
[McpServerTool]
public static string GetTextBoxContent()
{
if (_mainForm == null) return "Error: Form not initialized.";
return (string)_mainForm.Invoke((Func<string>)(() => _mainForm.textBox1.Text));
return "123";
}
[McpServerTool, Description("desc")]
public static string SetTextBoxContent(
[Description ( "要设置的文本内容")] string newText)
{
if (_mainForm == null) return "Error: Form not initialized.";
_mainForm.Invoke((MethodInvoker)(() => _mainForm.textBox1.Text = newText));
return $"文本已成功设置为: {newText}";
}
[McpServerTool]
public static string ClickButton()
{
if (_mainForm == null) return "Error: Form not initialized.";
//_mainForm.Invoke((MethodInvoker)(() => _mainForm.button1.PerformClick()));
return "按钮已点击。";
}
}
}
第四步:TCP 通信层(备选方案)
以下是基于 TCP 的 MCP 服务器实现(当前已注释,改用 HTTP 方案):
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace McpTestDemo1
{
//public class TcpMcpServer
//{
// private readonly TcpListener _listener;
// private readonly int _port;
// private CancellationTokenSource _cts;
// public TcpMcpServer(int port = 12345)
// {
// _port = port;
// _listener = new TcpListener(IPAddress.Loopback, port);
// }
// public void Start()
// {
// _cts = new CancellationTokenSource();
// _listener.Start();
// Console.WriteLine($"TCP MCP Server started on port {_port}");
// Task.Run(() => AcceptClientsAsync(_cts.Token));
// }
// public void Stop()
// {
// _cts?.Cancel();
// _listener.Stop();
// }
// private async Task AcceptClientsAsync(CancellationToken ct)
// {
// while (!ct.IsCancellationRequested)
// {
// var client = await _listener.AcceptTcpClientAsync();
// _ = HandleClientAsync(client, ct);
// }
// }
// private async Task HandleClientAsync(TcpClient client, CancellationToken ct)
// {
// using (client)
// using (var stream = client.GetStream())
// using (var reader = new StreamReader(stream, Encoding.UTF8))
// using (var writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true })
// {
// Console.WriteLine("Client connected");
// while (!ct.IsCancellationRequested)
// {
// var line = await reader.ReadLineAsync();
// if (line == null) break;
// var response = ProcessRequest(line);
// await writer.WriteLineAsync(response);
// }
// Console.WriteLine("Client disconnected");
// }
// }
// private string ProcessRequest(string request)
// {
// try
// {
// var json = JsonDocument.Parse(request);
// var method = json.RootElement.GetProperty("method").GetString();
// var id = json.RootElement.TryGetProperty("id", out var idProp) ? idProp.Clone() : default;
// object result = method switch
// {
// "tools/list" => new
// {
// tools = new object[]
// {
// new { name = "getTextBoxContent", description = "获取主窗口文本框中的文本内容", inputSchema = new { type = "object", properties = new {} } },
// new { name = "setTextBoxContent", description = "设置主窗口文本框的文本内容", inputSchema = new { type = "object", properties = new { text = new { type = "string", description = "要设置的文本" } }, required = new[] { "text" } } },
// new { name = "clickButton", description = "模拟点击主窗口上的按钮", inputSchema = new { type = "object", properties = new {} } }
// }
// },
// "tools/call" => HandleToolCall(json.RootElement.GetProperty("params")),
// _ => new { error = "Unknown method" }
// };
// return JsonSerializer.Serialize(new { jsonrpc = "2.0", id, result });
// }
// catch (Exception ex)
// {
// return JsonSerializer.Serialize(new { jsonrpc = "2.0", error = new { code = -1, message = ex.Message } });
// }
// }
// private object HandleToolCall(JsonElement paramsElement)
// {
// var toolName = paramsElement.GetProperty("name").GetString();
// var arguments = paramsElement.TryGetProperty("arguments", out var args) ? args : default;
// string result = toolName switch
// {
// "getTextBoxContent" => MyWinFormTools.GetTextBoxContent(),
// "setTextBoxContent" => MyWinFormTools.SetTextBoxContent(arguments.GetProperty("text").GetString()),
// "clickButton" => MyWinFormTools.ClickButton(),
// _ => throw new ArgumentException($"Unknown tool: {toolName}")
// };
// return new
// {
// content = new object[]
// {
// new { type = "text", text = result }
// }
// };
// }
//}
}
第五步:启动 MCP 服务器
修改 Program.cs,在启动 WinForms 的同时启动 HTTP MCP 服务器:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ModelContextProtocol.Protocol;
namespace McpTestDemo1
{
internal static class Program
{
private static WebApplication _webApp;
private static Form1 _mainForm;
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
// 启动 HTTP MCP 服务器(后台线程)
var thread = new Thread(() =>
{
_webApp = CreateWebApp();
_webApp.Run();
});
thread.IsBackground = true;
thread.Start();
Console.WriteLine("MCP Server started on http://localhost:5000/mcp");
// 启动 WinForms
_mainForm = new Form1();
MyWinFormTools.Initialize(_mainForm);
Application.Run(_mainForm);
}
//static WebApplication CreateWebApp()
//{
// var builder = WebApplication.CreateBuilder();
// builder.Services
// .AddMcpServer(o => o.ServerInfo = new Implementation
// {
// Name = "MyWinFormsApp",
// Version = "1.0.0"
// })
// .WithHttpTransport()
// .WithToolsFromAssembly();
// var app = builder.Build();
// app.MapMcp();
// return app;
//}
static WebApplication CreateWebApp()
{
var builder = WebApplication.CreateBuilder();
builder.Services
.AddMcpServer(o => o.ServerInfo = new Implementation
{
Name = "MyWinFormsApp",
Version = "1.0.0"
})
.WithHttpTransport()
.WithToolsFromAssembly();
var app = builder.Build();
app.MapMcp("/mcp"); // 指定路径为 /mcp
return app;
}
}
}```
## 第六步:运行和测试
### 启动应用
```bash
dotnet run
你应该会看到:
- 一个 WinForms 窗口弹出,中间有一个文本框
- 控制台输出
MCP Server started on http://localhost:5000/mcp
用 MCP Inspector 测试
MCP Inspector 是官方提供的调试工具:
npx @modelcontextprotocol/inspector
在 Inspector 界面中:
- 选择传输类型为 Streamable HTTP
- 输入 URL:
http://localhost:5000/mcp - 点击 Connect
连接成功后,你应该能看到我们定义的 3 个工具:
| 工具名 | 描述 |
|---|---|
GetTextBoxContent |
获取主窗口文本框中的文本内容 |
SetTextBoxContent |
设置主窗口文本框的文本内容 |
ClickButton |
模拟点击主窗口上的按钮 |
用 Claude Desktop 测试
如果你安装了 Claude Desktop,可以在配置文件中添加 MCP 服务器:
找到配置文件:
- Windows:
%APPDATA%\Claude\claude_desktop_config.json - macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
添加以下配置:
{
"mcpServers": {
"MyWinFormsApp": {
"url": "http://localhost:5000/mcp"
}
}
}
重启 Claude Desktop,你就可以在对话中让 AI 操控你的 WinForms 应用了。试试输入:
请帮我把文本框的内容改成"测试成功"
AI 会自动调用 SetTextBoxContent 工具完成操作。
核心 API 解释
工具发现(tools/list)
当 AI 客户端连接到 MCP 服务器时,首先会发送一个 tools/list 请求,获取所有可用工具的列表。服务器会返回每个工具的名称和描述,类似于:
{
"tools": [
{
"name": "GetTextBoxContent",
"description": "获取主窗口文本框中的文本内容",
"inputSchema": { "type": "object", "properties": {} }
},
{
"name": "SetTextBoxContent",
"description": "设置主窗口文本框的文本内容",
"inputSchema": {
"type": "object",
"properties": {
"newText": { "type": "string", "description": "要设置的文本内容" }
},
"required": ["newText"]
}
}
]
}
工具调用(tools/call)
当 AI 决定调用某个工具时,会发送 tools/call 请求:
{
"method": "tools/call",
"params": {
"name": "SetTextBoxContent",
"arguments": {
"newText": "Hello World"
}
}
}
服务器执行对应的 C# 方法,返回结果:
{
"content": [{
"type": "text",
"text": "文本已成功设置为: Hello World"
}]
}
传输方式
本教程使用 HTTP + SSE(Server-Sent Events) 传输,这是 MCP 推荐的方式。优点:
- 基于标准 HTTP,防火墙友好
- SSE 支持服务端主动推送
- 适合远程访问场景
另一种方式是 stdio,通过标准输入输出通信,适合本地进程间通信。
逐行解读关键点
1. 类标记 [McpServerToolType]
[McpServerToolType]
public class MyWinFormTools
这个特性告诉 MCP 框架:这个类里有工具方法,请扫描并注册它们。
2. 方法标记 [McpServerTool]
[McpServerTool]
[Description("desc")]
public static string SetTextBoxContent(
[Description("要设置的文本内容")] string newText)
每个工具方法都需要这个标记。方法名就是 AI 看到的工具名称。
3. 描述标记 [Description]
[Description("desc")]
这个描述会发送给 AI,让 AI 理解工具的用途。写好描述至关重要——AI 根据描述来决定何时调用哪个工具。
4. 线程安全:_mainForm.Invoke()
_mainForm.Invoke((MethodInvoker)(() => _mainForm.textBox1.Text = newText));
这是本教程最重要的技术细节。WinForms 的 UI 控件只能在创建它们的线程(主线程)上访问,而 MCP 工具是在 ASP.NET Core 的工作线程上被调用的。Invoke() 方法会将操作调度到主线程执行,确保线程安全。
如果直接写 _mainForm.textBox1.Text = newText,在非 UI 线程调用时会抛出 InvalidOperationException。
5. 双线程架构
// 后台线程:运行 HTTP 服务器
var thread = new Thread(() =>
{
_webApp = CreateWebApp();
_webApp.Run();
});
thread.IsBackground = true;
thread.Start();
// 主线程:运行 WinForms
Application.Run(_mainForm);
ASP.NET Core 需要一个线程来处理 HTTP 请求,而 WinForms 需要主线程来处理 UI 事件和消息循环。两者必须并行运行,所以在后台线程启动 HTTP 服务器。
扩展思路
添加更多 UI 控件支持
[McpServerTool]
[Description("获取指定按钮的文本")]
public static string GetButtonText([Description("按钮索引")] int index)
{
return (string)_mainForm.Invoke(
(Func<string>)(() => _mainForm.button1.Text));
}
安全性考虑
在生产环境中使用 MCP 时需要注意:
- 绑定地址:将
app.Run()改为app.Run("http://localhost:5000")确保只监听本地 - 认证授权:添加 API Key 或 OAuth 认证
- 输入校验:对 AI 传入的参数做严格校验,防止注入攻击
- 权限控制:限制 AI 可以调用的工具范围

浙公网安备 33010602011771号