@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
@@ -0,0 +1,2418 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // libs/testing/src/fixtures/index.ts
31
+ var fixtures_exports = {};
32
+ __export(fixtures_exports, {
33
+ cleanupSharedResources: () => cleanupSharedResources,
34
+ cleanupTestFixtures: () => cleanupTestFixtures,
35
+ createTestFixtures: () => createTestFixtures,
36
+ initializeSharedResources: () => initializeSharedResources,
37
+ test: () => test
38
+ });
39
+ module.exports = __toCommonJS(fixtures_exports);
40
+
41
+ // libs/testing/src/platform/platform-client-info.ts
42
+ var MCP_APPS_EXTENSION_KEY = "io.modelcontextprotocol/ui";
43
+ function getPlatformClientInfo(platform) {
44
+ switch (platform) {
45
+ case "openai":
46
+ return {
47
+ name: "ChatGPT",
48
+ version: "1.0"
49
+ };
50
+ case "ext-apps":
51
+ return {
52
+ name: "mcp-ext-apps",
53
+ version: "1.0"
54
+ };
55
+ case "claude":
56
+ return {
57
+ name: "claude-desktop",
58
+ version: "1.0"
59
+ };
60
+ case "cursor":
61
+ return {
62
+ name: "cursor",
63
+ version: "1.0"
64
+ };
65
+ case "continue":
66
+ return {
67
+ name: "continue",
68
+ version: "1.0"
69
+ };
70
+ case "cody":
71
+ return {
72
+ name: "cody",
73
+ version: "1.0"
74
+ };
75
+ case "gemini":
76
+ return {
77
+ name: "gemini",
78
+ version: "1.0"
79
+ };
80
+ case "generic-mcp":
81
+ return {
82
+ name: "generic-mcp-client",
83
+ version: "1.0"
84
+ };
85
+ case "unknown":
86
+ default:
87
+ return {
88
+ name: "mcp-test-client",
89
+ version: "1.0"
90
+ };
91
+ }
92
+ }
93
+ function getPlatformCapabilities(platform) {
94
+ const baseCapabilities = {
95
+ sampling: {}
96
+ };
97
+ if (platform === "ext-apps") {
98
+ return {
99
+ ...baseCapabilities,
100
+ experimental: {
101
+ [MCP_APPS_EXTENSION_KEY]: {
102
+ mimeTypes: ["text/html+mcp"]
103
+ }
104
+ }
105
+ };
106
+ }
107
+ return baseCapabilities;
108
+ }
109
+
110
+ // libs/testing/src/client/mcp-test-client.builder.ts
111
+ var McpTestClientBuilder = class {
112
+ config;
113
+ constructor(config) {
114
+ this.config = { ...config };
115
+ }
116
+ /**
117
+ * Set the authentication configuration
118
+ */
119
+ withAuth(auth) {
120
+ this.config.auth = { ...this.config.auth, ...auth };
121
+ return this;
122
+ }
123
+ /**
124
+ * Set the bearer token for authentication
125
+ */
126
+ withToken(token) {
127
+ this.config.auth = { ...this.config.auth, token };
128
+ return this;
129
+ }
130
+ /**
131
+ * Add custom headers to all requests
132
+ */
133
+ withHeaders(headers) {
134
+ this.config.auth = {
135
+ ...this.config.auth,
136
+ headers: { ...this.config.auth?.headers, ...headers }
137
+ };
138
+ return this;
139
+ }
140
+ /**
141
+ * Set the transport type
142
+ */
143
+ withTransport(transport) {
144
+ this.config.transport = transport;
145
+ return this;
146
+ }
147
+ /**
148
+ * Set the request timeout in milliseconds
149
+ */
150
+ withTimeout(timeoutMs) {
151
+ this.config.timeout = timeoutMs;
152
+ return this;
153
+ }
154
+ /**
155
+ * Enable debug logging
156
+ */
157
+ withDebug(enabled = true) {
158
+ this.config.debug = enabled;
159
+ return this;
160
+ }
161
+ /**
162
+ * Enable public mode - skip authentication entirely.
163
+ * When true, no Authorization header is sent and anonymous token is not requested.
164
+ * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.
165
+ */
166
+ withPublicMode(enabled = true) {
167
+ this.config.publicMode = enabled;
168
+ return this;
169
+ }
170
+ /**
171
+ * Set the MCP protocol version to request
172
+ */
173
+ withProtocolVersion(version) {
174
+ this.config.protocolVersion = version;
175
+ return this;
176
+ }
177
+ /**
178
+ * Set the client info sent during initialization
179
+ */
180
+ withClientInfo(info) {
181
+ this.config.clientInfo = info;
182
+ return this;
183
+ }
184
+ /**
185
+ * Set the platform type for testing platform-specific meta keys.
186
+ * Automatically configures clientInfo and capabilities for platform detection.
187
+ *
188
+ * Platform-specific behavior:
189
+ * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
190
+ * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
191
+ * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
192
+ * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
193
+ * - Other platforms follow similar patterns
194
+ *
195
+ * @example
196
+ * ```typescript
197
+ * const client = await McpTestClient.create({ baseUrl })
198
+ * .withPlatform('openai')
199
+ * .buildAndConnect();
200
+ *
201
+ * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
202
+ * const extAppsClient = await McpTestClient.create({ baseUrl })
203
+ * .withPlatform('ext-apps')
204
+ * .buildAndConnect();
205
+ * ```
206
+ */
207
+ withPlatform(platform) {
208
+ this.config.platform = platform;
209
+ this.config.clientInfo = getPlatformClientInfo(platform);
210
+ this.config.capabilities = getPlatformCapabilities(platform);
211
+ return this;
212
+ }
213
+ /**
214
+ * Set custom client capabilities for MCP initialization.
215
+ * Use this for fine-grained control over capabilities sent during initialization.
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * const client = await McpTestClient.create({ baseUrl })
220
+ * .withCapabilities({
221
+ * sampling: {},
222
+ * experimental: {
223
+ * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html+mcp'] }
224
+ * }
225
+ * })
226
+ * .buildAndConnect();
227
+ * ```
228
+ */
229
+ withCapabilities(capabilities) {
230
+ this.config.capabilities = capabilities;
231
+ return this;
232
+ }
233
+ /**
234
+ * Build the McpTestClient instance (does not connect)
235
+ */
236
+ build() {
237
+ return new McpTestClient(this.config);
238
+ }
239
+ /**
240
+ * Build the McpTestClient and connect to the server
241
+ */
242
+ async buildAndConnect() {
243
+ const client = this.build();
244
+ await client.connect();
245
+ return client;
246
+ }
247
+ };
248
+
249
+ // libs/testing/src/transport/streamable-http.transport.ts
250
+ var DEFAULT_TIMEOUT = 3e4;
251
+ var StreamableHttpTransport = class {
252
+ config;
253
+ state = "disconnected";
254
+ sessionId;
255
+ authToken;
256
+ connectionCount = 0;
257
+ reconnectCount = 0;
258
+ lastRequestHeaders = {};
259
+ interceptors;
260
+ publicMode;
261
+ constructor(config) {
262
+ this.config = {
263
+ baseUrl: config.baseUrl.replace(/\/$/, ""),
264
+ // Remove trailing slash
265
+ timeout: config.timeout ?? DEFAULT_TIMEOUT,
266
+ auth: config.auth ?? {},
267
+ publicMode: config.publicMode ?? false,
268
+ debug: config.debug ?? false,
269
+ interceptors: config.interceptors,
270
+ clientInfo: config.clientInfo
271
+ };
272
+ this.authToken = config.auth?.token;
273
+ this.interceptors = config.interceptors;
274
+ this.publicMode = config.publicMode ?? false;
275
+ }
276
+ async connect() {
277
+ this.state = "connecting";
278
+ this.connectionCount++;
279
+ try {
280
+ if (this.publicMode) {
281
+ this.log("Public mode: connecting without authentication");
282
+ this.state = "connected";
283
+ return;
284
+ }
285
+ if (!this.authToken) {
286
+ await this.requestAnonymousToken();
287
+ }
288
+ this.state = "connected";
289
+ this.log("Connected to StreamableHTTP transport");
290
+ } catch (error) {
291
+ this.state = "error";
292
+ throw error;
293
+ }
294
+ }
295
+ /**
296
+ * Request an anonymous token from the FrontMCP OAuth endpoint
297
+ * This allows the test client to authenticate without user interaction
298
+ */
299
+ async requestAnonymousToken() {
300
+ const clientId = crypto.randomUUID();
301
+ const tokenUrl = `${this.config.baseUrl}/oauth/token`;
302
+ this.log(`Requesting anonymous token from ${tokenUrl}`);
303
+ try {
304
+ const response = await fetch(tokenUrl, {
305
+ method: "POST",
306
+ headers: {
307
+ "Content-Type": "application/json"
308
+ },
309
+ body: JSON.stringify({
310
+ grant_type: "anonymous",
311
+ client_id: clientId,
312
+ resource: this.config.baseUrl
313
+ })
314
+ });
315
+ if (!response.ok) {
316
+ const errorText = await response.text();
317
+ this.log(`Failed to get anonymous token: ${response.status} ${errorText}`);
318
+ return;
319
+ }
320
+ const tokenResponse = await response.json();
321
+ if (tokenResponse.access_token) {
322
+ this.authToken = tokenResponse.access_token;
323
+ this.log("Anonymous token acquired successfully");
324
+ }
325
+ } catch (error) {
326
+ this.log(`Error requesting anonymous token: ${error}`);
327
+ }
328
+ }
329
+ async request(message) {
330
+ this.ensureConnected();
331
+ const startTime = Date.now();
332
+ if (this.interceptors) {
333
+ const interceptResult = await this.interceptors.processRequest(message, {
334
+ timestamp: /* @__PURE__ */ new Date(),
335
+ transport: "streamable-http",
336
+ sessionId: this.sessionId
337
+ });
338
+ switch (interceptResult.type) {
339
+ case "mock": {
340
+ const mockResponse2 = await this.interceptors.processResponse(
341
+ message,
342
+ interceptResult.response,
343
+ Date.now() - startTime
344
+ );
345
+ return mockResponse2;
346
+ }
347
+ case "error":
348
+ throw interceptResult.error;
349
+ case "continue":
350
+ message = interceptResult.request;
351
+ break;
352
+ }
353
+ }
354
+ const headers = this.buildHeaders();
355
+ this.lastRequestHeaders = headers;
356
+ const url = `${this.config.baseUrl}/`;
357
+ this.log(`POST ${url}`, message);
358
+ const controller = new AbortController();
359
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
360
+ try {
361
+ const response = await fetch(url, {
362
+ method: "POST",
363
+ headers,
364
+ body: JSON.stringify(message),
365
+ signal: controller.signal
366
+ });
367
+ clearTimeout(timeoutId);
368
+ const newSessionId = response.headers.get("mcp-session-id");
369
+ if (newSessionId) {
370
+ this.sessionId = newSessionId;
371
+ }
372
+ let jsonResponse;
373
+ if (!response.ok) {
374
+ const errorText = await response.text();
375
+ this.log(`HTTP Error ${response.status}: ${errorText}`);
376
+ jsonResponse = {
377
+ jsonrpc: "2.0",
378
+ id: message.id ?? null,
379
+ error: {
380
+ code: -32e3,
381
+ message: `HTTP ${response.status}: ${response.statusText}`,
382
+ data: errorText
383
+ }
384
+ };
385
+ } else {
386
+ const contentType = response.headers.get("content-type") ?? "";
387
+ const text = await response.text();
388
+ this.log("Response:", text);
389
+ if (!text.trim()) {
390
+ jsonResponse = {
391
+ jsonrpc: "2.0",
392
+ id: message.id ?? null,
393
+ result: void 0
394
+ };
395
+ } else if (contentType.includes("text/event-stream")) {
396
+ const { response: sseResponse, sseSessionId } = this.parseSSEResponseWithSession(text, message.id);
397
+ jsonResponse = sseResponse;
398
+ if (sseSessionId && !this.sessionId) {
399
+ this.sessionId = sseSessionId;
400
+ this.log("Session ID from SSE:", this.sessionId);
401
+ }
402
+ } else {
403
+ jsonResponse = JSON.parse(text);
404
+ }
405
+ }
406
+ if (this.interceptors) {
407
+ jsonResponse = await this.interceptors.processResponse(message, jsonResponse, Date.now() - startTime);
408
+ }
409
+ return jsonResponse;
410
+ } catch (error) {
411
+ clearTimeout(timeoutId);
412
+ if (error instanceof Error && error.name === "AbortError") {
413
+ return {
414
+ jsonrpc: "2.0",
415
+ id: message.id ?? null,
416
+ error: {
417
+ code: -32e3,
418
+ message: `Request timeout after ${this.config.timeout}ms`
419
+ }
420
+ };
421
+ }
422
+ throw error;
423
+ }
424
+ }
425
+ async notify(message) {
426
+ this.ensureConnected();
427
+ const headers = this.buildHeaders();
428
+ this.lastRequestHeaders = headers;
429
+ const url = `${this.config.baseUrl}/`;
430
+ this.log(`POST ${url} (notification)`, message);
431
+ const controller = new AbortController();
432
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
433
+ try {
434
+ const response = await fetch(url, {
435
+ method: "POST",
436
+ headers,
437
+ body: JSON.stringify(message),
438
+ signal: controller.signal
439
+ });
440
+ clearTimeout(timeoutId);
441
+ const newSessionId = response.headers.get("mcp-session-id");
442
+ if (newSessionId) {
443
+ this.sessionId = newSessionId;
444
+ }
445
+ if (!response.ok) {
446
+ const errorText = await response.text();
447
+ this.log(`HTTP Error ${response.status} on notification: ${errorText}`);
448
+ }
449
+ } catch (error) {
450
+ clearTimeout(timeoutId);
451
+ if (error instanceof Error && error.name !== "AbortError") {
452
+ throw error;
453
+ }
454
+ }
455
+ }
456
+ async sendRaw(data) {
457
+ this.ensureConnected();
458
+ const headers = this.buildHeaders();
459
+ this.lastRequestHeaders = headers;
460
+ const url = `${this.config.baseUrl}/`;
461
+ this.log(`POST ${url} (raw)`, data);
462
+ const controller = new AbortController();
463
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
464
+ try {
465
+ const response = await fetch(url, {
466
+ method: "POST",
467
+ headers,
468
+ body: data,
469
+ signal: controller.signal
470
+ });
471
+ clearTimeout(timeoutId);
472
+ const text = await response.text();
473
+ if (!text.trim()) {
474
+ return {
475
+ jsonrpc: "2.0",
476
+ id: null,
477
+ error: {
478
+ code: -32700,
479
+ message: "Parse error"
480
+ }
481
+ };
482
+ }
483
+ return JSON.parse(text);
484
+ } catch (error) {
485
+ clearTimeout(timeoutId);
486
+ return {
487
+ jsonrpc: "2.0",
488
+ id: null,
489
+ error: {
490
+ code: -32700,
491
+ message: "Parse error",
492
+ data: error instanceof Error ? error.message : "Unknown error"
493
+ }
494
+ };
495
+ }
496
+ }
497
+ async close() {
498
+ this.state = "disconnected";
499
+ this.sessionId = void 0;
500
+ this.log("StreamableHTTP transport closed");
501
+ }
502
+ isConnected() {
503
+ return this.state === "connected";
504
+ }
505
+ getState() {
506
+ return this.state;
507
+ }
508
+ getSessionId() {
509
+ return this.sessionId;
510
+ }
511
+ setAuthToken(token) {
512
+ this.authToken = token;
513
+ }
514
+ setTimeout(ms) {
515
+ this.config.timeout = ms;
516
+ }
517
+ setInterceptors(interceptors2) {
518
+ this.interceptors = interceptors2;
519
+ }
520
+ getInterceptors() {
521
+ return this.interceptors;
522
+ }
523
+ getConnectionCount() {
524
+ return this.connectionCount;
525
+ }
526
+ getReconnectCount() {
527
+ return this.reconnectCount;
528
+ }
529
+ getLastRequestHeaders() {
530
+ return { ...this.lastRequestHeaders };
531
+ }
532
+ async simulateDisconnect() {
533
+ this.state = "disconnected";
534
+ this.sessionId = void 0;
535
+ }
536
+ async waitForReconnect(timeoutMs) {
537
+ const deadline = Date.now() + timeoutMs;
538
+ this.reconnectCount++;
539
+ await this.connect();
540
+ while (Date.now() < deadline) {
541
+ if (this.state === "connected") {
542
+ return;
543
+ }
544
+ await new Promise((r) => setTimeout(r, 50));
545
+ }
546
+ throw new Error("Timeout waiting for reconnection");
547
+ }
548
+ // ═══════════════════════════════════════════════════════════════════
549
+ // PRIVATE HELPERS
550
+ // ═══════════════════════════════════════════════════════════════════
551
+ buildHeaders() {
552
+ const headers = {
553
+ "Content-Type": "application/json",
554
+ Accept: "application/json, text/event-stream"
555
+ };
556
+ if (this.config.clientInfo) {
557
+ headers["User-Agent"] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
558
+ }
559
+ if (this.authToken && !this.publicMode) {
560
+ headers["Authorization"] = `Bearer ${this.authToken}`;
561
+ }
562
+ if (this.sessionId) {
563
+ headers["mcp-session-id"] = this.sessionId;
564
+ }
565
+ if (this.config.auth.headers) {
566
+ Object.assign(headers, this.config.auth.headers);
567
+ }
568
+ return headers;
569
+ }
570
+ ensureConnected() {
571
+ if (this.state !== "connected") {
572
+ throw new Error("Transport not connected. Call connect() first.");
573
+ }
574
+ }
575
+ log(message, data) {
576
+ if (this.config.debug) {
577
+ console.log(`[StreamableHTTP] ${message}`, data ?? "");
578
+ }
579
+ }
580
+ /**
581
+ * Parse SSE (Server-Sent Events) response format with session ID extraction
582
+ * SSE format is:
583
+ * event: message
584
+ * id: sessionId:messageId
585
+ * data: {"jsonrpc":"2.0",...}
586
+ *
587
+ * The id field contains the session ID followed by a colon and the message ID.
588
+ *
589
+ * @param text - The raw SSE response text
590
+ * @param requestId - The original request ID
591
+ * @returns Object with parsed JSON-RPC response and session ID (if found)
592
+ */
593
+ parseSSEResponseWithSession(text, requestId) {
594
+ const lines = text.split("\n");
595
+ const dataLines = [];
596
+ let sseSessionId;
597
+ for (const line of lines) {
598
+ if (line.startsWith("data: ")) {
599
+ dataLines.push(line.slice(6));
600
+ } else if (line === "data:") {
601
+ dataLines.push("");
602
+ } else if (line.startsWith("id: ")) {
603
+ const idValue = line.slice(4);
604
+ const colonIndex = idValue.lastIndexOf(":");
605
+ if (colonIndex > 0) {
606
+ sseSessionId = idValue.substring(0, colonIndex);
607
+ } else {
608
+ sseSessionId = idValue;
609
+ }
610
+ }
611
+ }
612
+ if (dataLines.length > 0) {
613
+ const jsonData = dataLines.join("\n");
614
+ try {
615
+ return {
616
+ response: JSON.parse(jsonData),
617
+ sseSessionId
618
+ };
619
+ } catch {
620
+ this.log("Failed to parse SSE data as JSON:", jsonData);
621
+ }
622
+ }
623
+ return {
624
+ response: {
625
+ jsonrpc: "2.0",
626
+ id: requestId ?? null,
627
+ error: {
628
+ code: -32700,
629
+ message: "Failed to parse SSE response",
630
+ data: text
631
+ }
632
+ },
633
+ sseSessionId
634
+ };
635
+ }
636
+ };
637
+
638
+ // libs/testing/src/interceptor/mock-registry.ts
639
+ var DefaultMockRegistry = class {
640
+ mocks = [];
641
+ add(mock) {
642
+ const entry = {
643
+ definition: mock,
644
+ callCount: 0,
645
+ calls: [],
646
+ remainingUses: mock.times ?? Infinity
647
+ };
648
+ this.mocks.push(entry);
649
+ return {
650
+ remove: () => {
651
+ const index = this.mocks.indexOf(entry);
652
+ if (index !== -1) {
653
+ this.mocks.splice(index, 1);
654
+ }
655
+ },
656
+ callCount: () => entry.callCount,
657
+ calls: () => [...entry.calls]
658
+ };
659
+ }
660
+ clear() {
661
+ this.mocks = [];
662
+ }
663
+ getAll() {
664
+ return this.mocks.map((e) => e.definition);
665
+ }
666
+ match(request) {
667
+ for (const entry of this.mocks) {
668
+ if (entry.remainingUses <= 0) continue;
669
+ const { definition } = entry;
670
+ if (definition.method !== request.method) continue;
671
+ if (definition.params !== void 0) {
672
+ const params = request.params ?? {};
673
+ if (typeof definition.params === "function") {
674
+ if (!definition.params(params)) continue;
675
+ } else {
676
+ if (!this.paramsMatch(definition.params, params)) continue;
677
+ }
678
+ }
679
+ entry.callCount++;
680
+ entry.calls.push(request);
681
+ entry.remainingUses--;
682
+ return definition;
683
+ }
684
+ return void 0;
685
+ }
686
+ /**
687
+ * Check if request params match the mock params definition
688
+ */
689
+ paramsMatch(expected, actual) {
690
+ for (const [key, value] of Object.entries(expected)) {
691
+ if (!(key in actual)) return false;
692
+ const actualValue = actual[key];
693
+ if (Array.isArray(value)) {
694
+ if (!Array.isArray(actualValue)) return false;
695
+ if (value.length !== actualValue.length) return false;
696
+ for (let i = 0; i < value.length; i++) {
697
+ const expectedItem = value[i];
698
+ const actualItem = actualValue[i];
699
+ if (typeof expectedItem === "object" && expectedItem !== null) {
700
+ if (typeof actualItem !== "object" || actualItem === null) return false;
701
+ if (!this.paramsMatch(expectedItem, actualItem)) {
702
+ return false;
703
+ }
704
+ } else if (actualItem !== expectedItem) {
705
+ return false;
706
+ }
707
+ }
708
+ } else if (typeof value === "object" && value !== null) {
709
+ if (typeof actualValue !== "object" || actualValue === null) return false;
710
+ if (!this.paramsMatch(value, actualValue)) {
711
+ return false;
712
+ }
713
+ } else if (actualValue !== value) {
714
+ return false;
715
+ }
716
+ }
717
+ return true;
718
+ }
719
+ };
720
+ var mockResponse = {
721
+ /**
722
+ * Create a successful JSON-RPC response
723
+ */
724
+ success(result, id = 1) {
725
+ return {
726
+ jsonrpc: "2.0",
727
+ id,
728
+ result
729
+ };
730
+ },
731
+ /**
732
+ * Create an error JSON-RPC response
733
+ */
734
+ error(code, message, data, id = 1) {
735
+ return {
736
+ jsonrpc: "2.0",
737
+ id,
738
+ error: { code, message, data }
739
+ };
740
+ },
741
+ /**
742
+ * Create a tool result response
743
+ */
744
+ toolResult(content, id = 1) {
745
+ return {
746
+ jsonrpc: "2.0",
747
+ id,
748
+ result: { content }
749
+ };
750
+ },
751
+ /**
752
+ * Create a tools/list response
753
+ */
754
+ toolsList(tools, id = 1) {
755
+ return {
756
+ jsonrpc: "2.0",
757
+ id,
758
+ result: { tools }
759
+ };
760
+ },
761
+ /**
762
+ * Create a resources/list response
763
+ */
764
+ resourcesList(resources, id = 1) {
765
+ return {
766
+ jsonrpc: "2.0",
767
+ id,
768
+ result: { resources }
769
+ };
770
+ },
771
+ /**
772
+ * Create a resources/read response
773
+ */
774
+ resourceRead(contents, id = 1) {
775
+ return {
776
+ jsonrpc: "2.0",
777
+ id,
778
+ result: { contents }
779
+ };
780
+ },
781
+ /**
782
+ * Common MCP errors
783
+ */
784
+ errors: {
785
+ methodNotFound: (method, id = 1) => mockResponse.error(-32601, `Method not found: ${method}`, void 0, id),
786
+ invalidParams: (message, id = 1) => mockResponse.error(-32602, message, void 0, id),
787
+ internalError: (message, id = 1) => mockResponse.error(-32603, message, void 0, id),
788
+ resourceNotFound: (uri, id = 1) => mockResponse.error(-32002, `Resource not found: ${uri}`, { uri }, id),
789
+ toolNotFound: (name, id = 1) => mockResponse.error(-32601, `Tool not found: ${name}`, { name }, id),
790
+ unauthorized: (id = 1) => mockResponse.error(-32001, "Unauthorized", void 0, id),
791
+ forbidden: (id = 1) => mockResponse.error(-32003, "Forbidden", void 0, id)
792
+ }
793
+ };
794
+
795
+ // libs/testing/src/interceptor/interceptor-chain.ts
796
+ var DefaultInterceptorChain = class {
797
+ request = [];
798
+ response = [];
799
+ mocks;
800
+ constructor() {
801
+ this.mocks = new DefaultMockRegistry();
802
+ }
803
+ /**
804
+ * Add a request interceptor
805
+ */
806
+ addRequestInterceptor(interceptor) {
807
+ this.request.push(interceptor);
808
+ return () => {
809
+ const index = this.request.indexOf(interceptor);
810
+ if (index !== -1) {
811
+ this.request.splice(index, 1);
812
+ }
813
+ };
814
+ }
815
+ /**
816
+ * Add a response interceptor
817
+ */
818
+ addResponseInterceptor(interceptor) {
819
+ this.response.push(interceptor);
820
+ return () => {
821
+ const index = this.response.indexOf(interceptor);
822
+ if (index !== -1) {
823
+ this.response.splice(index, 1);
824
+ }
825
+ };
826
+ }
827
+ /**
828
+ * Process a request through the interceptor chain
829
+ * Returns either:
830
+ * - { type: 'continue', request } - continue with (possibly modified) request
831
+ * - { type: 'mock', response } - return mock response immediately
832
+ * - { type: 'error', error } - throw error
833
+ */
834
+ async processRequest(request, meta) {
835
+ let currentRequest = request;
836
+ const mockDef = this.mocks.match(request);
837
+ if (mockDef) {
838
+ if (mockDef.delay && mockDef.delay > 0) {
839
+ await sleep(mockDef.delay);
840
+ }
841
+ let mockResponse2;
842
+ if (typeof mockDef.response === "function") {
843
+ mockResponse2 = await mockDef.response(request);
844
+ } else {
845
+ mockResponse2 = mockDef.response;
846
+ }
847
+ return {
848
+ type: "mock",
849
+ response: { ...mockResponse2, id: request.id ?? mockResponse2.id }
850
+ };
851
+ }
852
+ for (const interceptor of this.request) {
853
+ const ctx = {
854
+ request: currentRequest,
855
+ meta
856
+ };
857
+ const result = await interceptor(ctx);
858
+ switch (result.action) {
859
+ case "passthrough":
860
+ break;
861
+ case "modify":
862
+ currentRequest = result.request;
863
+ break;
864
+ case "mock":
865
+ return {
866
+ type: "mock",
867
+ response: { ...result.response, id: request.id ?? result.response.id }
868
+ };
869
+ case "error":
870
+ return { type: "error", error: result.error };
871
+ }
872
+ }
873
+ return { type: "continue", request: currentRequest };
874
+ }
875
+ /**
876
+ * Process a response through the interceptor chain
877
+ */
878
+ async processResponse(request, response, durationMs) {
879
+ let currentResponse = response;
880
+ for (const interceptor of this.response) {
881
+ const ctx = {
882
+ request,
883
+ response: currentResponse,
884
+ durationMs
885
+ };
886
+ const result = await interceptor(ctx);
887
+ switch (result.action) {
888
+ case "passthrough":
889
+ break;
890
+ case "modify":
891
+ currentResponse = result.response;
892
+ break;
893
+ }
894
+ }
895
+ return currentResponse;
896
+ }
897
+ /**
898
+ * Clear all interceptors and mocks
899
+ */
900
+ clear() {
901
+ this.request = [];
902
+ this.response = [];
903
+ this.mocks.clear();
904
+ }
905
+ };
906
+ function sleep(ms) {
907
+ return new Promise((resolve) => setTimeout(resolve, ms));
908
+ }
909
+
910
+ // libs/testing/src/client/mcp-test-client.ts
911
+ var DEFAULT_TIMEOUT2 = 3e4;
912
+ var DEFAULT_PROTOCOL_VERSION = "2025-06-18";
913
+ var DEFAULT_CLIENT_INFO = {
914
+ name: "@frontmcp/testing",
915
+ version: "0.4.0"
916
+ };
917
+ var McpTestClient = class {
918
+ // Platform and capabilities are optional - only set when testing platform-specific behavior
919
+ config;
920
+ transport = null;
921
+ initResult = null;
922
+ requestIdCounter = 0;
923
+ _lastRequestId = 0;
924
+ _sessionId;
925
+ _sessionInfo = null;
926
+ _authState = { isAnonymous: true, scopes: [] };
927
+ // Logging and tracing
928
+ _logs = [];
929
+ _traces = [];
930
+ _notifications = [];
931
+ _progressUpdates = [];
932
+ // Interceptor chain
933
+ _interceptors;
934
+ // ═══════════════════════════════════════════════════════════════════
935
+ // CONSTRUCTOR & FACTORY
936
+ // ═══════════════════════════════════════════════════════════════════
937
+ constructor(config) {
938
+ this.config = {
939
+ baseUrl: config.baseUrl,
940
+ transport: config.transport ?? "streamable-http",
941
+ auth: config.auth ?? {},
942
+ publicMode: config.publicMode ?? false,
943
+ timeout: config.timeout ?? DEFAULT_TIMEOUT2,
944
+ debug: config.debug ?? false,
945
+ protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
946
+ clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
947
+ platform: config.platform,
948
+ capabilities: config.capabilities
949
+ };
950
+ if (config.auth?.token) {
951
+ this._authState = {
952
+ isAnonymous: false,
953
+ token: config.auth.token,
954
+ scopes: this.parseScopesFromToken(config.auth.token),
955
+ user: this.parseUserFromToken(config.auth.token)
956
+ };
957
+ }
958
+ this._interceptors = new DefaultInterceptorChain();
959
+ }
960
+ /**
961
+ * Create a new McpTestClientBuilder for fluent configuration
962
+ */
963
+ static create(config) {
964
+ return new McpTestClientBuilder(config);
965
+ }
966
+ // ═══════════════════════════════════════════════════════════════════
967
+ // CONNECTION & LIFECYCLE
968
+ // ═══════════════════════════════════════════════════════════════════
969
+ /**
970
+ * Connect to the MCP server and perform initialization
971
+ */
972
+ async connect() {
973
+ this.log("debug", `Connecting to ${this.config.baseUrl}...`);
974
+ this.transport = this.createTransport();
975
+ await this.transport.connect();
976
+ const initResponse = await this.initialize();
977
+ if (!initResponse.success || !initResponse.data) {
978
+ throw new Error(`Failed to initialize MCP connection: ${initResponse.error?.message ?? "Unknown error"}`);
979
+ }
980
+ this.initResult = initResponse.data;
981
+ this._sessionId = this.transport.getSessionId();
982
+ this._sessionInfo = {
983
+ id: this._sessionId ?? `session-${Date.now()}`,
984
+ createdAt: /* @__PURE__ */ new Date(),
985
+ lastActivityAt: /* @__PURE__ */ new Date(),
986
+ requestCount: 1
987
+ };
988
+ await this.transport.notify({
989
+ jsonrpc: "2.0",
990
+ method: "notifications/initialized"
991
+ });
992
+ this.log("info", `Connected to ${this.initResult.serverInfo?.name ?? "MCP Server"}`);
993
+ return this.initResult;
994
+ }
995
+ /**
996
+ * Disconnect from the MCP server
997
+ */
998
+ async disconnect() {
999
+ if (this.transport) {
1000
+ await this.transport.close();
1001
+ this.transport = null;
1002
+ }
1003
+ this.initResult = null;
1004
+ this.log("info", "Disconnected from MCP server");
1005
+ }
1006
+ /**
1007
+ * Reconnect to the server, optionally with an existing session ID
1008
+ */
1009
+ async reconnect(options) {
1010
+ await this.disconnect();
1011
+ if (options?.sessionId && this.transport) {
1012
+ this._sessionId = options.sessionId;
1013
+ }
1014
+ await this.connect();
1015
+ }
1016
+ /**
1017
+ * Check if the client is currently connected
1018
+ */
1019
+ isConnected() {
1020
+ return this.transport?.isConnected() ?? false;
1021
+ }
1022
+ // ═══════════════════════════════════════════════════════════════════
1023
+ // SESSION & AUTH PROPERTIES
1024
+ // ═══════════════════════════════════════════════════════════════════
1025
+ get sessionId() {
1026
+ return this._sessionId ?? "";
1027
+ }
1028
+ get session() {
1029
+ const info = this._sessionInfo ?? {
1030
+ id: "",
1031
+ createdAt: /* @__PURE__ */ new Date(),
1032
+ lastActivityAt: /* @__PURE__ */ new Date(),
1033
+ requestCount: 0
1034
+ };
1035
+ return {
1036
+ ...info,
1037
+ expire: async () => {
1038
+ this._sessionId = void 0;
1039
+ this._sessionInfo = null;
1040
+ }
1041
+ };
1042
+ }
1043
+ get auth() {
1044
+ return this._authState;
1045
+ }
1046
+ /**
1047
+ * Authenticate with a token
1048
+ */
1049
+ async authenticate(token) {
1050
+ this._authState = {
1051
+ isAnonymous: false,
1052
+ token,
1053
+ scopes: this.parseScopesFromToken(token),
1054
+ user: this.parseUserFromToken(token)
1055
+ };
1056
+ if (this.transport) {
1057
+ this.transport.setAuthToken(token);
1058
+ }
1059
+ this.log("debug", "Authentication updated");
1060
+ }
1061
+ // ═══════════════════════════════════════════════════════════════════
1062
+ // SERVER INFO & CAPABILITIES
1063
+ // ═══════════════════════════════════════════════════════════════════
1064
+ get serverInfo() {
1065
+ return {
1066
+ name: this.initResult?.serverInfo?.name ?? "",
1067
+ version: this.initResult?.serverInfo?.version ?? ""
1068
+ };
1069
+ }
1070
+ get protocolVersion() {
1071
+ return this.initResult?.protocolVersion ?? "";
1072
+ }
1073
+ get instructions() {
1074
+ return this.initResult?.instructions ?? "";
1075
+ }
1076
+ get capabilities() {
1077
+ return this.initResult?.capabilities ?? {};
1078
+ }
1079
+ /**
1080
+ * Check if server has a specific capability
1081
+ */
1082
+ hasCapability(name) {
1083
+ return !!this.capabilities[name];
1084
+ }
1085
+ // ═══════════════════════════════════════════════════════════════════
1086
+ // TOOLS API
1087
+ // ═══════════════════════════════════════════════════════════════════
1088
+ tools = {
1089
+ /**
1090
+ * List all available tools
1091
+ */
1092
+ list: async () => {
1093
+ const response = await this.listTools();
1094
+ if (!response.success || !response.data) {
1095
+ throw new Error(`Failed to list tools: ${response.error?.message}`);
1096
+ }
1097
+ return response.data.tools;
1098
+ },
1099
+ /**
1100
+ * Call a tool by name with arguments
1101
+ */
1102
+ call: async (name, args) => {
1103
+ const response = await this.callTool(name, args);
1104
+ return this.wrapToolResult(response);
1105
+ }
1106
+ };
1107
+ // ═══════════════════════════════════════════════════════════════════
1108
+ // RESOURCES API
1109
+ // ═══════════════════════════════════════════════════════════════════
1110
+ resources = {
1111
+ /**
1112
+ * List all static resources
1113
+ */
1114
+ list: async () => {
1115
+ const response = await this.listResources();
1116
+ if (!response.success || !response.data) {
1117
+ throw new Error(`Failed to list resources: ${response.error?.message}`);
1118
+ }
1119
+ return response.data.resources;
1120
+ },
1121
+ /**
1122
+ * List all resource templates
1123
+ */
1124
+ listTemplates: async () => {
1125
+ const response = await this.listResourceTemplates();
1126
+ if (!response.success || !response.data) {
1127
+ throw new Error(`Failed to list resource templates: ${response.error?.message}`);
1128
+ }
1129
+ return response.data.resourceTemplates;
1130
+ },
1131
+ /**
1132
+ * Read a resource by URI
1133
+ */
1134
+ read: async (uri) => {
1135
+ const response = await this.readResource(uri);
1136
+ return this.wrapResourceContent(response);
1137
+ },
1138
+ /**
1139
+ * Subscribe to resource changes (placeholder for future implementation)
1140
+ */
1141
+ subscribe: async (_uri) => {
1142
+ this.log("warn", "Resource subscription not yet implemented");
1143
+ },
1144
+ /**
1145
+ * Unsubscribe from resource changes (placeholder for future implementation)
1146
+ */
1147
+ unsubscribe: async (_uri) => {
1148
+ this.log("warn", "Resource unsubscription not yet implemented");
1149
+ }
1150
+ };
1151
+ // ═══════════════════════════════════════════════════════════════════
1152
+ // PROMPTS API
1153
+ // ═══════════════════════════════════════════════════════════════════
1154
+ prompts = {
1155
+ /**
1156
+ * List all available prompts
1157
+ */
1158
+ list: async () => {
1159
+ const response = await this.listPrompts();
1160
+ if (!response.success || !response.data) {
1161
+ throw new Error(`Failed to list prompts: ${response.error?.message}`);
1162
+ }
1163
+ return response.data.prompts;
1164
+ },
1165
+ /**
1166
+ * Get a prompt with arguments
1167
+ */
1168
+ get: async (name, args) => {
1169
+ const response = await this.getPrompt(name, args);
1170
+ return this.wrapPromptResult(response);
1171
+ }
1172
+ };
1173
+ // ═══════════════════════════════════════════════════════════════════
1174
+ // RAW PROTOCOL ACCESS
1175
+ // ═══════════════════════════════════════════════════════════════════
1176
+ raw = {
1177
+ /**
1178
+ * Send any JSON-RPC request
1179
+ */
1180
+ request: async (message) => {
1181
+ this.ensureConnected();
1182
+ const start = Date.now();
1183
+ const response = await this.transport.request(message);
1184
+ this.traceRequest(message.method, message.params, message.id, response, Date.now() - start);
1185
+ return response;
1186
+ },
1187
+ /**
1188
+ * Send a notification (no response expected)
1189
+ */
1190
+ notify: async (message) => {
1191
+ this.ensureConnected();
1192
+ await this.transport.notify(message);
1193
+ },
1194
+ /**
1195
+ * Send raw string data (for error testing)
1196
+ */
1197
+ sendRaw: async (data) => {
1198
+ this.ensureConnected();
1199
+ return this.transport.sendRaw(data);
1200
+ }
1201
+ };
1202
+ get lastRequestId() {
1203
+ return this._lastRequestId;
1204
+ }
1205
+ // ═══════════════════════════════════════════════════════════════════
1206
+ // TRANSPORT INFO
1207
+ // ═══════════════════════════════════════════════════════════════════
1208
+ /**
1209
+ * Get transport information and utilities
1210
+ */
1211
+ get transport_info() {
1212
+ return {
1213
+ type: this.config.transport,
1214
+ isConnected: () => this.transport?.isConnected() ?? false,
1215
+ messageEndpoint: this.transport?.getMessageEndpoint?.(),
1216
+ connectionCount: this.transport?.getConnectionCount?.() ?? 0,
1217
+ reconnectCount: this.transport?.getReconnectCount?.() ?? 0,
1218
+ lastRequestHeaders: this.transport?.getLastRequestHeaders?.() ?? {},
1219
+ simulateDisconnect: async () => {
1220
+ await this.transport?.simulateDisconnect?.();
1221
+ },
1222
+ waitForReconnect: async (timeoutMs) => {
1223
+ await this.transport?.waitForReconnect?.(timeoutMs);
1224
+ }
1225
+ };
1226
+ }
1227
+ // Alias for transport info
1228
+ get transport_() {
1229
+ return this.transport_info;
1230
+ }
1231
+ // ═══════════════════════════════════════════════════════════════════
1232
+ // NOTIFICATIONS
1233
+ // ═══════════════════════════════════════════════════════════════════
1234
+ notifications = {
1235
+ /**
1236
+ * Start collecting server notifications
1237
+ */
1238
+ collect: () => {
1239
+ return new NotificationCollector(this._notifications);
1240
+ },
1241
+ /**
1242
+ * Collect progress notifications specifically
1243
+ */
1244
+ collectProgress: () => {
1245
+ return new ProgressCollector(this._progressUpdates);
1246
+ },
1247
+ /**
1248
+ * Send a notification to the server
1249
+ */
1250
+ send: async (method, params) => {
1251
+ await this.raw.notify({ jsonrpc: "2.0", method, params });
1252
+ }
1253
+ };
1254
+ // ═══════════════════════════════════════════════════════════════════
1255
+ // LOGGING & DEBUGGING
1256
+ // ═══════════════════════════════════════════════════════════════════
1257
+ logs = {
1258
+ all: () => [...this._logs],
1259
+ filter: (level) => this._logs.filter((l) => l.level === level),
1260
+ search: (text) => this._logs.filter((l) => l.message.includes(text)),
1261
+ last: () => this._logs[this._logs.length - 1],
1262
+ clear: () => {
1263
+ this._logs = [];
1264
+ }
1265
+ };
1266
+ trace = {
1267
+ all: () => [...this._traces],
1268
+ last: () => this._traces[this._traces.length - 1],
1269
+ clear: () => {
1270
+ this._traces = [];
1271
+ }
1272
+ };
1273
+ // ═══════════════════════════════════════════════════════════════════
1274
+ // MOCKING & INTERCEPTION
1275
+ // ═══════════════════════════════════════════════════════════════════
1276
+ /**
1277
+ * API for mocking MCP requests
1278
+ *
1279
+ * @example
1280
+ * ```typescript
1281
+ * // Mock a specific tool call
1282
+ * const handle = mcp.mock.tool('my-tool', { result: 'mocked!' });
1283
+ *
1284
+ * // Mock with params matching
1285
+ * mcp.mock.add({
1286
+ * method: 'tools/call',
1287
+ * params: { name: 'my-tool' },
1288
+ * response: mockResponse.toolResult([{ type: 'text', text: 'mocked' }]),
1289
+ * });
1290
+ *
1291
+ * // Clear all mocks after test
1292
+ * mcp.mock.clear();
1293
+ * ```
1294
+ */
1295
+ mock = {
1296
+ /**
1297
+ * Add a mock definition
1298
+ */
1299
+ add: (mock) => {
1300
+ return this._interceptors.mocks.add(mock);
1301
+ },
1302
+ /**
1303
+ * Mock a tools/call request for a specific tool
1304
+ */
1305
+ tool: (name, result, options) => {
1306
+ return this._interceptors.mocks.add({
1307
+ method: "tools/call",
1308
+ params: { name },
1309
+ response: mockResponse.toolResult([
1310
+ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result) }
1311
+ ]),
1312
+ times: options?.times,
1313
+ delay: options?.delay
1314
+ });
1315
+ },
1316
+ /**
1317
+ * Mock a tools/call request to return an error
1318
+ */
1319
+ toolError: (name, code, message, options) => {
1320
+ return this._interceptors.mocks.add({
1321
+ method: "tools/call",
1322
+ params: { name },
1323
+ response: mockResponse.error(code, message),
1324
+ times: options?.times
1325
+ });
1326
+ },
1327
+ /**
1328
+ * Mock a resources/read request
1329
+ */
1330
+ resource: (uri, content, options) => {
1331
+ const contentObj = typeof content === "string" ? { uri, text: content } : { uri, ...content };
1332
+ return this._interceptors.mocks.add({
1333
+ method: "resources/read",
1334
+ params: { uri },
1335
+ response: mockResponse.resourceRead([contentObj]),
1336
+ times: options?.times,
1337
+ delay: options?.delay
1338
+ });
1339
+ },
1340
+ /**
1341
+ * Mock a resources/read request to return an error
1342
+ */
1343
+ resourceError: (uri, options) => {
1344
+ return this._interceptors.mocks.add({
1345
+ method: "resources/read",
1346
+ params: { uri },
1347
+ response: mockResponse.errors.resourceNotFound(uri),
1348
+ times: options?.times
1349
+ });
1350
+ },
1351
+ /**
1352
+ * Mock the tools/list response
1353
+ */
1354
+ toolsList: (tools, options) => {
1355
+ return this._interceptors.mocks.add({
1356
+ method: "tools/list",
1357
+ response: mockResponse.toolsList(tools),
1358
+ times: options?.times
1359
+ });
1360
+ },
1361
+ /**
1362
+ * Mock the resources/list response
1363
+ */
1364
+ resourcesList: (resources, options) => {
1365
+ return this._interceptors.mocks.add({
1366
+ method: "resources/list",
1367
+ response: mockResponse.resourcesList(resources),
1368
+ times: options?.times
1369
+ });
1370
+ },
1371
+ /**
1372
+ * Clear all mocks
1373
+ */
1374
+ clear: () => {
1375
+ this._interceptors.mocks.clear();
1376
+ },
1377
+ /**
1378
+ * Get all active mocks
1379
+ */
1380
+ all: () => {
1381
+ return this._interceptors.mocks.getAll();
1382
+ }
1383
+ };
1384
+ /**
1385
+ * API for intercepting requests and responses
1386
+ *
1387
+ * @example
1388
+ * ```typescript
1389
+ * // Log all requests
1390
+ * const remove = mcp.intercept.request((ctx) => {
1391
+ * console.log('Request:', ctx.request.method);
1392
+ * return { action: 'passthrough' };
1393
+ * });
1394
+ *
1395
+ * // Modify requests
1396
+ * mcp.intercept.request((ctx) => {
1397
+ * if (ctx.request.method === 'tools/call') {
1398
+ * return {
1399
+ * action: 'modify',
1400
+ * request: { ...ctx.request, params: { ...ctx.request.params, extra: true } },
1401
+ * };
1402
+ * }
1403
+ * return { action: 'passthrough' };
1404
+ * });
1405
+ *
1406
+ * // Add latency to all requests
1407
+ * mcp.intercept.delay(100);
1408
+ *
1409
+ * // Clean up
1410
+ * remove();
1411
+ * mcp.intercept.clear();
1412
+ * ```
1413
+ */
1414
+ intercept = {
1415
+ /**
1416
+ * Add a request interceptor
1417
+ * @returns Function to remove the interceptor
1418
+ */
1419
+ request: (interceptor) => {
1420
+ return this._interceptors.addRequestInterceptor(interceptor);
1421
+ },
1422
+ /**
1423
+ * Add a response interceptor
1424
+ * @returns Function to remove the interceptor
1425
+ */
1426
+ response: (interceptor) => {
1427
+ return this._interceptors.addResponseInterceptor(interceptor);
1428
+ },
1429
+ /**
1430
+ * Add latency to all requests
1431
+ * @returns Function to remove the interceptor
1432
+ */
1433
+ delay: (ms) => {
1434
+ return this._interceptors.addRequestInterceptor(async () => {
1435
+ await new Promise((r) => setTimeout(r, ms));
1436
+ return { action: "passthrough" };
1437
+ });
1438
+ },
1439
+ /**
1440
+ * Fail requests matching a method
1441
+ * @returns Function to remove the interceptor
1442
+ */
1443
+ failMethod: (method, error) => {
1444
+ return this._interceptors.addRequestInterceptor((ctx) => {
1445
+ if (ctx.request.method === method) {
1446
+ return { action: "error", error: new Error(error ?? `Intercepted: ${method}`) };
1447
+ }
1448
+ return { action: "passthrough" };
1449
+ });
1450
+ },
1451
+ /**
1452
+ * Clear all interceptors (but not mocks)
1453
+ */
1454
+ clear: () => {
1455
+ this._interceptors.request = [];
1456
+ this._interceptors.response = [];
1457
+ },
1458
+ /**
1459
+ * Clear everything (interceptors and mocks)
1460
+ */
1461
+ clearAll: () => {
1462
+ this._interceptors.clear();
1463
+ }
1464
+ };
1465
+ // ═══════════════════════════════════════════════════════════════════
1466
+ // TIMEOUT
1467
+ // ═══════════════════════════════════════════════════════════════════
1468
+ setTimeout(ms) {
1469
+ this.config.timeout = ms;
1470
+ if (this.transport) {
1471
+ this.transport.setTimeout(ms);
1472
+ }
1473
+ }
1474
+ // ═══════════════════════════════════════════════════════════════════
1475
+ // PRIVATE: MCP OPERATIONS
1476
+ // ═══════════════════════════════════════════════════════════════════
1477
+ async initialize() {
1478
+ const capabilities = this.config.capabilities ?? {
1479
+ sampling: {}
1480
+ };
1481
+ return this.request("initialize", {
1482
+ protocolVersion: this.config.protocolVersion,
1483
+ capabilities,
1484
+ clientInfo: this.config.clientInfo
1485
+ });
1486
+ }
1487
+ async listTools() {
1488
+ return this.request("tools/list", {});
1489
+ }
1490
+ async callTool(name, args) {
1491
+ return this.request("tools/call", {
1492
+ name,
1493
+ arguments: args ?? {}
1494
+ });
1495
+ }
1496
+ async listResources() {
1497
+ return this.request("resources/list", {});
1498
+ }
1499
+ async listResourceTemplates() {
1500
+ return this.request("resources/templates/list", {});
1501
+ }
1502
+ async readResource(uri) {
1503
+ return this.request("resources/read", { uri });
1504
+ }
1505
+ async listPrompts() {
1506
+ return this.request("prompts/list", {});
1507
+ }
1508
+ async getPrompt(name, args) {
1509
+ return this.request("prompts/get", {
1510
+ name,
1511
+ arguments: args ?? {}
1512
+ });
1513
+ }
1514
+ // ═══════════════════════════════════════════════════════════════════
1515
+ // PRIVATE: TRANSPORT & REQUEST HELPERS
1516
+ // ═══════════════════════════════════════════════════════════════════
1517
+ createTransport() {
1518
+ switch (this.config.transport) {
1519
+ case "streamable-http":
1520
+ return new StreamableHttpTransport({
1521
+ baseUrl: this.config.baseUrl,
1522
+ timeout: this.config.timeout,
1523
+ auth: this.config.auth,
1524
+ publicMode: this.config.publicMode,
1525
+ debug: this.config.debug,
1526
+ interceptors: this._interceptors,
1527
+ clientInfo: this.config.clientInfo
1528
+ });
1529
+ case "sse":
1530
+ throw new Error("SSE transport not yet implemented");
1531
+ default:
1532
+ throw new Error(`Unknown transport type: ${this.config.transport}`);
1533
+ }
1534
+ }
1535
+ async request(method, params) {
1536
+ this.ensureConnected();
1537
+ const id = ++this.requestIdCounter;
1538
+ this._lastRequestId = id;
1539
+ const start = Date.now();
1540
+ try {
1541
+ const response = await this.transport.request({
1542
+ jsonrpc: "2.0",
1543
+ id,
1544
+ method,
1545
+ params
1546
+ });
1547
+ const durationMs = Date.now() - start;
1548
+ this.updateSessionActivity();
1549
+ if ("error" in response && response.error) {
1550
+ const error = response.error;
1551
+ this.traceRequest(method, params, id, response, durationMs);
1552
+ return {
1553
+ success: false,
1554
+ error,
1555
+ durationMs,
1556
+ requestId: id
1557
+ };
1558
+ }
1559
+ this.traceRequest(method, params, id, response, durationMs);
1560
+ return {
1561
+ success: true,
1562
+ data: response.result,
1563
+ durationMs,
1564
+ requestId: id
1565
+ };
1566
+ } catch (err) {
1567
+ const durationMs = Date.now() - start;
1568
+ const error = {
1569
+ code: -32603,
1570
+ message: err instanceof Error ? err.message : "Unknown error"
1571
+ };
1572
+ return {
1573
+ success: false,
1574
+ error,
1575
+ durationMs,
1576
+ requestId: id
1577
+ };
1578
+ }
1579
+ }
1580
+ ensureConnected() {
1581
+ if (!this.transport?.isConnected()) {
1582
+ throw new Error("Not connected to MCP server. Call connect() first.");
1583
+ }
1584
+ }
1585
+ updateSessionActivity() {
1586
+ if (this._sessionInfo) {
1587
+ this._sessionInfo.lastActivityAt = /* @__PURE__ */ new Date();
1588
+ this._sessionInfo.requestCount++;
1589
+ }
1590
+ }
1591
+ // ═══════════════════════════════════════════════════════════════════
1592
+ // PRIVATE: RESULT WRAPPERS
1593
+ // ═══════════════════════════════════════════════════════════════════
1594
+ wrapToolResult(response) {
1595
+ const raw = response.data ?? { content: [] };
1596
+ const isError = !response.success || raw.isError === true;
1597
+ const meta = raw._meta;
1598
+ const hasUI = meta?.["ui/html"] !== void 0 || meta?.["ui/component"] !== void 0 || meta?.["openai/html"] !== void 0 || meta?.["frontmcp/html"] !== void 0;
1599
+ const structuredContent = raw["structuredContent"];
1600
+ return {
1601
+ raw,
1602
+ isSuccess: !isError,
1603
+ isError,
1604
+ error: response.error,
1605
+ durationMs: response.durationMs,
1606
+ json() {
1607
+ if (hasUI && structuredContent !== void 0) {
1608
+ return structuredContent;
1609
+ }
1610
+ const textContent = raw.content?.find((c) => c.type === "text");
1611
+ if (textContent && "text" in textContent) {
1612
+ return JSON.parse(textContent.text);
1613
+ }
1614
+ throw new Error("No text content to parse as JSON");
1615
+ },
1616
+ text() {
1617
+ const textContent = raw.content?.find((c) => c.type === "text");
1618
+ if (textContent && "text" in textContent) {
1619
+ return textContent.text;
1620
+ }
1621
+ return void 0;
1622
+ },
1623
+ hasTextContent() {
1624
+ return raw.content?.some((c) => c.type === "text") ?? false;
1625
+ },
1626
+ hasImageContent() {
1627
+ return raw.content?.some((c) => c.type === "image") ?? false;
1628
+ },
1629
+ hasResourceContent() {
1630
+ return raw.content?.some((c) => c.type === "resource") ?? false;
1631
+ },
1632
+ hasToolUI() {
1633
+ return hasUI;
1634
+ }
1635
+ };
1636
+ }
1637
+ wrapResourceContent(response) {
1638
+ const raw = response.data ?? { contents: [] };
1639
+ const isError = !response.success;
1640
+ const firstContent = raw.contents?.[0];
1641
+ return {
1642
+ raw,
1643
+ isSuccess: !isError,
1644
+ isError,
1645
+ error: response.error,
1646
+ durationMs: response.durationMs,
1647
+ json() {
1648
+ if (firstContent && "text" in firstContent) {
1649
+ return JSON.parse(firstContent.text);
1650
+ }
1651
+ throw new Error("No text content to parse as JSON");
1652
+ },
1653
+ text() {
1654
+ if (firstContent && "text" in firstContent) {
1655
+ return firstContent.text;
1656
+ }
1657
+ return void 0;
1658
+ },
1659
+ mimeType() {
1660
+ return firstContent?.mimeType;
1661
+ },
1662
+ hasMimeType(type) {
1663
+ return firstContent?.mimeType === type;
1664
+ }
1665
+ };
1666
+ }
1667
+ wrapPromptResult(response) {
1668
+ const raw = response.data ?? { messages: [] };
1669
+ const isError = !response.success;
1670
+ return {
1671
+ raw,
1672
+ isSuccess: !isError,
1673
+ isError,
1674
+ error: response.error,
1675
+ durationMs: response.durationMs,
1676
+ messages: raw.messages ?? [],
1677
+ description: raw.description
1678
+ };
1679
+ }
1680
+ // ═══════════════════════════════════════════════════════════════════
1681
+ // PRIVATE: LOGGING & TRACING
1682
+ // ═══════════════════════════════════════════════════════════════════
1683
+ log(level, message, data) {
1684
+ const entry = {
1685
+ level,
1686
+ message,
1687
+ timestamp: /* @__PURE__ */ new Date(),
1688
+ data
1689
+ };
1690
+ this._logs.push(entry);
1691
+ if (this.config.debug) {
1692
+ console.log(`[${level.toUpperCase()}] ${message}`, data ?? "");
1693
+ }
1694
+ }
1695
+ traceRequest(method, params, id, response, durationMs) {
1696
+ this._traces.push({
1697
+ request: { method, params, id },
1698
+ response: {
1699
+ result: "result" in response ? response.result : void 0,
1700
+ error: "error" in response ? response.error : void 0
1701
+ },
1702
+ durationMs,
1703
+ timestamp: /* @__PURE__ */ new Date()
1704
+ });
1705
+ }
1706
+ // ═══════════════════════════════════════════════════════════════════
1707
+ // PRIVATE: TOKEN PARSING
1708
+ // ═══════════════════════════════════════════════════════════════════
1709
+ parseScopesFromToken(token) {
1710
+ try {
1711
+ const payload = this.decodeJwtPayload(token);
1712
+ if (!payload) return [];
1713
+ const scope = payload["scope"];
1714
+ const scopes = payload["scopes"];
1715
+ if (typeof scope === "string") {
1716
+ return scope.split(" ");
1717
+ }
1718
+ if (Array.isArray(scopes)) {
1719
+ return scopes;
1720
+ }
1721
+ return [];
1722
+ } catch {
1723
+ return [];
1724
+ }
1725
+ }
1726
+ parseUserFromToken(token) {
1727
+ try {
1728
+ const payload = this.decodeJwtPayload(token);
1729
+ const sub = payload?.["sub"];
1730
+ if (!sub || typeof sub !== "string") return void 0;
1731
+ return {
1732
+ sub,
1733
+ email: payload["email"],
1734
+ name: payload["name"]
1735
+ };
1736
+ } catch {
1737
+ return void 0;
1738
+ }
1739
+ }
1740
+ decodeJwtPayload(token) {
1741
+ try {
1742
+ const parts = token.split(".");
1743
+ if (parts.length !== 3) return null;
1744
+ const payload = Buffer.from(parts[1], "base64url").toString("utf-8");
1745
+ return JSON.parse(payload);
1746
+ } catch {
1747
+ return null;
1748
+ }
1749
+ }
1750
+ };
1751
+ var NotificationCollector = class {
1752
+ constructor(notifications) {
1753
+ this.notifications = notifications;
1754
+ }
1755
+ get received() {
1756
+ return [...this.notifications];
1757
+ }
1758
+ has(method) {
1759
+ return this.notifications.some((n) => n.method === method);
1760
+ }
1761
+ async waitFor(method, timeoutMs) {
1762
+ const deadline = Date.now() + timeoutMs;
1763
+ while (Date.now() < deadline) {
1764
+ const found = this.notifications.find((n) => n.method === method);
1765
+ if (found) return found;
1766
+ await new Promise((r) => setTimeout(r, 50));
1767
+ }
1768
+ throw new Error(`Timeout waiting for notification: ${method}`);
1769
+ }
1770
+ };
1771
+ var ProgressCollector = class {
1772
+ constructor(updates) {
1773
+ this.updates = updates;
1774
+ }
1775
+ get all() {
1776
+ return [...this.updates];
1777
+ }
1778
+ async waitForComplete(timeoutMs) {
1779
+ const deadline = Date.now() + timeoutMs;
1780
+ while (Date.now() < deadline) {
1781
+ const last = this.updates[this.updates.length - 1];
1782
+ if (last && last.total !== void 0 && last.progress >= last.total) {
1783
+ return;
1784
+ }
1785
+ await new Promise((r) => setTimeout(r, 50));
1786
+ }
1787
+ throw new Error("Timeout waiting for progress to complete");
1788
+ }
1789
+ };
1790
+
1791
+ // libs/testing/src/auth/token-factory.ts
1792
+ var import_jose = require("jose");
1793
+ var TestTokenFactory = class {
1794
+ issuer;
1795
+ audience;
1796
+ privateKey = null;
1797
+ publicKey = null;
1798
+ jwk = null;
1799
+ keyId;
1800
+ constructor(options = {}) {
1801
+ this.issuer = options.issuer ?? "https://test.frontmcp.local";
1802
+ this.audience = options.audience ?? "frontmcp-test";
1803
+ this.keyId = `test-key-${Date.now()}`;
1804
+ }
1805
+ /**
1806
+ * Initialize the key pair (called automatically on first use)
1807
+ */
1808
+ async ensureKeys() {
1809
+ if (this.privateKey && this.publicKey) return;
1810
+ const { publicKey, privateKey } = await (0, import_jose.generateKeyPair)("RS256", {
1811
+ extractable: true
1812
+ });
1813
+ this.privateKey = privateKey;
1814
+ this.publicKey = publicKey;
1815
+ this.jwk = await (0, import_jose.exportJWK)(publicKey);
1816
+ this.jwk.kid = this.keyId;
1817
+ this.jwk.use = "sig";
1818
+ this.jwk.alg = "RS256";
1819
+ }
1820
+ /**
1821
+ * Create a JWT token with the specified claims
1822
+ */
1823
+ async createTestToken(options) {
1824
+ await this.ensureKeys();
1825
+ const now = Math.floor(Date.now() / 1e3);
1826
+ const exp = options.exp ?? 3600;
1827
+ const payload = {
1828
+ iss: options.iss ?? this.issuer,
1829
+ sub: options.sub,
1830
+ aud: options.aud ?? this.audience,
1831
+ iat: now,
1832
+ exp: now + exp,
1833
+ scope: options.scopes?.join(" "),
1834
+ ...options.claims
1835
+ };
1836
+ const token = await new import_jose.SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId }).sign(this.privateKey);
1837
+ return token;
1838
+ }
1839
+ /**
1840
+ * Create an admin token with full access
1841
+ */
1842
+ async createAdminToken(sub = "admin-001") {
1843
+ return this.createTestToken({
1844
+ sub,
1845
+ scopes: ["admin:*", "read", "write", "delete"],
1846
+ claims: {
1847
+ email: "admin@test.local",
1848
+ name: "Test Admin",
1849
+ role: "admin"
1850
+ }
1851
+ });
1852
+ }
1853
+ /**
1854
+ * Create a regular user token
1855
+ */
1856
+ async createUserToken(sub = "user-001", scopes = ["read", "write"]) {
1857
+ return this.createTestToken({
1858
+ sub,
1859
+ scopes,
1860
+ claims: {
1861
+ email: "user@test.local",
1862
+ name: "Test User",
1863
+ role: "user"
1864
+ }
1865
+ });
1866
+ }
1867
+ /**
1868
+ * Create an anonymous user token
1869
+ */
1870
+ async createAnonymousToken() {
1871
+ return this.createTestToken({
1872
+ sub: `anon:${Date.now()}`,
1873
+ scopes: ["anonymous"],
1874
+ claims: {
1875
+ name: "Anonymous",
1876
+ role: "anonymous"
1877
+ }
1878
+ });
1879
+ }
1880
+ /**
1881
+ * Create an expired token (for testing token expiration)
1882
+ */
1883
+ async createExpiredToken(options) {
1884
+ await this.ensureKeys();
1885
+ const now = Math.floor(Date.now() / 1e3);
1886
+ const payload = {
1887
+ iss: this.issuer,
1888
+ sub: options.sub,
1889
+ aud: this.audience,
1890
+ iat: now - 7200,
1891
+ // 2 hours ago
1892
+ exp: now - 3600
1893
+ // Expired 1 hour ago
1894
+ };
1895
+ const token = await new import_jose.SignJWT(payload).setProtectedHeader({ alg: "RS256", kid: this.keyId }).sign(this.privateKey);
1896
+ return token;
1897
+ }
1898
+ /**
1899
+ * Create a token with an invalid signature (for testing signature validation)
1900
+ */
1901
+ createTokenWithInvalidSignature(options) {
1902
+ const now = Math.floor(Date.now() / 1e3);
1903
+ const header = Buffer.from(JSON.stringify({ alg: "RS256", kid: this.keyId })).toString("base64url");
1904
+ const payload = Buffer.from(
1905
+ JSON.stringify({
1906
+ iss: this.issuer,
1907
+ sub: options.sub,
1908
+ aud: this.audience,
1909
+ iat: now,
1910
+ exp: now + 3600
1911
+ })
1912
+ ).toString("base64url");
1913
+ const signature = Buffer.from("invalid-signature-" + Date.now()).toString("base64url");
1914
+ return `${header}.${payload}.${signature}`;
1915
+ }
1916
+ /**
1917
+ * Get the public JWKS for verifying tokens
1918
+ */
1919
+ async getPublicJwks() {
1920
+ await this.ensureKeys();
1921
+ return {
1922
+ keys: [this.jwk]
1923
+ };
1924
+ }
1925
+ /**
1926
+ * Get the issuer URL
1927
+ */
1928
+ getIssuer() {
1929
+ return this.issuer;
1930
+ }
1931
+ /**
1932
+ * Get the audience
1933
+ */
1934
+ getAudience() {
1935
+ return this.audience;
1936
+ }
1937
+ };
1938
+
1939
+ // libs/testing/src/server/test-server.ts
1940
+ var import_child_process = require("child_process");
1941
+ var TestServer = class _TestServer {
1942
+ process = null;
1943
+ options;
1944
+ _info;
1945
+ logs = [];
1946
+ constructor(options, port) {
1947
+ this.options = {
1948
+ port,
1949
+ command: options.command ?? "",
1950
+ cwd: options.cwd ?? process.cwd(),
1951
+ env: options.env ?? {},
1952
+ startupTimeout: options.startupTimeout ?? 3e4,
1953
+ healthCheckPath: options.healthCheckPath ?? "/health",
1954
+ debug: options.debug ?? false
1955
+ };
1956
+ this._info = {
1957
+ baseUrl: `http://localhost:${port}`,
1958
+ port
1959
+ };
1960
+ }
1961
+ /**
1962
+ * Start a test server with custom command
1963
+ */
1964
+ static async start(options) {
1965
+ const port = options.port ?? await findAvailablePort();
1966
+ const server = new _TestServer(options, port);
1967
+ try {
1968
+ await server.startProcess();
1969
+ } catch (error) {
1970
+ await server.stop();
1971
+ throw error;
1972
+ }
1973
+ return server;
1974
+ }
1975
+ /**
1976
+ * Start an Nx project as test server
1977
+ */
1978
+ static async startNx(project, options = {}) {
1979
+ if (!/^[\w-]+$/.test(project)) {
1980
+ throw new Error(
1981
+ `Invalid project name: ${project}. Must contain only alphanumeric, underscore, and hyphen characters.`
1982
+ );
1983
+ }
1984
+ const port = options.port ?? await findAvailablePort();
1985
+ const serverOptions = {
1986
+ ...options,
1987
+ port,
1988
+ command: `npx nx serve ${project} --port ${port}`,
1989
+ cwd: options.cwd ?? process.cwd()
1990
+ };
1991
+ const server = new _TestServer(serverOptions, port);
1992
+ try {
1993
+ await server.startProcess();
1994
+ } catch (error) {
1995
+ await server.stop();
1996
+ throw error;
1997
+ }
1998
+ return server;
1999
+ }
2000
+ /**
2001
+ * Create a test server connected to an already running server
2002
+ */
2003
+ static connect(baseUrl) {
2004
+ const url = new URL(baseUrl);
2005
+ const port = parseInt(url.port, 10) || (url.protocol === "https:" ? 443 : 80);
2006
+ const server = new _TestServer(
2007
+ {
2008
+ command: "",
2009
+ port
2010
+ },
2011
+ port
2012
+ );
2013
+ server._info = {
2014
+ baseUrl: baseUrl.replace(/\/$/, ""),
2015
+ port
2016
+ };
2017
+ return server;
2018
+ }
2019
+ /**
2020
+ * Get server information
2021
+ */
2022
+ get info() {
2023
+ return { ...this._info };
2024
+ }
2025
+ /**
2026
+ * Stop the test server
2027
+ */
2028
+ async stop() {
2029
+ if (this.process) {
2030
+ this.log("Stopping server...");
2031
+ this.process.kill("SIGTERM");
2032
+ const exitPromise = new Promise((resolve) => {
2033
+ if (this.process) {
2034
+ this.process.once("exit", () => resolve());
2035
+ } else {
2036
+ resolve();
2037
+ }
2038
+ });
2039
+ const killTimeout = setTimeout(() => {
2040
+ if (this.process) {
2041
+ this.log("Force killing server after timeout...");
2042
+ this.process.kill("SIGKILL");
2043
+ }
2044
+ }, 5e3);
2045
+ await exitPromise;
2046
+ clearTimeout(killTimeout);
2047
+ this.process = null;
2048
+ this.log("Server stopped");
2049
+ }
2050
+ }
2051
+ /**
2052
+ * Wait for server to be ready
2053
+ */
2054
+ async waitForReady(timeout) {
2055
+ const timeoutMs = timeout ?? this.options.startupTimeout;
2056
+ const deadline = Date.now() + timeoutMs;
2057
+ const checkInterval = 100;
2058
+ while (Date.now() < deadline) {
2059
+ try {
2060
+ const response = await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`, {
2061
+ method: "GET",
2062
+ signal: AbortSignal.timeout(1e3)
2063
+ });
2064
+ if (response.ok || response.status === 404) {
2065
+ this.log("Server is ready");
2066
+ return;
2067
+ }
2068
+ } catch {
2069
+ }
2070
+ await sleep2(checkInterval);
2071
+ }
2072
+ throw new Error(`Server did not become ready within ${timeoutMs}ms`);
2073
+ }
2074
+ /**
2075
+ * Restart the server
2076
+ */
2077
+ async restart() {
2078
+ await this.stop();
2079
+ await this.startProcess();
2080
+ }
2081
+ /**
2082
+ * Get captured server logs
2083
+ */
2084
+ getLogs() {
2085
+ return [...this.logs];
2086
+ }
2087
+ /**
2088
+ * Clear captured logs
2089
+ */
2090
+ clearLogs() {
2091
+ this.logs = [];
2092
+ }
2093
+ // ═══════════════════════════════════════════════════════════════════
2094
+ // PRIVATE METHODS
2095
+ // ═══════════════════════════════════════════════════════════════════
2096
+ async startProcess() {
2097
+ if (!this.options.command) {
2098
+ await this.waitForReady();
2099
+ return;
2100
+ }
2101
+ this.log(`Starting server: ${this.options.command}`);
2102
+ const env = {
2103
+ ...process.env,
2104
+ ...this.options.env,
2105
+ PORT: String(this.options.port)
2106
+ };
2107
+ this.process = (0, import_child_process.spawn)(this.options.command, [], {
2108
+ cwd: this.options.cwd,
2109
+ env,
2110
+ shell: true,
2111
+ stdio: ["pipe", "pipe", "pipe"]
2112
+ });
2113
+ if (this.process.pid !== void 0) {
2114
+ this._info.pid = this.process.pid;
2115
+ }
2116
+ let processExited = false;
2117
+ let exitCode = null;
2118
+ let exitError = null;
2119
+ this.process.stdout?.on("data", (data) => {
2120
+ const text = data.toString();
2121
+ this.logs.push(text);
2122
+ if (this.options.debug) {
2123
+ console.log("[SERVER]", text);
2124
+ }
2125
+ });
2126
+ this.process.stderr?.on("data", (data) => {
2127
+ const text = data.toString();
2128
+ this.logs.push(`[ERROR] ${text}`);
2129
+ if (this.options.debug) {
2130
+ console.error("[SERVER ERROR]", text);
2131
+ }
2132
+ });
2133
+ this.process.on("error", (err) => {
2134
+ this.logs.push(`[SPAWN ERROR] ${err.message}`);
2135
+ exitError = err;
2136
+ if (this.options.debug) {
2137
+ console.error("[SERVER SPAWN ERROR]", err);
2138
+ }
2139
+ });
2140
+ this.process.once("exit", (code) => {
2141
+ processExited = true;
2142
+ exitCode = code;
2143
+ this.log(`Server process exited with code ${code}`);
2144
+ });
2145
+ await this.waitForReadyWithExitDetection(() => {
2146
+ if (exitError) {
2147
+ return { exited: true, error: exitError };
2148
+ }
2149
+ if (processExited) {
2150
+ const recentLogs = this.logs.slice(-10).join("\n");
2151
+ return {
2152
+ exited: true,
2153
+ error: new Error(`Server process exited unexpectedly with code ${exitCode}.
2154
+
2155
+ Recent logs:
2156
+ ${recentLogs}`)
2157
+ };
2158
+ }
2159
+ return { exited: false };
2160
+ });
2161
+ }
2162
+ /**
2163
+ * Wait for server to be ready, but also detect early process exit
2164
+ */
2165
+ async waitForReadyWithExitDetection(checkExit) {
2166
+ const timeoutMs = this.options.startupTimeout;
2167
+ const deadline = Date.now() + timeoutMs;
2168
+ const checkInterval = 100;
2169
+ while (Date.now() < deadline) {
2170
+ const exitStatus = checkExit();
2171
+ if (exitStatus.exited) {
2172
+ throw exitStatus.error ?? new Error("Server process exited unexpectedly");
2173
+ }
2174
+ try {
2175
+ const response = await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`, {
2176
+ method: "GET",
2177
+ signal: AbortSignal.timeout(1e3)
2178
+ });
2179
+ if (response.ok || response.status === 404) {
2180
+ this.log("Server is ready");
2181
+ return;
2182
+ }
2183
+ } catch {
2184
+ }
2185
+ await sleep2(checkInterval);
2186
+ }
2187
+ const finalExitStatus = checkExit();
2188
+ if (finalExitStatus.exited) {
2189
+ throw finalExitStatus.error ?? new Error("Server process exited unexpectedly");
2190
+ }
2191
+ throw new Error(`Server did not become ready within ${timeoutMs}ms`);
2192
+ }
2193
+ log(message) {
2194
+ if (this.options.debug) {
2195
+ console.log(`[TestServer] ${message}`);
2196
+ }
2197
+ }
2198
+ };
2199
+ async function findAvailablePort() {
2200
+ const { createServer } = await import("net");
2201
+ return new Promise((resolve, reject) => {
2202
+ const server = createServer();
2203
+ server.listen(0, () => {
2204
+ const address = server.address();
2205
+ if (address && typeof address !== "string") {
2206
+ const port = address.port;
2207
+ server.close(() => resolve(port));
2208
+ } else {
2209
+ reject(new Error("Could not get port"));
2210
+ }
2211
+ });
2212
+ server.on("error", reject);
2213
+ });
2214
+ }
2215
+ function sleep2(ms) {
2216
+ return new Promise((resolve) => setTimeout(resolve, ms));
2217
+ }
2218
+
2219
+ // libs/testing/src/fixtures/test-fixture.ts
2220
+ var currentConfig = {};
2221
+ var serverInstance = null;
2222
+ var tokenFactory = null;
2223
+ var serverStartedByUs = false;
2224
+ async function initializeSharedResources() {
2225
+ if (!tokenFactory) {
2226
+ tokenFactory = new TestTokenFactory();
2227
+ }
2228
+ if (!serverInstance) {
2229
+ if (currentConfig.baseUrl) {
2230
+ serverInstance = TestServer.connect(currentConfig.baseUrl);
2231
+ serverStartedByUs = false;
2232
+ } else if (currentConfig.server) {
2233
+ serverInstance = await TestServer.start({
2234
+ port: currentConfig.port,
2235
+ command: resolveServerCommand(currentConfig.server),
2236
+ env: currentConfig.env,
2237
+ startupTimeout: currentConfig.startupTimeout ?? 3e4,
2238
+ debug: currentConfig.logLevel === "debug"
2239
+ });
2240
+ serverStartedByUs = true;
2241
+ } else {
2242
+ throw new Error(
2243
+ 'test.use() requires either "server" (entry file path) or "baseUrl" (for external server) option'
2244
+ );
2245
+ }
2246
+ }
2247
+ }
2248
+ async function createTestFixtures() {
2249
+ await initializeSharedResources();
2250
+ const clientInstance = await McpTestClient.create({
2251
+ baseUrl: serverInstance.info.baseUrl,
2252
+ transport: currentConfig.transport ?? "streamable-http",
2253
+ publicMode: currentConfig.publicMode
2254
+ }).buildAndConnect();
2255
+ const auth = createAuthFixture(tokenFactory);
2256
+ const server = createServerFixture(serverInstance);
2257
+ return {
2258
+ mcp: clientInstance,
2259
+ auth,
2260
+ server
2261
+ };
2262
+ }
2263
+ async function cleanupTestFixtures(fixtures, _testFailed = false) {
2264
+ if (fixtures.mcp.isConnected()) {
2265
+ await fixtures.mcp.disconnect();
2266
+ }
2267
+ }
2268
+ async function cleanupSharedResources() {
2269
+ if (serverInstance && serverStartedByUs) {
2270
+ await serverInstance.stop();
2271
+ }
2272
+ serverInstance = null;
2273
+ tokenFactory = null;
2274
+ serverStartedByUs = false;
2275
+ }
2276
+ function createAuthFixture(factory) {
2277
+ const users = {
2278
+ admin: {
2279
+ sub: "admin-001",
2280
+ scopes: ["admin:*", "read", "write", "delete"],
2281
+ email: "admin@test.local",
2282
+ name: "Test Admin"
2283
+ },
2284
+ user: {
2285
+ sub: "user-001",
2286
+ scopes: ["read", "write"],
2287
+ email: "user@test.local",
2288
+ name: "Test User"
2289
+ },
2290
+ readOnly: {
2291
+ sub: "readonly-001",
2292
+ scopes: ["read"],
2293
+ email: "readonly@test.local",
2294
+ name: "Read Only User"
2295
+ }
2296
+ };
2297
+ return {
2298
+ createToken: (opts) => factory.createTestToken({
2299
+ sub: opts.sub,
2300
+ scopes: opts.scopes,
2301
+ claims: {
2302
+ email: opts.email,
2303
+ name: opts.name,
2304
+ ...opts.claims
2305
+ },
2306
+ exp: opts.expiresIn
2307
+ }),
2308
+ createExpiredToken: (opts) => factory.createExpiredToken(opts),
2309
+ createInvalidToken: (opts) => factory.createTokenWithInvalidSignature(opts),
2310
+ users: {
2311
+ admin: users["admin"],
2312
+ user: users["user"],
2313
+ readOnly: users["readOnly"]
2314
+ },
2315
+ getJwks: () => factory.getPublicJwks(),
2316
+ getIssuer: () => factory.getIssuer(),
2317
+ getAudience: () => factory.getAudience()
2318
+ };
2319
+ }
2320
+ function createServerFixture(server) {
2321
+ return {
2322
+ info: server.info,
2323
+ createClient: async (opts) => {
2324
+ return McpTestClient.create({
2325
+ baseUrl: server.info.baseUrl,
2326
+ transport: opts?.transport ?? "streamable-http",
2327
+ auth: opts?.token ? { token: opts.token } : void 0,
2328
+ clientInfo: opts?.clientInfo,
2329
+ publicMode: currentConfig.publicMode
2330
+ }).buildAndConnect();
2331
+ },
2332
+ createClientBuilder: () => {
2333
+ const builder = new McpTestClientBuilder({
2334
+ baseUrl: server.info.baseUrl,
2335
+ publicMode: currentConfig.publicMode
2336
+ });
2337
+ return builder;
2338
+ },
2339
+ restart: () => server.restart(),
2340
+ getLogs: () => server.getLogs(),
2341
+ clearLogs: () => server.clearLogs()
2342
+ };
2343
+ }
2344
+ function resolveServerCommand(server) {
2345
+ if (server.includes(" ")) {
2346
+ return server;
2347
+ }
2348
+ return `npx tsx ${server}`;
2349
+ }
2350
+ function testWithFixtures(name, fn) {
2351
+ it(name, async () => {
2352
+ const fixtures = await createTestFixtures();
2353
+ let testFailed = false;
2354
+ try {
2355
+ await fn(fixtures);
2356
+ } catch (error) {
2357
+ testFailed = true;
2358
+ throw error;
2359
+ } finally {
2360
+ await cleanupTestFixtures(fixtures, testFailed);
2361
+ }
2362
+ });
2363
+ }
2364
+ function use(config) {
2365
+ currentConfig = { ...currentConfig, ...config };
2366
+ afterAll(async () => {
2367
+ await cleanupSharedResources();
2368
+ });
2369
+ }
2370
+ function skip(name, fn) {
2371
+ it.skip(name, async () => {
2372
+ const fixtures = await createTestFixtures();
2373
+ let testFailed = false;
2374
+ try {
2375
+ await fn(fixtures);
2376
+ } catch (error) {
2377
+ testFailed = true;
2378
+ throw error;
2379
+ } finally {
2380
+ await cleanupTestFixtures(fixtures, testFailed);
2381
+ }
2382
+ });
2383
+ }
2384
+ function only(name, fn) {
2385
+ it.only(name, async () => {
2386
+ const fixtures = await createTestFixtures();
2387
+ let testFailed = false;
2388
+ try {
2389
+ await fn(fixtures);
2390
+ } catch (error) {
2391
+ testFailed = true;
2392
+ throw error;
2393
+ } finally {
2394
+ await cleanupTestFixtures(fixtures, testFailed);
2395
+ }
2396
+ });
2397
+ }
2398
+ function todo(name) {
2399
+ it.todo(name);
2400
+ }
2401
+ var test = testWithFixtures;
2402
+ test.use = use;
2403
+ test.describe = describe;
2404
+ test.beforeAll = beforeAll;
2405
+ test.beforeEach = beforeEach;
2406
+ test.afterEach = afterEach;
2407
+ test.afterAll = afterAll;
2408
+ test.skip = skip;
2409
+ test.only = only;
2410
+ test.todo = todo;
2411
+ // Annotate the CommonJS export names for ESM import in node:
2412
+ 0 && (module.exports = {
2413
+ cleanupSharedResources,
2414
+ cleanupTestFixtures,
2415
+ createTestFixtures,
2416
+ initializeSharedResources,
2417
+ test
2418
+ });