@frontmcp/testing 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1358 -0
  3. package/jest-preset.js +61 -0
  4. package/package.json +94 -0
  5. package/src/assertions/index.d.ts +5 -0
  6. package/src/assertions/index.js +18 -0
  7. package/src/assertions/index.js.map +1 -0
  8. package/src/assertions/mcp-assertions.d.ts +81 -0
  9. package/src/assertions/mcp-assertions.js +220 -0
  10. package/src/assertions/mcp-assertions.js.map +1 -0
  11. package/src/auth/auth-headers.d.ts +29 -0
  12. package/src/auth/auth-headers.js +62 -0
  13. package/src/auth/auth-headers.js.map +1 -0
  14. package/src/auth/index.d.ts +9 -0
  15. package/src/auth/index.js +15 -0
  16. package/src/auth/index.js.map +1 -0
  17. package/src/auth/token-factory.d.ts +94 -0
  18. package/src/auth/token-factory.js +181 -0
  19. package/src/auth/token-factory.js.map +1 -0
  20. package/src/auth/user-fixtures.d.ts +26 -0
  21. package/src/auth/user-fixtures.js +92 -0
  22. package/src/auth/user-fixtures.js.map +1 -0
  23. package/src/client/index.d.ts +7 -0
  24. package/src/client/index.js +12 -0
  25. package/src/client/index.js.map +1 -0
  26. package/src/client/mcp-test-client.builder.d.ts +72 -0
  27. package/src/client/mcp-test-client.builder.js +111 -0
  28. package/src/client/mcp-test-client.builder.js.map +1 -0
  29. package/src/client/mcp-test-client.d.ts +360 -0
  30. package/src/client/mcp-test-client.js +929 -0
  31. package/src/client/mcp-test-client.js.map +1 -0
  32. package/src/client/mcp-test-client.types.d.ts +216 -0
  33. package/src/client/mcp-test-client.types.js +7 -0
  34. package/src/client/mcp-test-client.types.js.map +1 -0
  35. package/src/errors/index.d.ts +45 -0
  36. package/src/errors/index.js +85 -0
  37. package/src/errors/index.js.map +1 -0
  38. package/src/expect.d.ts +67 -0
  39. package/src/expect.js +31 -0
  40. package/src/expect.js.map +1 -0
  41. package/src/fixtures/fixture-types.d.ts +166 -0
  42. package/src/fixtures/fixture-types.js +7 -0
  43. package/src/fixtures/fixture-types.js.map +1 -0
  44. package/src/fixtures/index.d.ts +7 -0
  45. package/src/fixtures/index.js +16 -0
  46. package/src/fixtures/index.js.map +1 -0
  47. package/src/fixtures/test-fixture.d.ts +41 -0
  48. package/src/fixtures/test-fixture.js +280 -0
  49. package/src/fixtures/test-fixture.js.map +1 -0
  50. package/src/http-mock/http-mock.d.ts +84 -0
  51. package/src/http-mock/http-mock.js +544 -0
  52. package/src/http-mock/http-mock.js.map +1 -0
  53. package/src/http-mock/http-mock.types.d.ts +124 -0
  54. package/src/http-mock/http-mock.types.js +10 -0
  55. package/src/http-mock/http-mock.types.js.map +1 -0
  56. package/src/http-mock/index.d.ts +6 -0
  57. package/src/http-mock/index.js +11 -0
  58. package/src/http-mock/index.js.map +1 -0
  59. package/src/index.d.ts +65 -0
  60. package/src/index.js +128 -0
  61. package/src/index.js.map +1 -0
  62. package/src/interceptor/index.d.ts +7 -0
  63. package/src/interceptor/index.js +15 -0
  64. package/src/interceptor/index.js.map +1 -0
  65. package/src/interceptor/interceptor-chain.d.ts +77 -0
  66. package/src/interceptor/interceptor-chain.js +207 -0
  67. package/src/interceptor/interceptor-chain.js.map +1 -0
  68. package/src/interceptor/interceptor.types.d.ts +131 -0
  69. package/src/interceptor/interceptor.types.js +7 -0
  70. package/src/interceptor/interceptor.types.js.map +1 -0
  71. package/src/interceptor/mock-registry.d.ts +82 -0
  72. package/src/interceptor/mock-registry.js +189 -0
  73. package/src/interceptor/mock-registry.js.map +1 -0
  74. package/src/matchers/index.d.ts +7 -0
  75. package/src/matchers/index.js +12 -0
  76. package/src/matchers/index.js.map +1 -0
  77. package/src/matchers/matcher-types.d.ts +266 -0
  78. package/src/matchers/matcher-types.js +10 -0
  79. package/src/matchers/matcher-types.js.map +1 -0
  80. package/src/matchers/mcp-matchers.d.ts +47 -0
  81. package/src/matchers/mcp-matchers.js +391 -0
  82. package/src/matchers/mcp-matchers.js.map +1 -0
  83. package/src/playwright/index.d.ts +37 -0
  84. package/src/playwright/index.js +49 -0
  85. package/src/playwright/index.js.map +1 -0
  86. package/src/server/index.d.ts +6 -0
  87. package/src/server/index.js +10 -0
  88. package/src/server/index.js.map +1 -0
  89. package/src/server/test-server.d.ts +99 -0
  90. package/src/server/test-server.js +286 -0
  91. package/src/server/test-server.js.map +1 -0
  92. package/src/setup.d.ts +22 -0
  93. package/src/setup.js +30 -0
  94. package/src/setup.js.map +1 -0
  95. package/src/transport/index.d.ts +6 -0
  96. package/src/transport/index.js +10 -0
  97. package/src/transport/index.js.map +1 -0
  98. package/src/transport/streamable-http.transport.d.ts +65 -0
  99. package/src/transport/streamable-http.transport.js +432 -0
  100. package/src/transport/streamable-http.transport.js.map +1 -0
  101. package/src/transport/transport.interface.d.ts +124 -0
  102. package/src/transport/transport.interface.js +7 -0
  103. package/src/transport/transport.interface.js.map +1 -0
  104. package/src/ui/index.d.ts +17 -0
  105. package/src/ui/index.js +23 -0
  106. package/src/ui/index.js.map +1 -0
  107. package/src/ui/ui-assertions.d.ts +94 -0
  108. package/src/ui/ui-assertions.js +215 -0
  109. package/src/ui/ui-assertions.js.map +1 -0
  110. package/src/ui/ui-matchers.d.ts +39 -0
  111. package/src/ui/ui-matchers.js +275 -0
  112. package/src/ui/ui-matchers.js.map +1 -0
