mirror of
https://github.com/HappyLifeOk/cc-3-8-x-mcp.git
synced 2026-08-21 22:17:08 +00:00
[cc-3-8-x-mcp] 重构: 改为 Gateway 私有 Editor Bridge
改了什么:移除项目级 MCP Server 与 universal-mcp-sdk,新增带随机 token 的本机 Editor Bridge、Gateway v2 注册和分级超时转发。 为什么:MCP 协议与多项目路由统一交给全局 Gateway,项目扩展只保留 Cocos Editor.Message 和离线 CLI 职责。 影响范围:扩展启动与面板、编辑器注册、router 转发、安全边界、使用文档及自动化测试。
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
'use strict';
|
||||
|
||||
const { test, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const http = require('node:http');
|
||||
const { once } = require('node:events');
|
||||
|
||||
const rpc = require('../src/http-json-rpc.js');
|
||||
|
||||
let server;
|
||||
let bridgeUrl;
|
||||
let lastRequest;
|
||||
|
||||
before(async () => {
|
||||
server = http.createServer((req, res) => {
|
||||
const chunks = [];
|
||||
req.on('data', (chunk) => chunks.push(chunk));
|
||||
req.on('end', () => {
|
||||
const body = JSON.parse(Buffer.concat(chunks).toString('utf8'));
|
||||
lastRequest = { headers: req.headers, body };
|
||||
if (body.method === 'slow') {
|
||||
setTimeout(() => {
|
||||
if (!res.writableEnded) {
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ jsonrpc: '2.0', id: body.id, result: {} }));
|
||||
}
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ jsonrpc: '2.0', id: body.id, result: { ok: true } }));
|
||||
});
|
||||
});
|
||||
server.listen(0, '127.0.0.1');
|
||||
await once(server, 'listening');
|
||||
bridgeUrl = `http://127.0.0.1:${server.address().port}/bridge`;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
if (!server) return;
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
});
|
||||
|
||||
test('Editor Bridge 只接受带显式端口的 loopback /bridge URL', () => {
|
||||
assert.equal(rpc.isLoopbackBridgeUrl('http://127.0.0.1:7523/bridge'), true);
|
||||
assert.equal(rpc.isLoopbackBridgeUrl('http://localhost:7523/bridge'), true);
|
||||
assert.equal(rpc.isLoopbackBridgeUrl('http://127.0.0.1:7523/mcp'), false);
|
||||
assert.equal(rpc.isLoopbackBridgeUrl('https://127.0.0.1:7523/bridge'), false);
|
||||
assert.equal(rpc.isLoopbackBridgeUrl('http://192.168.1.2:7523/bridge'), false);
|
||||
});
|
||||
|
||||
test('gateway v2 editor-bridge registry 必须声明 bridge 版本、loopback 地址和 token', () => {
|
||||
const base = {
|
||||
url: bridgeUrl,
|
||||
transport: 'editor-bridge',
|
||||
pid: process.pid,
|
||||
projectPath: '/tmp/project',
|
||||
projectShortName: 'project',
|
||||
gatewayApiVersion: 2,
|
||||
bridgeApiVersion: 1,
|
||||
authToken: 'd'.repeat(64),
|
||||
};
|
||||
assert.equal(rpc.validateRegistryEntry(base), null);
|
||||
assert.match(rpc.validateRegistryEntry(Object.assign({}, base, { url: 'http://127.0.0.1:7523/mcp' })), /bridge/);
|
||||
assert.match(rpc.validateRegistryEntry(Object.assign({}, base, { transport: 'mcp' })), /transport/);
|
||||
assert.match(rpc.validateRegistryEntry(Object.assign({}, base, { bridgeApiVersion: 2 })), /bridgeApiVersion/);
|
||||
assert.match(rpc.validateRegistryEntry(Object.assign({}, base, { gatewayApiVersion: 1 })), /gatewayApiVersion/);
|
||||
assert.match(rpc.validateRegistryEntry(Object.assign({}, base, { authToken: '' })), /authToken/);
|
||||
assert.match(rpc.validateRegistryEntry(Object.assign({}, base, { pid: 99999999 })), /not alive/);
|
||||
});
|
||||
|
||||
test('Bridge 转发只携带私有 Bridge 版本和 Authorization,不伪装 MCP 下游', async () => {
|
||||
const result = await rpc.bridgeJsonRpc(bridgeUrl, {
|
||||
jsonrpc: '2.0', id: 11, method: 'bridge/invoke',
|
||||
params: { name: 'scene_query_node', arguments: {} },
|
||||
}, {
|
||||
authToken: 'e'.repeat(64),
|
||||
timeoutMs: 1000,
|
||||
});
|
||||
|
||||
assert.deepEqual(result.result, { ok: true });
|
||||
assert.equal(lastRequest.headers['x-cocos-bridge-version'], '1');
|
||||
assert.equal(lastRequest.headers['mcp-protocol-version'], undefined);
|
||||
assert.equal(lastRequest.headers['mcp-method'], undefined);
|
||||
assert.equal(lastRequest.headers.authorization, `Bearer ${'e'.repeat(64)}`);
|
||||
});
|
||||
|
||||
test('拒绝非 loopback Bridge 目标,并对下游超时给出明确错误', async () => {
|
||||
await assert.rejects(
|
||||
rpc.bridgeJsonRpc('http://example.com:80/bridge', { jsonrpc: '2.0', id: 2, method: 'bridge/ping' }),
|
||||
/invalid loopback/
|
||||
);
|
||||
await assert.rejects(
|
||||
rpc.bridgeJsonRpc(bridgeUrl, { jsonrpc: '2.0', id: 3, method: 'slow' }, { timeoutMs: 20 }),
|
||||
/timeout after 20ms/
|
||||
);
|
||||
});
|
||||
|
||||
test('资源刷新和场景保存类操作使用长超时', () => {
|
||||
assert.equal(rpc.toolTimeoutMs('asset_refresh'), rpc.LONG_TOOL_TIMEOUT_MS);
|
||||
assert.equal(rpc.toolTimeoutMs('preview_refresh_and_reload'), rpc.LONG_TOOL_TIMEOUT_MS);
|
||||
assert.equal(rpc.toolTimeoutMs('scene_save_scene'), rpc.LONG_TOOL_TIMEOUT_MS);
|
||||
assert.equal(rpc.toolTimeoutMs('scene_query_node'), rpc.TOOL_TIMEOUT_MS);
|
||||
});
|
||||
Reference in New Issue
Block a user