@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.
- package/package.json +4 -4
- package/src/auth/mock-api-server.d.ts +99 -0
- package/src/auth/mock-api-server.js +200 -0
- package/src/auth/mock-api-server.js.map +1 -0
- package/src/auth/mock-oauth-server.d.ts +85 -0
- package/src/auth/mock-oauth-server.js +253 -0
- package/src/auth/mock-oauth-server.js.map +1 -0
- package/src/client/mcp-test-client.builder.d.ts +43 -1
- package/src/client/mcp-test-client.builder.js +52 -0
- package/src/client/mcp-test-client.builder.js.map +1 -1
- package/src/client/mcp-test-client.js +22 -14
- package/src/client/mcp-test-client.js.map +1 -1
- package/src/client/mcp-test-client.types.d.ts +67 -6
- package/src/client/mcp-test-client.types.js +9 -0
- package/src/client/mcp-test-client.types.js.map +1 -1
- package/src/example-tools/index.d.ts +19 -0
- package/src/example-tools/index.js +40 -0
- package/src/example-tools/index.js.map +1 -0
- package/src/example-tools/tool-configs.d.ts +170 -0
- package/src/example-tools/tool-configs.js +222 -0
- package/src/example-tools/tool-configs.js.map +1 -0
- package/src/expect.d.ts +6 -5
- package/src/expect.js.map +1 -1
- package/src/fixtures/fixture-types.d.ts +19 -0
- package/src/fixtures/fixture-types.js.map +1 -1
- package/src/fixtures/test-fixture.d.ts +3 -1
- package/src/fixtures/test-fixture.js +35 -4
- package/src/fixtures/test-fixture.js.map +1 -1
- package/src/index.d.ts +7 -0
- package/src/index.js +40 -1
- package/src/index.js.map +1 -1
- package/src/matchers/matcher-types.js.map +1 -1
- package/src/matchers/mcp-matchers.d.ts +7 -0
- package/src/matchers/mcp-matchers.js +8 -4
- package/src/matchers/mcp-matchers.js.map +1 -1
- package/src/platform/index.d.ts +28 -0
- package/src/platform/index.js +47 -0
- package/src/platform/index.js.map +1 -0
- package/src/platform/platform-client-info.d.ts +97 -0
- package/src/platform/platform-client-info.js +155 -0
- package/src/platform/platform-client-info.js.map +1 -0
- package/src/platform/platform-types.d.ts +72 -0
- package/src/platform/platform-types.js +110 -0
- package/src/platform/platform-types.js.map +1 -0
- package/src/server/test-server.d.ts +4 -0
- package/src/server/test-server.js +58 -3
- package/src/server/test-server.js.map +1 -1
- package/src/transport/streamable-http.transport.js +6 -0
- package/src/transport/streamable-http.transport.js.map +1 -1
- package/src/transport/transport.interface.d.ts +3 -0
- package/src/transport/transport.interface.js.map +1 -1
- package/src/ui/ui-assertions.d.ts +59 -0
- package/src/ui/ui-assertions.js +152 -0
- package/src/ui/ui-assertions.js.map +1 -1
- package/src/ui/ui-matchers.d.ts +8 -0
- package/src/ui/ui-matchers.js +218 -0
- 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;;;
|
|
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
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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,
|