MCP协议实战:大模型标准接口告别孤岛

2026-06-11阅读 0热度 0
usb

前言

咱们这个大模型实战的博客系列已经快到尾声了。一路走来,各位应该对大模型的实战方向有了不少概念——从本地部署、云端微调,到RAG知识库、Agent开发,一步步把这些听起来唬人的名词拉下神坛。

当然,这些内容可能还停留在入门阶段,深度有限。但至少有一点是确定的:咱们已经告别大模型小白的阶段了。这个系列从一开始的目标,就是想让各位对大模型的实际应用方向和概念有一个整体认知。大模型不是"王谢堂前燕",普通人一样可以掌握。

往后的路,就得靠各位自己感兴趣的方向去深入探索了——在实践中碰壁,选择更合适的框架和技术栈,了解工程上的指标和最佳实践。

好了,感慨到此为止。这是本系列的最后一篇博客,我们来聊聊MCP。

1. MCP的概念

先一句话概括:MCP就是一个Agent工具的USB接口

回忆一下,在USB接口普及之前,手机数据传输虽然也能用,但每个品牌甚至每款手机的接口都不一样,充电线换来换去,麻烦得很。USB标准一出来,所有设备都统一了——同样的接口,通用的协议,省心省力。

MCP干的也是同样的事。全称Model Context Protocol(模型上下文协议),这个名字听起来有点抽象。通俗点说,它就是告诉大模型三件事:

  • 有什么工具可以调用
  • 有哪些资源可以读取
  • 有哪些预置提示词可以用

这三样合起来,就是"Model Context"(模型上下文)。而MCP本身,就是定义如何让模型知道这些信息的通信协议。

它的核心架构分为三层:

  • MCP Host(客户端):内置大模型的调用方,发起请求的一方。常见的包括OpenCode、ClaudeCode、Claude Desktop、Cursor或VS Code。正因为协议统一,这些客户端都可以通过同一个接口去调用同一个服务。
  • MCP Server(服务端):提供工具和信息的服务方。一般来说,我们开发的就是这一层——将我们的工具、资源、提示词封装起来提供服务。
  • Transport(传输层):客户端和服务端之间的通信通道。目前主要有两种:STDIO(用于本地)和Streamable HTTP(用于云端服务)。

时序关系大致如下:

2. 上手实现MCP Server

2.1 用uv管理项目

前面在Kaggle上我们已经用过很多次uv了,这次不跑Notebook,在本地运行。

  • 通过命令 pip install uv 安装uv
  • 如果是克隆了项目地址,可以直接 uv sync 同步依赖,跳过 uv adduv init
  • 在新建的项目目录下运行 uv init 初始化项目
  • 通过 uv add mcp requests feedparser python-dotenv 安装依赖,这里不用原生的pip了

2.2 实现MCP服务

我们将之前写的获取最新博客和发送通知的工具,封装成MCP的Tool即可。

写Agent的时候,我们是定义工具,转成Agent可用的Tool,然后交给Agent。而这里因为和客户端(大模型方)通过MCP沟通,我们只需要按MCP库的标准把工具暴露出去就行。

