@frontmcp/testing 0.5.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.
Files changed (112) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1358 -0
  3. package/jest-preset.js +61 -0
  4. package/package.json +94 -0
  5. package/src/assertions/index.d.ts +5 -0
  6. package/src/assertions/index.js +18 -0
  7. package/src/assertions/index.js.map +1 -0
  8. package/src/assertions/mcp-assertions.d.ts +81 -0
  9. package/src/assertions/mcp-assertions.js +220 -0
  10. package/src/assertions/mcp-assertions.js.map +1 -0
  11. package/src/auth/auth-headers.d.ts +29 -0
  12. package/src/auth/auth-headers.js +62 -0
  13. package/src/auth/auth-headers.js.map +1 -0
  14. package/src/auth/index.d.ts +9 -0
  15. package/src/auth/index.js +15 -0
  16. package/src/auth/index.js.map +1 -0
  17. package/src/auth/token-factory.d.ts +94 -0
  18. package/src/auth/token-factory.js +181 -0
  19. package/src/auth/token-factory.js.map +1 -0
  20. package/src/auth/user-fixtures.d.ts +26 -0
  21. package/src/auth/user-fixtures.js +92 -0
  22. package/src/auth/user-fixtures.js.map +1 -0
  23. package/src/client/index.d.ts +7 -0
  24. package/src/client/index.js +12 -0
  25. package/src/client/index.js.map +1 -0
  26. package/src/client/mcp-test-client.builder.d.ts +72 -0
  27. package/src/client/mcp-test-client.builder.js +111 -0
  28. package/src/client/mcp-test-client.builder.js.map +1 -0
  29. package/src/client/mcp-test-client.d.ts +360 -0
  30. package/src/client/mcp-test-client.js +929 -0
  31. package/src/client/mcp-test-client.js.map +1 -0
  32. package/src/client/mcp-test-client.types.d.ts +216 -0
  33. package/src/client/mcp-test-client.types.js +7 -0
  34. package/src/client/mcp-test-client.types.js.map +1 -0
  35. package/src/errors/index.d.ts +45 -0
  36. package/src/errors/index.js +85 -0
  37. package/src/errors/index.js.map +1 -0
  38. package/src/expect.d.ts +67 -0
  39. package/src/expect.js +31 -0
  40. package/src/expect.js.map +1 -0
  41. package/src/fixtures/fixture-types.d.ts +166 -0
  42. package/src/fixtures/fixture-types.js +7 -0
  43. package/src/fixtures/fixture-types.js.map +1 -0
  44. package/src/fixtures/index.d.ts +7 -0
  45. package/src/fixtures/index.js +16 -0
  46. package/src/fixtures/index.js.map +1 -0
  47. package/src/fixtures/test-fixture.d.ts +41 -0
  48. package/src/fixtures/test-fixture.js +280 -0
  49. package/src/fixtures/test-fixture.js.map +1 -0
  50. package/src/http-mock/http-mock.d.ts +84 -0
  51. package/src/http-mock/http-mock.js +544 -0
  52. package/src/http-mock/http-mock.js.map +1 -0
  53. package/src/http-mock/http-mock.types.d.ts +124 -0
  54. package/src/http-mock/http-mock.types.js +10 -0
  55. package/src/http-mock/http-mock.types.js.map +1 -0
  56. package/src/http-mock/index.d.ts +6 -0
  57. package/src/http-mock/index.js +11 -0
  58. package/src/http-mock/index.js.map +1 -0
  59. package/src/index.d.ts +65 -0
  60. package/src/index.js +128 -0
  61. package/src/index.js.map +1 -0
  62. package/src/interceptor/index.d.ts +7 -0
  63. package/src/interceptor/index.js +15 -0
  64. package/src/interceptor/index.js.map +1 -0
  65. package/src/interceptor/interceptor-chain.d.ts +77 -0
  66. package/src/interceptor/interceptor-chain.js +207 -0
  67. package/src/interceptor/interceptor-chain.js.map +1 -0
  68. package/src/interceptor/interceptor.types.d.ts +131 -0
  69. package/src/interceptor/interceptor.types.js +7 -0
  70. package/src/interceptor/interceptor.types.js.map +1 -0
  71. package/src/interceptor/mock-registry.d.ts +82 -0
  72. package/src/interceptor/mock-registry.js +189 -0
  73. package/src/interceptor/mock-registry.js.map +1 -0
  74. package/src/matchers/index.d.ts +7 -0
  75. package/src/matchers/index.js +12 -0
  76. package/src/matchers/index.js.map +1 -0
  77. package/src/matchers/matcher-types.d.ts +266 -0
  78. package/src/matchers/matcher-types.js +10 -0
  79. package/src/matchers/matcher-types.js.map +1 -0
  80. package/src/matchers/mcp-matchers.d.ts +47 -0
  81. package/src/matchers/mcp-matchers.js +391 -0
  82. package/src/matchers/mcp-matchers.js.map +1 -0
  83. package/src/playwright/index.d.ts +37 -0
  84. package/src/playwright/index.js +49 -0
  85. package/src/playwright/index.js.map +1 -0
  86. package/src/server/index.d.ts +6 -0
  87. package/src/server/index.js +10 -0
  88. package/src/server/index.js.map +1 -0
  89. package/src/server/test-server.d.ts +99 -0
  90. package/src/server/test-server.js +286 -0
  91. package/src/server/test-server.js.map +1 -0
  92. package/src/setup.d.ts +22 -0
  93. package/src/setup.js +30 -0
  94. package/src/setup.js.map +1 -0
  95. package/src/transport/index.d.ts +6 -0
  96. package/src/transport/index.js +10 -0
  97. package/src/transport/index.js.map +1 -0
  98. package/src/transport/streamable-http.transport.d.ts +65 -0
  99. package/src/transport/streamable-http.transport.js +432 -0
  100. package/src/transport/streamable-http.transport.js.map +1 -0
  101. package/src/transport/transport.interface.d.ts +124 -0
  102. package/src/transport/transport.interface.js +7 -0
  103. package/src/transport/transport.interface.js.map +1 -0
  104. package/src/ui/index.d.ts +17 -0
  105. package/src/ui/index.js +23 -0
  106. package/src/ui/index.js.map +1 -0
  107. package/src/ui/ui-assertions.d.ts +94 -0
  108. package/src/ui/ui-assertions.js +215 -0
  109. package/src/ui/ui-assertions.js.map +1 -0
  110. package/src/ui/ui-matchers.d.ts +39 -0
  111. package/src/ui/ui-matchers.js +275 -0
  112. package/src/ui/ui-matchers.js.map +1 -0
