@frontmcp/skills 1.8.5 → 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 (55) 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 +7 -6
  4. package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +4 -6
  5. package/catalog/create-tool/examples/27-tool-with-examples-metadata.md +3 -2
  6. package/catalog/create-tool/references/decorator-options.md +1 -1
  7. package/catalog/create-tool/references/elicitation.md +1 -0
  8. package/catalog/create-tool/references/file-layout.md +2 -0
  9. package/catalog/create-tool/references/ui-widgets.md +96 -40
  10. package/catalog/create-tool/rules/widget-paths-anchor-with-import-meta-url.md +10 -3
  11. package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +8 -0
  12. package/catalog/frontmcp-config/references/configure-auth.md +1 -0
  13. package/catalog/frontmcp-config/references/configure-deployment-targets.md +19 -1
  14. package/catalog/frontmcp-config/references/configure-http.md +6 -4
  15. package/catalog/frontmcp-config/references/configure-security-headers.md +18 -13
  16. package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +19 -4
  17. package/catalog/frontmcp-config/references/configure-throttle.md +25 -7
  18. package/catalog/frontmcp-deployment/SKILL.md +19 -19
  19. package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +5 -3
  20. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-mcp-endpoint-test.md +1 -1
  21. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +3 -1
  22. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-skills-cache.md +1 -1
  23. package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md +7 -3
  24. package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/vercel-config-with-security-headers.md +4 -3
  25. package/catalog/frontmcp-deployment/references/build-for-browser.md +8 -0
  26. package/catalog/frontmcp-deployment/references/build-for-mcpb.md +9 -8
  27. package/catalog/frontmcp-deployment/references/build-for-sdk.md +9 -9
  28. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +3 -1
  29. package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +9 -8
  30. package/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md +15 -3
  31. package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +16 -8
  32. package/catalog/frontmcp-deployment/references/protocol-versions.md +9 -0
  33. package/catalog/frontmcp-development/examples/official-plugins/cache-and-feature-flags.md +5 -0
  34. package/catalog/frontmcp-development/examples/official-plugins/remember-plugin-session-memory.md +7 -5
  35. package/catalog/frontmcp-development/references/create-agent.md +8 -7
  36. package/catalog/frontmcp-development/references/create-plugin.md +17 -0
  37. package/catalog/frontmcp-development/references/official-plugins.md +66 -8
  38. package/catalog/frontmcp-guides/references/example-task-manager.md +2 -2
  39. package/catalog/frontmcp-guides/references/example-weather-api.md +2 -2
  40. package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +1 -0
  41. package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md +2 -2
  42. package/catalog/frontmcp-production-readiness/examples/production-vercel/vercel-edge-config.md +1 -0
  43. package/catalog/frontmcp-production-readiness/references/common-checklist.md +4 -0
  44. package/catalog/frontmcp-production-readiness/references/distributed-ha.md +17 -7
  45. package/catalog/frontmcp-production-readiness/references/health-readiness-endpoints.md +3 -1
  46. package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +1 -1
  47. package/catalog/frontmcp-setup/references/nx-workflow.md +25 -17
  48. package/catalog/frontmcp-setup/references/setup-redis.md +37 -7
  49. package/catalog/frontmcp-setup/references/setup-sqlite.md +9 -6
  50. package/catalog/frontmcp-testing/SKILL.md +24 -23
  51. package/catalog/frontmcp-testing/references/setup-testing.md +28 -3
  52. package/catalog/frontmcp-testing/references/test-auth.md +8 -0
  53. package/catalog/frontmcp-testing/references/test-e2e-handler.md +1 -0
  54. package/catalog/skills-manifest.json +9 -6
  55. package/package.json +1 -1
@@ -5,7 +5,7 @@ description: Configure CSP, HSTS, X-Frame-Options, and X-Content-Type-Options vi
5
5
 
6
6
  # Configure Security Headers
7
7
 