开始之前,先介绍一下 python-dotenv 这个新库。在Kaggle上我们用Secrets存密钥,在本地环境一般用环境变量来存。dotenv可以用一个 .env 文件来写这些密钥,覆盖环境变量,就像Kaggle的Secrets一样。因为里面有密钥信息,所以一定要注意:不要上传你的 .env 文件

  • 配置密钥:在项目目录下新建 .env 文件,然后配置这些老朋友:

    TARGET_EMAIL="xxxxx"
    EMAIL_API_KEY="xxxxxxx"
    WECHAT_API_KEY="xxxxxxx"
  • 编写MCP服务:先贴出整体代码,再来讲解。

    import os
    import requests
    import feedparser
    from dotenv import load_dotenv
    from mcp.server.fastmcp import FastMCP
    
    # 初始化 MCP 服务器
    load_dotenv()
    mcp = FastMCP("Blog_Monitor_Notifier")
    
    @mcp.tool()
    def get_latest_blog_post(rss_url: str) -> str:
        """
        请求并解析目标博客的 RSS feed,获取最新的一篇博客文章的标题和链接。
        当需要检查博客是否有更新时,调用此工具。
        """
        try:
            feed = feedparser.parse(rss_url)
            if feed.entries:
                latest_entry = feed.entries[0]
                return f"Title: {latest_entry.title}\nLink: {latest_entry.link}"
            return f"在 RSS 源 {rss_url} 中未找到任何文章。"
        except Exception as e:
            return f"获取博客失败: {str(e)}"
    
    @mcp.tool()
    def send_email_notification(post_title: str, post_link: str) -> str:
        """
        当发现博客有更新时,调用此工具发送邮件通知。
        必须提供新博客的标题 (post_title) 和链接 (post_link)。
        """
        target_email = os.environ.get("TARGET_EMAIL")
        email_api_key = os.environ.get("EMAIL_API_KEY")
    
        if not target_email or not email_api_key:
            return "邮件发送失败:未配置 TARGET_EMAIL 或 EMAIL_API_KEY 环境变量。"
    
        headers = {
            "Authorization": f"Bearer {email_api_key}",
            "Content-Type": "application/json"
        }
        payload = {
            "from" : "onboarding@resend.dev",
            "to": target_email,
            "subject": f"阿尔的代码屋更新咯:{post_title}",
            "text": f"检测到 阿尔的代码屋 更新了一篇新博客 \n\n标题:{post_title}\n链接: {post_link}"
        }
    
        try:
            response = requests.post("https://api.resend.com/emails", headers=headers, json=payload)
            if response.status_code == 200:
                return "邮件通知发送成功"
            return f"邮件发送失败: {response.text}"
        except Exception as e:
            return f"发送邮件时发生异常: {str(e)}"
    
    @mcp.tool()
    def send_wechat_notification(post_title: str, post_link: str) -> str:
        """
        当发现博客有更新时,调用此工具发送微信通知。
        必须提供新博客的标题 (post_title) 和链接 (post_link)。
        """
        wechat_api_key = os.environ.get("WECHAT_API_KEY")
    
        if not wechat_api_key:
            return "微信通知发送失败:未配置 WECHAT_API_KEY 环境变量。"
    
        url = f"https://sctapi.ftqq.com/{wechat_api_key}.send"
        data = {
            "title": f"阿尔的代码屋更新咯:{post_title}",
            "desp": f"检测到 阿尔的代码屋 更新了一篇新博客 \n\n标题:{post_title}\n链接: {post_link}"
        }
        try:
            response = requests.post(url, data=data)
            if response.status_code != 200:
                return f"微信消息发送失败: {response.text}"
    
            result = response.json()
            if result.get("code") != 0:
                return f"API 拒绝请求: {result.get('message')}"
    
            return "微信通知发送成功"
        except Exception as e:
            return f"发送微信通知时发生异常: {str(e)}"
    
    if __name__ == "__main__":
        # 启动 MCP 服务器,默认监听 stdio
        mcp.run()

2.3 解读MCP服务

from mcp.server.fastmcp import FastMCP

这里用的是 FastMCP,一个快捷部署的模块。它会自动处理初始化、请求路由,不需要手动监听请求。如果有需要,也可以使用 mcp.server.Server 做更精细化的控制。

import os
import requests
import feedparser
import logging
import sys
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP

# 初始化 MCP 服务器
current_dir = os.path.dirname(os.path.abspath(__file__)) # 锁死脚本所在路径,避免从外部通过client运行mcp时读不到.env
env_path = os.path.join(current_dir, '.env')
load_dotenv(env_path, override=True) # 强制使用.env中的密钥
mcp = FastMCP("Blog_Monitor_Notifier")

# 配置日志
log_file_path = os.path.join(current_dir, 'mcp_server.log')

# 1. 获取专属的 logger 实例并设置捕获级别
logger = logging.getLogger("blog_monitor")
logger.setLevel(logging.INFO)

# 2. 核心避坑:清空可能被框架提前注入的默认 handler
if logger.hasHandlers():
    logger.handlers.clear()

# 3. 创建格式化器
formatter = logging.Formatter('%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')

# 4. 配置文件 Handler
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# 5. 配置标准错误流 Handler (给 OpenCode 这种客户端看的)
stderr_handler = logging.StreamHandler(sys.stderr)
stderr_handler.setFormatter(formatter)
logger.addHandler(stderr_handler)

