重构: 去 fallback,改 submodule 依赖 universal-mcp-sdk

- 加 universal-mcp-sdk 作 git submodule (mcp-sdk/),单一真相
- main.js: SDK_PATH 指向仓库内 submodule,删除 bundled fallback 分支
- 删除自带的 server/mcp-server.js (不再维护两套 MCP 实现)
- README: 加 submodule clone 说明
This commit is contained in:
furao
2026-06-06 13:51:02 +08:00
parent 14c5b00f14
commit 33b90dab22
5 changed files with 13 additions and 320 deletions
+3
View File
@@ -0,0 +1,3 @@
[submodule "mcp-sdk"]
path = mcp-sdk
url = git@github.com:HappyLifeOk/universal-mcp-sdk.git
+7 -1
View File
@@ -187,8 +187,14 @@ router 定期扫此目录,心跳超过 120s 的记录视为死亡自动剔除
## 接入 Claude Code
> ⚠️ 本仓库通过 git submodule 依赖 [universal-mcp-sdk](https://github.com/HappyLifeOk/universal-mcp-sdk)clone 后**必须先拉 submodule**,否则 MCP server 起不来:
>
> ```bash
> git submodule update --init --recursive
> ```
```bash
claude mcp add cocos -- node /path/to/forest/extensions/cc-3-8-x-mcp/router/bin.js
claude mcp add cocos -- node /path/to/cc-3-8-x-mcp/router/bin.js
```
接入后 Claude Code 即可调用所有活跃编辑器的 tool,以及全局 offline prefab tools。
+2 -17
View File
@@ -556,7 +556,7 @@ exports.methods = {
var _mcpServer = null;
var MCP_DEFAULT_PORT = 7523;
var REGISTRY_DIR = path.join(require('os').homedir(), '.cocos-mcp', 'editors');
var SDK_PATH = path.join(__dirname, '..', 'mcp-sdk', 'index.js');
var SDK_PATH = path.join(__dirname, 'mcp-sdk', 'index.js');
/**
* 计算项目短名(MCP 工具名前缀,需能区分不同项目)。
@@ -641,22 +641,7 @@ function findFreePort(startPort) {
async function startMcpServer() {
if (_mcpServer && _mcpServer.started) return;
var port = await findFreePort(MCP_DEFAULT_PORT);
var sdk;
try {
sdk = require(SDK_PATH);
} catch (e) {
// SDK not found, fall back to bundled mcp-server
var mcp = require('./server/mcp-server');
_mcpServer = mcp.createServer({ port: port, host: '127.0.0.1', logger: console });
var tdef = require('./server/tools');
var ctx = buildToolCtx();
tdef.defineTools(ctx).forEach(function (t) { _mcpServer.registerTool(t); });
tdef.defineResources(ctx).forEach(function (r) { _mcpServer.registerResource(r); });
await _mcpServer.start();
writeRegistry();
log('MCP server up (bundled) — http://127.0.0.1:' + port + '/mcp');
return;
}
var sdk = require(SDK_PATH);
// ── 使用 mcp-sdk ──────────────────────────────────────────────
var tdef = require('./server/tools');
Submodule
+1
Submodule mcp-sdk added at 321f0b8b61
-302
View File
@@ -1,302 +0,0 @@
'use strict';
/**
* 最小 MCP Server 实现JSON-RPC 2.0 over HTTP
*
* 协议参考https://modelcontextprotocol.io/
* 只实现 MCP 客户端常用的方法 Claude Code / Cursor 调用即可
* - initialize
* - tools/list
* - tools/call
* - resources/list
* - resources/read
* - ping
*
* 传输streamable HTTP 子集
* - 唯一端点 POST /mcpbody JSON-RPC 请求response JSON-RPC 响应
* - 不处理 SSE / session resumption第一版足够
* - 额外暴露 GET /status 用于面板自检
*/
var http = require('http');
var url = require('url');
var SERVER_INFO = {
name: 'cc-3-8-x-mcp',
version: '2.0.0',
};
var PROTOCOL_VERSION = '2024-11-05';
/**
* @typedef {Object} ToolDef
* @property {string} name
* @property {string} description
* @property {object} inputSchema JSON Schema
* @property {(args: object) => Promise<any>} handler
*/
function createServer(options) {
options = options || {};
var port = options.port || 7523;
var host = options.host || '127.0.0.1';
var logger = options.logger || console;
/** @type {Map<string, ToolDef>} */
var tools = new Map();
/** @type {Map<string, {uri:string, name:string, description:string, mimeType:string, read:()=>Promise<any>}>} */
var resources = new Map();
var httpServer = null;
var started = false;
var stats = {
startedAt: null,
requestCount: 0,
lastRequest: null,
lastError: null,
};
function registerTool(def) {
if (!def || !def.name || typeof def.handler !== 'function') {
throw new Error('invalid tool def');
}
tools.set(def.name, def);
}
function registerResource(def) {
if (!def || !def.uri || typeof def.read !== 'function') {
throw new Error('invalid resource def');
}
resources.set(def.uri, def);
}
/** 把 tool handler 抛错统一转成 JSON-RPC error payload */
async function callTool(name, args) {
var tool = tools.get(name);
if (!tool) {
var err = new Error('unknown tool: ' + name);
err.code = -32601;
throw err;
}
try {
var result = await tool.handler(args || {});
// MCP tools/call result shape: { content: [{type:'text', text:...}], isError?: boolean }
if (result && result.content) return result;
return {
content: [{
type: 'text',
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2),
}],
};
} catch (e) {
return {
content: [{
type: 'text',
text: 'Error: ' + (e && (e.stack || e.message) || String(e)),
}],
isError: true,
};
}
}
async function handleJsonRpc(req) {
// 批量请求
if (Array.isArray(req)) {
var out = [];
for (var i = 0; i < req.length; i++) {
var r = await handleJsonRpc(req[i]);
if (r) out.push(r);
}
return out;
}
if (!req || req.jsonrpc !== '2.0') {
return { jsonrpc: '2.0', id: (req && req.id) || null, error: { code: -32600, message: 'invalid request' } };
}
var id = req.id;
var method = req.method;
var params = req.params || {};
try {
var result;
switch (method) {
case 'initialize':
result = {
protocolVersion: PROTOCOL_VERSION,
serverInfo: SERVER_INFO,
capabilities: {
tools: { listChanged: false },
resources: { subscribe: false, listChanged: false },
logging: {},
},
};
break;
case 'initialized':
case 'notifications/initialized':
// notification, no response
if (id == null) return null;
result = {};
break;
case 'ping':
result = {};
break;
case 'tools/list':
result = {
tools: Array.from(tools.values()).map(function (t) {
return {
name: t.name,
description: t.description || '',
inputSchema: t.inputSchema || { type: 'object', properties: {} },
};
}),
};
break;
case 'tools/call':
result = await callTool(params.name, params.arguments);
break;
case 'resources/list':
result = {
resources: Array.from(resources.values()).map(function (r) {
return {
uri: r.uri,
name: r.name || r.uri,
description: r.description || '',
mimeType: r.mimeType || 'application/json',
};
}),
};
break;
case 'resources/read':
var uri = params.uri;
var res = resources.get(uri);
if (!res) {
throw Object.assign(new Error('unknown resource: ' + uri), { code: -32602 });
}
var content = await res.read();
result = {
contents: [{
uri: res.uri,
mimeType: res.mimeType || 'application/json',
text: typeof content === 'string' ? content : JSON.stringify(content, null, 2),
}],
};
break;
default:
throw Object.assign(new Error('method not found: ' + method), { code: -32601 });
}
// notification: id 缺失,不返回
if (id == null) return null;
return { jsonrpc: '2.0', id: id, result: result };
} catch (e) {
stats.lastError = { at: new Date().toISOString(), method: method, message: e.message };
if (id == null) return null;
return {
jsonrpc: '2.0',
id: id,
error: {
code: e.code || -32603,
message: e.message || 'internal error',
},
};
}
}
function handleHttp(httpReq, httpRes) {
var parsed = url.parse(httpReq.url, true);
var pathname = parsed.pathname || '/';
// CORS / 允许任意来源本机调试
httpRes.setHeader('Access-Control-Allow-Origin', '*');
httpRes.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
httpRes.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
if (httpReq.method === 'OPTIONS') {
httpRes.writeHead(204);
httpRes.end();
return;
}
if (pathname === '/status' && httpReq.method === 'GET') {
httpRes.writeHead(200, { 'Content-Type': 'application/json' });
httpRes.end(JSON.stringify({
server: SERVER_INFO,
protocolVersion: PROTOCOL_VERSION,
toolCount: tools.size,
resourceCount: resources.size,
stats: stats,
}));
return;
}
if (pathname === '/mcp' && httpReq.method === 'POST') {
var chunks = [];
httpReq.on('data', function (c) { chunks.push(c); });
httpReq.on('end', async function () {
var raw = Buffer.concat(chunks).toString('utf-8');
var body;
try { body = JSON.parse(raw); }
catch (e) {
httpRes.writeHead(400, { 'Content-Type': 'application/json' });
httpRes.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'parse error' } }));
return;
}
stats.requestCount++;
stats.lastRequest = {
at: new Date().toISOString(),
method: body && body.method,
};
var response = await handleJsonRpc(body);
httpRes.writeHead(response == null ? 204 : 200, { 'Content-Type': 'application/json' });
httpRes.end(response == null ? '' : JSON.stringify(response));
});
return;
}
httpRes.writeHead(404, { 'Content-Type': 'text/plain' });
httpRes.end('not found');
}
function start() {
if (started) return Promise.resolve({ port: port, host: host });
return new Promise(function (resolve, reject) {
httpServer = http.createServer(handleHttp);
httpServer.on('error', function (e) {
if (!started) reject(e);
else logger.warn('[cc-mcp] server error:', e.message);
});
httpServer.listen(port, host, function () {
started = true;
stats.startedAt = new Date().toISOString();
logger.log('[cc-mcp] MCP server listening http://' + host + ':' + port + '/mcp');
resolve({ port: port, host: host });
});
});
}
function stop() {
if (!started || !httpServer) return Promise.resolve();
return new Promise(function (resolve) {
httpServer.close(function () {
started = false;
httpServer = null;
logger.log('[cc-mcp] MCP server stopped');
resolve();
});
});
}
return {
registerTool: registerTool,
registerResource: registerResource,
start: start,
stop: stop,
get started() { return started; },
get port() { return port; },
get host() { return host; },
get toolCount() { return tools.size; },
get resourceCount() { return resources.size; },
get stats() { return stats; },
};
}
module.exports = { createServer: createServer, PROTOCOL_VERSION: PROTOCOL_VERSION };