@frontmcp/testing 0.5.0 → 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 +4 -4
  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,253 @@
1
+ "use strict";
2
+ /**
3
+ * @file mock-oauth-server.ts
4
+ * @description Mock OAuth server for testing transparent auth mode
5
+ *
6
+ * This module provides a mock OAuth/OIDC server that serves:
7
+ * - JWKS endpoint for token verification
8
+ * - OAuth metadata endpoint (optional)
9
+ * - Token endpoint for anonymous tokens (optional)
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { MockOAuthServer, TestTokenFactory } from '@frontmcp/testing';
14
+ *
15
+ * const tokenFactory = new TestTokenFactory();
16
+ * const oauthServer = new MockOAuthServer(tokenFactory);
17
+ *
18
+ * // Start the mock server
19
+ * await oauthServer.start();
20
+ *
21
+ * // Configure your MCP server to use this mock
22
+ * // IDP_PROVIDER_URL = oauthServer.baseUrl
23
+ *
24
+ * // Create tokens using the same factory
25
+ * const token = await tokenFactory.createTestToken({ sub: 'user-123' });
26
+ *
27
+ * // Stop when done
28
+ * await oauthServer.stop();
29
+ * ```
30
+ */
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.MockOAuthServer = void 0;
33
+ const http_1 = require("http");
34
+ // ═══════════════════════════════════════════════════════════════════
35
+ // MOCK OAUTH SERVER
36
+ // ═══════════════════════════════════════════════════════════════════
37
+ /**
38
+ * Mock OAuth/OIDC server for testing transparent auth mode
39
+ *
40
+ * Serves JWKS from a TestTokenFactory so that MCP servers can
41
+ * validate test tokens without connecting to a real IdP.
42
+ */
43
+ class MockOAuthServer {
44
+ tokenFactory;
45
+ options;
46
+ server = null;
47
+ _info = null;
48
+ connections = new Set();
49
+ constructor(tokenFactory, options = {}) {
50
+ this.tokenFactory = tokenFactory;
51
+ this.options = options;
52
+ }
53
+ /**
54
+ * Start the mock OAuth server
55
+ */
56
+ async start() {
57
+ if (this.server) {
58
+ throw new Error('Mock OAuth server is already running');
59
+ }
60
+ const port = this.options.port ?? 0; // 0 = random available port
61
+ return new Promise((resolve, reject) => {
62
+ const server = (0, http_1.createServer)(this.handleRequest.bind(this));
63
+ this.server = server;
64
+ // Track connections for proper cleanup
65
+ server.on('connection', (socket) => {
66
+ this.connections.add(socket);
67
+ socket.on('close', () => this.connections.delete(socket));
68
+ });
69
+ server.on('error', (err) => {
70
+ this.log(`Server error: ${err.message}`);
71
+ reject(err);
72
+ });
73
+ server.listen(port, () => {
74
+ const address = server.address();
75
+ if (!address || typeof address === 'string') {
76
+ reject(new Error('Failed to get server address'));
77
+ return;
78
+ }
79
+ const actualPort = address.port;
80
+ const issuer = this.options.issuer ?? `http://localhost:${actualPort}`;
81
+ this._info = {
82
+ baseUrl: `http://localhost:${actualPort}`,
83
+ port: actualPort,
84
+ issuer,
85
+ jwksUrl: `http://localhost:${actualPort}/.well-known/jwks.json`,
86
+ };
87
+ this.log(`Mock OAuth server started at ${this._info.baseUrl}`);
88
+ resolve(this._info);
89
+ });
90
+ });
91
+ }
92
+ /**
93
+ * Stop the mock OAuth server
94
+ */
95
+ async stop() {
96
+ const server = this.server;
97
+ if (!server) {
98
+ return;
99
+ }
100
+ // Destroy all active connections to allow server.close() to complete
101
+ for (const socket of this.connections) {
102
+ socket.destroy();
103
+ }
104
+ this.connections.clear();
105
+ return new Promise((resolve, reject) => {
106
+ server.close((err) => {
107
+ if (err) {
108
+ reject(err);
109
+ }
110
+ else {
111
+ this.server = null;
112
+ this._info = null;
113
+ this.log('Mock OAuth server stopped');
114
+ resolve();
115
+ }
116
+ });
117
+ });
118
+ }
119
+ /**
120
+ * Get server info
121
+ */
122
+ get info() {
123
+ if (!this._info) {
124
+ throw new Error('Mock OAuth server is not running');
125
+ }
126
+ return this._info;
127
+ }
128
+ /**
129
+ * Get the token factory (for creating tokens)
130
+ */
131
+ getTokenFactory() {
132
+ return this.tokenFactory;
133
+ }
134
+ // ═══════════════════════════════════════════════════════════════════
135
+ // PRIVATE
136
+ // ═══════════════════════════════════════════════════════════════════
137
+ async handleRequest(req, res) {
138
+ const url = req.url ?? '/';
139
+ this.log(`${req.method} ${url}`);
140
+ // CORS headers
141
+ res.setHeader('Access-Control-Allow-Origin', '*');
142
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
143
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
144
+ if (req.method === 'OPTIONS') {
145
+ res.writeHead(204);
146
+ res.end();
147
+ return;
148
+ }
149
+ try {
150
+ if (url === '/.well-known/jwks.json' || url === '/.well-known/jwks') {
151
+ await this.handleJwks(req, res);
152
+ }
153
+ else if (url === '/.well-known/openid-configuration') {
154
+ await this.handleOidcConfig(req, res);
155
+ }
156
+ else if (url === '/.well-known/oauth-authorization-server') {
157
+ await this.handleOAuthMetadata(req, res);
158
+ }
159
+ else if (url === '/oauth/token') {
160
+ await this.handleTokenEndpoint(req, res);
161
+ }
162
+ else {
163
+ res.writeHead(404, { 'Content-Type': 'application/json' });
164
+ res.end(JSON.stringify({ error: 'not_found', error_description: 'Endpoint not found' }));
165
+ }
166
+ }
167
+ catch (error) {
168
+ this.log(`Error handling request: ${error}`);
169
+ res.writeHead(500, { 'Content-Type': 'application/json' });
170
+ res.end(JSON.stringify({ error: 'server_error', error_description: 'Internal server error' }));
171
+ }
172
+ }
173
+ async handleJwks(_req, res) {
174
+ const jwks = await this.tokenFactory.getPublicJwks();
175
+ res.writeHead(200, { 'Content-Type': 'application/json' });
176
+ res.end(JSON.stringify(jwks));
177
+ this.log('Served JWKS');
178
+ }
179
+ async handleOidcConfig(_req, res) {
180
+ const issuer = this._info?.issuer ?? 'http://localhost';
181
+ const config = {
182
+ issuer,
183
+ authorization_endpoint: `${issuer}/oauth/authorize`,
184
+ token_endpoint: `${issuer}/oauth/token`,
185
+ jwks_uri: `${issuer}/.well-known/jwks.json`,
186
+ response_types_supported: ['code', 'token'],
187
+ subject_types_supported: ['public'],
188
+ id_token_signing_alg_values_supported: ['RS256'],
189
+ scopes_supported: ['openid', 'profile', 'email'],
190
+ token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'none'],
191
+ claims_supported: ['sub', 'iss', 'aud', 'exp', 'iat', 'email', 'name'],
192
+ grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials', 'anonymous'],
193
+ };
194
+ res.writeHead(200, { 'Content-Type': 'application/json' });
195
+ res.end(JSON.stringify(config));
196
+ this.log('Served OIDC configuration');
197
+ }
198
+ async handleOAuthMetadata(_req, res) {
199
+ const issuer = this._info?.issuer ?? 'http://localhost';
200
+ const metadata = {
201
+ issuer,
202
+ authorization_endpoint: `${issuer}/oauth/authorize`,
203
+ token_endpoint: `${issuer}/oauth/token`,
204
+ jwks_uri: `${issuer}/.well-known/jwks.json`,
205
+ response_types_supported: ['code', 'token'],
206
+ grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials', 'anonymous'],
207
+ token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'none'],
208
+ scopes_supported: ['openid', 'profile', 'email', 'anonymous'],
209
+ };
210
+ res.writeHead(200, { 'Content-Type': 'application/json' });
211
+ res.end(JSON.stringify(metadata));
212
+ this.log('Served OAuth metadata');
213
+ }
214
+ async handleTokenEndpoint(req, res) {
215
+ // Parse request body
216
+ const body = await this.readBody(req);
217
+ const params = new URLSearchParams(body);
218
+ const grantType = params.get('grant_type');
219
+ if (grantType === 'anonymous') {
220
+ // Issue an anonymous token
221
+ const token = await this.tokenFactory.createAnonymousToken();
222
+ res.writeHead(200, { 'Content-Type': 'application/json' });
223
+ res.end(JSON.stringify({
224
+ access_token: token,
225
+ token_type: 'Bearer',
226
+ expires_in: 3600,
227
+ }));
228
+ this.log('Issued anonymous token');
229
+ }
230
+ else {
231
+ res.writeHead(400, { 'Content-Type': 'application/json' });
232
+ res.end(JSON.stringify({
233
+ error: 'unsupported_grant_type',
234
+ error_description: 'Only anonymous grant type is supported in mock server',
235
+ }));
236
+ }
237
+ }
238
+ readBody(req) {
239
+ return new Promise((resolve, reject) => {
240
+ const chunks = [];
241
+ req.on('data', (chunk) => chunks.push(chunk));
242
+ req.on('end', () => resolve(Buffer.concat(chunks).toString()));
243
+ req.on('error', reject);
244
+ });
245
+ }
246
+ log(message) {
247
+ if (this.options.debug) {
248
+ console.log(`[MockOAuthServer] ${message}`);
249
+ }
250
+ }
251
+ }
252
+ exports.MockOAuthServer = MockOAuthServer;
253
+ //# sourceMappingURL=mock-oauth-server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-oauth-server.js","sourceRoot":"","sources":["../../../src/auth/mock-oauth-server.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file mock-oauth-server.ts\n * @description Mock OAuth server for testing transparent auth mode\n *\n * This module provides a mock OAuth/OIDC server that serves:\n * - JWKS endpoint for token verification\n * - OAuth metadata endpoint (optional)\n * - Token endpoint for anonymous tokens (optional)\n *\n * @example\n * ```typescript\n * import { MockOAuthServer, TestTokenFactory } from '@frontmcp/testing';\n *\n * const tokenFactory = new TestTokenFactory();\n * const oauthServer = new MockOAuthServer(tokenFactory);\n *\n * // Start the mock server\n * await oauthServer.start();\n *\n * // Configure your MCP server to use this mock\n * // IDP_PROVIDER_URL = oauthServer.baseUrl\n *\n * // Create tokens using the same factory\n * const token = await tokenFactory.createTestToken({ sub: 'user-123' });\n *\n * // Stop when done\n * await oauthServer.stop();\n * ```\n */\n\nimport { createServer, Server, IncomingMessage, ServerResponse } from 'http';\nimport type { TestTokenFactory } from './token-factory';\n\n// ═══════════════════════════════════════════════════════════════════\n// TYPES\n// ═══════════════════════════════════════════════════════════════════\n\nexport interface MockOAuthServerOptions {\n /** Port to listen on (default: random available port) */\n port?: number;\n /** Issuer URL (default: http://localhost:{port}) */\n issuer?: string;\n /** Enable debug logging */\n debug?: boolean;\n}\n\nexport interface MockOAuthServerInfo {\n /** Base URL of the server */\n baseUrl: string;\n /** Port the server is listening on */\n port: number;\n /** Issuer URL */\n issuer: string;\n /** JWKS endpoint URL */\n jwksUrl: string;\n}\n\n// ═══════════════════════════════════════════════════════════════════\n// MOCK OAUTH SERVER\n// ═══════════════════════════════════════════════════════════════════\n\n/**\n * Mock OAuth/OIDC server for testing transparent auth mode\n *\n * Serves JWKS from a TestTokenFactory so that MCP servers can\n * validate test tokens without connecting to a real IdP.\n */\nexport class MockOAuthServer {\n private readonly tokenFactory: TestTokenFactory;\n private readonly options: MockOAuthServerOptions;\n private server: Server | null = null;\n private _info: MockOAuthServerInfo | null = null;\n private connections: Set<import('net').Socket> = new Set();\n\n constructor(tokenFactory: TestTokenFactory, options: MockOAuthServerOptions = {}) {\n this.tokenFactory = tokenFactory;\n this.options = options;\n }\n\n /**\n * Start the mock OAuth server\n */\n async start(): Promise<MockOAuthServerInfo> {\n if (this.server) {\n throw new Error('Mock OAuth server is already running');\n }\n\n const port = this.options.port ?? 0; // 0 = random available port\n\n return new Promise((resolve, reject) => {\n const server = createServer(this.handleRequest.bind(this));\n this.server = server;\n\n // Track connections for proper cleanup\n server.on('connection', (socket) => {\n this.connections.add(socket);\n socket.on('close', () => this.connections.delete(socket));\n });\n\n server.on('error', (err) => {\n this.log(`Server error: ${err.message}`);\n reject(err);\n });\n\n server.listen(port, () => {\n const address = server.address();\n if (!address || typeof address === 'string') {\n reject(new Error('Failed to get server address'));\n return;\n }\n\n const actualPort = address.port;\n const issuer = this.options.issuer ?? `http://localhost:${actualPort}`;\n\n this._info = {\n baseUrl: `http://localhost:${actualPort}`,\n port: actualPort,\n issuer,\n jwksUrl: `http://localhost:${actualPort}/.well-known/jwks.json`,\n };\n\n this.log(`Mock OAuth server started at ${this._info.baseUrl}`);\n resolve(this._info);\n });\n });\n }\n\n /**\n * Stop the mock OAuth server\n */\n async stop(): Promise<void> {\n const server = this.server;\n if (!server) {\n return;\n }\n\n // Destroy all active connections to allow server.close() to complete\n for (const socket of this.connections) {\n socket.destroy();\n }\n this.connections.clear();\n\n return new Promise((resolve, reject) => {\n server.close((err) => {\n if (err) {\n reject(err);\n } else {\n this.server = null;\n this._info = null;\n this.log('Mock OAuth server stopped');\n resolve();\n }\n });\n });\n }\n\n /**\n * Get server info\n */\n get info(): MockOAuthServerInfo {\n if (!this._info) {\n throw new Error('Mock OAuth server is not running');\n }\n return this._info;\n }\n\n /**\n * Get the token factory (for creating tokens)\n */\n getTokenFactory(): TestTokenFactory {\n return this.tokenFactory;\n }\n\n // ═══════════════════════════════════════════════════════════════════\n // PRIVATE\n // ═══════════════════════════════════════════════════════════════════\n\n private async handleRequest(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = req.url ?? '/';\n this.log(`${req.method} ${url}`);\n\n // CORS headers\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204);\n res.end();\n return;\n }\n\n try {\n if (url === '/.well-known/jwks.json' || url === '/.well-known/jwks') {\n await this.handleJwks(req, res);\n } else if (url === '/.well-known/openid-configuration') {\n await this.handleOidcConfig(req, res);\n } else if (url === '/.well-known/oauth-authorization-server') {\n await this.handleOAuthMetadata(req, res);\n } else if (url === '/oauth/token') {\n await this.handleTokenEndpoint(req, res);\n } else {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'not_found', error_description: 'Endpoint not found' }));\n }\n } catch (error) {\n this.log(`Error handling request: ${error}`);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'server_error', error_description: 'Internal server error' }));\n }\n }\n\n private async handleJwks(_req: IncomingMessage, res: ServerResponse): Promise<void> {\n const jwks = await this.tokenFactory.getPublicJwks();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(jwks));\n this.log('Served JWKS');\n }\n\n private async handleOidcConfig(_req: IncomingMessage, res: ServerResponse): Promise<void> {\n const issuer = this._info?.issuer ?? 'http://localhost';\n const config = {\n issuer,\n authorization_endpoint: `${issuer}/oauth/authorize`,\n token_endpoint: `${issuer}/oauth/token`,\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n response_types_supported: ['code', 'token'],\n subject_types_supported: ['public'],\n id_token_signing_alg_values_supported: ['RS256'],\n scopes_supported: ['openid', 'profile', 'email'],\n token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'none'],\n claims_supported: ['sub', 'iss', 'aud', 'exp', 'iat', 'email', 'name'],\n grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials', 'anonymous'],\n };\n\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(config));\n this.log('Served OIDC configuration');\n }\n\n private async handleOAuthMetadata(_req: IncomingMessage, res: ServerResponse): Promise<void> {\n const issuer = this._info?.issuer ?? 'http://localhost';\n const metadata = {\n issuer,\n authorization_endpoint: `${issuer}/oauth/authorize`,\n token_endpoint: `${issuer}/oauth/token`,\n jwks_uri: `${issuer}/.well-known/jwks.json`,\n response_types_supported: ['code', 'token'],\n grant_types_supported: ['authorization_code', 'refresh_token', 'client_credentials', 'anonymous'],\n token_endpoint_auth_methods_supported: ['client_secret_basic', 'client_secret_post', 'none'],\n scopes_supported: ['openid', 'profile', 'email', 'anonymous'],\n };\n\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(metadata));\n this.log('Served OAuth metadata');\n }\n\n private async handleTokenEndpoint(req: IncomingMessage, res: ServerResponse): Promise<void> {\n // Parse request body\n const body = await this.readBody(req);\n const params = new URLSearchParams(body);\n const grantType = params.get('grant_type');\n\n if (grantType === 'anonymous') {\n // Issue an anonymous token\n const token = await this.tokenFactory.createAnonymousToken();\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n access_token: token,\n token_type: 'Bearer',\n expires_in: 3600,\n }),\n );\n this.log('Issued anonymous token');\n } else {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n error: 'unsupported_grant_type',\n error_description: 'Only anonymous grant type is supported in mock server',\n }),\n );\n }\n }\n\n private readBody(req: IncomingMessage): Promise<string> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n req.on('data', (chunk) => chunks.push(chunk));\n req.on('end', () => resolve(Buffer.concat(chunks).toString()));\n req.on('error', reject);\n });\n }\n\n private log(message: string): void {\n if (this.options.debug) {\n console.log(`[MockOAuthServer] ${message}`);\n }\n }\n}\n"]}
@@ -2,8 +2,9 @@
2
2
  * @file mcp-test-client.builder.ts
