@frontmcp/testing 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/package.json +3 -3
  2. package/src/auth/mock-api-server.d.ts +99 -0
  3. package/src/auth/mock-api-server.js +200 -0
  4. package/src/auth/mock-api-server.js.map +1 -0
  5. package/src/auth/mock-oauth-server.d.ts +85 -0
  6. package/src/auth/mock-oauth-server.js +253 -0
  7. package/src/auth/mock-oauth-server.js.map +1 -0
  8. package/src/client/mcp-test-client.builder.d.ts +43 -1
  9. package/src/client/mcp-test-client.builder.js +52 -0
  10. package/src/client/mcp-test-client.builder.js.map +1 -1
  11. package/src/client/mcp-test-client.js +22 -14
  12. package/src/client/mcp-test-client.js.map +1 -1
  13. package/src/client/mcp-test-client.types.d.ts +67 -6
  14. package/src/client/mcp-test-client.types.js +9 -0
  15. package/src/client/mcp-test-client.types.js.map +1 -1
  16. package/src/example-tools/index.d.ts +19 -0
  17. package/src/example-tools/index.js +40 -0
  18. package/src/example-tools/index.js.map +1 -0
  19. package/src/example-tools/tool-configs.d.ts +170 -0
  20. package/src/example-tools/tool-configs.js +222 -0
  21. package/src/example-tools/tool-configs.js.map +1 -0
  22. package/src/expect.d.ts +6 -5
  23. package/src/expect.js.map +1 -1
  24. package/src/fixtures/fixture-types.d.ts +19 -0
  25. package/src/fixtures/fixture-types.js.map +1 -1
  26. package/src/fixtures/test-fixture.d.ts +3 -1
  27. package/src/fixtures/test-fixture.js +35 -4
  28. package/src/fixtures/test-fixture.js.map +1 -1
  29. package/src/index.d.ts +7 -0
  30. package/src/index.js +40 -1
  31. package/src/index.js.map +1 -1
  32. package/src/matchers/matcher-types.js.map +1 -1
  33. package/src/matchers/mcp-matchers.d.ts +7 -0
  34. package/src/matchers/mcp-matchers.js +8 -4
  35. package/src/matchers/mcp-matchers.js.map +1 -1
  36. package/src/platform/index.d.ts +28 -0
  37. package/src/platform/index.js +47 -0
  38. package/src/platform/index.js.map +1 -0
  39. package/src/platform/platform-client-info.d.ts +97 -0
  40. package/src/platform/platform-client-info.js +155 -0
  41. package/src/platform/platform-client-info.js.map +1 -0
  42. package/src/platform/platform-types.d.ts +72 -0
  43. package/src/platform/platform-types.js +110 -0
  44. package/src/platform/platform-types.js.map +1 -0
  45. package/src/server/test-server.d.ts +4 -0
  46. package/src/server/test-server.js +58 -3
  47. package/src/server/test-server.js.map +1 -1
  48. package/src/transport/streamable-http.transport.js +6 -0
  49. package/src/transport/streamable-http.transport.js.map +1 -1
  50. package/src/transport/transport.interface.d.ts +3 -0
  51. package/src/transport/transport.interface.js.map +1 -1
  52. package/src/ui/ui-assertions.d.ts +59 -0
  53. package/src/ui/ui-assertions.js +152 -0
  54. package/src/ui/ui-assertions.js.map +1 -1
  55. package/src/ui/ui-matchers.d.ts +8 -0
  56. package/src/ui/ui-matchers.js +218 -0
  57. package/src/ui/ui-matchers.js.map +1 -1
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ /**
3
+ * @file platform-types.ts
4
+ * @description Platform type definitions for E2E testing.
5
+ *
6
+ * These types mirror the AIPlatformType from @frontmcp/ui/adapters
7
+ * for use in testing without creating a hard dependency.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { TestPlatformType } from '@frontmcp/testing';
12
+ *
13
+ * const platform: TestPlatformType = 'openai';
14
+ * ```
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.getPlatformMetaNamespace = getPlatformMetaNamespace;
18
+ exports.getPlatformMimeType = getPlatformMimeType;
19
+ exports.isOpenAIPlatform = isOpenAIPlatform;
20
+ exports.isExtAppsPlatform = isExtAppsPlatform;
21
+ exports.isFrontmcpPlatform = isFrontmcpPlatform;
22
+ exports.getToolsListMetaPrefixes = getToolsListMetaPrefixes;
23
+ exports.getToolCallMetaPrefixes = getToolCallMetaPrefixes;
24
+ exports.getForbiddenMetaPrefixes = getForbiddenMetaPrefixes;
25
+ /**
26
+ * Get the meta namespace for a platform type.
27
+ */
28
+ function getPlatformMetaNamespace(platform) {
29
+ switch (platform) {
30
+ case 'openai':
31
+ return 'openai';
32
+ case 'ext-apps':
33
+ return 'ui';
34
+ default:
35
+ return 'frontmcp';
36
+ }
37
+ }
38
+ /**
39
+ * Get the expected MIME type for a platform.
40
+ *
41
+ * - OpenAI uses `text/html+skybridge`
42
+ * - All other platforms use `text/html+mcp`
43
+ */
44
+ function getPlatformMimeType(platform) {
45
+ return platform === 'openai' ? 'text/html+skybridge' : 'text/html+mcp';
46
+ }
47
+ /**
48
+ * Check if a platform uses OpenAI-specific meta keys.
49
+ */
50
+ function isOpenAIPlatform(platform) {
51
+ return platform === 'openai';
52
+ }
53
+ /**
54
+ * Check if a platform is ext-apps (SEP-1865 MCP Apps).
55
+ */
56
+ function isExtAppsPlatform(platform) {
57
+ return platform === 'ext-apps';
58
+ }
59
+ /**
60
+ * Check if a platform uses FrontMCP meta keys (non-OpenAI, non-ext-apps).
61
+ */
62
+ function isFrontmcpPlatform(platform) {
63
+ return platform !== 'openai' && platform !== 'ext-apps';
64
+ }
65
+ /**
66
+ * Get all expected meta key prefixes for a platform's tools/list response.
67
+ */
68
+ function getToolsListMetaPrefixes(platform) {
69
+ switch (platform) {
70
+ case 'openai':
71
+ return ['openai/'];
72
+ case 'ext-apps':
73
+ return ['ui/'];
74
+ default:
75
+ // Other platforms use frontmcp/* + ui/* for compatibility
76
+ return ['frontmcp/', 'ui/'];
77
+ }
78
+ }
79
+ /**
80
+ * Get all expected meta key prefixes for a platform's tool/call response.
81
+ */
82
+ function getToolCallMetaPrefixes(platform) {
83
+ switch (platform) {
84
+ case 'openai':
85
+ return ['openai/'];
86
+ case 'ext-apps':
87
+ return ['ui/'];
88
+ default:
89
+ // Other platforms use frontmcp/* + ui/* for compatibility
90
+ return ['frontmcp/', 'ui/'];
91
+ }
92
+ }
93
+ /**
94
+ * Get forbidden meta key prefixes for a platform.
95
+ * These prefixes should NOT appear in responses for the given platform.
96
+ */
97
+ function getForbiddenMetaPrefixes(platform) {
98
+ switch (platform) {
99
+ case 'openai':
100
+ // OpenAI should NOT have ui/* or frontmcp/* keys
101
+ return ['ui/', 'frontmcp/'];
102
+ case 'ext-apps':
103
+ // ext-apps should NOT have openai/* or frontmcp/* keys
104
+ return ['openai/', 'frontmcp/'];
105
+ default:
106
+ // Other platforms should NOT have openai/* keys
107
+ return ['openai/'];
108
+ }
109
+ }
110
+ //# sourceMappingURL=platform-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform-types.js","sourceRoot":"","sources":["../../../src/platform/platform-types.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;AAsCH,4DASC;AAQD,kDAEC;AAKD,4CAEC;AAKD,8CAEC;AAKD,gDAEC;AAKD,4DAUC;AAKD,0DAUC;AAMD,4DAYC;AA3FD;;GAEG;AACH,SAAgB,wBAAwB,CAAC,QAA0B;IACjE,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,QAAQ,CAAC;QAClB,KAAK,UAAU;YACb,OAAO,IAAI,CAAC;QACd;YACE,OAAO,UAAU,CAAC;IACtB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,QAA0B;IAC5D,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,eAAe,CAAC;AACzE,CAAC;AAED;;GAEG;AACH,SAAgB,gBAAgB,CAAC,QAA0B;IACzD,OAAO,QAAQ,KAAK,QAAQ,CAAC;AAC/B,CAAC;AAED;;GAEG;AACH,SAAgB,iBAAiB,CAAC,QAA0B;IAC1D,OAAO,QAAQ,KAAK,UAAU,CAAC;AACjC,CAAC;AAED;;GAEG;AACH,SAAgB,kBAAkB,CAAC,QAA0B;IAC3D,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,UAAU,CAAC;AAC1D,CAAC;AAED;;GAEG;AACH,SAAgB,wBAAwB,CAAC,QAA0B;IACjE,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,CAAC,SAAS,CAAC,CAAC;QACrB,KAAK,UAAU;YACb,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB;YACE,0DAA0D;YAC1D,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;AACH,CAAC;AAED;;GAEG;AACH,SAAgB,uBAAuB,CAAC,QAA0B;IAChE,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,OAAO,CAAC,SAAS,CAAC,CAAC;QACrB,KAAK,UAAU;YACb,OAAO,CAAC,KAAK,CAAC,CAAC;QACjB;YACE,0DAA0D;YAC1D,OAAO,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;IAChC,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,QAA0B;IACjE,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,QAAQ;YACX,iDAAiD;YACjD,OAAO,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;QAC9B,KAAK,UAAU;YACb,uDAAuD;YACvD,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;QAClC;YACE,gDAAgD;YAChD,OAAO,CAAC,SAAS,CAAC,CAAC;IACvB,CAAC;AACH,CAAC","sourcesContent":["/**\n * @file platform-types.ts\n * @description Platform type definitions for E2E testing.\n *\n * These types mirror the AIPlatformType from @frontmcp/ui/adapters\n * for use in testing without creating a hard dependency.\n *\n * @example\n * ```typescript\n * import { TestPlatformType } from '@frontmcp/testing';\n *\n * const platform: TestPlatformType = 'openai';\n * ```\n */\n\n/**\n * Supported AI platform types for testing.\n *\n * - `openai`: OpenAI ChatGPT (uses openai/* meta keys)\n * - `ext-apps`: MCP Apps per SEP-1865 (uses ui/* meta keys)\n * - `claude`: Claude Desktop (uses frontmcp/* + ui/* keys)\n * - `cursor`: Cursor IDE (uses frontmcp/* + ui/* keys)\n * - `continue`: Continue Dev (uses frontmcp/* + ui/* keys)\n * - `cody`: Sourcegraph Cody (uses frontmcp/* + ui/* keys)\n * - `gemini`: Google Gemini (uses frontmcp/* + ui/* keys)\n * - `generic-mcp`: Generic MCP client (uses frontmcp/* + ui/* keys)\n * - `unknown`: Unknown platform (uses frontmcp/* + ui/* keys)\n */\nexport type TestPlatformType =\n | 'openai'\n | 'ext-apps'\n | 'claude'\n | 'cursor'\n | 'continue'\n | 'cody'\n | 'gemini'\n | 'generic-mcp'\n | 'unknown';\n\n/**\n * Platform meta namespace used for tool responses.\n *\n * - `openai`: Uses `openai/*` keys only\n * - `ui`: Uses `ui/*` keys only (ext-apps per SEP-1865)\n * - `frontmcp`: Uses `frontmcp/*` + `ui/*` keys for compatibility\n */\nexport type PlatformMetaNamespace = 'openai' | 'ui' | 'frontmcp';\n\n/**\n * Get the meta namespace for a platform type.\n */\nexport function getPlatformMetaNamespace(platform: TestPlatformType): PlatformMetaNamespace {\n switch (platform) {\n case 'openai':\n return 'openai';\n case 'ext-apps':\n return 'ui';\n default:\n return 'frontmcp';\n }\n}\n\n/**\n * Get the expected MIME type for a platform.\n *\n * - OpenAI uses `text/html+skybridge`\n * - All other platforms use `text/html+mcp`\n */\nexport function getPlatformMimeType(platform: TestPlatformType): string {\n return platform === 'openai' ? 'text/html+skybridge' : 'text/html+mcp';\n}\n\n/**\n * Check if a platform uses OpenAI-specific meta keys.\n */\nexport function isOpenAIPlatform(platform: TestPlatformType): boolean {\n return platform === 'openai';\n}\n\n/**\n * Check if a platform is ext-apps (SEP-1865 MCP Apps).\n */\nexport function isExtAppsPlatform(platform: TestPlatformType): boolean {\n return platform === 'ext-apps';\n}\n\n/**\n * Check if a platform uses FrontMCP meta keys (non-OpenAI, non-ext-apps).\n */\nexport function isFrontmcpPlatform(platform: TestPlatformType): boolean {\n return platform !== 'openai' && platform !== 'ext-apps';\n}\n\n/**\n * Get all expected meta key prefixes for a platform's tools/list response.\n */\nexport function getToolsListMetaPrefixes(platform: TestPlatformType): string[] {\n switch (platform) {\n case 'openai':\n return ['openai/'];\n case 'ext-apps':\n return ['ui/'];\n default:\n // Other platforms use frontmcp/* + ui/* for compatibility\n return ['frontmcp/', 'ui/'];\n }\n}\n\n/**\n * Get all expected meta key prefixes for a platform's tool/call response.\n */\nexport function getToolCallMetaPrefixes(platform: TestPlatformType): string[] {\n switch (platform) {\n case 'openai':\n return ['openai/'];\n case 'ext-apps':\n return ['ui/'];\n default:\n // Other platforms use frontmcp/* + ui/* for compatibility\n return ['frontmcp/', 'ui/'];\n }\n}\n\n/**\n * Get forbidden meta key prefixes for a platform.\n * These prefixes should NOT appear in responses for the given platform.\n */\nexport function getForbiddenMetaPrefixes(platform: TestPlatformType): string[] {\n switch (platform) {\n case 'openai':\n // OpenAI should NOT have ui/* or frontmcp/* keys\n return ['ui/', 'frontmcp/'];\n case 'ext-apps':\n // ext-apps should NOT have openai/* or frontmcp/* keys\n return ['openai/', 'frontmcp/'];\n default:\n // Other platforms should NOT have openai/* keys\n return ['openai/'];\n }\n}\n"]}
@@ -91,6 +91,10 @@ export declare class TestServer {
91
91
  */
92
92
  clearLogs(): void;
93
93
  private startProcess;
94
+ /**
95
+ * Wait for server to be ready, but also detect early process exit
96
+ */
97
+ private waitForReadyWithExitDetection;
94
98
  private log;
95
99
  }