# 6. 核心避坑:切断向 root logger 的传播
# 防止日志被 FastMCP 底层拦截或吞噬
logger.propagate = False

# 测试日志是否正常工作
logger.info("=== 日志系统初始化成功,MCP Server 启动中 ===")

@mcp.tool()
def get_latest_blog_post(rss_url: str) -> str:
    """
    请求并解析目标博客的 RSS feed,获取最新的一篇博客文章的标题和链接。
    当需要检查博客是否有更新时,调用此工具。
    """
    logger.info(f"开始检查 RSS 源: {rss_url}")
    try:
        feed = feedparser.parse(rss_url)
        if feed.entries:
            latest_entry = feed.entries[0]
            logger.info(f"成功获取到最新文章: {latest_entry.title}")
            return f"Title: {latest_entry.title}\nLink: {latest_entry.link}"

        logger.warning(f"RSS 源 {rss_url} 解析成功,但没有找到文章条目。")
        return f"在 RSS 源 {rss_url} 中未找到任何文章。"
    except Exception as e:
        logger.error(f"解析 RSS 失败: {str(e)}", exc_info=True)
        return f"获取博客失败: {str(e)}"

@mcp.tool()
def send_email_notification(post_title: str, post_link: str) -> str:
    """
    当发现博客有更新时,调用此工具发送邮件通知。
    必须提供新博客的标题 (post_title) 和链接 (post_link)。
    """
    logger.info(f"准备发送邮件通知,目标文章: {post_title}")

    target_email = os.environ.get("TARGET_EMAIL")
    email_api_key = os.environ.get("EMAIL_API_KEY")

    # 打印脱敏后的鉴权信息,用于排查环境注入问题
    masked_email = target_email if target_email else "未配置"
    masked_key = f"{email_api_key[:5]}...{email_api_key[-3:]}" if email_api_key else "未配置"
    logger.info(f"读取到的配置 -> 目标邮箱: {masked_email}, API_KEY: {masked_key}")

    if not target_email or not email_api_key:
        logger.error("邮件发送终止:核心环境变量缺失。")
        return "邮件发送失败:未配置 TARGET_EMAIL 或 EMAIL_API_KEY 环境变量。"

    headers = {
        "Authorization": f"Bearer {email_api_key}",
        "Content-Type": "application/json"
    }
    payload = {
        "from" : "onboarding@resend.dev",
        "to": target_email,
        "subject": f"阿尔的代码屋更新咯:{post_title}",
        "text": f"检测到 阿尔的代码屋 更新了一篇新博客 \n\n标题:{post_title}\n链接: {post_link}"
    }

    try:
        logger.info("正在向 Resend API 发起 POST 请求...")
        response = requests.post("https://api.resend.com/emails", headers=headers, json=payload)

        if response.status_code == 200:
            logger.info("邮件 API 调用成功,邮件已发送。")
            return "邮件通知发送成功"

        logger.error(f"邮件 API 返回错误状态码: {response.status_code}, 详情: {response.text}")
        return f"邮件发送失败,API 返回: {response.text}"
    except Exception as e:
        logger.error(f"请求 Resend API 时发生异常: {str(e)}", exc_info=True)
        return f"发送邮件时发生网络异常: {str(e)}"

@mcp.tool()
def send_wechat_notification(post_title: str, post_link: str) -> str:
    """
    当发现博客有更新时,调用此工具发送微信通知。
    必须提供新博客的标题 (post_title) 和链接 (post_link)。
    """
    wechat_api_key = os.environ.get("WECHAT_API_KEY")

    if not wechat_api_key:
        return "微信通知发送失败:未配置 WECHAT_API_KEY 环境变量。"

    url = f"https://sctapi.ftqq.com/{wechat_api_key}.send"
    data = {
        "title": f"阿尔的代码屋更新咯:{post_title}",
        "desp": f"检测到 阿尔的代码屋 更新了一篇新博客 \n\n标题:{post_title}\n链接: {post_link}"
    }
    try:
        response = requests.post(url, data=data)
        if response.status_code != 200:
            return f"微信消息发送失败: {response.text}"

        result = response.json()
        if result.get("code") != 0:
            return f"API 拒绝请求: {result.get('message')}"

        return "微信通知发送成功"
    except Exception as e:
        return f"发送微信通知时发生异常: {str(e)}"

