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';
import { ValidationTools } from './tools/validation-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[] = [];
private enabledTools: any[] = []; // 存储启用的工具列表
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();
this.tools.validation = new ValidationTools();
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 = [];
// 如果没有启用工具配置,返回所有工具
if (!this.enabledTools || this.enabledTools.length === 0) {
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
});
}
}
} else {
// 根据启用的工具配置过滤
const enabledToolNames = new Set(this.enabledTools.map(tool => `${tool.category}_${tool.name}`));
for (const [category, toolSet] of Object.entries(this.tools)) {
const tools = toolSet.getTools();
for (const tool of tools) {
const toolName = `${category}_${tool.name}`;
if (enabledToolNames.has(toolName)) {
this.toolsList.push({
name: toolName,
description: tool.description,
inputSchema: tool.inputSchema
});
}
}
}
}
console.log(`[MCPServer] Setup tools: ${this.toolsList.length} tools available`);
}
public getFilteredTools(enabledTools: any[]): ToolDefinition[] {
if (!enabledTools || enabledTools.length === 0) {
return this.toolsList; // 如果没有过滤配置,返回所有工具
}
const enabledToolNames = new Set(enabledTools.map(tool => `${tool.category}_${tool.name}`));
return this.toolsList.filter(tool => enabledToolNames.has(tool.name));
}
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 updateEnabledTools(enabledTools: any[]): void {
console.log(`[MCPServer] Updating enabled tools: ${enabledTools.length} tools`);
this.enabledTools = enabledTools;
this.setupTools(); // 重新设置工具列表
}
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 if (pathname?.startsWith('/api/') && req.method === 'POST') {
await this.handleSimpleAPIRequest(req, res, pathname);
} else if (pathname === '/api/tools' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify({ tools: this.getSimplifiedToolsList() }));
} 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 {
// Enhanced JSON parsing with better error handling
let message;
try {
message = JSON.parse(body);
} catch (parseError: any) {
// Try to fix common JSON issues
const fixedBody = this.fixCommonJsonIssues(body);
try {
message = JSON.parse(fixedBody);
console.log('[MCPServer] Fixed JSON parsing issue');
} catch (secondError) {
throw new Error(`JSON parsing failed: ${parseError.message}. Original body: ${body.substring(0, 500)}...`);
}
}
const response = await this.handleMessage(message);
res.writeHead(200);
res.end(JSON.stringify(response));
} catch (error: any) {
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: ${error.message}`
}
}));
}
});
}
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
}
};
}
}
private fixCommonJsonIssues(jsonStr: string): string {
let fixed = jsonStr;
// Fix common escape character issues
fixed = fixed
// Fix unescaped quotes in strings
.replace(/([^\\])"([^"]*[^\\])"([^,}\]:])/g, '$1\\"$2\\"$3')
// Fix unescaped backslashes
.replace(/([^\\])\\([^"\\\/bfnrt])/g, '$1\\\\$2')
// Fix trailing commas
.replace(/,(\s*[}\]])/g, '$1')
// Fix single quotes (should be double quotes)
.replace(/'/g, '"')
// Fix common control characters
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t');
return fixed;
}
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
};
}
private async handleSimpleAPIRequest(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', async () => {
try {
// Extract tool name from path like /api/node/set_position
const pathParts = pathname.split('/').filter(p => p);
if (pathParts.length < 3) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid API path. Use /api/{category}/{tool_name}' }));
return;
}
const category = pathParts[1];
const toolName = pathParts[2];
const fullToolName = `${category}_${toolName}`;
// Parse parameters with enhanced error handling
let params;
try {
params = body ? JSON.parse(body) : {};
} catch (parseError: any) {
// Try to fix JSON issues
const fixedBody = this.fixCommonJsonIssues(body);
try {
params = JSON.parse(fixedBody);
console.log('[MCPServer] Fixed API JSON parsing issue');
} catch (secondError: any) {
res.writeHead(400);
res.end(JSON.stringify({
error: 'Invalid JSON in request body',
details: parseError.message,
receivedBody: body.substring(0, 200)
}));
return;
}
}
// Execute tool
const result = await this.executeToolCall(fullToolName, params);
res.writeHead(200);
res.end(JSON.stringify({
success: true,
tool: fullToolName,
result: result
}));
} catch (error: any) {
console.error('Simple API error:', error);
res.writeHead(500);
res.end(JSON.stringify({
success: false,
error: error.message,
tool: pathname
}));
}
});
}
private getSimplifiedToolsList(): any[] {
return this.toolsList.map(tool => {
const parts = tool.name.split('_');
const category = parts[0];
const toolName = parts.slice(1).join('_');
return {
name: tool.name,
category: category,
toolName: toolName,
description: tool.description,
apiPath: `/api/${category}/${toolName}`,
curlExample: this.generateCurlExample(category, toolName, tool.inputSchema)
};
});
}
private generateCurlExample(category: string, toolName: string, schema: any): string {
// Generate sample parameters based on schema
const sampleParams = this.generateSampleParams(schema);
const jsonString = JSON.stringify(sampleParams, null, 2);
return `curl -X POST http://127.0.0.1:8585/api/${category}/${toolName} \\
-H "Content-Type: application/json" \\
-d '${jsonString}'`;
}
private generateSampleParams(schema: any): any {
if (!schema || !schema.properties) return {};
const sample: any = {};
for (const [key, prop] of Object.entries(schema.properties as any)) {
const propSchema = prop as any;
switch (propSchema.type) {
case 'string':
sample[key] = propSchema.default || 'example_string';
break;
case 'number':
sample[key] = propSchema.default || 42;
break;
case 'boolean':
sample[key] = propSchema.default || true;
break;
case 'object':
sample[key] = propSchema.default || { x: 0, y: 0, z: 0 };
break;
default:
sample[key] = 'example_value';
}
}
return sample;
}
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