MCP 从入门到实战:用 Python 构建一个真正可用的本地知识便签服务

MCP(Model Context Protocol,模型上下文协议)正在成为 AI 应用连接外部数据与工具的一种标准方式。本文不会停留在概念介绍,而是带你从零实现一个 MCP Server:它可以新增便签、搜索便签、以资源形式读取内容,还能向 AI 客户端提供可复用的 Prompt。最后,我们会使用 Python Client 和 MCP Inspector 完成连接、调用与调试。

本文适合第一次接触 MCP 的开发者。你只需要具备基础 Python 知识,跟着步骤即可完成实践。


一、MCP 到底是什么?

MCP 全称 Model Context Protocol,可以理解为一套让 AI 应用连接外部系统的开放协议。

大模型本身只擅长处理输入给它的内容。它默认不知道你的本地文件、数据库、内部 API,也不能直接操作真实业务系统。如果每接入一种数据源都编写一套私有适配代码,应用和工具之间会形成大量重复集成:

AI 应用 A ── 私有适配 ── 文件系统
AI 应用 A ── 私有适配 ── 数据库
AI 应用 B ── 私有适配 ── 文件系统
AI 应用 B ── 私有适配 ── 数据库

MCP 在中间定义统一协议后,关系变成:

AI 应用 ── MCP Client ── MCP Server ── 文件、数据库、API、业务系统

MCP Server 用统一方式声明“我有哪些工具、资源和提示模板”,MCP Client 用统一方式发现和调用这些能力。

一个便于新手理解的比喻是:MCP 类似 AI 应用的通用扩展接口。它规定连接与通信方式,但并不规定你必须使用哪个模型、数据库或业务框架。

MCP 不是什么?

MCP 经常被误解,需要先排除几个错误认识:

  • MCP 不是大模型,也不会让模型本身变得更聪明;
  • MCP 不是 Agent 框架,不负责任务规划和循环执行;
  • MCP 不是函数调用的替代品,MCP Tool 最终仍可能映射为模型工具调用;
  • MCP 不是数据库,它只是提供访问数据库的标准接口;
  • MCP Server 不一定部署在远程,也可以是客户端启动的本地进程。

MCP 解决的是连接标准化。模型是否调用工具、Agent 如何规划、应用怎样管理状态,仍由上层系统决定。


二、先搞懂 Host、Client 与 Server

MCP 采用 Host–Client–Server 架构。

角色 作用 通俗理解
Host 承载用户交互、模型与权限策略的 AI 应用 使用插件的应用程序
Client Host 内部负责与某个 Server 通信的协议客户端 一条独立连接
Server 对外提供 Tools、Resources、Prompts 等能力 插件能力提供方

需要特别注意:Host 和 Client 不是同一个概念。

一个 Host 可以同时连接多个 MCP Server,并为每个 Server 创建独立 Client:

                         ┌─ MCP Client A ─ MCP 文件服务
用户 ─ AI Host ─ 模型 ──┼─ MCP Client B ─ MCP 数据库服务
                         └─ MCP Client C ─ MCP 搜索服务

这样设计有利于隔离连接、权限和上下文。文件 Server 不需要知道数据库 Server 的存在,Server 之间也不应该默认共享数据。

一次连接通常发生什么?

从协议角度看,连接大致经历以下过程:

  1. Client 与 Server 建立传输连接;
  2. 双方执行初始化并协商协议版本和能力;
  3. Client 查询 Server 提供的 Tools、Resources、Prompts;
  4. Host 根据用户请求和权限选择能力;
  5. Client 发起调用,Server 返回结构化结果;
  6. 连接关闭或继续处理后续请求。

MCP 消息基于 JSON-RPC 2.0 的请求、响应和通知模型。日常使用 SDK 时通常不用手写 JSON-RPC,但了解这一点有助于排查请求 ID、错误响应和超时问题。


三、MCP Server 的三种核心能力

初学 MCP 最容易混淆 Tools、Resources 和 Prompts。判断方法是看谁控制、有没有副作用、返回的是什么

3.1 Tools:执行动作

Tool 是可以被模型或 Host 选择调用的函数,例如:

  • 查询订单;
  • 搜索知识库;
  • 计算数据;
  • 创建便签;
  • 调用业务 API。

Tool 有名称、描述、参数 Schema 和返回结果。它可能产生副作用,所以写入、删除、发送等操作必须有清晰权限和确认机制。

