From a1438cb77ae24d9c541f7cb3c653b55987b15b27 Mon Sep 17 00:00:00 2001 From: STetsing <41009393+STetsing@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:01:42 +0200 Subject: [PATCH 01/31] initial mcp integration --- .../src/app/plugins/remixAIPlugin.tsx | 231 +++++- libs/remix-ai-core/src/index.ts | 4 +- .../src/inferencers/mcp/mcpInferencer.ts | 693 ++++++++++++++++++ libs/remix-ai-core/src/types/mcp.ts | 133 ++++ .../remix-ui-remix-ai-assistant.tsx | 50 ++ .../settings/src/lib/mcp-server-manager.tsx | 392 ++++++++++ .../settings/src/lib/remix-ui-settings.tsx | 21 + .../settings/src/lib/settings-section.tsx | 7 + libs/remix-ui/settings/src/types/index.ts | 7 +- package.json | 1 + 10 files changed, 1530 insertions(+), 9 deletions(-) create mode 100644 libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts create mode 100644 libs/remix-ai-core/src/types/mcp.ts create mode 100644 libs/remix-ui/settings/src/lib/mcp-server-manager.tsx diff --git a/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx b/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx index 6503110da8c..39b8a3cc4b6 100644 --- a/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx +++ b/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx @@ -2,6 +2,8 @@ import * as packageJson from '../../../../../package.json' import { Plugin } from '@remixproject/engine'; import { IModel, RemoteInferencer, IRemoteModel, IParams, GenerationParams, AssistantParams, CodeExplainAgent, SecurityAgent, CompletionParams, OllamaInferencer, isOllamaAvailable, getBestAvailableModel } from '@remix/remix-ai-core'; import { CodeCompletionAgent, ContractAgent, workspaceAgent, IContextType } from '@remix/remix-ai-core'; +import { MCPInferencer } from '@remix/remix-ai-core'; +import { MCPServer, MCPConnectionStatus } from '@remix/remix-ai-core'; import axios from 'axios'; import { endpointUrls } from "@remix-endpoints-helper" const _paq = (window._paq = window._paq || []) @@ -18,7 +20,9 @@ const profile = { "code_insertion", "error_explaining", "vulnerability_check", 'generate', "initialize", 'chatPipe', 'ProcessChatRequestBuffer', 'isChatRequestPending', 'resetChatRequestBuffer', 'setAssistantThrId', - 'getAssistantThrId', 'getAssistantProvider', 'setAssistantProvider', 'setModel'], + 'getAssistantThrId', 'getAssistantProvider', 'setAssistantProvider', 'setModel', + 'addMCPServer', 'removeMCPServer', 'getMCPConnectionStatus', 'getMCPResources', 'getMCPTools', 'executeMCPTool', + 'enableMCPEnhancement', 'disableMCPEnhancement', 'isMCPEnabled'], events: [], icon: 'assets/img/remix-logo-blue.png', description: 'RemixAI provides AI services to Remix IDE.', @@ -45,6 +49,10 @@ export class RemixAIPlugin extends Plugin { assistantThreadId: string = '' useRemoteInferencer:boolean = false completionAgent: CodeCompletionAgent + mcpServers: MCPServer[] = [] + mcpInferencer: MCPInferencer | null = null + mcpEnabled: boolean = false + baseInferencer: any = null constructor(inDesktop:boolean) { super(profile) @@ -67,6 +75,9 @@ export class RemixAIPlugin extends Plugin { this.codeExpAgent = new CodeExplainAgent(this) this.contractor = ContractAgent.getInstance(this) this.workspaceAgent = workspaceAgent.getInstance(this) + + // Load MCP servers from settings + this.loadMCPServersFromSettings(); } async initialize(model1?:IModel, model2?:IModel, remoteModel?:IRemoteModel, useRemote?:boolean){ @@ -102,10 +113,12 @@ export class RemixAIPlugin extends Plugin { } async code_generation(prompt: string, params: IParams=CompletionParams): Promise { + const enrichedPrompt = await this.enrichWithMCPContext(prompt, params); + if (this.isOnDesktop && !this.useRemoteInferencer) { - return await this.call(this.remixDesktopPluginName, 'code_generation', prompt, params) + return await this.call(this.remixDesktopPluginName, 'code_generation', enrichedPrompt, params) } else { - return await this.remoteInferencer.code_generation(prompt, params) + return await this.remoteInferencer.code_generation(enrichedPrompt, params) } } @@ -122,8 +135,9 @@ export class RemixAIPlugin extends Plugin { } async answer(prompt: string, params: IParams=GenerationParams): Promise { + const enrichedPrompt = await this.enrichWithMCPContext(prompt, params); - let newPrompt = await this.codeExpAgent.chatCommand(prompt) + let newPrompt = await this.codeExpAgent.chatCommand(enrichedPrompt) // add workspace context newPrompt = !this.workspaceAgent.ctxFiles ? newPrompt : "Using the following context: ```\n" + this.workspaceAgent.ctxFiles + "```\n\n" + newPrompt @@ -138,12 +152,13 @@ export class RemixAIPlugin extends Plugin { } async code_explaining(prompt: string, context: string, params: IParams=GenerationParams): Promise { + const enrichedPrompt = await this.enrichWithMCPContext(prompt, params); let result if (this.isOnDesktop && !this.useRemoteInferencer) { - result = await this.call(this.remixDesktopPluginName, 'code_explaining', prompt, context, params) + result = await this.call(this.remixDesktopPluginName, 'code_explaining', enrichedPrompt, context, params) } else { - result = await this.remoteInferencer.code_explaining(prompt, context, params) + result = await this.remoteInferencer.code_explaining(enrichedPrompt, context, params) } if (result && params.terminal_output) this.call('terminal', 'log', { type: 'aitypewriterwarning', value: result }) return result @@ -367,6 +382,37 @@ export class RemixAIPlugin extends Plugin { this.isInferencing = false }) } + } else if (provider === 'mcp') { + // Switch to MCP inferencer + if (!this.mcpInferencer || !(this.mcpInferencer instanceof MCPInferencer)) { + this.mcpInferencer = new MCPInferencer(this.mcpServers); + this.mcpInferencer.event.on('onInference', () => { + this.isInferencing = true + }) + this.mcpInferencer.event.on('onInferenceDone', () => { + this.isInferencing = false + }) + this.mcpInferencer.event.on('mcpServerConnected', (serverName: string) => { + console.log(`MCP server connected: ${serverName}`) + }) + this.mcpInferencer.event.on('mcpServerError', (serverName: string, error: Error) => { + console.error(`MCP server error (${serverName}):`, error) + }) + + // Connect to all configured servers + await this.mcpInferencer.connectAllServers(); + } + + this.remoteInferencer = this.mcpInferencer; + + if (this.assistantProvider !== provider){ + // clear the threadIds + this.assistantThreadId = '' + GenerationParams.threadId = '' + CompletionParams.threadId = '' + AssistantParams.threadId = '' + } + this.assistantProvider = provider } else if (provider === 'ollama') { const isAvailable = await isOllamaAvailable(); if (!isAvailable) { @@ -436,4 +482,177 @@ export class RemixAIPlugin extends Plugin { this.chatRequestBuffer = null } + // MCP Server Management Methods + async addMCPServer(server: MCPServer): Promise { + try { + // Add to local configuration + this.mcpServers.push(server); + + // If MCP inferencer is active, add the server dynamically + if (this.assistantProvider === 'mcp' && this.mcpInferencer) { + await this.mcpInferencer.addMCPServer(server); + } + + // Persist configuration + await this.call('settings', 'set', 'settings/mcp/servers', JSON.stringify(this.mcpServers)); + } catch (error) { + console.error('Failed to add MCP server:', error); + throw error; + } + } + + async removeMCPServer(serverName: string): Promise { + try { + // Remove from local configuration + this.mcpServers = this.mcpServers.filter(s => s.name !== serverName); + + // If MCP inferencer is active, remove the server dynamically + if (this.assistantProvider === 'mcp' && this.mcpInferencer) { + await this.mcpInferencer.removeMCPServer(serverName); + } + + // Persist configuration + await this.call('settings', 'set', 'settings/mcp/servers', JSON.stringify(this.mcpServers)); + } catch (error) { + console.error('Failed to remove MCP server:', error); + throw error; + } + } + + getMCPConnectionStatus(): MCPConnectionStatus[] { + if (this.assistantProvider === 'mcp' && this.mcpInferencer) { + return this.mcpInferencer.getConnectionStatuses(); + } + return []; + } + + async getMCPResources(): Promise> { + if (this.assistantProvider === 'mcp' && this.mcpInferencer) { + return await this.mcpInferencer.getAllResources(); + } + return {}; + } + + async getMCPTools(): Promise> { + if (this.assistantProvider === 'mcp' && this.mcpInferencer) { + return await this.mcpInferencer.getAllTools(); + } + return {}; + } + + async executeMCPTool(serverName: string, toolName: string, arguments_: Record): Promise { + if (this.assistantProvider === 'mcp' && this.mcpInferencer) { + return await this.mcpInferencer.executeTool(serverName, { name: toolName, arguments: arguments_ }); + } + throw new Error('MCP provider not active'); + } + + private async loadMCPServersFromSettings(): Promise { + try { + const savedServers = await this.call('settings', 'get', 'settings/mcp/servers'); + if (savedServers) { + this.mcpServers = JSON.parse(savedServers); + } else { + // Initialize with default MCP servers + const defaultServers: MCPServer[] = [ + { + name: 'OpenZeppelin Contracts', + description: 'OpenZeppelin smart contract library and security tools', + transport: 'sse', + url: 'https://mcp.openzeppelin.com/contracts/solidity/mcp', + autoStart: true, + enabled: true, + timeout: 30000 + } + ]; + this.mcpServers = defaultServers; + // Save default servers to settings + await this.call('settings', 'set', 'settings/mcp/servers', JSON.stringify(defaultServers)); + } + } catch (error) { + console.warn('Failed to load MCP servers from settings:', error); + this.mcpServers = []; + } + } + + async enableMCPEnhancement(): Promise { + if (!this.mcpServers || this.mcpServers.length === 0) { + console.warn('No MCP servers configured'); + return; + } + + // Initialize MCP inferencer if not already done + if (!this.mcpInferencer) { + this.mcpInferencer = new MCPInferencer(this.mcpServers); + this.mcpInferencer.event.on('mcpServerConnected', (serverName: string) => { + console.log(`MCP server connected: ${serverName}`); + }); + this.mcpInferencer.event.on('mcpServerError', (serverName: string, error: Error) => { + console.error(`MCP server error (${serverName}):`, error); + }); + + // Connect to all MCP servers + await this.mcpInferencer.connectAllServers(); + } + + this.mcpEnabled = true; + console.log('MCP enhancement enabled'); + } + + async disableMCPEnhancement(): Promise { + this.mcpEnabled = false; + console.log('MCP enhancement disabled'); + } + + isMCPEnabled(): boolean { + return this.mcpEnabled; + } + + private async enrichWithMCPContext(prompt: string, params: IParams): Promise { + if (!this.mcpEnabled || !this.mcpInferencer) { + return prompt; + } + + try { + // Get MCP resources and tools context + const resources = await this.mcpInferencer.getAllResources(); + const tools = await this.mcpInferencer.getAllTools(); + + let mcpContext = ''; + + // Add available resources context + if (Object.keys(resources).length > 0) { + mcpContext += '\n--- Available MCP Resources ---\n'; + for (const [serverName, serverResources] of Object.entries(resources)) { + if (serverResources.length > 0) { + mcpContext += `Server: ${serverName}\n`; + for (const resource of serverResources.slice(0, 3)) { // Limit to first 3 + mcpContext += `- ${resource.name}: ${resource.description || resource.uri}\n`; + } + } + } + mcpContext += '--- End Resources ---\n'; + } + + // Add available tools context + if (Object.keys(tools).length > 0) { + mcpContext += '\n--- Available MCP Tools ---\n'; + for (const [serverName, serverTools] of Object.entries(tools)) { + if (serverTools.length > 0) { + mcpContext += `Server: ${serverName}\n`; + for (const tool of serverTools) { + mcpContext += `- ${tool.name}: ${tool.description || 'No description'}\n`; + } + } + } + mcpContext += '--- End Tools ---\n'; + } + + return mcpContext ? `${mcpContext}\n${prompt}` : prompt; + } catch (error) { + console.warn('Failed to enrich with MCP context:', error); + return prompt; + } + } + } diff --git a/libs/remix-ai-core/src/index.ts b/libs/remix-ai-core/src/index.ts index 1a8a9693bdd..412745643e7 100644 --- a/libs/remix-ai-core/src/index.ts +++ b/libs/remix-ai-core/src/index.ts @@ -7,6 +7,7 @@ import { DefaultModels, InsertionParams, CompletionParams, GenerationParams, Ass import { buildChatPrompt } from './prompts/promptBuilder' import { RemoteInferencer } from './inferencers/remote/remoteInference' import { OllamaInferencer } from './inferencers/local/ollamaInferencer' +import { MCPInferencer, MCPEnhancedInferencer } from './inferencers/mcp/mcpInferencer' import { isOllamaAvailable, getBestAvailableModel, listModels, discoverOllamaHost } from './inferencers/local/ollama' import { FIMModelManager, FIMModelConfig, FIM_MODEL_CONFIGS } from './inferencers/local/fimModelConfig' import { ChatHistory } from './prompts/chat' @@ -15,13 +16,14 @@ import { ChatCommandParser } from './helpers/chatCommandParser' export { IModel, IModelResponse, ChatCommandParser, ModelType, DefaultModels, ICompletions, IParams, IRemoteModel, buildChatPrompt, - RemoteInferencer, OllamaInferencer, isOllamaAvailable, getBestAvailableModel, listModels, discoverOllamaHost, + RemoteInferencer, OllamaInferencer, MCPInferencer, MCPEnhancedInferencer, isOllamaAvailable, getBestAvailableModel, listModels, discoverOllamaHost, FIMModelManager, FIMModelConfig, FIM_MODEL_CONFIGS, InsertionParams, CompletionParams, GenerationParams, AssistantParams, ChatEntry, AIRequestType, ChatHistory, downloadLatestReleaseExecutable } export * from './types/types' +export * from './types/mcp' export * from './helpers/streamHandler' export * from './agents/codeExplainAgent' export * from './agents/completionAgent' diff --git a/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts b/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts new file mode 100644 index 00000000000..f848f915d5e --- /dev/null +++ b/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts @@ -0,0 +1,693 @@ +import { ICompletions, IGeneration, IParams, AIRequestType } from "../../types/types"; +import { GenerationParams, CompletionParams, InsertionParams } from "../../types/models"; +import { RemoteInferencer } from "../remote/remoteInference"; +import EventEmitter from "events"; +import { + MCPServer, + MCPResource, + MCPResourceContent, + MCPTool, + MCPToolCall, + MCPToolResult, + MCPConnectionStatus, + MCPInitializeResult, + MCPProviderParams, + MCPAwareParams +} from "../../types/mcp"; + +export class MCPClient { + private server: MCPServer; + private connected: boolean = false; + private capabilities?: any; + private eventEmitter: EventEmitter; + private resources: MCPResource[] = []; + private tools: MCPTool[] = []; + + constructor(server: MCPServer) { + this.server = server; + this.eventEmitter = new EventEmitter(); + } + + async connect(): Promise { + try { + this.eventEmitter.emit('connecting', this.server.name); + + // TODO: Implement actual MCP client connection + // This is a placeholder implementation + // In a real implementation, this would: + // 1. Establish connection based on transport type (stdio/sse/websocket) + // 2. Send initialize request + // 3. Handle initialization response + + await this.delay(1000); // Simulate connection delay + + this.connected = true; + this.capabilities = { + resources: { subscribe: true, listChanged: true }, + tools: { listChanged: true }, + prompts: { listChanged: true } + }; + + const result: MCPInitializeResult = { + protocolVersion: "2024-11-05", + capabilities: this.capabilities, + serverInfo: { + name: this.server.name, + version: "1.0.0" + }, + instructions: `Connected to ${this.server.name} MCP server` + }; + + this.eventEmitter.emit('connected', this.server.name, result); + return result; + + } catch (error) { + this.eventEmitter.emit('error', this.server.name, error); + throw error; + } + } + + async disconnect(): Promise { + if (this.connected) { + this.connected = false; + this.resources = []; + this.tools = []; + this.eventEmitter.emit('disconnected', this.server.name); + } + } + + async listResources(): Promise { + if (!this.connected) { + throw new Error(`MCP server ${this.server.name} is not connected`); + } + + // TODO: Implement actual resource listing + // Placeholder implementation + const mockResources: MCPResource[] = [ + { + uri: `file://${this.server.name}/README.md`, + name: "README", + description: "Project documentation", + mimeType: "text/markdown" + } + ]; + + this.resources = mockResources; + return mockResources; + } + + async readResource(uri: string): Promise { + if (!this.connected) { + throw new Error(`MCP server ${this.server.name} is not connected`); + } + + // TODO: Implement actual resource reading + return { + uri, + mimeType: "text/plain", + text: `Content from ${uri} via ${this.server.name}` + }; + } + + async listTools(): Promise { + if (!this.connected) { + throw new Error(`MCP server ${this.server.name} is not connected`); + } + + // TODO: Implement actual tool listing + const mockTools: MCPTool[] = [ + { + name: "file_read", + description: "Read file contents", + inputSchema: { + type: "object", + properties: { + path: { type: "string" } + }, + required: ["path"] + } + } + ]; + + this.tools = mockTools; + return mockTools; + } + + async callTool(toolCall: MCPToolCall): Promise { + if (!this.connected) { + throw new Error(`MCP server ${this.server.name} is not connected`); + } + + // TODO: Implement actual tool execution + return { + content: [{ + type: 'text', + text: `Tool ${toolCall.name} executed with args: ${JSON.stringify(toolCall.arguments)}` + }] + }; + } + + isConnected(): boolean { + return this.connected; + } + + getServerName(): string { + return this.server.name; + } + + on(event: string, listener: (...args: any[]) => void): void { + this.eventEmitter.on(event, listener); + } + + off(event: string, listener: (...args: any[]) => void): void { + this.eventEmitter.off(event, listener); + } + + private delay(ms: number): Promise { + return new Promise(resolve => setTimeout(resolve, ms)); + } +} + +/** + * MCPInferencer extends RemoteInferencer to support Model Context Protocol + * It manages MCP server connections and integrates MCP resources/tools with AI requests + */ +export class MCPInferencer extends RemoteInferencer implements ICompletions, IGeneration { + private mcpClients: Map = new Map(); + private connectionStatuses: Map = new Map(); + private resourceCache: Map = new Map(); + private cacheTimeout: number = 300000; // 5 minutes + + constructor(servers: MCPServer[] = [], apiUrl?: string, completionUrl?: string) { + super(apiUrl, completionUrl); + this.initializeMCPServers(servers); + } + + private initializeMCPServers(servers: MCPServer[]): void { + for (const server of servers) { + if (server.enabled !== false) { + const client = new MCPClient(server); + this.mcpClients.set(server.name, client); + this.connectionStatuses.set(server.name, { + status: 'disconnected', + serverName: server.name + }); + + // Set up event listeners + client.on('connected', (serverName: string, result: MCPInitializeResult) => { + this.connectionStatuses.set(serverName, { + status: 'connected', + serverName, + capabilities: result.capabilities + }); + this.event.emit('mcpServerConnected', serverName, result); + }); + + client.on('error', (serverName: string, error: Error) => { + this.connectionStatuses.set(serverName, { + status: 'error', + serverName, + error: error.message, + lastAttempt: Date.now() + }); + this.event.emit('mcpServerError', serverName, error); + }); + + client.on('disconnected', (serverName: string) => { + this.connectionStatuses.set(serverName, { + status: 'disconnected', + serverName + }); + this.event.emit('mcpServerDisconnected', serverName); + }); + } + } + } + + async connectAllServers(): Promise { + const promises = Array.from(this.mcpClients.values()).map(async (client) => { + try { + await client.connect(); + } catch (error) { + console.warn(`Failed to connect to MCP server ${client.getServerName()}:`, error); + } + }); + + await Promise.allSettled(promises); + } + + async disconnectAllServers(): Promise { + const promises = Array.from(this.mcpClients.values()).map(client => client.disconnect()); + await Promise.allSettled(promises); + this.resourceCache.clear(); + } + + async addMCPServer(server: MCPServer): Promise { + if (this.mcpClients.has(server.name)) { + throw new Error(`MCP server ${server.name} already exists`); + } + + const client = new MCPClient(server); + this.mcpClients.set(server.name, client); + this.connectionStatuses.set(server.name, { + status: 'disconnected', + serverName: server.name + }); + + if (server.autoStart !== false) { + try { + await client.connect(); + } catch (error) { + console.warn(`Failed to auto-connect to MCP server ${server.name}:`, error); + } + } + } + + async removeMCPServer(serverName: string): Promise { + const client = this.mcpClients.get(serverName); + if (client) { + await client.disconnect(); + this.mcpClients.delete(serverName); + this.connectionStatuses.delete(serverName); + } + } + + private async enrichContextWithMCPResources(params: IParams): Promise { + const mcpParams = (params as any).mcp as MCPProviderParams; + if (!mcpParams?.mcpServers?.length) { + return ""; + } + + let mcpContext = ""; + const maxResources = mcpParams.maxResources || 10; + let resourceCount = 0; + + for (const serverName of mcpParams.mcpServers) { + if (resourceCount >= maxResources) break; + + const client = this.mcpClients.get(serverName); + if (!client || !client.isConnected()) continue; + + try { + const resources = await client.listResources(); + + for (const resource of resources) { + if (resourceCount >= maxResources) break; + + // Check resource priority if specified + if (mcpParams.resourcePriorityThreshold && + resource.annotations?.priority && + resource.annotations.priority < mcpParams.resourcePriorityThreshold) { + continue; + } + + // Try to get from cache first + let content = this.resourceCache.get(resource.uri); + if (!content) { + content = await client.readResource(resource.uri); + // Cache with TTL + this.resourceCache.set(resource.uri, content); + setTimeout(() => { + this.resourceCache.delete(resource.uri); + }, this.cacheTimeout); + } + + if (content.text) { + mcpContext += `\n--- Resource: ${resource.name} (${resource.uri}) ---\n`; + mcpContext += content.text; + mcpContext += "\n--- End Resource ---\n"; + resourceCount++; + } + } + } catch (error) { + console.warn(`Failed to get resources from MCP server ${serverName}:`, error); + } + } + + return mcpContext; + } + + // Override completion methods to include MCP context + async code_completion(prompt: string, promptAfter: string, ctxFiles: any, fileName: string, options: IParams = CompletionParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; + + return super.code_completion(enrichedPrompt, promptAfter, ctxFiles, fileName, options); + } + + async code_insertion(msg_pfx: string, msg_sfx: string, ctxFiles: any, fileName: string, options: IParams = InsertionParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedPrefix = mcpContext ? `${mcpContext}\n\n${msg_pfx}` : msg_pfx; + + return super.code_insertion(enrichedPrefix, msg_sfx, ctxFiles, fileName, options); + } + + async code_generation(prompt: string, options: IParams = GenerationParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; + + return super.code_generation(enrichedPrompt, options); + } + + async answer(prompt: string, options: IParams = GenerationParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; + + return super.answer(enrichedPrompt, options); + } + + async code_explaining(prompt: string, context: string = "", options: IParams = GenerationParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedContext = mcpContext ? `${mcpContext}\n\n${context}` : context; + + return super.code_explaining(prompt, enrichedContext, options); + } + + // MCP-specific methods + getConnectionStatuses(): MCPConnectionStatus[] { + return Array.from(this.connectionStatuses.values()); + } + + getConnectedServers(): string[] { + return Array.from(this.connectionStatuses.entries()) + .filter(([_, status]) => status.status === 'connected') + .map(([name, _]) => name); + } + + async getAllResources(): Promise> { + const result: Record = {}; + + for (const [serverName, client] of this.mcpClients) { + if (client.isConnected()) { + try { + result[serverName] = await client.listResources(); + } catch (error) { + console.warn(`Failed to list resources from ${serverName}:`, error); + result[serverName] = []; + } + } + } + + return result; + } + + async getAllTools(): Promise> { + const result: Record = {}; + + for (const [serverName, client] of this.mcpClients) { + if (client.isConnected()) { + try { + result[serverName] = await client.listTools(); + } catch (error) { + console.warn(`Failed to list tools from ${serverName}:`, error); + result[serverName] = []; + } + } + } + + return result; + } + + async executeTool(serverName: string, toolCall: MCPToolCall): Promise { + const client = this.mcpClients.get(serverName); + if (!client) { + throw new Error(`MCP server ${serverName} not found`); + } + + if (!client.isConnected()) { + throw new Error(`MCP server ${serverName} is not connected`); + } + + return client.callTool(toolCall); + } +} + +/** + * MCPEnhancedInferencer wraps any inferencer to add MCP support + * It manages MCP server connections and integrates MCP resources/tools with AI requests + */ +export class MCPEnhancedInferencer implements ICompletions, IGeneration { + private baseInferencer: ICompletions & IGeneration; + private mcpClients: Map = new Map(); + private connectionStatuses: Map = new Map(); + private resourceCache: Map = new Map(); + private cacheTimeout: number = 300000; // 5 minutes + public event: EventEmitter; + + constructor(baseInferencer: ICompletions & IGeneration, servers: MCPServer[] = []) { + this.baseInferencer = baseInferencer; + this.event = this.baseInferencer.event || new EventEmitter(); + this.initializeMCPServers(servers); + } + + // Delegate all properties to base inferencer + get api_url(): string { + return (this.baseInferencer as any).api_url; + } + + get completion_url(): string { + return (this.baseInferencer as any).completion_url; + } + + get max_history(): number { + return (this.baseInferencer as any).max_history || 7; + } + + private initializeMCPServers(servers: MCPServer[]): void { + for (const server of servers) { + if (server.enabled !== false) { + const client = new MCPClient(server); + this.mcpClients.set(server.name, client); + this.connectionStatuses.set(server.name, { + status: 'disconnected', + serverName: server.name + }); + + // Set up event listeners + client.on('connected', (serverName: string, result: MCPInitializeResult) => { + this.connectionStatuses.set(serverName, { + status: 'connected', + serverName, + capabilities: result.capabilities + }); + this.event.emit('mcpServerConnected', serverName, result); + }); + + client.on('error', (serverName: string, error: Error) => { + this.connectionStatuses.set(serverName, { + status: 'error', + serverName, + error: error.message, + lastAttempt: Date.now() + }); + this.event.emit('mcpServerError', serverName, error); + }); + + client.on('disconnected', (serverName: string) => { + this.connectionStatuses.set(serverName, { + status: 'disconnected', + serverName + }); + this.event.emit('mcpServerDisconnected', serverName); + }); + } + } + } + + async connectAllServers(): Promise { + const promises = Array.from(this.mcpClients.values()).map(async (client) => { + try { + await client.connect(); + } catch (error) { + console.warn(`Failed to connect to MCP server ${client.getServerName()}:`, error); + } + }); + + await Promise.allSettled(promises); + } + + async disconnectAllServers(): Promise { + const promises = Array.from(this.mcpClients.values()).map(client => client.disconnect()); + await Promise.allSettled(promises); + this.resourceCache.clear(); + } + + async addMCPServer(server: MCPServer): Promise { + if (this.mcpClients.has(server.name)) { + throw new Error(`MCP server ${server.name} already exists`); + } + + const client = new MCPClient(server); + this.mcpClients.set(server.name, client); + this.connectionStatuses.set(server.name, { + status: 'disconnected', + serverName: server.name + }); + + if (server.autoStart !== false) { + try { + await client.connect(); + } catch (error) { + console.warn(`Failed to auto-connect to MCP server ${server.name}:`, error); + } + } + } + + async removeMCPServer(serverName: string): Promise { + const client = this.mcpClients.get(serverName); + if (client) { + await client.disconnect(); + this.mcpClients.delete(serverName); + this.connectionStatuses.delete(serverName); + } + } + + private async enrichContextWithMCPResources(params: IParams): Promise { + const mcpParams = (params as any).mcp as MCPProviderParams; + if (!mcpParams?.mcpServers?.length) { + return ""; + } + + let mcpContext = ""; + const maxResources = mcpParams.maxResources || 10; + let resourceCount = 0; + + for (const serverName of mcpParams.mcpServers) { + if (resourceCount >= maxResources) break; + + const client = this.mcpClients.get(serverName); + if (!client || !client.isConnected()) continue; + + try { + const resources = await client.listResources(); + + for (const resource of resources) { + if (resourceCount >= maxResources) break; + + // Check resource priority if specified + if (mcpParams.resourcePriorityThreshold && + resource.annotations?.priority && + resource.annotations.priority < mcpParams.resourcePriorityThreshold) { + continue; + } + + // Try to get from cache first + let content = this.resourceCache.get(resource.uri); + if (!content) { + content = await client.readResource(resource.uri); + // Cache with TTL + this.resourceCache.set(resource.uri, content); + setTimeout(() => { + this.resourceCache.delete(resource.uri); + }, this.cacheTimeout); + } + + if (content.text) { + mcpContext += `\n--- Resource: ${resource.name} (${resource.uri}) ---\n`; + mcpContext += content.text; + mcpContext += "\n--- End Resource ---\n"; + resourceCount++; + } + } + } catch (error) { + console.warn(`Failed to get resources from MCP server ${serverName}:`, error); + } + } + + return mcpContext; + } + + // Override completion methods to include MCP context + async code_completion(prompt: string, promptAfter: string, ctxFiles: any, fileName: string, options: IParams = CompletionParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; + + return this.baseInferencer.code_completion(enrichedPrompt, promptAfter, ctxFiles, fileName, options); + } + + async code_insertion(msg_pfx: string, msg_sfx: string, ctxFiles: any, fileName: string, options: IParams = InsertionParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedPrefix = mcpContext ? `${mcpContext}\n\n${msg_pfx}` : msg_pfx; + + return this.baseInferencer.code_insertion(enrichedPrefix, msg_sfx, ctxFiles, fileName, options); + } + + async code_generation(prompt: string, options: IParams = GenerationParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; + + return this.baseInferencer.code_generation(enrichedPrompt, options); + } + + async answer(prompt: string, options: IParams = GenerationParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; + + return this.baseInferencer.answer(enrichedPrompt, options); + } + + async code_explaining(prompt: string, context: string = "", options: IParams = GenerationParams): Promise { + const mcpContext = await this.enrichContextWithMCPResources(options); + const enrichedContext = mcpContext ? `${mcpContext}\n\n${context}` : context; + + return this.baseInferencer.code_explaining(prompt, enrichedContext, options); + } + + // MCP-specific methods + getConnectionStatuses(): MCPConnectionStatus[] { + return Array.from(this.connectionStatuses.values()); + } + + getConnectedServers(): string[] { + return Array.from(this.connectionStatuses.entries()) + .filter(([_, status]) => status.status === 'connected') + .map(([name, _]) => name); + } + + async getAllResources(): Promise> { + const result: Record = {}; + + for (const [serverName, client] of this.mcpClients) { + if (client.isConnected()) { + try { + result[serverName] = await client.listResources(); + } catch (error) { + console.warn(`Failed to list resources from ${serverName}:`, error); + result[serverName] = []; + } + } + } + + return result; + } + + async getAllTools(): Promise> { + const result: Record = {}; + + for (const [serverName, client] of this.mcpClients) { + if (client.isConnected()) { + try { + result[serverName] = await client.listTools(); + } catch (error) { + console.warn(`Failed to list tools from ${serverName}:`, error); + result[serverName] = []; + } + } + } + + return result; + } + + async executeTool(serverName: string, toolCall: MCPToolCall): Promise { + const client = this.mcpClients.get(serverName); + if (!client) { + throw new Error(`MCP server ${serverName} not found`); + } + + if (!client.isConnected()) { + throw new Error(`MCP server ${serverName} is not connected`); + } + + return client.callTool(toolCall); + } +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/types/mcp.ts b/libs/remix-ai-core/src/types/mcp.ts new file mode 100644 index 00000000000..05d617e7bcc --- /dev/null +++ b/libs/remix-ai-core/src/types/mcp.ts @@ -0,0 +1,133 @@ +/** + * MCP (Model Context Protocol) types and interfaces for Remix AI integration + */ + +export interface MCPServer { + name: string; + description?: string; + transport: 'stdio' | 'sse' | 'websocket'; + command?: string[]; + args?: string[]; + url?: string; + env?: Record; + autoStart?: boolean; + timeout?: number; + enabled?: boolean; +} + +export interface MCPResource { + uri: string; + name: string; + description?: string; + mimeType?: string; + annotations?: { + audience?: string[]; + priority?: number; + }; +} + +export interface MCPResourceContent { + uri: string; + mimeType?: string; + text?: string; + blob?: string; +} + +export interface MCPTool { + name: string; + description?: string; + inputSchema: { + type: string; + properties?: Record; + required?: string[]; + additionalProperties?: boolean; + }; +} + +export interface MCPToolCall { + name: string; + arguments?: Record; +} + +export interface MCPToolResult { + content: Array<{ + type: 'text' | 'image' | 'resource'; + text?: string; + data?: string; + mimeType?: string; + }>; + isError?: boolean; +} + +export interface MCPPrompt { + name: string; + description?: string; + arguments?: Array<{ + name: string; + description?: string; + required?: boolean; + }>; +} + +export interface MCPServerCapabilities { + resources?: { + subscribe?: boolean; + listChanged?: boolean; + }; + tools?: { + listChanged?: boolean; + }; + prompts?: { + listChanged?: boolean; + }; + logging?: {}; + experimental?: Record; +} + +export interface MCPClientCapabilities { + resources?: { + subscribe?: boolean; + }; + sampling?: {}; + roots?: { + listChanged?: boolean; + }; + experimental?: Record; +} + +export interface MCPInitializeResult { + protocolVersion: string; + capabilities: MCPServerCapabilities; + serverInfo: { + name: string; + version: string; + }; + instructions?: string; +} + +export interface MCPConnectionStatus { + status: 'disconnected' | 'connecting' | 'connected' | 'error'; + serverName: string; + error?: string; + lastAttempt?: number; + capabilities?: MCPServerCapabilities; +} + +/** + * MCP provider configuration for AI parameters + */ +export interface MCPProviderParams { + mcpServers?: string[]; + maxResources?: number; + resourcePriorityThreshold?: number; + enableTools?: boolean; + toolTimeout?: number; +} + +/** + * Extended IParams interface with MCP support + */ +export interface MCPAwareParams { + /** MCP-specific parameters */ + mcp?: MCPProviderParams; +} \ No newline at end of file diff --git a/libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx b/libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx index 272ec80b6c7..35ca31ad83f 100644 --- a/libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx +++ b/libs/remix-ui/remix-ai-assistant/src/components/remix-ui-remix-ai-assistant.tsx @@ -48,6 +48,7 @@ export const RemixUiRemixAiAssistant = React.forwardRef< const [contextChoice, setContextChoice] = useState<'none' | 'current' | 'opened' | 'workspace'>( 'none' ) + const [mcpEnhanced, setMcpEnhanced] = useState(false) const [availableModels, setAvailableModels] = useState([]) const [selectedModel, setSelectedModel] = useState(null) const [isOllamaFailureFallback, setIsOllamaFailureFallback] = useState(false) @@ -466,6 +467,37 @@ export const RemixUiRemixAiAssistant = React.forwardRef< fetchAssistantChoice() }, [assistantChoice, isOllamaFailureFallback]) + // Initialize MCP enhancement state + useEffect(() => { + const initMCPState = async () => { + try { + const mcpStatus = await props.plugin.call('remixAI', 'isMCPEnabled') + setMcpEnhanced(mcpStatus) + } catch (error) { + console.warn('Failed to get MCP status:', error) + } + } + initMCPState() + }, []) + + // Handle MCP enhancement toggle + useEffect(() => { + const handleMCPToggle = async () => { + try { + if (mcpEnhanced) { + await props.plugin.call('remixAI', 'enableMCPEnhancement') + } else { + await props.plugin.call('remixAI', 'disableMCPEnhancement') + } + } catch (error) { + console.warn('Failed to toggle MCP enhancement:', error) + } + } + if (mcpEnhanced !== null) { // Only call when state is initialized + handleMCPToggle() + } + }, [mcpEnhanced]) + // Fetch available models everytime Ollama is selected useEffect(() => { const fetchModels = async () => { @@ -680,6 +712,24 @@ export const RemixUiRemixAiAssistant = React.forwardRef< choice={assistantChoice} groupList={aiAssistantGroupList} /> +
+
MCP Enhancement
+
+ setMcpEnhanced(e.target.checked)} + /> + +
+
+ Adds relevant context from configured MCP servers to AI requests +
+
)} {showModelOptions && assistantChoice === 'ollama' && ( diff --git a/libs/remix-ui/settings/src/lib/mcp-server-manager.tsx b/libs/remix-ui/settings/src/lib/mcp-server-manager.tsx new file mode 100644 index 00000000000..6f0214416d9 --- /dev/null +++ b/libs/remix-ui/settings/src/lib/mcp-server-manager.tsx @@ -0,0 +1,392 @@ +import React, { useState, useEffect } from 'react' +import { FormattedMessage } from 'react-intl' +import { ViewPlugin } from '@remixproject/engine-web' + +interface MCPServer { + name: string + description?: string + transport: 'stdio' | 'sse' | 'websocket' + command?: string[] + args?: string[] + url?: string + env?: Record + autoStart?: boolean + timeout?: number + enabled?: boolean +} + +interface MCPConnectionStatus { + status: 'disconnected' | 'connecting' | 'connected' | 'error' + serverName: string + error?: string + lastAttempt?: number +} + +interface MCPServerManagerProps { + plugin: ViewPlugin +} + +export const MCPServerManager: React.FC = ({ plugin }) => { + const [servers, setServers] = useState([]) + const [connectionStatuses, setConnectionStatuses] = useState>({}) + const [showAddForm, setShowAddForm] = useState(false) + const [editingServer, setEditingServer] = useState(null) + const [formData, setFormData] = useState>({ + name: '', + description: '', + transport: 'stdio', + command: [], + args: [], + url: '', + autoStart: true, + enabled: true, + timeout: 30000 + }) + + useEffect(() => { + loadServers() + loadConnectionStatuses() + }, []) + + const loadServers = async () => { + try { + const savedServers = await plugin.call('settings', 'get', 'settings/mcp/servers') + if (savedServers) { + setServers(JSON.parse(savedServers)) + } + } catch (error) { + console.warn('Failed to load MCP servers:', error) + } + } + + const loadConnectionStatuses = async () => { + try { + const statuses = await plugin.call('remixAI', 'getMCPConnectionStatus') + const statusMap: Record = {} + statuses.forEach((status: MCPConnectionStatus) => { + statusMap[status.serverName] = status + }) + setConnectionStatuses(statusMap) + } catch (error) { + console.warn('Failed to load MCP connection statuses:', error) + } + } + + const saveServer = async () => { + try { + const server: MCPServer = { + name: formData.name!, + description: formData.description, + transport: formData.transport!, + command: formData.transport === 'stdio' ? formData.command : undefined, + args: formData.transport === 'stdio' ? formData.args : undefined, + url: formData.transport !== 'stdio' ? formData.url : undefined, + env: formData.env, + autoStart: formData.autoStart, + enabled: formData.enabled, + timeout: formData.timeout + } + + let newServers: MCPServer[] + if (editingServer) { + // Update existing server + newServers = servers.map(s => s.name === editingServer.name ? server : s) + } else { + // Add new server + newServers = [...servers, server] + } + + setServers(newServers) + await plugin.call('settings', 'set', 'settings/mcp/servers', JSON.stringify(newServers)) + + // Add server to AI plugin + if (!editingServer) { + await plugin.call('remixAI', 'addMCPServer', server) + } + + resetForm() + } catch (error) { + console.error('Failed to save MCP server:', error) + } + } + + const deleteServer = async (serverName: string) => { + try { + const newServers = servers.filter(s => s.name !== serverName) + setServers(newServers) + await plugin.call('settings', 'set', 'settings/mcp/servers', JSON.stringify(newServers)) + await plugin.call('remixAI', 'removeMCPServer', serverName) + loadConnectionStatuses() + } catch (error) { + console.error('Failed to delete MCP server:', error) + } + } + + const toggleServer = async (server: MCPServer) => { + try { + const updatedServer = { ...server, enabled: !server.enabled } + const newServers = servers.map(s => s.name === server.name ? updatedServer : s) + setServers(newServers) + await plugin.call('settings', 'set', 'settings/mcp/servers', JSON.stringify(newServers)) + + if (updatedServer.enabled) { + await plugin.call('remixAI', 'addMCPServer', updatedServer) + } else { + await plugin.call('remixAI', 'removeMCPServer', server.name) + } + loadConnectionStatuses() + } catch (error) { + console.error('Failed to toggle MCP server:', error) + } + } + + const resetForm = () => { + setFormData({ + name: '', + description: '', + transport: 'stdio', + command: [], + args: [], + url: '', + autoStart: true, + enabled: true, + timeout: 30000 + }) + setShowAddForm(false) + setEditingServer(null) + } + + const editServer = (server: MCPServer) => { + setFormData(server) + setEditingServer(server) + setShowAddForm(true) + } + + const getStatusIcon = (status?: MCPConnectionStatus) => { + if (!status) return + + switch (status.status) { + case 'connected': return + case 'connecting': return + case 'error': return + default: return + } + } + + const getStatusText = (status?: MCPConnectionStatus) => { + if (!status) return 'Not initialized' + return status.status.charAt(0).toUpperCase() + status.status.slice(1) + } + + return ( +
+
+
MCP Servers
+ +
+ + {showAddForm && ( +
+
+
{editingServer ? 'Edit Server' : 'Add New MCP Server'}
+ +
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Server name" + /> +
+ +
+ + setFormData({ ...formData, description: e.target.value })} + placeholder="Optional description" + /> +
+ +
+ + +
+ + {formData.transport === 'stdio' ? ( + <> +
+ + setFormData({ ...formData, command: e.target.value.split(' ').filter(Boolean) })} + placeholder="python -m mcp_server" + /> +
+
+ + setFormData({ ...formData, args: e.target.value.split(' ').filter(Boolean) })} + placeholder="--port 8080" + /> +
+ + ) : ( +
+ + setFormData({ ...formData, url: e.target.value })} + placeholder={formData.transport === 'sse' ? 'http://localhost:8080/sse' : 'ws://localhost:8080/ws'} + /> +
+ )} + +
+ + setFormData({ ...formData, timeout: parseInt(e.target.value) || 30000 })} + min="1000" + max="300000" + /> +
+ +
+ setFormData({ ...formData, autoStart: e.target.checked })} + /> + +
+ +
+ + +
+
+
+ )} + +
+ {servers.length === 0 ? ( +
+

No MCP servers configured

+ Add a server to start using MCP integration +
+ ) : ( +
+ {servers.map((server) => ( +
+
+
+
+ {getStatusIcon(connectionStatuses[server.name])} + {server.name} + {!server.enabled && Disabled} +
+ {server.description && ( +

{server.description}

+ )} +
+
Transport: {server.transport}
+ {server.transport === 'stdio' ? ( +
Command: {server.command?.join(' ')}
+ ) : ( +
URL: {server.url}
+ )} +
Status: {getStatusText(connectionStatuses[server.name])}
+ {connectionStatuses[server.name]?.error && ( +
Error: {connectionStatuses[server.name]?.error}
+ )} +
+
+
+ + + +
+
+
+ ))} +
+ )} +
+ +
+ +
+ +
+

