@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.
Files changed (40) hide show
  1. package/catalog/create-tool/SKILL.md +24 -24
  2. package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +0 -1
  3. package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +1 -3
  4. package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +4 -6
  5. package/catalog/create-tool/references/decorator-options.md +1 -1
  6. package/catalog/create-tool/references/ui-widgets.md +68 -41
  7. package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +3 -3
  8. package/catalog/frontmcp-config/references/configure-deployment-targets.md +19 -1
  9. package/catalog/frontmcp-config/references/configure-http.md +6 -4
  10. package/catalog/frontmcp-config/references/configure-security-headers.md +18 -13
  11. package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +4 -4
  12. package/catalog/frontmcp-config/references/configure-throttle.md +1 -1
  13. package/catalog/frontmcp-deployment/SKILL.md +19 -19
  14. package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +5 -3
  15. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +2 -0
  16. package/catalog/frontmcp-deployment/references/build-for-browser.md +8 -0
  17. package/catalog/frontmcp-deployment/references/build-for-mcpb.md +9 -8
  18. package/catalog/frontmcp-deployment/references/build-for-sdk.md +9 -9
  19. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +3 -1
  20. package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +9 -8
  21. package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +15 -7
  22. package/catalog/frontmcp-development/references/create-agent.md +8 -7
  23. package/catalog/frontmcp-development/references/create-plugin.md +17 -0
  24. package/catalog/frontmcp-development/references/official-plugins.md +12 -5
  25. package/catalog/frontmcp-guides/references/example-task-manager.md +2 -2
  26. package/catalog/frontmcp-guides/references/example-weather-api.md +2 -2
  27. package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +1 -0
  28. package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md +2 -2
  29. package/catalog/frontmcp-production-readiness/references/common-checklist.md +1 -1
  30. package/catalog/frontmcp-production-readiness/references/distributed-ha.md +17 -7
  31. package/catalog/frontmcp-production-readiness/references/health-readiness-endpoints.md +3 -1
  32. package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +1 -1
  33. package/catalog/frontmcp-setup/references/nx-workflow.md +25 -17
  34. package/catalog/frontmcp-setup/references/setup-redis.md +1 -1
  35. package/catalog/frontmcp-setup/references/setup-sqlite.md +9 -6
  36. package/catalog/frontmcp-testing/SKILL.md +24 -23
  37. package/catalog/frontmcp-testing/references/setup-testing.md +26 -2
  38. package/catalog/frontmcp-testing/references/test-auth.md +8 -0
  39. package/catalog/skills-manifest.json +4 -4
  40. 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/sdk';
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 | Cause | Solution |
178
- | ------------------------------------------------------ | -------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
179
- | `@frontmcp/sdk is required for schema extraction` | SDK missing or externalized from bundle | Ensure `@frontmcp/sdk` is installed |
180
- | Archive > 100 MB | node_modules bundled or node runtime bloated | Tune `build.esbuild.external`, drop `--sea`, or disable `includeNodeModules` |
181
- | `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 |
182
- | `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` |
183
- | 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 |
184
- | `platform_overrides.{platform}.command` missing binary | `--merge-from` folders don't match MCPB platform keys | See the expected layout below |
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 available resources |
216
- | `readResource(uri)` | Read a resource |
217
- | `listPrompts()` | List available prompts |
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 | 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
- | `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` |
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 | Cause | Solution |
209
- | -------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------- |
210
- | Function timeout | Operation exceeds plan's max duration | Check plan limits (Hobby: 10s, Pro: 60s); upgrade plan or refactor the slow tool |
211
- | 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` |
212
- | 404 on every route | Build did not produce `.vercel/output/` | Ensure `frontmcp build --target vercel` ran before `vercel` deploys |
213
- | Bundle too large | Unnecessary dependencies included | Review dependencies and remove unused packages to reduce bundle size |
214
- | Cold starts too slow | Heavy decorator initialization | Lazy-load providers; defer heavy work until first tool call |
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 | 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 |
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: `userGrantor(<user id>)`, i.e. `{ source: 'user', identifier: '<user id>', method:
506
- 'interactive' }`. Without a signed-in user (no principal, or an anonymous `anon:` subject) it is
507
- `{ source: 'user' }` with no identifier. `revokeApproval()` records `revokedBy` the same way
508
- (`userRevoker(<user id>)`). Releases up to 1.8.5 recorded both as `'policy'`. Pass `grantedBy` to
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. If the adapter is unavailable the gate uses the ref's
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": "^29.0.0",
35
- "ts-jest": "^29.0.0",
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": "^29.0.0",
34
- "ts-jest": "^29.0.0",
33
+ "jest": "^30.0.0",
34
+ "ts-jest": "^29.4.0",
35
35
  "typescript": "^5.4.0",
36
36
  },
37
37
  }
@@ -51,6 +51,7 @@ class MainApp {}
51
51
  provider: 'redis',
52
52
  host: process.env['REDIS_HOST'] || 'redis',
53
53
  port: 6379,
54
+ defaultTtlMs: 30 * 60_000, // slides while the owning pod serves the session; default 1 hour
54
55
  },
55
56
  },
56
57
  },
@@ -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": "^29.0.0",
62
- "ts-jest": "^29.0.0",
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'` starts with per-instance counters instead
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
- | Session takeover race failures | High pod count + simultaneous restarts | Increase `takeoverGracePeriodMs` |
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
 
@@ -52,7 +52,7 @@ apps/billing/
52
52
  index.ts # barrel exports updated automatically
53
53
  project.json
54
54
  tsconfig.json
55
- jest.config.ts
55
+ jest.config.cjs
56
56
  ```
57
57
 
58
58
  ```bash
@@ -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
- yarn add -D @frontmcp/nx
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/`. Plugins participate in lifecycle events and can contribute additional capabilities.
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 Jest tests for the specified project. Test files must use `.spec.ts` extension (not `.test.ts`).
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.ts
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 | Solution |
409
- | ---------------------------------------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
410
- | `Cannot find module '@frontmcp/nx'` | Plugin not installed | Run `yarn add -D @frontmcp/nx` and ensure it appears in `devDependencies` |
411
- | 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/` |
412
- | `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 |
413
- | 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 |
414
- | Cache not working (full rebuild every time) | Missing or misconfigured `cacheableOperations` in `nx.json` | Ensure `build`, `test`, and `lint` are listed in `targetDefaults` with `cache: true` |
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: {