8
- Set Content Security Policy (CSP), HSTS, and other security headers on every HTTP response. Configure them in `frontmcp.config` per deployment target — the build adapter injects them as environment variables that the built-in middleware reads at runtime.
8
+ Set Content Security Policy (CSP), HSTS, and other security headers on every HTTP response. Configure them in `frontmcp.config` per deployment target — `frontmcp dev` and the `cloudflare`, `vercel`, `lambda` and `distributed` builds pass them to the server as `FRONTMCP_*` environment variables (set only when the platform has not already defined them). The `node` target has no setup file: set the variables where the server runs or use `@FrontMcp({ http: { securityHeaders } })`. `X-Content-Type-Options: nosniff` and `X-Frame-Options: DENY` are on by default and `X-Powered-By` is never sent; the Express host and the Workers/Vercel Edge fetch handler share one resolver.
9
9
 
10
10
  ## When to Use This Skill
11
11
 
@@ -109,7 +109,9 @@ curl -I http://localhost:3000/healthz
109
109
  | `frameOptions` | `string \| false` | `DENY` | `X-Frame-Options` |
110
110
  | `custom` | `Record<string,string>` | --- | Any custom headers |
111
111
 
112
- Set any of the first three fields to `false` to explicitly disable that header.
112
+ Set any of the first three fields to `false` to omit that header (env var value: `off`).
113
+
114
+ The same settings work on the decorator: `@FrontMcp({ http: { securityHeaders: { hsts, contentTypeOptions, frameOptions, csp, custom } } })`. Precedence: decorator, then `FRONTMCP_*` variables, then defaults.
113
115
 
114
116
  ### Value-Less CSP Directives
115
117
 