模型判断需要搜索
  → Client 调用 search_notes
  → Server 执行 Python 函数
  → 返回搜索结果
  → 结果进入模型上下文

3.2 Resources:提供可读取的上下文

Resource 更像“可寻址的数据”,由应用选择读取,例如:

  • file:///project/README.md
  • notes://index
  • notes://42
  • db://schema/users

Resource 通常用于读取,不应把明显的修改操作伪装成 Resource。它可以返回文本或二进制内容,并带有 URI、名称和 MIME Type。

3.3 Prompts:提供可复用的交互模板

Prompt 是 Server 暴露的参数化模板,例如:

  • 总结指定主题的便签;
  • 根据代码生成审查清单;
  • 按固定格式分析数据。

Prompt 的价值在于将某个数据源或工具的最佳使用方式一起封装起来。它通常由用户或 Host 显式选择,而不是被模型当作普通工具随意调用。

3.4 快速对照

能力 核心问题 控制方倾向 是否可能有副作用
Tool “执行什么动作?” 模型/应用 可能有
Resource “读取什么内容?” 应用 通常没有
Prompt “怎样组织一次交互?” 用户/应用 本身没有

如果你在设计 Server 时拿不准,就问自己:这是一个动词、一个可寻址名词,还是一个对话模板?


四、MCP 与 Function Calling、RAG、Agent 的关系

这四个概念处于不同层次,并不互相替代。

技术 解决的问题
Function Calling 模型怎样用结构化参数表达“我要调用某个函数”
MCP AI 应用怎样发现、连接并调用外部能力
RAG 怎样检索外部知识并把证据加入模型上下文
Agent 怎样围绕目标规划、行动、观察并循环执行

它们可以组成完整链路:

用户提出任务
  ↓
Agent 判断需要外部知识
  ↓
模型通过 Function Calling 选择工具
  ↓
MCP Client 调用 MCP Server 的检索 Tool
  ↓
Server 执行 RAG 检索并返回证据
  ↓
Agent 基于证据继续完成任务

所以,MCP 更像连接层,而不是整个 AI 应用架构。


五、传输方式:stdio 与 Streamable HTTP

MCP 需要传输层承载协议消息。实践中重点理解两种方式。

5.1 stdio:适合本地 Server

Client 启动一个 Server 子进程,通过标准输入和标准输出交换消息:

MCP Client ── stdin/stdout ── 本地 MCP Server 进程

优点:

  • 不需要监听端口;
  • 本地部署简单;
  • 进程生命周期由 Client 管理;
  • 适合文件、IDE、开发工具等本地能力。

stdio 有一条非常重要的规则:不要向 stdout 打印调试信息。stdout 是协议通道,普通 print() 可能破坏消息。日志应输出到 stderr 或写入日志文件。

5.2 Streamable HTTP:适合远程服务

远程 Server 通常通过 Streamable HTTP 提供 MCP 端点:

MCP Client ── HTTPS ── 网关/鉴权 ── 远程 MCP Server

它适合团队共享服务、云端数据源和集中式部署。与本地 stdio 相比,必须额外处理:

  • HTTPS;
  • 身份认证与授权;
  • Origin 校验;
  • 限流和超时;
  • 会话与负载均衡;
  • 审计日志;
  • 服务端请求伪造等网络风险。

早期 MCP 示例中经常出现独立 HTTP+SSE 传输。新项目应优先查看当前 SDK 对 Streamable HTTP 的支持,不要盲目复制过时教程。

本文先使用 stdio,因为它依赖最少、最适合第一次跑通 MCP。


六、实战目标:本地知识便签 MCP Server

我们将实现一个 notes-assistant Server,提供以下能力:

Tools

  • add_note:新增便签;
  • search_notes:按关键词搜索;
  • delete_note:删除便签,并要求显式确认。

Resources

  • notes://index:读取便签索引;
  • notes://{note_id}:读取指定便签。

Prompt

  • summarize_topic:生成指定主题的总结任务模板。

数据保存在本地 JSON 文件中,不需要数据库、API Key 或外部网络。


七、准备开发环境

7.1 环境要求

  • Python 3.10 或更高版本;
  • 推荐使用 uv 管理虚拟环境,也可以使用 pip
  • 一个支持 MCP 的客户端,或者直接使用本文的 Python Client。

7.2 创建项目

mkdir mcp-notes-demo
cd mcp-notes-demo

