MCP 协议实战:用 50 行代码给本地大模型接上“工具手“,让 Ollama 也能干 Agent 的活
本文面向:对 MCP 协议有基本了解、希望把它真正接到自己本地模型上的开发者。
全文示例已在 Windows 11 + Python 3.11 + Ollama 0.5+ 环境下完整跑通。
0. 写在前面:为什么写这篇
2026 年了,MCP(Model Context Protocol)已经从 Anthropic 自家的协议,变成了几乎所有主流 AI 工具的"事实标准"——Claude Desktop、Cursor、Windsurf、Cline,乃至大部分国产 IDE 插件,都在原生支持。
但当我在 CSDN、掘金、知乎搜了一圈"MCP 教程"以后,发现一个有意思的现象:
90% 的内容停在"MCP 是什么"和"怎么把官方 demo 跑起来";
真正回答"怎么把 MCP 接到本地模型(Ollama / vLLM)上"的资料几乎没有;
而这恰好是企业内网部署、私有化场景下,最刚需的一件事。
所以这篇文章不再重复 MCP 的概念铺垫,而是直接动手——用一个最简单的例子,把"自己写的工具 → 通过 MCP 协议 → 喂给本地 Ollama 模型"这整条链路打通,全部代码不超过 200 行。
1. 三分钟看懂 MCP 在干什么
如果你已经熟悉 MCP,可以跳过本节。
简单来说,MCP 把"模型 ↔ 工具"之间的接线,标准化成了三个角色:
Host(宿主):跑模型的那个程序,比如 Claude Desktop、Cursor、或者下面我们自己写的 Python 脚本。
Client(客户端):Host 内部的一个连接器,负责跟下面的 Server 握手、收发消息。
Server(服务端):暴露工具/资源/Prompt 的进程,比如"查数据库""读文件""调内网接口"。
三者之间走的是 JSON-RPC,传输层既可以是stdio(最常用,本地拉起子进程),也可以是SSE / Streamable HTTP(远程部署)。
MCP 架构示意
理解到这里就够了。MCP 的精髓不是"协议有多复杂",而是它把"插一个工具进来"这件事变成了即插即用——你写一个 MCP Server,全世界支持 MCP 的客户端都能直接用它。
但反过来想——既然 MCP Server 是标准的,那么只要我自己写一个最小的 Host,让它能跟 MCP Server 通信、再把工具转给 Ollama 调用,本地模型就同样能享受这套生态。这就是本文的主线。
2. 环境准备
# Python 端
pip install "mcp[cli]" ollama
# Ollama 端(如果还没装)
# Windows: https://ollama.com/download
# 拉一个支持 tool calling 的模型,体积友好的可以选 qwen3:8b
ollama pull qwen3:8b
ollama serve
需要注意两点:
模型必须支持 tool calling。Qwen3、Llama 3.1+、Mistral、DeepSeek-V3 等都已经支持。早期模型(如 Llama 2、Qwen1.5)不支持,硬接会得到一堆乱七八糟的输出。
Ollama 版本 ≥ 0.4.x才有完善的 tool 字段,建议升到最新版。
3. 第一步:写一个最小的 MCP Server(≈ 50 行)
为了让 demo 有"业务感",我们假装自己是某个企业内部场景,需要两个工具:
1.query_orders— 按客户 ID 查最近订单
2.search_docs— 在本地知识库里做一个简单的关键词检索
新建my_mcp_server.py:
import asyncio
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
# —— 假数据:真实场景这里就是连数据库、调内部 API ——
ORDERS = {
"C001": ["2026-05-12 ¥899 蓝牙耳机", "2026-05-18 ¥2399 显示器"],
"C002": ["2026-05-09 ¥159 USB-C 扩展坞"],
}
DOCS = {
"退货政策": "签收 7 天内可无理由退货,需保持原包装完整。",
"发票申请": "登录会员中心 > 我的订单 > 申请发票,3 个工作日内开具。",
"保修说明": "标配 1 年保修,主板/电池半年保修,人为损坏不在保修范围。",
}
server = Server("demo-tools")
@server.list_tools()
async def list_tools():
return [
Tool(
name="query_orders",
description="按客户 ID 查询最近订单。输入参数:customer_id(字符串)",
inputSchema={
"type": "object",
"properties": {"customer_id": {"type": "string"}},
"required": ["customer_id"],
},
),
Tool(
name="search_docs",
description="在企业知识库中按关键词检索条目。输入参数:keyword(字符串)",
inputSchema={
"type": "object",
"properties": {"keyword": {"type": "string"}},
"required": ["keyword"],
},
),
]
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "query_orders":
cid = arguments.get("customer_id", "")
rows = ORDERS.get(cid)
text = "未查到该客户订单" if not rows else ";".join(rows)
return [TextContent(type="text", text=text)]
if name == "search_docs":
kw = arguments.get("keyword", "")
hits = [f"【{k}】{v}" for k, v in DOCS.items() if kw in k or kw in v]
text = "未命中条目" if not hits else "\n".join(hits)
return [TextContent(type="text", text=text)]
return [TextContent(type="text", text=f"未知工具: {name}")]
async def main():
async with stdio_server() as (read, write):
await server.run(read, write, server.create_initialization_options())
if __name__ == "__main__":
asyncio.run(main())
这个 Server 现在已经是一个"标准 MCP Server",所有支持 MCP 的客户端都能拿来即用。
4. 第二步:先用 Claude Desktop / Cursor 验证一下
在写自己的 Host 之前,强烈建议先用现成客户端测一下,确保 Server 本身没问题。
以 Claude Desktop 为例,编辑配置文件:
Windows:%APPDATA%\Claude\claude_desktop_config.json
macOS:~/Library/Application Support/Claude/claude_desktop_config.json
加入:
{"mcpServers": {
"demo-tools": {
"command": "python",
"args": ["D:/your/path/my_mcp_server.py"]
}
}
}
重启 Claude Desktop,在对话框里你应该能看到demo-tools已经被加载,工具图标里多了query_orders和search_docs。
试着问:"客户 C001 最近买了什么?"
如果你能看到 Claude 主动调用query_orders,并把订单内容回出来——恭喜,Server 已经验证通过。
5. 关键章节:让 Ollama 也能用这套 MCP Server
到这里为止,所有教程都讲过。真正的难点是:怎么让本地跑的 Ollama 模型用上这个 Server?
思路其实只有一句话——
我们自己写一个最小的"Host",它一手连 MCP Server(说协议方言),一手连 Ollama(说 OpenAI 兼容的 tool calling 方言),中间做翻译。
新建local_host.py:
import asyncio, json
import ollama
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
MODEL = "qwen3:8b" # 任何支持 tool calling 的 Ollama 模型
SERVER_CMD = "python"
SERVER_ARGS = ["my_mcp_server.py"]
def mcp_tool_to_ollama(t):
"""把 MCP 协议里的 Tool 对象,翻成 Ollama / OpenAI 兼容的 tool schema。"""
return {
"type": "function",
"function": {
"name": t.name,
"description": t.description,
"parameters": t.inputSchema,
},
}
async def chat_loop(user_query: str):
params = StdioServerParameters(command=SERVER_CMD, args=SERVER_ARGS)
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# 1) 拿到 MCP Server 暴露的所有工具
tools_resp = await session.list_tools()
ollama_tools = [mcp_tool_to_ollama(t) for t in tools_resp.tools]
messages = [
{"role": "system",
"content": "你是企业客服助手,需要时调用工具查询订单和知识库。"},
{"role": "user", "content": user_query},
]
# 2) 让 Ollama 决定要不要调用工具(最多 5 轮,防止死循环)
for _ in range(5):
resp = ollama.chat(model=MODEL, messages=messages, tools=ollama_tools)
msg = resp["message"]
messages.append(msg)
tool_calls = msg.get("tool_calls") or []
if not tool_calls:
print("\n[最终回答]\n", msg["content"])
return
# 3) 模型决定调用工具 → 走 MCP 真实执行
for call in tool_calls:
fname = call["function"]["name"]
fargs = call["function"]["arguments"]
if isinstance(fargs, str):
fargs = json.loads(fargs)
print(f"[模型调用工具] {fname}({fargs})")
result = await session.call_tool(fname, fargs)
text = result.content[0].text if result.content else ""
print(f"[工具返回] {text}\n")
messages.append({
"role": "tool",
"content": text,
})
if __name__ == "__main__":
asyncio.run(chat_loop("客户 C001 最近买了什么?另外帮我查下退货政策。"))
这就是全部"翻译层"代码。不到 50 行,就把一个标准 MCP Server 接到了本地 Ollama 模型上。
6. 跑通效果
# 终端 1:确保 Ollama 在运行
ollama serve
# 终端 2
python local_host.py
预期输出大概是这样:
[模型调用工具] query_orders({'customer_id': 'C001'})
[工具返回] 2026-05-12 ¥899 蓝牙耳机;2026-05-18 ¥2399 显示器
[模型调用工具] search_docs({'keyword': '退货'})
[工具返回] 【退货政策】签收 7 天内可无理由退货,需保持原包装完整。
[最终回答]
客户 C001 最近购买记录:
1)2026-05-12,蓝牙耳机,¥899
2)2026-05-18,显示器,¥2399
退货政策:自签收之日起 7 天内可无理由退货,需保持原包装完整。
注意三点细节:
模型自己判断了"这个问题里有两件事,要分别调两个工具";
两次 tool 调用都真实走了 MCP 协议,而不是模型自己幻觉编造数据;
整套流程完全跑在本地,没有任何一次外部 API 调用——这正是企业内网场景最需要的形态。
7. 踩坑清单(建议收藏)
下面这些坑,是我把这套链路跑通过程中真实踩到的,按出现频率排序:
1)模型不调用工具,直接瞎回答。
99% 的情况是模型没选对。Qwen3、Llama 3.1+、DeepSeek-V3 这类才行;Llama 2 / 早期 Qwen 不支持。
另一个原因是 system prompt 太软,把"需要时调用工具"改成"必须先调用工具再回答"会立竿见影。
2)`stdio_client` 启动报 "Failed to spawn"。
SERVER_ARGS里的脚本路径必须 Host 那边能解析到。直接写绝对路径最稳。
Windows 下 Python 不在 PATH 时,把SERVER_CMD = "python"改成 Python 绝对路径。
3)`tool_calls` 字段一直为空。
检查 Ollama 版本,0.3.x 之前的版本对 tool 字段支持不全。
也可以打印resp看完整结构——有些模型会把工具调用塞到content里的 JSON 字符串,需要兼容解析。
4)中文输入参数被截断或乱码。
MCP 走 JSON-RPC,本身是 UTF-8。问题通常出在 Windows 终端编码。
进入脚本前执行chcp 65001,或在 Python 启动前设置PYTHONIOENCODING=utf-8。
5)多工具并行调用顺序乱。
上面的 demo 是顺序执行,生产环境如果模型一次返回多个 tool call,记得用asyncio.gather并发跑,但要小心写库类工具的并发安全。
8. 接下来还可以怎么扩展
这套最小骨架跑通以后,往下走的路非常宽:
把 stdio 换成 SSE / Streamable HTTP:MCP Server 部署到内网服务器,多台机器共用一个工具集。
把 Ollama 换成 vLLM / SGLang:吞吐量和并发上一个台阶,对外提供企业级 Agent 服务。
接入更多 Server:MCP 生态目前已经有几百个开源 Server——文件系统、Git、PostgreSQL、Slack、Notion……都是开箱即用的,你只需要在 Host 里同时连接多个ClientSession即可。
加上权限控制:MCP 协议本身没规定鉴权,企业场景需要自己在 Host 层加一层"哪个用户能调哪个工具"的策略。
结合本地 RAG:让 MCP 工具暴露知识库检索能力,再叠加长上下文,本地模型就能搞定 80% 的客服/内部助手类需求。
9. 结语
回到开头那句话——
MCP 的真正价值,不是给 Claude 用的,是给"所有模型"用的。
在 2026 年这个节点,谁能率先把"本地大模型 + MCP + 自有工具"这条链路在自己业务里跑通,谁就站在了私有化 Agent 的入场口上。
而入场的代价,看完这篇你应该知道了——
不到 200 行 Python。
文中所有代码均原创编写,已在本地环境跑通。如果你按文章操作时遇到坑,欢迎评论区留版本号和报错信息,我会在置顶补充。
后续会写:《MCP Server 走 SSE 远程部署 + 鉴权方案》《把 MCP 接入 vLLM 提供企业级 Agent 服务》。感兴趣可以先关注。
