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
+351
View File
@@ -0,0 +1,351 @@
'use strict';
const path = require('path');
const fs = require('fs');
const os = require('os');
const manifest = require('./package.json');
const { configureTarget, getTargetStatuses } = require('./lib/client-config');
const { loadConfig, getProjectPath, getProjectName, getCocosVersion } = require('./lib/config');
const { McpServer } = require('./lib/server');
const { createToolRegistry } = require('./lib/tool-registry');
const { ResourceProvider } = require('./lib/resources');
const { PromptProvider } = require('./lib/prompts');
const { InteractionLog } = require('./lib/interaction-log');
const EXTENSION_NAME = manifest.name || 'funplay-cocos-mcp';
const LOG_PREFIX = '[Funplay Cocos MCP]';
const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;
class ExtensionService {
constructor() {
this.config = null;
this.server = null;
this.toolRegistry = null;
this.resourceProvider = null;
this.promptProvider = null;
this.interactionLog = new InteractionLog();
}
load() {
console.log(`${LOG_PREFIX} Extension loading...`);
this.reloadRuntime();
if (this.config.autostart) {
console.log(`${LOG_PREFIX} Autostart is enabled, starting MCP server.`);
return this.startServer();
}
console.log(`${LOG_PREFIX} Autostart is disabled. MCP server is idle.`);
return this.getStatus();
}
unload() {
console.log(`${LOG_PREFIX} Extension unloading...`);
if (this.server) {
this.server.stop();
this.server = null;
}
console.log(`${LOG_PREFIX} Extension unloaded.`);
}
openPanel() {
if (!global.Editor || !Editor.Panel || typeof Editor.Panel.open !== 'function') {
throw new Error('Editor.Panel.open is unavailable in this Cocos extension host.');
}
return Editor.Panel.open(EXTENSION_NAME);
}
reloadRuntime() {
this.config = loadConfig();
console.log(
`${LOG_PREFIX} Runtime config loaded: host=${this.config.host}, port=${this.config.port}, ` +
`profile=${this.config.toolProfile}, autostart=${this.config.autostart}`
);
this.interactionLog = new InteractionLog(this.config.maxInteractionLogEntries);
const sceneBridge = {
call: async (method, payload) => {
if (!global.Editor || !Editor.Message || typeof Editor.Message.request !== 'function') {
throw new Error('Editor.Message.request is unavailable in the Cocos extension host.');
}
return await Editor.Message.request('scene', 'execute-scene-script', {
name: EXTENSION_NAME,
method,
args: [payload || {}],
});
},
};
const runtimeContext = () => ({
extensionName: EXTENSION_NAME,
version: manifest.version || '0.0.0',
config: this.config,
projectPath: getProjectPath(),
projectName: getProjectName(),
cocosVersion: getCocosVersion(),
packagePath: path.dirname(__filename),
});
this.toolRegistry = createToolRegistry({
getRuntimeContext: runtimeContext,
interactionLog: this.interactionLog,
sceneBridge,
editorExecutor: async (payload) => await this.executeEditorScript(payload, runtimeContext),
});
this.resourceProvider = new ResourceProvider(runtimeContext, sceneBridge, this.interactionLog);
this.promptProvider = new PromptProvider(runtimeContext);
}
async startServer() {
if (this.server && this.server.isRunning()) {
console.log(`${LOG_PREFIX} Start requested but MCP server is already running at ${this.getStatus().url}`);
return this.getStatus();
}
console.log(`${LOG_PREFIX} Starting MCP server...`);
this.reloadRuntime();
this.server = new McpServer({
config: this.config,
interactionLog: this.interactionLog,
toolRegistry: this.toolRegistry,
resourceProvider: this.resourceProvider,
promptProvider: this.promptProvider,
serverName: `Funplay Cocos MCP - ${getProjectName()}`,
serverVersion: manifest.version || '0.0.0',
});
await this.server.start();
console.log(`${LOG_PREFIX} MCP server started at ${this.getStatus().url}`);
return this.getStatus();
}
async stopServer() {
console.log(`${LOG_PREFIX} Stop requested.`);
if (this.server) {
await this.server.stop();
this.server = null;
console.log(`${LOG_PREFIX} MCP server stopped.`);
} else {
console.log(`${LOG_PREFIX} Stop requested but MCP server was not running.`);
}
return this.getStatus();
}
async restartServer() {
console.log(`${LOG_PREFIX} Restart requested.`);
await this.stopServer();
const status = await this.startServer();
console.log(`${LOG_PREFIX} Restart completed. MCP server running=${status.running}, url=${status.url}`);
return status;
}
getStatus() {
return {
running: Boolean(this.server && this.server.isRunning()),
host: this.config.host,
port: this.config.port,
toolProfile: this.config.toolProfile,
autostart: this.config.autostart,
projectPath: getProjectPath(),
projectName: getProjectName(),
cocosVersion: getCocosVersion(),
url: `http://${this.config.host}:${this.config.port}/`,
};
}
getPanelState() {
this.ensureRuntime();
const status = this.getStatus();
const tools = this.toolRegistry.listTools();
const resources = this.resourceProvider.listResources();
const prompts = this.promptProvider.listPrompts();
return {
status,
tools,
resources,
prompts,
recentInteractions: this.interactionLog.list(20),
config: this.config,
clientConfig: this.getClientConfig(),
clientTargets: getTargetStatuses(this.config),
};
}
listToolsForPanel() {
this.ensureRuntime();
return this.toolRegistry.listTools();
}
async callToolFromPanel(name, args) {
this.ensureRuntime();
console.log(`${LOG_PREFIX} Panel calling tool: ${name}`);
return await this.toolRegistry.callTool(name, args || {});
}
async executeEditorScript(payload, runtimeContext) {
const code = String(payload && payload.code || '');
if (!code.trim()) {
throw new Error('code is required.');
}
const args = payload && payload.args ? payload.args : {};
const context = runtimeContext();
const helpers = {
getStatus: () => this.getStatus(),
listTools: () => this.toolRegistry.listTools(),
readResource: async (uri) => await this.resourceProvider.readResource(uri),
callTool: async (name, toolArgs) => await this.toolRegistry.callTool(name, toolArgs || {}),
listClientTargets: () => getTargetStatuses(this.config),
getClientConfig: () => this.getClientConfig(),
configureClient: async (targetId) => this.configureClient(targetId),
};
const runner = new AsyncFunction(
'require',
'Editor',
'args',
'context',
'helpers',
'fs',
'path',
'os',
`
const module = { exports: {} };
const exports = module.exports;
${code}
if (typeof run === 'function') {
return await run({ Editor, args, context, helpers, fs, path, os, require });
}
if (typeof module.exports === 'function') {
return await module.exports({ Editor, args, context, helpers, fs, path, os, require });
}
if (module.exports && typeof module.exports.run === 'function') {
return await module.exports.run({ Editor, args, context, helpers, fs, path, os, require });
}
`
);
return await runner(require, global.Editor, args, context, helpers, fs, path, os);
}
async readResourceFromPanel(uri) {
this.ensureRuntime();
console.log(`${LOG_PREFIX} Panel reading resource: ${uri}`);
return await this.resourceProvider.readResource(uri);
}
getClientConfig() {
const url = `http://${this.config.host}:${this.config.port}/`;
return {
url,
codex: `[mcp_servers.funplay_cocos]\nurl = "${url}"\n`,
json: JSON.stringify({
mcpServers: {
funplay_cocos: {
url,
},
},
}, null, 2),
};
}
configureClient(targetId) {
this.ensureRuntime();
console.log(`${LOG_PREFIX} Configuring MCP client target: ${targetId}`);
const result = configureTarget(this.config, targetId);
console.log(`${LOG_PREFIX} MCP client configured: ${result.name} -> ${result.configPath}`);
return {
...result,
clientTargets: getTargetStatuses(this.config),
};
}
async saveConfig(partialConfig) {
this.ensureRuntime();
const nextPort = partialConfig && partialConfig.port !== undefined
? Number(partialConfig.port)
: this.config.port;
const nextMaxEntries = partialConfig && partialConfig.maxInteractionLogEntries !== undefined
? Number(partialConfig.maxInteractionLogEntries)
: this.config.maxInteractionLogEntries;
const nextConfig = {
host: partialConfig && partialConfig.host ? String(partialConfig.host) : this.config.host,
port: Number.isInteger(nextPort) && nextPort > 0 && nextPort <= 65535 ? nextPort : this.config.port,
toolProfile: partialConfig && partialConfig.toolProfile
? (partialConfig.toolProfile === 'full' ? 'full' : 'core')
: this.config.toolProfile,
autostart: partialConfig && typeof partialConfig.autostart === 'boolean'
? partialConfig.autostart
: this.config.autostart,
maxInteractionLogEntries: Number.isInteger(nextMaxEntries)
? Math.max(10, Math.min(500, nextMaxEntries))
: this.config.maxInteractionLogEntries,
};
const configPath = this.config.configPath;
fs.writeFileSync(configPath, JSON.stringify(nextConfig, null, 2) + '\n', 'utf8');
const wasRunning = Boolean(this.server && this.server.isRunning());
if (wasRunning) {
await this.stopServer();
}
this.reloadRuntime();
if (wasRunning) {
await this.startServer();
}
return this.getPanelState();
}
ensureRuntime() {
if (!this.config || !this.toolRegistry || !this.resourceProvider || !this.promptProvider) {
this.reloadRuntime();
}
}
}
const service = new ExtensionService();
module.exports = {
load() {
return service.load();
},
unload() {
return service.unload();
},
methods: {
openPanel() {
return service.openPanel();
},
startServer() {
return service.startServer();
},
stopServer() {
return service.stopServer();
},
restartServer() {
return service.restartServer();
},
getStatus() {
return service.getStatus();
},
getPanelState() {
return service.getPanelState();
},
saveConfig(config) {
return service.saveConfig(config);
},
listToolsForPanel() {
return service.listToolsForPanel();
},
callToolFromPanel(name, args) {
return service.callToolFromPanel(name, args);
},
readResourceFromPanel(uri) {
return service.readResourceFromPanel(uri);
},
getClientConfig() {
return service.getClientConfig();
},
configureClient(targetId) {
return service.configureClient(targetId);
},
},
};