MCP服务器 用 MCP 让 AI 操控你的 WinForms 桌面应用

用 MCP 让 AI 操控你的 WinForms 桌面应用

为什么要做这件事?

大语言模型(LLM)很擅长处理文本,但它们无法直接操作你的桌面程序。想象一下这样的场景:

  • 你正在写一个工控上位机软件,想让 AI 帮你自动填写参数、读取界面数据
  • 你开发了一个内部管理工具,想让 AI 助手帮你批量操作界面上的表单
  • 你做了一个数据可视化应用,想让 AI 根据用户对话内容动态更新界面

MCP(Model Context Protocol) 正是解决这个问题的协议。它定义了 AI 模型与外部工具之间的通信标准,让你可以把任何功能"暴露"给 AI 调用。

本教程演示如何搭建一个 WinForms 应用,通过 MCP 协议让 AI 可以:

  1. 读取文本框中的内容
  2. 设置文本框的内容
  3. 点击界面上的按钮

环境准备

工具 版本要求 用途
.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

你应该会看到:

  1. 一个 WinForms 窗口弹出,中间有一个文本框
  2. 控制台输出 MCP Server started on http://localhost:5000/mcp

用 MCP Inspector 测试

MCP Inspector 是官方提供的调试工具:

npx @modelcontextprotocol/inspector

在 Inspector 界面中:

  1. 选择传输类型为 Streamable HTTP
  2. 输入 URL:http://localhost:5000/mcp
  3. 点击 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 时需要注意:

  1. 绑定地址:将 app.Run() 改为 app.Run("http://localhost:5000") 确保只监听本地
  2. 认证授权:添加 API Key 或 OAuth 认证
  3. 输入校验:对 AI 传入的参数做严格校验,防止注入攻击
  4. 权限控制:限制 AI 可以调用的工具范围

参考资料

posted @ 2026-07-11 10:41  家煜宝宝  阅读(24)  评论(0)    收藏  举报