mirror of
https://github.com/HappyLifeOk/cc-3-8-x-mcp.git
synced 2026-06-10 09:46:47 +00:00
重构: 去 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:
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "mcp-sdk"]
|
||||||
|
path = mcp-sdk
|
||||||
|
url = git@github.com:HappyLifeOk/universal-mcp-sdk.git
|
||||||
@@ -187,8 +187,14 @@ router 定期扫此目录,心跳超过 120s 的记录视为死亡自动剔除
|
|||||||
|
|
||||||
## 接入 Claude Code
|
## 接入 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
|
```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。
|
接入后 Claude Code 即可调用所有活跃编辑器的 tool,以及全局 offline prefab tools。
|
||||||
|
|||||||
@@ -556,7 +556,7 @@ exports.methods = {
|
|||||||
var _mcpServer = null;
|
var _mcpServer = null;
|
||||||
var MCP_DEFAULT_PORT = 7523;
|
var MCP_DEFAULT_PORT = 7523;
|
||||||
var REGISTRY_DIR = path.join(require('os').homedir(), '.cocos-mcp', 'editors');
|
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 工具名前缀,需能区分不同项目)。
|
* 计算项目短名(MCP 工具名前缀,需能区分不同项目)。
|
||||||
@@ -641,22 +641,7 @@ function findFreePort(startPort) {
|
|||||||
async function startMcpServer() {
|
async function startMcpServer() {
|
||||||
if (_mcpServer && _mcpServer.started) return;
|
if (_mcpServer && _mcpServer.started) return;
|
||||||
var port = await findFreePort(MCP_DEFAULT_PORT);
|
var port = await findFreePort(MCP_DEFAULT_PORT);
|
||||||
var sdk;
|
var sdk = require(SDK_PATH);
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 使用 mcp-sdk ──────────────────────────────────────────────
|
// ── 使用 mcp-sdk ──────────────────────────────────────────────
|
||||||
var tdef = require('./server/tools');
|
var tdef = require('./server/tools');
|
||||||
|
|||||||
Submodule
+1
Submodule mcp-sdk added at 321f0b8b61
@@ -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<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 };
|
|
||||||
Reference in New Issue
Block a user