核心摘要 (TL;DR)
- 概念澄清:MCP (Model Context Protocol) 不是新模型,而是连接大模型与外部工具/数据的“标准 USB 接口”,彻底解决 N 个大模型对接 M 个数据源的 N×M 灾难。
- 核心架构:拆解 MCP 的三层架构(Client 客户端、Server 服务端、Transport 传输层),理解其基于 STDIO 的安全本地通信机制。
- 实战目标:使用 管理项目,基于 框架,将上节课的“博客监控与通知”工具封装成标准的 MCP 服务,并在 OpenCode 客户端中成功调用。
咱们的大模型实战的博客系列已经快到尾声,咱们一路以来,各位友人对大模型的实战方向应该或多或少都有了一些概念,我们一步步将大模型的那些“唬人”的名头慢慢拉下神坛。咱们对大模型的本地部署,云端微调,RAG知识库,agent开发都有一定的概念。这些内容可能不够深,还停留在一个入门的介绍,但是咱们已经告别大模型小白的阶段了,这个博客系列的最初目标,就是想尽可能让各位友人对大模型的实战方向,概念有一个整体的了解。大模型不是“王谢堂前燕”,是可以掌握的。 当然咱们目前只是对各个方向有了大概的了解,剩下的就需要咱们根据自己感兴趣的,深入去做,在实践中去碰壁,去选择更适合的框架,更适合的技术栈,去了解工程上的指标,工程上的最佳实践。
好了,感慨完了,这里就是咱们本系列的最后一篇博客,我们来了解一下MCP。
首先,一言以概之,MCP只是一个Agent工具的USB接口。在USB接口出现前,手机数据传输并不是不能用,它只是将各种各样的接口统一成了一个,方便大家用同样的接口,而不是每家用一个其对应的接口。
好,MCP是干啥的? MCP全称Model Context Protocol, 模型上下文协议,这个名字可能有点难以理解。它实际上就是一个告诉模型,有什么工具可以怎么用,有哪些资源可以读,有哪些预设提示词可以调, 这三样就是Model Context。 MCP本身就是定义了,我们如何让模型知道这些信息的通信协议。

其核心架构有三部分
- MCP Host(客户端): 内置了大模型的调用方,发起调用/查询请求的一方,我们一般用OpenCode,ClaudeCode,Claude Desktop,Cursor或者vscode。正是因为协议统一,所以各方都可以通过这个接口去调同一个服务。
- MCP Server(服务端): 提供服务的工具/信息提供方,一般来说我们会开发的就是这部分,将我们的工具,资源,提示词封装起来,提供服务。
- Transport(传输层):客户端和服务端之间的通信通道,就目前而言有两种STDIO和Streamable HTTP(以前还有SSE,但是由于双端管理问题,安全认证问题等等问题废弃了)。一般STDIO用于本地,Streamable HTTP用于云端服务。
我们在前面Kaggle上已经用过很多次uv了,我们这次不跑notebook,我们这次在本地运行。
- 通过命令 ,安装uv依赖
-
- 如果是克隆了项目地址,可以直接同步项目依赖,跳过和
- 在新建的项目目录下运行 来初始化项目
- 通过来安装依赖, 我们就不用原生的pip了
开始之前,咱们先介绍一下这个新的库。在Kaggle上,我们使用Secrets去存我们的密钥,在本地环境,一般我们是用环境变量来存这些密钥,而dotenv可以用一个文件来写这些密钥,去覆盖环境变量,就像Kaggle的Secrets一样。 因为里面有密钥信息,所以我们一定要注意:不要上传我们的.env文件
- 配置密钥:在项目目录下新建.env文件,然后配置我们的老朋友们
- 编写mcp服务
这里我先把整体的代码贴出,然后再来讲解
我们这里用的是FastMCP,是一个快捷部署的模块,它会自动帮我们处理初始化,请求路由,不用我们手动监听请求。如果有需要的话,我们可以使用来做更精细化的控制
- 函数的参数类型和返回值类型要标明:FastMCP会将参数类型转为一个json schema发给client,告知调用的方式。
- docString要写详细:就是函数下用三引号括起来的这部分,最好写明函数使用场景,示例,参数说明。因为这里我们的例子比较简单,就没写那么复杂。这部分内容也是会发给大模型进行读取理解的。
- 不要往STDIO输出:因为我们是通过STDIO跟大模型通信,如果我们用print之类的打印,输出信息到STDIO,可能会将跟大模型的通信内容破坏。
最后一部分内容,就是运行的主函数了,然后我们运行这个脚本
这样这个mcp服务就运行起来了
我先自己写了一个客户端,来验证通信是否通路。代码如下
- 安装OpenCode
OpenCode是一个基于命令行的Ai编码工具,我们可以通过多种方式来下载
- 运行OpenCode
选一个项目文件夹,然后运行,就可以将opencode运行起来,其内置了一些免费模型供咱们使用

