基于.NET与MCP协议构建AI编程助手专属工具链实战指南
1. 项目概述与MCP核心概念解析
最近在折腾AI编程助手,发现Cursor、Windsurf这些新工具确实能极大提升开发效率。但用久了总感觉它们对项目上下文的理解还不够深,尤其是涉及到一些内部工具、私有API或者特定业务逻辑时,AI助手往往显得“力不从心”。直到我接触到了OpenAI推出的 模型上下文协议 ,也就是 MCP ,才找到了一个优雅的解决方案。简单来说,MCP允许你为自己的AI助手“定制化”能力,让它不仅能写通用代码,还能直接调用你为它“教会”的专属工具。这就像给你的编程伙伴装上了一套专属的“瑞士军刀”。
这个项目, mehrandvd/tutorial-mcp-server-dotnet ,就是一个绝佳的入门指南,手把手教你如何用我们熟悉的.NET和C#来构建一个MCP服务器。为什么选择.NET?对于已经深耕微软技术栈的团队来说,这意味着无需切换语言和环境,就能快速将现有的业务能力(比如调用内部用户系统API、查询特定数据库schema、生成公司标准的代码模板)封装成AI助手可以理解和使用“工具”。本教程将带你从零开始,深入MCP协议的核心,并用C#实现一个功能完整的服务器,最终让你的Cursor或Claude Desktop等客户端能够直接调用你定义的功能。
2. MCP协议深度剖析与.NET实现选型
在动手写代码之前,我们必须先吃透MCP到底是什么,以及它为何能成为连接AI模型与外部世界的桥梁。MCP本质上是一个基于JSON-RPC 2.0的通信协议。你可以把它想象成AI模型(客户端)和你编写的服务(服务器)之间约定好的一套“暗号”。客户端(比如Cursor)通过这套“暗号”向服务器询问:“你有什么本事?”(即列出可用工具),或者下达指令:“请使用那个叫‘查询订单’的工具,参数是订单号123”(即调用工具)。
2.1 协议核心:工具、资源与提示词
MCP协议主要围绕三大核心概念展开,理解它们对设计服务器至关重要:
-
工具 :这是最核心的功能单元。一个工具就是一个可以被AI模型调用的函数。它必须有明确的名称、描述和参数定义。例如,你可以定义一个名为
get_weather的工具,描述为“获取指定城市的当前天气”,参数是city_name(字符串类型)。当AI模型认为需要查询天气来完成用户请求时,它就会通过MCP协议调用这个工具。 -
资源 :代表AI模型可以读取的静态或动态内容。比如,你可以将一个项目的架构图文档、API文档网址或者数据库ER图作为“资源”提供给AI。这样,AI在分析问题时,就能主动去“阅读”这些资源,获得更丰富的上下文信息,而不仅仅是依赖有限的对话历史。
-
提示词 :你可以预定义一些高质量的提示词模板,作为“资源”提供给AI。当用户触发特定场景时,AI可以直接使用这些优化过的提示词,从而生成更准确、更符合要求的响应。
对于.NET开发者而言,实现一个MCP服务器,就是创建一个能够处理特定JSON-RPC消息的应用程序。我们需要处理诸如 initialize , tools/list , tools/call , resources/list 等标准请求。
2.2 .NET实现技术栈选型
原教程项目给出了清晰的实现路径。这里我结合自己的实践,对选型做进一步解读:
- 通信层 :MCP协议可以通过 标准输入输出 或 SSE 进行通信。对于本地集成的场景(如与Cursor、Claude Desktop配合), Stdio 是最简单、最可靠的方式。我们的.NET控制台应用将从标准输入读取JSON-RPC请求,处理后再将响应写入标准输出。这种方式无需处理网络端口,依赖最少。
- 序列化 :毫无疑问,使用 System.Text.Json 。它是.NET Core以来高性能、低分配的默认选择。我们需要定义与MCP协议对应的请求、响应、工具描述等数据模型,并用
JsonSerializer进行序列化和反序列化。 - 依赖注入与架构 :即使是一个简单的教程项目,我也强烈建议从一开始就采用清晰的架构。使用内置的
IServiceCollection进行依赖注入,将工具的定义、注册与执行逻辑分离。这样便于后续扩展,添加新的工具就像实现一个接口并注册服务那么简单。 - 日志与调试 :由于服务器通过Stdio运行,调试输出不能简单用
Console.WriteLine,否则会污染JSON-RPC通信通道。正确的做法是配置一个日志器,将日志写入标准错误。在开发时,我们可以同时运行服务器和模拟客户端,或者通过日志文件来观察内部执行过程。
注意 :在实现JSON-RPC消息处理时,务必注意协议的“id”字段。每个请求都有一个唯一的id,你的响应必须携带相同的id,这是JSON-RPC 2.0规范的要求,客户端靠它来匹配请求与响应。
3. 逐步构建一个功能完整的MCP服务器
接下来,我们抛开理论,直接进入实战环节。我会以一个具体的例子——构建一个“项目信息查询”MCP服务器——来演示全过程。这个服务器将提供两个工具:一个是获取当前Git仓库信息,另一个是查询指定NuGet包的详细信息。
3.1 项目初始化与基础框架搭建
首先,创建一个新的.NET控制台应用:
dotnet new console -n MyCompany.McpServer
cd MyCompany.McpServer
然后,添加必要的NuGet包。我们主要需要 System.Text.Json 来处理JSON,但为了更好的开发体验,可以引入 Microsoft.Extensions.DependencyInjection 和 Microsoft.Extensions.Logging.Console 。
dotnet add package Microsoft.Extensions.DependencyInjection
dotnet add package Microsoft.Extensions.Logging.Console
现在,创建核心的数据模型。这些类直接对应MCP协议中的数据结构。我们先创建几个关键的:
McpRequest.cs/McpResponse.cs:表示通用的JSON-RPC请求和响应。Tool.cs:表示一个工具的定义,包括名称、描述、输入参数模式。ToolCallRequest.cs/ToolCallResponse.cs:表示工具调用的具体请求和结果。
这里以 Tool 类为例,展示其结构。参数模式通常是一个JSON Schema对象,用于描述输入参数的格式。
// Tool.cs
public class Tool
{
[JsonPropertyName("name")]
public string Name { get; set; } = string.Empty;
[JsonPropertyName("description")]
public string Description { get; set; } = string.Empty;
[JsonPropertyName("inputSchema")]
public JsonElement InputSchema { get; set; } // 使用JsonElement灵活处理JSON Schema
}
3.2 实现核心消息处理循环
服务器的核心是一个从标准输入持续读取、解析、处理并写回标准输出的循环。我们在 Program.cs 的 Main 方法中构建这个骨架。
// Program.cs - 简化版主循环
using System.Text.Json;
static async Task Main(string[] args)
{
// 1. 配置依赖注入容器
var services = new ServiceCollection();
ConfigureServices(services);
var serviceProvider = services.BuildServiceProvider();
// 2. 获取日志器和工具执行器
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
var toolExecutor = serviceProvider.GetRequiredService<IToolExecutor>();
// 3. 主循环:从标准输入读取
using var stdin = Console.OpenStandardInput();
using var stdout = Console.OpenStandardOutput();
var reader = new StreamReader(stdin);
var writer = new StreamWriter(stdout) { AutoFlush = true };
while (!reader.EndOfStream)
{
try
{
var line = await reader.ReadLineAsync();
if (string.IsNullOrWhiteSpace(line)) continue;
var request = JsonSerializer.Deserialize<McpRequest>(line);
if (request == null) continue;
McpResponse response;
switch (request.Method)
{
case "initialize":
response = HandleInitialize(request);
break;
case "tools/list":
response = await HandleListTools(request, toolExecutor);
break;
case "tools/call":
response = await HandleCallTool(request, toolExecutor, logger);
break;
// ... 处理其他方法如 resources/list
default:
response = CreateErrorResponse(request.Id, -32601, "Method not found");
break;
}
var responseJson = JsonSerializer.Serialize(response);
await writer.WriteLineAsync(responseJson);
}
catch (Exception ex)
{
logger.LogError(ex, "Error processing request");
// 向标准错误输出日志,避免干扰标准输出的JSON-RPC通信
Console.Error.WriteLine($"[Error] {ex.Message}");
}
}
}
在上面的代码中, HandleCallTool 方法是关键。它需要解析请求中的工具名和参数,找到对应的工具实现并执行,最后将结果封装成MCP响应。
3.3 定义并实现具体的工具
让我们实现之前提到的两个工具。首先,定义一个工具接口 ITool :
public interface ITool
{
string Name { get; }
string Description { get; }
Task<ToolResult> ExecuteAsync(JsonElement arguments, CancellationToken cancellationToken);
}
public record ToolResult(bool IsSuccess, string? Content, string? Error = null);
然后,实现 GetGitInfoTool :
public class GetGitInfoTool : ITool
{
public string Name => "get_git_info";
public string Description => "获取当前项目Git仓库的详细信息,包括分支、最新提交等。";
public async Task<ToolResult> ExecuteAsync(JsonElement arguments, CancellationToken ct)
{
try
{
// 使用 libgit2sharp 或执行 git 命令
// 这里为简化,使用 Process 执行 git 命令
var processInfo = new ProcessStartInfo("git")
{
Arguments = "log --oneline -1",
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
WorkingDirectory = Directory.GetCurrentDirectory()
};
using var process = Process.Start(processInfo);
if (process == null)
return new ToolResult(false, null, "Failed to start git process.");
var output = await process.StandardOutput.ReadToEndAsync();
await process.WaitForExitAsync(ct);
return new ToolResult(true, $"最新提交信息:\n{output}");
}
catch (Exception ex)
{
return new ToolResult(false, null, $"执行git命令时出错:{ex.Message}");
}
}
}
接着,实现 SearchNuGetPackageTool 。这个工具需要接收一个参数 packageName :
public class SearchNuGetPackageTool : ITool
{
public string Name => "search_nuget_package";
public string Description => "查询NuGet官方仓库中指定包的详细信息。";
public async Task<ToolResult> ExecuteAsync(JsonElement arguments, CancellationToken ct)
{
// 1. 解析参数
if (!arguments.TryGetProperty("packageName", out var nameProp) || nameProp.ValueKind != JsonValueKind.String)
{
return new ToolResult(false, null, "缺少或无效的参数 'packageName'。");
}
var packageName = nameProp.GetString();
// 2. 调用NuGet API (简化示例,使用 HttpClient)
try
{
using var httpClient = new HttpClient();
// 注意:使用NuGet V3 API的查询端点
var queryUrl = $"https://api.nuget.org/v3-flatcontainer/{packageName.ToLowerInvariant()}/index.json";
var response = await httpClient.GetAsync(queryUrl, ct);
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync(ct);
// 解析JSON,提取版本等信息
var versionsDoc = JsonDocument.Parse(content);
var versions = versionsDoc.RootElement.GetProperty("versions").EnumerateArray().Select(v => v.GetString()).ToArray();
return new ToolResult(true, $"包 '{packageName}' 找到 {versions.Length} 个版本。最新版本: {versions.LastOrDefault()}");
}
else if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
{
return new ToolResult(true, $"未在NuGet官方仓库中找到包 '{packageName}'。");
}
else
{
return new ToolResult(false, null, $"查询NuGet API失败,状态码:{response.StatusCode}");
}
}
catch (Exception ex)
{
return new ToolResult(false, null, $"查询过程中发生错误:{ex.Message}");
}
}
}
最后,在 ConfigureServices 方法中注册这些工具:
private static void ConfigureServices(IServiceCollection services)
{
services.AddLogging(configure => configure.AddConsole());
services.AddSingleton<IToolExecutor, ToolExecutor>(); // 工具执行器,负责查找和调用ITool
services.AddSingleton<ITool, GetGitInfoTool>();
services.AddSingleton<ITool, SearchNuGetPackageTool>();
// 未来可以轻松地在这里 AddSingleton<ITool, YourNewTool>();
}
3.4 配置客户端以使用我们的MCP服务器
服务器构建完成后,需要让AI客户端知道它的存在。以Cursor为例,我们需要编辑Cursor的MCP配置文件。
对于Cursor :配置文件通常位于 ~/.cursor/mcp.json (macOS/Linux)或 %APPDATA%\Cursor\mcp.json (Windows)。
在该JSON文件中添加我们的服务器配置:
{
"mcpServers": {
"my-dotnet-mcp-server": {
"command": "dotnet",
"args": ["/path/to/your/MyCompany.McpServer.dll"],
"env": {
"DOTNET_ENVIRONMENT": "Development"
}
}
}
}
配置完成后,重启Cursor。当你在Cursor中询问:“我这个项目现在在哪个Git分支?”或者“帮我看看Newtonsoft.Json这个包在NuGet上最新版本是什么?”时,Cursor的AI模型就会识别出它可以通过你定义的MCP工具来获取这些信息,并自动调用它们,将结果融入它的回答中。
实操心得 :在配置
args路径时,建议使用dotnet run项目的绝对路径,或者直接指向发布后的独立可执行文件。确保Cursor有权限执行该命令。第一次配置后,如果工具没有出现,可以尝试在Cursor中手动触发重新加载MCP配置(通常有相关命令),或者检查标准错误输出中是否有服务器启动失败的日志。
4. 高级主题、调试与性能优化
一个能跑通的服务器只是第一步。要将其用于生产环境或团队共享,还需要考虑更多。
4.1 实现资源与提示词支持
除了工具,让AI能读取你提供的资源文档,能极大提升其回答的准确性。实现 resources/list 和 resources/read 方法与工具类似。
- 定义资源 :创建一个
IResource接口,包含Uri(资源标识符,如"file:///docs/architecture.md")、Name、Description和MimeType属性,以及一个GetContentAsync方法。 - 注册资源 :在服务容器中注册你的资源提供者,例如一个能读取项目目录下
docs/文件夹内所有Markdown文件的资源提供者。 - 处理请求 :在消息循环中,当收到
resources/list请求时,返回所有已注册资源的元数据列表。当收到resources/read请求时,根据请求中的uri找到对应的资源提供者,读取内容并返回。
这样,你就可以在配置中告诉AI:“这是我们的系统架构图资源”,AI在回答相关问题时,会主动去读取这个资源来获取信息。
4.2 服务器调试技巧
调试一个Stdio模式的服务器有其特殊性。你不能直接按F5启动,因为需要有一个客户端(如Cursor)来驱动它。以下是几种有效的调试方法:
- 模拟客户端 :编写一个简单的控制台程序作为模拟客户端,它启动你的MCP服务器进程,然后通过标准输入发送预设的JSON-RPC请求(如
tools/list),并打印服务器的标准输出。这是最直接的单元测试方式。 - 日志输出到文件 :在服务器中,将
ILogger的输出同时配置到文件和控制台错误流。这样,即使服务器在Cursor中运行,你也能在日志文件中看到详细的执行轨迹和异常信息。
services.AddLogging(configure =>
{
configure.AddConsole(); // 输出到 stderr,可在终端查看
configure.AddFile("logs/mcp-server-{Date}.txt"); // 需要添加 Serilog.Extensions.Logging.File 包
});
- 使用调试器附加 :首先以独立方式启动你的MCP服务器程序(例如,在一个终端里运行
dotnet run),它会等待输入。然后,从你的IDE(如VS Code、Rider)附加调试器到这个运行的进程上。最后,再用你的模拟客户端或配置好的Cursor发送请求,此时就可以命中断点进行调试了。
4.3 性能、安全与扩展性考量
- 性能 :MCP调用通常是同步的(AI等待工具结果)。因此,工具的实现必须高效,避免长时间阻塞。对于涉及网络IO(如调用外部API)或复杂计算的操作,务必使用异步编程,并考虑设置合理的超时(CancellationToken)。
- 安全 :这是重中之重。你的工具将直接暴露给AI模型调用。
- 输入验证 :严格校验所有输入参数,防止注入攻击。即使协议层有JSON Schema描述,服务器端也必须再次验证。
- 权限控制 :思考每个工具需要的权限级别。查询天气的工具可能无害,但一个能执行任意Shell命令或访问生产数据库的工具是极度危险的。可以考虑在工具执行层加入基于上下文(如项目路径、用户身份)的权限检查。
- 沙箱化 :对于执行代码或命令的工具,尽可能在沙箱环境中运行。
- 扩展性 :采用依赖注入和接口设计,使得添加新工具就像实现一个
ITool接口并注册一样简单。你可以考虑将工具定义放在配置文件中,实现动态加载。
5. 实战案例:打造团队专属的智能开发助手
理论说再多,不如一个真实案例。假设我们团队内部有一个管理用户信息的服务,我们想让它能被AI助手查询。
- 定义工具 :创建一个
GetUserInfoTool,接收userId参数,通过调用内部HTTP API获取用户信息。 - 安全处理 :该工具不能无条件暴露。我们在工具执行前,检查当前工作目录是否在我们公司的特定项目路径下,并且可以读取一个本地配置文件中的访问令牌(该令牌权限被严格限制为只读)。
- 提供资源 :将内部用户服务的Swagger/OpenAPI文档URL作为一个“资源”提供。AI在思考如何回答用户管理相关问题时,会先去阅读这个API文档,从而更准确地理解如何调用工具或直接给出建议。
- 团队分发 :将编译好的MCP服务器和配置文件打包,通过内部NuGet源或共享目录分发给团队成员。大家只需修改本地Cursor配置中的服务器路径,即可立即获得这套增强能力。
经过这样的改造,当团队成员在Cursor中提问:“用户张三的邮箱是什么?”或者“给新员工李四开通系统权限的流程是怎样的?”时,AI不仅能基于通用知识回答,更能直接调用内部工具或参考内部文档,给出精准、可操作的答案。
构建MCP服务器的过程,实质上是在为你的AI助手构建一个专属的“技能库”。它打破了AI通用知识与具体业务上下文之间的壁垒。用.NET来实现,让这一切在微软技术生态内变得异常顺畅。从今天开始,尝试为你和你的团队定制第一个MCP工具吧,你会发现人机协作的效率提升到一个新的层次。
更多推荐



所有评论(0)