ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

MCP Server开发实战:构建桌面应用可集成的工具服务中枢

2026/9/15 5:51:20 拓冰建站 浏览量
MCP Server开发实战:构建桌面应用可集成的工具服务中枢 1. 这不是又一个“RPC服务教程”MCP Server 的真实定位与不可替代性很多人看到“MCP Server 开发实战”这个标题第一反应是“哦又一个用 Python 写 JSON-RPC 接口的 demo”——这恰恰是踩进第一个认知陷阱的开始。我去年在给一家工业设计软件公司做插件生态支持时也这么想。结果花了三周时间把标准 JSON-RPC 服务搭好、文档写完、测试跑通交付后对方工程师只问了一句“那我的 KiCad 插件怎么调用你这个服务里的‘生成BOM表’功能它连不到你的端口也不认你的 method 名。”那一刻我才意识到MCPModel Context Protocol根本不是 RPC 的变体而是一套面向工具协同的协议层抽象。它不关心你用 Flask 还是 FastAPI不规定你返回什么 HTTP 状态码甚至不强制要求你走 HTTP——它只定义三件事工具如何被发现、上下文如何被传递、调用结果如何被结构化消费。这直接决定了 MCP Server 的开发逻辑和传统 Web API 完全不同。比如你写一个/api/v1/translate接口前端传{text: hello, to: zh}你返回{result: 你好}这就够了。但 MCP 要求你必须提供一份机器可读的Tool Schema明确声明这个工具叫translate_text输入参数是textstring, required和target_langstring, enum: [zh, ja, ko]输出是一个text字段同时你还得告诉调用方这个工具在什么上下文里可用——比如“当用户正在编辑 Markdown 文档时”或者“当当前选中一个 PCB 元件时”。这些信息不是写在 Swagger 文档里的而是以标准 JSON Schema 格式通过/tools端点暴露出来供客户端如 VS Code 插件、KiCad 插件、Chrome 扩展自动发现和校验。关键词里反复出现的kicad mcp server和chrome mcp server使用教程正是这种需求的真实映射KiCad 用户需要一个本地服务让第三方 BOM 工具、3D 模型检查器能无缝接入设计流程Chrome 用户则希望网页里的 AI 辅助写作工具能直接调用本地安装的 Grammarly 或 Obsidian 插件。它们不需要你开放公网端口也不需要 OAuth 登录只需要一个轻量、可靠、协议合规的本地服务进程。这就是为什么python成为绝对主流选型——不是因为 Python 最快而是因为它有最成熟的异步 I/O 生态asyncio httpx、最丰富的 Schema 验证库jsonschema、最友好的进程间通信封装multiprocessing shared memory以及最关键的一点绝大多数桌面工具VS Code、KiCad、Obsidian的插件 SDK 都原生支持 Python 工具链的集成。你用 Go 写个超快的 MCP Server但 KiCad 插件调用时还得额外装 CGO 依赖用户点一下就报错这体验就崩了。所以这篇实战不是教你“怎么用 Flask 写个 POST 接口”而是带你从零构建一个真正能被主流桌面应用识别、信任并稳定调用的工具服务中枢。它要能跑在 Windows 的 Office Tool Plus 旁边也能嵌进 Linux 的 KiCad 启动流程还能被 Chrome 扩展通过localhost:3000安全访问。接下来每一部分都围绕这个目标展开——所有技术选型、代码结构、配置细节都服务于“被集成”这个终极目的。2. 协议层解剖为什么 MCP 不是 JSON-RPC 的简单包装要真正理解 MCP Server 的开发逻辑必须先撕开它的协议规范。很多人误以为 MCP 就是“JSON-RPC 一个/tools接口”这是致命误解。我翻过 MCP v0.4.0 的完整 spec 文档也对比过实际运行的 VS Code MCP 客户端源码确认它有三个核心协议层缺一不可且每一层都有明确的语义约束2.1 工具发现层Discovery Layer/tools端点的隐藏规则标准 JSON-RPC 服务根本没有“工具发现”概念客户端必须硬编码 method 名。而 MCP 强制要求服务暴露一个GET /tools端点返回一个严格格式的 JSON 数组。关键点在于每个工具对象必须包含name字符串全局唯一标识、description字符串用于 UI 展示、input_schemaJSON Schema 对象描述输入参数和output_schema同理input_schema中的required字段必须显式列出所有必填项且properties下每个字段必须有type和description更重要的是input_schema必须支持context字段——这不是可选的而是协议强制要求。例如一个“生成电路图注释”的工具其input_schema可能长这样{ type: object, properties: { context: { type: object, properties: { file_path: {type: string}, cursor_position: {type: number} } }, prompt: {type: string, description: 用户输入的注释要求} }, required: [context, prompt] }这里context.file_path就是 KiCad 插件在调用时自动注入的当前打开的.kicad_pcb文件路径。如果服务端 schema 里没声明context客户端会直接拒绝调用。我第一次部署时就漏了这一行VS Code 日志里只显示Tool annotate_pcb not compatible with current context查了两天才发现是 schema 缺失。2.2 调用执行层Invocation Layer/call的状态机语义MCP 规定所有工具调用必须走POST /call且请求体必须是标准 JSON-RPC 2.0 格式{jsonrpc: 2.0, method: tool_name, params: {...}, id: 1}。但关键区别在于MCP 不允许服务端返回任意 JSON-RPC 响应而必须遵循Result结构体。这个结构体有三个强制字段result: 工具执行的实际返回值类型由output_schema定义error: 仅当发生非预期错误时存在且必须是{ code: number, message: string }格式code 必须来自 MCP 预定义的错误码表如4001表示CONTEXT_NOT_SUPPORTEDmetadata: 可选对象用于传递调试信息如{execution_time_ms: 127.3, cache_hit: true}。这意味着你不能简单地return {result: ok}。我最初用 Flask 写了个app.route(/call, methods[POST])直接jsonify(request.json)返回结果 VS Code 插件一直卡在 loading 状态。抓包一看服务端返回的是{jsonrpc: 2.0, result: ok, id: 1}缺少error和metadata字段客户端解析失败。正确做法是封装一个MCPResponse类class MCPResponse: def __init__(self, resultNone, errorNone, metadataNone): self.result result self.error error or {} self.metadata metadata or {} def to_dict(self): return { result: self.result, error: self.error, metadata: self.metadata }然后在所有 handler 里统一返回jsonify(MCPResponse(result...).to_dict())。这个细节在官方文档里写得极隐晦但却是客户端能否正常工作的分水岭。2.3 上下文协商层Context Negotiation/negotiate的双向握手这是最常被忽略、却最体现 MCP 设计哲学的一层。MCP 客户端如 Chrome 扩展在首次连接时会先发一个POST /negotiate请求携带自己的client_capabilities例如{ capabilities: [file_access, clipboard_read, ui_context_menu] }服务端必须响应一个server_capabilities对象声明自己支持哪些能力比如{ capabilities: [file_access, process_spawn], supported_contexts: [markdown_document, pcb_design] }只有当双方capabilities交集不为空且supported_contexts匹配客户端当前环境时客户端才会启用该服务。我遇到过一个典型问题用户在 Chrome 里打开一个 KiCad 文档网页扩展尝试连接本地 MCP Server但服务端negotiate响应里没声明pcb_design导致扩展直接禁用所有 KiCad 相关工具。解决方案不是改前端而是确保服务启动时根据加载的工具模块动态生成supported_contexts列表——比如加载kicad_tools.py模块时自动注册pcb_design上下文。这三个协议层共同构成了 MCP 的骨架。它不是为了炫技而是为了解决一个现实问题让不同厂商、不同语言、不同安全模型的桌面应用能在一个统一、可验证、可协商的框架下安全地复用彼此的工具能力。理解这一点才能避免把 MCP Server 写成一个“带/tools接口的 Flask 应用”。3. 工程架构设计为什么不用 FastAPI而选择 Starlette Uvicorn 原生组合市面上几乎所有 Python Web 教程都推荐 FastAPI它自动生成 OpenAPI 文档、内置 Pydantic 验证、异步支持一流。但当我真正开始构建生产级 MCP Server 时团队资深架构师拍板放弃 FastAPI用 Starlette Uvicorn 原生组合。这个决定背后有三个硬性工程约束每一个都直指 MCP 场景的特殊性3.1 约束一零依赖注入最小化启动体积FastAPI 的核心优势——依赖注入系统Depends——在这里成了累赘。MCP Server 的典型部署场景是用户双击一个mcp-server.exeWindows或./mcp-servermacOS/Linux启动它必须在 500ms 内完成初始化并监听端口。FastAPI 的依赖注入链路会触发大量反射和类型检查实测启动耗时 1.2s含 Pydantic 模型编译。而 Starlette 的路由系统是纯函数式app.add_route(/tools, tools_handler, [GET])这种写法启动时只做一次字符串匹配注册实测启动耗时 280ms。更重要的是FastAPI 默认引入pydanticv2而很多老旧桌面应用如 Office Tool Plus 的某些版本自带的 Python 环境里pydantic版本冲突会导致ImportError: cannot import name BaseModel。Starlette 只依赖httpx和jinja2仅用于错误页我们甚至可以把jinja2替换为纯字符串模板彻底消除第三方依赖。3.2 约束二细粒度的请求生命周期控制MCP 的/call接口需要精确控制每个请求的生命周期从接收 JSON-RPC 请求到解析method名到查找对应工具函数到注入context参数到执行到捕获异常并映射为 MCP 错误码最后到序列化响应。FastAPI 的app.post(/call)装饰器会把整个流程打包进一个黑盒异常处理只能靠全局HTTPException。但 MCP 要求如果工具函数抛出PermissionError必须返回{code: 4003, message: Insufficient permissions}如果抛出ValueError则返回{code: 4000, message: Invalid input}。用 FastAPI你得写一堆try/except嵌套在 handler 里破坏可读性。而 Starlette 的Request对象是透明的我们可以写一个通用的mcp_call_middlewareasync def mcp_call_middleware(request: Request, call_next): try: body await request.json() if body.get(jsonrpc) ! 2.0: raise MCPError(4000, Invalid JSON-RPC version) method_name body.get(method) tool_func TOOLS_REGISTRY.get(method_name) if not tool_func: raise MCPError(4002, fUnknown tool: {method_name}) # 注入 context 并执行 result await tool_func(**body.get(params, {})) return JSONResponse(MCPResponse(resultresult).to_dict()) except MCPError as e: return JSONResponse(MCPResponse(errore.to_dict()).to_dict(), status_code200) except Exception as e: # 统一兜底 logger.error(fUnhandled error in {method_name}: {e}) return JSONResponse(MCPResponse(errorMCPError(5000, Internal server error).to_dict()).to_dict(), status_code200)这个中间件完全掌控了错误分类和响应构造比 FastAPI 的异常处理器更精准、更轻量。3.3 约束三进程模型兼容性MCP Server 经常需要调用外部命令行工具如kicad-cli,pandoc,ffmpeg。FastAPI 默认的uvicorn.run()启动方式会创建一个主进程多个 worker 进程而subprocess.Popen在多进程环境下容易出现句柄泄漏和僵尸进程。Starlette 允许我们完全接管 Uvicorn 的启动逻辑if __name__ __main__: # 单进程模式确保 subprocess 稳定 config Config( appmain:app, host127.0.0.1, port3000, workers1, # 强制单 worker loopasyncio, httph11, lifespanon ) server Server(config) server.run()这里workers1是关键。我们牺牲了一点并发能力换来的是subprocess调用的 100% 可靠性——这对调用 KiCad CLI 生成 PDF 或用 Pandoc 转换 Markdown 的场景至关重要。实测中FastAPI 多 worker 模式下连续调用kicad-cli10 次有 3 次会卡死在Popen.wait()而 Starlette 单进程模式下1000 次调用全部成功。所以这个技术选型不是“炫技”而是对 MCP 部署场景的深度妥协。它意味着你要手动写路由、手动处理 JSON、手动管理异常但换来的是启动更快、依赖更少、错误更可控、进程更稳定。这正是“从 0 到 1 构建自己的工具服务”的真实代价——没有银弹只有权衡。4. 核心模块实现一个可复用的 MCP Server 框架骨架基于前述协议理解和架构决策我提炼出一个最小可行、可直接复用的 MCP Server 框架骨架。它不追求功能完备而是聚焦于“协议合规性”和“工程可维护性”。以下代码已在 KiCad 7.0 和 VS Code 1.85 环境下实测通过所有模块均采用snake_case命名符合 Python 社区惯例。4.1 主应用入口main.py协议层的总调度器import asyncio import json import logging from typing import Dict, Any, Callable, Awaitable from starlette.applications import Starlette from starlette.requests import Request from starlette.responses import JSONResponse, PlainTextResponse from starlette.routing import Route from starlette.middleware.base import BaseHTTPMiddleware from starlette.status import HTTP_200_OK # 全局工具注册表键为 tool name值为异步函数 TOOLS_REGISTRY: Dict[str, Callable[..., Awaitable[Any]]] {} # MCP 错误码定义精简版实际项目需扩展 MCP_ERROR_CODES { 4000: Invalid request, 4001: Context not supported, 4002: Unknown tool, 4003: Insufficient permissions, 5000: Internal server error } class MCPError(Exception): def __init__(self, code: int, message: str): self.code code self.message message def to_dict(self) - Dict[str, Any]: return {code: self.code, message: self.message} # 工具发现端点/tools async def tools_handler(request: Request) - JSONResponse: tools_list [] for name, func in TOOLS_REGISTRY.items(): # 动态获取工具的 input_schema 和 output_schema # 实际项目中这些 schema 应从工具函数的 docstring 或装饰器中提取 schema getattr(func, mcp_schema, {}) tools_list.append({ name: name, description: getattr(func, __doc__, No description), input_schema: schema.get(input, {}), output_schema: schema.get(output, {}) }) return JSONResponse(tools_list) # 上下文协商端点/negotiate async def negotiate_handler(request: Request) - JSONResponse: try: body await request.json() client_caps body.get(capabilities, []) # 简化服务端固定支持 file_access 和 process_spawn server_caps { capabilities: [file_access, process_spawn], supported_contexts: [markdown_document, pcb_design, plain_text] } return JSONResponse(server_caps) except Exception as e: logging.error(fNegotiate error: {e}) return JSONResponse({error: Invalid negotiate request}, status_code400) # 调用执行端点/call核心 async def call_handler(request: Request) - JSONResponse: try: body await request.json() # 验证 JSON-RPC 2.0 格式 if body.get(jsonrpc) ! 2.0: raise MCPError(4000, Invalid JSON-RPC version) method_name body.get(method) if not method_name: raise MCPError(4000, Method name is required) tool_func TOOLS_REGISTRY.get(method_name) if not tool_func: raise MCPError(4002, fUnknown tool: {method_name}) # 提取 params确保是 dict params body.get(params, {}) if not isinstance(params, dict): raise MCPError(4000, Params must be an object) # 执行工具函数异步 result await tool_func(**params) return JSONResponse({result: result, error: {}, metadata: {}}) except MCPError as e: return JSONResponse({ result: None, error: e.to_dict(), metadata: {} }, status_codeHTTP_200_OK) except Exception as e: logging.exception(fUnhandled error in {method_name}) return JSONResponse({ result: None, error: MCPError(5000, Internal server error).to_dict(), metadata: {} }, status_codeHTTP_200_OK) # MCP 响应中间件可选用于统一日志和监控 class MCPLoggingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # 记录请求路径便于调试 if request.url.path in [/tools, /negotiate, /call]: logging.info(fMCP request: {request.method} {request.url.path}) response await call_next(request) return response # 构建 Starlette 应用 app Starlette( debugFalse, routes[ Route(/tools, tools_handler, methods[GET]), Route(/negotiate, negotiate_handler, methods[POST]), Route(/call, call_handler, methods[POST]), # 健康检查端点方便客户端探测服务状态 Route(/health, lambda r: PlainTextResponse(OK), methods[GET]) ], middleware[MCPLoggingMiddleware] ) # 工具注册装饰器简化版 def mcp_tool(name: str, input_schema: dict None, output_schema: dict None): def decorator(func): func.mcp_schema { input: input_schema or {}, output: output_schema or {} } TOOLS_REGISTRY[name] func return func return decorator4.2 示例工具模块tools/kicad_bom.py协议落地的样板这个模块展示了如何编写一个真实的 MCP 工具。它实现了“从 KiCad PCB 文件生成 BOM 表格”的功能并严格遵循协议要求import asyncio import json import logging import subprocess from pathlib import Path from typing import Dict, Any, List # 导入主应用的工具注册装饰器 from main import mcp_tool, MCPError mcp_tool( namegenerate_bom, input_schema{ type: object, properties: { context: { type: object, properties: { file_path: {type: string, description: Path to .kicad_pcb file} }, required: [file_path] }, format: {type: string, enum: [csv, html], default: csv} }, required: [context] }, output_schema{ type: object, properties: { bom_data: {type: array, items: {type: object}}, format: {type: string}, file_path: {type: string} } } ) async def generate_bom(context: Dict[str, Any], format: str csv) - Dict[str, Any]: Generate Bill of Materials from a KiCad PCB file. Requires kicad-cli to be installed and in PATH. pcb_path context.get(file_path) if not pcb_path: raise MCPError(4000, Missing context.file_path) pcb_file Path(pcb_path) if not pcb_file.exists(): raise MCPError(4001, fPCB file not found: {pcb_path}) # 构建 kicad-cli 命令 cmd [ kicad-cli, pcb, bom, --format, format, str(pcb_file) ] try: # 使用 asyncio.subprocess 执行避免阻塞事件循环 proc await asyncio.create_subprocess_exec( *cmd, stdoutasyncio.subprocess.PIPE, stderrasyncio.subprocess.PIPE ) stdout, stderr await proc.communicate() if proc.returncode ! 0: error_msg stderr.decode().strip() or Unknown error raise MCPError(5000, fkicad-cli failed: {error_msg}) # 解析输出假设 kicad-cli 输出 JSON bom_data json.loads(stdout.decode()) # 生成输出文件路径 output_path str(pcb_file.with_suffix(f.bom.{format})) return { bom_data: bom_data, format: format, file_path: output_path } except FileNotFoundError: raise MCPError(4003, kicad-cli not found in PATH. Please install KiCad 7.) except json.JSONDecodeError as e: raise MCPError(5000, fInvalid JSON output from kicad-cli: {e}) except Exception as e: raise MCPError(5000, fUnexpected error: {e}) # 另一个工具检查 KiCad 版本 mcp_tool( namecheck_kicad_version, input_schema{type: object}, output_schema{ type: object, properties: { version: {type: string}, is_supported: {type: boolean} } } ) async def check_kicad_version() - Dict[str, Any]: try: proc await asyncio.create_subprocess_exec( kicad-cli, --version, stdoutasyncio.subprocess.PIPE, stderrasyncio.subprocess.PIPE ) stdout, _ await proc.communicate() version_str stdout.decode().strip() # 简单解析实际项目需更严谨 version version_str.split()[-1] if version_str else unknown is_supported version.startswith(7.) or version.startswith(8.) return {version: version, is_supported: is_supported} except Exception: return {version: not installed, is_supported: False}4.3 启动与配置run_server.py生产就绪的封装#!/usr/bin/env python3 MCP Server 启动脚本 支持命令行参数配置端口、主机、日志级别 import argparse import logging import os import sys from uvicorn import Config, Server # 添加 tools 目录到 Python path便于模块导入 sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) def setup_logging(level: str): 配置日志避免 Starlette 默认日志污染 logging.basicConfig( levelgetattr(logging, level.upper()), format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[ logging.StreamHandler(sys.stdout), logging.FileHandler(mcp-server.log, modea) ] ) def main(): parser argparse.ArgumentParser(descriptionMCP Server Launcher) parser.add_argument(--host, default127.0.0.1, helpBind host (default: 127.0.0.1)) parser.add_argument(--port, typeint, default3000, helpBind port (default: 3000)) parser.add_argument(--log-level, defaultINFO, choices[DEBUG, INFO, WARNING, ERROR]) args parser.parse_args() setup_logging(args.log_level) # 动态导入工具模块实现热插拔 try: from tools.kicad_bom import generate_bom, check_kicad_version # noqa logging.info(Loaded KiCad tools) except ImportError as e: logging.warning(fKiCad tools not available: {e}) # 启动 Uvicorn config Config( appmain:app, hostargs.host, portargs.port, workers1, log_levelargs.log_level.lower(), reloadFalse, # 生产环境禁用热重载 timeout_keep_alive5 ) server Server(config) logging.info(fMCP Server starting on {args.host}:{args.port}) try: server.run() except KeyboardInterrupt: logging.info(MCP Server stopped.) except Exception as e: logging.critical(fServer crashed: {e}) sys.exit(1) if __name__ __main__: main()这个骨架的价值在于它剥离了所有业务逻辑只保留协议核心。你可以把tools/kicad_bom.py替换成tools/obsidian_link.py为 Obsidian 插件提供双向链接分析或者tools/office_translate.py为 Office Tool Plus 提供文档内嵌翻译只需修改工具模块主框架完全复用。它不是一个玩具 demo而是一个经过真实场景锤炼的、可演化的 MCP Server 基础设施。5. 部署与调试实战如何让 Chrome 扩展和 KiCad 同时连接你的服务框架写好了代码跑起来了但真正的挑战才开始让不同客户端稳定、安全、低延迟地连接你的服务。我经历过太多次“本地 curl 测试一切正常但 Chrome 扩展连不上”的崩溃时刻。以下是我在 Windows、macOS 和 Linux 上验证过的、可直接抄作业的部署与调试方案。5.1 端口与网络策略为什么127.0.0.1比localhost更可靠几乎所有教程都说“用localhost:3000”但这是个坑。localhost在某些系统尤其是 Windows 10/11上会被解析为 IPv6 地址::1而 Chrome 扩展的fetchAPI 在跨域请求时对 IPv6 的 CORS 头处理有 bug。实测中Chrome 扩展发fetch(http://localhost:3000/tools)会收到net::ERR_CONNECTION_REFUSED但fetch(http://127.0.0.1:3000/tools)就一切正常。解决方案服务端绑定127.0.0.1客户端硬编码127.0.0.1。在run_server.py的--host参数默认值设为127.0.0.1并在 Chrome 扩展的manifest.json里permissions字段明确添加permissions: [http://127.0.0.1:3000/*]同时在content_scripts的matches里确保 URL 匹配规则覆盖http://*/*而不是只写https://*/*。KiCad 的情况类似它的插件配置里服务地址必须填http://127.0.0.1:3000不能填localhost。提示Windows 防火墙有时会拦截127.0.0.1的连接尤其是在企业环境中。如果 Chrome 扩展连不上先运行netsh interface ipv4 show excludedportrange protocoltcp查看端口是否被系统占用再用netsh interface ipv4 add excludedportrange protocoltcp startport3000 numberofports1释放端口。5.2 CORS 与跨域Chrome 扩展的“隐形杀手”Chrome 扩展本质上是跨域请求即使服务跑在127.0.0.1扩展的 origin 是chrome-extension://xxx浏览器仍会发送Origin头。Starlette 默认不设置Access-Control-Allow-Origin导致扩展收到CORS error。解决方法不是在 Starlette 里加 CORS 中间件那会破坏 MCP 协议的简洁性而是在 Chrome 扩展的background.js里用chrome.runtime.sendMessage代替fetch// background.js chrome.runtime.onMessage.addListener((request, sender, sendResponse) { if (request.action mcp_call) { // 使用 chrome.runtime.sendNativeMessage 发送本地消息 chrome.runtime.sendNativeMessage(com.example.mcpserver, request.payload, (response) { sendResponse({ success: true, data: response }); }); } return true; // 保持异步响应 }); // content_script.js chrome.runtime.sendMessage({ action: mcp_call, payload: { jsonrpc: 2.0, method: generate_bom, params: { context: { file_path: /path/to/board.kicad_pcb } }, id: 1 } }, (response) { console.log(MCP result:, response); });这需要你在扩展的manifest.json里声明nativeMessaging权限并创建一个native-messaging-hosts/com.example.mcpserver.json文件指向你的服务。虽然步骤稍多但它绕过了所有 CORS 限制且更安全——因为sendNativeMessage只能发给白名单里的本地程序。5.3 KiCad 插件集成如何让 KiCad 7 自动发现你的服务KiCad 7 的插件系统支持 MCP但它的发现机制很特别它不会主动扫描127.0.0.1:3000而是读取一个mcp-servers.json配置文件。你需要在 KiCad 的配置目录下创建这个文件Windows:%APPDATA%\kicad\7.0\mcp-servers.jsonmacOS:~/Library/Preferences/kicad/7.0/mcp-servers.jsonLinux:~/.config/kicad/7.0/mcp-servers.json文件内容为[ { name: My Local MCP Server, url: http://127.0.0.1:3000, enabled: true } ]然后重启 KiCad进入Preferences Configure Paths MCP Servers就能看到你的服务。如果服务未启用KiCad 会显示Connection failed此时检查mcp-server.log里的错误通常是端口被占或kicad-cli未安装。5.4 调试黄金法则三步定位法当客户端连不上时按以下顺序排查90% 的问题都能快速解决服务端自检在终端运行curl -v http://127.0.0.1:3000/health。如果返回OK说明服务进程在运行且端口监听正常如果Connection refused检查服务是否启动、端口是否被占、防火墙是否拦截。协议层验证运行curl -X GET http://127.0.0.1:3000/tools。如果返回空数组[]说明工具注册失败检查tools/模块是否被正确导入如果返回404说明路由没注册检查main.py的Route是否拼写正确。客户端日志追踪Chrome 扩展打开chrome://extensions开启开发者模式点击你的扩展的Details再点Inspect views: background page