uv init
uv add "mcp[cli]"

如果没有安装 uv,也可以使用传统方式:

python -m venv .venv

# Windows PowerShell
.venv\Scripts\Activate.ps1

# macOS / Linux
source .venv/bin/activate

pip install "mcp[cli]"

最终目录如下:

mcp-notes-demo/
├── data/
│   └── notes.json       # 首次写入时自动创建
├── server.py
├── client.py
└── pyproject.toml

不要手动创建 notes.json,代码会自动完成初始化。


八、完整实现 MCP Server

创建 server.py

from __future__ import annotations

import json
import logging
import sys
from datetime import datetime, timezone
from pathlib import Path
from threading import Lock
from uuid import uuid4

from mcp.server.fastmcp import FastMCP


# stdio 模式下 stdout 用于 MCP 协议,日志必须写到 stderr
logging.basicConfig(
    stream=sys.stderr,
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)
logger = logging.getLogger("notes-mcp")


mcp = FastMCP("notes-assistant")

BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
NOTES_FILE = DATA_DIR / "notes.json"
FILE_LOCK = Lock()


def ensure_storage() -> None:
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    if not NOTES_FILE.exists():
        NOTES_FILE.write_text("[]", encoding="utf-8")


def load_notes() -> list[dict]:
    ensure_storage()
    with FILE_LOCK:
        try:
            content = NOTES_FILE.read_text(encoding="utf-8")
            data = json.loads(content)
        except (OSError, json.JSONDecodeError) as exc:
            logger.exception("failed to read notes")
            raise RuntimeError("便签存储暂时不可用") from exc

    if not isinstance(data, list):
        raise RuntimeError("便签文件格式错误")
    return data


def save_notes(notes: list[dict]) -> None:
    ensure_storage()
    # 先写临时文件,再替换正式文件,降低写到一半损坏数据的概率
    temp_file = NOTES_FILE.with_suffix(".tmp")
    payload = json.dumps(notes, ensure_ascii=False, indent=2)

    with FILE_LOCK:
        try:
            temp_file.write_text(payload, encoding="utf-8")
            temp_file.replace(NOTES_FILE)
        except OSError as exc:
            logger.exception("failed to save notes")
            raise RuntimeError("便签保存失败") from exc


@mcp.tool()
def add_note(title: str, content: str, tags: list[str] | None = None) -> dict:
    """新增一条本地便签。

    Args:
        title: 便签标题,不能为空,最长 100 个字符。
        content: 便签正文,不能为空,最长 5000 个字符。
        tags: 可选标签列表,每条最多 20 个字符。
    """
    title = title.strip()
    content = content.strip()
    normalized_tags = [tag.strip() for tag in (tags or []) if tag.strip()]

    if not title or len(title) > 100:
        raise ValueError("title 长度必须在 1 到 100 之间")
    if not content or len(content) > 5000:
        raise ValueError("content 长度必须在 1 到 5000 之间")
    if len(normalized_tags) > 10:
        raise ValueError("tags 最多包含 10 项")
    if any(len(tag) > 20 for tag in normalized_tags):
        raise ValueError("每个 tag 最长 20 个字符")

    notes = load_notes()
    note = {
        "id": uuid4().hex[:12],
        "title": title,
        "content": content,
        "tags": normalized_tags,
        "created_at": datetime.now(timezone.utc).isoformat(),
    }
    notes.append(note)
    save_notes(notes)
    logger.info("created note id=%s", note["id"])
    return note


@mcp.tool()
def search_notes(keyword: str = "", limit: int = 10) -> list[dict]:
    """按标题、正文或标签搜索便签;keyword 为空时返回最近便签。"""
    keyword = keyword.strip().casefold()
    if not 1 <= limit <= 50:
        raise ValueError("limit 必须在 1 到 50 之间")

    notes = load_notes()
    notes.reverse()

    if keyword:
        notes = [
            note
            for note in notes
            if keyword
            in " ".join(
                [
                    note.get("title", ""),
                    note.get("content", ""),
                    *note.get("tags", []),
                ]
            ).casefold()
        ]

    # 搜索列表只返回摘要,完整内容通过 Resource 获取
    return [
        {
            "id": note["id"],
            "title": note["title"],
            "preview": note["content"][:120],
            "tags": note.get("tags", []),
            "created_at": note["created_at"],
        }
        for note in notes[:limit]
    ]


