@frontmcp/skills 1.6.1 → 1.7.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/catalog/frontmcp-config/examples/configure-http/cors-restricted-origins.md +2 -2
- package/catalog/frontmcp-config/references/configure-http.md +72 -18
- package/catalog/frontmcp-deployment/examples/build-for-sdk/create-flat-config.md +4 -0
- package/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md +2 -0
- package/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md +2 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/basic-multistage-dockerfile.md +2 -0
- package/catalog/frontmcp-deployment/examples/deploy-to-node-dockerfile/secure-nonroot-dockerfile.md +2 -0
- package/catalog/frontmcp-deployment/references/build-for-sdk.md +29 -3
- package/catalog/frontmcp-deployment/references/deploy-to-node-dockerfile.md +2 -0
- package/catalog/frontmcp-deployment/references/deploy-to-node.md +11 -8
- package/catalog/frontmcp-production-readiness/examples/production-node-server/docker-multi-stage.md +2 -0
- package/catalog/frontmcp-testing/SKILL.md +19 -18
- package/catalog/frontmcp-testing/examples/setup-testing/jest-config-with-coverage.md +8 -1
- package/catalog/frontmcp-testing/references/setup-testing.md +29 -1
- package/catalog/skills-manifest.json +1 -1
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@ level: basic
|
|
|
5
5
|
description: 'Configure CORS to allow only specific frontend origins with credentials.'
|
|
6
6
|
tags: [config, browser, http, cors, restricted, origins]
|
|
7
7
|
features:
|
|
8
|
-
- '
|
|
8
|
+
- 'Naming the origins a browser may read from — omitting `cors` sends no headers at all'
|
|
9
9
|
- 'Enabling `credentials: true` with specific origins (required -- browsers reject `*` with credentials)'
|
|
10
10
|
- 'Setting `maxAge` to reduce preflight request overhead'
|
|
11
11
|
- 'Reading port from an environment variable with a fallback'
|
|
@@ -41,7 +41,7 @@ class Server {}
|
|
|
41
41
|
|
|
42
42
|
## What This Demonstrates
|
|
43
43
|
|
|
44
|
-
-
|
|
44
|
+
- Naming the origins a browser may read from — omitting `cors` sends no headers at all
|
|
45
45
|
- Enabling `credentials: true` with specific origins (required -- browsers reject `*` with credentials)
|
|
46
46
|
- Setting `maxAge` to reduce preflight request overhead
|
|
47
47
|
- Reading port from an environment variable with a fallback
|
|
@@ -33,7 +33,9 @@ Configure the HTTP server — port, CORS policy, unix sockets, entry path prefix
|
|
|
33
33
|
- Only need rate limiting or IP filtering without changing HTTP binding -- use `configure-throttle`
|
|
34
34
|
- Need to configure TLS/HTTPS termination -- handle at the reverse proxy or load balancer level, not in FrontMCP
|
|
35
35
|
|
|
36
|
-
> **Decision:** Use this skill when you need to customize how the HTTP listener binds (port, socket, prefix) or how it handles CORS; skip if the
|
|
36
|
+
> **Decision:** Use this skill when you need to customize how the HTTP listener binds (port, socket, prefix, network interface) or how it handles CORS; skip if the defaults are sufficient — port 3000, bound to loopback, sending no CORS headers.
|
|
37
|
+
|
|
38
|
+
> **Changed in v1.7.0.** The server now binds `127.0.0.1` (was `0.0.0.0`) and omitting `cors` now sends no CORS headers (was `{ origin: true }`). Reaching the server from another host or another origin is an explicit opt-in — see [Network Binding](#network-binding) and [CORS Configuration](#cors-configuration).
|
|
37
39
|
|
|
38
40
|
## HttpOptionsInput
|
|
39
41
|
|
|
@@ -46,7 +48,7 @@ Configure the HTTP server — port, CORS policy, unix sockets, entry path prefix
|
|
|
46
48
|
entryPath: '', // default: '' (root)
|
|
47
49
|
socketPath: undefined, // unix socket path (overrides port)
|
|
48
50
|
cors: {
|
|
49
|
-
// default:
|
|
51
|
+
// default: undefined — no CORS headers at all
|
|
50
52
|
origin: ['https://myapp.com'],
|
|
51
53
|
credentials: true,
|
|
52
54
|
maxAge: 86400,
|
|
@@ -87,15 +89,61 @@ http: {
|
|
|
87
89
|
}
|
|
88
90
|
```
|
|
89
91
|
|
|
92
|
+
## Network Binding
|
|
93
|
+
|
|
94
|
+
The server binds `127.0.0.1` unless told otherwise — a server that says nothing about security is
|
|
95
|
+
local-only. The effective address is resolved in this order, first match wins:
|
|
96
|
+
|
|
97
|
+
1. `http.security.bindAddress` — `'loopback'`, `'all'`, or a literal address
|
|
98
|
+
2. The `FRONTMCP_BIND_ADDRESS` environment variable — same three forms
|
|
99
|
+
3. Strict mode (`http.security.strict`) — `0.0.0.0` when distributed, `127.0.0.1` otherwise
|
|
100
|
+
4. A distributed build (`FRONTMCP_DEPLOYMENT_MODE=distributed`) — `0.0.0.0`
|
|
101
|
+
5. The default — `127.0.0.1`
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
http: {
|
|
105
|
+
security: {
|
|
106
|
+
bindAddress: 'all', // 0.0.0.0 — reachable from other hosts
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
In a container, prefer the environment variable: a Dockerfile cannot reach into the server's
|
|
112
|
+
TypeScript config, and the env var needs no rebuild.
|
|
113
|
+
|
|
114
|
+
```dockerfile
|
|
115
|
+
ENV FRONTMCP_BIND_ADDRESS=all
|
|
116
|
+
EXPOSE 3000
|
|
117
|
+
```
|
|
118
|
+
|
|
90
119
|
## CORS Configuration
|
|
91
120
|
|
|
92
|
-
###
|
|
121
|
+
### No CORS Headers (Default)
|
|
93
122
|
|
|
94
|
-
When `cors` is not specified, the server
|
|
123
|
+
When `cors` is not specified, the server sends **no CORS headers**. A cross-origin request still
|
|
124
|
+
reaches the server and is served normally — the browser simply refuses to let the calling page read
|
|
125
|
+
the response. Non-browser clients are unaffected: CORS is a browser rule, **not server-side access
|
|
126
|
+
control**. If you need to keep callers out, use authentication.
|
|
127
|
+
|
|
128
|
+
`cors: {}` and `cors: { origin: false }` behave identically to omitting the option — the middleware
|
|
129
|
+
is installed only when `origin` is set to something other than `false`.
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// No CORS headers (default behavior)
|
|
133
|
+
http: {
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Allow Any Origin
|
|
138
|
+
|
|
139
|
+
The pre-v1.7.0 default. Reflects whatever `Origin` the request carries, so any page a user visits
|
|
140
|
+
can read this server's responses — never use it in production.
|
|
95
141
|
|
|
96
142
|
```typescript
|
|
97
|
-
// All origins allowed (default behavior)
|
|
98
143
|
http: {
|
|
144
|
+
cors: {
|
|
145
|
+
origin: true,
|
|
146
|
+
},
|
|
99
147
|
}
|
|
100
148
|
```
|
|
101
149
|
|
|
@@ -139,11 +187,11 @@ http: {
|
|
|
139
187
|
|
|
140
188
|
### CORS Fields
|
|
141
189
|
|
|
142
|
-
| Field | Type | Default
|
|
143
|
-
| ------------- | ------------------------------------------- |
|
|
144
|
-
| `origin` | `boolean \| string \| string[] \| function` | `
|
|
145
|
-
| `credentials` | `boolean` | `false`
|
|
146
|
-
| `maxAge` | `number` | —
|
|
190
|
+
| Field | Type | Default | Description |
|
|
191
|
+
| ------------- | ------------------------------------------- | ------- | ---------------------------------------------------------------------------------- |
|
|
192
|
+
| `origin` | `boolean \| string \| string[] \| function` | none | Allowed origins. No default — omitting it (or `false`) installs no CORS middleware |
|
|
193
|
+
| `credentials` | `boolean` | `false` | Allow cookies/auth headers |
|
|
194
|
+
| `maxAge` | `number` | — | Preflight cache duration (seconds) |
|
|
147
195
|
|
|
148
196
|
## Request Body Limits
|
|
149
197
|
|
|
@@ -345,13 +393,13 @@ curl --unix-socket /tmp/my-mcp-server.sock http://localhost/
|
|
|
345
393
|
|
|
346
394
|
## Common Patterns
|
|
347
395
|
|
|
348
|
-
| Pattern | Correct | Incorrect | Why
|
|
349
|
-
| --------------------- | ------------------------------------------------------------ | -------------------------------------------- |
|
|
350
|
-
| Port from environment | `port: Number(process.env.PORT) \|\| 3000` | `port: process.env.PORT` | The `port` field expects a number; passing a string causes a silent bind failure
|
|
351
|
-
| CORS with credentials | `cors: { origin: ['https://myapp.com'], credentials: true }` | `cors: { origin: true, credentials: true }` | Browsers reject `Access-Control-Allow-Origin: *` when credentials are enabled; you must list explicit origins
|
|
352
|
-
| Unix socket mode | `socketPath: '/tmp/my-mcp.sock'` with no `port` field | Setting both `socketPath` and `port` | When `socketPath` is set, `port` is silently ignored which can cause confusion during debugging
|
|
353
|
-
| Entry path prefix | `entryPath: '/api/mcp'` (no trailing slash) | `entryPath: '/api/mcp/'` with trailing slash | Trailing slashes cause double-slash issues in route matching (e.g., `/api/mcp//sse`)
|
|
354
|
-
|
|
|
396
|
+
| Pattern | Correct | Incorrect | Why |
|
|
397
|
+
| --------------------- | ------------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
398
|
+
| Port from environment | `port: Number(process.env.PORT) \|\| 3000` | `port: process.env.PORT` | The `port` field expects a number; passing a string causes a silent bind failure |
|
|
399
|
+
| CORS with credentials | `cors: { origin: ['https://myapp.com'], credentials: true }` | `cors: { origin: true, credentials: true }` | Browsers reject `Access-Control-Allow-Origin: *` when credentials are enabled; you must list explicit origins |
|
|
400
|
+
| Unix socket mode | `socketPath: '/tmp/my-mcp.sock'` with no `port` field | Setting both `socketPath` and `port` | When `socketPath` is set, `port` is silently ignored which can cause confusion during debugging |
|
|
401
|
+
| Entry path prefix | `entryPath: '/api/mcp'` (no trailing slash) | `entryPath: '/api/mcp/'` with trailing slash | Trailing slashes cause double-slash issues in route matching (e.g., `/api/mcp//sse`) |
|
|
402
|
+
| Allowing cross-origin | `cors: { origin: ['https://myapp.com'] }` | Omitting the `cors` field entirely | Omitting `cors` (like `cors: false`) sends no CORS headers at all; a browser on another origin needs an explicit `origin` list |
|
|
355
403
|
|
|
356
404
|
## Verification Checklist
|
|
357
405
|
|
|
@@ -362,8 +410,14 @@ curl --unix-socket /tmp/my-mcp-server.sock http://localhost/
|
|
|
362
410
|
- [ ] If `socketPath` is set, `port` is removed or commented out to avoid confusion
|
|
363
411
|
- [ ] `entryPath` does not have a trailing slash
|
|
364
412
|
|
|
413
|
+
### Network Binding
|
|
414
|
+
|
|
415
|
+
- [ ] If the server must be reachable from another host, `security.bindAddress` or `FRONTMCP_BIND_ADDRESS` is set — the default is loopback-only
|
|
416
|
+
- [ ] Containers set `FRONTMCP_BIND_ADDRESS=all` so the published port reaches the process
|
|
417
|
+
|
|
365
418
|
### CORS
|
|
366
419
|
|
|
420
|
+
- [ ] `cors` is set only if a browser on another origin needs access — omitting it sends no headers
|
|
367
421
|
- [ ] If `credentials: true`, `origin` lists explicit allowed origins (not `true` or `*`)
|
|
368
422
|
- [ ] `maxAge` is set to a reasonable value for production (e.g., `86400` for 24 hours)
|
|
369
423
|
- [ ] Dynamic origin function handles `undefined` origin (non-browser requests)
|
|
@@ -390,7 +444,7 @@ curl --unix-socket /tmp/my-mcp-server.sock http://localhost/
|
|
|
390
444
|
| CORS errors in the browser console | Origin not included in the `cors.origin` list or `credentials: true` with wildcard origin | Add the frontend origin to the `origin` array and ensure credentials and origin settings are compatible |
|
|
391
445
|
| Unix socket file not created | Missing write permissions on the target directory or stale socket file from a previous run | Check directory permissions and remove the stale `.sock` file before restarting |
|
|
392
446
|
| Routes return 404 after setting `entryPath` | Client is still requesting the root path without the prefix | Update client base URL to include the entry path (e.g., `http://localhost:3000/api/mcp`) |
|
|
393
|
-
| Server binds but external clients cannot connect |
|
|
447
|
+
| Server binds but external clients cannot connect | The default bind address is `127.0.0.1`, which a published container port cannot reach | Set `FRONTMCP_BIND_ADDRESS=all` in the container (or `http.security.bindAddress: 'all'` in the config) |
|
|
394
448
|
| `413 Payload Too Large` with JSON-RPC envelope | Request body exceeded `bodyLimit` (default `'4mb'`) | Raise `http.bodyLimit` to fit the payload, or move large blobs to a separate upload endpoint |
|
|
395
449
|
| Server throws at startup mentioning a "reserved" path | A custom `http.routes` path collides with the MCP entry path, `/oauth/*`, `/.well-known/*`, `/health`, or `/metrics` | Rename the custom route to a non-reserved path |
|
|
396
450
|
| Custom route returns JSON when HTML/bytes expected | The Express adapter defaults responses to `application/json` | Set `res.setHeader('Content-Type', ...)` (or `res.type(...)`) in the handler before sending the body |
|
|
@@ -42,6 +42,10 @@ async function main() {
|
|
|
42
42
|
case 'multiply':
|
|
43
43
|
return { result: input.a * input.b };
|
|
44
44
|
case 'divide':
|
|
45
|
+
// `z.number()` accepts 0, so guard the divisor: 1/0 is Infinity, which JSON
|
|
46
|
+
// serialises as null, and 0/0 is NaN, which the output schema rejects with an
|
|
47
|
+
// opaque error. Fail with a message the caller can act on instead.
|
|
48
|
+
if (input.b === 0) throw new Error('Cannot divide by zero');
|
|
45
49
|
return { result: input.a / input.b };
|
|
46
50
|
}
|
|
47
51
|
}),
|
|
@@ -70,6 +70,8 @@ RUN npx frontmcp build --target node
|
|
|
70
70
|
FROM node:24-alpine AS production
|
|
71
71
|
WORKDIR /app
|
|
72
72
|
ENV NODE_ENV=production
|
|
73
|
+
# The server binds 127.0.0.1 by default, which a published container port cannot reach.
|
|
74
|
+
ENV FRONTMCP_BIND_ADDRESS=all
|
|
73
75
|
COPY --from=builder /app/dist ./dist
|
|
74
76
|
COPY --from=builder /app/package.json ./
|
|
75
77
|
COPY --from=builder /app/yarn.lock ./
|
|
@@ -54,7 +54,8 @@ server {
|
|
|
54
54
|
# .env
|
|
55
55
|
PORT=3000
|
|
56
56
|
NODE_ENV=production
|
|
57
|
-
|
|
57
|
+
# No FRONTMCP_BIND_ADDRESS here on purpose: the server binds 127.0.0.1 by default,
|
|
58
|
+
# which is exactly right when NGINX on the same host is the only thing talking to it.
|
|
58
59
|
REDIS_URL=redis://localhost:6379
|
|
59
60
|
LOG_LEVEL=info
|
|
60
61
|
```
|
|
@@ -31,6 +31,8 @@ RUN yarn frontmcp build --target node
|
|
|
31
31
|
FROM node:24-alpine AS production
|
|
32
32
|
WORKDIR /app
|
|
33
33
|
ENV NODE_ENV=production
|
|
34
|
+
# The server binds 127.0.0.1 by default, which a published container port cannot reach.
|
|
35
|
+
ENV FRONTMCP_BIND_ADDRESS=all
|
|
34
36
|
ENV PORT=3000
|
|
35
37
|
COPY --from=builder /app/dist ./dist
|
|
36
38
|
COPY --from=builder /app/package.json ./
|
|
@@ -69,15 +69,34 @@ const server = await create({
|
|
|
69
69
|
tool({
|
|
70
70
|
name: 'calculate',
|
|
71
71
|
description: 'Perform calculation',
|
|
72
|
-
inputSchema: {
|
|
72
|
+
inputSchema: {
|
|
73
|
+
a: z.number(),
|
|
74
|
+
b: z.number(),
|
|
75
|
+
operation: z.enum(['add', 'subtract', 'multiply', 'divide']),
|
|
76
|
+
},
|
|
73
77
|
outputSchema: { result: z.number() },
|
|
74
|
-
})((input) =>
|
|
78
|
+
})((input) => {
|
|
79
|
+
switch (input.operation) {
|
|
80
|
+
case 'add':
|
|
81
|
+
return { result: input.a + input.b };
|
|
82
|
+
case 'subtract':
|
|
83
|
+
return { result: input.a - input.b };
|
|
84
|
+
case 'multiply':
|
|
85
|
+
return { result: input.a * input.b };
|
|
86
|
+
case 'divide':
|
|
87
|
+
// `z.number()` accepts 0, so guard the divisor: 1/0 is Infinity, which JSON
|
|
88
|
+
// serialises as null, and 0/0 is NaN, which the output schema rejects with an
|
|
89
|
+
// opaque error. Fail with a message the caller can act on instead.
|
|
90
|
+
if (input.b === 0) throw new Error('Cannot divide by zero');
|
|
91
|
+
return { result: input.a / input.b };
|
|
92
|
+
}
|
|
93
|
+
}),
|
|
75
94
|
],
|
|
76
95
|
cacheKey: 'my-service', // Reuse same instance on repeated calls
|
|
77
96
|
});
|
|
78
97
|
|
|
79
98
|
// Call tools directly
|
|
80
|
-
const result = await server.callTool('calculate', {
|
|
99
|
+
const result = await server.callTool('calculate', { a: 2, b: 2, operation: 'add' });
|
|
81
100
|
|
|
82
101
|
// List available tools
|
|
83
102
|
const { tools } = await server.listTools();
|
|
@@ -86,6 +105,13 @@ const { tools } = await server.listTools();
|
|
|
86
105
|
await server.dispose();
|
|
87
106
|
```
|
|
88
107
|
|
|
108
|
+
> **Never `eval()` tool input.** A tool's arguments come from the MCP caller, or from an LLM acting
|
|
109
|
+
> on caller-controlled prompts — they are untrusted by definition. `eval` on that value runs with the
|
|
110
|
+
> embedding process's full authority: environment credentials, the filesystem, the network, and
|
|
111
|
+
> `process.getBuiltinModule('child_process')`. Output-schema validation cannot help, because the side
|
|
112
|
+
> effects happen before the result is validated. Model the operation in the schema, as above, so the
|
|
113
|
+
> set of things the tool can do is fixed at design time.
|
|
114
|
+
|
|
89
115
|
### CreateConfig Fields
|
|
90
116
|
|
|
91
117
|
```typescript
|
|
@@ -130,7 +130,10 @@ Create a `.env` file or set variables in your deployment environment:
|
|
|
130
130
|
# Server
|
|
131
131
|
PORT=3000
|
|
132
132
|
NODE_ENV=production
|
|
133
|
-
|
|
133
|
+
# The server binds 127.0.0.1 by default. Set this only when it must be reachable
|
|
134
|
+
# from another host — a container, a VM, direct access. Behind a reverse proxy on
|
|
135
|
+
# the same machine, leave it unset.
|
|
136
|
+
FRONTMCP_BIND_ADDRESS=all
|
|
134
137
|
|
|
135
138
|
# Redis (required for session storage in production)
|
|
136
139
|
REDIS_URL=redis://localhost:6379
|
|
@@ -139,13 +142,13 @@ REDIS_URL=redis://localhost:6379
|
|
|
139
142
|
LOG_LEVEL=info
|
|
140
143
|
```
|
|
141
144
|
|
|
142
|
-
| Variable
|
|
143
|
-
|
|
|
144
|
-
| `PORT`
|
|
145
|
-
| `NODE_ENV`
|
|
146
|
-
| `REDIS_URL`
|
|
147
|
-
| `
|
|
148
|
-
| `LOG_LEVEL`
|
|
145
|
+
| Variable | Description | Default |
|
|
146
|
+
| ----------------------- | ------------------------------------------------------------------ | ------------------------ |
|
|
147
|
+
| `PORT` | HTTP port for the server | `3000` |
|
|
148
|
+
| `NODE_ENV` | Runtime environment | `development` |
|
|
149
|
+
| `REDIS_URL` | Redis connection string for storage | (none) |
|
|
150
|
+
| `FRONTMCP_BIND_ADDRESS` | Network interface to bind: `all`, `loopback`, or a literal address | `loopback` (`127.0.0.1`) |
|
|
151
|
+
| `LOG_LEVEL` | Logging verbosity | `info` |
|
|
149
152
|
|
|
150
153
|
## Step 5: Health Checks
|
|
151
154
|
|
package/catalog/frontmcp-production-readiness/examples/production-node-server/docker-multi-stage.md
CHANGED
|
@@ -33,6 +33,8 @@ RUN npx frontmcp build
|
|
|
33
33
|
FROM node:24-slim AS runtime
|
|
34
34
|
WORKDIR /app
|
|
35
35
|
ENV NODE_ENV=production
|
|
36
|
+
# The server binds 127.0.0.1 by default, which a published container port cannot reach.
|
|
37
|
+
ENV FRONTMCP_BIND_ADDRESS=all
|
|
36
38
|
COPY package.json yarn.lock ./
|
|
37
39
|
RUN yarn install --frozen-lockfile --production && yarn cache clean
|
|
38
40
|
COPY --from=builder /app/dist ./dist
|
|
@@ -95,17 +95,17 @@ This is a router skill. Follow this order to pick a testing approach, then move
|
|
|
95
95
|
|
|
96
96
|
## Cross-Cutting Testing Patterns
|
|
97
97
|
|
|
98
|
-
| Pattern | Rule
|
|
99
|
-
| ------------------ |
|
|
100
|
-
| File naming | Always `.spec.ts` (not `.test.ts`); E2E uses `.e2e.spec.ts`
|
|
101
|
-
| File organization | Split E2E tests by app/feature: `e2e/calc.e2e.spec.ts`, `e2e/ecommerce.e2e.spec.ts`. Never put all tests in a single `server.e2e.spec.ts`
|
|
102
|
-
| Test runner | Standalone projects: use `frontmcp test` (auto-generates Jest/SWC config; discovers `src/**/*.spec.ts(x)`, `__tests__/**/*.spec.ts(x)`, and `e2e/**/*.e2e.spec.ts(x)`; transforms both `.ts` and `.tsx` with the automatic JSX runtime; delegates to a user-provided `jest.config.{ts,js,mjs,cjs,json}` if present). Nx monorepos: use `nx test <lib>` (resolves the project's `jest.config.ts`). Never invoke `jest --config ...` directly |
|
|
103
|
-
| Coverage threshold | 95%+ across statements, branches, functions, lines
|
|
104
|
-
| Test descriptions | Plain English, no prefixes like "PT-001"; describe behavior not implementation
|
|
105
|
-
| Mocking | Mock providers via DI token replacement, never mock the framework
|
|
106
|
-
| httpMock scope | `httpMock` intercepts HTTP in the **test process** only, NOT in the MCP server subprocess. Do not use httpMock to intercept server-to-API calls — those happen in the child process. Use httpMock for verifying client-to-server request shapes or mocking external APIs called from the test itself
|
|
107
|
-
| Error testing | Assert `instanceof` specific error class AND MCP error code
|
|
108
|
-
| Async | Always `await` async operations; use `expect(...).rejects.toThrow()` for async errors
|
|
98
|
+
| Pattern | Rule |
|
|
99
|
+
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
100
|
+
| File naming | Always `.spec.ts` (not `.test.ts`); E2E uses `.e2e.spec.ts` |
|
|
101
|
+
| File organization | Split E2E tests by app/feature: `e2e/calc.e2e.spec.ts`, `e2e/ecommerce.e2e.spec.ts`. Never put all tests in a single `server.e2e.spec.ts` |
|
|
102
|
+
| Test runner | Standalone projects: use `frontmcp test` (auto-generates Jest/SWC config; discovers `src/**/*.spec.ts(x)`, `__tests__/**/*.spec.ts(x)`, and `e2e/**/*.e2e.spec.ts(x)`; transforms both `.ts` and `.tsx` with the automatic JSX runtime; transpiles ESM-only deps such as `jose` under npm, yarn AND pnpm's `node_modules/.pnpm/` store — add your own via `test.esmPackages` in `frontmcp.config.ts`; delegates to a user-provided `jest.config.{ts,js,mjs,cjs,json}` if present, which drops the injected ESM transforms). Nx monorepos: use `nx test <lib>` (resolves the project's `jest.config.ts`). Never invoke `jest --config ...` directly |
|
|
103
|
+
| Coverage threshold | 95%+ across statements, branches, functions, lines |
|
|
104
|
+
| Test descriptions | Plain English, no prefixes like "PT-001"; describe behavior not implementation |
|
|
105
|
+
| Mocking | Mock providers via DI token replacement, never mock the framework |
|
|
106
|
+
| httpMock scope | `httpMock` intercepts HTTP in the **test process** only, NOT in the MCP server subprocess. Do not use httpMock to intercept server-to-API calls — those happen in the child process. Use httpMock for verifying client-to-server request shapes or mocking external APIs called from the test itself |
|
|
107
|
+
| Error testing | Assert `instanceof` specific error class AND MCP error code |
|
|
108
|
+
| Async | Always `await` async operations; use `expect(...).rejects.toThrow()` for async errors |
|
|
109
109
|
|
|
110
110
|
## Common Patterns
|
|
111
111
|
|
|
@@ -145,13 +145,14 @@ This is a router skill. Follow this order to pick a testing approach, then move
|
|
|
145
145
|
|
|
146
146
|
## Troubleshooting
|
|
147
147
|
|
|
148
|
-
| Problem
|
|
149
|
-
|
|
|
150
|
-
| Jest not finding test files
|
|
151
|
-
|
|
|
152
|
-
|
|
|
153
|
-
|
|
|
154
|
-
|
|
|
148
|
+
| Problem | Cause | Solution |
|
|
149
|
+
| ---------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
150
|
+
| Jest not finding test files | Wrong file extension (`.test.ts` instead of `.spec.ts`) | Rename to `.spec.ts`; check `testMatch` in jest.config |
|
|
151
|
+
| `SyntaxError: Unexpected token 'export'` | An ESM-only dependency is being ignored instead of transpiled | Add it to `test.esmPackages` in `frontmcp.config.ts`. With a hand-written `jest.config.ts`, use the pnpm-safe `transformIgnorePatterns` in [`setup-testing`](./references/setup-testing.md#jest-configuration) AND make sure `transform` matches `.js` (`^.+\.[tj]sx?$` + `allowJs`) — un-ignoring a file does nothing if no transform matches it |
|
|
152
|
+
| Coverage below 95% | Untested error paths or conditional branches | Run `frontmcp test --coverage` and inspect uncovered lines in the report |
|
|
153
|
+
| E2E test timeout | Server startup too slow or port conflict | Increase Jest timeout; use random port allocation |
|
|
154
|
+
| DI resolution fails in tests | Provider not registered in test scope | Register mock providers before creating the test context |
|
|
155
|
+
| Istanbul shows 0% on async methods | TypeScript source-map mismatch with Istanbul | Known issue with some TS compilation settings; verify coverage with actual test output |
|
|
155
156
|
|
|
156
157
|
## Examples
|
|
157
158
|
|
|
@@ -22,8 +22,14 @@ export default {
|
|
|
22
22
|
displayName: 'my-lib',
|
|
23
23
|
preset: '../../jest.preset.js',
|
|
24
24
|
transform: {
|
|
25
|
-
|
|
25
|
+
// Must cover `.js` too: `transformIgnorePatterns` only un-ignores a file,
|
|
26
|
+
// the transform still has to match it. An ESM dep's `.js` would otherwise
|
|
27
|
+
// reach Jest untransformed.
|
|
28
|
+
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
|
26
29
|
},
|
|
30
|
+
// ESM-only deps (jose, reached via @frontmcp/sdk) must be transpiled, not
|
|
31
|
+
// ignored. The `.pnpm` skip keeps this correct under pnpm's symlinked store.
|
|
32
|
+
transformIgnorePatterns: ['node_modules[/\\\\](?!\\.pnpm[/\\\\])(?!(jose)[/\\\\])'],
|
|
27
33
|
coverageThreshold: {
|
|
28
34
|
global: {
|
|
29
35
|
statements: 95,
|
|
@@ -42,6 +48,7 @@ export default {
|
|
|
42
48
|
"compilerOptions": {
|
|
43
49
|
"outDir": "../../dist/out-tsc",
|
|
44
50
|
"module": "commonjs",
|
|
51
|
+
"allowJs": true,
|
|
45
52
|
"types": ["jest", "node"]
|
|
46
53
|
},
|
|
47
54
|
"include": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.spec.tsx"]
|
|
@@ -432,8 +432,16 @@ export default {
|
|
|
432
432
|
displayName: 'my-lib',
|
|
433
433
|
preset: '../../jest.preset.js',
|
|
434
434
|
transform: {
|
|
435
|
-
|
|
435
|
+
// Must cover `.js` too: `transformIgnorePatterns` only un-ignores a file,
|
|
436
|
+
// the transform still has to match it. An ESM dep's `.js` would otherwise
|
|
437
|
+
// reach Jest untransformed.
|
|
438
|
+
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
|
|
436
439
|
},
|
|
440
|
+
// ESM-only deps (jose, reached via @frontmcp/sdk) must be transpiled, not
|
|
441
|
+
// ignored. Skipping the `.pnpm` segment re-anchors the regex on the inner
|
|
442
|
+
// `node_modules/`, so this holds under npm, yarn AND pnpm's symlinked store;
|
|
443
|
+
// the trailing character classes cover Windows separators.
|
|
444
|
+
transformIgnorePatterns: ['node_modules[/\\\\](?!\\.pnpm[/\\\\])(?!(jose)[/\\\\])'],
|
|
437
445
|
coverageThreshold: {
|
|
438
446
|
global: {
|
|
439
447
|
statements: 95,
|
|
@@ -445,6 +453,26 @@ export default {
|
|
|
445
453
|
};
|
|
446
454
|
```
|
|
447
455
|
|
|
456
|
+
Add further ESM-only packages to the alternation (`(jose|nanoid)`). A plain
|
|
457
|
+
`node_modules/(?!(jose)/)` silently breaks under pnpm: the real path is
|
|
458
|
+
`node_modules/.pnpm/jose@6.2.3/node_modules/jose/...`, the unanchored regex
|
|
459
|
+
matches at the first `node_modules/`, and the run fails with
|
|
460
|
+
`SyntaxError: Unexpected token 'export'`.
|
|
461
|
+
|
|
462
|
+
In standalone projects driven by `frontmcp test`, prefer `test.esmPackages` in
|
|
463
|
+
`frontmcp.config.ts` — the injected config already carries the pattern above:
|
|
464
|
+
|
|
465
|
+
```typescript
|
|
466
|
+
// frontmcp.config.ts
|
|
467
|
+
import { defineConfig } from 'frontmcp';
|
|
468
|
+
|
|
469
|
+
export default defineConfig({
|
|
470
|
+
name: 'my-server',
|
|
471
|
+
deployments: [{ target: 'node' }],
|
|
472
|
+
test: { esmPackages: ['nanoid'] },
|
|
473
|
+
});
|
|
474
|
+
```
|
|
475
|
+
|
|
448
476
|
## Manual Testing with frontmcp dev
|
|
449
477
|
|
|
450
478
|
For interactive development and manual testing, use the CLI:
|
|
@@ -781,7 +781,7 @@
|
|
|
781
781
|
"level": "basic",
|
|
782
782
|
"tags": ["config", "browser", "http", "cors", "restricted", "origins"],
|
|
783
783
|
"features": [
|
|
784
|
-
"
|
|
784
|
+
"Naming the origins a browser may read from — omitting `cors` sends no headers at all",
|
|
785
785
|
"Enabling `credentials: true` with specific origins (required -- browsers reject `*` with credentials)",
|
|
786
786
|
"Setting `maxAge` to reduce preflight request overhead",
|
|
787
787
|
"Reading port from an environment variable with a fallback"
|