if __name__ == "__main__":
    # 启动 MCP 服务器,默认监听 stdio
    mcp.run()

这部分代码大家应该很熟悉了,唯一的区别就是在函数上加了个 @mcp.tool() 装饰器。没错,MCP的Tool定义就这么简单。

但有几点需要额外注意:

  • 函数的参数类型和返回值类型要标明:FastMCP会将参数类型转成JSON Schema发给客户端,告知调用方式。
  • docString要写详细:就是函数下面用三引号括起来的部分,最好写明使用场景、示例、参数说明。虽然这里的例子比较简单没写复杂,但这部分内容会发给大模型读取理解。
  • 不要往STDIO输出:因为是通过STDIO跟大模型通信,如果用了print之类的打印,输出信息到STDIO,可能会破坏通信内容。
if __name__ == "__main__":
    # 启动 MCP 服务器,默认监听 stdio
    mcp.run()

最后这部分是主函数,运行脚本 uv run 脚本名.py 即可启动MCP服务。

2.4 运行客户端

先自己写一个客户端来验证通信是否通畅。代码如下:

import asyncio
from mcp.client.session import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters

async def main():
    server_params = StdioServerParameters(
        command="python",
        args=["./llm08-mcp-intro/mcp_server.py"] # 需要结合自己的项目路径、项目工作目录、MCP服务脚本名称填写
    )

    async with stdio_client(server=server_params) as (read_stream, write_stream):
        async with ClientSession(read_stream=read_stream, write_stream=write_stream) as session:
            await session.initialize()

            tool_response = await session.list_tools()
            for tool in tool_response.tools:
                    print(tool)

            prompt_response = await session.list_prompts()
            for prompt in prompt_response.prompts:
                 print(prompt)

            resource_response =  await session.list_resources()
            for resource in resource_response.resources:
                 print(resource)

            ### ============模拟大模型选择调用的函数和填入参数====== ###
            # mock llm
            rss_target = "https://blog.algieba12.cn/atom.xml"
            target_tool_name = tool_response.tools[0].name
            target_tool_arguments = {"rss_url":rss_target}
            ### ================================================= ###

            call_result = await session.call_tool(
                 name=target_tool_name,
                 arguments=target_tool_arguments

            )

            for content in call_result.content:
                 if content.type == "text":
                      print(content.text)

if "__main__" == __name__:
    asyncio.run(main())

可以看到,整个客户端的逻辑完全符合之前的通信时序图:先注册、建立连接、获取工具/资源/Prompt列表,然后对工具进行调用。这里没有真的去调一个大模型,而是人工模拟了大模型的输出。

2.5 在OpenCode客户端使用

  • 安装OpenCode

OpenCode是一个基于命令行的AI编码工具,可以通过多种方式下载。

如果安装了Node.js,可以使用 npm i -g opencode-ai,也比较推荐这种方式,因为很多MCP是基于npx使用的。如果没有Node.js,可以用更通用的 curl -fsSL https://opencode.ai/install | bash 安装。

  • 运行OpenCode

选一个项目文件夹,然后运行 opencode,就可以把OpenCode运行起来。它内置了一些免费模型供使用。

  • 安装MCP

在所有MCP客户端中,MCP都可以通过配置文件来设置,大体结构如下:

{
  "mcpServers": {
    "BlogMonitor": {
      "command": "python",
      "args": ["绝对路径/llm08-mcp-intro/mcp_server.py"],
      "env": {
        "TARGET_EMAIL": "algieba.king@gmail.com",
        "EMAIL_API_KEY": "你的_resend_api_key",
        "WECHAT_API_KEY": "你的_wechat_api_key"
      }
    }
  }
}

本质上就是把运行服务的命令配置上。也可以通过 env 这个键配置密钥,但也可以不配置(OpenCode不支持env),因为服务端代码里已经通过 .env 配置了。

对于OpenCode,配置文件名是 opencode.json,可以放在 ~/.config/opencode/opencode.json 作为全局配置,也可以在项目文件夹下创建项目级别的配置。

这里在项目文件夹下创建 opencode.json,填入:

{
  "mcp": {
    "blog_monitor": {
      "type": "local",
      "command": ["uv", "run", "llm08-mcp-intro/mcp_server.py"],
      "enabled": true
    }
  }
}

