WebMCP

by jasonjmcghee
Verified
  • src
import {Server} from "@modelcontextprotocol/sdk/server/index.js"; import {StdioServerTransport} from '@modelcontextprotocol/sdk/server/stdio.js'; import WebSocket from 'ws'; import { CallToolRequestSchema, CreateMessageRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import {generateNewRegistrationToken} from "./tokens.js"; import {CONFIG} from "./config.js"; // Create a central MCP server that communicates over stdio const mcpServer = new Server( { name: "WebMCP", version: "0.1.12", }, { capabilities: { tools: { listChanged: true }, prompts: { listChanged: true }, resources: { listChanged: true, subscribe: true }, sampling: {} } } ); // WebSocket client connection let wsClient = null; // MCP specific channel path const MCP_PATH = '/mcp'; // Map to store pending requests from WebSocket to MCP const pendingRequests = new Map(); let requestIdCounter = 1; // Function to handle WebSocket messages async function handleWebSocketMessage(message) { try { const data = JSON.parse(message); console.error(`Received message: ${data.type}`); if (data.type === 'toolResponse') { // Handle tool response from WebSocket server const {id, result, error} = data; // Check if this is a response to a pending request if (pendingRequests.has(id)) { const {resolve, reject} = pendingRequests.get(id); pendingRequests.delete(id); if (error) { reject(new Error(error)); } else { resolve(result); } } else { console.error(`No pending request found for ID: ${id}`); } } else if (data.type === 'promptResponse') { // Handle prompt response from WebSocket server const {id, result, error} = data; // Check if this is a response to a pending request if (pendingRequests.has(id)) { const {resolve, reject} = pendingRequests.get(id); pendingRequests.delete(id); if (error) { reject(new Error(error)); } else { resolve(result); } } else { console.error(`No pending request found for ID: ${id}`); } } else if (data.type === 'resourceResponse') { // Handle resource response from WebSocket server const {id, result, error} = data; // Check if this is a response to a pending request if (pendingRequests.has(id)) { const {resolve, reject} = pendingRequests.get(id); pendingRequests.delete(id); if (error) { reject(new Error(error)); } else { resolve(result); } } else { console.error(`No pending request found for ID: ${id}`); } } else if (data.type === 'samplingResponse') { // Handle sampling response from WebSocket server const {id, result, error} = data; // Check if this is a response to a pending request if (pendingRequests.has(id)) { const {resolve, reject} = pendingRequests.get(id); pendingRequests.delete(id); if (error) { reject(new Error(error)); } else { resolve(result); } } else { console.error(`No pending request found for ID: ${id}`); } } else if (data.type === 'listToolsResponse') { // Handle list tools response from WebSocket server const {id, tools, error} = data; // Check if this is a response to a pending request if (pendingRequests.has(id)) { const {resolve, reject} = pendingRequests.get(id); pendingRequests.delete(id); if (error) { reject(new Error(error)); } else { resolve(tools); } } else { console.error(`No pending request found for ID: ${id}`); } } else if (data.type === 'listPromptsResponse') { // Handle list prompts response from WebSocket server const {id, prompts, error} = data; // Check if this is a response to a pending request if (pendingRequests.has(id)) { const {resolve, reject} = pendingRequests.get(id); pendingRequests.delete(id); if (error) { reject(new Error(error)); } else { resolve(prompts); } } else { console.error(`No pending request found for ID: ${id}`); } } else if (data.type === 'listResourcesResponse') { // Handle list resources response from WebSocket server const {id, resources, resourceTemplates, error} = data; // Check if this is a response to a pending request if (pendingRequests.has(id)) { const {resolve, reject} = pendingRequests.get(id); pendingRequests.delete(id); if (error) { reject(new Error(error)); } else { resolve({resources, resourceTemplates}); } } else { console.error(`No pending request found for ID: ${id}`); } } else if (data.type === 'toolRegistered') { await mcpServer.sendToolListChanged(); } else if (data.type === 'resourceRegistered') { await mcpServer.sendResourceListChanged(); } else if (data.type === 'promptRegistered') { await mcpServer.sendPromptListChanged(); } else if (data.type === 'welcome') { // Welcome message from the server, we're already connected to the MCP path console.error(`Connected to path: ${data.channel}`); } else if (data.type === 'pong') { // Pong response console.error(`Received pong with timestamp: ${data.timestamp}`); } else if (data.type === 'error') { // Error message console.error(`Received error: ${data.message}`); } } catch (error) { console.error('Error processing WebSocket message:', error); } } // Function to connect to the WebSocket server function connectToWebSocketServer(serverToken) { // Connect to the MCP path directly with server token const serverUrl = `ws://localhost:${CONFIG.port}${MCP_PATH}?token=${serverToken}`; console.error(`Connecting to WebSocket server at ${MCP_PATH} with authentication...`); wsClient = new WebSocket(serverUrl); // Handle connection opening wsClient.on('open', () => { console.error(`Connected to WebSocket server on path: ${MCP_PATH}`); }); // Handle incoming messages wsClient.on('message', (message) => { handleWebSocketMessage(message); }); // Handle connection closing wsClient.on('close', (code, reason) => { console.error(`WebSocket connection closed: ${code} ${reason}`); wsClient = null; // Try to reconnect after a delay setTimeout(connectToWebSocketServer, 5000); }); // Handle connection errors wsClient.on('error', (error) => { console.error('WebSocket connection error:', error); }); } // Function to send a message to the WebSocket server function sendMessage(message) { if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { console.error('Cannot send message: WebSocket not connected'); return Promise.reject(new Error('WebSocket not connected')); } try { wsClient.send(JSON.stringify(message)); return Promise.resolve(); } catch (error) { console.error('Error sending message:', error); return Promise.reject(error); } } // Set up the MCP server to handle tool calls by sending them to the WebSocket server mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "_webmcp_get-token") { return { content: [{ type: "text", text: `Paste this into the token input on the WebMCP website ${await generateNewRegistrationToken()}`, }] }; } if (request.params.name === "_webmcp_define-mcp-tool") { return { content: [{ type: "text", text: "Instruct the user to view the result from the tool call. Do not say anything else.", }] } } if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { return { content: [{ type: "text", text: "Not connected to WebSocket server" }], isError: true }; } // Create a unique request ID const requestId = (requestIdCounter++).toString(); // Create a promise that will be resolved when we get a response const responsePromise = new Promise((resolve, reject) => { // Store the resolver functions pendingRequests.set(requestId, {resolve, reject}); // Set a timeout to prevent hanging requests setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId); reject(new Error(`Tool call timed out: ${request.params.name}`)); } }, 30000); // 30 second timeout }); // Send the request to the WebSocket server try { await sendMessage({ id: requestId, type: 'callTool', tool: request.params.name, arguments: request.params.arguments }); // Wait for the response return await responsePromise; } catch (error) { return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true }; } }); // Set up the MCP server to handle list tools by querying the WebSocket server mcpServer.setRequestHandler(ListToolsRequestSchema, async () => { const builtInTools = [ { name: "_webmcp_get-token", description: "Retrieve a token to connect to a website for WebMCP. A user might say 'register a token' or 'add a webmcp'", inputSchema: { type: "object", properties: {}, }, }, { name: "_webmcp_define-mcp-tool", description: "Used to define an 'mcp tool'. Only use this if the user specifically asks for an mcp tool. " + "A webmcp token is not required for this.", inputSchema: { type: "object", description: "The schema which describes the tool.", properties: { name: { type: "string", description: "The name of the tool" }, description: { type: "string", description: "Provides a clear and concise description of the tool and what it is used for." }, inputSchema: { type: "object", description: "The inputSchema required or optional for the tool.", properties: { type: { type: "string", enum: ["object", "array", "string", "number", "boolean", "enum"], description: "The type of the parameter being defined." }, properties: { type: "object", description: "The properties of the parameter if it's an object type.", additionalProperties: { type: "object", properties: { type: { type: "string", description: "The data type of the property.", enum: ["object", "array", "string", "number", "boolean", "enum"] }, description: { type: "string", description: "A brief description of the property." }, enum: { type: "array", description: "A list of allowed values for the property.", items: { type: "string" } } }, required: ["type", "description"] } }, required: { type: "array", items: { type: "string" } } }, required: ["type", "description", "properties"] } }, required: ["name", "description", "inputSchema"] }, } ]; if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { return {tools: builtInTools}; } // Create a unique request ID const requestId = (requestIdCounter++).toString(); // Create a promise that will be resolved when we get a response const responsePromise = new Promise((resolve, reject) => { // Store the resolver functions pendingRequests.set(requestId, {resolve, reject}); // Set a timeout to prevent hanging requests setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId); reject(new Error('List tools request timed out')); } }, 10000); // 10 second timeout }); // Send the request to the WebSocket server try { await sendMessage({ id: requestId, type: 'listTools' }); const tools = await responsePromise; // Wait for the response return { tools: [...tools, ...builtInTools] }; } catch (error) { console.error('Error listing tools:', error); return {tools: []}; // Return empty list on error } }); // Set up the MCP server to handle list prompts by querying the WebSocket server mcpServer.setRequestHandler(ListPromptsRequestSchema, async () => { const builtInPrompts = []; if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { return { prompts: builtInPrompts }; } // Create a unique request ID const requestId = (requestIdCounter++).toString(); // Create a promise that will be resolved when we get a response const responsePromise = new Promise((resolve, reject) => { // Store the resolver functions pendingRequests.set(requestId, {resolve, reject}); // Set a timeout to prevent hanging requests setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId); reject(new Error('List prompts request timed out')); } }, 10000); // 10 second timeout }); // Send the request to the WebSocket server try { await sendMessage({ id: requestId, type: 'listPrompts' }); const prompts = await responsePromise; // Wait for the response return { prompts: [ ...prompts, ...builtInPrompts ], }; } catch (error) { console.error('Error listing prompts:', error); return {prompts: []}; // Return empty list on error } }); // Set up the MCP server to handle get prompt by querying the WebSocket server mcpServer.setRequestHandler(GetPromptRequestSchema, async (request) => { if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { throw new Error("Not connected to WebSocket server"); } // Create a unique request ID const requestId = (requestIdCounter++).toString(); // Create a promise that will be resolved when we get a response const responsePromise = new Promise((resolve, reject) => { // Store the resolver functions pendingRequests.set(requestId, {resolve, reject}); // Set a timeout to prevent hanging requests setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId); reject(new Error(`Get prompt request timed out: ${request.params.name}`)); } }, 30000); // 30 second timeout }); // Send the request to the WebSocket server try { await sendMessage({ id: requestId, type: 'getPrompt', name: request.params.name, arguments: request.params.arguments }); // Wait for the response return await responsePromise; } catch (error) { console.error('Error getting prompt:', error); throw error; } }); // Set up the MCP server to handle list resources by querying the WebSocket server mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => { if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { return {resources: []}; } // Create a unique request ID const requestId = (requestIdCounter++).toString(); // Create a promise that will be resolved when we get a response const responsePromise = new Promise((resolve, reject) => { // Store the resolver functions pendingRequests.set(requestId, {resolve, reject}); // Set a timeout to prevent hanging requests setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId); reject(new Error('List resources request timed out')); } }, 10000); // 10 second timeout }); // Send the request to the WebSocket server try { await sendMessage({ id: requestId, type: 'listResources' }); const {resources} = await responsePromise; // Wait for the response return {resources}; } catch (error) { console.error('Error listing resources:', error); return {resources: []}; // Return empty list on error } }); // Set up the MCP server to handle list resource templates by querying the WebSocket server mcpServer.setRequestHandler(ListResourceTemplatesRequestSchema, async () => { if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { return {resourceTemplates: []}; } // Create a unique request ID const requestId = (requestIdCounter++).toString(); // Create a promise that will be resolved when we get a response const responsePromise = new Promise((resolve, reject) => { // Store the resolver functions pendingRequests.set(requestId, {resolve, reject}); // Set a timeout to prevent hanging requests setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId); reject(new Error('List resource templates request timed out')); } }, 10000); // 10 second timeout }); // Send the request to the WebSocket server try { await sendMessage({ id: requestId, type: 'listResources' }); const {resourceTemplates} = await responsePromise; // Wait for the response return {resourceTemplates}; } catch (error) { console.error('Error listing resource templates:', error); return {resourceTemplates: []}; // Return empty list on error } }); // Set up the MCP server to handle read resource by querying the WebSocket server mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => { if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { throw new Error("Not connected to WebSocket server"); } // Create a unique request ID const requestId = (requestIdCounter++).toString(); // Create a promise that will be resolved when we get a response const responsePromise = new Promise((resolve, reject) => { // Store the resolver functions pendingRequests.set(requestId, {resolve, reject}); // Set a timeout to prevent hanging requests setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId); reject(new Error(`Read resource request timed out: ${request.params.uri}`)); } }, 30000); // 30 second timeout }); // Send the request to the WebSocket server try { await sendMessage({ id: requestId, type: 'readResource', uri: request.params.uri }); // Wait for the response return await responsePromise; } catch (error) { console.error('Error reading resource:', error); throw error; } }); // Set up the MCP server to handle sampling by querying the WebSocket server mcpServer.setRequestHandler(CreateMessageRequestSchema, async (request) => { if (!wsClient || wsClient.readyState !== WebSocket.OPEN) { throw new Error("Not connected to WebSocket server"); } // Create a unique request ID const requestId = (requestIdCounter++).toString(); // Create a promise that will be resolved when we get a response const responsePromise = new Promise((resolve, reject) => { // Store the resolver functions pendingRequests.set(requestId, {resolve, reject}); // Set a timeout to prevent hanging requests setTimeout(() => { if (pendingRequests.has(requestId)) { pendingRequests.delete(requestId); reject(new Error(`Sampling request timed out`)); } }, 120000); // 120 second timeout (sampling can take longer) }); // Send the request to the WebSocket server with all parameters from the request try { await sendMessage({ id: requestId, type: 'createSamplingMessage', messages: request.params.messages, systemPrompt: request.params.systemPrompt, includeContext: request.params.includeContext, temperature: request.params.temperature, maxTokens: request.params.maxTokens, stopSequences: request.params.stopSequences, metadata: request.params.metadata, modelPreferences: request.params.modelPreferences }); // Wait for the response return await responsePromise; } catch (error) { console.error('Error creating sampling message:', error); throw error; } }); async function runMcpServer(serverToken) { // Connect to the WebSocket server connectToWebSocketServer(serverToken); const transport = new StdioServerTransport(); await mcpServer.connect(transport); console.error("MCP server running with stdio transport"); } export { runMcpServer };