3
3
  * @description Builder pattern for creating McpTestClient instances
4
4
  */
5
- import type { McpTestClientConfig, TestTransportType, TestAuthConfig } from './mcp-test-client.types';
5
+ import type { McpTestClientConfig, TestTransportType, TestAuthConfig, TestClientCapabilities } from './mcp-test-client.types';
6
6
  import { McpTestClient } from './mcp-test-client';
7
+ import type { TestPlatformType } from '../platform/platform-types';
7
8
  /**
8
9
  * Builder for creating McpTestClient instances with fluent API
9
10
  *
@@ -61,6 +62,47 @@ export declare class McpTestClientBuilder {
61
62
  name: string;
62
63
  version: string;
63
64
  }): this;
65
+ /**
66
+ * Set the platform type for testing platform-specific meta keys.
67
+ * Automatically configures clientInfo and capabilities for platform detection.
68
+ *
69
+ * Platform-specific behavior:
70
+ * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
71
+ * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
72
+ * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
73
+ * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
74
+ * - Other platforms follow similar patterns
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * const client = await McpTestClient.create({ baseUrl })
79
+ * .withPlatform('openai')
80
+ * .buildAndConnect();
81
+ *
82
+ * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
83
+ * const extAppsClient = await McpTestClient.create({ baseUrl })
84
+ * .withPlatform('ext-apps')
85
+ * .buildAndConnect();
86
+ * ```
87
+ */
88
+ withPlatform(platform: TestPlatformType): this;
89
+ /**
90
+ * Set custom client capabilities for MCP initialization.
91
+ * Use this for fine-grained control over capabilities sent during initialization.
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * const client = await McpTestClient.create({ baseUrl })
96
+ * .withCapabilities({
97
+ * sampling: {},
98
+ * experimental: {
99
+ * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html+mcp'] }
100
+ * }
101
+ * })
102
+ * .buildAndConnect();
103
+ * ```
104
+ */
105
+ withCapabilities(capabilities: TestClientCapabilities): this;
64
106
  /**
65
107
  * Build the McpTestClient instance (does not connect)
66
108
  */
