@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,130 @@
|
|
|
1
|
+
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
2
|
+
import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { findActualExecutable } from "spawn-rx";
|
|
5
|
+
import { validateServerConfig, ConsoleLogger } from "./utils.js";
|
|
6
|
+
const SSE_HEADERS_PASSTHROUGH = ["authorization"];
|
|
7
|
+
const STREAMABLE_HTTP_HEADERS_PASSTHROUGH = [
|
|
8
|
+
"authorization",
|
|
9
|
+
"mcp-session-id",
|
|
10
|
+
"last-event-id",
|
|
11
|
+
];
|
|
12
|
+
export class TransportFactory {
|
|
13
|
+
logger;
|
|
14
|
+
defaultEnvironment;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.logger = options.logger || new ConsoleLogger();
|
|
17
|
+
this.defaultEnvironment = {
|
|
18
|
+
...getDefaultEnvironment(),
|
|
19
|
+
...(process.env.MCP_ENV_VARS ? JSON.parse(process.env.MCP_ENV_VARS) : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
async createTransport(config, requestHeaders) {
|
|
23
|
+
validateServerConfig(config);
|
|
24
|
+
this.logger.info(`Creating ${config.type} transport for ${config.name}`);
|
|
25
|
+
try {
|
|
26
|
+
switch (config.type) {
|
|
27
|
+
case "stdio":
|
|
28
|
+
return await this.createStdioTransport(config);
|
|
29
|
+
case "sse":
|
|
30
|
+
return await this.createSSETransport(config, requestHeaders);
|
|
31
|
+
case "streamable-http":
|
|
32
|
+
return await this.createStreamableHTTPTransport(config, requestHeaders);
|
|
33
|
+
default:
|
|
34
|
+
throw new Error(`Unsupported transport type: ${config.type}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
this.logger.error(`Failed to create transport for ${config.name}:`, error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async createStdioTransport(config) {
|
|
43
|
+
const command = config.command;
|
|
44
|
+
const origArgs = config.args || [];
|
|
45
|
+
const queryEnv = config.env || {};
|
|
46
|
+
// Filter out undefined values from process.env
|
|
47
|
+
const processEnv = Object.fromEntries(Object.entries(process.env).filter(([, value]) => value !== undefined));
|
|
48
|
+
const env = { ...processEnv, ...this.defaultEnvironment, ...queryEnv };
|
|
49
|
+
const { cmd, args } = findActualExecutable(command, origArgs);
|
|
50
|
+
this.logger.info(`🚀 Stdio transport: command=${cmd}, args=${args}`);
|
|
51
|
+
const transport = new StdioClientTransport({
|
|
52
|
+
command: cmd,
|
|
53
|
+
args,
|
|
54
|
+
env,
|
|
55
|
+
stderr: "pipe",
|
|
56
|
+
});
|
|
57
|
+
await this.setupTransportLifecycle(transport, config.id);
|
|
58
|
+
await transport.start();
|
|
59
|
+
return transport;
|
|
60
|
+
}
|
|
61
|
+
async createSSETransport(config, requestHeaders) {
|
|
62
|
+
const url = config.url;
|
|
63
|
+
const headers = {
|
|
64
|
+
Accept: "text/event-stream",
|
|
65
|
+
...config.headers,
|
|
66
|
+
};
|
|
67
|
+
// Add headers passed through from the request
|
|
68
|
+
if (requestHeaders) {
|
|
69
|
+
for (const key of SSE_HEADERS_PASSTHROUGH) {
|
|
70
|
+
if (requestHeaders[key] !== undefined) {
|
|
71
|
+
headers[key] = requestHeaders[key];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
this.logger.info(`🚀 SSE transport: url=${url}`);
|
|
76
|
+
const transport = new SSEClientTransport(new URL(url), {
|
|
77
|
+
eventSourceInit: {
|
|
78
|
+
fetch: (url, init) => fetch(url, { ...init, headers }),
|
|
79
|
+
},
|
|
80
|
+
requestInit: {
|
|
81
|
+
headers,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
await this.setupTransportLifecycle(transport, config.id);
|
|
85
|
+
await transport.start();
|
|
86
|
+
return transport;
|
|
87
|
+
}
|
|
88
|
+
async createStreamableHTTPTransport(config, requestHeaders) {
|
|
89
|
+
const url = config.url;
|
|
90
|
+
const headers = {
|
|
91
|
+
Accept: "text/event-stream, application/json",
|
|
92
|
+
...config.headers,
|
|
93
|
+
};
|
|
94
|
+
// Add headers passed through from the request
|
|
95
|
+
if (requestHeaders) {
|
|
96
|
+
for (const key of STREAMABLE_HTTP_HEADERS_PASSTHROUGH) {
|
|
97
|
+
if (requestHeaders[key] !== undefined) {
|
|
98
|
+
headers[key] = requestHeaders[key];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.logger.info(`🚀 StreamableHTTP transport: url=${url}`);
|
|
103
|
+
const transport = new StreamableHTTPClientTransport(new URL(url), {
|
|
104
|
+
requestInit: {
|
|
105
|
+
headers,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
await this.setupTransportLifecycle(transport, config.id);
|
|
109
|
+
await transport.start();
|
|
110
|
+
return transport;
|
|
111
|
+
}
|
|
112
|
+
async setupTransportLifecycle(transport, configId) {
|
|
113
|
+
// Set up event handlers without aggressive timeouts
|
|
114
|
+
// The original server didn't have connection timeouts, so we preserve that behavior
|
|
115
|
+
const originalOnClose = transport.onclose;
|
|
116
|
+
transport.onclose = () => {
|
|
117
|
+
this.logger.info(`Transport closed for ${configId}`);
|
|
118
|
+
if (originalOnClose) {
|
|
119
|
+
originalOnClose();
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const originalOnError = transport.onerror;
|
|
123
|
+
transport.onerror = (error) => {
|
|
124
|
+
this.logger.error(`Transport error for ${configId}:`, error);
|
|
125
|
+
if (originalOnError) {
|
|
126
|
+
originalOnError(error);
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
export function generateSessionId() {
|
|
3
|
+
return randomUUID();
|
|
4
|
+
}
|
|
5
|
+
export function validateServerConfig(config) {
|
|
6
|
+
if (!config.id || !config.type || !config.name) {
|
|
7
|
+
throw new Error("Invalid server configuration: id, type, and name are required");
|
|
8
|
+
}
|
|
9
|
+
if (config.type === "stdio" && !config.command) {
|
|
10
|
+
throw new Error("STDIO transport requires command");
|
|
11
|
+
}
|
|
12
|
+
if ((config.type === "sse" || config.type === "streamable-http") &&
|
|
13
|
+
!config.url) {
|
|
14
|
+
throw new Error("SSE and StreamableHTTP transports require URL");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class ConsoleLogger {
|
|
18
|
+
info(message, ...args) {
|
|
19
|
+
console.log(`[INFO] ${message}`, ...args);
|
|
20
|
+
}
|
|
21
|
+
error(message, ...args) {
|
|
22
|
+
console.error(`[ERROR] ${message}`, ...args);
|
|
23
|
+
}
|
|
24
|
+
warn(message, ...args) {
|
|
25
|
+
console.warn(`[WARN] ${message}`, ...args);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { TestServer } from "./testing/TestServer.js";
|
|
3
|
+
import { MCPProxyService } from "./shared/MCPProxyService.js";
|
|
4
|
+
import { DatabaseManager } from "./database/DatabaseManager.js";
|
|
5
|
+
import { ConsoleLogger } from "./shared/utils.js";
|
|
6
|
+
import { getDatabaseConfig } from "./database/utils.js";
|
|
7
|
+
import { createServer } from "node:net";
|
|
8
|
+
import { parseArgs } from "node:util";
|
|
9
|
+
// Function to find an available port
|
|
10
|
+
const findAvailablePort = async (startPort) => {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const server = createServer();
|
|
13
|
+
server.listen(startPort, () => {
|
|
14
|
+
const port = server.address()?.port;
|
|
15
|
+
server.close(() => {
|
|
16
|
+
resolve(port);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
server.on("error", (err) => {
|
|
20
|
+
if (err.code === "EADDRINUSE") {
|
|
21
|
+
// Port is in use, try the next one
|
|
22
|
+
findAvailablePort(startPort + 1)
|
|
23
|
+
.then(resolve)
|
|
24
|
+
.catch(reject);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
reject(err);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
async function startTestServer() {
|
|
33
|
+
const logger = new ConsoleLogger();
|
|
34
|
+
// Parse command line arguments
|
|
35
|
+
const { values } = parseArgs({
|
|
36
|
+
args: process.argv.slice(2),
|
|
37
|
+
options: {
|
|
38
|
+
port: { type: "string", short: "p", default: "3002" },
|
|
39
|
+
host: { type: "string", short: "h", default: "localhost" },
|
|
40
|
+
env: { type: "string", short: "e", default: "development" },
|
|
41
|
+
config: { type: "string", short: "c" },
|
|
42
|
+
help: { type: "boolean", default: false },
|
|
43
|
+
},
|
|
44
|
+
allowPositionals: true,
|
|
45
|
+
});
|
|
46
|
+
if (values.help) {
|
|
47
|
+
console.log(`
|
|
48
|
+
🧪 MCPJam Inspector Test Server
|
|
49
|
+
|
|
50
|
+
Usage: node test-server.js [options]
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
-p, --port <port> Port to run the test server on (default: 3002)
|
|
54
|
+
-h, --host <host> Host to bind the server to (default: localhost)
|
|
55
|
+
-e, --env <env> Environment mode (default: development)
|
|
56
|
+
-c, --config <file> Configuration file path
|
|
57
|
+
--help Show this help message
|
|
58
|
+
|
|
59
|
+
Environment Variables:
|
|
60
|
+
TEST_PORT Override the default port
|
|
61
|
+
TEST_HOST Override the default host
|
|
62
|
+
NODE_ENV Set the environment mode
|
|
63
|
+
DATABASE_URL Database connection string
|
|
64
|
+
LOG_LEVEL Logging level (debug, info, warn, error)
|
|
65
|
+
|
|
66
|
+
Examples:
|
|
67
|
+
node test-server.js --port 4000 --host 0.0.0.0
|
|
68
|
+
TEST_PORT=5000 node test-server.js
|
|
69
|
+
NODE_ENV=production node test-server.js
|
|
70
|
+
`);
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
// Initialize core services
|
|
75
|
+
const mcpProxyService = new MCPProxyService({
|
|
76
|
+
logger,
|
|
77
|
+
maxConnections: 100, // Higher limit for test server
|
|
78
|
+
});
|
|
79
|
+
const dbConfig = getDatabaseConfig();
|
|
80
|
+
const database = new DatabaseManager(dbConfig);
|
|
81
|
+
await database.initialize();
|
|
82
|
+
// Determine port with fallback logic
|
|
83
|
+
const preferredPort = process.env.TEST_PORT
|
|
84
|
+
? parseInt(process.env.TEST_PORT)
|
|
85
|
+
: parseInt(values.port);
|
|
86
|
+
const actualPort = await findAvailablePort(preferredPort);
|
|
87
|
+
// Create and start test server
|
|
88
|
+
const testServer = new TestServer({
|
|
89
|
+
port: actualPort,
|
|
90
|
+
host: process.env.TEST_HOST || values.host,
|
|
91
|
+
cors: values.env !== "production",
|
|
92
|
+
rateLimiting: true,
|
|
93
|
+
database: {
|
|
94
|
+
url: process.env.DATABASE_URL || "sqlite://test.db",
|
|
95
|
+
maxConnections: 10,
|
|
96
|
+
timeout: 5000,
|
|
97
|
+
},
|
|
98
|
+
logging: {
|
|
99
|
+
level: process.env.LOG_LEVEL || "info",
|
|
100
|
+
format: "json",
|
|
101
|
+
outputs: ["console"],
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
await testServer.start(mcpProxyService, database);
|
|
105
|
+
if (actualPort !== preferredPort) {
|
|
106
|
+
logger.info(`⚠️ Port ${preferredPort} was in use, using available port ${actualPort} instead`);
|
|
107
|
+
}
|
|
108
|
+
logger.info(`🧪 Test server started on http://${testServer.config.host}:${testServer.config.port}`);
|
|
109
|
+
logger.info(`📊 Health check available at http://${testServer.config.host}:${testServer.config.port}/api/test/health`);
|
|
110
|
+
logger.info(`🔍 Status endpoint available at http://${testServer.config.host}:${testServer.config.port}/api/test/status`);
|
|
111
|
+
logger.info(`🎯 Test execution endpoint available at http://${testServer.config.host}:${testServer.config.port}/api/test/run`);
|
|
112
|
+
// Graceful shutdown
|
|
113
|
+
const shutdown = async (signal) => {
|
|
114
|
+
logger.info(`🔄 Received ${signal}, shutting down test server...`);
|
|
115
|
+
try {
|
|
116
|
+
await testServer.stop();
|
|
117
|
+
await mcpProxyService.closeAllConnections();
|
|
118
|
+
await database.close();
|
|
119
|
+
logger.info("✅ Test server shutdown complete");
|
|
120
|
+
process.exit(0);
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
logger.error("❌ Error during shutdown:", error);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
128
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
129
|
+
// Handle unhandled promise rejections
|
|
130
|
+
process.on("unhandledRejection", (reason, promise) => {
|
|
131
|
+
logger.error("❌ Unhandled Rejection at:", promise, "reason:", reason);
|
|
132
|
+
});
|
|
133
|
+
process.on("uncaughtException", (error) => {
|
|
134
|
+
logger.error("❌ Uncaught Exception:", error);
|
|
135
|
+
process.exit(1);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
logger.error("❌ Failed to start test server:", error);
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
144
|
+
startTestServer();
|
|
145
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export class HealthCheck {
|
|
2
|
+
mcpProxyService;
|
|
3
|
+
database;
|
|
4
|
+
logger;
|
|
5
|
+
constructor(mcpProxyService, database, logger) {
|
|
6
|
+
this.mcpProxyService = mcpProxyService;
|
|
7
|
+
this.database = database;
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
getStatus() {
|
|
11
|
+
return {
|
|
12
|
+
testing: true,
|
|
13
|
+
status: "healthy",
|
|
14
|
+
version: process.env.npm_package_version || "0.3.5",
|
|
15
|
+
timestamp: new Date().toISOString(),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
getDetailedStatus() {
|
|
19
|
+
return {
|
|
20
|
+
testing: true,
|
|
21
|
+
server: {
|
|
22
|
+
status: "running",
|
|
23
|
+
uptime: process.uptime(),
|
|
24
|
+
memory: process.memoryUsage(),
|
|
25
|
+
version: process.env.npm_package_version || "0.3.5",
|
|
26
|
+
},
|
|
27
|
+
connections: {
|
|
28
|
+
active: this.mcpProxyService.getActiveConnections().length,
|
|
29
|
+
list: this.mcpProxyService.getActiveConnections(),
|
|
30
|
+
},
|
|
31
|
+
database: {
|
|
32
|
+
connected: this.isDatabaseConnected(),
|
|
33
|
+
status: this.isDatabaseConnected() ? "connected" : "disconnected",
|
|
34
|
+
},
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
isDatabaseConnected() {
|
|
39
|
+
// Simple check - in a real implementation you might want to ping the database
|
|
40
|
+
return this.database !== null && this.database !== undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// server/src/testing/TestExecutor.ts
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
export class TestExecutor {
|
|
4
|
+
mcpProxyService;
|
|
5
|
+
logger;
|
|
6
|
+
constructor(mcpProxyService, logger) {
|
|
7
|
+
this.mcpProxyService = mcpProxyService;
|
|
8
|
+
this.logger = logger;
|
|
9
|
+
}
|
|
10
|
+
async executeTest(testCase) {
|
|
11
|
+
const startTime = Date.now();
|
|
12
|
+
const resultId = randomUUID();
|
|
13
|
+
this.logger.info(`🧪 Starting test execution: ${testCase.name} (${testCase.id})`);
|
|
14
|
+
try {
|
|
15
|
+
// Validate test case
|
|
16
|
+
this.validateTestCase(testCase);
|
|
17
|
+
// Initialize connections to MCP servers
|
|
18
|
+
const connections = await this.initializeConnections(testCase);
|
|
19
|
+
// Execute the test
|
|
20
|
+
const toolCalls = await this.executeTestLogic(testCase, connections);
|
|
21
|
+
// Clean up connections
|
|
22
|
+
await this.cleanupConnections(connections);
|
|
23
|
+
const duration = Date.now() - startTime;
|
|
24
|
+
const result = {
|
|
25
|
+
id: resultId,
|
|
26
|
+
testCase: testCase,
|
|
27
|
+
toolCalls: toolCalls,
|
|
28
|
+
duration: duration,
|
|
29
|
+
success: true,
|
|
30
|
+
timestamp: new Date(),
|
|
31
|
+
metadata: {
|
|
32
|
+
executorVersion: "1.0.0",
|
|
33
|
+
executionMode: "single",
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
this.logger.info(`✅ Test execution completed: ${testCase.name} (${duration}ms)`);
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const duration = Date.now() - startTime;
|
|
41
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
42
|
+
this.logger.error(`❌ Test execution failed: ${testCase.name} - ${errorMessage}`);
|
|
43
|
+
const result = {
|
|
44
|
+
id: resultId,
|
|
45
|
+
testCase: testCase,
|
|
46
|
+
toolCalls: [],
|
|
47
|
+
duration: duration,
|
|
48
|
+
success: false,
|
|
49
|
+
error: errorMessage,
|
|
50
|
+
timestamp: new Date(),
|
|
51
|
+
metadata: {
|
|
52
|
+
executorVersion: "1.0.0",
|
|
53
|
+
executionMode: "single",
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
validateTestCase(testCase) {
|
|
60
|
+
if (!testCase.id) {
|
|
61
|
+
throw new Error("Test case must have an ID");
|
|
62
|
+
}
|
|
63
|
+
if (!testCase.name) {
|
|
64
|
+
throw new Error("Test case must have a name");
|
|
65
|
+
}
|
|
66
|
+
if (!testCase.prompt) {
|
|
67
|
+
throw new Error("Test case must have a prompt");
|
|
68
|
+
}
|
|
69
|
+
if (!testCase.serverConfigs || testCase.serverConfigs.length === 0) {
|
|
70
|
+
throw new Error("Test case must have at least one server configuration");
|
|
71
|
+
}
|
|
72
|
+
// Validate timeout
|
|
73
|
+
if (testCase.timeout && testCase.timeout < 1000) {
|
|
74
|
+
throw new Error("Test case timeout must be at least 1000ms");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async initializeConnections(testCase) {
|
|
78
|
+
const connections = new Map();
|
|
79
|
+
for (const serverConfig of testCase.serverConfigs) {
|
|
80
|
+
try {
|
|
81
|
+
this.logger.info(`🔗 Initializing connection to server: ${serverConfig.name}`);
|
|
82
|
+
// Use MCPProxyService to create connection
|
|
83
|
+
let sessionId;
|
|
84
|
+
if (serverConfig.type === "stdio") {
|
|
85
|
+
// For STDIO, we need to create a mock response object for SSE
|
|
86
|
+
const mockResponse = {
|
|
87
|
+
writeHead: () => { },
|
|
88
|
+
write: () => { },
|
|
89
|
+
end: () => { },
|
|
90
|
+
on: () => { },
|
|
91
|
+
setHeader: () => { },
|
|
92
|
+
};
|
|
93
|
+
const connection = await this.mcpProxyService.createSSEConnection(serverConfig, mockResponse, {});
|
|
94
|
+
sessionId = connection.sessionId;
|
|
95
|
+
}
|
|
96
|
+
else if (serverConfig.type === "streamable-http") {
|
|
97
|
+
const connection = await this.mcpProxyService.createStreamableHTTPConnection(serverConfig, {});
|
|
98
|
+
sessionId = connection.sessionId;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
throw new Error(`Unsupported server type: ${serverConfig.type}`);
|
|
102
|
+
}
|
|
103
|
+
connections.set(serverConfig.id, sessionId);
|
|
104
|
+
this.logger.info(`✅ Connected to server: ${serverConfig.name} (${sessionId})`);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
this.logger.error(`❌ Failed to connect to server: ${serverConfig.name} - ${error}`);
|
|
108
|
+
throw new Error(`Failed to connect to server: ${serverConfig.name}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return connections;
|
|
112
|
+
}
|
|
113
|
+
async executeTestLogic(testCase, connections) {
|
|
114
|
+
const toolCalls = [];
|
|
115
|
+
// Set timeout for the entire test execution
|
|
116
|
+
const timeout = testCase.timeout || 30000;
|
|
117
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
118
|
+
setTimeout(() => reject(new Error(`Test execution timed out after ${timeout}ms`)), timeout);
|
|
119
|
+
});
|
|
120
|
+
try {
|
|
121
|
+
// Execute the test logic with timeout
|
|
122
|
+
const result = await Promise.race([
|
|
123
|
+
this.runTestWithConnections(testCase, connections),
|
|
124
|
+
timeoutPromise,
|
|
125
|
+
]);
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
if (error instanceof Error && error.message.includes("timed out")) {
|
|
130
|
+
this.logger.error(`⏱️ Test execution timed out: ${testCase.name}`);
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async runTestWithConnections(testCase, connections) {
|
|
136
|
+
const toolCalls = [];
|
|
137
|
+
// For now, implement a simple test execution that simulates tool calls
|
|
138
|
+
// In a real implementation, this would involve:
|
|
139
|
+
// 1. Sending the prompt to an LLM
|
|
140
|
+
// 2. Processing tool calls returned by the LLM
|
|
141
|
+
// 3. Executing those tool calls against the connected MCP servers
|
|
142
|
+
// 4. Recording the results
|
|
143
|
+
this.logger.info(`🤖 Processing prompt: ${testCase.prompt.substring(0, 100)}...`);
|
|
144
|
+
// Simulate tool calls based on expected tools
|
|
145
|
+
if (testCase.expectedTools && testCase.expectedTools.length > 0) {
|
|
146
|
+
for (const expectedTool of testCase.expectedTools) {
|
|
147
|
+
for (const [serverId, _sessionId] of connections.entries()) {
|
|
148
|
+
const serverConfig = testCase.serverConfigs.find((s) => s.id === serverId);
|
|
149
|
+
if (!serverConfig)
|
|
150
|
+
continue;
|
|
151
|
+
const toolCallStartTime = Date.now();
|
|
152
|
+
try {
|
|
153
|
+
// Simulate tool call execution
|
|
154
|
+
await new Promise((resolve) => setTimeout(resolve, 100 + Math.random() * 400));
|
|
155
|
+
const toolCall = {
|
|
156
|
+
toolName: expectedTool,
|
|
157
|
+
serverId: serverId,
|
|
158
|
+
serverName: serverConfig.name,
|
|
159
|
+
parameters: {
|
|
160
|
+
prompt: testCase.prompt,
|
|
161
|
+
timestamp: new Date().toISOString(),
|
|
162
|
+
},
|
|
163
|
+
response: {
|
|
164
|
+
success: true,
|
|
165
|
+
data: `Mock response for ${expectedTool}`,
|
|
166
|
+
timestamp: new Date().toISOString(),
|
|
167
|
+
},
|
|
168
|
+
executionTimeMs: Date.now() - toolCallStartTime,
|
|
169
|
+
success: true,
|
|
170
|
+
timestamp: new Date(),
|
|
171
|
+
};
|
|
172
|
+
toolCalls.push(toolCall);
|
|
173
|
+
this.logger.info(`🔧 Tool call executed: ${expectedTool} on ${serverConfig.name}`);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
177
|
+
const toolCall = {
|
|
178
|
+
toolName: expectedTool,
|
|
179
|
+
serverId: serverId,
|
|
180
|
+
serverName: serverConfig.name,
|
|
181
|
+
parameters: {
|
|
182
|
+
prompt: testCase.prompt,
|
|
183
|
+
timestamp: new Date().toISOString(),
|
|
184
|
+
},
|
|
185
|
+
response: null,
|
|
186
|
+
executionTimeMs: Date.now() - toolCallStartTime,
|
|
187
|
+
success: false,
|
|
188
|
+
error: errorMessage,
|
|
189
|
+
timestamp: new Date(),
|
|
190
|
+
};
|
|
191
|
+
toolCalls.push(toolCall);
|
|
192
|
+
this.logger.error(`❌ Tool call failed: ${expectedTool} on ${serverConfig.name} - ${errorMessage}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
// If no expected tools, simulate a basic interaction
|
|
199
|
+
const serverId = connections.keys().next().value;
|
|
200
|
+
if (serverId) {
|
|
201
|
+
const _sessionId = connections.get(serverId);
|
|
202
|
+
const serverConfig = testCase.serverConfigs.find((s) => s.id === serverId);
|
|
203
|
+
if (serverConfig) {
|
|
204
|
+
const toolCallStartTime = Date.now();
|
|
205
|
+
const toolCall = {
|
|
206
|
+
toolName: "default_interaction",
|
|
207
|
+
serverId: serverId,
|
|
208
|
+
serverName: serverConfig.name,
|
|
209
|
+
parameters: {
|
|
210
|
+
prompt: testCase.prompt,
|
|
211
|
+
timestamp: new Date().toISOString(),
|
|
212
|
+
},
|
|
213
|
+
response: {
|
|
214
|
+
success: true,
|
|
215
|
+
data: "Mock response for default interaction",
|
|
216
|
+
timestamp: new Date().toISOString(),
|
|
217
|
+
},
|
|
218
|
+
executionTimeMs: Date.now() - toolCallStartTime,
|
|
219
|
+
success: true,
|
|
220
|
+
timestamp: new Date(),
|
|
221
|
+
};
|
|
222
|
+
toolCalls.push(toolCall);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return toolCalls;
|
|
227
|
+
}
|
|
228
|
+
async cleanupConnections(connections) {
|
|
229
|
+
for (const [serverId, sessionId] of connections.entries()) {
|
|
230
|
+
try {
|
|
231
|
+
// Note: MCPProxyService doesn't have a direct cleanup method for individual sessions
|
|
232
|
+
// In a real implementation, you might want to add this functionality
|
|
233
|
+
this.logger.info(`🧹 Cleaning up connection: ${serverId} (${sessionId})`);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
this.logger.error(`❌ Failed to cleanup connection: ${serverId} - ${error}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|