@sparkleideas/testing 3.0.0-alpha.7

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.
Files changed (42) hide show
  1. package/README.md +547 -0
  2. package/__tests__/framework.test.ts +21 -0
  3. package/package.json +61 -0
  4. package/src/fixtures/agent-fixtures.ts +793 -0
  5. package/src/fixtures/agents.ts +212 -0
  6. package/src/fixtures/configurations.ts +491 -0
  7. package/src/fixtures/index.ts +21 -0
  8. package/src/fixtures/mcp-fixtures.ts +1030 -0
  9. package/src/fixtures/memory-entries.ts +328 -0
  10. package/src/fixtures/memory-fixtures.ts +750 -0
  11. package/src/fixtures/swarm-fixtures.ts +837 -0
  12. package/src/fixtures/tasks.ts +309 -0
  13. package/src/helpers/assertion-helpers.ts +616 -0
  14. package/src/helpers/assertions.ts +286 -0
  15. package/src/helpers/create-mock.ts +200 -0
  16. package/src/helpers/index.ts +182 -0
  17. package/src/helpers/mock-factory.ts +711 -0
  18. package/src/helpers/setup-teardown.ts +678 -0
  19. package/src/helpers/swarm-instance.ts +326 -0
  20. package/src/helpers/test-application.ts +310 -0
  21. package/src/helpers/test-utils.ts +670 -0
  22. package/src/index.ts +232 -0
  23. package/src/mocks/index.ts +29 -0
  24. package/src/mocks/mock-mcp-client.ts +723 -0
  25. package/src/mocks/mock-services.ts +793 -0
  26. package/src/regression/api-contract.ts +473 -0
  27. package/src/regression/index.ts +46 -0
  28. package/src/regression/integration-regression.ts +416 -0
  29. package/src/regression/performance-baseline.ts +356 -0
  30. package/src/regression/regression-runner.ts +339 -0
  31. package/src/regression/security-regression.ts +331 -0
  32. package/src/setup.ts +127 -0
  33. package/src/v2-compat/api-compat.test.ts +590 -0
  34. package/src/v2-compat/cli-compat.test.ts +484 -0
  35. package/src/v2-compat/compatibility-validator.ts +1072 -0
  36. package/src/v2-compat/hooks-compat.test.ts +602 -0
  37. package/src/v2-compat/index.ts +58 -0
  38. package/src/v2-compat/mcp-compat.test.ts +557 -0
  39. package/src/v2-compat/report-generator.ts +441 -0
  40. package/tmp.json +0 -0
  41. package/tsconfig.json +20 -0
  42. package/vitest.config.ts +12 -0
