Files
cocos-mcp/source/mcp-server.ts

265 lines
9.5 KiB
TypeScript

import * as http from 'http';
import * as url from 'url';
import { v4 as uuidv4 } from 'uuid';
import { MCPServerSettings, ServerStatus, MCPClient, ToolDefinition } from './types';
import { SceneTools } from './tools/scene-tools';
import { NodeTools } from './tools/node-tools';
import { ComponentTools } from './tools/component-tools';
import { PrefabTools } from './tools/prefab-tools';
import { ProjectTools } from './tools/project-tools';
import { DebugTools } from './tools/debug-tools';
import { PreferencesTools } from './tools/preferences-tools';
import { ServerTools } from './tools/server-tools';
import { BroadcastTools } from './tools/broadcast-tools';
import { SceneAdvancedTools } from './tools/scene-advanced-tools';
import { SceneViewTools } from './tools/scene-view-tools';
import { ReferenceImageTools } from './tools/reference-image-tools';
import { AssetAdvancedTools } from './tools/asset-advanced-tools';
export class MCPServer {
private settings: MCPServerSettings;
private httpServer: http.Server | null = null;
private clients: Map<string, MCPClient> = new Map();
private tools: Record<string, any> = {};
private toolsList: ToolDefinition[] = [];
constructor(settings: MCPServerSettings) {
this.settings = settings;
this.initializeTools();
}
private initializeTools(): void {
try {
console.log('[MCPServer] Initializing tools...');
this.tools.scene = new SceneTools();
this.tools.node = new NodeTools();
this.tools.component = new ComponentTools();
this.tools.prefab = new PrefabTools();
this.tools.project = new ProjectTools();
this.tools.debug = new DebugTools();
this.tools.preferences = new PreferencesTools();
this.tools.server = new ServerTools();
this.tools.broadcast = new BroadcastTools();
this.tools.sceneAdvanced = new SceneAdvancedTools();
this.tools.sceneView = new SceneViewTools();
this.tools.referenceImage = new ReferenceImageTools();
this.tools.assetAdvanced = new AssetAdvancedTools();
console.log('[MCPServer] Tools initialized successfully');
} catch (error) {
console.error('[MCPServer] Error initializing tools:', error);
throw error;
}
}
public async start(): Promise<void> {
if (this.httpServer) {
console.log('[MCPServer] Server is already running');
return;
}
try {
console.log(`[MCPServer] Starting HTTP server on port ${this.settings.port}...`);
this.httpServer = http.createServer(this.handleHttpRequest.bind(this));
await new Promise<void>((resolve, reject) => {
this.httpServer!.listen(this.settings.port, '127.0.0.1', () => {
console.log(`[MCPServer] ✅ HTTP server started successfully on http://127.0.0.1:${this.settings.port}`);
console.log(`[MCPServer] Health check: http://127.0.0.1:${this.settings.port}/health`);
console.log(`[MCPServer] MCP endpoint: http://127.0.0.1:${this.settings.port}/mcp`);
resolve();
});
this.httpServer!.on('error', (err: any) => {
console.error('[MCPServer] ❌ Failed to start server:', err);
if (err.code === 'EADDRINUSE') {
console.error(`[MCPServer] Port ${this.settings.port} is already in use. Please change the port in settings.`);
}
reject(err);
});
});
this.setupTools();
console.log('[MCPServer] 🚀 MCP Server is ready for connections');
} catch (error) {
console.error('[MCPServer] ❌ Failed to start server:', error);
throw error;
}
}
private setupTools(): void {
this.toolsList = [];
for (const [category, toolSet] of Object.entries(this.tools)) {
const tools = toolSet.getTools();
for (const tool of tools) {
this.toolsList.push({
name: `${category}_${tool.name}`,
description: tool.description,
inputSchema: tool.inputSchema
});
}
}
}
public async executeToolCall(toolName: string, args: any): Promise<any> {
const parts = toolName.split('_');
const category = parts[0];
const toolMethodName = parts.slice(1).join('_');
if (this.tools[category]) {
return await this.tools[category].execute(toolMethodName, args);
}
throw new Error(`Tool ${toolName} not found`);
}
public getClients(): MCPClient[] {
return Array.from(this.clients.values());
}
public getAvailableTools(): ToolDefinition[] {
return this.toolsList;
}
public getSettings(): MCPServerSettings {
return this.settings;
}
private async handleHttpRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
const parsedUrl = url.parse(req.url || '', true);
const pathname = parsedUrl.pathname;
// Set CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Content-Type', 'application/json');
if (req.method === 'OPTIONS') {
res.writeHead(200);
res.end();
return;
}
try {
if (pathname === '/mcp' && req.method === 'POST') {
await this.handleMCPRequest(req, res);
} else if (pathname === '/health' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify({ status: 'ok', tools: this.toolsList.length }));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Not found' }));
}
} catch (error) {
console.error('HTTP request error:', error);
res.writeHead(500);
res.end(JSON.stringify({ error: 'Internal server error' }));
}
}
private async handleMCPRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', async () => {
try {
const message = JSON.parse(body);
const response = await this.handleMessage(message);
res.writeHead(200);
res.end(JSON.stringify(response));
} catch (error) {
console.error('Error handling MCP request:', error);
res.writeHead(400);
res.end(JSON.stringify({
jsonrpc: '2.0',
id: null,
error: {
code: -32700,
message: 'Parse error'
}
}));
}
});
}
private async handleMessage(message: any): Promise<any> {
const { id, method, params } = message;
try {
let result: any;
switch (method) {
case 'tools/list':
result = { tools: this.getAvailableTools() };
break;
case 'tools/call':
const { name, arguments: args } = params;
const toolResult = await this.executeToolCall(name, args);
result = { content: [{ type: 'text', text: JSON.stringify(toolResult) }] };
break;
case 'initialize':
// MCP initialization
result = {
protocolVersion: '2024-11-05',
capabilities: {
tools: {}
},
serverInfo: {
name: 'cocos-mcp-server',
version: '1.0.0'
}
};
break;
default:
throw new Error(`Unknown method: ${method}`);
}
return {
jsonrpc: '2.0',
id,
result
};
} catch (error: any) {
return {
jsonrpc: '2.0',
id,
error: {
code: -32603,
message: error.message
}
};
}
}
public stop(): void {
if (this.httpServer) {
this.httpServer.close();
this.httpServer = null;
console.log('[MCPServer] HTTP server stopped');
}
this.clients.clear();
}
public getStatus(): ServerStatus {
return {
running: !!this.httpServer,
port: this.settings.port,
clients: 0 // HTTP is stateless, no persistent clients
};
}
public updateSettings(settings: MCPServerSettings) {
this.settings = settings;
if (this.httpServer) {
this.stop();
this.start();
}
}
}
// HTTP transport doesn't need persistent connections
// MCP over HTTP uses request-response pattern