@frontmcp/skills 1.8.6 → 1.9.0

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 (77) hide show
  1. package/README.md +107 -155
  2. package/catalog/create-tool/SKILL.md +24 -24
  3. package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +1 -1
  4. package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +0 -1
  5. package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +1 -3
  6. package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +4 -6
  7. package/catalog/create-tool/references/availability.md +10 -10
  8. package/catalog/create-tool/references/decorator-options.md +1 -1
  9. package/catalog/create-tool/references/ui-widgets.md +91 -42
  10. package/catalog/frontmcp-authorities/SKILL.md +5 -0
  11. package/catalog/frontmcp-channels/SKILL.md +17 -16
  12. package/catalog/frontmcp-channels/references/channel-sources.md +4 -4
  13. package/catalog/frontmcp-channels/references/channel-two-way.md +1 -1
  14. package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md +2 -0
  15. package/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md +1 -2
  16. package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +3 -3
  17. package/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md +0 -1
  18. package/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md +0 -1
  19. package/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md +2 -3
  20. package/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md +2 -3
  21. package/catalog/frontmcp-config/references/configure-auth.md +1 -1
  22. package/catalog/frontmcp-config/references/configure-deployment-targets.md +68 -16
  23. package/catalog/frontmcp-config/references/configure-http.md +11 -6
  24. package/catalog/frontmcp-config/references/configure-security-headers.md +18 -13
  25. package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +4 -4
  26. package/catalog/frontmcp-config/references/configure-throttle.md +4 -2
  27. package/catalog/frontmcp-config/references/configure-transport.md +4 -5
  28. package/catalog/frontmcp-deployment/SKILL.md +19 -19
  29. package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +5 -3
  30. package/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md +1 -1
  31. package/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md +1 -1
  32. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +2 -0
  33. package/catalog/frontmcp-deployment/references/build-for-browser.md +46 -9
  34. package/catalog/frontmcp-deployment/references/build-for-mcpb.md +15 -8
  35. package/catalog/frontmcp-deployment/references/build-for-sdk.md +11 -10
  36. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +5 -1
  37. package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +11 -10
  38. package/catalog/frontmcp-deployment/references/deploy-to-node.md +11 -11
  39. package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +15 -7
  40. package/catalog/frontmcp-deployment/references/mcp-client-integration.md +12 -7
  41. package/catalog/frontmcp-deployment/references/protocol-versions.md +7 -3
  42. package/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md +50 -10
  43. package/catalog/frontmcp-development/examples/openapi-adapter/ref-security-and-filtering.md +13 -7
  44. package/catalog/frontmcp-development/references/create-adapter.md +14 -0
  45. package/catalog/frontmcp-development/references/create-agent.md +82 -48
  46. package/catalog/frontmcp-development/references/create-plugin-hooks.md +15 -5
  47. package/catalog/frontmcp-development/references/create-plugin.md +23 -2
  48. package/catalog/frontmcp-development/references/create-skill-with-tools.md +7 -2
  49. package/catalog/frontmcp-development/references/create-skill.md +4 -0
  50. package/catalog/frontmcp-development/references/decorators-guide.md +10 -11
  51. package/catalog/frontmcp-development/references/official-adapters.md +1 -1
  52. package/catalog/frontmcp-development/references/official-plugins.md +138 -28
  53. package/catalog/frontmcp-development/references/openapi-adapter.md +52 -2
  54. package/catalog/frontmcp-guides/references/example-task-manager.md +2 -2
  55. package/catalog/frontmcp-guides/references/example-weather-api.md +2 -2
  56. package/catalog/frontmcp-observability/references/metrics-endpoint.md +3 -1
  57. package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +13 -1
  58. package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md +2 -2
  59. package/catalog/frontmcp-production-readiness/references/common-checklist.md +3 -2
  60. package/catalog/frontmcp-production-readiness/references/distributed-ha.md +54 -24
  61. package/catalog/frontmcp-production-readiness/references/health-readiness-endpoints.md +3 -1
  62. package/catalog/frontmcp-setup/examples/nx-workflow/build-test-affected.md +2 -2
  63. package/catalog/frontmcp-setup/examples/nx-workflow/multi-server-deployment.md +9 -5
  64. package/catalog/frontmcp-setup/examples/nx-workflow/scaffold-and-generate.md +1 -1
  65. package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +2 -2
  66. package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +28 -15
  67. package/catalog/frontmcp-setup/references/multi-app-composition.md +11 -8
  68. package/catalog/frontmcp-setup/references/nx-workflow.md +68 -28
  69. package/catalog/frontmcp-setup/references/project-structure-nx.md +7 -4
  70. package/catalog/frontmcp-setup/references/setup-project.md +15 -0
  71. package/catalog/frontmcp-setup/references/setup-redis.md +12 -3
  72. package/catalog/frontmcp-setup/references/setup-sqlite.md +21 -14
  73. package/catalog/frontmcp-testing/SKILL.md +28 -23
  74. package/catalog/frontmcp-testing/references/setup-testing.md +39 -2
  75. package/catalog/frontmcp-testing/references/test-auth.md +8 -0
  76. package/catalog/skills-manifest.json +14 -12
  77. package/package.json +1 -1
