@frontmcp/testing 0.8.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +50 -1304
  2. package/esm/package.json +5 -5
  3. package/package.json +5 -5
package/README.md CHANGED
@@ -1,78 +1,23 @@
1
1
  # @frontmcp/testing
2
2
 
3
- > **E2E Testing Framework for FrontMCP Servers**
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
- ## Table of Contents
5
+ [![NPM](https://img.shields.io/npm/v/@frontmcp/testing.svg)](https://www.npmjs.com/package/@frontmcp/testing)
8
6
 
9
- - [Installation](#installation)
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
- > **Note:** `@playwright/test` is optional - only needed for browser-based OAuth flow testing. `jest` and `@jest/globals` are optional if using a different test runner.
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
- ### 1. Create your first test
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
- ### Testing Prompts
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
- test('maintains persistent connection', async ({ mcp }) => {
885
- await mcp.tools.list();
886
- await mcp.resources.list();
887
- await mcp.prompts.list();
40
+ ## Fixtures
888
41
 
889
- expect(mcp.transport.connectionCount).toBe(1);
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
- test('handles reconnection', async ({ mcp }) => {
893
- await mcp.transport.simulateDisconnect();
894
- await mcp.transport.waitForReconnect(5000);
48
+ ## Custom Matchers
895
49
 
896
- expect(mcp.transport.isConnected()).toBe(true);
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
- ### Testing Adapters
52
+ **Resources** `toContainResource`, `toContainResourceTemplate`, `toHaveMimeType`
1044
53
 
1045
- ```typescript
1046
- import { httpMock } from '@frontmcp/testing';
54
+ **Prompts** — `toContainPrompt`, `toHaveMessages`, `toHaveRole`, `toContainText`
1047
55
 
1048
- test.describe('OpenAPI Adapter', () => {
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
- test('exposes operations as tools', async ({ mcp }) => {
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
- ### Jest Configuration
1188
-
1189
- Create `jest.e2e.config.ts`:
1190
-
1191
- ```typescript
1192
- import type { Config } from 'jest';
1193
-
1194
- const config: Config = {
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
- ### Debug a specific test
73
+ ## Docs
1326
74
 
1327
- ```bash
1328
- # Run single test with debug output
1329
- FRONTMCP_TEST_DEBUG=true npx jest --testNamePattern "my test"
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
- ## Error Codes Reference
86
+ ## Related Packages
1335
87
 
1336
- | Code | Name | Description |
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
- ## Contributing
95
+ <!-- links -->
1357
96
 
1358
- See [CONTRIBUTING.md](../../CONTRIBUTING.md) for development setup and guidelines.
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