MCP 是什么?Model Context Protocol 核心概念 + TypeScript 实现

说实话,我关注MCP不是因为它火,而是因为我真的受够了。
受够了什么?受够了那些所谓的"集成代码"——一堆临时拼凑的脚本,看似聪明,实则脆弱得一碰就碎。每次同事问我:"能不能让AI安全地读取内部数据,但不用重写半个后端?",我就知道又要开始一场艰难的架构讨论。
直到我真正理解了MCP,才发现这不是什么时髦的新框架,而是一个早就该存在的抽象层。就像REST让服务和服务对话,MCP让模型和系统对话——这个类比一旦想通,其他问题就迎刃而解了。
这篇文章不谈理论,不讲故事,只讲怎么用TypeScript搭建你的第一个MCP服务器,以及我踩过的所有坑。
先搞清楚一件事:MCP不是什么
在动手之前,咱们得澄清一个误区。
MCP(Model Context Protocol)不是:
- 又一个需要你供起来的框架
- 什么神奇的AI服务器
- 你现有后端的替代品
MCP是一份契约——一份非常固执己见的契约。
它定义了工具(Tools)、资源(Resources)和提示词(Prompts)如何以可预测、可检查、可审计的方式暴露给模型。
打个比方:REST让微服务之间能说话,MCP让AI模型和你的业务系统能说话。
如果这个概念还不清楚,建议先去看官方规范(https://modelcontextprotocol.io),看一遍就够了。看完回来,我们接着聊。
为什么TypeScript是最优选?
能用Python写MCP服务器吗?当然。
能用Rust写吗?没问题。
但为什么我强烈推荐TypeScript?
理由很简单:
- 你的技术栈里很可能已经有Node了
- JSON是MCP的原生语言,TypeScript天然亲和
- 类型系统能在模型开始胡编乱造之前就把错误拦住
- 工具链成熟且稳定(这是夸奖,不是讽刺)
更重要的是,大部分MCP服务器都是集成密集型,而非计算密集型。TypeScript在这种场景下如鱼得水。
必须理解的心智模型(否则你会很痛苦)
在写代码之前,你必须搞懂这个核心概念:
一个MCP服务器暴露三样东西:
┌─────────────────────────────────────┐│ MCP Server 核心 │├─────────────────────────────────────┤│ ││ Resources ← 只读数据 ││ (查询配置、读取文档) ││ ││ Tools ← 有副作用的动作 ││ (创建记录、发送请求) ││ ││ Prompts ← 结构化的推理引导 ││ (摘要模板、分析框架) ││ │└─────────────────────────────────────┘就这三样,别的没了。
如果你试图模糊这三者的边界,你的服务器会变成一团乱麻。
记住这个原则:
- 如果它会改变状态 → 它是Tool
- 如果它获取数据 → 它是Resource
- 如果它塑造推理 → 它是Prompt
大声说出来,这很重要。
搭建第一个服务器(无聊但必要的部分)
创建项目
mkdir my-first-mcp-servercd my-first-mcp-servernpm init -ynpm install @modelcontextprotocol/sdk zod对,安装zod。等会你就知道为什么了。
创建 index.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";初始化服务器
const server = new McpServer({ name: "my-first-mcp-server", version: "1.0.0",});传输层很关键
对于本地开发和大多数工具场景:
const transport = new StdioServerTransport();await server.connect(transport);就这样,你的服务器起来了。
没有HTTP,没有Express,没有废话。
添加第一个Resource(从简单开始,保持清醒)
咱们暴露点无聊的东西——这是故意的。
server.resource( "config", "config://app", async () => ({ name: "Demo App", environment: "development", }));这个Resource:
- 有稳定的URI
- 返回JSON
- 安全可读
模型喜欢无聊、可预测的数据。你也应该喜欢。
Tools:大多数人翻车的地方
Tools很强大,这意味着它们很危险。
来看一个好的Tool示例:
import { z } from"zod";server.tool("create_note", { title: z.string(), body: z.string(), },async ({ title, body }) => { // 这里写入数据库 return { success: true }; });为什么这个Tool设计得好?
- 输入有验证(用了zod schema)
- 输出简单无聊
- 副作用明确清晰
现实警告:
如果你的Tool:
- 接受没有schema的原始字符串
- 一次干五件事
- 依赖隐藏的全局状态
……那你就是在造脚射神器。
一个开发者熟悉的场景
假设你在做一个内部工具,需要让AI助手能够查询公司的Jira工单。
错误做法:
// ❌ 危险!没有任何输入验证server.tool( "query_jira", {}, async (params: any) => { // 直接拼接SQL或者API查询,等着被注入吧 return await jiraApi.search(params.query); });正确做法:
// ✅ 安全且可靠server.tool("query_jira", { project: z.string().max(20), status: z.enum(["open", "in_progress", "done"]), assignee: z.string().email().optional(), },async ({ project, status, assignee }) => { // 类型安全的查询,有明确的参数范围 returnawait jiraApi.search({ jql: `project = ${project} AND status = ${status}${ assignee ? ` AND assignee = ${assignee}` : "" }`, }); });看到区别了吗?Schema就是你的防火墙。
Prompts不是花哨的字符串
这是新手最容易低估MCP的地方。
Prompts是接口,不是文本块。
server.prompt( "summarize_notes", { notes: z.string(), },({ notes }) => ({ messages: [ { role: "system", content: "你是一位精准的技术写作专家。", }, { role: "user", content: `请总结以下笔记:\n${notes}`, }, ], }));你不是在告诉模型该想什么,你是在给它设置护栏。
一个实际应用场景
假设你在做一个代码审查助手,需要让AI按照团队规范检查代码:
server.prompt( "code_review", { code: z.string(), language: z.enum(["typescript", "javascript", "python"]), focus: z.enum(["performance", "security", "style"]).optional(), },({ code, language, focus = "style" }) => ({ messages: [ { role: "system", content: `你是一位经验丰富的${language}代码审查专家。审查重点:${focus}- 如果是performance,关注性能瓶颈和优化机会- 如果是security,关注安全漏洞和潜在风险- 如果是style,关注代码风格和最佳实践请给出具体的改进建议,并标注严重程度(critical/major/minor)。`, }, { role: "user", content: `\`\`\`${language}\n${code}\n\`\`\``, }, ], }));这样设计的好处:
- 审查重点可配置
- 输出格式标准化
- 容易集成到CI/CD流程
测试你的服务器(别像其他人一样跳过这步)
如果你不测试MCP服务器,模型会替你测试。
而且它们毫不留情。
使用官方的MCP inspector: https://modelcontextprotocol.io/docs/tools/inspector
检查这些:
- Schema验证是否生效
- Tool命名是否清晰
- Resource是否可被发现
如果你自己都觉得困惑,模型会更困惑。
简单的测试工作流
┌──────────────┐│ 1. 启动服务器 │└──────┬───────┘ │ ▼┌──────────────────┐│ 2. 用inspector ││ 连接并检查 │└──────┬───────────┘ │ ▼┌──────────────────┐│ 3. 调用每个Tool ││ 尝试边界输入 │└──────┬───────────┘ │ ▼┌──────────────────┐│ 4. 检查错误处理 ││ 和返回格式 │└──────────────────┘安全性:大家都摆手的部分(大错特错)
MCP不会替你保护数据。
你必须:
- 严格限定Tool范围
- 避免暴露原始凭证
- 记录Tool使用日志
- 假设模型会尝试各种奇怪的输入
MCP让访问变得结构化,但不会让访问默认安全。这是你的活。
一个真实的安全案例
假设你在为团队搭建一个能访问数据库的AI助手:
❌ 危险做法:
// 千万别这么干!server.tool( "execute_sql", { query: z.string(), }, async ({ query }) => { // 直接执行任意SQL?这是在找死 return await db.raw(query); });✅ 安全做法:
// 预定义的安全查询const ALLOWED_QUERIES = { user_count: "SELECT COUNT(*) FROM users WHERE active = true", recent_orders: "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days' LIMIT 100", product_stats: "SELECT category, COUNT(*) FROM products GROUP BY category",} asconst;server.tool("execute_predefined_query", { query_name: z.enum(Object.keys(ALLOWED_QUERIES) as [string, ...string[]]), },async ({ query_name }) => { // 只能执行预定义的查询 returnawait db.raw(ALLOWED_QUERIES[query_name]); });关键原则:永远不要相信模型的输入,哪怕它看起来很"智能"。
什么时候MCP是错误的选择
说实话,别盲目用MCP。
不要用MCP如果:
- 你只需要一个定时任务
- 你在暴露公共API
- 根本没有模型会碰这个系统
MCP适合的场景:
- 模型需要受控访问
- 你需要可审计性
- 你在乎长期的可维护性
真正的收益(不是炒作,是杠杆)
在我们团队上线第一个MCP服务器之后:
✅ 集成变得无聊了(这是好事)✅ Tool行为变得可预测✅ Prompt混乱大幅下降✅ Debug不再像占卜
这才是真正的价值。不是炒作,是杠杆。
一个团队的实际案例
某电商团队用MCP搭建了一个订单管理助手:
之前的做法:
- 各种API散落在不同服务
- 每次接入新功能都要写大量胶水代码
- AI经常因为接口不一致而出错
- 调试全靠猜
用MCP重构后:
订单查询 → MCP Resource (只读,安全)订单创建 → MCP Tool (有验证,有日志)订单分析 → MCP Prompt (标准化输出)结果:
- 新功能接入时间从2天降到2小时
- AI错误率下降70%
- 代码量减少40%
- 团队终于能好好睡觉了
最后(以及一点点个人看法)
MCP不会让你的产品变魔法。
但它会让你的AI集成少犯蠢。
说实话?这就是大多数团队现在需要跨越的门槛。
本文内容仅供参考,不构成任何专业建议。使用本文提供的信息时,请自行判断并承担相应风险。



