@frontmcp/testing 0.8.1 → 0.9.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/README.md +50 -1304
- package/esm/package.json +4 -4
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,78 +1,23 @@
|
|
|
1
1
|
# @frontmcp/testing
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
>
|
|
5
|
-
> The official testing library for FrontMCP - providing a complete toolkit for end-to-end testing of MCP servers including tools, resources, prompts, authentication, plugins, adapters, and the full MCP protocol.
|
|
3
|
+
E2E testing framework for FrontMCP servers.
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@frontmcp/testing)
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
- [Quick Start](#quick-start)
|
|
11
|
-
- [Core Concepts](#core-concepts)
|
|
12
|
-
- [API Reference](#api-reference)
|
|
13
|
-
- [Test Runner](#test-runner)
|
|
14
|
-
- [MCP Client Fixture](#mcp-client-fixture)
|
|
15
|
-
- [Server Fixture](#server-fixture)
|
|
16
|
-
- [Auth Fixture](#auth-fixture)
|
|
17
|
-
- [Custom Matchers](#custom-matchers)
|
|
18
|
-
- [Testing Guide](#testing-guide)
|
|
19
|
-
- [Tools](#testing-tools)
|
|
20
|
-
- [Tool UI](#testing-tool-ui)
|
|
21
|
-
- [Resources](#testing-resources)
|
|
22
|
-
- [Prompts](#testing-prompts)
|
|
23
|
-
- [Authentication](#testing-authentication)
|
|
24
|
-
- [Transports](#testing-transports)
|
|
25
|
-
- [Notifications](#testing-notifications)
|
|
26
|
-
- [Logging & Debugging](#logging--debugging)
|
|
27
|
-
- [Plugins](#testing-plugins)
|
|
28
|
-
- [Adapters](#testing-adapters)
|
|
29
|
-
- [Raw Protocol](#raw-protocol-access)
|
|
30
|
-
- [Configuration](#configuration)
|
|
31
|
-
- [Best Practices](#best-practices)
|
|
32
|
-
- [Troubleshooting](#troubleshooting)
|
|
33
|
-
|
|
34
|
-
---
|
|
35
|
-
|
|
36
|
-
## Installation
|
|
7
|
+
## Install
|
|
37
8
|
|
|
38
9
|
```bash
|
|
39
|
-
# npm
|
|
40
10
|
npm install -D @frontmcp/testing
|
|
41
|
-
|
|
42
|
-
# yarn
|
|
43
|
-
yarn add -D @frontmcp/testing
|
|
44
|
-
|
|
45
|
-
# pnpm
|
|
46
|
-
pnpm add -D @frontmcp/testing
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
### Peer Dependencies
|
|
50
|
-
|
|
51
|
-
```json
|
|
52
|
-
{
|
|
53
|
-
"peerDependencies": {
|
|
54
|
-
"@frontmcp/sdk": "^0.4.0",
|
|
55
|
-
"jest": "^29.0.0",
|
|
56
|
-
"@jest/globals": "^29.0.0",
|
|
57
|
-
"@playwright/test": "^1.40.0"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
11
|
```
|
|
61
12
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
---
|
|
13
|
+
Peer dependencies: `@frontmcp/sdk`, `jest`, `@jest/globals`. Optional: `@playwright/test` (for browser OAuth flow testing).
|
|
65
14
|
|
|
66
15
|
## Quick Start
|
|
67
16
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
// my-server.e2e.ts
|
|
17
|
+
```ts
|
|
72
18
|
import { test, expect } from '@frontmcp/testing';
|
|
73
19
|
import MyServer from './src/main';
|
|
74
20
|
|
|
75
|
-
// Pass your FrontMCP server class
|
|
76
21
|
test.use({ server: MyServer });
|
|
77
22
|
|
|
78
23
|
test('server exposes tools', async ({ mcp }) => {
|
|
@@ -86,1273 +31,74 @@ test('tool execution works', async ({ mcp }) => {
|
|
|
86
31
|
});
|
|
87
32
|
```
|
|
88
33
|
|
|
89
|
-
### 2. Run tests
|
|
90
|
-
|
|
91
34
|
```bash
|
|
92
|
-
# Using frontmcp CLI
|
|
93
35
|
npx frontmcp test
|
|
94
|
-
|
|
95
|
-
# Using nx
|
|
96
|
-
nx e2e my-app
|
|
97
|
-
|
|
98
|
-
# Using jest directly
|
|
99
|
-
jest --config jest.e2e.config.ts
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
That's it! The library handles:
|
|
103
|
-
|
|
104
|
-
- Starting your server on an available port
|
|
105
|
-
- Connecting an MCP client
|
|
106
|
-
- Running your tests
|
|
107
|
-
- Cleanup after tests complete
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## Core Concepts
|
|
112
|
-
|
|
113
|
-
### Fixtures
|
|
114
|
-
|
|
115
|
-
`@frontmcp/testing` provides several fixtures that are automatically available in your tests:
|
|
116
|
-
|
|
117
|
-
| Fixture | Description |
|
|
118
|
-
| -------- | --------------------------------------------- |
|
|
119
|
-
| `mcp` | Auto-connected MCP client for making requests |
|
|
120
|
-
| `server` | Server instance with control methods |
|
|
121
|
-
| `auth` | Token factory for authentication testing |
|
|
122
|
-
|
|
123
|
-
### Test Configuration
|
|
124
|
-
|
|
125
|
-
Configure tests using `test.use()`:
|
|
126
|
-
|
|
127
|
-
```typescript
|
|
128
|
-
test.use({
|
|
129
|
-
server: MyServer, // Required: Your FrontMCP server class
|
|
130
|
-
port: 3003, // Optional: Specific port (default: auto)
|
|
131
|
-
transport: 'streamable-http', // Optional: 'sse' | 'streamable-http'
|
|
132
|
-
auth: { mode: 'public' }, // Optional: Override auth config
|
|
133
|
-
logLevel: 'debug', // Optional: Server log level
|
|
134
|
-
env: { API_KEY: 'test' }, // Optional: Environment variables
|
|
135
|
-
});
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
## API Reference
|
|
141
|
-
|
|
142
|
-
### Test Runner
|
|
143
|
-
|
|
144
|
-
```typescript
|
|
145
|
-
import { test, expect } from '@frontmcp/testing';
|
|
146
|
-
|
|
147
|
-
// Define test suite
|
|
148
|
-
test.describe('My Feature', () => {
|
|
149
|
-
// Configure for this suite
|
|
150
|
-
test.use({ server: MyServer });
|
|
151
|
-
|
|
152
|
-
// Setup/teardown
|
|
153
|
-
test.beforeAll(async ({ server }) => {
|
|
154
|
-
/* ... */
|
|
155
|
-
});
|
|
156
|
-
test.beforeEach(async ({ mcp }) => {
|
|
157
|
-
/* ... */
|
|
158
|
-
});
|
|
159
|
-
test.afterEach(async ({ mcp }) => {
|
|
160
|
-
/* ... */
|
|
161
|
-
});
|
|
162
|
-
test.afterAll(async ({ server }) => {
|
|
163
|
-
/* ... */
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
// Tests
|
|
167
|
-
test('test case', async ({ mcp }) => {
|
|
168
|
-
/* ... */
|
|
169
|
-
});
|
|
170
|
-
test.skip('skipped test', async ({ mcp }) => {
|
|
171
|
-
/* ... */
|
|
172
|
-
});
|
|
173
|
-
test.only('focused test', async ({ mcp }) => {
|
|
174
|
-
/* ... */
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### MCP Client Fixture
|
|
180
|
-
|
|
181
|
-
The `mcp` fixture is your primary interface for testing:
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
interface McpTestClient {
|
|
185
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
186
|
-
// CONNECTION & SESSION
|
|
187
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
188
|
-
|
|
189
|
-
isConnected(): boolean;
|
|
190
|
-
sessionId: string;
|
|
191
|
-
|
|
192
|
-
disconnect(): Promise<void>;
|
|
193
|
-
reconnect(options?: { sessionId?: string }): Promise<void>;
|
|
194
|
-
|
|
195
|
-
session: {
|
|
196
|
-
createdAt: Date;
|
|
197
|
-
lastActivityAt: Date;
|
|
198
|
-
requestCount: number;
|
|
199
|
-
expire(): Promise<void>; // Force session expiration (testing)
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
203
|
-
// SERVER INFO & CAPABILITIES
|
|
204
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
205
|
-
|
|
206
|
-
serverInfo: {
|
|
207
|
-
name: string;
|
|
208
|
-
version: string;
|
|
209
|
-
title?: string;
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
protocolVersion: string; // e.g., '2024-11-05'
|
|
213
|
-
instructions: string; // Server instructions text
|
|
214
|
-
|
|
215
|
-
capabilities: {
|
|
216
|
-
tools?: { listChanged?: boolean };
|
|
217
|
-
resources?: { subscribe?: boolean; listChanged?: boolean };
|
|
218
|
-
prompts?: { listChanged?: boolean };
|
|
219
|
-
logging?: object;
|
|
220
|
-
sampling?: object;
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
hasCapability(name: 'tools' | 'resources' | 'prompts' | 'logging' | 'sampling'): boolean;
|
|
224
|
-
|
|
225
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
226
|
-
// TOOLS
|
|
227
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
228
|
-
|
|
229
|
-
tools: {
|
|
230
|
-
/** List all available tools */
|
|
231
|
-
list(): Promise<Tool[]>;
|
|
232
|
-
|
|
233
|
-
/** Call a tool by name with arguments */
|
|
234
|
-
call(name: string, args?: Record<string, unknown>): Promise<ToolResult>;
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
238
|
-
// RESOURCES
|
|
239
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
240
|
-
|
|
241
|
-
resources: {
|
|
242
|
-
/** List all static resources */
|
|
243
|
-
list(): Promise<Resource[]>;
|
|
244
|
-
|
|
245
|
-
/** List all resource templates */
|
|
246
|
-
listTemplates(): Promise<ResourceTemplate[]>;
|
|
247
|
-
|
|
248
|
-
/** Read a resource by URI */
|
|
249
|
-
read(uri: string): Promise<ResourceContent>;
|
|
250
|
-
|
|
251
|
-
/** Subscribe to resource changes (if supported) */
|
|
252
|
-
subscribe(uri: string): Promise<void>;
|
|
253
|
-
unsubscribe(uri: string): Promise<void>;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
257
|
-
// PROMPTS
|
|
258
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
259
|
-
|
|
260
|
-
prompts: {
|
|
261
|
-
/** List all available prompts */
|
|
262
|
-
list(): Promise<Prompt[]>;
|
|
263
|
-
|
|
264
|
-
/** Get a prompt with arguments */
|
|
265
|
-
get(name: string, args?: Record<string, string>): Promise<PromptResult>;
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
269
|
-
// RAW PROTOCOL ACCESS
|
|
270
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
271
|
-
|
|
272
|
-
raw: {
|
|
273
|
-
/** Send any JSON-RPC request */
|
|
274
|
-
request(message: {
|
|
275
|
-
jsonrpc: '2.0';
|
|
276
|
-
id: string | number;
|
|
277
|
-
method: string;
|
|
278
|
-
params?: unknown;
|
|
279
|
-
}): Promise<JSONRPCResponse>;
|
|
280
|
-
|
|
281
|
-
/** Send a notification (no response expected) */
|
|
282
|
-
notify(message: { jsonrpc: '2.0'; method: string; params?: unknown }): Promise<void>;
|
|
283
|
-
|
|
284
|
-
/** Send raw string data (for error testing) */
|
|
285
|
-
sendRaw(data: string): Promise<JSONRPCResponse>;
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
lastRequestId: string | number; // ID of last request sent
|
|
289
|
-
|
|
290
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
291
|
-
// TRANSPORT
|
|
292
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
293
|
-
|
|
294
|
-
transport: {
|
|
295
|
-
type: 'sse' | 'streamable-http';
|
|
296
|
-
isConnected(): boolean;
|
|
297
|
-
|
|
298
|
-
// SSE-specific
|
|
299
|
-
messageEndpoint?: string; // POST endpoint for messages
|
|
300
|
-
|
|
301
|
-
// Metrics
|
|
302
|
-
connectionCount: number; // Number of connections made
|
|
303
|
-
reconnectCount: number; // Number of reconnections
|
|
304
|
-
lastRequestHeaders: Record<string, string>;
|
|
305
|
-
|
|
306
|
-
// Testing helpers
|
|
307
|
-
simulateDisconnect(): Promise<void>;
|
|
308
|
-
waitForReconnect(timeoutMs: number): Promise<void>;
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
312
|
-
// NOTIFICATIONS
|
|
313
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
314
|
-
|
|
315
|
-
notifications: {
|
|
316
|
-
/** Start collecting server notifications */
|
|
317
|
-
collect(): NotificationCollector;
|
|
318
|
-
|
|
319
|
-
/** Collect progress notifications specifically */
|
|
320
|
-
collectProgress(): ProgressCollector;
|
|
321
|
-
|
|
322
|
-
/** Send a notification to the server */
|
|
323
|
-
send(method: string, params?: unknown): Promise<void>;
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
327
|
-
// LOGGING & DEBUGGING
|
|
328
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
329
|
-
|
|
330
|
-
logs: {
|
|
331
|
-
/** Get all captured log entries */
|
|
332
|
-
all(): LogEntry[];
|
|
333
|
-
|
|
334
|
-
/** Filter logs by level */
|
|
335
|
-
filter(level: 'debug' | 'info' | 'warn' | 'error'): LogEntry[];
|
|
336
|
-
|
|
337
|
-
/** Search logs by text */
|
|
338
|
-
search(text: string): LogEntry[];
|
|
339
|
-
|
|
340
|
-
/** Get the last log entry */
|
|
341
|
-
last(): LogEntry | undefined;
|
|
342
|
-
|
|
343
|
-
/** Clear captured logs */
|
|
344
|
-
clear(): void;
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
trace: {
|
|
348
|
-
/** Get all request/response traces */
|
|
349
|
-
all(): RequestTrace[];
|
|
350
|
-
|
|
351
|
-
/** Get the last trace */
|
|
352
|
-
last(): RequestTrace | undefined;
|
|
353
|
-
|
|
354
|
-
/** Clear traces */
|
|
355
|
-
clear(): void;
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
359
|
-
// AUTHENTICATION
|
|
360
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
361
|
-
|
|
362
|
-
auth: {
|
|
363
|
-
isAnonymous: boolean;
|
|
364
|
-
token?: string;
|
|
365
|
-
scopes: string[];
|
|
366
|
-
user?: { sub: string; email?: string; name?: string };
|
|
367
|
-
};
|
|
368
|
-
|
|
369
|
-
/** Authenticate with a token */
|
|
370
|
-
authenticate(token: string): Promise<void>;
|
|
371
|
-
|
|
372
|
-
/** Set request timeout */
|
|
373
|
-
setTimeout(ms: number): void;
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### Server Fixture
|
|
378
|
-
|
|
379
|
-
The `server` fixture provides server control:
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
interface ServerFixture {
|
|
383
|
-
/** Server information */
|
|
384
|
-
info: {
|
|
385
|
-
baseUrl: string;
|
|
386
|
-
port: number;
|
|
387
|
-
pid?: number;
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
/** Create additional MCP clients */
|
|
391
|
-
createClient(options?: {
|
|
392
|
-
transport?: 'sse' | 'streamable-http';
|
|
393
|
-
protocolVersion?: string;
|
|
394
|
-
token?: string;
|
|
395
|
-
}): Promise<McpTestClient>;
|
|
396
|
-
|
|
397
|
-
/** Register hook listener (for testing) */
|
|
398
|
-
onHook(
|
|
399
|
-
hookPath: string, // e.g., 'tools:call-tool:pre:execute'
|
|
400
|
-
callback: (ctx: HookContext) => void,
|
|
401
|
-
): void;
|
|
402
|
-
|
|
403
|
-
/** Restart the server */
|
|
404
|
-
restart(): Promise<void>;
|
|
405
|
-
|
|
406
|
-
/** Get server logs */
|
|
407
|
-
getLogs(): LogEntry[];
|
|
408
|
-
}
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### Auth Fixture
|
|
412
|
-
|
|
413
|
-
The `auth` fixture helps with authentication testing:
|
|
414
|
-
|
|
415
|
-
```typescript
|
|
416
|
-
interface AuthFixture {
|
|
417
|
-
/** Create a JWT token with claims */
|
|
418
|
-
createToken(options: {
|
|
419
|
-
sub: string;
|
|
420
|
-
scopes?: string[];
|
|
421
|
-
email?: string;
|
|
422
|
-
name?: string;
|
|
423
|
-
claims?: Record<string, unknown>;
|
|
424
|
-
expiresIn?: number; // seconds
|
|
425
|
-
}): Promise<string>;
|
|
426
|
-
|
|
427
|
-
/** Create an expired token */
|
|
428
|
-
createExpiredToken(options: { sub: string }): Promise<string>;
|
|
429
|
-
|
|
430
|
-
/** Create token with invalid signature */
|
|
431
|
-
createInvalidToken(options: { sub: string }): string;
|
|
432
|
-
|
|
433
|
-
/** Pre-built test users */
|
|
434
|
-
users: {
|
|
435
|
-
admin: { sub: string; scopes: string[] };
|
|
436
|
-
user: { sub: string; scopes: string[] };
|
|
437
|
-
readOnly: { sub: string; scopes: string[] };
|
|
438
|
-
anonymous: { sub: string; scopes: string[] };
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
/** Get the public JWKS */
|
|
442
|
-
getJwks(): JSONWebKeySet;
|
|
443
|
-
}
|
|
444
|
-
```
|
|
445
|
-
|
|
446
|
-
### Custom Matchers
|
|
447
|
-
|
|
448
|
-
`@frontmcp/testing` extends `expect` with MCP-specific matchers:
|
|
449
|
-
|
|
450
|
-
```typescript
|
|
451
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
452
|
-
// TOOL MATCHERS
|
|
453
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
454
|
-
|
|
455
|
-
// Check if tools array contains a tool by name
|
|
456
|
-
expect(tools).toContainTool('tool-name');
|
|
457
|
-
|
|
458
|
-
// Check tool result success
|
|
459
|
-
expect(result).toBeSuccessful();
|
|
460
|
-
expect(result).toBeError();
|
|
461
|
-
expect(result).toBeError(-32602); // Specific error code
|
|
462
|
-
|
|
463
|
-
// Check tool result content
|
|
464
|
-
expect(result).toHaveTextContent();
|
|
465
|
-
expect(result).toHaveTextContent('expected text');
|
|
466
|
-
expect(result).toHaveImageContent();
|
|
467
|
-
expect(result).toHaveResourceContent();
|
|
468
|
-
|
|
469
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
470
|
-
// RESOURCE MATCHERS
|
|
471
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
472
|
-
|
|
473
|
-
// Check if resources array contains a resource
|
|
474
|
-
expect(resources).toContainResource('notes://all');
|
|
475
|
-
expect(resources).toContainResourceTemplate('notes://note/{id}');
|
|
476
|
-
|
|
477
|
-
// Check resource content
|
|
478
|
-
expect(content).toHaveMimeType('application/json');
|
|
479
|
-
expect(content).toHaveTextContent();
|
|
480
|
-
|
|
481
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
482
|
-
// PROMPT MATCHERS
|
|
483
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
484
|
-
|
|
485
|
-
// Check if prompts array contains a prompt
|
|
486
|
-
expect(prompts).toContainPrompt('prompt-name');
|
|
487
|
-
|
|
488
|
-
// Check prompt result
|
|
489
|
-
expect(prompt).toHaveMessages(2);
|
|
490
|
-
expect(prompt.messages[0]).toHaveRole('user');
|
|
491
|
-
expect(prompt.messages[0]).toContainText('expected');
|
|
492
|
-
|
|
493
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
494
|
-
// PROTOCOL MATCHERS
|
|
495
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
496
|
-
|
|
497
|
-
// JSON-RPC response validation
|
|
498
|
-
expect(response).toBeValidJsonRpc();
|
|
499
|
-
expect(response).toHaveResult();
|
|
500
|
-
expect(response).toHaveError();
|
|
501
|
-
expect(response).toHaveErrorCode(-32601);
|
|
502
|
-
|
|
503
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
504
|
-
// TOOL UI MATCHERS
|
|
505
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
506
|
-
|
|
507
|
-
// Check if tool result has rendered HTML UI (not mdx-fallback)
|
|
508
|
-
expect(result).toHaveRenderedHtml();
|
|
509
|
-
|
|
510
|
-
// Check if HTML contains specific elements
|
|
511
|
-
expect(result).toContainHtmlElement('div');
|
|
512
|
-
expect(result).toContainHtmlElement('h1');
|
|
513
|
-
|
|
514
|
-
// Check if tool output values appear in rendered HTML (data binding)
|
|
515
|
-
expect(result).toContainBoundValue('London'); // String value
|
|
516
|
-
expect(result).toContainBoundValue(25); // Number value
|
|
517
|
-
|
|
518
|
-
// Check XSS safety (no script tags, event handlers, javascript: URIs)
|
|
519
|
-
expect(result).toBeXssSafe();
|
|
520
|
-
|
|
521
|
-
// Check for widget metadata in response
|
|
522
|
-
expect(result).toHaveWidgetMetadata();
|
|
523
|
-
|
|
524
|
-
// Check for specific CSS classes
|
|
525
|
-
expect(result).toHaveCssClass('weather-card');
|
|
526
|
-
|
|
527
|
-
// Check that raw content is NOT present (useful for fallback detection)
|
|
528
|
-
expect(result).toNotContainRawContent('mdx-fallback');
|
|
529
|
-
expect(result).toNotContainRawContent('<Alert'); // Custom components rendered
|
|
530
|
-
|
|
531
|
-
// Check HTML structure is proper (not escaped text)
|
|
532
|
-
expect(result).toHaveProperHtmlStructure();
|
|
533
|
-
```
|
|
534
|
-
|
|
535
|
-
---
|
|
536
|
-
|
|
537
|
-
## Testing Guide
|
|
538
|
-
|
|
539
|
-
### Testing Tools
|
|
540
|
-
|
|
541
|
-
```typescript
|
|
542
|
-
import { test, expect } from '@frontmcp/testing';
|
|
543
|
-
import MyServer from './src/main';
|
|
544
|
-
|
|
545
|
-
test.use({ server: MyServer });
|
|
546
|
-
|
|
547
|
-
test.describe('Tools', () => {
|
|
548
|
-
test('list all tools', async ({ mcp }) => {
|
|
549
|
-
const tools = await mcp.tools.list();
|
|
550
|
-
|
|
551
|
-
expect(tools).toHaveLength(5);
|
|
552
|
-
expect(tools).toContainTool('create-note');
|
|
553
|
-
expect(tools).toContainTool('list-notes');
|
|
554
|
-
|
|
555
|
-
// Check tool schema
|
|
556
|
-
const createNote = tools.find((t) => t.name === 'create-note');
|
|
557
|
-
expect(createNote.inputSchema).toMatchObject({
|
|
558
|
-
type: 'object',
|
|
559
|
-
required: ['title'],
|
|
560
|
-
});
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
test('call tool with valid input', async ({ mcp }) => {
|
|
564
|
-
const result = await mcp.tools.call('create-note', {
|
|
565
|
-
title: 'Test Note',
|
|
566
|
-
content: 'Hello world',
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
expect(result).toBeSuccessful();
|
|
570
|
-
expect(result).toHaveTextContent();
|
|
571
|
-
|
|
572
|
-
// Use generic type parameter for type-safe JSON parsing
|
|
573
|
-
const data = result.json<{ id: string; title: string }>();
|
|
574
|
-
expect(data.id).toBeDefined();
|
|
575
|
-
expect(data.title).toBe('Test Note');
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
test('call tool with invalid input', async ({ mcp }) => {
|
|
579
|
-
const result = await mcp.tools.call('create-note', {
|
|
580
|
-
// missing required 'title'
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
expect(result).toBeError(-32602); // Invalid params
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
test('call non-existent tool', async ({ mcp }) => {
|
|
587
|
-
const result = await mcp.tools.call('unknown-tool', {});
|
|
588
|
-
|
|
589
|
-
expect(result).toBeError(-32601); // Method not found
|
|
590
|
-
});
|
|
591
|
-
});
|
|
592
|
-
```
|
|
593
|
-
|
|
594
|
-
### Testing Tool UI
|
|
595
|
-
|
|
596
|
-
Tools with UI templates (React, MDX) render HTML in the response `_meta['ui/html']`. The testing library provides specialized matchers and assertions for validating rendered UI.
|
|
597
|
-
|
|
598
|
-
```typescript
|
|
599
|
-
import { test, expect, UIAssertions } from '@frontmcp/testing';
|
|
600
|
-
import MyServer from './src/main';
|
|
601
|
-
|
|
602
|
-
test.use({ server: MyServer });
|
|
603
|
-
|
|
604
|
-
test.describe('Tool UI', () => {
|
|
605
|
-
test('renders weather UI with data binding', async ({ mcp }) => {
|
|
606
|
-
const result = await mcp.tools.call('get_weather', { location: 'London' });
|
|
607
|
-
|
|
608
|
-
// Basic assertions
|
|
609
|
-
expect(result).toBeSuccessful();
|
|
610
|
-
expect(result).toHaveRenderedHtml(); // Has ui/html, not mdx-fallback
|
|
611
|
-
expect(result).toBeXssSafe(); // No script tags or event handlers
|
|
612
|
-
expect(result).toHaveWidgetMetadata(); // Has platform metadata
|
|
613
|
-
|
|
614
|
-
// Data binding - output values appear in rendered HTML
|
|
615
|
-
const output = result.json<WeatherOutput>();
|
|
616
|
-
expect(result).toContainBoundValue(output.location);
|
|
617
|
-
expect(result).toContainBoundValue(output.temperature);
|
|
618
|
-
|
|
619
|
-
// HTML structure
|
|
620
|
-
expect(result).toContainHtmlElement('div');
|
|
621
|
-
expect(result).toHaveProperHtmlStructure();
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
test('MDX components render correctly', async ({ mcp }) => {
|
|
625
|
-
const result = await mcp.tools.call('get_weather_mdx', { location: 'Tokyo' });
|
|
626
|
-
|
|
627
|
-
expect(result).toHaveRenderedHtml();
|
|
628
|
-
|
|
629
|
-
// Custom MDX components should be rendered, not raw tags
|
|
630
|
-
expect(result).toNotContainRawContent('<Alert');
|
|
631
|
-
expect(result).toNotContainRawContent('<WeatherCard');
|
|
632
|
-
expect(result).toNotContainRawContent('mdx-fallback');
|
|
633
|
-
|
|
634
|
-
// Markdown should render as HTML
|
|
635
|
-
expect(result).toContainHtmlElement('h1'); // # Header
|
|
636
|
-
expect(result).toContainHtmlElement('li'); // - List item
|
|
637
|
-
});
|
|
638
|
-
|
|
639
|
-
test('handles XSS in user input', async ({ mcp }) => {
|
|
640
|
-
const result = await mcp.tools.call('get_weather', {
|
|
641
|
-
location: '<script>alert("xss")</script>',
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
expect(result).toBeSuccessful();
|
|
645
|
-
expect(result).toBeXssSafe();
|
|
646
|
-
expect(result).toNotContainRawContent('<script>');
|
|
647
|
-
});
|
|
648
|
-
});
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
#### UIAssertions Helper
|
|
652
|
-
|
|
653
|
-
For more control, use the `UIAssertions` helper class:
|
|
654
|
-
|
|
655
|
-
```typescript
|
|
656
|
-
import { UIAssertions } from '@frontmcp/testing';
|
|
657
|
-
|
|
658
|
-
test('comprehensive UI validation', async ({ mcp }) => {
|
|
659
|
-
const result = await mcp.tools.call('get_weather', { location: 'Paris' });
|
|
660
|
-
|
|
661
|
-
// assertValidUI runs multiple checks and returns the HTML
|
|
662
|
-
const html = UIAssertions.assertValidUI(result, ['location', 'temperature', 'conditions']);
|
|
663
|
-
|
|
664
|
-
// Additional assertions on extracted HTML
|
|
665
|
-
UIAssertions.assertContainsElement(html, 'div');
|
|
666
|
-
UIAssertions.assertNotContainsRaw(html, 'mdx-fallback');
|
|
667
|
-
UIAssertions.assertXssSafe(html);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
test('data binding validation', async ({ mcp }) => {
|
|
671
|
-
const result = await mcp.tools.call('get_weather', { location: 'Berlin' });
|
|
672
|
-
|
|
673
|
-
const html = UIAssertions.assertRenderedUI(result);
|
|
674
|
-
const output = result.json<WeatherOutput>();
|
|
675
|
-
|
|
676
|
-
// Verify all output fields are bound in the HTML
|
|
677
|
-
UIAssertions.assertDataBinding(html, output, ['location', 'temperature', 'conditions', 'humidity', 'windSpeed']);
|
|
678
|
-
});
|
|
679
|
-
```
|
|
680
|
-
|
|
681
|
-
#### UIAssertions API
|
|
682
|
-
|
|
683
|
-
```typescript
|
|
684
|
-
const UIAssertions = {
|
|
685
|
-
/** Assert result has valid rendered HTML (not mdx-fallback) */
|
|
686
|
-
assertRenderedUI(result: ToolResultWrapper): string;
|
|
687
|
-
|
|
688
|
-
/** Assert HTML contains all expected bound values from output */
|
|
689
|
-
assertDataBinding(
|
|
690
|
-
html: string,
|
|
691
|
-
output: Record<string, unknown>,
|
|
692
|
-
keys: string[],
|
|
693
|
-
): void;
|
|
694
|
-
|
|
695
|
-
/** Assert HTML is XSS safe */
|
|
696
|
-
assertXssSafe(html: string): void;
|
|
697
|
-
|
|
698
|
-
/** Assert HTML has proper structure (not escaped text) */
|
|
699
|
-
assertProperHtmlStructure(html: string): void;
|
|
700
|
-
|
|
701
|
-
/** Assert HTML contains a specific element */
|
|
702
|
-
assertContainsElement(html: string, tag: string): void;
|
|
703
|
-
|
|
704
|
-
/** Assert HTML has a specific CSS class */
|
|
705
|
-
assertHasCssClass(html: string, className: string): void;
|
|
706
|
-
|
|
707
|
-
/** Assert HTML does NOT contain specific raw content */
|
|
708
|
-
assertNotContainsRaw(html: string, content: string): void;
|
|
709
|
-
|
|
710
|
-
/** Assert result has widget metadata */
|
|
711
|
-
assertWidgetMetadata(result: ToolResultWrapper): void;
|
|
712
|
-
|
|
713
|
-
/** Run comprehensive validation and return HTML */
|
|
714
|
-
assertValidUI(
|
|
715
|
-
result: ToolResultWrapper,
|
|
716
|
-
boundKeys?: string[],
|
|
717
|
-
): string;
|
|
718
|
-
};
|
|
719
|
-
```
|
|
720
|
-
|
|
721
|
-
#### Tool Result json() for UI
|
|
722
|
-
|
|
723
|
-
When a tool has UI enabled, `result.json()` automatically returns the `structuredContent` (typed output) instead of parsing text content:
|
|
724
|
-
|
|
725
|
-
```typescript
|
|
726
|
-
// Tool with UI: json() returns structuredContent
|
|
727
|
-
const result = await mcp.tools.call('get_weather', { location: 'London' });
|
|
728
|
-
const output = result.json<WeatherOutput>(); // Returns structuredContent
|
|
729
|
-
|
|
730
|
-
// Tool without UI: json() parses text content as JSON
|
|
731
|
-
const result = await mcp.tools.call('simple_tool', {});
|
|
732
|
-
const output = result.json<SimpleOutput>(); // Parses content[0].text
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
### Testing Resources
|
|
736
|
-
|
|
737
|
-
```typescript
|
|
738
|
-
test.describe('Resources', () => {
|
|
739
|
-
test('list resources', async ({ mcp }) => {
|
|
740
|
-
const resources = await mcp.resources.list();
|
|
741
|
-
|
|
742
|
-
expect(resources).toContainResource('notes://all');
|
|
743
|
-
expect(resources).toContainResource('tasks://all');
|
|
744
|
-
});
|
|
745
|
-
|
|
746
|
-
test('list resource templates', async ({ mcp }) => {
|
|
747
|
-
const templates = await mcp.resources.listTemplates();
|
|
748
|
-
|
|
749
|
-
expect(templates).toContainResourceTemplate('notes://note/{noteId}');
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
test('read static resource', async ({ mcp }) => {
|
|
753
|
-
const content = await mcp.resources.read('notes://all');
|
|
754
|
-
|
|
755
|
-
expect(content).toBeSuccessful();
|
|
756
|
-
expect(content).toHaveMimeType('application/json');
|
|
757
|
-
|
|
758
|
-
const data = content.json();
|
|
759
|
-
expect(data.notes).toBeInstanceOf(Array);
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
test('read templated resource', async ({ mcp }) => {
|
|
763
|
-
const content = await mcp.resources.read('notes://note/123');
|
|
764
|
-
|
|
765
|
-
expect(content).toBeSuccessful();
|
|
766
|
-
expect(content.json().id).toBe('123');
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
test('read non-existent resource', async ({ mcp }) => {
|
|
770
|
-
const content = await mcp.resources.read('notes://note/nonexistent');
|
|
771
|
-
|
|
772
|
-
expect(content).toBeError(-32002); // Resource not found
|
|
773
|
-
});
|
|
774
|
-
});
|
|
775
36
|
```
|
|
776
37
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
```typescript
|
|
780
|
-
test.describe('Prompts', () => {
|
|
781
|
-
test('list prompts', async ({ mcp }) => {
|
|
782
|
-
const prompts = await mcp.prompts.list();
|
|
783
|
-
|
|
784
|
-
expect(prompts).toContainPrompt('summarize-notes');
|
|
785
|
-
expect(prompts).toContainPrompt('prioritize-tasks');
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
test('get prompt', async ({ mcp }) => {
|
|
789
|
-
const result = await mcp.prompts.get('summarize-notes', {
|
|
790
|
-
tag: 'work',
|
|
791
|
-
format: 'detailed',
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
expect(result).toBeSuccessful();
|
|
795
|
-
expect(result.messages).toHaveLength(1);
|
|
796
|
-
expect(result.messages[0]).toHaveRole('user');
|
|
797
|
-
expect(result.messages[0]).toContainText('work');
|
|
798
|
-
});
|
|
799
|
-
});
|
|
800
|
-
```
|
|
801
|
-
|
|
802
|
-
### Testing Authentication
|
|
803
|
-
|
|
804
|
-
```typescript
|
|
805
|
-
test.describe('Authentication', () => {
|
|
806
|
-
// Public mode - no auth required
|
|
807
|
-
test.describe('Public Mode', () => {
|
|
808
|
-
test.use({ server: MyServer, auth: { mode: 'public' } });
|
|
809
|
-
|
|
810
|
-
test('allows anonymous access', async ({ mcp }) => {
|
|
811
|
-
expect(mcp.auth.isAnonymous).toBe(true);
|
|
812
|
-
|
|
813
|
-
const tools = await mcp.tools.list();
|
|
814
|
-
expect(tools.length).toBeGreaterThan(0);
|
|
815
|
-
});
|
|
816
|
-
});
|
|
817
|
-
|
|
818
|
-
// Orchestrated mode - auth required
|
|
819
|
-
test.describe('Orchestrated Mode', () => {
|
|
820
|
-
test.use({
|
|
821
|
-
server: MyServer,
|
|
822
|
-
auth: { mode: 'orchestrated', type: 'local' },
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
test('requires authentication', async ({ mcp }) => {
|
|
826
|
-
// Client without token
|
|
827
|
-
await expect(mcp.tools.list()).rejects.toThrow('Unauthorized');
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
test('accepts valid token', async ({ mcp, auth }) => {
|
|
831
|
-
const token = await auth.createToken({
|
|
832
|
-
sub: 'user-123',
|
|
833
|
-
scopes: ['read', 'write'],
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
await mcp.authenticate(token);
|
|
837
|
-
|
|
838
|
-
const tools = await mcp.tools.list();
|
|
839
|
-
expect(tools.length).toBeGreaterThan(0);
|
|
840
|
-
});
|
|
841
|
-
|
|
842
|
-
test('rejects expired token', async ({ mcp, auth }) => {
|
|
843
|
-
const token = await auth.createExpiredToken({ sub: 'user-123' });
|
|
844
|
-
|
|
845
|
-
await expect(mcp.authenticate(token)).rejects.toThrow('expired');
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
test('enforces scopes', async ({ mcp, auth }) => {
|
|
849
|
-
// Token with only 'read' scope
|
|
850
|
-
const token = await auth.createToken({
|
|
851
|
-
sub: 'user-123',
|
|
852
|
-
scopes: ['read'],
|
|
853
|
-
});
|
|
854
|
-
|
|
855
|
-
await mcp.authenticate(token);
|
|
856
|
-
|
|
857
|
-
// Read operations work
|
|
858
|
-
const resources = await mcp.resources.list();
|
|
859
|
-
expect(resources.length).toBeGreaterThan(0);
|
|
860
|
-
|
|
861
|
-
// Write operations fail (if tool requires 'write' scope)
|
|
862
|
-
const result = await mcp.tools.call('create-note', { title: 'Test' });
|
|
863
|
-
expect(result).toBeError(); // Insufficient scopes
|
|
864
|
-
});
|
|
865
|
-
});
|
|
866
|
-
});
|
|
867
|
-
```
|
|
868
|
-
|
|
869
|
-
### Testing Transports
|
|
870
|
-
|
|
871
|
-
```typescript
|
|
872
|
-
test.describe('SSE Transport', () => {
|
|
873
|
-
test.use({ server: MyServer, transport: 'sse' });
|
|
874
|
-
|
|
875
|
-
test('connects via SSE', async ({ mcp }) => {
|
|
876
|
-
expect(mcp.transport.type).toBe('sse');
|
|
877
|
-
expect(mcp.transport.isConnected()).toBe(true);
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
test('receives endpoint event', async ({ mcp }) => {
|
|
881
|
-
expect(mcp.transport.messageEndpoint).toContain('/message');
|
|
882
|
-
});
|
|
38
|
+
The library handles server startup, MCP client connection, test execution, and cleanup.
|
|
883
39
|
|
|
884
|
-
|
|
885
|
-
await mcp.tools.list();
|
|
886
|
-
await mcp.resources.list();
|
|
887
|
-
await mcp.prompts.list();
|
|
40
|
+
## Fixtures
|
|
888
41
|
|
|
889
|
-
|
|
890
|
-
|
|
42
|
+
| Fixture | Description |
|
|
43
|
+
| -------- | ----------------------------------------------------------------------------------------------------- |
|
|
44
|
+
| `mcp` | Auto-connected MCP client — tools, resources, prompts, raw protocol, notifications, logs |
|
|
45
|
+
| `server` | Server control — `createClient()`, `restart()`, `onHook()`, `getLogs()` |
|
|
46
|
+
| `auth` | Token factory — `createToken()`, `createExpiredToken()`, `createInvalidToken()`, pre-built test users |
|
|
891
47
|
|
|
892
|
-
|
|
893
|
-
await mcp.transport.simulateDisconnect();
|
|
894
|
-
await mcp.transport.waitForReconnect(5000);
|
|
48
|
+
## Custom Matchers
|
|
895
49
|
|
|
896
|
-
|
|
897
|
-
expect(mcp.transport.reconnectCount).toBe(1);
|
|
898
|
-
});
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
test.describe('StreamableHTTP Transport', () => {
|
|
902
|
-
test.use({ server: MyServer, transport: 'streamable-http' });
|
|
903
|
-
|
|
904
|
-
test('connects via StreamableHTTP', async ({ mcp }) => {
|
|
905
|
-
expect(mcp.transport.type).toBe('streamable-http');
|
|
906
|
-
expect(mcp.transport.isConnected()).toBe(true);
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
test('uses session headers', async ({ mcp }) => {
|
|
910
|
-
await mcp.tools.list();
|
|
911
|
-
|
|
912
|
-
expect(mcp.transport.lastRequestHeaders['mcp-session-id']).toBe(mcp.sessionId);
|
|
913
|
-
});
|
|
914
|
-
});
|
|
915
|
-
|
|
916
|
-
test.describe('Transport Comparison', () => {
|
|
917
|
-
test('both transports return same data', async ({ server }) => {
|
|
918
|
-
const sseClient = await server.createClient({ transport: 'sse' });
|
|
919
|
-
const httpClient = await server.createClient({ transport: 'streamable-http' });
|
|
920
|
-
|
|
921
|
-
const sseTools = await sseClient.tools.list();
|
|
922
|
-
const httpTools = await httpClient.tools.list();
|
|
923
|
-
|
|
924
|
-
expect(sseTools).toEqual(httpTools);
|
|
925
|
-
|
|
926
|
-
await sseClient.disconnect();
|
|
927
|
-
await httpClient.disconnect();
|
|
928
|
-
});
|
|
929
|
-
});
|
|
930
|
-
```
|
|
931
|
-
|
|
932
|
-
### Testing Notifications
|
|
933
|
-
|
|
934
|
-
```typescript
|
|
935
|
-
test.describe('Notifications', () => {
|
|
936
|
-
test('receives list_changed notification', async ({ mcp }) => {
|
|
937
|
-
const notifications = mcp.notifications.collect();
|
|
938
|
-
|
|
939
|
-
// Trigger a change
|
|
940
|
-
await mcp.tools.call('create-note', { title: 'New' });
|
|
941
|
-
|
|
942
|
-
await notifications.waitFor('notifications/resources/list_changed', 1000);
|
|
943
|
-
|
|
944
|
-
expect(notifications.has('notifications/resources/list_changed')).toBe(true);
|
|
945
|
-
});
|
|
946
|
-
|
|
947
|
-
test('receives progress notifications', async ({ mcp }) => {
|
|
948
|
-
const progress = mcp.notifications.collectProgress();
|
|
949
|
-
|
|
950
|
-
const resultPromise = mcp.tools.call('long-task', {});
|
|
951
|
-
await progress.waitForComplete(10000);
|
|
952
|
-
|
|
953
|
-
expect(progress.updates.length).toBeGreaterThan(0);
|
|
954
|
-
expect(progress.updates[progress.updates.length - 1].progress).toBe(100);
|
|
955
|
-
|
|
956
|
-
await resultPromise;
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
test('sends client notification', async ({ mcp }) => {
|
|
960
|
-
await mcp.notifications.send('notifications/roots/list_changed');
|
|
961
|
-
// No error = success
|
|
962
|
-
});
|
|
963
|
-
|
|
964
|
-
test('cancels request', async ({ mcp }) => {
|
|
965
|
-
const promise = mcp.tools.call('slow-tool', {});
|
|
966
|
-
const requestId = mcp.lastRequestId;
|
|
967
|
-
|
|
968
|
-
await mcp.notifications.send('notifications/cancelled', {
|
|
969
|
-
requestId,
|
|
970
|
-
reason: 'User cancelled',
|
|
971
|
-
});
|
|
972
|
-
|
|
973
|
-
await expect(promise).rejects.toMatchObject({ code: -32800 });
|
|
974
|
-
});
|
|
975
|
-
});
|
|
976
|
-
```
|
|
977
|
-
|
|
978
|
-
### Logging & Debugging
|
|
979
|
-
|
|
980
|
-
```typescript
|
|
981
|
-
test.describe('Logging', () => {
|
|
982
|
-
test.use({ server: MyServer, logLevel: 'debug' });
|
|
983
|
-
|
|
984
|
-
test('captures server logs', async ({ mcp }) => {
|
|
985
|
-
await mcp.tools.call('create-note', { title: 'Test' });
|
|
986
|
-
|
|
987
|
-
const logs = mcp.logs.all();
|
|
988
|
-
expect(logs.length).toBeGreaterThan(0);
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
test('filters logs by level', async ({ mcp }) => {
|
|
992
|
-
await mcp.tools.call('create-note', { title: 'Test' });
|
|
993
|
-
|
|
994
|
-
expect(mcp.logs.filter('error')).toHaveLength(0);
|
|
995
|
-
expect(mcp.logs.filter('debug').length).toBeGreaterThan(0);
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
test('traces requests', async ({ mcp }) => {
|
|
999
|
-
await mcp.tools.call('create-note', { title: 'Test' });
|
|
1000
|
-
|
|
1001
|
-
const trace = mcp.trace.last();
|
|
1002
|
-
expect(trace.request.method).toBe('tools/call');
|
|
1003
|
-
expect(trace.response.result).toBeDefined();
|
|
1004
|
-
expect(trace.durationMs).toBeLessThan(1000);
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
test('dumps full conversation', async ({ mcp }) => {
|
|
1008
|
-
await mcp.tools.list();
|
|
1009
|
-
await mcp.tools.call('create-note', { title: 'Test' });
|
|
1010
|
-
|
|
1011
|
-
const history = mcp.trace.all();
|
|
1012
|
-
expect(history).toHaveLength(3); // init + list + call
|
|
1013
|
-
});
|
|
1014
|
-
});
|
|
1015
|
-
```
|
|
1016
|
-
|
|
1017
|
-
### Testing Plugins
|
|
1018
|
-
|
|
1019
|
-
```typescript
|
|
1020
|
-
test.describe('Plugin Testing', () => {
|
|
1021
|
-
test.use({ server: MyServer });
|
|
1022
|
-
|
|
1023
|
-
test('plugin registers tools', async ({ mcp }) => {
|
|
1024
|
-
const tools = await mcp.tools.list();
|
|
1025
|
-
expect(tools).toContainTool('plugin:my-action');
|
|
1026
|
-
});
|
|
1027
|
-
|
|
1028
|
-
test('plugin tool executes', async ({ mcp }) => {
|
|
1029
|
-
const result = await mcp.tools.call('plugin:my-action', {
|
|
1030
|
-
data: 'test',
|
|
1031
|
-
});
|
|
1032
|
-
expect(result).toBeSuccessful();
|
|
1033
|
-
});
|
|
1034
|
-
|
|
1035
|
-
test('plugin tool returns expected data', async ({ mcp }) => {
|
|
1036
|
-
const result = await mcp.tools.call('plugin:my-action', { data: 'test' });
|
|
1037
|
-
expect(result).toBeSuccessful();
|
|
1038
|
-
expect(result.json()).toMatchObject({ processed: true });
|
|
1039
|
-
});
|
|
1040
|
-
});
|
|
1041
|
-
```
|
|
50
|
+
**Tools** — `toContainTool`, `toBeSuccessful`, `toBeError`, `toHaveTextContent`, `toHaveImageContent`, `toHaveResourceContent`
|
|
1042
51
|
|
|
1043
|
-
|
|
52
|
+
**Resources** — `toContainResource`, `toContainResourceTemplate`, `toHaveMimeType`
|
|
1044
53
|
|
|
1045
|
-
|
|
1046
|
-
import { httpMock } from '@frontmcp/testing';
|
|
54
|
+
**Prompts** — `toContainPrompt`, `toHaveMessages`, `toHaveRole`, `toContainText`
|
|
1047
55
|
|
|
1048
|
-
|
|
1049
|
-
test.use({
|
|
1050
|
-
server: MyServer,
|
|
1051
|
-
env: { OPENAPI_URL: 'https://api.example.com/openapi.json' },
|
|
1052
|
-
});
|
|
56
|
+
**Protocol** — `toBeValidJsonRpc`, `toHaveResult`, `toHaveError`, `toHaveErrorCode`
|
|
1053
57
|
|
|
1054
|
-
|
|
1055
|
-
const tools = await mcp.tools.list();
|
|
1056
|
-
expect(tools).toContainTool('openapi:getUsers');
|
|
1057
|
-
expect(tools).toContainTool('openapi:createUser');
|
|
1058
|
-
});
|
|
1059
|
-
|
|
1060
|
-
test('calls external API', async ({ mcp }) => {
|
|
1061
|
-
// Mock external API using httpMock
|
|
1062
|
-
const interceptor = httpMock.interceptor();
|
|
1063
|
-
// Use { body: ... } format for response data
|
|
1064
|
-
interceptor.get('https://api.example.com/users', {
|
|
1065
|
-
body: [{ id: 1, name: 'John' }],
|
|
1066
|
-
});
|
|
1067
|
-
|
|
1068
|
-
const result = await mcp.tools.call('openapi:getUsers', {});
|
|
1069
|
-
|
|
1070
|
-
expect(result).toBeSuccessful();
|
|
1071
|
-
expect(result.json()).toContainEqual({ id: 1, name: 'John' });
|
|
1072
|
-
|
|
1073
|
-
interceptor.restore();
|
|
1074
|
-
});
|
|
1075
|
-
});
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
### HTTP Mocking
|
|
1079
|
-
|
|
1080
|
-
Mock external HTTP requests made by your tools for fully offline testing:
|
|
1081
|
-
|
|
1082
|
-
```typescript
|
|
1083
|
-
import { httpMock, httpResponse } from '@frontmcp/testing';
|
|
1084
|
-
|
|
1085
|
-
test.describe('HTTP Mocking', () => {
|
|
1086
|
-
test('mock external API calls', async ({ mcp }) => {
|
|
1087
|
-
// Create an HTTP interceptor
|
|
1088
|
-
const interceptor = httpMock.interceptor();
|
|
1089
|
-
|
|
1090
|
-
// Mock GET request - use { body: ... } for response data
|
|
1091
|
-
interceptor.get('https://api.weather.com/london', {
|
|
1092
|
-
body: { temperature: 72, conditions: 'sunny' },
|
|
1093
|
-
});
|
|
1094
|
-
|
|
1095
|
-
// Mock POST request with pattern matching
|
|
1096
|
-
interceptor.post(/api\.example\.com\/users/, {
|
|
1097
|
-
status: 201,
|
|
1098
|
-
body: { id: 2, name: 'Jane' },
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
// Call your tool that makes HTTP requests
|
|
1102
|
-
const result = await mcp.tools.call('fetch-weather', { city: 'london' });
|
|
1103
|
-
expect(result).toBeSuccessful();
|
|
1104
|
-
|
|
1105
|
-
// Verify all mocks were used
|
|
1106
|
-
interceptor.assertDone();
|
|
1107
|
-
|
|
1108
|
-
// Clean up
|
|
1109
|
-
interceptor.restore();
|
|
1110
|
-
});
|
|
1111
|
-
|
|
1112
|
-
test('use response helpers', async ({ mcp }) => {
|
|
1113
|
-
const interceptor = httpMock.interceptor();
|
|
1114
|
-
|
|
1115
|
-
// Use helper methods for common responses
|
|
1116
|
-
interceptor.get('/api/data', httpResponse.json({ id: 1 }));
|
|
1117
|
-
interceptor.get('/api/missing', httpResponse.notFound());
|
|
1118
|
-
interceptor.get('/api/slow', httpResponse.delayed({ data: 'result' }, 500));
|
|
1119
|
-
|
|
1120
|
-
// ...test your tools...
|
|
1121
|
-
|
|
1122
|
-
interceptor.restore();
|
|
1123
|
-
});
|
|
1124
|
-
|
|
1125
|
-
test('track calls', async ({ mcp }) => {
|
|
1126
|
-
const interceptor = httpMock.interceptor();
|
|
1127
|
-
const handle = interceptor.get('https://api.example.com/users', { body: [] });
|
|
1128
|
-
|
|
1129
|
-
await mcp.tools.call('list-users', {});
|
|
1130
|
-
|
|
1131
|
-
// Check call count
|
|
1132
|
-
expect(handle.callCount()).toBe(1);
|
|
1133
|
-
|
|
1134
|
-
// Get call details
|
|
1135
|
-
const calls = handle.calls();
|
|
1136
|
-
expect(calls[0].headers['authorization']).toBeDefined();
|
|
1137
|
-
|
|
1138
|
-
interceptor.restore();
|
|
1139
|
-
});
|
|
1140
|
-
});
|
|
1141
|
-
```
|
|
1142
|
-
|
|
1143
|
-
### Raw Protocol Access
|
|
1144
|
-
|
|
1145
|
-
```typescript
|
|
1146
|
-
test.describe('Raw Protocol', () => {
|
|
1147
|
-
test('send custom request', async ({ mcp }) => {
|
|
1148
|
-
const response = await mcp.raw.request({
|
|
1149
|
-
jsonrpc: '2.0',
|
|
1150
|
-
id: 1,
|
|
1151
|
-
method: 'tools/list',
|
|
1152
|
-
params: {},
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
expect(response).toBeValidJsonRpc();
|
|
1156
|
-
expect(response.result.tools).toBeDefined();
|
|
1157
|
-
});
|
|
1158
|
-
|
|
1159
|
-
test('send notification', async ({ mcp }) => {
|
|
1160
|
-
await mcp.raw.notify({
|
|
1161
|
-
jsonrpc: '2.0',
|
|
1162
|
-
method: 'notifications/initialized',
|
|
1163
|
-
});
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
test('handle parse error', async ({ mcp }) => {
|
|
1167
|
-
const response = await mcp.raw.sendRaw('invalid json');
|
|
1168
|
-
expect(response).toHaveErrorCode(-32700);
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
test('handle method not found', async ({ mcp }) => {
|
|
1172
|
-
const response = await mcp.raw.request({
|
|
1173
|
-
jsonrpc: '2.0',
|
|
1174
|
-
id: 1,
|
|
1175
|
-
method: 'unknown/method',
|
|
1176
|
-
params: {},
|
|
1177
|
-
});
|
|
1178
|
-
expect(response).toHaveErrorCode(-32601);
|
|
1179
|
-
});
|
|
1180
|
-
});
|
|
1181
|
-
```
|
|
1182
|
-
|
|
1183
|
-
---
|
|
58
|
+
**Tool UI** — `toHaveRenderedHtml`, `toContainHtmlElement`, `toContainBoundValue`, `toBeXssSafe`, `toHaveWidgetMetadata`, `toHaveCssClass`, `toNotContainRawContent`, `toHaveProperHtmlStructure`
|
|
1184
59
|
|
|
1185
60
|
## Configuration
|
|
1186
61
|
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
displayName: 'e2e',
|
|
1196
|
-
preset: '@frontmcp/testing/jest-preset',
|
|
1197
|
-
testMatch: ['**/*.e2e.ts'],
|
|
1198
|
-
testTimeout: 30000,
|
|
1199
|
-
setupFilesAfterEnv: ['@frontmcp/testing/setup'],
|
|
1200
|
-
};
|
|
1201
|
-
|
|
1202
|
-
export default config;
|
|
1203
|
-
```
|
|
1204
|
-
|
|
1205
|
-
### Environment Variables
|
|
1206
|
-
|
|
1207
|
-
```bash
|
|
1208
|
-
# Test configuration
|
|
1209
|
-
FRONTMCP_TEST_PORT=3003 # Default port for test servers
|
|
1210
|
-
FRONTMCP_TEST_TIMEOUT=30000 # Default test timeout (ms)
|
|
1211
|
-
FRONTMCP_TEST_LOG_LEVEL=warn # Log level during tests
|
|
1212
|
-
|
|
1213
|
-
# Debug mode
|
|
1214
|
-
FRONTMCP_TEST_DEBUG=true # Enable verbose logging
|
|
1215
|
-
FRONTMCP_TEST_KEEP_SERVER=true # Don't stop server after tests
|
|
1216
|
-
```
|
|
1217
|
-
|
|
1218
|
-
---
|
|
1219
|
-
|
|
1220
|
-
## Best Practices
|
|
1221
|
-
|
|
1222
|
-
### 1. Isolate Tests
|
|
1223
|
-
|
|
1224
|
-
Each test should be independent:
|
|
1225
|
-
|
|
1226
|
-
```typescript
|
|
1227
|
-
test.beforeEach(async ({ mcp }) => {
|
|
1228
|
-
// Reset state before each test
|
|
1229
|
-
await mcp.tools.call('reset-database', {});
|
|
1230
|
-
});
|
|
1231
|
-
```
|
|
1232
|
-
|
|
1233
|
-
### 2. Use Descriptive Names
|
|
1234
|
-
|
|
1235
|
-
```typescript
|
|
1236
|
-
test.describe('Notes API', () => {
|
|
1237
|
-
test.describe('create-note tool', () => {
|
|
1238
|
-
test('creates note with valid input', async ({ mcp }) => {});
|
|
1239
|
-
test('fails with missing title', async ({ mcp }) => {});
|
|
1240
|
-
test('sanitizes HTML in content', async ({ mcp }) => {});
|
|
1241
|
-
});
|
|
1242
|
-
});
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
### 3. Test Error Cases
|
|
1246
|
-
|
|
1247
|
-
```typescript
|
|
1248
|
-
test('handles all error scenarios', async ({ mcp }) => {
|
|
1249
|
-
// Invalid params
|
|
1250
|
-
expect(await mcp.tools.call('tool', {})).toBeError(-32602);
|
|
1251
|
-
|
|
1252
|
-
// Not found
|
|
1253
|
-
expect(await mcp.resources.read('unknown://x')).toBeError(-32002);
|
|
1254
|
-
|
|
1255
|
-
// Unauthorized (if applicable)
|
|
1256
|
-
expect(await mcp.tools.call('admin-tool', {})).toBeError(-32001);
|
|
1257
|
-
});
|
|
1258
|
-
```
|
|
1259
|
-
|
|
1260
|
-
### 4. Use Snapshots for Schemas
|
|
1261
|
-
|
|
1262
|
-
```typescript
|
|
1263
|
-
test('tool schema matches snapshot', async ({ mcp }) => {
|
|
1264
|
-
const tools = await mcp.tools.list();
|
|
1265
|
-
const schema = tools.find((t) => t.name === 'my-tool')?.inputSchema;
|
|
1266
|
-
|
|
1267
|
-
expect(schema).toMatchSnapshot();
|
|
1268
|
-
});
|
|
1269
|
-
```
|
|
1270
|
-
|
|
1271
|
-
### 5. Test Both Transports
|
|
1272
|
-
|
|
1273
|
-
```typescript
|
|
1274
|
-
const transports = ['sse', 'streamable-http'] as const;
|
|
1275
|
-
|
|
1276
|
-
for (const transport of transports) {
|
|
1277
|
-
test.describe(`${transport} transport`, () => {
|
|
1278
|
-
test.use({ server: MyServer, transport });
|
|
1279
|
-
|
|
1280
|
-
test('basic operations work', async ({ mcp }) => {
|
|
1281
|
-
const tools = await mcp.tools.list();
|
|
1282
|
-
expect(tools.length).toBeGreaterThan(0);
|
|
1283
|
-
});
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
```
|
|
1287
|
-
|
|
1288
|
-
---
|
|
1289
|
-
|
|
1290
|
-
## Troubleshooting
|
|
1291
|
-
|
|
1292
|
-
### Server won't start
|
|
1293
|
-
|
|
1294
|
-
```typescript
|
|
1295
|
-
// Check if port is available
|
|
1296
|
-
test.use({ server: MyServer, port: 0 }); // Use random available port
|
|
1297
|
-
|
|
1298
|
-
// Enable debug logging
|
|
1299
|
-
test.use({ server: MyServer, logLevel: 'debug' });
|
|
1300
|
-
```
|
|
1301
|
-
|
|
1302
|
-
### Connection timeout
|
|
1303
|
-
|
|
1304
|
-
```typescript
|
|
1305
|
-
// Increase timeout
|
|
1306
|
-
test.use({ server: MyServer, startupTimeout: 60000 });
|
|
1307
|
-
|
|
1308
|
-
// Or per-test
|
|
1309
|
-
test.setTimeout(60000);
|
|
1310
|
-
```
|
|
1311
|
-
|
|
1312
|
-
### Tests are flaky
|
|
1313
|
-
|
|
1314
|
-
```typescript
|
|
1315
|
-
// Use explicit waits
|
|
1316
|
-
await mcp.notifications.waitFor('event', 5000);
|
|
1317
|
-
|
|
1318
|
-
// Clear state between tests
|
|
1319
|
-
test.beforeEach(async ({ mcp }) => {
|
|
1320
|
-
mcp.logs.clear();
|
|
1321
|
-
mcp.trace.clear();
|
|
62
|
+
```ts
|
|
63
|
+
test.use({
|
|
64
|
+
server: MyServer,
|
|
65
|
+
port: 3003, // default: auto
|
|
66
|
+
transport: 'streamable-http', // or 'sse'
|
|
67
|
+
auth: { mode: 'public' },
|
|
68
|
+
logLevel: 'debug',
|
|
69
|
+
env: { API_KEY: 'test' },
|
|
1322
70
|
});
|
|
1323
71
|
```
|
|
1324
72
|
|
|
1325
|
-
|
|
73
|
+
## Docs
|
|
1326
74
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
75
|
+
| Topic | Link |
|
|
76
|
+
| ------------------ | --------------------------------- |
|
|
77
|
+
| Getting started | [Testing Overview][docs-overview] |
|
|
78
|
+
| Testing tools | [Tools][docs-tools] |
|
|
79
|
+
| Testing Tool UI | [Tool UI][docs-tool-ui] |
|
|
80
|
+
| Testing resources | [Resources][docs-resources] |
|
|
81
|
+
| Testing prompts | [Prompts][docs-prompts] |
|
|
82
|
+
| Testing auth | [Authentication][docs-auth] |
|
|
83
|
+
| Testing transports | [Transports][docs-transports] |
|
|
84
|
+
| HTTP mocking | [HTTP Mocking][docs-mocking] |
|
|
1333
85
|
|
|
1334
|
-
##
|
|
86
|
+
## Related Packages
|
|
1335
87
|
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
| -32700 | Parse error | Invalid JSON |
|
|
1339
|
-
| -32600 | Invalid request | Invalid JSON-RPC |
|
|
1340
|
-
| -32601 | Method not found | Unknown method |
|
|
1341
|
-
| -32602 | Invalid params | Invalid method parameters |
|
|
1342
|
-
| -32603 | Internal error | Server error |
|
|
1343
|
-
| -32000 | Server error | Generic server error |
|
|
1344
|
-
| -32001 | Unauthorized | Authentication required |
|
|
1345
|
-
| -32002 | Resource not found | Resource doesn't exist |
|
|
1346
|
-
| -32800 | Request cancelled | Request was cancelled |
|
|
1347
|
-
|
|
1348
|
-
---
|
|
88
|
+
- [`@frontmcp/sdk`](../sdk) — core framework
|
|
89
|
+
- [`@frontmcp/ui`](../ui) — UI components tested with `toHaveRenderedHtml` and friends
|
|
1349
90
|
|
|
1350
91
|
## License
|
|
1351
92
|
|
|
1352
|
-
Apache-2.0
|
|
1353
|
-
|
|
1354
|
-
---
|
|
93
|
+
Apache-2.0 — see [LICENSE](../../LICENSE).
|
|
1355
94
|
|
|
1356
|
-
|
|
95
|
+
<!-- links -->
|
|
1357
96
|
|
|
1358
|
-
|
|
97
|
+
[docs-overview]: https://docs.agentfront.dev/frontmcp/testing/overview
|
|
98
|
+
[docs-tools]: https://docs.agentfront.dev/frontmcp/testing/tools
|
|
99
|
+
[docs-tool-ui]: https://docs.agentfront.dev/frontmcp/testing/tool-ui
|
|
100
|
+
[docs-resources]: https://docs.agentfront.dev/frontmcp/testing/resources
|
|
101
|
+
[docs-prompts]: https://docs.agentfront.dev/frontmcp/testing/prompts
|
|
102
|
+
[docs-auth]: https://docs.agentfront.dev/frontmcp/testing/authentication
|
|
103
|
+
[docs-transports]: https://docs.agentfront.dev/frontmcp/testing/transports
|
|
104
|
+
[docs-mocking]: https://docs.agentfront.dev/frontmcp/testing/http-mocking
|