96
100
  /**
@@ -216,6 +216,10 @@ class TestServer {
216
216
  if (this.process.pid !== undefined) {
217
217
  this._info.pid = this.process.pid;
218
218
  }
219
+ // Track process exit for early failure detection
220
+ let processExited = false;
221
+ let exitCode = null;
222
+ let exitError = null;
219
223
  // Capture stdout
220
224
  this.process.stdout?.on('data', (data) => {
221
225
  const text = data.toString();
@@ -235,16 +239,67 @@ class TestServer {
235
239
  // Handle spawn errors to prevent unhandled error events
236
240
  this.process.on('error', (err) => {
237
241
  this.logs.push(`[SPAWN ERROR] ${err.message}`);
242
+ exitError = err;
238
243
  if (this.options.debug) {
239
244
  console.error('[SERVER SPAWN ERROR]', err);
240
245
  }
241
246
  });
242
- // Handle process exit - use once() to avoid memory leak from listener not being cleaned up
247
+ // Handle process exit - track early failures
243
248
  this.process.once('exit', (code) => {
249
+ processExited = true;
250
+ exitCode = code;
244
251
  this.log(`Server process exited with code ${code}`);
245
252
  });
246
- // Wait for server to be ready
247
- await this.waitForReady();
253
+ // Wait for server to be ready, but detect early process exit
254
+ await this.waitForReadyWithExitDetection(() => {
255
+ if (exitError) {
256
+ return { exited: true, error: exitError };
257
+ }
258
+ if (processExited) {
259
+ const recentLogs = this.logs.slice(-10).join('\n');
260
+ return {
261
+ exited: true,
262
+ error: new Error(`Server process exited unexpectedly with code ${exitCode}.\n\nRecent logs:\n${recentLogs}`),
263
+ };
264
+ }
265
+ return { exited: false };
266
+ });
267
+ }
268
+ /**
269
+ * Wait for server to be ready, but also detect early process exit
270
+ */
271
+ async waitForReadyWithExitDetection(checkExit) {
272
+ const timeoutMs = this.options.startupTimeout;
273
+ const deadline = Date.now() + timeoutMs;
274
+ const checkInterval = 100;
275
+ while (Date.now() < deadline) {
276
+ // Check if process has exited before continuing to poll
277
+ const exitStatus = checkExit();
278
+ if (exitStatus.exited) {
279
+ throw exitStatus.error ?? new Error('Server process exited unexpectedly');
280
+ }
281
+ try {
282
+ const response = await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`, {
283
+ method: 'GET',
284
+ signal: AbortSignal.timeout(1000),
285
+ });
286
+ if (response.ok || response.status === 404) {
287
+ // 404 is okay - it means the server is running but might not have a health endpoint
288
+ this.log('Server is ready');
289
+ return;
290
+ }
291
+ }
292
+ catch {
293
+ // Server not ready yet
294
+ }
295
+ await sleep(checkInterval);
296
+ }
297
+ // Final check before throwing timeout error
298
+ const finalExitStatus = checkExit();
299
+ if (finalExitStatus.exited) {
300
+ throw finalExitStatus.error ?? new Error('Server process exited unexpectedly');
301
+ }
302
+ throw new Error(`Server did not become ready within ${timeoutMs}ms`);
248
303
  }
249
304
  log(message) {
250
305
  if (this.options.debug) {
@@ -1 +1 @@
1
- {"version":3,"file":"test-server.js","sourceRoot":"","sources":["../../../src/server/test-server.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file test-server.ts\n * @description Test server management for E2E testing\n */\n\nimport { spawn, ChildProcess } from 'child_process';\n\n// ═══════════════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════════════\n\nexport interface TestServerOptions {\n /** Port to run the server on (default: random available port) */\n port?: number;\n /** Command to start the server */\n command?: string;\n /** Working directory */\n cwd?: string;\n /** Environment variables */\n env?: Record<string, string>;\n /** Timeout for server startup in milliseconds (default: 30000) */\n startupTimeout?: number;\n /** Path to check for server readiness (default: /health) */\n healthCheckPath?: string;\n /** Enable debug logging */\n debug?: boolean;\n}\n\nexport interface TestServerInfo {\n /** Base URL of the server */\n baseUrl: string;\n /** Port the server is running on */\n port: number;\n /** Process ID (if available) */\n pid?: number;\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// TEST SERVER CLASS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Manages test server lifecycle for E2E testing\n *\n * @example\n * ```typescript\n * // Start a server with custom command\n * const server = await TestServer.start({\n * command: 'node dist/main.js',\n * port: 3003,\n * cwd: './apps/my-server',\n * });\n *\n * // Or start an Nx project\n * const server = await TestServer.startNx('demo-public', { port: 3003 });\n *\n * // Use the server\n * console.log(server.info.baseUrl); // http://localhost:3003\n *\n * // Stop when done\n * await server.stop();\n * ```\n */\nexport class TestServer {\n private process: ChildProcess | null = null;\n private readonly options: Required<TestServerOptions>;\n private _info: TestServerInfo;\n private logs: string[] = [];\n\n private constructor(options: TestServerOptions, port: number) {\n this.options = {\n port,\n command: options.command ?? '',\n cwd: options.cwd ?? process.cwd(),\n env: options.env ?? {},\n startupTimeout: options.startupTimeout ?? 30000,\n healthCheckPath: options.healthCheckPath ?? '/health',\n debug: options.debug ?? false,\n };\n\n this._info = {\n baseUrl: `http://localhost:${port}`,\n port,\n };\n }\n\n /**\n * Start a test server with custom command\n */\n static async start(options: TestServerOptions): Promise<TestServer> {\n const port = options.port ?? (await findAvailablePort());\n const server = new TestServer(options, port);\n try {\n await server.startProcess();\n } catch (error) {\n await server.stop(); // Clean up spawned process to prevent leaks\n throw error;\n }\n return server;\n }\n\n /**\n * Start an Nx project as test server\n */\n static async startNx(project: string, options: Partial<TestServerOptions> = {}): Promise<TestServer> {\n // Validate project name contains only safe characters to prevent shell injection\n if (!/^[\\w-]+$/.test(project)) {\n throw new Error(\n `Invalid project name: ${project}. Must contain only alphanumeric, underscore, and hyphen characters.`,\n );\n }\n\n const port = options.port ?? (await findAvailablePort());\n\n const serverOptions: TestServerOptions = {\n ...options,\n port,\n command: `npx nx serve ${project} --port ${port}`,\n cwd: options.cwd ?? process.cwd(),\n };\n\n const server = new TestServer(serverOptions, port);\n try {\n await server.startProcess();\n } catch (error) {\n await server.stop(); // Clean up spawned process to prevent leaks\n throw error;\n }\n return server;\n }\n\n /**\n * Create a test server connected to an already running server\n */\n static connect(baseUrl: string): TestServer {\n const url = new URL(baseUrl);\n const port = parseInt(url.port, 10) || (url.protocol === 'https:' ? 443 : 80);\n\n const server = new TestServer(\n {\n command: '',\n port,\n },\n port,\n );\n\n server._info = {\n baseUrl: baseUrl.replace(/\\/$/, ''),\n port,\n };\n\n return server;\n }\n\n /**\n * Get server information\n */\n get info(): TestServerInfo {\n return { ...this._info };\n }\n\n /**\n * Stop the test server\n */\n async stop(): Promise<void> {\n if (this.process) {\n this.log('Stopping server...');\n\n // Try graceful shutdown first\n this.process.kill('SIGTERM');\n\n // Wait for process to exit\n const exitPromise = new Promise<void>((resolve) => {\n if (this.process) {\n this.process.once('exit', () => resolve());\n } else {\n resolve();\n }\n });\n\n // Force kill after timeout (but still wait for actual exit)\n const killTimeout = setTimeout(() => {\n if (this.process) {\n this.log('Force killing server after timeout...');\n this.process.kill('SIGKILL');\n }\n }, 5000);\n\n await exitPromise;\n clearTimeout(killTimeout);\n this.process = null;\n this.log('Server stopped');\n }\n }\n\n /**\n * Wait for server to be ready\n */\n async waitForReady(timeout?: number): Promise<void> {\n const timeoutMs = timeout ?? this.options.startupTimeout;\n const deadline = Date.now() + timeoutMs;\n const checkInterval = 100;\n\n while (Date.now() < deadline) {\n try {\n const response = await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`, {\n method: 'GET',\n signal: AbortSignal.timeout(1000),\n });\n\n if (response.ok || response.status === 404) {\n // 404 is okay - it means the server is running but might not have a health endpoint\n this.log('Server is ready');\n return;\n }\n } catch {\n // Server not ready yet\n }\n\n await sleep(checkInterval);\n }\n\n throw new Error(`Server did not become ready within ${timeoutMs}ms`);\n }\n\n /**\n * Restart the server\n */\n async restart(): Promise<void> {\n await this.stop();\n await this.startProcess();\n }\n\n /**\n * Get captured server logs\n */\n getLogs(): string[] {\n return [...this.logs];\n }\n\n /**\n * Clear captured logs\n */\n clearLogs(): void {\n this.logs = [];\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // PRIVATE METHODS\n // ═══════════════════════════════════════════════════════════════════\n\n private async startProcess(): Promise<void> {\n if (!this.options.command) {\n // No command means we're connecting to an existing server\n await this.waitForReady();\n return;\n }\n\n this.log(`Starting server: ${this.options.command}`);\n\n const env = {\n ...process.env,\n ...this.options.env,\n PORT: String(this.options.port),\n };\n\n // Use shell: true to handle complex commands with quoted arguments\n // This avoids fragile command parsing with split(' ')\n this.process = spawn(this.options.command, [], {\n cwd: this.options.cwd,\n env,\n shell: true,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n // pid can be undefined if spawn fails\n if (this.process.pid !== undefined) {\n this._info.pid = this.process.pid;\n }\n\n // Capture stdout\n this.process.stdout?.on('data', (data: Buffer) => {\n const text = data.toString();\n this.logs.push(text);\n if (this.options.debug) {\n console.log('[SERVER]', text);\n }\n });\n\n // Capture stderr\n this.process.stderr?.on('data', (data: Buffer) => {\n const text = data.toString();\n this.logs.push(`[ERROR] ${text}`);\n if (this.options.debug) {\n console.error('[SERVER ERROR]', text);\n }\n });\n\n // Handle spawn errors to prevent unhandled error events\n this.process.on('error', (err) => {\n this.logs.push(`[SPAWN ERROR] ${err.message}`);\n if (this.options.debug) {\n console.error('[SERVER SPAWN ERROR]', err);\n }\n });\n\n // Handle process exit - use once() to avoid memory leak from listener not being cleaned up\n this.process.once('exit', (code) => {\n this.log(`Server process exited with code ${code}`);\n });\n\n // Wait for server to be ready\n await this.waitForReady();\n }\n\n private log(message: string): void {\n if (this.options.debug) {\n console.log(`[TestServer] ${message}`);\n }\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// UTILITIES\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Find an available port\n */\nexport async function findAvailablePort(): Promise<number> {\n // Use a simple approach: try to create a server on port 0 to get an available port\n const { createServer } = await import('net');\n\n return new Promise((resolve, reject) => {\n const server = createServer();\n\n server.listen(0, () => {\n const address = server.address();\n if (address && typeof address !== 'string') {\n const port = address.port;\n server.close(() => resolve(port));\n } else {\n reject(new Error('Could not get port'));\n }\n });\n\n server.on('error', reject);\n });\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"]}
1
+ {"version":3,"file":"test-server.js","sourceRoot":"","sources":["../../../src/server/test-server.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file test-server.ts\n * @description Test server management for E2E testing\n */\n\nimport { spawn, ChildProcess } from 'child_process';\n\n// ═══════════════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════════════\n\nexport interface TestServerOptions {\n /** Port to run the server on (default: random available port) */\n port?: number;\n /** Command to start the server */\n command?: string;\n /** Working directory */\n cwd?: string;\n /** Environment variables */\n env?: Record<string, string>;\n /** Timeout for server startup in milliseconds (default: 30000) */\n startupTimeout?: number;\n /** Path to check for server readiness (default: /health) */\n healthCheckPath?: string;\n /** Enable debug logging */\n debug?: boolean;\n}\n\nexport interface TestServerInfo {\n /** Base URL of the server */\n baseUrl: string;\n /** Port the server is running on */\n port: number;\n /** Process ID (if available) */\n pid?: number;\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// TEST SERVER CLASS\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Manages test server lifecycle for E2E testing\n *\n * @example\n * ```typescript\n * // Start a server with custom command\n * const server = await TestServer.start({\n * command: 'node dist/main.js',\n * port: 3003,\n * cwd: './apps/my-server',\n * });\n *\n * // Or start an Nx project\n * const server = await TestServer.startNx('demo-public', { port: 3003 });\n *\n * // Use the server\n * console.log(server.info.baseUrl); // http://localhost:3003\n *\n * // Stop when done\n * await server.stop();\n * ```\n */\nexport class TestServer {\n private process: ChildProcess | null = null;\n private readonly options: Required<TestServerOptions>;\n private _info: TestServerInfo;\n private logs: string[] = [];\n\n private constructor(options: TestServerOptions, port: number) {\n this.options = {\n port,\n command: options.command ?? '',\n cwd: options.cwd ?? process.cwd(),\n env: options.env ?? {},\n startupTimeout: options.startupTimeout ?? 30000,\n healthCheckPath: options.healthCheckPath ?? '/health',\n debug: options.debug ?? false,\n };\n\n this._info = {\n baseUrl: `http://localhost:${port}`,\n port,\n };\n }\n\n /**\n * Start a test server with custom command\n */\n static async start(options: TestServerOptions): Promise<TestServer> {\n const port = options.port ?? (await findAvailablePort());\n const server = new TestServer(options, port);\n try {\n await server.startProcess();\n } catch (error) {\n await server.stop(); // Clean up spawned process to prevent leaks\n throw error;\n }\n return server;\n }\n\n /**\n * Start an Nx project as test server\n */\n static async startNx(project: string, options: Partial<TestServerOptions> = {}): Promise<TestServer> {\n // Validate project name contains only safe characters to prevent shell injection\n if (!/^[\\w-]+$/.test(project)) {\n throw new Error(\n `Invalid project name: ${project}. Must contain only alphanumeric, underscore, and hyphen characters.`,\n );\n }\n\n const port = options.port ?? (await findAvailablePort());\n\n const serverOptions: TestServerOptions = {\n ...options,\n port,\n command: `npx nx serve ${project} --port ${port}`,\n cwd: options.cwd ?? process.cwd(),\n };\n\n const server = new TestServer(serverOptions, port);\n try {\n await server.startProcess();\n } catch (error) {\n await server.stop(); // Clean up spawned process to prevent leaks\n throw error;\n }\n return server;\n }\n\n /**\n * Create a test server connected to an already running server\n */\n static connect(baseUrl: string): TestServer {\n const url = new URL(baseUrl);\n const port = parseInt(url.port, 10) || (url.protocol === 'https:' ? 443 : 80);\n\n const server = new TestServer(\n {\n command: '',\n port,\n },\n port,\n );\n\n server._info = {\n baseUrl: baseUrl.replace(/\\/$/, ''),\n port,\n };\n\n return server;\n }\n\n /**\n * Get server information\n */\n get info(): TestServerInfo {\n return { ...this._info };\n }\n\n /**\n * Stop the test server\n */\n async stop(): Promise<void> {\n if (this.process) {\n this.log('Stopping server...');\n\n // Try graceful shutdown first\n this.process.kill('SIGTERM');\n\n // Wait for process to exit\n const exitPromise = new Promise<void>((resolve) => {\n if (this.process) {\n this.process.once('exit', () => resolve());\n } else {\n resolve();\n }\n });\n\n // Force kill after timeout (but still wait for actual exit)\n const killTimeout = setTimeout(() => {\n if (this.process) {\n this.log('Force killing server after timeout...');\n this.process.kill('SIGKILL');\n }\n }, 5000);\n\n await exitPromise;\n clearTimeout(killTimeout);\n this.process = null;\n this.log('Server stopped');\n }\n }\n\n /**\n * Wait for server to be ready\n */\n async waitForReady(timeout?: number): Promise<void> {\n const timeoutMs = timeout ?? this.options.startupTimeout;\n const deadline = Date.now() + timeoutMs;\n const checkInterval = 100;\n\n while (Date.now() < deadline) {\n try {\n const response = await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`, {\n method: 'GET',\n signal: AbortSignal.timeout(1000),\n });\n\n if (response.ok || response.status === 404) {\n // 404 is okay - it means the server is running but might not have a health endpoint\n this.log('Server is ready');\n return;\n }\n } catch {\n // Server not ready yet\n }\n\n await sleep(checkInterval);\n }\n\n throw new Error(`Server did not become ready within ${timeoutMs}ms`);\n }\n\n /**\n * Restart the server\n */\n async restart(): Promise<void> {\n await this.stop();\n await this.startProcess();\n }\n\n /**\n * Get captured server logs\n */\n getLogs(): string[] {\n return [...this.logs];\n }\n\n /**\n * Clear captured logs\n */\n clearLogs(): void {\n this.logs = [];\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // PRIVATE METHODS\n // ═══════════════════════════════════════════════════════════════════\n\n private async startProcess(): Promise<void> {\n if (!this.options.command) {\n // No command means we're connecting to an existing server\n await this.waitForReady();\n return;\n }\n\n this.log(`Starting server: ${this.options.command}`);\n\n const env = {\n ...process.env,\n ...this.options.env,\n PORT: String(this.options.port),\n };\n\n // Use shell: true to handle complex commands with quoted arguments\n // This avoids fragile command parsing with split(' ')\n this.process = spawn(this.options.command, [], {\n cwd: this.options.cwd,\n env,\n shell: true,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n // pid can be undefined if spawn fails\n if (this.process.pid !== undefined) {\n this._info.pid = this.process.pid;\n }\n\n // Track process exit for early failure detection\n let processExited = false;\n let exitCode: number | null = null;\n let exitError: Error | null = null;\n\n // Capture stdout\n this.process.stdout?.on('data', (data: Buffer) => {\n const text = data.toString();\n this.logs.push(text);\n if (this.options.debug) {\n console.log('[SERVER]', text);\n }\n });\n\n // Capture stderr\n this.process.stderr?.on('data', (data: Buffer) => {\n const text = data.toString();\n this.logs.push(`[ERROR] ${text}`);\n if (this.options.debug) {\n console.error('[SERVER ERROR]', text);\n }\n });\n\n // Handle spawn errors to prevent unhandled error events\n this.process.on('error', (err) => {\n this.logs.push(`[SPAWN ERROR] ${err.message}`);\n exitError = err;\n if (this.options.debug) {\n console.error('[SERVER SPAWN ERROR]', err);\n }\n });\n\n // Handle process exit - track early failures\n this.process.once('exit', (code) => {\n processExited = true;\n exitCode = code;\n this.log(`Server process exited with code ${code}`);\n });\n\n // Wait for server to be ready, but detect early process exit\n await this.waitForReadyWithExitDetection(() => {\n if (exitError) {\n return { exited: true, error: exitError };\n }\n if (processExited) {\n const recentLogs = this.logs.slice(-10).join('\\n');\n return {\n exited: true,\n error: new Error(`Server process exited unexpectedly with code ${exitCode}.\\n\\nRecent logs:\\n${recentLogs}`),\n };\n }\n return { exited: false };\n });\n }\n\n /**\n * Wait for server to be ready, but also detect early process exit\n */\n private async waitForReadyWithExitDetection(checkExit: () => { exited: boolean; error?: Error }): Promise<void> {\n const timeoutMs = this.options.startupTimeout;\n const deadline = Date.now() + timeoutMs;\n const checkInterval = 100;\n\n while (Date.now() < deadline) {\n // Check if process has exited before continuing to poll\n const exitStatus = checkExit();\n if (exitStatus.exited) {\n throw exitStatus.error ?? new Error('Server process exited unexpectedly');\n }\n\n try {\n const response = await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`, {\n method: 'GET',\n signal: AbortSignal.timeout(1000),\n });\n\n if (response.ok || response.status === 404) {\n // 404 is okay - it means the server is running but might not have a health endpoint\n this.log('Server is ready');\n return;\n }\n } catch {\n // Server not ready yet\n }\n\n await sleep(checkInterval);\n }\n\n // Final check before throwing timeout error\n const finalExitStatus = checkExit();\n if (finalExitStatus.exited) {\n throw finalExitStatus.error ?? new Error('Server process exited unexpectedly');\n }\n\n throw new Error(`Server did not become ready within ${timeoutMs}ms`);\n }\n\n private log(message: string): void {\n if (this.options.debug) {\n console.log(`[TestServer] ${message}`);\n }\n }\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// UTILITIES\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Find an available port\n */\nexport async function findAvailablePort(): Promise<number> {\n // Use a simple approach: try to create a server on port 0 to get an available port\n const { createServer } = await import('net');\n\n return new Promise((resolve, reject) => {\n const server = createServer();\n\n server.listen(0, () => {\n const address = server.address();\n if (address && typeof address !== 'string') {\n const port = address.port;\n server.close(() => resolve(port));\n } else {\n reject(new Error('Could not get port'));\n }\n });\n\n server.on('error', reject);\n });\n}\n\n/**\n * Sleep for specified milliseconds\n */\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"]}
@@ -30,6 +30,7 @@ class StreamableHttpTransport {
30
30
  publicMode: config.publicMode ?? false,
31
31
  debug: config.debug ?? false,
32
32
  interceptors: config.interceptors,
33
+ clientInfo: config.clientInfo,
33
34
  };
34
35
  this.authToken = config.auth?.token;
35
36
  this.interceptors = config.interceptors;
@@ -337,6 +338,11 @@ class StreamableHttpTransport {
337
338
  'Content-Type': 'application/json',
338
339
  Accept: 'application/json, text/event-stream',
339
340
  };
341
+ // Add User-Agent header based on clientInfo for platform detection
342
+ // Server uses this to detect the AI platform before MCP initialize
343
+ if (this.config.clientInfo) {
344
+ headers['User-Agent'] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;
345
+ }
340
346
  // Only add Authorization header if we have a token AND not in public mode
341
347
  // Public mode explicitly skips auth headers for CI/CD and public docs testing
342
348
  if (this.authToken && !this.publicMode) {
@@ -1 +1 @@
1
- {"version":3,"file":"streamable-http.transport.js","sourceRoot":"","sources":["../../../src/transport/streamable-http.transport.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file streamable-http.transport.ts\n * @description StreamableHTTP transport implementation for MCP Test Client\n */\n\nimport type {\n McpTransport,\n TransportConfig,\n TransportState,\n JsonRpcRequest,\n JsonRpcResponse,\n} from './transport.interface';\nimport type { InterceptorChain } from '../interceptor';\n\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * StreamableHTTP transport for MCP communication\n *\n * This transport uses HTTP POST requests for all communication,\n * following the MCP StreamableHTTP specification.\n */\nexport class StreamableHttpTransport implements McpTransport {\n private readonly config: Required<Omit<TransportConfig, 'interceptors'>> & { interceptors?: InterceptorChain };\n private state: TransportState = 'disconnected';\n private sessionId: string | undefined;\n private authToken: string | undefined;\n private connectionCount = 0;\n private reconnectCount = 0;\n private lastRequestHeaders: Record<string, string> = {};\n private interceptors?: InterceptorChain;\n private readonly publicMode: boolean;\n\n constructor(config: TransportConfig) {\n this.config = {\n baseUrl: config.baseUrl.replace(/\\/$/, ''), // Remove trailing slash\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n auth: config.auth ?? {},\n publicMode: config.publicMode ?? false,\n debug: config.debug ?? false,\n interceptors: config.interceptors,\n };\n\n this.authToken = config.auth?.token;\n this.interceptors = config.interceptors;\n this.publicMode = config.publicMode ?? false;\n }\n\n async connect(): Promise<void> {\n this.state = 'connecting';\n this.connectionCount++;\n\n try {\n // Public mode: Skip all authentication - connect without any token\n if (this.publicMode) {\n this.log('Public mode: connecting without authentication');\n this.state = 'connected';\n return;\n }\n\n // If no auth token provided, request anonymous token from FrontMCP SDK\n if (!this.authToken) {\n await this.requestAnonymousToken();\n }\n\n // StreamableHTTP doesn't require an explicit connection step\n // The session is established on the first request\n this.state = 'connected';\n this.log('Connected to StreamableHTTP transport');\n } catch (error) {\n this.state = 'error';\n throw error;\n }\n }\n\n /**\n * Request an anonymous token from the FrontMCP OAuth endpoint\n * This allows the test client to authenticate without user interaction\n */\n private async requestAnonymousToken(): Promise<void> {\n const clientId = crypto.randomUUID();\n const tokenUrl = `${this.config.baseUrl}/oauth/token`;\n\n this.log(`Requesting anonymous token from ${tokenUrl}`);\n\n try {\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n grant_type: 'anonymous',\n client_id: clientId,\n resource: this.config.baseUrl,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n this.log(`Failed to get anonymous token: ${response.status} ${errorText}`);\n // Continue without token - server may allow unauthenticated access\n return;\n }\n\n const tokenResponse = await response.json();\n if (tokenResponse.access_token) {\n this.authToken = tokenResponse.access_token;\n this.log('Anonymous token acquired successfully');\n }\n } catch (error) {\n this.log(`Error requesting anonymous token: ${error}`);\n // Continue without token - server may allow unauthenticated access\n }\n }\n\n async request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & { result?: T }> {\n this.ensureConnected();\n\n const startTime = Date.now();\n\n // Process through interceptors if available\n if (this.interceptors) {\n const interceptResult = await this.interceptors.processRequest(message, {\n timestamp: new Date(),\n transport: 'streamable-http',\n sessionId: this.sessionId,\n });\n\n switch (interceptResult.type) {\n case 'mock': {\n // Return mock response directly, run through response interceptors\n const mockResponse = await this.interceptors.processResponse(\n message,\n interceptResult.response,\n Date.now() - startTime,\n );\n return mockResponse as JsonRpcResponse & { result?: T };\n }\n\n case 'error':\n throw interceptResult.error;\n\n case 'continue':\n // Use possibly modified request\n message = interceptResult.request;\n break;\n }\n }\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url}`, message);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Check for session ID in response headers\n const newSessionId = response.headers.get('mcp-session-id');\n if (newSessionId) {\n this.sessionId = newSessionId;\n }\n\n let jsonResponse: JsonRpcResponse;\n\n if (!response.ok) {\n // Handle HTTP errors\n const errorText = await response.text();\n this.log(`HTTP Error ${response.status}: ${errorText}`);\n\n jsonResponse = {\n jsonrpc: '2.0',\n id: message.id ?? null,\n error: {\n code: -32000,\n message: `HTTP ${response.status}: ${response.statusText}`,\n data: errorText,\n },\n };\n } else {\n // Parse response - may be JSON or SSE\n const contentType = response.headers.get('content-type') ?? '';\n const text = await response.text();\n this.log('Response:', text);\n\n // Handle empty response (for notifications)\n if (!text.trim()) {\n jsonResponse = {\n jsonrpc: '2.0',\n id: message.id ?? null,\n result: undefined,\n };\n } else if (contentType.includes('text/event-stream')) {\n // Parse SSE response - extract data and session ID from event stream\n const { response: sseResponse, sseSessionId } = this.parseSSEResponseWithSession(text, message.id);\n jsonResponse = sseResponse;\n // Store session ID from SSE id field (format: sessionId:messageId)\n if (sseSessionId && !this.sessionId) {\n this.sessionId = sseSessionId;\n this.log('Session ID from SSE:', this.sessionId);\n }\n } else {\n jsonResponse = JSON.parse(text) as JsonRpcResponse;\n }\n }\n\n // Process response through interceptors\n if (this.interceptors) {\n jsonResponse = await this.interceptors.processResponse(message, jsonResponse, Date.now() - startTime);\n }\n\n return jsonResponse as JsonRpcResponse & { result?: T };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name === 'AbortError') {\n return {\n jsonrpc: '2.0',\n id: message.id ?? null,\n error: {\n code: -32000,\n message: `Request timeout after ${this.config.timeout}ms`,\n },\n };\n }\n\n throw error;\n }\n }\n\n async notify(message: JsonRpcRequest): Promise<void> {\n this.ensureConnected();\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url} (notification)`, message);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Update session ID if present\n const newSessionId = response.headers.get('mcp-session-id');\n if (newSessionId) {\n this.sessionId = newSessionId;\n }\n\n if (!response.ok) {\n const errorText = await response.text();\n this.log(`HTTP Error ${response.status} on notification: ${errorText}`);\n }\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name !== 'AbortError') {\n throw error;\n }\n }\n }\n\n async sendRaw(data: string): Promise<JsonRpcResponse> {\n this.ensureConnected();\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url} (raw)`, data);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: data,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n const text = await response.text();\n\n if (!text.trim()) {\n return {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32700,\n message: 'Parse error',\n },\n };\n }\n\n return JSON.parse(text) as JsonRpcResponse;\n } catch (error) {\n clearTimeout(timeoutId);\n\n return {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32700,\n message: 'Parse error',\n data: error instanceof Error ? error.message : 'Unknown error',\n },\n };\n }\n }\n\n async close(): Promise<void> {\n this.state = 'disconnected';\n this.sessionId = undefined;\n this.log('StreamableHTTP transport closed');\n }\n\n isConnected(): boolean {\n return this.state === 'connected';\n }\n\n getState(): TransportState {\n return this.state;\n }\n\n getSessionId(): string | undefined {\n return this.sessionId;\n }\n\n setAuthToken(token: string): void {\n this.authToken = token;\n }\n\n setTimeout(ms: number): void {\n this.config.timeout = ms;\n }\n\n setInterceptors(interceptors: InterceptorChain): void {\n this.interceptors = interceptors;\n }\n\n getInterceptors(): InterceptorChain | undefined {\n return this.interceptors;\n }\n\n getConnectionCount(): number {\n return this.connectionCount;\n }\n\n getReconnectCount(): number {\n return this.reconnectCount;\n }\n\n getLastRequestHeaders(): Record<string, string> {\n return { ...this.lastRequestHeaders };\n }\n\n async simulateDisconnect(): Promise<void> {\n this.state = 'disconnected';\n this.sessionId = undefined;\n }\n\n async waitForReconnect(timeoutMs: number): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n\n // Auto-reconnect\n this.reconnectCount++;\n await this.connect();\n\n while (Date.now() < deadline) {\n if (this.state === 'connected') {\n return;\n }\n await new Promise((r) => setTimeout(r, 50));\n }\n\n throw new Error('Timeout waiting for reconnection');\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // PRIVATE HELPERS\n // ═══════════════════════════════════════════════════════════════════\n\n private buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json, text/event-stream',\n };\n\n // Only add Authorization header if we have a token AND not in public mode\n // Public mode explicitly skips auth headers for CI/CD and public docs testing\n if (this.authToken && !this.publicMode) {\n headers['Authorization'] = `Bearer ${this.authToken}`;\n }\n\n // Always send session ID if we have one (even in public mode - server creates sessions)\n if (this.sessionId) {\n headers['mcp-session-id'] = this.sessionId;\n }\n\n // Add custom headers from config (allow override even in public mode)\n if (this.config.auth.headers) {\n Object.assign(headers, this.config.auth.headers);\n }\n\n return headers;\n }\n\n private ensureConnected(): void {\n if (this.state !== 'connected') {\n throw new Error('Transport not connected. Call connect() first.');\n }\n }\n\n private log(message: string, data?: unknown): void {\n if (this.config.debug) {\n console.log(`[StreamableHTTP] ${message}`, data ?? '');\n }\n }\n\n /**\n * Parse SSE (Server-Sent Events) response format with session ID extraction\n * SSE format is:\n * event: message\n * id: sessionId:messageId\n * data: {\"jsonrpc\":\"2.0\",...}\n *\n * The id field contains the session ID followed by a colon and the message ID.\n *\n * @param text - The raw SSE response text\n * @param requestId - The original request ID\n * @returns Object with parsed JSON-RPC response and session ID (if found)\n */\n private parseSSEResponseWithSession(\n text: string,\n requestId: string | number | undefined,\n ): { response: JsonRpcResponse; sseSessionId?: string } {\n const lines = text.split('\\n');\n const dataLines: string[] = [];\n let sseSessionId: string | undefined;\n\n // Collect all data lines and extract session ID from id: field\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n dataLines.push(line.slice(6)); // Remove 'data: ' prefix\n } else if (line === 'data:') {\n // Empty data line represents a newline in the data\n dataLines.push('');\n } else if (line.startsWith('id: ')) {\n // Extract session ID from id field (format: sessionId:messageId)\n const idValue = line.slice(4);\n const colonIndex = idValue.lastIndexOf(':');\n if (colonIndex > 0) {\n sseSessionId = idValue.substring(0, colonIndex);\n } else {\n // No colon, use the whole id as session ID\n sseSessionId = idValue;\n }\n }\n }\n\n if (dataLines.length > 0) {\n const jsonData = dataLines.join('\\n');\n try {\n return {\n response: JSON.parse(jsonData) as JsonRpcResponse,\n sseSessionId,\n };\n } catch {\n this.log('Failed to parse SSE data as JSON:', jsonData);\n }\n }\n\n // Fallback: return error response\n return {\n response: {\n jsonrpc: '2.0',\n id: requestId ?? null,\n error: {\n code: -32700,\n message: 'Failed to parse SSE response',\n data: text,\n },\n },\n sseSessionId,\n };\n }\n}\n"]}
1
+ {"version":3,"file":"streamable-http.transport.js","sourceRoot":"","sources":["../../../src/transport/streamable-http.transport.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file streamable-http.transport.ts\n * @description StreamableHTTP transport implementation for MCP Test Client\n */\n\nimport type {\n McpTransport,\n TransportConfig,\n TransportState,\n JsonRpcRequest,\n JsonRpcResponse,\n} from './transport.interface';\nimport type { InterceptorChain } from '../interceptor';\nimport type { ClientInfo } from '../client/mcp-test-client.types';\n\nconst DEFAULT_TIMEOUT = 30000;\n\n/**\n * StreamableHTTP transport for MCP communication\n *\n * This transport uses HTTP POST requests for all communication,\n * following the MCP StreamableHTTP specification.\n */\nexport class StreamableHttpTransport implements McpTransport {\n private readonly config: Required<Omit<TransportConfig, 'interceptors' | 'clientInfo'>> & {\n interceptors?: InterceptorChain;\n clientInfo?: ClientInfo;\n };\n private state: TransportState = 'disconnected';\n private sessionId: string | undefined;\n private authToken: string | undefined;\n private connectionCount = 0;\n private reconnectCount = 0;\n private lastRequestHeaders: Record<string, string> = {};\n private interceptors?: InterceptorChain;\n private readonly publicMode: boolean;\n\n constructor(config: TransportConfig) {\n this.config = {\n baseUrl: config.baseUrl.replace(/\\/$/, ''), // Remove trailing slash\n timeout: config.timeout ?? DEFAULT_TIMEOUT,\n auth: config.auth ?? {},\n publicMode: config.publicMode ?? false,\n debug: config.debug ?? false,\n interceptors: config.interceptors,\n clientInfo: config.clientInfo,\n };\n\n this.authToken = config.auth?.token;\n this.interceptors = config.interceptors;\n this.publicMode = config.publicMode ?? false;\n }\n\n async connect(): Promise<void> {\n this.state = 'connecting';\n this.connectionCount++;\n\n try {\n // Public mode: Skip all authentication - connect without any token\n if (this.publicMode) {\n this.log('Public mode: connecting without authentication');\n this.state = 'connected';\n return;\n }\n\n // If no auth token provided, request anonymous token from FrontMCP SDK\n if (!this.authToken) {\n await this.requestAnonymousToken();\n }\n\n // StreamableHTTP doesn't require an explicit connection step\n // The session is established on the first request\n this.state = 'connected';\n this.log('Connected to StreamableHTTP transport');\n } catch (error) {\n this.state = 'error';\n throw error;\n }\n }\n\n /**\n * Request an anonymous token from the FrontMCP OAuth endpoint\n * This allows the test client to authenticate without user interaction\n */\n private async requestAnonymousToken(): Promise<void> {\n const clientId = crypto.randomUUID();\n const tokenUrl = `${this.config.baseUrl}/oauth/token`;\n\n this.log(`Requesting anonymous token from ${tokenUrl}`);\n\n try {\n const response = await fetch(tokenUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n grant_type: 'anonymous',\n client_id: clientId,\n resource: this.config.baseUrl,\n }),\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n this.log(`Failed to get anonymous token: ${response.status} ${errorText}`);\n // Continue without token - server may allow unauthenticated access\n return;\n }\n\n const tokenResponse = await response.json();\n if (tokenResponse.access_token) {\n this.authToken = tokenResponse.access_token;\n this.log('Anonymous token acquired successfully');\n }\n } catch (error) {\n this.log(`Error requesting anonymous token: ${error}`);\n // Continue without token - server may allow unauthenticated access\n }\n }\n\n async request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & { result?: T }> {\n this.ensureConnected();\n\n const startTime = Date.now();\n\n // Process through interceptors if available\n if (this.interceptors) {\n const interceptResult = await this.interceptors.processRequest(message, {\n timestamp: new Date(),\n transport: 'streamable-http',\n sessionId: this.sessionId,\n });\n\n switch (interceptResult.type) {\n case 'mock': {\n // Return mock response directly, run through response interceptors\n const mockResponse = await this.interceptors.processResponse(\n message,\n interceptResult.response,\n Date.now() - startTime,\n );\n return mockResponse as JsonRpcResponse & { result?: T };\n }\n\n case 'error':\n throw interceptResult.error;\n\n case 'continue':\n // Use possibly modified request\n message = interceptResult.request;\n break;\n }\n }\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url}`, message);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Check for session ID in response headers\n const newSessionId = response.headers.get('mcp-session-id');\n if (newSessionId) {\n this.sessionId = newSessionId;\n }\n\n let jsonResponse: JsonRpcResponse;\n\n if (!response.ok) {\n // Handle HTTP errors\n const errorText = await response.text();\n this.log(`HTTP Error ${response.status}: ${errorText}`);\n\n jsonResponse = {\n jsonrpc: '2.0',\n id: message.id ?? null,\n error: {\n code: -32000,\n message: `HTTP ${response.status}: ${response.statusText}`,\n data: errorText,\n },\n };\n } else {\n // Parse response - may be JSON or SSE\n const contentType = response.headers.get('content-type') ?? '';\n const text = await response.text();\n this.log('Response:', text);\n\n // Handle empty response (for notifications)\n if (!text.trim()) {\n jsonResponse = {\n jsonrpc: '2.0',\n id: message.id ?? null,\n result: undefined,\n };\n } else if (contentType.includes('text/event-stream')) {\n // Parse SSE response - extract data and session ID from event stream\n const { response: sseResponse, sseSessionId } = this.parseSSEResponseWithSession(text, message.id);\n jsonResponse = sseResponse;\n // Store session ID from SSE id field (format: sessionId:messageId)\n if (sseSessionId && !this.sessionId) {\n this.sessionId = sseSessionId;\n this.log('Session ID from SSE:', this.sessionId);\n }\n } else {\n jsonResponse = JSON.parse(text) as JsonRpcResponse;\n }\n }\n\n // Process response through interceptors\n if (this.interceptors) {\n jsonResponse = await this.interceptors.processResponse(message, jsonResponse, Date.now() - startTime);\n }\n\n return jsonResponse as JsonRpcResponse & { result?: T };\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name === 'AbortError') {\n return {\n jsonrpc: '2.0',\n id: message.id ?? null,\n error: {\n code: -32000,\n message: `Request timeout after ${this.config.timeout}ms`,\n },\n };\n }\n\n throw error;\n }\n }\n\n async notify(message: JsonRpcRequest): Promise<void> {\n this.ensureConnected();\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url} (notification)`, message);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(message),\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n // Update session ID if present\n const newSessionId = response.headers.get('mcp-session-id');\n if (newSessionId) {\n this.sessionId = newSessionId;\n }\n\n if (!response.ok) {\n const errorText = await response.text();\n this.log(`HTTP Error ${response.status} on notification: ${errorText}`);\n }\n } catch (error) {\n clearTimeout(timeoutId);\n\n if (error instanceof Error && error.name !== 'AbortError') {\n throw error;\n }\n }\n }\n\n async sendRaw(data: string): Promise<JsonRpcResponse> {\n this.ensureConnected();\n\n const headers = this.buildHeaders();\n this.lastRequestHeaders = headers;\n\n const url = `${this.config.baseUrl}/`;\n this.log(`POST ${url} (raw)`, data);\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: data,\n signal: controller.signal,\n });\n\n clearTimeout(timeoutId);\n\n const text = await response.text();\n\n if (!text.trim()) {\n return {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32700,\n message: 'Parse error',\n },\n };\n }\n\n return JSON.parse(text) as JsonRpcResponse;\n } catch (error) {\n clearTimeout(timeoutId);\n\n return {\n jsonrpc: '2.0',\n id: null,\n error: {\n code: -32700,\n message: 'Parse error',\n data: error instanceof Error ? error.message : 'Unknown error',\n },\n };\n }\n }\n\n async close(): Promise<void> {\n this.state = 'disconnected';\n this.sessionId = undefined;\n this.log('StreamableHTTP transport closed');\n }\n\n isConnected(): boolean {\n return this.state === 'connected';\n }\n\n getState(): TransportState {\n return this.state;\n }\n\n getSessionId(): string | undefined {\n return this.sessionId;\n }\n\n setAuthToken(token: string): void {\n this.authToken = token;\n }\n\n setTimeout(ms: number): void {\n this.config.timeout = ms;\n }\n\n setInterceptors(interceptors: InterceptorChain): void {\n this.interceptors = interceptors;\n }\n\n getInterceptors(): InterceptorChain | undefined {\n return this.interceptors;\n }\n\n getConnectionCount(): number {\n return this.connectionCount;\n }\n\n getReconnectCount(): number {\n return this.reconnectCount;\n }\n\n getLastRequestHeaders(): Record<string, string> {\n return { ...this.lastRequestHeaders };\n }\n\n async simulateDisconnect(): Promise<void> {\n this.state = 'disconnected';\n this.sessionId = undefined;\n }\n\n async waitForReconnect(timeoutMs: number): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n\n // Auto-reconnect\n this.reconnectCount++;\n await this.connect();\n\n while (Date.now() < deadline) {\n if (this.state === 'connected') {\n return;\n }\n await new Promise((r) => setTimeout(r, 50));\n }\n\n throw new Error('Timeout waiting for reconnection');\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // PRIVATE HELPERS\n // ═══════════════════════════════════════════════════════════════════\n\n private buildHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n Accept: 'application/json, text/event-stream',\n };\n\n // Add User-Agent header based on clientInfo for platform detection\n // Server uses this to detect the AI platform before MCP initialize\n if (this.config.clientInfo) {\n headers['User-Agent'] = `${this.config.clientInfo.name}/${this.config.clientInfo.version}`;\n }\n\n // Only add Authorization header if we have a token AND not in public mode\n // Public mode explicitly skips auth headers for CI/CD and public docs testing\n if (this.authToken && !this.publicMode) {\n headers['Authorization'] = `Bearer ${this.authToken}`;\n }\n\n // Always send session ID if we have one (even in public mode - server creates sessions)\n if (this.sessionId) {\n headers['mcp-session-id'] = this.sessionId;\n }\n\n // Add custom headers from config (allow override even in public mode)\n if (this.config.auth.headers) {\n Object.assign(headers, this.config.auth.headers);\n }\n\n return headers;\n }\n\n private ensureConnected(): void {\n if (this.state !== 'connected') {\n throw new Error('Transport not connected. Call connect() first.');\n }\n }\n\n private log(message: string, data?: unknown): void {\n if (this.config.debug) {\n console.log(`[StreamableHTTP] ${message}`, data ?? '');\n }\n }\n\n /**\n * Parse SSE (Server-Sent Events) response format with session ID extraction\n * SSE format is:\n * event: message\n * id: sessionId:messageId\n * data: {\"jsonrpc\":\"2.0\",...}\n *\n * The id field contains the session ID followed by a colon and the message ID.\n *\n * @param text - The raw SSE response text\n * @param requestId - The original request ID\n * @returns Object with parsed JSON-RPC response and session ID (if found)\n */\n private parseSSEResponseWithSession(\n text: string,\n requestId: string | number | undefined,\n ): { response: JsonRpcResponse; sseSessionId?: string } {\n const lines = text.split('\\n');\n const dataLines: string[] = [];\n let sseSessionId: string | undefined;\n\n // Collect all data lines and extract session ID from id: field\n for (const line of lines) {\n if (line.startsWith('data: ')) {\n dataLines.push(line.slice(6)); // Remove 'data: ' prefix\n } else if (line === 'data:') {\n // Empty data line represents a newline in the data\n dataLines.push('');\n } else if (line.startsWith('id: ')) {\n // Extract session ID from id field (format: sessionId:messageId)\n const idValue = line.slice(4);\n const colonIndex = idValue.lastIndexOf(':');\n if (colonIndex > 0) {\n sseSessionId = idValue.substring(0, colonIndex);\n } else {\n // No colon, use the whole id as session ID\n sseSessionId = idValue;\n }\n }\n }\n\n if (dataLines.length > 0) {\n const jsonData = dataLines.join('\\n');\n try {\n return {\n response: JSON.parse(jsonData) as JsonRpcResponse,\n sseSessionId,\n };\n } catch {\n this.log('Failed to parse SSE data as JSON:', jsonData);\n }\n }\n\n // Fallback: return error response\n return {\n response: {\n jsonrpc: '2.0',\n id: requestId ?? null,\n error: {\n code: -32700,\n message: 'Failed to parse SSE response',\n data: text,\n },\n },\n sseSessionId,\n };\n }\n}\n"]}
@@ -2,6 +2,7 @@
2
2
  * @file transport.interface.ts
3
3
  * @description Interface for MCP transport implementations
4
4
  */
5
+ import type { ClientInfo } from '../client/mcp-test-client.types';
5
6
  export interface JsonRpcRequest {
6
7
  jsonrpc: '2.0';
7
8
  id?: string | number;
@@ -121,4 +122,6 @@ export interface TransportConfig {
121
122
  debug?: boolean;
122
123
  /** Interceptor chain for request/response interception */
123
124
  interceptors?: import('../interceptor').InterceptorChain;
125
+ /** Client info for User-Agent header (enables platform detection on server) */
126
+ clientInfo?: ClientInfo;
124
127
  }
@@ -1 +1 @@
1
- {"version":3,"file":"transport.interface.js","sourceRoot":"","sources":["../../../src/transport/transport.interface.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * @file transport.interface.ts\n * @description Interface for MCP transport implementations\n */\n\n// Simplified JSON-RPC types for transport layer\nexport interface JsonRpcRequest {\n jsonrpc: '2.0';\n id?: string | number;\n method: string;\n params?: Record<string, unknown>;\n}\n\nexport interface JsonRpcResponse {\n jsonrpc: '2.0';\n id: string | number | null;\n result?: unknown;\n error?: {\n code: number;\n message: string;\n data?: unknown;\n };\n}\n\nexport type TransportState = 'disconnected' | 'connecting' | 'connected' | 'error';\n\n/**\n * Interface that all MCP transports must implement\n */\nexport interface McpTransport {\n /**\n * Connect to the MCP server\n */\n connect(): Promise<void>;\n\n /**\n * Send a JSON-RPC request and wait for response\n */\n request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & { result?: T }>;\n\n /**\n * Send a notification (no response expected)\n */\n notify(message: JsonRpcRequest): Promise<void>;\n\n /**\n * Send raw string data (for error testing)\n */\n sendRaw(data: string): Promise<JsonRpcResponse>;\n\n /**\n * Close the connection\n */\n close(): Promise<void>;\n\n /**\n * Check if transport is connected\n */\n isConnected(): boolean;\n\n /**\n * Get current transport state\n */\n getState(): TransportState;\n\n /**\n * Get the session ID (if applicable)\n */\n getSessionId(): string | undefined;\n\n /**\n * Set the authentication token\n */\n setAuthToken(token: string): void;\n\n /**\n * Set the request timeout\n */\n setTimeout(ms: number): void;\n\n // Optional methods for testing\n\n /**\n * Get the message endpoint URL (SSE transport)\n */\n getMessageEndpoint?(): string | undefined;\n\n /**\n * Get number of connections made\n */\n getConnectionCount?(): number;\n\n /**\n * Get number of reconnections\n */\n getReconnectCount?(): number;\n\n /**\n * Get the headers from the last request\n */\n getLastRequestHeaders?(): Record<string, string>;\n\n /**\n * Simulate a disconnect (for testing reconnection)\n */\n simulateDisconnect?(): Promise<void>;\n\n /**\n * Wait for reconnection to complete\n */\n waitForReconnect?(timeoutMs: number): Promise<void>;\n\n /**\n * Set interceptor chain for request/response interception\n */\n setInterceptors?(interceptors: import('../interceptor').InterceptorChain): void;\n\n /**\n * Get the current interceptor chain\n */\n getInterceptors?(): import('../interceptor').InterceptorChain | undefined;\n}\n\n/**\n * Configuration for transport implementations\n */\nexport interface TransportConfig {\n /** Base URL of the MCP server */\n baseUrl: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n /** Authentication configuration */\n auth?: {\n token?: string;\n headers?: Record<string, string>;\n };\n /**\n * Enable public mode - skip authentication entirely.\n * When true, no Authorization header is sent and anonymous token is not requested.\n * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.\n */\n publicMode?: boolean;\n /** Enable debug logging */\n debug?: boolean;\n /** Interceptor chain for request/response interception */\n interceptors?: import('../interceptor').InterceptorChain;\n}\n"]}
1
+ {"version":3,"file":"transport.interface.js","sourceRoot":"","sources":["../../../src/transport/transport.interface.ts"],"names":[],"mappings":";AAAA;;;GAGG","sourcesContent":["/**\n * @file transport.interface.ts\n * @description Interface for MCP transport implementations\n */\n\nimport type { ClientInfo } from '../client/mcp-test-client.types';\n\n// Simplified JSON-RPC types for transport layer\nexport interface JsonRpcRequest {\n jsonrpc: '2.0';\n id?: string | number;\n method: string;\n params?: Record<string, unknown>;\n}\n\nexport interface JsonRpcResponse {\n jsonrpc: '2.0';\n id: string | number | null;\n result?: unknown;\n error?: {\n code: number;\n message: string;\n data?: unknown;\n };\n}\n\nexport type TransportState = 'disconnected' | 'connecting' | 'connected' | 'error';\n\n/**\n * Interface that all MCP transports must implement\n */\nexport interface McpTransport {\n /**\n * Connect to the MCP server\n */\n connect(): Promise<void>;\n\n /**\n * Send a JSON-RPC request and wait for response\n */\n request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & { result?: T }>;\n\n /**\n * Send a notification (no response expected)\n */\n notify(message: JsonRpcRequest): Promise<void>;\n\n /**\n * Send raw string data (for error testing)\n */\n sendRaw(data: string): Promise<JsonRpcResponse>;\n\n /**\n * Close the connection\n */\n close(): Promise<void>;\n\n /**\n * Check if transport is connected\n */\n isConnected(): boolean;\n\n /**\n * Get current transport state\n */\n getState(): TransportState;\n\n /**\n * Get the session ID (if applicable)\n */\n getSessionId(): string | undefined;\n\n /**\n * Set the authentication token\n */\n setAuthToken(token: string): void;\n\n /**\n * Set the request timeout\n */\n setTimeout(ms: number): void;\n\n // Optional methods for testing\n\n /**\n * Get the message endpoint URL (SSE transport)\n */\n getMessageEndpoint?(): string | undefined;\n\n /**\n * Get number of connections made\n */\n getConnectionCount?(): number;\n\n /**\n * Get number of reconnections\n */\n getReconnectCount?(): number;\n\n /**\n * Get the headers from the last request\n */\n getLastRequestHeaders?(): Record<string, string>;\n\n /**\n * Simulate a disconnect (for testing reconnection)\n */\n simulateDisconnect?(): Promise<void>;\n\n /**\n * Wait for reconnection to complete\n */\n waitForReconnect?(timeoutMs: number): Promise<void>;\n\n /**\n * Set interceptor chain for request/response interception\n */\n setInterceptors?(interceptors: import('../interceptor').InterceptorChain): void;\n\n /**\n * Get the current interceptor chain\n */\n getInterceptors?(): import('../interceptor').InterceptorChain | undefined;\n}\n\n/**\n * Configuration for transport implementations\n */\nexport interface TransportConfig {\n /** Base URL of the MCP server */\n baseUrl: string;\n /** Request timeout in milliseconds */\n timeout?: number;\n /** Authentication configuration */\n auth?: {\n token?: string;\n headers?: Record<string, string>;\n };\n /**\n * Enable public mode - skip authentication entirely.\n * When true, no Authorization header is sent and anonymous token is not requested.\n * Use this for testing public/unauthenticated endpoints in CI/CD pipelines.\n */\n publicMode?: boolean;\n /** Enable debug logging */\n debug?: boolean;\n /** Interceptor chain for request/response interception */\n interceptors?: import('../interceptor').InterceptorChain;\n /** Client info for User-Agent header (enables platform detection on server) */\n clientInfo?: ClientInfo;\n}\n"]}
@@ -22,6 +22,7 @@
22
22
  * ```
23
23
  */
24
24
  import type { ToolResultWrapper } from '../client/mcp-test-client.types';
25
+ import type { TestPlatformType } from '../platform/platform-types';
25
26
  /**
26
27
  * UI-specific assertion helpers.
27
28
  * Use these for imperative-style assertions with detailed error messages.
@@ -91,4 +92,62 @@ export declare const UIAssertions: {
91
92
  * @throws Error if any validation fails
92
93
  */
93
94
  assertValidUI(result: ToolResultWrapper, boundKeys?: string[]): string;
95
+ /**
96
+ * Assert tool result has correct meta keys for OpenAI platform.
97
+ * Verifies openai/* keys are present and ui/*, frontmcp/* keys are absent.
98
+ * @param result - The tool result wrapper
99
+ * @throws Error if meta keys don't match OpenAI expectations
100
+ */
101
+ assertOpenAIMeta(result: ToolResultWrapper): void;
102
+ /**
103
+ * Assert tool result has correct meta keys for ext-apps platform (SEP-1865).
104
+ * Verifies ui/* keys are present and openai/*, frontmcp/* keys are absent.
105
+ * @param result - The tool result wrapper
106
+ * @throws Error if meta keys don't match ext-apps expectations
107
+ */
108
+ assertExtAppsMeta(result: ToolResultWrapper): void;
109
+ /**
110
+ * Assert tool result has correct meta keys for FrontMCP platforms (Claude, Cursor, etc.).
111
+ * Verifies frontmcp/* + ui/* keys are present and openai/* keys are absent.
112
+ * @param result - The tool result wrapper
113
+ * @throws Error if meta keys don't match FrontMCP expectations
114
+ */
115
+ assertFrontmcpMeta(result: ToolResultWrapper): void;
116
+ /**
117
+ * Assert tool result has correct meta keys for a specific platform.
118
+ * @param result - The tool result wrapper
119
+ * @param platform - The platform type to check for
120
+ * @throws Error if meta keys don't match platform expectations
121
+ */
122
+ assertPlatformMeta(result: ToolResultWrapper, platform: TestPlatformType): void;
123
+ /**
124
+ * Assert that no cross-namespace pollution exists in meta.
125
+ * @param result - The tool result wrapper
126
+ * @param expectedNamespace - The namespace that SHOULD be present
127
+ * @throws Error if other namespaces are found
128
+ */
129
+ assertNoMixedNamespaces(result: ToolResultWrapper, expectedNamespace: string): void;
130
+ /**
131
+ * Assert that _meta has the correct MIME type for a platform.
132
+ * @param result - The tool result wrapper
133
+ * @param platform - The platform type to check for
134
+ * @throws Error if MIME type doesn't match platform expectations
135
+ */
136
+ assertPlatformMimeType(result: ToolResultWrapper, platform: TestPlatformType): void;
137
+ /**
138
+ * Assert that _meta has HTML in the correct platform-specific key.
139
+ * @param result - The tool result wrapper
140
+ * @param platform - The platform type to check for
141
+ * @returns The HTML string
142
+ * @throws Error if HTML is missing or in wrong key
143
+ */
144
+ assertPlatformHtml(result: ToolResultWrapper, platform: TestPlatformType): string;
145
+ /**
146
+ * Comprehensive platform meta validation.
147
+ * @param result - The tool result wrapper
148
+ * @param platform - The platform type to validate for
149
+ * @returns The platform-specific HTML string
150
+ * @throws Error if any platform-specific validation fails
151
+ */
152
+ assertValidPlatformMeta(result: ToolResultWrapper, platform: TestPlatformType): string;
94
153
  };