Release v0.1.2

This commit is contained in:
winlifes
2026-04-30 18:12:29 +08:00
parent 8c46313ac6
commit a903f7f0af
12 changed files with 423 additions and 24 deletions
+132 -14
View File
@@ -1,12 +1,24 @@
'use strict';
const http = require('http');
const { safeStringify } = require('./utils');
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
const LOG_PREFIX = '[Funplay Cocos MCP Server]';
const MAX_PORT_FALLBACK_ATTEMPTS = 20;
const MAX_REQUEST_BODY_BYTES = 4 * 1024 * 1024;
const MCP_PROTOCOL_VERSION = '2025-11-25';
const SUPPORTED_PROTOCOL_VERSIONS = [
MCP_PROTOCOL_VERSION,
'2025-06-18',
'2025-03-26',
'2024-11-05',
];
function json(response, statusCode, payload) {
response.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
function json(response, statusCode, payload, protocolVersion = MCP_PROTOCOL_VERSION) {
response.writeHead(statusCode, {
'Content-Type': 'application/json; charset=utf-8',
'MCP-Protocol-Version': protocolVersion,
});
response.end(JSON.stringify(payload));
}
@@ -33,6 +45,22 @@ function textContent(value) {
];
}
function isStructuredValue(value) {
return value !== null && typeof value === 'object' && !Buffer.isBuffer(value);
}
function structuredContent(value) {
if (!isStructuredValue(value)) {
return null;
}
try {
return JSON.parse(safeStringify(value));
} catch (error) {
return null;
}
}
class McpServer {
constructor(options) {
this.config = options.config;
@@ -45,6 +73,7 @@ class McpServer {
this.server = null;
this.actualPort = null;
this.portFallbackInfo = null;
this.negotiatedProtocolVersion = MCP_PROTOCOL_VERSION;
}
isRunning() {
@@ -82,34 +111,54 @@ class McpServer {
try {
if (request.method === 'GET' && request.url === '/health') {
console.log(`${LOG_PREFIX} GET /health`);
return json(response, 200, { ok: true, name: this.serverName, version: this.serverVersion });
return json(response, 200, { ok: true, name: this.serverName, version: this.serverVersion }, this.negotiatedProtocolVersion);
}
if (!this.isAllowedOrigin(request)) {
console.warn(`${LOG_PREFIX} Rejected ${request.method} ${request.url}: invalid Origin header.`);
return json(response, 403, { error: 'Forbidden: invalid Origin header' }, this.negotiatedProtocolVersion);
}
if (request.method !== 'POST') {
console.warn(`${LOG_PREFIX} Rejected ${request.method} ${request.url}: method not allowed.`);
return json(response, 405, { error: 'Method Not Allowed' });
return json(response, 405, { error: 'Method Not Allowed' }, this.negotiatedProtocolVersion);
}
const body = await this.readBody(request);
if (!body) {
return json(response, 400, this.createError(null, -32700, 'Parse error: empty body'));
return json(response, 400, this.createError(null, -32700, 'Parse error: empty body'), this.negotiatedProtocolVersion);
}
let rpc;
try {
rpc = JSON.parse(body);
} catch (error) {
return json(response, 400, this.createError(null, -32700, `Parse error: ${error.message}`), this.negotiatedProtocolVersion);
}
const rpc = JSON.parse(body);
if (rpc && rpc.method) {
console.log(`${LOG_PREFIX} RPC ${rpc.method}`);
}
const protocolHeaderError = this.validateProtocolVersionHeader(request, rpc);
if (protocolHeaderError) {
return json(response, 400, protocolHeaderError, this.negotiatedProtocolVersion);
}
const result = await this.handleRpcRequest(rpc);
if (result == null) {
response.writeHead(204);
response.writeHead(204, { 'MCP-Protocol-Version': this.negotiatedProtocolVersion });
response.end();
return;
}
return json(response, 200, result);
return json(response, 200, result, this.negotiatedProtocolVersion);
} catch (error) {
console.error(`${LOG_PREFIX} Request handling failed: ${error.message}`);
return json(response, 500, this.createError(null, -32603, `Internal error: ${error.message}`));
const statusCode = error.statusCode || 500;
const rpcCode = error.rpcCode || -32603;
const message = statusCode === 500 ? `Internal error: ${error.message}` : error.message;
return json(response, statusCode, this.createError(null, rpcCode, message), this.negotiatedProtocolVersion);
}
};
@@ -209,12 +258,61 @@ class McpServer {
readBody(request) {
return new Promise((resolve, reject) => {
const chunks = [];
request.on('data', (chunk) => chunks.push(chunk));
let size = 0;
request.on('data', (chunk) => {
size += chunk.length;
if (size > MAX_REQUEST_BODY_BYTES) {
const error = new Error(`Request body exceeds ${MAX_REQUEST_BODY_BYTES} bytes.`);
error.statusCode = 413;
error.rpcCode = -32600;
reject(error);
request.destroy();
return;
}
chunks.push(chunk);
});
request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
request.on('error', reject);
});
}
isAllowedOrigin(request) {
const origin = request.headers && request.headers.origin;
if (!origin) {
return true;
}
try {
const parsed = new URL(String(origin));
const hostname = parsed.hostname.toLowerCase();
const configuredHost = String(this.config.host || '').toLowerCase();
return hostname === 'localhost'
|| hostname === '127.0.0.1'
|| hostname === '::1'
|| (configuredHost && hostname === configuredHost);
} catch (error) {
return false;
}
}
validateProtocolVersionHeader(request, rpc) {
const header = request.headers && request.headers['mcp-protocol-version'];
if (!header || (rpc && rpc.method === 'initialize')) {
return null;
}
const version = Array.isArray(header) ? header[0] : String(header);
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(version)) {
return this.createError(
rpc && rpc.id,
-32600,
`Unsupported MCP protocol version header: ${version}`
);
}
return null;
}
async handleRpcRequest(request) {
if (!request || request.jsonrpc !== '2.0') {
return this.createError(request && request.id, -32600, 'Invalid Request');
@@ -226,8 +324,9 @@ class McpServer {
}
if (method === 'initialize') {
this.negotiatedProtocolVersion = this.negotiateProtocolVersion(request.params && request.params.protocolVersion);
return this.createResult(request.id, {
protocolVersion: '2024-11-05',
protocolVersion: this.negotiatedProtocolVersion,
serverInfo: {
name: this.serverName,
version: this.serverVersion,
@@ -255,10 +354,20 @@ class McpServer {
}
try {
const output = await this.toolRegistry.callTool(params.name, params.arguments || {});
return this.createResult(request.id, { content: textContent(output) });
const output = typeof this.toolRegistry.callToolDetailed === 'function'
? await this.toolRegistry.callToolDetailed(params.name, params.arguments || {})
: { value: null, text: await this.toolRegistry.callTool(params.name, params.arguments || {}) };
const result = { content: textContent(output.text) };
const structured = structuredContent(output.value);
if (structured) {
result.structuredContent = structured;
}
return this.createResult(request.id, result);
} catch (error) {
return this.createError(request.id, -32603, error.message);
return this.createResult(request.id, {
content: textContent(error.message),
isError: true,
});
}
}
@@ -293,6 +402,13 @@ class McpServer {
return this.createError(request.id, -32601, `Method not found: ${method}`);
}
negotiateProtocolVersion(clientVersion) {
if (SUPPORTED_PROTOCOL_VERSIONS.includes(clientVersion)) {
return clientVersion;
}
return MCP_PROTOCOL_VERSION;
}
createResult(id, result) {
return {
jsonrpc: '2.0',
@@ -315,4 +431,6 @@ class McpServer {
module.exports = {
McpServer,
MCP_PROTOCOL_VERSION,
SUPPORTED_PROTOCOL_VERSIONS,
};