@@ -0,0 +1,286 @@
1
+ "use strict";
2
+ /**
3
+ * @file test-server.ts
4
+ * @description Test server management for E2E testing
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.TestServer = void 0;
8
+ exports.findAvailablePort = findAvailablePort;
9
+ const child_process_1 = require("child_process");
10
+ // ═══════════════════════════════════════════════════════════════════
11
+ // TEST SERVER CLASS
12
+ // ═══════════════════════════════════════════════════════════════════
13
+ /**
14
+ * Manages test server lifecycle for E2E testing
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * // Start a server with custom command
19
+ * const server = await TestServer.start({
20
+ * command: 'node dist/main.js',
21
+ * port: 3003,
22
+ * cwd: './apps/my-server',
23
+ * });
24
+ *
25
+ * // Or start an Nx project
26
+ * const server = await TestServer.startNx('demo-public', { port: 3003 });
27
+ *
28
+ * // Use the server
29
+ * console.log(server.info.baseUrl); // http://localhost:3003
30
+ *
31
+ * // Stop when done
32
+ * await server.stop();
33
+ * ```
34
+ */
35
+ class TestServer {
36
+ process = null;
37
+ options;
38
+ _info;
39
+ logs = [];
40
+ constructor(options, port) {
41
+ this.options = {
42
+ port,
43
+ command: options.command ?? '',
44
+ cwd: options.cwd ?? process.cwd(),
45
+ env: options.env ?? {},
46
+ startupTimeout: options.startupTimeout ?? 30000,
47
+ healthCheckPath: options.healthCheckPath ?? '/health',
48
+ debug: options.debug ?? false,
49
+ };
50
+ this._info = {
51
+ baseUrl: `http://localhost:${port}`,
52
+ port,
53
+ };
54
+ }
55
+ /**
56
+ * Start a test server with custom command
57
+ */
58
+ static async start(options) {
59
+ const port = options.port ?? (await findAvailablePort());
60
+ const server = new TestServer(options, port);
61
+ try {
62
+ await server.startProcess();
63
+ }
64
+ catch (error) {
65
+ await server.stop(); // Clean up spawned process to prevent leaks
66
+ throw error;
67
+ }
68
+ return server;
69
+ }
70
+ /**
71
+ * Start an Nx project as test server
72
+ */
73
+ static async startNx(project, options = {}) {
74
+ // Validate project name contains only safe characters to prevent shell injection
75
+ if (!/^[\w-]+$/.test(project)) {
76
+ throw new Error(`Invalid project name: ${project}. Must contain only alphanumeric, underscore, and hyphen characters.`);
77
+ }
78
+ const port = options.port ?? (await findAvailablePort());
79
+ const serverOptions = {
80
+ ...options,
81
+ port,
82
+ command: `npx nx serve ${project} --port ${port}`,
83
+ cwd: options.cwd ?? process.cwd(),
84
+ };
85
+ const server = new TestServer(serverOptions, port);
86
+ try {
87
+ await server.startProcess();
88
+ }
89
+ catch (error) {
90
+ await server.stop(); // Clean up spawned process to prevent leaks
91
+ throw error;
92
+ }
93
+ return server;
94
+ }
95
+ /**
96
+ * Create a test server connected to an already running server
97
+ */
98
+ static connect(baseUrl) {
99
+ const url = new URL(baseUrl);
100
+ const port = parseInt(url.port, 10) || (url.protocol === 'https:' ? 443 : 80);
101
+ const server = new TestServer({
102
+ command: '',
103
+ port,
104
+ }, port);
105
+ server._info = {
106
+ baseUrl: baseUrl.replace(/\/$/, ''),
107
+ port,
108
+ };
109
+ return server;
110
+ }
111
+ /**
112
+ * Get server information
113
+ */
114
+ get info() {
115
+ return { ...this._info };
116
+ }
117
+ /**
118
+ * Stop the test server
119
+ */
120
+ async stop() {
121
+ if (this.process) {
122
+ this.log('Stopping server...');
123
+ // Try graceful shutdown first
124
+ this.process.kill('SIGTERM');
125
+ // Wait for process to exit
126
+ const exitPromise = new Promise((resolve) => {
127
+ if (this.process) {
128
+ this.process.once('exit', () => resolve());
129
+ }
130
+ else {
131
+ resolve();
132
+ }
133
+ });
134
+ // Force kill after timeout (but still wait for actual exit)
135
+ const killTimeout = setTimeout(() => {
136
+ if (this.process) {
137
+ this.log('Force killing server after timeout...');
138
+ this.process.kill('SIGKILL');
139
+ }
140
+ }, 5000);
141
+ await exitPromise;
142
+ clearTimeout(killTimeout);
143
+ this.process = null;
144
+ this.log('Server stopped');
145
+ }
146
+ }
147
+ /**
148
+ * Wait for server to be ready
149
+ */
150
+ async waitForReady(timeout) {
151
+ const timeoutMs = timeout ?? this.options.startupTimeout;
152
+ const deadline = Date.now() + timeoutMs;
153
+ const checkInterval = 100;
154
+ while (Date.now() < deadline) {
155
+ try {
156
+ const response = await fetch(`${this._info.baseUrl}${this.options.healthCheckPath}`, {
157
+ method: 'GET',
158
+ signal: AbortSignal.timeout(1000),
159
+ });
160
+ if (response.ok || response.status === 404) {
161
+ // 404 is okay - it means the server is running but might not have a health endpoint
162
+ this.log('Server is ready');
163
+ return;
164
+ }
165
+ }
166
+ catch {
167
+ // Server not ready yet
168
+ }
169
+ await sleep(checkInterval);
170
+ }
171
+ throw new Error(`Server did not become ready within ${timeoutMs}ms`);
172
+ }
173
+ /**
174
+ * Restart the server
175
+ */
176
+ async restart() {
177
+ await this.stop();
178
+ await this.startProcess();
179
+ }
180
+ /**
181
+ * Get captured server logs
182
+ */
183
+ getLogs() {
184
+ return [...this.logs];
185
+ }
186
+ /**
187
+ * Clear captured logs
188
+ */
189
+ clearLogs() {
190
+ this.logs = [];
191
+ }
192
+ // ═══════════════════════════════════════════════════════════════════
193
+ // PRIVATE METHODS
194
+ // ═══════════════════════════════════════════════════════════════════
195
+ async startProcess() {
196
+ if (!this.options.command) {
197
+ // No command means we're connecting to an existing server
198
+ await this.waitForReady();
199
+ return;
200
+ }
201
+ this.log(`Starting server: ${this.options.command}`);
202
+ const env = {
203
+ ...process.env,
204
+ ...this.options.env,
205
+ PORT: String(this.options.port),
206
+ };
207
+ // Use shell: true to handle complex commands with quoted arguments
208
+ // This avoids fragile command parsing with split(' ')
209
+ this.process = (0, child_process_1.spawn)(this.options.command, [], {
210
+ cwd: this.options.cwd,
211
+ env,
212
+ shell: true,
213
+ stdio: ['pipe', 'pipe', 'pipe'],
214
+ });
215
+ // pid can be undefined if spawn fails
216
+ if (this.process.pid !== undefined) {
217
+ this._info.pid = this.process.pid;
218
+ }
219
+ // Capture stdout
220
+ this.process.stdout?.on('data', (data) => {
221
+ const text = data.toString();
222
+ this.logs.push(text);
223
+ if (this.options.debug) {
224
+ console.log('[SERVER]', text);
225
+ }
226
+ });
227
+ // Capture stderr
228
+ this.process.stderr?.on('data', (data) => {
229
+ const text = data.toString();
230
+ this.logs.push(`[ERROR] ${text}`);
231
+ if (this.options.debug) {
232
+ console.error('[SERVER ERROR]', text);
233
+ }
234
+ });
235
+ // Handle spawn errors to prevent unhandled error events
236
+ this.process.on('error', (err) => {
237
+ this.logs.push(`[SPAWN ERROR] ${err.message}`);
238
+ if (this.options.debug) {
239
+ console.error('[SERVER SPAWN ERROR]', err);
240
+ }
241
+ });
242
+ // Handle process exit - use once() to avoid memory leak from listener not being cleaned up
243
+ this.process.once('exit', (code) => {
244
+ this.log(`Server process exited with code ${code}`);
245
+ });
246
+ // Wait for server to be ready
247
+ await this.waitForReady();
248
+ }
249
+ log(message) {
250
+ if (this.options.debug) {
251
+ console.log(`[TestServer] ${message}`);
252
+ }
253
+ }
254
+ }
255
+ exports.TestServer = TestServer;
256
+ // ═══════════════════════════════════════════════════════════════════
257
+ // UTILITIES
258
+ // ═══════════════════════════════════════════════════════════════════
259
+ /**
260
+ * Find an available port
261
+ */
262
+ async function findAvailablePort() {
263
+ // Use a simple approach: try to create a server on port 0 to get an available port
264
+ const { createServer } = await import('net');
265
+ return new Promise((resolve, reject) => {
266
+ const server = createServer();
267
+ server.listen(0, () => {
268
+ const address = server.address();
269
+ if (address && typeof address !== 'string') {
270
+ const port = address.port;
271
+ server.close(() => resolve(port));
272
+ }
273
+ else {
274
+ reject(new Error('Could not get port'));
275
+ }
276
+ });
277
+ server.on('error', reject);
278
+ });
279
+ }
280
+ /**
281
+ * Sleep for specified milliseconds
282
+ */
283
+ function sleep(ms) {
284
+ return new Promise((resolve) => setTimeout(resolve, ms));
285
+ }
286
+ //# sourceMappingURL=test-server.js.map
@@ -0,0 +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"]}
package/src/setup.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @file setup.ts
3
+ * @description Jest setup file for @frontmcp/testing
4
+ *
5
+ * This file registers custom MCP matchers with Jest.
6
+ * Include it in your Jest config's setupFilesAfterEnv:
7
+ *
8
+ * @example jest.config.ts
9
+ * ```typescript
10
+ * export default {
11
+ * setupFilesAfterEnv: ['@frontmcp/testing/setup'],
12
+ * };
13
+ * ```
14
+ *
15
+ * Or use the preset:
16
+ * ```typescript
17
+ * export default {
18
+ * preset: '@frontmcp/testing/jest-preset',
19
+ * };
20
+ * ```
21
+ */
22
+ import './matchers/matcher-types';
package/src/setup.js ADDED
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ /**
3
+ * @file setup.ts
4
+ * @description Jest setup file for @frontmcp/testing
5
+ *
6
+ * This file registers custom MCP matchers with Jest.
7
+ * Include it in your Jest config's setupFilesAfterEnv:
8
+ *
9
+ * @example jest.config.ts
10
+ * ```typescript
11
+ * export default {
12
+ * setupFilesAfterEnv: ['@frontmcp/testing/setup'],
13
+ * };
14
+ * ```
15
+ *
16
+ * Or use the preset:
17
+ * ```typescript
18
+ * export default {
19
+ * preset: '@frontmcp/testing/jest-preset',
20
+ * };
21
+ * ```
22
+ */
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ const globals_1 = require("@jest/globals");
25
+ const mcp_matchers_1 = require("./matchers/mcp-matchers");
26
+ // Register custom matchers with Jest
27
+ globals_1.expect.extend(mcp_matchers_1.mcpMatchers);
28
+ // Import type augmentation
29
+ require("./matchers/matcher-types");
30
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/setup.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;AAEH,2CAAuC;AACvC,0DAAsD;AAEtD,qCAAqC;AACrC,gBAAM,CAAC,MAAM,CAAC,0BAAW,CAAC,CAAC;AAE3B,2BAA2B;AAC3B,oCAAkC","sourcesContent":["/**\n * @file setup.ts\n * @description Jest setup file for @frontmcp/testing\n *\n * This file registers custom MCP matchers with Jest.\n * Include it in your Jest config's setupFilesAfterEnv:\n *\n * @example jest.config.ts\n * ```typescript\n * export default {\n * setupFilesAfterEnv: ['@frontmcp/testing/setup'],\n * };\n * ```\n *\n * Or use the preset:\n * ```typescript\n * export default {\n * preset: '@frontmcp/testing/jest-preset',\n * };\n * ```\n */\n\nimport { expect } from '@jest/globals';\nimport { mcpMatchers } from './matchers/mcp-matchers';\n\n// Register custom matchers with Jest\nexpect.extend(mcpMatchers);\n\n// Import type augmentation\nimport './matchers/matcher-types';\n"]}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @file transport/index.ts
3
+ * @description Transport layer exports
4
+ */
5
+ export type { McpTransport, TransportConfig, TransportState, JsonRpcRequest, JsonRpcResponse, } from './transport.interface';
6
+ export { StreamableHttpTransport } from './streamable-http.transport';
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * @file transport/index.ts
4
+ * @description Transport layer exports
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.StreamableHttpTransport = void 0;
8
+ var streamable_http_transport_1 = require("./streamable-http.transport");
9
+ Object.defineProperty(exports, "StreamableHttpTransport", { enumerable: true, get: function () { return streamable_http_transport_1.StreamableHttpTransport; } });
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/transport/index.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AASH,yEAAsE;AAA7D,oIAAA,uBAAuB,OAAA","sourcesContent":["/**\n * @file transport/index.ts\n * @description Transport layer exports\n */\n\nexport type {\n McpTransport,\n TransportConfig,\n TransportState,\n JsonRpcRequest,\n JsonRpcResponse,\n} from './transport.interface';\nexport { StreamableHttpTransport } from './streamable-http.transport';\n"]}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * @file streamable-http.transport.ts
3
+ * @description StreamableHTTP transport implementation for MCP Test Client
4
+ */
5
+ import type { McpTransport, TransportConfig, TransportState, JsonRpcRequest, JsonRpcResponse } from './transport.interface';
6
+ import type { InterceptorChain } from '../interceptor';
7
+ /**
8
+ * StreamableHTTP transport for MCP communication
9
+ *
10
+ * This transport uses HTTP POST requests for all communication,
11
+ * following the MCP StreamableHTTP specification.
12
+ */
13
+ export declare class StreamableHttpTransport implements McpTransport {
14
+ private readonly config;
15
+ private state;
16
+ private sessionId;
17
+ private authToken;
18
+ private connectionCount;
19
+ private reconnectCount;
20
+ private lastRequestHeaders;
21
+ private interceptors?;
22
+ private readonly publicMode;
23
+ constructor(config: TransportConfig);
24
+ connect(): Promise<void>;
25
+ /**
26
+ * Request an anonymous token from the FrontMCP OAuth endpoint
27
+ * This allows the test client to authenticate without user interaction
28
+ */
29
+ private requestAnonymousToken;
30
+ request<T = unknown>(message: JsonRpcRequest): Promise<JsonRpcResponse & {
31
+ result?: T;
32
+ }>;
33
+ notify(message: JsonRpcRequest): Promise<void>;
34
+ sendRaw(data: string): Promise<JsonRpcResponse>;
35
+ close(): Promise<void>;
36
+ isConnected(): boolean;
37
+ getState(): TransportState;
38
+ getSessionId(): string | undefined;
39
+ setAuthToken(token: string): void;
40
+ setTimeout(ms: number): void;
41
+ setInterceptors(interceptors: InterceptorChain): void;
42
+ getInterceptors(): InterceptorChain | undefined;
43
+ getConnectionCount(): number;
44
+ getReconnectCount(): number;
45
+ getLastRequestHeaders(): Record<string, string>;
46
+ simulateDisconnect(): Promise<void>;
47
+ waitForReconnect(timeoutMs: number): Promise<void>;
48
+ private buildHeaders;
49
+ private ensureConnected;
50
+ private log;
51
+ /**
52
+ * Parse SSE (Server-Sent Events) response format with session ID extraction
53
+ * SSE format is:
54
+ * event: message
55
+ * id: sessionId:messageId
56
+ * data: {"jsonrpc":"2.0",...}
57
+ *
58
+ * The id field contains the session ID followed by a colon and the message ID.
59
+ *
60
+ * @param text - The raw SSE response text
61
+ * @param requestId - The original request ID
62
+ * @returns Object with parsed JSON-RPC response and session ID (if found)
63
+ */
64
+ private parseSSEResponseWithSession;
65
+ }