MCP-最基础的 MCP 该怎么写
前端 AI 工程化基石:最基础的 MCP 该怎么写
一、MCP 是什么?
MCP(Model Context Protocol) 解决的核心问题是——"模型如何访问外部系统和私有知识"。
在 AI 编程的语境下,模型本身不知道你的组件库有哪些 API、不知道设计规范的色值、不知道后端接口的参数定义。MCP 就是把这些团队知识,按**标准化开放规范(OpenSpec)**定义好,通过统一接口暴露给 AI 模型。
一句话:MCP 是团队知识与 AI 模型之间的桥梁。
底层理论基础:OpenSpec — 所有可被机器消费的知识,都应该以标准化的开放规范形式定义。
二、MCP 在工程化体系中的位置
整个前端 AI 工程化体系分为三层,从底向上:
Prompt 层 ← 自然语言意图表达(开发者用日常语言描述需求)
↑
Skill 层 ← 流程化最佳实践(把重复开发流程封装为可复用指令)
↑
MCP 层 ← 标准化上下文接口(暴露组件库/设计规范/API 文档给 AI)- MCP = 原子能力:单个工具的标准化封装
- Skill = 流程编排:把多个 MCP 工具串联成完整的开发工作流
三、最基础的 MCP Server 怎么写?
3.1 依赖
pnpm add @modelcontextprotocol/sdk zod@modelcontextprotocol/sdk:官方 SDK,提供McpServer类和StdioServerTransportzod:定义 Tool 的 Input Schema(参数类型与约束)
3.2 最小可运行示例(新版 SDK)
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
// 1. 创建 Server 实例
const server = new McpServer({
name: "my-first-mcp",
version: "1.0.0",
});
// 2. 注册 Tool
server.tool(
"get_user_info", // Tool 名称
"根据用户ID查询用户详细信息", // Description — 让 LLM 理解"什么时候该用这个工具"
{ userId: z.string().describe("用户唯一ID") }, // Input Schema — zod 定义参数类型和约束
async ({ userId }) => {
// 3. 实现业务逻辑
const user = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
// 4. 返回结构化结果
return {
content: [
{ type: "text", text: JSON.stringify(user, null, 2) }
],
};
}
);
// 5. 启动 Server(stdio 传输)
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("My First MCP Server running on stdio");3.3 注册第二个 Tool(传参演示)
server.tool(
"search_components",
"根据关键词搜索组件库中的组件",
{
keyword: z.string().describe("搜索关键词"),
category: z.enum(["base", "business", "chart"]).optional().describe("组件分类"),
limit: z.number().min(1).max(50).default(10).describe("返回数量上限"),
},
async ({ keyword, category, limit }) => {
const results = await searchComponents({ keyword, category, limit });
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);四、MCP 设计规范
4.1 命名规范
使用 领域_动作 命名方式,见名知意:
| 领域 | 动作 | 完整 Tool 名 |
|---|---|---|
component | list | component_list |
design | getToken | design_get_token |
api | query | api_query |
user | search | user_search |
4.2 参数设计规范
// ✅ 好:参数扁平、类型明确
{ userId: z.string().describe("用户唯一ID") }
{ keyword: z.string().describe("搜索关键词") }
// ❌ 差:复杂嵌套结构,LLM 难以理解
{ filter: z.object({ ...很深的嵌套... }).describe("...") }4.3 返回格式规范
| 规范 | 说明 |
|---|---|
| 格式 | 纯文本或 JSON,避免 HTML 或复杂 Markdown |
| 大小 | 返回内容应简洁,直接可被 Agent 决策 |
| 分页 | 大数据量场景支持分页查询 |
| 结构 | content 数组格式,{ type: "text", text: "..." } |
4.4 Tool Description 写法规范
这是最重要的设计点之一。Tool Description 不是给人看的,是给 LLM 看的——它决定了 LLM 能否在正确的时机选择正确的工具:
✅ 好:{ name: "query_user", description: "根据用户ID查询SaaS系统用户详细信息,返回用户名、部门、角色权限" }
✅ 好:{ name: "get_component_doc", description: "获取指定组件的API文档,包括Props、Events、Slots和使用示例" }
❌ 差:{ name: "query_user", description: "查询用户" }
❌ 差:{ name: "get_component_doc", description: "组件文档" }五、关键细节
5.1 传输协议
MCP 底层基于 JSON-RPC 2.0,支持两种传输方式:
| 传输方式 | 适用场景 | 实现 |
|---|---|---|
| stdio | 本地进程间通信(最常用) | StdioServerTransport |
| SSE (Server-Sent Events) | 远程/HTTP 通信 | SSEServerTransport |
绝大多数场景用 stdio 就够了——Client 启动子进程,通过 stdin/stdout 与 MCP Server 通信。
5.2 无状态设计
// ✅ 好:每个请求独立,不依赖前序调用
server.tool("get_user", "...", { userId: z.string() }, async ({ userId }) => {
return { content: [{ type: "text", text: await fetchUser(userId) }] };
});
// ❌ 差:依赖全局状态
let lastQueryResult = null;
server.tool("filter_user", "...", { keyword: z.string() }, async ({ keyword }) => {
// 依赖上一次查询的结果 — 不可靠
return filter(lastQueryResult, keyword);
});5.3 多 Tool 注册
一个 MCP Server 可以注册多个 Tool,按领域分组:
const server = new McpServer({ name: "my-mcp", version: "1.0.0" });
// 组件相关 Tools
server.tool("list_components", "...", {}, async () => { /* ... */ });
server.tool("get_component_doc", "...", { name: z.string() }, async ({ name }) => { /* ... */ });
server.tool("search_component_examples", "...", { name: z.string() }, async ({ name }) => { /* ... */ });
// 设计规范相关 Tools
server.tool("design_get_token", "...", { key: z.string() }, async ({ key }) => { /* ... */ });
server.tool("design_list_tokens", "...", {}, async () => { /* ... */ });5.4 配置注册
在 Claude Code 的 .claude/settings.json 或 claude_desktop_config.json 中注册:
{
"mcpServers": {
"my-first-mcp": {
"command": "npx",
"args": ["tsx", "./mcp-servers/my-first-mcp.ts"],
"env": {
"API_KEY": "${API_KEY}"
}
}
}
}六、注意事项
6.1 错误处理
server.tool("query_api", "...", { endpoint: z.string() }, async ({ endpoint }) => {
try {
const data = await callUpstreamAPI(endpoint);
return { content: [{ type: "text", text: JSON.stringify(data) }] };
} catch (error) {
if (error.code === 'RATE_LIMIT') {
// ✅ 返回可重试错误,Agent 会自动退避重试
return {
content: [{ type: "text", text: "请求频率过高,请稍后重试" }],
isError: true,
};
}
// ✅ 返回不可重试错误,Agent 会跳过并通知人工
return {
content: [{ type: "text", text: `查询失败:${error.message}` }],
isError: true,
};
}
});核心原则:不要抛出裸异常让进程崩溃,返回标准化的错误信息,让 Agent 能据此决策。
6.2 MCP vs 传统 API 封装
| 维度 | 传统 API | MCP Tool |
|---|---|---|
| 调用方 | 人 | AI Agent |
| Description | 文档给人看 | Tool Description 需让 LLM 理解"什么时候用" |
| Input Schema | 无强制约束 | zod 精确到参数类型和约束 |
| 返回格式 | 完整的 HTTP JSON | 简洁到 Agent 能直接决策 |
| 发现机制 | HTTP 路由注册 | ListToolsRequestSchema 自动发现 |
这个对比是理解 MCP 的关键——你要从 "Agent 怎么理解和使用这个工具" 的角度来设计每个 Tool。
6.3 接口设计平衡
在"通用性"和"易用性"之间取平衡:
- 太通用 → 难落地,Agent 不知道什么时候该调用
- 太定制 → 不可复用,换个项目就得重写
建议:先从本团队最高频的业务场景出发,设计专属 Tool,再逐步抽象通用接口。
6.4 性能优化
- 大型组件库考虑加缓存机制
- 大数据量回报必须支持分页
- 同步操作不要阻塞太久(Agent 在等返回)
6.5 安全注意事项
- 不要在 Tool 的 Input Schema 中暴露内部系统细节
- 环境变量用
${ENV_VAR}引用的方式管理敏感信息 - Tool 返回的数据要做脱敏处理(如电话号码、密码 hash 等)
七、完整实例:组件库 MCP Server
下面是一个可直接运行的组件库 MCP Server 完整实现:
// src/mcp-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
/**
* 前端团队组件库 MCP Server
* 将团队组件库的 API 文档暴露给 AI 模型
*/
// ---------- 组件库数据(实际项目中从 OpenComponentSpec JSON 文件加载) ----------
const componentDB = [
{
name: "Button",
description: "按钮组件,用于触发一个操作",
props: [
{ name: "type", type: "'primary' | 'secondary' | 'danger'", default: "'primary'", desc: "按钮类型" },
{ name: "size", type: "'small' | 'medium' | 'large'", default: "'medium'", desc: "按钮尺寸" },
{ name: "disabled", type: "boolean", default: false, desc: "是否禁用" },
{ name: "loading", type: "boolean", default: false, desc: "是否加载中" },
],
events: [
{ name: "onClick", type: "(e: MouseEvent) => void", desc: "点击事件" },
],
examples: [
`<Button type="primary" onClick={handleClick}>提交</Button>`,
`<Button type="danger" disabled>不可用</Button>`,
],
},
{
name: "Input",
description: "输入框组件",
props: [
{ name: "placeholder", type: "string", default: "''", desc: "占位文本" },
{ name: "maxLength", type: "number", default: "undefined", desc: "最大字符数" },
{ name: "clearable", type: "boolean", default: false, desc: "是否可清空" },
],
events: [
{ name: "onChange", type: "(value: string) => void", desc: "值变更事件" },
],
examples: [
`<Input placeholder="请输入用户名" onChange={(v) => setName(v)} />`,
],
},
{
name: "Form",
description: "表单容器组件,用于包裹表单字段并统一管理校验、提交",
props: [
{ name: "model", type: "Record<string, any>", default: "{}", desc: "表单数据模型" },
{ name: "rules", type: "Record<string, Rule[]>", default: "{}", desc: "校验规则" },
],
events: [
{ name: "onSubmit", type: "(model: Record<string, any>) => void", desc: "提交事件" },
],
examples: [
`<Form model={formData} rules={formRules} onSubmit={handleSubmit}>
<Form.Item label="用户名" prop="username">
<Input placeholder="请输入" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">提交</Button>
</Form.Item>
</Form>`,
],
},
];
// ---------- 创建 MCP Server ----------
const server = new McpServer({
name: "myteam-ui-mcp",
version: "1.0.0",
});
// Tool 1: 列出所有可用组件
server.tool(
"list_components",
"列出组件库中所有可用的组件名称及简要描述,用于 AI 了解有哪些组件可用",
{},
async () => {
const list = componentDB.map((c) => ({
name: c.name,
description: c.description,
}));
return {
content: [{ type: "text", text: JSON.stringify(list, null, 2) }],
};
}
);
// Tool 2: 获取单个组件的完整 API 文档
server.tool(
"get_component_doc",
"获取指定组件的完整 API 文档,包括 Props、Events 和使用示例。在需要使用某个组件时先调用此 Tool 获取用法。",
{
componentName: z
.string()
.describe("组件名称,如 Button、Input、Form 等"),
},
async ({ componentName }) => {
const component = componentDB.find(
(c) => c.name.toLowerCase() === componentName.toLowerCase()
);
if (!component) {
return {
content: [
{
type: "text",
text: `组件 "${componentName}" 未找到。可用组件:${componentDB.map((c) => c.name).join(", ")}`,
},
],
isError: true,
};
}
return {
content: [{ type: "text", text: JSON.stringify(component, null, 2) }],
};
}
);
// Tool 3: 搜索组件
server.tool(
"search_components",
"根据关键词搜索组件,返回匹配组件的基本信息和 API 文档,适用于不确定组件名称时",
{
keyword: z.string().describe("搜索关键词,会匹配组件名和描述"),
},
async ({ keyword }) => {
const results = componentDB.filter(
(c) =>
c.name.toLowerCase().includes(keyword.toLowerCase()) ||
c.description.toLowerCase().includes(keyword.toLowerCase())
);
if (results.length === 0) {
return {
content: [
{ type: "text", text: `未找到与 "${keyword}" 相关的组件` },
],
};
}
return {
content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
};
}
);
// ---------- 启动 ----------
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Team UI MCP Server running on stdio");
}
main().catch((err) => {
console.error("MCP Server 启动失败:", err);
process.exit(1);
});配套配置文件
// .claude/settings.json
{
"mcpServers": {
"myteam-ui": {
"command": "npx",
"args": ["tsx", "./src/mcp-server.ts"]
}
}
}八、效果量化参考
业界数据参考——字节跳动 Arco Design 2.0 构建 MCP 服务后的效果:
| 指标 | 接入前 | 接入后 | 提升 |
|---|---|---|---|
| 代码符合率 | 28% | 92% | +229% |
| 人工修改时间 | 45 分钟 | 5 分钟 | -89% |
| 视觉还原度 | 65% | 95% | +46% |
| 组件使用率 | 12% | 100% | +733% |
九、总结:MCP 开发检查清单
动手写一个 MCP Server 之前,按以下清单自检:
- 传输方式:标准场景用
StdioServerTransport,远程场景用 SSE - 命名:
领域_动作命名,如component_list、user_search - Description:写清楚"什么时候该用这个工具",而非简单一句话
- Input Schema:用 zod 精确定义参数类型、约束、默认值
- 返回格式:
{ content: [{ type: "text", text: "..." }] }结构 - 错误处理:不抛裸异常,返回
isError: true+ 清晰的错误文本 - 无状态:每个请求独立,不依赖前序调用
- 数据分页:大数据量必须分页
- 安全性:不暴露内部细节,敏感字段脱敏