@frontmcp/skills 1.8.6 → 1.8.7
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/create-tool/SKILL.md +24 -24
- package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +0 -1
- package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +1 -3
- package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +4 -6
- package/catalog/create-tool/references/decorator-options.md +1 -1
- package/catalog/create-tool/references/ui-widgets.md +68 -41
- package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +3 -3
- package/catalog/frontmcp-config/references/configure-deployment-targets.md +19 -1
- package/catalog/frontmcp-config/references/configure-http.md +6 -4
- package/catalog/frontmcp-config/references/configure-security-headers.md +18 -13
- package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +4 -4
- package/catalog/frontmcp-config/references/configure-throttle.md +1 -1
- package/catalog/frontmcp-deployment/SKILL.md +19 -19
- package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +5 -3
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +2 -0
- package/catalog/frontmcp-deployment/references/build-for-browser.md +8 -0
- package/catalog/frontmcp-deployment/references/build-for-mcpb.md +9 -8
- package/catalog/frontmcp-deployment/references/build-for-sdk.md +9 -9
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +3 -1
- package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +9 -8
- package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +15 -7
- package/catalog/frontmcp-development/references/create-agent.md +8 -7
- package/catalog/frontmcp-development/references/create-plugin.md +17 -0
- package/catalog/frontmcp-development/references/official-plugins.md +12 -5
- package/catalog/frontmcp-guides/references/example-task-manager.md +2 -2
- package/catalog/frontmcp-guides/references/example-weather-api.md +2 -2
- package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +1 -0
- package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md +2 -2
- package/catalog/frontmcp-production-readiness/references/common-checklist.md +1 -1
- package/catalog/frontmcp-production-readiness/references/distributed-ha.md +17 -7
- package/catalog/frontmcp-production-readiness/references/health-readiness-endpoints.md +3 -1
- package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +1 -1
- package/catalog/frontmcp-setup/references/nx-workflow.md +25 -17
- package/catalog/frontmcp-setup/references/setup-redis.md +1 -1
- package/catalog/frontmcp-setup/references/setup-sqlite.md +9 -6
- package/catalog/frontmcp-testing/SKILL.md +24 -23
- package/catalog/frontmcp-testing/references/setup-testing.md +26 -2
- package/catalog/frontmcp-testing/references/test-auth.md +8 -0
- package/catalog/skills-manifest.json +4 -4
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ Connect a React application to a FrontMCP server using `@frontmcp/react`. `Front
|
|
|
23
23
|
|
|
24
24
|
```typescript
|
|
25
25
|
// src/server.ts — create a DirectMcpServer (in-memory) for the React app to consume.
|
|
26
|
-
import { create, tool, z } from '@frontmcp/
|
|
26
|
+
import { create, tool, z } from '@frontmcp/react'; // `z` is re-exported; `@frontmcp/sdk` works too
|
|
27
27
|
|
|
28
28
|
export const server = await create({
|
|
29
29
|
info: { name: 'browser-app', version: '1.0.0' },
|
|
@@ -70,10 +70,12 @@ function ToolUI() {
|
|
|
70
70
|
// useCallTool requires the tool name as a hook arg, so each row owns its own
|
|
71
71
|
// hook instance. The mutate fn takes just the arguments object — not `{ name, arguments }`.
|
|
72
72
|
function ToolButton({ tool }: { tool: { name: string; description?: string } }) {
|
|
73
|
-
const [callTool] = useCallTool<{ name: string }>(tool.name);
|
|
73
|
+
const [callTool, { data }] = useCallTool<{ name: string }>(tool.name);
|
|
74
|
+
// `data` is the full CallToolResult; failures arrive as `data.isError`, not in `error`.
|
|
75
|
+
const text = data?.content?.[0]?.type === 'text' ? data.content[0].text : null;
|
|
74
76
|
return (
|
|
75
77
|
<button onClick={() => callTool({ name: 'World' })}>
|
|
76
|
-
{tool.name}: {tool.description}
|
|
78
|
+
{tool.name}: {tool.description} {data?.isError ? '(failed)' : text}
|
|
77
79
|
</button>
|
|
78
80
|
);
|
|
79
81
|
}
|
|
@@ -82,3 +82,5 @@ curl https://your-project.vercel.app/healthz
|
|
|
82
82
|
## Related
|
|
83
83
|
|
|
84
84
|
- See `deploy-to-vercel` for KV provisioning, environment variables, and cold start optimization
|
|
85
|
+
|
|
86
|
+
> **Tasks and elicitation on Vercel KV:** Vercel KV has no pub/sub, so background tasks and elicitation cannot use it. The server still starts — tasks are skipped with a `[tasks]` startup warning unless `tasks: { enabled: true }` is set (which keeps `TaskStoreNotSupportedError`), and elicitation throws `ElicitationNotSupportedError` only when its store actually resolves to Vercel KV. Give tasks their own backend with `tasks: { redis }` or `tasks: { sqlite }` (an explicit backend ignores the ambient `KV_REST_API_URL`).
|
|
@@ -117,6 +117,14 @@ function ToolUI() {
|
|
|
117
117
|
}
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
Things that trip people up with `@frontmcp/react`:
|
|
121
|
+
|
|
122
|
+
- `useCallTool`'s `data` is the whole MCP `CallToolResult` (`content`, `structuredContent`, `isError`), not the tool's return value. Render `data.structuredContent ?? data.content`, never `String(data)`. A tool-side failure resolves with `data.isError === true`; `error` is only set when the call throws (for example when the client is not connected).
|
|
123
|
+
- `z` is re-exported from `@frontmcp/react`, but `zod` remains a required peer dependency of `@frontmcp/lazy-zod` and must be installed in the consuming project.
|
|
124
|
+
- `create({ resources })` takes `@Resource` classes or `resource(...)(handler)` / `resourceTemplate(...)(handler)` values. A plain `{ uri, name, read }` object is rejected with "Expected a class or a resource function".
|
|
125
|
+
- The subpath entries (`@frontmcp/react/state`, `/api`, `/ai`, `/router`) share the root entry's provider and server registry, so `useStoreResource`, `useApiClient`, `useAITools` and `useTools` register against the same `FrontMcpProvider`.
|
|
126
|
+
- `createRouterEntries()` from `@frontmcp/react/router` returns `{ tools, resources }` that go straight into `create({ tools, resources })`.
|
|
127
|
+
|
|
120
128
|
For connecting to a remote MCP server (HTTP), create a server-bound `DirectMcpServer` via `connect()` from `@frontmcp/sdk` and pass that instance to the provider.
|
|
121
129
|
|
|
122
130
|
## Browser vs Node vs SDK Target
|
|
@@ -174,14 +174,15 @@ bundled JS, so a partial matrix is fine.
|
|
|
174
174
|
|
|
175
175
|
## Troubleshooting
|
|
176
176
|
|
|
177
|
-
| Problem
|
|
178
|
-
|
|
|
179
|
-
| `@frontmcp/sdk is required for schema extraction`
|
|
180
|
-
|
|
|
181
|
-
|
|
|
182
|
-
| `
|
|
183
|
-
|
|
|
184
|
-
|
|
|
177
|
+
| Problem | Cause | Solution |
|
|
178
|
+
| ---------------------------------------------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
179
|
+
| `@frontmcp/sdk is required for schema extraction` | SDK missing or externalized from bundle | Ensure `@frontmcp/sdk` is installed |
|
|
180
|
+
| `… requires "@frontmcp/sdk" but the archive has no node_modules` | Server entry still `require()`s a runtime package | Rebuild with `frontmcp build --target mcpb` so runtime packages are bundled; `validate` reports archives that are not self-contained |
|
|
181
|
+
| Archive > 100 MB | node_modules bundled or node runtime bloated | Tune `build.esbuild.external`, drop `--sea`, or disable `includeNodeModules` |
|
|
182
|
+
| `Unknown substitution variable` on validate | Typo in `mcp_config.args` / `env` | Only `__dirname`, `HOME`, `DESKTOP`, `DOCUMENTS`, `DOWNLOADS`, `pathSeparator`, and declared `user_config` keys are allowed |
|
|
183
|
+
| `entry_point is not present in archive` | Custom `--entry` flag or bundler moved the file | Re-run without the override, or update the config's `entry` |
|
|
184
|
+
| Two builds produce different SHA-256 | `--no-deterministic` set, or inputs embed a changing timestamp | Restore deterministic mode; scan your sources for live date/time values |
|
|
185
|
+
| `platform_overrides.{platform}.command` missing binary | `--merge-from` folders don't match MCPB platform keys | See the expected layout below |
|
|
185
186
|
|
|
186
187
|
Expected `--merge-from` layout (platform dirs must match MCPB platform keys):
|
|
187
188
|
|
|
@@ -208,15 +208,15 @@ const client = await connectOpenAI(config, {
|
|
|
208
208
|
|
|
209
209
|
All `connect*()` functions return a `DirectClient` with these methods:
|
|
210
210
|
|
|
211
|
-
| Method | Description
|
|
212
|
-
| ----------------------- |
|
|
213
|
-
| `listTools()` | List tools in platform-specific format
|
|
214
|
-
| `callTool(name, args)` | Execute a tool
|
|
215
|
-
| `listResources()` | List
|
|
216
|
-
| `readResource(uri)` | Read a resource
|
|
217
|
-
| `listPrompts()` | List
|
|
218
|
-
| `getPrompt(name, args)` | Get a prompt
|
|
219
|
-
| `close()` | Clean up connection
|
|
211
|
+
| Method | Description |
|
|
212
|
+
| ----------------------- | --------------------------------------- |
|
|
213
|
+
| `listTools()` | List tools in platform-specific format |
|
|
214
|
+
| `callTool(name, args)` | Execute a tool |
|
|
215
|
+
| `listResources()` | List all resources (follows every page) |
|
|
216
|
+
| `readResource(uri)` | Read a resource |
|
|
217
|
+
| `listPrompts()` | List all prompts (follows every page) |
|
|
218
|
+
| `getPrompt(name, args)` | Get a prompt |
|
|
219
|
+
| `close()` | Clean up connection |
|
|
220
220
|
|
|
221
221
|
## SDK vs Node Target
|
|
222
222
|
|
|
@@ -123,6 +123,8 @@ npx wrangler kv:namespace create FRONTMCP_KV
|
|
|
123
123
|
|
|
124
124
|
Copy the returned `id` into your `wrangler.toml`.
|
|
125
125
|
|
|
126
|
+
Cloudflare storage: `redis: { provider: 'vercel-kv' }` (the HTTP-based Upstash/Vercel KV client) is accepted by `--target cloudflare`; only TCP `redis` and `sqlite` configs are rejected at build time, since Workers cannot open raw sockets or load native modules.
|
|
127
|
+
|
|
126
128
|
## Step 4: Configure the Server
|
|
127
129
|
|
|
128
130
|
```typescript
|
|
@@ -149,7 +151,7 @@ For session storage, use Upstash Redis (HTTP) via `redis: { provider: 'vercel-kv
|
|
|
149
151
|
|
|
150
152
|
### Secrets, vars and `process.env`
|
|
151
153
|
|
|
152
|
-
Worker bindings arrive as an argument to `fetch`, not as environment variables. The generated entry copies every **string** binding into `process.env` on the first request (existing values are never overwritten), so ordinary `process.env.MY_API_KEY` reads behave the same on Workers as under `frontmcp dev`. Non-string bindings (KV, D1, R2, Durable Objects) stay on `env`, which the entry forwards to the handler along with `ctx`.
|
|
154
|
+
Worker bindings arrive as an argument to `fetch`, not as environment variables. The generated entry copies every **string** binding into `process.env` on the first request (existing values are never overwritten), so ordinary `process.env.MY_API_KEY` reads behave the same on Workers as under `frontmcp dev`. Non-string bindings (KV, D1, R2, Durable Objects) stay on `env`, which the entry forwards to the handler along with `ctx`. Inside a tool, resource, prompt or agent, read them with `this.workerEnv` (e.g. `this.workerEnv?.MY_KV as KVNamespace | undefined`) — it is the request's Worker `env`, read-only, and `undefined` outside a Worker request. `NODE_ENV = "production"` set in `wrangler.toml` `[vars]` is read live by the runtime context, so production mode takes effect even though the value reaches `process.env` only on the first request.
|
|
153
155
|
|
|
154
156
|
A value read at module-eval time — inside the `@FrontMcp({...})` argument itself — is still `undefined`, because the copy happens on the first request. Read configuration inside `execute()` / `read()`, or rely on `nodejs_compat_populate_process_env` (emitted by default), which populates `process.env` before your module evaluates.
|
|
155
157
|
|
|
@@ -318,14 +318,15 @@ Lambda cold starts occur when a new execution environment is initialized. Strate
|
|
|
318
318
|
|
|
319
319
|
## Troubleshooting
|
|
320
320
|
|
|
321
|
-
| Problem | Cause
|
|
322
|
-
| ------------------------------------ |
|
|
323
|
-
| Timeout errors | Function timeout too low or waiting on unreachable resource
|
|
324
|
-
| 502 Bad Gateway | Handler path mismatch, missing env vars, or unhandled exception
|
|
325
|
-
| `Cannot find module @codegenie/...` | Peer dep not installed locally or in Layer
|
|
326
|
-
| Cold starts too slow | Low memory, x86 architecture, or large bundle
|
|
327
|
-
| Redis connection refused from Lambda | Lambda not in the same VPC as ElastiCache
|
|
328
|
-
| `
|
|
321
|
+
| Problem | Cause | Solution |
|
|
322
|
+
| ------------------------------------ | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
|
|
323
|
+
| Timeout errors | Function timeout too low or waiting on unreachable resource | Increase `Timeout` in the SAM template; verify network connectivity to dependencies |
|
|
324
|
+
| 502 Bad Gateway | Handler path mismatch, missing env vars, or unhandled exception | Check CloudWatch Logs; confirm `Handler: handler.handler` and `CodeUri: dist/lambda/` |
|
|
325
|
+
| `Cannot find module @codegenie/...` | Peer dep not installed locally or in Layer | `npm install @codegenie/serverless-express` (the build's `validate` hook checks for it) |
|
|
326
|
+
| Cold starts too slow | Low memory, x86 architecture, or large bundle | Increase memory to 512+ MB, use `arm64`, or enable provisioned concurrency |
|
|
327
|
+
| Redis connection refused from Lambda | Lambda not in the same VPC as ElastiCache | Place the Lambda in the ElastiCache VPC with appropriate security group rules |
|
|
328
|
+
| 500 `server_misconfigured` | A required secret is missing (`SESSION_SECRET_REQUIRED`, `JWT_SECRET_REQUIRED`, ...) | The response body names the `code` and the remedy; set the variable in the function's environment and redeploy |
|
|
329
|
+
| `sam deploy` fails with IAM error | Insufficient permissions for CloudFormation stack creation | Ensure the deploying IAM user/role has `cloudformation:*`, `lambda:*`, `apigateway:*`, and `iam:PassRole` |
|
|
329
330
|
|
|
330
331
|
## Examples
|
|
331
332
|
|
|
@@ -90,6 +90,8 @@ export default MyServer;
|
|
|
90
90
|
|
|
91
91
|
Provision the KV store in the Vercel dashboard under **Storage > Create Database > KV (Redis)**, then link it to your project. Vercel automatically injects the required environment variables.
|
|
92
92
|
|
|
93
|
+
> **Tasks and elicitation on Vercel KV:** Vercel KV has no pub/sub, so background tasks and elicitation cannot use it. The server still starts — tasks are skipped with a `[tasks]` startup warning unless `tasks: { enabled: true }` is set (which keeps `TaskStoreNotSupportedError`), and elicitation throws `ElicitationNotSupportedError` only when its store actually resolves to Vercel KV. Give tasks their own backend with `tasks: { redis }` or `tasks: { sqlite }` (an explicit backend ignores the ambient `KV_REST_API_URL`).
|
|
94
|
+
|
|
93
95
|
## Step 3: Environment Variables
|
|
94
96
|
|
|
95
97
|
Vercel KV variables are injected automatically when the store is linked. For manual setup or additional configuration, set them in the Vercel dashboard (**Settings > Environment Variables**) or via the CLI:
|
|
@@ -178,6 +180,11 @@ Serverless functions are stateless between invocations. All persistent state mus
|
|
|
178
180
|
| Routing | `.vercel/output/config.json` (auto) | Hand-written `rewrites` to `/api/frontmcp` | The adapter writes `routes: [{ src: '/(.*)', dest: '/index' }]` for you |
|
|
179
181
|
| Environment variables | Link KV store in dashboard (auto-injected) | Hardcode `KV_REST_API_URL` in source | Linked stores inject vars automatically and rotate tokens safely |
|
|
180
182
|
|
|
183
|
+
### Bundle-time behavior of serverless builds
|
|
184
|
+
|
|
185
|
+
- `process.env.NODE_ENV` stays a runtime lookup in the Vercel and Lambda bundles. It is not inlined as `"production"` at build time, so a deployment's own `NODE_ENV` (and the production-only checks that read it) behave the same as on a Node server.
|
|
186
|
+
- Optional packages the server may or may not use (`@frontmcp/storage-sqlite`, `@frontmcp/observability`, `@vercel/kv`, `@opentelemetry/sdk-trace-base`) are bundled when installed and left as a lazy `require()` when they are not, so a missing optional package no longer fails the build with "Module not found". `better-sqlite3` (a native addon) is always left external.
|
|
187
|
+
|
|
181
188
|
## Verification Checklist
|
|
182
189
|
|
|
183
190
|
**Build**
|
|
@@ -205,13 +212,14 @@ Serverless functions are stateless between invocations. All persistent state mus
|
|
|
205
212
|
|
|
206
213
|
## Troubleshooting
|
|
207
214
|
|
|
208
|
-
| Problem
|
|
209
|
-
|
|
|
210
|
-
| Function timeout
|
|
211
|
-
| KV connection errors
|
|
212
|
-
| 404 on every route
|
|
213
|
-
| Bundle too large
|
|
214
|
-
| Cold starts too slow
|
|
215
|
+
| Problem | Cause | Solution |
|
|
216
|
+
| -------------------------- | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
217
|
+
| Function timeout | Operation exceeds plan's max duration | Check plan limits (Hobby: 10s, Pro: 60s); upgrade plan or refactor the slow tool |
|
|
218
|
+
| KV connection errors | KV store not linked or env vars missing | Re-link the KV store in the Vercel dashboard; verify `KV_REST_API_URL` and `KV_REST_API_TOKEN` |
|
|
219
|
+
| 404 on every route | Build did not produce `.vercel/output/` | Ensure `frontmcp build --target vercel` ran before `vercel` deploys |
|
|
220
|
+
| Bundle too large | Unnecessary dependencies included | Review dependencies and remove unused packages to reduce bundle size |
|
|
221
|
+
| Cold starts too slow | Heavy decorator initialization | Lazy-load providers; defer heavy work until first tool call |
|
|
222
|
+
| 500 `server_misconfigured` | A required secret is missing (`SESSION_SECRET_REQUIRED`, `JWT_SECRET_REQUIRED`, ...) | The response body names the `code` and the remedy; set the variable in Vercel Project Settings and redeploy |
|
|
215
223
|
|
|
216
224
|
## Examples
|
|
217
225
|
|
|
@@ -610,13 +610,14 @@ class DocsAgent extends AgentContext {}
|
|
|
610
610
|
|
|
611
611
|
## Troubleshooting
|
|
612
612
|
|
|
613
|
-
| Problem
|
|
614
|
-
|
|
|
615
|
-
| Agent not appearing in tool listing
|
|
616
|
-
| LLM authentication error
|
|
617
|
-
| Inner tools not being called
|
|
618
|
-
| Agent times out
|
|
619
|
-
| Peer agent not callable
|
|
613
|
+
| Problem | Cause | Solution |
|
|
614
|
+
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
615
|
+
| Agent not appearing in tool listing | Not registered in `agents` array | Add agent class to `@App` or `@FrontMcp` `agents` array |
|
|
616
|
+
| LLM authentication error | API key not set or incorrect env variable | Verify the environment variable name in `apiKey: { env: '...' }` is set |
|
|
617
|
+
| Inner tools not being called | Tools not listed in `tools` array of `@Agent` | Add tool classes to the `tools` field in the `@Agent` decorator |
|
|
618
|
+
| Agent times out | No timeout or rate limit configured | Add `timeout: { executeMs: 120_000 }` and `rateLimit` to `@Agent` options |
|
|
619
|
+
| Peer agent not callable | Peer has `isVisible: false`, or orchestrator lacks `canSeeOtherAgents: true`, or peer is not in `visibleAgents` whitelist | Set `swarm.isVisible: true` on the peer and `swarm.canSeeOtherAgents: true` (and add the peer to `visibleAgents`) on the orchestrator |
|
|
620
|
+
| Agent call fails with `INVALID_OUTPUT` | The model's reply does not match the agent's `outputSchema` (a value outside an enum, or text that is not JSON) | Tighten the prompt or loosen the schema; the error message names the field, for example `output does not match outputSchema at priority`. The result never carries a stack trace |
|
|
620
621
|
|
|
621
622
|
## Examples
|
|
622
623
|
|
|
@@ -68,6 +68,7 @@ For plugins that accept runtime configuration, extend `DynamicPlugin<TOptions, T
|
|
|
68
68
|
```typescript
|
|
69
69
|
abstract class DynamicPlugin<TOptions extends object, TInput extends object = TOptions> {
|
|
70
70
|
static dynamicProviders?(options: any): readonly ProviderType[];
|
|
71
|
+
static dynamicTools?(options: any): readonly ToolType[];
|
|
71
72
|
static init<TThis>(options: InitOptions<TInput>): PluginReturn<TOptions>;
|
|
72
73
|
get<T>(token: Reference<T>): T;
|
|
73
74
|
}
|
|
@@ -77,6 +78,7 @@ abstract class DynamicPlugin<TOptions extends object, TInput extends object = TO
|
|
|
77
78
|
- `TInput` -- the input type users provide to `init()` (may have optional fields)
|
|
78
79
|
- `init()` creates a provider entry for use in `plugins: [...]` arrays
|
|
79
80
|
- `dynamicProviders()` returns providers computed from the input options
|
|
81
|
+
- `dynamicTools()` returns tools computed from the input options
|
|
80
82
|
|
|
81
83
|
## Quick Start: Minimal DynamicPlugin
|
|
82
84
|
|
|
@@ -328,6 +330,21 @@ export default class MyPlugin extends DynamicPlugin<MyPluginOptions, MyPluginOpt
|
|
|
328
330
|
|
|
329
331
|
The reverse does not work: an option-derived provider cannot inject a provider that a nested plugin exports.
|
|
330
332
|
|
|
333
|
+
### Options named like plugin metadata, and option-derived tools
|
|
334
|
+
|
|
335
|
+
`init(options)` spreads the options into the plugin's metadata, so an option named like a list-valued metadata key (`tools`, `resources`, `prompts`, `skills`, `adapters`, `plugins`, `exports`) used to be read as that list: `RememberPlugin.init({ tools: { enabled: true } })` crashed at startup. A non-array value under one of those keys is now an option and stays out of the metadata; an array still contributes.
|
|
336
|
+
|
|
337
|
+
To register tools only when an option asks for it, declare `static dynamicTools(options)`, the counterpart of `dynamicProviders`. Its tools are added to those from `@Plugin({ tools })` and from an array `tools` option:
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
export default class MemoryPlugin extends DynamicPlugin<MemoryOptions, MemoryOptionsInput> {
|
|
341
|
+
static override dynamicTools = (options: MemoryOptionsInput): readonly ToolType[] =>
|
|
342
|
+
options.tools?.enabled ? [RememberTool, RecallTool] : [];
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
`dynamicTools` runs for `init(options)`; `init({ inject, useFactory })` takes its tools from the `@Plugin` metadata, since the options are unknown until the factory runs.
|
|
347
|
+
|
|
331
348
|
### Installing the same plugin in several apps
|
|
332
349
|
|
|
333
350
|
Each app that installs a plugin gets its own copy of the plugin's providers, including CONTEXT-scoped ones. Tools, resources and prompts resolve the nearest definition in their own hierarchy (plugin, then app, then server). So `this.myService` in app A uses A's options even when app B installs `MyPlugin.init()` with different options:
|
|
@@ -346,6 +346,9 @@ clear the legacy prefixes manually if you want the storage back.
|
|
|
346
346
|
- `forget` -- Remove a stored value by key
|
|
347
347
|
- `list_memories` -- List all stored keys, optionally filtered by pattern
|
|
348
348
|
|
|
349
|
+
`tools.prefix` renames them (`prefix: 'memory_'` gives `memory_recall`, ...; each description names the prefixed siblings) and
|
|
350
|
+
`tools.allowedScopes` rejects any other `scope` (including the default `session` when a call omits it) with a public `REMEMBER_SCOPE_NOT_ALLOWED` error that lists the allowed scopes. With `enabled` unset or `false` none is registered.
|
|
351
|
+
|
|
349
352
|
All four take an optional `scope` (default `session`) and describe it to the model the same way:
|
|
350
353
|
`session` is this session, or without one (stateless HTTP, MCP 2026-07-28) the signed-in caller
|
|
351
354
|
across its requests; `user` is the signed-in caller across all of its sessions; `tool` is the tool
|
|
@@ -502,10 +505,12 @@ class DangerousActionTool extends ToolContext {
|
|
|
502
505
|
```
|
|
503
506
|
|
|
504
507
|
Each grant records its grantor in `grantedBy`. Without one, it is the signed-in caller whose tool
|
|
505
|
-
made the grant: `
|
|
506
|
-
|
|
507
|
-
`
|
|
508
|
-
|
|
508
|
+
made the grant: `{ source: 'user', identifier: '<user id>', method: 'implicit' }` (a tool that grants
|
|
509
|
+
through `this.approval` asked no one, so it is not an `'interactive'` answer; a tool that did ask passes
|
|
510
|
+
`grantedBy: userGrantor(<user id>)`). Without a signed-in user (no principal, or an anonymous `anon:`
|
|
511
|
+
subject) it is `{ source: 'user', method: 'implicit' }` with no identifier. `revokeApproval()` records
|
|
512
|
+
`revokedBy` the same way and `getRevocations(toolId)` reads it back (kept 24 hours). Releases up to 1.8.5
|
|
513
|
+
recorded both as `'policy'`; 1.8.6 recorded `'interactive'` and kept no `revokedBy`. Pass `grantedBy` to
|
|
509
514
|
record anything else; `userGrantor`'s third argument is an options object, not the method:
|
|
510
515
|
|
|
511
516
|
```typescript
|
|
@@ -711,7 +716,9 @@ A disabled flag filters the entry out of `tools/list`, `resources/list`, `prompt
|
|
|
711
716
|
|
|
712
717
|
That matters because a listing is not an access control. Clients cache listings and hold
|
|
713
718
|
resource URIs and prompt names from earlier sessions, so anything gated only at list time
|
|
714
|
-
stays reachable by name.
|
|
719
|
+
stays reachable by name. The refusal is a public `FeatureFlagDisabledError` (`FEATURE_FLAG_DISABLED`, 403)
|
|
720
|
+
that names the capability and the flag. `FeatureFlagPlugin.init()` with no (or an unknown) `adapter` throws a
|
|
721
|
+
`FeatureFlagConfigurationError` at startup. If the adapter is unavailable the gate uses the ref's
|
|
715
722
|
`defaultValue`, and a bare string ref (no default) fails closed.
|
|
716
723
|
|
|
717
724
|
### Installation
|
|
@@ -31,8 +31,8 @@ An authenticated task management MCP server with CRUD tools, a Redis-backed prov
|
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@frontmcp/testing": "^1.0.0",
|
|
34
|
-
"jest": "^
|
|
35
|
-
"ts-jest": "^29.
|
|
34
|
+
"jest": "^30.0.0",
|
|
35
|
+
"ts-jest": "^29.4.0",
|
|
36
36
|
"typescript": "^5.4.0",
|
|
37
37
|
},
|
|
38
38
|
}
|
|
@@ -30,8 +30,8 @@ A complete beginner MCP server that exposes a weather lookup tool and a static r
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@frontmcp/testing": "^1.0.0",
|
|
33
|
-
"jest": "^
|
|
34
|
-
"ts-jest": "^29.
|
|
33
|
+
"jest": "^30.0.0",
|
|
34
|
+
"ts-jest": "^29.4.0",
|
|
35
35
|
"typescript": "^5.4.0",
|
|
36
36
|
},
|
|
37
37
|
}
|
package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md
CHANGED
|
@@ -58,8 +58,8 @@ Shows the correct package.json configuration for publishing a FrontMCP SDK packa
|
|
|
58
58
|
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@frontmcp/testing": "^1.0.0",
|
|
61
|
-
"jest": "^
|
|
62
|
-
"ts-jest": "^29.
|
|
61
|
+
"jest": "^30.0.0",
|
|
62
|
+
"ts-jest": "^29.4.0",
|
|
63
63
|
"typescript": "^5.4.0",
|
|
64
64
|
"zod": "^4.0.0",
|
|
65
65
|
},
|
|
@@ -63,7 +63,7 @@ These checks apply to ALL deployment targets. Run them first, then proceed to yo
|
|
|
63
63
|
- [ ] Per-client/per-IP limits are set
|
|
64
64
|
- [ ] Throttle configuration uses `@FrontMcp({ throttle: {...} })`
|
|
65
65
|
- [ ] Multi-instance: `throttle.storage` uses the storage shape `{ type: 'redis', redis: { config: { host, port } } }` (not the top-level `redis` shape, which silently falls back to auto-detection)
|
|
66
|
-
- [ ] Decided what happens when the throttle Redis is down at startup: the default fails closed (`GuardStorageUnavailableError`); `throttle.storage.fallback: 'memory'`
|
|
66
|
+
- [ ] Decided what happens when the throttle Redis is down, at startup or mid-run: the default fails closed (`GuardStorageUnavailableError`); `throttle.storage.fallback: 'memory'` uses per-instance counters instead
|
|
67
67
|
- [ ] Large payload limits are set to prevent memory exhaustion
|
|
68
68
|
|
|
69
69
|
### Dependencies
|
|
@@ -122,12 +122,20 @@ When a request arrives for a session owned by a dead pod:
|
|
|
122
122
|
|
|
123
123
|
Each pod subscribes to `mcp:ha:notify:{nodeId}` via Redis Pub/Sub. Cross-pod MCP notifications (progress updates, resource changes) are published to the target pod's channel for local delivery.
|
|
124
124
|
|
|
125
|
+
## Redis Connection, TTL and Recovery
|
|
126
|
+
|
|
127
|
+
- HA uses one dedicated ioredis client built from the top-level `redis` config (host/port/password/db/tls or `url`). It reconnects on its own, logs errors at a rate-limited interval, and is closed on shutdown. Vercel KV cannot back HA.
|
|
128
|
+
- The orphan scanner reads `<keyPrefix>session:` (default `mcp:transport:session:`), the same prefix the session store writes, and only runs when `transport.persistence.redis` is set.
|
|
129
|
+
- 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
|
+
- 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
|
+
- `.frontmcp/machine-id` is only read/written in standalone development (never in `distributed` or `serverless`); in Kubernetes the machine ID is `HOSTNAME`.
|
|
132
|
+
|
|
125
133
|
## Load Balancer Affinity
|
|
126
134
|
|
|
127
135
|
FrontMCP sets:
|
|
128
136
|
|
|
129
137
|
- **Cookie**: `__frontmcp_node` on Streamable HTTP initialize
|
|
130
|
-
- **Header**: `X-FrontMCP-Machine-Id` on every distributed response
|
|
138
|
+
- **Header**: `X-FrontMCP-Machine-Id` on every distributed response (initialize, message POSTs, DELETE, stateless requests and SSE), applied by the hookable `applyNodeHeaders` flow stage
|
|
131
139
|
|
|
132
140
|
NGINX sticky session example:
|
|
133
141
|
|
|
@@ -173,12 +181,14 @@ upstream mcp_backend {
|
|
|
173
181
|
|
|
174
182
|
## Troubleshooting
|
|
175
183
|
|
|
176
|
-
| Problem | Cause | Solution
|
|
177
|
-
| ---------------------------------------- | -------------------------------------- |
|
|
178
|
-
| Sessions not transferred after pod death | `heartbeatTtlMs` too high | Lower TTL while keeping >= 2x interval (e.g., 20-30s for a 10s interval)
|
|
179
|
-
| `HaConfigurationError` on startup | Missing Redis config | Add `redis` to `@FrontMcp()` decorator
|
|
180
|
-
| Duplicate notifications | Shared Redis subscriber connection | Use dedicated connections per relay
|
|
181
|
-
|
|
|
184
|
+
| Problem | Cause | Solution |
|
|
185
|
+
| ---------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
186
|
+
| Sessions not transferred after pod death | `heartbeatTtlMs` too high | Lower TTL while keeping >= 2x interval (e.g., 20-30s for a 10s interval) |
|
|
187
|
+
| `HaConfigurationError` on startup | Missing Redis config | Add `redis` to `@FrontMcp()` decorator |
|
|
188
|
+
| Duplicate notifications | Shared Redis subscriber connection | Use dedicated connections per relay |
|
|
189
|
+
| 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 |
|
|
190
|
+
| 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 |
|
|
191
|
+
| Session takeover race failures | High pod count + simultaneous restarts | Increase `takeoverGracePeriodMs` |
|
|
182
192
|
|
|
183
193
|
## Examples
|
|
184
194
|
|
|
@@ -81,7 +81,9 @@ Deep check: probes all registered dependencies, returns catalog hash and registr
|
|
|
81
81
|
The health service automatically registers probes for:
|
|
82
82
|
|
|
83
83
|
- **Session store** (Redis/Vercel KV) via `TransportService.pingSessionStore()`
|
|
84
|
-
- **Remote MCP apps** via the existing `HealthCheckManager` background checks
|
|
84
|
+
- **Remote MCP apps** via the existing `HealthCheckManager` background checks. Until the first check completes the probe is `degraded` (`state: unknown`), so `/readyz` stays 200; a remote that fails its checks is `unhealthy` and gives 503.
|
|
85
|
+
|
|
86
|
+
The fetch handler (Workers, Vercel Edge, Deno) honours the same `health` settings (`healthzPath`, `readyzPath`, `readyz.enabled`, `enabled: false` gives 404).
|
|
85
87
|
|
|
86
88
|
## Custom Probes
|
|
87
89
|
|
|
@@ -41,12 +41,14 @@ This creates a full Nx workspace with `@frontmcp/nx` pre-installed, sample app,
|
|
|
41
41
|
|
|
42
42
|
### Option B: Add FrontMCP to an existing Nx workspace
|
|
43
43
|
|
|
44
|
-
Install the plugin:
|
|
44
|
+
Install the plugin with `nx add`. It runs the plugin's `init` generator, which adds `@frontmcp/sdk`, `frontmcp`, `@frontmcp/testing` and the Jest toolchain to `package.json` (existing versions are kept) and makes the `@frontmcp/nx:build`, `build-exec` and `test` executors cacheable in `nx.json` `targetDefaults`:
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
|
-
|
|
47
|
+
nx add @frontmcp/nx
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
If you install the packages yourself (`yarn add -D @frontmcp/nx @frontmcp/sdk frontmcp @frontmcp/testing`), run `nx g @frontmcp/nx:init` once to get the same setup.
|
|
51
|
+
|
|
50
52
|
Then initialize the workspace structure:
|
|
51
53
|
|
|
52
54
|
```bash
|
|
@@ -148,7 +150,7 @@ Creates a `SKILL.md`-based skill directory in `apps/my-app/src/skills/my-skill/`
|
|
|
148
150
|
nx g @frontmcp/nx:agent my-agent --project=my-app
|
|
149
151
|
```
|
|
150
152
|
|
|
151
|
-
Creates an `@Agent`-decorated class in `apps/my-app/src/agents/`. Agents are autonomous AI components with their own LLM providers and isolated scopes, automatically exposed as `use-agent:<agent_id>` tools.
|
|
153
|
+
Creates an `@Agent`-decorated class in `apps/my-app/src/agents/`. Agents are autonomous AI components with their own LLM providers and isolated scopes, automatically exposed as `use-agent:<agent_id>` tools. The generated `llm` block picks `anthropic` (`ANTHROPIC_API_KEY`) for `claude*` models and `openai` (`OPENAI_API_KEY`) otherwise, and `--tools a,b` imports each tool class from `../tools/<name>.tool` (de-duplicated) instead of using string names.
|
|
152
154
|
|
|
153
155
|
### Plugin
|
|
154
156
|
|
|
@@ -156,7 +158,7 @@ Creates an `@Agent`-decorated class in `apps/my-app/src/agents/`. Agents are aut
|
|
|
156
158
|
nx g @frontmcp/nx:plugin my-plugin --project=my-app
|
|
157
159
|
```
|
|
158
160
|
|
|
159
|
-
Creates a `@Plugin` class extending `DynamicPlugin` in `apps/my-app/src/plugins/`.
|
|
161
|
+
Creates a `@Plugin` class extending `DynamicPlugin` in `apps/my-app/src/plugins/`. The plugin takes its options in the constructor and contributes providers through a **static** `dynamicProviders(options)` method; there is no `onRegister` hook to implement.
|
|
160
162
|
|
|
161
163
|
### Adapter
|
|
162
164
|
|
|
@@ -164,7 +166,7 @@ Creates a `@Plugin` class extending `DynamicPlugin` in `apps/my-app/src/plugins/
|
|
|
164
166
|
nx g @frontmcp/nx:adapter my-adapter --project=my-app
|
|
165
167
|
```
|
|
166
168
|
|
|
167
|
-
Creates an `@Adapter` class extending `DynamicAdapter` in `apps/my-app/src/adapters/`. Adapters convert external definitions (OpenAPI, Lambda, etc.) into generated tools, resources, and prompts.
|
|
169
|
+
Creates an `@Adapter` class extending `DynamicAdapter` in `apps/my-app/src/adapters/`. Adapters convert external definitions (OpenAPI, Lambda, etc.) into generated tools, resources, and prompts. The generated class stores its `{ name } & Options` constructor argument and `fetch()` returns a `FrontMcpAdapterResponse`.
|
|
168
170
|
|
|
169
171
|
### Provider
|
|
170
172
|
|
|
@@ -172,7 +174,7 @@ Creates an `@Adapter` class extending `DynamicAdapter` in `apps/my-app/src/adapt
|
|
|
172
174
|
nx g @frontmcp/nx:provider my-provider --project=my-app
|
|
173
175
|
```
|
|
174
176
|
|
|
175
|
-
Creates a `@Provider` class in `apps/my-app/src/providers/`. Providers are named singletons resolved via DI (e.g., database pools, API clients, config).
|
|
177
|
+
Creates a `@Provider` class in `apps/my-app/src/providers/`. Providers are named singletons resolved via DI (e.g., database pools, API clients, config). The class is its own token: register `providers: [MyProvider]` and resolve it with `this.get(MyProvider)`; `--scope singleton` maps to `ProviderScope.GLOBAL`, `request`/`context` to `ProviderScope.CONTEXT`.
|
|
176
178
|
|
|
177
179
|
### Flow
|
|
178
180
|
|
|
@@ -180,7 +182,7 @@ Creates a `@Provider` class in `apps/my-app/src/providers/`. Providers are named
|
|
|
180
182
|
nx g @frontmcp/nx:flow my-flow --project=my-app
|
|
181
183
|
```
|
|
182
184
|
|
|
183
|
-
Creates a `@Flow` class extending `FlowBase` in `apps/my-app/src/flows/`. Flows define execution pipelines with hooks and stages.
|
|
185
|
+
Creates a `@Flow` class extending `FlowBase` in `apps/my-app/src/flows/`. Flows define execution pipelines with hooks and stages. The generated flow declares its schemas, registers itself through `declare global { interface ExtendFlows }` so `runFlow` is typed, and implements each plan step with a `@Stage` method from `FlowHooksOf(name)`.
|
|
184
186
|
|
|
185
187
|
### Job
|
|
186
188
|
|
|
@@ -214,7 +216,9 @@ Creates an `@AuthProvider` class in `apps/my-app/src/auth-providers/`. Auth prov
|
|
|
214
216
|
nx build my-server
|
|
215
217
|
```
|
|
216
218
|
|
|
217
|
-
Builds the server and all its dependencies in the correct order. Nx caches build outputs so subsequent builds of unchanged projects are instant.
|
|
219
|
+
Builds the server and all its dependencies in the correct order. Nx caches build outputs so subsequent builds of unchanged projects are instant (generated projects set `cache: true`, and `init` covers existing workspaces).
|
|
220
|
+
|
|
221
|
+
The `@frontmcp/nx:build` executor runs `frontmcp build` from the project root using the `frontmcp` CLI installed in the workspace (never `npx`, which would download the newest CLI). Choose the platform with the `target` option (`node`, `vercel`, `lambda`, `cloudflare`); `adapter` is a deprecated alias. Code imported from workspace libraries through `tsconfig.base.json` path aliases is resolved and bundled for every target.
|
|
218
222
|
|
|
219
223
|
### Test a Single Project
|
|
220
224
|
|
|
@@ -222,7 +226,9 @@ Builds the server and all its dependencies in the correct order. Nx caches build
|
|
|
222
226
|
nx test my-app
|
|
223
227
|
```
|
|
224
228
|
|
|
225
|
-
Runs
|
|
229
|
+
Runs `frontmcp test` from the project root. The generated `jest.config.cjs` uses the swc transform, loads `@frontmcp/testing/setup`, and maps the `tsconfig.base.json` path aliases so imports of workspace libraries resolve. Test files must use `.spec.ts` extension (not `.test.ts`).
|
|
230
|
+
|
|
231
|
+
The `inspector` executor forwards its `port` option as the `CLIENT_PORT` environment variable (the `frontmcp inspector` command has no port flag).
|
|
226
232
|
|
|
227
233
|
### Build All Projects
|
|
228
234
|
|
|
@@ -284,8 +290,9 @@ my-project/
|
|
|
284
290
|
my-app.app.ts # @App class
|
|
285
291
|
index.ts # barrel exports
|
|
286
292
|
project.json
|
|
293
|
+
package.json # minimal manifest so `frontmcp build` runs in the project root
|
|
287
294
|
tsconfig.json
|
|
288
|
-
jest.config.
|
|
295
|
+
jest.config.cjs
|
|
289
296
|
libs/
|
|
290
297
|
my-lib/
|
|
291
298
|
src/
|
|
@@ -405,13 +412,14 @@ Complete list of all `@frontmcp/nx` generators from `generators.json`:
|
|
|
405
412
|
|
|
406
413
|
## Troubleshooting
|
|
407
414
|
|
|
408
|
-
| Problem | Cause
|
|
409
|
-
| ---------------------------------------------- |
|
|
410
|
-
| `Cannot find module '@frontmcp/nx'` | Plugin not installed
|
|
411
|
-
| Generator creates files in the wrong directory | Missing or incorrect `--project` flag
|
|
412
|
-
| `nx affected` runs nothing despite changes | Base branch not configured or no dependency link
|
|
413
|
-
| Build fails with circular dependency error | Library A imports from Library B and vice versa
|
|
414
|
-
| Cache not working (full rebuild every time) |
|
|
415
|
+
| Problem | Cause | Solution |
|
|
416
|
+
| ---------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
417
|
+
| `Cannot find module '@frontmcp/nx'` | Plugin not installed | Run `yarn add -D @frontmcp/nx` and ensure it appears in `devDependencies` |
|
|
418
|
+
| Generator creates files in the wrong directory | Missing or incorrect `--project` flag | Always pass `--project=<app-name>` for primitive generators; verify the app exists in `apps/` |
|
|
419
|
+
| `nx affected` runs nothing despite changes | Base branch not configured or no dependency link | Check `nx.json` for `defaultBase` setting; verify the changed file belongs to a project in the graph |
|
|
420
|
+
| Build fails with circular dependency error | Library A imports from Library B and vice versa | Use `nx graph` to visualize the cycle; extract shared code into a new library |
|
|
421
|
+
| Cache not working (full rebuild every time) | Executor targets are not marked cacheable | Run `nx g @frontmcp/nx:init`, or set `cache: true` on the target / in `targetDefaults` |
|
|
422
|
+
| `Cannot find module '@scope/lib'` in Jest | Old `jest.config.ts` without the path-alias mapper | Use the generated `jest.config.cjs` (maps `tsconfig.base.json` paths) or add a `moduleNameMapper` |
|
|
415
423
|
|
|
416
424
|
## Examples
|
|
417
425
|
|
|
@@ -319,7 +319,7 @@ Every instance behind the load balancer needs the **same** values:
|
|
|
319
319
|
### What happens when Redis is down at startup
|
|
320
320
|
|
|
321
321
|
- `redis` and `transport.persistence` fall back to in-memory storage and log the failure (`[TransportService] Failed to connect to redis - session persistence disabled`); the server starts.
|
|
322
|
-
- `throttle.storage` fails closed: startup aborts with `GuardStorageUnavailableError` (`throttle.storage (redis) is unavailable: …`), the default in production. Opt in to per-instance counters explicitly:
|
|
322
|
+
- `throttle.storage` fails closed: startup aborts with `GuardStorageUnavailableError` (`throttle.storage (redis) is unavailable: …`), the default in production. If Redis goes away while the server runs, a rate-limited call is refused with the same error, not `Internal FrontMCP error`. Opt in to per-instance counters explicitly (they also cover a mid-run outage):
|
|
323
323
|
|
|
324
324
|
```typescript
|
|
325
325
|
throttle: {
|