@frontmcp/skills 1.8.7 → 1.9.1-rc.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/README.md +107 -155
- package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +1 -1
- package/catalog/create-tool/references/availability.md +10 -10
- package/catalog/create-tool/references/ui-widgets.md +30 -8
- package/catalog/frontmcp-authorities/SKILL.md +5 -0
- package/catalog/frontmcp-channels/SKILL.md +17 -16
- package/catalog/frontmcp-channels/references/channel-sources.md +4 -4
- package/catalog/frontmcp-channels/references/channel-two-way.md +1 -1
- package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md +2 -0
- package/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md +1 -2
- package/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md +0 -1
- package/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md +0 -1
- package/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md +2 -3
- package/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md +2 -3
- package/catalog/frontmcp-config/references/configure-auth.md +1 -1
- package/catalog/frontmcp-config/references/configure-deployment-targets.md +54 -20
- package/catalog/frontmcp-config/references/configure-http.md +5 -2
- package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +1 -1
- package/catalog/frontmcp-config/references/configure-throttle.md +4 -2
- package/catalog/frontmcp-config/references/configure-transport.md +4 -5
- package/catalog/frontmcp-deployment/SKILL.md +19 -19
- package/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md +1 -1
- package/catalog/frontmcp-deployment/references/build-for-browser.md +38 -9
- package/catalog/frontmcp-deployment/references/build-for-mcpb.md +15 -9
- package/catalog/frontmcp-deployment/references/build-for-sdk.md +2 -1
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +3 -1
- package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +2 -2
- package/catalog/frontmcp-deployment/references/deploy-to-node.md +11 -11
- package/catalog/frontmcp-deployment/references/mcp-client-integration.md +12 -7
- package/catalog/frontmcp-deployment/references/protocol-versions.md +7 -3
- package/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md +50 -10
- package/catalog/frontmcp-development/examples/openapi-adapter/ref-security-and-filtering.md +13 -7
- package/catalog/frontmcp-development/references/create-adapter.md +14 -0
- package/catalog/frontmcp-development/references/create-agent.md +82 -49
- package/catalog/frontmcp-development/references/create-plugin-hooks.md +15 -5
- package/catalog/frontmcp-development/references/create-plugin.md +8 -4
- package/catalog/frontmcp-development/references/create-skill-with-tools.md +7 -2
- package/catalog/frontmcp-development/references/create-skill.md +4 -0
- package/catalog/frontmcp-development/references/decorators-guide.md +10 -11
- package/catalog/frontmcp-development/references/official-adapters.md +1 -1
- package/catalog/frontmcp-development/references/official-plugins.md +127 -24
- package/catalog/frontmcp-development/references/openapi-adapter.md +52 -2
- package/catalog/frontmcp-observability/references/metrics-endpoint.md +3 -1
- package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +12 -1
- package/catalog/frontmcp-production-readiness/references/common-checklist.md +2 -1
- package/catalog/frontmcp-production-readiness/references/distributed-ha.md +48 -28
- package/catalog/frontmcp-setup/examples/multi-app-composition/local-apps-with-shared-tools.md +10 -6
- package/catalog/frontmcp-setup/examples/nx-workflow/build-test-affected.md +2 -2
- package/catalog/frontmcp-setup/examples/nx-workflow/multi-server-deployment.md +9 -5
- package/catalog/frontmcp-setup/examples/nx-workflow/scaffold-and-generate.md +1 -1
- package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +1 -1
- package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +28 -15
- package/catalog/frontmcp-setup/references/multi-app-composition.md +25 -16
- package/catalog/frontmcp-setup/references/nx-workflow.md +53 -21
- package/catalog/frontmcp-setup/references/project-structure-nx.md +7 -4
- package/catalog/frontmcp-setup/references/setup-project.md +15 -0
- package/catalog/frontmcp-setup/references/setup-redis.md +12 -3
- package/catalog/frontmcp-setup/references/setup-sqlite.md +12 -8
- package/catalog/frontmcp-testing/SKILL.md +16 -12
- package/catalog/frontmcp-testing/references/setup-testing.md +14 -1
- package/catalog/skills-manifest.json +13 -11
- package/package.json +1 -1
|
@@ -29,6 +29,8 @@ class Server {}
|
|
|
29
29
|
|
|
30
30
|
Then scrape: `curl http://localhost:3000/metrics` (the default port is `PORT`, else 3000) — Content-Type is the canonical Prometheus `text/plain; version=0.0.4; charset=utf-8`.
|
|
31
31
|
|
|
32
|
+
`FrontMcpInstance.createFetchHandler(config)` (the Web-standard `(Request) => Response` handler) answers `GET /metrics` too — same body, auth and headers as the Express listener (it needs `@frontmcp/observability` installed, like the Express endpoint).
|
|
33
|
+
|
|
32
34
|
## Configuration
|
|
33
35
|
|
|
34
36
|
```typescript
|
|
@@ -105,7 +107,7 @@ Keep label values bounded (status codes, enum members, tool names) — unbounded
|
|
|
105
107
|
|
|
106
108
|
## Path conflict guard
|
|
107
109
|
|
|
108
|
-
`metrics.path` MUST NOT collide with MCP transport paths (`/mcp`, `/sse`, `/messages`). The service constructor throws `MetricsPathConflictError` at startup if it detects an overlap.
|
|
110
|
+
`metrics.path` MUST NOT collide with MCP transport paths (`/mcp`, `/sse`, `/messages`). The service constructor throws `MetricsPathConflictError` at startup if it detects an overlap. `createFetchHandler()` also throws `MetricsPathConflictError` when `metrics.path` equals its MCP entry path (`http.entryPath`, `/` when unset), since the metrics route would otherwise answer the MCP `GET` that opens the event stream.
|
|
109
111
|
|
|
110
112
|
## Common Patterns
|
|
111
113
|
|
package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md
CHANGED
|
@@ -135,6 +135,12 @@ spec:
|
|
|
135
135
|
value: distributed
|
|
136
136
|
- name: REDIS_HOST
|
|
137
137
|
value: redis
|
|
138
|
+
# Same secret on every pod: session ids are encrypted with it.
|
|
139
|
+
- name: MCP_SESSION_SECRET
|
|
140
|
+
valueFrom:
|
|
141
|
+
secretKeyRef:
|
|
142
|
+
name: mcp-server
|
|
143
|
+
key: session-secret
|
|
138
144
|
ports:
|
|
139
145
|
- containerPort: 3000
|
|
140
146
|
livenessProbe:
|
|
@@ -211,9 +217,14 @@ kubectl exec -it deploy/redis -- redis-cli KEYS "mcp:ha:heartbeat:*"
|
|
|
211
217
|
# 2) "mcp:ha:heartbeat:mcp-server-7b8f9-def34"
|
|
212
218
|
# 3) "mcp:ha:heartbeat:mcp-server-7b8f9-ghi56"
|
|
213
219
|
|
|
220
|
+
# Every pod listens on its relay channel: a request that reaches a pod which
|
|
221
|
+
# does not own the session is relayed to the owner and answered from there.
|
|
222
|
+
kubectl exec -it deploy/redis -- redis-cli PUBSUB CHANNELS "mcp:ha:notify:*"
|
|
223
|
+
|
|
214
224
|
# Kill a pod and watch takeover
|
|
215
225
|
kubectl delete pod mcp-server-7b8f9-abc12
|
|
216
|
-
#
|
|
226
|
+
# Until its heartbeat expires (~30s) its sessions answer 503 + Retry-After;
|
|
227
|
+
# then surviving pods take them over and serve them.
|
|
217
228
|
```
|
|
218
229
|
|
|
219
230
|
## What This Demonstrates
|
|
@@ -36,7 +36,8 @@ These checks apply to ALL deployment targets. Run them first, then proceed to yo
|
|
|
36
36
|
- [ ] `security.dnsRebindingProtection.allowedHosts` (or `FRONTMCP_ALLOWED_HOSTS`) names the public
|
|
37
37
|
hostname(s) — on a routable bind the derived default is **not** enforced, and FrontMCP logs a
|
|
38
38
|
warning saying so
|
|
39
|
-
- [ ] Startup logs show no `DNS-rebinding protection is not enforcing a Host allow-list` warning
|
|
39
|
+
- [ ] Startup logs show no `DNS-rebinding protection is not enforcing a Host allow-list` warning, and
|
|
40
|
+
the production audit reports `[Security] DNS_REBINDING_PROTECTED` (not `DNS_REBINDING_NOT_ENFORCED`)
|
|
40
41
|
- [ ] Include the port when the public URL uses a non-default one (`api.example.com:8443`)
|
|
41
42
|
- [ ] `allowedOrigins` is set when a browser client connects, so a foreign `Origin` is refused
|
|
42
43
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: distributed-ha
|
|
3
|
-
description: Deploy FrontMCP across multiple pods with heartbeat, session takeover, and notification relay for zero-downtime failover
|
|
3
|
+
description: Deploy FrontMCP across multiple pods with heartbeat, cross-pod request relay, session takeover, and notification relay for zero-downtime failover
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Distributed High Availability
|
|
7
7
|
|
|
8
|
-
FrontMCP's HA module
|
|
8
|
+
FrontMCP's HA module lets any pod receive any request of an MCP session, using Redis. Four components work together: HeartbeatService (liveness detection), request relay (a request for a session another live pod owns is served by that pod), session takeover (atomic CAS — a session whose owner stopped is served by the receiving pod), and NotificationRelay (cross-pod MCP notifications).
|
|
9
9
|
|
|
10
10
|
## When to Use This Skill
|
|
11
11
|
|
|
@@ -33,6 +33,7 @@ FrontMCP's HA module provides automatic session failover across multiple pods us
|
|
|
33
33
|
- Redis 6+ accessible from all pods
|
|
34
34
|
- `@frontmcp/sdk` and `@frontmcp/cli` installed
|
|
35
35
|
- `FRONTMCP_DEPLOYMENT_MODE=distributed` environment variable
|
|
36
|
+
- The same `MCP_SESSION_SECRET` on every pod (session ids are encrypted with it; a pod with another secret answers other pods' sessions with `404`)
|
|
36
37
|
|
|
37
38
|
## Step 1: Configure @FrontMcp Decorator
|
|
38
39
|
|
|
@@ -79,9 +80,12 @@ export default defineConfig({
|
|
|
79
80
|
|
|
80
81
|
```bash
|
|
81
82
|
export FRONTMCP_DEPLOYMENT_MODE=distributed
|
|
83
|
+
export MCP_SESSION_SECRET='<same value on every pod>' # replace with your shared secret
|
|
82
84
|
frontmcp build --target distributed
|
|
83
85
|
```
|
|
84
86
|
|
|
87
|
+
The build writes the `ha` block to `FRONTMCP_HA_*` variables in the generated setup file (only when the platform has not set them), and every pod reads them at startup.
|
|
88
|
+
|
|
85
89
|
Deploy with Docker or Kubernetes (see example below).
|
|
86
90
|
|
|
87
91
|
## Step 4: Verify Heartbeats
|
|
@@ -97,12 +101,14 @@ redis-cli GET "mcp:ha:heartbeat:mcp-server-7b8f9-abc12"
|
|
|
97
101
|
|
|
98
102
|
## Configuration
|
|
99
103
|
|
|
100
|
-
| Field | Type | Default | Description |
|
|
101
|
-
| ----------------------- | ------ | --------- | ------------------------------------------------ |
|
|
102
|
-
| `heartbeatIntervalMs` | number | 10000 | How often each pod writes its heartbeat to Redis |
|
|
103
|
-
| `heartbeatTtlMs` | number | 30000 | TTL for heartbeat key (should be 2-3x interval) |
|
|
104
|
-
| `takeoverGracePeriodMs` | number | 5000 | Wait time before claiming orphaned sessions |
|
|
105
|
-
| `redisKeyPrefix` | string | `mcp:ha:` | Redis key prefix for all HA keys |
|
|
104
|
+
| Field | Environment variable | Type | Default | Description |
|
|
105
|
+
| ----------------------- | ----------------------------------- | ------ | --------- | ------------------------------------------------ |
|
|
106
|
+
| `heartbeatIntervalMs` | `FRONTMCP_HA_HEARTBEAT_INTERVAL_MS` | number | 10000 | How often each pod writes its heartbeat to Redis |
|
|
107
|
+
| `heartbeatTtlMs` | `FRONTMCP_HA_HEARTBEAT_TTL_MS` | number | 30000 | TTL for heartbeat key (should be 2-3x interval) |
|
|
108
|
+
| `takeoverGracePeriodMs` | `FRONTMCP_HA_TAKEOVER_GRACE_MS` | number | 5000 | Wait time before claiming orphaned sessions |
|
|
109
|
+
| `redisKeyPrefix` | `FRONTMCP_HA_KEY_PREFIX` | string | `mcp:ha:` | Redis key prefix for all HA keys |
|
|
110
|
+
|
|
111
|
+
`heartbeatTtlMs` is also how long a request for a stopped pod's session is answered with `503` + `Retry-After` before another pod takes it over.
|
|
106
112
|
|
|
107
113
|
## Architecture
|
|
108
114
|
|
|
@@ -110,22 +116,30 @@ redis-cli GET "mcp:ha:heartbeat:mcp-server-7b8f9-abc12"
|
|
|
110
116
|
|
|
111
117
|
Each pod writes `mcp:ha:heartbeat:{nodeId}` to Redis every `heartbeatIntervalMs` with PX TTL of `heartbeatTtlMs`. The value contains `{ nodeId, startedAt, lastBeat, sessionCount }`. When a pod dies, the key expires.
|
|
112
118
|
|
|
119
|
+
### Request Relay
|
|
120
|
+
|
|
121
|
+
The owner of each session is recorded on the transport bus (`mcp:bus:session:{sessionId}`) and in the persisted session record. The hookable `relayToSessionOwner` stage of `http:request` (after the IP filter, before quota and auth) finds it — only for session ids the deployment minted (they decrypt under `MCP_SESSION_SECRET`), so other ids cost no Redis lookup; when it is another **live** pod the request (method, URL, headers, parsed body, client address) is published to `mcp:ha:notify:{ownerNodeId}`. The owner runs it through its own full `http:request` flow — auth, quota, transport and hooks run there — and streams the response (status, headers, each chunk, end; SSE included, with keepalive frames while it is quiet) back. A client disconnect aborts it on the owner. An owner that does not listen, does not acknowledge within 5s, loses its heartbeat mid-request, or sends nothing for three heartbeat intervals yields `503` + `Retry-After` (`SessionOwnerUnreachableError`), never a 500 (a response already started is ended). A response frame the owner cannot publish aborts the response there and ends it on the relaying pod. A relayed request is never relayed again.
|
|
122
|
+
|
|
113
123
|
### Session Takeover
|
|
114
124
|
|
|
115
125
|
When a request arrives for a session owned by a dead pod:
|
|
116
126
|
|
|
117
127
|
1. The live pod checks if the owner's heartbeat key exists
|
|
118
128
|
2. If missing, runs an atomic Lua CAS script: verifies `expectedOldNodeId`, updates `nodeId` + `reassignedAt`
|
|
119
|
-
3.
|
|
129
|
+
3. On success it recreates the transport from the persisted session (keeping `reassignedAt` / `reassignedFrom`), records itself as owner on the bus, and serves the request; if another pod won the race, it relays the request to that pod
|
|
130
|
+
|
|
131
|
+
A pod whose heartbeat lapsed (Redis unreachable for `heartbeatTtlMs`) may have lost sessions it still holds. Before serving one again it re-reads the persisted record (once per lapse); if another pod owns it now, it drops its transport, leaves the record to the new owner, and relays the request there. While Redis stays unreachable it keeps serving what it holds.
|
|
132
|
+
|
|
133
|
+
Takeover needs `transport.persistence` (Streamable HTTP only — an SSE stream cannot move to another pod).
|
|
120
134
|
|
|
121
135
|
### Notification Relay
|
|
122
136
|
|
|
123
|
-
Each pod subscribes to `mcp:ha:notify:{nodeId}` via Redis Pub/Sub.
|
|
137
|
+
Each pod subscribes to `mcp:ha:notify:{nodeId}` via Redis Pub/Sub. A notification for a session on another pod is published to the channel of the pod that owns it (looked up on the bus) and delivered there; it is never relayed twice.
|
|
124
138
|
|
|
125
139
|
## Redis Connection, TTL and Recovery
|
|
126
140
|
|
|
127
|
-
- HA uses one dedicated ioredis client built from the top-level `redis` config (host/port/password/db/tls or `url`).
|
|
128
|
-
- The orphan scanner reads `<keyPrefix>session:` (default `mcp:
|
|
141
|
+
- HA uses one dedicated ioredis client built from the top-level `redis` config (host/port/password/db/tls or `url`) for commands and publishing, plus a second connection for the relay channel subscription (retried 1s→30s until it succeeds). Both reconnect on their own, log errors at a rate-limited interval, and are closed on shutdown. Vercel KV cannot back HA.
|
|
142
|
+
- The orphan scanner reads `<keyPrefix>session:` (default `mcp:session:`, as `keyPrefix` defaults to `mcp:`), the same prefix the session store writes, and only runs when `transport.persistence.redis` is set. A claimed session is re-advertised on the bus, so every pod relays its next requests to the claimer, which recreates the transport on the first one.
|
|
129
143
|
- Session TTL is `persistence.defaultTtlMs`, then `persistence.redis.defaultTtlMs`, then 1 hour. The pod serving a session refreshes it at most once per quarter TTL.
|
|
130
144
|
- If Redis is unreachable at startup the server still starts; the session store retries with exponential backoff (1s doubling to 30s) and persistence resumes without a restart.
|
|
131
145
|
- `.frontmcp/machine-id` is only read/written in standalone development (never in `distributed` or `serverless`); in Kubernetes the machine ID is `HOSTNAME`.
|
|
@@ -134,10 +148,10 @@ Each pod subscribes to `mcp:ha:notify:{nodeId}` via Redis Pub/Sub. Cross-pod MCP
|
|
|
134
148
|
|
|
135
149
|
FrontMCP sets:
|
|
136
150
|
|
|
137
|
-
- **Cookie**: `__frontmcp_node` on Streamable HTTP initialize
|
|
138
|
-
- **Header**: `X-FrontMCP-Machine-Id` on every distributed response (initialize, message POSTs, DELETE, stateless requests and
|
|
151
|
+
- **Cookie**: `__frontmcp_node` on Streamable HTTP initialize — name, `Domain` and `SameSite` come from the deployment's `server.cookies` (`affinity`, `domain`, `sameSite`) in `frontmcp.config`; keep the load balancer's cookie name in step
|
|
152
|
+
- **Header**: `X-FrontMCP-Machine-Id` on every distributed response (initialize, message POSTs, DELETE, stateless and MCP 2026-07-28 requests, SSE, `/healthz`, `/readyz`, `/metrics` and 404s). The Express host and the web-fetch handler add it next to the security headers; the session flows also set it in the hookable `applyNodeHeaders` stage. Only distributed mode (`FRONTMCP_DEPLOYMENT_MODE=distributed`) sends it
|
|
139
153
|
|
|
140
|
-
NGINX sticky session example:
|
|
154
|
+
Affinity is an optimization: without it a request on the wrong pod is relayed to the owner (one Redis round trip each way). NGINX sticky session example:
|
|
141
155
|
|
|
142
156
|
```nginx
|
|
143
157
|
upstream mcp_backend {
|
|
@@ -158,16 +172,18 @@ upstream mcp_backend {
|
|
|
158
172
|
|
|
159
173
|
## Errors
|
|
160
174
|
|
|
161
|
-
| Error
|
|
162
|
-
|
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
175
|
+
| Error | When | Solution |
|
|
176
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
|
177
|
+
| `SessionOwnerUnreachableError` | `503` + `Retry-After`: owner alive by heartbeat but did not answer the relay (just stopped) | Retry after `Retry-After` seconds; by then the owner answers or is taken over |
|
|
178
|
+
| `SessionClaimConflictError` | Takeover lost to a pod that is gone too, or the session expired (client gets `404`) | None — a lost race against a live pod is relayed to it instead |
|
|
179
|
+
| `HaConfigurationError` | Redis not configured for distributed mode | Add `redis` to `@FrontMcp()` config |
|
|
165
180
|
|
|
166
181
|
## Verification Checklist
|
|
167
182
|
|
|
168
183
|
### Configuration
|
|
169
184
|
|
|
170
185
|
- [ ] `FRONTMCP_DEPLOYMENT_MODE=distributed` set in deployment
|
|
186
|
+
- [ ] Same `MCP_SESSION_SECRET` on every pod
|
|
171
187
|
- [ ] Redis accessible from all pods
|
|
172
188
|
- [ ] `heartbeatTtlMs` >= 2x `heartbeatIntervalMs`
|
|
173
189
|
- [ ] Transport persistence configured with Redis
|
|
@@ -175,20 +191,24 @@ upstream mcp_backend {
|
|
|
175
191
|
### Runtime
|
|
176
192
|
|
|
177
193
|
- [ ] `redis-cli --scan --pattern "mcp:ha:heartbeat:*"` shows entries for each pod
|
|
194
|
+
- [ ] `redis-cli PUBSUB CHANNELS "mcp:ha:notify:*"` lists one channel per pod
|
|
195
|
+
- [ ] A request sent to a pod that does not own the session is answered (relayed; `X-FrontMCP-Machine-Id` names the owner)
|
|
178
196
|
- [ ] Killing a pod results in its heartbeat expiring within TTL
|
|
179
|
-
- [ ] Surviving pods claim orphaned sessions after takeover grace period
|
|
197
|
+
- [ ] Surviving pods claim orphaned sessions after takeover grace period (`redis-cli HGETALL "mcp:bus:session:<id>"` names the new owner)
|
|
180
198
|
- [ ] `/healthz` and `/readyz` return healthy on all pods
|
|
181
199
|
|
|
182
200
|
## Troubleshooting
|
|
183
201
|
|
|
184
|
-
| Problem
|
|
185
|
-
|
|
|
186
|
-
|
|
|
187
|
-
| `
|
|
188
|
-
|
|
|
189
|
-
|
|
|
190
|
-
|
|
|
191
|
-
|
|
|
202
|
+
| Problem | Cause | Solution |
|
|
203
|
+
| ------------------------------------------ | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
204
|
+
| `503` "did not answer the relayed request" | Owner just stopped (heartbeat not expired yet) | Expected for up to `heartbeatTtlMs`; the client retries after `Retry-After` and the session is taken over |
|
|
205
|
+
| `404` on another pod for a live session | Pods use different `MCP_SESSION_SECRET` | Give every pod the same `MCP_SESSION_SECRET` |
|
|
206
|
+
| Sessions not transferred after pod death | `heartbeatTtlMs` too high | Lower TTL while keeping >= 2x interval (e.g., 20-30s for a 10s interval) |
|
|
207
|
+
| `HaConfigurationError` on startup | Missing Redis config | Add `redis` to `@FrontMcp()` decorator |
|
|
208
|
+
| Duplicate notifications | Shared Redis subscriber connection | Use dedicated connections per relay |
|
|
209
|
+
| Sessions expire too early or too late | TTL not configured | Set `transport.persistence.defaultTtlMs` (or `persistence.redis.defaultTtlMs`); default is 1 hour and slides while the owning pod serves requests |
|
|
210
|
+
| Redis was down when pods started | Startup connect failed | Nothing to do: the session store reconnects with backoff (1s to 30s) and `/readyz` turns 200 |
|
|
211
|
+
| Session takeover race failures | High pod count + simultaneous restarts | Increase `takeoverGracePeriodMs` |
|
|
192
212
|
|
|
193
213
|
## Examples
|
|
194
214
|
|
package/catalog/frontmcp-setup/examples/multi-app-composition/local-apps-with-shared-tools.md
CHANGED
|
@@ -5,10 +5,10 @@ level: basic
|
|
|
5
5
|
description: 'Compose multiple local `@App` classes into a server with shared tools available to all apps.'
|
|
6
6
|
tags: [setup, multi-app, local, multi, app, composition]
|
|
7
7
|
features:
|
|
8
|
-
- 'Multiple `@App` classes with unique `id` fields
|
|
9
|
-
- 'Server-level `tools` array for shared tools
|
|
8
|
+
- 'Multiple `@App` classes with unique `id` fields, which prefix their tools when two tools share a name (`billing:charge`)'
|
|
9
|
+
- 'Server-level `tools` array for shared tools every app serves under their own name (`server:<name>` when an app tool has the same name)'
|
|
10
10
|
- 'Each app is self-contained with its own tools array'
|
|
11
|
-
- 'The `id` field on `@App`
|
|
11
|
+
- 'The `id` field on `@App` is the prefix of its tools when names collide'
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Local Apps with Shared Tools
|
|
@@ -20,6 +20,7 @@ Compose multiple local `@App` classes into a server with shared tools available
|
|
|
20
20
|
```typescript
|
|
21
21
|
// src/apps/billing.app.ts
|
|
22
22
|
import { App } from '@frontmcp/sdk';
|
|
23
|
+
|
|
23
24
|
import { ChargeTool } from '../tools/charge.tool';
|
|
24
25
|
import { RefundTool } from '../tools/refund.tool';
|
|
25
26
|
|
|
@@ -34,6 +35,7 @@ export class BillingApp {}
|
|
|
34
35
|
```typescript
|
|
35
36
|
// src/apps/inventory.app.ts
|
|
36
37
|
import { App } from '@frontmcp/sdk';
|
|
38
|
+
|
|
37
39
|
import { CheckStockTool } from '../tools/check-stock.tool';
|
|
38
40
|
|
|
39
41
|
@App({
|
|
@@ -62,7 +64,9 @@ export default class HealthCheckTool extends ToolContext {
|
|
|
62
64
|
```typescript
|
|
63
65
|
// src/main.ts
|
|
64
66
|
import 'reflect-metadata';
|
|
67
|
+
|
|
65
68
|
import { FrontMcp } from '@frontmcp/sdk';
|
|
69
|
+
|
|
66
70
|
import { BillingApp } from './apps/billing.app';
|
|
67
71
|
import { InventoryApp } from './apps/inventory.app';
|
|
68
72
|
import HealthCheckTool from './tools/health-check.tool';
|
|
@@ -77,10 +81,10 @@ export default class Server {}
|
|
|
77
81
|
|
|
78
82
|
## What This Demonstrates
|
|
79
83
|
|
|
80
|
-
- Multiple `@App` classes with unique `id` fields
|
|
81
|
-
- Server-level `tools` array for shared tools
|
|
84
|
+
- Multiple `@App` classes with unique `id` fields, which prefix their tools when two tools share a name (`billing:charge`)
|
|
85
|
+
- Server-level `tools` array for shared tools every app serves under their own name (`server:<name>` when an app tool has the same name)
|
|
82
86
|
- Each app is self-contained with its own tools array
|
|
83
|
-
- The `id` field on `@App`
|
|
87
|
+
- The `id` field on `@App` is the prefix of its tools when names collide
|
|
84
88
|
|
|
85
89
|
## Related
|
|
86
90
|
|
|
@@ -19,7 +19,7 @@ Use Nx commands for efficient building, testing, and CI with affected-only execu
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
# Build a single server (builds all dependencies in correct order)
|
|
22
|
-
nx build gateway
|
|
22
|
+
nx build server-gateway
|
|
23
23
|
|
|
24
24
|
# Test a single app
|
|
25
25
|
nx test billing
|
|
@@ -59,7 +59,7 @@ nx g @frontmcp/nx:tool calculate-tax --project=billing
|
|
|
59
59
|
nx test billing
|
|
60
60
|
|
|
61
61
|
# 4. Build the server that includes this app
|
|
62
|
-
nx build gateway
|
|
62
|
+
nx build server-gateway
|
|
63
63
|
|
|
64
64
|
# 5. Or test everything affected by your changes
|
|
65
65
|
nx affected -t test
|
|
@@ -36,10 +36,12 @@ nx g @frontmcp/nx:lib shared-db
|
|
|
36
36
|
```typescript
|
|
37
37
|
// servers/public-gateway/src/main.ts
|
|
38
38
|
import 'reflect-metadata';
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
import { BillingApp } from '@my-workspace/billing';
|
|
41
41
|
import { CrmApp } from '@my-workspace/crm';
|
|
42
42
|
|
|
43
|
+
import { FrontMcp } from '@frontmcp/sdk';
|
|
44
|
+
|
|
43
45
|
@FrontMcp({
|
|
44
46
|
info: { name: 'public-gateway', version: '1.0.0' },
|
|
45
47
|
apps: [BillingApp, CrmApp],
|
|
@@ -58,9 +60,11 @@ export default PublicGateway;
|
|
|
58
60
|
```typescript
|
|
59
61
|
// servers/admin-portal/src/main.ts
|
|
60
62
|
import 'reflect-metadata';
|
|
61
|
-
|
|
63
|
+
|
|
62
64
|
import { AdminApp } from '@my-workspace/admin';
|
|
63
65
|
|
|
66
|
+
import { FrontMcp } from '@frontmcp/sdk';
|
|
67
|
+
|
|
64
68
|
@FrontMcp({
|
|
65
69
|
info: { name: 'admin-portal', version: '1.0.0' },
|
|
66
70
|
apps: [AdminApp],
|
|
@@ -73,9 +77,9 @@ export default AdminPortal;
|
|
|
73
77
|
```
|
|
74
78
|
|
|
75
79
|
```bash
|
|
76
|
-
# Build each server independently
|
|
77
|
-
nx build public-gateway
|
|
78
|
-
nx build admin-portal
|
|
80
|
+
# Build each server independently (server projects are named server-<name>)
|
|
81
|
+
nx build server-public-gateway
|
|
82
|
+
nx build server-admin-portal
|
|
79
83
|
|
|
80
84
|
# Test all projects
|
|
81
85
|
nx run-many -t test
|
|
@@ -146,15 +146,15 @@ Install one or many skills to a provider-specific directory. `[name]` is
|
|
|
146
146
|
optional when one of `--all`, `--tag`, or `--category` is supplied —
|
|
147
147
|
those flags select skills in bulk.
|
|
148
148
|
|
|
149
|
-
| Flag | Description | Default
|
|
150
|
-
| --------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
151
|
-
| `-p, --provider <provider>` | Target provider: `claude` or `codex` | `claude` |
|
|
152
|
-
| `-d, --dir <directory>` | Custom install directory (overrides provider default) | —
|
|
153
|
-
| `-a, --all` | Install **every** skill in the catalog (or every `@Skill` entry when `--from-*` is set) | `false`
|
|
154
|
-
| `-t, --tag <tag>` | Install every skill matching a tag (catalog only) | —
|
|
155
|
-
| `-c, --category <c>` | Install every skill in a category (catalog only) | —
|
|
156
|
-
| `--from-entry <path>` | Install `@Skill` entries discovered in a **local project entry file** instead of the framework catalog | —
|
|
157
|
-
| `--from-package <pkg>` | Install `@Skill` entries discovered in a **published package's** main entry | —
|
|
149
|
+
| Flag | Description | Default |
|
|
150
|
+
| --------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
|
|
151
|
+
| `-p, --provider <provider>` | Target provider: `claude` or `codex` | `skills.provider` in `frontmcp.config`, else `claude` |
|
|
152
|
+
| `-d, --dir <directory>` | Custom install directory (overrides provider default) | — |
|
|
153
|
+
| `-a, --all` | Install **every** skill in the catalog (or every `@Skill` entry when `--from-*` is set) | `false` |
|
|
154
|
+
| `-t, --tag <tag>` | Install every skill matching a tag (catalog only) | — |
|
|
155
|
+
| `-c, --category <c>` | Install every skill in a category (catalog only) | — |
|
|
156
|
+
| `--from-entry <path>` | Install `@Skill` entries discovered in a **local project entry file** instead of the framework catalog | — |
|
|
157
|
+
| `--from-package <pkg>` | Install `@Skill` entries discovered in a **published package's** main entry | — |
|
|
158
158
|
|
|
159
159
|
```bash
|
|
160
160
|
# Single-skill install (positional name)
|
|
@@ -172,18 +172,31 @@ frontmcp skills install --from-entry src/main.ts --all -p claude
|
|
|
172
172
|
frontmcp skills install --from-package my-frontmcp-server my-skill -p claude
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
+
With no name and no `--all` / `--tag` / `--category` / `--from-*`, the command installs the
|
|
176
|
+
`skills.install` list of the nearest `frontmcp.config`, or else the catalog skills of `skills.bundle`
|
|
177
|
+
(`'none'` installs nothing). Explicit flags always win.
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// frontmcp.config.ts
|
|
181
|
+
export default defineConfig({
|
|
182
|
+
name: 'my-server',
|
|
183
|
+
deployments: [{ target: 'node' }],
|
|
184
|
+
skills: { provider: 'claude', install: ['create-tool', 'setup-testing'], exportTarget: 'cursor' },
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
175
188
|
### `frontmcp skills export`
|
|
176
189
|
|
|
177
190
|
Convert one or many catalog skills into a rule file for IDEs that
|
|
178
191
|
**don't** speak the skills protocol (Cursor, Windsurf, Copilot). The
|
|
179
192
|
emitted file lives in the current directory by default.
|
|
180
193
|
|
|
181
|
-
| Flag | Description | Default
|
|
182
|
-
| ----------------------- | ----------------------------------------------------- |
|
|
183
|
-
| `-t, --target <target>` | Target IDE: `cursor`, `windsurf`, or `copilot` | `cursor` |
|
|
184
|
-
| `-n, --name <name>` | Skill name to export (required unless `--all` is set) | —
|
|
185
|
-
| `-a, --all` | Export **every** skill in the catalog | `false`
|
|
186
|
-
| `-d, --out <directory>` | Output directory | `cwd`
|
|
194
|
+
| Flag | Description | Default |
|
|
195
|
+
| ----------------------- | ----------------------------------------------------- | --------------------------------------------------------- |
|
|
196
|
+
| `-t, --target <target>` | Target IDE: `cursor`, `windsurf`, or `copilot` | `skills.exportTarget` in `frontmcp.config`, else `cursor` |
|
|
197
|
+
| `-n, --name <name>` | Skill name to export (required unless `--all` is set) | — |
|
|
198
|
+
| `-a, --all` | Export **every** skill in the catalog | `false` |
|
|
199
|
+
| `-d, --out <directory>` | Output directory | `cwd` |
|
|
187
200
|
|
|
188
201
|
```bash
|
|
189
202
|
frontmcp skills export --name frontmcp-development --target cursor
|
|
@@ -138,7 +138,7 @@ export default class Server {}
|
|
|
138
138
|
| `namespace` | `string` | Namespace prefix for tools, resources, and prompts |
|
|
139
139
|
| `description` | `string` | Human-readable description |
|
|
140
140
|
| `standalone` | `boolean \| 'includeInParent'` | Scope isolation mode (default: `false`) |
|
|
141
|
-
| `transportOptions` | `RemoteTransportOptions` | Timeout, retries, headers, SSE fallback
|
|
141
|
+
| `transportOptions` | `RemoteTransportOptions` | Timeout, retries, headers, SSE fallback, MCP revision |
|
|
142
142
|
| `remoteAuth` | `RemoteAuthConfig` | Auth config: `'static'`, `'forward'`, or `'oauth'` |
|
|
143
143
|
| `refreshInterval` | `number` | Interval (ms) to refresh capabilities from remote |
|
|
144
144
|
| `cacheTTL` | `number` | TTL (ms) for cached capabilities (default: 60000) |
|
|
@@ -146,13 +146,16 @@ export default class Server {}
|
|
|
146
146
|
|
|
147
147
|
`RemoteTransportOptions` fields:
|
|
148
148
|
|
|
149
|
-
| Field
|
|
150
|
-
|
|
|
151
|
-
| `timeout`
|
|
152
|
-
| `retryAttempts`
|
|
153
|
-
| `retryDelayMs`
|
|
154
|
-
| `fallbackToSSE`
|
|
155
|
-
| `headers`
|
|
149
|
+
| Field | Type | Default | Description |
|
|
150
|
+
| ----------------- | ------------------------------------ | ---------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
151
|
+
| `timeout` | `number` | `30000` | Request timeout in ms |
|
|
152
|
+
| `retryAttempts` | `number` | `3` | Retry attempts for failed requests |
|
|
153
|
+
| `retryDelayMs` | `number` | `1000` | Delay between retries in ms |
|
|
154
|
+
| `fallbackToSSE` | `boolean` | `true` | Fallback to SSE if Streamable HTTP fails |
|
|
155
|
+
| `headers` | `Record<string, string>` | - | Additional headers for all requests |
|
|
156
|
+
| `protocolVersion` | `'legacy' \| '2026-07-28' \| 'auto'` | `'legacy'` | MCP revision: session + `initialize`, the stateless 2026-07-28 client (URL remotes only), or probe `server/discover` first |
|
|
157
|
+
|
|
158
|
+
Each remote tool, resource, resource template and prompt is listed once; when `cacheTTL` expires the gateway re-reads the remote's lists and replaces what it proxied before (dropped entries disappear, nothing is duplicated).
|
|
156
159
|
|
|
157
160
|
`RemoteAuthConfig` modes:
|
|
158
161
|
|
|
@@ -190,16 +193,16 @@ The type is: `standalone?: 'includeInParent' | boolean` (defaults to `false`).
|
|
|
190
193
|
|
|
191
194
|
## Tool Namespacing
|
|
192
195
|
|
|
193
|
-
|
|
196
|
+
A local app's tool keeps its own name while no other tool in the server shares it. When two tools share a name, each is listed with its owner's id as a prefix: `appId:toolName`.
|
|
194
197
|
|
|
195
198
|
```typescript
|
|
196
|
-
@App({ id: 'billing', name: 'Billing', tools: [
|
|
199
|
+
@App({ id: 'billing', name: 'Billing', tools: [SearchTool] })
|
|
197
200
|
class BillingApp {}
|
|
198
|
-
//
|
|
201
|
+
// Listed as: billing:search (another app also has `search`)
|
|
199
202
|
|
|
200
|
-
@App({ id: 'inventory', name: 'Inventory', tools: [CheckStockTool] })
|
|
203
|
+
@App({ id: 'inventory', name: 'Inventory', tools: [SearchTool, CheckStockTool] })
|
|
201
204
|
class InventoryApp {}
|
|
202
|
-
//
|
|
205
|
+
// Listed as: inventory:search and check_stock
|
|
203
206
|
```
|
|
204
207
|
|
|
205
208
|
For remote and ESM apps, the `namespace` option controls the prefix:
|
|
@@ -214,7 +217,7 @@ app.esm('@acme/tools@^1.0.0', { namespace: 'acme' });
|
|
|
214
217
|
|
|
215
218
|
## Shared Tools
|
|
216
219
|
|
|
217
|
-
Tools declared directly on `@FrontMcp` (not inside an `@App`) are
|
|
220
|
+
Tools declared directly on `@FrontMcp` (not inside an `@App`) are served next to every app's tools, through the same flows: server-level plugin hooks, hooks on the tool class, `authorities`, rate limits, `availableWhen` and the startup checks apply to them as to app tools. They resolve server-level `providers`, not an app's.
|
|
218
221
|
|
|
219
222
|
```typescript
|
|
220
223
|
@FrontMcp({
|
|
@@ -225,7 +228,13 @@ Tools declared directly on `@FrontMcp` (not inside an `@App`) are shared across
|
|
|
225
228
|
export default class Server {}
|
|
226
229
|
```
|
|
227
230
|
|
|
228
|
-
|
|
231
|
+
- A shared tool keeps its own name. If an app in the same scope has a tool of that name, both are prefixed: `billing:health_check` for the app's and `server:health_check` for the shared one. No app may have the id `server` while `@FrontMcp` declares tools or resources (startup fails with `ReservedAppIdError`).
|
|
232
|
+
- With `splitByApp: true`, and for a `standalone` app, each app's scope serves its own instance of every shared tool and resource.
|
|
233
|
+
- An app's plugin hooks run for a shared tool only when declared with `appliesTo: 'uncovered-apps'`; server-level plugin hooks always run.
|
|
234
|
+
- A shared tool belongs to no app, so `incrementalAuth` asks for no app grant before it runs.
|
|
235
|
+
- `@FrontMcp` does not accept `prompts`; declare them on an `@App`.
|
|
236
|
+
|
|
237
|
+
The same pattern works for shared resources (and resource templates) and shared skills:
|
|
229
238
|
|
|
230
239
|
```typescript
|
|
231
240
|
@FrontMcp({
|
|
@@ -380,7 +389,7 @@ export default class Server {}
|
|
|
380
389
|
### Runtime
|
|
381
390
|
|
|
382
391
|
- [ ] All app tools appear in `tools/list` with correct namespace prefixes
|
|
383
|
-
- [ ] Shared tools appear
|
|
392
|
+
- [ ] Shared tools appear under their own name, or as `server:<name>` when an app tool has the same name
|
|
384
393
|
- [ ] `standalone: true` apps are isolated and do not appear in parent tool listing
|
|
385
394
|
- [ ] `standalone: 'includeInParent'` apps have isolated scope but visible tools
|
|
386
395
|
- [ ] Per-app auth modes are enforced independently per app
|