Skip to main content
Glama
index.ts7.97 kB
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { z } from "zod"; import { JSONRPCClient } from "json-rpc-2.0"; import * as net from "net"; // Check if debug mode is enabled const DEBUG = !!process.env.DEBUG; // Debug logging function function debug(message: string, ...args: any[]): void { if (DEBUG) { console.error(`[MCP-RPC-Bridge] ${message}`, ...args); } } // Parse environment variables const contextToken = process.env.CONTEXT_TOKEN; const toolsConfig = process.env.TOOLS; const transportMode = process.env.TRANSPORT_MODE || 'socket'; const pipeAddress = process.env.PIPE; const rpcApiUrl = process.env.RPC_API_URL; debug("Starting MCP RPC Bridge"); debug("Debug mode enabled"); debug("Transport mode:", transportMode); if (!contextToken) { throw new Error("CONTEXT_TOKEN environment variable is required"); } if (!toolsConfig) { throw new Error("TOOLS environment variable is required"); } if (transportMode === 'socket' && !pipeAddress) { throw new Error("PIPE environment variable is required for socket mode"); } if (transportMode === 'http' && !rpcApiUrl) { throw new Error("RPC_API_URL environment variable is required for HTTP mode"); } // Parse tools configuration let tools: Record<string, any>; try { tools = JSON.parse(toolsConfig); debug("Loaded tools configuration:", Object.keys(tools)); } catch (error) { throw new Error("TOOLS must be valid JSON"); } // Create RPC client based on transport mode let rpcClient: JSONRPCClient; if (transportMode === 'http') { // HTTP transport implementation debug("Initializing HTTP transport"); debug("RPC API URL:", rpcApiUrl); const send = async (data: string) => { try { const response = await fetch(rpcApiUrl!, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${contextToken}`, }, body: data, }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const responseText = await response.text(); debug("Received HTTP response:", responseText); // Parse and receive the response try { const jsonResponse = JSON.parse(responseText); rpcClient.receive(jsonResponse); } catch (error) { debug("Error parsing HTTP response:", error); } } catch (error) { debug("HTTP request error:", error); // Create an error response for the RPC client rpcClient.receive({ jsonrpc: "2.0", error: { code: -32603, message: "Internal error", data: error instanceof Error ? error.message : String(error), }, id: null, }); } }; rpcClient = new JSONRPCClient(send); debug("HTTP RPC client initialized"); } else { // Socket transport implementation (existing code) debug("Initializing socket transport"); debug("Pipe address:", pipeAddress); const socket = net.createConnection(pipeAddress!); socket.on("connect", () => { debug("Connected to parent app via pipe"); }); socket.on("error", (error) => { debug("Socket error:", error); process.exit(1); }); socket.on("close", () => { debug("Socket connection closed"); process.exit(1); }); const send = (data: string) => { if (socket.writable) { // Ensure data is properly stringified if it's somehow an object const stringData = typeof data === "string" ? data : JSON.stringify(data); debug("Sending RPC request:", stringData); socket.write(stringData + "\n"); } else { debug("Socket not writable"); } }; rpcClient = new JSONRPCClient(send); // Handle incoming RPC responses socket.on("data", (data) => { const lines = data .toString() .split("\n") .filter((line) => line.trim()); for (const line of lines) { try { const response = JSON.parse(line); debug("Received RPC response:", response); rpcClient.receive(response); } catch (error) { debug("Error parsing RPC response:", error); } } }); debug("Socket RPC client initialized"); } // Create an MCP server const server = new McpServer({ name: "rpc-bridge-server", version: "1.0.0", }); // Function to convert JSON Schema to Zod schema function jsonSchemaToZod(schema: any): z.ZodType<any> { if (schema.type === "object") { const shape: Record<string, z.ZodType<any>> = {}; if (schema.properties) { for (const [key, prop] of Object.entries(schema.properties)) { let zodType = jsonSchemaToZod(prop as any); // Add description if available if ((prop as any).description) { zodType = zodType.describe((prop as any).description); } // Check if this property is required if (!schema.required || !schema.required.includes(key)) { zodType = zodType.optional(); } shape[key] = zodType; } } return z.object(shape); } else if (schema.type === "string") { return z.string(); } else if (schema.type === "number") { return z.number(); } else if (schema.type === "boolean") { return z.boolean(); } else if (schema.type === "array") { return z.array(jsonSchemaToZod(schema.items)); } return z.any(); } // Function to convert JSON Schema to inputSchema format function createInputSchema(schema: any): Record<string, z.ZodType<any>> { const inputSchema: Record<string, z.ZodType<any>> = {}; if (schema.type === "object" && schema.properties) { for (const [key, prop] of Object.entries(schema.properties)) { let zodType = jsonSchemaToZod(prop as any); // Add description if available if ((prop as any).description) { zodType = zodType.describe((prop as any).description); } // Check if this property is required if (!schema.required || !schema.required.includes(key)) { zodType = zodType.optional(); } inputSchema[key] = zodType; } } return inputSchema; } // Dynamically register tools from configuration for (const [toolName, toolConfig] of Object.entries(tools)) { const inputSchema = createInputSchema(toolConfig.inputSchema); debug(`Registering tool: ${toolName} -> ${toolConfig.functionName}`); server.registerTool( toolName, { title: toolConfig.title, description: toolConfig.description, inputSchema: inputSchema, }, async (args) => { debug(`Tool called: ${toolName} with args:`, args); try { // Make RPC call to parent app const result = await rpcClient.request(toolConfig.functionName, [ contextToken, args, ]); debug(`Tool ${toolName} response:`, result); if (typeof result === "string") { return { content: [{ type: "text", text: result }] }; } else if (Array.isArray(result)) { return { content: result }; } else if (result && typeof result === "object" && "type" in result) { return { content: [result] }; } else { return { content: [{ type: "text", text: JSON.stringify(result) }] }; } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); debug(`Tool ${toolName} error:`, errorMessage); return { content: [ { type: "text", text: `Error calling ${toolConfig.functionName}: ${errorMessage}`, }, ], }; } } ); } // Start receiving messages on stdin and sending messages on stdout const transport = new StdioServerTransport(); debug("Starting MCP server with stdio transport"); await server.connect(transport); debug("MCP server connected and ready");

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/botanicastudios/mcp-host-rpc'

If you have feedback or need assistance with the MCP directory API, please join our Discord server