@frontmcp/skills 1.7.0 → 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
@@ -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.7.0",
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",