@frontmcp/testing 0.6.1 → 0.6.3

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