universal-mcp-sdk
by dekaic
README.md
# universal-mcp-sdk
跨 Agent 的 MCP Server 开发套件。一次实现,多端接入。
**已实测接通:**
- **Claude Code** / **Cursor** — stdio 传输
- **Mavis** — streamable-http 传输
- **OpenAI / ChatGPT** — remote MCP(streamable-http,需公网 HTTPS `/mcp` 或 Secure MCP Tunnel)
- **任何 HTTP-based AI Agent** — streamable-http 传输
> OpenAI Responses API 与 ChatGPT Apps 侧接入时,`server_url` 必须是外部可访问的 HTTPS `/mcp` 端点;本机 `127.0.0.1` 只适合本地客户端或通过 Secure MCP Tunnel / ngrok / Cloudflare Tunnel 暴露。
---
## 安装
```bash
npm install universal-mcp-sdk
# 或直接复制本目录到你的项目
```
---
## 快速开始
```js
const { createServer, tool, textContent } = require('universal-mcp-sdk');
const server = createServer({
name: 'my-first-mcp',
version: '1.0.0',
tools: [
tool('hello', '打招呼', { name: { type: 'string' } },
async ({ name }) => textContent(`Hello, ${name}!`)),
tool('add', '两数相加', { a: { type: 'number' }, b: { type: 'number' } },
async ({ a, b }) => textContent(String(a + b))),
],
resources: [
{
uri: 'hello://greeting',
name: 'Default Greeting',
description: '默认问候语',
mimeType: 'text/plain',
read: async () => 'Hello World',
},
],
});
server.start().catch((error) => {
console.error(error);
process.exitCode = 1;
});
```
直接 `node your-server.js` 启动时,SDK 会自动检测运行环境:
- **TTY 终端**(交互式启动)→ stdio 模式
- **非 TTY / 后台服务**(默认)→ HTTP 模式,监听 `http://127.0.0.1:8080/mcp`
HTTP 模式直接执行 bind;若端口已被占用,会从请求端口开始递增重试(最多 20 次),避免“先探测空闲、稍后监听”的并发竞态。需要记录实际 endpoint 时应等待返回值:
```js
const started = await server.start('http');
console.log(started.port, started.host, started.path);
```
需要强制指定模式时:
```bash
node your-server.js --stdio # 强制 stdio(Claude Code / Cursor 子进程)
node your-server.js --http # 强制 HTTP
node your-server.js --http --port 9000 # HTTP + 指定端口
```
---
## 完整示例
```js
const {
createServer,
tool,
command,
staticResource,
dynamicResource,
textContent,
errorContent,
imageContentFromFile,
} = require('universal-mcp-sdk');
const server = createServer({
name: 'my-mcp-server',
version: '2.0.0',
tools: [
tool('scene_query_node', '查询场景节点',
{ uuid: { type: 'string' } },
async ({ uuid }) => {
const data = await queryNode(uuid);
return textContent(JSON.stringify(data, null, 2));
}
),
tool('preview_screenshot', '截图预览页面',
{ path: { type: 'string' } },
async ({ path }) => {
const absPath = await captureScreenshot(path);
return textContent(absPath);
}
),
command('preview_refresh', '刷新预览', async () => {
await doRefresh();
return textContent('ok');
}),
],
resources: [
staticResource('project://info', 'Project Info',
JSON.stringify({ name: 'MyGame', version: '1.0.0' })
),
dynamicResource('scene://tree', 'Current Scene Tree', '当前场景节点树',
async () => await querySceneTree()
),
],
});
server.start();
```
---
## 启动模式
| 模式 | 命令 | 适用场景 |
|------|------|---------|
| stdio | `node your-server.js --stdio` | Claude Code / Cursor(子进程) |
| http | `node your-server.js --http --port 8080` | Mavis / OpenAI / HTTP Agent(常驻服务),--port 真正生效 |
| auto | `node your-server.js` | 自动检测:TTY → stdio,否则 → HTTP |
---
## 接入协议(Wire Protocol)
任何客户端 / Agent 接入本 SDK 起的 server,都走 **MCP over JSON-RPC 2.0**,协议版本 `2024-11-05`。stdio 与 streamable-http 两种通道的**消息体格式完全一致**,区别只在传输方式。
### 握手顺序
```
client server
│── initialize ───────────────────────▶│ 必须最先调,换取 protocolVersion / capabilities / serverInfo
│◀──────────────────────── result ─────│
│── notifications/initialized ────────▶│ (可选) 通知,无 id、无响应
│── tools/list ───────────────────────▶│ 枚举工具
│◀──────────────────────── result ─────│
│── tools/call {name, arguments} ─────▶│ 调用工具
│◀──────────────────────── result ─────│
```
### 方法表
| method | params | 响应 | 备注 |
|--------|--------|------|------|
| `initialize` | — | `{protocolVersion, capabilities, serverInfo}` | 握手,必须第一个调 |
| `notifications/initialized`(亦接受 `initialized`) | — | 无 | 通知,无 id 不回包 |
| `ping` | — | `{}` | 心跳 |
| `tools/list` | — | `{tools:[{name, description, inputSchema}]}` | 列工具 |
| `tools/call` | `{name, arguments}` | `{content:[…]}` | 调工具 |
| `resources/list` | — | `{resources:[{uri, name, description, mimeType}]}` | 列资源 |
| `resources/read` | `{uri}` | `{contents:[{type, text, mimeType}]}` | 读资源 |
| 自定义 method | 任意 | 由 `customHandlers` 返回 | 见 `createServer` 的 `customHandlers` |
> 调用任何业务方法前**必须先 `initialize`**;未知 method 一律返回 `-32601`。
### 消息格式
```jsonc
// 请求(带 id,要响应)
{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hello","arguments":{"name":"world"}}}
// 通知(无 id,不响应)
{"jsonrpc":"2.0","method":"notifications/initialized"}
// 成功响应
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Hello, world!"}]}}
```
### 错误处理(分两层,接入方必须分清)
**① 协议级错误** —— 返回标准 JSON-RPC `error` 字段:
| code | 触发条件 |
|------|---------|
| `-32700` | body 不是合法 JSON(Parse error) |
| `-32600` | 不是合法 JSON-RPC 2.0(缺 `jsonrpc:"2.0"` / `method` 非字符串 / HTTP 用了非 POST 方法) |
| `-32601` | method 不存在 |
| `-32603` | 服务器内部异常 |
**② 工具执行错误** —— `tools/call` / `resources/read` 中 handler 抛异常、或 tool/resource 不存在时,**不返回协议 `error`**,而是返回**成功响应**,错误信息塞进 content、文本以 `[Error]` 开头:
```json
{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"[Error] [hello] Error: something broke"}],"isError":true}}
```
> handler 异常和缺少 schema 顶层 `required` 参数时会同时返回 `[Error]` 文本与 `isError: true`。兼容旧调用方时仍应识别 `[Error]` 文本;当前只统一检查顶层 required,不执行完整 JSON Schema 校验。
### content block 类型
handler 返回值会被包成 content block 数组,可用类型:
| type | 结构 | 辅助函数 |
|------|------|---------|
| text | `{type:'text', text, mimeType?}` | `textContent(text, mimeType?)` |
| image | `{type:'image', data(base64), mimeType, width?, height?}` | `imageContent()` / `imageContentFromFile(path)` |
| audio | `{type:'audio', data(base64), mimeType}` | `audioContent()` |
handler 直接返回 string / object 时,SDK 自动包成 text(见下方 API 参考)。
### 两种传输通道
**stdio** —— 被 Agent 当子进程拉起(Claude Code / Cursor):
- 每行一条 JSON-RPC 消息,`\n` 分隔 → 消息体必须是**单行 compact JSON**,中间不能有换行
- client 写 `stdin`,server 响应写 `stdout`(每条以 `\n` 结尾);通知无响应
- `stderr` 仅用于日志,**不混 JSON-RPC**
**streamable-http** —— 常驻服务(Mavis / 任何 HTTP Agent):
| 端点 | 方法 | 作用 |
|------|------|------|
| `/mcp` | `POST` | 主通道:body = JSON-RPC 请求,回 `200 application/json`;通知 → `204` 无 body |
| `/mcp` | `GET` | 返回 server 信息 + 端点说明(**注意:握手 `initialize` 走 POST,不是这个 GET**) |
| `/` | `GET` | 健康检查 `{status:"ok"}` |
- 所有端点开 **CORS**(`Access-Control-Allow-Origin: *`),浏览器 / 跨域客户端可直连,`OPTIONS` 预检回 `204`
- **无 session、无 SSE**:每个 POST 是独立的请求 / 响应,接入方不需要维护 session id
---
## 注册到不同 Agent
### Claude Code / Cursor
在项目根目录创建 `.mcp.json`:
```json
{
"mcpServers": {
"my-mcp": {
"command": "node",
"args": ["/path/to/your-server.js", "--stdio"]
}
}
}
```
### Mavis
在 `~/.mavis/mcp/mcp.json`(或项目 `.mavis/mcp/mcp.json`)添加:
```json
{
"mcpServers": {
"my-mcp": {
"url": "http://127.0.0.1:8080/mcp",
"type": "streamable-http",
"env": {},
"enabled": true,
"description": "我的 MCP 服务"
}
}
}
```
### OpenAI Responses API / ChatGPT
OpenAI 侧使用 remote MCP 时,需要一个公网 HTTPS `/mcp` 地址。本地调试可先用 Secure MCP Tunnel、ngrok 或 Cloudflare Tunnel 把 `http://127.0.0.1:<port>/mcp` 暴露出去。
Responses API 示例:
```js
const OpenAI = require('openai');
const client = new OpenAI();
const resp = await client.responses.create({
model: 'gpt-5.5',
tools: [{
type: 'mcp',
server_label: 'my_mcp',
server_description: 'My MCP server',
server_url: 'https://your-domain.example/mcp',
require_approval: 'always',
}],
input: 'Call a tool from my MCP server.',
});
console.log(resp.output_text);
```
ChatGPT Apps / Connectors 开发者模式里创建 connector 时,`Connector URL` 填同一个公网 HTTPS `/mcp` 地址。
### 其他 HTTP Agent
直接 POST 请求到 `http://127.0.0.1:8080/mcp`:
```bash
# 1) 握手 initialize(走 POST,不是 GET —— GET /mcp 只返回 server 信息)
curl -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
# 2) 列工具
curl -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
# 3) 调用 tool
curl -X POST http://127.0.0.1:8080/mcp \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hello","arguments":{"name":"world"}}}'
```
---
## API 参考
### createServer(options)
| 选项 | 类型 | 必需 | 说明 |
|------|------|------|------|
| `name` | string | 是 | 服务器名称 |
| `version` | string | 是 | 服务器版本 |
| `tools` | Array | 否 | tool 定义列表 |
| `resources` | Array | 否 | resource 定义列表 |
| `customHandlers` | object | 否 | 自定义 JSON-RPC 方法 |
| `port` | number | 否 | HTTP 模式起始端口,默认 8080;占用时递增 bind |
| `entryPoint` | string | 否 | 用于生成配置文件的入口路径 |
### tool(name, description, inputSchema, handler)
- `name`: 工具唯一标识,snake_case
- `description`: AI 靠这个理解工具用途
- `inputSchema`: JSON Schema(定义参数结构)
- `handler`: `async (args) => result`
handler 返回值会自动包装为 MCP content block:
| 返回类型 | 包装方式 |
|---------|---------|
| `string` | `textContent(text)` |
| `object` | `textContent(JSON.stringify(result))` |
| `Array` (content block) | 直接使用 |
| `undefined` / `null` | `textContent('(ok)')` |
`server.start(mode)` 返回 Promise。HTTP 模式 resolve 为 `{mode:'http', port, host, path}`,且只在真实监听成功后完成;`server.stop()` 也返回 Promise,并等待 HTTP listener 关闭。tool schema 的顶层 `required` 字段会在 handler 前检查。
### command(name, description, handler)
无参数工具的简写,内部调用 `tool()` 并传入空 `inputSchema`。
### staticResource(uri, name, text, mimeType)
静态文本 resource,每次 read 返回相同内容。
### dynamicResource(uri, name, description, readFn, mimeType)
动态 resource,每次 read 调用 `readFn()` 获取最新内容。
---
## 动态注册
```js
server.addTool({
name: 'dynamic_tool',
description: '运行时动态添加的工具',
inputSchema: { type: 'object', properties: {} },
handler: async () => textContent('dynamic!'),
});
server.addResource({
uri: 'dynamic://resource',
name: 'Dynamic Resource',
description: '运行时添加',
mimeType: 'text/plain',
read: async () => 'fresh data',
});
```
---
## 配置生成
```js
server.printConfig();
```
输出 Claude Code 配置和 Mavis 配置,可直接复制使用。
---
## 文件结构
```
mcp-sdk/
├── index.js # 入口,createServer()
├── tool.js # tool 定义辅助
├── resource.js # resource 定义辅助
├── test-server.js # 可运行的测试示例服务器
├── test/ # node:test 回归(端口并发与 required 参数)
├── protocol/
│ ├── dispatcher.js # JSON-RPC 2.0 核心分发
│ └── content.js # content block 封装
├── transport/
│ ├── stdio.js # Claude Code / Cursor 传输
│ └── streamable-http.js # Mavis / HTTP Agent 传输
└── README.md
```
---
## License
Apache-2.0 — Copyright 2026 dekaic. See [LICENSE](./LICENSE) for details.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues