@mcpjam/inspector 0.3.5 → 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,198 @@
1
+ // server/src/testing/TestRunner.ts
2
+ import { EventEmitter } from "events";
3
+ import { randomUUID } from "node:crypto";
4
+ export class TestRunner extends EventEmitter {
5
+ mcpProxyService;
6
+ _database;
7
+ logger;
8
+ activeTests = new Map();
9
+ constructor(mcpProxyService, _database, logger) {
10
+ super();
11
+ this.mcpProxyService = mcpProxyService;
12
+ this._database = _database;
13
+ this.logger = logger;
14
+ }
15
+ async runTest(testCase) {
16
+ const testId = randomUUID();
17
+ const startTime = Date.now();
18
+ this.logger.info(`Starting test: ${testCase.name} (${testId})`);
19
+ try {
20
+ // Create test execution context
21
+ const execution = new TestExecution(testId, testCase, this.logger);
22
+ this.activeTests.set(testId, execution);
23
+ // Create MCP connections
24
+ const connections = await this.createConnections(testCase.serverConfigs);
25
+ execution.setConnections(connections);
26
+ // Execute test
27
+ const toolCalls = await this.executeTest(testCase, connections);
28
+ // Create result
29
+ const result = {
30
+ id: testId,
31
+ testCase,
32
+ toolCalls,
33
+ duration: Date.now() - startTime,
34
+ success: true,
35
+ timestamp: new Date(),
36
+ };
37
+ this.logger.info(`Test completed: ${testCase.name} (${testId})`);
38
+ this.emit("testComplete", result);
39
+ return result;
40
+ }
41
+ catch (error) {
42
+ this.logger.error(`Test failed: ${testCase.name} (${testId}):`, error);
43
+ const result = {
44
+ id: testId,
45
+ testCase,
46
+ toolCalls: [],
47
+ duration: Date.now() - startTime,
48
+ success: false,
49
+ error: error instanceof Error ? error.message : String(error),
50
+ timestamp: new Date(),
51
+ };
52
+ this.emit("testError", result, error);
53
+ return result;
54
+ }
55
+ finally {
56
+ this.activeTests.delete(testId);
57
+ }
58
+ }
59
+ async runBatch(testCases) {
60
+ this.logger.info(`Starting batch test: ${testCases.length} tests`);
61
+ // Run tests in parallel with controlled concurrency
62
+ const maxConcurrency = 5;
63
+ const results = [];
64
+ for (let i = 0; i < testCases.length; i += maxConcurrency) {
65
+ const batch = testCases.slice(i, i + maxConcurrency);
66
+ const batchResults = await Promise.all(batch.map((testCase) => this.runTest(testCase)));
67
+ results.push(...batchResults);
68
+ }
69
+ this.logger.info(`Batch test completed: ${results.length} results`);
70
+ return results;
71
+ }
72
+ async getResults(_query) {
73
+ // Mock implementation - in real version would query database
74
+ return [];
75
+ }
76
+ async getResult(_id) {
77
+ // Mock implementation - in real version would query database
78
+ return null;
79
+ }
80
+ getActiveConnections() {
81
+ return this.mcpProxyService.getActiveConnections();
82
+ }
83
+ async closeConnection(id) {
84
+ // Note: MCPProxyService doesn't have closeConnection method
85
+ // Would need to be implemented
86
+ this.logger.info(`Closing connection: ${id}`);
87
+ }
88
+ async close() {
89
+ // Close all active tests
90
+ for (const [_testId, execution] of this.activeTests) {
91
+ await execution.cancel();
92
+ }
93
+ this.activeTests.clear();
94
+ // Close MCP connections
95
+ await this.mcpProxyService.closeAllConnections();
96
+ this.logger.info("Test runner closed");
97
+ }
98
+ async createConnections(serverConfigs) {
99
+ const connections = [];
100
+ for (const config of serverConfigs) {
101
+ try {
102
+ let sessionId;
103
+ if (config.type === "stdio") {
104
+ // For STDIO, create a mock response for SSE
105
+ const mockResponse = {
106
+ writeHead: () => { },
107
+ write: () => { },
108
+ end: () => { },
109
+ on: () => { },
110
+ setHeader: () => { },
111
+ };
112
+ const connection = await this.mcpProxyService.createSSEConnection(config, mockResponse, {});
113
+ sessionId = connection.sessionId;
114
+ }
115
+ else if (config.type === "streamable-http") {
116
+ const connection = await this.mcpProxyService.createStreamableHTTPConnection(config, {});
117
+ sessionId = connection.sessionId;
118
+ }
119
+ else {
120
+ throw new Error(`Unsupported server type: ${config.type}`);
121
+ }
122
+ connections.push(sessionId);
123
+ this.logger.info(`Created connection: ${config.name} (${sessionId})`);
124
+ }
125
+ catch (error) {
126
+ this.logger.error(`Failed to create connection for ${config.name}:`, error);
127
+ throw error;
128
+ }
129
+ }
130
+ return connections;
131
+ }
132
+ async executeTest(testCase, connections) {
133
+ const toolCalls = [];
134
+ const _timeout = testCase.timeout || 30000;
135
+ // Simple mock execution for now
136
+ // In real implementation, would use LLM to process prompt and make tool calls
137
+ if (testCase.expectedTools && testCase.expectedTools.length > 0) {
138
+ for (const expectedTool of testCase.expectedTools) {
139
+ const toolCallStartTime = Date.now();
140
+ try {
141
+ // Mock tool call execution
142
+ await new Promise((resolve) => setTimeout(resolve, 100));
143
+ const toolCall = {
144
+ toolName: expectedTool,
145
+ serverId: connections[0] || "unknown",
146
+ serverName: "Mock Server",
147
+ parameters: { test: true },
148
+ response: {
149
+ success: true,
150
+ data: `Mock response for ${expectedTool}`,
151
+ },
152
+ executionTimeMs: Date.now() - toolCallStartTime,
153
+ success: true,
154
+ timestamp: new Date(),
155
+ };
156
+ toolCalls.push(toolCall);
157
+ }
158
+ catch (error) {
159
+ const toolCall = {
160
+ toolName: expectedTool,
161
+ serverId: connections[0] || "unknown",
162
+ serverName: "Mock Server",
163
+ parameters: { test: true },
164
+ response: null,
165
+ executionTimeMs: Date.now() - toolCallStartTime,
166
+ success: false,
167
+ error: error instanceof Error ? error.message : String(error),
168
+ timestamp: new Date(),
169
+ };
170
+ toolCalls.push(toolCall);
171
+ }
172
+ }
173
+ }
174
+ return toolCalls;
175
+ }
176
+ }
177
+ class TestExecution {
178
+ id;
179
+ testCase;
180
+ logger;
181
+ _connections = [];
182
+ cancelled = false;
183
+ constructor(id, testCase, logger) {
184
+ this.id = id;
185
+ this.testCase = testCase;
186
+ this.logger = logger;
187
+ }
188
+ setConnections(connections) {
189
+ this._connections = connections;
190
+ }
191
+ async cancel() {
192
+ this.cancelled = true;
193
+ this.logger.info(`Test execution cancelled: ${this.id}`);
194
+ }
195
+ isCancelled() {
196
+ return this.cancelled;
197
+ }
198
+ }
@@ -0,0 +1,440 @@
1
+ // server/src/testing/TestServer.ts
2
+ import express from "express";
3
+ import cors from "cors";
4
+ import { HealthCheck } from "./HealthCheck.js";
5
+ import { ConsoleLogger } from "../shared/utils.js";
6
+ export class TestServer {
7
+ app;
8
+ server;
9
+ healthCheck = null;
10
+ logger;
11
+ config;
12
+ constructor(config) {
13
+ this.config = config;
14
+ this.logger = new ConsoleLogger();
15
+ this.app = express();
16
+ this.setupMiddleware();
17
+ }
18
+ setupMiddleware() {
19
+ // CORS support
20
+ if (this.config.cors) {
21
+ this.app.use(cors({
22
+ origin: true,
23
+ credentials: true,
24
+ }));
25
+ }
26
+ // Body parsing
27
+ this.app.use(express.json({ limit: "10mb" }));
28
+ this.app.use(express.urlencoded({ extended: true }));
29
+ // Request logging
30
+ this.app.use((req, res, next) => {
31
+ this.logger.info(`${req.method} ${req.path}`, {
32
+ ip: req.ip,
33
+ userAgent: req.get("User-Agent"),
34
+ });
35
+ next();
36
+ });
37
+ }
38
+ setupRoutes() {
39
+ // Health check endpoints
40
+ this.app.get("/api/test/health", (req, res) => {
41
+ if (!this.healthCheck) {
42
+ return res.status(503).json({ error: "Health check not initialized" });
43
+ }
44
+ res.json(this.healthCheck.getStatus());
45
+ });
46
+ this.app.get("/api/test/status", (req, res) => {
47
+ if (!this.healthCheck) {
48
+ return res.status(503).json({ error: "Health check not initialized" });
49
+ }
50
+ res.json(this.healthCheck.getDetailedStatus());
51
+ });
52
+ // Test execution endpoints
53
+ this.app.post("/api/test/run", async (req, res) => {
54
+ try {
55
+ const { testCase } = req.body;
56
+ if (!testCase ||
57
+ !testCase.id ||
58
+ !testCase.prompt ||
59
+ !testCase.serverConfigs) {
60
+ return res.status(400).json({
61
+ success: false,
62
+ error: "Invalid test case format. Required fields: id, prompt, serverConfigs",
63
+ });
64
+ }
65
+ this.logger.info(`🧪 Starting test execution for: ${testCase.name || testCase.id}`);
66
+ // Mock test execution for now - will be replaced with actual implementation
67
+ const startTime = Date.now();
68
+ // Simulate test execution
69
+ await new Promise((resolve) => setTimeout(resolve, 1000));
70
+ const result = {
71
+ id: `result-${Date.now()}`,
72
+ testCase: testCase,
73
+ toolCalls: [],
74
+ duration: Date.now() - startTime,
75
+ success: true,
76
+ timestamp: new Date().toISOString(),
77
+ metadata: {
78
+ executionMode: "single",
79
+ server: "test-server",
80
+ },
81
+ };
82
+ this.logger.info(`✅ Test execution completed for: ${testCase.name || testCase.id}`);
83
+ res.json({
84
+ success: true,
85
+ result: result,
86
+ });
87
+ }
88
+ catch (error) {
89
+ this.logger.error("❌ Test execution failed:", error);
90
+ res.status(500).json({
91
+ success: false,
92
+ error: "Test execution failed",
93
+ details: error instanceof Error ? error.message : String(error),
94
+ });
95
+ }
96
+ });
97
+ this.app.post("/api/test/run-batch", async (req, res) => {
98
+ try {
99
+ const { testCases } = req.body;
100
+ if (!Array.isArray(testCases) || testCases.length === 0) {
101
+ return res.status(400).json({
102
+ success: false,
103
+ error: "Invalid request format. Expected array of test cases",
104
+ });
105
+ }
106
+ this.logger.info(`🧪 Starting batch test execution for ${testCases.length} tests`);
107
+ const results = [];
108
+ const startTime = Date.now();
109
+ // Process each test case
110
+ for (const testCase of testCases) {
111
+ if (!testCase.id || !testCase.prompt || !testCase.serverConfigs) {
112
+ results.push({
113
+ id: `result-${Date.now()}`,
114
+ testCase: testCase,
115
+ toolCalls: [],
116
+ duration: 0,
117
+ success: false,
118
+ error: "Invalid test case format",
119
+ timestamp: new Date().toISOString(),
120
+ });
121
+ continue;
122
+ }
123
+ const testStartTime = Date.now();
124
+ // Simulate test execution
125
+ await new Promise((resolve) => setTimeout(resolve, 500));
126
+ results.push({
127
+ id: `result-${Date.now()}`,
128
+ testCase: testCase,
129
+ toolCalls: [],
130
+ duration: Date.now() - testStartTime,
131
+ success: true,
132
+ timestamp: new Date().toISOString(),
133
+ metadata: {
134
+ executionMode: "batch",
135
+ server: "test-server",
136
+ },
137
+ });
138
+ }
139
+ const totalDuration = Date.now() - startTime;
140
+ const successCount = results.filter((r) => r.success).length;
141
+ this.logger.info(`✅ Batch test execution completed: ${successCount}/${testCases.length} passed`);
142
+ res.json({
143
+ success: true,
144
+ results: results,
145
+ summary: {
146
+ total: testCases.length,
147
+ passed: successCount,
148
+ failed: testCases.length - successCount,
149
+ duration: totalDuration,
150
+ },
151
+ });
152
+ }
153
+ catch (error) {
154
+ this.logger.error("❌ Batch test execution failed:", error);
155
+ res.status(500).json({
156
+ success: false,
157
+ error: "Batch test execution failed",
158
+ details: error instanceof Error ? error.message : String(error),
159
+ });
160
+ }
161
+ });
162
+ this.app.get("/api/test/results", (req, res) => {
163
+ try {
164
+ const { limit = 50, offset = 0, testCaseId } = req.query;
165
+ // Mock results for now - will be replaced with database queries
166
+ const mockResults = [
167
+ {
168
+ id: "result-1",
169
+ testCase: {
170
+ id: "test-1",
171
+ name: "Sample Test",
172
+ prompt: "Test prompt",
173
+ },
174
+ toolCalls: [],
175
+ duration: 1250,
176
+ success: true,
177
+ timestamp: new Date().toISOString(),
178
+ },
179
+ ];
180
+ res.json({
181
+ success: true,
182
+ results: mockResults,
183
+ pagination: {
184
+ limit: parseInt(limit),
185
+ offset: parseInt(offset),
186
+ total: mockResults.length,
187
+ },
188
+ });
189
+ }
190
+ catch (error) {
191
+ this.logger.error("❌ Failed to retrieve test results:", error);
192
+ res.status(500).json({
193
+ success: false,
194
+ error: "Failed to retrieve test results",
195
+ details: error instanceof Error ? error.message : String(error),
196
+ });
197
+ }
198
+ });
199
+ this.app.get("/api/test/results/:id", (req, res) => {
200
+ try {
201
+ const { id } = req.params;
202
+ // Mock result for now - will be replaced with database query
203
+ const mockResult = {
204
+ id: id,
205
+ testCase: {
206
+ id: "test-1",
207
+ name: "Sample Test",
208
+ prompt: "Test prompt",
209
+ },
210
+ toolCalls: [],
211
+ duration: 1250,
212
+ success: true,
213
+ timestamp: new Date().toISOString(),
214
+ metadata: {
215
+ executionMode: "single",
216
+ server: "test-server",
217
+ },
218
+ };
219
+ res.json({
220
+ success: true,
221
+ result: mockResult,
222
+ });
223
+ }
224
+ catch (error) {
225
+ this.logger.error("❌ Failed to retrieve test result:", error);
226
+ res.status(500).json({
227
+ success: false,
228
+ error: "Failed to retrieve test result",
229
+ details: error instanceof Error ? error.message : String(error),
230
+ });
231
+ }
232
+ });
233
+ this.app.get("/api/test/connections", (req, res) => {
234
+ try {
235
+ // Mock connection info for now
236
+ const mockConnections = [
237
+ {
238
+ id: "conn-1",
239
+ name: "Test Server",
240
+ type: "stdio",
241
+ status: "connected",
242
+ lastActivity: new Date().toISOString(),
243
+ },
244
+ ];
245
+ res.json({
246
+ success: true,
247
+ connections: mockConnections,
248
+ summary: {
249
+ total: mockConnections.length,
250
+ active: mockConnections.filter((c) => c.status === "connected")
251
+ .length,
252
+ inactive: mockConnections.filter((c) => c.status !== "connected")
253
+ .length,
254
+ },
255
+ });
256
+ }
257
+ catch (error) {
258
+ this.logger.error("❌ Failed to retrieve connections:", error);
259
+ res.status(500).json({
260
+ success: false,
261
+ error: "Failed to retrieve connections",
262
+ details: error instanceof Error ? error.message : String(error),
263
+ });
264
+ }
265
+ });
266
+ // Test case management endpoints
267
+ this.app.get("/api/test/cases", (req, res) => {
268
+ try {
269
+ const { limit = 50, offset = 0 } = req.query;
270
+ // Mock test cases for now
271
+ const mockTestCases = [
272
+ {
273
+ id: "test-1",
274
+ name: "Sample Test Case",
275
+ prompt: "Test the weather tool functionality",
276
+ expectedTools: ["get_weather"],
277
+ serverConfigs: [{ id: "server-1", name: "Weather Server" }],
278
+ timeout: 30000,
279
+ metadata: { category: "weather" },
280
+ },
281
+ ];
282
+ res.json({
283
+ success: true,
284
+ testCases: mockTestCases,
285
+ pagination: {
286
+ limit: parseInt(limit),
287
+ offset: parseInt(offset),
288
+ total: mockTestCases.length,
289
+ },
290
+ });
291
+ }
292
+ catch (error) {
293
+ this.logger.error("❌ Failed to retrieve test cases:", error);
294
+ res.status(500).json({
295
+ success: false,
296
+ error: "Failed to retrieve test cases",
297
+ details: error instanceof Error ? error.message : String(error),
298
+ });
299
+ }
300
+ });
301
+ this.app.post("/api/test/cases", async (req, res) => {
302
+ try {
303
+ const testCase = req.body;
304
+ if (!testCase.name || !testCase.prompt || !testCase.serverConfigs) {
305
+ return res.status(400).json({
306
+ success: false,
307
+ error: "Invalid test case format. Required fields: name, prompt, serverConfigs",
308
+ });
309
+ }
310
+ // Generate ID if not provided
311
+ if (!testCase.id) {
312
+ testCase.id = `test-${Date.now()}`;
313
+ }
314
+ this.logger.info(`💾 Creating test case: ${testCase.name}`);
315
+ // Mock save operation
316
+ const savedTestCase = {
317
+ ...testCase,
318
+ createdAt: new Date().toISOString(),
319
+ updatedAt: new Date().toISOString(),
320
+ };
321
+ res.status(201).json({
322
+ success: true,
323
+ testCase: savedTestCase,
324
+ });
325
+ }
326
+ catch (error) {
327
+ this.logger.error("❌ Failed to create test case:", error);
328
+ res.status(500).json({
329
+ success: false,
330
+ error: "Failed to create test case",
331
+ details: error instanceof Error ? error.message : String(error),
332
+ });
333
+ }
334
+ });
335
+ this.app.get("/api/test/cases/:id", (req, res) => {
336
+ try {
337
+ const { id } = req.params;
338
+ // Mock test case
339
+ const mockTestCase = {
340
+ id: id,
341
+ name: "Sample Test Case",
342
+ prompt: "Test the weather tool functionality",
343
+ expectedTools: ["get_weather"],
344
+ serverConfigs: [{ id: "server-1", name: "Weather Server" }],
345
+ timeout: 30000,
346
+ metadata: { category: "weather" },
347
+ createdAt: new Date().toISOString(),
348
+ updatedAt: new Date().toISOString(),
349
+ };
350
+ res.json({
351
+ success: true,
352
+ testCase: mockTestCase,
353
+ });
354
+ }
355
+ catch (error) {
356
+ this.logger.error("❌ Failed to retrieve test case:", error);
357
+ res.status(500).json({
358
+ success: false,
359
+ error: "Failed to retrieve test case",
360
+ details: error instanceof Error ? error.message : String(error),
361
+ });
362
+ }
363
+ });
364
+ // Statistics endpoint
365
+ this.app.get("/api/test/stats", (_req, res) => {
366
+ try {
367
+ const stats = {
368
+ testCases: {
369
+ total: 1,
370
+ active: 1,
371
+ inactive: 0,
372
+ },
373
+ testResults: {
374
+ total: 1,
375
+ passed: 1,
376
+ failed: 0,
377
+ recentRuns: 1,
378
+ },
379
+ connections: {
380
+ total: 1,
381
+ active: 1,
382
+ inactive: 0,
383
+ },
384
+ performance: {
385
+ averageTestDuration: 1250,
386
+ successRate: 100,
387
+ lastRunTime: new Date().toISOString(),
388
+ },
389
+ };
390
+ res.json({
391
+ success: true,
392
+ stats: stats,
393
+ });
394
+ }
395
+ catch (error) {
396
+ this.logger.error("❌ Failed to retrieve statistics:", error);
397
+ res.status(500).json({
398
+ success: false,
399
+ error: "Failed to retrieve statistics",
400
+ details: error instanceof Error ? error.message : String(error),
401
+ });
402
+ }
403
+ });
404
+ // Error handling
405
+ this.app.use((error, _req, res, _next) => {
406
+ this.logger.error("Unhandled error:", error);
407
+ res.status(500).json({
408
+ success: false,
409
+ error: "Internal server error",
410
+ });
411
+ });
412
+ }
413
+ async start(mcpProxyService, database) {
414
+ // Initialize components
415
+ this.healthCheck = new HealthCheck(mcpProxyService, database, this.logger);
416
+ // Setup routes
417
+ this.setupRoutes();
418
+ // Start server
419
+ return new Promise((resolve, reject) => {
420
+ this.server = this.app.listen(this.config.port, this.config.host, () => {
421
+ this.logger.info(`🧪 Test server listening on ${this.config.host}:${this.config.port}`);
422
+ resolve();
423
+ });
424
+ this.server.on("error", (error) => {
425
+ this.logger.error("Server error:", error);
426
+ reject(error);
427
+ });
428
+ });
429
+ }
430
+ async stop() {
431
+ if (this.server) {
432
+ await new Promise((resolve) => {
433
+ this.server.close(() => {
434
+ this.logger.info("🧪 Test server stopped");
435
+ resolve();
436
+ });
437
+ });
438
+ }
439
+ }
440
+ }
@@ -0,0 +1 @@
1
+ export {};