@mcpspec/server 1.0.0

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/dist/index.js ADDED
@@ -0,0 +1,1312 @@
1
+ // src/index.ts
2
+ import { dirname as dirname2, join as join3 } from "path";
3
+ import { fileURLToPath } from "url";
4
+
5
+ // src/app.ts
6
+ import { Hono } from "hono";
7
+ import { cors } from "hono/cors";
8
+
9
+ // src/middleware/localhost-only.ts
10
+ var LOCALHOST_ADDRS = /* @__PURE__ */ new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1", "localhost"]);
11
+ function localhostOnly() {
12
+ return async (c, next) => {
13
+ const remoteAccess = process.env["MCPSPEC_REMOTE_ACCESS"] === "true";
14
+ if (remoteAccess) {
15
+ const token = process.env["MCPSPEC_TOKEN"];
16
+ if (token) {
17
+ const authHeader = c.req.header("Authorization");
18
+ if (authHeader !== `Bearer ${token}`) {
19
+ return c.json({ error: "unauthorized", message: "Invalid or missing token" }, 401);
20
+ }
21
+ }
22
+ return next();
23
+ }
24
+ const remoteAddr = c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip");
25
+ if (remoteAddr && !LOCALHOST_ADDRS.has(remoteAddr)) {
26
+ return c.json(
27
+ { error: "forbidden", message: "Remote access is disabled. Set MCPSPEC_REMOTE_ACCESS=true to enable." },
28
+ 403
29
+ );
30
+ }
31
+ return next();
32
+ };
33
+ }
34
+
35
+ // src/middleware/auth.ts
36
+ function authMiddleware() {
37
+ return async (_c, next) => {
38
+ return next();
39
+ };
40
+ }
41
+
42
+ // src/routes/servers.ts
43
+ import { createServerSchema, updateServerSchema } from "@mcpspec/shared";
44
+ import { MCPClient, ProcessManagerImpl } from "@mcpspec/core";
45
+ function serversRoutes(app, db) {
46
+ app.get("/api/servers", (c) => {
47
+ const servers = db.listServers();
48
+ return c.json({ data: servers, total: servers.length });
49
+ });
50
+ app.post("/api/servers", async (c) => {
51
+ const body = await c.req.json();
52
+ const parsed = createServerSchema.safeParse(body);
53
+ if (!parsed.success) {
54
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
55
+ }
56
+ const server = db.createServer(parsed.data);
57
+ return c.json({ data: server }, 201);
58
+ });
59
+ app.get("/api/servers/:id", (c) => {
60
+ const server = db.getServer(c.req.param("id"));
61
+ if (!server) return c.json({ error: "not_found", message: "Server not found" }, 404);
62
+ return c.json({ data: server });
63
+ });
64
+ app.put("/api/servers/:id", async (c) => {
65
+ const body = await c.req.json();
66
+ const parsed = updateServerSchema.safeParse(body);
67
+ if (!parsed.success) {
68
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
69
+ }
70
+ const server = db.updateServer(c.req.param("id"), parsed.data);
71
+ if (!server) return c.json({ error: "not_found", message: "Server not found" }, 404);
72
+ return c.json({ data: server });
73
+ });
74
+ app.delete("/api/servers/:id", (c) => {
75
+ const deleted = db.deleteServer(c.req.param("id"));
76
+ if (!deleted) return c.json({ error: "not_found", message: "Server not found" }, 404);
77
+ return c.json({ data: { deleted: true } });
78
+ });
79
+ app.post("/api/servers/:id/test", async (c) => {
80
+ const server = db.getServer(c.req.param("id"));
81
+ if (!server) return c.json({ error: "not_found", message: "Server not found" }, 404);
82
+ const processManager = new ProcessManagerImpl();
83
+ const config = {
84
+ name: server.name,
85
+ transport: server.transport,
86
+ command: server.command,
87
+ args: server.args,
88
+ url: server.url,
89
+ env: server.env
90
+ };
91
+ const client = new MCPClient({ serverConfig: config, processManager });
92
+ try {
93
+ await client.connect();
94
+ const tools = await client.listTools();
95
+ await client.disconnect();
96
+ await processManager.shutdownAll();
97
+ return c.json({ data: { connected: true, toolCount: tools.length } });
98
+ } catch (err) {
99
+ await processManager.shutdownAll();
100
+ const message = err instanceof Error ? err.message : "Connection failed";
101
+ return c.json({ data: { connected: false, error: message } });
102
+ }
103
+ });
104
+ }
105
+
106
+ // src/routes/collections.ts
107
+ import { createCollectionSchema, updateCollectionSchema, collectionSchema } from "@mcpspec/shared";
108
+ import { loadYamlSafely } from "@mcpspec/core";
109
+ function collectionsRoutes(app, db) {
110
+ app.get("/api/collections", (c) => {
111
+ const collections = db.listCollections();
112
+ return c.json({ data: collections, total: collections.length });
113
+ });
114
+ app.post("/api/collections", async (c) => {
115
+ const body = await c.req.json();
116
+ const parsed = createCollectionSchema.safeParse(body);
117
+ if (!parsed.success) {
118
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
119
+ }
120
+ const collection = db.createCollection(parsed.data);
121
+ return c.json({ data: collection }, 201);
122
+ });
123
+ app.get("/api/collections/:id", (c) => {
124
+ const collection = db.getCollection(c.req.param("id"));
125
+ if (!collection) return c.json({ error: "not_found", message: "Collection not found" }, 404);
126
+ return c.json({ data: collection });
127
+ });
128
+ app.put("/api/collections/:id", async (c) => {
129
+ const body = await c.req.json();
130
+ const parsed = updateCollectionSchema.safeParse(body);
131
+ if (!parsed.success) {
132
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
133
+ }
134
+ const collection = db.updateCollection(c.req.param("id"), parsed.data);
135
+ if (!collection) return c.json({ error: "not_found", message: "Collection not found" }, 404);
136
+ return c.json({ data: collection });
137
+ });
138
+ app.delete("/api/collections/:id", (c) => {
139
+ const deleted = db.deleteCollection(c.req.param("id"));
140
+ if (!deleted) return c.json({ error: "not_found", message: "Collection not found" }, 404);
141
+ return c.json({ data: { deleted: true } });
142
+ });
143
+ app.post("/api/collections/:id/validate", (c) => {
144
+ const collection = db.getCollection(c.req.param("id"));
145
+ if (!collection) return c.json({ error: "not_found", message: "Collection not found" }, 404);
146
+ try {
147
+ const yamlContent = loadYamlSafely(collection.yaml);
148
+ const result = collectionSchema.safeParse(yamlContent);
149
+ if (!result.success) {
150
+ return c.json({ data: { valid: false, errors: result.error.issues } });
151
+ }
152
+ return c.json({ data: { valid: true } });
153
+ } catch (err) {
154
+ const message = err instanceof Error ? err.message : "YAML parse error";
155
+ return c.json({ data: { valid: false, errors: [{ message }] } });
156
+ }
157
+ });
158
+ }
159
+
160
+ // src/routes/runs.ts
161
+ import { triggerRunSchema, collectionSchema as collectionSchema2 } from "@mcpspec/shared";
162
+ import { loadYamlSafely as loadYamlSafely2, TestRunner } from "@mcpspec/core";
163
+ function runsRoutes(app, db) {
164
+ app.get("/api/runs", (c) => {
165
+ const limit = Number(c.req.query("limit") ?? "50");
166
+ const runs = db.listRuns(limit);
167
+ return c.json({ data: runs, total: runs.length });
168
+ });
169
+ app.post("/api/runs", async (c) => {
170
+ const body = await c.req.json();
171
+ const parsed = triggerRunSchema.safeParse(body);
172
+ if (!parsed.success) {
173
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
174
+ }
175
+ const collection = db.getCollection(parsed.data.collectionId);
176
+ if (!collection) {
177
+ return c.json({ error: "not_found", message: "Collection not found" }, 404);
178
+ }
179
+ let collectionDef;
180
+ try {
181
+ const raw = loadYamlSafely2(collection.yaml);
182
+ const validated = collectionSchema2.parse(raw);
183
+ collectionDef = coerceCollection(validated);
184
+ } catch (err) {
185
+ const message = err instanceof Error ? err.message : "Invalid collection YAML";
186
+ return c.json({ error: "validation_error", message }, 400);
187
+ }
188
+ const run = db.createRun({
189
+ collectionId: collection.id,
190
+ collectionName: collectionDef.name,
191
+ serverId: parsed.data.serverId
192
+ });
193
+ executeRunInBackground(db, run.id, collectionDef, {
194
+ environment: parsed.data.environment,
195
+ tags: parsed.data.tags,
196
+ parallelism: parsed.data.parallelism
197
+ });
198
+ return c.json({ data: run }, 202);
199
+ });
200
+ app.get("/api/runs/:id", (c) => {
201
+ const run = db.getRun(c.req.param("id"));
202
+ if (!run) return c.json({ error: "not_found", message: "Run not found" }, 404);
203
+ return c.json({ data: run });
204
+ });
205
+ app.delete("/api/runs/:id", (c) => {
206
+ const deleted = db.deleteRun(c.req.param("id"));
207
+ if (!deleted) return c.json({ error: "not_found", message: "Run not found" }, 404);
208
+ return c.json({ data: { deleted: true } });
209
+ });
210
+ }
211
+ function executeRunInBackground(db, runId, collection, options) {
212
+ const runner = new TestRunner();
213
+ const results = [];
214
+ const reporter = {
215
+ onRunStart() {
216
+ },
217
+ onTestStart() {
218
+ },
219
+ onTestComplete(result) {
220
+ results.push(result);
221
+ },
222
+ onRunComplete(runResult) {
223
+ db.updateRun(runId, {
224
+ status: "completed",
225
+ summary: runResult.summary,
226
+ results: runResult.results,
227
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
228
+ duration: runResult.duration
229
+ });
230
+ }
231
+ };
232
+ runner.run(collection, {
233
+ reporter,
234
+ environment: options.environment,
235
+ tags: options.tags,
236
+ parallelism: options.parallelism
237
+ }).catch((err) => {
238
+ db.updateRun(runId, {
239
+ status: "failed",
240
+ results,
241
+ completedAt: (/* @__PURE__ */ new Date()).toISOString()
242
+ });
243
+ console.error(`Run ${runId} failed:`, err instanceof Error ? err.message : err);
244
+ });
245
+ }
246
+ function coerceCollection(raw) {
247
+ const tests = raw["tests"];
248
+ return {
249
+ ...raw,
250
+ tests: tests.map((t) => ({
251
+ ...t,
252
+ timeout: t["timeout"] !== void 0 ? Number(t["timeout"]) : void 0,
253
+ retries: t["retries"] !== void 0 ? Number(t["retries"]) : void 0,
254
+ expectError: t["expectError"] !== void 0 ? t["expectError"] === "true" || t["expectError"] === true : void 0
255
+ }))
256
+ };
257
+ }
258
+
259
+ // src/routes/inspect.ts
260
+ import { inspectConnectSchema, inspectCallSchema } from "@mcpspec/shared";
261
+ import { MCPClient as MCPClient2, ProcessManagerImpl as ProcessManagerImpl2 } from "@mcpspec/core";
262
+ import { randomUUID } from "crypto";
263
+ var MAX_LOG_ENTRIES = 1e4;
264
+ var sessions = /* @__PURE__ */ new Map();
265
+ var SESSION_TIMEOUT_MS = 5 * 60 * 1e3;
266
+ setInterval(() => {
267
+ const now = Date.now();
268
+ for (const [id, session] of sessions) {
269
+ if (now - session.lastUsed > SESSION_TIMEOUT_MS) {
270
+ session.client.disconnect().catch(() => {
271
+ });
272
+ session.processManager.shutdownAll().catch(() => {
273
+ });
274
+ sessions.delete(id);
275
+ }
276
+ }
277
+ }, 6e4);
278
+ function inspectRoutes(app, wsHandler) {
279
+ app.post("/api/inspect/connect", async (c) => {
280
+ const body = await c.req.json();
281
+ const parsed = inspectConnectSchema.safeParse(body);
282
+ if (!parsed.success) {
283
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
284
+ }
285
+ const sessionId = randomUUID();
286
+ const processManager = new ProcessManagerImpl2();
287
+ const config = {
288
+ transport: parsed.data.transport,
289
+ command: parsed.data.command,
290
+ args: parsed.data.args,
291
+ url: parsed.data.url,
292
+ env: parsed.data.env
293
+ };
294
+ const session = {
295
+ client: null,
296
+ processManager,
297
+ lastUsed: Date.now(),
298
+ protocolLog: [],
299
+ pendingRequests: /* @__PURE__ */ new Map()
300
+ };
301
+ const onProtocolMessage = (direction, message) => {
302
+ const jsonrpcId = message.id;
303
+ const method = message.method;
304
+ const isError = message.error !== void 0;
305
+ const now = Date.now();
306
+ const entry = {
307
+ id: randomUUID(),
308
+ timestamp: now,
309
+ direction,
310
+ message,
311
+ jsonrpcId: jsonrpcId ?? void 0,
312
+ method,
313
+ isError: isError || void 0
314
+ };
315
+ if (direction === "outgoing" && jsonrpcId != null && method) {
316
+ session.pendingRequests.set(jsonrpcId, now);
317
+ } else if (direction === "incoming" && jsonrpcId != null) {
318
+ const sentAt = session.pendingRequests.get(jsonrpcId);
319
+ if (sentAt !== void 0) {
320
+ entry.roundTripMs = now - sentAt;
321
+ session.pendingRequests.delete(jsonrpcId);
322
+ }
323
+ }
324
+ if (session.protocolLog.length >= MAX_LOG_ENTRIES) {
325
+ session.protocolLog.shift();
326
+ }
327
+ session.protocolLog.push(entry);
328
+ if (wsHandler) {
329
+ wsHandler.broadcast(`inspect:${sessionId}`, "protocol-message", entry);
330
+ }
331
+ };
332
+ const client = new MCPClient2({ serverConfig: config, processManager, onProtocolMessage });
333
+ session.client = client;
334
+ try {
335
+ await client.connect();
336
+ sessions.set(sessionId, session);
337
+ return c.json({ data: { sessionId, connected: true } });
338
+ } catch (err) {
339
+ await processManager.shutdownAll();
340
+ const message = err instanceof Error ? err.message : "Connection failed";
341
+ return c.json({ error: "connection_error", message }, 500);
342
+ }
343
+ });
344
+ app.post("/api/inspect/tools", async (c) => {
345
+ const body = await c.req.json();
346
+ const sessionId = body?.sessionId;
347
+ if (!sessionId) {
348
+ return c.json({ error: "validation_error", message: "sessionId is required" }, 400);
349
+ }
350
+ const session = sessions.get(sessionId);
351
+ if (!session) {
352
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
353
+ }
354
+ session.lastUsed = Date.now();
355
+ try {
356
+ const tools = await session.client.listTools();
357
+ return c.json({ data: tools });
358
+ } catch (err) {
359
+ const message = err instanceof Error ? err.message : "Failed to list tools";
360
+ return c.json({ error: "tool_error", message }, 500);
361
+ }
362
+ });
363
+ app.post("/api/inspect/call", async (c) => {
364
+ const body = await c.req.json();
365
+ const parsed = inspectCallSchema.safeParse(body);
366
+ if (!parsed.success) {
367
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
368
+ }
369
+ const session = sessions.get(parsed.data.sessionId);
370
+ if (!session) {
371
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
372
+ }
373
+ session.lastUsed = Date.now();
374
+ try {
375
+ const result = await session.client.callTool(parsed.data.tool, parsed.data.input ?? {});
376
+ return c.json({ data: result });
377
+ } catch (err) {
378
+ const message = err instanceof Error ? err.message : "Tool call failed";
379
+ return c.json({ error: "tool_error", message }, 500);
380
+ }
381
+ });
382
+ app.post("/api/inspect/resources", async (c) => {
383
+ const body = await c.req.json();
384
+ const sessionId = body?.sessionId;
385
+ if (!sessionId) {
386
+ return c.json({ error: "validation_error", message: "sessionId is required" }, 400);
387
+ }
388
+ const session = sessions.get(sessionId);
389
+ if (!session) {
390
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
391
+ }
392
+ session.lastUsed = Date.now();
393
+ try {
394
+ const resources = await session.client.listResources();
395
+ return c.json({ data: resources });
396
+ } catch (err) {
397
+ const message = err instanceof Error ? err.message : "Failed to list resources";
398
+ return c.json({ error: "resource_error", message }, 500);
399
+ }
400
+ });
401
+ app.post("/api/inspect/messages", async (c) => {
402
+ const body = await c.req.json();
403
+ const sessionId = body?.sessionId;
404
+ if (!sessionId) {
405
+ return c.json({ error: "validation_error", message: "sessionId is required" }, 400);
406
+ }
407
+ const session = sessions.get(sessionId);
408
+ if (!session) {
409
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
410
+ }
411
+ session.lastUsed = Date.now();
412
+ const after = typeof body.after === "number" ? body.after : void 0;
413
+ let entries = session.protocolLog;
414
+ if (after !== void 0) {
415
+ entries = entries.filter((e) => e.timestamp > after);
416
+ }
417
+ return c.json({ data: entries, total: session.protocolLog.length });
418
+ });
419
+ app.post("/api/inspect/disconnect", async (c) => {
420
+ const body = await c.req.json();
421
+ const sessionId = body?.sessionId;
422
+ if (!sessionId) {
423
+ return c.json({ error: "validation_error", message: "sessionId is required" }, 400);
424
+ }
425
+ const session = sessions.get(sessionId);
426
+ if (!session) {
427
+ return c.json({ data: { disconnected: true } });
428
+ }
429
+ try {
430
+ await session.client.disconnect();
431
+ await session.processManager.shutdownAll();
432
+ } catch {
433
+ }
434
+ sessions.delete(sessionId);
435
+ return c.json({ data: { disconnected: true } });
436
+ });
437
+ }
438
+
439
+ // src/routes/audit.ts
440
+ import { auditStartSchema } from "@mcpspec/shared";
441
+ import { MCPClient as MCPClient3, ProcessManagerImpl as ProcessManagerImpl3, SecurityScanner, ScanConfig } from "@mcpspec/core";
442
+ import { randomUUID as randomUUID2 } from "crypto";
443
+ var sessions2 = /* @__PURE__ */ new Map();
444
+ var SESSION_TIMEOUT_MS2 = 10 * 60 * 1e3;
445
+ setInterval(() => {
446
+ const now = Date.now();
447
+ for (const [id, session] of sessions2) {
448
+ if (now - session.lastUsed > SESSION_TIMEOUT_MS2) {
449
+ session.client.disconnect().catch(() => {
450
+ });
451
+ session.processManager.shutdownAll().catch(() => {
452
+ });
453
+ sessions2.delete(id);
454
+ }
455
+ }
456
+ }, 6e4);
457
+ function auditRoutes(app, wsHandler) {
458
+ app.post("/api/audit/start", async (c) => {
459
+ const body = await c.req.json();
460
+ const parsed = auditStartSchema.safeParse(body);
461
+ if (!parsed.success) {
462
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
463
+ }
464
+ const sessionId = randomUUID2();
465
+ const processManager = new ProcessManagerImpl3();
466
+ const config = {
467
+ transport: parsed.data.transport,
468
+ command: parsed.data.command,
469
+ args: parsed.data.args,
470
+ url: parsed.data.url,
471
+ env: parsed.data.env
472
+ };
473
+ const scanConfig = new ScanConfig({
474
+ mode: parsed.data.mode,
475
+ rules: parsed.data.rules,
476
+ acknowledgeRisk: true
477
+ // UI handles confirmation client-side
478
+ });
479
+ const session = {
480
+ client: null,
481
+ processManager,
482
+ status: "connecting",
483
+ progress: { completedRules: 0, totalRules: scanConfig.rules.length, findingsCount: 0 },
484
+ lastUsed: Date.now()
485
+ };
486
+ const client = new MCPClient3({ serverConfig: config, processManager });
487
+ session.client = client;
488
+ sessions2.set(sessionId, session);
489
+ (async () => {
490
+ try {
491
+ await client.connect();
492
+ session.status = "scanning";
493
+ const scanner = new SecurityScanner();
494
+ const result = await scanner.scan(client, scanConfig, {
495
+ onRuleStart: (ruleId, ruleName) => {
496
+ session.progress.currentRule = ruleName;
497
+ session.lastUsed = Date.now();
498
+ wsHandler?.broadcast(`scan:${sessionId}`, "rule-start", { ruleId, ruleName });
499
+ },
500
+ onRuleComplete: (ruleId, findingsCount) => {
501
+ session.progress.completedRules++;
502
+ session.lastUsed = Date.now();
503
+ wsHandler?.broadcast(`scan:${sessionId}`, "rule-complete", { ruleId, findingsCount });
504
+ },
505
+ onFinding: (finding) => {
506
+ session.progress.findingsCount++;
507
+ session.lastUsed = Date.now();
508
+ wsHandler?.broadcast(`scan:${sessionId}`, "finding", { finding });
509
+ }
510
+ });
511
+ session.result = result;
512
+ session.status = "completed";
513
+ session.lastUsed = Date.now();
514
+ wsHandler?.broadcast(`scan:${sessionId}`, "completed", { summary: result.summary });
515
+ } catch (err) {
516
+ session.status = "error";
517
+ session.error = err instanceof Error ? err.message : "Scan failed";
518
+ session.lastUsed = Date.now();
519
+ wsHandler?.broadcast(`scan:${sessionId}`, "error", { message: session.error });
520
+ }
521
+ })();
522
+ return c.json({ data: { sessionId } });
523
+ });
524
+ app.get("/api/audit/status/:sessionId", (c) => {
525
+ const sessionId = c.req.param("sessionId");
526
+ const session = sessions2.get(sessionId);
527
+ if (!session) {
528
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
529
+ }
530
+ session.lastUsed = Date.now();
531
+ return c.json({
532
+ data: {
533
+ status: session.status,
534
+ progress: session.progress,
535
+ error: session.error,
536
+ result: session.status === "completed" ? session.result : void 0
537
+ }
538
+ });
539
+ });
540
+ app.get("/api/audit/result/:sessionId", (c) => {
541
+ const sessionId = c.req.param("sessionId");
542
+ const session = sessions2.get(sessionId);
543
+ if (!session) {
544
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
545
+ }
546
+ if (session.status !== "completed" || !session.result) {
547
+ return c.json({ error: "not_ready", message: "Scan not completed yet" }, 404);
548
+ }
549
+ session.lastUsed = Date.now();
550
+ return c.json({ data: session.result });
551
+ });
552
+ app.post("/api/audit/stop/:sessionId", async (c) => {
553
+ const sessionId = c.req.param("sessionId");
554
+ const session = sessions2.get(sessionId);
555
+ if (!session) {
556
+ return c.json({ data: { stopped: true } });
557
+ }
558
+ try {
559
+ await session.client.disconnect();
560
+ await session.processManager.shutdownAll();
561
+ } catch {
562
+ }
563
+ sessions2.delete(sessionId);
564
+ return c.json({ data: { stopped: true } });
565
+ });
566
+ }
567
+
568
+ // src/routes/benchmark.ts
569
+ import { benchmarkStartSchema } from "@mcpspec/shared";
570
+ import { MCPClient as MCPClient4, ProcessManagerImpl as ProcessManagerImpl4, BenchmarkRunner } from "@mcpspec/core";
571
+ import { randomUUID as randomUUID3 } from "crypto";
572
+ var sessions3 = /* @__PURE__ */ new Map();
573
+ var SESSION_TIMEOUT_MS3 = 10 * 60 * 1e3;
574
+ setInterval(() => {
575
+ const now = Date.now();
576
+ for (const [id, session] of sessions3) {
577
+ if (now - session.lastUsed > SESSION_TIMEOUT_MS3) {
578
+ session.client.disconnect().catch(() => {
579
+ });
580
+ session.processManager.shutdownAll().catch(() => {
581
+ });
582
+ sessions3.delete(id);
583
+ }
584
+ }
585
+ }, 6e4);
586
+ function benchmarkRoutes(app, wsHandler) {
587
+ app.post("/api/benchmark/start", async (c) => {
588
+ const body = await c.req.json();
589
+ const parsed = benchmarkStartSchema.safeParse(body);
590
+ if (!parsed.success) {
591
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
592
+ }
593
+ const sessionId = randomUUID3();
594
+ const processManager = new ProcessManagerImpl4();
595
+ const config = {
596
+ transport: parsed.data.transport,
597
+ command: parsed.data.command,
598
+ args: parsed.data.args,
599
+ url: parsed.data.url,
600
+ env: parsed.data.env
601
+ };
602
+ const session = {
603
+ client: null,
604
+ processManager,
605
+ status: "connecting",
606
+ progress: { completedIterations: 0, totalIterations: parsed.data.iterations, phase: "warmup" },
607
+ lastUsed: Date.now()
608
+ };
609
+ const client = new MCPClient4({ serverConfig: config, processManager });
610
+ session.client = client;
611
+ sessions3.set(sessionId, session);
612
+ (async () => {
613
+ try {
614
+ await client.connect();
615
+ session.status = "running";
616
+ const runner = new BenchmarkRunner();
617
+ const result = await runner.run(
618
+ client,
619
+ parsed.data.tool,
620
+ parsed.data.toolArgs,
621
+ {
622
+ iterations: parsed.data.iterations,
623
+ warmupIterations: parsed.data.warmup,
624
+ timeout: parsed.data.timeout
625
+ },
626
+ {
627
+ onWarmupStart: () => {
628
+ session.progress.phase = "warmup";
629
+ session.lastUsed = Date.now();
630
+ wsHandler?.broadcast(`benchmark:${sessionId}`, "warmup-start", {});
631
+ },
632
+ onIterationComplete: (iteration, total, durationMs) => {
633
+ session.progress.phase = "measuring";
634
+ session.progress.completedIterations = iteration;
635
+ session.progress.totalIterations = total;
636
+ session.lastUsed = Date.now();
637
+ wsHandler?.broadcast(`benchmark:${sessionId}`, "iteration", {
638
+ iteration,
639
+ duration: durationMs,
640
+ success: true
641
+ });
642
+ },
643
+ onComplete: (benchResult) => {
644
+ session.result = benchResult;
645
+ session.status = "completed";
646
+ session.lastUsed = Date.now();
647
+ wsHandler?.broadcast(`benchmark:${sessionId}`, "completed", { result: benchResult });
648
+ }
649
+ }
650
+ );
651
+ if (session.status !== "completed") {
652
+ session.result = result;
653
+ session.status = "completed";
654
+ session.lastUsed = Date.now();
655
+ }
656
+ } catch (err) {
657
+ session.status = "error";
658
+ session.error = err instanceof Error ? err.message : "Benchmark failed";
659
+ session.lastUsed = Date.now();
660
+ wsHandler?.broadcast(`benchmark:${sessionId}`, "error", { message: session.error });
661
+ }
662
+ })();
663
+ return c.json({ data: { sessionId } });
664
+ });
665
+ app.get("/api/benchmark/status/:sessionId", (c) => {
666
+ const sessionId = c.req.param("sessionId");
667
+ const session = sessions3.get(sessionId);
668
+ if (!session) {
669
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
670
+ }
671
+ session.lastUsed = Date.now();
672
+ return c.json({
673
+ data: {
674
+ status: session.status,
675
+ progress: session.progress,
676
+ error: session.error,
677
+ result: session.status === "completed" ? session.result : void 0
678
+ }
679
+ });
680
+ });
681
+ app.get("/api/benchmark/result/:sessionId", (c) => {
682
+ const sessionId = c.req.param("sessionId");
683
+ const session = sessions3.get(sessionId);
684
+ if (!session) {
685
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
686
+ }
687
+ if (session.status !== "completed" || !session.result) {
688
+ return c.json({ error: "not_ready", message: "Benchmark not completed yet" }, 404);
689
+ }
690
+ session.lastUsed = Date.now();
691
+ return c.json({ data: session.result });
692
+ });
693
+ app.get("/api/benchmark/tools/:sessionId", async (c) => {
694
+ const sessionId = c.req.param("sessionId");
695
+ const session = sessions3.get(sessionId);
696
+ if (!session) {
697
+ return c.json({ error: "not_found", message: "Session not found or expired" }, 404);
698
+ }
699
+ session.lastUsed = Date.now();
700
+ try {
701
+ const tools = await session.client.listTools();
702
+ return c.json({ data: tools });
703
+ } catch (err) {
704
+ const message = err instanceof Error ? err.message : "Failed to list tools";
705
+ return c.json({ error: "tool_error", message }, 500);
706
+ }
707
+ });
708
+ app.post("/api/benchmark/stop/:sessionId", async (c) => {
709
+ const sessionId = c.req.param("sessionId");
710
+ const session = sessions3.get(sessionId);
711
+ if (!session) {
712
+ return c.json({ data: { stopped: true } });
713
+ }
714
+ try {
715
+ await session.client.disconnect();
716
+ await session.processManager.shutdownAll();
717
+ } catch {
718
+ }
719
+ sessions3.delete(sessionId);
720
+ return c.json({ data: { stopped: true } });
721
+ });
722
+ }
723
+
724
+ // src/routes/docs.ts
725
+ import { docsGenerateSchema } from "@mcpspec/shared";
726
+ import { MCPClient as MCPClient5, ProcessManagerImpl as ProcessManagerImpl5, DocGenerator } from "@mcpspec/core";
727
+ function docsRoutes(app) {
728
+ app.post("/api/docs/generate", async (c) => {
729
+ const body = await c.req.json();
730
+ const parsed = docsGenerateSchema.safeParse(body);
731
+ if (!parsed.success) {
732
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
733
+ }
734
+ const processManager = new ProcessManagerImpl5();
735
+ const config = {
736
+ transport: parsed.data.transport,
737
+ command: parsed.data.command,
738
+ args: parsed.data.args,
739
+ url: parsed.data.url,
740
+ env: parsed.data.env
741
+ };
742
+ const client = new MCPClient5({ serverConfig: config, processManager });
743
+ try {
744
+ await client.connect();
745
+ const generator = new DocGenerator();
746
+ const content = await generator.generate(client, {
747
+ format: parsed.data.format
748
+ });
749
+ await client.disconnect();
750
+ await processManager.shutdownAll();
751
+ return c.json({ data: { content, format: parsed.data.format } });
752
+ } catch (err) {
753
+ await client.disconnect().catch(() => {
754
+ });
755
+ await processManager.shutdownAll().catch(() => {
756
+ });
757
+ return c.json(
758
+ { error: "generation_error", message: err instanceof Error ? err.message : "Failed to generate docs" },
759
+ 500
760
+ );
761
+ }
762
+ });
763
+ }
764
+
765
+ // src/routes/score.ts
766
+ import { scoreCalculateSchema } from "@mcpspec/shared";
767
+ import { MCPClient as MCPClient6, ProcessManagerImpl as ProcessManagerImpl6, MCPScoreCalculator, BadgeGenerator } from "@mcpspec/core";
768
+ import { randomUUID as randomUUID4 } from "crypto";
769
+ function scoreRoutes(app, wsHandler) {
770
+ app.post("/api/score/calculate", async (c) => {
771
+ const body = await c.req.json();
772
+ const parsed = scoreCalculateSchema.safeParse(body);
773
+ if (!parsed.success) {
774
+ return c.json({ error: "validation_error", message: parsed.error.message }, 400);
775
+ }
776
+ const sessionId = randomUUID4();
777
+ const processManager = new ProcessManagerImpl6();
778
+ const config = {
779
+ transport: parsed.data.transport,
780
+ command: parsed.data.command,
781
+ args: parsed.data.args,
782
+ url: parsed.data.url,
783
+ env: parsed.data.env
784
+ };
785
+ const client = new MCPClient6({ serverConfig: config, processManager });
786
+ try {
787
+ await client.connect();
788
+ const calculator = new MCPScoreCalculator();
789
+ const score = await calculator.calculate(client, {
790
+ onCategoryStart: (category) => {
791
+ wsHandler?.broadcast(`score:${sessionId}`, "category-start", { category });
792
+ },
793
+ onCategoryComplete: (category, categoryScore) => {
794
+ wsHandler?.broadcast(`score:${sessionId}`, "category-complete", { category, score: categoryScore });
795
+ }
796
+ });
797
+ await client.disconnect();
798
+ await processManager.shutdownAll();
799
+ return c.json({ data: { sessionId, score } });
800
+ } catch (err) {
801
+ await client.disconnect().catch(() => {
802
+ });
803
+ await processManager.shutdownAll().catch(() => {
804
+ });
805
+ return c.json(
806
+ { error: "score_error", message: err instanceof Error ? err.message : "Failed to calculate score" },
807
+ 500
808
+ );
809
+ }
810
+ });
811
+ app.post("/api/score/badge", async (c) => {
812
+ const body = await c.req.json();
813
+ const score = body.score;
814
+ if (!score || typeof score.overall !== "number") {
815
+ return c.json({ error: "validation_error", message: "Invalid score object" }, 400);
816
+ }
817
+ const generator = new BadgeGenerator();
818
+ const svg = generator.generate(score);
819
+ return c.json({ data: { svg } });
820
+ });
821
+ }
822
+
823
+ // src/routes/index.ts
824
+ function registerRoutes(app, db, wsHandler) {
825
+ serversRoutes(app, db);
826
+ collectionsRoutes(app, db);
827
+ runsRoutes(app, db);
828
+ inspectRoutes(app, wsHandler);
829
+ auditRoutes(app, wsHandler);
830
+ benchmarkRoutes(app, wsHandler);
831
+ docsRoutes(app);
832
+ scoreRoutes(app, wsHandler);
833
+ }
834
+
835
+ // src/app.ts
836
+ import { readFileSync, existsSync } from "fs";
837
+ import { join, extname } from "path";
838
+ var MIME_TYPES = {
839
+ ".html": "text/html",
840
+ ".js": "application/javascript",
841
+ ".css": "text/css",
842
+ ".json": "application/json",
843
+ ".png": "image/png",
844
+ ".jpg": "image/jpeg",
845
+ ".svg": "image/svg+xml",
846
+ ".ico": "image/x-icon",
847
+ ".woff": "font/woff",
848
+ ".woff2": "font/woff2"
849
+ };
850
+ function createApp(options) {
851
+ const app = new Hono();
852
+ app.use("*", localhostOnly());
853
+ app.use("*", cors({ origin: "*" }));
854
+ app.use("/api/*", authMiddleware());
855
+ registerRoutes(app, options.db, options.wsHandler);
856
+ if (options.uiDistPath) {
857
+ app.get("*", (c) => {
858
+ const urlPath = new URL(c.req.url).pathname;
859
+ const filePath = join(options.uiDistPath, urlPath === "/" ? "index.html" : urlPath);
860
+ if (existsSync(filePath) && !filePath.endsWith("/")) {
861
+ const ext = extname(filePath);
862
+ const mime = MIME_TYPES[ext] ?? "application/octet-stream";
863
+ const content = readFileSync(filePath);
864
+ return c.body(content, 200, { "Content-Type": mime });
865
+ }
866
+ const indexPath = join(options.uiDistPath, "index.html");
867
+ if (existsSync(indexPath)) {
868
+ const content = readFileSync(indexPath, "utf-8");
869
+ return c.html(content);
870
+ }
871
+ return c.json({ error: "not_found", message: "UI not found" }, 404);
872
+ });
873
+ }
874
+ return app;
875
+ }
876
+
877
+ // src/start.ts
878
+ import { serve } from "@hono/node-server";
879
+
880
+ // src/websocket.ts
881
+ import { WebSocketServer, WebSocket } from "ws";
882
+ var WebSocketHandler = class {
883
+ wss;
884
+ subscriptions = /* @__PURE__ */ new Map();
885
+ constructor() {
886
+ this.wss = new WebSocketServer({ noServer: true });
887
+ this.wss.on("connection", (ws) => this.onConnection(ws));
888
+ }
889
+ handleUpgrade(req, socket, head) {
890
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
891
+ this.wss.emit("connection", ws, req);
892
+ });
893
+ }
894
+ onConnection(ws) {
895
+ this.subscriptions.set(ws, /* @__PURE__ */ new Set());
896
+ ws.on("message", (raw) => {
897
+ try {
898
+ const msg = JSON.parse(raw.toString());
899
+ this.handleMessage(ws, msg);
900
+ } catch {
901
+ }
902
+ });
903
+ ws.on("close", () => {
904
+ this.subscriptions.delete(ws);
905
+ });
906
+ ws.on("error", () => {
907
+ this.subscriptions.delete(ws);
908
+ });
909
+ }
910
+ handleMessage(ws, msg) {
911
+ switch (msg.type) {
912
+ case "subscribe": {
913
+ const channels = this.subscriptions.get(ws);
914
+ if (channels) {
915
+ channels.add(msg.channel);
916
+ this.send(ws, { type: "subscribed", channel: msg.channel });
917
+ }
918
+ break;
919
+ }
920
+ case "unsubscribe": {
921
+ const channels = this.subscriptions.get(ws);
922
+ if (channels) {
923
+ channels.delete(msg.channel);
924
+ }
925
+ break;
926
+ }
927
+ case "ping": {
928
+ this.send(ws, { type: "pong" });
929
+ break;
930
+ }
931
+ }
932
+ }
933
+ broadcast(channel, event, data) {
934
+ const message = { type: "event", channel, event, data };
935
+ const payload = JSON.stringify(message);
936
+ for (const [ws, channels] of this.subscriptions) {
937
+ if (channels.has(channel) && ws.readyState === WebSocket.OPEN) {
938
+ ws.send(payload);
939
+ }
940
+ }
941
+ }
942
+ send(ws, msg) {
943
+ if (ws.readyState === WebSocket.OPEN) {
944
+ ws.send(JSON.stringify(msg));
945
+ }
946
+ }
947
+ closeAll() {
948
+ for (const ws of this.subscriptions.keys()) {
949
+ ws.close();
950
+ }
951
+ this.subscriptions.clear();
952
+ this.wss.close();
953
+ }
954
+ get clientCount() {
955
+ return this.subscriptions.size;
956
+ }
957
+ };
958
+
959
+ // src/db/database.ts
960
+ import initSqlJs from "sql.js";
961
+ import { mkdirSync, readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
962
+ import { dirname } from "path";
963
+ import { randomUUID as randomUUID5 } from "crypto";
964
+
965
+ // src/db/migrations.ts
966
+ var MIGRATIONS = [
967
+ {
968
+ version: 1,
969
+ up: [
970
+ `CREATE TABLE IF NOT EXISTS schema_version (
971
+ version INTEGER NOT NULL
972
+ )`,
973
+ `CREATE TABLE IF NOT EXISTS server_connections (
974
+ id TEXT PRIMARY KEY,
975
+ name TEXT NOT NULL,
976
+ transport TEXT NOT NULL DEFAULT 'stdio',
977
+ command TEXT,
978
+ args TEXT, -- JSON array
979
+ url TEXT,
980
+ env TEXT, -- JSON object
981
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
982
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
983
+ )`,
984
+ `CREATE TABLE IF NOT EXISTS collections (
985
+ id TEXT PRIMARY KEY,
986
+ name TEXT NOT NULL,
987
+ description TEXT,
988
+ yaml TEXT NOT NULL,
989
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
990
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
991
+ )`,
992
+ `CREATE TABLE IF NOT EXISTS test_runs (
993
+ id TEXT PRIMARY KEY,
994
+ collection_id TEXT,
995
+ collection_name TEXT NOT NULL,
996
+ server_id TEXT,
997
+ status TEXT NOT NULL DEFAULT 'running',
998
+ summary TEXT, -- JSON
999
+ results TEXT, -- JSON
1000
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
1001
+ completed_at TEXT,
1002
+ duration INTEGER
1003
+ )`,
1004
+ `INSERT INTO schema_version (version) VALUES (1)`
1005
+ ]
1006
+ }
1007
+ ];
1008
+ function getSchemaVersion(db) {
1009
+ try {
1010
+ const result = db.exec("SELECT version FROM schema_version ORDER BY version DESC LIMIT 1");
1011
+ if (result.length > 0 && result[0].values.length > 0) {
1012
+ return result[0].values[0][0];
1013
+ }
1014
+ } catch {
1015
+ }
1016
+ return 0;
1017
+ }
1018
+ function runMigrations(db) {
1019
+ const currentVersion = getSchemaVersion(db);
1020
+ for (const migration of MIGRATIONS) {
1021
+ if (migration.version > currentVersion) {
1022
+ for (const sql of migration.up) {
1023
+ db.run(sql);
1024
+ }
1025
+ }
1026
+ }
1027
+ }
1028
+
1029
+ // src/db/database.ts
1030
+ var Database = class {
1031
+ db;
1032
+ dbPath;
1033
+ constructor(dbPath) {
1034
+ this.dbPath = dbPath ?? null;
1035
+ }
1036
+ async init() {
1037
+ const SQL = await initSqlJs();
1038
+ if (this.dbPath && existsSync2(this.dbPath)) {
1039
+ const buffer = readFileSync2(this.dbPath);
1040
+ this.db = new SQL.Database(buffer);
1041
+ } else {
1042
+ this.db = new SQL.Database();
1043
+ }
1044
+ this.db.run("PRAGMA journal_mode=WAL");
1045
+ this.db.run("PRAGMA foreign_keys=ON");
1046
+ runMigrations(this.db);
1047
+ this.save();
1048
+ }
1049
+ save() {
1050
+ if (!this.dbPath) return;
1051
+ mkdirSync(dirname(this.dbPath), { recursive: true });
1052
+ const data = this.db.export();
1053
+ writeFileSync(this.dbPath, Buffer.from(data));
1054
+ }
1055
+ close() {
1056
+ this.db.close();
1057
+ }
1058
+ // --- Server Connections ---
1059
+ listServers() {
1060
+ const stmt = this.db.prepare("SELECT * FROM server_connections ORDER BY updated_at DESC");
1061
+ const rows = [];
1062
+ while (stmt.step()) {
1063
+ rows.push(this.rowToServer(stmt.getAsObject()));
1064
+ }
1065
+ stmt.free();
1066
+ return rows;
1067
+ }
1068
+ getServer(id) {
1069
+ const stmt = this.db.prepare("SELECT * FROM server_connections WHERE id = ?");
1070
+ stmt.bind([id]);
1071
+ if (!stmt.step()) {
1072
+ stmt.free();
1073
+ return null;
1074
+ }
1075
+ const row = this.rowToServer(stmt.getAsObject());
1076
+ stmt.free();
1077
+ return row;
1078
+ }
1079
+ createServer(data) {
1080
+ const id = randomUUID5();
1081
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1082
+ this.db.run(
1083
+ `INSERT INTO server_connections (id, name, transport, command, args, url, env, created_at, updated_at)
1084
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1085
+ [id, data.name, data.transport, data.command ?? null, data.args ? JSON.stringify(data.args) : null, data.url ?? null, data.env ? JSON.stringify(data.env) : null, now, now]
1086
+ );
1087
+ this.save();
1088
+ return this.getServer(id);
1089
+ }
1090
+ updateServer(id, data) {
1091
+ const existing = this.getServer(id);
1092
+ if (!existing) return null;
1093
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1094
+ this.db.run(
1095
+ `UPDATE server_connections SET name=?, transport=?, command=?, args=?, url=?, env=?, updated_at=? WHERE id=?`,
1096
+ [
1097
+ data.name ?? existing.name,
1098
+ data.transport ?? existing.transport,
1099
+ data.command !== void 0 ? data.command ?? null : existing.command ?? null,
1100
+ data.args !== void 0 ? data.args ? JSON.stringify(data.args) : null : existing.args ? JSON.stringify(existing.args) : null,
1101
+ data.url !== void 0 ? data.url ?? null : existing.url ?? null,
1102
+ data.env !== void 0 ? data.env ? JSON.stringify(data.env) : null : existing.env ? JSON.stringify(existing.env) : null,
1103
+ now,
1104
+ id
1105
+ ]
1106
+ );
1107
+ this.save();
1108
+ return this.getServer(id);
1109
+ }
1110
+ deleteServer(id) {
1111
+ const existing = this.getServer(id);
1112
+ if (!existing) return false;
1113
+ this.db.run("DELETE FROM server_connections WHERE id = ?", [id]);
1114
+ this.save();
1115
+ return true;
1116
+ }
1117
+ // --- Collections ---
1118
+ listCollections() {
1119
+ const stmt = this.db.prepare("SELECT * FROM collections ORDER BY updated_at DESC");
1120
+ const rows = [];
1121
+ while (stmt.step()) {
1122
+ rows.push(this.rowToCollection(stmt.getAsObject()));
1123
+ }
1124
+ stmt.free();
1125
+ return rows;
1126
+ }
1127
+ getCollection(id) {
1128
+ const stmt = this.db.prepare("SELECT * FROM collections WHERE id = ?");
1129
+ stmt.bind([id]);
1130
+ if (!stmt.step()) {
1131
+ stmt.free();
1132
+ return null;
1133
+ }
1134
+ const row = this.rowToCollection(stmt.getAsObject());
1135
+ stmt.free();
1136
+ return row;
1137
+ }
1138
+ createCollection(data) {
1139
+ const id = randomUUID5();
1140
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1141
+ this.db.run(
1142
+ `INSERT INTO collections (id, name, description, yaml, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
1143
+ [id, data.name, data.description ?? null, data.yaml, now, now]
1144
+ );
1145
+ this.save();
1146
+ return this.getCollection(id);
1147
+ }
1148
+ updateCollection(id, data) {
1149
+ const existing = this.getCollection(id);
1150
+ if (!existing) return null;
1151
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1152
+ this.db.run(
1153
+ `UPDATE collections SET name=?, description=?, yaml=?, updated_at=? WHERE id=?`,
1154
+ [
1155
+ data.name ?? existing.name,
1156
+ data.description !== void 0 ? data.description ?? null : existing.description ?? null,
1157
+ data.yaml ?? existing.yaml,
1158
+ now,
1159
+ id
1160
+ ]
1161
+ );
1162
+ this.save();
1163
+ return this.getCollection(id);
1164
+ }
1165
+ deleteCollection(id) {
1166
+ const existing = this.getCollection(id);
1167
+ if (!existing) return false;
1168
+ this.db.run("DELETE FROM collections WHERE id = ?", [id]);
1169
+ this.save();
1170
+ return true;
1171
+ }
1172
+ // --- Test Runs ---
1173
+ listRuns(limit = 50) {
1174
+ const stmt = this.db.prepare("SELECT * FROM test_runs ORDER BY started_at DESC LIMIT ?");
1175
+ stmt.bind([limit]);
1176
+ const rows = [];
1177
+ while (stmt.step()) {
1178
+ rows.push(this.rowToRun(stmt.getAsObject()));
1179
+ }
1180
+ stmt.free();
1181
+ return rows;
1182
+ }
1183
+ getRun(id) {
1184
+ const stmt = this.db.prepare("SELECT * FROM test_runs WHERE id = ?");
1185
+ stmt.bind([id]);
1186
+ if (!stmt.step()) {
1187
+ stmt.free();
1188
+ return null;
1189
+ }
1190
+ const row = this.rowToRun(stmt.getAsObject());
1191
+ stmt.free();
1192
+ return row;
1193
+ }
1194
+ createRun(data) {
1195
+ const id = randomUUID5();
1196
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1197
+ this.db.run(
1198
+ `INSERT INTO test_runs (id, collection_id, collection_name, server_id, status, started_at) VALUES (?, ?, ?, ?, 'running', ?)`,
1199
+ [id, data.collectionId ?? null, data.collectionName, data.serverId ?? null, now]
1200
+ );
1201
+ this.save();
1202
+ return this.getRun(id);
1203
+ }
1204
+ updateRun(id, data) {
1205
+ const existing = this.getRun(id);
1206
+ if (!existing) return null;
1207
+ this.db.run(
1208
+ `UPDATE test_runs SET status=?, summary=?, results=?, completed_at=?, duration=? WHERE id=?`,
1209
+ [
1210
+ data.status ?? existing.status,
1211
+ data.summary ? JSON.stringify(data.summary) : existing.summary ? JSON.stringify(existing.summary) : null,
1212
+ data.results ? JSON.stringify(data.results) : existing.results ? JSON.stringify(existing.results) : null,
1213
+ data.completedAt ?? existing.completedAt ?? null,
1214
+ data.duration ?? existing.duration ?? null,
1215
+ id
1216
+ ]
1217
+ );
1218
+ this.save();
1219
+ return this.getRun(id);
1220
+ }
1221
+ deleteRun(id) {
1222
+ const existing = this.getRun(id);
1223
+ if (!existing) return false;
1224
+ this.db.run("DELETE FROM test_runs WHERE id = ?", [id]);
1225
+ this.save();
1226
+ return true;
1227
+ }
1228
+ // --- Row mappers ---
1229
+ rowToServer(row) {
1230
+ return {
1231
+ id: row["id"],
1232
+ name: row["name"],
1233
+ transport: row["transport"],
1234
+ command: row["command"] || void 0,
1235
+ args: row["args"] ? JSON.parse(row["args"]) : void 0,
1236
+ url: row["url"] || void 0,
1237
+ env: row["env"] ? JSON.parse(row["env"]) : void 0,
1238
+ createdAt: row["created_at"],
1239
+ updatedAt: row["updated_at"]
1240
+ };
1241
+ }
1242
+ rowToCollection(row) {
1243
+ return {
1244
+ id: row["id"],
1245
+ name: row["name"],
1246
+ description: row["description"] || void 0,
1247
+ yaml: row["yaml"],
1248
+ createdAt: row["created_at"],
1249
+ updatedAt: row["updated_at"]
1250
+ };
1251
+ }
1252
+ rowToRun(row) {
1253
+ return {
1254
+ id: row["id"],
1255
+ collectionId: row["collection_id"] || void 0,
1256
+ collectionName: row["collection_name"],
1257
+ serverId: row["server_id"] || void 0,
1258
+ status: row["status"],
1259
+ summary: row["summary"] ? JSON.parse(row["summary"]) : void 0,
1260
+ results: row["results"] ? JSON.parse(row["results"]) : void 0,
1261
+ startedAt: row["started_at"],
1262
+ completedAt: row["completed_at"] || void 0,
1263
+ duration: row["duration"] || void 0
1264
+ };
1265
+ }
1266
+ };
1267
+
1268
+ // src/start.ts
1269
+ import { getPlatformInfo } from "@mcpspec/core";
1270
+ import { join as join2 } from "path";
1271
+ async function startServer(options = {}) {
1272
+ const port = options.port ?? 6274;
1273
+ const host = options.host ?? "127.0.0.1";
1274
+ const dbPath = options.dbPath ?? join2(getPlatformInfo().dataDir, "mcpspec.db");
1275
+ const db = new Database(dbPath);
1276
+ await db.init();
1277
+ const wsHandler = new WebSocketHandler();
1278
+ const app = createApp({ db, uiDistPath: options.uiDistPath, wsHandler });
1279
+ const server = serve({ fetch: app.fetch, port, hostname: host }, (info) => {
1280
+ console.log(`MCPSpec server running at http://${host}:${info.port}`);
1281
+ });
1282
+ server.on("upgrade", (req, socket, head) => {
1283
+ if (req.url === "/ws") {
1284
+ wsHandler.handleUpgrade(req, socket, head);
1285
+ } else {
1286
+ socket.destroy();
1287
+ }
1288
+ });
1289
+ return {
1290
+ port,
1291
+ host,
1292
+ app,
1293
+ db,
1294
+ wsHandler,
1295
+ close: () => {
1296
+ wsHandler.closeAll();
1297
+ db.close();
1298
+ server.close();
1299
+ }
1300
+ };
1301
+ }
1302
+
1303
+ // src/index.ts
1304
+ var __serverDir = dirname2(fileURLToPath(import.meta.url));
1305
+ var UI_DIST_PATH = join3(__serverDir, "..", "ui-dist");
1306
+ export {
1307
+ Database,
1308
+ UI_DIST_PATH,
1309
+ WebSocketHandler,
1310
+ createApp,
1311
+ startServer
1312
+ };