@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.
- 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/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
|
|
@@ -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"
|