@frontmcp/testing 0.6.0 → 0.6.2
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/esm/fixtures/index.mjs +2377 -0
- package/esm/index.mjs +4768 -0
- package/esm/matchers/index.mjs +646 -0
- package/esm/package.json +127 -0
- package/esm/playwright/index.mjs +19 -0
- package/esm/setup.mjs +680 -0
- package/fixtures/index.js +2418 -0
- package/index.js +4866 -0
- package/jest-preset.js +3 -3
- package/matchers/index.js +673 -0
- package/package.json +52 -24
- package/playwright/index.js +46 -0
- package/setup.js +651 -0
- package/src/assertions/index.js +0 -18
- package/src/assertions/index.js.map +0 -1
- package/src/assertions/mcp-assertions.js +0 -220
- package/src/assertions/mcp-assertions.js.map +0 -1
- package/src/auth/auth-headers.js +0 -62
- package/src/auth/auth-headers.js.map +0 -1
- package/src/auth/index.js +0 -15
- package/src/auth/index.js.map +0 -1
- package/src/auth/mock-api-server.js +0 -200
- package/src/auth/mock-api-server.js.map +0 -1
- package/src/auth/mock-oauth-server.js +0 -253
- package/src/auth/mock-oauth-server.js.map +0 -1
- package/src/auth/token-factory.js +0 -181
- package/src/auth/token-factory.js.map +0 -1
- package/src/auth/user-fixtures.js +0 -92
- package/src/auth/user-fixtures.js.map +0 -1
- package/src/client/index.js +0 -12
- package/src/client/index.js.map +0 -1
- package/src/client/mcp-test-client.builder.js +0 -163
- package/src/client/mcp-test-client.builder.js.map +0 -1
- package/src/client/mcp-test-client.js +0 -937
- package/src/client/mcp-test-client.js.map +0 -1
- package/src/client/mcp-test-client.types.js +0 -16
- package/src/client/mcp-test-client.types.js.map +0 -1
- package/src/errors/index.js +0 -85
- package/src/errors/index.js.map +0 -1
- package/src/example-tools/index.js +0 -40
- package/src/example-tools/index.js.map +0 -1
- package/src/example-tools/tool-configs.js +0 -222
- package/src/example-tools/tool-configs.js.map +0 -1
- package/src/expect.js +0 -31
- package/src/expect.js.map +0 -1
- package/src/fixtures/fixture-types.js +0 -7
- package/src/fixtures/fixture-types.js.map +0 -1
- package/src/fixtures/index.js +0 -16
- package/src/fixtures/index.js.map +0 -1
- package/src/fixtures/test-fixture.js +0 -311
- package/src/fixtures/test-fixture.js.map +0 -1
- package/src/http-mock/http-mock.js +0 -544
- package/src/http-mock/http-mock.js.map +0 -1
- package/src/http-mock/http-mock.types.js +0 -10
- package/src/http-mock/http-mock.types.js.map +0 -1
- package/src/http-mock/index.js +0 -11
- package/src/http-mock/index.js.map +0 -1
- package/src/index.js +0 -167
- package/src/index.js.map +0 -1
- package/src/interceptor/index.js +0 -15
- package/src/interceptor/index.js.map +0 -1
- package/src/interceptor/interceptor-chain.js +0 -207
- package/src/interceptor/interceptor-chain.js.map +0 -1
- package/src/interceptor/interceptor.types.js +0 -7
- package/src/interceptor/interceptor.types.js.map +0 -1
- package/src/interceptor/mock-registry.js +0 -189
- package/src/interceptor/mock-registry.js.map +0 -1
- package/src/matchers/index.js +0 -12
- package/src/matchers/index.js.map +0 -1
- package/src/matchers/matcher-types.js +0 -10
- package/src/matchers/matcher-types.js.map +0 -1
- package/src/matchers/mcp-matchers.js +0 -395
- package/src/matchers/mcp-matchers.js.map +0 -1
- package/src/platform/index.js +0 -47
- package/src/platform/index.js.map +0 -1
- package/src/platform/platform-client-info.js +0 -155
- package/src/platform/platform-client-info.js.map +0 -1
- package/src/platform/platform-types.js +0 -110
- package/src/platform/platform-types.js.map +0 -1
- package/src/playwright/index.js +0 -49
- package/src/playwright/index.js.map +0 -1
- package/src/server/index.js +0 -10
- package/src/server/index.js.map +0 -1
- package/src/server/test-server.js +0 -341
- package/src/server/test-server.js.map +0 -1
- package/src/setup.js +0 -30
- package/src/setup.js.map +0 -1
- package/src/transport/index.js +0 -10
- package/src/transport/index.js.map +0 -1
- package/src/transport/streamable-http.transport.js +0 -438
- package/src/transport/streamable-http.transport.js.map +0 -1
- package/src/transport/transport.interface.js +0 -7
- package/src/transport/transport.interface.js.map +0 -1
- package/src/ui/index.js +0 -23
- package/src/ui/index.js.map +0 -1
- package/src/ui/ui-assertions.js +0 -367
- package/src/ui/ui-assertions.js.map +0 -1
- package/src/ui/ui-matchers.js +0 -493
- package/src/ui/ui-matchers.js.map +0 -1
- /package/{src/assertions → assertions}/index.d.ts +0 -0
- /package/{src/assertions → assertions}/mcp-assertions.d.ts +0 -0
- /package/{src/auth → auth}/auth-headers.d.ts +0 -0
- /package/{src/auth → auth}/index.d.ts +0 -0
- /package/{src/auth → auth}/mock-api-server.d.ts +0 -0
- /package/{src/auth → auth}/mock-oauth-server.d.ts +0 -0
- /package/{src/auth → auth}/token-factory.d.ts +0 -0
- /package/{src/auth → auth}/user-fixtures.d.ts +0 -0
- /package/{src/client → client}/index.d.ts +0 -0
- /package/{src/client → client}/mcp-test-client.builder.d.ts +0 -0
- /package/{src/client → client}/mcp-test-client.d.ts +0 -0
- /package/{src/client → client}/mcp-test-client.types.d.ts +0 -0
- /package/{src/errors → errors}/index.d.ts +0 -0
- /package/{src/example-tools → example-tools}/index.d.ts +0 -0
- /package/{src/example-tools → example-tools}/tool-configs.d.ts +0 -0
- /package/{src/expect.d.ts → expect.d.ts} +0 -0
- /package/{src/fixtures → fixtures}/fixture-types.d.ts +0 -0
- /package/{src/fixtures → fixtures}/index.d.ts +0 -0
- /package/{src/fixtures → fixtures}/test-fixture.d.ts +0 -0
- /package/{src/http-mock → http-mock}/http-mock.d.ts +0 -0
- /package/{src/http-mock → http-mock}/http-mock.types.d.ts +0 -0
- /package/{src/http-mock → http-mock}/index.d.ts +0 -0
- /package/{src/index.d.ts → index.d.ts} +0 -0
- /package/{src/interceptor → interceptor}/index.d.ts +0 -0
- /package/{src/interceptor → interceptor}/interceptor-chain.d.ts +0 -0
- /package/{src/interceptor → interceptor}/interceptor.types.d.ts +0 -0
- /package/{src/interceptor → interceptor}/mock-registry.d.ts +0 -0
- /package/{src/matchers → matchers}/index.d.ts +0 -0
- /package/{src/matchers → matchers}/matcher-types.d.ts +0 -0
- /package/{src/matchers → matchers}/mcp-matchers.d.ts +0 -0
- /package/{src/platform → platform}/index.d.ts +0 -0
- /package/{src/platform → platform}/platform-client-info.d.ts +0 -0
- /package/{src/platform → platform}/platform-types.d.ts +0 -0
- /package/{src/playwright → playwright}/index.d.ts +0 -0
- /package/{src/server → server}/index.d.ts +0 -0
- /package/{src/server → server}/test-server.d.ts +0 -0
- /package/{src/setup.d.ts → setup.d.ts} +0 -0
- /package/{src/transport → transport}/index.d.ts +0 -0
- /package/{src/transport → transport}/streamable-http.transport.d.ts +0 -0
- /package/{src/transport → transport}/transport.interface.d.ts +0 -0
- /package/{src/ui → ui}/index.d.ts +0 -0
- /package/{src/ui → ui}/ui-assertions.d.ts +0 -0
- /package/{src/ui → ui}/ui-matchers.d.ts +0 -0
|
@@ -1,937 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* @file mcp-test-client.ts
|
|
4
|
-
* @description Main MCP Test Client implementation for E2E testing
|
|
5
|
-
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.McpTestClient = void 0;
|
|
8
|
-
const mcp_test_client_builder_1 = require("./mcp-test-client.builder");
|
|
9
|
-
const streamable_http_transport_1 = require("../transport/streamable-http.transport");
|
|
10
|
-
const interceptor_1 = require("../interceptor");
|
|
11
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
12
|
-
// CONSTANTS
|
|
13
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
14
|
-
const DEFAULT_TIMEOUT = 30000;
|
|
15
|
-
const DEFAULT_PROTOCOL_VERSION = '2025-06-18';
|
|
16
|
-
const DEFAULT_CLIENT_INFO = {
|
|
17
|
-
name: '@frontmcp/testing',
|
|
18
|
-
version: '0.4.0',
|
|
19
|
-
};
|
|
20
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
21
|
-
// MAIN CLIENT CLASS
|
|
22
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
23
|
-
class McpTestClient {
|
|
24
|
-
// Platform and capabilities are optional - only set when testing platform-specific behavior
|
|
25
|
-
config;
|
|
26
|
-
transport = null;
|
|
27
|
-
initResult = null;
|
|
28
|
-
requestIdCounter = 0;
|
|
29
|
-
_lastRequestId = 0;
|
|
30
|
-
_sessionId;
|
|
31
|
-
_sessionInfo = null;
|
|
32
|
-
_authState = { isAnonymous: true, scopes: [] };
|
|
33
|
-
// Logging and tracing
|
|
34
|
-
_logs = [];
|
|
35
|
-
_traces = [];
|
|
36
|
-
_notifications = [];
|
|
37
|
-
_progressUpdates = [];
|
|
38
|
-
// Interceptor chain
|
|
39
|
-
_interceptors;
|
|
40
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
41
|
-
// CONSTRUCTOR & FACTORY
|
|
42
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
43
|
-
constructor(config) {
|
|
44
|
-
this.config = {
|
|
45
|
-
baseUrl: config.baseUrl,
|
|
46
|
-
transport: config.transport ?? 'streamable-http',
|
|
47
|
-
auth: config.auth ?? {},
|
|
48
|
-
publicMode: config.publicMode ?? false,
|
|
49
|
-
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
|
50
|
-
debug: config.debug ?? false,
|
|
51
|
-
protocolVersion: config.protocolVersion ?? DEFAULT_PROTOCOL_VERSION,
|
|
52
|
-
clientInfo: config.clientInfo ?? DEFAULT_CLIENT_INFO,
|
|
53
|
-
platform: config.platform,
|
|
54
|
-
capabilities: config.capabilities,
|
|
55
|
-
};
|
|
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) {
|
|
59
|
-
this._authState = {
|
|
60
|
-
isAnonymous: false,
|
|
61
|
-
token: config.auth.token,
|
|
62
|
-
scopes: this.parseScopesFromToken(config.auth.token),
|
|
63
|
-
user: this.parseUserFromToken(config.auth.token),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
// Otherwise, user is anonymous (default _authState is already { isAnonymous: true, scopes: [] })
|
|
67
|
-
// Initialize interceptor chain
|
|
68
|
-
this._interceptors = new interceptor_1.DefaultInterceptorChain();
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Create a new McpTestClientBuilder for fluent configuration
|
|
72
|
-
*/
|
|
73
|
-
static create(config) {
|
|
74
|
-
return new mcp_test_client_builder_1.McpTestClientBuilder(config);
|
|
75
|
-
}
|
|
76
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
77
|
-
// CONNECTION & LIFECYCLE
|
|
78
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
79
|
-
/**
|
|
80
|
-
* Connect to the MCP server and perform initialization
|
|
81
|
-
*/
|
|
82
|
-
async connect() {
|
|
83
|
-
this.log('debug', `Connecting to ${this.config.baseUrl}...`);
|
|
84
|
-
// Create transport based on config
|
|
85
|
-
this.transport = this.createTransport();
|
|
86
|
-
// Connect transport
|
|
87
|
-
await this.transport.connect();
|
|
88
|
-
// Perform MCP initialization
|
|
89
|
-
const initResponse = await this.initialize();
|
|
90
|
-
if (!initResponse.success || !initResponse.data) {
|
|
91
|
-
throw new Error(`Failed to initialize MCP connection: ${initResponse.error?.message ?? 'Unknown error'}`);
|
|
92
|
-
}
|
|
93
|
-
this.initResult = initResponse.data;
|
|
94
|
-
this._sessionId = this.transport.getSessionId();
|
|
95
|
-
this._sessionInfo = {
|
|
96
|
-
id: this._sessionId ?? `session-${Date.now()}`,
|
|
97
|
-
createdAt: new Date(),
|
|
98
|
-
lastActivityAt: new Date(),
|
|
99
|
-
requestCount: 1,
|
|
100
|
-
};
|
|
101
|
-
// Send initialized notification per MCP protocol
|
|
102
|
-
// This notification MUST be sent after receiving initialize response
|
|
103
|
-
// before the client can make any other requests
|
|
104
|
-
await this.transport.notify({
|
|
105
|
-
jsonrpc: '2.0',
|
|
106
|
-
method: 'notifications/initialized',
|
|
107
|
-
});
|
|
108
|
-
this.log('info', `Connected to ${this.initResult.serverInfo?.name ?? 'MCP Server'}`);
|
|
109
|
-
return this.initResult;
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* Disconnect from the MCP server
|
|
113
|
-
*/
|
|
114
|
-
async disconnect() {
|
|
115
|
-
if (this.transport) {
|
|
116
|
-
await this.transport.close();
|
|
117
|
-
this.transport = null;
|
|
118
|
-
}
|
|
119
|
-
this.initResult = null;
|
|
120
|
-
this.log('info', 'Disconnected from MCP server');
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Reconnect to the server, optionally with an existing session ID
|
|
124
|
-
*/
|
|
125
|
-
async reconnect(options) {
|
|
126
|
-
await this.disconnect();
|
|
127
|
-
if (options?.sessionId && this.transport) {
|
|
128
|
-
// Set session ID before reconnecting
|
|
129
|
-
this._sessionId = options.sessionId;
|
|
130
|
-
}
|
|
131
|
-
await this.connect();
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Check if the client is currently connected
|
|
135
|
-
*/
|
|
136
|
-
isConnected() {
|
|
137
|
-
return this.transport?.isConnected() ?? false;
|
|
138
|
-
}
|
|
139
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
140
|
-
// SESSION & AUTH PROPERTIES
|
|
141
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
142
|
-
get sessionId() {
|
|
143
|
-
return this._sessionId ?? '';
|
|
144
|
-
}
|
|
145
|
-
get session() {
|
|
146
|
-
const info = this._sessionInfo ?? {
|
|
147
|
-
id: '',
|
|
148
|
-
createdAt: new Date(),
|
|
149
|
-
lastActivityAt: new Date(),
|
|
150
|
-
requestCount: 0,
|
|
151
|
-
};
|
|
152
|
-
return {
|
|
153
|
-
...info,
|
|
154
|
-
expire: async () => {
|
|
155
|
-
// Force session expiration for testing
|
|
156
|
-
this._sessionId = undefined;
|
|
157
|
-
this._sessionInfo = null;
|
|
158
|
-
},
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
get auth() {
|
|
162
|
-
return this._authState;
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Authenticate with a token
|
|
166
|
-
*/
|
|
167
|
-
async authenticate(token) {
|
|
168
|
-
this._authState = {
|
|
169
|
-
isAnonymous: false,
|
|
170
|
-
token,
|
|
171
|
-
scopes: this.parseScopesFromToken(token),
|
|
172
|
-
user: this.parseUserFromToken(token),
|
|
173
|
-
};
|
|
174
|
-
// Update transport headers
|
|
175
|
-
if (this.transport) {
|
|
176
|
-
this.transport.setAuthToken(token);
|
|
177
|
-
}
|
|
178
|
-
this.log('debug', 'Authentication updated');
|
|
179
|
-
}
|
|
180
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
181
|
-
// SERVER INFO & CAPABILITIES
|
|
182
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
183
|
-
get serverInfo() {
|
|
184
|
-
return {
|
|
185
|
-
name: this.initResult?.serverInfo?.name ?? '',
|
|
186
|
-
version: this.initResult?.serverInfo?.version ?? '',
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
get protocolVersion() {
|
|
190
|
-
return this.initResult?.protocolVersion ?? '';
|
|
191
|
-
}
|
|
192
|
-
get instructions() {
|
|
193
|
-
return this.initResult?.instructions ?? '';
|
|
194
|
-
}
|
|
195
|
-
get capabilities() {
|
|
196
|
-
return this.initResult?.capabilities ?? {};
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Check if server has a specific capability
|
|
200
|
-
*/
|
|
201
|
-
hasCapability(name) {
|
|
202
|
-
return !!this.capabilities[name];
|
|
203
|
-
}
|
|
204
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
205
|
-
// TOOLS API
|
|
206
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
207
|
-
tools = {
|
|
208
|
-
/**
|
|
209
|
-
* List all available tools
|
|
210
|
-
*/
|
|
211
|
-
list: async () => {
|
|
212
|
-
const response = await this.listTools();
|
|
213
|
-
if (!response.success || !response.data) {
|
|
214
|
-
throw new Error(`Failed to list tools: ${response.error?.message}`);
|
|
215
|
-
}
|
|
216
|
-
return response.data.tools;
|
|
217
|
-
},
|
|
218
|
-
/**
|
|
219
|
-
* Call a tool by name with arguments
|
|
220
|
-
*/
|
|
221
|
-
call: async (name, args) => {
|
|
222
|
-
const response = await this.callTool(name, args);
|
|
223
|
-
return this.wrapToolResult(response);
|
|
224
|
-
},
|
|
225
|
-
};
|
|
226
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
227
|
-
// RESOURCES API
|
|
228
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
229
|
-
resources = {
|
|
230
|
-
/**
|
|
231
|
-
* List all static resources
|
|
232
|
-
*/
|
|
233
|
-
list: async () => {
|
|
234
|
-
const response = await this.listResources();
|
|
235
|
-
if (!response.success || !response.data) {
|
|
236
|
-
throw new Error(`Failed to list resources: ${response.error?.message}`);
|
|
237
|
-
}
|
|
238
|
-
return response.data.resources;
|
|
239
|
-
},
|
|
240
|
-
/**
|
|
241
|
-
* List all resource templates
|
|
242
|
-
*/
|
|
243
|
-
listTemplates: async () => {
|
|
244
|
-
const response = await this.listResourceTemplates();
|
|
245
|
-
if (!response.success || !response.data) {
|
|
246
|
-
throw new Error(`Failed to list resource templates: ${response.error?.message}`);
|
|
247
|
-
}
|
|
248
|
-
return response.data.resourceTemplates;
|
|
249
|
-
},
|
|
250
|
-
/**
|
|
251
|
-
* Read a resource by URI
|
|
252
|
-
*/
|
|
253
|
-
read: async (uri) => {
|
|
254
|
-
const response = await this.readResource(uri);
|
|
255
|
-
return this.wrapResourceContent(response);
|
|
256
|
-
},
|
|
257
|
-
/**
|
|
258
|
-
* Subscribe to resource changes (placeholder for future implementation)
|
|
259
|
-
*/
|
|
260
|
-
subscribe: async (_uri) => {
|
|
261
|
-
// TODO: Implement resource subscription
|
|
262
|
-
this.log('warn', 'Resource subscription not yet implemented');
|
|
263
|
-
},
|
|
264
|
-
/**
|
|
265
|
-
* Unsubscribe from resource changes (placeholder for future implementation)
|
|
266
|
-
*/
|
|
267
|
-
unsubscribe: async (_uri) => {
|
|
268
|
-
// TODO: Implement resource unsubscription
|
|
269
|
-
this.log('warn', 'Resource unsubscription not yet implemented');
|
|
270
|
-
},
|
|
271
|
-
};
|
|
272
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
273
|
-
// PROMPTS API
|
|
274
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
275
|
-
prompts = {
|
|
276
|
-
/**
|
|
277
|
-
* List all available prompts
|
|
278
|
-
*/
|
|
279
|
-
list: async () => {
|
|
280
|
-
const response = await this.listPrompts();
|
|
281
|
-
if (!response.success || !response.data) {
|
|
282
|
-
throw new Error(`Failed to list prompts: ${response.error?.message}`);
|
|
283
|
-
}
|
|
284
|
-
return response.data.prompts;
|
|
285
|
-
},
|
|
286
|
-
/**
|
|
287
|
-
* Get a prompt with arguments
|
|
288
|
-
*/
|
|
289
|
-
get: async (name, args) => {
|
|
290
|
-
const response = await this.getPrompt(name, args);
|
|
291
|
-
return this.wrapPromptResult(response);
|
|
292
|
-
},
|
|
293
|
-
};
|
|
294
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
295
|
-
// RAW PROTOCOL ACCESS
|
|
296
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
297
|
-
raw = {
|
|
298
|
-
/**
|
|
299
|
-
* Send any JSON-RPC request
|
|
300
|
-
*/
|
|
301
|
-
request: async (message) => {
|
|
302
|
-
this.ensureConnected();
|
|
303
|
-
const start = Date.now();
|
|
304
|
-
const response = await this.transport.request(message);
|
|
305
|
-
this.traceRequest(message.method, message.params, message.id, response, Date.now() - start);
|
|
306
|
-
return response;
|
|
307
|
-
},
|
|
308
|
-
/**
|
|
309
|
-
* Send a notification (no response expected)
|
|
310
|
-
*/
|
|
311
|
-
notify: async (message) => {
|
|
312
|
-
this.ensureConnected();
|
|
313
|
-
await this.transport.notify(message);
|
|
314
|
-
},
|
|
315
|
-
/**
|
|
316
|
-
* Send raw string data (for error testing)
|
|
317
|
-
*/
|
|
318
|
-
sendRaw: async (data) => {
|
|
319
|
-
this.ensureConnected();
|
|
320
|
-
return this.transport.sendRaw(data);
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
get lastRequestId() {
|
|
324
|
-
return this._lastRequestId;
|
|
325
|
-
}
|
|
326
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
327
|
-
// TRANSPORT INFO
|
|
328
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
329
|
-
/**
|
|
330
|
-
* Get transport information and utilities
|
|
331
|
-
*/
|
|
332
|
-
get transport_info() {
|
|
333
|
-
return {
|
|
334
|
-
type: this.config.transport,
|
|
335
|
-
isConnected: () => this.transport?.isConnected() ?? false,
|
|
336
|
-
messageEndpoint: this.transport?.getMessageEndpoint?.(),
|
|
337
|
-
connectionCount: this.transport?.getConnectionCount?.() ?? 0,
|
|
338
|
-
reconnectCount: this.transport?.getReconnectCount?.() ?? 0,
|
|
339
|
-
lastRequestHeaders: this.transport?.getLastRequestHeaders?.() ?? {},
|
|
340
|
-
simulateDisconnect: async () => {
|
|
341
|
-
await this.transport?.simulateDisconnect?.();
|
|
342
|
-
},
|
|
343
|
-
waitForReconnect: async (timeoutMs) => {
|
|
344
|
-
await this.transport?.waitForReconnect?.(timeoutMs);
|
|
345
|
-
},
|
|
346
|
-
};
|
|
347
|
-
}
|
|
348
|
-
// Alias for transport info
|
|
349
|
-
get transport_() {
|
|
350
|
-
return this.transport_info;
|
|
351
|
-
}
|
|
352
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
353
|
-
// NOTIFICATIONS
|
|
354
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
355
|
-
notifications = {
|
|
356
|
-
/**
|
|
357
|
-
* Start collecting server notifications
|
|
358
|
-
*/
|
|
359
|
-
collect: () => {
|
|
360
|
-
return new NotificationCollector(this._notifications);
|
|
361
|
-
},
|
|
362
|
-
/**
|
|
363
|
-
* Collect progress notifications specifically
|
|
364
|
-
*/
|
|
365
|
-
collectProgress: () => {
|
|
366
|
-
return new ProgressCollector(this._progressUpdates);
|
|
367
|
-
},
|
|
368
|
-
/**
|
|
369
|
-
* Send a notification to the server
|
|
370
|
-
*/
|
|
371
|
-
send: async (method, params) => {
|
|
372
|
-
await this.raw.notify({ jsonrpc: '2.0', method, params });
|
|
373
|
-
},
|
|
374
|
-
};
|
|
375
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
376
|
-
// LOGGING & DEBUGGING
|
|
377
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
378
|
-
logs = {
|
|
379
|
-
all: () => [...this._logs],
|
|
380
|
-
filter: (level) => this._logs.filter((l) => l.level === level),
|
|
381
|
-
search: (text) => this._logs.filter((l) => l.message.includes(text)),
|
|
382
|
-
last: () => this._logs[this._logs.length - 1],
|
|
383
|
-
clear: () => {
|
|
384
|
-
this._logs = [];
|
|
385
|
-
},
|
|
386
|
-
};
|
|
387
|
-
trace = {
|
|
388
|
-
all: () => [...this._traces],
|
|
389
|
-
last: () => this._traces[this._traces.length - 1],
|
|
390
|
-
clear: () => {
|
|
391
|
-
this._traces = [];
|
|
392
|
-
},
|
|
393
|
-
};
|
|
394
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
395
|
-
// MOCKING & INTERCEPTION
|
|
396
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
397
|
-
/**
|
|
398
|
-
* API for mocking MCP requests
|
|
399
|
-
*
|
|
400
|
-
* @example
|
|
401
|
-
* ```typescript
|
|
402
|
-
* // Mock a specific tool call
|
|
403
|
-
* const handle = mcp.mock.tool('my-tool', { result: 'mocked!' });
|
|
404
|
-
*
|
|
405
|
-
* // Mock with params matching
|
|
406
|
-
* mcp.mock.add({
|
|
407
|
-
* method: 'tools/call',
|
|
408
|
-
* params: { name: 'my-tool' },
|
|
409
|
-
* response: mockResponse.toolResult([{ type: 'text', text: 'mocked' }]),
|
|
410
|
-
* });
|
|
411
|
-
*
|
|
412
|
-
* // Clear all mocks after test
|
|
413
|
-
* mcp.mock.clear();
|
|
414
|
-
* ```
|
|
415
|
-
*/
|
|
416
|
-
mock = {
|
|
417
|
-
/**
|
|
418
|
-
* Add a mock definition
|
|
419
|
-
*/
|
|
420
|
-
add: (mock) => {
|
|
421
|
-
return this._interceptors.mocks.add(mock);
|
|
422
|
-
},
|
|
423
|
-
/**
|
|
424
|
-
* Mock a tools/call request for a specific tool
|
|
425
|
-
*/
|
|
426
|
-
tool: (name, result, options) => {
|
|
427
|
-
return this._interceptors.mocks.add({
|
|
428
|
-
method: 'tools/call',
|
|
429
|
-
params: { name },
|
|
430
|
-
response: interceptor_1.mockResponse.toolResult([
|
|
431
|
-
{ type: 'text', text: typeof result === 'string' ? result : JSON.stringify(result) },
|
|
432
|
-
]),
|
|
433
|
-
times: options?.times,
|
|
434
|
-
delay: options?.delay,
|
|
435
|
-
});
|
|
436
|
-
},
|
|
437
|
-
/**
|
|
438
|
-
* Mock a tools/call request to return an error
|
|
439
|
-
*/
|
|
440
|
-
toolError: (name, code, message, options) => {
|
|
441
|
-
return this._interceptors.mocks.add({
|
|
442
|
-
method: 'tools/call',
|
|
443
|
-
params: { name },
|
|
444
|
-
response: interceptor_1.mockResponse.error(code, message),
|
|
445
|
-
times: options?.times,
|
|
446
|
-
});
|
|
447
|
-
},
|
|
448
|
-
/**
|
|
449
|
-
* Mock a resources/read request
|
|
450
|
-
*/
|
|
451
|
-
resource: (uri, content, options) => {
|
|
452
|
-
const contentObj = typeof content === 'string' ? { uri, text: content } : { uri, ...content };
|
|
453
|
-
return this._interceptors.mocks.add({
|
|
454
|
-
method: 'resources/read',
|
|
455
|
-
params: { uri },
|
|
456
|
-
response: interceptor_1.mockResponse.resourceRead([contentObj]),
|
|
457
|
-
times: options?.times,
|
|
458
|
-
delay: options?.delay,
|
|
459
|
-
});
|
|
460
|
-
},
|
|
461
|
-
/**
|
|
462
|
-
* Mock a resources/read request to return an error
|
|
463
|
-
*/
|
|
464
|
-
resourceError: (uri, options) => {
|
|
465
|
-
return this._interceptors.mocks.add({
|
|
466
|
-
method: 'resources/read',
|
|
467
|
-
params: { uri },
|
|
468
|
-
response: interceptor_1.mockResponse.errors.resourceNotFound(uri),
|
|
469
|
-
times: options?.times,
|
|
470
|
-
});
|
|
471
|
-
},
|
|
472
|
-
/**
|
|
473
|
-
* Mock the tools/list response
|
|
474
|
-
*/
|
|
475
|
-
toolsList: (tools, options) => {
|
|
476
|
-
return this._interceptors.mocks.add({
|
|
477
|
-
method: 'tools/list',
|
|
478
|
-
response: interceptor_1.mockResponse.toolsList(tools),
|
|
479
|
-
times: options?.times,
|
|
480
|
-
});
|
|
481
|
-
},
|
|
482
|
-
/**
|
|
483
|
-
* Mock the resources/list response
|
|
484
|
-
*/
|
|
485
|
-
resourcesList: (resources, options) => {
|
|
486
|
-
return this._interceptors.mocks.add({
|
|
487
|
-
method: 'resources/list',
|
|
488
|
-
response: interceptor_1.mockResponse.resourcesList(resources),
|
|
489
|
-
times: options?.times,
|
|
490
|
-
});
|
|
491
|
-
},
|
|
492
|
-
/**
|
|
493
|
-
* Clear all mocks
|
|
494
|
-
*/
|
|
495
|
-
clear: () => {
|
|
496
|
-
this._interceptors.mocks.clear();
|
|
497
|
-
},
|
|
498
|
-
/**
|
|
499
|
-
* Get all active mocks
|
|
500
|
-
*/
|
|
501
|
-
all: () => {
|
|
502
|
-
return this._interceptors.mocks.getAll();
|
|
503
|
-
},
|
|
504
|
-
};
|
|
505
|
-
/**
|
|
506
|
-
* API for intercepting requests and responses
|
|
507
|
-
*
|
|
508
|
-
* @example
|
|
509
|
-
* ```typescript
|
|
510
|
-
* // Log all requests
|
|
511
|
-
* const remove = mcp.intercept.request((ctx) => {
|
|
512
|
-
* console.log('Request:', ctx.request.method);
|
|
513
|
-
* return { action: 'passthrough' };
|
|
514
|
-
* });
|
|
515
|
-
*
|
|
516
|
-
* // Modify requests
|
|
517
|
-
* mcp.intercept.request((ctx) => {
|
|
518
|
-
* if (ctx.request.method === 'tools/call') {
|
|
519
|
-
* return {
|
|
520
|
-
* action: 'modify',
|
|
521
|
-
* request: { ...ctx.request, params: { ...ctx.request.params, extra: true } },
|
|
522
|
-
* };
|
|
523
|
-
* }
|
|
524
|
-
* return { action: 'passthrough' };
|
|
525
|
-
* });
|
|
526
|
-
*
|
|
527
|
-
* // Add latency to all requests
|
|
528
|
-
* mcp.intercept.delay(100);
|
|
529
|
-
*
|
|
530
|
-
* // Clean up
|
|
531
|
-
* remove();
|
|
532
|
-
* mcp.intercept.clear();
|
|
533
|
-
* ```
|
|
534
|
-
*/
|
|
535
|
-
intercept = {
|
|
536
|
-
/**
|
|
537
|
-
* Add a request interceptor
|
|
538
|
-
* @returns Function to remove the interceptor
|
|
539
|
-
*/
|
|
540
|
-
request: (interceptor) => {
|
|
541
|
-
return this._interceptors.addRequestInterceptor(interceptor);
|
|
542
|
-
},
|
|
543
|
-
/**
|
|
544
|
-
* Add a response interceptor
|
|
545
|
-
* @returns Function to remove the interceptor
|
|
546
|
-
*/
|
|
547
|
-
response: (interceptor) => {
|
|
548
|
-
return this._interceptors.addResponseInterceptor(interceptor);
|
|
549
|
-
},
|
|
550
|
-
/**
|
|
551
|
-
* Add latency to all requests
|
|
552
|
-
* @returns Function to remove the interceptor
|
|
553
|
-
*/
|
|
554
|
-
delay: (ms) => {
|
|
555
|
-
return this._interceptors.addRequestInterceptor(async () => {
|
|
556
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
557
|
-
return { action: 'passthrough' };
|
|
558
|
-
});
|
|
559
|
-
},
|
|
560
|
-
/**
|
|
561
|
-
* Fail requests matching a method
|
|
562
|
-
* @returns Function to remove the interceptor
|
|
563
|
-
*/
|
|
564
|
-
failMethod: (method, error) => {
|
|
565
|
-
return this._interceptors.addRequestInterceptor((ctx) => {
|
|
566
|
-
if (ctx.request.method === method) {
|
|
567
|
-
return { action: 'error', error: new Error(error ?? `Intercepted: ${method}`) };
|
|
568
|
-
}
|
|
569
|
-
return { action: 'passthrough' };
|
|
570
|
-
});
|
|
571
|
-
},
|
|
572
|
-
/**
|
|
573
|
-
* Clear all interceptors (but not mocks)
|
|
574
|
-
*/
|
|
575
|
-
clear: () => {
|
|
576
|
-
this._interceptors.request = [];
|
|
577
|
-
this._interceptors.response = [];
|
|
578
|
-
},
|
|
579
|
-
/**
|
|
580
|
-
* Clear everything (interceptors and mocks)
|
|
581
|
-
*/
|
|
582
|
-
clearAll: () => {
|
|
583
|
-
this._interceptors.clear();
|
|
584
|
-
},
|
|
585
|
-
};
|
|
586
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
587
|
-
// TIMEOUT
|
|
588
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
589
|
-
setTimeout(ms) {
|
|
590
|
-
this.config.timeout = ms;
|
|
591
|
-
if (this.transport) {
|
|
592
|
-
this.transport.setTimeout(ms);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
596
|
-
// PRIVATE: MCP OPERATIONS
|
|
597
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
598
|
-
async initialize() {
|
|
599
|
-
// Use configured capabilities or default to base capabilities
|
|
600
|
-
const capabilities = this.config.capabilities ?? {
|
|
601
|
-
sampling: {},
|
|
602
|
-
};
|
|
603
|
-
return this.request('initialize', {
|
|
604
|
-
protocolVersion: this.config.protocolVersion,
|
|
605
|
-
capabilities,
|
|
606
|
-
clientInfo: this.config.clientInfo,
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
async listTools() {
|
|
610
|
-
return this.request('tools/list', {});
|
|
611
|
-
}
|
|
612
|
-
async callTool(name, args) {
|
|
613
|
-
return this.request('tools/call', {
|
|
614
|
-
name,
|
|
615
|
-
arguments: args ?? {},
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
async listResources() {
|
|
619
|
-
return this.request('resources/list', {});
|
|
620
|
-
}
|
|
621
|
-
async listResourceTemplates() {
|
|
622
|
-
return this.request('resources/templates/list', {});
|
|
623
|
-
}
|
|
624
|
-
async readResource(uri) {
|
|
625
|
-
return this.request('resources/read', { uri });
|
|
626
|
-
}
|
|
627
|
-
async listPrompts() {
|
|
628
|
-
return this.request('prompts/list', {});
|
|
629
|
-
}
|
|
630
|
-
async getPrompt(name, args) {
|
|
631
|
-
return this.request('prompts/get', {
|
|
632
|
-
name,
|
|
633
|
-
arguments: args ?? {},
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
637
|
-
// PRIVATE: TRANSPORT & REQUEST HELPERS
|
|
638
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
639
|
-
createTransport() {
|
|
640
|
-
switch (this.config.transport) {
|
|
641
|
-
case 'streamable-http':
|
|
642
|
-
return new streamable_http_transport_1.StreamableHttpTransport({
|
|
643
|
-
baseUrl: this.config.baseUrl,
|
|
644
|
-
timeout: this.config.timeout,
|
|
645
|
-
auth: this.config.auth,
|
|
646
|
-
publicMode: this.config.publicMode,
|
|
647
|
-
debug: this.config.debug,
|
|
648
|
-
interceptors: this._interceptors,
|
|
649
|
-
clientInfo: this.config.clientInfo,
|
|
650
|
-
});
|
|
651
|
-
case 'sse':
|
|
652
|
-
// TODO: Implement SSE transport
|
|
653
|
-
throw new Error('SSE transport not yet implemented');
|
|
654
|
-
default:
|
|
655
|
-
throw new Error(`Unknown transport type: ${this.config.transport}`);
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
async request(method, params) {
|
|
659
|
-
this.ensureConnected();
|
|
660
|
-
const id = ++this.requestIdCounter;
|
|
661
|
-
this._lastRequestId = id;
|
|
662
|
-
const start = Date.now();
|
|
663
|
-
try {
|
|
664
|
-
const response = await this.transport.request({
|
|
665
|
-
jsonrpc: '2.0',
|
|
666
|
-
id,
|
|
667
|
-
method,
|
|
668
|
-
params,
|
|
669
|
-
});
|
|
670
|
-
const durationMs = Date.now() - start;
|
|
671
|
-
this.updateSessionActivity();
|
|
672
|
-
if ('error' in response && response.error) {
|
|
673
|
-
const error = response.error;
|
|
674
|
-
this.traceRequest(method, params, id, response, durationMs);
|
|
675
|
-
return {
|
|
676
|
-
success: false,
|
|
677
|
-
error,
|
|
678
|
-
durationMs,
|
|
679
|
-
requestId: id,
|
|
680
|
-
};
|
|
681
|
-
}
|
|
682
|
-
this.traceRequest(method, params, id, response, durationMs);
|
|
683
|
-
return {
|
|
684
|
-
success: true,
|
|
685
|
-
data: response.result,
|
|
686
|
-
durationMs,
|
|
687
|
-
requestId: id,
|
|
688
|
-
};
|
|
689
|
-
}
|
|
690
|
-
catch (err) {
|
|
691
|
-
const durationMs = Date.now() - start;
|
|
692
|
-
const error = {
|
|
693
|
-
code: -32603,
|
|
694
|
-
message: err instanceof Error ? err.message : 'Unknown error',
|
|
695
|
-
};
|
|
696
|
-
return {
|
|
697
|
-
success: false,
|
|
698
|
-
error,
|
|
699
|
-
durationMs,
|
|
700
|
-
requestId: id,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
ensureConnected() {
|
|
705
|
-
if (!this.transport?.isConnected()) {
|
|
706
|
-
throw new Error('Not connected to MCP server. Call connect() first.');
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
updateSessionActivity() {
|
|
710
|
-
if (this._sessionInfo) {
|
|
711
|
-
this._sessionInfo.lastActivityAt = new Date();
|
|
712
|
-
this._sessionInfo.requestCount++;
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
716
|
-
// PRIVATE: RESULT WRAPPERS
|
|
717
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
718
|
-
wrapToolResult(response) {
|
|
719
|
-
const raw = response.data ?? { content: [] };
|
|
720
|
-
const isError = !response.success || raw.isError === true;
|
|
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)
|
|
726
|
-
const meta = raw._meta;
|
|
727
|
-
const hasUI = meta?.['ui/html'] !== undefined ||
|
|
728
|
-
meta?.['ui/component'] !== undefined ||
|
|
729
|
-
meta?.['openai/html'] !== undefined ||
|
|
730
|
-
meta?.['frontmcp/html'] !== undefined;
|
|
731
|
-
const structuredContent = raw['structuredContent'];
|
|
732
|
-
return {
|
|
733
|
-
raw,
|
|
734
|
-
isSuccess: !isError,
|
|
735
|
-
isError,
|
|
736
|
-
error: response.error,
|
|
737
|
-
durationMs: response.durationMs,
|
|
738
|
-
json() {
|
|
739
|
-
// For Tool UI responses, return structuredContent (the typed output)
|
|
740
|
-
if (hasUI && structuredContent !== undefined) {
|
|
741
|
-
return structuredContent;
|
|
742
|
-
}
|
|
743
|
-
// For regular responses, parse text content as JSON
|
|
744
|
-
const textContent = raw.content?.find((c) => c.type === 'text');
|
|
745
|
-
if (textContent && 'text' in textContent) {
|
|
746
|
-
return JSON.parse(textContent.text);
|
|
747
|
-
}
|
|
748
|
-
throw new Error('No text content to parse as JSON');
|
|
749
|
-
},
|
|
750
|
-
text() {
|
|
751
|
-
const textContent = raw.content?.find((c) => c.type === 'text');
|
|
752
|
-
if (textContent && 'text' in textContent) {
|
|
753
|
-
return textContent.text;
|
|
754
|
-
}
|
|
755
|
-
return undefined;
|
|
756
|
-
},
|
|
757
|
-
hasTextContent() {
|
|
758
|
-
return raw.content?.some((c) => c.type === 'text') ?? false;
|
|
759
|
-
},
|
|
760
|
-
hasImageContent() {
|
|
761
|
-
return raw.content?.some((c) => c.type === 'image') ?? false;
|
|
762
|
-
},
|
|
763
|
-
hasResourceContent() {
|
|
764
|
-
return raw.content?.some((c) => c.type === 'resource') ?? false;
|
|
765
|
-
},
|
|
766
|
-
hasToolUI() {
|
|
767
|
-
return hasUI;
|
|
768
|
-
},
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
wrapResourceContent(response) {
|
|
772
|
-
const raw = response.data ?? { contents: [] };
|
|
773
|
-
const isError = !response.success;
|
|
774
|
-
const firstContent = raw.contents?.[0];
|
|
775
|
-
return {
|
|
776
|
-
raw,
|
|
777
|
-
isSuccess: !isError,
|
|
778
|
-
isError,
|
|
779
|
-
error: response.error,
|
|
780
|
-
durationMs: response.durationMs,
|
|
781
|
-
json() {
|
|
782
|
-
if (firstContent && 'text' in firstContent) {
|
|
783
|
-
return JSON.parse(firstContent.text);
|
|
784
|
-
}
|
|
785
|
-
throw new Error('No text content to parse as JSON');
|
|
786
|
-
},
|
|
787
|
-
text() {
|
|
788
|
-
if (firstContent && 'text' in firstContent) {
|
|
789
|
-
return firstContent.text;
|
|
790
|
-
}
|
|
791
|
-
return undefined;
|
|
792
|
-
},
|
|
793
|
-
mimeType() {
|
|
794
|
-
return firstContent?.mimeType;
|
|
795
|
-
},
|
|
796
|
-
hasMimeType(type) {
|
|
797
|
-
return firstContent?.mimeType === type;
|
|
798
|
-
},
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
wrapPromptResult(response) {
|
|
802
|
-
const raw = response.data ?? { messages: [] };
|
|
803
|
-
const isError = !response.success;
|
|
804
|
-
return {
|
|
805
|
-
raw,
|
|
806
|
-
isSuccess: !isError,
|
|
807
|
-
isError,
|
|
808
|
-
error: response.error,
|
|
809
|
-
durationMs: response.durationMs,
|
|
810
|
-
messages: raw.messages ?? [],
|
|
811
|
-
description: raw.description,
|
|
812
|
-
};
|
|
813
|
-
}
|
|
814
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
815
|
-
// PRIVATE: LOGGING & TRACING
|
|
816
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
817
|
-
log(level, message, data) {
|
|
818
|
-
const entry = {
|
|
819
|
-
level,
|
|
820
|
-
message,
|
|
821
|
-
timestamp: new Date(),
|
|
822
|
-
data,
|
|
823
|
-
};
|
|
824
|
-
this._logs.push(entry);
|
|
825
|
-
if (this.config.debug) {
|
|
826
|
-
console.log(`[${level.toUpperCase()}] ${message}`, data ?? '');
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
traceRequest(method, params, id, response, durationMs) {
|
|
830
|
-
this._traces.push({
|
|
831
|
-
request: { method, params, id },
|
|
832
|
-
response: {
|
|
833
|
-
result: 'result' in response ? response.result : undefined,
|
|
834
|
-
error: 'error' in response ? response.error : undefined,
|
|
835
|
-
},
|
|
836
|
-
durationMs,
|
|
837
|
-
timestamp: new Date(),
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
841
|
-
// PRIVATE: TOKEN PARSING
|
|
842
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
843
|
-
parseScopesFromToken(token) {
|
|
844
|
-
try {
|
|
845
|
-
const payload = this.decodeJwtPayload(token);
|
|
846
|
-
if (!payload)
|
|
847
|
-
return [];
|
|
848
|
-
const scope = payload['scope'];
|
|
849
|
-
const scopes = payload['scopes'];
|
|
850
|
-
if (typeof scope === 'string') {
|
|
851
|
-
return scope.split(' ');
|
|
852
|
-
}
|
|
853
|
-
if (Array.isArray(scopes)) {
|
|
854
|
-
return scopes;
|
|
855
|
-
}
|
|
856
|
-
return [];
|
|
857
|
-
}
|
|
858
|
-
catch {
|
|
859
|
-
return [];
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
parseUserFromToken(token) {
|
|
863
|
-
try {
|
|
864
|
-
const payload = this.decodeJwtPayload(token);
|
|
865
|
-
const sub = payload?.['sub'];
|
|
866
|
-
if (!sub || typeof sub !== 'string')
|
|
867
|
-
return undefined;
|
|
868
|
-
return {
|
|
869
|
-
sub,
|
|
870
|
-
email: payload['email'],
|
|
871
|
-
name: payload['name'],
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
catch {
|
|
875
|
-
return undefined;
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
decodeJwtPayload(token) {
|
|
879
|
-
try {
|
|
880
|
-
const parts = token.split('.');
|
|
881
|
-
if (parts.length !== 3)
|
|
882
|
-
return null;
|
|
883
|
-
const payload = Buffer.from(parts[1], 'base64url').toString('utf-8');
|
|
884
|
-
return JSON.parse(payload);
|
|
885
|
-
}
|
|
886
|
-
catch {
|
|
887
|
-
return null;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
exports.McpTestClient = McpTestClient;
|
|
892
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
893
|
-
// NOTIFICATION COLLECTORS
|
|
894
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
895
|
-
class NotificationCollector {
|
|
896
|
-
notifications;
|
|
897
|
-
constructor(notifications) {
|
|
898
|
-
this.notifications = notifications;
|
|
899
|
-
}
|
|
900
|
-
get received() {
|
|
901
|
-
return [...this.notifications];
|
|
902
|
-
}
|
|
903
|
-
has(method) {
|
|
904
|
-
return this.notifications.some((n) => n.method === method);
|
|
905
|
-
}
|
|
906
|
-
async waitFor(method, timeoutMs) {
|
|
907
|
-
const deadline = Date.now() + timeoutMs;
|
|
908
|
-
while (Date.now() < deadline) {
|
|
909
|
-
const found = this.notifications.find((n) => n.method === method);
|
|
910
|
-
if (found)
|
|
911
|
-
return found;
|
|
912
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
913
|
-
}
|
|
914
|
-
throw new Error(`Timeout waiting for notification: ${method}`);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
class ProgressCollector {
|
|
918
|
-
updates;
|
|
919
|
-
constructor(updates) {
|
|
920
|
-
this.updates = updates;
|
|
921
|
-
}
|
|
922
|
-
get all() {
|
|
923
|
-
return [...this.updates];
|
|
924
|
-
}
|
|
925
|
-
async waitForComplete(timeoutMs) {
|
|
926
|
-
const deadline = Date.now() + timeoutMs;
|
|
927
|
-
while (Date.now() < deadline) {
|
|
928
|
-
const last = this.updates[this.updates.length - 1];
|
|
929
|
-
if (last && last.total !== undefined && last.progress >= last.total) {
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
932
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
933
|
-
}
|
|
934
|
-
throw new Error('Timeout waiting for progress to complete');
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
//# sourceMappingURL=mcp-test-client.js.map
|