@@ -6,6 +6,7 @@
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.McpTestClientBuilder = void 0;
8
8
  const mcp_test_client_1 = require("./mcp-test-client");
9
+ const platform_client_info_1 = require("../platform/platform-client-info");
9
10
  /**
10
11
  * Builder for creating McpTestClient instances with fluent API
11
12
  *
@@ -92,6 +93,57 @@ class McpTestClientBuilder {
92
93
  this.config.clientInfo = info;
93
94
  return this;
94
95
  }
96
+ /**
97
+ * Set the platform type for testing platform-specific meta keys.
98
+ * Automatically configures clientInfo and capabilities for platform detection.
99
+ *
100
+ * Platform-specific behavior:
101
+ * - `openai`: Uses openai/* meta keys, sets User-Agent to "ChatGPT/1.0"
102
+ * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability
103
+ * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to "claude-desktop/1.0"
104
+ * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to "cursor/1.0"
105
+ * - Other platforms follow similar patterns
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * const client = await McpTestClient.create({ baseUrl })
110
+ * .withPlatform('openai')
111
+ * .buildAndConnect();
112
+ *
113
+ * // ext-apps automatically sets the io.modelcontextprotocol/ui capability
114
+ * const extAppsClient = await McpTestClient.create({ baseUrl })
115
+ * .withPlatform('ext-apps')
116
+ * .buildAndConnect();
117
+ * ```
118
+ */
119
+ withPlatform(platform) {
120
+ this.config.platform = platform;
121
+ // Auto-set clientInfo based on platform for User-Agent detection
122
+ this.config.clientInfo = (0, platform_client_info_1.getPlatformClientInfo)(platform);
123
+ // Auto-set capabilities based on platform (ext-apps requires io.modelcontextprotocol/ui)
124
+ this.config.capabilities = (0, platform_client_info_1.getPlatformCapabilities)(platform);
125
+ return this;
126
+ }
127
+ /**
128
+ * Set custom client capabilities for MCP initialization.
129
+ * Use this for fine-grained control over capabilities sent during initialization.
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const client = await McpTestClient.create({ baseUrl })
134
+ * .withCapabilities({
135
+ * sampling: {},
136
+ * experimental: {
137
+ * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html+mcp'] }
138
+ * }
139
+ * })
140
+ * .buildAndConnect();
141
+ * ```
142
+ */
143
+ withCapabilities(capabilities) {
144
+ this.config.capabilities = capabilities;
145
+ return this;
146
+ }
95
147
  /**
96
148
  * Build the McpTestClient instance (does not connect)
97
149
  */
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-test-client.builder.js","sourceRoot":"","sources":["../../../src/client/mcp-test-client.builder.ts"],"names":[],"mappings":";AAAA;;;GAGG;;;AAGH,uDAAkD;AAElD;;;;;;;;;;;;GAYG;AACH,MAAa,oBAAoB;IACvB,MAAM,CAAsB;IAEpC,YAAY,MAA2B;QACrC,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,CAAC;IAC9B,CAAC;IAED;;OAEG;IACH,QAAQ,CAAC,IAAoB;QAC3B,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;QACpD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,SAAS,CAAC,KAAa;QACrB,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QAClD,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,OAA+B;QACzC,IAAI,CAAC,MAAM,CAAC,IAAI,GAAG;YACjB,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI;YACnB,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE;SACtD,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,aAAa,CAAC,SAA4B;QACxC,IAAI,CAAC,MAAM,CAAC,SAAS,GAAG,SAAS,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,WAAW,CAAC,SAAiB;QAC3B,IAAI,CAAC,MAAM,CAAC,OAAO,GAAG,SAAS,CAAC;QAChC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,SAAS,CAAC,OAAO,GAAG,IAAI;QACtB,IAAI,CAAC,MAAM,CAAC,KAAK,GAAG,OAAO,CAAC;QAC5B,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;;;OAIG;IACH,cAAc,CAAC,OAAO,GAAG,IAAI;QAC3B,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,OAAO,CAAC;QACjC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,mBAAmB,CAAC,OAAe;QACjC,IAAI,CAAC,MAAM,CAAC,eAAe,GAAG,OAAO,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,cAAc,CAAC,IAAuC;QACpD,IAAI,CAAC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC;QAC9B,OAAO,IAAI,CAAC;IACd,CAAC;IAED;;OAEG;IACH,KAAK;QACH,OAAO,IAAI,+BAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACxC,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,eAAe;QACnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,EAAE,CAAC;QAC5B,MAAM,MAAM,CAAC,OAAO,EAAE,CAAC;QACvB,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAnGD,oDAmGC","sourcesContent":["/**\n * @file mcp-test-client.builder.ts\n * @description Builder pattern for creating McpTestClient instances\n */\n\nimport type { McpTestClientConfig, TestTransportType, TestAuthConfig } from './mcp-test-client.types';\nimport { McpTestClient } from './mcp-test-client';\n\n/**\n * Builder for creating McpTestClient instances with fluent API\n *\n * @example\n * ```typescript\n * const client = await McpTestClient.create({ baseUrl: 'http://localhost:3003' })\n * .withTransport('streamable-http')\n * .withToken('my-jwt-token')\n * .withTimeout(5000)\n * .withDebug()\n * .buildAndConnect();\n * ```\n */\nexport class McpTestClientBuilder {\n private config: McpTestClientConfig;\n\n constructor(config: McpTestClientConfig) {\n this.config = { ...config };\n }\n\n /**\n * Set the authentication configuration\n */\n withAuth(auth: TestAuthConfig): this {\n this.config.auth = { ...this.config.auth, ...auth };\n return this;\n }\n\n /**\n * Set the bearer token for authentication\n */\n withToken(token: string): this {\n this.config.auth = { ...this.config.auth, token };\n return this;\n }\n\n /**\n * Add custom headers to all requests\n */\n withHeaders(headers: Record<string, string>): this {\n this.config.auth = {\n ...this.config.auth,\n headers: { ...this.config.auth?.headers, ...headers },\n };\n return this;\n }\n\n /**\n * Set the transport type\n */\n withTransport(transport: TestTransportType): this {\n this.config.transport = transport;\n return this;\n }\n\n /**\n * Set the request timeout in milliseconds\n */\n withTimeout(timeoutMs: number): this {\n this.config.timeout = timeoutMs;\n return this;\n }\n\n /**\n * Enable debug logging\n */\n withDebug(enabled = true): this {\n this.config.debug = enabled;\n return this;\n }\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 withPublicMode(enabled = true): this {\n this.config.publicMode = enabled;\n return this;\n }\n\n /**\n * Set the MCP protocol version to request\n */\n withProtocolVersion(version: string): this {\n this.config.protocolVersion = version;\n return this;\n }\n\n /**\n * Set the client info sent during initialization\n */\n withClientInfo(info: { name: string; version: string }): this {\n this.config.clientInfo = info;\n return this;\n }\n\n /**\n * Build the McpTestClient instance (does not connect)\n */\n build(): McpTestClient {\n return new McpTestClient(this.config);\n }\n\n /**\n * Build the McpTestClient and connect to the server\n */\n async buildAndConnect(): Promise<McpTestClient> {\n const client = this.build();\n await client.connect();\n return client;\n }\n}\n"]}
1
+ {"version":3,"file":"mcp-test-client.builder.js","sourceRoot":"","sources":["../../../src/client/mcp-test-client.builder.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @file mcp-test-client.builder.ts\n * @description Builder pattern for creating McpTestClient instances\n */\n\nimport type {\n McpTestClientConfig,\n TestTransportType,\n TestAuthConfig,\n TestClientCapabilities,\n} from './mcp-test-client.types';\nimport { McpTestClient } from './mcp-test-client';\nimport type { TestPlatformType } from '../platform/platform-types';\nimport { getPlatformClientInfo, getPlatformCapabilities } from '../platform/platform-client-info';\n\n/**\n * Builder for creating McpTestClient instances with fluent API\n *\n * @example\n * ```typescript\n * const client = await McpTestClient.create({ baseUrl: 'http://localhost:3003' })\n * .withTransport('streamable-http')\n * .withToken('my-jwt-token')\n * .withTimeout(5000)\n * .withDebug()\n * .buildAndConnect();\n * ```\n */\nexport class McpTestClientBuilder {\n private config: McpTestClientConfig;\n\n constructor(config: McpTestClientConfig) {\n this.config = { ...config };\n }\n\n /**\n * Set the authentication configuration\n */\n withAuth(auth: TestAuthConfig): this {\n this.config.auth = { ...this.config.auth, ...auth };\n return this;\n }\n\n /**\n * Set the bearer token for authentication\n */\n withToken(token: string): this {\n this.config.auth = { ...this.config.auth, token };\n return this;\n }\n\n /**\n * Add custom headers to all requests\n */\n withHeaders(headers: Record<string, string>): this {\n this.config.auth = {\n ...this.config.auth,\n headers: { ...this.config.auth?.headers, ...headers },\n };\n return this;\n }\n\n /**\n * Set the transport type\n */\n withTransport(transport: TestTransportType): this {\n this.config.transport = transport;\n return this;\n }\n\n /**\n * Set the request timeout in milliseconds\n */\n withTimeout(timeoutMs: number): this {\n this.config.timeout = timeoutMs;\n return this;\n }\n\n /**\n * Enable debug logging\n */\n withDebug(enabled = true): this {\n this.config.debug = enabled;\n return this;\n }\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 withPublicMode(enabled = true): this {\n this.config.publicMode = enabled;\n return this;\n }\n\n /**\n * Set the MCP protocol version to request\n */\n withProtocolVersion(version: string): this {\n this.config.protocolVersion = version;\n return this;\n }\n\n /**\n * Set the client info sent during initialization\n */\n withClientInfo(info: { name: string; version: string }): this {\n this.config.clientInfo = info;\n return this;\n }\n\n /**\n * Set the platform type for testing platform-specific meta keys.\n * Automatically configures clientInfo and capabilities for platform detection.\n *\n * Platform-specific behavior:\n * - `openai`: Uses openai/* meta keys, sets User-Agent to \"ChatGPT/1.0\"\n * - `ext-apps`: Uses ui/* meta keys per SEP-1865, sets io.modelcontextprotocol/ui capability\n * - `claude`: Uses frontmcp/* + ui/* keys, sets User-Agent to \"claude-desktop/1.0\"\n * - `cursor`: Uses frontmcp/* + ui/* keys, sets User-Agent to \"cursor/1.0\"\n * - Other platforms follow similar patterns\n *\n * @example\n * ```typescript\n * const client = await McpTestClient.create({ baseUrl })\n * .withPlatform('openai')\n * .buildAndConnect();\n *\n * // ext-apps automatically sets the io.modelcontextprotocol/ui capability\n * const extAppsClient = await McpTestClient.create({ baseUrl })\n * .withPlatform('ext-apps')\n * .buildAndConnect();\n * ```\n */\n withPlatform(platform: TestPlatformType): this {\n this.config.platform = platform;\n // Auto-set clientInfo based on platform for User-Agent detection\n this.config.clientInfo = getPlatformClientInfo(platform);\n // Auto-set capabilities based on platform (ext-apps requires io.modelcontextprotocol/ui)\n this.config.capabilities = getPlatformCapabilities(platform);\n return this;\n }\n\n /**\n * Set custom client capabilities for MCP initialization.\n * Use this for fine-grained control over capabilities sent during initialization.\n *\n * @example\n * ```typescript\n * const client = await McpTestClient.create({ baseUrl })\n * .withCapabilities({\n * sampling: {},\n * experimental: {\n * 'io.modelcontextprotocol/ui': { mimeTypes: ['text/html+mcp'] }\n * }\n * })\n * .buildAndConnect();\n * ```\n */\n withCapabilities(capabilities: TestClientCapabilities): this {\n this.config.capabilities = capabilities;\n return this;\n }\n\n /**\n * Build the McpTestClient instance (does not connect)\n */\n build(): McpTestClient {\n return new McpTestClient(this.config);\n }\n\n /**\n * Build the McpTestClient and connect to the server\n */\n async buildAndConnect(): Promise<McpTestClient> {\n const client = this.build();\n await client.connect();\n return client;\n }\n}\n"]}
@@ -21,6 +21,7 @@ const DEFAULT_CLIENT_INFO = {
21
21
  // MAIN CLIENT CLASS
22
22
  // ═══════════════════════════════════════════════════════════════════
23
23
  class McpTestClient {
24
+ // Platform and capabilities are optional - only set when testing platform-specific behavior
24
25
  config;
25
26
  transport = null;
26
27
  initResult = null;
@@ -49,16 +50,12 @@ class McpTestClient {
49
50
  debug: config.debug ?? false,
50
51
  protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
51
52
  clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
53
+ platform: config.platform,
54
+ capabilities: config.capabilities,
52
55
  };
53
- // In public mode, user is always anonymous (no token authentication)
54
- if (config.publicMode) {
55
- this._authState = {
56
- isAnonymous: true,
57
- scopes: [],
58
- };
59
- }
60
- else if (config.auth?.token) {
61
- // Parse auth state from config
56
+ // If a token is provided, user is authenticated (even in public mode)
57
+ // Public mode just means anonymous access is allowed, not that tokens are ignored
58
+ if (config.auth?.token) {
62
59
  this._authState = {
63
60
  isAnonymous: false,
64
61
  token: config.auth.token,
@@ -66,6 +63,7 @@ class McpTestClient {
66
63
  user: this.parseUserFromToken(config.auth.token),
67
64
  };
68
65
  }
66
+ // Otherwise, user is anonymous (default _authState is already { isAnonymous: true, scopes: [] })
69
67
  // Initialize interceptor chain
70
68
  this._interceptors = new interceptor_1.DefaultInterceptorChain();
71
69
  }
@@ -598,11 +596,13 @@ class McpTestClient {
598
596
  // PRIVATE: MCP OPERATIONS
599
597
  // ═══════════════════════════════════════════════════════════════════
600
598
  async initialize() {
599
+ // Use configured capabilities or default to base capabilities
600
+ const capabilities = this.config.capabilities ?? {
601
+ sampling: {},
602
+ };
601
603
  return this.request('initialize', {
602
604
  protocolVersion: this.config.protocolVersion,
603
- capabilities: {
604
- sampling: {},
605
- },
605
+ capabilities,
606
606
  clientInfo: this.config.clientInfo,
607
607
  });
608
608
  }
@@ -646,6 +646,7 @@ class McpTestClient {
646
646
  publicMode: this.config.publicMode,
647
647
  debug: this.config.debug,
648
648
  interceptors: this._interceptors,
649
+ clientInfo: this.config.clientInfo,
649
650
  });
650
651
  case 'sse':
651
652
  // TODO: Implement SSE transport
@@ -717,9 +718,16 @@ class McpTestClient {
717
718
  wrapToolResult(response) {
718
719
  const raw = response.data ?? { content: [] };
719
720
  const isError = !response.success || raw.isError === true;
720
- // Check for Tool UI response - has structuredContent and ui/html in _meta
721
+ // Check for Tool UI response - has UI metadata in _meta
722
+ // Platform-specific HTML keys:
723
+ // - OpenAI: openai/html
724
+ // - ext-apps: ui/html
725
+ // - Others: frontmcp/html (+ ui/html for compatibility)
721
726
  const meta = raw._meta;
722
- const hasUI = meta?.['ui/html'] !== undefined;
727
+ const hasUI = meta?.['ui/html'] !== undefined ||
728
+ meta?.['ui/component'] !== undefined ||
729
+ meta?.['openai/html'] !== undefined ||
730
+ meta?.['frontmcp/html'] !== undefined;
723
731
  const structuredContent = raw['structuredContent'];
724
732
  return {
725
733
  raw,