先说个现象。最近这一两年,我在圈子里聊得最多的话题从“要不要上微服务”变成了“能不能给老系统接上AI”。手里捏着跑了好几年的订单系统、CRM、内部ERP,要说推倒重写,老板第一个不同意;但要说继续装作看不见AI这波浪潮,业务部门又天天来问“为什么别人家的系统能自动查单、自动填工单,我们的还得靠人肉点”。
后来我陆续帮几个客户把一套用了将近十年的老CRM接上了大模型,核心系统一行业务代码没动,数据库表结构也没碰,靠的就是在系统旁边加了一层MCP服务。这篇就把我踩过的坑、试出来的路子和可以直接抄的代码整理出来,给同样被旧系统绑住手脚的人一个参考。
1. 先搞清楚MCP到底解决什么问题
1.1 为什么老系统需要AI能力,而不是换一套新系统
很多老系统并不是“不能用了”,而是“太难改了”。业务逻辑埋在一堆存储过程里,表单校验散落在前端JS里,权限模型是十几年前设计的,换一个开发都能琢磨半个月。这种系统的核心资产不是代码,而是里面沉淀的数据和跑了几年的业务规则。
所以业务方说要“引入AI”,真正想要的不是换系统,而是让AI能帮他们处理那些重复、繁琐、需要查多个系统的操作。比如查一个客户的订单状态、根据合同条款生成催款通知、在工单系统里自动建单。这些事情本质上是“读老系统数据、按规则操作老系统”,而不是“重建老系统”。
这就引出一个关键问题:怎么让大模型安全、可控地触达老系统的数据和功能。直接给大模型开数据库权限那是灾难,把老系统的API一个个教会大模型去调也不现实。MCP就是来解决这个连接问题的。
1.2 MCP和以前那堆API网关有什么区别
我最早听到MCP时,第一反应是“这不就是个API网关吗”。等真正用起来才发现,两者解决的问题层面不一样。
API网关解决的是“谁来调、怎么路由、怎么鉴权”,它假设调用方是一个明确的前端应用或者服务,接口的格式、语义是事先约定死的。而MCP解决的是“AI客户端怎么发现工具、怎么理解工具、怎么调用工具”。它多了一层非常关键的东西:机器可读的工具描述。
打个比方,API网关相当于给系统开了一扇门,但门外的人得自己知道按哪个门铃、说什么暗号。MCP则是给门旁边装了一个公告栏,上面写着“这里有什么服务、每个服务需要什么参数、会返回什么结果”,AI来了先看公告栏,然后照着说明按门铃。大模型恰恰是那种“给它一份说明书它就能干活”的东西,所以MCP天然就是给AI用的接口标准。
它也不是某个厂商私有的东西,而是一个开放协议。官方定义里分了三个角色:宿主(Host,就是你运行的AI应用,比如Claude Desktop、Cursor,或者你自己写的Agent)、客户端(Client,负责跟Server通信)、服务器(Server,把具体能力暴露给AI)。它们之间走JSON-RPC 2.0的消息格式,传输方式支持本地子进程的stdio,也支持基于HTTP的SSE或Streamable HTTP。
1.3 一条消息在MCP里是怎么走的
我习惯用一套完整的调用链路来理解它。假设AI想查一个老CRM里的订单:
- 第一步,AI客户端启动时向MCP Server发一个
initialize请求,告诉Server“我是什么客户端、我支持什么协议版本”。 - 第二步,Server回一个自身的协议版本和它支持的能力列表。
- 第三步,AI客户端调用
tools/list,把Server上所有工具的描述拉下来。这段描述包括工具名字、用途说明、参数用什么样的JSON Schema。 - 第四步,大模型看完工具列表,决定要用哪个工具,于是发
tools/call,带上参数。 - 第五步,Server收到调用请求,内部去请求老系统的API、查数据库或者读写文件,把结果整理成结构化JSON返回给AI客户端。
这个过程里,老系统完全不知道MCP的存在。它只是被MCP Server以“一个普通API调用方”的身份请求了一下。这就是“旁路接入”的核心:不改变老系统内部,只改变老系统往外暴露能力的路径。
2. 旧系统接MCP的路线怎么选,我的判断标准
2.1 摆在面前的三条路线
我在动手之前列过三个方案,分别评估过成本、风险、见效速度。
第一条路,底层重写或者大重构。听起来最“彻底”,但现实是:老系统的数据模型和业务逻辑长年累月地耦合在一起,重构相当于在心脏上动刀,即便功能测试全过一遍,业务部门也未必敢上线。成本少说几个月,多的按年算。除非系统小到能抄底重来,否则不适合。
第二条路,硬在老系统内部嵌AI模块。直接在老代码里加SDK、加AI调用、加工具函数。问题是老系统通常没有很好的模块化边界,改一处常常牵出好几处编译错误不说,光是给老系统部署环境装上AI SDK依赖,就可能引发版本冲突。而且业务代码一改,整个系统的上线流程、压测、回滚方案全要重走。
第三条路,在老系统旁边部署一层MCP Server适配层。这层Server由新团队独立维护,它通过老系统已有的接口(REST、SOAP、数据库视图甚至直接读表)去访问数据和功能,对AI客户端暴露MCP标准协议。老系统那边最多只加一个只读账号或者开几个内网接口,不碰业务代码、不碰表结构、不用改部署方式。
2.2 我为什么最终选了“旁路适配层”
原因很直白:它对老系统的侵入面最小,又刚好能把现代工程手段都用上。
旁路方案里,MCP Server跑在独立进程、独立环境,依赖随便装,不用担心污染老系统运行时。它跟老系统之间只用最“老土”的方式通信——HTTP调用、SQL查询、文件读取。这些手段是任何老系统都具备的。而对外它又是标准的MCP,任何支持MCP的AI客户端拿过来就能用。
更重要的是,这个适配层的逻辑是“纯增量”的。AI相关的权限控制、数据脱敏、调用限流、日志审计,都可以放在这一层做。出了问题,把MCP Server停掉,老系统该干嘛还干嘛,回滚成本几乎为零。对老板来说,这是“有退路”的方案,决策阻力小了一大截。
2.3 桥接层到底该放哪些能力
不是老系统的一切都要暴露给AI。我建议按这四类来筛:
- 读操作类:查订单、查客户、查库存、查物流轨迹。这是AI最常用的,安全风险低,优先接。
- 写操作类:创建工单、更新状态、发送通知。风险中等,要加参数白名单、状态校验。
- 高风险操作类:改价、退款、删除数据。能不放就不放,非要放的话,必须加人工审批环节,AI只负责起草,最终执行权留给人在界面上点。
- 分析计算类:汇总统计、导出报表、生成合同。这类通常耗时长,要注意超时和异步任务设计。
放能力的时候记住一个原则:让AI“能读尽读,能写慎写,不改核心”。尤其写操作,能推进“待确认”状态的绝不让AI直接落库,这是我跟业务方博弈后总结出来的安全底线。
3. 不写一句业务代码,把老接口封装成MCP Server
3.1 技术选型:FastMCP帮你省掉一半工作量
MCP官方提供了Python和TypeScript的SDK。我给老系统做桥接,大多用Python,理由很实际:老系统周边往往已经有Python写的运维脚本、数据修正脚本,复用现成的连接池、数据库工具类,比用TypeScript从零搭更顺手。
SDK里我常用的是FastMCP这个封装,它在官方mcp包基础上简化了Server的定义过程,让你不用手写JSON-RPC消息处理逻辑。你只需要定义Python函数,加上@mcp.tool()装饰器,函数名和docstring会自动生成AI能读的工具描述。
这里有一个小知识点:MCP的Tool描述里,docstring写得好不好,直接决定大模型判断“该不该用这个工具、参数怎么填”。所以接口函数的说明要写清楚“这个工具是干什么的、参数含义、返回值结构”,跟给同事写交接文档一个道理。
3.2 核心代码:一个老CRM桥接Server的实例
假设我的老CRM系统提供了一套REST API,格式是老式的那种,前缀是/legacy/api,登录后才给token。我要在MCP Server里封装三个能力:按客户ID查订单列表、按订单号查状态、写一条催单记录。
下面是完整可跑的代码,用Python写:
#!/usr/bin/env python3 # legacy_crm_bridge.py import os import logging import requests from mcp.server.fastmcp import FastMCP logging.basicConfig( level=logging.INFO, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) logger = logging.getLogger("legacy-bridge") LEGACY_API_BASE = os.getenv("LEGACY_API_BASE", "http://legacy-crm.internal:8080/legacy/api") LEGACY_USER = os.getenv("LEGACY_USER", "mcp_bridge") LEGACY_PASS = os.getenv("LEGACY_PASS", "change-me") TIMEOUT = int(os.getenv("MCP_HTTP_TIMEOUT", "10")) mcp = FastMCP("legacy-crm-bridge") def _get_token() -> str: """登录老系统换token,REST接口的老套路""" resp = requests.post( f"{LEGACY_API_BASE}/auth/login", json={"username": LEGACY_USER, "password": LEGACY_PASS}, timeout=TIMEOUT, ) resp.raise_for_status() return resp.json()["token"] def _call_legacy(method: str, path: str, **kwargs) -> dict: """统一的请求入口,把token塞进header""" token = _get_token() headers = {"Authorization": f"Bearer {token}"} url = f"{LEGACY_API_BASE}{path}" logger.info("calling legacy API: %s %s", method, path) resp = requests.request(method, url, headers=headers, timeout=TIMEOUT, **kwargs) resp.raise_for_status() return resp.json() @mcp.tool() def query_orders_by_customer(customer_id: str) -> list[dict]: """按客户ID查询订单列表。customer_id是CRM系统中的客户主键,形如C-10023。返回订单列表,每个订单含订单号、金额、创建时间和当前状态。""" data = _call_legacy("GET", f"/customers/{customer_id}/orders") return data.get("orders", []) @mcp.tool() def query_order_status(order_no: str) -> dict: """按订单号查询订单状态。order_no是订单唯一编号,形如SO-2024-000123。返回订单状态、物流状态和最近更新备注。""" data = _call_legacy("GET", f"/orders/{order_no}/status") return data @mcp.tool() def create_collection_reminder(order_no: str, content: str) -> dict: """在CRM中创建一条催单提醒。order_no为目标订单号,content为提醒内容,建议在200字以内。返回创建的提醒记录ID和状态。""" payload = {"order_no": order_no, "content": content, "source": "ai_assistant"} data = _call_legacy("POST", "/reminders", json=payload) return data if __name__ == "__main__": mcp.run(transport="stdio")这段代码的核心在于,_call_legacy把所有对老系统的HTTP调用统一收口,加日志、加异常兜底都方便。实际对接中,你们老系统的鉴权可能是Cookie Session、可能是AppKey,甚至可能是“先调一个接口拿加密sign”,那就把_get_token替换成对应的逻辑就好,其余框架不用动。
用transport="stdio"是短跑阶段最稳的选择。AI客户端通过标准输入输出跟Server通信,不需要搞端口监听、不需要配防火墙,本地跑起来就通。如果你们的AI应用部署在远程,Server又必须在老系统内网跑,再用SSE或Streamable HTTP。
3.3 配置AI客户端,把Server挂上去
代码写完后,要用支持MCP的客户端验证。我这儿以Claude Desktop和Cursor为例,都是JSON配置就能挂载。
Claude Desktop的配置在claude_desktop_config.json里:
{ "mcpServers": { "legacy-crm-bridge": { "command": "python", "args": ["/data/mcp_servers/legacy_crm_bridge.py"], "env": { "LEGACY_API_BASE": "http://legacy-crm.internal:8080/legacy/api", "LEGACY_USER": "mcp_bridge", "LEGACY_PASS": "change-me", "MCP_HTTP_TIMEOUT": "15" } } } }重启客户端,在对话窗口里直接说“帮我查一下C-10023这个客户的订单”,如果Server挂载成功,客户端会自动发现query_orders_by_customer这个工具,让AI去调用它。首次跑通这个链路,整个项目的可行性就算验证完成了。
Cursor的话,项目根目录放一个.cursor/mcp.json,结构差不多,只是command要写绝对路径,因为Cursor的进程工作目录不一定是项目目录。
3.4 调通之后AI真的能干活了
我的经验是:第一个工具调通后,别急着接第二个,先让业务方来试用一轮。让真实用户用“自然语言”问AI各种问题,看它能不能正确选对工具、填对参数。这一步暴露出来的问题,比你自己闷头写10个工具有用得多。
第一次调通当天,业务方就试出好几个坑:他们习惯用“上周的订单”这种模糊时间概念,但我们老系统的接口只支持按日期范围查。解决方案不是我改老系统,而是在MCP Server里加一个“自然语言日期转范围”的逻辑,比如用Python的dateparser库,或者干脆把“上周”这类词在工具描述里写清楚让大模型自己转换成日期参数。
4. 老系统接AI必踩的坑和排查实录
4.1 我在实战中踩过的具体坑
坑一:老接口响应太慢,AI客户端先超时
MCP客户端调用工具有默认超时,Claude Desktop默认大概几十秒。我接的那个订单系统,有个查询接口要关联七八张表,冷查询要跑40秒。AI调用后干等,然后报超时,反复试几次之后AI就会跟用户说“这个工具不可用”。
解决思路:在MCP Server里做“缓存+超时分级”。高频查询加Redis缓存,缓存过期时间设短一点,比如5分钟。超过5秒的查询降级到异步任务:先返回“正在查询”的任务ID,AI看到任务ID后再轮询结果。这一套做完,AI的体验顺畅多了。
坑二:老系统鉴权方式奇葩,MCP Server频繁登录被限流
有个老系统的登录接口没有做频率限制,但登录一次拿到的token有效期只有30分钟。我一开始每个请求都重新登录,跑了半天,老系统那边的安全团队找过来说账号被风控了。
后来改成:MCP Server内存里维护一个token缓存,带过期时间,只在token过期前1分钟才重新登录。代码很简单,但能避免把老系统的账号搞出问题。
坑三:字段语义不一致,AI给出的参数和老系统对不上
老系统里的“客户名称”可能是“customer_name”,也可能是“cust_nm”,还有可能是“客户全称”这种中文键。大模型从对话里提取“张三”去调用工具,如果工具描述里没写清楚,它很可能填错字段。
我的办法是:在工具函数的docstring里写清字段别名和示例值,并且在MCP Server里做一层“参数归一化”。比如客户名,不管AI传的是customer_name还是cust_nm还是中文“客户名称”,都在Server内映射到老系统真正认的那个字段。
4.2 一条排查路径
MCP Server报错,优先看日志,不要瞎猜。我给Server加了统一的日志出口,本地跑的时候直接看标准输出,部署到远程后打到文件。日志要重点打三件事:收到什么工具调用、带什么参数、老系统返回什么(或者报什么错)。
我见过很多同行卡在“AI说工具不可用”,其实是MCP Server进程启动时环境变量没配全,初始化就挂了。这种问题看启动日志一眼就明白。所以客户端配置里的env,务必跟Server代码里读的环境变量一一对齐。
4.3 再补几个设计层面的经验
- 审计日志单独建表:AI对老系统的每次读写,都要能追溯到是哪次对话、哪个用户触发的。这个可以直接在MCP Server里加一个装饰器,用AOP的思路统一记录。
- 测试工具用“影子模式”:写操作的API,先在测试环境完全模拟一遍。真实环境只先开放读工具,等业务方对AI行为有信任了,再逐步放开写工具。
- 回滚按钮要物理存在:MCP Server做成独立服务,出问题就停进程。老系统的防火墙出站规则里,可以随时断开MCP Server到老系统的访问,这样就算Server被攻破,影响面也限制在“断了桥”。
5. 从“接口能通”到“AI真好用”,还要补哪些功夫
5.1 日志管理和可观测性
MCP Server的日志不能只靠print。建议用标准logging模块,分模块、分级别地打。我给实际项目定的规范是:
- INFO级别:记录每次工具调用、调用来源、耗时。
- WARNING级别:老系统接口返回非200、参数接近白名单边界。
- ERROR级别:调用失败、鉴权失败、数据解析异常。
如果MCP Server跑在容器里,日志直接打到stdout,交给日志采集系统统一收。这样出了问题,能从“AI客户端请求”一直追到“老系统SQL执行”,整条链路看得清。
5.2 权限控制,尤其是召回和数据边界
很多老系统的账号体系没有细粒度权限,一个接口可能返回客户全字段,包括手机号、身份证号、价格成本。AI调用这个接口后,如果它把这些信息原样回复给用户,就是数据泄露。
我建议在MCP Server上加一层“返回字段白名单”。老系统返回什么我不管,MCP Server往外吐数据之前,按工具级别把敏感字段剥掉。比如查订单接口,我默认只保留订单号、商品名、金额、状态,手机号、身份证这种除非专门的高级权限工具,否则一律过滤。
5.3 打通回写链路,让AI不只是“只读助手”
业务方最满意的功能其实是“回写”。他们不满足于AI只查数据,还希望AI能直接帮他们把结果写回系统。比如AI分析了客户的订单习惯,生成一条跟进任务,自动写到CRM的日程表里。
回写比读取麻烦的地方在于幂等。AI可能因为网络不明原因把同一个工具调用发了两次,如果Server没做幂等,老系统里就会多出两条一样的工单。解决办法是:写操作的工具,接收一个由AI生成的request_id,老系统那边如果支持,就在业务表里建唯一索引;不支持的话,MCP Server里用Redis的SETNX做一个幂等标记。
import redis import uuid r = redis.Redis(host=os.getenv("REDIS_HOST", "127.0.0.1"), port=6379, db=0) @mcp.tool() def create_collection_reminder(order_no: str, content: str) -> dict: """在CRM中创建一条催单提醒。order_no为目标订单号,content为提醒内容。返回创建的提醒记录ID和状态。""" request_id = str(uuid.uuid4()) dedup_key = f"mcp:dedup:reminder:{request_id}" if not r.set(dedup_key, "1", nx=True, ex=600): raise ValueError("重复请求已被拦截") payload = {"order_no": order_no, "content": content, "source": "ai_assistant", "request_id": request_id} data = _call_legacy("POST", "/reminders", json=payload) return data这里request_id也可以由AI客户端主动传,但让Server自己生成更省心。Redis的SETNX保证同一把钥匙只能成功一次,10分钟内重复提交自动拦截。
5.4 多个AI客户端共用同一个Server
我最初是给Claude Desktop配好就结束了,后来Cursor、自研Agent也都要用同一套老系统能力。不要让每个客户端都去装一遍桥接代码,而是把MCP Server单独部署成一个常驻服务,用SSE或Streamable HTTP暴露给不同客户端。
这样一来,工具代码只维护一份,权限、日志、幂等都在这一层统一做。客户端那边只需各自配置一行Server地址。这套架构的演进方向,其实就是企业内部慢慢会长出一个“AI能力网关”,老系统的各种能力围绕它逐步开放。
我自己在这几轮落地里最大的感受是:接AI这件事,本质不是技术突破,而是“系统对外开放能力的标准化”。老系统之所以让人望而生畏,是因为它太封闭、太特别、太依赖于懂它的人。MCP给了我们一个契机,用一套现代AI完全听得懂的语言,把老系统的能力重新讲了一遍。只要抓住“旁路适配、最小侵入、按需开放”这三个原则,哪怕系统再老,都能一步一步把它带进AI时代。