1. 从零跑通 .NET MCP 服务端 + Ollama 工具调用
MCP(Model Context Protocol)是让大模型安全调用外部工具的开放协议,而 .NET 凭借强类型和成熟的依赖注入体系,非常适合用来写 MCP 服务端。这篇内容聚焦一个具体场景:用 .NET 8 搭一个 MCP 服务端,注册几个工具方法,再让本地 Ollama 模型通过 MCP 客户端把工具调起来,最终实现「用户问一句,模型自动选工具、执行、返回结果」的完整链路。适合正在搭本地 AI 工具链、想让本地模型具备函数调用能力的开发者,也适合已经写过 WebApi 但没接触过 MCP 的同学。整条链路我会拆成服务端骨架、工具注册、客户端对接、连通性验证和排障五块,每一步都给可复制的代码和参数,照着做基本能一次跑通。过程中如果本地模型或网络通道需要统一管理,我会顺带给出用 TaoToken 统一 Key 与 API 通道的配置片段,方便你把模型调用和工具调用放在同一套凭证体系里。
2. 前置准备:.NET 环境、Ollama 与 TaoToken 通道
2.1 环境清单
先把基础环境对齐,版本不一致是后面报错的高发区。
| 组件 | 建议版本 | 说明 |
|---|---|---|
| .NET SDK | 8.0 及以上 | MCP 的 AspNetCore 包依赖较新运行时 |
| Ollama | 最新稳定版 | 本地跑模型,默认端口 11434 |
| 模型 | llama3.1:8b 或 qwen2.5:7b | 需要支持 tool calls 的模型 |
| IDE | VS 2022 / Rider / VS Code | 任意即可 |
Ollama 装好后先拉模型并确认服务在跑:
ollama pull llama3.1:8b ollama list curl http://localhost:11434/api/tagscurl能返回模型列表,说明本地推理服务正常。注意模型必须支持工具调用,纯对话模型即使接了 MCP 工具也不会触发ToolCalls,这是后面排查的重点之一。
2.2 用 TaoToken 统一模型通道
本地 Ollama 适合离线调试,但一旦你要把同一套工具调用逻辑接到云端模型,或者团队里多人共用额度,逐个管理 Key 会很乱。我的做法是用 TaoToken 做统一入口:官网 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= 注册后,在控制台 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite 生成 Key,API 基址用 https://taotoken.net/api(不加 UTM)。这样 MCP 客户端里模型地址和 Key 都走同一套配置,切换本地/云端只改一个 base url。
在项目根目录建一个settings.json,把模型通道和 MCP 服务端地址都放进去:
{ "ModelProvider": { "BaseUrl": "https://taotoken.net/api", "ApiKey": "sk-你的TaoToken密钥", "Model": "llama3.1:8b" }, "McpServer": { "Name": "csharp-mcp-sse-server", "Command": "http://localhost:5069/", "TransportType": "Sse" } }Key 建议放环境变量或用户机密,别硬编码进仓库。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,里面有各语言 SDK 的调用示例,配置字段对不上时对着查最快。
3. 可复制配置:.NET MCP 服务端骨架与工具注册
3.1 创建项目并引入包
用 WebApi 模板建项目,控制台项目也能跑,但 WebApi 方便用 SSE 传输和 Swagger 调试:
dotnet new webapi -n McpServerDemo cd McpServerDemo dotnet add package ModelContextProtocol.AspNetCore包版本以 NuGet 最新为准,装完dotnet restore确认无冲突。
3.2 编写工具类
MCP 的工具靠特性标注,方法签名里的参数会被自动解析成 JSON Schema 暴露给模型。下面两个工具一个做字符串拼接,一个做加法,覆盖「文本类」和「数值类」两种典型入参:
using System.ComponentModel; using ModelContextProtocol.Server; [McpServerToolType] public static class EchoTool { [McpServerTool, Description("拼接后返回给客户端")] public static string Echo(string message) => $"你好你好 {message}"; [McpServerTool, Description("用于计算两个数字的和,接收两个整数参数 a 和 b")] public static int Add( [Description("第一个加数")] int a, [Description("第二个加数")] int b) => a + b; }Description不是装饰,模型就是靠这段文字判断该不该调这个工具、参数怎么填。描述写得含糊,模型选错工具或参数缺失的概率会明显上升。
3.3 注册 MCP 并启动
在Program.cs里把 MCP 服务注册进依赖注入容器,并映射端点:
using ModelContextProtocol.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Logging.AddConsole(consoleLogOptions => { consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; }); builder.Services .AddMcpServer() .WithHttpTransport(options => { options.Stateless = false; // false 时保留 /sse 端点 }) .WithToolsFromAssembly(); // 自动扫描 [McpServerToolType] var app = builder.Build(); app.UseSwagger(); app.UseSwaggerUI(); app.UseAuthorization(); app.MapControllers(); app.MapMcp(); app.Run();WithToolsFromAssembly()会扫描当前程序集里所有带[McpServerToolType]的类,工具多了也不用逐个注册。Stateless = false表示走有状态的 SSE 会话,客户端需要保持连接;如果你的场景是短连接无状态调用,可以改成true,但那样/sse端点会被禁用,客户端传输方式也要跟着换。
启动服务:
dotnet run --urls "http://localhost:5069"浏览器打开http://localhost:5069/swagger能看到接口文档,说明服务端起来了。
4. 客户端对接 Ollama 与工具调用验证
4.1 引入客户端包并读取配置
客户端项目引入:
dotnet add package OllamaSharp.ModelContextProtocol把settings.json读进来,避免地址散落在代码里:
var config = new ConfigurationBuilder() .AddJsonFile("settings.json") .Build(); var modelBaseUrl = config["ModelProvider:BaseUrl"]; var apiKey = config["ModelProvider:ApiKey"]; var modelName = config["ModelProvider:Model"]; var mcpServerUrl = config["McpServer:Command"];4.2 从 MCP 服务端拉取工具
MCP 客户端先连服务端,把工具列表取回来,再塞给模型:
using Microsoft.Extensions.Logging; using OllamaSharp; using OllamaSharp.ModelContextProtocol; var loggerFactory = LoggerFactory.Create(b => b.AddConsole()); var serverConfigs = new[] { new McpServerConfiguration { Name = "csharp-mcp-sse-server", Command = mcpServerUrl, TransportType = McpServerTransportType.Sse } }; var tools = await Tools.GetFromMcpServers( mcpServers: serverConfigs, clientOptions: new McpClientOptions { LoggerFactory = loggerFactory, InitializationTimeout = TimeSpan.FromSeconds(30) }); foreach (var tool in tools) { Console.WriteLine($"- {tool.Function.Name}: {tool.Function.Description}"); }正常会打印出Echo和Add两个工具。如果列表为空,先确认服务端MapMcp()已调用、SSE 地址能访问。
4.3 发起对话并执行工具调用
把工具挂到ChatRequest.Tools上,模型返回ToolCalls时手动执行对应工具:
var ollama = new OllamaApiClient(new Uri(modelBaseUrl)); var chatRequest = new ChatRequest { Model = modelName, Stream = false, Think = false, Messages = new List<Message> { new Message { Role = ChatRole.User, Content = "请计算3加5的和是多少" } }, Tools = tools }; var resp = await ollama.ChatAsync(chatRequest, CancellationToken.None) .StreamToEndAsync(); if (resp.Message.ToolCalls.Any()) { var toolCall = resp.Message.ToolCalls.First(); Console.WriteLine($"调用的工具: {toolCall.Function.Name}"); var tool = tools.FirstOrDefault(t => t.Function.Name == toolCall.Function.Name); var toolResult = await tool.InvokeMethodAsync(toolCall.Function.Arguments); Console.WriteLine($"工具调用结果: {toolResult}"); return toolResult.ToString(); } return resp.Message.Content;入参「请计算3和5的和是多少」,模型会选中Add工具,参数a=3, b=5,执行后返回8。控制台能看到「调用的工具: Add」和「工具调用结果: 8」两行,链路就算通了。
5. 本篇常见报错排查
5.1 工具列表为空或连接超时
最常见的是 SSE 地址写错或服务端没起。先curl http://localhost:5069/sse看有没有事件流返回;如果服务端在另一台机器,把localhost换成实际 IP,并确认防火墙放行 5069。InitializationTimeout默认偏短,跨机调用可以调到 30 秒以上。
5.2 模型不触发 ToolCalls
先确认模型本身支持工具调用,llama3.1:8b、qwen2.5:7b这类可以,纯 chat 模型不行。其次检查Tools = tools是否真的传进去了,以及工具Description是否和用户问题语义匹配。如果模型返回的是自然语言答案而不是ToolCalls,多半是描述没写清楚,把「用于计算两个数字的和」这类意图写明确。
5.3 参数解析失败或类型不匹配
InvokeMethodAsync传的是 JSON 字符串,字段名要和 C# 参数名一致。比如Add的参数是a、b,模型生成的 arguments 也必须是这两个键。如果模型生成了num1、num2,就会解析失败。解决办法是在Description里把参数名和含义写死,减少模型自由发挥。
5.4 走 TaoToken 通道时 401 或模型不存在
检查settings.json里BaseUrl是否为https://taotoken.net/api,Key 是否带多余空格。模型名要和通道支持的名称一致,不确定时在模型对话 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 里确认可用列表。如果本地 Ollama 和云端通道混用,注意OllamaApiClient的 base url 要指向对应服务,别把本地地址和云端 Key 拼在一起。
6. 把工具调用接进长期编码工作流
单次跑通只是起点。如果你打算把 MCP 服务端常驻,让 IDE 或 Agent 长期调用,建议把模型通道固定成 TaoToken 的 Coding Plan https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite ,额度和 Key 统一管理,本地调试和线上调用共用一套配置,省得每次切环境改代码。API Key 在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite 生成,接入细节看文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。工具注册这块,我的经验是每个工具只做一件事,描述里把「什么时候用」写清楚,比堆一堆参数更能提升模型选对工具的概率;工具方法保持无副作用、可重复调用,排障时直接单测工具本身,比反复问模型快得多。