MCP + DeepSeek 对话框:让 AI 自动操控你的 WinForms 应用
MCP + DeepSeek 对话框:让 AI 自动操控你的 WinForms 应用
前言
前面我们实现了 MCP 客户端和服务端,但都是手动操作工具。如果能让 AI 自己决定何时调用什么工具,那才是真正的智能。
本文将 MCP 客户端 + DeepSeek 大模型 集成到一个对话框中,实现:
- 连接 MCP Server,自动获取工具列表
- 将 MCP 工具注册为 DeepSeek 的 function calling 工具
- 用户输入自然语言,AI 自动调用 MCP 工具完成操作
- 支持多轮对话和连续工具调用
架构设计
┌──────────────────────────────────────────────────────────────┐
│ WinForms 对话框 UI │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ [用户] 请帮我把文本框改成 Hello │ │
│ │ [AI] 好的,我来帮你设置。(调用 SetTextBoxContent) │ │
│ │ [AI] 已经设置好了。 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────┐ ┌──────────┐ │
│ │ 输入消息... │ │ 发送 │ │
│ └─────────────────────────────┘ └──────────┘ │
└──────────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ DeepSeek API │ │ MCP Server │
│ (OpenAI 兼容) │──tool_call──▶│ (tools/list, │
│ deepseek-v4 │◀─result─────│ tools/call) │
└─────────────────┘ └─────────────────────┘
核心流程
1. 连接 MCP Server → 获取工具列表
2. 将 MCP 工具转换为 OpenAI function calling 格式
3. 用户输入消息 → 发送给 DeepSeek(附带工具定义)
4. DeepSeek 返回 tool_calls → 执行 MCP 工具调用
5. 将工具结果反馈给 DeepSeek → 获取最终回复
6. 循环 3-5 直到 AI 不再调用工具
环境准备
| 工具 | 版本要求 | 用途 |
|---|---|---|
| .NET SDK | 8.0+ | 编译运行项目 |
| DeepSeek API Key | - | 从 platform.deepseek.com 获取 |
| MCP Server | - | 已有 MCP 服务器(如 McpTestDemo1) |
文件结构
Deepseek/
├── DeepSeekClient.cs # DeepSeek API 客户端
├── ToolConverter.cs # MCP 工具 → OpenAI 格式转换
├── ChatOrchestrator.cs # 对话编排器(LLM + MCP 工具循环)
├── ChatForm.cs # 对话框逻辑
└── ChatForm.Designer.cs # 对话框布局
第一步:DeepSeek API 客户端 DeepSeekClient.cs
DeepSeek 兼容 OpenAI API 格式,通过 HTTP 调用:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace McpClientDemo.Deepseek
{
public class DeepSeekClient
{
private readonly HttpClient _http;
private readonly string _model;
public DeepSeekClient(string apiKey, string baseUrl, string model)
{
_model = model;
_http = new HttpClient { BaseAddress = new Uri(baseUrl) };
_http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
}
public async Task<ChatResponse> SendAsync(
List<ChatMessage> messages,
List<ToolDefinition>? tools = null)
{
// 用 Dictionary 构建请求,精确控制序列化
var body = new Dictionary<string, object>
{
["model"] = _model,
["messages"] = messages.Select(m => SerializeMessage(m)).ToList(),
["stream"] = false
};
// tools 非空时才添加
if (tools != null && tools.Count > 0)
{
body["tools"] = tools.Select(t => new Dictionary<string, object>
{
["type"] = "function",
["function"] = new Dictionary<string, object>
{
["name"] = t.Function.Name,
["description"] = t.Function.Description ?? "",
["parameters"] = t.Function.Parameters != null
? JsonSerializer.Deserialize<Dictionary<string, object>>(
t.Function.Parameters.Value.GetRawText())
: new Dictionary<string, object>
{
["type"] = "object",
["properties"] = new Dictionary<string, object>()
}
}
}).ToList();
}
var options = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
var response = await _http.PostAsJsonAsync("/chat/completions", body, options);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync();
throw new Exception($"DeepSeek API 错误 ({(int)response.StatusCode}): {errorBody}");
}
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
return ParseResponse(json);
}
private Dictionary<string, object> SerializeMessage(ChatMessage m)
{
var msg = new Dictionary<string, object>
{
["role"] = m.Role
};
if (m.Content != null)
msg["content"] = m.Content;
if (m.ToolCalls != null && m.ToolCalls.Count > 0)
{
msg["tool_calls"] = m.ToolCalls.Select(tc => new Dictionary<string, object>
{
["id"] = tc.Id,
["type"] = "function",
["function"] = new Dictionary<string, string>
{
["name"] = tc.Name,
["arguments"] = tc.Arguments
}
}).ToList();
}
if (m.ToolCallId != null)
msg["tool_call_id"] = m.ToolCallId;
return msg;
}
private ChatResponse ParseResponse(JsonElement json)
{
var choice = json.GetProperty("choices")[0];
var message = choice.GetProperty("message");
var result = new ChatResponse
{
Role = message.GetProperty("role").GetString() ?? "",
Content = message.TryGetProperty("content", out var c) && c.ValueKind != JsonValueKind.Null
? c.GetString() : null
};
if (message.TryGetProperty("tool_calls", out var toolCalls) && toolCalls.ValueKind == JsonValueKind.Array)
{
result.ToolCalls = new List<ToolCall>();
foreach (var tc in toolCalls.EnumerateArray())
{
var func = tc.GetProperty("function");
result.ToolCalls.Add(new ToolCall
{
Id = tc.GetProperty("id").GetString() ?? "",
Name = func.GetProperty("name").GetString() ?? "",
Arguments = func.GetProperty("arguments").GetString() ?? "{}"
});
}
}
return result;
}
}
public class ChatMessage
{
[JsonPropertyName("role")]
public string Role { get; set; } = "";
[JsonPropertyName("content")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Content { get; set; }
[JsonPropertyName("tool_calls")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public List<ToolCall>? ToolCalls { get; set; }
[JsonPropertyName("tool_call_id")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? ToolCallId { get; set; }
}
public class ChatResponse
{
public string Role { get; set; } = "";
public string? Content { get; set; }
public List<ToolCall>? ToolCalls { get; set; }
}
public class ToolCall
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string Arguments { get; set; } = "{}";
}
public class ToolDefinition
{
[JsonPropertyName("type")]
public string Type { get; set; } = "function";
[JsonPropertyName("function")]
public FunctionDefinition Function { get; set; } = new();
}
public class FunctionDefinition
{
[JsonPropertyName("name")]
public string Name { get; set; } = "";
[JsonPropertyName("description")]
public string Description { get; set; } = "";
[JsonPropertyName("parameters")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonElement? Parameters { get; set; }
}
}```
## 第二步:工具格式转换 `ToolConverter.cs`
将 MCP 工具列表转换为 DeepSeek(OpenAI)的 function calling 格式:
```csharp
using ModelContextProtocol.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
namespace McpClientDemo.Deepseek
{
/// <summary>
/// 将 MCP 工具转换为 OpenAI function calling 格式
/// </summary>
public static class ToolConverter
{
/// <summary>
/// 将 MCP 工具列表转换为 OpenAI tools 格式
/// </summary>
public static List<ToolDefinition> ToOpenAITools(IList<McpClientTool> mcpTools)
{
return mcpTools.Select(tool => new ToolDefinition
{
Type = "function",
Function = new FunctionDefinition
{
Name = tool.Name,
Description = tool.Description ?? "",
Parameters = tool.JsonSchema.ValueKind == JsonValueKind.Object
? tool.JsonSchema
: null
}
}).ToList();
}
}
}
第三步:对话编排器 ChatOrchestrator.cs
核心组件——管理 DeepSeek 对话 + MCP 工具调用循环:
using ModelContextProtocol.Client;
using ModelContextProtocol.Protocol;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using McpClientSdk = ModelContextProtocol.Client.McpClient;
namespace McpClientDemo.Deepseek
{
/// <summary>
/// 对话编排器:管理 DeepSeek 对话 + MCP 工具调用循环
/// </summary>
public class ChatOrchestrator
{
private readonly DeepSeekClient _deepSeek;
private McpClientSdk? _mcpClient;
private List<ChatMessage> _messages = new();
private List<ToolDefinition> _tools = new();
/// <summary>日志事件</summary>
public event EventHandler<string>? LogMessage;
/// <summary>工具调用事件(用于 UI 显示)</summary>
public event EventHandler<string>? ToolCallStarted;
public ChatOrchestrator(DeepSeekClient deepSeek)
{
_deepSeek = deepSeek;
}
/// <summary>
/// 连接 MCP 服务器并加载工具
/// </summary>
public async Task ConnectMcpServerAsync(IClientTransport transport)
{
_mcpClient = await McpClientSdk.CreateAsync(transport);
// 获取 MCP 工具列表
var mcpTools = await _mcpClient.ListToolsAsync();
// 转换为 OpenAI function calling 格式
_tools = ToolConverter.ToOpenAITools(mcpTools);
OnLog($"已连接 MCP 服务器,加载了 {_tools.Count} 个工具");
foreach (var tool in _tools)
OnLog($" - {tool.Function.Name}: {tool.Function.Description}");
}
/// <summary>
/// 发送用户消息并获取 AI 回复(自动处理工具调用循环)
/// </summary>
public async Task<string> SendMessageAsync(string userMessage)
{
// 添加用户消息
_messages.Add(new ChatMessage { Role = "user", Content = userMessage });
// 工具调用循环(最多 10 次,防止死循环)
for (int i = 0; i < 10; i++)
{
// 调用 DeepSeek API
var response = await _deepSeek.SendAsync(
_messages, _tools.Count > 0 ? _tools : null);
// 如果没有工具调用,直接返回文本回复
if (response.ToolCalls == null || response.ToolCalls.Count == 0)
{
var reply = response.Content ?? "(无回复)";
_messages.Add(new ChatMessage { Role = "assistant", Content = reply });
return reply;
}
// 有工具调用:执行工具并继续循环
_messages.Add(new ChatMessage
{
Role = "assistant",
ToolCalls = response.ToolCalls
});
foreach (var call in response.ToolCalls)
{
OnToolCall($"调用工具: {call.Name}({call.Arguments})");
var result = await ExecuteToolCallAsync(call);
_messages.Add(new ChatMessage
{
Role = "tool",
ToolCallId = call.Id,
Content = result
});
OnLog($"工具结果: {result}");
}
}
return "(工具调用循环次数超限)";
}
/// <summary>
/// 执行单个工具调用
/// </summary>
private async Task<string> ExecuteToolCallAsync(ToolCall call)
{
if (_mcpClient == null)
return "错误:未连接 MCP 服务器";
try
{
// 解析参数
var argsDict = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
call.Arguments);
var args = argsDict?.ToDictionary(
kvp => kvp.Key,
kvp => (object?)kvp.Value);
// 调用 MCP 工具
var result = await _mcpClient.CallToolAsync(call.Name, args);
// 提取文本结果
var textParts = result.Content
.OfType<TextContentBlock>()
.Select(c => c.Text);
return string.Join("\n", textParts);
}
catch (Exception ex)
{
return $"工具调用失败: {ex.Message}";
}
}
/// <summary>清空对话历史</summary>
public void ClearHistory() => _messages.Clear();
/// <summary>设置系统提示词</summary>
public void SetSystemPrompt(string prompt)
{
_messages.RemoveAll(m => m.Role == "system");
_messages.Insert(0, new ChatMessage { Role = "system", Content = prompt });
}
private void OnLog(string msg) => LogMessage?.Invoke(this, msg);
private void OnToolCall(string msg) => ToolCallStarted?.Invoke(this, msg);
}
}
第四步:对话框 UI ChatForm.cs
using ModelContextProtocol.Client;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Xml.Linq;
using static System.Net.Mime.MediaTypeNames;
namespace McpClientDemo.Deepseek
{
public partial class ChatForm : Form
{
private readonly ChatOrchestrator _orchestrator;
private bool _isLoading = false;
string Key = "sk-4826f0";
public ChatForm()
{
InitializeComponent();
// 初始化 DeepSeek 客户端
var deepSeek = new DeepSeekClient(
Key,
"https://api.deepseek.com",
"deepseek-v4-flash");
_orchestrator = new ChatOrchestrator(deepSeek);
_orchestrator.LogMessage += (s, msg) => AppendLog(msg);
_orchestrator.ToolCallStarted += (s, msg) => AppendLog(msg);
// 设置系统提示词
_orchestrator.SetSystemPrompt(
"你是一个工控上位机助手,可以通过 MCP 工具操控 WinForms 应用程序。" +
"当用户要求操作界面时,请调用相应的工具完成。" +
"回复时使用中文,简洁明了。");
btnConnect.Click += async (s, e) => await ConnectMcpAsync();
btnSend.Click += async (s, e) => await SendMessageAsync();
btnClear.Click += (s, e) =>
{
_orchestrator.ClearHistory();
rtbMessages.Clear();
};
txtInput.KeyDown += async (s, e) =>
{
if (e.KeyCode == Keys.Enter && !e.Shift)
{
e.Handled = true;
await SendMessageAsync();
}
};
}
private async Task ConnectMcpAsync()
{
btnConnect.Enabled = false;
try
{
var url = txtMcpUrl.Text.Trim();
if (string.IsNullOrEmpty(url))
{
MessageBox.Show("请输入 MCP 服务器地址"); return;
}
var transport = new HttpClientTransport(new HttpClientTransportOptions
{
Endpoint = new Uri(url),
TransportMode = HttpTransportMode.AutoDetect,
});
await _orchestrator.ConnectMcpServerAsync(transport);
lblStatus.Text = "已连接";
lblStatus.ForeColor = Color.Green;
btnSend.Enabled = true;
}
catch (Exception ex)
{
MessageBox.Show($"连接失败: {ex.Message}");
lblStatus.Text = "连接失败";
lblStatus.ForeColor = Color.Red;
}
finally { btnConnect.Enabled = true; }
}
private async Task SendMessageAsync()
{
if (_isLoading) return;
var text = txtInput.Text.Trim();
if (string.IsNullOrEmpty(text)) return;
_isLoading = true;
btnSend.Enabled = false;
txtInput.Clear();
AppendMessage("用户", text);
try
{
var reply = await _orchestrator.SendMessageAsync(text);
AppendMessage("AI", reply);
}
catch (Exception ex)
{
AppendMessage("错误", ex.Message);
}
finally
{
_isLoading = false;
btnSend.Enabled = true;
}
}
private void AppendMessage(string role, string content)
{
var prefix = role == "用户" ? "👤 " : "🤖 ";
rtbMessages.AppendText($"{prefix}{role}: {content}\r\n\r\n");
rtbMessages.ScrollToCaret();
}
private void AppendLog(string msg)
{
var timestamp = DateTime.Now.ToString("HH:mm:ss");
rtbMessages.AppendText($" [{timestamp}] {msg}\r\n");
}
private void btnSend_Click(object sender, EventArgs e)
{
}
}
}
对话框布局 ChatForm.Designer.cs:
using System.Xml.Linq;
using static System.Net.Mime.MediaTypeNames;
namespace McpClientDemo.Deepseek
{
partial class ChatForm
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
private void InitializeComponent()
{
lblMcpUrl = new Label();
txtMcpUrl = new TextBox();
btnConnect = new Button();
lblStatus = new Label();
rtbMessages = new RichTextBox();
txtInput = new TextBox();
btnSend = new Button();
btnClear = new Button();
SuspendLayout();
//
// lblMcpUrl
//
lblMcpUrl.AutoSize = true;
lblMcpUrl.Location = new Point(12, 15);
lblMcpUrl.Name = "lblMcpUrl";
lblMcpUrl.Size = new Size(78, 17);
lblMcpUrl.TabIndex = 0;
lblMcpUrl.Text = "MCP 服务器:";
//
// txtMcpUrl
//
txtMcpUrl.Location = new Point(90, 12);
txtMcpUrl.Name = "txtMcpUrl";
txtMcpUrl.Size = new Size(350, 23);
txtMcpUrl.TabIndex = 1;
txtMcpUrl.Text = "http://localhost:5000/mcp";
//
// btnConnect
//
btnConnect.Location = new Point(450, 11);
btnConnect.Name = "btnConnect";
btnConnect.Size = new Size(70, 25);
btnConnect.TabIndex = 2;
btnConnect.Text = "连接";
//
// lblStatus
//
lblStatus.AutoSize = true;
lblStatus.ForeColor = Color.Red;
lblStatus.Location = new Point(530, 15);
lblStatus.Name = "lblStatus";
lblStatus.Size = new Size(44, 17);
lblStatus.TabIndex = 3;
lblStatus.Text = "未连接";
//
// rtbMessages
//
rtbMessages.BackColor = SystemColors.Window;
rtbMessages.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F);
rtbMessages.Location = new Point(12, 45);
rtbMessages.Name = "rtbMessages";
rtbMessages.ReadOnly = true;
rtbMessages.Size = new Size(588, 350);
rtbMessages.TabIndex = 4;
rtbMessages.Text = "";
//
// txtInput
//
txtInput.Font = new System.Drawing.Font("Microsoft YaHei UI", 10F);
txtInput.Location = new Point(12, 405);
txtInput.Multiline = true;
txtInput.Name = "txtInput";
txtInput.ScrollBars = ScrollBars.Vertical;
txtInput.Size = new Size(480, 50);
txtInput.TabIndex = 5;
//
// btnSend
//
btnSend.Enabled = false;
btnSend.Location = new Point(500, 405);
btnSend.Name = "btnSend";
btnSend.Size = new Size(100, 24);
btnSend.TabIndex = 6;
btnSend.Text = "发送";
btnSend.Click += btnSend_Click;
//
// btnClear
//
btnClear.Location = new Point(500, 431);
btnClear.Name = "btnClear";
btnClear.Size = new Size(100, 24);
btnClear.TabIndex = 7;
btnClear.Text = "清空";
//
// ChatForm
//
AutoScaleDimensions = new SizeF(7F, 17F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(614, 471);
Controls.Add(lblMcpUrl);
Controls.Add(txtMcpUrl);
Controls.Add(btnConnect);
Controls.Add(lblStatus);
Controls.Add(rtbMessages);
Controls.Add(txtInput);
Controls.Add(btnSend);
Controls.Add(btnClear);
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
Name = "ChatForm";
StartPosition = FormStartPosition.CenterParent;
Text = "MCP 对话助手";
ResumeLayout(false);
PerformLayout();
}
private Label lblMcpUrl;
private TextBox txtMcpUrl;
private Button btnConnect;
private Label lblStatus;
private RichTextBox rtbMessages;
private TextBox txtInput;
private Button btnSend;
private Button btnClear;
}
}```
## 核心 API 解释
### DeepSeek Tool Calls 流程
用户: 请帮我把文本框改成 Hello
DeepSeek 请求:
{
model: "deepseek-v4-flash",
messages: [...],
tools: [
{ type: "function", function: { name: "SetTextBoxContent", ... } }
]
}
DeepSeek 响应:
{
choices: [{
message: {
role: "assistant",
tool_calls: [{
id: "call_001",
function: { name: "SetTextBoxContent", arguments: "{\"newText\":\"Hello\"}" }
}]
}
}]
}
→ 执行 MCP 工具调用 → 返回结果
→ 将结果反馈给 DeepSeek → 获取最终回复
## 测试验证
### 1. 启动 MCP Server
```bash
cd McpTestDemo1
dotnet run
2. 启动 MCP Client(带对话框)
cd McpClient
dotnet run
3. 打开对话框,输入测试
- 输入 MCP 服务器地址
http://localhost:5000/mcp,点击"连接" - 输入"请帮我把文本框改成 Hello"
- 观察:AI 自动调用 SetTextBoxContent 工具,McpTestDemo1 的文本框内容变化
踩坑总结
1. DeepSeek API 400 Bad Request
// tools 为 null 时不能发送,否则报 400
// 错误: body["tools"] = tools; // tools 可能是 null
// 正确: 只在 tools 非空时才添加
if (tools != null && tools.Count > 0)
body["tools"] = ...;
2. messages 中的 null 字段
// OpenAI 格式不允许发送 null 字段
// 用 JsonIgnore(WhenWritingNull) 过滤
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Content { get; set; }
3. MCP 工具 parameters 格式
// MCP 工具的 JsonSchema 可能不是标准 OpenAI 格式
// 需要手动转换为 Dictionary 确保格式正确
[JsonPropertyName("parameters")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonElement? Parameters { get; set; }
4. 工具调用循环
// AI 可能连续调用多个工具,需要循环处理
// 最多循环 10 次防止死循环
for (int i = 0; i < 10; i++) {
var response = await deepSeek.SendAsync(messages, tools);
if (response.ToolCalls == null) break; // 没有工具调用,返回
// 执行工具调用...
}
扩展方向
- 流式输出 — 使用 SSE 实现打字机效果
- 多轮记忆 — 支持上下文对话
- 思考模式 — 启用 DeepSeek 的 thinking 功能
- 工具调用可视化 — 在对话框中显示工具调用过程
- 配置持久化 — 将 API Key 和服务器地址保存到文件

浙公网安备 33010602011771号