@frontmcp/skills 1.8.0 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/catalog/create-tool/SKILL.md +1 -1
  2. package/catalog/create-tool/examples/08-tool-with-provider-injection.md +2 -2
  3. package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +28 -17
  4. package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +10 -7
  5. package/catalog/create-tool/references/decorator-options.md +1 -0
  6. package/catalog/create-tool/references/elicitation.md +9 -0
  7. package/catalog/create-tool/references/error-handling.md +10 -6
  8. package/catalog/create-tool/references/execution-context.md +17 -12
  9. package/catalog/create-tool/references/function-style-builder.md +1 -1
  10. package/catalog/create-tool/references/input-schema.md +2 -0
  11. package/catalog/create-tool/references/output-schema.md +6 -1
  12. package/catalog/create-tool/references/throttling.md +7 -8
  13. package/catalog/create-tool/references/ui-widgets.md +44 -6
  14. package/catalog/frontmcp-authorities/SKILL.md +1 -0
  15. package/catalog/frontmcp-authorities/references/custom-evaluators.md +13 -2
  16. package/catalog/frontmcp-authorities/references/rbac-abac-rebac.md +2 -0
  17. package/catalog/frontmcp-config/examples/configure-skills-http/inject-instructions.md +6 -4
  18. package/catalog/frontmcp-config/references/configure-auth.md +1 -0
  19. package/catalog/frontmcp-config/references/configure-skills-http.md +4 -2
  20. package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +5 -5
  21. package/catalog/frontmcp-config/references/configure-throttle.md +49 -22
  22. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md +4 -0
  23. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +1 -1
  24. package/catalog/frontmcp-development/examples/create-plugin-hooks/caching-with-around.md +7 -6
  25. package/catalog/frontmcp-development/examples/official-plugins/production-multi-plugin-setup.md +2 -2
  26. package/catalog/frontmcp-development/references/create-plugin-hooks.md +21 -17
  27. package/catalog/frontmcp-development/references/create-plugin.md +41 -7
  28. package/catalog/frontmcp-development/references/create-prompt.md +18 -16
  29. package/catalog/frontmcp-development/references/create-provider.md +3 -3
  30. package/catalog/frontmcp-development/references/create-resource.md +10 -8
  31. package/catalog/frontmcp-development/references/create-skill.md +7 -0
  32. package/catalog/frontmcp-development/references/decorators-guide.md +9 -8
  33. package/catalog/frontmcp-development/references/official-plugins.md +88 -6
  34. package/catalog/frontmcp-development/references/openapi-adapter.md +2 -1
  35. package/catalog/frontmcp-observability/references/metrics-endpoint.md +1 -1
  36. package/catalog/frontmcp-observability/references/structured-logging.md +35 -0
  37. package/catalog/frontmcp-testing/examples/test-e2e-handler/tool-call-and-error-e2e.md +3 -1
  38. package/catalog/frontmcp-testing/references/setup-testing.md +9 -9
  39. package/catalog/frontmcp-testing/references/test-e2e-handler.md +35 -0
  40. package/catalog/skills-manifest.json +9 -6
  41. package/package.json +1 -1
@@ -199,7 +199,7 @@ If a request seems to conflict with an inherited default (e.g., "wrap `inputSche
199
199
 
200
200
  11. Should the result render as a widget in the host UI?
201
201
  YES → ui: { template, … }
202
- ├── Quick HTML → ui: { template: (ctx) => '<div>…</div>' }
202
+ ├── Quick HTML → ui: { template: (ctx) => ctx.helpers.html`<div>…</div>` }
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
@@ -70,7 +70,7 @@ import { inputSchema, outputSchema, type GetUserInput, type GetUserOutput } from
70
70
  })
