MCP 插件开发
MCP 插件(type: "mcp")通过 Model Context Protocol 向智能体暴露工具,运行方式有三种:本地 stdio 入口、外部包(ConfigOnly)、远程 SSE。传输方式的选择见传输模式,本篇聚焦代码实现与工具设计。
与工具插件的对比
| 维度 | MCP 插件(stdio) | 工具插件(in-process) |
|---|---|---|
| 进程 | 独立子进程 | 主进程内 |
| 运行时依赖 | 需自行安装 @modelcontextprotocol/sdk | 由宿主注入 |
| 能否访问宿主原生能力 | 否 | 能 |
| 崩溃隔离 | 有 | 无 |
| 打包方式 | Tarball(本地入口)/ 无包(ConfigOnly) | Tarball / 内联代码 |
形态 A:本地 stdio 入口
1. 目录结构
plugins/my-mcp/
├── manifest.json
├── index.js
├── package.json # 声明 @modelcontextprotocol/sdk 依赖
└── icon/icon.png2. manifest.json
json
{
"id": "my-mcp",
"name": "My MCP",
"nameZh": "我的 MCP 服务",
"description": "Search and fetch data from example.com",
"descriptionZh": "从 example.com 搜索与获取数据",
"type": "mcp",
"version": "1.0.0",
"main": "index.js",
"icon": "Plug",
"category": "ai",
"capabilities": {
"mcp": {
"transport": "stdio",
"command": "node",
"args": ["{{pluginDir}}/index.js"],
"autoConnect": true,
"timeout": 30000
}
},
"configSchema": {
"fields": [
{
"key": "API_KEY",
"label": "API Key",
"labelZh": "API 密钥",
"type": "password",
"required": true,
"secret": true
}
]
},
"permissions": ["network"],
"platforms": ["darwin", "win32", "linux"],
"license": "MIT"
}3. index.js
javascript
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
const API_KEY = process.env.API_KEY
if (!API_KEY) {
// 注意:stdout 是协议通道,日志一律写 stderr
console.error('[my-mcp] missing API_KEY env var')
process.exit(1)
}
const server = new Server(
{ name: 'my-mcp', version: '1.0.0' },
{ capabilities: { tools: {} } },
)
// ① 声明工具清单
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'search',
description: 'Search example.com for a keyword and return top results',
inputSchema: {
type: 'object',
properties: {
keyword: { type: 'string', description: 'Search keyword' },
limit: { type: 'number', description: 'Max results, default 5' },
},
required: ['keyword'],
},
},
],
}))
// ② 处理调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params
switch (name) {
case 'search': {
if (!args?.keyword) {
return { content: [{ type: 'text', text: 'Missing required param: keyword' }], isError: true }
}
try {
const url = `https://api.example.com/search?q=${encodeURIComponent(args.keyword)}&limit=${args.limit ?? 5}`
const resp = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } })
if (!resp.ok) {
return {
content: [{ type: 'text', text: `API error: ${resp.status} ${resp.statusText}` }],
isError: true,
}
}
const data = await resp.json()
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }
} catch (e) {
return { content: [{ type: 'text', text: `Request failed: ${e.message}` }], isError: true }
}
}
default:
return { content: [{ type: 'text', text: `Unknown tool: ${name}` }], isError: true }
}
})
// ③ 启动 stdio 服务
await server.connect(new StdioServerTransport())三条硬性约束:
- stdout 只能输出协议数据。
console.log会破坏 JSON-RPC 流导致连接失败,日志一律用console.error(写 stderr)。 - 缺失必需配置时立即退出。启动阶段校验环境变量,用
process.exit(1)报错退出,否则客户端会看到一个"连上了但调不通"的服务。 - 错误用返回值表达,不要抛未捕获异常。
4. package.json
json
{
"name": "my-mcp",
"version": "1.0.0",
"type": "module",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0"
}
}打包会排除
node_modules。若插件依赖第三方库,需在发布前把依赖内联打包(用 esbuild 打成单文件),或改用 in-process 模式。
形态 B:外部包(ConfigOnly)
复用社区 MCP 包时不写代码,把 main 留空,command 指向 npx / uvx:
json
{
"id": "mcp-filesystem",
"name": "Filesystem",
"nameZh": "文件系统",
"description": "Secure file operations via MCP",
"descriptionZh": "通过 MCP 提供安全的文件操作",
"type": "mcp",
"version": "1.0.0",
"main": "",
"icon": "Folder",
"category": "filesystem",
"capabilities": {
"mcp": {
"transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "{{config.ALLOWED_DIR}}"],
"autoConnect": true
}
},
"configSchema": {
"fields": [
{
"key": "ALLOWED_DIR",
"label": "Allowed Directory",
"labelZh": "允许访问的目录",
"type": "text",
"required": true
}
]
},
"permissions": ["filesystem.read", "filesystem.write"]
}要点:
main为空字符串 不能省略,这是 ConfigOnly 模式的判定条件。category取值必须来自分类表,上例若用filesystem会校验失败 —— 应改为utility或development。- Python 生态用
"command": "uvx",args写 PyPI 包名;用户本机需已安装uv。 npx -y首次启动会联网拉包,网络受限环境会连接失败。
形态 C:远程 SSE
json
{
"id": "my-remote-mcp",
"type": "mcp",
"version": "1.0.0",
"main": "",
"capabilities": {
"mcp": {
"transport": "sse",
"url": "https://mcp.example.com/sse",
"autoConnect": true
}
},
"permissions": ["network"]
}若需要鉴权,把 Token 做成配置项,在 URL 或请求头中携带(具体取决于服务端约定)。
工具设计规范
MCP 工具是 LLM 直接看到的接口,描述质量决定调用准确率。
命名
| 规则 | 示例 |
|---|---|
| 小写 + 下划线 | search_web ✅ / searchWeb ❌ |
| 动词开头,语义单一 | create_item、list_items、delete_item |
| 不要用无意义前缀 | mcp_myplugin_do_search ❌ |
描述
描述要回答"做什么 + 何时用 + 返回什么":
javascript
// ❌ 太笼统,模型不知道何时调用
description: '搜索'
// ✅ 明确用途与边界
description: 'Search the web for up-to-date information. Use when the answer depends on current events. Returns a list of title/url/snippet.'参数
- 每个参数都写
description; - 用
enum收敛取值范围,避免模型乱填; - 用
default减少必填项数量; required只放真正必不可少的参数。
输出
- 结构化数据用
JSON.stringify(data, null, 2)输出,模型对 JSON 的解析稳定性最好; - 长文本结果建议截断并说明"已截断,共 N 条",避免撑爆上下文。
本地测试
MCP Inspector(推荐)
bash
cd plugins/my-mcp
API_KEY="your-test-key" npx @modelcontextprotocol/inspector node index.js打开浏览器界面即可列出工具、填参数、看返回。
手工发一帧初始化请求
bash
cd plugins/my-mcp
export API_KEY="your-test-key"
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' | node index.js语法与依赖自检
bash
node --check index.js
node -e "import('./index.js').catch(e => { console.error(e); process.exit(1) })"常见问题
| 现象 | 原因 |
|---|---|
| 连接成功但无工具 | ListToolsRequestSchema 未注册或返回结构不对 |
| 连接直接失败 | stdout 混入了 console.log 输出 |
| 启动即退出 | 缺少必填配置导致 process.exit(1),检查环境变量注入 |
| 调用默认 30 秒超时 | 长任务需在 manifest 里放大 capabilities.mcp.timeout |
npx 启动失败 | 用户机器无网络或未安装 Node |

