@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.
- package/README.md +107 -155
- package/catalog/create-tool/SKILL.md +24 -24
- package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +1 -1
- package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +0 -1
- package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +1 -3
- package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +4 -6
- package/catalog/create-tool/references/availability.md +10 -10
- package/catalog/create-tool/references/decorator-options.md +1 -1
- package/catalog/create-tool/references/ui-widgets.md +91 -42
- package/catalog/frontmcp-authorities/SKILL.md +5 -0
- package/catalog/frontmcp-channels/SKILL.md +17 -16
- package/catalog/frontmcp-channels/references/channel-sources.md +4 -4
- package/catalog/frontmcp-channels/references/channel-two-way.md +1 -1
- package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md +2 -0
- package/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md +1 -2
- package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +3 -3
- package/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md +0 -1
- package/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md +0 -1
- package/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md +2 -3
- package/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md +2 -3
- package/catalog/frontmcp-config/references/configure-auth.md +1 -1
- package/catalog/frontmcp-config/references/configure-deployment-targets.md +68 -16
- package/catalog/frontmcp-config/references/configure-http.md +11 -6
- package/catalog/frontmcp-config/references/configure-security-headers.md +18 -13
- package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +4 -4
- package/catalog/frontmcp-config/references/configure-throttle.md +4 -2
- package/catalog/frontmcp-config/references/configure-transport.md +4 -5
- package/catalog/frontmcp-deployment/SKILL.md +19 -19
- package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +5 -3
- package/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +2 -0
- package/catalog/frontmcp-deployment/references/build-for-browser.md +46 -9
- package/catalog/frontmcp-deployment/references/build-for-mcpb.md +15 -8
- package/catalog/frontmcp-deployment/references/build-for-sdk.md +11 -10
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +5 -1
- package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +11 -10
- package/catalog/frontmcp-deployment/references/deploy-to-node.md +11 -11
- package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +15 -7
- package/catalog/frontmcp-deployment/references/mcp-client-integration.md +12 -7
- package/catalog/frontmcp-deployment/references/protocol-versions.md +7 -3
- package/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md +50 -10
- package/catalog/frontmcp-development/examples/openapi-adapter/ref-security-and-filtering.md +13 -7
- package/catalog/frontmcp-development/references/create-adapter.md +14 -0
- package/catalog/frontmcp-development/references/create-agent.md +82 -48
- package/catalog/frontmcp-development/references/create-plugin-hooks.md +15 -5
- package/catalog/frontmcp-development/references/create-plugin.md +23 -2
- package/catalog/frontmcp-development/references/create-skill-with-tools.md +7 -2
- package/catalog/frontmcp-development/references/create-skill.md +4 -0
- package/catalog/frontmcp-development/references/decorators-guide.md +10 -11
- package/catalog/frontmcp-development/references/official-adapters.md +1 -1
- package/catalog/frontmcp-development/references/official-plugins.md +138 -28
- package/catalog/frontmcp-development/references/openapi-adapter.md +52 -2
- package/catalog/frontmcp-guides/references/example-task-manager.md +2 -2
- package/catalog/frontmcp-guides/references/example-weather-api.md +2 -2
- package/catalog/frontmcp-observability/references/metrics-endpoint.md +3 -1
- package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +13 -1
- package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md +2 -2
- package/catalog/frontmcp-production-readiness/references/common-checklist.md +3 -2
- package/catalog/frontmcp-production-readiness/references/distributed-ha.md +54 -24
- package/catalog/frontmcp-production-readiness/references/health-readiness-endpoints.md +3 -1
- package/catalog/frontmcp-setup/examples/nx-workflow/build-test-affected.md +2 -2
- package/catalog/frontmcp-setup/examples/nx-workflow/multi-server-deployment.md +9 -5
- package/catalog/frontmcp-setup/examples/nx-workflow/scaffold-and-generate.md +1 -1
- package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +2 -2
- package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +28 -15
- package/catalog/frontmcp-setup/references/multi-app-composition.md +11 -8
- package/catalog/frontmcp-setup/references/nx-workflow.md +68 -28
- package/catalog/frontmcp-setup/references/project-structure-nx.md +7 -4
- package/catalog/frontmcp-setup/references/setup-project.md +15 -0
- package/catalog/frontmcp-setup/references/setup-redis.md +12 -3
- package/catalog/frontmcp-setup/references/setup-sqlite.md +21 -14
- package/catalog/frontmcp-testing/SKILL.md +28 -23
- package/catalog/frontmcp-testing/references/setup-testing.md +39 -2
- package/catalog/frontmcp-testing/references/test-auth.md +8 -0
- package/catalog/skills-manifest.json +14 -12
- 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,
|
|
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
|
|
43
|
-
| ---------------------------- |
|
|
44
|
-
| **FileSource (recommended)** | `{ file: widgetPath }`
|
|
45
|
-
| **Function** | `` (ctx) => ctx.helpers.html`…` ``
|
|
46
|
-
| **HTML /
|
|
47
|
-
| **React component** | `MyWidget`
|
|
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
|
|
68
|
-
|
|
|
69
|
-
| `template`
|
|
70
|
-
| `widgetDescription`
|
|
71
|
-
| `servingMode`
|
|
72
|
-
| `displayMode`
|
|
73
|
-
| `preferredHeight`
|
|
74
|
-
| `minHeight` / `maxHeight`
|
|
75
|
-
| `aspectRatio`
|
|
76
|
-
| `autoResize`
|
|
77
|
-
| `csp`
|
|
78
|
-
| `contentSecurity`
|
|
79
|
-
| `escapeStringResults`
|
|
80
|
-
| `widgetAccessible`
|
|
81
|
-
| `resourceUri`
|
|
82
|
-
| `uiType`
|
|
83
|
-
| `resourceMode`
|
|
84
|
-
| `hydrate`
|
|
85
|
-
| `externals`, `dependencies`
|
|
86
|
-
| `customShell`, `invocationStatus`, `widgetCapabilities
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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 + `
|
|
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
|
|
79
|
-
| -------------- |
|
|
80
|
-
| Meta keys | `meta: { env: 'prod' }`
|
|
81
|
-
| Source naming | `name: 'deploy-alerts'`
|
|
82
|
-
| Two-way gating | Check sender identity before emitting
|
|
83
|
-
| Error channels | Use `app-event` source with event bus
|
|
84
|
-
| Manual push |
|
|
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
|
|
117
|
-
| ---------------------------- |
|
|
118
|
-
| No notifications arrive | Client doesn't support channels
|
|
119
|
-
| `channel-reply` tool missing | No two-way channels registered
|
|
120
|
-
| Webhook returns 500 | `onEvent()` throws
|
|
121
|
-
|
|
|
122
|
-
|
|
|
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
|
|
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
|
|
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
|
|
package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md
CHANGED
|
@@ -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 {
|
|
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
|
|
|
@@ -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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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.
|