@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.
- package/README.md +1 -0
- package/cli/build/cli.js +57 -23
- package/client/bin/client.js +23 -9
- package/client/bin/start.js +6 -11
- package/client/dist/assets/{OAuthCallback-59IQqASq.js → OAuthCallback-Bgz6lAmx.js} +2 -1
- package/client/dist/assets/{OAuthDebugCallback-Bm010sVQ.js → OAuthDebugCallback-Gd08QO37.js} +1 -1
- package/client/dist/assets/{index-B_8Xm9gw.js → index-Bgrnc5s2.js} +1727 -1151
- package/client/dist/assets/{index-BT47S2Qb.css → index-CWDemo1t.css} +85 -100
- package/client/dist/index.html +3 -3
- package/package.json +2 -1
- package/server/build/database/DatabaseManager.js +108 -0
- package/server/build/database/index.js +8 -0
- package/server/build/database/routes.js +86 -0
- package/server/build/database/types.js +27 -0
- package/server/build/database/utils.js +86 -0
- package/server/build/index.js +109 -131
- package/server/build/shared/MCPProxyService.js +221 -0
- package/server/build/shared/TransportFactory.js +130 -0
- package/server/build/shared/index.js +4 -0
- package/server/build/shared/types.js +1 -0
- package/server/build/shared/utils.js +27 -0
- package/server/build/test-server.js +145 -0
- package/server/build/testing/HealthCheck.js +42 -0
- package/server/build/testing/TestExecutor.js +240 -0
- package/server/build/testing/TestRunner.js +198 -0
- package/server/build/testing/TestServer.js +440 -0
- package/server/build/testing/types.js +1 -0
|
@@ -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
|
+
}
|
package/server/build/index.js
CHANGED
|
@@ -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 {
|
|
7
|
-
import {
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 =
|
|
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
|
-
|
|
139
|
+
// Create server config and headers from request
|
|
140
|
+
const serverConfig = createServerConfigFromRequest(req);
|
|
141
|
+
const requestHeaders = extractRequestHeaders(req);
|
|
131
142
|
try {
|
|
132
|
-
|
|
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 =
|
|
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
|
-
|
|
188
|
-
const
|
|
189
|
-
|
|
181
|
+
// Create server config and headers from request
|
|
182
|
+
const serverConfig = createServerConfigFromRequest(req);
|
|
183
|
+
const requestHeaders = extractRequestHeaders(req);
|
|
190
184
|
try {
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
206
|
+
// Create server config and headers from request
|
|
207
|
+
const serverConfig = createServerConfigFromRequest(req);
|
|
208
|
+
const requestHeaders = extractRequestHeaders(req);
|
|
236
209
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
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 =
|
|
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
|
+
}
|