@mcpjam/inspector 0.3.4 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Database utilities for MCPJam Inspector
3
+ * Basic utilities for local SQLite database setup
4
+ */
5
+ import { mkdir, access } from "fs/promises";
6
+ import { dirname } from "path";
7
+ import { homedir } from "os";
8
+ import { join } from "path";
9
+ /**
10
+ * Ensures the .mcpjam directory exists in the user's home directory
11
+ */
12
+ export async function ensureMCPJamDirectory() {
13
+ const mcpjamDir = join(homedir(), ".mcpjam");
14
+ try {
15
+ await access(mcpjamDir);
16
+ }
17
+ catch {
18
+ // Directory doesn't exist, create it
19
+ await mkdir(mcpjamDir, { recursive: true });
20
+ console.log(`📁 Created MCPJam directory: ${mcpjamDir}`);
21
+ }
22
+ return mcpjamDir;
23
+ }
24
+ /**
25
+ * Ensures the directory for a given file path exists
26
+ */
27
+ export async function ensureDirectoryExists(filePath) {
28
+ const dir = dirname(filePath);
29
+ try {
30
+ await access(dir);
31
+ }
32
+ catch {
33
+ await mkdir(dir, { recursive: true });
34
+ }
35
+ }
36
+ /**
37
+ * Gets the resolved database path - THE SINGLE SOURCE OF TRUTH
38
+ * Priority: MCPJAM_DB_PATH env var > default ~/.mcpjam/data.db
39
+ */
40
+ export function getResolvedDatabasePath() {
41
+ return process.env.MCPJAM_DB_PATH || join(homedir(), ".mcpjam", "data.db");
42
+ }
43
+ /**
44
+ * Environment configuration helper
45
+ */
46
+ export function getDatabaseConfig() {
47
+ return {
48
+ localPath: getResolvedDatabasePath(),
49
+ };
50
+ }
51
+ /**
52
+ * Checks if the database file exists
53
+ */
54
+ export async function databaseExists(databasePath) {
55
+ try {
56
+ await access(databasePath);
57
+ return true;
58
+ }
59
+ catch {
60
+ return false;
61
+ }
62
+ }
63
+ /**
64
+ * Database connection test
65
+ */
66
+ export async function testDatabaseConnection(config) {
67
+ try {
68
+ const { createClient } = await import("@libsql/client");
69
+ // Use the single source of truth for database path
70
+ const dbPath = config?.localPath || getResolvedDatabasePath();
71
+ await ensureDirectoryExists(dbPath);
72
+ const client = createClient({
73
+ url: `file:${dbPath}`,
74
+ });
75
+ // Test the connection with a simple query
76
+ await client.execute("SELECT 1");
77
+ client.close();
78
+ return { success: true };
79
+ }
80
+ catch (error) {
81
+ return {
82
+ success: false,
83
+ error: error instanceof Error ? error.message : "Unknown error",
84
+ };
85
+ }
86
+ }
@@ -3,15 +3,14 @@ import cors from "cors";
3
3
  import { parseArgs } from "node:util";
4
4
  import { parse as shellParseArgs } from "shell-quote";
5
5
  import { createServer } from "node:net";
6
- import { SSEClientTransport, SseError, } from "@modelcontextprotocol/sdk/client/sse.js";
7
- import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/sdk/client/stdio.js";
8
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
9
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
- import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
6
+ import { SseError } from "@modelcontextprotocol/sdk/client/sse.js";
7
+ import { getDefaultEnvironment } from "@modelcontextprotocol/sdk/client/stdio.js";
11
8
  import express from "express";
12
- import { findActualExecutable } from "spawn-rx";
13
- import mcpProxy from "./mcpProxy.js";
14
9
  import { randomUUID } from "node:crypto";
