UNPKG

askexperts

Version:

AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol

202 lines 8.32 kB
import express from "express"; import cors from "cors"; import { randomUUID } from "node:crypto"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; import { AskExpertsMCP } from "./AskExpertsMCP.js"; import { AskExpertsSmartMCP } from "./AskExpertsSmartMCP.js"; import { debugMCP, debugError } from "../common/debug.js"; /** * AskExpertsHttp class that implements an MCP server with HTTP transport */ export class AskExpertsHttp { /** * Creates a new AskExpertsHttp instance * * @param options - Configuration options */ constructor(options) { this.transports = {}; this.servers = {}; this.stopped = true; if (options.type === "smart" && !(options.openaiBaseUrl)) throw new Error("OpenAI base URL required for smart server"); this.port = options.port; this.basePath = options.basePath ? options.basePath.startsWith("/") ? options.basePath : `/${options.basePath}` : "/"; this.type = options.type; this.discoveryRelays = options.discoveryRelays; this.openaiBaseUrl = options.openaiBaseUrl; this.openaiApiKey = options.openaiApiKey; // Create Express app this.app = express(); this.app.use(express.json()); // Enable CORS this.app.use(cors()); // Set up routes this.setupRoutes(); } /** * Set up the Express routes */ setupRoutes() { // Handle POST requests for client-to-server communication this.app.post(`${this.basePath}mcp`, async (req, res) => { if (this.stopped) { res.status(503).send("Service not available"); return; } // Check for existing session ID const sessionId = req.headers["mcp-session-id"]; let transport; if (sessionId && this.transports[sessionId]) { // Reuse existing transport transport = this.transports[sessionId]; } else if (!sessionId && isInitializeRequest(req.body)) { // Extract NWC string from Authorization header const authHeader = req.headers.authorization; if (!authHeader || !authHeader.startsWith("Bearer ")) { res.status(401).json({ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized: Missing or invalid Authorization header", }, id: null, }); return; } const nwcString = authHeader.substring(7); // Remove 'Bearer ' prefix // Create new transport transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), onsessioninitialized: (sessionId) => { // Store the transport by session ID this.transports[sessionId] = transport; }, // Note: DNS rebinding protection is not available in this version of the SDK }); // Clean up transport when closed transport.onclose = async () => { if (transport.sessionId) { // Clean up server if it exists const server = this.servers[transport.sessionId]; if (server) { try { await server.close(); } catch (error) { debugError("Error closing server:", error); } } delete this.servers[transport.sessionId]; delete this.transports[transport.sessionId]; } }; // Create the appropriate MCP server based on type let server; if (this.type === "smart") { // Create Smart MCP server server = new AskExpertsSmartMCP(nwcString, this.openaiApiKey, this.openaiBaseUrl, this.discoveryRelays); } else { // Create regular MCP server server = new AskExpertsMCP(nwcString, this.discoveryRelays); } // Connect to the MCP server await server.connect(transport); // Store the server instance if (transport.sessionId) { this.servers[transport.sessionId] = server; } } else { // Invalid request res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "Bad Request: No valid session ID provided", }, id: null, }); return; } // Handle the request await transport.handleRequest(req, res, req.body); }); // Reusable handler for GET and DELETE requests const handleSessionRequest = async (req, res) => { if (this.stopped) { res.status(503).send("Service not available"); return; } const sessionId = req.headers["mcp-session-id"]; if (!sessionId || !this.transports[sessionId]) { res.status(400).send("Invalid or missing session ID"); return; } const transport = this.transports[sessionId]; await transport.handleRequest(req, res); }; // Handle GET requests for server-to-client notifications via SSE this.app.get(`${this.basePath}mcp`, handleSessionRequest); // Handle DELETE requests for session termination this.app.delete(`${this.basePath}mcp`, handleSessionRequest); } /** * Start the HTTP server */ start() { if (this.server) throw new Error("Already started"); this.stopped = false; this.server = this.app.listen(this.port); debugMCP(`AskExperts HTTP server (${this.type}) started on port ${this.port}`); debugMCP(`Base URL: ${this.basePath}`); } /** * Stop the HTTP server and clean up resources */ stop() { return new Promise(async (resolve) => { // Ensure it's marked as stopped this.stopped = false; // Didn't really start? if (!this.server) { resolve(); return; } debugError("AskExperts HTTP server stopping..."); // Stop accepting new connections const closePromise = new Promise((ok) => this.server.close(ok)); // FIXME: if clients have active requests, we // risk losing their money by cutting them off, // ideally we would stop accepting new requests, // and wait for a while until existing requests are over, // and start terminating only after that. // Close all existing MCP transports await Promise.allSettled(Object.values(this.transports).map(transport => transport.close())); // Wait until all connections are closed with timeout await Promise.race([ closePromise, new Promise((ok) => setTimeout(ok, 5000)), ]); // Cleanup the MCP servers Object.values(this.servers).forEach((server) => { server[Symbol.dispose](); }); // Clear the rest this.servers = {}; this.transports = {}; this.server = undefined; debugError("AskExperts HTTP server stopped"); resolve(); }); } } //# sourceMappingURL=AskExpertsHttp.js.map