diff --git a/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx b/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx index c1d82061864..0b36f39ee1a 100644 --- a/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx +++ b/apps/remix-ide/src/app/plugins/remixAIPlugin.tsx @@ -8,6 +8,8 @@ import { IMCPServer, IMCPConnectionStatus } from '@remix/remix-ai-core'; import { RemixMCPServer, createRemixMCPServer } from '@remix/remix-ai-core'; import axios from 'axios'; import { endpointUrls } from "@remix-endpoints-helper" +import { QueryParams } from '@remix-project/remix-lib' + type chatRequestBufferT = { [key in keyof T]: T[key] } @@ -92,7 +94,12 @@ export class RemixAIPlugin extends Plugin { (window as any).getRemixAIPlugin = this // initialize the remix MCP server - this.remixMCPServer = await createRemixMCPServer(this) + const qp = new QueryParams() + const hasFlag = qp.exists('experimental') + if (hasFlag) { + this.remixMCPServer = await createRemixMCPServer(this) + } + return true } diff --git a/libs/remix-ai-core/src/inferencers/mcp/index.ts b/libs/remix-ai-core/src/inferencers/mcp/index.ts new file mode 100644 index 00000000000..199642764a6 --- /dev/null +++ b/libs/remix-ai-core/src/inferencers/mcp/index.ts @@ -0,0 +1,14 @@ +export { MCPClient } from './mcpClient'; +export { MCPInferencer } from './mcpInferencer'; + +export type { + IMCPServer, + IMCPResource, + IMCPResourceContent, + IMCPTool, + IMCPToolCall, + IMCPToolResult, + IMCPConnectionStatus, + IMCPInitializeResult, + IEnhancedMCPProviderParams, +} from '../../types/mcp'; diff --git a/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts b/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts index 20423f60b4e..e8881f5a0a0 100644 --- a/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts +++ b/libs/remix-ai-core/src/inferencers/mcp/mcpInferencer.ts @@ -777,12 +777,11 @@ ${toolsList}`, const result = await this.executeTool(targetServer, innerToolCall); return result }, - 30000 // 30 second timeout + 50000 // 50 second timeout ); // Execute the code const result = await codeExecutor.execute(code); - console.log(`[MCP Code Mode] inner tool executed with result`, result); // Convert code execution result to MCP tool result format if (result.success) { diff --git a/libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts b/libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts index 87898448fe8..24b826bd14c 100644 --- a/libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts +++ b/libs/remix-ai-core/src/remix-mcp-server/RemixMCPServer.ts @@ -39,6 +39,11 @@ import { CompilationResourceProvider } from './providers/CompilationResourceProv import { DeploymentResourceProvider } from './providers/DeploymentResourceProvider'; import { TutorialsResourceProvider } from './providers/TutorialsResourceProvider'; +// Import middleware +import { SecurityMiddleware } from './middleware/SecurityMiddleware'; +import { ValidationMiddleware } from './middleware/ValidationMiddleware'; +import { MCPConfigManager } from './config/MCPConfigManager'; + /** * Main Remix MCP Server implementation */ @@ -53,6 +58,10 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { private _resourceCache: Map = new Map(); private _auditLog: AuditLogEntry[] = []; private _startTime: Date = new Date(); + private _securityMiddleware: SecurityMiddleware; + private _validationMiddleware: ValidationMiddleware; + private _configManager: MCPConfigManager; + private _isInitialized: boolean = false; constructor(plugin, config: RemixMCPServerConfig) { super(); @@ -60,6 +69,7 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { this._plugin = plugin this._tools = new RemixToolRegistry(); this._resources = new RemixResourceProviderRegistry(plugin); + this._isInitialized = false; this._stats = { uptime: 0, @@ -71,6 +81,19 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { lastActivity: new Date() }; + // Initialize config manager + this._configManager = new MCPConfigManager(this._plugin); + + // Initialize middleware with tool registry (will be updated after config is loaded) + this._securityMiddleware = new SecurityMiddleware( + this._tools as RemixToolRegistry, + this._configManager + ); + this._validationMiddleware = new ValidationMiddleware( + this._plugin, + this._configManager + ); + this.setupEventHandlers(); } @@ -97,35 +120,54 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { } get plugin(): any{ - return this.plugin + return this._plugin + } + + get configManager(): MCPConfigManager { + return this._configManager } /** * Initialize the MCP server */ async initialize(): Promise { + const initResult: IMCPInitializeResult = { + 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}. Configuration loaded from workspace.` + }; + try { - this.setState(ServerState.STARTING); + if (this._isInitialized) return initResult; + + try { + await this._configManager.loadConfig(); + console.log('[RemixMCPServer] MCP configuration loaded and connected to middlewares'); + console.log('[RemixMCPServer] Configuration summary:', this._configManager.getConfigSummary()); + + const securityConfig = this._configManager.getSecurityConfig(); + const validationConfig = this._configManager.getValidationConfig(); + console.log('[RemixMCPServer] Middlewares connected:'); + console.log(` - SecurityMiddleware: using ${securityConfig.excludeTools?.length || 0} excluded tools`); + console.log(` - ValidationMiddleware: strictMode=${validationConfig.strictMode}, ${Object.keys(validationConfig.toolValidation || {}).length} tool-specific rules`); + + this._configManager.startPolling(5000); + } catch (error) { + console.log(`[RemixMCPServer] Failed to load MCP config: ${error.message}, using defaults`); + } await this.initializeDefaultTools(); - await this.initializeDefaultResourceProviders(); this.setupCleanupIntervals(); - - const result: IMCPInitializeResult = { - 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); - return result; + this._isInitialized = true; + return initResult; } catch (error) { this.setState(ServerState.ERROR); throw error; @@ -146,6 +188,9 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { async stop(): Promise { this.setState(ServerState.STOPPING); + // Stop config polling + this._configManager.stopPolling(); + // Cancel active tool executions for (const [id, execution] of this._activeExecutions) { execution.status = 'failed'; @@ -253,12 +298,17 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { } /** - * Execute a tool + * Execute a tool with security and validation middleware */ private async executeTool(call: IMCPToolCall): Promise { - const executionId = `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + const executionId = `exec_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`; const startTime = new Date(); + // Get current user (default to 'default' role) + const currentUser = 'default'; // Can be extended to get from plugin context + + const permissionCheckResult = await this.checkPermissions(call.name, currentUser); + const execution: ToolExecutionStatus = { id: executionId, toolName: call.name, @@ -266,8 +316,8 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { status: 'running', context: { workspace: await this.getCurrentWorkspace(), - user: 'default', // TODO: Get actual user - permissions: ["*"] // TODO: Get actual permissions + user: currentUser, + permissions: permissionCheckResult.userPermissions } }; @@ -275,27 +325,57 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { 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}`); + const context = { + workspace: execution.context.workspace, + currentFile: await this.getCurrentFile(), + permissions: permissionCheckResult.userPermissions, + timestamp: Date.now(), + requestId: executionId + }; + + // STEP 1: Security Validation (uses MCPConfigManager for dynamic config) + console.log(`[RemixMCPServer] Step 1: Security validation for tool '${call.name}' (using MCPConfigManager)`); + const securityResult = await this._securityMiddleware.validateToolCall(call, context, this._plugin); + + if (!securityResult.allowed) { + console.log(`[RemixMCPServer] Security validation FAILED for tool '${call.name}': ${securityResult.reason}`); + throw new Error(`Security validation failed: ${securityResult.reason}`); + } + console.log(`[RemixMCPServer] Security validation PASSED for tool '${call.name}'`); + + // STEP 2: Input Validation (uses MCPConfigManager for dynamic config) + console.log(`[RemixMCPServer] Step 2: Input validation for tool '${call.name}' (using MCPConfigManager)`); + const toolDefinition = this._tools.get(call.name); + const inputSchema = toolDefinition?.inputSchema; + + const validationResult = await this._validationMiddleware.validateToolCall( + call, + inputSchema, + context, + this._plugin + ); + + if (!validationResult.valid) { + const errorMessages = validationResult.errors.map(e => e.message).join(', '); + console.log(`[RemixMCPServer] Input validation FAILED for tool '${call.name}': ${errorMessages}`); + throw new Error(`Input validation failed: ${errorMessages}`); } - // Set timeout - const timeout = this._config.toolTimeout || 30000; + // Log warnings if any + if (validationResult.warnings.length > 0) { + const warnings = validationResult.warnings.map(w => w.message).join(', '); + console.log(`[RemixMCPServer] Input validation warnings for tool '${call.name}': ${warnings}`); + } else { + console.log(`[RemixMCPServer] Input validation PASSED for tool '${call.name}'`); + } + + const timeout = this._config.toolTimeout || 60000; 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._plugin); - + const toolPromise = this._tools.execute(call, context, this._plugin); const result = await Promise.race([toolPromise, timeoutPromise]); // Update execution status @@ -303,6 +383,7 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { execution.endTime = new Date(); this._stats.totalToolCalls++; + console.log(`[RemixMCPServer] Tool '${call.name}' executed successfully`); this.emit('tool-executed', execution); return result; @@ -312,16 +393,17 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { execution.endTime = new Date(); this._stats.errorCount++; + console.log(`[RemixMCPServer] Tool '${call.name}' execution FAILED: ${error.message}`); this.emit('tool-executed', execution); - throw error; + return { + isError:true, + content: [{ type: 'text', text:error.message }] + } } finally { this._activeExecutions.delete(executionId); } } - /** - * Get resource content with caching - */ private async getResourceContent(uri: string): Promise { // Check cache first if (this._config.enableResourceCache !== false) { @@ -357,13 +439,250 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { } async checkPermissions(operation: string, user: string, resource?: string): Promise { - // TODO: Implement actual permission checking - // For now, allow all operations - return { - allowed: true, - requiredPermissions: [], - userPermissions: ['*'] + try { + const securityConfig = this._configManager.getSecurityConfig(); + + if (!securityConfig.requirePermissions) { + return { + allowed: true, + requiredPermissions: [], + userPermissions: ['*'], + reason: 'Permissions not required by configuration' + }; + } + + const userPermissions = this.getUserPermissions(user, securityConfig); + const requiredPermissions = this.getOperationPermissions(operation); + + if (userPermissions.includes('*')) { + return { + allowed: true, + requiredPermissions, + userPermissions, + reason: 'User has wildcard permission (*)' + }; + } + + // check if user has all required permissions + const missingPermissions: string[] = []; + for (const requiredPermission of requiredPermissions) { + if (!userPermissions.includes(requiredPermission)) { + missingPermissions.push(requiredPermission); + } + } + + // If there are missing permissions, deny the operation + if (missingPermissions.length > 0) { + const reason = `Missing required permissions: ${missingPermissions.join(', ')}`; + + // Log denied permission check + this.logAuditEntry({ + id: `perm_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`, + timestamp: new Date(), + type: 'permission_check', + user, + details: { + permission: operation, + resourceUri: resource, + result: 'denied', + args: { missingPermissions, requiredPermissions, userPermissions } + }, + severity: 'warning' + }); + + return { + allowed: false, + requiredPermissions, + userPermissions, + reason + }; + } + + // Additional resource-specific checks + if (resource) { + const resourceCheck = this.checkResourcePermissions(resource, userPermissions, securityConfig); + console.log("Resource check:", resourceCheck) + if (!resourceCheck.allowed) { + // Log denied resource access + this.logAuditEntry({ + id: `perm_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`, + timestamp: new Date(), + type: 'permission_check', + user, + details: { + permission: operation, + resourceUri: resource, + result: 'denied', + args: { reason: resourceCheck.reason } + }, + severity: 'warning' + }); + + return { + allowed: false, + requiredPermissions, + userPermissions, + reason: resourceCheck.reason + }; + } + } + + const result = { + allowed: true, + requiredPermissions, + userPermissions, + reason: 'All required permissions granted' + }; + + this.logAuditEntry({ + id: `perm_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`, + timestamp: new Date(), + type: 'permission_check', + user, + details: { + permission: operation, + resourceUri: resource, + result: 'allowed' + }, + severity: 'info' + }); + + return result; + + } catch (error) { + console.error('[RemixMCPServer] Error checking permissions:', error); + + this.logAuditEntry({ + id: `perm_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`, + timestamp: new Date(), + type: 'permission_check', + user, + details: { + permission: operation, + resourceUri: resource, + error: error.message, + result: 'error' + }, + severity: 'error' + }); + + return { + allowed: false, + requiredPermissions: [], + userPermissions: [], + reason: `Permission check failed: ${error.message}` + }; + } + } + + private logAuditEntry(entry: AuditLogEntry): void { + const securityConfig = this._configManager.getSecurityConfig(); + + if (!securityConfig.enableAuditLog) { + return; + } + + this._auditLog.push(entry); + if (this._auditLog.length > 1000) { + this._auditLog = this._auditLog.slice(-500); + } + + if (entry.severity === 'error') { + console.error('[RemixMCPServer] Audit:', entry); + } + } + + private getUserPermissions(user: string, securityConfig: any): string[] { + console.log('security config', securityConfig) + const permissions: string[] = []; + + if (securityConfig.permissions?.defaultPermissions) { + permissions.push(...securityConfig.permissions.defaultPermissions); + } + + if (securityConfig.permissions?.roles && securityConfig.permissions.roles[user]) { + permissions.push(...securityConfig.permissions.roles[user]); + } + return Array.from(new Set(permissions)); + } + + private getOperationPermissions(operation: string): string[] { + const toolDefinition = this._tools.get(operation); + if (toolDefinition && toolDefinition.permissions) { + return toolDefinition.permissions; + } + + const defaultPermissionMap: Record = { + // File operations + 'file_read': ['file:read'], + 'file_write': ['file:write'], + 'file_create': ['file:write', 'file:create'], + 'file_delete': ['file:delete'], + 'file_move': ['file:write', 'file:move'], + 'file_copy': ['file:read', 'file:write'], + 'list_directory': ['file:read'], + + // Compilation + 'compile_solidity': ['compile:solidity'], + 'get_compiler_config': ['compile:read'], + 'set_compiler_config': ['compile:config'], + + // Deployment + 'deploy_contract': ['deploy:contract'], + 'call_contract': ['contract:interact'], + 'send_transaction': ['transaction:send'], + 'get_deployed_contracts': ['deploy:read'], + 'set_execution_environment': ['environment:config'], + 'get_account_balance': ['account:read'], + 'get_user_accounts': ['accounts:read'], + 'set_selected_account': ['accounts:write'], + 'get_current_environment': ['environment:read'], + + // Debugging + 'start_debugger': ['debug:start'], + 'set_breakpoint': ['debug:breakpoint'], + 'step_debugger': ['debug:control'], + 'inspect_variable': ['debug:inspect'], + + // Analysis + 'analyze_code': ['analysis:static'], + 'security_scan': ['analysis:security'], + 'estimate_gas': ['analysis:gas'] }; + + return defaultPermissionMap[operation] || ['*']; + } + + private checkResourcePermissions(resource: string, userPermissions: string[], securityConfig: any): { allowed: boolean; reason?: string } { + if (securityConfig.blockedPaths) { + for (const blockedPath of securityConfig.blockedPaths) { + if (resource.includes(blockedPath)) { + return { + allowed: false, + reason: `Access to blocked path: ${blockedPath}` + }; + } + } + } + + if (securityConfig.allowedPaths && securityConfig.allowedPaths.length > 0) { + let pathAllowed = false; + for (const allowedPath of securityConfig.allowedPaths) { + if (resource.includes(allowedPath) || resource.startsWith(allowedPath)) { + pathAllowed = true; + break; + } + } + + if (!pathAllowed) { + return { + allowed: false, + reason: 'Resource path not in allowed paths list' + }; + } + } + + return { allowed: true }; } getActiveExecutions(): ToolExecutionStatus[] { @@ -401,6 +720,39 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { } } + /** + * Reload MCP configuration from workspace + */ + async reloadConfig(): Promise { + try { + console.log('[RemixMCPServer] Reloading MCP configuration...'); + const mcpConfig = await this._configManager.reloadConfig(); + console.log('[RemixMCPServer] Configuration reloaded successfully'); + console.log('[RemixMCPServer] Configuration summary:', this._configManager.getConfigSummary()); + this.emit('config-reloaded', mcpConfig); + } catch (error) { + console.log(`[RemixMCPServer] Failed to reload config: ${error.message}`); + throw error; + } + } + + getMCPConfig() { + return this._configManager.getConfig(); + } + + updateMCPConfig(partialConfig: Partial): void { + this._configManager.updateConfig(partialConfig); + console.log('[RemixMCPServer] Configuration updated at runtime'); + this.emit('config-updated', this._configManager.getConfig()); + } + + /** + * Check if config polling is active + */ + isConfigPollingActive(): boolean { + return this._configManager.isPolling(); + } + /** * Set server state */ @@ -496,9 +848,6 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { } } - /** - * Setup cleanup intervals - */ private setupCleanupIntervals(): void { setInterval(() => { const now = Date.now(); @@ -516,20 +865,14 @@ export class RemixMCPServer extends EventEmitter implements IRemixMCPServer { }, 300000); } - /** - * Get current workspace - */ private async getCurrentWorkspace(): Promise { try { return await this.plugin.call('filePanel', 'getCurrentWorkspace') } catch (error) { - return 'default'; + return ''; } } - /** - * Get current file - */ private async getCurrentFile(): Promise { try { return await this.plugin.call('fileManager', 'getCurrentFile'); diff --git a/libs/remix-ai-core/src/remix-mcp-server/config/MCPConfigManager.ts b/libs/remix-ai-core/src/remix-mcp-server/config/MCPConfigManager.ts new file mode 100644 index 00000000000..5af0d513745 --- /dev/null +++ b/libs/remix-ai-core/src/remix-mcp-server/config/MCPConfigManager.ts @@ -0,0 +1,210 @@ +/** + * MCP Configuration Manager + * Loads and manages .mcp.config.json configuration + */ + +import { Plugin } from '@remixproject/engine'; +import { MCPConfig, defaultMCPConfig, minimalMCPConfig } from '../types/mcpConfig'; + +export class MCPConfigManager { + private config: MCPConfig; + private plugin: Plugin; + private configPath: string = 'artifacts/.mcp.config.json'; + private pollingInterval?: NodeJS.Timeout; + + constructor(plugin: Plugin) { + this.plugin = plugin; + this.config = defaultMCPConfig; + } + + async loadConfig(): Promise { + try { + + const exists = await this.plugin.call('fileManager', 'exists', this.configPath); + + if (exists) { + const configContent = await this.plugin.call('fileManager', 'readFile', this.configPath); + const userConfig = JSON.parse(configContent); + // Merge with defaults + this.config = this.mergeConfig(defaultMCPConfig, userConfig); + } else { + this.config = minimalMCPConfig; + // Create default config file + await this.plugin.call('fileManager', 'writeFile', this.configPath, JSON.stringify(this.config, null, 2)); + } + + return this.config; + } catch (error) { + this.config = defaultMCPConfig; + return this.config; + } + } + + async saveConfig(config: MCPConfig): Promise { + try { + const configContent = JSON.stringify(config, null, 2); + + await this.plugin.call('fileManager', 'writeFile', this.configPath, configContent); + this.config = config; + + console.log(`[MCPConfigManager] Config saved to: ${this.configPath}`); + } catch (error) { + console.error(`[MCPConfigManager] Error saving config: ${error.message}`); + throw error; + } + } + + async createDefaultConfig(): Promise { + try { + + const exists = await this.plugin.call('fileManager', 'exists', this.configPath); + if (exists) { + console.log('[MCPConfigManager] Config file already exists, skipping creation'); + return; + } + + await this.saveConfig(defaultMCPConfig); + console.log('[MCPConfigManager] Default config file created'); + } catch (error) { + console.error(`[MCPConfigManager] Error creating default config: ${error.message}`); + throw error; + } + } + + getConfig(): MCPConfig { + return this.config; + } + + getSecurityConfig() { + return this.config.security; + } + + getValidationConfig() { + return this.config.validation; + } + + getResourceConfig() { + return this.config.resources; + } + + updateConfig(partialConfig: Partial): void { + this.config = this.mergeConfig(this.config, partialConfig); + console.log('[MCPConfigManager] Config updated at runtime'); + } + + isToolAllowed(toolName: string): boolean { + const { excludeTools } = this.config.security; + + if (excludeTools && excludeTools.includes(toolName)) { + return false; + } + + return true; + } + + isPathAllowed(path: string): boolean { + const { blockedPaths, allowedPaths } = this.config.security; + + if (blockedPaths) { + for (const blocked of blockedPaths) { + if (path.includes(blocked)) { + return false; + } + } + } + + // If allowedPaths is set, only allow paths matching patterns + if (allowedPaths && allowedPaths.length > 0) { + let allowed = false; + for (const allowedPattern of allowedPaths) { + if (path.includes(allowedPattern) || this.matchPattern(path, allowedPattern)) { + allowed = true; + break; + } + } + return allowed; + } + + // Otherwise, allow by default + return true; + } + + private matchPattern(str: string, pattern: string): boolean { + // Convert glob pattern to regex + const regexPattern = pattern + .replace(/\./g, '\\.') + .replace(/\*/g, '.*') + .replace(/\?/g, '.'); + + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(str); + } + + private mergeConfig(base: any, override: any): any { + const result = { ...base }; + + for (const key in override) { + if (override[key] !== undefined) { + if (typeof override[key] === 'object' && !Array.isArray(override[key]) && override[key] !== null) { + result[key] = this.mergeConfig(base[key] || {}, override[key]); + } else { + result[key] = override[key]; + } + } + } + + return result; + } + + async reloadConfig(): Promise { + return this.loadConfig(); + } + + /** + * Get configuration summary for logging + */ + getConfigSummary(): string { + const config = this.getConfig(); + return JSON.stringify({ + version: config.version, + security: { + excludeTools: config.security.excludeTools?.length || 0, + rateLimitEnabled: config.security.rateLimit?.enabled || false, + maxRequestsPerMinute: config.security.rateLimit?.requestsPerMinute || config.security.maxRequestsPerMinute + }, + validation: { + strictMode: config.validation.strictMode, + schemasEnabled: config.validation.validateSchemas, + toolValidationRules: Object.keys(config.validation.toolValidation || {}).length + }, + resources: { + cacheEnabled: config.resources?.enableCache || false, + cacheTTL: config.resources?.cacheTTL || 0 + } + }, null, 2); + } + + startPolling(intervalMs: number = 10000): void { + if (this.pollingInterval) { + return; + } + + this.pollingInterval = setInterval(async () => { + try { + await this.reloadConfig(); + } catch (error) { + } + }, intervalMs); + } + + stopPolling(): void { + if (this.pollingInterval) { + clearInterval(this.pollingInterval); + this.pollingInterval = undefined; + } + } + + isPolling(): boolean { + return !!this.pollingInterval; + } +} diff --git a/libs/remix-ai-core/src/remix-mcp-server/index.ts b/libs/remix-ai-core/src/remix-mcp-server/index.ts index 8e0efd957eb..2957d243fd0 100644 --- a/libs/remix-ai-core/src/remix-mcp-server/index.ts +++ b/libs/remix-ai-core/src/remix-mcp-server/index.ts @@ -6,10 +6,16 @@ // 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'; + +// Configuration +export { MCPConfigManager } from './config/MCPConfigManager'; +export type { + MCPConfig, + MCPSecurityConfig, + MCPValidationConfig, + MCPResourceConfig, +} from './types/mcpConfig'; +export { defaultMCPConfig } from './types/mcpConfig'; // Tool Handlers export { createFileManagementTools } from './handlers/FileManagementHandler'; @@ -25,22 +31,14 @@ export { DeploymentResourceProvider } from './providers/DeploymentResourceProvid export { TutorialsResourceProvider } from './providers/TutorialsResourceProvider'; // Middleware -export { - SecurityMiddleware, - defaultSecurityConfig -} from './middleware/SecurityMiddleware'; export type { - SecurityConfig, SecurityValidationResult, + SecurityMiddleware, AuditLogEntry } from './middleware/SecurityMiddleware'; -export { - ValidationMiddleware, - defaultValidationConfig -} from './middleware/ValidationMiddleware'; export type { - ValidationConfig, + ValidationMiddleware, ValidationResult, ValidationError, ValidationWarning @@ -69,8 +67,8 @@ export async function createRemixMCPServer( options: { enableSecurity?: boolean; enableValidation?: boolean; - securityConfig?: SecurityConfig; - validationConfig?: ValidationConfig; + securityConfig?: any; + validationConfig?: any; customTools?: any[]; customProviders?: any[]; } = {}, @@ -78,13 +76,10 @@ export async function createRemixMCPServer( 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', @@ -95,8 +90,8 @@ export async function createRemixMCPServer( resourceCacheTTL: 5000, enableResourceCache: false, security: enableSecurity ? { - enablePermissions: securityConfig.requirePermissions, - enableAuditLog: securityConfig.enableAuditLog, + enablePermissions: true, + enableAuditLog: true, allowedFilePatterns: [], blockedFilePatterns: [] } : undefined, @@ -113,29 +108,22 @@ export async function createRemixMCPServer( const server = new RemixMCPServer(plugin, serverConfig); - // 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 + console.log("Initializing server") await server.initialize(); return server; } -/** - * Default export - */ export default RemixMCPServer; \ No newline at end of file diff --git a/libs/remix-ai-core/src/remix-mcp-server/middleware/BaseMiddleware.ts b/libs/remix-ai-core/src/remix-mcp-server/middleware/BaseMiddleware.ts new file mode 100644 index 00000000000..80f9c985dbe --- /dev/null +++ b/libs/remix-ai-core/src/remix-mcp-server/middleware/BaseMiddleware.ts @@ -0,0 +1,109 @@ +/** + * Base Middleware class with shared utilities + * + * This base class provides common functionality used by both SecurityMiddleware + * and ValidationMiddleware to eliminate code duplication. + */ + +import { MCPConfigManager } from '../config/MCPConfigManager'; + +export abstract class BaseMiddleware { + protected configManager?: MCPConfigManager; + + constructor(configManager?: MCPConfigManager) { + this.configManager = configManager; + } + + /** + * Match a string against a pattern (supports wildcards) + * Shared between SecurityMiddleware and ValidationMiddleware + * + * Pattern syntax: + * - * matches any characters except / + * - ** matches any characters including / + * - ? matches a single character + * + * @example + * matchPattern('src/file.ts', 'src/*.ts') // true + * matchPattern('src/sub/file.ts', 'src/**\/*.ts') // true + * matchPattern('test.js', 'test.?s') // true + */ + protected matchPattern(str: string, pattern: string): boolean { + // Convert glob pattern to regex + const regexPattern = pattern + .replace(/\./g, '\\.') // Escape dots + .replace(/\*\*/g, '___DOUBLESTAR___') // Temporarily replace ** + .replace(/\*/g, '[^/]*') // * matches anything except / + .replace(/___DOUBLESTAR___/g, '.*') // ** matches anything including / + .replace(/\?/g, '.'); // ? matches single character + + const regex = new RegExp(`^${regexPattern}$`); + return regex.test(str); + } + + /** + * Check if a value is likely code content (to avoid false positives in validation) + * Code content should not trigger injection warnings for patterns like require(), eval(), etc. + * + * This helps differentiate between: + * - Malicious input: eval(userInput) in a parameter + * - Legitimate code: 'function test() { eval("x"); }' in file content + */ + protected isLikelyCodeContent(value: string): boolean { + // Check for common code patterns that indicate this is actual source code + const codeIndicators = [ + /^pragma solidity/, // Solidity contract + /^\/\*[\s\S]*\*\//, // Block comment at start + /^\/\//, // Line comment at start + /function\s+\w+\s*\(/, // Function declarations + /contract\s+\w+/, // Contract declarations + /import\s+.*from/, // Import statements + /\n\s*function\s+/, // Function on new line + /\n\s*contract\s+/, // Contract on new line + ]; + + // If it contains multiple code indicators, it's likely source code + const matchCount = codeIndicators.filter(pattern => pattern.test(value)).length; + return matchCount >= 2 || value.length > 500; // Long content is likely code + } + + /** + * Check if a string contains potentially dangerous patterns + * Returns the pattern that matched, or null if safe + * + * IMPORTANT: This should only be used for user inputs, not for file content + * that might legitimately contain these patterns as code. + */ + protected findDangerousPattern(value: string, context: 'input' | 'code' = 'input'): RegExp | null { + // If this is code content, be much more lenient + if (context === 'code' || this.isLikelyCodeContent(value)) { + // Only check for actual command injection patterns in code + const severePatterns = [ + /;\s*rm\s+-rf\s+\//, // Dangerous rm commands + /&&\s*rm\s+-rf\s+\//, // Chained dangerous commands + /\|\s*rm\s+-rf\s+\//, // Piped dangerous commands + ]; + + for (const pattern of severePatterns) { + if (pattern.test(value)) return pattern; + } + return null; + } + + // For user inputs, be more strict + const dangerousPatterns = [ + /;\s*rm\s/, // rm commands + /&&\s*rm\s/, // Chained rm + /\|\s*rm\s/, // Piped rm + />\s*\/dev\//, // Redirect to devices + /curl\s.*\|/, // Piped curl (potential malware download) + /wget\s.*\|/, // Piped wget (potential malware download) + ]; + + for (const pattern of dangerousPatterns) { + if (pattern.test(value)) return pattern; + } + + return null; + } +} diff --git a/libs/remix-ai-core/src/remix-mcp-server/middleware/SecurityMiddleware.ts b/libs/remix-ai-core/src/remix-mcp-server/middleware/SecurityMiddleware.ts index 05beb8cade7..07f5ca4fb86 100644 --- a/libs/remix-ai-core/src/remix-mcp-server/middleware/SecurityMiddleware.ts +++ b/libs/remix-ai-core/src/remix-mcp-server/middleware/SecurityMiddleware.ts @@ -1,20 +1,11 @@ -/** - * Security Middleware for Remix MCP Server - */ import { Plugin } from '@remixproject/engine'; -import { IMCPToolCall, IMCPToolResult } from '../../types/mcp'; +import { IMCPToolCall } 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; -} +import { RemixToolRegistry } from '../registry/RemixToolRegistry'; +import { MCPSecurityConfig } from '../types/mcpConfig'; +import { MCPConfigManager } from '../config/MCPConfigManager'; +import { BaseMiddleware } from './BaseMiddleware'; export interface SecurityValidationResult { allowed: boolean; @@ -33,27 +24,48 @@ export interface AuditLogEntry { riskLevel: 'low' | 'medium' | 'high'; } -/** - * Security middleware for validating and securing MCP tool calls - */ -export class SecurityMiddleware { +export class SecurityMiddleware extends BaseMiddleware { private rateLimitTracker = new Map(); private auditLog: AuditLogEntry[] = []; - private blockedIPs = new Set(); + private toolRegistry?: RemixToolRegistry; + private config: MCPSecurityConfig; + private rateLimitCleanupInterval?: NodeJS.Timeout; - constructor(private config: SecurityConfig) {} + constructor(toolRegistry?: RemixToolRegistry, configManager?: MCPConfigManager) { + super(configManager); + this.toolRegistry = toolRegistry; + + this.config = configManager.getSecurityConfig() as MCPSecurityConfig; + + // Setup periodic cleanup of rate limit tracker (every 5 minutes) + this.rateLimitCleanupInterval = setInterval(() => { + this.cleanupRateLimitTracker(); + }, 300000); + } + + private getConfig(): MCPSecurityConfig { + if (this.configManager) { + return this.configManager.getSecurityConfig(); + } + return this.config; + } - /** - * Validate a tool call before execution - */ async validateToolCall( call: IMCPToolCall, context: ToolExecutionContext, plugin: Plugin ): Promise { const startTime = Date.now(); + const config = this.getConfig(); try { + // Check if tool is allowed (exclude/allow lists) + const toolAllowedResult = this.checkToolAllowed(call.name); + if (!toolAllowedResult.allowed) { + this.logAudit(call, context, 'blocked', toolAllowedResult.reason, startTime, 'high'); + return toolAllowedResult; + } + // Rate limiting check const rateLimitResult = this.checkRateLimit(context); if (!rateLimitResult.allowed) { @@ -68,6 +80,13 @@ export class SecurityMiddleware { return permissionResult; } + // Mainnet transaction validation (must be before other validations to block early) + const mainnetResult = await this.validateMainnetOperation(call, plugin); + if (!mainnetResult.allowed) { + this.logAudit(call, context, 'blocked', mainnetResult.reason, startTime, 'high'); + return mainnetResult; + } + // Argument validation const argumentResult = await this.validateArguments(call, plugin); if (!argumentResult.allowed) { @@ -102,67 +121,56 @@ export class SecurityMiddleware { } } - /** - * 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); + private checkToolAllowed(toolName: string): SecurityValidationResult { + const config = this.getConfig(); - 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' - ); + // Use ConfigManager if available + if (this.configManager) { + const allowed = this.configManager.isToolAllowed(toolName); + if (!allowed) { + return { + allowed: false, + reason: `Tool '${toolName}' is not allowed by configuration`, + risk: 'high' + }; + } + return { allowed: true, risk: 'low' }; + } - throw error; + if (config.excludeTools && config.excludeTools.includes(toolName)) { + return { + allowed: false, + reason: `Tool '${toolName}' is excluded by security configuration`, + risk: 'high' + }; } + + return { allowed: true, risk: 'low' }; } - /** - * Check rate limiting for user/session - */ private checkRateLimit(context: ToolExecutionContext): SecurityValidationResult { + const config = this.getConfig(); const identifier = context.userId || context.sessionId || 'anonymous'; const now = Date.now(); const resetTime = Math.floor(now / 60000) * 60000 + 60000; // Next minute + // Check if rate limiting is disabled + if (config.rateLimit && !config.rateLimit.enabled) { + return { allowed: true, risk: 'low' }; + } + + const maxRequests = config.rateLimit?.requestsPerMinute || config.maxRequestsPerMinute; + 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) { + if (userLimit.count >= maxRequests) { return { allowed: false, - reason: `Rate limit exceeded: ${userLimit.count}/${this.config.maxRequestsPerMinute} requests per minute`, + reason: `Rate limit exceeded: ${userLimit.count}/${maxRequests} requests per minute`, risk: 'medium' }; } @@ -202,32 +210,30 @@ export class SecurityMiddleware { /** * Validate tool arguments for security issues + * + * IMPORTANT: For file operations (file_write, file_create), we treat 'content' + * arguments as code, not user input, to avoid false positives for legitimate + * code patterns like require(), eval(), etc. */ private async validateArguments(call: IMCPToolCall, plugin: Plugin): Promise { const args = call.arguments || {}; + console.log('validateArguments', args) - // Check for potentially dangerous patterns - const dangerousPatterns = [ - /eval\s*\(/i, - /function\s*\(/i, - /javascript:/i, - /