- 安装mcp
在所有的mcp client中,mcp都是可以通过一个配置文件去配置,大体上都是长这样
其本身,就是将运行咱们服务的命令配置上,这里也可以通过env这个键去配置一些密钥,但是我们可以不配置(opencode也不支持env),因为在咱们的server代码中,我们已经通过.env配置了。
okay, 那对于OpenCode,我们的配置文件名称是,可以配置在目录作为全局,也可以在项目文件夹下创建项目级别的配置。
我们这里在项目文件夹下创建,然后填入
开发小贴士:相对路径的陷阱
这里的命令使用了相对路径 。这要求咱们在 OpenCode 中打开的正好是包含该项目的根目录,否则 可能会找不到虚拟环境或报错。如果运行失败,建议直接替换为绝对路径以确保万无一失。
然后就可以输入prompt来让它查询并通知咱们。
- 教程完整代码
Q1: 为什么突然冒出来一个 MCP 协议?以前咱们写 Agent 不也是直接调各种 Tool 吗?
A: 为了解决“N 对 M”的重复造轮子问题。
- 现象:在 MCP 出现之前,如果咱们写了一个很棒的“查询本地数据库”的工具,咱们想让 ChatGPT 用,咱们需要对接一遍 OpenAI 的 Function Calling API;想让 Claude 用,又要对接一遍 Anthropic 的 API;想放在 Cursor 里,还得去写 Cursor 的插件。这就形成了 N 个大模型对接 M 个数据源的 N×M 复杂网络。
- 结论:MCP 就是那个“USB 接口”。它定义了一套标准的通信规范。咱们只需要按照 MCP 的标准把工具写一次(变成 MCP Server),任何支持 MCP 的客户端(无论是 Claude Desktop、Cursor 还是咱们自己写的脚本)都可以无缝接入。复杂网络瞬间变成了 N+M 的星型拓扑结构。
Q2: MCP 协议里的 “Host”、”Client” 和 “Server” 到底是怎么交互的?
A: 记住“菜单”和“点菜”的比喻。
- 现象:很多初学者容易搞混谁在调用谁。
- 结论:
- MCP Host/Client(客户端):像 Claude Desktop 或 OpenCode,它们是大模型的“宿主”,负责发起连接。
- MCP Server(服务端):也就是咱们写的 Python 脚本,负责提供具体的工具和数据。
- 交互流程:客户端连接后,第一件事是要求看“菜单”( 等)。服务端把写好注释的函数列表(JSON Schema)发过去。用户提问时,大模型看着这份菜单决定要用哪个工具,然后让客户端向服务端发送“点菜”指令()。服务端执行完 Python 代码,把结果“上菜”给客户端。
Q3: 为什么本地 MCP 服务通常使用 STDIO(标准输入输出)而不是 HTTP/REST API 进行通信?
A: 为了极致的便捷性、安全性和生命周期管理。
- 现象:大家习惯了写微服务用 HTTP 暴露端口,但 MCP 本地开发却偏爱 模式。
- 结论:
- 零端口冲突:不需要像传统 Web 服务那样去抢占 或 端口。
- 同生共死:客户端(如 OpenCode)在后台通过命令行拉起 Python 子进程。当咱们关闭编辑器时,子进程会被操作系统自动回收,不会留下僵尸进程。
- 天生安全:数据只在父子进程间的标准输入输出流中传递,不需要进行复杂的网络鉴权,也不用担心被同网段的其他机器恶意调用。
Q4: 既然 FastMCP 这么好用,为什么官方还要保留底层的 Low-level Server API?
A: 为了极致的动态能力和底层控制权。
- 现象:FastMCP 就像自动挡汽车,它强依赖 Python 的类型提示(Type Hints),在服务启动时就“静态”定死了工具的说明书(JSON Schema)。
- 结论:如果咱们的业务场景极其复杂,比如需要根据数据库中存在的表,实时动态生成或注销可用的工具;或者咱们需要让另一个大模型来实时决定当前有哪些工具可用,咱们就必须切回“手动挡”的底层 API(),手动监听 并动态拼装返回类型。
Q5: MCP 里的 Tools(工具)、Resources(资源)和 Prompts(提示词)到底有什么本质区别?
A: 核心区别在于“调用方”以及“是否有副作用”。
这是 MCP 协议设计最优雅的三板斧:
- Tools(工具):赋予大模型“行动力”。需要大模型主动思考并传入参数来执行,通常包含副作用(比如发邮件、写数据库、调外部 API)。
- Resources(资源):赋予大模型“感知力”。它是只读的数据源(如本地报错日志、配置文件)。它就像一个“挂载的网盘”,大模型或用户可以直接读取里面的文本作为对话上下文,没有任何副作用。
- Prompts(提示词):标准化的“工作流”。它通常由用户在客户端主动触发(带变量参数),快速生成一长串复杂的、包含角色设定的系统指令。
Q6: 在暴露资源时,咱们自己捏造的 URI(比如 或 ),大模型是怎么发起网络请求去读它的?
A: 大模型和客户端根本不发真正的网络请求!(划重点)
- 现象:很多有 Web 开发经验的朋友会疑惑,计算机网络里根本没有 这种协议,客户端是怎么解析的?
- 结论:在 MCP 的世界里,URI 只是一个路由“暗号”。当咱们在代码里写下 时,只是向客户端注册了这个字符串。当大模型想要这个资源时,客户端只会把 这串纯文本通过 STDIO 发给咱们的 Python 后端,由咱们的 Python 函数负责去本地硬盘或数据库捞数据并返回。所以,前缀怎么写完全由咱们自由发挥,只要符合业务语义即可。
Q7: 如果咱们有一整个文件夹(比如 100 篇本地 Markdown 笔记)想作为资源给大模型读,难道要写 100 个 吗?
A: 完全不需要,使用“资源模板 (Resource Templates)”即可。
- 现象:静态绑定(Direct Resources)只适合全局唯一的固定资源。
- 解决方案:MCP 允许在 URI 中使用大括号 定义动态参数。例如定义 ,大模型在分析问题时,会自动将 替换为它想查阅的笔记名传给咱们的函数。咱们的 Python 代码只需接收这个变量,拼凑出真实的文件路径读取即可(注意:实际开发中务必做好路径安全校验,防止目录穿越漏洞)。
Q8: 为什么在终端里单独运行脚本没问题,一挂载到 OpenCode 或 Claude Desktop 就报“未配置 API Key”或鉴权失败?
A: 这是因为子进程的“当前工作目录 (CWD)”发生了错位。(划重点)
这也是本次实战中最容易踩的坑!
- 现象:在终端运行脚本时,当前目录就是项目目录, 能顺利找到同级的 文件。但当宿主客户端(如 OpenCode)拉起 Python 子进程时,它的工作目录往往是编辑器的根目录甚至系统的临时目录,导致 寻址失败被静默跳过,API Key 自然读取为空。
- 解决方案:放弃默认的相对路径。在代码最顶端使用 动态锁定脚本所在的绝对路径,并拼接出 的绝对路径传给 。
Q9: 既然环境变量容易丢,那咱们在 里直接加个 字段配置cursor 教程密钥行不行?
A: 有些客户端可以(如 Claude Desktop),但在 OpenCode 中会直接报错失效。
- 现象:如果在 OpenCode 的配置里加上 对象,会直接提示 ,导致服务无法注册。
- 结论:这是因为不同客户端对 MCP 配置的 JSON Schema 校验严格程度不同。OpenCode 目前针对 的配置并没有开放 字段的支持。因此,采用 Q8 中的代码级绝对路径锁定,才是无视客户端环境差异的终极最佳实践。
Q10: 为了排查 API 报错咱们加了 ,但程序运行后日志文件里依然空空如也,怎么回事?
A: 咱们的日志配置被底层框架给“截胡”了。
- 现象:Python 的 有个非常隐蔽的特性:如果在此之前根节点(root logger)已经被其他模块(比如 及其底层的异步机制)初始化过了, 就会静默失效,什么都不会写入。且在 MCP 的 STDIO 模式下,普通的 会污染通信流导致解析崩溃。
- 解决方案:放弃全局配置。手动创建一个专属的 Logger(如 ),通过 切断它向根节点的传播,并手动为其添加 和 。
Q11: 咱们明明已经在 里填入了真实的 API Key,怎么日志里打印出来的还是旧的占位符(或者报 401 错误)?
A: 可能是旧环境变量的残留,或者子进程并未真正重启。
- 现象:操作系统终端里可能残留了之前跑测试时的环境变量,或者咱们修改了 文件但宿主客户端还在用旧的进程通信。
- 解决方案:
- 在代码中开启强制覆盖:,这能确保 里的值无视系统残留,绝对生效。
- 修改密钥后不需要重启整个 OpenCode,只需快捷键调出 MCP 菜单,将对应的 Server Toggle Off 然后再 Toggle On。这会杀掉旧进程并重新拉起,瞬间加载最新配置。
Q12: 启动 MCP 服务的命令是写 还是 ?这两者有什么区别?
A: 必须写 。两者的作用域完全不同!
- 现象:许多刚接触 uv 的开发者容易混淆这两个极为相似的命令。
- 结论: 专门用来跑当前项目的脚本,它会自动寻找项目下的 虚拟环境,因此能正确加载咱们刚安装的 和 等依赖。而 (等同于 )是用来在临时、隔离的环境里运行全局第三方工具(如代码格式化工具 )。跑咱们自己写的 MCP 本地服务,永远只用 。
本文作者: Algieba
本文链接: https://blog.algieba12.cn/llm08-mcp-intro/
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
发布者:Ai探索者,转载请注明出处:https://javaforall.net/279133.html原文链接:https://javaforall.net