@@ -0,0 +1,723 @@
1
+ /**
2
+ * @sparkleideas/testing - Mock MCP Client
3
+ *
4
+ * Comprehensive mock MCP client for testing CLI and server interactions.
5
+ * Simulates full MCP protocol behavior with request/response tracking.
6
+ */
7
+ import { vi, type Mock } from 'vitest';
8
+ import type {
9
+ MCPTool,
10
+ MCPToolResult,
11
+ MCPResource,
12
+ MCPPrompt,
13
+ MCPServerConfig,
14
+ MCPSessionContext,
15
+ MCPError,
16
+ MCPContent,
17
+ mcpTools,
18
+ mcpResources,
19
+ mcpPrompts,
20
+ mcpToolResults,
21
+ mcpErrors,
22
+ } from '../fixtures/mcp-fixtures.js';
23
+
24
+ /**
25
+ * Mock MCP Client with full protocol simulation
26
+ */
27
+ export class MockMCPClient {
28
+ private _connected = false;
29
+ private _session: MCPSessionContext | null = null;
30
+ private _tools = new Map<string, MCPTool>();
31
+ private _resources = new Map<string, MCPResource>();
32
+ private _prompts = new Map<string, MCPPrompt>();
33
+ private _requestHistory: MCPRequest[] = [];
34
+ private _responseHistory: MCPResponse[] = [];
35
+ private _toolHandlers = new Map<string, ToolHandler>();
36
+ private _errorSimulation: ErrorSimulation | null = null;
37
+ private _latencySimulation = 0;
38
+
39
+ // Mock methods for verification
40
+ connect = vi.fn(async () => {
41
+ if (this._errorSimulation?.onConnect) {
42
+ throw new Error(this._errorSimulation.onConnect);
43
+ }
44
+
45
+ await this.simulateLatency();
46
+
47
+ this._connected = true;
48
+ this._session = {
49
+ sessionId: `session-${Date.now()}`,
50
+ clientInfo: {
51
+ name: 'mock-client',
52
+ version: '1.0.0',
53
+ },
54
+ capabilities: {
55
+ tools: true,
56
+ resources: true,
57
+ prompts: true,
58
+ },
59
+ startedAt: new Date(),
60
+ lastActivity: new Date(),
61
+ requestCount: 0,
62
+ };
63
+ });
64
+
65
+ disconnect = vi.fn(async () => {
66
+ if (this._errorSimulation?.onDisconnect) {
67
+ throw new Error(this._errorSimulation.onDisconnect);
68
+ }
69
+
70
+ this._connected = false;
71
+ this._session = null;
72
+ });
73
+
74
+ callTool = vi.fn(async (name: string, params: Record<string, unknown>): Promise<MCPToolResult> => {
75
+ this.ensureConnected();
76
+
77
+ const request: MCPRequest = {
78
+ id: `req-${Date.now()}`,
79
+ method: 'tools/call',
80
+ params: { name, arguments: params },
81
+ timestamp: new Date(),
82
+ };
83
+ this._requestHistory.push(request);
84
+
85
+ if (this._session) {
86
+ this._session.requestCount++;
87
+ this._session.lastActivity = new Date();
88
+ }
89
+
90
+ await this.simulateLatency();
91
+
92
+ // Check for error simulation
93
+ if (this._errorSimulation?.onToolCall?.[name]) {
94
+ const error = this._errorSimulation.onToolCall[name];
95
+ const response: MCPResponse = {
96
+ id: request.id,
97
+ error: typeof error === 'string' ? { code: -32000, message: error } : error,
98
+ timestamp: new Date(),
99
+ };
100
+ this._responseHistory.push(response);
101
+ throw new MCPClientError(response.error!.message, response.error!.code);
102
+ }
103
+
104
+ // Check for custom handler
105
+ const handler = this._toolHandlers.get(name);
106
+ if (handler) {
107
+ const result = await handler(params);
108
+ const response: MCPResponse = {
109
+ id: request.id,
110
+ result,
111
+ timestamp: new Date(),
112
+ };
113
+ this._responseHistory.push(response);
114
+ return result;
115
+ }
116
+
117
+ // Check for registered tool
118
+ const tool = this._tools.get(name);
119
+ if (!tool) {
120
+ const response: MCPResponse = {
121
+ id: request.id,
122
+ error: { code: -32601, message: `Tool not found: ${name}` },
123
+ timestamp: new Date(),
124
+ };
125
+ this._responseHistory.push(response);
126
+ throw new MCPClientError(`Tool not found: ${name}`, -32601);
127
+ }
128
+
129
+ // Default success response
130
+ const result: MCPToolResult = {
131
+ content: [{ type: 'text', text: JSON.stringify({ success: true, tool: name, params }) }],
132
+ };
133
+ const response: MCPResponse = {
134
+ id: request.id,
135
+ result,
136
+ timestamp: new Date(),
137
+ };
138
+ this._responseHistory.push(response);
139
+
140
+ return result;
141
+ });
142
+
143
+ listTools = vi.fn(async (): Promise<MCPTool[]> => {
144
+ this.ensureConnected();
145
+ await this.simulateLatency();
146
+ return Array.from(this._tools.values());
147
+ });
148
+
149
+ readResource = vi.fn(async (uri: string): Promise<MCPResourceContent> => {
150
+ this.ensureConnected();
151
+
152
+ const request: MCPRequest = {
153
+ id: `req-${Date.now()}`,
154
+ method: 'resources/read',
155
+ params: { uri },
156
+ timestamp: new Date(),
157
+ };
158
+ this._requestHistory.push(request);
159
+
160
+ await this.simulateLatency();
161
+
162
+ const resource = this._resources.get(uri);
163
+ if (!resource) {
164
+ throw new MCPClientError(`Resource not found: ${uri}`, -32002);
165
+ }
166
+
167
+ return {
168
+ type: 'resource',
169
+ resource: {
170
+ uri,
171
+ mimeType: resource.mimeType,
172
+ text: JSON.stringify({ name: resource.name, description: resource.description }),
173
+ },
174
+ };
175
+ });
176
+
177
+ listResources = vi.fn(async (): Promise<MCPResource[]> => {
178
+ this.ensureConnected();
179
+ await this.simulateLatency();
180
+ return Array.from(this._resources.values());
181
+ });
182
+
183
+ getPrompt = vi.fn(async (name: string, args: Record<string, string>): Promise<MCPPromptResult> => {
184
+ this.ensureConnected();
185
+
186
+ const prompt = this._prompts.get(name);
187
+ if (!prompt) {
188
+ throw new MCPClientError(`Prompt not found: ${name}`, -32003);
189
+ }
190
+
191
+ await this.simulateLatency();
192
+
193
+ return {
194
+ messages: [
195
+ {
196
+ role: 'user',
197
+ content: { type: 'text', text: `Prompt: ${name}\nArgs: ${JSON.stringify(args)}` },
198
+ },
199
+ ],
200
+ };
201
+ });
202
+
203
+ listPrompts = vi.fn(async (): Promise<MCPPrompt[]> => {
204
+ this.ensureConnected();
205
+ await this.simulateLatency();
206
+ return Array.from(this._prompts.values());
207
+ });
208
+
209
+ isConnected = vi.fn(() => this._connected);
210
+
211
+ getSession = vi.fn(() => this._session);
212
+
213
+ /**
214
+ * Register a tool
215
+ */
216
+ registerTool(tool: MCPTool): void {
217
+ this._tools.set(tool.name, tool);
218
+ }
219
+
220
+ /**
221
+ * Register multiple tools
222
+ */
223
+ registerTools(tools: MCPTool[]): void {
224
+ for (const tool of tools) {
225
+ this.registerTool(tool);
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Register a custom tool handler
231
+ */
232
+ setToolHandler(name: string, handler: ToolHandler): void {
233
+ this._toolHandlers.set(name, handler);
234
+ }
235
+
236
+ /**
237
+ * Register a resource
238
+ */
239
+ registerResource(resource: MCPResource): void {
240
+ this._resources.set(resource.uri, resource);
241
+ }
242
+
243
+ /**
244
+ * Register multiple resources
245
+ */
246
+ registerResources(resources: MCPResource[]): void {
247
+ for (const resource of resources) {
248
+ this.registerResource(resource);
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Register a prompt
254
+ */
255
+ registerPrompt(prompt: MCPPrompt): void {
256
+ this._prompts.set(prompt.name, prompt);
257
+ }
258
+
259
+ /**
260
+ * Register multiple prompts
261
+ */
262
+ registerPrompts(prompts: MCPPrompt[]): void {
263
+ for (const prompt of prompts) {
264
+ this.registerPrompt(prompt);
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Configure error simulation
270
+ */
271
+ simulateErrors(config: ErrorSimulation): void {
272
+ this._errorSimulation = config;
273
+ }
274
+
275
+ /**
276
+ * Configure latency simulation
277
+ */
278
+ setLatency(ms: number): void {
279
+ this._latencySimulation = ms;
280
+ }
281
+
282
+ /**
283
+ * Get request history
284
+ */
285
+ getRequestHistory(): MCPRequest[] {
286
+ return [...this._requestHistory];
287
+ }
288
+
289
+ /**
290
+ * Get response history
291
+ */
292
+ getResponseHistory(): MCPResponse[] {
293
+ return [...this._responseHistory];
294
+ }
295
+
296
+ /**
297
+ * Get last request
298
+ */
299
+ getLastRequest(): MCPRequest | undefined {
300
+ return this._requestHistory[this._requestHistory.length - 1];
301
+ }
302
+
303
+ /**
304
+ * Get last response
305
+ */
306
+ getLastResponse(): MCPResponse | undefined {
307
+ return this._responseHistory[this._responseHistory.length - 1];
308
+ }
309
+
310
+ /**
311
+ * Clear history
312
+ */
313
+ clearHistory(): void {
314
+ this._requestHistory = [];
315
+ this._responseHistory = [];
316
+ }
317
+
318
+ /**
319
+ * Reset client to initial state
320
+ */
321
+ reset(): void {
322
+ this._connected = false;
323
+ this._session = null;
324
+ this._tools.clear();
325
+ this._resources.clear();
326
+ this._prompts.clear();
327
+ this._requestHistory = [];
328
+ this._responseHistory = [];
329
+ this._toolHandlers.clear();
330
+ this._errorSimulation = null;
331
+ this._latencySimulation = 0;
332
+ vi.clearAllMocks();
333
+ }
334
+
335
+ private ensureConnected(): void {
336
+ if (!this._connected) {
337
+ throw new MCPClientError('Not connected', -32000);
338
+ }
339
+ }
340
+
341
+ private async simulateLatency(): Promise<void> {
342
+ if (this._latencySimulation > 0) {
343
+ await new Promise(resolve => setTimeout(resolve, this._latencySimulation));
344
+ }
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Mock MCP Server for testing server-side behavior
350
+ */
351
+ export class MockMCPServer {
352
+ private _running = false;
353
+ private _config: MCPServerConfig | null = null;
354
+ private _tools = new Map<string, MCPTool>();
355
+ private _resources = new Map<string, MCPResource>();
356
+ private _prompts = new Map<string, MCPPrompt>();
357
+ private _connections: MockMCPConnection[] = [];
358
+ private _requestLog: MCPRequest[] = [];
359
+ private _errorCount = 0;
360
+
361
+ start = vi.fn(async (config: MCPServerConfig) => {
362
+ this._config = config;
363
+ this._running = true;
364
+
365
+ // Register configured tools/resources/prompts
366
+ if (config.tools) {
367
+ for (const tool of config.tools) {
368
+ this._tools.set(tool.name, tool);
369
+ }
370
+ }
371
+ if (config.resources) {
372
+ for (const resource of config.resources) {
373
+ this._resources.set(resource.uri, resource);
374
+ }
375
+ }
376
+ if (config.prompts) {
377
+ for (const prompt of config.prompts) {
378
+ this._prompts.set(prompt.name, prompt);
379
+ }
380
+ }
381
+ });
382
+
383
+ stop = vi.fn(async () => {
384
+ for (const conn of this._connections) {
385
+ await conn.close();
386
+ }
387
+ this._connections = [];
388
+ this._running = false;
389
+ });
390
+
391
+ handleRequest = vi.fn(async (request: MCPRequest): Promise<MCPResponse> => {
392
+ this._requestLog.push(request);
393
+
394
+ try {
395
+ switch (request.method) {
396
+ case 'tools/call':
397
+ return this.handleToolCall(request);
398
+ case 'resources/read':
399
+ return this.handleResourceRead(request);
400
+ case 'prompts/get':
401
+ return this.handlePromptGet(request);
402
+ case 'tools/list':
403
+ return { id: request.id, result: Array.from(this._tools.values()), timestamp: new Date() };
404
+ case 'resources/list':
405
+ return { id: request.id, result: Array.from(this._resources.values()), timestamp: new Date() };
406
+ case 'prompts/list':
407
+ return { id: request.id, result: Array.from(this._prompts.values()), timestamp: new Date() };
408
+ default:
409
+ throw new MCPClientError(`Unknown method: ${request.method}`, -32601);
410
+ }
411
+ } catch (error) {
412
+ this._errorCount++;
413
+ return {
414
+ id: request.id,
415
+ error: { code: -32000, message: (error as Error).message },
416
+ timestamp: new Date(),
417
+ };
418
+ }
419
+ });
420
+
421
+ registerTool = vi.fn((tool: MCPTool) => {
422
+ this._tools.set(tool.name, tool);
423
+ });
424
+
425
+ registerResource = vi.fn((resource: MCPResource) => {
426
+ this._resources.set(resource.uri, resource);
427
+ });
428
+
429
+ registerPrompt = vi.fn((prompt: MCPPrompt) => {
430
+ this._prompts.set(prompt.name, prompt);
431
+ });
432
+
433
+ getStatus = vi.fn((): MCPServerStatus => ({
434
+ running: this._running,
435
+ transport: this._config?.transport.type ?? 'stdio',
436
+ connectedClients: this._connections.length,
437
+ toolsRegistered: this._tools.size,
438
+ resourcesRegistered: this._resources.size,
439
+ promptsRegistered: this._prompts.size,
440
+ requestsHandled: this._requestLog.length,
441
+ errorsCount: this._errorCount,
442
+ uptime: this._running ? Date.now() : 0,
443
+ }));
444
+
445
+ /**
446
+ * Simulate a client connection
447
+ */
448
+ acceptConnection(): MockMCPConnection {
449
+ const conn = new MockMCPConnection(this);
450
+ this._connections.push(conn);
451
+ return conn;
452
+ }
453
+
454
+ /**
455
+ * Get request log
456
+ */
457
+ getRequestLog(): MCPRequest[] {
458
+ return [...this._requestLog];
459
+ }
460
+
461
+ /**
462
+ * Reset server
463
+ */
464
+ reset(): void {
465
+ this._running = false;
466
+ this._config = null;
467
+ this._tools.clear();
468
+ this._resources.clear();
469
+ this._prompts.clear();
470
+ this._connections = [];
471
+ this._requestLog = [];
472
+ this._errorCount = 0;
473
+ vi.clearAllMocks();
474
+ }
475
+
476
+ private handleToolCall(request: MCPRequest): MCPResponse {
477
+ const { name, arguments: params } = request.params as { name: string; arguments: Record<string, unknown> };
478
+ const tool = this._tools.get(name);
479
+
480
+ if (!tool) {
481
+ return {
482
+ id: request.id,
483
+ error: { code: -32601, message: `Tool not found: ${name}` },
484
+ timestamp: new Date(),
485
+ };
486
+ }
487
+
488
+ return {
489
+ id: request.id,
490
+ result: {
491
+ content: [{ type: 'text', text: JSON.stringify({ success: true, tool: name }) }],
492
+ },
493
+ timestamp: new Date(),
494
+ };
495
+ }
496
+
497
+ private handleResourceRead(request: MCPRequest): MCPResponse {
498
+ const { uri } = request.params as { uri: string };
499
+ const resource = this._resources.get(uri);
500
+
501
+ if (!resource) {
502
+ return {
503
+ id: request.id,
504
+ error: { code: -32002, message: `Resource not found: ${uri}` },
505
+ timestamp: new Date(),
506
+ };
507
+ }
508
+
509
+ return {
510
+ id: request.id,
511
+ result: {
512
+ type: 'resource',
513
+ resource: { uri, mimeType: resource.mimeType, text: '{}' },
514
+ },
515
+ timestamp: new Date(),
516
+ };
517
+ }
518
+
519
+ private handlePromptGet(request: MCPRequest): MCPResponse {
520
+ const { name } = request.params as { name: string };
521
+ const prompt = this._prompts.get(name);
522
+
523
+ if (!prompt) {
524
+ return {
525
+ id: request.id,
526
+ error: { code: -32003, message: `Prompt not found: ${name}` },
527
+ timestamp: new Date(),
528
+ };
529
+ }
530
+
531
+ return {
532
+ id: request.id,
533
+ result: {
534
+ messages: [{ role: 'user', content: { type: 'text', text: `Prompt: ${name}` } }],
535
+ },
536
+ timestamp: new Date(),
537
+ };
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Mock MCP Connection
543
+ */
544
+ export class MockMCPConnection {
545
+ private _open = true;
546
+ private _server: MockMCPServer;
547
+
548
+ constructor(server: MockMCPServer) {
549
+ this._server = server;
550
+ }
551
+
552
+ send = vi.fn(async (request: MCPRequest): Promise<MCPResponse> => {
553
+ if (!this._open) {
554
+ throw new MCPClientError('Connection closed', -32000);
555
+ }
556
+ return this._server.handleRequest(request);
557
+ });
558
+
559
+ close = vi.fn(async () => {
560
+ this._open = false;
561
+ });
562
+
563
+ isOpen(): boolean {
564
+ return this._open;
565
+ }
566
+ }
567
+
568
+ /**
569
+ * MCP Client Error
570
+ */
571
+ export class MCPClientError extends Error {
572
+ constructor(message: string, public code: number) {
573
+ super(message);
574
+ this.name = 'MCPClientError';
575
+ }
576
+ }
577
+
578
+ // Supporting types
579
+ interface MCPRequest {
580
+ id: string;
581
+ method: string;
582
+ params?: Record<string, unknown>;
583
+ timestamp: Date;
584
+ }
585
+
586
+ interface MCPResponse {
587
+ id: string;
588
+ result?: unknown;
589
+ error?: MCPError;
590
+ timestamp: Date;
591
+ }
592
+
593
+ interface MCPResourceContent {
594
+ type: 'resource';
595
+ resource: {
596
+ uri: string;
597
+ mimeType?: string;
598
+ text?: string;
599
+ blob?: string;
600
+ };
601
+ }
602
+
603
+ interface MCPPromptResult {
604
+ messages: Array<{
605
+ role: 'user' | 'assistant';
606
+ content: MCPContent;
607
+ }>;
608
+ }
609
+
610
+ interface MCPServerStatus {
611
+ running: boolean;
612
+ transport: string;
613
+ connectedClients: number;
614
+ toolsRegistered: number;
615
+ resourcesRegistered: number;
616
+ promptsRegistered: number;
617
+ requestsHandled: number;
618
+ errorsCount: number;
619
+ uptime: number;
620
+ }
621
+
622
+ type ToolHandler = (params: Record<string, unknown>) => Promise<MCPToolResult>;
623
+
624
+ interface ErrorSimulation {
625
+ onConnect?: string;
626
+ onDisconnect?: string;
627
+ onToolCall?: Record<string, string | MCPError>;
628
+ }
629
+
630
+ /**
631
+ * Create a pre-configured mock MCP client with standard tools
632
+ */
633
+ export function createStandardMockMCPClient(): MockMCPClient {
634
+ const client = new MockMCPClient();
635
+
636
+ // Register standard Claude-Flow tools
637
+ client.registerTool({
638
+ name: 'swarm_init',
639
+ description: 'Initialize a new swarm',
640
+ inputSchema: {
641
+ type: 'object',
642
+ properties: {
643
+ topology: { type: 'string' },
644
+ maxAgents: { type: 'number' },
645
+ },
646
+ required: ['topology'],
647
+ },
648
+ });
649
+
650
+ client.registerTool({
651
+ name: 'agent_spawn',
652
+ description: 'Spawn a new agent',
653
+ inputSchema: {
654
+ type: 'object',
655
+ properties: {
656
+ type: { type: 'string' },
657
+ name: { type: 'string' },
658
+ },
659
+ required: ['type'],
660
+ },
661
+ });
662
+
663
+ client.registerTool({
664
+ name: 'task_orchestrate',
665
+ description: 'Orchestrate a task',
666
+ inputSchema: {
667
+ type: 'object',
668
+ properties: {
669
+ taskName: { type: 'string' },
670
+ taskType: { type: 'string' },
671
+ },
672
+ required: ['taskName', 'taskType'],
673
+ },
674
+ });
675
+
676
+ client.registerTool({
677
+ name: 'memory_store',
678
+ description: 'Store a value in memory',
679
+ inputSchema: {
680
+ type: 'object',
681
+ properties: {
682
+ key: { type: 'string' },
683
+ value: { type: 'object' },
684
+ },
685
+ required: ['key', 'value'],
686
+ },
687
+ });
688
+
689
+ client.registerTool({
690
+ name: 'memory_search',
691
+ description: 'Search memory',
692
+ inputSchema: {
693
+ type: 'object',
694
+ properties: {
695
+ query: { type: 'string' },
696
+ topK: { type: 'number' },
697
+ },
698
+ required: ['query'],
699
+ },
700
+ });
701
+
702
+ return client;
703
+ }
704
+
705
+ /**
706
+ * Create a mock MCP client that simulates failures
707
+ */
708
+ export function createFailingMockMCPClient(
709
+ errorConfig: ErrorSimulation
710
+ ): MockMCPClient {
711
+ const client = new MockMCPClient();
712
+ client.simulateErrors(errorConfig);
713
+ return client;
714
+ }
715
+
716
+ /**
717
+ * Create a mock MCP client with latency
718
+ */
719
+ export function createSlowMockMCPClient(latencyMs: number): MockMCPClient {
720
+ const client = new MockMCPClient();
721
+ client.setLatency(latencyMs);
722
+ return client;
723
+ }