@@ -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` | Not supported: a bare component reference cannot be bundled, so the page is an empty root. Startup warns once per tool; use `{ file }`. |
48
48
 
49
49
  The renderer auto-detects which one you passed.
50
50
 
@@ -64,34 +64,82 @@ 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` | 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 | `{ 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. See [CSP origins](#csp-origins). |
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
 
90
118
  - `resources/read ui://widget/{toolName}.html` serves only what was compiled at startup (`static`, the `hybrid` shell), rendered without caller data, or a data-free placeholder that gets the result through the bridge.
91
119
  - An `inline` render embeds the call's input and output. It is returned only in that call's `_meta['ui/html']` and is never cached where `resources/read` can serve it, so one caller can't read another caller's widget (GHSA-rhr9-vhpf-jqp7).
120
+ - A page compiled at startup injects no call data (`window.__mcpToolInput` / `window.__mcpToolOutput` are `null`), so the bridge takes the result from the host: `window.openai.toolOutput` (ChatGPT, read at load and followed after) or `ui/notifications/tool-result` (MCP Apps). A `.tsx` widget renders with `loading: true` until it arrives, and `useToolOutput()` returns `null` until then.
92
121
  - Hosts that load the widget via `resources/read` (MCP Apps hosts such as Claude) need `servingMode: 'static'` and a template that reads data from `window.FrontMcpBridge`; set `resourceMode: 'inline'` explicitly for Claude in static mode.
122
+ - With the default `servingMode`, every `tools/call` result carries the page in `_meta['ui/html']` — hosts that load the `ui://` resource don't read it, so use `servingMode: 'static'` to leave it out. A call the widget makes back through `ui/callServerTool` gets the data only.
93
123
  - The advertised URI percent-encodes the tool name (`app:tool` → `ui://widget/app%3Atool.html`). Encoded and raw forms both read back; a name that decodes to anything outside `A-Z a-z 0-9 _ - . / : @` is rejected.
94
124
 
125
+ ## CSP origins
126
+
127
+ The page FrontMCP writes also carries its own Content-Security-Policy built from `ui.csp`:
128
+
129
+ - An origin must be `https://` or `wss://` (a WebSocket API needs `wss://` in `connectDomains`), a `https://*.` / `wss://*.` wildcard, or `http://` / `ws://` on `localhost` / `127.0.0.1` / `[::1]`.
130
+ - Declared origins are added to what the page already reaches: the CDNs and `resourceDomains` stay in `connect-src`.
131
+ - Any other origin (bare host, `ftp://`, plain `http://` host, a value with a `?query` or `#fragment`) is left out of the page policy; startup logs a warning naming the tool and the origin. The resource `_meta.ui.csp` keeps the origins as written.
132
+
133
+ ```typescript
134
+ ui: {
135
+ template: { file: widgetPath },
136
+ csp: {
137
+ connectDomains: ['https://api.example.com', 'wss://live.example.com'],
138
+ resourceDomains: ['https://cdn.example.com'],
139
+ },
140
+ }
141
+ ```
142
+
95
143
  ## Trusted markup and escaping template results
