Initial commit: funplay cocos mcp

This commit is contained in:
winlifes
2026-04-15 17:45:24 +08:00
commit efd3330922
19 changed files with 5379 additions and 0 deletions
+234
View File
@@ -0,0 +1,234 @@
'use strict';
const http = require('http');
const IMAGE_DATA_URI_PREFIX = 'data:image/png;base64,';
const LOG_PREFIX = '[Funplay Cocos MCP Server]';
function json(response, statusCode, payload) {
response.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
response.end(JSON.stringify(payload));
}
function textContent(value) {
if (typeof value === 'string' && value.startsWith(IMAGE_DATA_URI_PREFIX)) {
return [
{
type: 'image',
data: value.slice(IMAGE_DATA_URI_PREFIX.length),
mimeType: 'image/png',
},
{
type: 'text',
text: 'Screenshot captured successfully.',
},
];
}
return [
{
type: 'text',
text: typeof value === 'string' ? value : JSON.stringify(value, null, 2),
},
];
}
class McpServer {
constructor(options) {
this.config = options.config;
this.toolRegistry = options.toolRegistry;
this.resourceProvider = options.resourceProvider;
this.promptProvider = options.promptProvider;
this.interactionLog = options.interactionLog;
this.serverName = options.serverName;
this.serverVersion = options.serverVersion;
this.server = null;
}
isRunning() {
return Boolean(this.server && this.server.listening);
}
async start() {
if (this.isRunning()) {
console.log(`${LOG_PREFIX} Start skipped: already running.`);
return;
}
console.log(`${LOG_PREFIX} Creating HTTP server on ${this.config.host}:${this.config.port}...`);
this.server = http.createServer(async (request, response) => {
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 });
}
if (request.method !== 'POST') {
console.warn(`${LOG_PREFIX} Rejected ${request.method} ${request.url}: method not allowed.`);
return json(response, 405, { error: 'Method Not Allowed' });
}
const body = await this.readBody(request);
if (!body) {
return json(response, 400, this.createError(null, -32700, 'Parse error: empty body'));
}
const rpc = JSON.parse(body);
if (rpc && rpc.method) {
console.log(`${LOG_PREFIX} RPC ${rpc.method}`);
}
const result = await this.handleRpcRequest(rpc);
if (result == null) {
response.writeHead(204);
response.end();
return;
}
return json(response, 200, result);
} catch (error) {
console.error(`${LOG_PREFIX} Request handling failed: ${error.message}`);
return json(response, 500, this.createError(null, -32603, `Internal error: ${error.message}`));
}
});
await new Promise((resolve, reject) => {
this.server.once('error', reject);
this.server.listen(this.config.port, this.config.host, () => {
this.server.off('error', reject);
console.log(`${LOG_PREFIX} Listening on http://${this.config.host}:${this.config.port}/`);
resolve();
});
});
}
async stop() {
if (!this.server) {
console.log(`${LOG_PREFIX} Stop skipped: server object is empty.`);
return;
}
console.log(`${LOG_PREFIX} Closing HTTP server...`);
const active = this.server;
this.server = null;
await new Promise((resolve, reject) => {
active.close((error) => {
if (error) {
console.error(`${LOG_PREFIX} Close failed: ${error.message}`);
reject(error);
return;
}
console.log(`${LOG_PREFIX} HTTP server closed.`);
resolve();
});
});
}
readBody(request) {
return new Promise((resolve, reject) => {
const chunks = [];
request.on('data', (chunk) => chunks.push(chunk));
request.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
request.on('error', reject);
});
}
async handleRpcRequest(request) {
if (!request || request.jsonrpc !== '2.0') {
return this.createError(request && request.id, -32600, 'Invalid Request');
}
const method = request.method;
if (typeof method !== 'string' || !method) {
return this.createError(request.id, -32600, 'Invalid Request: method is required');
}
if (method === 'initialize') {
return this.createResult(request.id, {
protocolVersion: '2024-11-05',
serverInfo: {
name: this.serverName,
version: this.serverVersion,
},
capabilities: {
tools: {},
resources: {},
prompts: {},
},
});
}
if (method === 'notifications/initialized' || method === 'notifications/cancelled' || method.startsWith('notifications/')) {
return null;
}
if (method === 'tools/list') {
return this.createResult(request.id, { tools: this.toolRegistry.listTools() });
}
if (method === 'tools/call') {
const params = request.params || {};
if (typeof params.name !== 'string' || !params.name) {
return this.createError(request.id, -32602, "Invalid params: 'name' is required");
}
try {
const output = await this.toolRegistry.callTool(params.name, params.arguments || {});
return this.createResult(request.id, { content: textContent(output) });
} catch (error) {
return this.createError(request.id, -32603, error.message);
}
}
if (method === 'resources/list') {
return this.createResult(request.id, { resources: this.resourceProvider.listResources() });
}
if (method === 'resources/read') {
const params = request.params || {};
if (typeof params.uri !== 'string' || !params.uri) {
return this.createError(request.id, -32602, "Invalid params: 'uri' is required");
}
return this.createResult(request.id, await this.resourceProvider.readResource(params.uri));
}
if (method === 'resources/templates/list') {
return this.createResult(request.id, { resourceTemplates: this.resourceProvider.listResourceTemplates() });
}
if (method === 'prompts/list') {
return this.createResult(request.id, { prompts: this.promptProvider.listPrompts() });
}
if (method === 'prompts/get') {
const params = request.params || {};
if (typeof params.name !== 'string' || !params.name) {
return this.createError(request.id, -32602, "Invalid params: 'name' is required");
}
return this.createResult(request.id, this.promptProvider.getPrompt(params.name, params.arguments || {}));
}
return this.createError(request.id, -32601, `Method not found: ${method}`);
}
createResult(id, result) {
return {
jsonrpc: '2.0',
id,
result,
};
}
createError(id, code, message) {
return {
jsonrpc: '2.0',
id,
error: {
code,
message,
},
};
}
}
module.exports = {
McpServer,
};