@@ -0,0 +1,929 @@
1
+ "use strict";
2
+ /**
3
+ * @file mcp-test-client.ts
4
+ * @description Main MCP Test Client implementation for E2E testing
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.McpTestClient = void 0;
8
+ const mcp_test_client_builder_1 = require("./mcp-test-client.builder");
9
+ const streamable_http_transport_1 = require("../transport/streamable-http.transport");
10
+ const interceptor_1 = require("../interceptor");
11
+ // ═══════════════════════════════════════════════════════════════════
12
+ // CONSTANTS
13
+ // ═══════════════════════════════════════════════════════════════════
14
+ const DEFAULT_TIMEOUT = 30000;
15
+ const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
16
+ const DEFAULT_CLIENT_INFO = {
17
+ name: '@frontmcp/testing',
18
+ version: '0.4.0',
19
+ };
20
+ // ═══════════════════════════════════════════════════════════════════
21
+ // MAIN CLIENT CLASS
22
+ // ═══════════════════════════════════════════════════════════════════
23
+ class McpTestClient {
24
+ config;
25
+ transport = null;
26
+ initResult = null;
27
+ requestIdCounter = 0;
28
+ _lastRequestId = 0;
29
+ _sessionId;
30
+ _sessionInfo = null;
31
+ _authState = { isAnonymous: true, scopes: [] };
32
+ // Logging and tracing
33
+ _logs = [];
34
+ _traces = [];
35
+ _notifications = [];
36
+ _progressUpdates = [];
37
+ // Interceptor chain
38
+ _interceptors;
39
+ // ═══════════════════════════════════════════════════════════════════
40
+ // CONSTRUCTOR & FACTORY
41
+ // ═══════════════════════════════════════════════════════════════════
42
+ constructor(config) {
43
+ this.config = {
44
+ baseUrl: config.baseUrl,
45
+ transport: config.transport ?? 'streamable-http',
46
+ auth: config.auth ?? {},
47
+ publicMode: config.publicMode ?? false,
48
+ timeout: config.timeout ?? DEFAULT_TIMEOUT,
49
+ debug: config.debug ?? false,
50
+ protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
51
+ clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
52
+ };
53
+ // In public mode, user is always anonymous (no token authentication)
54
+ if (config.publicMode) {
55
+ this._authState = {
56
+ isAnonymous: true,
57
+ scopes: [],
58
+ };
59
+ }
60
+ else if (config.auth?.token) {
61
+ // Parse auth state from config
62
+ this._authState = {
63
+ isAnonymous: false,
64
+ token: config.auth.token,
65
+ scopes: this.parseScopesFromToken(config.auth.token),
66
+ user: this.parseUserFromToken(config.auth.token),
67
+ };
68
+ }
69
+ // Initialize interceptor chain
70
+ this._interceptors = new interceptor_1.DefaultInterceptorChain();
71
+ }
72
+ /**
73
+ * Create a new McpTestClientBuilder for fluent configuration
74
+ */
75
+ static create(config) {
76
+ return new mcp_test_client_builder_1.McpTestClientBuilder(config);
77
+ }
78
+ // ═══════════════════════════════════════════════════════════════════
79
+ // CONNECTION & LIFECYCLE
80
+ // ═══════════════════════════════════════════════════════════════════
81
+ /**
82
+ * Connect to the MCP server and perform initialization
83
+ */
84
+ async connect() {
85
+ this.log('debug', `Connecting to ${this.config.baseUrl}...`);
86
+ // Create transport based on config
87
+ this.transport = this.createTransport();
88
+ // Connect transport
89
+ await this.transport.connect();
90
+ // Perform MCP initialization
91
+ const initResponse = await this.initialize();
92
+ if (!initResponse.success || !initResponse.data) {
93
+ throw new Error(`Failed to initialize MCP connection: ${initResponse.error?.message ?? 'Unknown error'}`);
94
+ }
95
+ this.initResult = initResponse.data;
96
+ this._sessionId = this.transport.getSessionId();
97
+ this._sessionInfo = {
98
+ id: this._sessionId ?? `session-${Date.now()}`,
99
+ createdAt: new Date(),
100
+ lastActivityAt: new Date(),
101
+ requestCount: 1,
102
+ };
103
+ // Send initialized notification per MCP protocol
104
+ // This notification MUST be sent after receiving initialize response
105
+ // before the client can make any other requests
106
+ await this.transport.notify({
107
+ jsonrpc: '2.0',
108
+ method: 'notifications/initialized',
109
+ });
110
+ this.log('info', `Connected to ${this.initResult.serverInfo?.name ?? 'MCP Server'}`);
111
+ return this.initResult;
112
+ }
113
+ /**
114
+ * Disconnect from the MCP server
115
+ */
116
+ async disconnect() {
117
+ if (this.transport) {
118
+ await this.transport.close();
119
+ this.transport = null;
120
+ }
121
+ this.initResult = null;
122
+ this.log('info', 'Disconnected from MCP server');
123
+ }
124
+ /**
125
+ * Reconnect to the server, optionally with an existing session ID
126
+ */
127
+ async reconnect(options) {
128
+ await this.disconnect();
129
+ if (options?.sessionId && this.transport) {
130
+ // Set session ID before reconnecting
131
+ this._sessionId = options.sessionId;
132
+ }
133
+ await this.connect();
134
+ }
135
+ /**
136
+ * Check if the client is currently connected
137
+ */
138
+ isConnected() {
139
+ return this.transport?.isConnected() ?? false;
140
+ }
141
+ // ═══════════════════════════════════════════════════════════════════
142
+ // SESSION & AUTH PROPERTIES
143
+ // ═══════════════════════════════════════════════════════════════════
144
+ get sessionId() {
145
+ return this._sessionId ?? '';
146
+ }
147
+ get session() {
148
+ const info = this._sessionInfo ?? {
149
+ id: '',
150
+ createdAt: new Date(),
151
+ lastActivityAt: new Date(),
152
+ requestCount: 0,
153
+ };
154
+ return {
155
+ ...info,
156
+ expire: async () => {
157
+ // Force session expiration for testing
158
+ this._sessionId = undefined;
159
+ this._sessionInfo = null;
160
+ },
161
+ };
162
+ }
163
+ get auth() {
164
+ return this._authState;
165
+ }
166
+ /**
167
+ * Authenticate with a token
168
+ */
169
+ async authenticate(token) {
170
+ this._authState = {
171
+ isAnonymous: false,
172
+ token,
173
+ scopes: this.parseScopesFromToken(token),
174
+ user: this.parseUserFromToken(token),
175
+ };
176
+ // Update transport headers
177
+ if (this.transport) {
178
+ this.transport.setAuthToken(token);
179
+ }
180
+ this.log('debug', 'Authentication updated');
181
+ }
182
+ // ═══════════════════════════════════════════════════════════════════
183
+ // SERVER INFO & CAPABILITIES
184
+ // ═══════════════════════════════════════════════════════════════════
185
+ get serverInfo() {
186
+ return {
187
+ name: this.initResult?.serverInfo?.name ?? '',
188
+ version: this.initResult?.serverInfo?.version ?? '',
189
+ };
190
+ }
191
+ get protocolVersion() {
192
+ return this.initResult?.protocolVersion ?? '';
193
+ }
194
+ get instructions() {
195
+ return this.initResult?.instructions ?? '';
196
+ }
197
+ get capabilities() {
198
+ return this.initResult?.capabilities ?? {};
199
+ }
200
+ /**
201
+ * Check if server has a specific capability
202
+ */
203
+ hasCapability(name) {
204
+ return !!this.capabilities[name];
205
+ }
206
+ // ═══════════════════════════════════════════════════════════════════
207
+ // TOOLS API
208
+ // ═══════════════════════════════════════════════════════════════════
209
+ tools = {
210
+ /**
211
+ * List all available tools
212
+ */
213
+ list: async () => {
214
+ const response = await this.listTools();
215
+ if (!response.success || !response.data) {
216
+ throw new Error(`Failed to list tools: ${response.error?.message}`);
217
+ }
218
+ return response.data.tools;
219
+ },
220
+ /**
221
+ * Call a tool by name with arguments
222
+ */
223
+ call: async (name, args) => {
224
+ const response = await this.callTool(name, args);
225
+ return this.wrapToolResult(response);
226
+ },
227
+ };
228
+ // ═══════════════════════════════════════════════════════════════════
229
+ // RESOURCES API
230
+ // ═══════════════════════════════════════════════════════════════════
231
+ resources = {
232
+ /**
233
+ * List all static resources
234
+ */
235
+ list: async () => {
236
+ const response = await this.listResources();
237
+ if (!response.success || !response.data) {
238
+ throw new Error(`Failed to list resources: ${response.error?.message}`);
239
+ }
240
+ return response.data.resources;
241
+ },
242
+ /**
243
+ * List all resource templates
244
+ */
245
+ listTemplates: async () => {
246
+ const response = await this.listResourceTemplates();
247
+ if (!response.success || !response.data) {
248
+ throw new Error(`Failed to list resource templates: ${response.error?.message}`);
249
+ }
250
+ return response.data.resourceTemplates;
251
+ },
252
+ /**
253
+ * Read a resource by URI
254
+ */
255
+ read: async (uri) => {
256
+ const response = await this.readResource(uri);
257
+ return this.wrapResourceContent(response);
258
+ },
259
+ /**
260
+ * Subscribe to resource changes (placeholder for future implementation)
261
+ */
262
+ subscribe: async (_uri) => {
263
+ // TODO: Implement resource subscription
264
+ this.log('warn', 'Resource subscription not yet implemented');
265
+ },
266
+ /**
267
+ * Unsubscribe from resource changes (placeholder for future implementation)
268
+ */
269
+ unsubscribe: async (_uri) => {
270
+ // TODO: Implement resource unsubscription
271
+ this.log('warn', 'Resource unsubscription not yet implemented');
272
+ },
273
+ };
274
+ // ═══════════════════════════════════════════════════════════════════
275
+ // PROMPTS API
276
+ // ═══════════════════════════════════════════════════════════════════
277
+ prompts = {
278
+ /**
279
+ * List all available prompts
280
+ */
281
+ list: async () => {
282
+ const response = await this.listPrompts();
283
+ if (!response.success || !response.data) {
284
+ throw new Error(`Failed to list prompts: ${response.error?.message}`);
285
+ }
286
+ return response.data.prompts;
287
+ },
288
+ /**
289
+ * Get a prompt with arguments
290
+ */
291
+ get: async (name, args) => {
292
+ const response = await this.getPrompt(name, args);
293
+ return this.wrapPromptResult(response);
294
+ },
295
+ };
296
+ // ═══════════════════════════════════════════════════════════════════
297
+ // RAW PROTOCOL ACCESS
298
+ // ═══════════════════════════════════════════════════════════════════
299
+ raw = {
300
+ /**
301
+ * Send any JSON-RPC request
302
+ */
303
+ request: async (message) => {
304
+ this.ensureConnected();
305
+ const start = Date.now();
306
+ const response = await this.transport.request(message);
307
+ this.traceRequest(message.method, message.params, message.id, response, Date.now() - start);
308
+ return response;
309
+ },
310
+ /**
311
+ * Send a notification (no response expected)
312
+ */
313
+ notify: async (message) => {
314
+ this.ensureConnected();
315
+ await this.transport.notify(message);
316
+ },
317
+ /**
318
+ * Send raw string data (for error testing)
319
+ */
320
+ sendRaw: async (data) => {
321
+ this.ensureConnected();
322
+ return this.transport.sendRaw(data);
323
+ },
324
+ };
325
+ get lastRequestId() {
326
+ return this._lastRequestId;
327
+ }
328
+ // ═══════════════════════════════════════════════════════════════════
329
+ // TRANSPORT INFO
330
+ // ═══════════════════════════════════════════════════════════════════
331
+ /**
332
+ * Get transport information and utilities
333
+ */
334
+ get transport_info() {
335
+ return {
336
+ type: this.config.transport,
337
+ isConnected: () => this.transport?.isConnected() ?? false,
338
+ messageEndpoint: this.transport?.getMessageEndpoint?.(),
339
+ connectionCount: this.transport?.getConnectionCount?.() ?? 0,
340
+ reconnectCount: this.transport?.getReconnectCount?.() ?? 0,
341
+ lastRequestHeaders: this.transport?.getLastRequestHeaders?.() ?? {},
342
+ simulateDisconnect: async () => {
343
+ await this.transport?.simulateDisconnect?.();
344
+ },
345
+ waitForReconnect: async (timeoutMs) => {
346
+ await this.transport?.waitForReconnect?.(timeoutMs);
347
+ },
348
+ };
349
+ }
350
+ // Alias for transport info
351
+ get transport_() {
352
+ return this.transport_info;
353
+ }
354
+ // ═══════════════════════════════════════════════════════════════════
355
+ // NOTIFICATIONS
356
+ // ═══════════════════════════════════════════════════════════════════
357
+ notifications = {
358
+ /**
359
+ * Start collecting server notifications
360
+ */
361
+ collect: () => {
362
+ return new NotificationCollector(this._notifications);
363
+ },
364
+ /**
365
+ * Collect progress notifications specifically
366
+ */
367
+ collectProgress: () => {
368
+ return new ProgressCollector(this._progressUpdates);
369
+ },
370
+ /**
371
+ * Send a notification to the server
372
+ */
373
+ send: async (method, params) => {
374
+ await this.raw.notify({ jsonrpc: '2.0', method, params });
375
+ },
376
+ };
377
+ // ═══════════════════════════════════════════════════════════════════
378
+ // LOGGING & DEBUGGING
379
+ // ═══════════════════════════════════════════════════════════════════
380
+ logs = {
381
+ all: () => [...this._logs],
382
+ filter: (level) => this._logs.filter((l) => l.level === level),
383
+ search: (text) => this._logs.filter((l) => l.message.includes(text)),
384
+ last: () => this._logs[this._logs.length - 1],
385
+ clear: () => {
386
+ this._logs = [];
387
+ },
388
+ };
389
+ trace = {
390
+ all: () => [...this._traces],
391
+ last: () => this._traces[this._traces.length - 1],
392
+ clear: () => {
393
+ this._traces = [];
394
+ },
395
+ };
396
+ // ═══════════════════════════════════════════════════════════════════
397
+ // MOCKING & INTERCEPTION
398
+ // ═══════════════════════════════════════════════════════════════════
399
+ /**
400
+ * API for mocking MCP requests
401
+ *
402
+ * @example
403
+ * ```typescript
404
+ * // Mock a specific tool call
405
+ * const handle = mcp.mock.tool('my-tool', { result: 'mocked!' });
406
+ *
407
+ * // Mock with params matching
408
+ * mcp.mock.add({
409
+ * method: 'tools/call',
410
+ * params: { name: 'my-tool' },
411
+ * response: mockResponse.toolResult([{ type: 'text', text: 'mocked' }]),
412
+ * });
413
+ *
414
+ * // Clear all mocks after test
415
+ * mcp.mock.clear();
416
+ * ```
417
+ */
418
+ mock = {
419
+ /**
420
+ * Add a mock definition
421
+ */
422
+ add: (mock) => {
423
+ return this._interceptors.mocks.add(mock);
424
+ },
425
+ /**
426
+ * Mock a tools/call request for a specific tool
427
+ */
428
+ tool: (name, result, options) => {
429
+ return this._interceptors.mocks.add({
430
+ method: 'tools/call',
431
+ params: { name },
432
+ response: interceptor_1.mockResponse.toolResult([
433
+ { type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) },
434
+ ]),
435
+ times: options?.times,
436
+ delay: options?.delay,
437
+ });
438
+ },
439
+ /**
440
+ * Mock a tools/call request to return an error
441
+ */
442
+ toolError: (name, code, message, options) => {
443
+ return this._interceptors.mocks.add({
444
+ method: 'tools/call',
445
+ params: { name },
446
+ response: interceptor_1.mockResponse.error(code, message),
447
+ times: options?.times,
448
+ });
449
+ },
450
+ /**
451
+ * Mock a resources/read request
452
+ */
453
+ resource: (uri, content, options) => {
454
+ const contentObj = typeof content === 'string' ? { uri, text: content } : { uri, ...content };
455
+ return this._interceptors.mocks.add({
456
+ method: 'resources/read',
457
+ params: { uri },
458
+ response: interceptor_1.mockResponse.resourceRead([contentObj]),
459
+ times: options?.times,
460
+ delay: options?.delay,
461
+ });
462
+ },
463
+ /**
464
+ * Mock a resources/read request to return an error
465
+ */
466
+ resourceError: (uri, options) => {
467
+ return this._interceptors.mocks.add({
468
+ method: 'resources/read',
469
+ params: { uri },
470
+ response: interceptor_1.mockResponse.errors.resourceNotFound(uri),
471
+ times: options?.times,
472
+ });
473
+ },
474
+ /**
475
+ * Mock the tools/list response
476
+ */
477
+ toolsList: (tools, options) => {
478
+ return this._interceptors.mocks.add({
479
+ method: 'tools/list',
480
+ response: interceptor_1.mockResponse.toolsList(tools),
481
+ times: options?.times,
482
+ });
483
+ },
484
+ /**
485
+ * Mock the resources/list response
486
+ */
487
+ resourcesList: (resources, options) => {
488
+ return this._interceptors.mocks.add({
489
+ method: 'resources/list',
490
+ response: interceptor_1.mockResponse.resourcesList(resources),
491
+ times: options?.times,
492
+ });
493
+ },
494
+ /**
495
+ * Clear all mocks
496
+ */
497
+ clear: () => {
498
+ this._interceptors.mocks.clear();
499
+ },
500
+ /**
501
+ * Get all active mocks
502
+ */
503
+ all: () => {
504
+ return this._interceptors.mocks.getAll();
505
+ },
506
+ };
507
+ /**
508
+ * API for intercepting requests and responses
509
+ *
510
+ * @example
511
+ * ```typescript
512
+ * // Log all requests
513
+ * const remove = mcp.intercept.request((ctx) => {
514
+ * console.log('Request:', ctx.request.method);
515
+ * return { action: 'passthrough' };
516
+ * });
517
+ *
518
+ * // Modify requests
519
+ * mcp.intercept.request((ctx) => {
520
+ * if (ctx.request.method === 'tools/call') {
521
+ * return {
522
+ * action: 'modify',
523
+ * request: { ...ctx.request, params: { ...ctx.request.params, extra: true } },
524
+ * };
525
+ * }
526
+ * return { action: 'passthrough' };
527
+ * });
528
+ *
529
+ * // Add latency to all requests
530
+ * mcp.intercept.delay(100);
531
+ *
532
+ * // Clean up
533
+ * remove();
534
+ * mcp.intercept.clear();
535
+ * ```
536
+ */
537
+ intercept = {
538
+ /**
539
+ * Add a request interceptor
540
+ * @returns Function to remove the interceptor
541
+ */
542
+ request: (interceptor) => {
543
+ return this._interceptors.addRequestInterceptor(interceptor);
544
+ },
545
+ /**
546
+ * Add a response interceptor
547
+ * @returns Function to remove the interceptor
548
+ */
549
+ response: (interceptor) => {
550
+ return this._interceptors.addResponseInterceptor(interceptor);
551
+ },
552
+ /**
553
+ * Add latency to all requests
554
+ * @returns Function to remove the interceptor
555
+ */
556
+ delay: (ms) => {
557
+ return this._interceptors.addRequestInterceptor(async () => {
558
+ await new Promise((r) => setTimeout(r, ms));
559
+ return { action: 'passthrough' };
560
+ });
561
+ },
562
+ /**
563
+ * Fail requests matching a method
564
+ * @returns Function to remove the interceptor
565
+ */
566
+ failMethod: (method, error) => {
567
+ return this._interceptors.addRequestInterceptor((ctx) => {
568
+ if (ctx.request.method === method) {
569
+ return { action: 'error', error: new Error(error ?? `Intercepted: ${method}`) };
570
+ }
571
+ return { action: 'passthrough' };
572
+ });
573
+ },
574
+ /**
575
+ * Clear all interceptors (but not mocks)
576
+ */
577
+ clear: () => {
578
+ this._interceptors.request = [];
579
+ this._interceptors.response = [];
580
+ },
581
+ /**
582
+ * Clear everything (interceptors and mocks)
583
+ */
584
+ clearAll: () => {
585
+ this._interceptors.clear();
586
+ },
587
+ };
588
+ // ═══════════════════════════════════════════════════════════════════
589
+ // TIMEOUT
590
+ // ═══════════════════════════════════════════════════════════════════
591
+ setTimeout(ms) {
592
+ this.config.timeout = ms;
593
+ if (this.transport) {
594
+ this.transport.setTimeout(ms);
595
+ }
596
+ }
597
+ // ═══════════════════════════════════════════════════════════════════
598
+ // PRIVATE: MCP OPERATIONS
599
+ // ═══════════════════════════════════════════════════════════════════
600
+ async initialize() {
601
+ return this.request('initialize', {
602
+ protocolVersion: this.config.protocolVersion,
603
+ capabilities: {
604
+ sampling: {},
605
+ },
606
+ clientInfo: this.config.clientInfo,
607
+ });
608
+ }
609
+ async listTools() {
610
+ return this.request('tools/list', {});
611
+ }
612
+ async callTool(name, args) {
613
+ return this.request('tools/call', {
614
+ name,
615
+ arguments: args ?? {},
616
+ });
617
+ }
618
+ async listResources() {
619
+ return this.request('resources/list', {});
620
+ }
621
+ async listResourceTemplates() {
622
+ return this.request('resources/templates/list', {});
623
+ }
624
+ async readResource(uri) {
625
+ return this.request('resources/read', { uri });
626
+ }
627
+ async listPrompts() {
628
+ return this.request('prompts/list', {});
629
+ }
630
+ async getPrompt(name, args) {
631
+ return this.request('prompts/get', {
632
+ name,
633
+ arguments: args ?? {},
634
+ });
635
+ }
636
+ // ═══════════════════════════════════════════════════════════════════
637
+ // PRIVATE: TRANSPORT & REQUEST HELPERS
638
+ // ═══════════════════════════════════════════════════════════════════
639
+ createTransport() {
640
+ switch (this.config.transport) {
641
+ case 'streamable-http':
642
+ return new streamable_http_transport_1.StreamableHttpTransport({
643
+ baseUrl: this.config.baseUrl,
644
+ timeout: this.config.timeout,
645
+ auth: this.config.auth,
646
+ publicMode: this.config.publicMode,
647
+ debug: this.config.debug,
648
+ interceptors: this._interceptors,
649
+ });
650
+ case 'sse':
651
+ // TODO: Implement SSE transport
652
+ throw new Error('SSE transport not yet implemented');
653
+ default:
654
+ throw new Error(`Unknown transport type: ${this.config.transport}`);
655
+ }
656
+ }
657
+ async request(method, params) {
658
+ this.ensureConnected();
659
+ const id = ++this.requestIdCounter;
660
+ this._lastRequestId = id;
661
+ const start = Date.now();
662
+ try {
663
+ const response = await this.transport.request({
664
+ jsonrpc: '2.0',
665
+ id,
666
+ method,
667
+ params,
668
+ });
669
+ const durationMs = Date.now() - start;
670
+ this.updateSessionActivity();
671
+ if ('error' in response && response.error) {
672
+ const error = response.error;
673
+ this.traceRequest(method, params, id, response, durationMs);
674
+ return {
675
+ success: false,
676
+ error,
677
+ durationMs,
678
+ requestId: id,
679
+ };
680
+ }
681
+ this.traceRequest(method, params, id, response, durationMs);
682
+ return {
683
+ success: true,
684
+ data: response.result,
685
+ durationMs,
686
+ requestId: id,
687
+ };
688
+ }
689
+ catch (err) {
690
+ const durationMs = Date.now() - start;
691
+ const error = {
692
+ code: -32603,
693
+ message: err instanceof Error ? err.message : 'Unknown error',
694
+ };
695
+ return {
696
+ success: false,
697
+ error,
698
+ durationMs,
699
+ requestId: id,
700
+ };
701
+ }
702
+ }
703
+ ensureConnected() {
704
+ if (!this.transport?.isConnected()) {
705
+ throw new Error('Not connected to MCP server. Call connect() first.');
706
+ }
707
+ }
708
+ updateSessionActivity() {
709
+ if (this._sessionInfo) {
710
+ this._sessionInfo.lastActivityAt = new Date();
711
+ this._sessionInfo.requestCount++;
712
+ }
713
+ }
714
+ // ═══════════════════════════════════════════════════════════════════
715
+ // PRIVATE: RESULT WRAPPERS
716
+ // ═══════════════════════════════════════════════════════════════════
717
+ wrapToolResult(response) {
718
+ const raw = response.data ?? { content: [] };
719
+ const isError = !response.success || raw.isError === true;
720
+ // Check for Tool UI response - has structuredContent and ui/html in _meta
721
+ const meta = raw._meta;
722
+ const hasUI = meta?.['ui/html'] !== undefined;
723
+ const structuredContent = raw['structuredContent'];
724
+ return {
725
+ raw,
726
+ isSuccess: !isError,
727
+ isError,
728
+ error: response.error,
729
+ durationMs: response.durationMs,
730
+ json() {
731
+ // For Tool UI responses, return structuredContent (the typed output)
732
+ if (hasUI && structuredContent !== undefined) {
733
+ return structuredContent;
734
+ }
735
+ // For regular responses, parse text content as JSON
736
+ const textContent = raw.content?.find((c) => c.type === 'text');
737
+ if (textContent && 'text' in textContent) {
738
+ return JSON.parse(textContent.text);
739
+ }
740
+ throw new Error('No text content to parse as JSON');
741
+ },
742
+ text() {
743
+ const textContent = raw.content?.find((c) => c.type === 'text');
744
+ if (textContent && 'text' in textContent) {
745
+ return textContent.text;
746
+ }
747
+ return undefined;
748
+ },
749
+ hasTextContent() {
750
+ return raw.content?.some((c) => c.type === 'text') ?? false;
751
+ },
752
+ hasImageContent() {
753
+ return raw.content?.some((c) => c.type === 'image') ?? false;
754
+ },
755
+ hasResourceContent() {
756
+ return raw.content?.some((c) => c.type === 'resource') ?? false;
757
+ },
758
+ hasToolUI() {
759
+ return hasUI;
760
+ },
761
+ };
762
+ }
763
+ wrapResourceContent(response) {
764
+ const raw = response.data ?? { contents: [] };
765
+ const isError = !response.success;
766
+ const firstContent = raw.contents?.[0];
767
+ return {
768
+ raw,
769
+ isSuccess: !isError,
770
+ isError,
771
+ error: response.error,
772
+ durationMs: response.durationMs,
773
+ json() {
774
+ if (firstContent && 'text' in firstContent) {
775
+ return JSON.parse(firstContent.text);
776
+ }
777
+ throw new Error('No text content to parse as JSON');
778
+ },
779
+ text() {
780
+ if (firstContent && 'text' in firstContent) {
781
+ return firstContent.text;
782
+ }
783
+ return undefined;
784
+ },
785
+ mimeType() {
786
+ return firstContent?.mimeType;
787
+ },
788
+ hasMimeType(type) {
789
+ return firstContent?.mimeType === type;
790
+ },
791
+ };
792
+ }
793
+ wrapPromptResult(response) {
794
+ const raw = response.data ?? { messages: [] };
795
+ const isError = !response.success;
796
+ return {
797
+ raw,
798
+ isSuccess: !isError,
799
+ isError,
800
+ error: response.error,
801
+ durationMs: response.durationMs,
802
+ messages: raw.messages ?? [],
803
+ description: raw.description,
804
+ };
805
+ }
806
+ // ═══════════════════════════════════════════════════════════════════
807
+ // PRIVATE: LOGGING & TRACING
808
+ // ═══════════════════════════════════════════════════════════════════
809
+ log(level, message, data) {
810
+ const entry = {
811
+ level,
812
+ message,
813
+ timestamp: new Date(),
814
+ data,
815
+ };
816
+ this._logs.push(entry);
817
+ if (this.config.debug) {
818
+ console.log(`[${level.toUpperCase()}] ${message}`, data ?? '');
819
+ }
820
+ }
821
+ traceRequest(method, params, id, response, durationMs) {
822
+ this._traces.push({
823
+ request: { method, params, id },
824
+ response: {
825
+ result: 'result' in response ? response.result : undefined,
826
+ error: 'error' in response ? response.error : undefined,
827
+ },
828
+ durationMs,
829
+ timestamp: new Date(),
830
+ });
831
+ }
832
+ // ═══════════════════════════════════════════════════════════════════
833
+ // PRIVATE: TOKEN PARSING
834
+ // ═══════════════════════════════════════════════════════════════════
835
+ parseScopesFromToken(token) {
836
+ try {
837
+ const payload = this.decodeJwtPayload(token);
838
+ if (!payload)
839
+ return [];
840
+ const scope = payload['scope'];
841
+ const scopes = payload['scopes'];
842
+ if (typeof scope === 'string') {
843
+ return scope.split(' ');
844
+ }
845
+ if (Array.isArray(scopes)) {
846
+ return scopes;
847
+ }
848
+ return [];
849
+ }
850
+ catch {
851
+ return [];
852
+ }
853
+ }
854
+ parseUserFromToken(token) {
855
+ try {
856
+ const payload = this.decodeJwtPayload(token);
857
+ const sub = payload?.['sub'];
858
+ if (!sub || typeof sub !== 'string')
859
+ return undefined;
860
+ return {
861
+ sub,
862
+ email: payload['email'],
863
+ name: payload['name'],
864
+ };
865
+ }
866
+ catch {
867
+ return undefined;
868
+ }
869
+ }
870
+ decodeJwtPayload(token) {
871
+ try {
872
+ const parts = token.split('.');
873
+ if (parts.length !== 3)
874
+ return null;
875
+ const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
876
+ return JSON.parse(payload);
877
+ }
878
+ catch {
879
+ return null;
880
+ }
881
+ }
882
+ }
883
+ exports.McpTestClient = McpTestClient;
884
+ // ═══════════════════════════════════════════════════════════════════
885
+ // NOTIFICATION COLLECTORS
886
+ // ═══════════════════════════════════════════════════════════════════
887
+ class NotificationCollector {
888
+ notifications;
889
+ constructor(notifications) {
890
+ this.notifications = notifications;
891
+ }
892
+ get received() {
893
+ return [...this.notifications];
894
+ }
895
+ has(method) {
896
+ return this.notifications.some((n) => n.method === method);
897
+ }
898
+ async waitFor(method, timeoutMs) {
899
+ const deadline = Date.now() + timeoutMs;
900
+ while (Date.now() < deadline) {
901
+ const found = this.notifications.find((n) => n.method === method);
902
+ if (found)
903
+ return found;
904
+ await new Promise((r) => setTimeout(r, 50));
905
+ }
906
+ throw new Error(`Timeout waiting for notification: ${method}`);
907
+ }
908
+ }
909
+ class ProgressCollector {
910
+ updates;
911
+ constructor(updates) {
912
+ this.updates = updates;
913
+ }
914
+ get all() {
915
+ return [...this.updates];
916
+ }
917
+ async waitForComplete(timeoutMs) {
918
+ const deadline = Date.now() + timeoutMs;
919
+ while (Date.now() < deadline) {
920
+ const last = this.updates[this.updates.length - 1];
921
+ if (last && last.total !== undefined && last.progress >= last.total) {
922
+ return;
923
+ }
924
+ await new Promise((r) => setTimeout(r, 50));
925
+ }
926
+ throw new Error('Timeout waiting for progress to complete');
927
+ }
928
+ }
929
+ //# sourceMappingURL=mcp-test-client.js.map