96
144
 
97
145
  Build function-template markup with the `ctx.helpers.html` tagged template. Literal parts stay markup; every interpolated value is HTML-escaped unless it is itself trusted markup:
@@ -165,11 +213,11 @@ Match the version to `@frontmcp/sdk`. Without it, server-side bundling fails wit
165
213
  npm install esbuild # in "dependencies", not "devDependencies"
166
214
  ```
167
215
 
168
- 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.
216
+ 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. Bundling works the same in CommonJS and ES-module (`"type": "module"`) projects.
169
217
 
170
218
  ## Widget bridge — `window.FrontMcpBridge`
171
219
 
172
- When the widget needs to read tool data or invoke other tools, the bridge IIFE is injected automatically. Set `widgetAccessible: true` to enable `callTool`:
220
+ 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):
173
221
 
174
222
  ```typescript
175
223
  ui: {
@@ -182,19 +230,18 @@ ui: {
182
230
  };
183
231
  </script>
184
232
  `,
185
- widgetAccessible: true,
186
233
  }
187
234
  ```
188
235
 
189
- | Bridge method | Purpose |
190
- | --------------------------------------------------------------- | ------------------------------------------------------- |
191
- | `callTool(name, args)` | Invoke another tool (requires `widgetAccessible: true`) |
192
- | `getToolInput()` / `getToolOutput()` / `getStructuredContent()` | Read the tool data |
193
- | `getWidgetState()` / `setWidgetState(state)` | Persisted per-widget state |
194
- | `getHostContext()` / `getTheme()` / `getDisplayMode()` | Host context |
195
- | `onContextChange(cb)` | Subscribe to host context changes (handshake included) |
196
- | `hasCapability(cap)` | Probe adapter capabilities |
197
- | `onToolResponseMetadata(cb)` | Subscribe to `ui/html` arrival (inline mode) |
236
+ | Bridge method | Purpose |
237
+ | --------------------------------------------------------------- | ------------------------------------------------------ |
238
+ | `callTool(name, args)` | Invoke another tool (the host may still refuse) |
239
+ | `getToolInput()` / `getToolOutput()` / `getStructuredContent()` | Read the tool data |
240
+ | `getWidgetState()` / `setWidgetState(state)` | Persisted per-widget state |
241
+ | `getHostContext()` / `getTheme()` / `getDisplayMode()` | Host context |
242
+ | `onContextChange(cb)` | Subscribe to host context changes (handshake included) |
243
+ | `hasCapability(cap)` | Probe adapter capabilities |
244
+ | `onToolResponseMetadata(cb)` | Subscribe to `ui/html` arrival (inline mode) |
198
245
 
199
246
  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.
200
247
 
@@ -217,6 +264,8 @@ The page follows the host theme. When an MCP Apps host sends `theme: 'light' | '
217
264
  | **MCP Inspector** | Useful for local development. Static mode works fine. |
218
265
  | **Gemini / unknown** | `ui` is ignored — JSON output is returned. |
219
266
 
267
+ The host is decided when the client connects: a `transport.platformDetection.mappings` entry matching the client name wins; then a client that declares the MCP Apps extension (`io.modelcontextprotocol/ui`) is `ext-apps` whatever its name (`gemini-cli` included); then the client name. Opt a client out of MCP Apps with a mapping: `platformDetection: { mappings: [{ pattern: 'gemini-cli', platform: 'gemini' }] }`.
268
+
220
269
  ## Widget sizing
221
270
 
222
271
  Set sizing in the `ui` config — no hand-rolled `ui/notifications/size-changed` + `ResizeObserver` needed:
@@ -238,13 +287,13 @@ What FrontMCP does with it:
238
287
  - **`_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.
239
288
  - **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.
240
289
  - **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.
241
- - **When it is sent** — reports wait for the bridge to initialize. In an ext-apps host the first report goes out once the `ui/initialize` handshake completes (a request sent earlier would be rejected); a report the host rejects is sent again on the next observation, even for the same height. A manual `setSize` called before the handshake is held and delivered right after it (only the latest size).
290
+ - **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.
242
291
 
243
292
  Per-host behavior:
244
293
 
245
294
  - **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.
246
295
  - **OpenAI ChatGPT** — auto-resize forwards to the Apps SDK sizing API when one is exposed; otherwise the SDK's own DOM measurement applies.
247
- - **ext-apps hosts** — the measured size is reported via a `ui/setSize` request (parallels `ui/setDisplayMode`).
296
+ - **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.
248
297
  - **Gemini / generic / unknown** — `setSize` is a no-op; only the static CSS applies.
249
298
 
250
299
  `displayMode: 'fullscreen'` remains a separate, best-effort hint a host may ignore.
@@ -257,7 +306,7 @@ Per-host behavior:
257
306
 
258
307
  - [`22-tool-with-ui-html-template`](../examples/22-tool-with-ui-html-template.md) — inline function template
259
308
  - [`23-tool-with-ui-filesource-tsx`](../examples/23-tool-with-ui-filesource-tsx.md) — `.tsx` widget, host-detect
260
- - [`24-tool-with-ui-csp-and-bridge`](../examples/24-tool-with-ui-csp-and-bridge.md) — CSP + `widgetAccessible` + bridge
309
+ - [`24-tool-with-ui-csp-and-bridge`](../examples/24-tool-with-ui-csp-and-bridge.md) — CSP + bridge `callTool`
261
310
 
262
311
  ## Related rules
263
312
 
@@ -287,6 +287,11 @@ authorities: {
287
287
  }
288
288
  ```
289
289
 
290
+ Tools, resources, agents and jobs (workflow steps included) run the pipes when their context is built, before any hook
291
+ or `execute()` reads `this.auth`, so `this.auth.tenantId` is set there. Pipes can be async; one that throws is logged
292
+ and leaves its fields `undefined`. Pipes extend `this.auth` only -- authority policies still evaluate the claims from
293
+ `claimsMapping` / `claimsResolver`. Up to 1.8.7 `pipes` never ran (their fields were always `undefined`).
294
+
290
295
  ### Skill Authorities (`@Skill({ authorities })`)
291
296
 
292
297
  `authorities` on `@Skill` is enforced exactly like the other entry types, across
@@ -75,13 +75,13 @@ Build push-based notification channels that stream real-time events into Claude
75
75
 
76
76
  ## Common Patterns
77
77
 
78
- | Pattern | Correct | Incorrect | Why |
79
- | -------------- | --------------------------------------- | -------------------------------------------- | ------------------------------------------------------------------ |
80
- | Meta keys | `meta: { env: 'prod' }` | `meta: { 'my-env': 'prod' }` | Meta keys must be valid identifiers (letters, digits, underscores) |
81
- | Source naming | `name: 'deploy-alerts'` | `name: 'Deploy Alerts!'` | Channel names should be kebab-case identifiers |
82
- | Two-way gating | Check sender identity before emitting | Trust room/group membership | Prevent prompt injection from untrusted group members |
83
- | Error channels | Use `app-event` source with event bus | Poll for errors in a loop | Event bus is push-based and efficient |
84
- | Manual push | Use `scope.channelNotifications.send()` | Call `pushNotification` on instance directly | Service handles capability filtering |
78
+ | Pattern | Correct | Incorrect | Why |
79
+ | -------------- | ------------------------------------------ | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | Meta keys | `meta: { env: 'prod' }` | `meta: { 'my-env': 'prod' }` | Meta keys must be valid identifiers (letters, digits, underscores) |
81
+ | Source naming | `name: 'deploy-alerts'` | `name: 'Deploy Alerts!'` | Channel names should be kebab-case identifiers |
82
+ | Two-way gating | Check sender identity before emitting | Trust room/group membership | Prevent prompt injection from untrusted group members |
83
+ | Error channels | Use `app-event` source with event bus | Poll for errors in a loop | Event bus is push-based and efficient |
84
+ | Manual push | `await scope.channelNotifications?.send()` | `sendToSubscribedSessions()` for a regular push | `send()` runs the hookable `channels:send-notification` flow (defaultMeta, channel meta, hooks); the `sendTo*` methods deliver directly |
85
85
 
86
86
  ## Verification Checklist
87
87
 
@@ -102,24 +102,25 @@ Build push-based notification channels that stream real-time events into Claude
102
102
 
103
103
  - [ ] `twoWay: true` is set on channels that need replies
104
104
  - [ ] `channel-reply` tool appears in tool list
105
- - [ ] `onReply()` is implemented and forwards to external system
105
+ - [ ] `onReply()` is implemented and forwards to external system (if it throws, `channel-reply` answers an error result)
106
106
  - [ ] Sender authentication is enforced before emitting events
107
107
 
108
108
  ### Sources
109
109
 
110
110
  - [ ] Webhook endpoints return 200 on success
111
- - [ ] Event bus subscriptions are cleaned up on scope teardown
111
+ - [ ] Event bus subscriptions are cleaned up on scope teardown, and service channels get `onDisconnect()` on shutdown and on `dispose()`
112
112
  - [ ] Agent/job completion filters match expected IDs
113
113
 
114
114
  ## Troubleshooting
115
115
 
116
- | Problem | Cause | Solution |
117
- | ---------------------------- | ------------------------------- | ------------------------------------------------------------------ |
118
- | No notifications arrive | Client doesn't support channels | Check client capabilities include `experimental['claude/channel']` |
119
- | `channel-reply` tool missing | No two-way channels registered | Set `twoWay: true` on at least one channel |
120
- | Webhook returns 500 | `onEvent()` throws | Check channel handler error logs |
121
- | Duplicate notifications | Multiple sessions subscribed | This is correct behavior -- each session gets its own copy |
122
- | Events lost on reconnect | Channels are in-memory | Channel state resets on server restart; use persistent sources |
116
+ | Problem | Cause | Solution |
117
+ | ---------------------------- | -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
118
+ | No notifications arrive | Client doesn't support channels | Check client capabilities include `experimental['claude/channel']` |
119
+ | `channel-reply` tool missing | No two-way channels registered | Set `twoWay: true` on at least one channel |
120
+ | Webhook returns 500 | `onEvent()` throws | Check channel handler error logs |
121
+ | A notification never arrives | A `ChannelSendHook` stopped it, or `ChannelListHook` left the channel out of the session's subscriptions | Check plugins hooking `channels:send-notification` / `channels:list` |
122
+ | Duplicate notifications | Multiple sessions subscribed | This is correct behavior -- each session gets its own copy |
123
+ | Events lost on reconnect | Channels are in-memory | Channel state resets on server restart; use persistent sources |
123
124
 
124
125
  ## Examples
125
126
 
@@ -59,8 +59,8 @@ class ErrorAlertChannel extends ChannelContext {
59
59
  }
60
60
  }
61
61
 
62
- // In your application code:
63
- scope.channelEventBus.emit('app:error', {
62
+ // In your application code (in a tool: this.scope.channelEventBus):
63
+ scope.channelEventBus?.emit('app:error', {
64
64
  message: 'Connection refused',
65
65
  stack: 'Error: ECONNREFUSED...',
66
66
  level: 'critical',
@@ -181,7 +181,7 @@ class LogWatcherChannel extends ChannelContext {
181
181
 
182
182
  ## Manual Source
183
183
 
184
- No automatic wiring. Push notifications programmatically via `scope.channelNotifications.send()`.
184
+ No automatic wiring. Push notifications programmatically via `scope.channelNotifications.send()`, which runs the hookable `channels:send-notification` flow like every channel notification: `channels.defaultMeta`, then the channel's `meta`, then the notification's own meta, with `source` set to the channel name.
185
185
 
186
186
  ```typescript
187
187
  const StatusChannel = channel({
@@ -193,7 +193,7 @@ const StatusChannel = channel({
193
193
  }));
194
194
 
195
195
  // Push from anywhere with scope access:
196
- scope.channelNotifications.send('status-updates', 'Server maintenance starting in 5 minutes');
196
+ await scope.channelNotifications?.send('status-updates', 'Server maintenance starting in 5 minutes');
197
197
  ```
198
198
 
199
199
  ## Replay Buffer
@@ -13,7 +13,7 @@ Two-way channels let external users communicate with Claude Code through messagi
13
13
  2. **Transform**: `onEvent()` converts to `ChannelNotification`
14
14
  3. **Push**: Notification sent to Claude Code session
15
15
  4. **Reply**: Claude calls `channel-reply` tool with response text
16
- 5. **Forward**: `onReply()` sends the reply back to the external platform
16
+ 5. **Forward**: `onReply()` sends the reply back to the external platform. If it throws, `channel-reply` answers an error result with the message, so Claude knows the reply did not go out (up to 1.8.7 the tool reported success)
17
17
 
18
18
  ## API Surface
19
19
 
@@ -70,6 +70,8 @@ frontmcp build --target node
70
70
  FRONTMCP_DEPLOYMENT_MODE=distributed frontmcp build --target distributed
71
71
  ```
72
72
 
73
+ The distributed build writes the `ha` block to `FRONTMCP_HA_HEARTBEAT_INTERVAL_MS`, `FRONTMCP_HA_HEARTBEAT_TTL_MS`, `FRONTMCP_HA_TAKEOVER_GRACE_MS` and `FRONTMCP_HA_KEY_PREFIX` in the generated setup file (only where the platform has not set them); each pod reads them at startup. Give every pod the same `MCP_SESSION_SECRET` so any pod can route a session's request to the pod that owns it.
74
+
73
75
  ### Server Code
74
76
 
75
77
  ```typescript
@@ -19,7 +19,7 @@ Configure Vercel KV for session storage in serverless Vercel deployments.
19
19
 
20
20
  ```typescript
21
21
  // src/server.ts
22
- import { FrontMcp, App } from '@frontmcp/sdk';
22
+ import { App, FrontMcp } from '@frontmcp/sdk';
23
23
 
24
24
  @App({ name: 'my-app' })
25
25
  class MyApp {}
@@ -33,7 +33,6 @@ class MyApp {}
33
33
  },
34
34
  transport: {
35
35
  protocol: 'stateless-api',
36
- sessionMode: 'stateless',
37
36
  },
38
37
  })