@mcp.tool()
def delete_note(note_id: str, confirm: bool = False) -> dict:
    """删除指定便签。只有 confirm=true 时才会真正删除。"""
    note_id = note_id.strip()
    if not confirm:
        return {
            "deleted": False,
            "message": "这是写操作,请确认 note_id 后以 confirm=true 再次调用。",
        }

    notes = load_notes()
    remaining = [note for note in notes if note.get("id") != note_id]
    if len(remaining) == len(notes):
        return {"deleted": False, "message": "未找到对应便签"}

    save_notes(remaining)
    logger.info("deleted note id=%s", note_id)
    return {"deleted": True, "id": note_id}


@mcp.resource("notes://index")
def get_notes_index() -> str:
    """返回所有便签的 Markdown 索引。"""
    notes = load_notes()
    if not notes:
        return "# 便签索引\n\n当前没有便签。"

    lines = ["# 便签索引", ""]
    for note in reversed(notes):
        tags = ", ".join(note.get("tags", [])) or "无标签"
        lines.append(
            f'- [{note["title"]}](notes://{note["id"]}) '
            f'— `{tags}` — {note["created_at"]}'
        )
    return "\n".join(lines)


@mcp.resource("notes://{note_id}")
def get_note(note_id: str) -> str:
    """通过 notes://{note_id} 读取一条完整便签。"""
    note = next(
        (item for item in load_notes() if item.get("id") == note_id),
        None,
    )
    if note is None:
        raise ValueError(f"未找到便签:{note_id}")

    tags = ", ".join(note.get("tags", [])) or "无标签"
    return (
        f'# {note["title"]}\n\n'
        f'- ID:`{note["id"]}`\n'
        f'- 标签:{tags}\n'
        f'- 创建时间:{note["created_at"]}\n\n'
        f'{note["content"]}'
    )


@mcp.prompt()
def summarize_topic(topic: str, style: str = "要点式") -> str:
    """生成一个基于本地便签总结指定主题的提示模板。"""
    return f"""请总结本地便签中与“{topic}”有关的内容。

执行要求:
1. 先调用 search_notes 搜索“{topic}”;
2. 需要完整内容时读取对应的 notes://{{note_id}} Resource;
3. 只根据便签中的内容总结,不要补充未出现的事实;
4. 如果没有相关便签,明确说明资料不足;
5. 使用“{style}”输出,并在结论后标注便签标题。
"""


if __name__ == "__main__":
    # stdio 是本地 MCP Server 最常见的传输方式
    mcp.run(transport="stdio")

代码里有哪些关键点?

  1. FastMCP("notes-assistant") 创建 Server;
  2. @mcp.tool() 从类型注解和 Docstring 生成工具定义;
  3. @mcp.resource() 声明固定 Resource 和动态 Resource 模板;
  4. @mcp.prompt() 暴露参数化 Prompt;
  5. 工具内部仍然执行普通 Python 代码,MCP 并没有替代业务逻辑;
  6. 所有外部参数都在 Server 端校验,不能只信任模型;
  7. 删除操作要求 confirm=true,体现高风险动作的二次确认思想;
  8. 搜索只返回摘要,完整内容按需读取,避免无意义地膨胀上下文。

九、使用 MCP Inspector 调试 Server

不要一开始就把 Server 接进复杂 AI 客户端。先用 MCP Inspector 独立验证协议和工具,可以大幅降低排错难度。

在项目目录执行:

uv run mcp dev server.py

如果使用的是 pip 环境,可按当前 SDK 提供的 CLI 方式启动 Inspector。终端会显示本地调试地址,浏览器打开后完成以下检查:

  1. 确认 Server 初始化成功;
  2. 查看 Tools 列表,检查参数 Schema 和描述;
  3. 调用 add_note
{
  "title": "MCP 学习记录",
  "content": "MCP Server 可以通过 Tools、Resources 和 Prompts 暴露能力。",
  "tags": ["MCP", "Python"]
}
  1. 调用 search_notes,关键词填写 MCP
  2. 打开 notes://index
  3. 使用新增便签的 ID 读取 notes://{note_id}
  4. 查看 summarize_topic Prompt 的展开结果;
  5. 先以 confirm=false 调用删除,再确认是否符合预期。

如果 Tools 页面没有工具,优先检查装饰器是否在 mcp.run() 之前执行;如果 Server 直接退出,检查终端中的 stderr 日志。


十、编写 Python MCP Client,完整验证协议调用

Inspector 适合人工调试,自动化测试更适合使用 Client SDK。

创建 client.py

import asyncio
import sys
from pathlib import Path

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


BASE_DIR = Path(__file__).resolve().parent


async def main() -> None:
    server = StdioServerParameters(
        command=sys.executable,
        args=[str(BASE_DIR / "server.py")],
    )

    async with stdio_client(server) as (read_stream, write_stream):
        async with ClientSession(read_stream, write_stream) as session:
            # 必须先初始化,初始化阶段会完成版本和能力协商
            await session.initialize()

            tools = await session.list_tools()
            print("可用工具:", [tool.name for tool in tools.tools])

            created = await session.call_tool(
                "add_note",
                arguments={
                    "title": "第一次调用 MCP Tool",
                    "content": "这条便签由 Python MCP Client 创建。",
                    "tags": ["MCP", "实战"],
                },
            )
            print("新增结果:", created.content)

            searched = await session.call_tool(
                "search_notes",
                arguments={"keyword": "MCP", "limit": 5},
            )
            print("搜索结果:", searched.content)

            resources = await session.list_resources()
            print("固定资源:", [str(item.uri) for item in resources.resources])

            index = await session.read_resource("notes://index")
            print("便签索引:", index.contents[0].text)

            prompts = await session.list_prompts()
            print("可用 Prompt:", [item.name for item in prompts.prompts])

            prompt = await session.get_prompt(
                "summarize_topic",
                arguments={"topic": "MCP", "style": "表格"},
            )
            print("Prompt:", prompt.messages[0].content)


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

运行:

uv run python client.py

或者在已激活的虚拟环境中:

python client.py

如果终端依次显示工具列表、新增结果、搜索结果、资源和 Prompt,说明你已经真正跑通了:

Client 启动 Server 子进程
  → initialize 能力协商
  → list_tools 能力发现
  → call_tool 工具调用
  → read_resource 资源读取
  → get_prompt 获取模板

这比“客户端界面里好像出现了一个工具”更有价值,因为我们验证了每一层。


十一、接入支持 MCP 的 AI 客户端

不同客户端的配置文件位置和格式不完全相同,但本地 stdio Server 的核心配置通常包含:

  • Server 名称;
  • 启动命令 command
  • 命令参数 args
  • 必要的环境变量 env

很多客户端使用类似下面的 JSON 结构:

{
  "mcpServers": {
    "notes-assistant": {
      "command": "uv",
      "args": [
        "--directory",
        "C:/projects/mcp-notes-demo",
        "run",
        "python",
        "server.py"
      ]
    }
  }
}

请把路径替换成自己的绝对路径。Windows JSON 中可以使用正斜杠 /;如果使用反斜杠,则必须写成 \\

保存配置并重启或刷新客户端后,测试这些自然语言请求:

帮我创建一条便签:标题“学习计划”,正文“本周完成 MCP Server 实战”,标签是 MCP 和 Python。
搜索包含 MCP 的便签,并告诉我每条便签的标题。
删除刚才创建的学习计划便签。执行前先让我确认。

当用户提出请求后,并不是 MCP Server 在调用模型。真实过程是:

  1. AI 客户端把工具定义提供给模型;
  2. 模型判断应该调用哪个 Tool;
  3. 客户端显示或执行工具调用;
  4. MCP Client 将调用发送给 Server;
  5. Server 执行业务函数并返回结果;
  6. 客户端再把结果交给模型组织回答。

如果工具在 Inspector 和 Python Client 中正常、但 AI 客户端里不可见,问题通常在客户端配置、启动路径或客户端日志,而不是业务代码。


十二、如何设计高质量 MCP Tool

工具能运行不代表模型会正确使用。Tool 的设计质量会直接影响工具选择准确率。

12.1 名称要明确

推荐:

search_notes
get_order_status
create_support_ticket

不推荐:

handle
execute
process_data

12.2 描述要告诉模型“何时使用”

差的描述:

搜索工具。

更好的描述:

按标题、正文或标签搜索本地便签。当用户询问过去记录、学习笔记或已保存信息时使用;keyword 为空时返回最近便签。

12.3 参数应该窄而具体

不要提供一个万能参数:

def execute(payload: dict) -> dict:
    ...

优先使用类型明确的参数:

def search_notes(keyword: str, limit: int = 10) -> list[dict]:
    ...

类型注解不仅帮助开发者,也会进入 JSON Schema,帮助模型生成正确参数。

12.4 返回模型真正需要的信息

工具返回内容要做到:

  • 结构稳定;
  • 字段含义明确;
  • 错误可理解;
  • 不包含无关大字段;
  • 必要时提供下一步所需的 ID 或 URI。

例如搜索结果返回 id,模型随后才能读取 notes://{id}。如果只返回一段不可解析文本,后续调用会更困难。

12.5 区分只读与写操作

推荐将查询和修改分成不同 Tool:

get_note     # 只读
update_note  # 写入
delete_note  # 高风险写入

不要设计一个 manage_note(action=...) 包揽所有行为,否则权限、确认和审计都会变得模糊。


十三、错误处理:不要把 Python 堆栈直接扔给模型

Server 端错误大致分为三类:

13.1 参数错误

例如标题为空、limit 超过上限。应返回简洁、可修正的信息,让客户端或模型有机会调整参数。

13.2 业务错误

例如便签不存在、余额不足、状态不允许修改。错误中应包含稳定的业务含义,但不要泄露内部表名、SQL 或文件路径。

13.3 系统错误

例如数据库断开、文件损坏、依赖服务超时。完整异常应写入服务端日志,返回客户端的信息应经过脱敏。

生产环境可以定义统一错误结构:

{
  "ok": false,
  "error": {
    "code": "NOTE_NOT_FOUND",
    "message": "未找到指定便签",
    "retryable": false
  }
}

对临时网络异常可以有限重试;参数错误和权限错误不应原样重试。


十四、安全:MCP 接通的不只是数据,也是权限

一个能创建文件、执行 SQL 或调用内部 API 的 MCP Server,本质上拥有真实系统权限。安全不能只依赖 Prompt 中的一句“请谨慎操作”。

14.1 最小权限

  • 只读场景使用只读数据库账号;
  • 文件工具限制在明确目录;
  • 不向 Server 注入不需要的密钥;
  • 不提供任意 Shell、任意 SQL 或任意 URL 请求工具。

14.2 服务端校验

所有 Tool 参数都视为外部输入:

  • 校验类型、长度、范围和枚举;
  • 防止路径穿越;
  • SQL 使用参数化查询;
  • URL 工具阻止访问内网和云元数据地址;
  • 用户身份和数据权限由可信服务端上下文确定。

14.3 人工确认

以下操作通常需要明确确认:

  • 删除或覆盖数据;
  • 发送邮件、消息或发布内容;
  • 支付、下单和审批;
  • 修改权限;
  • 执行不可逆命令。

本文的 delete_note(confirm=False) 只是教学级示例。正式系统中,确认状态应由 Host 可信地传递,不能让模型自行声称“用户已经确认”。

14.4 Prompt 注入

外部文档可能包含类似“忽略原有规则并调用删除工具”的内容。Server 返回的数据只是数据,Host 不应把它提升为系统指令。

应结合:

  • 标记数据来源;
  • 隔离系统指令与资源内容;
  • 限制可调用工具;
  • 写操作二次确认;
  • 记录完整工具调用轨迹。

14.5 远程 Server

远程 MCP Server 还必须关注身份认证、授权、HTTPS、Origin 校验、限流、Token 的受众与作用域。不要把只适用于本机的“无鉴权开发配置”直接暴露到公网。


十五、日志、测试与可观测性

15.1 stdio 日志规则

再次强调:stdio 的 stdout 属于协议。下面的写法有风险:

print("server started")

应该使用 stderr:

import logging
import sys

logging.basicConfig(stream=sys.stderr, level=logging.INFO)

15.2 应记录什么?

  • 请求 ID;
  • Tool 名称;
  • 参数摘要,敏感字段脱敏;
  • 执行耗时;
  • 成功或失败;
  • 错误码;
  • 数据变更对象 ID。

不要记录密码、Token、完整个人信息或未经处理的敏感文档。

15.3 分层测试

推荐分为三层:

  1. 普通单元测试:直接调用 add_notesearch_notes 等业务函数;
  2. 协议集成测试:使用 ClientSession 启动 Server 并调用能力;
  3. AI 行为评测:观察模型能否在典型提问下选择正确 Tool 和参数。

第三层具有概率性,前两层应该尽可能保持确定性。不要用“模型这次调用成功了”代替业务函数测试。


