@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.
- package/catalog/create-tool/SKILL.md +24 -24
- 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 +7 -6
- package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +4 -6
- package/catalog/create-tool/examples/27-tool-with-examples-metadata.md +3 -2
- package/catalog/create-tool/references/decorator-options.md +1 -1
- package/catalog/create-tool/references/elicitation.md +1 -0
- package/catalog/create-tool/references/file-layout.md +2 -0
- package/catalog/create-tool/references/ui-widgets.md +96 -40
- package/catalog/create-tool/rules/widget-paths-anchor-with-import-meta-url.md +10 -3
- package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +8 -0
- package/catalog/frontmcp-config/references/configure-auth.md +1 -0
- package/catalog/frontmcp-config/references/configure-deployment-targets.md +19 -1
- package/catalog/frontmcp-config/references/configure-http.md +6 -4
- package/catalog/frontmcp-config/references/configure-security-headers.md +18 -13
- package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +19 -4
- package/catalog/frontmcp-config/references/configure-throttle.md +25 -7
- 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-vercel/vercel-mcp-endpoint-test.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +3 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-skills-cache.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md +7 -3
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/vercel-config-with-security-headers.md +4 -3
- package/catalog/frontmcp-deployment/references/build-for-browser.md +8 -0
- package/catalog/frontmcp-deployment/references/build-for-mcpb.md +9 -8
- package/catalog/frontmcp-deployment/references/build-for-sdk.md +9 -9
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +3 -1
- package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +9 -8
- package/catalog/frontmcp-deployment/references/deploy-to-vercel-config.md +15 -3
- package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +16 -8
- package/catalog/frontmcp-deployment/references/protocol-versions.md +9 -0
- package/catalog/frontmcp-development/examples/official-plugins/cache-and-feature-flags.md +5 -0
- package/catalog/frontmcp-development/examples/official-plugins/remember-plugin-session-memory.md +7 -5
- package/catalog/frontmcp-development/references/create-agent.md +8 -7
- package/catalog/frontmcp-development/references/create-plugin.md +17 -0
- package/catalog/frontmcp-development/references/official-plugins.md +66 -8
- 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-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +1 -0
- package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md +2 -2
- package/catalog/frontmcp-production-readiness/examples/production-vercel/vercel-edge-config.md +1 -0
- package/catalog/frontmcp-production-readiness/references/common-checklist.md +4 -0
- package/catalog/frontmcp-production-readiness/references/distributed-ha.md +17 -7
- package/catalog/frontmcp-production-readiness/references/health-readiness-endpoints.md +3 -1
- package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +1 -1
- package/catalog/frontmcp-setup/references/nx-workflow.md +25 -17
- package/catalog/frontmcp-setup/references/setup-redis.md +37 -7
- package/catalog/frontmcp-setup/references/setup-sqlite.md +9 -6
- package/catalog/frontmcp-testing/SKILL.md +24 -23
- package/catalog/frontmcp-testing/references/setup-testing.md +28 -3
- package/catalog/frontmcp-testing/references/test-auth.md +8 -0
- package/catalog/frontmcp-testing/references/test-e2e-handler.md +1 -0
- package/catalog/skills-manifest.json +9 -6
- 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 (
|
|
36
|
-
|
|
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 →
|
|
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) | `
|
|
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,
|
|
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
|
-
> **
|
|
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
|
|
45
|
-
//
|
|
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
|
-
-
|
|
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,
|
|
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
|
-
- '
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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` +
|
|
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,
|
|
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` | 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
|
|
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. |
|
|
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
|
|
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 (
|
|
176
|
-
| `getToolInput()` / `getToolOutput()` / `getStructuredContent()` | Read the tool data
|
|
177
|
-
| `getWidgetState()` / `setWidgetState(state)` | Persisted per-widget state
|
|
178
|
-
| `getHostContext()` / `getTheme()` / `getDisplayMode()` | Host context
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
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
|
|
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
|
|
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 + `
|
|
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
|
|
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
|
|
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
|
|
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 >
|
|
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:
|