1. 从零跑通 .NET MCP 服务端 Ollama 工具调用MCPModel Context Protocol是让大模型安全调用外部工具的开放协议而 .NET 凭借强类型和成熟的依赖注入体系非常适合用来写 MCP 服务端。这篇内容聚焦一个具体场景用 .NET 8 搭一个 MCP 服务端注册几个工具方法再让本地 Ollama 模型通过 MCP 客户端把工具调起来最终实现「用户问一句模型自动选工具、执行、返回结果」的完整链路。适合正在搭本地 AI 工具链、想让本地模型具备函数调用能力的开发者也适合已经写过 WebApi 但没接触过 MCP 的同学。整条链路我会拆成服务端骨架、工具注册、客户端对接、连通性验证和排障五块每一步都给可复制的代码和参数照着做基本能一次跑通。过程中如果本地模型或网络通道需要统一管理我会顺带给出用 TaoToken 统一 Key 与 API 通道的配置片段方便你把模型调用和工具调用放在同一套凭证体系里。2. 前置准备.NET 环境、Ollama 与 TaoToken 通道2.1 环境清单先把基础环境对齐版本不一致是后面报错的高发区。组件建议版本说明.NET SDK8.0 及以上MCP 的 AspNetCore 包依赖较新运行时Ollama最新稳定版本地跑模型默认端口 11434模型llama3.1:8b 或 qwen2.5:7b需要支持 tool calls 的模型IDEVS 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_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 生成 KeyAPI 基址用 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_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各语言 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 ListMessage { 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工具参数a3, b5执行后返回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/apiKey 是否带多余空格。模型名要和通道支持的名称一致不确定时在模型对话 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 里确认可用列表。如果本地 Ollama 和云端通道混用注意OllamaApiClient的 base url 要指向对应服务别把本地地址和云端 Key 拼在一起。6. 把工具调用接进长期编码工作流单次跑通只是起点。如果你打算把 MCP 服务端常驻让 IDE 或 Agent 长期调用建议把模型通道固定成 TaoToken 的 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 额度和 Key 统一管理本地调试和线上调用共用一套配置省得每次切环境改代码。API Key 在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 生成接入细节看文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。工具注册这块我的经验是每个工具只做一件事描述里把「什么时候用」写清楚比堆一堆参数更能提升模型选对工具的概率工具方法保持无副作用、可重复调用排障时直接单测工具本身比反复问模型快得多。