39
38
  class Server {}
@@ -9,7 +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 with `GuardStorageUnavailableError` when Redis is down, unless `fallback: 'memory'`"
12
+ - "Startup fails closed, and a limited call is refused, with `GuardStorageUnavailableError` when Redis is down, unless `fallback: 'memory'`"
13
13
  ---
14
14
 
15
15
  # Distributed Rate Limiting with Redis
@@ -87,13 +87,13 @@ class Server {}
87
87
  - Using `keyPrefix` to namespace guard keys in a shared Redis instance
88
88
  - Combining `partitionBy: 'ip'` for global limits with `partitionBy: 'session'` per tool
89
89
  - In-memory counters are per-process and would allow N times the intended rate with N instances
90
- - Startup fails closed with `GuardStorageUnavailableError` when Redis is down, unless `fallback: 'memory'`
90
+ - Startup fails closed, and a limited call is refused, with `GuardStorageUnavailableError` when Redis is down, unless `fallback: 'memory'`
91
91
 
92
92
  ## Notes
93
93
 
94
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
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, add `fallback: 'memory'` to `storage`.
96
+ - To keep serving with per-instance counters while Redis is down (at startup or mid-run), add `fallback: 'memory'` to `storage`.
97
97
 
98
98
  ## Related
99
99
 
