一个人,3天,用MCP(Model Context Protocol,模型上下文协议)把一款AI旅游规划产品从零做到上线。做完之后我最大的感触是:这套技术栈的成熟度,已经比大多数人想象中要高得多,高到一个人不需要团队配合,也能独立交付一个可用、能跑、能展示的产品。这篇文章不聊虚的,我把从需求拆解、协议理解、Server设计、Agent调通到部署上线的完整过程都记录下来,包括踩过的坑和当天现场的操作日志,希望对正在观望MCP或者准备做AI Agent产品的朋友有参考价值。不管你是刚接触MCP的新手,还是已经在做智能体应用的开发者,这篇内容都按“为什么这样做、具体怎么落地、遇到问题怎么排”的顺序来写,可以直接照着思路复现。
1. 为什么要用MCP做AI旅游规划:先把账算清楚
1.1 需求到底长什么样
先说清楚我要做的产品是啥。名字就叫“AI旅游规划”,核心输入是4个信息:目的地、出行天数、预算上限、游玩偏好(比如偏美食、偏打卡、偏亲子、偏休闲)。输出是一份可以直接拿去用的行程方案,包含每天去哪几个地方、顺序怎么排、每个点位预计待多久、中午晚上吃什么、酒店住在哪个区域、交通怎么衔接,以及最后一张预算拆分表。
听起来不复杂,但传统开发方式做这个需求,难点全在“动态编排”上。目的地一变、天数一变、预算一变,整个行程逻辑就要重新算。你要自己写推荐算法、写路线排序、写时间估算,还得维护POI(兴趣点)数据库,光数据清洗就能干半个月。一个人3天做出这个产品,用传统接口开发的思路,基本不可能。
所以我选择了一条相对取巧的路线:产品的大脑交给AI大模型,用MCP协议把数据和外部工具暴露给模型,让模型自己完成用户意图理解、行程编排和推荐决策。我需要做的,是把MCP Server搭好,把Tool定义清楚,再做一个足够简单的展示壳子。
1.2 为什么是MCP而不是传统API开发
这里要讲明白一个关键区别。传统API开发是“功能先行”:你写一个接口,接收参数,返回数据,调用方把逻辑写死。比如我做一个“获取景点列表”的接口,前端拿到结果之后再用另一个接口算路线,流程是固定的,模型不参与决策,所有编排逻辑都要我一行一行写代码。
MCP的思路反过来,它把我写好的能力声明给AI模型,然后让模型自己决定“需要用哪个能力、按什么顺序调用、参数怎么填”。比如说,模型要规划一天的行程,它看到我提供了get_poi_list(获取景点列表)、plan_daily_route(规划单日路线)这两个Tool,它会自己先调第一个拿到景点信息,再基于景点信息调第二个生成路线。这就是AI Agent的工作方式:不是人告诉模型每一步干什么,而是给模型工具,让模型根据目标自主编排。
这个差别带来的效率提升是数量级的。我不需要写“自然语言理解”模块——用户说“带老人孩子去西安,4天,预算8000”,这句话怎么拆解成参数,由模型完成。我也不需要写推荐算法——选哪些景点、怎么排序,模型根据偏好自己判断。我只需要保证Tool返回的数据质量足够高,接口足够稳。
另外,MCP协议本身帮我解决了一个更头疼的问题:接口对接的标准化。以前每个AI应用接外部工具都要自己写一套适配层,现在MCP把工具调用、资源读取、提示词模板全部定义成统一协议,任何支持MCP的Host(比如Claude Desktop、Cursor、Dify、自研Agent)都能直接接我这个Server。一次开发,多端复用,这套标准化带来的长期收益,比眼前这3天短期上线更值。
2. MCP核心概念和一天的准备工作
2.1 MCP到底是什么:一个USB-C的例子
很多人在热词榜上看到MCP,知道它火,但不清楚它到底是软件协议还是硬件协议。这里先给个明确界定:MCP是软件协议,对应的是AI应用层,解决的是AI模型与外部工具、数据源之间的互联问题。如果非要用硬件类比,它最像的其实是USB-C接口标准——统一了物理连接形态(插口),统一了传输协议(数据怎么走),统一了能力协商(我这个设备支持视频输出、你这个设备支持快充)。MCP做的事情类似,只不过连的不是显示器,是AI模型和工具。
具体一点:你没有MCP的时候,想给AI应用加上“查询天气”“查数据库”“发送邮件”这些能力,每个都要单独写一套接入逻辑,不同框架之间还不通用。有了MCP之后,每个工具提供方只需要实现一个MCP Server,利用标准原语把能力暴露出来,任何MCP Host都能自动发现并调用它。这套机制下,“工具”变成了一种可插拔的资源,AI应用的能力边界被极大扩展。
这里还有个对新手很友好的点:MCP本身不是某个公司的私有标准,它是由Anthropic在2024年底开源推出来的开放协议,现在已经有不少社区贡献的SDK和Server实现。我这次选的是Python生态的FastMCP库,写工具就像写函数一样简单,一个装饰器就能把一个普通函数暴露成MCP Tool,学习成本非常低。
2.2 Host、Client、Server:三个角色分清
MCP架构里三个角色必须彻底搞清楚,否则后面配置会绕晕:
- MCP Host:最终用户面对的AI应用。比如Claude Desktop、Claude Code,或者我的产品Web页面底层的Agent运行时。Host负责跟用户交互,决定要不要调用工具。
- MCP Client:内嵌在Host里的协议客户端。它负责和Server建立会话、发送调用请求、接收结果。这个角色一般不用开发者操心——Host内部已经集成好了。
- MCP Server:能力提供方。我这次写的就是这个角色。它通过一组标准原语向外暴露功能,被Client连接和访问。
理解这三个角色最好办的一件事,就是帮你搞清楚配置文件里到底在配置什么。比如在Claude Desktop里配置一个MCP Server,你要做的是告诉Host:“我这里有一个Server,它在本机的某个路径下跑,启动命令是python xxx.py”,然后Host就会按照协议连过去。配置不是说你写代码,而是做“连接”这件事。
MCP协议里面有三个核心原语:Tool、Resource、Prompt。Tool是让模型调用外部函数用的,像发起请求、查数据库、算价格这类操作,都通过Tool完成;Resource是给模型提供结构化上下文数据的,比如一个CSV文件、一段JSON配置;Prompt是预置好的提示词模板,让模型在特定场景下用固定格式输出。我的产品里主要用Tool和Resource,Prompt用的相对少。
2.3 工具链选型:用什么框架、什么模型
工具选型上我遵循的原则是:能少装就少装,能云上跑不上本地扛。具体清单如下:
- MCP Server:Python + FastMCP。FastMCP是当前Python生态里把MCP开发简化到极致的库,声明Tool、定义参数Schema都很直观,还内置了stdio和Streamable HTTP两种传输方式的支持。
- Host调试:Claude Desktop。主要用来做Agent工作流的调试。它的配置管理非常直观,能看到模型调用工具的完整日志,是排查Agent行为错误的利器。
- 辅助编码:Cursor。平时写代码修Bug用。Cursor本身支持MCP,可以接入我自己的Server,相当于让AI助手直连我的工具环境,查数据库、跑代码验证都不用切换上下文。
- 数据库:Neon托管的PostgreSQL。旅游行程数据是结构化数据,用PostgreSQL存再合适不过。选择Neon主要图它便宜和省心,免费额度对个人项目完全够用。
- 前端和部署:Next.js + Vercel。3天时间不允许我在UI上花太多精力,直接用Next.js模板改造页面,部署到Vercel,域名解析到上就完事。后端跑在云服务器上,用FastAPI处理几个简单的接口转发和存储请求。
这里说一句选型的思路。很多人一上来就纠结“到底用Claude还是OpenAI、用Dify还是自研”,我的建议是先想清楚谁干体力活。我选择Claude Desktop做Host是因为它的MCP支持最成熟、日志最清晰;选择FastMCP是因为我用Python写最顺手;选择Vercel是因为前端部署真的快。它们没有绝对优劣,关键是每一个选择都替你省了时间,而不是增加学习成本。
3. 核心功能实现:旅游规划Agent的骨架
3.1 整体工作流长什么样
我的产品整体流程是这样设计的:用户在网页表单里输入目的地、天数、预算和偏好,点击生成,前端把请求发给后端,后端组装成一个任务描述(prompt),交给Agent运行时。Agent运行时拿到任务后,先调用我的MCP Server里的search_city_info这个Tool拿到城市基础信息,再根据天数决定要查多少天的POI数据,接着逐个调用get_poi_list和is_open_today筛选可用景点,最后进入行程编排环节,由模型综合所有信息生成每日路线,再调用calculate_budget算预算。
这个流程不是我在代码里硬编码的,是模型根据我定义的Tool自动决定的。可能你会问:这样是不是不可控?我的经验是:不可控反而是产品的灵活性所在,因为用户输入千变万化,硬编码根本覆盖不完。我要做的是“边界管理”,不是“步骤控制”。怎么管理边界?答案在Tool设计上。
3.2 六个Tool的设计与实现
Tool是MCP Server的核心,也是Agent能力的天花板。我最终定了6个Tool:
| Tool名 | 功能 | 关键参数 | 备注 |
|---|---|---|---|
| search_city_info | 获取城市基础信息(别称、最佳旅游季节、核心商圈片区) | city_name | 数据来自自有小库的静态整理 |
| get_poi_list | 获取某城市的景点/美食/亲子/打卡点位列表 | city_name, interest_type, limit | 真实数据我用的是公开可用的旅游开放数据源,做了一层清洗存成静态JSON |
| is_open_today | 判断某个景点今天是否开放 | poi_id, date | 简化为每周固定开放日历 |
| plan_daily_route | 生成单日路线(按地理位置聚合动线) | poi_ids, start_time, end_time, prefer_walk | 核心编排Tool,返回建议动线文本 |
| calculate_budget | 计算预算拆分 | trip_id, budget, days | 返回吃住行玩的预算占比建议 |
| save_trip_to_db | 保存行程到数据库 | destination, days, budget, itinerary_json | 供前端展示和后续历史记录查看 |
每个Tool的实现,本质就是一个普通Python函数。用FastMCP写起来大概长这样:
from fastmcp import FastMCP mcp = FastMCP("travel-planner") @mcp.tool() def get_poi_list(city_name: str, interest_type: str = "general", limit: int = 10) -> list[dict]: """ 获取城市兴趣点列表,支持景点/美食/亲子等不同类型。 """ data = load_poi_data(city_name) filtered = [p for p in data if p.get("type") == interest_type or interest_type == "general"] return filtered[:limit] @mcp.tool() def plan_daily_route(poi_ids: list[str], start_time: str = "09:00", end_time: str = "18:00") -> dict: """ 根据POI分布规划单日路线,返回按地理位置排序的动线建议。 """ pois = [get_poi_by_id(pid) for pid in poi_ids] # 这里我做了一个按区域分组、再按推荐时长排序的简单编排 ordered = sort_pois_by_zone_and_time(pois) return { "route": ordered, "suggestion": "建议上午优先dz1区域,下午转dz2区域,动线更顺" }看明白了吗?MCP Tool的妙处在于,函数定义里的docstring会被协议自动提取,变成模型理解工具用途的说明。也就是说,你写注释其实是写给模型看的,不是写给同事看的。这个概念很多人第一次接触会忽略,但它在Agent开发里极其重要:注释越清晰,模型调用工具的准确率越高。
3.3 Tool声明中的参数Schema约束
类一行写下来,背后还有一层容易被忽略的细节:MCP协议要求每个Tool的输入参数都要有清晰的JSON Schema描述。FastMCP会自动根据函数的类型注解生成Schema,但如果你想约束得更细,比如限定某些参数的可选值,或者标注参数的中文含义,建议在函数签名和自定义元数据上下点功夫:
@mcp.tool( title="获取景点开放状态", description="判断指定景点在某天是否开放", params={ "poi_id": {"description": "兴趣点ID,必须先调用get_poi_list获得", "required": True}, "date": {"description": "日期,格式YYYY-MM-DD", "required": True} } ) def is_open_today(poi_id: str, date: str) -> dict: ...我当时踩过一个很典型的坑:一开始没写这个细化描述,模型经常把date参数填成“今天”这种自然语言,Tool直接把“今天”两个字拿去数据库匹配,结果当然是查询失败。加上明确的格式约束之后,模型会自动转换成正确格式再调用。这个教训充分说明:在Agent类产品里,你写的每一个字段描述,都是在跟模型做接口契约。
3.4 数据的存储和前端展示怎么处理
整个产品的数据流分为两块:前端展示用的最终行程,和历史记录。最终行程由Agent生成后,我先让它输出一个结构化的JSON,再通过save_trip_to_db这个Tool存入PostgreSQL。表结构很简单,重点说下行程表:
CREATE TABLE trips ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), destination varchar(50) NOT NULL, days int NOT NULL, budget numeric NOT NULL, preferences jsonb NOT NULL, itinerary jsonb NOT NULL, total_cost_estimate numeric, created_at timestamptz NOT NULL DEFAULT now() );itinerary字段直接存JSON,因为行程方案本身是嵌套结构(每天有多个点位、每个点位有建议停留时间),用关系表拆开反而麻烦。PostgreSQL的jsonb类型非常适合这种场景,既能存复杂结构,又能对内部字段做查询索引。
你可能会问:这不是把数据形态做得很糙吗?行程不是用户看到的最终形态吗?我的考虑是:3天产品,优先保证“能从数据库里捞出来展示”。JSON字段在PostgreSQL里是正规用法,后续如果要做更细的报表分析,再拆关系表也不迟。这里的原则是别过度设计,先把产品跑通。
前端这边,我花了很少的时间。Next.js页面拿到后端返回的行程JSON后,按天渲染一个时间线组件:上午、中午、下午、晚上四个时段,每个时段列地点和交通建议。预算表单独放一个区块,用最简单的方式展示吃住行玩四块的占比。说实话这个UI颜值一般,但信息够清晰,用户能一眼看懂行程。
3.5 部署上线:从本地到外网
上线步骤比想象中简单,但里面有几个细节坑得很。首先我把MCP Server部署到一台云服务器上,用systemd管理进程,Uvicorn跑FastAPI,反代用Caddy自动配HTTPS。然后Claude Desktop里配置远程Server地址,走Streamable HTTP传输协议。
这里有个重点:本地调试MCP Server用stdio传输就够了(Server进程和Host进程同机启动),但生产环境下的Host和Server不在同一台机器,必须走远程模式,用Streamable HTTP。这个区别一定要分清,否则你本地调得好好的,一部署就全部失联,大概率就是传输模式没切。
部署完成之后,我还做了一个小优化:用Dify的浏览器MCP插件做了一次端到端验证。Dify本身支持MCP接入,可以在工作流里调用我的Server工具,相当于多了一个独立的Host验证入口。这一步让我确信Server的兼容性OK,不是只跟着Claude Desktop捆绑可用。
上线时顺便把域名解析、SSL证书、简单鉴权都处理了。这里提一句:给Tool加鉴权很有必要,否则你的Server就是公网上一个任何人都能调用的裸接口,既费钱又不安全。我用的是一个固定Bearer Token,虽然简单,但对个人项目够了。
4. 三天时间线实录
4.1 Day 1:搭骨架、通连接
第一天的核心任务只有两件事:搭MCP Server骨架,以及本地把stdio传输跑通。上午我把6个Tool的基本函数全写了出来,数据源用静态JSON顶上去,保证接口有返回;下午开始研究Claude Desktop的MCP配置。
配置Claude Desktop的步骤对新手不算太友好,因为配置文件路径在不同系统上不一样,macOS在~/Library/Application Support/Claude/claude_desktop_config.json,Windows在%APPDATA%\Claude\claude_desktop_config.json。我在里面声明了MyTravelServer的启动命令:
{ "mcpServers": { "travel-planner-local": { "command": "python", "args": ["/path/to/travel_server.py"], "cwd": "/path/to/project" } } }配置完重启Claude Desktop,它在界面上会显示查到了几个Tool。如果显示0个Tool,多半是Server启动时报错了。排查办法很简单:手动在终端跑一遍启动命令,看有没有异常输出,比在客户端里瞎猜快得多。
第一天晚上我就卡在一个细节上:Tool调通了,但模型胡乱传参。后来我意识到是docstring写得不够清晰,模型不理解poi_id和date的具体格式,于是连夜把每个Tool的说明和参数描述补全。这算是一个无声的开局教训:MCP的协议本身不难,难的是让模型正确理解你的工具,这完全取决于你对工具的设计和描述。
4.2 Day 2:调Agent,治幻觉
第二天的关键词是“调通全链路”。上午我把规划流程跑通了:给Claude Desktop发“西安4天预算8000带老人孩子”,它开始自己调用search_city_info、get_poi_list,最后plan_daily_route,输出了一份看起来有模有样的行程单。那一刻确实兴奋,但兴奋劲没过就开始暴露问题。
最明显的是行程生成的“幻觉”问题。模型的倾向是:宁可编造也不能留白。我明明没给它某个景点的开放时间,它会在行程里默认写“09:00-17:30开放”,甚至在预算表里编出“酒店每晚520元”这种精确到离谱的数。这很危险,因为用户一旦按这个行程执行,发现门票价格不对、餐饮费用差得远,对产品的信任就崩了。
我的对策是做“后置校验”,在Agent输出最终行程之前强制它再调一次is_open_today和calculate_budget。同时我在Tool返回里加上“数据置信度”字段:从静态库读的POI,置信度标为high;模型自己推理出来的数据,要求它必须标注“估算”。这等于把模型生成内容和我的实际数据做了显式区分,用户看到标注也知道哪些是可靠数据。治不了幻觉的根,但能有效防用户被幻觉坑。
第二天晚上我把PostgreSQL接上,save_trip_to_db正式可用,前端页面也搭了个雏形。到这儿,核心功能已经能用了。
4.3 Day 3:打磨外壳,置线上线
第三天上午我做的是“降级从快”的收尾工作:把前端页面从本地切到线上Vercel,配置环境变量、API地址、域名解析。有一个细节值得说:因为前后端域名不同,CORS问题如期而至。我在后端加了跨域中间件,允许Vercel域名来源的请求,加上鉴权Token,10分钟就处理完了。
下午主要用来做真机测试。我模拟了三个不同用户场景:一个大学生穷游,一个家庭亲子游,一个商务出差顺便逛两天。分别给不同的目的地、天数和预算,观察生成的行程质量。测下来发现两个系统性问题:一是预算分配逻辑比较机械,模型给穷游用户也推荐“当地五星级酒店”附近动线;二是景点的地理聚类不够准确,有时候一天安排的几个点其实分散在城市两端。
这两个问题我当天没有完全修复。原因是它们本质上涉及推荐模型和数据质量,不是配置层面能调好的。我的处理方式是:在Tool的返回里加入“推荐安全时间”和“区域标签”两个辅助字段,帮助模型更好地判断,同时在前端显示“行程建议仅供参考,请以实际营业信息为准”的提示。这里也体现了一个产品经理心态:MVP阶段的优先级不是“十全十美”,而是“核心链路打通+用户可用+风险可控”。
晚上8点,域名解析生效,产品正式对外可访问。3天,一个人,从零到可用,完成了。
5. 踩坑与调试实录
5.1 问题一:工具返回的JSON格式不稳定
MCP调用和函数调用不一样。普通函数返回什么,调用方拿到的就是什么。但Agent场景下,Tool的返回结果会经过大模型的语言理解环节,模型有可能会“二次加工”你的返回内容,比如把原本规整的JSON改得面目全非,或者在字段缺失时偷偷补一个默认值。
我遇到的一个典型案例:get_poi_list明明返回5个景点,模型却只挑了自己听说过的2个写进规划,剩下3个被“忽略”了。原因是模型在理解时觉得“这3个景点不著名”,自作主张过滤了。这个问题很隐蔽,因为链路是通的,日志也是正常的,就是结果不对。
解决方案是:在Tool的返回结构上加一层“断言”,明确告诉模型“这个列表是完整的关键清单,不要做筛选,按顺序评估”。你可以把这个理解为一种对模型的“System级别约束”。另外,我强烈建议Tool返回的顶层字段不要超过5个,嵌套层级控制在两层以内,否则模型解析很容易丢信息。经验法则:给模型的数据,越平越简单越好。
5.2 问题二:Agent陷入循环调用
有次测试“杭州3天2000预算”时,我观察到模型连续调用了7次calculate_budget,每次参数还都一样。看日志才发现,是我在Tool返回里给了“预算可能超支”的提示,模型看到提示后就反复调用来“确认”,陷入循环。
这个问题的根源是我没有给Tool返回设置终止条件。Agent的决策边界不清晰,它无法判断什么时候该停止某类操作。后来我给所有Tool的返回都加了一个字段叫action_required:当值为false时,明确示意模型“这一步已闭环,不要再调用此工具”。这相当于给Agent一个“刹车信号”,有效避免了绝大多数循环调用和重复请求。
这个经验扩展到通用场景也成立:在设计MCP Tool时,不要只考虑“这个Tool能做什么”,还要考虑“这个Tool怎么告诉模型已经做完了”。动作的终止信号,和动作的能力声明,同等重要。
5.3 问题三:stdio本地通了,远程环境却连不上
这是上线过程中最让我头疼的一个坑。本地调试时Claude Desktop用stdio模式跑我的Server一切正常,一切换到生产环境的远程Server就报“connection refused”。排查后发现,问题出在监听地址上:本地Server跑在127.0.0.1,只监听本机回环地址,生产服务器上的Host当然连不上。
解决办法就是在启动远程模式的Server时,要监听0.0.0.0,然后通过Caddy或Nginx把对应端口的HTTPS代理出去。同时还有个容易被忽略的问题:很多MCP Server默认只允许一个活跃会话,本地调试没问题,远程一旦有多个请求同时进来就会互相排队甚至超时,需要在Server配置里允许并发会话。
5.4 常见问题速查表
我把这次遇到的、以及MCP开发里同事朋友经常碰到的典型问题整理成一张速查表,方便以后直接对照排查:
| 现象 | 原因 | 解法 |
|---|---|---|
| Host显示0个Tool | Server启动失败或配置命令错误 | 手动运行启动命令看报错,检查workdir |
| Tool调用时参数传错 | docstring和参数描述不清晰 | 补充Tool描述、参数格式、字段取值说明 |
| 返回JSON被模型改动 | 模型对Tool返回做了二次理解 | 约束返回结构扁平,加“不要筛选”的明确指令 |
| Agent反复调用同一Tool | 缺少动作终止信号 | 返回字段加action_required=False表达闭环 |
| 远程Server连不上 | 监听地址只绑了127.0.0.1 | 改监听0.0.0.0,并用反代暴露HTTPS |
| CORS报错 | 前后端域名不同 | 后端加跨域白名单,允许前端域名 |
| 数据库连接数爆掉 | 单连接连接池配置不够 | 使用连接池,按需配置最大连接数 |
这张表也差不多是我预留给自己以后做同类产品的排查手册,如果你照着自己的MCP项目做一遍,大概率会撞上其中一半以上。
最后再分享一个实操中的小技巧:调试MCP Server的时候,别光盯着日志,直接用命令行工具mcp-inspector(MCP官方检查器)跑一遍,它能直观展示所有Tool的Schema、模拟调用并看到原始返回结构。很多我以为是协议问题的问题,其实都是代码问题,用这个工具检查能省一大半排查时间。我个人对这套技术栈的体会是:MCP真正打开了AI应用的工具接入层,让独立开发者也能用很少的人力和成本,做出以前需要团队才能完成的智能产品。如果你也想动手,我建议你先拿一个最小场景(比如查天气、查数据库、发邮件)把协议跑通,再往业务里加复杂度。3天能上线一个产品,越往后这套方法论的价值会越明显。