Initial commit: funplay cocos mcp
This commit is contained in:
+351
@@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user