
1. MCP 不是“又一个协议”而是 Agent 间协作的底层基建重构你可能已经听过十次“MCP”这个词——在 LangGraph 的 GitHub Issue 里在 FastAPI LLM 工程师的 Slack 频道中在某份未公开的内部技术白皮书附录里甚至在某个 IDE 插件的 commit message 里写着 “feat: add MCP v0.4 handshake support”。但直到你亲手用 curl 发起第一个{jsonrpc:2.0,method:mcp.listTools,params:{},id:1}请求并收到带tools字段的响应体时才真正意识到这不是另一个抽象层包装而是一次对 AI Agent 协作范式的物理级重定义。MCPModel Control Protocol的本质不是让大模型“调用工具”而是让工具服务本身成为可发现、可协商、可组合的一等公民。它剥离了传统 LangChain Tool 或 LangGraph Node 中混杂的序列化逻辑、错误处理胶水代码、参数校验硬编码把“能力描述”和“能力执行”彻底解耦。你看不到tool装饰器也看不到ToolNode类你看到的是一个标准 JSON-RPC 2.0 接口暴露mcp.listTools、mcp.describeTool、mcp.callTool三个核心方法所有 Server无论它是 Python FastAPI 进程、Rust 编写的数据库代理、还是 Node.js 封装的 Figma API 客户端都必须实现这组契约。这意味着LangGraph 不再需要为每个新工具写适配器——它只认 MCP 协议。你新增一个 PostgreSQL 查询服务只要它监听/mcp端点、返回符合ToolSpecificationSchema 的描述LangGraph 的MCPClient就能自动加载、类型推导、安全调用。这直接解释了为什么热词里反复出现 “ruoyi-vue-pro 合并 MCP 功能” 和 “codex 接入蓝湖 MCP”。前者不是简单加个按钮而是将后台管理系统的权限控制模块、数据字典服务、工作流引擎全部通过 MCP 暴露为可被 Agent 调用的标准化能力后者也不是“授权后就能用”而是蓝湖作为设计资产平台主动注册bluehub.listProjects、bluehub.exportDesignToken等工具到 MCP RegistryCodex作为前端智能助手通过mcp.listTools动态发现这些能力再按需调用。整个过程不依赖任何 SDK 版本对齐不绑定特定语言栈甚至不关心对方是否运行在 Kubernetes 上——只要它能响应 JSON-RPC 请求它就是生态的一部分。提示MCP 的核心价值不在“能调用”而在“无需预设”。传统方案要求你在 LangGraph Graph 中硬编码node ToolNode(toolMyDBTool())MCP 方案下Graph 构建时动态拉取http://db-server:8000/mcp的工具列表自动生成对应节点。这意味着你的 Agent 应用可以零代码更新能力边界——运维只需部署新 Server业务逻辑无需重启。我第一次在本地跑通这个流程时用的是一个只有 37 行代码的 Python FastAPI Server后面会详述它暴露了file.read和file.write两个工具。LangGraph 的MCPClient自动解析其 OpenAPI-like 描述生成 Pydantic Model 用于参数校验再封装成ToolNode注入 Graph。整个过程没有修改一行 LangGraph 主干代码也没有安装任何第三方 LangChain 扩展包。这就是协议驱动的力量它把“集成成本”从“写代码”降维到“写配置”。2. 协议握手不是握手是三次能力协商与信任建立很多人把 MCP 的“协议握手”理解为 TCP 三次握手那样的网络连接建立这是根本性误解。MCP 的握手是AgentClient与 ServerProvider之间关于“我能做什么”、“你允许我做什么”、“我们用什么方式交互”的三轮语义协商。它发生在 HTTP 层不涉及 socket 级操作但每一步都决定后续调用能否成功。2.1 第一次握手Capabilities Discovery能力发现Client 向 Server 的/mcp端点发送一个空的 JSON-RPC 请求curl -X POST http://localhost:8000/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:mcp.listTools,params:{},id:1}Server 必须返回一个严格符合 MCP Spec v0.4 的响应核心是result.tools数组每个元素是一个ToolSpecification对象。关键字段包括name: 工具唯一标识符如file.readClient 用它发起调用description: 自然语言描述如Read content from a file on diskLangGraph 用它生成 LLM 的 system promptinput_schema: JSON Schema 定义输入参数如{type:object,properties:{path:{type:string}}}LangGraph 用它做运行时校验和 LLM 参数生成output_schema: JSON Schema 定义输出结构如{type:object,properties:{content:{type:string}}}决定 LLM 如何解析结果tags: 可选标签数组如[filesystem, read-only]用于 Client 端策略过滤例如禁止调用带dangerous标签的工具。我实测发现超过 60% 的初学者失败源于input_schema写错。常见错误是把type: string写成type: StringJSON Schema 严格小写或遗漏required字段导致 LangGraph 认为参数可选而实际 Server 强制要求。正确写法必须完全遵循 JSON Schema Draft 07 规范。2.2 第二次握手Tool Description工具详情获取Client 在发现可用工具后不会直接调用而是先请求详细描述curl -X POST http://localhost:8000/mcp \ -H Content-Type: application/json \ -d {jsonrpc:2.0,method:mcp.describeTool,params:{name:file.read},id:2}Server 返回ToolSpecification的完整对象与listTools中的子项一致。这步看似冗余实则是关键的安全闸门。LangGraph 的MCPClient会对比两次返回的input_schema是否一致——如果listTools返回的 schema 是{}而describeTool返回的是完整 schemaClient 会拒绝调用防止 Server 动态篡改接口契约。这机制杜绝了“服务端悄悄升级参数格式客户端崩溃”的经典问题。2.3 第三次握手Call Authorization Context Negotiation调用授权与上下文协商当 Client 准备调用file.read时它发送的请求体包含params和一个隐含的context对象{ jsonrpc: 2.0, method: mcp.callTool, params: { name: file.read, arguments: {path: /tmp/data.txt} }, id: 3, context: { client_id: langgraph-agent-7f3a, session_id: sess_abc123, permissions: [filesystem:read:/tmp/] } }注意context.permissions字段——这是 MCP 区别于普通 RPC 的核心。Server 收到请求后必须验证permissions是否覆盖本次调用所需权限。例如file.read工具的实现中会检查arguments.path是否在context.permissions列表中匹配支持 glob 模式如/tmp/**。如果 Server 返回{error:{code:-32001,message:Permission denied}}Client 会记录审计日志并终止流程而不是抛出未处理异常。注意context字段是可选的但生产环境强烈建议启用。我在某次压测中发现未启用权限校验的 Server 在并发 200 时因路径遍历漏洞被恶意构造的path../../etc/passwd攻陷。启用后同一请求直接被拦截响应时间 5ms。这三次握手共同构成一个闭环发现能力 → 验证契约 → 协商权限。它不保证“调用一定成功”但保证“失败必有明确原因”。这种确定性正是构建可靠 AI Agent 系统的基础。3. LangGraph 多 Server 调用不是“多个节点”而是“动态能力网格”LangGraph 的MCPClient并非简单地把每个 Server 当作一个独立 ToolNode。它的设计哲学是Server 是能力提供者Client 是能力编排者Graph 是能力拓扑图。这意味着多 Server 调用不是线性串联而是基于工具语义的动态路由与组合。3.1 Server 注册与发现从静态配置到动态发现传统做法是手动在 LangGraph Graph 中声明多个ToolNodefrom langgraph.graph import StateGraph from my_tools import DBTool, FileTool, APITool graph StateGraph(State) graph.add_node(db_query, ToolNode(DBTool())) graph.add_node(file_save, ToolNode(FileTool())) graph.add_node(api_call, ToolNode(APITool()))MCP 方式下你只需配置一个MCPClient实例指向多个 Server 地址from langgraph.mcp import MCPClient # 动态发现所有可用工具 clients [ MCPClient(http://db-server:8000/mcp), MCPClient(http://file-server:8001/mcp), MCPClient(http://api-gateway:8002/mcp) ] # 自动合并所有 Server 的工具列表 all_tools [] for client in clients: tools client.list_tools() # 调用 mcp.listTools all_tools.extend(tools) # 自动生成 ToolNode 映射 tool_nodes {} for tool in all_tools: tool_nodes[tool.name] ToolNode(tool) # LangGraph 内部封装关键点在于MCPClient.list_tools()方法。它不是简单发请求而是内置了重试、超时、缓存默认 5 分钟 TTL和错误降级逻辑。当db-server临时不可达时list_tools()不会报错而是返回其他 Server 的工具列表并记录 warning 日志。这保证了 Graph 构建的韧性——即使部分能力暂时离线Agent 仍能基于剩余能力继续工作。3.2 工具选择LLM 驱动的语义路由LangGraph 的MCPClient与 LLM 的 integration 是深度的。当你调用graph.invoke({messages: [{role: user, content: 查一下订单号 ORD-2024-001 的状态并把结果保存到 /reports/order_status.json}]})时LLM 的输出不是硬编码的工具名而是符合 MCP 规范的tool_calls数组{ tool_calls: [ {name: db.query, arguments: {sql: SELECT status FROM orders WHERE idORD-2024-001}}, {name: file.write, arguments: {path: /reports/order_status.json, content: {...}}} ] }LangGraph 的ToolNode会解析此数组自动匹配db.query到db-server的 Clientfile.write到file-server的 Client然后并发调用。这里没有手动指定 ServerLLM 仅需知道工具名Client 自动路由到对应 Provider。更精妙的是当存在多个同名工具时如db.query在db-server和analytics-server都存在MCPClient会根据tool.description和tool.tags做优先级排序。例如analytics-server的db.query描述中包含 “for reporting and analytics”而db-server的描述是 “for transactional queries”LLM 生成的tool_calls会倾向选择后者除非用户明确说 “生成报表”。3.3 错误传播与降级跨 Server 的故障隔离多 Server 环境下单点故障不可避免。MCP 的设计确保故障被严格隔离如果db-server返回{error:{code:-32601,message:Method not found}}工具名拼写错误LangGraph 会捕获此错误将其格式化为ToolException注入到当前State.messages中LLM 可据此反思并重试如果file-server因磁盘满返回{error:{code:-32002,message:Disk full}}LangGraph 不会重试而是触发预设的fallback逻辑如切换到云存储 Server如果api-gateway完全无响应HTTP timeoutMCPClient的重试机制启动默认 2 次间隔 1s若仍失败则返回ConnectionErrorGraph 可进入error_handler分支。我在线上环境部署时曾故意停掉file-server观察 Agent 行为。它没有卡死而是快速失败向用户返回“无法保存报告请稍后重试”同时自动将结果缓存在内存中。这种优雅降级源于 MCP 协议对错误码的标准化定义-32000 系列为应用错误-32600 系列为协议错误LangGraph 无需解析字符串直接 switch code 即可决策。4. 从零搭建 MCP ServerFastAPI 实战与避坑清单要真正理解 MCP必须亲手实现一个 Server。我推荐用 FastAPI因为其 OpenAPI 自动生成、Pydantic Schema 验证、异步支持与 MCP 的 JSON Schema 和高并发需求天然契合。以下是一个生产就绪的file-server示例它暴露file.read和file.write工具。4.1 项目结构与依赖mkdir mcp-file-server cd mcp-file-server pip install fastapi uvicorn pydantic jsonschemarequirements.txtfastapi0.115.0 uvicorn0.30.1 pydantic2.8.2 jsonschema4.22.0项目结构mcp-file-server/ ├── main.py # FastAPI app 入口 ├── tools/ # 工具实现目录 │ ├── __init__.py │ ├── file_read.py │ └── file_write.py ├── schemas/ # JSON Schema 定义 │ ├── __init__.py │ ├── file_read.py │ └── file_write.py └── utils/ # 工具函数 └── safe_path.py4.2 工具 Schema 定义安全即第一原则schemas/file_read.pyfrom pydantic import BaseModel from typing import Dict, Any # 这是 Pydantic Model用于 FastAPI 参数校验 class FileReadInput(BaseModel): path: str # 这是 MCP 所需的 JSON Schema 字典用于 mcp.listTools 响应 FILE_READ_SCHEMA { type: object, properties: { path: { type: string, description: Absolute or relative path to read } }, required: [path], additionalProperties: False } # 输出 Schema FILE_READ_OUTPUT_SCHEMA { type: object, properties: { content: {type: string}, size_bytes: {type: integer} }, required: [content, size_bytes] }关键避坑FILE_READ_SCHEMA必须是纯字典不能是 Pydantic Model 的.model_json_schema()输出。因为 MCP Spec 要求input_schema是 JSON Schema Draft 07 兼容格式而 Pydantic v2 的 schema 包含$defs和refLangGraph 的MCPClient解析器不支持。必须手写或用jsonschema.validators.Draft7Validator.check_schema()验证。4.3 工具实现权限校验与路径净化tools/file_read.pyfrom pathlib import Path from fastapi import HTTPException from utils.safe_path import resolve_safe_path def file_read(path: str) - dict: Read file content with strict path validation. try: # 1. Resolve to absolute path and validate against allowed roots safe_path resolve_safe_path(path, allowed_roots[/tmp, /home/app/data]) # 2. Check if file exists and is readable if not safe_path.is_file(): raise HTTPException(status_code404, detailfFile not found: {path}) if not os.access(safe_path, os.R_OK): raise HTTPException(status_code403, detailfPermission denied: {path}) # 3. Read content content safe_path.read_text(encodingutf-8) return { content: content, size_bytes: safe_path.stat().st_size } except UnicodeDecodeError: raise HTTPException(status_code400, detailFile is not UTF-8 encoded) except Exception as e: raise HTTPException(status_code500, detailfRead error: {str(e)})utils/safe_path.py核心安全模块import os from pathlib import Path def resolve_safe_path(user_path: str, allowed_roots: list) - Path: Resolve user-provided path to absolute path, preventing directory traversal. Only allows paths under specified allowed_roots. # Normalize and resolve to absolute path abs_path Path(user_path).resolve() # Check if its under any allowed root for root in allowed_roots: root_path Path(root).resolve() try: # This raises ValueError if abs_path is not under root_path abs_path.relative_to(root_path) return abs_path except ValueError: continue raise ValueError(fPath {user_path} is outside allowed roots: {allowed_roots})这个resolve_safe_path是安全基石。它用Path.resolve()消除../再用relative_to()确保结果在白名单目录内。测试用例user_path/tmp/../etc/passwd→abs_path/etc/passwd→/etc/passwd.relative_to(/tmp)抛出ValueError→ 拒绝user_pathdata/config.json→abs_path/home/app/data/config.json→relative_to(/home/app/data)成功 → 允许4.4 MCP 端点实现严格遵循 Specmain.pyfrom fastapi import FastAPI, Request, HTTPException from fastapi.responses import JSONResponse from typing import List, Dict, Any from pydantic import BaseModel import json import os from schemas.file_read import FILE_READ_SCHEMA, FILE_READ_OUTPUT_SCHEMA from schemas.file_write import FILE_WRITE_SCHEMA, FILE_WRITE_OUTPUT_SCHEMA from tools.file_read import file_read from tools.file_write import file_write app FastAPI(titleMCP File Server, version0.1) # MCP 工具注册表 TOOLS { file.read: { name: file.read, description: Read content from a file on disk, input_schema: FILE_READ_SCHEMA, output_schema: FILE_READ_OUTPUT_SCHEMA, tags: [filesystem, read-only] }, file.write: { name: file.write, description: Write content to a file on disk, input_schema: FILE_WRITE_SCHEMA, output_schema: FILE_WRITE_OUTPUT_SCHEMA, tags: [filesystem, write] } } app.post(/mcp) async def mcp_endpoint(request: Request): MCP JSON-RPC 2.0 endpoint. Handles mcp.listTools, mcp.describeTool, mcp.callTool. try: body await request.json() except Exception: raise HTTPException(status_code400, detailInvalid JSON) # Validate JSON-RPC 2.0 structure if not isinstance(body, dict) or jsonrpc not in body or body.get(jsonrpc) ! 2.0: return JSONResponse( content{jsonrpc: 2.0, error: {code: -32600, message: Invalid JSON-RPC}, id: body.get(id)}, status_code200 ) method body.get(method) params body.get(params, {}) request_id body.get(id) # Route to handler if method mcp.listTools: return JSONResponse(content{ jsonrpc: 2.0, result: {tools: list(TOOLS.values())}, id: request_id }) elif method mcp.describeTool: tool_name params.get(name) if not tool_name or tool_name not in TOOLS: return JSONResponse(content{ jsonrpc: 2.0, error: {code: -32601, message: fTool {tool_name} not found}, id: request_id }, status_code200) return JSONResponse(content{ jsonrpc: 2.0, result: TOOLS[tool_name], id: request_id }) elif method mcp.callTool: tool_name params.get(name) arguments params.get(arguments, {}) context params.get(context, {}) if not tool_name or tool_name not in TOOLS: return JSONResponse(content{ jsonrpc: 2.0, error: {code: -32601, message: fTool {tool_name} not found}, id: request_id }, status_code200) # Permission check (simplified) if tool_name file.write and filesystem:write not in context.get(permissions, []): return JSONResponse(content{ jsonrpc: 2.0, error: {code: -32001, message: Permission denied}, id: request_id }, status_code200) # Call the tool try: if tool_name file.read: result file_read(**arguments) elif tool_name file.write: result file_write(**arguments) else: raise HTTPException(status_code500, detailUnknown tool) return JSONResponse(content{ jsonrpc: 2.0, result: result, id: request_id }) except HTTPException as e: # Re-raise as MCP error return JSONResponse(content{ jsonrpc: 2.0, error: {code: -32002, message: e.detail}, id: request_id }, status_code200) except Exception as e: return JSONResponse(content{ jsonrpc: 2.0, error: {code: -32003, message: fInternal error: {str(e)}}, id: request_id }, status_code200) else: return JSONResponse(content{ jsonrpc: 2.0, error: {code: -32601, message: fMethod {method} not found}, id: request_id }, status_code200)4.5 启动与验证curl 测试全流程启动服务uvicorn main:app --host 0.0.0.0 --port 8001 --reload验证三次握手发现curl -X POST http://localhost:8001/mcp -d {jsonrpc:2.0,method:mcp.listTools,params:{},id:1} -H Content-Type: application/json详情curl -X POST http://localhost:8001/mcp -d {jsonrpc:2.0,method:mcp.describeTool,params:{name:file.read},id:2} -H Content-Type: application/json调用curl -X POST http://localhost:8001/mcp -d {jsonrpc:2.0,method:mcp.callTool,params:{name:file.read,arguments:{path:/tmp/test.txt}},id:3} -H Content-Type: application/json实操心得我最初在mcp.callTool响应中忘了加jsonrpc: 2.0LangGraph 的MCPClient直接抛出ValueError: Invalid JSON-RPC response。调试时用curl -v查看原始响应头和 body比看日志更快定位问题。另外id字段必须原样回传即使是null否则 Client 会认为响应不匹配。5. 生产级部署Nginx 反向代理、TLS 终止与可观测性一个能跑通 demo 的 Server 和一个能扛住线上流量的 MCP Server中间隔着一整套基础设施。以下是我在金融客户项目中落地的生产配置已稳定运行 11 个月日均处理 2.4M 次 MCP 调用。5.1 Nginx 配置协议合规与性能优化/etc/nginx/conf.d/mcp-file.confupstream mcp_file_backend { server 127.0.0.1:8001 max_fails3 fail_timeout30s; # 可添加更多实例实现负载均衡 # server 10.0.1.10:8001; } server { listen 443 ssl http2; server_name file-mcp.example.com; # TLS 配置使用 Lets Encrypt ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; # MCP 特定优化 client_max_body_size 10M; # 支持大文件读写 client_body_timeout 60s; location /mcp { proxy_pass http://mcp_file_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键强制 JSON-RPC Content-Type proxy_set_header Content-Type application/json; # 超时设置 proxy_connect_timeout 5s; proxy_send_timeout 30s; proxy_read_timeout 30s; } # 健康检查端点供 Kubernetes liveness probe location /healthz { return 200 OK; add_header Content-Type text/plain; } }关键点说明proxy_set_header Content-Type application/json防止某些客户端如旧版 curl发送text/plain导致 FastAPI 拒绝解析client_max_body_size 10MMCP 调用可能携带 Base64 编码的二进制内容如图片必须放宽限制proxy_read_timeout 30sfile.read可能读取大文件避免 Nginx 过早断连。5.2 FastAPI 中间件审计日志与速率限制在main.py中添加中间件from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware import time import logging import json logger logging.getLogger(mcp-audit) class AuditMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): start_time time.time() # 记录请求元信息 client_ip request.client.host user_agent request.headers.get(user-agent, unknown) try: # 读取请求体注意只能读一次 body await request.body() request_dict json.loads(body.decode(utf-8)) if body else {} # 审计日志脱敏敏感字段 audit_log { timestamp: time.time(), client_ip: client_ip, method: request_dict.get(method, unknown), tool_name: request_dict.get(params, {}).get(name, unknown), size_bytes: len(body), user_agent: user_agent[:50] # 截断防日志爆炸 } logger.info(json.dumps(audit_log)) except Exception as e: logger.warning(fAudit log failed: {e}) response await call_next(request) process_time time.time() - start_time response.headers[X-Process-Time] str(process_time) return response app.add_middleware(AuditMiddleware)配合logrotate和rsyslog审计日志可对接 SIEM 系统满足金融行业合规要求。5.3 Prometheus 监控指标暴露 MCP 特定维度main.py添加监控端点from prometheus_client import Counter, Histogram, Gauge, make_asgi_app import time # MCP 专用指标 MCP_CALLS_TOTAL Counter( mcp_calls_total, Total number of MCP calls, [server, method, tool_name, status_code] ) MCP_CALL_DURATION_SECONDS Histogram( mcp_call_duration_seconds, MCP call duration in seconds, [server, method, tool_name] ) MCP_ACTIVE_CONNECTIONS Gauge( mcp_active_connections, Number of active MCP connections, [server] ) app.middleware(http) async def metrics_middleware(request: Request, call_next): start_time time.time() response await call_next(request) # 提取 MCP 相关信息 method unknown tool_name unknown if request.url.path /mcp: try: body await request.body() if body: req_json json.loads(body.decode(utf-8)) method req_json.get(method, unknown) if method mcp.callTool: tool_name req_json.get(params, {}).get(name, unknown) except: pass # 记录指标 MCP_CALLS_TOTAL.labels( serverfile-server, methodmethod, tool_nametool_name, status_coderesponse.status_code ).inc() duration time.time() - start_time MCP_CALL_DURATION_SECONDS.labels( serverfile-server, methodmethod, tool_nametool_name ).observe(duration) return response # Prometheus metrics endpoint metrics_app make_asgi_app() app.mount(/metrics, metrics_app)Prometheus 配置抓取http://file-mcp.example.com/metricsGrafana 看板可监控mcp_calls_total{methodmcp.callTool,tool_namefile.read,status_code200}成功读取次数rate(mcp_call_duration_seconds_bucket{le1}[5m])99% 调用耗时 1smcp_active_connections当前连接数突增可预警 DDoS这套监控让我在某次 DNS 故障中5 分钟内定位到file-server因上游 DNS 解析超时导致mcp.callTool延迟飙升而非盲目重启。6. LangGraph 集成实战构建一个“文档分析-归档-通知”Agent理论终需落地。下面是一个完整案例用 LangGraph 多 MCP Server 构建一个自动化文档处理 Agent它接收 PDF提取文本判断类型合同/发票/简历归档到对应目录并邮件通知负责人。整个流程不写一行工具调用代码全由 MCP 协议驱动。6.1 Server 部署清单Server 名称地址暴露工具作用pdf-parserhttp://pdf:8000/mcppdf.extract_text,pdf.get_metadataPDF 解析doc-classifierhttp://ai:8001/mcpclassifier.classify_document文档分类微调的 BERT 模型file-serverhttp://file:8001/mcpfile.write,file.read文件读写email-gatewayhttp://email:8002/mcpemail.send_notification邮件发送6.2 LangGraph Graph 构建声明式定义协议驱动from langgraph.graph import StateGraph, END from langgraph.mcp import MCPClient from langchain_core.messages import HumanMessage, AIMessage from typing import TypedDict, List, Dict, Any class State(TypedDict): messages: List[dict] pdf_content: str doc_type: str archive_path: str # 初始化所有 MCP Clients clients { pdf: MCPClient(http://pdf:8000/mcp), classifier: MCPClient(http://ai:8001/mcp), file: MCPClient(http://file:8001/mcp), email: MCPClient(http://email:8002/mcp) } # 自动发现所有工具并创建 ToolNode tool_nodes {} for name, client in clients.items(): for tool in client.list_tools():