@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.
@@ -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
- - 'Restricting CORS to explicit origins instead of the permissive default'
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
- - Restricting CORS to explicit origins instead of the permissive default
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 default port 3000 with permissive CORS is sufficient.
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: permissive (all origins)
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
- ### Permissive (Default)
121
+ ### No CORS Headers (Default)
93
122
 
94
- When `cors` is not specified, the server allows all origins without credentials:
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 | Description |
143
- | ------------- | ------------------------------------------- | ------------ | ---------------------------------- |
144
- | `origin` | `boolean \| string \| string[] \| function` | `true` (all) | Allowed origins |
145
- | `credentials` | `boolean` | `false` | Allow cookies/auth headers |
146
- | `maxAge` | `number` | — | Preflight cache duration (seconds) |
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
- | Disabling CORS | `cors: false` | Omitting the `cors` field entirely | Omitting `cors` applies permissive defaults (all origins allowed); set `false` explicitly to send no CORS headers |
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 | Server bound to `localhost` or `127.0.0.1` inside a container | Set `host: '0.0.0.0'` or use Docker port mapping to expose the container port |
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
- HOST=0.0.0.0
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 ./
@@ -50,6 +50,8 @@ USER frontmcp
50
50
 
51
51
  # Environment defaults
52
52
  ENV NODE_ENV=production
53
+ # The server binds 127.0.0.1 by default, which a published container port cannot reach.
54
+ ENV FRONTMCP_BIND_ADDRESS=all
53
55
  ENV PORT=3000
54
56
 
55
57
  EXPOSE 3000
@@ -69,15 +69,34 @@ const server = await create({
69
69
  tool({
70
70
  name: 'calculate',
71
71
  description: 'Perform calculation',
72
- inputSchema: { expression: z.string() },
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) => ({ result: eval(input.expression) })),
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', { expression: '2 + 2' });
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
@@ -65,6 +65,8 @@ USER frontmcp
65
65
 
66
66
  # Environment defaults
67
67
  ENV NODE_ENV=production
68
+ # The server binds 127.0.0.1 by default, which a published container port cannot reach.
69
+ ENV FRONTMCP_BIND_ADDRESS=all
68
70
  ENV PORT=3000
69
71
 
70
72
  EXPOSE 3000
@@ -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
- HOST=0.0.0.0
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 | Description | Default |
143
- | ----------- | ----------------------------------- | ------------- |
144
- | `PORT` | HTTP port for the server | `3000` |
145
- | `NODE_ENV` | Runtime environment | `development` |
146
- | `REDIS_URL` | Redis connection string for storage | (none) |
147
- | `HOST` | Network interface to bind | `0.0.0.0` |
148
- | `LOG_LEVEL` | Logging verbosity | `info` |
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
 
@@ -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 | 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
- | Coverage below 95% | Untested error paths or conditional branches | Run `frontmcp test --coverage` and inspect uncovered lines in the report |
152
- | E2E test timeout | Server startup too slow or port conflict | Increase Jest timeout; use random port allocation |
153
- | DI resolution fails in tests | Provider not registered in test scope | Register mock providers before creating the test context |
154
- | 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 |
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
- '^.+\\.tsx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
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
- '^.+\\.tsx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
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
- "Restricting CORS to explicit origins instead of the permissive default",
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"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/skills",
3
- "version": "1.6.1",
3
+ "version": "1.7.1",
4
4
  "description": "Curated skills catalog for FrontMCP projects",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",