@@ -44,7 +44,6 @@ class DevtoolsApp {}
44
44
  info: { name: 'custom-protocol-server', version: '1.0.0' },
45
45
  apps: [DevtoolsApp],
46
46
  transport: {
47
- sessionMode: 'stateful',
48
47
  protocol: {
49
48
  sse: true, // SSE endpoint enabled
50
49
  streamable: true, // Streamable HTTP POST enabled
@@ -44,7 +44,6 @@ class ReportsApp {}
44
44
  info: { name: 'distributed-server', version: '1.0.0' },
45
45
  apps: [ReportsApp],
46
46
  transport: {
47
- sessionMode: 'stateful',
48
47
  protocol: 'modern',
49
48
  distributedMode: true,
50
49
  persistence: {
@@ -5,7 +5,7 @@ level: basic
5
5
  description: 'Configure stateless transport for Vercel, Lambda, or Cloudflare deployments.'
6
6
  tags: [config, vercel, lambda, cloudflare, session, transport]
7
7
  features:
8
- - "Using `sessionMode: 'stateless'` to disable session management"
8
+ - 'Serving without sessions: whether the server keeps sessions follows `transport.protocol` (`sessionMode` has no effect)'
9
9
  - "Using the `'stateless-api'` preset: no SSE, no streaming, pure request/response"
10
10
  - 'Each request is standalone with no server-side state between invocations'
11
11
  - 'Required for serverless targets (Vercel, Lambda, Cloudflare Workers)'
@@ -48,7 +48,6 @@ class CurrencyApp {}
48
48
  info: { name: 'serverless-server', version: '1.0.0' },
49
49
  apps: [CurrencyApp],
50
50
  transport: {
51
- sessionMode: 'stateless',
52
51
  protocol: 'stateless-api',
53
52
  },
54
53
  })
@@ -57,7 +56,7 @@ class Server {}
57
56
 
58
57
  ## What This Demonstrates
59
58
 
60
- - Using `sessionMode: 'stateless'` to disable session management
59
+ - Serving without sessions: whether the server keeps sessions follows `transport.protocol` (`sessionMode` has no effect)
61
60
  - Using the `'stateless-api'` preset: no SSE, no streaming, pure request/response
62
61
  - Each request is standalone with no server-side state between invocations
63
62
  - Required for serverless targets (Vercel, Lambda, Cloudflare Workers)
@@ -7,7 +7,7 @@ tags: [config, vercel, lambda, cloudflare, session, transport]
7
7
  features:
8
8
  - "The `'stateless-api'` preset disables SSE, streaming, and sessions entirely"
9
9
  - 'Each request is standalone with no server-side state'
10
- - "Pair with `sessionMode: 'stateless'` for serverless execution"
10
+ - 'No `sessionMode` needed: sessions follow the protocol preset'
11
11
  - 'Required for Vercel, Lambda, Cloudflare Workers where persistent connections are not allowed'
12
12
  ---
13
13
 
@@ -46,7 +46,6 @@ class TranslateApp {}
46
46
  info: { name: 'serverless-translate', version: '1.0.0' },
47
47
  apps: [TranslateApp],
48
48
  transport: {
49
- sessionMode: 'stateless',
50
49
  protocol: 'stateless-api',
51
50
  },
52
51
  })
