@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
@@ -32,8 +32,8 @@ when_to_use: |
32
32
  Trigger when creating or editing a `*.tool.ts` / `*.tool.tsx` file, adding a `@Tool`
33
33
  decorator, defining `inputSchema` / `outputSchema` for a tool, deriving `execute()`
34
34
  parameter or return types, wiring dependency injection into a tool, returning
35
- structured / media / resource content, adding a `ui:` block (HTML / MDX / React /
36
- FileSource), configuring throttling, declaring auth providers, restricting platforms
35
+ structured / media / resource content, adding a `ui:` block (FileSource / React /
36
+ HTML / Markdown), configuring throttling, declaring auth providers, restricting platforms
37
37
  via `availableWhen`, requesting interactive input via `this.elicit`, adding tool
38
38
  `annotations`, or registering a tool in `@App({ tools })`.
39
39
 
@@ -203,7 +203,7 @@ If a request seems to conflict with an inherited default (e.g., "wrap `inputSche
203
203
  │ See: examples/22-tool-with-ui-html-template.md
204
204
  ├── React widget (file) → ui: { template: { file: widgetPath } }
205
205
  │ See: examples/23-tool-with-ui-filesource-tsx.md
206
- ├── Calls other tools → widgetAccessible: true + window.FrontMcpBridge
206
+ ├── Calls other tools → window.FrontMcpBridge.callTool
207
207
  │ See: examples/24-tool-with-ui-csp-and-bridge.md
208
208
  └── Claude target → resourceMode is auto-detected; do not set
209
209
  See: references/ui-widgets.md
@@ -240,7 +240,7 @@ If a request seems to conflict with an inherited default (e.g., "wrap `inputSche
240
240
  | Tool restricted to one OS / runtime / target | [`21-tool-with-availability-constraints`](./examples/21-tool-with-availability-constraints.md) | `availableWhen` axes |
241
241
  | Tool with a quick inline HTML widget | [`22-tool-with-ui-html-template`](./examples/22-tool-with-ui-html-template.md) | `ui: { template: (ctx) => '<div>…</div>' }` |
242
242
  | Tool with a separate `.tsx` widget file | [`23-tool-with-ui-filesource-tsx`](./examples/23-tool-with-ui-filesource-tsx.md) | `FileSource` + `import.meta.url` anchoring |
243
- | Tool widget that calls other tools | [`24-tool-with-ui-csp-and-bridge`](./examples/24-tool-with-ui-csp-and-bridge.md) | `widgetAccessible: true` + `window.FrontMcpBridge.callTool` |
243
+ | Tool widget that calls other tools | [`24-tool-with-ui-csp-and-bridge`](./examples/24-tool-with-ui-csp-and-bridge.md) | `window.FrontMcpBridge.callTool` |
244
244
  | Tool that triggers a job + tracks it | [`25-tool-handing-off-to-job`](./examples/25-tool-handing-off-to-job.md) | Thin tool + heavy job — the right split |
245
245
  | Tool that returns a resource handle | [`26-tool-with-resource-link-output`](./examples/26-tool-with-resource-link-output.md) | `outputSchema: 'resource_link'` — the host fetches the resource |
246
246
  | Tool with `examples` metadata for discovery | [`27-tool-with-examples-metadata`](./examples/27-tool-with-examples-metadata.md) | `examples: [{ description, input, output? }]` |
@@ -269,26 +269,26 @@ Before considering a tool "done":
269
269
 
270
270
  ## References (deep dives)
271
271
 
272
- | Reference | Covers |
273
- | --------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
274
- | [`quick-start.md`](./references/quick-start.md) | 60-second tour: minimal tool, registration, calling it from a test |
275
- | [`decorator-options.md`](./references/decorator-options.md) | Every field on `@Tool({...})` — what it does, default, when to set it |
276
- | [`input-schema.md`](./references/input-schema.md) | Raw shape vs `z.object`, refinements, defaults, optional, describe |
277
- | [`output-schema.md`](./references/output-schema.md) | All supported output types: Zod shape, Zod schema, primitives, media, arrays |
278
- | [`derived-types.md`](./references/derived-types.md) | `ToolInputOf` / `ToolOutputOf` patterns, file layout, schema hoisting |
279
- | [`execution-context.md`](./references/execution-context.md) | `ToolContext` methods + properties — `this.get`, `this.fetch`, `this.notify`, `this.context`, etc. |
280
- | [`error-handling.md`](./references/error-handling.md) | `this.fail`, MCP error classes (`PublicMcpError`, `ResourceNotFoundError`), error flow, when to throw vs `fail` |
281
- | [`throttling.md`](./references/throttling.md) | `rateLimit`, `concurrency`, `timeout` — semantics, interaction, defaults |
282
- | [`auth-providers.md`](./references/auth-providers.md) | `authProviders` string shorthand vs full mapping, scopes, alias, credential vault basics |
283
- | [`availability.md`](./references/availability.md) | `availableWhen` axes (os / runtime / deployment / provider / target / surface / env), `missingAxes`, `isPlatform` |
284
- | [`elicitation.md`](./references/elicitation.md) | `this.elicit`, server-level enable, `ElicitationDisabledError`, accept / decline / cancel |
285
- | [`ui-widgets.md`](./references/ui-widgets.md) | `@Tool({ ui })` — template formats, `servingMode`, `resourceMode` host-detect, CSP, `widgetAccessible`, MCP Apps spec |
286
- | [`annotations.md`](./references/annotations.md) | `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, `title` |
287
- | [`function-style-builder.md`](./references/function-style-builder.md) | `tool({...})(handler)` — when to pick over a class, register, ctx parameter |
288
- | [`remote-and-esm.md`](./references/remote-and-esm.md) | `Tool.esm(...)` / `Tool.remote(...)` — load tools from ESM URLs or remote MCP servers |
289
- | [`registration.md`](./references/registration.md) | `@App({ tools })` vs `@FrontMcp({ tools })`, multi-app composition |
290
- | [`file-layout.md`](./references/file-layout.md) | Flat-sibling vs folder-per-tool, `<name>.schema.ts` / `<name>.tool.ts` / `<name>.tool.spec.ts` |
291
- | [`testing.md`](./references/testing.md) | Per-tool unit tests — `@frontmcp/testing`, mocking DI, asserting output validation |
272
+ | Reference | Covers |
273
+ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
274
+ | [`quick-start.md`](./references/quick-start.md) | 60-second tour: minimal tool, registration, calling it from a test |
275
+ | [`decorator-options.md`](./references/decorator-options.md) | Every field on `@Tool({...})` — what it does, default, when to set it |
276
+ | [`input-schema.md`](./references/input-schema.md) | Raw shape vs `z.object`, refinements, defaults, optional, describe |
277
+ | [`output-schema.md`](./references/output-schema.md) | All supported output types: Zod shape, Zod schema, primitives, media, arrays |
278
+ | [`derived-types.md`](./references/derived-types.md) | `ToolInputOf` / `ToolOutputOf` patterns, file layout, schema hoisting |
279
+ | [`execution-context.md`](./references/execution-context.md) | `ToolContext` methods + properties — `this.get`, `this.fetch`, `this.notify`, `this.context`, etc. |
280
+ | [`error-handling.md`](./references/error-handling.md) | `this.fail`, MCP error classes (`PublicMcpError`, `ResourceNotFoundError`), error flow, when to throw vs `fail` |
281
+ | [`throttling.md`](./references/throttling.md) | `rateLimit`, `concurrency`, `timeout` — semantics, interaction, defaults |
282
+ | [`auth-providers.md`](./references/auth-providers.md) | `authProviders` string shorthand vs full mapping, scopes, alias, credential vault basics |
283
+ | [`availability.md`](./references/availability.md) | `availableWhen` axes (os / runtime / deployment / provider / target / surface / env), `missingAxes`, `isPlatform` |
284
+ | [`elicitation.md`](./references/elicitation.md) | `this.elicit`, server-level enable, `ElicitationDisabledError`, accept / decline / cancel |
285
+ | [`ui-widgets.md`](./references/ui-widgets.md) | `@Tool({ ui })` — template formats, `servingMode`, `resourceMode` host-detect, CSP, ignored options, MCP Apps spec |
286
+ | [`annotations.md`](./references/annotations.md) | `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, `title` |
287
+ | [`function-style-builder.md`](./references/function-style-builder.md) | `tool({...})(handler)` — when to pick over a class, register, ctx parameter |
288
+ | [`remote-and-esm.md`](./references/remote-and-esm.md) | `Tool.esm(...)` / `Tool.remote(...)` — load tools from ESM URLs or remote MCP servers |
289
+ | [`registration.md`](./references/registration.md) | `@App({ tools })` vs `@FrontMcp({ tools })`, multi-app composition |
290
+ | [`file-layout.md`](./references/file-layout.md) | Flat-sibling vs folder-per-tool, `<name>.schema.ts` / `<name>.tool.ts` / `<name>.tool.spec.ts` |
291
+ | [`testing.md`](./references/testing.md) | Per-tool unit tests — `@frontmcp/testing`, mocking DI, asserting output validation |
292
292
 
293
293
  ## Rules (constraints — read these once, then they're enforced)
294
294
 
@@ -38,7 +38,6 @@ type Out = { city: string; temperatureF: number; conditions: string };
38
38
  inputSchema,
39
39
  outputSchema,
40
40
  ui: {
41
- widgetDescription: 'Current weather card',
42
41
  // `html` escapes interpolated values — no manual escapeHtml needed
43
42
  template: (ctx: TemplateContext<In, Out>) => ctx.helpers.html`
44
43
  <div style="padding:16px;font-family:system-ui;border-radius:12px;background:#f5f7fa">
@@ -16,7 +16,7 @@ Tool with a `.tsx` widget in a separate file via the `FileSource` form — the r
16
16
 
17
17
  For any React widget, FileSource is the right pattern. The widget lives in its own `.widget.tsx` file with its own React imports; the tool decorator just points at it.
18
18
 
19
- > **Prerequisite:** `@frontmcp/ui` installed at the same version as `@frontmcp/sdk`. Without it, server-side bundling fails — the framework injects an auto-generated React mount that imports `McpBridgeProvider` from `@frontmcp/ui/react`.
19
+ > **Prerequisites:** `@frontmcp/ui` installed at the same version as `@frontmcp/sdk` — the framework injects an auto-generated React mount that imports `McpBridgeProvider` from `@frontmcp/ui/react`. And `esbuild` installed as a runtime dependency (`npm install esbuild`, not a devDependency) — `@frontmcp/uipack` loads it to bundle the widget when the tool is called. `frontmcp create` projects get `esbuild` through the `frontmcp` package.
20
20
 
21
21
  ## Code
22
22
 
@@ -41,8 +41,10 @@ import { Tool, ToolContext } from '@frontmcp/sdk';
41
41
 
42
42
  import { inputSchema, outputSchema, type SalesChartInput, type SalesChartOutput } from './sales-chart.schema';
43
43
 
44
- // Anchor the widget path to THIS source file — bare relative paths resolve
45
- // against process.cwd() (issue #444), which fails in any non-trivial layout.
44
+ // Anchor the widget path to THIS file — bare relative paths resolve against
45
+ // process.cwd() (issue #444), which fails in any non-trivial layout. Once
46
+ // compiled, this resolves next to the compiled file, so the widget must ship
47
+ // with the build (`frontmcp build` copies *.widget.tsx files — #649).
46
48
  const widgetPath = fileURLToPath(new URL('./sales-chart.widget.tsx', import.meta.url));
47
49
 
48
50
  @Tool({
@@ -51,12 +53,10 @@ const widgetPath = fileURLToPath(new URL('./sales-chart.widget.tsx', import.meta
51
53
  inputSchema,
52
54
  outputSchema,
53
55
  ui: {
54
- widgetDescription: 'Monthly revenue chart',
55
56
  template: { file: widgetPath },
56
57
  // resourceMode is intentionally UNSET — framework host-detects: 'inline' for Claude
57
58
  // (React bundled in, widget renders under Claude's CSP), 'cdn' for OpenAI / ChatGPT /
58
59
  // Cursor / MCP Inspector (smaller payload from esm.sh). Issue #456.
59
- hydrate: false, // SSR-only — dodges React error #418 in iframe sandboxes
60
60
  },
61
61
  })
62
62
  export class SalesChartTool extends ToolContext {
@@ -107,6 +107,7 @@ export default function SalesChartWidget({ output }: Props) {
107
107
  ## Why these defaults matter
108
108
 
109
109
  - **`import.meta.url` anchoring** — relative paths in `FileSource` resolve against `process.cwd()`, not the tool file (#444). Running the server from a different directory breaks the widget at tool-call time. Anchoring fixes it once.
110
+ - **Ship the widget** — the anchored path points next to the **compiled** file after a build, and tsc doesn't emit `*.widget.tsx`. `frontmcp build` copies widget files into the output (directly next to the bundle for the bundled `node` / `cli` / `lambda` / `vercel` targets, so keep widget file names unique); a plain `tsc` build needs its own copy step (#649).
110
111
  - **`resourceMode` unset** — leave it. The framework picks `'inline'` for Claude (React bundled into the widget — actually renders) and `'cdn'` for everyone else (smaller payload via esm.sh). Setting it explicitly only locks in one behavior across all clients.
111
- - **`hydrate: false`** — default. React SSR output is static HTML; the bridge IIFE handles any interactivity. Enabling hydration creates React error #418 in Claude's iframe sandbox where the client-side render diverges from the SSR render.
112
+ - **No `hydrate` option** — `ui.hydrate` is accepted but has no effect yet (startup logs a warning). The `.tsx` widget is bundled and mounted on the client by the generated entry.
112
113
  - **`*.widget.tsx` naming** — the scaffolded `tsconfig.json` excludes `**/*.widget.tsx` from the server typecheck (#445). The widget compiles via uipack/esbuild at render time with its own React-aware config.
@@ -2,10 +2,10 @@
2
2
  name: 24-tool-with-ui-csp-and-bridge
3
3
  level: advanced
4
4
  description: 'Interactive tool widget that fetches from an allow-listed CSP origin and invokes another tool via `window.FrontMcpBridge.callTool` — the full pattern for live-data widgets that need cross-tool composition.'
5
- tags: [ui, csp, widgetAccessible, FrontMcpBridge, interactive-widget]
5
+ tags: [ui, csp, callTool, FrontMcpBridge, interactive-widget]
6
6
  features:
7
7
  - "Restricting the widget's outbound `fetch` via `ui.csp.connectDomains` (emitted on the resource per #455)"
8
- - 'Opting the widget into cross-tool calls with `widgetAccessible: true` and using `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs'
8
+ - 'Calling other tools from the widget with `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs'
9
9
  - 'Building the markup with `ctx.helpers.html` and embedding initial data into the inline `<script>` via `trustedHtml(jsonEmbed(...))` (`jsonEmbed` escapes `<`, `>` and `&`)'
10
10
  - 'Surfacing in-flight status via `invocationStatus.invoking` / `invoked` so the host UI shows feedback'
11
11
  ---
@@ -58,8 +58,6 @@ type Out = { symbol: string; priceUsd: number; asOf: string };
58
58
  inputSchema,
59
59
  outputSchema,
60
60
  ui: {
61
- widgetDescription: 'Live stock quote with refresh',
62
- widgetAccessible: true, // required for window.FrontMcpBridge.callTool
63
61
  invocationStatus: { invoking: 'Fetching quote…', invoked: 'Quote loaded' },
64
62
  csp: {
65
63
  // CSP applies to the widget iframe — only allow fetches to our own market-data API.
@@ -117,13 +115,13 @@ export class ShowQuoteTool extends ToolContext {
117
115
  ## What This Demonstrates
118
116
 
119
117
  - Restricting the widget's outbound `fetch` via `ui.csp.connectDomains` (emitted on the resource per #455)
120
- - Opting the widget into cross-tool calls with `widgetAccessible: true` and using `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs
118
+ - Calling other tools from the widget with `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs
121
119
  - Building the markup with `ctx.helpers.html` and embedding initial data into the inline `<script>` via `trustedHtml(jsonEmbed(...))` (`jsonEmbed` escapes `<`, `>` and `&`)
122
120
  - Surfacing in-flight status via `invocationStatus.invoking` / `invoked` so the host UI shows feedback
123
121
 
124
122
  ## Why these choices
125
123
 
126
- - **`widgetAccessible: true`** — required for `window.FrontMcpBridge.callTool`. Without it, the bridge is read-only (the widget can read `getToolInput` / `getToolOutput` but can't invoke tools).
124
+ - **No `widgetAccessible` / `widgetDescription`** — both are accepted by the schema but nothing reads them yet (startup logs a warning), so they are left out. Whether the widget may call tools is decided by the host.
127
125
  - **`csp.connectDomains`** — limits what the widget can `fetch` to. Without a CSP, the host's default applies (which may block everything in Claude). With `connectDomains: ['https://api.market.example']`, only that origin is reachable.
128
126
  - **`window.FrontMcpBridge.callTool` not `window.openai.callTool`** — the bridge handles host detection. `window.openai.*` works on OpenAI Apps SDK but breaks everywhere else.
129
127
  - **`jsonEmbed` not `JSON.stringify`** — `JSON.stringify` doesn't escape `</script>` or `<!--` and can break out of the inline script tag. `jsonEmbed` writes `<`, `>` and `&` as `\u003c`, `\u003e`, `\u0026`.
@@ -14,7 +14,7 @@ features:
14
14
 
15
15
  Tool with the `examples: [...]` field on `@Tool({...})` — concrete input (and optional expected output) examples consumed by the CodeCall `codecall:describe` tool to give agents accurate usage examples.
16
16
 
17
- The `examples` field is purely advisory — the CodeCall `describe` tool uses it as the highest-priority source of usage examples (user-provided examples take precedence over auto-generated ones, up to 5). It is **not** emitted in the `tools/list` MCP response. Use it for any tool that benefits from concrete usage hints when CodeCall is enabled.
17
+ The `examples` field is purely advisory — the CodeCall `describe` tool uses it as the source of usage examples: when a tool declares examples, `codecall:describe` returns only those (up to 5) and adds no auto-generated one. It is **not** emitted in the `tools/list` MCP response. Use it for any tool that benefits from concrete usage hints when CodeCall is enabled.
18
18
 
19
19
  ## Code
20
20
 
@@ -74,7 +74,8 @@ export class ConvertCurrencyTool extends ToolContext {
74
74
 
75
75
  ## Where examples show up
76
76
 
77
- - **`codecall:describe`** — the CodeCall plugin's `describe` tool uses these as its top-priority source of usage examples (user-provided examples win over auto-generated ones, capped at 5). This is the one place `examples` is actually read.
77
+ - **`codecall:describe`** — the CodeCall plugin's `describe` tool returns these as the tool's usage examples (capped at 5), with no auto-generated example next to them. This is the one place `examples` is actually read.
78
+ - **Without `examples`** — `codecall:describe` generates one example from the tool's intent, with arguments built only from the input schema's properties (required ones, the query-like or first filter property for a search, the real pagination properties for a list, the first enum value), or `{}` when none fits. Values stay within the property's `enum`, `const`, bounds and length limits; a `pattern` or `format` is not generated, so an optional property that has one is left out. It never shows an argument the schema does not declare, such as a made-up `query`.
78
79
  - **Not in `tools/list`** — the `tools/list` MCP response does not include `examples`; clients never see them there.
79
80
 
80
81
  ## When to include `output?`
@@ -43,7 +43,7 @@ Full surface of the `@Tool` decorator. Mandatory fields are bolded.
43
43
 
44
44
  - **`rateLimit` + `concurrency`** — independent. Rate-limit caps invocations over time; concurrency caps simultaneous in-flight. A "1 req/s with max 2 concurrent" tool is fine: bursts can run two at once, then back off.
45
45
  - **`timeout` + `rateLimit`** — orthogonal. Timeout wraps a single call; rate-limit wraps the rate of calls.
46
- - **`authProviders` + `ui.widgetAccessible`** — the widget bridge respects the tool's auth requirements. A widget that calls back to a tool requiring `authProviders: ['github']` will fail in the bridge if no GitHub session exists.
46
+ - **`authProviders` + widget tool calls** — a widget that calls back to a tool requiring `authProviders: ['github']` fails if no GitHub session exists. (`ui.widgetAccessible` is accepted but has no effect yet.)
47
47
  - **`availableWhen` + `visibility`** — `availableWhen` is a hard constraint (filtered out of `tools/list` AND blocked from execution when context doesn't match); `visibility: 'hidden'` is a soft hide (filtered from `tools/list` but still callable by name). `visibility: 'internal'` blocks external `tools/call` entirely (in-process `this.callTool` only).
48
48
  - **`ui.servingMode === 'static'` + `availableWhen`** — static widgets pre-compile at startup. If a tool is filtered out by `availableWhen`, its static widget isn't compiled either.
49
49
 
@@ -129,6 +129,7 @@ There are no server-to-client requests in this revision. `this.elicit()` answers
129
129
  - A client without the `elicitation` capability for the mode gets error `-32021`. Capabilities come with each request, and `this.elicit()` checks them itself. A session-based check such as `this.scope.notifications.getClientCapabilities(sessionId)` finds nothing here, so call `this.elicit()` directly.
130
130
  - `sendElicitationResult` is not listed to 2026-07-28 clients.
131
131
  - Anonymous callers keep their `requestState` across rounds (it binds to one shared anonymous principal).
132
+ - **More than one instance? Set `VAULT_SECRET` (or `JWT_SECRET`) to the same value on every instance.** `requestState` is signed with `VAULT_SECRET`, else `JWT_SECRET`, else a random per-process key; under the per-process key a round that lands on another instance (or after a restart) fails verification and the tool asks its first question again. Production servers with `redis`/`transport.persistence` but neither secret log a startup warning, and each rejected round logs `reason: 'bad-signature'` with a `hint` naming `VAULT_SECRET`.
132
133
 
133
134
  ## See also
134
135
 
@@ -89,6 +89,8 @@ If you hoisted the entire `@Tool({...})` config, consumers would drag the `@Tool
89
89
 
90
90
  `.tsx` / `.jsx` widget files use the `*.widget.tsx` naming convention. The scaffolded `tsconfig.json` excludes `**/*.widget.tsx` from the server typecheck (#445 fix) — widgets are bundled separately by `@frontmcp/uipack` (esbuild) at render time, with React loaded externally. If you want IDE typecheck for widget sources, add a sibling `tsconfig.widget.json` with `jsx: 'react-jsx'` and `include: ['src/**/*.widget.tsx']`.
91
91
 
92
+ Keep each widget beside the tool that uses it, with a unique file name. tsc never emits widget files, so `frontmcp build` copies them into the output — for the bundled `node` / `cli` / `lambda` / `vercel` targets directly next to the bundle, where every tool's `__dirname` points (#649). Two widgets with the same file name can't both go there; the build skips them with a warning.
93
+
92
94
  ## See also
93
95
 
94
96
  - [`derived-types.md`](./derived-types.md) — why schemas hoist
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ui-widgets
3
- description: @Tool({ ui }) — template formats, trusted markup (html / escapeStringResults), servingMode, host-detect resourceMode, CSP, widgetAccessible, MCP Apps spec.
3
+ description: @Tool({ ui }) — template formats, trusted markup (html / escapeStringResults), servingMode, host-detect resourceMode, CSP, ignored options, MCP Apps spec.
4
4
  ---
5
5
 
6
6
  # Tool UI widgets
@@ -39,12 +39,12 @@ That's it. The framework:
39
39
 
40
40
  ## Template formats
41
41
 
42
- | Format | Shape | When |
43
- | ---------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
44
- | **FileSource (recommended)** | `{ file: widgetPath }` | `.tsx` / `.jsx` / `.html` source files. Anchor with `import.meta.url`. |
45
- | **Function** | `` (ctx) => ctx.helpers.html`…` `` | Quick demo / one-liner HTML. Annotate `ctx: TemplateContext<In, Out>` ([why](#typescript-gotcha-ts7006)). |
46
- | **HTML / MDX string** | `'<div>…</div>'` or `'# Title\n<Card />'` | Static markup; pair with `mdxComponents` for MDX. |
47
- | **React component** | `MyWidget` | SSR React. Set `hydrate: false` (default) for Claude/ChatGPT. |
42
+ | Format | Shape | When |
43
+ | ---------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
44
+ | **FileSource (recommended)** | `{ file: widgetPath }` | `.tsx` / `.jsx` / `.html` source files. Anchor with `import.meta.url`. |
45
+ | **Function** | `` (ctx) => ctx.helpers.html`…` `` | Quick demo / one-liner HTML. Annotate `ctx: TemplateContext<In, Out>` ([why](#typescript-gotcha-ts7006)). |
46
+ | **HTML / Markdown string** | `'<div>…</div>'` or `'# Title\n- item'` | A string with both `<` and `>` is HTML as written; any other string is Markdown, converted on the server. MDX is **not** compiled. |
47
+ | **React component** | `MyWidget` | A bare component reference cannot be bundled for the widget; use the `{ file }` form instead. |
48
48
 
49
49
  The renderer auto-detects which one you passed.
50
50
 
@@ -64,26 +64,54 @@ Or use the FileSource form — it sidesteps the issue.
64
64
 
65
65
  ## `ToolUIConfig` fields
66
66
 
67
- | Field | Default | Purpose |
68
- | --------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
69
- | `template` | — | Required. Function / HTML-string / React component / `{ file }` FileSource. |
70
- | `widgetDescription` | — | Human-readable description surfaced to the host UI. |
71
- | `servingMode` | `'auto'` | `'inline'` / `'static'` / `'hybrid'` / `'direct-url'` / `'custom-url'`. `'auto'` picks the best per-host. |
72
- | `displayMode` | `'inline'` | `'inline'` / `'fullscreen'` / `'pip'` — host display hint. |
73
- | `preferredHeight` | — | `number` (px) or CSS string (`'50vh'`). Initial widget height; auto-resize grows/shrinks from this baseline. |
74
- | `minHeight` / `maxHeight` | — | `number` (px) or CSS string. Clamp the widget height; auto-resize never reports outside this range. |
75
- | `aspectRatio` | — | CSS `aspect-ratio` (`'16 / 9'` or `1.5`). Hosts that honor it size by ratio instead of measured height. |
76
- | `autoResize` | `true` | Auto-report content height to the host via a debounced `ResizeObserver` on `#root`. Set `false` to opt out (CSS still applies). |
77
- | `csp` | — | `{ connectDomains?, resourceDomains? }` — emitted on the resource content's `_meta.ui.csp` (#455). Claude honors CSP only here. |
78
- | `contentSecurity` | strict | `{ allowUnsafeLinks?, allowInlineScripts?, bypassSanitization? }` — keep defaults. |
79
- | `escapeStringResults` | unset | `true` escapes plain string results of a template function; `html` / `trustedHtml` stay markup. Default in 1.9. |
80
- | `widgetAccessible` | `false` | `true` exposes `window.FrontMcpBridge.callTool` in the widget. |
81
- | `resourceUri` | auto | Override the `ui://widget/{toolName}.html` URI. |
82
- | `uiType` | `'auto'` | Force `'html'` / `'react'` / `'mdx'` / `'markdown'`. |
83
- | `resourceMode` | host-detect | `'cdn'` / `'inline'`. Leave unset — the framework host-detects (Claude → `'inline'`, #456). |
84
- | `hydrate` | `false` | Enable React hydration after SSR. Off by default — avoids React error #418 in Claude. |
85
- | `externals`, `dependencies` | — | CDN externals for FileSource widgets. |
86
- | `customShell`, `invocationStatus`, `widgetCapabilities`, `prefersBorder`, `sandboxDomain`, `htmlResponsePrefix` | — | Platform-specific knobs. |
67
+ | Field | Default | Purpose |
68
+ | --------------------------------------------------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
+ | `template` | — | Required. `{ file }` FileSource (recommended) / function / HTML or Markdown string / React component. |
70
+ | `widgetDescription` | — | **No effect yet** — accepted, not read. |
71
+ | `servingMode` | `'auto'` | `'inline'` / `'static'` / `'hybrid'`. `'auto'` picks the best per-host. `'direct-url'` / `'custom-url'` are not implemented (served inline, startup warning). `'hybrid'` sends only `_meta['ui/component']` = `{ type, hash, toolName }`, no code. |
72
+ | `displayMode` | `'inline'` | **No effect yet.** |
73
+ | `preferredHeight` | — | `number` (px) or CSS string (`'50vh'`). Initial widget height; auto-resize grows/shrinks from this baseline. |
74
+ | `minHeight` / `maxHeight` | — | `number` (px) or CSS string. Clamp the widget height; auto-resize never reports outside this range. |
75
+ | `aspectRatio` | — | CSS `aspect-ratio` (`'16 / 9'` or `1.5`). Hosts that honor it size by ratio instead of measured height. |
76
+ | `autoResize` | `true` | Report the document height (margins included) to the host after the handshake. Set `false` to opt out (CSS still applies). |
77
+ | `csp` | — | `{ connectDomains?, resourceDomains? }` — emitted on the resource content's `_meta.ui.csp` (#455). Claude honors CSP only here. |
78
+ | `contentSecurity` | strict | **No effect yet.** |
79
+ | `escapeStringResults` | unset | `true` escapes plain string results of a template function; `html` / `trustedHtml` stay markup. Default in 1.9. |
80
+ | `widgetAccessible` | `false` | **No effect yet.** |
81
+ | `resourceUri` | auto | Override the `ui://widget/{toolName}.html` URI. |
82
+ | `uiType` | `'auto'` | **No effect yet** — the type is auto-detected. |
83
+ | `resourceMode` | host-detect | `'cdn'` / `'inline'`. Leave unset — the framework host-detects (Claude → `'inline'`, #456). |
84
+ | `hydrate` | `false` | **No effect yet.** |
85
+ | `externals`, `dependencies` | — | CDN externals for FileSource widgets. |
86
+ | `customShell`, `invocationStatus`, `widgetCapabilities` | — | Platform-specific knobs. |
87
+ | `prefersBorder`, `sandboxDomain`, `runtimeOptions`, `mdxComponents`, `bundlingMode`, `htmlResponsePrefix` | — | **No effect yet.** There is no dual HTML payload for Claude. |
88
+
89
+ ## Components and hooks
90
+
91
+ `@frontmcp/ui` has **no** `card()`, `badge()`, `descriptionList()`, `button()`, `form()` or `input()` functions. Widgets use React: components from `@frontmcp/ui/components` (`Alert`, `Avatar`, `Badge`, `Button`, `Card`, `List`, `Loader`, `Modal`, `Select`, `Table`, `TextField`) and bridge hooks from `@frontmcp/ui/react` (`useToolInput`, `useToolOutput`, `useCallTool`, `useTheme`, `useHostContext`, ...). A `.tsx` widget receives `{ output, loading }`.
92
+
93
+ `useCallTool` returns a **tuple**, not an object:
94
+
95
+ ```tsx
96
+ import { Card } from '@frontmcp/ui/components';
97
+ import { useCallTool } from '@frontmcp/ui/react';
98
+
99
+ export default function Widget({ output }: { output: { id: string } | null }) {
100
+ const [refresh, { data, loading, error, called }, reset] = useCallTool('get_order');
101
+ if (!output) return <Card title="Loading..." />;
102
+ return (
103
+ <button disabled={loading} onClick={() => refresh({ orderId: output.id })}>
104
+ Refresh
105
+ </button>
106
+ );
107
+ }
108
+ ```
109
+
110
+ ## Markdown, MDX and sanitization
111
+
112
+ - A string template with both `<` and `>` is HTML and is used as written. Any other string is Markdown: headings, paragraphs, fenced code, lists, bold, italic, inline code and links are converted server-side; raw HTML is escaped; a link survives only if its target starts with `http:`, `https:`, `mailto:`, `/` or `#`.
113
+ - MDX is not compiled: `{output.field}` expressions are not evaluated and `mdxComponents` is ignored. Use a `.tsx` FileSource widget for interactivity.
114
+ - Author-written markup (template literals, HTML strings, `.tsx`) is trusted and not sanitized. Values interpolated into `` html`…` `` are escaped; plain string results are escaped only with `escapeStringResults: true`.
87
115
 
88
116
  ## Widget resources and per-call renders
89
117
 
@@ -140,6 +168,12 @@ ui: {
140
168
 
141
169
  See [`rules/widget-paths-anchor-with-import-meta-url.md`](../rules/widget-paths-anchor-with-import-meta-url.md).
142
170
 
171
+ The widget is read when the tool is called, from the path the **compiled** tool computes — an anchored path points into the build output once the tool is compiled, so the file has to ship there (#649):
172
+
173
+ - `frontmcp build` copies every `*.widget.tsx` / `*.widget.jsx` under the entry's directory into the output. tsc-output targets (`distributed`, `cloudflare`) get them at the same relative path, next to each compiled tool. Bundled targets (`node`, `cli`, `lambda`, `vercel`) get them directly next to the bundle, because every bundled module's `__dirname` is the bundle's directory — keep each widget beside its tool and give it a unique file name (the build skips, and warns about, names used twice).
174
+ - Only widget files are copied, not other local files a widget imports.
175
+ - A plain `tsc` build copies nothing — add a copy step. A missing widget fails the call with an `ENOENT` error naming the path it looked for, and the `src/` file when one matches.
176
+
143
177
  ## `@frontmcp/ui` prerequisite (#443)
144
178
 
145
179
  `.tsx` / `.jsx` FileSource widgets require `@frontmcp/ui` in the consuming project — the bundler injects an auto-generated React mount that imports `McpBridgeProvider` from `@frontmcp/ui/react`:
@@ -151,9 +185,19 @@ npm install @frontmcp/ui
151
185
 
152
186
  Match the version to `@frontmcp/sdk`. Without it, server-side bundling fails with a friendly error pointing at this requirement.
153
187
 
188
+ ## `esbuild` prerequisite (#649)
189
+
190
+ `@frontmcp/uipack` loads `esbuild` on demand to bundle a `.tsx` / `.jsx` widget **when the tool is called**, so it must be installed where the server runs — as a runtime dependency:
191
+
192
+ ```bash
193
+ npm install esbuild # in "dependencies", not "devDependencies"
194
+ ```
195
+
196
+ Projects created with `frontmcp create` already have it through the `frontmcp` package. `@frontmcp/uipack` declares it as an optional peer dependency (`>=0.27.0 <1`). Without it, the call fails with an error naming the widget.
197
+
154
198
  ## Widget bridge — `window.FrontMcpBridge`
155
199
 
156
- When the widget needs to read tool data or invoke other tools, the bridge IIFE is injected automatically. Set `widgetAccessible: true` to enable `callTool`:
200
+ When the widget needs to read tool data or invoke other tools, the bridge IIFE is injected automatically; no option is needed (`widgetAccessible` is accepted but has no effect yet):
157
201
 
158
202
  ```typescript
159
203
  ui: {
@@ -166,21 +210,31 @@ ui: {
166
210
  };
167
211
  </script>
168
212
  `,
169
- widgetAccessible: true,
170
213
  }
171
214
  ```
172
215
 
173
- | Bridge method | Purpose |
174
- | --------------------------------------------------------------- | ------------------------------------------------------- |
175
- | `callTool(name, args)` | Invoke another tool (requires `widgetAccessible: true`) |
176
- | `getToolInput()` / `getToolOutput()` / `getStructuredContent()` | Read the tool data |
177
- | `getWidgetState()` / `setWidgetState(state)` | Persisted per-widget state |
178
- | `getHostContext()` / `getTheme()` / `getDisplayMode()` | Host context |
179
- | `hasCapability(cap)` | Probe adapter capabilities |
180
- | `onToolResponseMetadata(cb)` | Subscribe to `ui/html` arrival (inline mode) |
216
+ | Bridge method | Purpose |
217
+ | --------------------------------------------------------------- | ------------------------------------------------------ |
218
+ | `callTool(name, args)` | Invoke another tool (the host may still refuse) |
219
+ | `getToolInput()` / `getToolOutput()` / `getStructuredContent()` | Read the tool data |
220
+ | `getWidgetState()` / `setWidgetState(state)` | Persisted per-widget state |
221
+ | `getHostContext()` / `getTheme()` / `getDisplayMode()` | Host context |
222
+ | `onContextChange(cb)` | Subscribe to host context changes (handshake included) |
223
+ | `hasCapability(cap)` | Probe adapter capabilities |
224
+ | `onToolResponseMetadata(cb)` | Subscribe to `ui/html` arrival (inline mode) |
181
225
 
182
226
  The bridge routes to the right host adapter (OpenAI SDK / Claude postMessage / FrontMCP direct) automatically. **Never call `window.openai.*` directly** — it works on OpenAI but breaks everywhere else.
183
227
 
228
+ ### Theme
229
+
230
+ The page follows the host theme. When an MCP Apps host sends `theme: 'light' | 'dark'` — in the `ui/initialize` result or a later `ui/notifications/host-context-changed` — the bridge sets `<meta name="color-scheme" content="…">` and `<html data-theme="…">`:
231
+
232
+ - The frame's canvas and form controls match the host (a dark host no longer gets an opaque white frame).
233
+ - Style dark mode with `[data-theme='dark'] …` selectors.
234
+ - `getTheme()` returns the same value; `onContextChange` listeners fire for the handshake context too.
235
+ - Nothing is written when the host sends no theme (the OS fallback `getTheme()` starts with is never applied).
236
+ - A widget styled for light only keeps it with `:root { color-scheme: light }` — author CSS wins over the meta tag.
237
+
184
238
  ## Host considerations
185
239
 
186
240
  | Host | Notes |
@@ -209,13 +263,15 @@ What FrontMCP does with it:
209
263
 
210
264
  - **Static sizing CSS** — `preferredHeight` (initial `height`), `minHeight`, `maxHeight`, and `aspectRatio` are injected as a `<style>` block on `html` / `body` / `#root`, so the widget opens at the right size before any JS runs.
211
265
  - **`_meta` hints** — the same values ride along on the response/discovery `_meta` as `ui/preferredHeight`, `ui/minHeight`, `ui/maxHeight`, `ui/aspectRatio` (and nested under `_meta.ui` in `tools/list`), so hosts that read sizing from metadata pick it up.
212
- - **Runtime auto-resize** — when `autoResize !== false` and `ResizeObserver` is available, the bridge observes `#root` and reports the measured height to the host (debounced via `requestAnimationFrame`), also firing a `widget:resize` event you can listen for. Call `window.FrontMcpBridge.setSize({ height, width, aspectRatio })` to report manually.
266
+ - **Runtime auto-resize** — when `autoResize !== false` and `ResizeObserver` is available, the bridge observes `<html>`, `<body>` and `#root` and reports the page height to the host (debounced via `requestAnimationFrame`), also firing a `widget:resize` event you can listen for. Call `window.FrontMcpBridge.setSize({ height, width, aspectRatio })` to report manually.
267
+ - **What is measured** — the whole document: `<html>` at `height: fit-content`, plus any content overflowing a fixed-height `<body>`, clamped by a px `max-height` on `<html>`. Body margins and margins collapsed through the body (an `<h2>` or `<ul>` at the edge) are counted, and the height shrinks when content does. `preferredHeight` / `minHeight` / `maxHeight` act as the floor and ceiling.
268
+ - **When it is sent** — auto-resize reports wait for the bridge to initialize. In an ext-apps host the first report is sent after the `ui/initialize` handshake settles. `ui/notifications/size-changed` is a notification, so the host never answers it; it is refused locally (the promise rejects) only when no trusted origin exists, and auto-resize then retries that height on a later observation. A manual `setSize` called before the handshake, with no trusted origin configured, is held and only the latest size is sent right after it. `aspectRatio` stays part of the cross-platform `FrontMcpBridge.setSize` API; the ext-apps adapter leaves it out of the notification payload.
213
269
 
214
270
  Per-host behavior:
215
271
 
216
272
  - **Claude / static widgets** — the host measures the iframe DOM height itself, so auto-resize is effectively CSS-only (the `setSize` report is a no-op). The injected CSS is what makes a fixed-tall widget (media players, canvases) open without clipping.
217
273
  - **OpenAI ChatGPT** — auto-resize forwards to the Apps SDK sizing API when one is exposed; otherwise the SDK's own DOM measurement applies.
218
- - **ext-apps hosts** — the measured size is reported via a `ui/setSize` request (parallels `ui/setDisplayMode`).
274
+ - **ext-apps hosts** — the measured size is reported with the standard `ui/notifications/size-changed` notification (`{ width, height }` in px), which any spec-compliant host handles.
219
275
  - **Gemini / generic / unknown** — `setSize` is a no-op; only the static CSS applies.
220
276
 
221
277
  `displayMode: 'fullscreen'` remains a separate, best-effort hint a host may ignore.
@@ -228,7 +284,7 @@ Per-host behavior:
228
284
 
229
285
  - [`22-tool-with-ui-html-template`](../examples/22-tool-with-ui-html-template.md) — inline function template
230
286
  - [`23-tool-with-ui-filesource-tsx`](../examples/23-tool-with-ui-filesource-tsx.md) — `.tsx` widget, host-detect
231
- - [`24-tool-with-ui-csp-and-bridge`](../examples/24-tool-with-ui-csp-and-bridge.md) — CSP + `widgetAccessible` + bridge
287
+ - [`24-tool-with-ui-csp-and-bridge`](../examples/24-tool-with-ui-csp-and-bridge.md) — CSP + bridge `callTool`
232
288
 
233
289
  ## Related rules
234
290
 
@@ -40,11 +40,11 @@ const widgetPath = fileURLToPath(new URL('./sales-chart.widget.tsx', import.meta
40
40
 
41
41
  - **`process.cwd()` is whoever launched the process.** `yarn dev` from the repo root, `node dist/main.js` from `/opt/app`, a containerized run from `/`, a serverless cold start from `/var/task`, an Nx executor from `apps/<thing>/` — all different cwds.
42
42
  - **Tool sources move around at build time.** ESM build output is often in `dist/`; `.tool.ts` becomes `.tool.js`. The relative reference's resolution chain is fragile to that.
43
- - **`fileURLToPath(new URL('./x', import.meta.url))` is invariant.** It anchors to the **source file** that contains the URL literal — same answer at dev, build, and runtime.
43
+ - **`fileURLToPath(new URL('./x', import.meta.url))` is independent of cwd.** It anchors to the file that contains the URL literal — the source file under `frontmcp dev`, the **compiled** file once the tool is built. That's why the widget must ship with the build (below).
44
44
 
45
45
  ## CommonJS projects (`__dirname`)
46
46
 
47
- `import.meta.url` is **ESM-only**. In a CommonJS project (`package.json` `"type": "commonjs"`, or `tsconfig` `"module": "commonjs"`) `import.meta` is unavailable and the build fails. Anchor with `__dirname` instead — the CJS equivalent, equally invariant to `process.cwd()`:
47
+ `import.meta.url` is **ESM-only**. In a CommonJS project (`package.json` `"type": "commonjs"`, or `tsconfig` `"module": "commonjs"`) `import.meta` is unavailable and the build fails. Anchor with `__dirname` instead — the CJS equivalent, equally independent of `process.cwd()`:
48
48
 
49
49
  ```typescript
50
50
  import { join } from 'node:path';
@@ -57,7 +57,14 @@ const widgetPath = join(__dirname, 'sales-chart.widget.tsx');
57
57
  })
58
58
  ```
59
59
 
60
- Pick the anchor that matches your module system — both resolve to the tool source's directory regardless of cwd. The rule is only that the path must **never** be a bare relative string.
60
+ Pick the anchor that matches your module system — both resolve to the directory of the file that is running, regardless of cwd. The rule is only that the path must **never** be a bare relative string.
61
+
62
+ ## Ship the widget with the build (#649)
63
+
64
+ The widget is read when the tool is called, from the path the **compiled** tool computes, so after a build it has to exist in the output — tsc never emits `*.widget.tsx`:
65
+
66
+ - `frontmcp build` copies every `*.widget.tsx` / `*.widget.jsx` under the entry's directory. tsc-output targets get them next to each compiled tool (same relative path). Bundled targets (`node`, `cli`, `lambda`, `vercel`) get them directly next to the bundle, because every bundled module's `__dirname` is the bundle's directory — so keep each widget beside the tool that uses it and give it a unique file name.
67
+ - A plain `tsc` build copies nothing — add a copy step, or the call fails with an `ENOENT` error that names the path it looked for.
61
68
 
62
69
  ## Also: name the widget `*.widget.tsx`
63
70
 
@@ -9,6 +9,7 @@ features:
9
9
  - 'Using `keyPrefix` to namespace guard keys in a shared Redis instance'
10
10
  - "Combining `partitionBy: 'ip'` for global limits with `partitionBy: 'session'` per tool"
11
11
  - 'In-memory counters are per-process and would allow N times the intended rate with N instances'
12
+ - "Startup fails closed, and a limited call is refused, with `GuardStorageUnavailableError` when Redis is down, unless `fallback: 'memory'`"
12
13
  ---
13
14
 
14
15
  # Distributed Rate Limiting with Redis
@@ -86,6 +87,13 @@ class Server {}
86
87
  - Using `keyPrefix` to namespace guard keys in a shared Redis instance
87
88
  - Combining `partitionBy: 'ip'` for global limits with `partitionBy: 'session'` per tool
88
89
  - In-memory counters are per-process and would allow N times the intended rate with N instances
90
+ - Startup fails closed, and a limited call is refused, with `GuardStorageUnavailableError` when Redis is down, unless `fallback: 'memory'`
91
+
92
+ ## Notes
93
+
94
+ - `storage` is the `@frontmcp/utils` storage shape (`type` + `redis: { config }` or `redis: { url }`), not the top-level `redis` shape. A block without `type` is auto-detected from the environment and otherwise runs in memory.
95
+ - Keys read `payments:guard:process_payment:<partition>:rl:...`: a trailing `:` on `keyPrefix` is dropped. Before 1.8.6 it was doubled (`payments:guard::...`), so counters briefly split between versions during a rolling deploy.
96
+ - To keep serving with per-instance counters while Redis is down (at startup or mid-run), add `fallback: 'memory'` to `storage`.
89
97
 
90
98
  ## Related
91
99
 
@@ -97,6 +97,7 @@ class Server {}
97
97
  - `local.issuer` -- the issuer named everywhere: discovery's `issuer` and `authorization_servers`, the RFC 9207 `iss` on every authorization response (errors included), and the tokens' `iss`. Without it: `FRONTMCP_PUBLIC_URL` (plus the entry path) when pinned, else the `FRONTMCP_PUBLIC_HOST` boot-time issuer (`http://<host>:<port>`), else the request's origin -- the same on the Node server and under `createFetchHandler()`.
98
98
  - The protected resource metadata's `scopes_supported` is what the mode grants: `allowedScopes` (local/remote), `anonymousScopes` (public), `scopes` (static), `requiredScopes` then `scopes` (transparent), plus `authProviders` scopes outside local/remote mode.
99
99
  - An MCP 2026-07-28 request has no session: anonymous and static-key callers get none minted, so `MCP_SESSION_SECRET` is needed only for session clients; `this.context.verifiedSessionId` is `undefined` there.
100
+ - A session client's `mcp-session-id` (or legacy SSE `?sessionId=`) is served only when `session:verify` verified it: in `public`, `static` and anonymous `transparent` mode it must decrypt under `MCP_SESSION_SECRET` with the mode's signature (static: of the token that opened it); in authenticated modes it must also belong to the caller's token. Any other id gets HTTP 404 (`-32000`) and the client re-initializes -- the raw id is never used to look up a transport, since anonymous sessions share an empty token and the id is their only credential. Run every instance with the same `MCP_SESSION_SECRET`; any instance then serves a session another one minted.
100
101
 
101
102
  Token signing uses **HS256, a symmetric secret** read from the `JWT_SECRET` environment variable -- there is **no RSA/EC key pair** and no key store. Generate a stable secret (`JWT_SECRET=$(openssl rand -hex 32)`); if it is unset, FrontMCP falls back to a random per-process secret and all tokens are invalidated on restart.
102
103
 
@@ -230,9 +230,11 @@ Within a directory:
230
230
  For every CLI option that's also expressible in the config:
231
231
 
232
232
  ```
233
- explicit CLI flag > FRONTMCP_<NAME> env var > frontmcp.config field > built-in default
233
+ explicit CLI flag > frontmcp.config field > built-in default
234
234
  ```
235
235
 
236
+ There are no per-field `FRONTMCP_<NAME>` environment overrides. The only environment variable the CLI reads for configuration is `FRONTMCP_CONFIG`, which selects the config file (an explicit `--config <path>` flag wins over it).
237
+
236
238
  ## Per-command consumption (issue #400)
237
239
 
238
240
  The config is consumed by every `frontmcp` command, not just `build`:
@@ -249,6 +251,22 @@ The config is consumed by every `frontmcp` command, not just `build`:
249
251
 
250
252
  See `transport`, `env`, `clients`, `test`, `skills` field reference in [docs/frontmcp/deployment/frontmcp-config](https://docs.agentfront.dev/frontmcp/deployment/frontmcp-config).
251
253
 
254
+ ## `transport.http.path` for every build target
255
+
256
+ `transport.http.path` is the mount path of the MCP endpoint for **every** build target, not only `frontmcp dev`:
257
+
258
+ | Target | How the path reaches the server |
259
+ | --------------------------------- | -------------------------------------------------------------------------------------------------- |
260
+ | `node` (and its SEA binary) | the generated runner script exports `FRONTMCP_HTTP_ENTRY_PATH` (default only; a real env var wins) |
261
+ | `vercel`, `lambda`, `distributed` | the generated setup file assigns `process.env.FRONTMCP_HTTP_ENTRY_PATH` before the server loads |
262
+ | `cloudflare` | the generated worker setup assigns it the same way |
263
+
264
+ A `@FrontMcp({ http: { entryPath } })` value still wins over the config. When the two differ, `frontmcp build` warns.
265
+
266
+ ## `eject-mcp-config --out` merges
267
+
268
+ `--out` merges into an existing client config instead of replacing it: the parent folder is created when missing, other top-level keys and other `mcpServers` entries are kept, and only this server's entry is replaced. A file that is not valid JSON (or not a JSON object) is refused and left untouched. `--dry-run` prints the merged result and writes nothing.
269
+
252
270
  ## JSON Schema for IDE Support
253
271
 
254
272
  For JSON configs, add `$schema` for autocomplete:
@@ -260,10 +260,12 @@ http: {
260
260
  }
261
261
  ```
262
262
 
263
- | Option | Type | Default | Notes |
264
- | ----------------- | ------------------ | ------------------------- | ---------------------------------------------------------------------- |
265
- | `bodyLimit` | `number \| string` | `'4mb'` | Bytes (number) or body-parser string (`'4mb'`, `'500kb'`, `'2gb'`, …). |
266
- | `urlencodedLimit` | `number \| string` | falls back to `bodyLimit` | Independent override for `application/x-www-form-urlencoded` bodies. |
263
+ | Option | Type | Default | Notes |
264
+ | ----------------- | ------------------ | ------------------------- | ---------------------------------------------------------------------------------------- |
265
+ | `bodyLimit` | `number \| string` | `'4mb'` | Bytes (number) or body-parser string (`'4mb'`, `'500kb'`, `'2gb'`, …). |
266
+ | `urlencodedLimit` | `number \| string` | falls back to `bodyLimit` | Independent override for `application/x-www-form-urlencoded` bodies (Express host only). |
267
+
268
+ The fetch handler used on Cloudflare Workers, Vercel Edge and Deno enforces `bodyLimit` too: it answers 413 for an oversized `Content-Length` without reading the body and stops reading a chunked body at the limit.
267
269
 
268
270
  Requests exceeding the configured limit receive a structured JSON-RPC 413
269
271
  response — never an Express HTML error page: