1. 为什么你需要一个本地MCP服务器

如果你最近在玩Claude或者Cursor这类AI工具,可能会发现一个现象:它们很聪明,但总感觉隔着一层玻璃。你问它“我上周写的那个项目文档里提到的API密钥是什么?”,它只能礼貌地告诉你它不知道,因为它无法访问你的本地文件。于是你不得不手动打开文件夹,找到文档,复制那段文本,再粘贴回对话框。这种频繁的“切屏-复制-粘贴”循环,正在悄悄吞噬你的效率。

这就是MCP(Model Context Protocol,模型上下文协议)要解决的核心痛点。简单来说,MCP是一个开放标准,它就像在AI模型(比如Claude)和你电脑里的各种资源(文件、数据库、内部API)之间,架起了一座标准化的桥梁。市面上已经有很多现成的MCP服务器,帮你连接GitHub、Slack、Notion等云端服务。但今天我想聊的,是 为什么以及如何为你自己独一无二的本地环境,在15分钟内搭建一个专属的MCP服务器

现成的方案解决的是通用问题,而你的问题往往是具体的、私密的。也许你有一个存放所有项目日志的SQLite数据库,希望能用自然语言查询:“上个月用户登录失败最多的时段是几点?”;也许你的团队知识库是一堆散落在 ~/Documents 里的Markdown文件,你希望AI能真正“读懂”并检索它们;又或者,你公司内部有一个至关重要的数据查询API,但不可能有第三方为你开发集成工具。

本地MCP服务器的价值就在于此: 零部署、零费用、零审批 。所有代码、所有数据、所有计算都发生在你自己的机器上,没有任何信息会离开你的网络。你既获得了AI强大的理解和交互能力,又牢牢守住了数据的边界。这不仅仅是技术上的便利,更是一种工作哲学上的转变——让AI适应你的工作流,而不是反过来。

2. 核心思路与工具选型解析

2.1 MCP协议的核心思想:标准化接口

在深入代码之前,理解MCP的设计哲学至关重要。你可以把它想象成电脑的USB接口。在USB标准出现之前,每个外设(鼠标、键盘、打印机)都需要自己的专用接口和驱动,混乱且低效。USB制定了一套通用的电气和通信标准,从此“即插即用”成为可能。

MCP扮演的就是AI世界的“USB”角色。它定义了一套标准的通信协议,规定了AI模型(客户端)如何发现工具、如何调用工具、工具如何返回结果。作为开发者,你只需要按照这个标准,实现一个“服务器”,告诉AI:“嗨,我这里有这些工具(比如‘搜索本地文件’、‘查询数据库’),这是调用它们的格式。” AI模型就能通过这个标准接口来使用你的工具,无需理解你内部是用Python还是TypeScript实现的。

这种设计的精妙之处在于 解耦 。AI模型提供商(如Anthropic)不需要为世界上每一个可能的工具去编写集成代码;工具开发者(比如你和我)也不需要去研究每个AI模型复杂的插件系统。大家只要共同遵守MCP这个“普通话”,就能顺畅沟通。

2.2 为什么选择FastMCP和Python路线

搭建MCP服务器,官方提供了多种SDK,主流是Python和TypeScript。对于快速启动一个本地服务器,我强烈推荐使用Python和 fastmcp 这个库。原因有三点:

第一,极简的入门曲线。 fastmcp 库对MCP协议进行了高度封装,将复杂的网络通信和协议细节隐藏起来。你几乎不用关心JSON-RPC、SSE(服务器发送事件)这些底层概念,只需要用装饰器 @mcp.tool() 来标记你的Python函数,它就会自动被暴露为AI可用的工具。这大大降低了心智负担,让你能专注于工具本身的逻辑。

第二,Python生态的天然优势。 我们构建本地工具,最常见的操作是什么?是文件系统操作( pathlib , os )、数据解析( json , csv , sqlite3 )、调用本地命令行工具( subprocess )。这些都是Python标准库或一两个 pip install 就能轻松搞定的领域。用Python来粘合这些本地资源,写起来非常顺手和快速。

第三,适合快速迭代和原型验证。 本地工具的需求往往是灵光一现的。Python脚本的修改-运行周期极短,配合 fastmcp ,你可以在几分钟内增加一个新工具,重启Claude Desktop立刻看到效果。这种即时反馈对于探索“我到底需要什么工具”至关重要。

当然,如果你的工具链本身就以Node.js/TypeScript为核心,或者你计划未来将其部署为需要高并发的远程服务,那么从官方的TypeScript SDK开始也是合理的选择。但对于绝大多数以“个人生产力”和“本地自动化”为目标的场景, fastmcp +Python是抵达终点最快的那条路。

注意 :确保你的Python版本在3.10及以上。一些新的语言特性(如类型联合语法 | )和库的依赖可能需要这个版本。你可以通过命令行输入 python --version 来检查。

3. 从零到一:你的第一个MCP服务器实战

理论说再多,不如动手跑一遍。我们的目标是:创建一个能搜索你电脑上某个文件夹内所有Markdown笔记的MCP服务器。

3.1 环境准备与项目初始化

首先,打开你的终端(命令行),创建一个专属的工作目录,并安装唯一的依赖。

# 创建一个项目文件夹
mkdir my-local-mcp && cd my-local-mcp

# 创建虚拟环境(推荐,避免包冲突)
python -m venv venv

# 激活虚拟环境
# 在 macOS/Linux 上:
source venv/bin/activate
# 在 Windows 上:
# venv\Scripts\activate

# 安装 fastmcp
pip install fastmcp

接下来,在你喜欢的代码编辑器(VS Code, PyCharm等)中,创建一个名为 note_search_server.py 的文件。我们将从这里开始编写代码。

3.2 十行代码实现核心功能

下面这段代码,就是一个功能完整的MCP服务器。请逐行阅读,我会解释每一部分的意图。

# note_search_server.py
from fastmcp import FastMCP
from pathlib import Path

# 1. 初始化一个MCP服务器实例,并给它起个名字
mcp = FastMCP("Local Notes Search")

# 2. 定义你的笔记存放目录。这里假设是用户主目录下的'notes'文件夹
#    你可以修改为任何你实际存放笔记的路径
NOTES_DIR = Path.home() / "notes"

# 3. 使用 @mcp.tool() 装饰器将一个普通函数声明为MCP工具
@mcp.tool()
def search_notes(query: str) -> str:
    """
    根据关键词搜索本地笔记文件。
    这个文档字符串(docstring)非常重要!AI会读取它来理解工具的用途。
    """
    results = []
    # 4. 遍历 NOTES_DIR 目录下所有 .md 文件
    for file_path in NOTES_DIR.glob("*.md"):
        content = file_path.read_text(encoding='utf-8')
        # 5. 进行简单的、不区分大小写的关键词匹配
        if query.lower() in content.lower():
            # 6. 如果匹配成功,将文件名和内容片段加入结果列表
            #    这里只截取前200字符作为预览,避免返回过于冗长的文本
            preview = content[:200] + "..." if len(content) > 200 else content
            results.append(f"**{file_path.name}**\n{preview}")

    # 7. 处理结果:有结果则拼接返回,无结果则返回友好提示
    if not results:
        return f"没有找到包含 '{query}' 的笔记。"
    return "\n\n---\n\n".join(results)

代码逻辑拆解与实操要点:

  1. FastMCP 实例 :这是服务器的核心对象。参数 ”Local Notes Search” 是服务器名称,会在Claude的工具列表中显示。
  2. 路径定义 Path.home() / “notes” 使用了 pathlib 库,这是一种面向对象的、跨平台的路径操作方式,比直接拼接字符串更安全、更清晰。请确保你的 ~/notes 目录真实存在,或者将其修改为你自己的笔记路径,例如 Path(“D:/MyNotes”)
  3. 工具装饰器 @mcp.tool() 是魔法发生的地方。它告诉 fastmcp :“这个函数应该被包装成一个MCP工具”。函数名 search_notes 将成为工具的命令标识。
  4. 函数签名与文档 (query: str) -> str 是类型注解,明确了输入是一个字符串,输出也是一个字符串。下方的三引号文档字符串 至关重要 ,Claude会依靠这个描述来理解何时以及如何调用这个工具。请用清晰、自然的语言描述工具的功能。
  5. 文件遍历与搜索 NOTES_DIR.glob(“*.md”) 会递归地(如果需要,可以用 **/*.md )查找所有Markdown文件。这里的搜索逻辑非常基础(子字符串匹配),但对于初步验证概念已经足够。
  6. 结果格式化 :返回给AI的字符串应该易于阅读和理解。我们使用了Markdown的粗体语法 ** 来突出文件名,并用 --- 分隔不同文件的结果,这样Claude在回复中也能较好地呈现它们。
  7. 错误处理 :我们处理了“未找到结果”的情况,返回了一个友好的提示信息。这是良好用户体验的基础。

实操心得 :在编写工具函数时,时刻思考 AI会如何理解和使用它 。给工具起一个动词开头的、描述性的名字(如 search_notes 而非 notes_searcher )。在文档字符串中,清晰地说明输入参数的意义和输出结果的格式。一个模糊的工具描述会导致AI错误地调用它或无法发挥其作用。

3.3 连接Claude Desktop:让工具“活”过来

代码写好了,但它还只是一个孤立的脚本。下一步是让它与Claude Desktop对话。

1. 定位Claude Desktop配置文件: 这个文件的位置因操作系统而异:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Linux: ~/.config/Claude/claude_desktop_config.json

如果文件或目录不存在,请先运行一次Claude Desktop应用,它通常会初始化生成这个配置文件。

2. 编辑配置文件: 用文本编辑器(如VS Code、Notepad++)打开这个JSON文件。其初始内容可能是一个空对象 {} 或者已经有一些其他MCP服务器的配置。我们需要添加我们自己的服务器配置。

关键是要正确指定Python解释器和脚本的路径。假设你的 note_search_server.py 放在 /Users/yourname/Projects/my-local-mcp/ 目录下。

{
  "mcpServers": {
    "local-notes": {
      "command": "/Users/yourname/Projects/my-local-mcp/venv/bin/python",
      "args": ["/Users/yourname/Projects/my-local-mcp/note_search_server.py"],
      "env": {}
    }
  }
}

配置参数详解:

  • ”local-notes” : 这是你给这个服务器实例起的键名,可以自定义,但建议用有意义的、不含空格的小写字符串。
  • command : 这是最容易出错的地方 。它必须指向 你项目虚拟环境中 的Python解释器绝对路径。上面例子中 venv/bin/python 就是虚拟环境下的Python。如果你没有使用虚拟环境,或者想用系统全局Python,可以简单地写成 ”python3″ ”python” ,但这可能引发包依赖冲突。
  • args : 一个数组,第一个元素就是你的Python脚本的 绝对路径 。确保路径正确。
  • env : 可以设置环境变量,通常留空对象 {} 即可。

Windows用户的特别注意事项: 在Windows上,路径使用反斜杠 \ ,并且需要转义。同时,虚拟环境下的Python可执行文件通常是一个 python.exe

{
  "mcpServers": {
    "local-notes": {
      "command": "C:\\Users\\YourName\\Projects\\my-local-mcp\\venv\\Scripts\\python.exe",
      "args": ["C:\\Users\\YourName\\Projects\\my-local-mcp\\note_search_server.py"]
    }
  }
}

3. 重启并验证: 保存配置文件,然后 完全关闭并重新启动Claude Desktop应用 。MCP服务器只在Claude启动时被加载。

启动后,新建一个对话。你应该能在输入框上方或侧边的工具菜单中,看到名为“Local Notes Search”的服务器及其工具“search_notes”。现在,你可以尝试输入:“请用我的本地笔记工具,搜索所有提到‘项目里程碑’的笔记。”

如果一切正常,Claude会调用你的Python脚本,执行搜索,并将格式化后的结果返回在对话中。如果出现错误,Claude通常会返回来自服务器的错误信息,这是排查问题的主要依据。

4. 构建实用工具集:超越简单搜索

第一个工具跑通后,你会立刻意识到它的潜力。一个服务器可以承载多个工具。让我们来升级这个服务器,让它成为一个更得力的本地助手。

4.1 多工具集成:文件管理与数据查询

我们将在一个服务器里集成四个常用工具:搜索笔记、列出所有笔记、读取特定笔记、查询本地JSON数据文件。

# enhanced_local_tools.py
from fastmcp import FastMCP
from pathlib import Path
import json
import sqlite3  # 导入sqlite3库,为后续扩展做准备
from datetime import datetime

mcp = FastMCP("My Enhanced Local Toolkit")

# 定义常用路径
NOTES_DIR = Path.home() / "notes"
DATA_DIR = Path.home() / "data"
DB_PATH = Path.home() / "data" / "app_logs.db"  # 假设有一个SQLite数据库

# --- 工具1:增强版笔记搜索 ---
@mcp.tool()
def search_notes(query: str, case_sensitive: bool = False) -> str:
    """
    在本地笔记目录中搜索包含关键词的Markdown文件。
    支持是否区分大小写的选项。

    Args:
        query: 要搜索的关键词。
        case_sensitive: 是否区分大小写,默认为False(不区分)。
    """
    if not NOTES_DIR.exists():
        return f"笔记目录不存在:{NOTES_DIR}"

    results = []
    search_text = query if case_sensitive else query.lower()

    for file_path in NOTES_DIR.glob("**/*.md"):  # 使用**递归搜索子目录
        try:
            content = file_path.read_text(encoding='utf-8')
            target_content = content if case_sensitive else content.lower()

            if search_text in target_content:
                # 找到匹配行作为上下文
                lines = content.split('\n')
                matching_lines = []
                for i, line in enumerate(lines[:10]):  # 只在前10行找匹配行,提高效率
                    if search_text in (line if case_sensitive else line.lower()):
                        matching_lines.append(f"  第{i+1}行: {line.strip()}")
                context = "\n".join(matching_lines[:3]) if matching_lines else "(匹配内容未在文件前部找到)"
                results.append(f"📄 **{file_path.relative_to(NOTES_DIR)}**\n{context}")
        except UnicodeDecodeError:
            results.append(f"⚠️ **{file_path.name}** - 文件编码错误,无法读取。")

    if not results:
        return f"在 {NOTES_DIR} 及其子目录中,未找到包含 '{query}' 的笔记。"
    return f"找到 {len(results)} 个相关笔记:\n\n" + "\n---\n".join(results)

# --- 工具2:列出所有可用笔记 ---
@mcp.tool()
def list_notes() -> str:
    """列出笔记目录下所有的Markdown文件,按修改时间排序。"""
    if not NOTES_DIR.exists():
        return "笔记目录不存在。"

    files = list(NOTES_DIR.glob("**/*.md"))
    if not files:
        return "笔记目录下未找到任何.md文件。"

    # 按最后修改时间排序,最新的在前
    files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
    output_lines = []
    for f in files:
        mtime = datetime.fromtimestamp(f.stat().st_mtime).strftime('%Y-%m-%d %H:%M')
        rel_path = f.relative_to(NOTES_DIR)
        output_lines.append(f"- `{rel_path}` (修改于: {mtime})")
    return "## 可用笔记列表\n" + "\n".join(output_lines)

# --- 工具3:读取特定笔记的完整内容 ---
@mcp.tool()
def read_note(filename: str) -> str:
    """
    读取并返回指定笔记文件的完整内容。
    文件名可以是相对路径(相对于笔记根目录)。
    """
    # 防止目录遍历攻击,确保路径在笔记目录内
    target_path = (NOTES_DIR / filename).resolve()
    if not NOTES_DIR.resolve() in target_path.parents and target_path != NOTES_DIR.resolve():
        return "错误:不允许访问笔记目录之外的文件。"

    if not target_path.exists():
        # 尝试添加.md后缀
        target_path_with_ext = target_path.with_suffix('.md')
        if target_path_with_ext.exists():
            target_path = target_path_with_ext
        else:
            return f"未找到文件: {filename}。请检查文件名,或使用`list_notes`工具查看所有文件。"

    try:
        content = target_path.read_text(encoding='utf-8')
        return f"# {target_path.name}\n\n{content}"
    except Exception as e:
        return f"读取文件时出错: {str(e)}"

# --- 工具4:查询本地JSON数据文件 ---
@mcp.tool()
def query_json_data(filename: str, key_path: str = "") -> str:
    """
    从本地JSON文件中查询数据。
    可以查询整个文件,或通过点号分隔的路径(如 'users.0.name')查询嵌套值。

    Args:
        filename: JSON文件名(位于DATA_DIR下)。
        key_path: (可选)要查询的键路径,例如 'status.total_users'。
    """
    file_path = DATA_DIR / filename
    if not file_path.exists():
        return f"JSON文件不存在: {file_path}"

    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
    except json.JSONDecodeError as e:
        return f"JSON文件解析错误: {str(e)}"

    # 如果没有指定key_path,返回整个文件的概览(例如,只显示顶层键)
    if not key_path:
        top_keys = list(data.keys()) if isinstance(data, dict) else f"[数组,长度:{len(data)}]"
        return f"文件 `{filename}` 加载成功。顶层结构: {top_keys}。请指定 `key_path` 参数以查询具体内容。"

    # 根据点号路径查询嵌套值
    keys = key_path.split('.')
    current = data
    try:
        for k in keys:
            if isinstance(current, list):
                k = int(k)  # 尝试将索引转换为整数
            current = current[k]
        # 成功找到,格式化返回
        return json.dumps(current, indent=2, ensure_ascii=False)
    except (KeyError, IndexError, ValueError, TypeError) as e:
        return f"无法找到路径 '{key_path}'。错误: {type(e).__name__}。可用的顶层键: {list(data.keys()) if isinstance(data, dict) else '这是一个数组'}"

# --- 工具5:SQLite数据库查询示例(扩展思路) ---
@mcp.tool()
def query_logs(days: int = 7, level: str = "ERROR") -> str:
    """
    查询最近N天内,指定级别的应用日志(示例,需要实际数据库)。

    Args:
        days: 查询最近多少天的日志,默认为7。
        level: 日志级别,如 INFO, WARN, ERROR。默认为 ERROR。
    """
    if not DB_PATH.exists():
        return f"数据库文件不存在: {DB_PATH}。此工具仅为示例。"

    try:
        conn = sqlite3.connect(DB_PATH)
        cursor = conn.cursor()
        since_date = datetime.now().date().isoformat()  # 简化处理,实际应按时间戳查询
        # 假设表结构为 logs(timestamp, level, message)
        cursor.execute(
            "SELECT timestamp, level, message FROM logs WHERE level = ? ORDER BY timestamp DESC LIMIT 20",
            (level,)
        )
        rows = cursor.fetchall()
        conn.close()

        if not rows:
            return f"最近{days}天内未找到级别为 '{level}' 的日志。"
        result_lines = [f"| 时间戳 | 级别 | 信息 |", f"|---|---|---|"]
        for ts, lvl, msg in rows:
            result_lines.append(f"| {ts} | {lvl} | {msg[:50]}... |")
        return "\n".join(result_lines)
    except sqlite3.Error as e:
        return f"数据库查询出错: {str(e)}"

这个增强版服务器带来的提升:

  1. 工具专业化与参数化 search_notes 增加了 case_sensitive 参数,让搜索更灵活。 query_json_data 支持通过点号路径(如 config.database.host )查询复杂的嵌套JSON结构。
  2. 健壮的错误处理 :每个工具都检查路径是否存在、文件是否能被解析,并返回对人友好的错误信息,而不是让Python异常直接崩溃。
  3. 递归文件搜索 glob(“**/*.md”) 能搜索 NOTES_DIR 下的所有子目录,更符合真实的文件组织方式。
  4. 安全考虑 :在 read_note 中,我们使用 resolve() 和路径检查来防止潜在的目录遍历攻击(虽然本地使用风险较低,但这是好习惯)。
  5. 扩展性示范 query_logs 工具展示了如何连接SQLite数据库。即使你现在没有这个数据库,它也提供了一个清晰的模板。当你真的有一个本地日志库时,只需修改SQL查询语句即可投入使用。

更新服务器代码后,你需要更新Claude Desktop配置文件中的 args 路径,指向这个新的 enhanced_local_tools.py 文件,然后重启Claude Desktop。

5. 进阶技巧与实战避坑指南

当你成功运行了几个基础工具后,可能会想打造更复杂、更强大的集成。以下是一些进阶思路和实践中必然遇到的坑及其解决方案。

5.1 性能优化:当文件数量爆炸时

最初的简单循环遍历在几十个文件时没问题,但如果你的笔记库有成千上万个文件,每次搜索都全文读取会非常慢。

解决方案:建立轻量级索引。

我们可以在服务器启动时,或在笔记目录变化时,构建一个内存中的索引:只记录文件名和文件内容的关键词(或摘要)。

# 示例:简单的基于关键词的倒排索引(简化版)
import hashlib
from typing import Dict, Set

class SimpleNoteIndex:
    def __init__(self, notes_dir: Path):
        self.notes_dir = notes_dir
        self.index: Dict[str, Set[str]] = {}  # 关键词 -> 包含该关键词的文件路径集合
        self.file_hashes: Dict[str, str] = {} # 文件路径 -> 内容哈希,用于判断是否更新
        self._build_index()

    def _build_index(self):
        """遍历目录,构建索引。这里简单地将每个非停用词作为关键词。"""
        # 一个简单的英文停用词列表,可根据需要扩充
        stop_words = {"the", "a", "an", "and", "or", "but", "in", "on", "at", "to", "for", "of", "with", "by"}
        self.index.clear()
        for file_path in self.notes_dir.glob("**/*.md"):
            try:
                content = file_path.read_text(encoding='utf-8').lower()
                current_hash = hashlib.md5(content.encode()).hexdigest()
                # 如果文件未更新,跳过重建索引
                if self.file_hashes.get(str(file_path)) == current_hash:
                    continue
                self.file_hashes[str(file_path)] = current_hash

                # 简单分词(按空格和标点分割)
                words = set(re.findall(r'\b\w+\b', content))
                for word in words:
                    if len(word) > 2 and word not in stop_words:  # 过滤短词和停用词
                        self.index.setdefault(word, set()).add(str(file_path))
            except Exception:
                continue

    def search(self, query: str) -> List[str]:
        """使用索引进行搜索"""
        query_words = set(re.findall(r'\b\w+\b', query.lower()))
        result_files = set()
        for word in query_words:
            if word in self.index:
                result_files.update(self.index[word])
        return list(result_files)

# 在MCP服务器初始化时创建索引对象
notes_index = SimpleNoteIndex(NOTES_DIR)

@mcp.tool()
def quick_search_notes(query: str) -> str:
    """(实验性)使用索引快速搜索笔记,适用于大型笔记库。"""
    matching_files = notes_index.search(query)
    # ... 后续读取匹配文件的部分内容进行展示 ...

这个索引非常简陋,但对于文本搜索的速度提升是数量级的。更复杂的方案可以考虑使用 Whoosh SQLite FTS 等嵌入式全文搜索引擎。

5.2 处理复杂交互:工具调用工具与状态管理

有时,一个任务需要多个工具协作完成。例如,用户可能说:“帮我找出上个月修改过的、且内容包含‘预算’的所有笔记,并把它们的摘要列出来。” 这涉及到 list_notes (按时间过滤)和 search_notes 两个工具。

MCP协议本身是 无状态 的,每次工具调用都是独立的。AI模型(Claude)负责理解复杂指令,并将其拆解成一系列顺序的工具调用。这意味着你 不需要 在服务器端维护复杂的会话状态。

你只需要确保每个工具是 幂等 的(给定相同输入,产生相同输出)和 职责单一 的。Claude会像导演一样,根据你的工具说明书(函数签名和文档字符串),编排它们的调用顺序,并传递中间结果。

5.3 常见问题排查表

在开发和配置过程中,你几乎一定会遇到下面这些问题。这张表可以帮你快速定位。

问题现象 可能原因 排查步骤与解决方案
Claude Desktop重启后,工具列表中没有出现我的服务器。 1. 配置文件路径错误。
2. 配置文件语法错误(JSON格式不对)。
3. command args 中的路径错误。
1. 检查配置文件路径 :确保文件在正确的操作系统目录下。
2. 验证JSON格式 :使用在线JSON校验工具或 python -m json.tool your_config.json 检查。
3. 检查路径 :在终端中手动执行 command args 组成的命令,看是否能成功运行Python脚本而不报错。
工具列表出现了,但调用时失败,提示“Server error”或“Tool execution failed”。 1. Python脚本本身有语法或运行时错误。
2. 依赖库未安装(如 fastmcp )。
3. 工具函数内部抛出未捕获的异常。
1. 直接运行脚本 :在终端用配置中的命令手动运行脚本,看是否有错误输出。这是最重要的调试手段。
2. 检查虚拟环境 :确认Claude配置中指定的Python路径确实安装了 fastmcp ( pip list | grep fastmcp )。
3. 在工具函数内部添加更详细的 try...except ,将错误信息通过 return 返回给Claude。
工具调用成功,但返回“No notes found”,而实际上文件存在。 1. NOTES_DIR 路径配置不正确。
2. 文件编码问题导致读取失败。
3. 搜索逻辑有误(如大小写问题)。
1. 打印路径确认 :在工具函数开头临时添加 print(f”Searching in: {NOTES_DIR}”) ,查看实际路径。
2. 指定文件编码 :在 read_text() 中明确指定 encoding=’utf-8’ ,并用 try...except 捕获 UnicodeDecodeError
3. 检查搜索逻辑 :确认你的 query 和文件内容是否真的匹配。可以先实现一个 list_notes 工具,确认AI能“看到”哪些文件。
工具响应速度非常慢。 1. 遍历的文件数量过多。
2. 单个文件非常大。
3. 工具内部有耗时的网络或计算操作。
1. 限制搜索范围 :如只搜索特定子目录,或使用索引(见5.1节)。
2. 分块读取大文件 :对于大文件,不要一次性 read_text() ,可以逐行读取或在固定位置搜索。
3. 考虑异步操作 :如果操作确实耗时,可以考虑让工具快速返回一个“任务已提交”的消息,然后通过其他方式(如另一个工具查询结果)来获取最终数据。但这超出了基础MCP模式。
我想让工具返回更丰富的内容(如图片、复杂表格),而不仅仅是文本。 MCP协议支持多种返回类型,但Claude等客户端主要处理文本。 目前,最通用的方式仍然是返回格式化的 Markdown文本 。Claude能很好地渲染Markdown表格、列表、代码块。你可以将复杂数据(如字典列表)转换为Markdown表格字符串返回。例如: ”| 列A | 列B |\n|—|—|\n| 数据1 | 数据2 |”

5.4 安全与隐私的再强调

构建本地MCP服务器的最大优势就是隐私。但这也意味着你需要对自己的代码负责。

  • 小心文件操作 :避免编写可以任意写入或删除文件的工具,除非你完全清楚后果。 read_note 工具中的路径解析和检查是一个好的范例。
  • 谨慎执行命令 :通过 subprocess 调用系统命令是强大的,也是危险的。永远不要直接将未经处理的用户输入传递给 shell=True subprocess.run()
  • 隔离环境 :使用虚拟环境( venv )来管理依赖,避免污染系统Python环境,也便于在不同项目间切换。

6. 从本地到远程:何时需要考虑部署

本地服务器完美契合个人单机使用场景。但在以下情况,你可能需要考虑将其部署为远程服务器:

  1. 团队共享 :你想让团队的其他成员也能使用这套工具。
  2. 跨设备访问 :你希望在办公室的台式机、家里的笔记本和公司的服务器上都能调用同一套工具。
  3. 与自动化流程集成 :你希望其他自动化脚本或系统(如CI/CD流水线)也能通过MCP调用这些工具。
  4. 资源集中 :工具需要访问只有某台中央服务器才能访问的资源(如内网数据库、特定硬件)。

如何部署? fastmcp 本身就是一个标准的Python HTTP服务器。你可以:

  • 将你的脚本改造成一个使用 uvicorn hypercorn 等ASGI服务器运行的FastAPI风格应用( fastmcp 基于 httpx sse-starlette ,兼容ASGI)。
  • 在服务器上运行它,并配置Claude Desktop通过 ssh 隧道或安全的网络地址来连接(在配置文件中,将 command 改为 ”ssh” ,并设置相应的 args 连接到远程主机执行命令,或者使用MCP over SSE/HTTP的方式)。

不过,那将是另一个话题了。对于绝大多数个人效率场景,一个精心设计的本地服务器,已经足以将你的工作流智能化程度提升好几个档次。

Logo

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

更多推荐