@@ -59,7 +58,7 @@ class Server {}
59
58
 
60
59
  - The `'stateless-api'` preset disables SSE, streaming, and sessions entirely
61
60
  - Each request is standalone with no server-side state
62
- - Pair with `sessionMode: 'stateless'` for serverless execution
61
+ - No `sessionMode` needed: sessions follow the protocol preset
63
62
  - Required for Vercel, Lambda, Cloudflare Workers where persistent connections are not allowed
64
63
 
65
64
  ## Related
@@ -105,7 +105,7 @@ Key local-mode options:
105
105
 
106
106
  - `tokenStorage` -- where authorization codes / refresh tokens / federated sessions persist. Defaults to `'memory'` (lost on restart). Use `{ sqlite: { path } }` for single-node persistence or `{ redis: { ... } }` for multi-instance. This is honored in local mode.
107
107
  - `requireEmail` (default `true`) -- when `false`, the login callback mints a code without prompting for an email, deriving a stable `sub` from `anonymousSubject` (default `'local-operator'`). Use for single-operator setups (e.g. Claude Code).
108
- - `consent` -- enables an **interactive tool-selection screen** during login AND **call-time enforcement**. When `consent.enabled` is `true`, `/oauth/callback` renders a consent screen (after authentication) listing the available tools; the user's checked tools are GET-submitted back to `/oauth/callback`, embedded in the token's `consent` claim, and enforced on every `tools/call` — a call to an unselected tool is rejected with `TOOL_NOT_CONSENTED` (JSON-RPC `-32003`). Honored flags: `groupByApp` (default `true`), `showDescriptions` (default `true`), `allowSelectAll` (default `true`), `requireSelection` (default `true` — rejects an empty submit), `customMessage`, `rememberConsent` (default `true`), `excludedTools` (never offered, always available), `defaultSelectedTools` (pre-checked). Federated logins show the screen after the last provider links. Tokens minted without consent (disabled, or via the test factory) carry no claim and stay all-tools-allowed. `rememberConsent` persists each user's per-client selection (keyed by `consent:{userSub}:{clientId}`, sharing the configured `tokenStorage` backend) and reuses it on the next login: the screen is skipped when no new tool appeared, or re-shown pre-filled when a NEW tool was added (a newly-added tool is never silently granted). Set `rememberConsent: false` to always re-show the screen.
108
+ - `consent` -- enables an **interactive tool-selection screen** during login AND **call-time enforcement**. When `consent.enabled` is `true`, `/oauth/callback` renders a consent screen (after authentication) listing the available tools; the user's checked tools are GET-submitted back to `/oauth/callback`, embedded in the token's `consent` claim, and enforced on every `tools/call` — a call to an unselected tool is rejected with `TOOL_NOT_CONSENTED` (JSON-RPC `-32003`). Honored flags: `groupByApp` (default `true`), `showDescriptions` (default `true`), `allowSelectAll` (default `true`), `requireSelection` (default `true` — rejects an empty submit), `customMessage`, `rememberConsent` (default `true`), `excludedTools` (never offered, always available), `defaultSelectedTools` (pre-checked). Federated logins show the screen after the last provider links. Tokens minted without consent (disabled, or via the test factory) carry no claim and stay all-tools-allowed. `rememberConsent` persists each user's per-client selection (keyed by `consent:{userSub}:{clientId}`, sharing the configured `tokenStorage` backend) and reuses it on the next login: the screen is skipped when no new tool appeared, or re-shown pre-filled when a NEW tool was added (a newly-added tool is never silently granted). Set `rememberConsent: false` to always re-show the screen. An agent is offered as its `invoke_<agent>` tool. The tools declared inside an `@Agent` and its nested agents are never offered, and the consent given to the agent covers them; the other agents and server tools an agent calls (`swarm`, `execution.inheritParentTools`) are offered and need their own consent.
109
109
  - `login` -- customize the built-in login page: `title` / `subtitle` / `logoUri`, declarative `fields` (each `{ type: 'text'|'password'|'email'|'select'|'hidden'; label?; required?; placeholder?; options? }`), a full HTML `render(ctx)` override, and a `subject` strategy (`{ fromField, strategy: 'per-session'|'per-account' }`). Omitting `login` keeps the default email/name page.
110
110
  - `authenticate(input, ctx)` -- custom verification run at the login callback **before** a token is minted. `input.fields` carries the submitted login fields (reserved OAuth params excluded); `ctx` is `{ get, fetch, logger, clientId?, clientName? }`. Return `{ ok: true, sub?, claims? }` to mint a token (custom `claims` are embedded in the JWT; reserved claims like `sub`/`iss`/`exp`/`scope` are stripped) or `{ ok: false, message, retryField? }` to re-render the login page with the error (no code issued). When set, the email requirement no longer applies.
111
111
  - `providers` -- declarative upstream OAuth providers (GitHub, Slack, Jira, …) to orchestrate. When set, FrontMCP federates them at `/oauth/authorize`, stores each provider's tokens **encrypted server-side**, and exposes them to tools via `this.orchestration.getToken(id)`. See [Multi-provider orchestration](#multi-provider-orchestration-providers--federatedauth) below.