diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..dc37168 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "mcp-sdk"] + path = mcp-sdk + url = git@github.com:HappyLifeOk/universal-mcp-sdk.git diff --git a/README.md b/README.md index afa872e..9227d01 100644 --- a/README.md +++ b/README.md @@ -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。 diff --git a/main.js b/main.js index 0d0782b..ff95e22 100644 --- a/main.js +++ b/main.js @@ -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'); diff --git a/mcp-sdk b/mcp-sdk new file mode 160000 index 0000000..321f0b8 --- /dev/null +++ b/mcp-sdk @@ -0,0 +1 @@ +Subproject commit 321f0b8b61b7855d49d2baa505ad17afac434447 diff --git a/server/mcp-server.js b/server/mcp-server.js deleted file mode 100644 index 295162e..0000000 --- a/server/mcp-server.js +++ /dev/null @@ -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 /mcp,body 是 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} 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} */ - var tools = new Map(); - /** @type {MapPromise}>} */ - 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 };