UNPKG

mcp-chrome-bridge

Version:

Chrome Native-Messaging host (Node)

241 lines 9.35 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NativeMessagingHost = void 0; const process_1 = require("process"); const uuid_1 = require("uuid"); const chrome_mcp_shared_1 = require("chrome-mcp-shared"); const constant_1 = require("./constant"); class NativeMessagingHost { constructor() { this.associatedServer = null; this.pendingRequests = new Map(); } setServer(serverInstance) { this.associatedServer = serverInstance; } // add message handler to wait for start server start() { try { this.setupMessageHandling(); } catch (error) { process.exit(1); } } setupMessageHandling() { let buffer = Buffer.alloc(0); let expectedLength = -1; process_1.stdin.on('readable', () => { let chunk; while ((chunk = process_1.stdin.read()) !== null) { buffer = Buffer.concat([buffer, chunk]); if (expectedLength === -1 && buffer.length >= 4) { expectedLength = buffer.readUInt32LE(0); buffer = buffer.slice(4); } if (expectedLength !== -1 && buffer.length >= expectedLength) { const messageBuffer = buffer.slice(0, expectedLength); buffer = buffer.slice(expectedLength); try { const message = JSON.parse(messageBuffer.toString()); this.handleMessage(message); } catch (error) { this.sendError(`Failed to parse message: ${error.message}`); } expectedLength = -1; // reset to get next data } } }); process_1.stdin.on('end', () => { this.cleanup(); }); process_1.stdin.on('error', () => { this.cleanup(); }); } async handleMessage(message) { var _a; if (!message || typeof message !== 'object') { this.sendError('Invalid message format'); return; } if (message.responseToRequestId) { const requestId = message.responseToRequestId; const pending = this.pendingRequests.get(requestId); if (pending) { clearTimeout(pending.timeoutId); if (message.error) { pending.reject(new Error(message.error)); } else { pending.resolve(message.payload); } this.pendingRequests.delete(requestId); } else { // just ignore } return; } // Handle directive messages from Chrome try { switch (message.type) { case chrome_mcp_shared_1.NativeMessageType.START: await this.startServer(((_a = message.payload) === null || _a === void 0 ? void 0 : _a.port) || 3000); break; case chrome_mcp_shared_1.NativeMessageType.STOP: await this.stopServer(); break; // Keep ping/pong for simple liveness detection, but this differs from request-response pattern case 'ping_from_extension': this.sendMessage({ type: 'pong_to_extension' }); break; default: // Double check when message type is not supported if (!message.responseToRequestId) { this.sendError(`Unknown message type or non-response message: ${message.type || 'no type'}`); } } } catch (error) { this.sendError(`Failed to handle directive message: ${error.message}`); } } /** * Send request to Chrome and wait for response * @param messagePayload Data to send to Chrome * @param timeoutMs Timeout for waiting response (milliseconds) * @returns Promise, resolves to Chrome's returned payload on success, rejects on failure */ sendRequestToExtensionAndWait(messagePayload, messageType = 'request_data', timeoutMs = constant_1.TIMEOUTS.DEFAULT_REQUEST_TIMEOUT) { return new Promise((resolve, reject) => { const requestId = (0, uuid_1.v4)(); // Generate unique request ID const timeoutId = setTimeout(() => { this.pendingRequests.delete(requestId); // Remove from Map after timeout reject(new Error(`Request timed out after ${timeoutMs}ms`)); }, timeoutMs); // Store request's resolve/reject functions and timeout ID this.pendingRequests.set(requestId, { resolve, reject, timeoutId }); // Send message with requestId to Chrome this.sendMessage({ type: messageType, // Define a request type, e.g. 'request_data' payload: messagePayload, requestId: requestId, // <--- Key: include request ID }); }); } /** * Start Fastify server (now accepts Server instance) */ async startServer(port) { if (!this.associatedServer) { this.sendError('Internal error: server instance not set'); return; } try { if (this.associatedServer.isRunning) { this.sendMessage({ type: chrome_mcp_shared_1.NativeMessageType.ERROR, payload: { message: 'Server is already running' }, }); return; } await this.associatedServer.start(port, this); this.sendMessage({ type: chrome_mcp_shared_1.NativeMessageType.SERVER_STARTED, payload: { port }, }); } catch (error) { this.sendError(`Failed to start server: ${error.message}`); } } /** * Stop Fastify server */ async stopServer() { if (!this.associatedServer) { this.sendError('Internal error: server instance not set'); return; } try { // Check status through associatedServer if (!this.associatedServer.isRunning) { this.sendMessage({ type: chrome_mcp_shared_1.NativeMessageType.ERROR, payload: { message: 'Server is not running' }, }); return; } await this.associatedServer.stop(); // this.serverStarted = false; // Server should update its own status after successful stop this.sendMessage({ type: chrome_mcp_shared_1.NativeMessageType.SERVER_STOPPED }); // Distinguish from previous 'stopped' } catch (error) { this.sendError(`Failed to stop server: ${error.message}`); } } /** * Send message to Chrome extension */ sendMessage(message) { try { const messageString = JSON.stringify(message); const messageBuffer = Buffer.from(messageString); const headerBuffer = Buffer.alloc(4); headerBuffer.writeUInt32LE(messageBuffer.length, 0); // Ensure atomic write process_1.stdout.write(Buffer.concat([headerBuffer, messageBuffer]), (err) => { if (err) { // Consider how to handle write failure, may affect request completion } else { // Message sent successfully, no action needed } }); } catch (error) { // Catch JSON.stringify or Buffer operation errors // If preparation stage fails, associated request may never be sent // Need to consider whether to reject corresponding Promise (if called within sendRequestToExtensionAndWait) } } /** * Send error message to Chrome extension (mainly for sending non-request-response type errors) */ sendError(errorMessage) { this.sendMessage({ type: chrome_mcp_shared_1.NativeMessageType.ERROR_FROM_NATIVE_HOST, // Use more explicit type payload: { message: errorMessage }, }); } /** * Clean up resources */ cleanup() { // Reject all pending requests this.pendingRequests.forEach((pending) => { clearTimeout(pending.timeoutId); pending.reject(new Error('Native host is shutting down or Chrome disconnected.')); }); this.pendingRequests.clear(); if (this.associatedServer && this.associatedServer.isRunning) { this.associatedServer .stop() .then(() => { process.exit(0); }) .catch(() => { process.exit(1); }); } else { process.exit(0); } } } exports.NativeMessagingHost = NativeMessagingHost; const nativeMessagingHostInstance = new NativeMessagingHost(); exports.default = nativeMessagingHostInstance; //# sourceMappingURL=native-messaging-host.js.map