开发小贴士:相对路径的陷阱

这里的命令使用了相对路径 llm08-mcp-intro/mcp_server.py。这要求你在OpenCode中打开的正好是包含该项目的根目录,否则 uv 可能会找不到虚拟环境或报错。如果运行失败,建议直接替换为绝对路径,确保万无一失。

这个命令本质上就是把服务跑起来,直接用python而不用uv也行,取决于怎么跑起server。各MCP客户端的配置可能稍有不同,但都相差不大。

配置好之后,重新用 opencode 命令在项目目录打开项目,然后按 shift+p 输入mcp。

选择 Toggle MCPs,就能看到这个MCP服务正在运行,已经连接上了。

然后就可以输入Prompt,让它查询并通知你。

3. 常见问题 (Q&A)

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: 记住"菜单"和"点菜"的比喻

很多初学者容易搞混谁在调用谁。实际上:

  1. MCP Host/Client(客户端):像Claude Desktop或OpenCode,是大模型的"宿主",负责发起连接。
  2. MCP Server(服务端):也就是你写的Python脚本,负责提供具体的工具和数据。
  3. 交互流程:客户端连接后,第一件事是要求看"菜单"(list_tools 等)。服务端把写好注释的函数列表(JSON Schema)发过去。用户提问时,大模型看着这份菜单决定要用哪个工具,然后让客户端向服务端发送"点菜"指令(call_tool)。服务端执行完Python代码,把结果"上菜"给客户端。

Q3: 为什么本地MCP服务通常使用STDIO(标准输入输出)而不是HTTP/REST API进行通信?

A: 为了极致的便捷性、安全性和生命周期管理

大家习惯了写微服务用HTTP暴露端口,但MCP本地开发却偏爱 stdio 模式:

  1. 零端口冲突:不需要像传统Web服务那样去抢占 80803000 端口。
  2. 同生共死:客户端(如OpenCode)在后台通过命令行拉起Python子进程。当你关闭编辑器时,子进程会被操作系统自动回收,不会留下僵尸进程。
  3. 天生安全:数据只在父子进程间的标准输入输出流中传递,不需要进行复杂的网络鉴权,也不用担心被同网段的其他机器恶意调用。

Q4: 既然FastMCP这么好用,为什么官方还要保留底层的Low-level Server API?

A: 为了极致的动态能力和底层控制权

FastMCP就像自动挡汽车,它强依赖Python的类型提示(Type Hints),在服务启动时就"静态"定好了工具的说明书(JSON Schema)。如果业务场景极其复杂——比如需要根据数据库中存在的表实时动态生成或注销可用的工具,或者需要让另一个大模型来实时决定当前有哪些工具可用——就必须切回"手动挡"的底层API(mcp.server.Server),手动监听 list_tools 并动态拼装返回类型。

Q5: MCP里的Tools(工具)、Resources(资源)和Prompts(提示词)到底有什么本质区别?

A: 核心区别在于"调用方"以及"是否有副作用"

这是MCP协议设计最优雅的三板斧:

  • Tools(工具):赋予大模型"行动力"。需要大模型主动思考并传入参数来执行,通常包含副作用(发邮件、写数据库、调外部API等)。
  • Resources(资源):赋予大模型"感知力"。它是只读的数据源(如本地报错日志、配置文件),就像一个"挂载的网盘",大模型或用户可以直接读取里面的文本作为对话上下文,没有任何副作用。
  • Prompts(提示词):标准化的"工作流"。通常由用户在客户端主动触发(带变量参数),快速生成一长串复杂的、包含角色设定的系统指令。

