UNPKG

@cgaspard/webappmcp

Version:

WebApp MCP - Model Context Protocol integration for web applications with server-side debugging tools

236 lines 10.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.MCPSSEServer = void 0; const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js"); const sse_js_1 = require("@modelcontextprotocol/sdk/server/sse.js"); const types_js_1 = require("@modelcontextprotocol/sdk/types.js"); const index_js_2 = require("./tools/index.js"); class MCPSSEServer { constructor(config) { this.sseTransports = new Map(); this.getClients = config.getClients; this.executeTool = config.executeTool; this.debug = config.debug ?? false; this.plugins = config.plugins ?? []; this.getServerLogs = config.getServerLogs; } log(...args) { if (this.debug) { console.log('[webappmcp]', ...args); } } logError(...args) { // Always log errors console.error('[webappmcp]', ...args); } createServer() { const server = new index_js_1.Server({ name: 'webapp-mcp-sse', version: '0.1.0', description: 'MCP server connected to a running web application instance. This server provides direct access to the DOM, state, and functionality of the currently connected web app.', }, { capabilities: { tools: {}, prompts: {}, }, }); server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => { this.log(`ListTools request received`); const builtInTools = (0, index_js_2.registerTools)(); // Convert plugin tools to MCP format const pluginTools = this.plugins.flatMap(plugin => (plugin.tools || []).map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema || { type: 'object', properties: {}, }, }))); const allTools = [...builtInTools, ...pluginTools]; this.log(`Returning ${allTools.length} tools (${builtInTools.length} built-in, ${pluginTools.length} from plugins)`); return { tools: allTools, }; }); server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => { this.log(`====== TOOL CALL REQUEST ======`); this.log(`Full request:`, JSON.stringify(request, null, 2)); const { name, arguments: args } = request.params; this.log(`Tool: ${name}`); this.log(`Arguments:`, JSON.stringify(args, null, 2)); // Special handling for webapp_list_clients - doesn't need WebSocket connection if (name === 'webapp_list_clients') { this.log(`Handling webapp_list_clients tool`); const clients = this.getClients ? this.getClients() : []; const result = { content: [ { type: 'text', text: JSON.stringify(clients, null, 2), }, ], }; this.log(`webapp_list_clients result:`, JSON.stringify(result, null, 2)); return result; } // Handle server-side tools if (name === 'console_get_server_logs') { if (!this.getServerLogs) { return { content: [ { type: 'text', text: 'Server log capture not enabled. Set captureServerLogs: true in middleware config.', }, ], }; } const { level = 'all', limit = 100, regex } = args || {}; const logs = this.getServerLogs(level, limit, regex); return { content: [ { type: 'text', text: JSON.stringify({ logs }, null, 2), }, ], }; } // Check if this is a plugin tool for (const plugin of this.plugins) { const pluginTool = plugin.tools?.find(t => t.name === name); if (pluginTool) { this.log(`Executing plugin tool ${name} from plugin ${plugin.name}`); try { const context = { executeClientTool: this.executeTool || (async () => { throw new Error('Client tool execution not available'); }), getClients: this.getClients || (() => []), log: this.log.bind(this), }; const result = await pluginTool.handler(args || {}, context); this.log(`Plugin tool ${name} execution completed`); return { content: [ { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result, null, 2), }, ], }; } catch (error) { this.log(`Plugin tool ${name} execution failed:`, error); return { content: [ { type: 'text', text: `Error executing plugin tool: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } } } if (!this.executeTool) { this.log(`No tool execution function provided`); return { content: [ { type: 'text', text: 'Tool execution not available. Please check middleware configuration.', }, ], }; } try { this.log(`Executing tool ${name} via middleware`); const result = await this.executeTool(name, args); this.log(`Tool ${name} execution completed with result:`, JSON.stringify(result, null, 2)); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2), }, ], }; } catch (error) { this.log(`Tool ${name} execution failed:`, error); return { content: [ { type: 'text', text: `Error executing tool: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } }); return server; } async initialize() { try { this.log('Initializing SSE server...'); // Don't require WebSocket connection during initialization // The connection will be established when needed for tool execution this.log('SSE server initialized successfully (WebSocket connection deferred)'); } catch (error) { this.logError('Failed to initialize server:', error); throw error; } } async handleSSERequest(req, res) { this.log(`${req.method} ${req.path}`); try { if (req.method === 'GET') { // Create a new server instance for this connection const server = this.createServer(); // Create SSE transport for this request const sseTransport = new sse_js_1.SSEServerTransport('/mcp/sse', res); try { // Add timeout to prevent hanging const connectPromise = server.connect(sseTransport); const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new Error('Server connection timeout')), 10000); }); await Promise.race([connectPromise, timeoutPromise]); this.log(`Client connected: ${sseTransport.sessionId}`); } catch (connectError) { this.logError(`ERROR connecting server to transport:`, connectError); throw connectError; } // Store the transport with its session ID this.sseTransports.set(sseTransport.sessionId, sseTransport); // Handle connection close req.on('close', () => { this.sseTransports.delete(sseTransport.sessionId); this.log(`Client disconnected: ${sseTransport.sessionId}`); }); } else if (req.method === 'POST') { // Extract session ID from URL params const sessionId = req.query.sessionId; if (!sessionId) { return res.status(400).json({ error: 'Session ID required for POST requests' }); } const sseTransport = this.sseTransports.get(sessionId); if (!sseTransport) { return res.status(404).json({ error: 'SSE session not found' }); } // Handle incoming POST messages (don't consume the body here - let SSE transport handle it) await sseTransport.handlePostMessage(req, res); } } catch (error) { this.logError(`Failed to handle SSE request:`, error); if (!res.headersSent) { res.status(500).json({ error: 'Failed to establish SSE connection' }); } } } } exports.MCPSSEServer = MCPSSEServer; //# sourceMappingURL=mcp-sse-server.js.map