10
+ import { DatabaseManager } from "./database/DatabaseManager.js";
11
+ import { createDatabaseRoutes } from "./database/routes.js";
12
+ import { getDatabaseConfig } from "./database/utils.js";
13
+ import { MCPProxyService, ConsoleLogger } from "./shared/index.js";
15
14
  const SSE_HEADERS_PASSTHROUGH = ["authorization"];
16
15
  const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
17
16
  "authorization",
@@ -22,6 +21,9 @@ const defaultEnvironment = {
22
21
  ...getDefaultEnvironment(),
23
22
  ...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
24
23
  };
24
+ const serverConfigs = process.env.MCP_SERVER_CONFIGS
25
+ ? JSON.parse(process.env.MCP_SERVER_CONFIGS)
26
+ : null;
25
27
  const { values } = parseArgs({
26
28
  args: process.argv.slice(2),
27
29
  options: {
@@ -35,79 +37,86 @@ app.use((req, res, next) => {
35
37
  res.header("Access-Control-Expose-Headers", "mcp-session-id");
36
38
  next();
37
39
  });
38
- const webAppTransports = new Map(); // Transports by sessionId
39
- const backingServerTransports = new Map();
40
- const createTransport = async (req) => {
40
+ // Initialize database
41
+ let databaseManager = null;
42
+ const initializeDatabase = async () => {
43
+ try {
44
+ const dbConfig = getDatabaseConfig();
45
+ databaseManager = new DatabaseManager(dbConfig);
46
+ await databaseManager.initialize();
47
+ // Add database routes after successful initialization
48
+ app.use("/api/db", express.json({ limit: "10mb" }), // JSON middleware only for database routes
49
+ (req, res, next) => {
50
+ if (!databaseManager) {
51
+ res.status(503).json({
52
+ success: false,
53
+ error: "Database not available",
54
+ });
55
+ return;
56
+ }
57
+ next();
58
+ }, createDatabaseRoutes(databaseManager));
59
+ console.log("✅ Database API routes registered");
60
+ }
61
+ catch (error) {
62
+ console.error("❌ Failed to initialize database:", error);
63
+ // Don't exit the process - continue without database functionality
64
+ }
65
+ };
66
+ // Initialize MCPProxyService
67
+ const mcpProxyService = new MCPProxyService({
68
+ logger: new ConsoleLogger(),
69
+ maxConnections: 50,
70
+ });
71
+ // Helper function to convert request query to ServerConfig
72
+ const createServerConfigFromRequest = (req) => {
41
73
  const query = req.query;
42
74
  const transportType = query.transportType;
75
+ const config = {
76
+ id: randomUUID(),
77
+ type: transportType,
78
+ name: `server-${Date.now()}`,
79
+ };
43
80
  if (transportType === "stdio") {
44
- const command = query.command;
45
- const origArgs = shellParseArgs(query.args);
46
- const queryEnv = query.env ? JSON.parse(query.env) : {};
47
- const env = { ...process.env, ...defaultEnvironment, ...queryEnv };
48
- const { cmd, args } = findActualExecutable(command, origArgs);
49
- console.log(`🚀 Stdio transport: command=${cmd}, args=${args}`);
50
- const transport = new StdioClientTransport({
51
- command: cmd,
52
- args,
53
- env,
54
- stderr: "pipe",
55
- });
56
- await transport.start();
57
- return transport;
58
- }
59
- else if (transportType === "sse") {
60
- const url = query.url;
61
- const headers = {
62
- Accept: "text/event-stream",
63
- };
64
- for (const key of SSE_HEADERS_PASSTHROUGH) {
65
- if (req.headers[key] === undefined) {
66
- continue;
81
+ config.command = query.command;
82
+ config.args = query.args
83
+ ? shellParseArgs(query.args)
84
+ : undefined;
85
+ // Safely parse env - only if it's a valid JSON string
86
+ if (query.env) {
87
+ try {
88
+ config.env = JSON.parse(query.env);
89
+ }
90
+ catch (error) {
91
+ console.warn(`Failed to parse env as JSON: ${query.env}, using empty object`);
92
+ config.env = {};
67
93
  }
68
- const value = req.headers[key];
69
- headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
70
94
  }
71
- const transport = new SSEClientTransport(new URL(url), {
72
- eventSourceInit: {
73
- fetch: (url, init) => fetch(url, { ...init, headers }),
74
- },
75
- requestInit: {
76
- headers,
77
- },
78
- });
79
- await transport.start();
80
- return transport;
81
95
  }
82
- else if (transportType === "streamable-http") {
83
- const headers = {
84
- Accept: "text/event-stream, application/json",
85
- };
86
- for (const key of STREAMABLE_HTTP_HEADERS_PASSTHROUGH) {
87
- if (req.headers[key] === undefined) {
88
- continue;
89
- }
90
- const value = req.headers[key];
96
+ else if (transportType === "sse" || transportType === "streamable-http") {
97
+ config.url = query.url;
98
+ }
99
+ return config;
100
+ };
101
+ // Helper function to extract headers for transport
102
+ const extractRequestHeaders = (req) => {
103
+ const headers = {};
104
+ const headersToPass = req.query.transportType === "sse"
105
+ ? SSE_HEADERS_PASSTHROUGH
106
+ : STREAMABLE_HTTP_HEADERS_PASSTHROUGH;
107
+ for (const key of headersToPass) {
108
+ const value = req.headers[key];
109
+ if (value !== undefined) {
91
110
  headers[key] = Array.isArray(value) ? value[value.length - 1] : value;
92
111
  }
93
- const transport = new StreamableHTTPClientTransport(new URL(query.url), {
94
- requestInit: {
95
- headers,
96
- },
97
- });
98
- await transport.start();
99
- return transport;
100
- }
101
- else {
102
- console.error(`❌ Invalid transport type: ${transportType}`);
103
- throw new Error("Invalid transport type specified");
104
112
  }
113
+ return headers;
105
114
  };
106
115
  app.get("/mcp", async (req, res) => {
107
116
  const sessionId = req.headers["mcp-session-id"];
108
117
  console.log(`📥 Received GET message for sessionId ${sessionId}`);
109
118
  try {
110
- const transport = webAppTransports.get(sessionId);
119
+ const transport = mcpProxyService.getWebAppTransport(sessionId);
111
120
  if (!transport) {
112
121
  res.status(404).end("Session not found");
113
122
  return;
@@ -127,9 +136,14 @@ app.post("/mcp", async (req, res) => {
127
136
  if (!sessionId) {
128
137
  try {
129
138
  console.log("🔄 New streamable-http connection");
130
- let backingServerTransport;
139
+ // Create server config and headers from request
140
+ const serverConfig = createServerConfigFromRequest(req);
141
+ const requestHeaders = extractRequestHeaders(req);
131
142
  try {
132
- backingServerTransport = await createTransport(req);
143
+ // Use MCPProxyService to create the streamable HTTP connection
144
+ const { sessionId: newSessionId, webAppTransport } = await mcpProxyService.createStreamableHTTPConnection(serverConfig, requestHeaders);
145
+ console.log(`✨ Connected MCP client to backing server transport for session ${newSessionId}`);
146
+ await webAppTransport.handleRequest(req, res, req.body);
133
147
  }
134
148
  catch (error) {
135
149
  if (error instanceof SseError && error.code === 401) {
@@ -139,26 +153,6 @@ app.post("/mcp", async (req, res) => {
139
153
  }
140
154
  throw error;
141
155
  }
142
- const webAppTransport = new StreamableHTTPServerTransport({
143
- sessionIdGenerator: randomUUID,
144
- onsessioninitialized: (newSessionId) => {
145
- console.log("✨ Created streamable web app transport " + newSessionId);
146
- webAppTransports.set(newSessionId, webAppTransport);
147
- backingServerTransports.set(newSessionId, backingServerTransport);
148
- console.log(`✨ Connected MCP client to backing server transport for session ${newSessionId}`);
149
- mcpProxy({
150
- transportToClient: webAppTransport,
151
- transportToServer: backingServerTransport,
152
- });
153
- webAppTransport.onclose = () => {
154
- console.log(`🧹 Cleaning up transports for session ${newSessionId}`);
155
- webAppTransports.delete(newSessionId);
156
- backingServerTransports.delete(newSessionId);
157
- };
158
- },
159
- });
160
- await webAppTransport.start();
161
- await webAppTransport.handleRequest(req, res, req.body);
162
156
  }
163
157
  catch (error) {
164
158
  console.error("❌ Error in /mcp POST route:", error);
@@ -167,7 +161,7 @@ app.post("/mcp", async (req, res) => {
167
161
  }
168
162
  else {
169
163
  try {
170
- const transport = webAppTransports.get(sessionId);
164
+ const transport = mcpProxyService.getWebAppTransport(sessionId);
171
165
  if (!transport) {
172
166
  res.status(404).end("Transport not found for sessionId " + sessionId);
173
167
  }
@@ -184,33 +178,12 @@ app.post("/mcp", async (req, res) => {
184
178
  app.get("/stdio", async (req, res) => {
185
179
  try {
186
180
  console.log("🔄 New stdio/sse connection");
187
- const webAppTransport = new SSEServerTransport("/message", res);
188
- const sessionId = webAppTransport.sessionId;
189
- webAppTransports.set(sessionId, webAppTransport);
181
+ // Create server config and headers from request
182
+ const serverConfig = createServerConfigFromRequest(req);
183
+ const requestHeaders = extractRequestHeaders(req);
190
184
  try {
191
- const backingServerTransport = await createTransport(req);
192
- backingServerTransports.set(sessionId, backingServerTransport);
193
- webAppTransport.onclose = () => {
194
- console.log(`🧹 Cleaning up transports for session ${sessionId}`);
195
- webAppTransports.delete(sessionId);
196
- backingServerTransports.delete(sessionId);
197
- };
198
- await webAppTransport.start();
199
- if (backingServerTransport instanceof StdioClientTransport) {
200
- backingServerTransport.stderr.on("data", (chunk) => {
201
- webAppTransport.send({
202
- jsonrpc: "2.0",
203
- method: "stderr",
204
- params: {
205
- data: chunk.toString(),
206
- },
207
- });
208
- });
209
- }
210
- mcpProxy({
211
- transportToClient: webAppTransport,
212
- transportToServer: backingServerTransport,
213
- });
185
+ // Use MCPProxyService to create the SSE connection (handles STDIO stderr automatically)
186
+ const { sessionId } = await mcpProxyService.createSSEConnection(serverConfig, res, requestHeaders);
214
187
  console.log(`✨ Connected MCP client to backing server transport for session ${sessionId}`);
215
188
  }
216
189
  catch (error) {
@@ -230,22 +203,12 @@ app.get("/stdio", async (req, res) => {
230
203
  app.get("/sse", async (req, res) => {
231
204
  try {
232
205
  console.log("🔄 New sse connection");
233
- const webAppTransport = new SSEServerTransport("/message", res);
234
- const sessionId = webAppTransport.sessionId;
235
- webAppTransports.set(sessionId, webAppTransport);
206
+ // Create server config and headers from request
207
+ const serverConfig = createServerConfigFromRequest(req);
208
+ const requestHeaders = extractRequestHeaders(req);
236
209
  try {
237
- const backingServerTransport = await createTransport(req);
238
- backingServerTransports.set(sessionId, backingServerTransport);
239
- webAppTransport.onclose = () => {
240
- console.log(`🧹 Cleaning up transports for session ${sessionId}`);
241
- webAppTransports.delete(sessionId);
242
- backingServerTransports.delete(sessionId);
243
- };
244
- await webAppTransport.start();
245
- mcpProxy({
246
- transportToClient: webAppTransport,
247
- transportToServer: backingServerTransport,
248
- });
210
+ // Use MCPProxyService to create the SSE connection
211
+ const { sessionId } = await mcpProxyService.createSSEConnection(serverConfig, res, requestHeaders);
249
212
  console.log(`✨ Connected MCP client to backing server transport for session ${sessionId}`);
250
213
  }
251
214
  catch (error) {
@@ -266,7 +229,7 @@ app.post("/message", async (req, res) => {
266
229
  try {
267
230
  const sessionId = req.query.sessionId;
268
231
  console.log(`📥 Received message for sessionId ${sessionId}`);
269
- const transport = webAppTransports.get(sessionId);
232
+ const transport = mcpProxyService.getWebAppTransport(sessionId);
270
233
  if (!transport) {
271
234
  res.status(404).end("Session not found");
272
235
  return;
@@ -289,6 +252,7 @@ app.get("/config", (req, res) => {
289
252
  defaultEnvironment,
290
253
  defaultCommand: values.env,
291
254
  defaultArgs: values.args,
255
+ serverConfigs,
292
256
  });
293
257
  }
294
258
  catch (error) {
@@ -296,6 +260,7 @@ app.get("/config", (req, res) => {
296
260
  res.status(500).json(error);
297
261
  }
298
262
  });
263
+ // Database API routes - will be added after database initialization
299
264
  // Function to find an available port
300
265
  const findAvailablePort = async (startPort) => {
301
266
  return new Promise((resolve, reject) => {
@@ -331,6 +296,8 @@ app.get("/port", (req, res) => {
331
296
  // Start server with dynamic port finding
332
297
  const startServer = async () => {
333
298
  try {
299
+ // Initialize database first
300
+ await initializeDatabase();
334
301
  const availablePort = await findAvailablePort(Number(PORT));
335
302
  actualPort = availablePort;
336
303
  const server = app.listen(availablePort);
@@ -344,6 +311,17 @@ const startServer = async () => {
344
311
  console.error(`❌ Server error: ${err.message}`);
345
312
  process.exit(1);
346
313
  });
314
+ // Graceful shutdown
315
+ process.on("SIGINT", async () => {
316
+ console.log("\n🔄 Shutting down server...");
317
+ server.close();
318
+ await mcpProxyService.closeAllConnections();
319
+ if (databaseManager) {
320
+ await databaseManager.close();
321
+ console.log("✅ Database connection closed");
322
+ }
323
+ process.exit(0);
324
+ });
347
325
  }
348
326
  catch (error) {
349
327
  console.error(`❌ Failed to start server: ${error}`);
@@ -0,0 +1,221 @@
1
+ import { EventEmitter } from "events";
2
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
+ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
4
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
5
+ import { TransportFactory } from "./TransportFactory.js";
6
+ import { generateSessionId, ConsoleLogger } from "./utils.js";
7
+ import mcpProxy from "../mcpProxy.js";
8
+ export class MCPProxyService extends EventEmitter {
9
+ webAppTransports = new Map();
10
+ backingServerTransports = new Map();
11
+ connectionStatus = new Map();
12
+ cleanupInProgress = new Set();
13
+ transportFactory;
14
+ logger;
15
+ maxConnections;
16
+ constructor(options = {}) {
17
+ super();
18
+ this.logger = options.logger || new ConsoleLogger();
19
+ this.maxConnections = options.maxConnections || 50;
20
+ this.transportFactory = new TransportFactory({
21
+ logger: this.logger,
22
+ });
23
+ }
24
+ async createConnection(serverConfig, requestHeaders) {
25
+ if (this.backingServerTransports.size >= this.maxConnections) {
26
+ throw new Error(`Maximum connections reached (${this.maxConnections})`);
27
+ }
28
+ const sessionId = generateSessionId();
29
+ try {
30
+ this.logger.info(`Creating connection ${sessionId} for ${serverConfig.name}`);
31
+ // Update status to connecting
32
+ this.connectionStatus.set(sessionId, {
33
+ id: sessionId,
34
+ status: "connecting",
35
+ lastActivity: new Date(),
36
+ errorCount: 0,
37
+ });
38
+ // Create transport
39
+ const transport = await this.transportFactory.createTransport(serverConfig, requestHeaders);
40
+ // Store transport
41
+ this.backingServerTransports.set(sessionId, transport);
42
+ // Set up transport event handlers
43
+ this.setupTransportEvents(sessionId, transport);
44
+ // Update status to connected
45
+ this.updateConnectionStatus(sessionId, "connected");
46
+ this.emit("connection", sessionId, serverConfig);
47
+ return sessionId;
48
+ }
49
+ catch (error) {
50
+ this.updateConnectionStatus(sessionId, "error");
51
+ this.logger.error(`Failed to create connection ${sessionId}:`, error);
52
+ throw error;
53
+ }
54
+ }
55
+ getActiveConnections() {
56
+ return Array.from(this.backingServerTransports.keys());
57
+ }
58
+ getConnectionStatus(sessionId) {
59
+ return this.connectionStatus.get(sessionId);
60
+ }
61
+ getAllConnectionStatuses() {
62
+ return Array.from(this.connectionStatus.values());
63
+ }
64
+ async sendMessage(sessionId, message) {
65
+ const transport = this.backingServerTransports.get(sessionId);
66
+ if (!transport) {
67
+ throw new Error(`No transport found for session: ${sessionId}`);
68
+ }
69
+ try {
70
+ this.updateConnectionStatus(sessionId, "connected");
71
+ await transport.send(message);
72
+ }
73
+ catch (error) {
74
+ this.incrementErrorCount(sessionId);
75
+ this.logger.error(`Message failed for session ${sessionId}:`, error);
76
+ throw error;
77
+ }
78
+ }
79
+ getTransport(sessionId) {
80
+ return this.backingServerTransports.get(sessionId);
81
+ }
82
+ getWebAppTransport(sessionId) {
83
+ return this.webAppTransports.get(sessionId);
84
+ }
85
+ setWebAppTransport(sessionId, transport) {
86
+ this.webAppTransports.set(sessionId, transport);
87
+ this.logger.info(`Web app transport set for session ${sessionId}`);
88
+ }
89
+ removeWebAppTransport(sessionId) {
90
+ this.webAppTransports.delete(sessionId);
91
+ this.logger.info(`Web app transport removed for session ${sessionId}`);
92
+ }
93
+ async closeConnection(sessionId) {
94
+ // Prevent duplicate cleanup calls
95
+ if (this.cleanupInProgress.has(sessionId)) {
96
+ return;
97
+ }
98
+ this.cleanupInProgress.add(sessionId);
99
+ try {
100
+ const transport = this.backingServerTransports.get(sessionId);
101
+ if (transport) {
102
+ try {
103
+ await transport.close();
104
+ }
105
+ catch (error) {
106
+ this.logger.error(`Error closing connection ${sessionId}:`, error);
107
+ }
108
+ }
109
+ this.backingServerTransports.delete(sessionId);
110
+ this.webAppTransports.delete(sessionId);
111
+ this.connectionStatus.delete(sessionId);
112
+ this.emit("disconnection", sessionId);
113
+ this.logger.info(`🧹 Cleaning up transports for session ${sessionId}`);
114
+ }
115
+ finally {
116
+ this.cleanupInProgress.delete(sessionId);
117
+ }
118
+ }
119
+ async closeAllConnections() {
120
+ const closePromises = Array.from(this.backingServerTransports.keys()).map((sessionId) => this.closeConnection(sessionId));
121
+ await Promise.all(closePromises);
122
+ this.logger.info(`All connections closed (${closePromises.length} total)`);
123
+ }
124
+ updateConnectionStatus(sessionId, status) {
125
+ const current = this.connectionStatus.get(sessionId);
126
+ if (current) {
127
+ current.status = status;
128
+ current.lastActivity = new Date();
129
+ this.connectionStatus.set(sessionId, current);
130
+ }
131
+ }
132
+ incrementErrorCount(sessionId) {
133
+ const current = this.connectionStatus.get(sessionId);
134
+ if (current) {
135
+ current.errorCount += 1;
136
+ this.connectionStatus.set(sessionId, current);
137
+ }
138
+ }
139
+ setupTransportEvents(sessionId, transport) {
140
+ // Store original handlers to preserve existing functionality
141
+ const originalOnClose = transport.onclose;
142
+ const originalOnError = transport.onerror;
143
+ transport.onclose = () => {
144
+ this.logger.info(`Transport closed for session ${sessionId}`);
145
+ this.updateConnectionStatus(sessionId, "disconnected");
146
+ this.emit("disconnection", sessionId);
147
+ // Call original handler if it exists
148
+ if (originalOnClose) {
149
+ originalOnClose();
150
+ }
151
+ };
152
+ transport.onerror = (error) => {
153
+ this.logger.error(`Transport error for session ${sessionId}:`, error);
154
+ this.updateConnectionStatus(sessionId, "error");
155
+ this.incrementErrorCount(sessionId);
156
+ this.emit("error", sessionId, error);
157
+ // Call original handler if it exists
158
+ if (originalOnError) {
159
+ originalOnError(error);
160
+ }
161
+ };
162
+ }
163
+ // Helper methods for StreamableHTTP transport handling
164
+ async createStreamableHTTPConnection(serverConfig, requestHeaders) {
165
+ const sessionId = await this.createConnection(serverConfig, requestHeaders);
166
+ const webAppTransport = new StreamableHTTPServerTransport({
167
+ sessionIdGenerator: () => sessionId,
168
+ onsessioninitialized: (newSessionId) => {
169
+ this.logger.info(`✨ Created streamable web app transport ${newSessionId}`);
170
+ this.setWebAppTransport(newSessionId, webAppTransport);
171
+ // Set up proxy between web app transport and backing server transport
172
+ const backingTransport = this.getTransport(newSessionId);
173
+ if (backingTransport) {
174
+ mcpProxy({
175
+ transportToClient: webAppTransport,
176
+ transportToServer: backingTransport,
177
+ });
178
+ }
179
+ // Set up cleanup handler
180
+ webAppTransport.onclose = () => {
181
+ this.closeConnection(newSessionId);
182
+ };
183
+ },
184
+ });
185
+ await webAppTransport.start();
186
+ return { sessionId, webAppTransport };
187
+ }
188
+ // Helper method for SSE transport handling
189
+ async createSSEConnection(serverConfig, res, requestHeaders) {
190
+ const connectionId = await this.createConnection(serverConfig, requestHeaders);
191
+ const webAppTransport = new SSEServerTransport("/message", res);
192
+ const sessionId = webAppTransport.sessionId;
193
+ this.setWebAppTransport(sessionId, webAppTransport);
194
+ // Set up cleanup handler
195
+ webAppTransport.onclose = () => {
196
+ this.closeConnection(connectionId);
197
+ };
198
+ await webAppTransport.start();
199
+ // Set up proxy between web app transport and backing server transport
200
+ const backingTransport = this.getTransport(connectionId);
201
+ if (backingTransport) {
202
+ // Special handling for STDIO stderr
203
+ if (backingTransport instanceof StdioClientTransport) {
204
+ backingTransport.stderr?.on("data", (chunk) => {
205
+ webAppTransport.send({
206
+ jsonrpc: "2.0",
207
+ method: "stderr",
208
+ params: {
209
+ data: chunk.toString(),
210
+ },
211
+ });
212
+ });
213
+ }
214
+ mcpProxy({
215
+ transportToClient: webAppTransport,
216
+ transportToServer: backingTransport,
217
+ });
218
+ }
219
+ return { sessionId, webAppTransport };
220
+ }
221
+ }