Skip to content

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.png

2. 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())

三条硬性约束:

  1. stdout 只能输出协议数据。console.log 会破坏 JSON-RPC 流导致连接失败,日志一律用 console.error(写 stderr)。
  2. 缺失必需配置时立即退出。启动阶段校验环境变量,用 process.exit(1) 报错退出,否则客户端会看到一个"连上了但调不通"的服务。
  3. 错误用返回值表达,不要抛未捕获异常。

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

更多排查见本地调试与常见问题。


下一步 ​

AweeClaw AI 应用构建平台