Transport Types:

+
    +
  • Standard I/O: Run MCP server as subprocess
  • +
  • Server-Sent Events: Connect via HTTP SSE
  • +
  • WebSocket: Connect via WebSocket protocol
  • +
+

Status Indicators: + Connected + Connecting + Error + Disconnected +

+
+
+ ) +} \ No newline at end of file diff --git a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx index 409c7a9dcc7..c26d8420593 100644 --- a/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx +++ b/libs/remix-ui/settings/src/lib/remix-ui-settings.tsx @@ -140,6 +140,27 @@ const settingsSections: SettingsSection[] = [ link: 'https://remix-ide.readthedocs.io/en/latest/ai.html' } }] + }, + { + title: 'MCP Servers', + options: [{ + name: 'mcp/servers/enable', + label: 'Enable MCP Integration', + description: 'Connect to Model Context Protocol servers for enhanced AI context', + type: 'toggle', + footnote: { + text: 'Learn more about MCP', + link: 'https://modelcontextprotocol.io/', + styleClass: 'text-primary' + } + }, + { + name: 'mcp-server-management', + label: 'MCP Server Configuration', + description: 'Manage your MCP server connections', + type: 'custom', + customComponent: 'mcpServerManager' + }] } ]}, { key: 'services', label: 'settings.services', description: 'settings.servicesDescription', subSections: [ diff --git a/libs/remix-ui/settings/src/lib/settings-section.tsx b/libs/remix-ui/settings/src/lib/settings-section.tsx index 7f23db9b36b..8339467e6d6 100644 --- a/libs/remix-ui/settings/src/lib/settings-section.tsx +++ b/libs/remix-ui/settings/src/lib/settings-section.tsx @@ -6,6 +6,7 @@ import SelectDropdown from './select-dropdown' import { ThemeContext } from '@remix-ui/home-tab' import type { ViewPlugin } from '@remixproject/engine-web' import { CustomTooltip } from '@remix-ui/helper' +import { MCPServerManager } from './mcp-server-manager' type SettingsSectionUIProps = { plugin: ViewPlugin, @@ -105,9 +106,15 @@ export const SettingsSectionUI: React.FC = ({ plugin, se {option.type === 'toggle' && handleToggle(option.name)} />} {option.type === 'select' &&
} {option.type === 'button' && } + {option.type === 'custom' && option.customComponent === 'mcpServerManager' && } {option.description && {typeof option.description === 'string' ? : option.description}} + {option.type === 'custom' && option.customComponent === 'mcpServerManager' && ( +
+ +
+ )} { option.footnote ? option.footnote.link ? {option.footnote.text} diff --git a/libs/remix-ui/settings/src/types/index.ts b/libs/remix-ui/settings/src/types/index.ts index 03e938aab22..0d27d1ae2ee 100644 --- a/libs/remix-ui/settings/src/types/index.ts +++ b/libs/remix-ui/settings/src/types/index.ts @@ -54,7 +54,7 @@ export interface SettingsSection { link?: string, styleClass?: string }, - type: 'toggle' | 'select' | 'button', + type: 'toggle' | 'select' | 'button' | 'custom', selectOptions?: { label: string, value: string @@ -71,7 +71,8 @@ export interface SettingsSection { pluginName?: string, pluginMethod?: string, pluginArgs?: string - } + }, + customComponent?: string }[] }[] } @@ -112,6 +113,8 @@ export interface SettingsState { 'sindri-access-token': ConfigState, 'etherscan-access-token': ConfigState, 'ai-privacy-policy': ConfigState, + 'mcp/servers/enable': ConfigState, + 'mcp-server-management': ConfigState, toaster: ConfigState } export interface SettingsActionPayloadTypes { diff --git a/package.json b/package.json index e01231d5815..cf402b1732c 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "@isomorphic-git/lightning-fs": "^4.4.1", "@metamask/eth-sig-util": "^7.0.2", "@microlink/react-json-view": "^1.23.0", + "@modelcontextprotocol/sdk": "^1.0.0", "@noir-lang/noir_wasm": "^1.0.0-beta.2", "@openzeppelin/contracts": "^5.0.0", "@openzeppelin/upgrades-core": "^1.30.0", From e4060b6abb3f9bae1932b2b4824129fe0552d340 Mon Sep 17 00:00:00 2001 From: pipper Date: Tue, 9 Sep 2025 13:33:28 +0200 Subject: [PATCH 02/31] adding remixmcpserver and matching user intends --- .../src/inferencers/local/systemPrompts.ts | 11 +- .../src/inferencers/mcp/mcpInferencer.ts | 252 +++++- .../src/servers/RemixAPIIntegration.ts | 598 ++++++++++++++ .../src/servers/RemixMCPServer.ts | 547 +++++++++++++ .../servers/handlers/CompilationHandler.ts | 520 +++++++++++++ .../src/servers/handlers/DebuggingHandler.ts | 692 ++++++++++++++++ .../src/servers/handlers/DeploymentHandler.ts | 606 ++++++++++++++ .../servers/handlers/FileManagementHandler.ts | 603 ++++++++++++++ libs/remix-ai-core/src/servers/index.ts | 172 ++++ .../servers/middleware/SecurityMiddleware.ts | 510 ++++++++++++ .../middleware/ValidationMiddleware.ts | 736 ++++++++++++++++++ .../providers/CompilationResourceProvider.ts | 523 +++++++++++++ .../providers/DeploymentResourceProvider.ts | 534 +++++++++++++ .../providers/ProjectResourceProvider.ts | 448 +++++++++++ .../registry/RemixResourceProviderRegistry.ts | 444 +++++++++++ .../src/servers/registry/RemixToolRegistry.ts | 296 +++++++ .../src/servers/tests/RemixMCPServer.test.ts | 492 ++++++++++++ .../src/servers/types/mcpResources.ts | 245 ++++++ .../src/servers/types/mcpServer.ts | 283 +++++++ .../src/servers/types/mcpTools.ts | 423 ++++++++++ .../services/__tests__/intentMatching.test.ts | 215 +++++ .../src/services/intentAnalyzer.ts | 217 ++++++ .../src/services/resourceScoring.ts | 309 ++++++++ libs/remix-ai-core/src/types/mcp.ts | 74 +- libs/remix-ai-core/src/types/types.ts | 2 +- .../settings/src/lib/settingsReducer.ts | 10 + yarn.lock | 345 +++++++- 27 files changed, 10064 insertions(+), 43 deletions(-) create mode 100644 libs/remix-ai-core/src/servers/RemixAPIIntegration.ts create mode 100644 libs/remix-ai-core/src/servers/RemixMCPServer.ts create mode 100644 libs/remix-ai-core/src/servers/handlers/CompilationHandler.ts create mode 100644 libs/remix-ai-core/src/servers/handlers/DebuggingHandler.ts create mode 100644 libs/remix-ai-core/src/servers/handlers/DeploymentHandler.ts create mode 100644 libs/remix-ai-core/src/servers/handlers/FileManagementHandler.ts create mode 100644 libs/remix-ai-core/src/servers/index.ts create mode 100644 libs/remix-ai-core/src/servers/middleware/SecurityMiddleware.ts create mode 100644 libs/remix-ai-core/src/servers/middleware/ValidationMiddleware.ts create mode 100644 libs/remix-ai-core/src/servers/providers/CompilationResourceProvider.ts create mode 100644 libs/remix-ai-core/src/servers/providers/DeploymentResourceProvider.ts create mode 100644 libs/remix-ai-core/src/servers/providers/ProjectResourceProvider.ts create mode 100644 libs/remix-ai-core/src/servers/registry/RemixResourceProviderRegistry.ts create mode 100644 libs/remix-ai-core/src/servers/registry/RemixToolRegistry.ts create mode 100644 libs/remix-ai-core/src/servers/tests/RemixMCPServer.test.ts create mode 100644 libs/remix-ai-core/src/servers/types/mcpResources.ts create mode 100644 libs/remix-ai-core/src/servers/types/mcpServer.ts create mode 100644 libs/remix-ai-core/src/servers/types/mcpTools.ts create mode 100644 libs/remix-ai-core/src/services/__tests__/intentMatching.test.ts create mode 100644 libs/remix-ai-core/src/services/intentAnalyzer.ts create mode 100644 libs/remix-ai-core/src/services/resourceScoring.ts diff --git a/libs/remix-ai-core/src/inferencers/local/systemPrompts.ts b/libs/remix-ai-core/src/inferencers/local/systemPrompts.ts index e325ebc2193..81f7fa4a333 100644 --- a/libs/remix-ai-core/src/inferencers/local/systemPrompts.ts +++ b/libs/remix-ai-core/src/inferencers/local/systemPrompts.ts @@ -43,9 +43,9 @@ For a simple ERC-20 token contract, the JSON output might look like this: ] }`; -export const WORKSPACE_PROMPT = "You are a coding assistant with full access to the user's project workspace.\nWhen the user provides a prompt describing a desired change or feature, follow these steps:\nAnalyze the Prompt: Understand the user's intent, including what functionality or change is required.\nInspect the Codebase: Review the relevant parts of the workspace to identify which files are related to the requested change.\nDetermine Affected Files: Decide which files need to be modified or created.\nGenerate Full Modified Files: For each affected file, return the entire updated file content, not just the diff or patch.\n\nOutput format\n {\n \"files\": [\n {\n \"fileName\": \"\",\n \"content\": \"FULL CONTENT OF THE MODIFIED FILE HERE\"\n }\n ]\n }\nOnly include files that need to be modified or created. Do not include files that are unchanged.\nBe precise, complete, and maintain formatting and coding conventions consistent with the rest of the project.\nIf the change spans multiple files, ensure that all related parts are synchronized.\n" +export const WORKSPACE_PROMPT = "You are a coding assistant with full access to the user's project workspace and intelligent access to relevant contextual resources.\nWhen the user provides a prompt describing a desired change or feature, follow these steps:\nAnalyze the Prompt: Understand the user's intent, including what functionality or change is required. Consider any provided contextual resources that may contain relevant patterns, examples, or documentation.\nInspect the Codebase: Review the relevant parts of the workspace to identify which files are related to the requested change. Use insights from contextual resources to better understand existing patterns and conventions.\nDetermine Affected Files: Decide which files need to be modified or created based on both workspace analysis and contextual insights from relevant resources.\nGenerate Full Modified Files: For each affected file, return the entire updated file content, not just the diff or patch. Ensure consistency with patterns and best practices shown in contextual resources.\n\nOutput format\n {\n \"files\": [\n {\n \"fileName\": \"\",\n \"content\": \"FULL CONTENT OF THE MODIFIED FILE HERE\"\n }\n ]\n }\nOnly include files that need to be modified or created. Do not include files that are unchanged.\nBe precise, complete, and maintain formatting and coding conventions consistent with the rest of the project.\nIf the change spans multiple files, ensure that all related parts are synchronized.\nLeverage provided contextual resources (documentation, examples, API references, code patterns) to ensure best practices, compatibility, and adherence to established conventions.\n" -export const CHAT_PROMPT = "You are a Web3 AI assistant integrated into the Remix IDE named RemixAI. Your primary role is to help developers write, understand, debug, and optimize smart contracts and other related Web3 code. You must provide secure, gas-efficient, and up-to-date advice. Be concise and accurate, especially when dealing with smart contract vulnerabilities, compiler versions, and Ethereum development best practices.\nYour capabilities include:\nExplaining Major web3 programming (solidity, noir, circom, Vyper) syntax, security issues (e.g., reentrancy, underflow/overflow), and design patterns.\nReviewing and improving smart contracts for gas efficiency, security, and readability.\nHelping with Remix plugins, compiler settings, and deployment via the Remix IDE interface.\nExplaining interactions with web3.js, ethers.js, Hardhat, Foundry, OpenZeppelin, etc., if needed.\nWriting and explaining unit tests, especially in JavaScript/typescript or Solidity.\nRules:\nPrioritize secure coding and modern Solidity (e.g., ^0.8.x).\nNever give advice that could result in loss of funds (e.g., suggest unguarded delegatecall).\nIf unsure about a version-specific feature or behavior, clearly state the assumption.\nDefault to using best practices (e.g., require, SafeERC20, OpenZeppelin libraries).\nBe helpful but avoid speculative or misleading answers — if a user asks for something unsafe, clearly warn them.\nIf a user shares code, analyze it carefully and suggest improvements with reasoning. If they ask for a snippet, return a complete, copy-pastable example formatted in Markdown code blocks." +export const CHAT_PROMPT = "You are a Web3 AI assistant integrated into the Remix IDE named RemixAI with intelligent access to contextual resources. Your primary role is to help developers write, understand, debug, and optimize smart contracts and other related Web3 code. You must provide secure, gas-efficient, and up-to-date advice. Be concise and accurate, especially when dealing with smart contract vulnerabilities, compiler versions, and Ethereum development best practices.\nWhen contextual resources are provided (documentation, examples, API references), use them to enhance your responses with relevant, up-to-date information and established patterns.\nYour capabilities include:\nExplaining Major web3 programming (solidity, noir, circom, Vyper) syntax, security issues (e.g., reentrancy, underflow/overflow), and design patterns, enhanced by relevant contextual resources.\nReviewing and improving smart contracts for gas efficiency, security, and readability using best practices from provided resources.\nHelping with Remix plugins, compiler settings, and deployment via the Remix IDE interface, referencing current documentation when available.\nExplaining interactions with web3.js, ethers.js, Hardhat, Foundry, OpenZeppelin, etc., using the most current information from contextual resources.\nWriting and explaining unit tests, especially in JavaScript/typescript or Solidity, following patterns from relevant examples.\nRules:\nPrioritize secure coding and modern Solidity (e.g., ^0.8.x), referencing security best practices from contextual resources.\nNever give advice that could result in loss of funds (e.g., suggest unguarded delegatecall).\nIf unsure about a version-specific feature or behavior, clearly state the assumption and reference contextual resources when available.\nDefault to using best practices (e.g., require, SafeERC20, OpenZeppelin libraries) and patterns shown in contextual resources.\nBe helpful but avoid speculative or misleading answers — if a user asks for something unsafe, clearly warn them and reference security resources if available.\nIf a user shares code, analyze it carefully and suggest improvements with reasoning. If they ask for a snippet, return a complete, copy-pastable example formatted in Markdown code blocks, incorporating patterns from contextual resources when relevant." // Additional system prompts for specific use cases export const CODE_COMPLETION_PROMPT = "You are a code completion assistant. Complete the code provided, focusing on the immediate next lines needed. Provide only the code that should be added, without explanations or comments unless they are part of the code itself. Do not return ``` for signalising code." @@ -58,4 +58,9 @@ export const CODE_EXPLANATION_PROMPT = "You are a code explanation assistant. Pr export const ERROR_EXPLANATION_PROMPT = "You are a debugging assistant. Help explain errors and provide practical solutions. Focus on what the error means, common causes, step-by-step solutions, and prevention tips." -export const SECURITY_ANALYSIS_PROMPT = "You are a security analysis assistant. Identify vulnerabilities and provide security recommendations for code. Check for common security issues, best practice violations, potential attack vectors, and provide detailed recommendations for fixes." +export const SECURITY_ANALYSIS_PROMPT = "You are a security analysis assistant with access to security documentation and best practices. Identify vulnerabilities and provide security recommendations for code. Check for common security issues, best practice violations, potential attack vectors, and provide detailed recommendations for fixes. Reference security patterns and guidelines from contextual resources when available." + +// MCP-enhanced prompts that leverage contextual resources +export const MCP_CONTEXT_INTEGRATION_PROMPT = "When contextual resources are provided, integrate them intelligently into your responses:\n- Use documentation resources to provide accurate, up-to-date information\n- Reference code examples to show established patterns and conventions\n- Apply API references to ensure correct usage and parameters\n- Follow security guidelines from relevant security resources\n- Adapt to project-specific patterns shown in contextual resources\nAlways indicate when you're referencing contextual resources and explain their relevance." + +export const INTENT_AWARE_PROMPT = "Based on the user's intent and query complexity:\n- For coding tasks: Prioritize code examples, templates, and implementation guides\n- For documentation tasks: Focus on explanatory resources, concept definitions, and tutorials\n- For debugging tasks: Emphasize troubleshooting guides, error references, and solution patterns\n- For explanation tasks: Use educational resources, concept explanations, and theoretical guides\n- For generation tasks: Leverage templates, boilerplates, and scaffold examples\n- For completion tasks: Reference API documentation, method signatures, and usage examples\nAdjust resource selection and response style to match the identified intent." diff --git a/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts b/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts index f848f915d5e..4268b26bd33 100644 --- a/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts +++ b/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts @@ -12,8 +12,14 @@ import { MCPConnectionStatus, MCPInitializeResult, MCPProviderParams, - MCPAwareParams + MCPAwareParams, + EnhancedMCPProviderParams, + UserIntent, + ResourceScore, + ResourceSelectionResult } from "../../types/mcp"; +import { IntentAnalyzer } from "../../services/intentAnalyzer"; +import { ResourceScoring } from "../../services/resourceScoring"; export class MCPClient { private server: MCPServer; @@ -177,6 +183,8 @@ export class MCPInferencer extends RemoteInferencer implements ICompletions, IGe private connectionStatuses: Map = new Map(); private resourceCache: Map = new Map(); private cacheTimeout: number = 300000; // 5 minutes + private intentAnalyzer: IntentAnalyzer = new IntentAnalyzer(); + private resourceScoring: ResourceScoring = new ResourceScoring(); constructor(servers: MCPServer[] = [], apiUrl?: string, completionUrl?: string) { super(apiUrl, completionUrl); @@ -272,17 +280,117 @@ export class MCPInferencer extends RemoteInferencer implements ICompletions, IGe } } - private async enrichContextWithMCPResources(params: IParams): Promise { - const mcpParams = (params as any).mcp as MCPProviderParams; + private async enrichContextWithMCPResources(params: IParams, prompt?: string): Promise { + const mcpParams = (params as any).mcp as EnhancedMCPProviderParams; if (!mcpParams?.mcpServers?.length) { return ""; } + // Use intelligent resource selection if enabled + if (mcpParams.enableIntentMatching && prompt) { + return this.intelligentResourceSelection(prompt, mcpParams); + } + + // Fallback to original logic + return this.legacyResourceSelection(mcpParams); + } + + private async intelligentResourceSelection(prompt: string, mcpParams: EnhancedMCPProviderParams): Promise { + try { + // Analyze user intent + const intent = await this.intentAnalyzer.analyzeIntent(prompt); + + // Gather all available resources + const allResources: Array<{ resource: MCPResource; serverName: string }> = []; + + for (const serverName of mcpParams.mcpServers || []) { + const client = this.mcpClients.get(serverName); + if (!client || !client.isConnected()) continue; + + try { + const resources = await client.listResources(); + resources.forEach(resource => { + allResources.push({ resource, serverName }); + }); + } catch (error) { + console.warn(`Failed to list resources from ${serverName}:`, error); + } + } + + if (allResources.length === 0) { + return ""; + } + + // Score resources against intent + const scoredResources = await this.resourceScoring.scoreResources( + allResources, + intent, + mcpParams + ); + + // Select best resources + const selectedResources = this.resourceScoring.selectResources( + scoredResources, + mcpParams.maxResources || 10, + mcpParams.selectionStrategy || 'hybrid' + ); + + // Log selection for debugging + this.event.emit('mcpResourceSelection', { + intent, + totalResourcesConsidered: allResources.length, + selectedResources: selectedResources.map(r => ({ + name: r.resource.name, + score: r.score, + reasoning: r.reasoning + })) + }); + + // Build context from selected resources + let mcpContext = ""; + for (const scoredResource of selectedResources) { + const { resource, serverName } = scoredResource; + + try { + // Try to get from cache first + let content = this.resourceCache.get(resource.uri); + if (!content) { + const client = this.mcpClients.get(serverName); + if (client) { + content = await client.readResource(resource.uri); + // Cache with TTL + this.resourceCache.set(resource.uri, content); + setTimeout(() => { + this.resourceCache.delete(resource.uri); + }, this.cacheTimeout); + } + } + + if (content?.text) { + mcpContext += `\n--- Resource: ${resource.name} (Score: ${Math.round(scoredResource.score * 100)}%) ---\n`; + mcpContext += `Relevance: ${scoredResource.reasoning}\n`; + mcpContext += content.text; + mcpContext += "\n--- End Resource ---\n"; + } + } catch (error) { + console.warn(`Failed to read resource ${resource.uri}:`, error); + } + } + + return mcpContext; + } catch (error) { + console.error('Error in intelligent resource selection:', error); + // Fallback to legacy selection + return this.legacyResourceSelection(mcpParams); + } + } + + private async legacyResourceSelection(mcpParams: EnhancedMCPProviderParams): Promise { let mcpContext = ""; const maxResources = mcpParams.maxResources || 10; let resourceCount = 0; - for (const serverName of mcpParams.mcpServers) { + for (const serverName of mcpParams.mcpServers || []) { if (resourceCount >= maxResources) break; const client = this.mcpClients.get(serverName); @@ -329,35 +437,35 @@ export class MCPInferencer extends RemoteInferencer implements ICompletions, IGe // Override completion methods to include MCP context async code_completion(prompt: string, promptAfter: string, ctxFiles: any, fileName: string, options: IParams = CompletionParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, prompt); const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; return super.code_completion(enrichedPrompt, promptAfter, ctxFiles, fileName, options); } async code_insertion(msg_pfx: string, msg_sfx: string, ctxFiles: any, fileName: string, options: IParams = InsertionParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, msg_pfx); const enrichedPrefix = mcpContext ? `${mcpContext}\n\n${msg_pfx}` : msg_pfx; return super.code_insertion(enrichedPrefix, msg_sfx, ctxFiles, fileName, options); } async code_generation(prompt: string, options: IParams = GenerationParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, prompt); const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; return super.code_generation(enrichedPrompt, options); } async answer(prompt: string, options: IParams = GenerationParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, prompt); const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; return super.answer(enrichedPrompt, options); } async code_explaining(prompt: string, context: string = "", options: IParams = GenerationParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, prompt); const enrichedContext = mcpContext ? `${mcpContext}\n\n${context}` : context; return super.code_explaining(prompt, enrichedContext, options); @@ -432,11 +540,13 @@ export class MCPEnhancedInferencer implements ICompletions, IGeneration { private connectionStatuses: Map = new Map(); private resourceCache: Map = new Map(); private cacheTimeout: number = 300000; // 5 minutes + private intentAnalyzer: IntentAnalyzer = new IntentAnalyzer(); + private resourceScoring: ResourceScoring = new ResourceScoring(); public event: EventEmitter; constructor(baseInferencer: ICompletions & IGeneration, servers: MCPServer[] = []) { this.baseInferencer = baseInferencer; - this.event = this.baseInferencer.event || new EventEmitter(); + this.event = new EventEmitter(); this.initializeMCPServers(servers); } @@ -542,17 +652,117 @@ export class MCPEnhancedInferencer implements ICompletions, IGeneration { } } - private async enrichContextWithMCPResources(params: IParams): Promise { - const mcpParams = (params as any).mcp as MCPProviderParams; + private async enrichContextWithMCPResources(params: IParams, prompt?: string): Promise { + const mcpParams = (params as any).mcp as EnhancedMCPProviderParams; if (!mcpParams?.mcpServers?.length) { return ""; } + // Use intelligent resource selection if enabled + if (mcpParams.enableIntentMatching && prompt) { + return this.intelligentResourceSelection(prompt, mcpParams); + } + + // Fallback to original logic + return this.legacyResourceSelection(mcpParams); + } + + private async intelligentResourceSelection(prompt: string, mcpParams: EnhancedMCPProviderParams): Promise { + try { + // Analyze user intent + const intent = await this.intentAnalyzer.analyzeIntent(prompt); + + // Gather all available resources + const allResources: Array<{ resource: MCPResource; serverName: string }> = []; + + for (const serverName of mcpParams.mcpServers || []) { + const client = this.mcpClients.get(serverName); + if (!client || !client.isConnected()) continue; + + try { + const resources = await client.listResources(); + resources.forEach(resource => { + allResources.push({ resource, serverName }); + }); + } catch (error) { + console.warn(`Failed to list resources from ${serverName}:`, error); + } + } + + if (allResources.length === 0) { + return ""; + } + + // Score resources against intent + const scoredResources = await this.resourceScoring.scoreResources( + allResources, + intent, + mcpParams + ); + + // Select best resources + const selectedResources = this.resourceScoring.selectResources( + scoredResources, + mcpParams.maxResources || 10, + mcpParams.selectionStrategy || 'hybrid' + ); + + // Log selection for debugging + this.event.emit('mcpResourceSelection', { + intent, + totalResourcesConsidered: allResources.length, + selectedResources: selectedResources.map(r => ({ + name: r.resource.name, + score: r.score, + reasoning: r.reasoning + })) + }); + + // Build context from selected resources + let mcpContext = ""; + for (const scoredResource of selectedResources) { + const { resource, serverName } = scoredResource; + + try { + // Try to get from cache first + let content = this.resourceCache.get(resource.uri); + if (!content) { + const client = this.mcpClients.get(serverName); + if (client) { + content = await client.readResource(resource.uri); + // Cache with TTL + this.resourceCache.set(resource.uri, content); + setTimeout(() => { + this.resourceCache.delete(resource.uri); + }, this.cacheTimeout); + } + } + + if (content?.text) { + mcpContext += `\n--- Resource: ${resource.name} (Score: ${Math.round(scoredResource.score * 100)}%) ---\n`; + mcpContext += `Relevance: ${scoredResource.reasoning}\n`; + mcpContext += content.text; + mcpContext += "\n--- End Resource ---\n"; + } + } catch (error) { + console.warn(`Failed to read resource ${resource.uri}:`, error); + } + } + + return mcpContext; + } catch (error) { + console.error('Error in intelligent resource selection:', error); + // Fallback to legacy selection + return this.legacyResourceSelection(mcpParams); + } + } + + private async legacyResourceSelection(mcpParams: EnhancedMCPProviderParams): Promise { let mcpContext = ""; const maxResources = mcpParams.maxResources || 10; let resourceCount = 0; - for (const serverName of mcpParams.mcpServers) { + for (const serverName of mcpParams.mcpServers || []) { if (resourceCount >= maxResources) break; const client = this.mcpClients.get(serverName); @@ -599,40 +809,46 @@ export class MCPEnhancedInferencer implements ICompletions, IGeneration { // Override completion methods to include MCP context async code_completion(prompt: string, promptAfter: string, ctxFiles: any, fileName: string, options: IParams = CompletionParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, prompt); const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; return this.baseInferencer.code_completion(enrichedPrompt, promptAfter, ctxFiles, fileName, options); } async code_insertion(msg_pfx: string, msg_sfx: string, ctxFiles: any, fileName: string, options: IParams = InsertionParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, msg_pfx); const enrichedPrefix = mcpContext ? `${mcpContext}\n\n${msg_pfx}` : msg_pfx; return this.baseInferencer.code_insertion(enrichedPrefix, msg_sfx, ctxFiles, fileName, options); } async code_generation(prompt: string, options: IParams = GenerationParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, prompt); const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; return this.baseInferencer.code_generation(enrichedPrompt, options); } async answer(prompt: string, options: IParams = GenerationParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, prompt); const enrichedPrompt = mcpContext ? `${mcpContext}\n\n${prompt}` : prompt; return this.baseInferencer.answer(enrichedPrompt, options); } async code_explaining(prompt: string, context: string = "", options: IParams = GenerationParams): Promise { - const mcpContext = await this.enrichContextWithMCPResources(options); + const mcpContext = await this.enrichContextWithMCPResources(options, prompt); const enrichedContext = mcpContext ? `${mcpContext}\n\n${context}` : context; return this.baseInferencer.code_explaining(prompt, enrichedContext, options); } + async error_explaining(prompt, params:IParams): Promise{} + async generate(prompt, params:IParams): Promise{} + async generateWorkspace(prompt, params:IParams): Promise{} + async vulnerability_check(prompt, params:IParams): Promise{} + + // MCP-specific methods getConnectionStatuses(): MCPConnectionStatus[] { return Array.from(this.connectionStatuses.values()); diff --git a/libs/remix-ai-core/src/servers/RemixAPIIntegration.ts b/libs/remix-ai-core/src/servers/RemixAPIIntegration.ts new file mode 100644 index 00000000000..712abd8bcd2 --- /dev/null +++ b/libs/remix-ai-core/src/servers/RemixAPIIntegration.ts @@ -0,0 +1,598 @@ +/** + * Remix API Integration Layer for MCP Server + * Bridges the gap between MCP tools and actual Remix IDE APIs + */ + +import { ICustomRemixApi } from '@remix-api'; + +export interface RemixAPIIntegration { + fileManager: RemixFileManager; + compiler: RemixCompiler; + deployer: RemixDeployer; + debugger: RemixDebugger; + config: RemixConfig; +} + +export interface RemixFileManager { + exists(path: string): Promise; + readFile(path: string): Promise; + writeFile(path: string, content: string): Promise; + readdir(path: string): Promise; + isDirectory(path: string): Promise; + mkdir(path: string): Promise; + remove(path: string): Promise; + rename(from: string, to: string): Promise; + getWorkspaceRoot(): Promise; +} + +export interface RemixCompiler { + compile(args?: any): Promise; + getCompilationResult(): Promise; + setCompilerConfig(config: any): Promise; + getCompilerConfig(): Promise; + getAvailableVersions(): Promise; +} + +export interface RemixDeployer { + deploy(args: any): Promise; + getDeployedContracts(): Promise; + getAccounts(): Promise; + getBalance(address: string): Promise; + setEnvironment(env: string): Promise; + getCurrentEnvironment(): Promise; +} + +export interface RemixDebugger { + startDebugSession(args: any): Promise; + setBreakpoint(args: any): Promise; + step(args: any): Promise; + getCallStack(sessionId: string): Promise; + getVariables(sessionId: string): Promise; + stopDebugSession(sessionId: string): Promise; +} + +export interface RemixConfig { + getAppParameter(key: string): Promise; + setAppParameter(key: string, value: string): Promise; + getWorkspaceConfig(): Promise; +} + +/** + * Implementation that integrates with actual Remix APIs + */ +export class RemixAPIIntegrationImpl implements RemixAPIIntegration { + public fileManager: RemixFileManager; + public compiler: RemixCompiler; + public deployer: RemixDeployer; + public debugger: RemixDebugger; + public config: RemixConfig; + + constructor(private remixApi: ICustomRemixApi) { + this.fileManager = new RemixFileManagerImpl(remixApi); + this.compiler = new RemixCompilerImpl(remixApi); + this.deployer = new RemixDeployerImpl(remixApi); + this.debugger = new RemixDebuggerImpl(remixApi); + this.config = new RemixConfigImpl(remixApi); + } +} + +/** + * File Manager Implementation + */ +class RemixFileManagerImpl implements RemixFileManager { + constructor(private api: ICustomRemixApi) {} + + async exists(path: string): Promise { + try { + return await this.api.fileManager.methods.exists(path); + } catch (error) { + console.warn(`File existence check failed for ${path}:`, error); + return false; + } + } + + async readFile(path: string): Promise { + try { + return await this.api.fileManager.methods.readFile(path); + } catch (error) { + throw new Error(`Failed to read file ${path}: ${error.message}`); + } + } + + async writeFile(path: string, content: string): Promise { + try { + await this.api.fileManager.methods.writeFile(path, content); + } catch (error) { + throw new Error(`Failed to write file ${path}: ${error.message}`); + } + } + + async readdir(path: string): Promise { + try { + return await this.api.fileManager.methods.readdir(path); + } catch (error) { + throw new Error(`Failed to read directory ${path}: ${error.message}`); + } + } + + async isDirectory(path: string): Promise { + try { + return await this.api.fileManager.methods.isDirectory(path); + } catch (error) { + console.warn(`Directory check failed for ${path}:`, error); + return false; + } + } + + async mkdir(path: string): Promise { + try { + await this.api.fileManager.methods.mkdir(path); + } catch (error) { + throw new Error(`Failed to create directory ${path}: ${error.message}`); + } + } + + async remove(path: string): Promise { + try { + await this.api.fileManager.methods.remove(path); + } catch (error) { + throw new Error(`Failed to remove ${path}: ${error.message}`); + } + } + + async rename(from: string, to: string): Promise { + try { + await this.api.fileManager.methods.rename(from, to); + } catch (error) { + throw new Error(`Failed to rename ${from} to ${to}: ${error.message}`); + } + } + + async getWorkspaceRoot(): Promise { + try { + // TODO: Implement getting workspace root from Remix API + // This might need to be accessed through workspace plugin + return ''; + } catch (error) { + throw new Error(`Failed to get workspace root: ${error.message}`); + } + } +} + +/** + * Compiler Implementation + */ +class RemixCompilerImpl implements RemixCompiler { + constructor(private api: ICustomRemixApi) {} + + async compile(args?: any): Promise { + try { + // TODO: Integrate with Remix Solidity plugin + // The compilation needs to be triggered through the solidity plugin + // For now, return mock data + throw new Error('Compilation integration not yet implemented - requires Solidity plugin integration'); + } catch (error) { + throw new Error(`Compilation failed: ${error.message}`); + } + } + + async getCompilationResult(): Promise { + try { + // TODO: Get compilation result from Solidity plugin + throw new Error('Get compilation result not yet implemented - requires Solidity plugin integration'); + } catch (error) { + throw new Error(`Failed to get compilation result: ${error.message}`); + } + } + + async setCompilerConfig(config: any): Promise { + try { + await this.api.config.methods.setAppParameter('solidity-compiler', JSON.stringify(config)); + } catch (error) { + throw new Error(`Failed to set compiler config: ${error.message}`); + } + } + + async getCompilerConfig(): Promise { + try { + const configString = await this.api.config.methods.getAppParameter('solidity-compiler'); + if (configString) { + return JSON.parse(configString); + } + return { + version: 'latest', + optimize: true, + runs: 200, + evmVersion: 'london', + language: 'Solidity' + }; + } catch (error) { + throw new Error(`Failed to get compiler config: ${error.message}`); + } + } + + async getAvailableVersions(): Promise { + try { + // TODO: Get available versions from Solidity plugin + return ['0.8.19', '0.8.18', '0.8.17', '0.8.16', '0.8.15']; + } catch (error) { + throw new Error(`Failed to get available versions: ${error.message}`); + } + } +} + +/** + * Deployer Implementation + */ +class RemixDeployerImpl implements RemixDeployer { + constructor(private api: ICustomRemixApi) {} + + async deploy(args: any): Promise { + try { + // TODO: Integrate with Remix Run Tab plugin for deployment + throw new Error('Deployment integration not yet implemented - requires Run Tab plugin integration'); + } catch (error) { + throw new Error(`Deployment failed: ${error.message}`); + } + } + + async getDeployedContracts(): Promise { + try { + // TODO: Get deployed contracts from Run Tab plugin storage + return []; + } catch (error) { + throw new Error(`Failed to get deployed contracts: ${error.message}`); + } + } + + async getAccounts(): Promise { + try { + // TODO: Get accounts from current provider + // This would need to access the current provider through Run Tab + return ['0x' + Math.random().toString(16).substr(2, 40)]; // Mock account + } catch (error) { + throw new Error(`Failed to get accounts: ${error.message}`); + } + } + + async getBalance(address: string): Promise { + try { + // TODO: Get balance from current provider + return (Math.random() * 10).toFixed(4); + } catch (error) { + throw new Error(`Failed to get balance for ${address}: ${error.message}`); + } + } + + async setEnvironment(env: string): Promise { + try { + // TODO: Set environment in Run Tab plugin + throw new Error('Set environment not yet implemented - requires Run Tab plugin integration'); + } catch (error) { + throw new Error(`Failed to set environment to ${env}: ${error.message}`); + } + } + + async getCurrentEnvironment(): Promise { + try { + // TODO: Get current environment from Run Tab plugin + return 'vm-london'; // Mock environment + } catch (error) { + throw new Error(`Failed to get current environment: ${error.message}`); + } + } +} + +/** + * Debugger Implementation + */ +class RemixDebuggerImpl implements RemixDebugger { + constructor(private api: ICustomRemixApi) {} + + async startDebugSession(args: any): Promise { + try { + // TODO: Integrate with Remix debugger plugin + throw new Error('Debug session start not yet implemented - requires Debugger plugin integration'); + } catch (error) { + throw new Error(`Failed to start debug session: ${error.message}`); + } + } + + async setBreakpoint(args: any): Promise { + try { + // TODO: Set breakpoint through debugger plugin + throw new Error('Set breakpoint not yet implemented - requires Debugger plugin integration'); + } catch (error) { + throw new Error(`Failed to set breakpoint: ${error.message}`); + } + } + + async step(args: any): Promise { + try { + // TODO: Step through debugger plugin + throw new Error('Debug step not yet implemented - requires Debugger plugin integration'); + } catch (error) { + throw new Error(`Failed to step: ${error.message}`); + } + } + + async getCallStack(sessionId: string): Promise { + try { + // TODO: Get call stack from debugger plugin + throw new Error('Get call stack not yet implemented - requires Debugger plugin integration'); + } catch (error) { + throw new Error(`Failed to get call stack: ${error.message}`); + } + } + + async getVariables(sessionId: string): Promise { + try { + // TODO: Get variables from debugger plugin + throw new Error('Get variables not yet implemented - requires Debugger plugin integration'); + } catch (error) { + throw new Error(`Failed to get variables: ${error.message}`); + } + } + + async stopDebugSession(sessionId: string): Promise { + try { + // TODO: Stop debug session through debugger plugin + throw new Error('Stop debug session not yet implemented - requires Debugger plugin integration'); + } catch (error) { + throw new Error(`Failed to stop debug session: ${error.message}`); + } + } +} + +/** + * Config Implementation + */ +class RemixConfigImpl implements RemixConfig { + constructor(private api: ICustomRemixApi) {} + + async getAppParameter(key: string): Promise { + try { + return await this.api.config.methods.getAppParameter(key); + } catch (error) { + throw new Error(`Failed to get app parameter ${key}: ${error.message}`); + } + } + + async setAppParameter(key: string, value: string): Promise { + try { + await this.api.config.methods.setAppParameter(key, value); + } catch (error) { + throw new Error(`Failed to set app parameter ${key}: ${error.message}`); + } + } + + async getWorkspaceConfig(): Promise { + try { + // TODO: Get workspace-specific configuration + return {}; + } catch (error) { + throw new Error(`Failed to get workspace config: ${error.message}`); + } + } +} + +/** + * Plugin Integration Helper + * Helps with accessing Remix plugins that aren't directly available through ICustomRemixApi + */ +export class PluginIntegrationHelper { + constructor(private api: ICustomRemixApi) {} + + /** + * Get Solidity compiler plugin + */ + async getSolidityPlugin(): Promise { + try { + // TODO: Access solidity plugin + // This would typically be through: + // return await this.api.pluginManager.getPlugin('solidity'); + throw new Error('Plugin access not yet implemented'); + } catch (error) { + throw new Error(`Failed to get Solidity plugin: ${error.message}`); + } + } + + /** + * Get Run Tab plugin (for deployment) + */ + async getRunTabPlugin(): Promise { + try { + // TODO: Access run tab plugin + throw new Error('Plugin access not yet implemented'); + } catch (error) { + throw new Error(`Failed to get Run Tab plugin: ${error.message}`); + } + } + + /** + * Get Debugger plugin + */ + async getDebuggerPlugin(): Promise { + try { + // TODO: Access debugger plugin + throw new Error('Plugin access not yet implemented'); + } catch (error) { + throw new Error(`Failed to get Debugger plugin: ${error.message}`); + } + } + + /** + * Get File Manager plugin + */ + async getFileManagerPlugin(): Promise { + try { + // TODO: Access file manager plugin + throw new Error('Plugin access not yet implemented'); + } catch (error) { + throw new Error(`Failed to get File Manager plugin: ${error.message}`); + } + } +} + +/** + * Factory function to create API integration + */ +export function createRemixAPIIntegration(remixApi: ICustomRemixApi): RemixAPIIntegration { + return new RemixAPIIntegrationImpl(remixApi); +} + +/** + * Mock implementation for testing + */ +export class MockRemixAPIIntegration implements RemixAPIIntegration { + public fileManager: RemixFileManager; + public compiler: RemixCompiler; + public deployer: RemixDeployer; + public debugger: RemixDebugger; + public config: RemixConfig; + + constructor() { + this.fileManager = new MockFileManager(); + this.compiler = new MockCompiler(); + this.deployer = new MockDeployer(); + this.debugger = new MockDebugger(); + this.config = new MockConfig(); + } +} + +// Mock implementations for testing +class MockFileManager implements RemixFileManager { + private files = new Map(); + + async exists(path: string): Promise { + return this.files.has(path); + } + + async readFile(path: string): Promise { + const content = this.files.get(path); + if (!content) throw new Error(`File not found: ${path}`); + return content; + } + + async writeFile(path: string, content: string): Promise { + this.files.set(path, content); + } + + async readdir(path: string): Promise { + return Array.from(this.files.keys()).filter(key => key.startsWith(path)); + } + + async isDirectory(path: string): Promise { + return path.endsWith('/'); + } + + async mkdir(path: string): Promise { + // Mock implementation + } + + async remove(path: string): Promise { + this.files.delete(path); + } + + async rename(from: string, to: string): Promise { + const content = this.files.get(from); + if (content) { + this.files.set(to, content); + this.files.delete(from); + } + } + + async getWorkspaceRoot(): Promise { + return '/workspace'; + } +} + +class MockCompiler implements RemixCompiler { + async compile(args?: any): Promise { + return { success: true, contracts: {} }; + } + + async getCompilationResult(): Promise { + return { success: true, contracts: {} }; + } + + async setCompilerConfig(config: any): Promise { + // Mock implementation + } + + async getCompilerConfig(): Promise { + return { version: '0.8.19', optimize: true }; + } + + async getAvailableVersions(): Promise { + return ['0.8.19', '0.8.18']; + } +} + +class MockDeployer implements RemixDeployer { + async deploy(args: any): Promise { + return { success: true, address: '0x1234' }; + } + + async getDeployedContracts(): Promise { + return []; + } + + async getAccounts(): Promise { + return ['0x1234567890abcdef']; + } + + async getBalance(address: string): Promise { + return '10.0'; + } + + async setEnvironment(env: string): Promise { + // Mock implementation + } + + async getCurrentEnvironment(): Promise { + return 'vm-london'; + } +} + +class MockDebugger implements RemixDebugger { + async startDebugSession(args: any): Promise { + return { sessionId: 'debug_123' }; + } + + async setBreakpoint(args: any): Promise { + return { breakpointId: 'bp_123' }; + } + + async step(args: any): Promise { + return { success: true }; + } + + async getCallStack(sessionId: string): Promise { + return { stack: [] }; + } + + async getVariables(sessionId: string): Promise { + return { variables: {} }; + } + + async stopDebugSession(sessionId: string): Promise { + // Mock implementation + } +} + +class MockConfig implements RemixConfig { + private config = new Map(); + + async getAppParameter(key: string): Promise { + return this.config.get(key) || ''; + } + + async setAppParameter(key: string, value: string): Promise { + this.config.set(key, value); + } + + async getWorkspaceConfig(): Promise { + return {}; + } +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/servers/RemixMCPServer.ts b/libs/remix-ai-core/src/servers/RemixMCPServer.ts new file mode 100644 index 00000000000..4e8454c1df7 --- /dev/null +++ b/libs/remix-ai-core/src/servers/RemixMCPServer.ts @@ -0,0 +1,547 @@ +/** + * Remix IDE MCP Server Implementation + */ + +import EventEmitter from 'events'; +import { ICustomRemixApi } from '@remix-api'; +import { + MCPInitializeResult, + MCPServerCapabilities, + MCPToolCall, + MCPToolResult, + MCPResource, + MCPResourceContent +} from '../types/mcp'; +import { + IRemixMCPServer, + RemixMCPServerConfig, + ServerState, + ServerStats, + ToolExecutionStatus, + ResourceCacheEntry, + AuditLogEntry, + PermissionCheckResult, + MCPMessage, + MCPResponse, + MCPErrorCode, + ServerEvents +} from './types/mcpServer'; +import { ToolRegistry } from './types/mcpTools'; +import { ResourceProviderRegistry } from './types/mcpResources'; +import { RemixToolRegistry } from './registry/RemixToolRegistry'; +import { RemixResourceProviderRegistry } from './registry/RemixResourceProviderRegistry'; + +/** + * Main Remix MCP Server implementation + */ +export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { + private _config: RemixMCPServerConfig; + private _state: ServerState = ServerState.STOPPED; + private _stats: ServerStats; + private _tools: ToolRegistry; + private _resources: ResourceProviderRegistry; + private _remixApi: ICustomRemixApi; + private _activeExecutions: Map = new Map(); + private _resourceCache: Map = new Map(); + private _auditLog: AuditLogEntry[] = []; + private _startTime: Date = new Date(); + + constructor(config: RemixMCPServerConfig, remixApi: ICustomRemixApi) { + super(); + this._config = config; + this._remixApi = remixApi; + this._tools = new RemixToolRegistry(); + this._resources = new RemixResourceProviderRegistry(); + + this._stats = { + uptime: 0, + totalToolCalls: 0, + totalResourcesServed: 0, + activeToolExecutions: 0, + cacheHitRate: 0, + errorCount: 0, + lastActivity: new Date() + }; + + this.setupEventHandlers(); + } + + get config(): RemixMCPServerConfig { + return this._config; + } + + get state(): ServerState { + return this._state; + } + + get stats(): ServerStats { + this._stats.uptime = Date.now() - this._startTime.getTime(); + this._stats.activeToolExecutions = this._activeExecutions.size; + return this._stats; + } + + get tools(): ToolRegistry { + return this._tools; + } + + get resources(): ResourceProviderRegistry { + return this._resources; + } + + get remixApi(): ICustomRemixApi { + return this._remixApi; + } + + /** + * Initialize the MCP server + */ + async initialize(): Promise { + try { + this.setState(ServerState.STARTING); + + // Initialize tool registry with default tools + await this.initializeDefaultTools(); + + // Initialize resource providers + await this.initializeDefaultResourceProviders(); + + // Setup cleanup intervals + this.setupCleanupIntervals(); + + const result: MCPInitializeResult = { + protocolVersion: '2024-11-05', + capabilities: this.getCapabilities(), + serverInfo: { + name: this._config.name, + version: this._config.version + }, + instructions: `Remix IDE MCP Server initialized. Available tools: ${this._tools.list().length}, Resource providers: ${this._resources.list().length}` + }; + + this.setState(ServerState.RUNNING); + this.log('Server initialized successfully', 'info'); + + return result; + } catch (error) { + this.setState(ServerState.ERROR); + this.log(`Server initialization failed: ${error.message}`, 'error'); + throw error; + } + } + + /** + * Start the server + */ + async start(): Promise { + if (this._state !== ServerState.STOPPED) { + throw new Error(`Cannot start server in state: ${this._state}`); + } + + await this.initialize(); + } + + /** + * Stop the server + */ + async stop(): Promise { + this.setState(ServerState.STOPPING); + + // Cancel active tool executions + for (const [id, execution] of this._activeExecutions) { + execution.status = 'failed'; + execution.error = 'Server shutdown'; + execution.endTime = new Date(); + this.emit('tool-executed', execution); + } + this._activeExecutions.clear(); + + // Clear cache + this._resourceCache.clear(); + this.emit('cache-cleared'); + + this.setState(ServerState.STOPPED); + this.log('Server stopped', 'info'); + } + + /** + * Get server capabilities + */ + getCapabilities(): MCPServerCapabilities { + return { + resources: { + subscribe: true, + listChanged: true + }, + tools: { + listChanged: true + }, + prompts: { + listChanged: false + }, + logging: {}, + experimental: { + remix: { + compilation: this._config.features?.compilation !== false, + deployment: this._config.features?.deployment !== false, + debugging: this._config.features?.debugging !== false, + analysis: this._config.features?.analysis !== false, + testing: this._config.features?.testing !== false, + git: this._config.features?.git !== false + } + } + }; + } + + /** + * Handle MCP protocol messages + */ + async handleMessage(message: MCPMessage): Promise { + try { + this._stats.lastActivity = new Date(); + + switch (message.method) { + case 'initialize': + const initResult = await this.initialize(); + return { id: message.id, result: initResult }; + + case 'tools/list': + const tools = this._tools.list().map(tool => ({ + name: tool.name, + description: tool.description, + inputSchema: tool.inputSchema + })); + return { id: message.id, result: { tools } }; + + case 'tools/call': + const toolResult = await this.executeTool(message.params as MCPToolCall); + return { id: message.id, result: toolResult }; + + case 'resources/list': + const resources = await this._resources.getResources(); + return { id: message.id, result: { resources: resources.resources } }; + + case 'resources/read': + const content = await this.getResourceContent(message.params.uri); + return { id: message.id, result: content }; + + case 'server/capabilities': + return { id: message.id, result: this.getCapabilities() }; + + case 'server/stats': + return { id: message.id, result: this.stats }; + + default: + return { + id: message.id, + error: { + code: MCPErrorCode.METHOD_NOT_FOUND, + message: `Unknown method: ${message.method}` + } + }; + } + } catch (error) { + this._stats.errorCount++; + this.log(`Message handling error: ${error.message}`, 'error'); + + return { + id: message.id, + error: { + code: MCPErrorCode.INTERNAL_ERROR, + message: error.message, + data: this._config.debug ? error.stack : undefined + } + }; + } + } + + /** + * Execute a tool + */ + private async executeTool(call: MCPToolCall): Promise { + const executionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const startTime = new Date(); + + const execution: ToolExecutionStatus = { + id: executionId, + toolName: call.name, + startTime, + status: 'running', + context: { + workspace: await this.getCurrentWorkspace(), + user: 'default', // TODO: Get actual user + permissions: [] // TODO: Get actual permissions + } + }; + + this._activeExecutions.set(executionId, execution); + this.emit('tool-executed', execution); + + try { + // Check permissions + const permissionCheck = await this.checkPermissions(`tool:${call.name}`, 'default'); + if (!permissionCheck.allowed) { + throw new Error(`Permission denied: ${permissionCheck.reason}`); + } + + // Set timeout + const timeout = this._config.toolTimeout || 30000; + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Tool execution timeout')), timeout); + }); + + // Execute tool + const toolPromise = this._tools.execute(call, { + workspace: execution.context.workspace, + currentFile: await this.getCurrentFile(), + permissions: execution.context.permissions, + timestamp: Date.now(), + requestId: executionId + }, this._remixApi); + + const result = await Promise.race([toolPromise, timeoutPromise]); + + // Update execution status + execution.status = 'completed'; + execution.endTime = new Date(); + this._stats.totalToolCalls++; + + this.emit('tool-executed', execution); + this.log(`Tool executed: ${call.name}`, 'info', { executionId, duration: execution.endTime.getTime() - startTime.getTime() }); + + return result; + + } catch (error) { + execution.status = error.message.includes('timeout') ? 'timeout' : 'failed'; + execution.error = error.message; + execution.endTime = new Date(); + this._stats.errorCount++; + + this.emit('tool-executed', execution); + this.log(`Tool execution failed: ${call.name}`, 'error', { executionId, error: error.message }); + + throw error; + } finally { + this._activeExecutions.delete(executionId); + } + } + + /** + * Get resource content with caching + */ + private async getResourceContent(uri: string): Promise { + // Check cache first + if (this._config.enableResourceCache !== false) { + const cached = this._resourceCache.get(uri); + if (cached && Date.now() - cached.timestamp.getTime() < cached.ttl) { + cached.accessCount++; + cached.lastAccess = new Date(); + this._stats.totalResourcesServed++; + this.emit('resource-accessed', uri, 'default'); + return cached.content; + } + } + + // Get from provider + const content = await this._resources.getResourceContent(uri); + + // Cache result + if (this._config.enableResourceCache !== false) { + this._resourceCache.set(uri, { + uri, + content, + timestamp: new Date(), + ttl: this._config.resourceCacheTTL || 300000, // 5 minutes default + accessCount: 1, + lastAccess: new Date() + }); + } + + this._stats.totalResourcesServed++; + this.emit('resource-accessed', uri, 'default'); + + return content; + } + + /** + * Check permissions for operation + */ + async checkPermissions(operation: string, user: string, resource?: string): Promise { + // TODO: Implement actual permission checking + // For now, allow all operations + return { + allowed: true, + requiredPermissions: [], + userPermissions: ['*'] + }; + } + + /** + * Get active tool executions + */ + getActiveExecutions(): ToolExecutionStatus[] { + return Array.from(this._activeExecutions.values()); + } + + /** + * Get cache statistics + */ + getCacheStats() { + const entries = Array.from(this._resourceCache.values()); + const totalAccess = entries.reduce((sum, entry) => sum + entry.accessCount, 0); + const cacheHits = totalAccess - entries.length; + + return { + size: entries.length, + hitRate: totalAccess > 0 ? cacheHits / totalAccess : 0, + entries + }; + } + + /** + * Get audit log entries + */ + getAuditLog(limit: number = 100): AuditLogEntry[] { + return this._auditLog.slice(-limit); + } + + /** + * Clear resource cache + */ + clearCache(): void { + this._resourceCache.clear(); + this.emit('cache-cleared'); + this.log('Resource cache cleared', 'info'); + } + + /** + * Refresh all resources + */ + async refreshResources(): Promise { + try { + const result = await this._resources.getResources(); + this.emit('resources-refreshed', result.resources.length); + this.log(`Resources refreshed: ${result.resources.length}`, 'info'); + } catch (error) { + this.log(`Failed to refresh resources: ${error.message}`, 'error'); + throw error; + } + } + + /** + * Set server state + */ + private setState(newState: ServerState): void { + const oldState = this._state; + this._state = newState; + this.emit('state-changed', newState, oldState); + } + + /** + * Setup event handlers + */ + private setupEventHandlers(): void { + // Tool registry events + this._tools.on?.('tool-registered', (toolName: string) => { + this.log(`Tool registered: ${toolName}`, 'info'); + }); + + this._tools.on?.('tool-unregistered', (toolName: string) => { + this.log(`Tool unregistered: ${toolName}`, 'info'); + }); + + // Resource registry events + this._resources.subscribe((event) => { + this.log(`Resource ${event.type}: ${event.resource.uri}`, 'info'); + }); + } + + /** + * Initialize default tools + */ + private async initializeDefaultTools(): Promise { + // Tools will be registered by their respective handlers + // This is a placeholder for tool initialization + this.log('Default tools initialized', 'info'); + } + + /** + * Initialize default resource providers + */ + private async initializeDefaultResourceProviders(): Promise { + // Resource providers will be registered by their respective classes + // This is a placeholder for resource provider initialization + this.log('Default resource providers initialized', 'info'); + } + + /** + * Setup cleanup intervals + */ + private setupCleanupIntervals(): void { + // Clean up old cache entries + setInterval(() => { + const now = Date.now(); + for (const [uri, entry] of this._resourceCache.entries()) { + if (now - entry.timestamp.getTime() > entry.ttl) { + this._resourceCache.delete(uri); + } + } + }, 60000); // Clean every minute + + // Truncate audit log + setInterval(() => { + if (this._auditLog.length > 1000) { + this._auditLog = this._auditLog.slice(-500); + } + }, 300000); // Clean every 5 minutes + } + + /** + * Get current workspace + */ + private async getCurrentWorkspace(): Promise { + try { + // TODO: Get actual current workspace from Remix API + return 'default'; + } catch (error) { + return 'default'; + } + } + + /** + * Get current file + */ + private async getCurrentFile(): Promise { + try { + // TODO: Get actual current file from Remix API + return ''; + } catch (error) { + return ''; + } + } + + /** + * Log message with audit trail + */ + private log(message: string, level: 'info' | 'warning' | 'error', details?: any): void { + if (this._config.debug || level !== 'info') { + console.log(`[RemixMCPServer] ${level.toUpperCase()}: ${message}`, details || ''); + } + + if (this._config.security?.enableAuditLog !== false) { + const entry: AuditLogEntry = { + id: `audit_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + timestamp: new Date(), + type: level === 'error' ? 'error' : 'info', + user: 'system', + details: { + message, + ...details + }, + severity: level + }; + + this._auditLog.push(entry); + this.emit('audit-log', entry); + } + } +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/servers/handlers/CompilationHandler.ts b/libs/remix-ai-core/src/servers/handlers/CompilationHandler.ts new file mode 100644 index 00000000000..a93bd596ca0 --- /dev/null +++ b/libs/remix-ai-core/src/servers/handlers/CompilationHandler.ts @@ -0,0 +1,520 @@ +/** + * Compilation Tool Handlers for Remix MCP Server + */ + +import { ICustomRemixApi } from '@remix-api'; +import { MCPToolResult } from '../../types/mcp'; +import { BaseToolHandler } from '../registry/RemixToolRegistry'; +import { + ToolCategory, + RemixToolDefinition, + SolidityCompileArgs, + CompilerConfigArgs, + CompilationResult +} from '../types/mcpTools'; + +/** + * Solidity Compile Tool Handler + */ +export class SolidityCompileHandler extends BaseToolHandler { + name = 'solidity_compile'; + description = 'Compile Solidity smart contracts'; + inputSchema = { + type: 'object', + properties: { + file: { + type: 'string', + description: 'Specific file to compile (optional, compiles all if not specified)' + }, + version: { + type: 'string', + description: 'Solidity compiler version (e.g., 0.8.19)', + default: 'latest' + }, + optimize: { + type: 'boolean', + description: 'Enable optimization', + default: true + }, + runs: { + type: 'number', + description: 'Number of optimization runs', + default: 200 + }, + evmVersion: { + type: 'string', + description: 'EVM version target', + enum: ['london', 'berlin', 'istanbul', 'petersburg', 'constantinople', 'byzantium'], + default: 'london' + } + } + }; + + getPermissions(): string[] { + return ['compile:solidity']; + } + + validate(args: SolidityCompileArgs): boolean | string { + const types = this.validateTypes(args, { + file: 'string', + version: 'string', + optimize: 'boolean', + runs: 'number', + evmVersion: 'string' + }); + if (types !== true) return types; + + if (args.runs !== undefined && (args.runs < 1 || args.runs > 10000)) { + return 'Optimization runs must be between 1 and 10000'; + } + + return true; + } + + async execute(args: SolidityCompileArgs, remixApi: ICustomRemixApi): Promise { + try { + // Get current compiler configuration or create new one + let compilerConfig: any = {}; + + try { + // Try to get existing compiler config + const currentConfig = await remixApi.config.methods.getAppParameter('solidity-compiler'); + if (currentConfig) { + compilerConfig = JSON.parse(currentConfig); + } + } catch (error) { + // Use default config if none exists + compilerConfig = { + version: args.version || 'latest', + optimize: args.optimize !== undefined ? args.optimize : true, + runs: args.runs || 200, + evmVersion: args.evmVersion || 'london', + language: 'Solidity' + }; + } + + // Update config with provided arguments + if (args.version) compilerConfig.version = args.version; + if (args.optimize !== undefined) compilerConfig.optimize = args.optimize; + if (args.runs) compilerConfig.runs = args.runs; + if (args.evmVersion) compilerConfig.evmVersion = args.evmVersion; + + // Set compiler configuration + await remixApi.config.methods.setAppParameter('solidity-compiler', JSON.stringify(compilerConfig)); + + // Trigger compilation + let compilationResult: any; + if (args.file) { + // Compile specific file - need to use plugin API or direct compilation + const content = await remixApi.fileManager.methods.readFile(args.file); + // TODO: Implement direct compilation with solc + compilationResult = { success: false, message: 'Direct file compilation not yet implemented' }; + } else { + // Compile current workspace - placeholder for actual compilation + compilationResult = { success: false, message: 'Workspace compilation not yet implemented' }; + } + + // Process compilation result + const result: CompilationResult = { + success: !compilationResult.errors || compilationResult.errors.length === 0, + contracts: {}, + errors: compilationResult.errors || [], + warnings: compilationResult.warnings || [], + sources: compilationResult.sources || {} + }; + + // Extract contract data + if (compilationResult.contracts) { + for (const [fileName, fileContracts] of Object.entries(compilationResult.contracts)) { + for (const [contractName, contractData] of Object.entries(fileContracts as any)) { + const contract = contractData as any; + result.contracts[`${fileName}:${contractName}`] = { + abi: contract.abi || [], + bytecode: contract.evm?.bytecode?.object || '', + deployedBytecode: contract.evm?.deployedBytecode?.object || '', + metadata: contract.metadata ? JSON.parse(contract.metadata) : {}, + gasEstimates: contract.evm?.gasEstimates || {} + }; + } + } + } + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Compilation failed: ${error.message}`); + } + } +} + +/** + * Get Compilation Result Tool Handler + */ +export class GetCompilationResultHandler extends BaseToolHandler { + name = 'get_compilation_result'; + description = 'Get the latest compilation result'; + inputSchema = { + type: 'object', + properties: {} + }; + + getPermissions(): string[] { + return ['compile:read']; + } + + async execute(args: any, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Implement getting compilation result from Remix API + const compilationResult: any = null; // await remixApi.solidity.getCompilationResult(); + + if (!compilationResult) { + return this.createErrorResult('No compilation result available'); + } + + const result: CompilationResult = { + success: !compilationResult.errors || compilationResult.errors.length === 0, + contracts: {}, + errors: compilationResult.errors || [], + warnings: compilationResult.warnings || [], + sources: compilationResult.sources || {} + }; + + // Process contracts + if (compilationResult.contracts) { + for (const [fileName, fileContracts] of Object.entries(compilationResult.contracts)) { + for (const [contractName, contractData] of Object.entries(fileContracts as any)) { + const contract = contractData as any; + result.contracts[`${fileName}:${contractName}`] = { + abi: contract.abi || [], + bytecode: contract.evm?.bytecode?.object || '', + deployedBytecode: contract.evm?.deployedBytecode?.object || '', + metadata: contract.metadata ? JSON.parse(contract.metadata) : {}, + gasEstimates: contract.evm?.gasEstimates || {} + }; + } + } + } + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to get compilation result: ${error.message}`); + } + } +} + +/** + * Set Compiler Config Tool Handler + */ +export class SetCompilerConfigHandler extends BaseToolHandler { + name = 'set_compiler_config'; + description = 'Set Solidity compiler configuration'; + inputSchema = { + type: 'object', + properties: { + version: { + type: 'string', + description: 'Compiler version' + }, + optimize: { + type: 'boolean', + description: 'Enable optimization' + }, + runs: { + type: 'number', + description: 'Number of optimization runs' + }, + evmVersion: { + type: 'string', + description: 'EVM version target' + }, + language: { + type: 'string', + description: 'Programming language', + default: 'Solidity' + } + }, + required: ['version'] + }; + + getPermissions(): string[] { + return ['compile:config']; + } + + validate(args: CompilerConfigArgs): boolean | string { + const required = this.validateRequired(args, ['version']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + version: 'string', + optimize: 'boolean', + runs: 'number', + evmVersion: 'string', + language: 'string' + }); + if (types !== true) return types; + + return true; + } + + async execute(args: CompilerConfigArgs, remixApi: ICustomRemixApi): Promise { + try { + const config = { + version: args.version, + optimize: args.optimize !== undefined ? args.optimize : true, + runs: args.runs || 200, + evmVersion: args.evmVersion || 'london', + language: args.language || 'Solidity' + }; + + await remixApi.config.methods.setAppParameter('solidity-compiler', JSON.stringify(config)); + + return this.createSuccessResult({ + success: true, + message: 'Compiler configuration updated', + config: config + }); + } catch (error) { + return this.createErrorResult(`Failed to set compiler config: ${error.message}`); + } + } +} + +/** + * Get Compiler Config Tool Handler + */ +export class GetCompilerConfigHandler extends BaseToolHandler { + name = 'get_compiler_config'; + description = 'Get current Solidity compiler configuration'; + inputSchema = { + type: 'object', + properties: {} + }; + + getPermissions(): string[] { + return ['compile:read']; + } + + async execute(args: any, remixApi: ICustomRemixApi): Promise { + try { + const configString = await remixApi.config.methods.getAppParameter('solidity-compiler'); + + let config: any; + if (configString) { + config = JSON.parse(configString); + } else { + config = { + version: 'latest', + optimize: true, + runs: 200, + evmVersion: 'london', + language: 'Solidity' + }; + } + + return this.createSuccessResult({ + success: true, + config: config + }); + } catch (error) { + return this.createErrorResult(`Failed to get compiler config: ${error.message}`); + } + } +} + +/** + * Compile with Hardhat Tool Handler + */ +export class CompileWithHardhatHandler extends BaseToolHandler { + name = 'compile_with_hardhat'; + description = 'Compile using Hardhat framework'; + inputSchema = { + type: 'object', + properties: { + configPath: { + type: 'string', + description: 'Path to hardhat.config.js file', + default: 'hardhat.config.js' + } + } + }; + + getPermissions(): string[] { + return ['compile:hardhat']; + } + + validate(args: { configPath?: string }): boolean | string { + const types = this.validateTypes(args, { configPath: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: { configPath?: string }, remixApi: ICustomRemixApi): Promise { + try { + const configPath = args.configPath || 'hardhat.config.js'; + + // Check if hardhat config exists + const exists = await remixApi.fileManager.methods.exists(configPath); + if (!exists) { + return this.createErrorResult(`Hardhat config file not found: ${configPath}`); + } + + // TODO: Compile with Hardhat - implement plugin integration + const result = { success: false, message: 'Hardhat compilation not yet implemented' }; + + return this.createSuccessResult({ + success: true, + message: 'Compiled with Hardhat successfully', + result: result + }); + } catch (error) { + return this.createErrorResult(`Hardhat compilation failed: ${error.message}`); + } + } +} + +/** + * Compile with Truffle Tool Handler + */ +export class CompileWithTruffleHandler extends BaseToolHandler { + name = 'compile_with_truffle'; + description = 'Compile using Truffle framework'; + inputSchema = { + type: 'object', + properties: { + configPath: { + type: 'string', + description: 'Path to truffle.config.js file', + default: 'truffle.config.js' + } + } + }; + + getPermissions(): string[] { + return ['compile:truffle']; + } + + validate(args: { configPath?: string }): boolean | string { + const types = this.validateTypes(args, { configPath: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: { configPath?: string }, remixApi: ICustomRemixApi): Promise { + try { + const configPath = args.configPath || 'truffle.config.js'; + + // Check if truffle config exists + const exists = await remixApi.fileManager.methods.exists(configPath); + if (!exists) { + return this.createErrorResult(`Truffle config file not found: ${configPath}`); + } + + // TODO: Compile with Truffle - implement plugin integration + const result = { success: false, message: 'Truffle compilation not yet implemented' }; + + return this.createSuccessResult({ + success: true, + message: 'Compiled with Truffle successfully', + result: result + }); + } catch (error) { + return this.createErrorResult(`Truffle compilation failed: ${error.message}`); + } + } +} + +/** + * Get Available Compiler Versions Tool Handler + */ +export class GetCompilerVersionsHandler extends BaseToolHandler { + name = 'get_compiler_versions'; + description = 'Get list of available Solidity compiler versions'; + inputSchema = { + type: 'object', + properties: {} + }; + + getPermissions(): string[] { + return ['compile:read']; + } + + async execute(_args: any, _remixApi: ICustomRemixApi): Promise { + try { + // TODO: Get available compiler versions from Remix API + const versions = ['0.8.19', '0.8.18', '0.8.17', '0.8.16', '0.8.15']; // Mock data + + return this.createSuccessResult({ + success: true, + versions: versions || [], + count: versions?.length || 0 + }); + } catch (error) { + return this.createErrorResult(`Failed to get compiler versions: ${error.message}`); + } + } +} + +/** + * Create compilation tool definitions + */ +export function createCompilationTools(): RemixToolDefinition[] { + return [ + { + name: 'solidity_compile', + description: 'Compile Solidity smart contracts', + inputSchema: new SolidityCompileHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['compile:solidity'], + handler: new SolidityCompileHandler() + }, + { + name: 'get_compilation_result', + description: 'Get the latest compilation result', + inputSchema: new GetCompilationResultHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['compile:read'], + handler: new GetCompilationResultHandler() + }, + { + name: 'set_compiler_config', + description: 'Set Solidity compiler configuration', + inputSchema: new SetCompilerConfigHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['compile:config'], + handler: new SetCompilerConfigHandler() + }, + { + name: 'get_compiler_config', + description: 'Get current Solidity compiler configuration', + inputSchema: new GetCompilerConfigHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['compile:read'], + handler: new GetCompilerConfigHandler() + }, + { + name: 'compile_with_hardhat', + description: 'Compile using Hardhat framework', + inputSchema: new CompileWithHardhatHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['compile:hardhat'], + handler: new CompileWithHardhatHandler() + }, + { + name: 'compile_with_truffle', + description: 'Compile using Truffle framework', + inputSchema: new CompileWithTruffleHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['compile:truffle'], + handler: new CompileWithTruffleHandler() + }, + { + name: 'get_compiler_versions', + description: 'Get list of available Solidity compiler versions', + inputSchema: new GetCompilerVersionsHandler().inputSchema, + category: ToolCategory.COMPILATION, + permissions: ['compile:read'], + handler: new GetCompilerVersionsHandler() + } + ]; +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/servers/handlers/DebuggingHandler.ts b/libs/remix-ai-core/src/servers/handlers/DebuggingHandler.ts new file mode 100644 index 00000000000..e54b9dae67c --- /dev/null +++ b/libs/remix-ai-core/src/servers/handlers/DebuggingHandler.ts @@ -0,0 +1,692 @@ +/** + * Debugging Tool Handlers for Remix MCP Server + */ + +import { ICustomRemixApi } from '@remix-api'; +import { MCPToolResult } from '../../types/mcp'; +import { BaseToolHandler } from '../registry/RemixToolRegistry'; +import { + ToolCategory, + RemixToolDefinition, + DebugSessionArgs, + BreakpointArgs, + DebugStepArgs, + DebugWatchArgs, + DebugEvaluateArgs, + DebugCallStackArgs, + DebugVariablesArgs, + DebugSessionResult, + BreakpointResult, + DebugStepResult +} from '../types/mcpTools'; + +/** + * Start Debug Session Tool Handler + */ +export class StartDebugSessionHandler extends BaseToolHandler { + name = 'start_debug_session'; + description = 'Start a debugging session for a smart contract'; + inputSchema = { + type: 'object', + properties: { + contractAddress: { + type: 'string', + description: 'Contract address to debug', + pattern: '^0x[a-fA-F0-9]{40}$' + }, + transactionHash: { + type: 'string', + description: 'Transaction hash to debug (optional)', + pattern: '^0x[a-fA-F0-9]{64}$' + }, + sourceFile: { + type: 'string', + description: 'Source file path to debug' + }, + network: { + type: 'string', + description: 'Network to debug on', + default: 'local' + } + }, + required: ['contractAddress'] + }; + + getPermissions(): string[] { + return ['debug:start']; + } + + validate(args: DebugSessionArgs): boolean | string { + const required = this.validateRequired(args, ['contractAddress']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + contractAddress: 'string', + transactionHash: 'string', + sourceFile: 'string', + network: 'string' + }); + if (types !== true) return types; + + if (!args.contractAddress.match(/^0x[a-fA-F0-9]{40}$/)) { + return 'Invalid contract address format'; + } + + if (args.transactionHash && !args.transactionHash.match(/^0x[a-fA-F0-9]{64}$/)) { + return 'Invalid transaction hash format'; + } + + return true; + } + + async execute(args: DebugSessionArgs, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Integrate with Remix debugger plugin + const sessionId = 'debug_' + Date.now(); + + // Mock debug session creation + const result: DebugSessionResult = { + success: true, + sessionId, + contractAddress: args.contractAddress, + network: args.network || 'local', + transactionHash: args.transactionHash, + sourceFile: args.sourceFile, + status: 'started', + createdAt: new Date().toISOString() + }; + + return this.createSuccessResult(result); + + } catch (error) { + return this.createErrorResult(`Failed to start debug session: ${error.message}`); + } + } +} + +/** + * Set Breakpoint Tool Handler + */ +export class SetBreakpointHandler extends BaseToolHandler { + name = 'set_breakpoint'; + description = 'Set a breakpoint in smart contract code'; + inputSchema = { + type: 'object', + properties: { + sourceFile: { + type: 'string', + description: 'Source file path' + }, + lineNumber: { + type: 'number', + description: 'Line number to set breakpoint', + minimum: 1 + }, + condition: { + type: 'string', + description: 'Conditional breakpoint expression (optional)' + }, + hitCount: { + type: 'number', + description: 'Hit count condition (optional)', + minimum: 1 + } + }, + required: ['sourceFile', 'lineNumber'] + }; + + getPermissions(): string[] { + return ['debug:breakpoint']; + } + + validate(args: BreakpointArgs): boolean | string { + const required = this.validateRequired(args, ['sourceFile', 'lineNumber']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + sourceFile: 'string', + lineNumber: 'number', + condition: 'string', + hitCount: 'number' + }); + if (types !== true) return types; + + if (args.lineNumber < 1) { + return 'Line number must be at least 1'; + } + + if (args.hitCount !== undefined && args.hitCount < 1) { + return 'Hit count must be at least 1'; + } + + return true; + } + + async execute(args: BreakpointArgs, remixApi: ICustomRemixApi): Promise { + try { + // Check if source file exists + const exists = await remixApi.fileManager.methods.exists(args.sourceFile); + if (!exists) { + return this.createErrorResult(`Source file not found: ${args.sourceFile}`); + } + + // TODO: Set breakpoint via Remix debugger API + const breakpointId = `bp_${Date.now()}`; + + const result: BreakpointResult = { + success: true, + breakpointId, + sourceFile: args.sourceFile, + lineNumber: args.lineNumber, + condition: args.condition, + hitCount: args.hitCount, + enabled: true, + setAt: new Date().toISOString() + }; + + return this.createSuccessResult(result); + + } catch (error) { + return this.createErrorResult(`Failed to set breakpoint: ${error.message}`); + } + } +} + +/** + * Debug Step Tool Handler + */ +export class DebugStepHandler extends BaseToolHandler { + name = 'debug_step'; + description = 'Step through code during debugging'; + inputSchema = { + type: 'object', + properties: { + sessionId: { + type: 'string', + description: 'Debug session ID' + }, + stepType: { + type: 'string', + enum: ['into', 'over', 'out', 'continue'], + description: 'Type of step to perform' + } + }, + required: ['sessionId', 'stepType'] + }; + + getPermissions(): string[] { + return ['debug:step']; + } + + validate(args: DebugStepArgs): boolean | string { + const required = this.validateRequired(args, ['sessionId', 'stepType']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + sessionId: 'string', + stepType: 'string' + }); + if (types !== true) return types; + + const validStepTypes = ['into', 'over', 'out', 'continue']; + if (!validStepTypes.includes(args.stepType)) { + return `Invalid step type. Must be one of: ${validStepTypes.join(', ')}`; + } + + return true; + } + + async execute(args: DebugStepArgs, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Execute step via Remix debugger API + + const result: DebugStepResult = { + success: true, + sessionId: args.sessionId, + stepType: args.stepType, + currentLocation: { + sourceFile: 'contracts/example.sol', + lineNumber: Math.floor(Math.random() * 100) + 1, + columnNumber: 1 + }, + stackTrace: [ + { + function: 'main', + sourceFile: 'contracts/example.sol', + lineNumber: 25 + } + ], + steppedAt: new Date().toISOString() + }; + + return this.createSuccessResult(result); + + } catch (error) { + return this.createErrorResult(`Debug step failed: ${error.message}`); + } + } +} + +/** + * Debug Watch Variable Tool Handler + */ +export class DebugWatchHandler extends BaseToolHandler { + name = 'debug_watch'; + description = 'Watch a variable or expression during debugging'; + inputSchema = { + type: 'object', + properties: { + sessionId: { + type: 'string', + description: 'Debug session ID' + }, + expression: { + type: 'string', + description: 'Variable name or expression to watch' + }, + watchType: { + type: 'string', + enum: ['variable', 'expression', 'memory'], + description: 'Type of watch to add', + default: 'variable' + } + }, + required: ['sessionId', 'expression'] + }; + + getPermissions(): string[] { + return ['debug:watch']; + } + + validate(args: DebugWatchArgs): boolean | string { + const required = this.validateRequired(args, ['sessionId', 'expression']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + sessionId: 'string', + expression: 'string', + watchType: 'string' + }); + if (types !== true) return types; + + if (args.watchType) { + const validTypes = ['variable', 'expression', 'memory']; + if (!validTypes.includes(args.watchType)) { + return `Invalid watch type. Must be one of: ${validTypes.join(', ')}`; + } + } + + return true; + } + + async execute(args: DebugWatchArgs, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Add watch via Remix debugger API + const watchId = `watch_${Date.now()}`; + + const result = { + success: true, + watchId, + sessionId: args.sessionId, + expression: args.expression, + watchType: args.watchType || 'variable', + currentValue: 'undefined', // Mock value + addedAt: new Date().toISOString() + }; + + return this.createSuccessResult(result); + + } catch (error) { + return this.createErrorResult(`Failed to add watch: ${error.message}`); + } + } +} + +/** + * Debug Evaluate Expression Tool Handler + */ +export class DebugEvaluateHandler extends BaseToolHandler { + name = 'debug_evaluate'; + description = 'Evaluate an expression in the current debug context'; + inputSchema = { + type: 'object', + properties: { + sessionId: { + type: 'string', + description: 'Debug session ID' + }, + expression: { + type: 'string', + description: 'Expression to evaluate' + }, + context: { + type: 'string', + enum: ['current', 'global', 'local'], + description: 'Evaluation context', + default: 'current' + } + }, + required: ['sessionId', 'expression'] + }; + + getPermissions(): string[] { + return ['debug:evaluate']; + } + + validate(args: DebugEvaluateArgs): boolean | string { + const required = this.validateRequired(args, ['sessionId', 'expression']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + sessionId: 'string', + expression: 'string', + context: 'string' + }); + if (types !== true) return types; + + if (args.context) { + const validContexts = ['current', 'global', 'local']; + if (!validContexts.includes(args.context)) { + return `Invalid context. Must be one of: ${validContexts.join(', ')}`; + } + } + + return true; + } + + async execute(args: DebugEvaluateArgs, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Evaluate expression via Remix debugger API + + const result = { + success: true, + sessionId: args.sessionId, + expression: args.expression, + result: '42', // Mock evaluation result + type: 'uint256', + context: args.context || 'current', + evaluatedAt: new Date().toISOString() + }; + + return this.createSuccessResult(result); + + } catch (error) { + return this.createErrorResult(`Expression evaluation failed: ${error.message}`); + } + } +} + +/** + * Get Debug Call Stack Tool Handler + */ +export class GetDebugCallStackHandler extends BaseToolHandler { + name = 'get_debug_call_stack'; + description = 'Get the current call stack during debugging'; + inputSchema = { + type: 'object', + properties: { + sessionId: { + type: 'string', + description: 'Debug session ID' + } + }, + required: ['sessionId'] + }; + + getPermissions(): string[] { + return ['debug:read']; + } + + validate(args: DebugCallStackArgs): boolean | string { + const required = this.validateRequired(args, ['sessionId']); + if (required !== true) return required; + + const types = this.validateTypes(args, { sessionId: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: DebugCallStackArgs, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Get call stack via Remix debugger API + + const result = { + success: true, + sessionId: args.sessionId, + callStack: [ + { + function: 'transfer', + contract: 'ERC20Token', + sourceFile: 'contracts/ERC20Token.sol', + lineNumber: 45, + address: '0x' + Math.random().toString(16).substr(2, 40) + }, + { + function: 'main', + contract: 'Main', + sourceFile: 'contracts/Main.sol', + lineNumber: 12, + address: '0x' + Math.random().toString(16).substr(2, 40) + } + ], + depth: 2, + retrievedAt: new Date().toISOString() + }; + + return this.createSuccessResult(result); + + } catch (error) { + return this.createErrorResult(`Failed to get call stack: ${error.message}`); + } + } +} + +/** + * Get Debug Variables Tool Handler + */ +export class GetDebugVariablesHandler extends BaseToolHandler { + name = 'get_debug_variables'; + description = 'Get current variable values during debugging'; + inputSchema = { + type: 'object', + properties: { + sessionId: { + type: 'string', + description: 'Debug session ID' + }, + scope: { + type: 'string', + enum: ['local', 'global', 'storage', 'memory'], + description: 'Variable scope to retrieve', + default: 'local' + } + }, + required: ['sessionId'] + }; + + getPermissions(): string[] { + return ['debug:read']; + } + + validate(args: DebugVariablesArgs): boolean | string { + const required = this.validateRequired(args, ['sessionId']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + sessionId: 'string', + scope: 'string' + }); + if (types !== true) return types; + + if (args.scope) { + const validScopes = ['local', 'global', 'storage', 'memory']; + if (!validScopes.includes(args.scope)) { + return `Invalid scope. Must be one of: ${validScopes.join(', ')}`; + } + } + + return true; + } + + async execute(args: DebugVariablesArgs, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Get variables via Remix debugger API + + const result = { + success: true, + sessionId: args.sessionId, + scope: args.scope || 'local', + variables: [ + { + name: 'balance', + value: '1000000000000000000', + type: 'uint256', + location: 'storage' + }, + { + name: 'owner', + value: '0x' + Math.random().toString(16).substr(2, 40), + type: 'address', + location: 'storage' + }, + { + name: 'amount', + value: '500', + type: 'uint256', + location: 'local' + } + ], + retrievedAt: new Date().toISOString() + }; + + return this.createSuccessResult(result); + + } catch (error) { + return this.createErrorResult(`Failed to get variables: ${error.message}`); + } + } +} + +/** + * Stop Debug Session Tool Handler + */ +export class StopDebugSessionHandler extends BaseToolHandler { + name = 'stop_debug_session'; + description = 'Stop an active debugging session'; + inputSchema = { + type: 'object', + properties: { + sessionId: { + type: 'string', + description: 'Debug session ID to stop' + } + }, + required: ['sessionId'] + }; + + getPermissions(): string[] { + return ['debug:stop']; + } + + validate(args: { sessionId: string }): boolean | string { + const required = this.validateRequired(args, ['sessionId']); + if (required !== true) return required; + + const types = this.validateTypes(args, { sessionId: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: { sessionId: string }, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Stop debug session via Remix debugger API + + const result = { + success: true, + sessionId: args.sessionId, + status: 'stopped', + stoppedAt: new Date().toISOString(), + message: 'Debug session stopped successfully' + }; + + return this.createSuccessResult(result); + + } catch (error) { + return this.createErrorResult(`Failed to stop debug session: ${error.message}`); + } + } +} + +/** + * Create debugging tool definitions + */ +export function createDebuggingTools(): RemixToolDefinition[] { + return [ + { + name: 'start_debug_session', + description: 'Start a debugging session for a smart contract', + inputSchema: new StartDebugSessionHandler().inputSchema, + category: ToolCategory.DEBUGGING, + permissions: ['debug:start'], + handler: new StartDebugSessionHandler() + }, + { + name: 'set_breakpoint', + description: 'Set a breakpoint in smart contract code', + inputSchema: new SetBreakpointHandler().inputSchema, + category: ToolCategory.DEBUGGING, + permissions: ['debug:breakpoint'], + handler: new SetBreakpointHandler() + }, + { + name: 'debug_step', + description: 'Step through code during debugging', + inputSchema: new DebugStepHandler().inputSchema, + category: ToolCategory.DEBUGGING, + permissions: ['debug:step'], + handler: new DebugStepHandler() + }, + { + name: 'debug_watch', + description: 'Watch a variable or expression during debugging', + inputSchema: new DebugWatchHandler().inputSchema, + category: ToolCategory.DEBUGGING, + permissions: ['debug:watch'], + handler: new DebugWatchHandler() + }, + { + name: 'debug_evaluate', + description: 'Evaluate an expression in the current debug context', + inputSchema: new DebugEvaluateHandler().inputSchema, + category: ToolCategory.DEBUGGING, + permissions: ['debug:evaluate'], + handler: new DebugEvaluateHandler() + }, + { + name: 'get_debug_call_stack', + description: 'Get the current call stack during debugging', + inputSchema: new GetDebugCallStackHandler().inputSchema, + category: ToolCategory.DEBUGGING, + permissions: ['debug:read'], + handler: new GetDebugCallStackHandler() + }, + { + name: 'get_debug_variables', + description: 'Get current variable values during debugging', + inputSchema: new GetDebugVariablesHandler().inputSchema, + category: ToolCategory.DEBUGGING, + permissions: ['debug:read'], + handler: new GetDebugVariablesHandler() + }, + { + name: 'stop_debug_session', + description: 'Stop an active debugging session', + inputSchema: new StopDebugSessionHandler().inputSchema, + category: ToolCategory.DEBUGGING, + permissions: ['debug:stop'], + handler: new StopDebugSessionHandler() + } + ]; +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/servers/handlers/DeploymentHandler.ts b/libs/remix-ai-core/src/servers/handlers/DeploymentHandler.ts new file mode 100644 index 00000000000..09c6f39d81c --- /dev/null +++ b/libs/remix-ai-core/src/servers/handlers/DeploymentHandler.ts @@ -0,0 +1,606 @@ +/** + * Deployment and Contract Interaction Tool Handlers for Remix MCP Server + */ + +import { ICustomRemixApi } from '@remix-api'; +import { MCPToolResult } from '../../types/mcp'; +import { BaseToolHandler } from '../registry/RemixToolRegistry'; +import { + ToolCategory, + RemixToolDefinition, + DeployContractArgs, + CallContractArgs, + SendTransactionArgs, + DeploymentResult, + ContractInteractionResult +} from '../types/mcpTools'; + +/** + * Deploy Contract Tool Handler + */ +export class DeployContractHandler extends BaseToolHandler { + name = 'deploy_contract'; + description = 'Deploy a smart contract'; + inputSchema = { + type: 'object', + properties: { + contractName: { + type: 'string', + description: 'Name of the contract to deploy' + }, + constructorArgs: { + type: 'array', + description: 'Constructor arguments', + items: { + type: 'string' + }, + default: [] + }, + gasLimit: { + type: 'number', + description: 'Gas limit for deployment', + minimum: 21000 + }, + gasPrice: { + type: 'string', + description: 'Gas price in wei' + }, + value: { + type: 'string', + description: 'ETH value to send with deployment', + default: '0' + }, + account: { + type: 'string', + description: 'Account to deploy from (address or index)' + } + }, + required: ['contractName'] + }; + + getPermissions(): string[] { + return ['deploy:contract']; + } + + validate(args: DeployContractArgs): boolean | string { + const required = this.validateRequired(args, ['contractName']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + contractName: 'string', + gasLimit: 'number', + gasPrice: 'string', + value: 'string', + account: 'string' + }); + if (types !== true) return types; + + if (args.gasLimit && args.gasLimit < 21000) { + return 'Gas limit must be at least 21000'; + } + + return true; + } + + async execute(args: DeployContractArgs, remixApi: ICustomRemixApi): Promise { + try { + // Get compilation result to find contract + // TODO: Get actual compilation result + const contracts = {}; // await remixApi.solidity.getCompilationResult(); + + if (!contracts || Object.keys(contracts).length === 0) { + return this.createErrorResult('No compiled contracts found. Please compile first.'); + } + + // Find the contract to deploy + const contractKey = Object.keys(contracts).find(key => + key.includes(args.contractName) + ); + + if (!contractKey) { + return this.createErrorResult(`Contract '${args.contractName}' not found in compilation result`); + } + + // Get current account + const accounts = await this.getAccounts(remixApi); + const deployAccount = args.account || accounts[0]; + + if (!deployAccount) { + return this.createErrorResult('No account available for deployment'); + } + + // Prepare deployment transaction + const deploymentData = { + contractName: args.contractName, + account: deployAccount, + constructorArgs: args.constructorArgs || [], + gasLimit: args.gasLimit, + gasPrice: args.gasPrice, + value: args.value || '0' + }; + + // TODO: Execute actual deployment via Remix Run Tab API + const mockResult: DeploymentResult = { + success: false, + contractAddress: undefined, + transactionHash: '0x' + Math.random().toString(16).substr(2, 64), + gasUsed: args.gasLimit || 1000000, + effectiveGasPrice: args.gasPrice || '20000000000', + blockNumber: Math.floor(Math.random() * 1000000), + logs: [] + }; + + // Mock implementation - in real implementation, use Remix deployment API + mockResult.success = true; + mockResult.contractAddress = '0x' + Math.random().toString(16).substr(2, 40); + + return this.createSuccessResult(mockResult); + + } catch (error) { + return this.createErrorResult(`Deployment failed: ${error.message}`); + } + } + + private async getAccounts(remixApi: ICustomRemixApi): Promise { + try { + // TODO: Get accounts from Remix API + return ['0x' + Math.random().toString(16).substr(2, 40)]; // Mock account + } catch (error) { + return []; + } + } +} + +/** + * Call Contract Method Tool Handler + */ +export class CallContractHandler extends BaseToolHandler { + name = 'call_contract'; + description = 'Call a smart contract method'; + inputSchema = { + type: 'object', + properties: { + address: { + type: 'string', + description: 'Contract address', + pattern: '^0x[a-fA-F0-9]{40}$' + }, + abi: { + type: 'array', + description: 'Contract ABI', + items: { + type: 'object' + } + }, + methodName: { + type: 'string', + description: 'Method name to call' + }, + args: { + type: 'array', + description: 'Method arguments', + items: { + type: 'string' + }, + default: [] + }, + gasLimit: { + type: 'number', + description: 'Gas limit for transaction', + minimum: 21000 + }, + gasPrice: { + type: 'string', + description: 'Gas price in wei' + }, + value: { + type: 'string', + description: 'ETH value to send', + default: '0' + }, + account: { + type: 'string', + description: 'Account to call from' + } + }, + required: ['address', 'abi', 'methodName'] + }; + + getPermissions(): string[] { + return ['contract:interact']; + } + + validate(args: CallContractArgs): boolean | string { + const required = this.validateRequired(args, ['address', 'abi', 'methodName']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + address: 'string', + methodName: 'string', + gasLimit: 'number', + gasPrice: 'string', + value: 'string', + account: 'string' + }); + if (types !== true) return types; + + if (!args.address.match(/^0x[a-fA-F0-9]{40}$/)) { + return 'Invalid contract address format'; + } + + if (!Array.isArray(args.abi)) { + return 'ABI must be an array'; + } + + return true; + } + + async execute(args: CallContractArgs, remixApi: ICustomRemixApi): Promise { + try { + // Find the method in ABI + const method = args.abi.find((item: any) => + item.name === args.methodName && item.type === 'function' + ); + + if (!method) { + return this.createErrorResult(`Method '${args.methodName}' not found in ABI`); + } + + // Get accounts + const accounts = await this.getAccounts(remixApi); + const callAccount = args.account || accounts[0]; + + if (!callAccount) { + return this.createErrorResult('No account available for contract call'); + } + + // Determine if this is a view function or transaction + const isView = method.stateMutability === 'view' || method.stateMutability === 'pure'; + + // TODO: Execute contract call via Remix Run Tab API + const mockResult: ContractInteractionResult = { + success: true, + result: isView ? 'mock_view_result' : undefined, + transactionHash: isView ? undefined : '0x' + Math.random().toString(16).substr(2, 64), + gasUsed: isView ? 0 : (args.gasLimit || 100000), + logs: [] + }; + + if (isView) { + mockResult.result = `View function result for ${args.methodName}`; + } else { + mockResult.transactionHash = '0x' + Math.random().toString(16).substr(2, 64); + mockResult.gasUsed = args.gasLimit || 100000; + } + + return this.createSuccessResult(mockResult); + + } catch (error) { + return this.createErrorResult(`Contract call failed: ${error.message}`); + } + } + + private async getAccounts(remixApi: ICustomRemixApi): Promise { + try { + // TODO: Get accounts from Remix API + return ['0x' + Math.random().toString(16).substr(2, 40)]; // Mock account + } catch (error) { + return []; + } + } +} + +/** + * Send Transaction Tool Handler + */ +export class SendTransactionHandler extends BaseToolHandler { + name = 'send_transaction'; + description = 'Send a raw transaction'; + inputSchema = { + type: 'object', + properties: { + to: { + type: 'string', + description: 'Recipient address', + pattern: '^0x[a-fA-F0-9]{40}$' + }, + value: { + type: 'string', + description: 'ETH value to send in wei', + default: '0' + }, + data: { + type: 'string', + description: 'Transaction data (hex)', + pattern: '^0x[a-fA-F0-9]*$' + }, + gasLimit: { + type: 'number', + description: 'Gas limit', + minimum: 21000 + }, + gasPrice: { + type: 'string', + description: 'Gas price in wei' + }, + account: { + type: 'string', + description: 'Account to send from' + } + }, + required: ['to'] + }; + + getPermissions(): string[] { + return ['transaction:send']; + } + + validate(args: SendTransactionArgs): boolean | string { + const required = this.validateRequired(args, ['to']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + to: 'string', + value: 'string', + data: 'string', + gasLimit: 'number', + gasPrice: 'string', + account: 'string' + }); + if (types !== true) return types; + + if (!args.to.match(/^0x[a-fA-F0-9]{40}$/)) { + return 'Invalid recipient address format'; + } + + if (args.data && !args.data.match(/^0x[a-fA-F0-9]*$/)) { + return 'Invalid data format (must be hex)'; + } + + return true; + } + + async execute(args: SendTransactionArgs, remixApi: ICustomRemixApi): Promise { + try { + // Get accounts + const accounts = await this.getAccounts(remixApi); + const sendAccount = args.account || accounts[0]; + + if (!sendAccount) { + return this.createErrorResult('No account available for sending transaction'); + } + + // TODO: Send transaction via Remix Run Tab API + const mockResult = { + success: true, + transactionHash: '0x' + Math.random().toString(16).substr(2, 64), + from: sendAccount, + to: args.to, + value: args.value || '0', + gasUsed: args.gasLimit || 21000, + blockNumber: Math.floor(Math.random() * 1000000) + }; + + return this.createSuccessResult(mockResult); + + } catch (error) { + return this.createErrorResult(`Transaction failed: ${error.message}`); + } + } + + private async getAccounts(remixApi: ICustomRemixApi): Promise { + try { + // TODO: Get accounts from Remix API + return ['0x' + Math.random().toString(16).substr(2, 40)]; // Mock account + } catch (error) { + return []; + } + } +} + +/** + * Get Deployed Contracts Tool Handler + */ +export class GetDeployedContractsHandler extends BaseToolHandler { + name = 'get_deployed_contracts'; + description = 'Get list of deployed contracts'; + inputSchema = { + type: 'object', + properties: { + network: { + type: 'string', + description: 'Network name (optional)' + } + } + }; + + getPermissions(): string[] { + return ['deploy:read']; + } + + async execute(args: { network?: string }, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Get deployed contracts from Remix storage/state + const mockDeployedContracts = [ + { + name: 'MyToken', + address: '0x' + Math.random().toString(16).substr(2, 40), + network: args.network || 'local', + deployedAt: new Date().toISOString(), + transactionHash: '0x' + Math.random().toString(16).substr(2, 64) + } + ]; + + return this.createSuccessResult({ + success: true, + contracts: mockDeployedContracts, + count: mockDeployedContracts.length + }); + + } catch (error) { + return this.createErrorResult(`Failed to get deployed contracts: ${error.message}`); + } + } +} + +/** + * Set Execution Environment Tool Handler + */ +export class SetExecutionEnvironmentHandler extends BaseToolHandler { + name = 'set_execution_environment'; + description = 'Set the execution environment for deployments'; + inputSchema = { + type: 'object', + properties: { + environment: { + type: 'string', + enum: ['vm-london', 'vm-berlin', 'injected', 'web3'], + description: 'Execution environment' + }, + networkUrl: { + type: 'string', + description: 'Network URL (for web3 environment)' + } + }, + required: ['environment'] + }; + + getPermissions(): string[] { + return ['environment:config']; + } + + validate(args: { environment: string; networkUrl?: string }): boolean | string { + const required = this.validateRequired(args, ['environment']); + if (required !== true) return required; + + const validEnvironments = ['vm-london', 'vm-berlin', 'injected', 'web3']; + if (!validEnvironments.includes(args.environment)) { + return `Invalid environment. Must be one of: ${validEnvironments.join(', ')}`; + } + + return true; + } + + async execute(args: { environment: string; networkUrl?: string }, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Set execution environment via Remix Run Tab API + + return this.createSuccessResult({ + success: true, + message: `Execution environment set to: ${args.environment}`, + environment: args.environment, + networkUrl: args.networkUrl + }); + + } catch (error) { + return this.createErrorResult(`Failed to set execution environment: ${error.message}`); + } + } +} + +/** + * Get Account Balance Tool Handler + */ +export class GetAccountBalanceHandler extends BaseToolHandler { + name = 'get_account_balance'; + description = 'Get account balance'; + inputSchema = { + type: 'object', + properties: { + account: { + type: 'string', + description: 'Account address', + pattern: '^0x[a-fA-F0-9]{40}$' + } + }, + required: ['account'] + }; + + getPermissions(): string[] { + return ['account:read']; + } + + validate(args: { account: string }): boolean | string { + const required = this.validateRequired(args, ['account']); + if (required !== true) return required; + + if (!args.account.match(/^0x[a-fA-F0-9]{40}$/)) { + return 'Invalid account address format'; + } + + return true; + } + + async execute(args: { account: string }, remixApi: ICustomRemixApi): Promise { + try { + // TODO: Get account balance from current provider + const mockBalance = (Math.random() * 10).toFixed(4); + + return this.createSuccessResult({ + success: true, + account: args.account, + balance: mockBalance, + unit: 'ETH' + }); + + } catch (error) { + return this.createErrorResult(`Failed to get account balance: ${error.message}`); + } + } +} + +/** + * Create deployment and interaction tool definitions + */ +export function createDeploymentTools(): RemixToolDefinition[] { + return [ + { + name: 'deploy_contract', + description: 'Deploy a smart contract', + inputSchema: new DeployContractHandler().inputSchema, + category: ToolCategory.DEPLOYMENT, + permissions: ['deploy:contract'], + handler: new DeployContractHandler() + }, + { + name: 'call_contract', + description: 'Call a smart contract method', + inputSchema: new CallContractHandler().inputSchema, + category: ToolCategory.DEPLOYMENT, + permissions: ['contract:interact'], + handler: new CallContractHandler() + }, + { + name: 'send_transaction', + description: 'Send a raw transaction', + inputSchema: new SendTransactionHandler().inputSchema, + category: ToolCategory.DEPLOYMENT, + permissions: ['transaction:send'], + handler: new SendTransactionHandler() + }, + { + name: 'get_deployed_contracts', + description: 'Get list of deployed contracts', + inputSchema: new GetDeployedContractsHandler().inputSchema, + category: ToolCategory.DEPLOYMENT, + permissions: ['deploy:read'], + handler: new GetDeployedContractsHandler() + }, + { + name: 'set_execution_environment', + description: 'Set the execution environment for deployments', + inputSchema: new SetExecutionEnvironmentHandler().inputSchema, + category: ToolCategory.DEPLOYMENT, + permissions: ['environment:config'], + handler: new SetExecutionEnvironmentHandler() + }, + { + name: 'get_account_balance', + description: 'Get account balance', + inputSchema: new GetAccountBalanceHandler().inputSchema, + category: ToolCategory.DEPLOYMENT, + permissions: ['account:read'], + handler: new GetAccountBalanceHandler() + } + ]; +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/servers/handlers/FileManagementHandler.ts b/libs/remix-ai-core/src/servers/handlers/FileManagementHandler.ts new file mode 100644 index 00000000000..abd3d073e43 --- /dev/null +++ b/libs/remix-ai-core/src/servers/handlers/FileManagementHandler.ts @@ -0,0 +1,603 @@ +/** + * File Management Tool Handlers for Remix MCP Server + */ + +import { ICustomRemixApi } from '@remix-api'; +import { MCPToolResult } from '../../types/mcp'; +import { BaseToolHandler } from '../registry/RemixToolRegistry'; +import { + ToolCategory, + RemixToolDefinition, + FileReadArgs, + FileWriteArgs, + FileCreateArgs, + FileDeleteArgs, + FileMoveArgs, + FileCopyArgs, + DirectoryListArgs, + FileOperationResult +} from '../types/mcpTools'; + +/** + * File Read Tool Handler + */ +export class FileReadHandler extends BaseToolHandler { + name = 'file_read'; + description = 'Read contents of a file'; + inputSchema = { + type: 'object', + properties: { + path: { + type: 'string', + description: 'File path to read' + } + }, + required: ['path'] + }; + + getPermissions(): string[] { + return ['file:read']; + } + + validate(args: FileReadArgs): boolean | string { + const required = this.validateRequired(args, ['path']); + if (required !== true) return required; + + const types = this.validateTypes(args, { path: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: FileReadArgs, remixApi: ICustomRemixApi): Promise { + try { + const exists = await remixApi.fileManager.methods.exists(args.path); + if (!exists) { + return this.createErrorResult(`File not found: ${args.path}`); + } + + const content = await remixApi.fileManager.methods.readFile(args.path); + + const result: FileOperationResult = { + success: true, + path: args.path, + content: content, + size: content.length + }; + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to read file: ${error.message}`); + } + } +} + +/** + * File Write Tool Handler + */ +export class FileWriteHandler extends BaseToolHandler { + name = 'file_write'; + description = 'Write content to a file'; + inputSchema = { + type: 'object', + properties: { + path: { + type: 'string', + description: 'File path to write' + }, + content: { + type: 'string', + description: 'Content to write to the file' + }, + encoding: { + type: 'string', + description: 'File encoding (default: utf8)', + default: 'utf8' + } + }, + required: ['path', 'content'] + }; + + getPermissions(): string[] { + return ['file:write']; + } + + validate(args: FileWriteArgs): boolean | string { + const required = this.validateRequired(args, ['path', 'content']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + path: 'string', + content: 'string', + encoding: 'string' + }); + if (types !== true) return types; + + return true; + } + + async execute(args: FileWriteArgs, remixApi: ICustomRemixApi): Promise { + try { + await remixApi.fileManager.methods.writeFile(args.path, args.content); + + const result: FileOperationResult = { + success: true, + path: args.path, + message: 'File written successfully', + size: args.content.length, + lastModified: new Date().toISOString() + }; + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to write file: ${error.message}`); + } + } +} + +/** + * File Create Tool Handler + */ +export class FileCreateHandler extends BaseToolHandler { + name = 'file_create'; + description = 'Create a new file or directory'; + inputSchema = { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path for the new file or directory' + }, + content: { + type: 'string', + description: 'Initial content for the file (optional)', + default: '' + }, + type: { + type: 'string', + enum: ['file', 'directory'], + description: 'Type of item to create', + default: 'file' + } + }, + required: ['path'] + }; + + getPermissions(): string[] { + return ['file:create']; + } + + validate(args: FileCreateArgs): boolean | string { + const required = this.validateRequired(args, ['path']); + if (required !== true) return required; + + const types = this.validateTypes(args, { + path: 'string', + content: 'string', + type: 'string' + }); + if (types !== true) return types; + + if (args.type && !['file', 'directory'].includes(args.type)) { + return 'Invalid type: must be "file" or "directory"'; + } + + return true; + } + + async execute(args: FileCreateArgs, remixApi: ICustomRemixApi): Promise { + try { + const exists = await remixApi.fileManager.methods.exists(args.path); + if (exists) { + return this.createErrorResult(`Path already exists: ${args.path}`); + } + + if (args.type === 'directory') { + await remixApi.fileManager.methods.mkdir(args.path); + } else { + await remixApi.fileManager.methods.writeFile(args.path, args.content || ''); + } + + const result: FileOperationResult = { + success: true, + path: args.path, + message: `${args.type === 'directory' ? 'Directory' : 'File'} created successfully`, + lastModified: new Date().toISOString() + }; + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to create ${args.type || 'file'}: ${error.message}`); + } + } +} + +/** + * File Delete Tool Handler + */ +export class FileDeleteHandler extends BaseToolHandler { + name = 'file_delete'; + description = 'Delete a file or directory'; + inputSchema = { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path of the file or directory to delete' + } + }, + required: ['path'] + }; + + getPermissions(): string[] { + return ['file:delete']; + } + + validate(args: FileDeleteArgs): boolean | string { + const required = this.validateRequired(args, ['path']); + if (required !== true) return required; + + const types = this.validateTypes(args, { path: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: FileDeleteArgs, remixApi: ICustomRemixApi): Promise { + try { + const exists = await remixApi.fileManager.methods.exists(args.path); + if (!exists) { + return this.createErrorResult(`Path not found: ${args.path}`); + } + + await remixApi.fileManager.methods.remove(args.path); + + const result: FileOperationResult = { + success: true, + path: args.path, + message: 'Path deleted successfully' + }; + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to delete: ${error.message}`); + } + } +} + +/** + * File Move Tool Handler + */ +export class FileMoveHandler extends BaseToolHandler { + name = 'file_move'; + description = 'Move or rename a file or directory'; + inputSchema = { + type: 'object', + properties: { + from: { + type: 'string', + description: 'Source path' + }, + to: { + type: 'string', + description: 'Destination path' + } + }, + required: ['from', 'to'] + }; + + getPermissions(): string[] { + return ['file:move']; + } + + validate(args: FileMoveArgs): boolean | string { + const required = this.validateRequired(args, ['from', 'to']); + if (required !== true) return required; + + const types = this.validateTypes(args, { from: 'string', to: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: FileMoveArgs, remixApi: ICustomRemixApi): Promise { + try { + const exists = await remixApi.fileManager.methods.exists(args.from); + if (!exists) { + return this.createErrorResult(`Source path not found: ${args.from}`); + } + + const destExists = await remixApi.fileManager.methods.exists(args.to); + if (destExists) { + return this.createErrorResult(`Destination path already exists: ${args.to}`); + } + + await remixApi.fileManager.methods.rename(args.from, args.to); + + const result: FileOperationResult = { + success: true, + path: args.to, + message: `Moved from ${args.from} to ${args.to}`, + lastModified: new Date().toISOString() + }; + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to move: ${error.message}`); + } + } +} + +/** + * File Copy Tool Handler + */ +export class FileCopyHandler extends BaseToolHandler { + name = 'file_copy'; + description = 'Copy a file or directory'; + inputSchema = { + type: 'object', + properties: { + from: { + type: 'string', + description: 'Source path' + }, + to: { + type: 'string', + description: 'Destination path' + } + }, + required: ['from', 'to'] + }; + + getPermissions(): string[] { + return ['file:copy']; + } + + validate(args: FileCopyArgs): boolean | string { + const required = this.validateRequired(args, ['from', 'to']); + if (required !== true) return required; + + const types = this.validateTypes(args, { from: 'string', to: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: FileCopyArgs, remixApi: ICustomRemixApi): Promise { + try { + const exists = await remixApi.fileManager.methods.exists(args.from); + if (!exists) { + return this.createErrorResult(`Source path not found: ${args.from}`); + } + + const content = await remixApi.fileManager.methods.readFile(args.from); + await remixApi.fileManager.methods.writeFile(args.to, content); + + const result: FileOperationResult = { + success: true, + path: args.to, + message: `Copied from ${args.from} to ${args.to}`, + size: content.length, + lastModified: new Date().toISOString() + }; + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to copy: ${error.message}`); + } + } +} + +/** + * Directory List Tool Handler + */ +export class DirectoryListHandler extends BaseToolHandler { + name = 'directory_list'; + description = 'List contents of a directory'; + inputSchema = { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Directory path to list' + }, + recursive: { + type: 'boolean', + description: 'List recursively', + default: false + } + }, + required: ['path'] + }; + + getPermissions(): string[] { + return ['file:read']; + } + + validate(args: DirectoryListArgs): boolean | string { + const required = this.validateRequired(args, ['path']); + if (required !== true) return required; + + const types = this.validateTypes(args, { path: 'string', recursive: 'boolean' }); + if (types !== true) return types; + + return true; + } + + async execute(args: DirectoryListArgs, remixApi: ICustomRemixApi): Promise { + try { + const exists = await remixApi.fileManager.methods.exists(args.path); + if (!exists) { + return this.createErrorResult(`Directory not found: ${args.path}`); + } + + const files = await remixApi.fileManager.methods.readdir(args.path); + const fileList = []; + + for (const file of files) { + const fullPath = `${args.path}/${file}`; + try { + const isDir = await remixApi.fileManager.methods.isDirectory(fullPath); + let size = 0; + + if (!isDir) { + const content = await remixApi.fileManager.methods.readFile(fullPath); + size = content.length; + } + + fileList.push({ + name: file, + path: fullPath, + isDirectory: isDir, + size: size + }); + + // Recursive listing + if (args.recursive && isDir) { + const subFiles = await this.execute({ path: fullPath, recursive: true }, remixApi); + if (!subFiles.isError && subFiles.content[0]?.text) { + const subResult = JSON.parse(subFiles.content[0].text); + if (subResult.files) { + fileList.push(...subResult.files); + } + } + } + } catch (error) { + // Skip files that can't be accessed + console.warn(`Couldn't access ${fullPath}:`, error.message); + } + } + + const result = { + success: true, + path: args.path, + files: fileList, + count: fileList.length + }; + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to list directory: ${error.message}`); + } + } +} + +/** + * File Exists Tool Handler + */ +export class FileExistsHandler extends BaseToolHandler { + name = 'file_exists'; + description = 'Check if a file or directory exists'; + inputSchema = { + type: 'object', + properties: { + path: { + type: 'string', + description: 'Path to check' + } + }, + required: ['path'] + }; + + getPermissions(): string[] { + return ['file:read']; + } + + validate(args: { path: string }): boolean | string { + const required = this.validateRequired(args, ['path']); + if (required !== true) return required; + + const types = this.validateTypes(args, { path: 'string' }); + if (types !== true) return types; + + return true; + } + + async execute(args: { path: string }, remixApi: ICustomRemixApi): Promise { + try { + const exists = await remixApi.fileManager.methods.exists(args.path); + + const result = { + success: true, + path: args.path, + exists: exists + }; + + return this.createSuccessResult(result); + } catch (error) { + return this.createErrorResult(`Failed to check file existence: ${error.message}`); + } + } +} + +/** + * Create file management tool definitions + */ +export function createFileManagementTools(): RemixToolDefinition[] { + return [ + { + name: 'file_read', + description: 'Read contents of a file', + inputSchema: new FileReadHandler().inputSchema, + category: ToolCategory.FILE_MANAGEMENT, + permissions: ['file:read'], + handler: new FileReadHandler() + }, + { + name: 'file_write', + description: 'Write content to a file', + inputSchema: new FileWriteHandler().inputSchema, + category: ToolCategory.FILE_MANAGEMENT, + permissions: ['file:write'], + handler: new FileWriteHandler() + }, + { + name: 'file_create', + description: 'Create a new file or directory', + inputSchema: new FileCreateHandler().inputSchema, + category: ToolCategory.FILE_MANAGEMENT, + permissions: ['file:create'], + handler: new FileCreateHandler() + }, + { + name: 'file_delete', + description: 'Delete a file or directory', + inputSchema: new FileDeleteHandler().inputSchema, + category: ToolCategory.FILE_MANAGEMENT, + permissions: ['file:delete'], + handler: new FileDeleteHandler() + }, + { + name: 'file_move', + description: 'Move or rename a file or directory', + inputSchema: new FileMoveHandler().inputSchema, + category: ToolCategory.FILE_MANAGEMENT, + permissions: ['file:move'], + handler: new FileMoveHandler() + }, + { + name: 'file_copy', + description: 'Copy a file or directory', + inputSchema: new FileCopyHandler().inputSchema, + category: ToolCategory.FILE_MANAGEMENT, + permissions: ['file:copy'], + handler: new FileCopyHandler() + }, + { + name: 'directory_list', + description: 'List contents of a directory', + inputSchema: new DirectoryListHandler().inputSchema, + category: ToolCategory.FILE_MANAGEMENT, + permissions: ['file:read'], + handler: new DirectoryListHandler() + }, + { + name: 'file_exists', + description: 'Check if a file or directory exists', + inputSchema: new FileExistsHandler().inputSchema, + category: ToolCategory.FILE_MANAGEMENT, + permissions: ['file:read'], + handler: new FileExistsHandler() + } + ]; +} \ No newline at end of file diff --git a/libs/remix-ai-core/src/servers/index.ts b/libs/remix-ai-core/src/servers/index.ts new file mode 100644 index 00000000000..411ffe4763c --- /dev/null +++ b/libs/remix-ai-core/src/servers/index.ts @@ -0,0 +1,172 @@ +/** + * Remix MCP Server - Main Export File + * Provides a comprehensive in-browser MCP server for Remix IDE + */ + +// Core Server +export { RemixMCPServer } from './RemixMCPServer'; +import { RemixMCPServer } from './RemixMCPServer'; +import { defaultSecurityConfig } from './middleware/SecurityMiddleware'; +import { defaultValidationConfig } from './middleware/ValidationMiddleware'; +import type { SecurityConfig } from './middleware/SecurityMiddleware'; +import type { ValidationConfig } from './middleware/ValidationMiddleware'; + +// API Integration +export { + RemixAPIIntegrationImpl, + MockRemixAPIIntegration, + createRemixAPIIntegration, + PluginIntegrationHelper +} from './RemixAPIIntegration'; + +// Tool Handlers +export { createFileManagementTools } from './handlers/FileManagementHandler'; +export { createCompilationTools } from './handlers/CompilationHandler'; +export { createDeploymentTools } from './handlers/DeploymentHandler'; +export { createDebuggingTools } from './handlers/DebuggingHandler'; + +// Resource Providers +export { ProjectResourceProvider } from './providers/ProjectResourceProvider'; +export { CompilationResourceProvider } from './providers/CompilationResourceProvider'; +export { DeploymentResourceProvider } from './providers/DeploymentResourceProvider'; + +// Middleware +export { + SecurityMiddleware, + defaultSecurityConfig +} from './middleware/SecurityMiddleware'; +export type { + SecurityConfig, + SecurityValidationResult, + AuditLogEntry +} from './middleware/SecurityMiddleware'; + +export { + ValidationMiddleware, + defaultValidationConfig +} from './middleware/ValidationMiddleware'; +export type { + ValidationConfig, + ValidationResult, + ValidationError, + ValidationWarning +} from './middleware/ValidationMiddleware'; + +// Registries +export { + RemixToolRegistry, + BaseToolHandler +} from './registry/RemixToolRegistry'; + +export { + RemixResourceProviderRegistry, + BaseResourceProvider +} from './registry/RemixResourceProviderRegistry'; + +// Types +export * from './types/mcpTools'; +export * from './types/mcpResources'; + +/** + * Factory function to create and initialize a complete Remix MCP Server + */ +export async function createRemixMCPServer( + remixApi: any, + options: { + enableSecurity?: boolean; + enableValidation?: boolean; + securityConfig?: SecurityConfig; + validationConfig?: ValidationConfig; + customTools?: any[]; + customProviders?: any[]; + } = {} +): Promise { + const { + enableSecurity = true, + enableValidation = true, + securityConfig = defaultSecurityConfig, + validationConfig = defaultValidationConfig, + customTools = [], + customProviders = [] + } = options; + + // Create server with configuration + const serverConfig = { + name: 'Remix MCP Server', + version: '1.0.0', + description: 'In-browser MCP server for Remix IDE providing comprehensive smart contract development tools', + debug: false, + maxConcurrentTools: 10, + toolTimeout: 30000, + resourceCacheTTL: 300000, + enableResourceCache: true, + security: enableSecurity ? { + enablePermissions: securityConfig.requirePermissions, + enableAuditLog: securityConfig.enableAuditLog, + allowedFilePatterns: [], + blockedFilePatterns: [] + } : undefined, + features: { + compilation: true, + deployment: true, + debugging: true, + fileManagement: true, + analysis: true, + workspace: true, + testing: true + } + }; + + const server = new RemixMCPServer(serverConfig, remixApi); + + // Register custom tools if provided + if (customTools.length > 0) { + // TODO: Add batch registration method to server + // for (const tool of customTools) { + // server.registerTool(tool); + // } + } + + // Register custom providers if provided + if (customProviders.length > 0) { + // TODO: Add provider registration method to server + // for (const provider of customProviders) { + // server.registerResourceProvider(provider); + // } + } + + // Initialize the server + await server.initialize(); + + return server; +} + +/** + * Quick setup function for development/testing + */ +export async function createDevMCPServer(remixApi: any): Promise { + return createRemixMCPServer(remixApi, { + enableSecurity: false, // Disable for easier development + enableValidation: true + }); +} + +/** + * Production setup function with all security enabled + */ +export async function createProductionMCPServer(remixApi: any): Promise { + return createRemixMCPServer(remixApi, { + enableSecurity: true, + enableValidation: true, + securityConfig: { + ...defaultSecurityConfig, + enableAuditLog: true, + requirePermissions: true + } + }); +} + +/** + * Default export + */ +export default RemixMCPServer; \ No newline at end of file diff --git a/libs/remix-ai-core/src/servers/middleware/SecurityMiddleware.ts b/libs/remix-ai-core/src/servers/middleware/SecurityMiddleware.ts new file mode 100644 index 00000000000..bd84a7e6603 --- /dev/null +++ b/libs/remix-ai-core/src/servers/middleware/SecurityMiddleware.ts @@ -0,0 +1,510 @@ +/** + * Security Middleware for Remix MCP Server + */ + +import { ICustomRemixApi } from '@remix-api'; +import { MCPToolCall, MCPToolResult } from '../../types/mcp'; +import { ToolExecutionContext } from '../types/mcpTools'; + +export interface SecurityConfig { + maxRequestsPerMinute: number; + maxFileSize: number; + allowedFileTypes: string[]; + blockedPaths: string[]; + requirePermissions: boolean; + enableAuditLog: boolean; + maxExecutionTime: number; +} + +export interface SecurityValidationResult { + allowed: boolean; + reason?: string; + risk?: 'low' | 'medium' | 'high'; +} + +export interface AuditLogEntry { + timestamp: Date; + toolName: string; + userId?: string; + arguments: any; + result: 'success' | 'error' | 'blocked'; + reason?: string; + executionTime: number; + riskLevel: 'low' | 'medium' | 'high'; +} + +/** + * Security middleware for validating and securing MCP tool calls + */ +export class SecurityMiddleware { + private rateLimitTracker = new Map(); + private auditLog: AuditLogEntry[] = []; + private blockedIPs = new Set(); + + constructor(private config: SecurityConfig) {} + + /** + * Validate a tool call before execution + */ + async validateToolCall( + call: MCPToolCall, + context: ToolExecutionContext, + remixApi: ICustomRemixApi + ): Promise { + const startTime = Date.now(); + + try { + // Rate limiting check + const rateLimitResult = this.checkRateLimit(context); + if (!rateLimitResult.allowed) { + this.logAudit(call, context, 'blocked', rateLimitResult.reason, startTime, 'medium'); + return rateLimitResult; + } + + // Permission validation + const permissionResult = this.validatePermissions(call, context); + if (!permissionResult.allowed) { + this.logAudit(call, context, 'blocked', permissionResult.reason, startTime, 'high'); + return permissionResult; + } + + // Argument validation + const argumentResult = await this.validateArguments(call, remixApi); + if (!argumentResult.allowed) { + this.logAudit(call, context, 'blocked', argumentResult.reason, startTime, argumentResult.risk || 'medium'); + return argumentResult; + } + + // File operation security checks + const fileResult = await this.validateFileOperations(call, remixApi); + if (!fileResult.allowed) { + this.logAudit(call, context, 'blocked', fileResult.reason, startTime, fileResult.risk || 'high'); + return fileResult; + } + + // Input sanitization + const sanitizationResult = this.validateInputSanitization(call); + if (!sanitizationResult.allowed) { + this.logAudit(call, context, 'blocked', sanitizationResult.reason, startTime, 'high'); + return sanitizationResult; + } + + this.logAudit(call, context, 'success', 'Validation passed', startTime, 'low'); + return { allowed: true, risk: 'low' }; + + } catch (error) { + this.logAudit(call, context, 'error', `Validation error: ${error.message}`, startTime, 'high'); + return { + allowed: false, + reason: `Security validation failed: ${error.message}`, + risk: 'high' + }; + } + } + + /** + * Wrap tool execution with security monitoring + */ + async secureExecute( + toolName: string, + context: ToolExecutionContext, + executor: () => Promise + ): Promise { + const startTime = Date.now(); + const timeoutId = setTimeout(() => { + throw new Error(`Tool execution timeout: ${toolName} exceeded ${this.config.maxExecutionTime}ms`); + }, this.config.maxExecutionTime); + + try { + const result = await executor(); + clearTimeout(timeoutId); + + this.logAudit( + { name: toolName, arguments: {} }, + context, + 'success', + 'Execution completed', + startTime, + 'low' + ); + + return result; + } catch (error) { + clearTimeout(timeoutId); + + this.logAudit( + { name: toolName, arguments: {} }, + context, + 'error', + error.message, + startTime, + 'high' + ); + + throw error; + } + } + + /** + * Check rate limiting for user/session + */ + private checkRateLimit(context: ToolExecutionContext): SecurityValidationResult { + const identifier = context.userId || context.sessionId || 'anonymous'; + const now = Date.now(); + const resetTime = Math.floor(now / 60000) * 60000 + 60000; // Next minute + + const userLimit = this.rateLimitTracker.get(identifier); + if (!userLimit || userLimit.resetTime <= now) { + this.rateLimitTracker.set(identifier, { count: 1, resetTime }); + return { allowed: true, risk: 'low' }; + } + + if (userLimit.count >= this.config.maxRequestsPerMinute) { + return { + allowed: false, + reason: `Rate limit exceeded: ${userLimit.count}/${this.config.maxRequestsPerMinute} requests per minute`, + risk: 'medium' + }; + } + + userLimit.count++; + return { allowed: true, risk: 'low' }; + } + + /** + * Validate user permissions for tool execution + */ + private validatePermissions(call: MCPToolCall, context: ToolExecutionContext): SecurityValidationResult { + if (!this.config.requirePermissions) { + return { allowed: true, risk: 'low' }; + } + + // Check if user has wildcard permission + if (context.permissions.includes('*')) { + return { allowed: true, risk: 'low' }; + } + + // Get required permissions for this tool (would need to be passed from tool definition) + const requiredPermissions = this.getRequiredPermissions(call.name); + + for (const permission of requiredPermissions) { + if (!context.permissions.includes(permission)) { + return { + allowed: false, + reason: `Missing required permission: ${permission}`, + risk: 'high' + }; + } + } + + return { allowed: true, risk: 'low' }; + } + + /** + * Validate tool arguments for security issues + */ + private async validateArguments(call: MCPToolCall, remixApi: ICustomRemixApi): Promise { + const args = call.arguments || {}; + + // Check for potentially dangerous patterns + const dangerousPatterns = [ + /eval\s*\(/i, + /function\s*\(/i, + /javascript:/i, + /