71
71
  export class GetUserTool extends ToolContext {
72
72
  async execute(input: GetUserInput): Promise<GetUserOutput> {
73
- const users = this.get(USER_SERVICE); // throws DependencyNotFoundError if not registered
73
+ const users = this.get(USER_SERVICE); // throws ProviderNotAvailableError if not registered
74
74
  const user = await users.findById(input.id);
75
75
  if (!user) {
76
76
  this.fail(new ResourceNotFoundError(`user:${input.id}`)); // never returns
@@ -106,5 +106,5 @@ export class MainApp {}
106
106
 
107
107
  ## `this.get` vs `this.tryGet`
108
108
 
109
- - `this.get(TOKEN)` — throws `DependencyNotFoundError` if not registered. Use when the tool genuinely requires the dep.
109
+ - `this.get(TOKEN)` — throws `ProviderNotAvailableError` if not registered. Use when the tool genuinely requires the dep.
110
110
  - `this.tryGet(TOKEN)` — returns `undefined` if not registered. Use when the tool degrades gracefully (e.g. optional cache).
@@ -2,11 +2,12 @@
2
2
  name: 22-tool-with-ui-html-template
3
3
  level: intermediate
4
4
  description: "Tool with an inline HTML function template — `ui: { template: (ctx) => '<div>…</div>' }` — for a quick widget that doesn't need a separate `.tsx` file."
5
- tags: [ui, ui-widgets, html-template, escapeHtml, TemplateContext]
5
+ tags: [ui, ui-widgets, html-template, html-tag, escapeStringResults, TemplateContext]
6
6
  features:
7
- - 'Adding a `ui:` block with a function template `(ctx: TemplateContext<In, Out>) => string`'
7
+ - 'Adding a `ui:` block with a function template that returns markup built with the `ctx.helpers.html` tagged template'
8
8
  - 'Annotating `ctx` explicitly to dodge the TS7006 inference gap on the union `ui.template` type'
9
- - "Always escaping user-controlled output with `ctx.helpers.escapeHtml(...)` so the widget can't XSS itself"
9
+ - "Letting `ctx.helpers.html` escape every interpolated value so tool output can't inject markup into the widget"
10
+ - 'Opting in to `escapeStringResults: true` so a plain string result is escaped — the default from FrontMCP 1.9'
10
11
  - 'Reading from `ctx.output` and `ctx.helpers` — the typed runtime context the template renderer hands you'
11
12
  ---
12
13
 
@@ -14,7 +15,7 @@ features:
14
15
 
15
16
  Tool with an inline HTML function template — `ui: { template: (ctx) => '<div>…</div>' }` — for a quick widget that doesn't need a separate `.tsx` file.
16
17
 
17
- For widgets that don't need React / state / interactivity, an inline function template is the simplest form. Read `ctx.output`, escape user-controlled fields, return an HTML string.
18
+ For widgets that don't need React / state / interactivity, an inline function template is the simplest form. Read `ctx.output` and return markup built with the `ctx.helpers.html` tagged template — it escapes every value you interpolate.
18
19
 
19
20
  ## Code
20
21
 
@@ -38,13 +39,16 @@ type Out = { city: string; temperatureF: number; conditions: string };
38
39
  outputSchema,
39
40
  ui: {
40
41
  widgetDescription: 'Current weather card',
41
- template: (ctx: TemplateContext<In, Out>) => `
42
+ // `html` escapes interpolated values — no manual escapeHtml needed
43
+ template: (ctx: TemplateContext<In, Out>) => ctx.helpers.html`
42
44
  <div style="padding:16px;font-family:system-ui;border-radius:12px;background:#f5f7fa">
43
- <h2 style="margin:0 0 8px">${ctx.helpers.escapeHtml(ctx.output.city)}</h2>
45
+ <h2 style="margin:0 0 8px">${ctx.output.city}</h2>
44
46
  <p style="font-size:48px;margin:0">${ctx.output.temperatureF}°F</p>
45
- <p style="margin:8px 0 0">${ctx.helpers.escapeHtml(ctx.output.conditions)}</p>
47
+ <p style="margin:8px 0 0">${ctx.output.conditions}</p>
46
48
  </div>
47
49
  `,
50
+ // Escape any plain string result; `html` results stay markup (the default from FrontMCP 1.9)
51
+ escapeStringResults: true,
48
52
  },
49
53
  })
50
54
  export class ShowWeatherCardTool extends ToolContext {
@@ -56,11 +60,16 @@ export class ShowWeatherCardTool extends ToolContext {
56
60
 
57
61
  ## What This Demonstrates
58
62
 
59
- - Adding a `ui:` block with a function template `(ctx: TemplateContext<In, Out>) => string`
63
+ - Adding a `ui:` block with a function template that returns markup built with the `ctx.helpers.html` tagged template
60
64
  - Annotating `ctx` explicitly to dodge the TS7006 inference gap on the union `ui.template` type
61
- - Always escaping user-controlled output with `ctx.helpers.escapeHtml(...)` so the widget can't XSS itself
65
+ - Letting `ctx.helpers.html` escape every interpolated value so tool output can't inject markup into the widget
66
+ - Opting in to `escapeStringResults: true` so a plain string result is escaped — the default from FrontMCP 1.9
62
67
  - Reading from `ctx.output` and `ctx.helpers` — the typed runtime context the template renderer hands you
63
68
 
69
+ ## Why `html` instead of a plain template literal
70
+
71
+ A plain string a template returns is rendered as markup when it looks like HTML, so `` `<p>${ctx.output.note}</p>` `` lets tool output inject tags unless you remember `escapeHtml` on every field. `ctx.helpers.html` escapes each interpolated value for you (nested `html` values and `ctx.helpers.trustedHtml(markup)` pass through as markup), and its result renders as markup whether or not `escapeStringResults` is on. Don't also call `escapeHtml` inside `html` — the value would be escaped twice. See [`ui-widgets.md`](../references/ui-widgets.md#trusted-markup-and-escaping-template-results).
72
+
64
73
  ## Why annotate `ctx` explicitly
65
74
 
66
75
  `ui.template` is a union of multiple callable shapes (`TemplateBuilderFn | string | ((props: any) => any) | FileSource`). TypeScript can't pick a single contextual type for the arrow's parameter, so `template: (ctx) => …` fails under `strict` / `noImplicitAny` with TS7006:
@@ -78,16 +87,18 @@ Two ways out:
78
87
 
79
88
  - Tiny widget — a card, a table row, a status badge
80
89
  - No state / interactivity
81
- - No external CSS / fonts / scripts beyond what `escapeHtml` can produce
90
+ - No external CSS / fonts / scripts beyond static markup
82
91
 
83
92
  Move to a `.tsx` FileSource widget the moment you reach for React, useState, event handlers, or anything beyond static markup.
84
93
 
85
94
  ## What `ctx.helpers` includes
86
95
 
87
- | Helper | Purpose |
88
- | ------------------------------ | ----------------------------------------------------------- |
89
- | `escapeHtml(str)` | Escape HTML entities; returns `''` for null/undefined |
90
- | `formatDate(date, format?)` | Locale-formatted date |
91
- | `formatCurrency(amount, ccy?)` | ISO-4217 currency formatting |
92
- | `uniqueId(prefix?)` | Deterministic unique ID for DOM elements |
93
- | `jsonEmbed(data)` | Safely embed JSON in a `<script>` tag (escapes `</script>`) |
96
+ | Helper | Purpose |
97
+ | ------------------------------ | ------------------------------------------------------------- |
98
+ | `` html`…` `` | Build trusted markup; interpolated values are escaped |
99
+ | `trustedHtml(markup)` | Mark markup you produced or sanitized yourself as trusted |
100
+ | `escapeHtml(str)` | Escape HTML entities; returns `''` for null/undefined |
101
+ | `formatDate(date, format?)` | Locale-formatted date |
102
+ | `formatCurrency(amount, ccy?)` | ISO-4217 currency formatting |
103
+ | `uniqueId(prefix?)` | Deterministic unique ID for DOM elements |
104
+ | `jsonEmbed(data)` | Safely embed JSON in a `<script>` tag (escapes `<`, `>`, `&`) |
@@ -6,7 +6,7 @@ tags: [ui, csp, widgetAccessible, 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
  - 'Opting the widget into cross-tool calls with `widgetAccessible: true` and using `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs'
9
- - "Embedding initial data into the widget's inline `<script>` safely via `ctx.helpers.jsonEmbed(...)` (escapes `</script>`)"
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
  ---
12
12
 
@@ -67,12 +67,14 @@ type Out = { symbol: string; priceUsd: number; asOf: string };
67
67
  connectDomains: ['https://api.market.example'],
68
68
  },
69
69
  template: (ctx: TemplateContext<In, Out>) => {
70
- const initial = ctx.helpers.jsonEmbed(ctx.output);
71
- return `
70
+ const { html, trustedHtml, jsonEmbed } = ctx.helpers;
71
+ // jsonEmbed output is script-safe; trustedHtml keeps `html` from HTML-escaping its quotes
72
+ const initial = trustedHtml(jsonEmbed(ctx.output));
73
+ return html`
72
74
  <div style="padding:16px;font-family:system-ui">
73
- <h2 style="margin:0">${ctx.helpers.escapeHtml(ctx.output.symbol)}</h2>
75
+ <h2 style="margin:0">${ctx.output.symbol}</h2>
74
76
  <p id="price" style="font-size:36px;margin:8px 0">$${ctx.output.priceUsd.toFixed(2)}</p>
75
- <p id="asof" style="color:#666;margin:0">As of ${ctx.helpers.escapeHtml(ctx.output.asOf)}</p>
77
+ <p id="asof" style="color:#666;margin:0">As of ${ctx.output.asOf}</p>
76
78
  <button id="refresh" style="margin-top:12px;padding:8px 16px">Refresh</button>
77
79
  <script>
78
80
  (function () {
@@ -116,7 +118,7 @@ export class ShowQuoteTool extends ToolContext {
116
118
 
117
119
  - Restricting the widget's outbound `fetch` via `ui.csp.connectDomains` (emitted on the resource per #455)
118
120
  - Opting the widget into cross-tool calls with `widgetAccessible: true` and using `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs
119
- - Embedding initial data into the widget's inline `<script>` safely via `ctx.helpers.jsonEmbed(...)` (escapes `</script>`)
121
+ - Building the markup with `ctx.helpers.html` and embedding initial data into the inline `<script>` via `trustedHtml(jsonEmbed(...))` (`jsonEmbed` escapes `<`, `>` and `&`)
120
122
  - Surfacing in-flight status via `invocationStatus.invoking` / `invoked` so the host UI shows feedback
121
123
 
122
124
  ## Why these choices
@@ -124,4 +126,5 @@ export class ShowQuoteTool extends ToolContext {
124
126
  - **`widgetAccessible: true`** — required for `window.FrontMcpBridge.callTool`. Without it, the bridge is read-only (the widget can read `getToolInput` / `getToolOutput` but can't invoke tools).
125
127
  - **`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.
126
128
  - **`window.FrontMcpBridge.callTool` not `window.openai.callTool`** — the bridge handles host detection. `window.openai.*` works on OpenAI Apps SDK but breaks everywhere else.
127
- - **`jsonEmbed` not `JSON.stringify`** — `JSON.stringify` doesn't escape `</script>` and can break out of the inline script tag. `jsonEmbed` does.
129
+ - **`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`.
130
+ - **`html` with `trustedHtml(jsonEmbed(...))`** — `ctx.helpers.html` escapes the symbol and timestamp for you, so tool output can't inject markup. Inside the `<script>` the JSON must stay as-is: `html` would HTML-escape its quotes, so the script-safe `jsonEmbed` output is wrapped with `trustedHtml`. The result renders the same with `escapeStringResults: true`, which FrontMCP 1.9 makes the default.
@@ -10,6 +10,7 @@ Full surface of the `@Tool` decorator. Mandatory fields are bolded.
10
10
  | Field | Type | Default | When to set |
11
11
  | ----------------- | ------------------------------------------------------------------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
12
12
  | **`name`** | `string` (snake_case) | — | Always. MCP protocol convention. `get_weather`, not `getWeather`. |
13
+ | `title` | `string` | — | When the tool needs a display name in `tools/list`. Without it, `annotations.title` is the only title. |
13
14
  | `description` | `string` | — | Almost always. Shows in `tools/list` and helps clients choose the right tool. |
14
15
  | **`inputSchema`** | Zod raw shape | — | Always. `{ field: z.string() }` — never wrapped in `z.object`. See [`input-schema.md`](./input-schema.md). |
15
16
  | `outputSchema` | Zod raw shape / Zod schema / primitive literal / media literal / array | — | **Always** — prevents data leaks and enables CodeCall chaining. See [`output-schema.md`](./output-schema.md). |
@@ -121,6 +121,15 @@ if (result.status === 'decline') {
121
121
  }
122
122
  ```
123
123
 
124
+ ## Under MCP 2026-07-28
125
+
126
+ There are no server-to-client requests in this revision. `this.elicit()` answers the first call with `resultType: 'input_required'` and a `requestState`; the client retries with `inputResponses`, and `execute()` runs again **from the top** with each asked `elicit()` returning its answer. Keep side effects after the last `elicit()`, or make them idempotent.
127
+
128
+ - An accepted answer is validated against the schema; a mismatch (or an unknown `action`) fails the call with `INVALID_INPUT`.
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
+ - `sendElicitationResult` is not listed to 2026-07-28 clients.
131
+ - Anonymous callers keep their `requestState` across rounds (it binds to one shared anonymous principal).
132
+
124
133
  ## See also
125
134
 
126
135
  - [`19-tool-with-elicitation`](../examples/19-tool-with-elicitation.md)
@@ -45,7 +45,9 @@ async execute(input: { id: string }) {
45
45
 
46
46
  ## Infrastructure errors → propagate
47
47
 
48
- For errors the framework should handle uniformly (network failure, DB unavailable, timeout), just let them throw. The framework wraps them in an `InternalMcpError` with the message redacted before reaching the client, and logs the original for ops.
48
+ For errors the framework should handle uniformly (network failure, DB unavailable, timeout), just let them throw. The framework wraps them in an `InternalMcpError` with the message redacted before reaching the client, and logs it once, with the same `errorId` the client sees. The logged message includes the original error's message and stack in every environment; the wrapper's own stack is logged in development only. A `PublicMcpError` (or subclass) thrown from `execute()` is not wrapped: tools, resources and prompts pass it on with its own message and code.
49
+
50
+ Code that catches errors around entries, such as a hook, can apply the same rules with two predicates from `@frontmcp/sdk`. `isClientFacingError(error)` is true for a public error or an authorities refusal, which pass through unwrapped. `isMrtrSignal(error)` is true for an `InputRequiredSignal` or a `MissingClientCapabilityError`, which the 2026-07-28 dispatcher answers itself, so rethrow them.
49
51
 
50
52
  ## MCP error classes
51
53
 
@@ -103,12 +105,14 @@ The `data` payload lets you surface structured info to the client (rate-limit re
103
105
 
104
106
  ## `PublicMcpError` vs raw `Error`
105
107
 
106
- | Throw | Client sees |
107
- | -------------------------------------- | ----------------------------------------------------------------------- |
108
- | `new PublicMcpError('Quota exceeded')` | `{ code: -32603, message: 'Quota exceeded' }` |
109
- | `new Error('Quota exceeded')` | `{ code: -32603, message: 'Internal error' }` (the message is REDACTED) |
108
+ | Throw | `resources/read` / `prompts/get` error |
109
+ | -------------------------------------- | ------------------------------------------------------------------------- |
110
+ | `new PublicMcpError('Quota exceeded')` | `{ code: -32602, message: 'Quota exceeded' }` |
111
+ | `new Error('Quota exceeded')` | `{ code: -32603, message: 'Internal FrontMCP error. … error ID: err_…' }` |
112
+
113
+ `tools/call` answers with an `isError: true` result instead, carrying the same message plus `_meta.code` and `_meta.errorId`. The error's `data` always carries the `errorId`. A public error's JSON-RPC code comes from its `toJsonRpcError()`, else its `statusCode`: 401 → -32001, 403 → -32003, other 4xx → -32602.
110
114
 
111
- Raw `Error`s have their messages **redacted** before reaching the client — the framework treats them as potentially-sensitive infrastructure errors. For anything the client should read, use `PublicMcpError` or a subclass.
115
+ In production, raw `Error`s have their messages **redacted** before reaching the client — the framework treats them as potentially-sensitive infrastructure errors. For anything the client should read, use `PublicMcpError` or a subclass.
112
116
 
113
117
  ## Non-null assertions are forbidden
114
118
 
@@ -12,7 +12,7 @@ description: What ToolContext provides at runtime — this.get, this.fetch, this
12
12
  | Method | Purpose |
13
13
  | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
14
14
  | `execute(input: In): Promise<Out>` | The method you implement |
15
- | `this.get(token)` | Resolve a DI dependency. Throws `DependencyNotFoundError` if not registered. |
15
+ | `this.get(token)` | Resolve a DI dependency. Throws `ProviderNotAvailableError` if not registered. |
16
16
  | `this.tryGet(token)` | Resolve a DI dependency. Returns `undefined` if not registered. |
17
17
  | `this.fail(err)` | Abort execution, trigger the error flow. **Never returns.** Use for business-logic errors. |
18
18
  | `this.respond(value)` | Early-return with a value. Validates against `outputSchema`. **Never returns** (throws `FlowControl.respond`). |
@@ -38,15 +38,16 @@ description: What ToolContext provides at runtime — this.get, this.fetch, this
38
38
 
39
39
  ## `this.context` (FrontMcpContext)
40
40
 
41
- | Property | Type | Description |
42
- | -------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
- | `requestId` | `string` | Unique ID for this request |
44
- | `sessionId` | `string` | Session identifier (for stateful transports) |
45
- | `scopeId` | `string` | Scope identifier (for multi-app servers) |
46
- | `authInfo` | `AuthInfo` | Raw validated access token — `token`, `clientId`, `scopes`, `expiresAt?`, `resource?`, `extra?`. For user identity / JWT claims use `this.auth` (`this.auth.user.sub`, `this.auth.claims['…']`) instead. |
47
- | `traceContext` | `TraceContext` | Distributed-tracing context (propagated to `this.fetch` automatically) |
48
- | `timestamp` | `number` | Request start timestamp |
49
- | `metadata` | `RequestMetadata` | Request headers, client IP, MCP client name/version |
41
+ | Property | Type | Description |
42
+ | ------------------- | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | `requestId` | `string` | Unique ID for this request |
44
+ | `sessionId` | `string` | Session identifier (for stateful transports) |
45
+ | `verifiedSessionId` | `string \| undefined` | `sessionId` when the server verified that session; `undefined` for a stateless request (shared stateless id, or the per-request id of a request without `mcp-session-id`). Key per-session state on it and fall back to the authenticated principal |
46
+ | `scopeId` | `string` | Scope identifier (for multi-app servers) |
47
+ | `authInfo` | `AuthInfo` | Raw validated access token — `token`, `clientId`, `scopes`, `expiresAt?`, `resource?`, `extra?`. For user identity / JWT claims use `this.auth` (`this.auth.user.sub`, `this.auth.claims['…']`) instead. |
48
+ | `traceContext` | `TraceContext` | Distributed-tracing context (propagated to `this.fetch` automatically) |
49
+ | `timestamp` | `number` | Request start timestamp |
50
+ | `metadata` | `RequestMetadata` | Request headers, client IP, MCP client name/version |
50
51
 
51
52
  ## DI: `this.get` vs `this.tryGet`
52
53
 
@@ -57,7 +58,7 @@ interface UserService { findById(id: string): Promise<User | null>; }
57
58
  const USER_SERVICE: Token<UserService> = Symbol('UserService');
58
59
 
59
60
  async execute(input: { userId: string }) {
60
- // Throws DependencyNotFoundError if USER_SERVICE isn't registered in scope
61
+ // Throws ProviderNotAvailableError if USER_SERVICE isn't registered in scope
61
62
  const users = this.get(USER_SERVICE);
62
63
 
63
64
  // Returns undefined if not registered — for optional deps
@@ -79,6 +80,8 @@ Use `this.get` (throws) when the tool genuinely requires the dependency. Use `th
79
80
 
80
81
  `this.fetch` is a thin wrapper around the standard `fetch` that propagates the request's `traceContext` so downstream services can stitch the call into the same trace.
81
82
 
83
+ It does **not** send the caller's MCP access token or the request's `x-frontmcp-*` headers anywhere unless the target origin is allow-listed in `@FrontMcp({ fetch: { forwardCallerTokenTo, forwardCustomHeadersTo } })` (both default to `[]`). The MCP spec forbids passing the client's token to upstream APIs; call third-party services with `credentials: { provider }` instead. The old `autoInjectAuthHeaders` context option, which sent the token everywhere, is gone. The allow-list only controls what the SDK sends. Listing an origin does not make passing the caller's token through compliant. Only list endpoints that belong to this same MCP server and validate the token as issued for it. For every upstream API, use `credentials: { provider }` or a token issued for that API, for example through token exchange. A request that does carry forwarded caller headers is sent with `redirect: 'manual'`, so a 3xx comes back to the tool instead of following to another origin; with an explicit `redirect: 'error'`, the redirect rejects the `fetch` instead.
84
+
82
85
  ```typescript
83
86
  async execute(input: { url: string }) {
84
87
  const response = await this.fetch(input.url);
@@ -100,6 +103,8 @@ this.fetch(url, {
100
103
  });
101
104
  ```
102
105
 
106
+ The configured `requestTimeout` (30s by default) applies when you pass no `signal` in the options, and so does the `Request`'s own signal; a `signal` passed in the options replaces both. `this.fetch()` is also aborted with `this.signal`, so a cancelled or timed-out call stops its requests.
107
+
103
108
  > Don't `try/catch` around the fetch and swallow errors — let infrastructure errors propagate to the framework. Only use `this.fail` for **business-logic** errors. See [`error-handling.md`](./error-handling.md).
104
109
 
105
110
  ## Notifications: `this.notify` + `this.progress`
@@ -119,7 +124,7 @@ async execute(input: { items: string[] }) {
119
124
  }
120
125
  ```
121
126
 
122
- - `this.notify(msg, level?)` — sends `notifications/message` to the client (`debug` / `info` / `warning` / `error`). Always-best-effort.
127
+ - `this.notify(msg, level?)` — sends `notifications/message` to the client at any of the eight MCP levels (`debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`), only once the client has asked for that level or a lower one (`logging/setLevel`, or `_meta["io.modelcontextprotocol/logLevel"]` under 2026-07-28). Always-best-effort: returns `false` when nothing was sent.
123
128
  - `this.progress(n, total?, msg?)` — sends `notifications/progress` IF the request had a progress token. Returns `false` when no token was provided (so the call costs almost nothing if nobody's listening).
124
129
  - `this.mark(stage)` — server-side breadcrumb, surfaced in logs / metrics / traces. No client notification.
125
130
  - `this.notifyResourceUpdated(uri)` — sends `notifications/resources/updated` to every session subscribed to `uri` (via `resources/subscribe`); no-op for non-subscribers. Call it when a tool mutates state that backs a `@Resource` so subscribers re-fetch.
@@ -104,7 +104,7 @@ const ShowCard = tool({
104
104
  inputSchema: { text: z.string() },
105
105
  outputSchema: { text: z.string() },
106
106
  ui: {
107
- template: (ctx) => `<div>${ctx.helpers.escapeHtml(ctx.output.text)}</div>`,
107
+ template: (ctx) => ctx.helpers.html`<div>${ctx.output.text}</div>`,
108
108
  },
109
109
  })((input) => ({ text: input.text }));
110
110
  ```
@@ -77,6 +77,8 @@ inputSchema: {
77
77
  }
78
78
  ```
79
79
 
80
+ `tools/list` describes what the caller sends: a field with `.default()` is not listed as required, and a field with `.transform()` is listed by its input type.
81
+
80
82
  ## Refinements
81
83
 
82
84
  For cross-field validation, wrap individual fields:
@@ -5,7 +5,7 @@ description: Define the tool's output contract — Zod shape, primitives, media,
5
5
 
6
6
  # `outputSchema` reference
7
7
 
8
- `outputSchema` is **always required** ([rule](../rules/always-define-output-schema.md)). It declares what `execute()` returns and gives the framework permission to strip any fields you didn't declare — the safety net against accidental PII / token / debug-trace leaks.
8
+ `outputSchema` is **always required** ([rule](../rules/always-define-output-schema.md)). It declares what `execute()` returns and gives the framework permission to strip any fields you didn't declare, which guards against accidental PII / token / debug-trace leaks in the result. The exception is a `_meta` object on the returned value: it is copied into the response's `_meta` without validation, so never put secrets there. A result that does not match fails the call with `_meta.code: 'INVALID_OUTPUT'`, and nothing from the rejected result is sent. A number field holding `Infinity`, `-Infinity`, or `NaN` fails the same way, unless the server sets `output: { allowNonFinite: true }`. JSON cannot encode those values, so with that setting the client receives `null` in their place.
9
9
 
10
10
  ## Supported shapes
11
11
 
@@ -17,6 +17,11 @@ description: Define the tool's output contract — Zod shape, primitives, media,
17
17
  | **Media literal** | Binary / link content | `'image'`, `'audio'`, `'resource'`, `'resource_link'` |
18
18
  | **Array of literals** | Multi-content response | `['string', 'image']` — text + image in one response |
19
19
 
20
+ Two runtime facts that follow from the shape:
21
+
22
+ - Without an `outputSchema`, a plain value is wrapped: `return 'hello'` sends `{"value":"hello"}` as text and `structuredContent: { value: 'hello' }` (numbers likewise, `{ value: 8 }`). Declare `outputSchema: 'string'` (or `'number'`) to send the bare value as text.
23
+ - `execute()` must return something. Returning `undefined` fails the call with `_meta.code: 'FLOW_EXITED_WITHOUT_OUTPUT'`; return an explicit result such as `{ ok: true }` from side-effect-only tools.
24
+
20
25
  ## Zod raw shape (most common)
21
26
 
22
27
  ```typescript
@@ -55,7 +55,7 @@ Hard deadline on a single execution.
55
55
  ```
56
56
 
57
57
  - **Scope**: per call. Wraps the entire `execute()` invocation.
58
- - **Behavior on timeout**: the framework throws an `ExecutionTimeoutError` (from `@frontmcp/guard`, code `'EXECUTION_TIMEOUT'`, HTTP status 408) and aborts the wrapped execution. The abort is internal to the timeout guard — it is **not** surfaced into `execute()` as a readable signal, so don't expect to observe it from inside the tool body.
58
+ - **Behavior on timeout**: the framework throws an `ExecutionTimeoutError` (from `@frontmcp/guard`, code `'EXECUTION_TIMEOUT'`, HTTP status 408) and aborts `this.signal`, so `execute()` can observe the abort and pass the signal to cancellable work (see [Timeout and abort signals](#timeout-and-abort-signals)).
59
59
  - **Default**: no timeout. Tools can hang forever unless `timeout` is set.
60
60
 
61
61
  ## Interaction
@@ -88,19 +88,18 @@ Order of effects per call:
88
88
 
89
89
  ## Timeout and abort signals
90
90
 
91
- `timeout` does **not** hand your `execute()` an abort signal — the abort lives inside the timeout guard and is not exposed on the context. `FrontMcpContext` has no `abortSignal` property, so don't reach for `this.context.abortSignal`.
91
+ `this.signal` is aborted when the call is cancelled (including `tasks/cancel` for a task-augmented call) and when its `timeout` passes. `FrontMcpContext` has no `abortSignal` property, so don't reach for `this.context.abortSignal`.
92
92
 
93
- The only tool-level abort signal is `this.signal`, and it is populated **only** for task-augmented `tools/call` invocations (cancelled via `tasks/cancel`) — not by `timeout`. It is `undefined` for ordinary calls, so guard for that:
93
+ `this.fetch()` is aborted with `this.signal` on its own, on top of its per-request timeout (default 30s). Pass `this.signal` to any other cancellable work, so a timed-out call stops instead of running on:
94
94
 
95
95
  ```typescript
96
- async execute(input: { url: string }) {
97
- // this.signal is defined only for task-augmented calls; undefined otherwise
98
- const response = await this.fetch(input.url, { signal: this.signal });
99
- return response.json();
96
+ async execute(input: { reportId: string }) {
97
+ const response = await this.fetch(`https://reports.example.com/${input.reportId}`);
98
+ return this.get(ReportRenderer).render(await response.json(), { signal: this.signal });
100
99
  }
101
100
  ```
102
101
 
103
- For ordinary calls, rely on `this.fetch`'s own per-request timeout (default 30s) to bound in-flight HTTP work; `timeout` then caps the overall `execute()` duration.
102
+ The deadline answers the client, but it cannot stop code that ignores the signal. Until `execute()` returns, the call keeps its concurrency slot.
104
103
 
105
104
  ## See also
106
105
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: ui-widgets
3
- description: @Tool({ ui }) — template formats, servingMode, host-detect resourceMode, CSP, widgetAccessible, MCP Apps spec.
3
+ description: @Tool({ ui }) — template formats, trusted markup (html / escapeStringResults), servingMode, host-detect resourceMode, CSP, widgetAccessible, MCP Apps spec.
4
4
  ---
5
5
 
6
6
  # Tool UI widgets
@@ -33,7 +33,7 @@ class GetWeatherTool extends ToolContext {
33
33
 
34
34
  That's it. The framework:
35
35
 
36
- - Pre-compiles the widget at startup, registers it at `ui://widget/get_weather.html`.
36
+ - Advertises the widget at `ui://widget/get_weather.html` in `tools/list` and renders it per call into that call's `_meta['ui/html']` (default `servingMode: 'auto'` → `inline`). With `servingMode: 'static'` it pre-compiles the widget at startup and serves it from `resources/read`.
37
37
  - Auto-detects the connecting client — `resourceMode: 'inline'` for Claude (React bundled in), `'cdn'` for OpenAI / ChatGPT / Cursor (esm.sh import map, smaller payload).
38
38
  - Emits `ui.csp` (if set) on the resource's `_meta.ui.csp` — Claude actually honors it (it ignores CSP declared on the tool).
39
39
 
@@ -42,7 +42,7 @@ That's it. The framework:
42
42
  | Format | Shape | When |
43
43
  | ---------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
44
44
  | **FileSource (recommended)** | `{ file: widgetPath }` | `.tsx` / `.jsx` / `.html` source files. Anchor with `import.meta.url`. |
45
- | **Function** | `(ctx) => string` | Quick demo / one-liner HTML. Annotate `ctx: TemplateContext<In, Out>` ([why](#typescript-gotcha-ts7006)). |
45
+ | **Function** | `` (ctx) => ctx.helpers.html`…` `` | Quick demo / one-liner HTML. Annotate `ctx: TemplateContext<In, Out>` ([why](#typescript-gotcha-ts7006)). |
46
46
  | **HTML / MDX string** | `'<div>…</div>'` or `'# Title\n<Card />'` | Static markup; pair with `mdxComponents` for MDX. |
47
47
  | **React component** | `MyWidget` | SSR React. Set `hydrate: false` (default) for Claude/ChatGPT. |
48
48
 
@@ -56,8 +56,7 @@ Inline `template: (ctx) => …` under `strict` fails with `Parameter 'ctx' impli
56
56
  import { type TemplateContext } from '@frontmcp/sdk';
57
57
 
58
58
  ui: {
59
- template: (ctx: TemplateContext<MyInput, MyOutput>) =>
60
- `<div>${ctx.helpers.escapeHtml(ctx.output.label)}</div>`,
59
+ template: (ctx: TemplateContext<MyInput, MyOutput>) => ctx.helpers.html`<div>${ctx.output.label}</div>`,
61
60
  }
62
61
  ```
63
62
 
@@ -77,6 +76,7 @@ Or use the FileSource form — it sidesteps the issue.
77
76
  | `autoResize` | `true` | Auto-report content height to the host via a debounced `ResizeObserver` on `#root`. Set `false` to opt out (CSS still applies). |
78
77
  | `csp` | — | `{ connectDomains?, resourceDomains? }` — emitted on the resource content's `_meta.ui.csp` (#455). Claude honors CSP only here. |
79
78
  | `contentSecurity` | strict | `{ allowUnsafeLinks?, allowInlineScripts?, bypassSanitization? }` — keep defaults. |
79
+ | `escapeStringResults` | unset | `true` escapes plain string results of a template function; `html` / `trustedHtml` stay markup. Default in 1.9. |
80
80
  | `widgetAccessible` | `false` | `true` exposes `window.FrontMcpBridge.callTool` in the widget. |
81
81
  | `resourceUri` | auto | Override the `ui://widget/{toolName}.html` URI. |
82
82
  | `uiType` | `'auto'` | Force `'html'` / `'react'` / `'mdx'` / `'markdown'`. |
@@ -85,6 +85,44 @@ Or use the FileSource form — it sidesteps the issue.
85
85
  | `externals`, `dependencies` | — | CDN externals for FileSource widgets. |
86
86
  | `customShell`, `invocationStatus`, `widgetCapabilities`, `prefersBorder`, `sandboxDomain`, `htmlResponsePrefix` | — | Platform-specific knobs. |
87
87
 
88
+ ## Widget resources and per-call renders
89
+
90
+ - `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
+ - 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).
92
+ - 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.
93
+ - 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
+
95
+ ## Trusted markup and escaping template results
96
+
97
+ 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:
98
+
99
+ ```typescript
100
+ template: (ctx: TemplateContext<In, Out>) => {
101
+ const { html } = ctx.helpers;
102
+ return html`
103
+ <h2>${ctx.output.title}</h2>
104
+ <ul>${ctx.output.items.map((item) => html`<li>${item.name}</li>`)}</ul>
105
+ `;
106
+ },
107
+ ```
108
+
109
+ - Nested `html` values are never escaped twice; arrays are joined without a separator; `null` / `undefined` / `false` render nothing. Don't pre-escape with `escapeHtml` inside `html` (double escaping). Quote attribute values; escaping doesn't validate `href` / `src` URLs.
110
+ - `ctx.helpers.trustedHtml(markup)` marks markup you produced or sanitized yourself as trusted. Never wrap raw tool output or user input.
111
+ - In an inline `<script>`, embed data with `${trustedHtml(jsonEmbed(data))}` — `jsonEmbed` writes `<`, `>`, `&`, U+2028 and U+2029 as `\uXXXX`, so it is safe in a script, and `html` would otherwise HTML-escape its quotes.
112
+ - `html`, `trustedHtml`, `isTrustedHtml` and `TrustedHtml` are also exported from `@frontmcp/uipack`; `TrustedHtml` is re-exported from `@frontmcp/sdk`.
113
+
114
+ A **plain string** a template function returns is rendered as markup when it looks like HTML — `template: (ctx) => ctx.output` renders any tags in the output. `escapeStringResults` opts in to escaping it:
115
+
116
+ | `escapeStringResults` | Plain string result | `html` / `trustedHtml` result |
117
+ | --------------------- | --------------------------------------------------- | ----------------------------- |
118
+ | unset (1.8 default) | Rendered as markup; one-time notice logged per tool | Markup |
119
+ | `true` | Escaped, shown as text | Markup |
120
+ | `false` | Rendered as markup, no notice | Markup |
121
+
122
+ Set it per tool (`ui: { escapeStringResults: true }`) or server-wide (`@FrontMcp({ ui: { escapeStringResults: true } })`; the tool setting wins). **FrontMCP 1.9 escapes plain string results by default** — return `html` / `trustedHtml` from every template function and set `escapeStringResults: true` to migrate now.
123
+
124
+ Everything else is always escaped: plain text as text, objects as JSON inside `<pre>`, chart configs and base64 PDFs (`JVBERi…`) as script data. A value that starts with `JVBERi` but isn't base64 is shown as text. A static string template (`template: '<div>…</div>'`) is author markup and is never escaped.
125
+
88
126
  ## Path resolution gotcha (#444)
89
127
 
90
128
  Bare `template: { file: './widget.tsx' }` resolves against `process.cwd()`, **not** the tool file. Always anchor:
@@ -119,7 +157,7 @@ When the widget needs to read tool data or invoke other tools, the bridge IIFE i
119
157
 
120
158
  ```typescript
121
159
  ui: {
122
- template: (ctx) => `
160
+ template: (ctx) => ctx.helpers.html`
123
161
  <button id="refresh">Refresh</button>
124
162
  <script>
125
163
  document.getElementById('refresh').onclick = async () => {
@@ -215,6 +215,7 @@ other policy fields via `operator` (default AND).
215
215
  import type { AuthorityGuardFn } from '@frontmcp/auth';
216
216
 
217
217
  const requireActiveSubscription: AuthorityGuardFn = async (ctx) => {
218
+ if (ctx.user.sub === undefined) return 'sign-in required';
218
219
  const active = await db.isSubscriptionActive(ctx.user.sub);
219
220
  return active ? true : 'subscription is not active';
220
221
  };
@@ -22,7 +22,7 @@ interface AuthoritiesEvaluator {
22
22
  }
23
23
  ```
24
24
 
25
- The `policy` parameter is whatever value is passed under the evaluator's key in the `custom` field. The `ctx` parameter provides the full evaluation context including user info, input, environment, and the relationship resolver.
25
+ The `policy` parameter is whatever value is passed under the evaluator's key in the `custom` field. The `ctx` parameter provides the full evaluation context including user info, input, environment, and the relationship resolver. `ctx.user.sub` is `undefined` for anonymous callers, so an evaluator that keys on the caller should decide explicitly what an anonymous caller gets.
26
26
 
27
27
  The return value must be an `AuthoritiesResult`:
28
28
 
@@ -109,6 +109,9 @@ const tenantAllowlistGuard: AuthoritiesEvaluator = {
109
109
  const activeSubscriptionGuard: AuthoritiesEvaluator = {
110
110
  name: 'activeSubscription',
111
111
  evaluate: async (_policy, ctx) => {
112
+ if (ctx.user.sub === undefined) {
113
+ return { granted: false, deniedBy: 'sign-in required', evaluatedPolicies: ['custom.activeSubscription'] };
114
+ }
112
115
  const row = await db.query('SELECT active FROM subscriptions WHERE user_id = $1', [ctx.user.sub]);
113
116
  const active = row?.active === true;
114
117
  return {
@@ -279,6 +282,13 @@ export const featureFlagEvaluator: AuthoritiesEvaluator = {
279
282
  name: 'featureFlag',
280
283
  async evaluate(policy: unknown, ctx: AuthoritiesEvaluationContext): Promise<AuthoritiesResult> {
281
284
  const { flag, inverse } = policy as FeatureFlagPolicy;
285
+ if (ctx.user.sub === undefined) {
286
+ return {
287
+ granted: false,
288
+ deniedBy: 'custom.featureFlag: sign-in required',
289
+ evaluatedPolicies: ['custom.featureFlag'],
290
+ };
291
+ }
282
292
  const enabled = await featureFlags.isEnabled(flag, ctx.user.sub);
283
293
  const granted = inverse ? !enabled : enabled;
284
294
 
@@ -399,7 +409,8 @@ export const rateLimitEvaluator: AuthoritiesEvaluator = {
399
409
  name: 'rateLimit',
400
410
  async evaluate(policy: unknown, ctx: AuthoritiesEvaluationContext): Promise<AuthoritiesResult> {
401
411
  const { max, windowSeconds } = policy as RateLimitPolicy;
402
- const key = `${ctx.user.sub}`;
412
+ // Anonymous callers share one bucket
413
+ const key = ctx.user.sub ?? 'anonymous';
403
414
  const now = Date.now();
404
415
 
405
416
  let entry = counters.get(key);
@@ -161,6 +161,8 @@ interface AbacCondition {
161
161
  | `exists` | Value exists (not null/undefined) | `{ path: 'user.sub', op: 'exists', value: true }` |
162
162
  | `matches` | Regex match | `{ path: 'claims.email', op: 'matches', value: '^.*@(acme\|corp)\\.com$' }` |
163
163
 
164
+ An anonymous caller (public mode, `allowAnonymous`, or an `anon:` session) has no `user.sub`, even when `claimsMapping.userId` points at another claim, and a subject that is not a string counts as anonymous too, so `{ path: 'user.sub', op: 'exists', value: true }` admits signed-in callers only. A caller whose identity carries no `sub` at all gets the string `claimsMapping.userId` resolves to as its `user.sub`, when there is one, and so passes `exists`. When `claimsMapping.userId` resolves to something other than a string, a signed-in caller keeps its own `sub`. An expected value that resolves to nothing (for example a missing `fromInput` field) never satisfies `eq` or `match`, even when the actual value is missing too. A denied `tools/call` returns an error result with `_meta.code: 'AUTHORITY_DENIED'`.
165
+
164
166
  ### Dynamic Value References
165
167
 
166
168
  Instead of hardcoding values, reference runtime data from tool input or JWT claims.
@@ -8,7 +8,8 @@ features:
8
8
  - 'Top-level `instructions` on `@FrontMcp` exposes a global system prompt to MCP clients'
9
9
  - "`skillsConfig.injectInstructions: 'append'` adds the skill catalog summary after the user prompt"
10
10
  - 'Dynamic skills are picked up because the composer runs on every initialize request'
11
- - 'Catalog summary is bounded at 16 KB with a truncation footer pointing at skill://catalog'
11
+ - 'The catalog only names skills the initializing caller may see (skill authorities and the `skills:filter` flow, e.g. feature flags)'
12
+ - 'Catalog summary is bounded at 16 KB with a truncation footer pointing at skill://index.json'
12
13
  ---
13
14
 
14
15
  # Inject Instructions on Initialize
@@ -38,8 +39,8 @@ import { MainApp } from './main.app';
38
39
  mcpResources: true,
39
40
  // 'append' (default) — the skill catalog summary is appended after instructions
40
41
  // 'prepend' — summary first, then instructions
41
- // 'replace' — summary only (skills drive the entire system prompt)
42
- // 'off' — instructions sent as-is, no summary
42
+ // 'replace' — instructions only; summary and channel hints dropped (falls back to 'append' if instructions is empty)
43
+ // 'off' — no summary; instructions and channel hints still sent
43
44
  injectInstructions: 'append',
44
45
  },
45
46
  })
@@ -51,7 +52,8 @@ export default class FlightBotServer {}
51
52
  - Top-level `instructions` on `@FrontMcp` exposes a global system prompt to MCP clients
52
53
  - `skillsConfig.injectInstructions: 'append'` adds the skill catalog summary after the user prompt
53
54
  - Dynamic skills are picked up because the composer runs on every initialize request
54
- - Catalog summary is bounded at 16 KB with a truncation footer pointing at skill://catalog
55
+ - The catalog only names skills the initializing caller may see (skill authorities and the `skills:filter` flow, e.g. feature flags)
56
+ - Catalog summary is bounded at 16 KB with a truncation footer pointing at skill://index.json
55
57
 
56
58
  ## Related
57
59
 
@@ -71,6 +71,7 @@ class MyApp {}
71
71
  ```
72
72
 
73
73
  - `provider` -- the authorization server URL. FrontMCP fetches JWKS from `{provider}/.well-known/jwks.json`.
74
+ - JWKS, discovery and CIMD client-id fetches are SSRF-guarded: a host that is, or resolves to, a loopback, private, link-local or cloud-metadata address is refused in any IPv4 or IPv6 spelling (IPv4-mapped, -translated, -compatible, NAT64 and 6to4 forms included; the local-use NAT64 prefix `64:ff9b:1::/48` is refused outright), every redirect hop is re-checked, and an opaque (status-0) redirect is refused. A `localhost` IdP is allowed outside production.
74
75
  - `expectedAudience` -- the `aud` claim value that tokens must contain. Supported in **transparent, local, AND remote** modes (it lives on the shared/orchestrated auth options), so set it wherever FrontMCP verifies bearer tokens — not transparent-only.
75
76
  - `allowAnonymous` (default `false`) -- when `true`, requests without a token get an anonymous session (scoped by `anonymousScopes`, default `['anonymous']`) instead of a 401.
76
77