Skip to main content
Glama
host.js11.8 kB
/** * MCP Host RPC Server Module * * This module provides a simplified API for host applications to set up an RPC server * that can be used with MCP (Model Context Protocol) servers. It handles the creation * of Unix domain sockets, JSON-RPC server setup, JWT-based authentication with context * scoping, and provides elegant callback registration for RPC functions. */ import { JSONRPCServer } from "json-rpc-2.0"; import * as path from "path"; import * as os from "os"; import * as crypto from "crypto"; // @ts-ignore - jsonwebtoken types may not be available in all environments import jwt from "jsonwebtoken"; import { SocketTransport, HttpTransport } from "./transports.js"; /** * Check if an object is a Zod schema */ function isZodSchema(obj) { return (obj && typeof obj === "object" && obj._def && typeof obj._def.typeName === "string" && obj._def.typeName.startsWith("Zod")); } /** * Convert Zod schema to JSON Schema */ function zodToJsonSchema(zodSchema) { function getDescription(schema) { return schema._def.description || undefined; } function processSchema(schema) { const typeName = schema._def.typeName; switch (typeName) { case "ZodObject": { const properties = {}; const required = []; for (const [key, value] of Object.entries(schema.shape)) { properties[key] = zodToJsonSchema(value); if (!value.isOptional()) { required.push(key); } } return { type: "object", properties, required, additionalProperties: false, }; } case "ZodString": { const result = { type: "string" }; const desc = getDescription(schema); if (desc) result.description = desc; // Handle string constraints const checks = schema._def.checks || []; for (const check of checks) { if (check.kind === "min") result.minLength = check.value; if (check.kind === "max") result.maxLength = check.value; } return result; } case "ZodNumber": { const result = { type: "number" }; const desc = getDescription(schema); if (desc) result.description = desc; return result; } case "ZodBoolean": { const result = { type: "boolean" }; const desc = getDescription(schema); if (desc) result.description = desc; return result; } case "ZodArray": { const result = { type: "array", items: zodToJsonSchema(schema._def.type), }; const desc = getDescription(schema); if (desc) result.description = desc; return result; } case "ZodEnum": { const result = { type: "string", enum: schema._def.values, }; const desc = getDescription(schema); if (desc) result.description = desc; return result; } case "ZodOptional": { return zodToJsonSchema(schema._def.innerType); } case "ZodDefault": { const innerSchema = zodToJsonSchema(schema._def.innerType); innerSchema.default = schema._def.defaultValue(); return innerSchema; } case "ZodEffects": { // For .refine() and .transform(), use the inner schema return zodToJsonSchema(schema._def.schema); } default: console.warn(`[MCP-Host] Unsupported Zod type: ${typeName}, falling back to string`); return { type: "string" }; } } return processSchema(zodSchema); } export class McpHost { server; socketServer; secret; pipePath; debug; rpcHandlers = new Map(); toolsConfig = {}; isStarted = false; transport; transportMode; httpPath; httpUrl; constructor(options = {}) { this.server = new JSONRPCServer(); this.secret = options.secret || this.generateAuthToken(); this.debug = options.debug ?? false; this.transportMode = options.transport || 'socket'; this.httpPath = options.httpPath; this.httpUrl = options.httpUrl; // Always use Unix socket for socket transport, generate path if not provided const tempDir = os.tmpdir(); this.pipePath = options.pipePath || path.join(tempDir, `mcp-pipe-${Date.now()}.sock`); // Create appropriate transport if (this.transportMode === 'http') { if (!this.httpPath) { throw new Error('httpPath is required for HTTP transport'); } this.transport = new HttpTransport(this, this.httpPath, { debug: this.debug, httpUrl: this.httpUrl }); } else { this.transport = new SocketTransport(this, this.pipePath, { debug: this.debug }); } // Auto-start if requested if (options.start) { this.start().catch((error) => { this.log("Failed to auto-start server:", error); }); } } generateAuthToken() { return crypto.randomBytes(32).toString("hex"); } log(message, ...args) { if (this.debug) { console.log(`[MCP-Host] ${message}`, ...args); } } // Make server accessible to transports get rpcServer() { return this.server; } createJWT(context) { return jwt.sign({ context }, this.secret, { noTimestamp: true }); } verifyJWT(token) { try { const decoded = jwt.verify(token, this.secret); return decoded.context; } catch (error) { throw new Error(`Invalid JWT token: ${error instanceof Error ? error.message : String(error)}`); } } registerTool(toolName, properties, handler) { this.rpcHandlers.set(properties.functionName, handler); // Automatically convert Zod schema to JSON Schema if needed const processedProperties = { ...properties }; if (isZodSchema(properties.inputSchema)) { this.log(`Converting Zod schema to JSON Schema for tool: ${toolName}`); processedProperties.inputSchema = zodToJsonSchema(properties.inputSchema); } this.toolsConfig[toolName] = processedProperties; // Create a wrapper that verifies JWT and extracts context const wrappedHandler = async (...params) => { // JSON-RPC 2.0 wraps the client array parameters in another array const [contextToken, args] = params[0]; if (typeof contextToken !== "string") { throw new Error(`Expected JWT token as string, got ${typeof contextToken}`); } const context = this.verifyJWT(contextToken); // Actually await the handler result before returning try { const result = await handler(context, args); return result; } catch (error) { // Re-throw the error to let JSON-RPC handle it properly throw error; } }; this.server.addMethod(properties.functionName, wrappedHandler); this.log(`Registered tool: ${toolName} -> ${properties.functionName}`); } getMCPServerEnvVars(tools, context) { // Filter tools config to only include requested tools const filteredTools = {}; for (const toolName of tools) { if (this.toolsConfig[toolName]) { filteredTools[toolName] = this.toolsConfig[toolName]; } } const contextToken = this.createJWT(context); const baseVars = { CONTEXT_TOKEN: contextToken, TOOLS: JSON.stringify(filteredTools), TRANSPORT_MODE: this.transportMode, }; if (this.transportMode === 'http') { baseVars.RPC_API_URL = this.getHttpUrl(); } else { baseVars.PIPE = this.pipePath; } if (this.debug) { baseVars.DEBUG = '1'; } return baseVars; } getMCPServerConfig(name, tools, context, options) { // Input validation to prevent potential bugs if (!name || typeof name !== 'string') { throw new Error('Server name must be a non-empty string'); } const envVars = { ...this.getMCPServerEnvVars(tools, context) }; let command = "npx"; let args = ["-y", "@botanicastudios/mcp-host-rpc"]; if (options?.command) { if (Array.isArray(options.command)) { if (options.command.length > 0) { command = options.command[0]; args = options.command.slice(1); } } else { // Use string command as-is and reset args to empty command = options.command; args = []; } if (options?.args) { args = args.concat(options.args); } } // Add DEBUG env var if debug option is enabled if (options?.debug) { envVars.DEBUG = "1"; } // Build the configuration object with defensive structure const serverConfig = { type: "stdio", command, args, env: envVars, }; // Return the properly structured configuration // Using explicit object construction to prevent any accidental nesting const result = {}; result[name] = serverConfig; return result; } async start() { if (this.isStarted) { throw new Error("Server is already started"); } await this.transport.start(); this.isStarted = true; this.log("RPC server started with", this.transportMode, "transport"); this.log("Available tools:", Object.keys(this.toolsConfig)); return { secret: this.secret, pipePath: this.pipePath, toolsConfig: this.toolsConfig, }; } async stop() { if (!this.isStarted) { return; } await this.transport.stop(); this.isStarted = false; this.log("Server stopped"); } async handleHttpRequest(req, res) { if (this.transportMode !== 'http') { throw new Error('handleHttpRequest can only be used with HTTP transport'); } const httpTransport = this.transport; await httpTransport.handleRequest(req, res); } getHttpUrl() { if (this.transportMode !== 'http') { throw new Error('getHttpUrl can only be used with HTTP transport'); } const httpTransport = this.transport; return httpTransport.getHttpUrl(); } } // Convenience function to create a new MCP host export function createMcpHost(options) { return new McpHost(options); } //# sourceMappingURL=host.js.map

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