@@ -137,17 +139,20 @@ csp: {
137
139
 
138
140
  ### Environment Variables
139
141
 
140
- The build adapter converts config to these env vars (can also be overridden at runtime):
141
-
142
- | Variable | Config Path |
143
- | ------------------------------- | ------------------------------------ |
144
- | `FRONTMCP_CSP_ENABLED` | `server.csp.enabled` |
145
- | `FRONTMCP_CSP_DIRECTIVES` | `server.csp.directives` (serialized) |
146
- | `FRONTMCP_CSP_REPORT_URI` | `server.csp.reportUri` |
147
- | `FRONTMCP_CSP_REPORT_ONLY` | `server.csp.reportOnly` |
148
- | `FRONTMCP_HSTS` | `server.headers.hsts` |
149
- | `FRONTMCP_CONTENT_TYPE_OPTIONS` | `server.headers.contentTypeOptions` |
150
- | `FRONTMCP_FRAME_OPTIONS` | `server.headers.frameOptions` |
142
+ The CLI converts config to these env vars for `dev` and the serverless/distributed builds (you can also set them yourself at runtime):
143
+
144
+ | Variable | Config Path |
145
+ | ------------------------------- | ------------------------------------- |
146
+ | `FRONTMCP_CSP_ENABLED` | `server.csp.enabled` |
147
+ | `FRONTMCP_CSP_DIRECTIVES` | `server.csp.directives` (serialized) |
148
+ | `FRONTMCP_CSP_REPORT_URI` | `server.csp.reportUri` |
149
+ | `FRONTMCP_CSP_REPORT_ONLY` | `server.csp.reportOnly` |
150
+ | `FRONTMCP_HSTS` | `server.headers.hsts` |
151
+ | `FRONTMCP_CONTENT_TYPE_OPTIONS` | `server.headers.contentTypeOptions` |
152
+ | `FRONTMCP_FRAME_OPTIONS` | `server.headers.frameOptions` |
153
+ | `FRONTMCP_HEADERS_CUSTOM` | `server.headers.custom` (JSON object) |
154
+
155
+ `off`, `false` or `none` in `FRONTMCP_HSTS`, `FRONTMCP_CONTENT_TYPE_OPTIONS` or `FRONTMCP_FRAME_OPTIONS` omits that header.
151
156
 
152
157
  ## Common Patterns
153
158
 
@@ -11,13 +11,23 @@ description: Complete GuardConfig interface reference for rate limiting, concurr
11
11
  interface GuardConfig {
12
12
  enabled: boolean;
13
13
 
14
- // Storage for distributed rate limiting
14
+ // Storage for distributed rate limiting -- a StorageConfig from @frontmcp/utils,
15
+ // NOT the top-level `redis` shape (a block without `type` is auto-detected
16
+ // from REDIS_URL / REDIS_HOST and otherwise runs in memory)
15
17
  storage?: {
16
- type: 'memory' | 'redis';
17
- redis?: RedisOptionsInput;
18
+ type?: 'memory' | 'redis' | 'vercel-kv' | 'upstash' | 'auto';
19
+ redis?:
20
+ | { config: { host: string; port?: number; password?: string; db?: number; tls?: boolean } }
21
+ | { url: string };
22
+ vercelKv?: { url?: string; token?: string };
23
+ upstash?: { url?: string; token?: string };
24
+ // What to do when the backend is unreachable, at startup or while running:
25
+ // 'error' (default in production) -- startup fails, and a limited call is refused, with GuardStorageUnavailableError (rate limits fail closed)
26
+ // 'memory' (default otherwise) -- use per-instance counters (and go back to the backend once it answers)
27
+ fallback?: 'error' | 'memory';
18
28
  };
19
29
 
20
- keyPrefix?: string; // default: 'mcp:guard:'
30
+ keyPrefix?: string; // default: 'mcp:guard:' -- a trailing ':' is dropped, keys read 'mcp:guard:<entity>:...'
21
31
 
22
32
  // Server-wide limits
23
33
  global?: RateLimitConfig;
@@ -57,6 +67,11 @@ interface IpFilterConfig {
57
67
  }
58
68
  ```
59
69
 
70
+ ## Storage Failure and Key Format
71
+
72
+ - **Fails closed.** When `storage` cannot be reached at startup, the server does not start: startup rejects with `GuardStorageUnavailableError` (code `GUARD_STORAGE_UNAVAILABLE`), whose message names `throttle.storage`. That is the default in production. If the backend goes away while the server runs, a limited call is refused with the same error (a readable 503, not `Internal FrontMCP error`). Set `storage.fallback: 'memory'` to use per-instance counters instead; a mid-run outage then logs one warning and the backend is retried every 30 seconds. (The top-level `redis` and `transport.persistence` differ: they fall back to memory with an error log.)
73
+ - **Keys.** `<keyPrefix><entity>:<partition>:<kind>:...`, e.g. `mcp:guard:export_tickets:global:rl:1790722980000`. Before 1.8.6 the default prefix wrote `mcp:guard::export_tickets:...`; old and new instances do not read each other's counters, so limits briefly split during a rolling deploy. A custom `keyPrefix` without a trailing `:` keeps its keys.
74
+
60
75
  ## Partition Strategies
61
76
 
62
77
  - **`'global'`**: Single counter shared by all clients. Protects total server capacity.
@@ -244,6 +244,22 @@ throttle: {
244
244
  }
245
245
  ```
246
246
 
247
+ `storage` is a `StorageConfig` from `@frontmcp/utils` -- `type` picks the backend, and its options go under the matching key (`redis: { config }` or `redis: { url }`, `vercelKv: { url, token }`, `upstash: { url, token }`). It is NOT the top-level `redis` shape: `{ provider: 'redis', host, port }` has no `type`, so it is auto-detected from `REDIS_URL` / `REDIS_HOST` and otherwise runs in memory.
248
+
249
+ **Rate limits fail closed.** If the store is unreachable at startup, the server does not start: startup rejects with `GuardStorageUnavailableError` (`throttle.storage (redis) is unavailable: ...`), the default in production. If the store goes away while the server is running, a limited call is refused with the same error rather than `Internal FrontMCP error`. To use per-instance counters instead (at startup and during a mid-run outage, going back to the store once it answers), opt in:
250
+
251
+ ```typescript
252
+ storage: {
253
+ type: 'redis',
254
+ redis: { url: process.env['REDIS_URL'] },
255
+ fallback: 'memory',
256
+ },
257
+ ```
258
+
259
+ The top-level `redis` and `transport.persistence` behave differently: they fall back to in-memory storage with an error log.
260
+
261
+ Keys are `<keyPrefix><entity>:<partition>:<kind>:...` (`mcp:guard:export_tickets:global:rl:...`); a trailing `:` on `keyPrefix` is dropped. Before 1.8.6 the default prefix wrote `mcp:guard::...`, so counters briefly split between versions during a rolling deploy.
262
+
247
263
  ## Verification
248
264
 
249
265
  ```bash
@@ -297,13 +313,15 @@ done
297
313
 
298
314
  ## Troubleshooting
299
315
 
300
- | Problem | Cause | Solution |
301
- | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
302
- | Rate limits not enforced across instances | In-memory storage used with multiple server replicas | Configure `storage: { type: 'redis' }` in the throttle block to share counters |
303
- | All requests rejected with 403 | `ipFilter.defaultAction` set to `'deny'` without any `allowList` entries, or the runtime reports no client IP (a custom fetch wrapper that drops the second handler argument on Deno/Bun) | Add the allowed IP ranges to `allowList`, pass the platform's second argument through to the handler, or change `defaultAction` to `'allow'` |
304
- | Tools timing out unexpectedly | `defaultTimeout.executeMs` too low for the tool's normal execution time | Increase the global default or set a per-tool `timeout.executeMs` override |
305
- | `X-Forwarded-For` header ignored | No trusted proxy declared. `ipFilter.trustProxy` / `trustedProxyDepth` are accepted by the schema but NOT read -- client-IP extraction happens in the SDK context layer, before guard config is reachable | Set the `FRONTMCP_TRUST_PROXY=true` and `FRONTMCP_TRUSTED_PROXY_DEPTH` environment variables instead |
306
- | Rate limit resets not aligned with expectations | `windowMs` misunderstood as a sliding window when it is a fixed window | The window is fixed; all counters reset at the end of each `windowMs` interval |
316
+ | Problem | Cause | Solution |
317
+ | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
318
+ | Rate limits not enforced across instances | In-memory storage used with multiple server replicas | Configure `storage: { type: 'redis' }` in the throttle block to share counters |
319
+ | Startup fails with `GuardStorageUnavailableError` | The `throttle.storage` backend is unreachable; rate limits fail closed | Bring the store up, or set `throttle.storage.fallback: 'memory'` to start with per-instance counters |
320
+ | Redis-backed limits still per-instance | `storage` written in the top-level `redis` shape (`{ provider: 'redis', host }`) with no `type`, so it was auto-detected as memory | Use `{ type: 'redis', redis: { config: { host, port } } }` |
321
+ | All requests rejected with 403 | `ipFilter.defaultAction` set to `'deny'` without any `allowList` entries, or the runtime reports no client IP (a custom fetch wrapper that drops the second handler argument on Deno/Bun) | Add the allowed IP ranges to `allowList`, pass the platform's second argument through to the handler, or change `defaultAction` to `'allow'` |
322
+ | Tools timing out unexpectedly | `defaultTimeout.executeMs` too low for the tool's normal execution time | Increase the global default or set a per-tool `timeout.executeMs` override |
323
+ | `X-Forwarded-For` header ignored | No trusted proxy declared. `ipFilter.trustProxy` / `trustedProxyDepth` are accepted by the schema but NOT read -- client-IP extraction happens in the SDK context layer, before guard config is reachable | Set the `FRONTMCP_TRUST_PROXY=true` and `FRONTMCP_TRUSTED_PROXY_DEPTH` environment variables instead |
324
+ | Rate limit resets not aligned with expectations | `windowMs` misunderstood as a sliding window when it is a fixed window | The window is fixed; all counters reset at the end of each `windowMs` interval |
307
325
 
308
326
  ## Examples
309
327
 
@@ -73,25 +73,25 @@ Entry point for deploying and building FrontMCP servers. This skill helps you ch
73
73
 
74
74
  Beyond `frontmcp build`, the CLI provides commands for the full deployment lifecycle:
75
75
 
76
- | Command | Description |
77
- | ---------------------------- | ----------------------------------------------------------------------------------- |
78
- | `frontmcp build -t <target>` | Build for target: `node`, `vercel`, `lambda`, `cloudflare`, `cli`, `browser`, `sdk` |
79
- | `frontmcp build -t cli --js` | Build CLI as JS bundle (instead of native binary via SEA) |
80
- | `frontmcp build --no-clean` | Keep existing output instead of clearing the target's output directory first |
81
- | `frontmcp start <name>` | Start a named MCP server with supervisor (process management) |
82
- | `frontmcp stop <name>` | Stop managed server (`-f` for force kill) |
83
- | `frontmcp restart <name>` | Restart managed server |
84
- | `frontmcp status [name]` | Show process status (detail if name given, table if omitted) |
85
- | `frontmcp list` | List all managed processes |
86
- | `frontmcp logs <name>` | Tail log output (`-F` follow, `-n` lines) |
87
- | `frontmcp socket <entry>` | Start Unix socket daemon for local MCP server |
88
- | `frontmcp service <action>` | Install/uninstall systemd (Linux) or launchd (macOS) service |
89
- | `frontmcp install <source>` | Install MCP app from npm, local path, or git |
90
- | `frontmcp uninstall <name>` | Remove installed MCP app |
91
- | `frontmcp configure <name>` | Re-run setup questionnaire for installed app |
92
- | `frontmcp doctor` | Check Node.js/npm versions and tsconfig requirements |
93
- | `frontmcp inspector` | Launch MCP Inspector for debugging |
94
- | `frontmcp init` | Create or fix tsconfig.json for FrontMCP |
76
+ | Command | Description |
77
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
78
+ | `frontmcp build -t <target>` | Build for target: `node`, `vercel`, `lambda`, `cloudflare`, `cli`, `browser`, `sdk` |
79
+ | `frontmcp build -t cli --js` | Build CLI as JS bundle (instead of native binary via SEA) |
80
+ | `frontmcp build --no-clean` | Keep existing output instead of clearing the target's output directory first |
81
+ | `frontmcp start <name>` | Start a named MCP server with supervisor (process management) |
82
+ | `frontmcp stop <name>` | Stop managed server (`-f` for force kill) |
83
+ | `frontmcp restart <name>` | Restart managed server |
84
+ | `frontmcp status [name]` | Show process status (detail if name given, table if omitted) |
85
+ | `frontmcp list` | List all managed processes |
86
+ | `frontmcp logs <name>` | Tail log output (`-F` follow, `-n` lines) |
87
+ | `frontmcp socket <entry>` | Start Unix socket daemon for local MCP server |
88
+ | `frontmcp service <action>` | Install/uninstall systemd (Linux) or launchd (macOS) service |
89
+ | `frontmcp install <source>` | Install MCP app from npm, local path, git, or a project's `dist/` / `dist/node` (installs external runtime packages; `frontmcp start <name>` then runs it from its install dir) |
90
+ | `frontmcp uninstall <name>` | Remove installed MCP app |
91
+ | `frontmcp configure <name>` | Re-run setup questionnaire for installed app |
92
+ | `frontmcp doctor` | Check Node.js/npm versions and tsconfig requirements |
93
+ | `frontmcp inspector` | Launch MCP Inspector for debugging |
94
+ | `frontmcp init` | Create or fix tsconfig.json for FrontMCP |
95
95
 
96
96
  ## Target Comparison
97
97
 
@@ -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
  }
@@ -55,7 +55,7 @@ vercel --prod
55
55
  // `rewrites` keyed on `api/frontmcp.ts`/`.js` (no such file exists).
56
56
  {
57
57
  "version": 2,
58
- "buildCommand": "yarn build",
58
+ "buildCommand": "yarn frontmcp build --target vercel",
59
59
  "installCommand": "yarn install"
60
60
  }
61
61
  ```
@@ -53,7 +53,7 @@ export default class MyServer {}
53
53
  // buildCommand/installCommand are detected from your lockfile.
54
54
  {
55
55
  "version": 2,
56
- "buildCommand": "yarn build",
56
+ "buildCommand": "yarn frontmcp build --target vercel",
57
57
  "installCommand": "yarn install"
58
58
  }
59
59
  ```
@@ -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`).
@@ -58,7 +58,7 @@ vercel env add LOG_LEVEL info
58
58
  // Do not add functions/rewrites referencing api/frontmcp.* (no such file).
59
59
  {
60
60
  "version": 2,
61
- "buildCommand": "yarn build",
61
+ "buildCommand": "yarn frontmcp build --target vercel",
62
62
  "installCommand": "yarn install"
63
63
  }
64
64
  ```
@@ -11,6 +11,7 @@ tags:
11
11
  - minimal
12
12
  features:
13
13
  - The exact shape of the auto-generated `vercel.json` — three keys, nothing else
14
+ - That `buildCommand` builds the vercel target (`<exec> frontmcp build --target vercel`), not the `build` script
14
15
  - That routing and function configuration live in `.vercel/output/`, not `vercel.json`
15
16
  - That hand-authoring `api/frontmcp.ts` references in `vercel.json` is unnecessary and breaks deploys
16
17
  ---
@@ -25,7 +26,7 @@ features:
25
26
  // vercel.json — yarn project (yarn.lock present)
26
27
  {
27
28
  "version": 2,
28
- "buildCommand": "yarn build",
29
+ "buildCommand": "yarn frontmcp build --target vercel",
29
30
  "installCommand": "yarn install"
30
31
  }
31
32
  ```
@@ -34,7 +35,7 @@ features:
34
35
  // vercel.json — pnpm project (pnpm-lock.yaml present)
35
36
  {
36
37
  "version": 2,
37
- "buildCommand": "pnpm run build",
38
+ "buildCommand": "pnpm exec frontmcp build --target vercel",
38
39
  "installCommand": "pnpm install"
39
40
  }
40
41
  ```
@@ -43,11 +44,13 @@ features:
43
44
  // vercel.json — npm project (package-lock.json present)
44
45
  {
45
46
  "version": 2,
46
- "buildCommand": "npm run build",
47
+ "buildCommand": "npx frontmcp build --target vercel",
47
48
  "installCommand": "npm install"
48
49
  }
49
50
  ```
50
51
 
52
+ `buildCommand` runs the vercel target through the project's package manager (bun projects get `bunx frontmcp build --target vercel`). It is never `<pm> run build`: the `build` script is `frontmcp build`, which builds the config's deployments and never writes `.vercel/output`.
53
+
51
54
  The actual function and routes live under `.vercel/output/`:
52
55
 
53
56
  ```text
@@ -64,6 +67,7 @@ The actual function and routes live under `.vercel/output/`:
64
67
  ## What This Demonstrates
65
68
 
66
69
  - The exact shape of the auto-generated `vercel.json` — three keys, nothing else
70
+ - That `buildCommand` builds the vercel target (`<exec> frontmcp build --target vercel`), not the `build` script
67
71
  - That routing and function configuration live in `.vercel/output/`, not `vercel.json`
68
72
  - That hand-authoring `api/frontmcp.ts` references in `vercel.json` is unnecessary and breaks deploys
69
73
 
@@ -25,11 +25,12 @@ The Vercel adapter emits a minimal `vercel.json` (version + buildCommand + insta
25
25
 
26
26
  ```json
27
27
  // vercel.json — extends the auto-generated minimum with regions + headers.
28
- // The adapter regenerates buildCommand/installCommand from your lockfile,
29
- // so keep them aligned (or let the build rewrite them).
28
+ // The build never overwrites an existing vercel.json, so keep buildCommand
29
+ // building the vercel target (`<exec> frontmcp build --target vercel`) —
30
+ // `yarn build` would run the config's deployments and deploy nothing.
30
31
  {
31
32
  "version": 2,
32
- "buildCommand": "yarn build",
33
+ "buildCommand": "yarn frontmcp build --target vercel",
33
34
  "installCommand": "yarn install",
34
35
  "regions": ["iad1"],
35
36
  "headers": [
@@ -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
 
@@ -16,12 +16,23 @@ After `frontmcp build --target vercel`, your `vercel.json` looks like:
16
16
  ```json
17
17
  {
18
18
  "version": 2,
19
- "buildCommand": "yarn build",
19
+ "buildCommand": "yarn frontmcp build --target vercel",
20
20
  "installCommand": "yarn install"
21
21
  }
22
22
  ```
23
23
 
24
- The `buildCommand` and `installCommand` are detected from your lockfile (`bun.lockb` -> bun, `pnpm-lock.yaml` -> pnpm, `yarn.lock` -> yarn, `package-lock.json` -> npm).
24
+ The package manager is detected from your lockfile, and `buildCommand` runs the **vercel target** through it:
25
+
26
+ | Lockfile | `buildCommand` | `installCommand` |
27
+ | --------------------------- | ------------------------------------------ | ---------------- |
28
+ | `package-lock.json` or none | `npx frontmcp build --target vercel` | `npm install` |
29
+ | `yarn.lock` | `yarn frontmcp build --target vercel` | `yarn install` |
30
+ | `pnpm-lock.yaml` | `pnpm exec frontmcp build --target vercel` | `pnpm install` |
31
+ | `bun.lock` / `bun.lockb` | `bunx frontmcp build --target vercel` | `bun install` |
32
+
33
+ `frontmcp create --target vercel` scaffolds the same file (plus a `$schema` line). The build never overwrites an existing `vercel.json`.
34
+
35
+ **Never use `<pm> run build` as the `buildCommand`.** The project's `build` script is `frontmcp build`, which builds the deployments in `frontmcp.config` (typically `node`) and never writes `.vercel/output` -- Vercel then has nothing to deploy. Files generated before 1.8.6 used `yarn build` / `npm run build`; replace them with the command above.
25
36
 
26
37
  ## Customizing
27
38
 
@@ -30,7 +41,7 @@ You can hand-edit `vercel.json` AFTER the build to add fields Vercel supports
30
41
  ```json
31
42
  {
32
43
  "version": 2,
33
- "buildCommand": "yarn build",
44
+ "buildCommand": "yarn frontmcp build --target vercel",
34
45
  "installCommand": "yarn install",
35
46
  "regions": ["iad1"]
36
47
  }
@@ -43,6 +54,7 @@ To add response headers, prefer setting them inside your FrontMCP server (where
43
54
  - Do **not** add a `rewrites` block routing to `/api/frontmcp` — there is no `api/` directory in the Vercel build output.
44
55
  - Do **not** add a `functions: { "api/frontmcp.ts": ... }` map — the function lives at `.vercel/output/functions/index.func/handler.cjs`, configured by `.vc-config.json` (runtime, handler) generated by the adapter.
45
56
  - Do **not** set `framework` to a value the FrontMCP build doesn't match. Leaving it unset is safest.
57
+ - Do **not** set `buildCommand` to `<pm> run build` / `yarn build` -- that builds the config's deployments, not the vercel target.
46
58
 
47
59
  ## Function Runtime Config
48
60
 
@@ -58,7 +58,7 @@ This produces a Vercel Build Output API v3 structure:
58
58
  vercel.json # version, buildCommand, installCommand
59
59
  ```
60
60
 
61
- The adapter detects your package manager from the lockfile and writes the matching `buildCommand`/`installCommand` into `vercel.json`. No `api/` directory is involved.
61
+ The adapter detects your package manager from the lockfile and writes the matching `installCommand` and a `buildCommand` that builds the **vercel target** through it: `npx frontmcp build --target vercel` (npm), `yarn frontmcp build --target vercel`, `pnpm exec frontmcp build --target vercel`, or `bunx frontmcp build --target vercel`. An existing `vercel.json` is left as it is — including one from `frontmcp create --target vercel`, which writes the same commands. Never set `buildCommand` to `<pm> run build`: the `build` script is `frontmcp build`, which builds the config's deployments (typically `node`) and never writes `.vercel/output`. No `api/` directory is involved.
62
62
 
63
63
  ## Step 2: Configure the Server for Vercel KV
64
64
 
@@ -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
 
@@ -122,6 +122,15 @@ before the first `elicit()`/`sample()`/`listRoots()` call.
122
122
  and a 10-minute expiry — a tampered or replayed blob is discarded and the
123
123
  exchange restarts.
124
124
 
125
+ **Multi-instance: set `VAULT_SECRET`.** The signing key is `VAULT_SECRET`, else
126
+ `JWT_SECRET`, else a random per-process key. With the per-process key, a round
127
+ that lands on another instance (or arrives after a restart) fails verification
128
+ and the tool asks its first question again. Set `VAULT_SECRET` (or `JWT_SECRET`)
129
+ to the same value on every instance. In production, `redis` or
130
+ `transport.persistence` without either secret logs a startup warning; each
131
+ rejection logs `mcp-20260728: rejected requestState` with `reason: 'bad-signature'`
132
+ and a `hint` naming `VAULT_SECRET`.
133
+
125
134
  The client MUST declare the matching capability, or the server answers `-32021`. (An unversioned call the server only defaulted to 2026-07-28 comes from a client that never declared it; `elicit()` answers that one with `ElicitationNotSupportedError`, as for a legacy client without a session.)
126
135
 
127
136
  ```json
@@ -9,6 +9,7 @@ features:
9
9
  - 'Using `toolPatterns` glob patterns to cache groups of tools without per-tool configuration'
10
10
  - 'Per-tool `cache` metadata with custom `ttl` (seconds) and `slideWindow` for TTL refresh on hits'
11
11
  - 'Using `cache: true` for simple default-TTL caching'
12
+ - "A cache hit returns the same `content`/`structuredContent` as the miss, marked by `_meta.cache: 'hit'` on the result"
12
13
  - "Gating a tool with `featureFlag: 'beta-search'` -- the tool is hidden from `list_tools` when the flag is off"
13
14
  - 'Accessing `this.featureFlags.isEnabled()` inside a tool for runtime flag checks'
14
15
  ---
@@ -75,6 +76,9 @@ class GetWeatherTool extends ToolContext {
75
76
  return { city: input.city, temperature: weather.temp, condition: weather.condition };
76
77
  }
77
78
  }
79
+
80
+ // A second identical call is a hit: execute() does not run, `structuredContent` is the same
81
+ // `{ city, temperature, condition }` as the first call, and `result._meta.cache === 'hit'`.
78
82
  ```
79
83
 
80
84
  ```typescript
@@ -106,6 +110,7 @@ class BetaSearchTool extends ToolContext {
106
110
  - Using `toolPatterns` glob patterns to cache groups of tools without per-tool configuration
107
111
  - Per-tool `cache` metadata with custom `ttl` (seconds) and `slideWindow` for TTL refresh on hits
108
112
  - Using `cache: true` for simple default-TTL caching
113
+ - A cache hit returns the same `content`/`structuredContent` as the miss, marked by `_meta.cache: 'hit'` on the result
109
114
  - Gating a tool with `featureFlag: 'beta-search'` -- the tool is hidden from `list_tools` when the flag is off
110
115
  - Accessing `this.featureFlags.isEnabled()` inside a tool for runtime flag checks
111
116