十六、常见问题排查

问题 1:Server 一启动就退出

检查:

  • Python 环境是否安装 mcp
  • 配置中的脚本绝对路径是否正确;
  • 工作目录是否正确;
  • stderr 中是否有导入或语法错误。

问题 2:客户端提示找不到命令

桌面应用启动时继承的 PATH 可能和终端不同。可以把 command 改成 uv 或 Python 的绝对路径。

Windows 查看路径:

Get-Command uv
Get-Command python

macOS / Linux:

which uv
which python

问题 3:日志导致协议解析失败

检查是否使用 print() 向 stdout 输出了普通文本。stdio Server 的调试信息全部改用 stderr。

问题 4:Inspector 能用,AI 客户端不能用

通常检查:

  • 客户端配置文件是否放在正确位置;
  • 修改配置后是否刷新或重启;
  • JSON/TOML 是否存在语法错误;
  • 启动命令在客户端环境中是否可执行;
  • 客户端是否允许并启用了该 Server。

问题 5:工具可见,但模型总选错

优先优化:

  • Tool 名称;
  • Tool 描述;
  • 参数名称和 Schema;
  • 重叠工具的职责边界;
  • Tool 返回结果。

不是所有问题都应该通过增加更长的系统 Prompt 解决。

问题 6:动态 Resource 没出现在固定资源列表

notes://{note_id} 属于 Resource Template,和固定的 notes://index 不是同一类列表。客户端需要支持资源模板,或者直接使用已知 URI 读取。


十七、如何把示例升级到生产项目

当前示例适合学习,但 JSON 文件不适合多实例、高并发生产服务。可以按以下顺序升级。

第一阶段:替换存储

  • 使用 SQLite 或 MySQL 保存便签;
  • 增加用户 ID 和数据权限;
  • 写操作增加事务;
  • 对常用查询建立索引。

第二阶段:增加语义检索

  • 对便签分块并生成 Embedding;
  • 将向量写入向量数据库;
  • search_notes 同时支持关键词与语义检索;
  • 返回来源和相关性分数。

第三阶段:远程部署

  • 切换到 SDK 当前支持的 Streamable HTTP;
  • 部署在 HTTPS 后;
  • 接入身份认证、授权、限流和审计;
  • 配置健康检查、超时和监控。

第四阶段:Agent 工作流

  • Agent 通过 MCP 搜索资料;
  • 根据资料生成结构化草稿;
  • 审核 Agent 检查引用与事实;
  • 高风险动作进入人工确认节点。

每一步都应该先建立测试和权限边界,再扩大 Server 能力。


十八、什么时候适合使用 MCP?

适合:

  • 希望同一套工具被多个 AI 客户端复用;
  • 需要统一暴露文件、数据库、搜索或内部 API;
  • 希望能力可发现、参数有 Schema、调用可审计;
  • 正在构建可插拔的 AI 应用或 Agent 平台。

不一定适合:

  • 应用内部只有一个固定函数,且没有复用需求;
  • 普通 REST API 已经满足所有非 AI 系统集成;
  • 只是想让模型回答一段静态文本;
  • 团队尚未准备好处理工具权限和安全问题。

MCP 不是为了替换所有 API。通常是 MCP Server 在内部继续调用已有 REST API、数据库 SDK 或本地函数,然后以 AI 应用容易发现和使用的方式对外暴露。


总结

学完本文,应该记住以下几点:

  1. MCP 是 AI 应用连接外部数据、工具和工作流的开放协议;
  2. Host 承载应用与模型,Client 管理协议连接,Server 提供能力;
  3. Tool 用于执行动作,Resource 用于读取数据,Prompt 用于复用交互模板;
  4. stdio 适合本地 Server,Streamable HTTP 适合远程服务;
  5. MCP 可以与 Function Calling、RAG 和 Agent 配合,但它们不是同一层技术;
  6. Server 端必须负责参数校验、权限、确认、错误处理和审计;
  7. 最有效的学习方式,是先用 Inspector 和 Python Client 跑通完整协议链路。

MCP 的代码入门并不困难,真正的工程难点在于:如何把外部能力设计成边界清晰、权限最小、返回稳定、可被模型正确理解的接口。先把本文的本地便签服务跑通,再尝试替换为真实数据库或知识库,你就完成了从“理解 MCP”到“能开发 MCP”的关键一步。


参考资料

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