Files
cc-3-8-x-mcp/router/src/http-json-rpc.js
T

129 lines
4.9 KiB
JavaScript
Raw Normal View History

'use strict';
const http = require('http');
const DEFAULT_PROBE_TIMEOUT_MS = 8000;
const DEFAULT_TOOL_TIMEOUT_MS = 60000;
const DEFAULT_LONG_TOOL_TIMEOUT_MS = 180000;
function positiveEnvInt(name, fallback) {
const raw = process.env[name];
if (!raw) return fallback;
const value = Number(raw);
return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
}
const PROBE_TIMEOUT_MS = positiveEnvInt('COCOS_MCP_PROBE_TIMEOUT_MS', DEFAULT_PROBE_TIMEOUT_MS);
const TOOL_TIMEOUT_MS = positiveEnvInt('COCOS_MCP_TOOL_TIMEOUT_MS', DEFAULT_TOOL_TIMEOUT_MS);
const LONG_TOOL_TIMEOUT_MS = positiveEnvInt('COCOS_MCP_LONG_TOOL_TIMEOUT_MS', DEFAULT_LONG_TOOL_TIMEOUT_MS);
function isLoopbackUrl(rawUrl, expectedPath) {
try {
const parsed = new URL(rawUrl);
const host = parsed.hostname.toLowerCase();
const isLoopback = host === '127.0.0.1' || host === 'localhost' || host === '[::1]' || host === '::1';
const port = Number(parsed.port);
return parsed.protocol === 'http:'
&& isLoopback
&& Number.isInteger(port) && port > 0 && port <= 65535
&& parsed.pathname === expectedPath
&& !parsed.username && !parsed.password
&& !parsed.search && !parsed.hash;
} catch (e) {
return false;
}
}
function isLoopbackBridgeUrl(rawUrl) {
return isLoopbackUrl(rawUrl, '/bridge');
}
function isPidAlive(pid) {
if (!Number.isInteger(Number(pid)) || Number(pid) <= 0) return false;
try {
process.kill(Number(pid), 0);
return true;
} catch (e) {
return e && e.code === 'EPERM';
}
}
function validateRegistryEntry(info) {
if (!info || typeof info !== 'object') return 'registry entry must be an object';
if (!isPidAlive(info.pid)) return 'editor pid is not alive';
if (typeof info.projectPath !== 'string' || !info.projectPath) return 'projectPath is required';
if (typeof info.projectShortName !== 'string' || !info.projectShortName) return 'projectShortName is required';
if (info.transport !== 'editor-bridge') return 'transport must be editor-bridge';
if (!isLoopbackBridgeUrl(info.url)) return 'editor-bridge endpoint must be http://loopback:<port>/bridge';
if (Number(info.gatewayApiVersion || 0) < 2) return 'editor-bridge requires gatewayApiVersion >= 2';
if (Number(info.bridgeApiVersion || 0) !== 1) return 'unsupported bridgeApiVersion';
if (!/^[0-9a-f]{64}$/i.test(info.authToken || '')) return 'editor-bridge authToken must be a 32-byte hex token';
return null;
}
function bridgeJsonRpc(targetUrl, body, options) {
options = options || {};
const timeoutMs = options.timeoutMs || PROBE_TIMEOUT_MS;
return new Promise(function (resolve, reject) {
try {
if (!isLoopbackBridgeUrl(targetUrl)) {
reject(new Error('refusing invalid loopback Editor Bridge endpoint'));
return;
}
const u = new URL(targetUrl);
const data = JSON.stringify(body);
const headers = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Content-Length': Buffer.byteLength(data),
'X-Cocos-Bridge-Version': '1',
};
if (options.authToken) headers.Authorization = 'Bearer ' + options.authToken;
const req = http.request({
hostname: u.hostname,
port: u.port,
path: u.pathname,
method: 'POST',
headers,
timeout: timeoutMs,
}, function (res) {
const chunks = [];
res.on('data', function (c) { chunks.push(c); });
res.on('end', function () {
const raw = Buffer.concat(chunks).toString('utf-8');
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(new Error('HTTP ' + res.statusCode + ' from Editor Bridge endpoint: ' + raw.slice(0, 160)));
return;
}
try { resolve(JSON.parse(raw)); }
catch (e) { reject(new Error('invalid json from ' + targetUrl + ': ' + raw.slice(0, 120))); }
});
});
req.on('error', reject);
req.on('timeout', function () { req.destroy(new Error('timeout after ' + timeoutMs + 'ms')); });
req.write(data);
req.end();
} catch (e) { reject(e); }
});
}
function toolTimeoutMs(name) {
return /(?:asset_refresh|asset_reimport|preview_refresh_and_reload|scene_open_scene|scene_save_scene)$/.test(name || '')
? LONG_TOOL_TIMEOUT_MS
: TOOL_TIMEOUT_MS;
}
module.exports = {
PROBE_TIMEOUT_MS,
TOOL_TIMEOUT_MS,
LONG_TOOL_TIMEOUT_MS,
isLoopbackUrl,
isLoopbackBridgeUrl,
isPidAlive,
validateRegistryEntry,
bridgeJsonRpc,
toolTimeoutMs,
};