@frontmcp/skills 0.0.1
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/LICENSE +201 -0
- package/README.md +135 -0
- package/catalog/TEMPLATE.md +49 -0
- package/catalog/adapters/create-adapter/SKILL.md +127 -0
- package/catalog/adapters/official-adapters/SKILL.md +136 -0
- package/catalog/auth/configure-auth/SKILL.md +250 -0
- package/catalog/auth/configure-auth/references/auth-modes.md +77 -0
- package/catalog/auth/configure-session/SKILL.md +201 -0
- package/catalog/config/configure-elicitation/SKILL.md +136 -0
- package/catalog/config/configure-http/SKILL.md +167 -0
- package/catalog/config/configure-throttle/SKILL.md +189 -0
- package/catalog/config/configure-throttle/references/guard-config.md +68 -0
- package/catalog/config/configure-transport/SKILL.md +151 -0
- package/catalog/config/configure-transport/references/protocol-presets.md +57 -0
- package/catalog/deployment/build-for-browser/SKILL.md +95 -0
- package/catalog/deployment/build-for-cli/SKILL.md +100 -0
- package/catalog/deployment/build-for-sdk/SKILL.md +218 -0
- package/catalog/deployment/deploy-to-cloudflare/SKILL.md +192 -0
- package/catalog/deployment/deploy-to-lambda/SKILL.md +304 -0
- package/catalog/deployment/deploy-to-node/SKILL.md +229 -0
- package/catalog/deployment/deploy-to-node/references/Dockerfile.example +45 -0
- package/catalog/deployment/deploy-to-vercel/SKILL.md +196 -0
- package/catalog/deployment/deploy-to-vercel/references/vercel.json.example +60 -0
- package/catalog/development/create-agent/SKILL.md +563 -0
- package/catalog/development/create-agent/references/llm-config.md +46 -0
- package/catalog/development/create-job/SKILL.md +566 -0
- package/catalog/development/create-prompt/SKILL.md +400 -0
- package/catalog/development/create-provider/SKILL.md +233 -0
- package/catalog/development/create-resource/SKILL.md +437 -0
- package/catalog/development/create-skill/SKILL.md +526 -0
- package/catalog/development/create-skill-with-tools/SKILL.md +579 -0
- package/catalog/development/create-tool/SKILL.md +418 -0
- package/catalog/development/create-tool/references/output-schema-types.md +56 -0
- package/catalog/development/create-tool/references/tool-annotations.md +34 -0
- package/catalog/development/create-workflow/SKILL.md +709 -0
- package/catalog/development/decorators-guide/SKILL.md +598 -0
- package/catalog/plugins/create-plugin/SKILL.md +336 -0
- package/catalog/plugins/create-plugin-hooks/SKILL.md +282 -0
- package/catalog/plugins/official-plugins/SKILL.md +667 -0
- package/catalog/setup/frontmcp-skills-usage/SKILL.md +200 -0
- package/catalog/setup/multi-app-composition/SKILL.md +358 -0
- package/catalog/setup/nx-workflow/SKILL.md +357 -0
- package/catalog/setup/project-structure-nx/SKILL.md +186 -0
- package/catalog/setup/project-structure-standalone/SKILL.md +153 -0
- package/catalog/setup/setup-project/SKILL.md +493 -0
- package/catalog/setup/setup-redis/SKILL.md +385 -0
- package/catalog/setup/setup-sqlite/SKILL.md +359 -0
- package/catalog/skills-manifest.json +414 -0
- package/catalog/testing/setup-testing/SKILL.md +539 -0
- package/catalog/testing/setup-testing/references/test-auth.md +88 -0
- package/catalog/testing/setup-testing/references/test-browser-build.md +57 -0
- package/catalog/testing/setup-testing/references/test-cli-binary.md +48 -0
- package/catalog/testing/setup-testing/references/test-direct-client.md +62 -0
- package/catalog/testing/setup-testing/references/test-e2e-handler.md +51 -0
- package/catalog/testing/setup-testing/references/test-tool-unit.md +41 -0
- package/package.json +34 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +16 -0
- package/src/index.js.map +1 -0
- package/src/loader.d.ts +46 -0
- package/src/loader.js +75 -0
- package/src/loader.js.map +1 -0
- package/src/manifest.d.ts +81 -0
- package/src/manifest.js +26 -0
- package/src/manifest.js.map +1 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: setup-testing
|
|
3
|
+
description: Configure and run unit and E2E tests for FrontMCP applications. Use when writing tests, setting up Jest, configuring coverage, or testing tools and resources.
|
|
4
|
+
tags:
|
|
5
|
+
- testing
|
|
6
|
+
- jest
|
|
7
|
+
- e2e
|
|
8
|
+
- quality
|
|
9
|
+
bundle:
|
|
10
|
+
- recommended
|
|
11
|
+
- full
|
|
12
|
+
visibility: both
|
|
13
|
+
priority: 5
|
|
14
|
+
parameters:
|
|
15
|
+
- name: test-type
|
|
16
|
+
description: Type of test to set up (unit, e2e, or both)
|
|
17
|
+
type: string
|
|
18
|
+
required: false
|
|
19
|
+
default: both
|
|
20
|
+
- name: coverage-threshold
|
|
21
|
+
description: Minimum coverage percentage required
|
|
22
|
+
type: number
|
|
23
|
+
required: false
|
|
24
|
+
default: 95
|
|
25
|
+
examples:
|
|
26
|
+
- scenario: Set up unit tests for a tool with Jest
|
|
27
|
+
parameters:
|
|
28
|
+
test-type: unit
|
|
29
|
+
expected-outcome: Tool execute method is tested with mocked context, assertions verify output schema
|
|
30
|
+
- scenario: Set up E2E tests against a running MCP server
|
|
31
|
+
parameters:
|
|
32
|
+
test-type: e2e
|
|
33
|
+
expected-outcome: McpTestClient connects to server, calls tools, and verifies responses with MCP matchers
|
|
34
|
+
- scenario: Configure full test suite with 95% coverage enforcement
|
|
35
|
+
parameters:
|
|
36
|
+
test-type: both
|
|
37
|
+
coverage-threshold: 95
|
|
38
|
+
expected-outcome: Jest runs unit and E2E tests with coverage thresholds enforced in CI
|
|
39
|
+
license: MIT
|
|
40
|
+
compatibility: Requires Node.js 18+, Jest 29+, and @frontmcp/testing for E2E tests
|
|
41
|
+
metadata:
|
|
42
|
+
category: testing
|
|
43
|
+
difficulty: beginner
|
|
44
|
+
docs: https://docs.agentfront.dev/frontmcp/testing/overview
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
# Set Up Testing for FrontMCP Applications
|
|
48
|
+
|
|
49
|
+
This skill covers testing FrontMCP applications at three levels: unit tests for individual tools/resources/prompts, E2E tests exercising the full MCP protocol, and manual testing with `frontmcp dev`.
|
|
50
|
+
|
|
51
|
+
## Testing Standards
|
|
52
|
+
|
|
53
|
+
FrontMCP requires:
|
|
54
|
+
|
|
55
|
+
- **95%+ coverage** across statements, branches, functions, and lines
|
|
56
|
+
- **All tests passing** with zero failures
|
|
57
|
+
- **File naming**: all test files use `.spec.ts` extension (NOT `.test.ts`)
|
|
58
|
+
- **E2E test naming**: use `.e2e.spec.ts` suffix
|
|
59
|
+
- **Performance test naming**: use `.perf.spec.ts` suffix
|
|
60
|
+
- **Playwright test naming**: use `.pw.spec.ts` suffix
|
|
61
|
+
|
|
62
|
+
## Unit Testing with Jest
|
|
63
|
+
|
|
64
|
+
### Test File Structure
|
|
65
|
+
|
|
66
|
+
Place test files next to the source file or in a `__tests__` directory:
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
src/
|
|
70
|
+
tools/
|
|
71
|
+
my-tool.ts
|
|
72
|
+
__tests__/
|
|
73
|
+
my-tool.spec.ts # Unit tests
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Testing a Tool
|
|
77
|
+
|
|
78
|
+
Tools extend `ToolContext` and implement `execute()`. Test the execute method by providing mock inputs and verifying outputs match the MCP `CallToolResult` shape.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// my-tool.spec.ts
|
|
82
|
+
import { MyTool } from '../my-tool';
|
|
83
|
+
|
|
84
|
+
describe('MyTool', () => {
|
|
85
|
+
let tool: MyTool;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
tool = new MyTool();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should return formatted result for valid input', async () => {
|
|
92
|
+
// Create a mock execution context
|
|
93
|
+
const mockContext = {
|
|
94
|
+
scope: {
|
|
95
|
+
get: jest.fn(),
|
|
96
|
+
tryGet: jest.fn(),
|
|
97
|
+
},
|
|
98
|
+
fail: jest.fn(),
|
|
99
|
+
mark: jest.fn(),
|
|
100
|
+
fetch: jest.fn(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Bind mock context
|
|
104
|
+
Object.assign(tool, mockContext);
|
|
105
|
+
|
|
106
|
+
const result = await tool.execute({ query: 'test input' });
|
|
107
|
+
|
|
108
|
+
expect(result).toEqual({
|
|
109
|
+
content: [{ type: 'text', text: expect.stringContaining('test input') }],
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should handle missing optional parameters', async () => {
|
|
114
|
+
const mockContext = {
|
|
115
|
+
scope: { get: jest.fn(), tryGet: jest.fn() },
|
|
116
|
+
fail: jest.fn(),
|
|
117
|
+
mark: jest.fn(),
|
|
118
|
+
fetch: jest.fn(),
|
|
119
|
+
};
|
|
120
|
+
Object.assign(tool, mockContext);
|
|
121
|
+
|
|
122
|
+
const result = await tool.execute({ query: 'test' });
|
|
123
|
+
|
|
124
|
+
expect(result.content).toBeDefined();
|
|
125
|
+
expect(result.content.length).toBeGreaterThan(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should throw for invalid input', async () => {
|
|
129
|
+
const mockContext = {
|
|
130
|
+
scope: { get: jest.fn(), tryGet: jest.fn() },
|
|
131
|
+
fail: jest.fn(),
|
|
132
|
+
};
|
|
133
|
+
Object.assign(tool, mockContext);
|
|
134
|
+
|
|
135
|
+
await expect(tool.execute({ query: '' })).rejects.toThrow();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Testing a Resource
|
|
141
|
+
|
|
142
|
+
Resources extend `ResourceContext` and implement `read()`. Verify the output matches the MCP `ReadResourceResult` shape.
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
// my-resource.spec.ts
|
|
146
|
+
import { MyResource } from '../my-resource';
|
|
147
|
+
|
|
148
|
+
describe('MyResource', () => {
|
|
149
|
+
it('should return resource contents', async () => {
|
|
150
|
+
const resource = new MyResource();
|
|
151
|
+
const result = await resource.read({ id: '123' });
|
|
152
|
+
|
|
153
|
+
expect(result).toEqual({
|
|
154
|
+
contents: [
|
|
155
|
+
{
|
|
156
|
+
uri: expect.stringMatching(/^resource:\/\//),
|
|
157
|
+
mimeType: 'application/json',
|
|
158
|
+
text: expect.any(String),
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Testing a Prompt
|
|
167
|
+
|
|
168
|
+
Prompts extend `PromptContext` and implement `execute()`. Verify the output matches the MCP `GetPromptResult` shape.
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
// my-prompt.spec.ts
|
|
172
|
+
import { MyPrompt } from '../my-prompt';
|
|
173
|
+
|
|
174
|
+
describe('MyPrompt', () => {
|
|
175
|
+
it('should return a valid GetPromptResult', async () => {
|
|
176
|
+
const prompt = new MyPrompt();
|
|
177
|
+
const result = await prompt.execute({ topic: 'testing' });
|
|
178
|
+
|
|
179
|
+
expect(result).toEqual({
|
|
180
|
+
messages: expect.arrayContaining([
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
role: 'user',
|
|
183
|
+
content: expect.objectContaining({ type: 'text' }),
|
|
184
|
+
}),
|
|
185
|
+
]),
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Testing Error Classes
|
|
192
|
+
|
|
193
|
+
Always verify error classes with `instanceof` checks and error codes:
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { ResourceNotFoundError, MCP_ERROR_CODES } from '@frontmcp/sdk';
|
|
197
|
+
|
|
198
|
+
describe('ResourceNotFoundError', () => {
|
|
199
|
+
it('should be instanceof ResourceNotFoundError', () => {
|
|
200
|
+
const error = new ResourceNotFoundError('test://resource');
|
|
201
|
+
expect(error).toBeInstanceOf(ResourceNotFoundError);
|
|
202
|
+
expect(error.mcpErrorCode).toBe(MCP_ERROR_CODES.RESOURCE_NOT_FOUND);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should produce correct JSON-RPC error', () => {
|
|
206
|
+
const error = new ResourceNotFoundError('test://resource');
|
|
207
|
+
const rpc = error.toJsonRpcError();
|
|
208
|
+
expect(rpc.code).toBe(-32002);
|
|
209
|
+
expect(rpc.data).toEqual({ uri: 'test://resource' });
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### Testing Constructor Validation
|
|
215
|
+
|
|
216
|
+
Always test that constructors throw on invalid input:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
describe('MyService constructor', () => {
|
|
220
|
+
it('should throw when required config is missing', () => {
|
|
221
|
+
expect(() => new MyService({})).toThrow();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should accept valid config', () => {
|
|
225
|
+
const service = new MyService({ endpoint: 'https://example.com' });
|
|
226
|
+
expect(service).toBeDefined();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## E2E Testing with @frontmcp/testing
|
|
232
|
+
|
|
233
|
+
The `@frontmcp/testing` library provides a full E2E testing framework with a test client, server lifecycle management, custom matchers, and fixture utilities.
|
|
234
|
+
|
|
235
|
+
### Key Exports from @frontmcp/testing
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import {
|
|
239
|
+
// Primary API (fixture-based)
|
|
240
|
+
test,
|
|
241
|
+
expect,
|
|
242
|
+
|
|
243
|
+
// Manual client API
|
|
244
|
+
McpTestClient,
|
|
245
|
+
McpTestClientBuilder,
|
|
246
|
+
|
|
247
|
+
// Server management
|
|
248
|
+
TestServer,
|
|
249
|
+
|
|
250
|
+
// Auth testing
|
|
251
|
+
TestTokenFactory,
|
|
252
|
+
AuthHeaders,
|
|
253
|
+
TestUsers,
|
|
254
|
+
MockOAuthServer,
|
|
255
|
+
MockAPIServer,
|
|
256
|
+
MockCimdServer,
|
|
257
|
+
|
|
258
|
+
// Assertions & matchers
|
|
259
|
+
McpAssertions,
|
|
260
|
+
mcpMatchers,
|
|
261
|
+
|
|
262
|
+
// Interceptors & mocking
|
|
263
|
+
DefaultMockRegistry,
|
|
264
|
+
DefaultInterceptorChain,
|
|
265
|
+
mockResponse,
|
|
266
|
+
interceptors,
|
|
267
|
+
httpMock,
|
|
268
|
+
httpResponse,
|
|
269
|
+
|
|
270
|
+
// Performance testing
|
|
271
|
+
perfTest,
|
|
272
|
+
MetricsCollector,
|
|
273
|
+
LeakDetector,
|
|
274
|
+
BaselineStore,
|
|
275
|
+
RegressionDetector,
|
|
276
|
+
ReportGenerator,
|
|
277
|
+
|
|
278
|
+
// Low-level client
|
|
279
|
+
McpClient,
|
|
280
|
+
McpStdioClientTransport,
|
|
281
|
+
} from '@frontmcp/testing';
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Install the Testing Package
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
yarn add -D @frontmcp/testing
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Fixture-Based E2E Tests (Recommended)
|
|
291
|
+
|
|
292
|
+
The fixture API manages server lifecycle automatically:
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
// my-server.e2e.spec.ts
|
|
296
|
+
import { test, expect } from '@frontmcp/testing';
|
|
297
|
+
|
|
298
|
+
test.use({
|
|
299
|
+
server: './src/main.ts',
|
|
300
|
+
port: 3003,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('server exposes expected tools', async ({ mcp }) => {
|
|
304
|
+
const tools = await mcp.tools.list();
|
|
305
|
+
expect(tools).toContainTool('create_record');
|
|
306
|
+
expect(tools).toContainTool('delete_record');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test('create_record tool returns success', async ({ mcp }) => {
|
|
310
|
+
const result = await mcp.tools.call('create_record', {
|
|
311
|
+
name: 'Test Record',
|
|
312
|
+
type: 'example',
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(result).toBeSuccessful();
|
|
316
|
+
expect(result).toHaveTextContent('created');
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test('reading a resource returns valid content', async ({ mcp }) => {
|
|
320
|
+
const result = await mcp.resources.read('config://server-info');
|
|
321
|
+
|
|
322
|
+
expect(result.contents).toHaveLength(1);
|
|
323
|
+
expect(result.contents[0]).toHaveProperty('mimeType', 'application/json');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test('prompts return well-formed messages', async ({ mcp }) => {
|
|
327
|
+
const result = await mcp.prompts.get('summarize', { topic: 'testing' });
|
|
328
|
+
|
|
329
|
+
expect(result.messages).toBeDefined();
|
|
330
|
+
expect(result.messages.length).toBeGreaterThan(0);
|
|
331
|
+
});
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### Manual Client E2E Tests
|
|
335
|
+
|
|
336
|
+
For more control, use `McpTestClient` and `TestServer` directly:
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
// advanced.e2e.spec.ts
|
|
340
|
+
import { McpTestClient, TestServer } from '@frontmcp/testing';
|
|
341
|
+
|
|
342
|
+
describe('Advanced E2E', () => {
|
|
343
|
+
let server: TestServer;
|
|
344
|
+
let client: McpTestClient;
|
|
345
|
+
|
|
346
|
+
beforeAll(async () => {
|
|
347
|
+
server = await TestServer.start({
|
|
348
|
+
command: 'npx tsx src/main.ts',
|
|
349
|
+
port: 3004,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
client = await McpTestClient.create({ baseUrl: server.info.baseUrl })
|
|
353
|
+
.withTransport('streamable-http')
|
|
354
|
+
.buildAndConnect();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
afterAll(async () => {
|
|
358
|
+
await client.disconnect();
|
|
359
|
+
await server.stop();
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should list tools after initialization', async () => {
|
|
363
|
+
const tools = await client.tools.list();
|
|
364
|
+
expect(tools.length).toBeGreaterThan(0);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should handle tool errors gracefully', async () => {
|
|
368
|
+
const result = await client.tools.call('nonexistent_tool', {});
|
|
369
|
+
expect(result).toBeError();
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### Testing with Authentication
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
import { test, expect, TestTokenFactory } from '@frontmcp/testing';
|
|
378
|
+
|
|
379
|
+
test.use({
|
|
380
|
+
server: './src/main.ts',
|
|
381
|
+
port: 3005,
|
|
382
|
+
auth: {
|
|
383
|
+
issuer: 'https://auth.example.com/',
|
|
384
|
+
audience: 'https://api.example.com',
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test('authenticated tool call succeeds', async ({ mcp, auth }) => {
|
|
389
|
+
const token = await auth.createToken({ sub: 'user-123', scopes: ['tools:read'] });
|
|
390
|
+
mcp.setAuthToken(token);
|
|
391
|
+
|
|
392
|
+
const result = await mcp.tools.call('get_user_profile', {});
|
|
393
|
+
expect(result).toBeSuccessful();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test('unauthenticated call is rejected', async ({ mcp }) => {
|
|
397
|
+
mcp.clearAuthToken();
|
|
398
|
+
|
|
399
|
+
const result = await mcp.tools.call('get_user_profile', {});
|
|
400
|
+
expect(result).toBeError();
|
|
401
|
+
});
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
## Custom MCP Matchers
|
|
405
|
+
|
|
406
|
+
`@frontmcp/testing` provides Jest matchers tailored for MCP responses. Import `expect` from `@frontmcp/testing` instead of from Jest:
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
import { expect } from '@frontmcp/testing';
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
| Matcher | Asserts |
|
|
413
|
+
| ------------------------- | ----------------------------------------------------- |
|
|
414
|
+
| `toContainTool(name)` | Tools list includes a tool with the given name |
|
|
415
|
+
| `toContainResource(uri)` | Resources list includes a resource with the given URI |
|
|
416
|
+
| `toContainPrompt(name)` | Prompts list includes a prompt with the given name |
|
|
417
|
+
| `toBeSuccessful()` | Tool call result is not an error |
|
|
418
|
+
| `toBeError()` | Tool call result is an MCP error |
|
|
419
|
+
| `toHaveTextContent(text)` | Result contains text content matching the string |
|
|
420
|
+
| `toHaveMimeType(mime)` | Resource content has the expected MIME type |
|
|
421
|
+
|
|
422
|
+
## Running Tests with Nx
|
|
423
|
+
|
|
424
|
+
FrontMCP uses Nx as its build system. Run tests with these commands:
|
|
425
|
+
|
|
426
|
+
```bash
|
|
427
|
+
# Run all tests for a specific library
|
|
428
|
+
nx test sdk
|
|
429
|
+
|
|
430
|
+
# Run tests for a specific file
|
|
431
|
+
nx test my-app --testFile=src/tools/__tests__/my-tool.spec.ts
|
|
432
|
+
|
|
433
|
+
# Run all tests across the monorepo
|
|
434
|
+
nx run-many -t test
|
|
435
|
+
|
|
436
|
+
# Run with coverage
|
|
437
|
+
nx test sdk --coverage
|
|
438
|
+
|
|
439
|
+
# Run only E2E tests (by pattern)
|
|
440
|
+
nx test sdk --testPathPattern='\.e2e\.spec\.ts$'
|
|
441
|
+
|
|
442
|
+
# Run a single test by name
|
|
443
|
+
nx test sdk --testNamePattern='should return formatted output'
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
## Jest Configuration
|
|
447
|
+
|
|
448
|
+
Each library has its own `jest.config.ts`. Coverage thresholds are enforced per library:
|
|
449
|
+
|
|
450
|
+
```typescript
|
|
451
|
+
// jest.config.ts
|
|
452
|
+
export default {
|
|
453
|
+
displayName: 'my-lib',
|
|
454
|
+
preset: '../../jest.preset.js',
|
|
455
|
+
transform: {
|
|
456
|
+
'^.+\\.tsx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
|
457
|
+
},
|
|
458
|
+
coverageThreshold: {
|
|
459
|
+
global: {
|
|
460
|
+
statements: 95,
|
|
461
|
+
branches: 95,
|
|
462
|
+
functions: 95,
|
|
463
|
+
lines: 95,
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
## Manual Testing with frontmcp dev
|
|
470
|
+
|
|
471
|
+
For interactive development and manual testing, use the CLI:
|
|
472
|
+
|
|
473
|
+
```bash
|
|
474
|
+
# Start the dev server with hot reload
|
|
475
|
+
frontmcp dev
|
|
476
|
+
|
|
477
|
+
# Start on a specific port
|
|
478
|
+
frontmcp dev --port 4000
|
|
479
|
+
|
|
480
|
+
# The dev server exposes your MCP server over Streamable HTTP
|
|
481
|
+
# Connect any MCP client (Claude Desktop, cursor, etc.) to test interactively
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
This is useful for:
|
|
485
|
+
|
|
486
|
+
- Verifying tool behavior with a real AI client
|
|
487
|
+
- Testing the full request/response cycle
|
|
488
|
+
- Debugging issues that are hard to reproduce in automated tests
|
|
489
|
+
- Validating authentication flows end-to-end
|
|
490
|
+
|
|
491
|
+
## Cleanup Before Committing
|
|
492
|
+
|
|
493
|
+
Always run the unused import cleanup script on changed files:
|
|
494
|
+
|
|
495
|
+
```bash
|
|
496
|
+
# Remove unused imports from files changed vs main
|
|
497
|
+
node scripts/fix-unused-imports.mjs
|
|
498
|
+
|
|
499
|
+
# Custom base branch
|
|
500
|
+
node scripts/fix-unused-imports.mjs feature/my-branch
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
## Testing Patterns Summary
|
|
504
|
+
|
|
505
|
+
| What to Test | How | File Suffix |
|
|
506
|
+
| ------------------------ | ------------------------------------------------- | --------------- |
|
|
507
|
+
| Tool execute logic | Unit test with mock context | `.spec.ts` |
|
|
508
|
+
| Resource read logic | Unit test with mock params | `.spec.ts` |
|
|
509
|
+
| Prompt output shape | Unit test verifying GetPromptResult | `.spec.ts` |
|
|
510
|
+
| Full MCP protocol flow | E2E with McpTestClient | `.e2e.spec.ts` |
|
|
511
|
+
| Error handling | Unit test verifying specific error classes/codes | `.spec.ts` |
|
|
512
|
+
| Plugin behavior | Unit test providers + integration via test server | `.spec.ts` |
|
|
513
|
+
| Performance regression | Perf tests with MetricsCollector | `.perf.spec.ts` |
|
|
514
|
+
| Playwright browser tests | UI tests with Playwright | `.pw.spec.ts` |
|
|
515
|
+
| Constructor validation | Unit test verifying throws on invalid input | `.spec.ts` |
|
|
516
|
+
|
|
517
|
+
## Common Mistakes
|
|
518
|
+
|
|
519
|
+
- **Using `.test.ts` file extension** -- all test files must use `.spec.ts`. The Nx and Jest configurations expect this convention.
|
|
520
|
+
- **Testing implementation details** -- test inputs and outputs, not internal method calls. Tools should be tested through their `execute` interface.
|
|
521
|
+
- **Skipping constructor validation tests** -- always test that constructors throw on invalid input.
|
|
522
|
+
- **Skipping error `instanceof` checks** -- verify that thrown errors are instances of the correct error class, not just that an error was thrown.
|
|
523
|
+
- **Using test ID prefixes** -- do not use prefixes like "PT-001" in test names. Use descriptive names like "should return formatted output for valid input".
|
|
524
|
+
- **Falling below 95% coverage** -- the CI pipeline enforces coverage thresholds. Run `nx test <lib> --coverage` locally before pushing.
|
|
525
|
+
- **Using `any` in test mocks** -- use `unknown` or properly typed mocks. Follow the strict TypeScript guidelines.
|
|
526
|
+
|
|
527
|
+
## Reference
|
|
528
|
+
|
|
529
|
+
- Testing package: [`@frontmcp/testing`](https://docs.agentfront.dev/frontmcp/testing/overview)
|
|
530
|
+
- Test client: `McpTestClient` — import from `@frontmcp/testing`
|
|
531
|
+
- Test client builder: `McpTestClient.builder()` — fluent API for test setup
|
|
532
|
+
- MCP matchers: `toContainTool()`, `toBeSuccessful()` — import from `@frontmcp/testing`
|
|
533
|
+
- Test fixtures: `createTestFixture()` — import from `@frontmcp/testing`
|
|
534
|
+
- Test server: `TestServer` — import from `@frontmcp/testing`
|
|
535
|
+
- Performance testing: `perfTest()`, `MetricsCollector` — import from `@frontmcp/testing`
|
|
536
|
+
- Auth testing: `TestTokenFactory`, `MockOAuthServer` — import from `@frontmcp/testing`
|
|
537
|
+
- Interceptors: `TestInterceptor` — import from `@frontmcp/testing`
|
|
538
|
+
- HTTP mocking: `HttpMock` — import from `@frontmcp/testing`
|
|
539
|
+
- [Source code on GitHub](https://github.com/agentfront/frontmcp/tree/main/libs/testing)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Testing with Authentication
|
|
2
|
+
|
|
3
|
+
```typescript
|
|
4
|
+
import { McpTestClient, TestServer, TestTokenFactory, MockOAuthServer } from '@frontmcp/testing';
|
|
5
|
+
import Server from '../src/main';
|
|
6
|
+
|
|
7
|
+
describe('Authenticated Server', () => {
|
|
8
|
+
let server: TestServer;
|
|
9
|
+
let tokenFactory: TestTokenFactory;
|
|
10
|
+
|
|
11
|
+
beforeAll(async () => {
|
|
12
|
+
server = await TestServer.create(Server);
|
|
13
|
+
tokenFactory = new TestTokenFactory({
|
|
14
|
+
issuer: 'https://test-idp.example.com',
|
|
15
|
+
audience: 'my-api',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterAll(async () => {
|
|
20
|
+
await server.dispose();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should reject unauthenticated requests', async () => {
|
|
24
|
+
const client = await server.connect();
|
|
25
|
+
const result = await client.callTool('protected_tool', {});
|
|
26
|
+
expect(result.isError).toBe(true);
|
|
27
|
+
await client.close();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should accept valid token', async () => {
|
|
31
|
+
const token = await tokenFactory.createToken({
|
|
32
|
+
sub: 'user-123',
|
|
33
|
+
scopes: ['read', 'write'],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const client = await server.connect({ authToken: token });
|
|
37
|
+
const result = await client.callTool('protected_tool', { data: 'test' });
|
|
38
|
+
expect(result).toBeSuccessful();
|
|
39
|
+
await client.close();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should enforce role-based access', async () => {
|
|
43
|
+
const adminToken = await tokenFactory.createToken({
|
|
44
|
+
sub: 'admin-1',
|
|
45
|
+
roles: ['admin'],
|
|
46
|
+
});
|
|
47
|
+
const userToken = await tokenFactory.createToken({
|
|
48
|
+
sub: 'user-1',
|
|
49
|
+
roles: ['user'],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const adminClient = await server.connect({ authToken: adminToken });
|
|
53
|
+
const adminResult = await adminClient.callTool('admin_only_tool', {});
|
|
54
|
+
expect(adminResult).toBeSuccessful();
|
|
55
|
+
|
|
56
|
+
const userClient = await server.connect({ authToken: userToken });
|
|
57
|
+
const userResult = await userClient.callTool('admin_only_tool', {});
|
|
58
|
+
expect(userResult.isError).toBe(true);
|
|
59
|
+
|
|
60
|
+
await adminClient.close();
|
|
61
|
+
await userClient.close();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('OAuth Flow', () => {
|
|
66
|
+
let mockOAuth: MockOAuthServer;
|
|
67
|
+
|
|
68
|
+
beforeAll(async () => {
|
|
69
|
+
mockOAuth = await MockOAuthServer.create({
|
|
70
|
+
issuer: 'https://test-idp.example.com',
|
|
71
|
+
port: 9999,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
afterAll(async () => {
|
|
76
|
+
await mockOAuth.close();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should complete OAuth authorization code flow', async () => {
|
|
80
|
+
const { authorizationUrl } = await mockOAuth.startFlow({
|
|
81
|
+
clientId: 'test-client',
|
|
82
|
+
redirectUri: 'http://localhost:3001/callback',
|
|
83
|
+
scopes: ['openid', 'profile'],
|
|
84
|
+
});
|
|
85
|
+
expect(authorizationUrl).toContain('code=');
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
```
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Testing Browser Build
|
|
2
|
+
|
|
3
|
+
After building with `frontmcp build --target browser`, validate the output:
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
const DIST_DIR = path.resolve(__dirname, '../dist/browser');
|
|
10
|
+
|
|
11
|
+
describe('Browser Build', () => {
|
|
12
|
+
it('should produce browser-compatible bundle', () => {
|
|
13
|
+
const files = fs.readdirSync(DIST_DIR);
|
|
14
|
+
expect(files.some((f) => f.endsWith('.js'))).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should not contain Node.js-only modules', () => {
|
|
18
|
+
const bundle = fs.readFileSync(path.join(DIST_DIR, 'index.js'), 'utf-8');
|
|
19
|
+
// These should be polyfilled or excluded
|
|
20
|
+
expect(bundle).not.toContain("require('fs')");
|
|
21
|
+
expect(bundle).not.toContain("require('child_process')");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should export expected functions', async () => {
|
|
25
|
+
// Use dynamic import to test ESM compatibility
|
|
26
|
+
const mod = await import(path.join(DIST_DIR, 'index.js'));
|
|
27
|
+
expect(mod).toBeDefined();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Testing with Playwright (.pw.spec.ts)
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { test, expect } from '@playwright/test';
|
|
36
|
+
|
|
37
|
+
test('browser MCP client loads tools', async ({ page }) => {
|
|
38
|
+
await page.goto('http://localhost:3000');
|
|
39
|
+
|
|
40
|
+
// Wait for tools to load from MCP server
|
|
41
|
+
await page.waitForSelector('[data-testid="tool-list"]');
|
|
42
|
+
|
|
43
|
+
const tools = await page.locator('[data-testid="tool-item"]').count();
|
|
44
|
+
expect(tools).toBeGreaterThan(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('browser client can call a tool', async ({ page }) => {
|
|
48
|
+
await page.goto('http://localhost:3000');
|
|
49
|
+
|
|
50
|
+
await page.fill('[data-testid="input-a"]', '5');
|
|
51
|
+
await page.fill('[data-testid="input-b"]', '3');
|
|
52
|
+
await page.click('[data-testid="call-tool"]');
|
|
53
|
+
|
|
54
|
+
const result = await page.textContent('[data-testid="result"]');
|
|
55
|
+
expect(result).toContain('8');
|
|
56
|
+
});
|
|
57
|
+
```
|