Q6: 在暴露资源时,捏造的URI(比如 postgres://system://logs),大模型是怎么发起网络请求去读它的?

A: 大模型和客户端根本不发真正的网络请求!(划重点)

很多有Web开发经验的朋友会疑惑,计算机网络里根本没有 memo:// 这种协议,客户端是怎么解析的?

在MCP的世界里,URI只是一个路由"暗号"。当你在代码里写下 @mcp.resource("memo://today") 时,只是向客户端注册了这个字符串。当大模型想要这个资源时,客户端只会把 memo://today 这串纯文本通过STDIO发给你的Python后端,由你的Python函数负责去本地硬盘或数据库捞数据并返回。所以,前缀怎么写完全由你自由发挥,只要符合业务语义即可。

Q7: 如果有一整个文件夹(比如100篇本地Markdown笔记)想作为资源给大模型读,难道要写100个 @mcp.resource 吗?

A: 完全不需要,使用"资源模板 (Resource Templates)"即可。

静态绑定(Direct Resources)只适合全局唯一的固定资源。MCP允许在URI中使用大括号 {} 定义动态参数。例如定义 @mcp.resource("file:///local_notes/{filename}"),大模型在分析问题时,会自动将 {filename} 替换为它想查阅的笔记名传给函数。Python代码只需接收这个变量,拼凑出真实的文件路径读取即可(注意:实际开发中务必做好路径安全校验,防止目录穿越漏洞)。

Q8: 为什么在终端里单独运行脚本没问题,一挂载到OpenCode或Claude Desktop就报"未配置API Key"或鉴权失败?

A: 这是因为子进程的"当前工作目录 (CWD)"发生了错位。(划重点)

这是本次实战中最容易踩的坑!在终端运行脚本时,当前目录就是项目目录,load_dotenv() 能顺利找到同级的 .env 文件。但当宿主客户端(如OpenCode)拉起Python子进程时,它的工作目录往往是编辑器的根目录甚至系统的临时目录,导致 .env 寻址失败被静默跳过,API Key自然读取为空。

解决方案:放弃默认的相对路径。在代码最顶端使用 current_dir = os.path.dirname(os.path.abspath(__file__)) 动态锁定脚本所在的绝对路径,并拼接出 .env 的绝对路径传给 load_dotenv()

Q9: 既然环境变量容易丢,那在 opencode.json 里直接加个 env 字段配置密钥行不行?

A: 有些客户端可以(如Claude Desktop),但在OpenCode中会直接报错失效。

如果在OpenCode的配置里加上 env 对象,会直接提示 Configuration is invalid... Invalid input,导致服务无法注册。这是因为不同客户端对MCP配置的JSON Schema校验严格程度不同。OpenCode目前针对 type: "local" 的配置并没有开放 env 字段的支持。因此,采用Q8中的代码级绝对路径锁定,才是无视客户端环境差异的终极最佳实践。

Q10: 为了排查API报错加了 logging.basicConfig(),但程序运行后日志文件里依然空空如也,怎么回事?

A: 日志配置被底层框架给"截胡"了

Python的 basicConfig 有个非常隐蔽的特性:如果在此之前根节点(root logger)已经被其他模块(比如 import FastMCP 及其底层的异步机制)初始化过了,basicConfig 就会静默失效,什么都不会写入。且在MCP的STDIO模式下,普通的 print() 会污染通信流导致解析崩溃。

解决方案:放弃全局配置。手动创建一个专属的Logger(如 logger = logging.getLogger("mcp_server")),通过 logger.propagate = False 切断它向根节点的传播,并手动为其添加 FileHandlerStreamHandler(sys.stderr)

Q11: 明明已经在 .env 里填入了真实的API Key,怎么日志里打印出来的还是旧的占位符(或者报401错误)?

A: 可能是旧环境变量的残留,或者子进程并未真正重启

操作系统终端里可能残留了之前跑测试时的环境变量,或者修改了 .env 文件但宿主客户端还在用旧的进程通信。

解决方案

  1. 在代码中开启强制覆盖:load_dotenv(env_path, override=True),这能确保 .env 里的值无视系统残留,绝对生效。
  2. 修改密钥后不需要重启整个OpenCode,只需快捷键调出MCP菜单,将对应的Server Toggle Off然后再Toggle On。这会杀掉旧进程并重新拉起,瞬间加载最新配置。

Q12: 启动MCP服务的命令是写 uv run 还是 uvx?这两者有什么区别?

A: 必须写 uv run。两者的作用域完全不同!

许多刚接触uv的开发者容易混淆这两个极为相似的命令。uv run 专门用来跑当前项目的脚本,它会自动寻找项目下的 .venv 虚拟环境,因此能正确加载刚安装的 mcpfeedparser 等依赖。而 uvx(等同于 uv tool run)是用来在临时、隔离的环境里运行全局第三方工具(如代码格式化工具 ruff)。跑自己写的MCP本地服务,永远只用 uv run

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策