// Using Node.js 18+ built-in fetch
export interface UnityHttpAdapterOptions {
url?: string;
timeout?: number;
}
export interface UnityResponse {
success: boolean;
result?: any;
error?: string;
}
export interface FolderEntry {
path: string;
name: string;
type: 'file' | 'folder';
extension?: string;
guid: string;
}
/**
* HTTP adapter for Unity MCP Server
* Provides a clean interface to communicate with Unity HTTP server
*/
export class UnityHttpAdapter {
private url: string;
private timeout: number;
constructor(options: UnityHttpAdapterOptions = {}) {
this.url = options.url || 'http://localhost:23457/';
this.timeout = options.timeout || 15000;
}
/**
* Call a method on the Unity server
*/
async call(method: string, params: Record<string, any> = {}): Promise<any> {
const startTime = Date.now();
console.error(`[Unity HTTP] Calling method: ${method}`);
const maxRetries = 3;
let lastError: any;
for (let retry = 0; retry < maxRetries; retry++) {
if (retry > 0) {
console.error(`[Unity HTTP] Retry ${retry}/${maxRetries - 1} for method: ${method}`);
await new Promise(resolve => setTimeout(resolve, 1000 * retry)); // Exponential backoff
}
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => {
console.error(`[Unity HTTP] Request timeout after ${this.timeout}ms for method: ${method}`);
controller.abort();
}, this.timeout);
const response = await fetch(this.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=utf-8',
'Accept': 'application/json; charset=utf-8'
},
body: JSON.stringify({ method, ...params }),
signal: controller.signal
});
clearTimeout(timeoutId);
const elapsed = Date.now() - startTime;
console.error(`[Unity HTTP] Response received in ${elapsed}ms for method: ${method}`);
const result = await response.json() as UnityResponse;
if (!result.success) {
throw new Error(result.error || 'Unknown error');
}
return result.result;
} catch (error: any) {
lastError = error;
if (error.name === 'AbortError') {
lastError = new Error('Request timeout');
} else if (error.message?.includes('ECONNREFUSED')) {
lastError = new Error('Unity HTTP server is not running');
} else if (error.message?.includes('Failed to fetch')) {
lastError = new Error('Failed to connect to Unity HTTP server');
}
console.error(`[Unity HTTP] Error on attempt ${retry + 1}: ${lastError.message}`);
// Don't retry on certain errors
if (error.message?.includes('Method not found')) {
throw error;
}
}
}
// All retries failed
throw lastError || new Error('Unknown error after retries');
}
/**
* Check if Unity server is connected
*/
async isConnected(): Promise<boolean> {
try {
await this.call('ping');
return true;
} catch {
return false;
}
}
// Script operations
async createScript(fileName: string, content?: string, folder?: string): Promise<any> {
return this.call('script/create', { fileName, content, folder });
}
async readScript(path: string): Promise<any> {
return this.call('script/read', { path });
}
async deleteScript(path: string): Promise<any> {
return this.call('script/delete', { path });
}
async applyDiff(path: string, diff: string, options?: any): Promise<any> {
return this.call('script/applyDiff', { path, diff, options });
}
// Shader operations
async createShader(name: string, content?: string, folder?: string): Promise<any> {
return this.call('shader/create', { name, content, folder });
}
async readShader(path: string): Promise<any> {
return this.call('shader/read', { path });
}
async deleteShader(path: string): Promise<any> {
return this.call('shader/delete', { path });
}
// Project operations
async getProjectInfo(): Promise<any> {
return this.call('project/info');
}
// Folder operations
async createFolder(path: string): Promise<{ path: string; guid: string }> {
return this.call('folder/create', { path });
}
async renameFolder(oldPath: string, newName: string): Promise<{ oldPath: string; newPath: string; guid: string }> {
return this.call('folder/rename', { oldPath, newName });
}
async moveFolder(sourcePath: string, targetPath: string): Promise<{ sourcePath: string; targetPath: string; guid: string }> {
return this.call('folder/move', { sourcePath, targetPath });
}
async deleteFolder(path: string, recursive: boolean = true): Promise<{ path: string }> {
return this.call('folder/delete', { path, recursive });
}
async listFolder(path?: string, recursive: boolean = false): Promise<{ path: string; entries: FolderEntry[] }> {
return this.call('folder/list', { path, recursive });
}
}