@frontmcp/skills 1.8.1 → 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.
- package/catalog/create-tool/SKILL.md +1 -1
- package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +21 -10
- package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +9 -6
- package/catalog/create-tool/references/execution-context.md +10 -9
- package/catalog/create-tool/references/function-style-builder.md +1 -1
- package/catalog/create-tool/references/ui-widgets.md +34 -9
- package/catalog/frontmcp-config/examples/configure-skills-http/inject-instructions.md +2 -0
- package/catalog/frontmcp-config/references/configure-skills-http.md +2 -0
- package/catalog/frontmcp-development/references/create-plugin.md +41 -7
- package/catalog/frontmcp-development/references/official-plugins.md +24 -13
- package/catalog/skills-manifest.json +7 -5
- 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) =>
|
|
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
|
|
@@ -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,
|
|
5
|
+
tags: [ui, ui-widgets, html-template, html-tag, escapeStringResults, TemplateContext]
|
|
6
6
|
features:
|
|
7
|
-
- 'Adding a `ui:` block with a function template
|
|
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
|
-
- "
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
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,7 +87,7 @@ 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
|
|
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
|
|
|
@@ -86,6 +95,8 @@ Move to a `.tsx` FileSource widget the moment you reach for React, useState, eve
|
|
|
86
95
|
|
|
87
96
|
| Helper | Purpose |
|
|
88
97
|
| ------------------------------ | ------------------------------------------------------------- |
|
|
98
|
+
| `` html`…` `` | Build trusted markup; interpolated values are escaped |
|
|
99
|
+
| `trustedHtml(markup)` | Mark markup you produced or sanitized yourself as trusted |
|
|
89
100
|
| `escapeHtml(str)` | Escape HTML entities; returns `''` for null/undefined |
|
|
90
101
|
| `formatDate(date, format?)` | Locale-formatted date |
|
|
91
102
|
| `formatCurrency(amount, ccy?)` | ISO-4217 currency formatting |
|
|
@@ -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
|
-
-
|
|
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
|
|
71
|
-
|
|
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.
|
|
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.
|
|
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
|
-
-
|
|
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
|
|
@@ -125,3 +127,4 @@ export class ShowQuoteTool extends ToolContext {
|
|
|
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
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.
|
|
@@ -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
|
|
42
|
-
|
|
|
43
|
-
| `requestId`
|
|
44
|
-
| `sessionId`
|
|
45
|
-
| `
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
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
|
|
|
@@ -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.
|
|
107
|
+
template: (ctx) => ctx.helpers.html`<div>${ctx.output.text}</div>`,
|
|
108
108
|
},
|
|
109
109
|
})((input) => ({ text: input.text }));
|
|
110
110
|
```
|
|
@@ -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
|
|
@@ -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** |
|
|
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'`. |
|
|
@@ -92,11 +92,36 @@ Or use the FileSource form — it sidesteps the issue.
|
|
|
92
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
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
94
|
|
|
95
|
-
##
|
|
95
|
+
## Trusted markup and escaping template results
|
|
96
96
|
|
|
97
|
-
-
|
|
98
|
-
|
|
99
|
-
|
|
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.
|
|
100
125
|
|
|
101
126
|
## Path resolution gotcha (#444)
|
|
102
127
|
|
|
@@ -132,7 +157,7 @@ When the widget needs to read tool data or invoke other tools, the bridge IIFE i
|
|
|
132
157
|
|
|
133
158
|
```typescript
|
|
134
159
|
ui: {
|
|
135
|
-
template: (ctx) => `
|
|
160
|
+
template: (ctx) => ctx.helpers.html`
|
|
136
161
|
<button id="refresh">Refresh</button>
|
|
137
162
|
<script>
|
|
138
163
|
document.getElementById('refresh').onclick = async () => {
|
|
@@ -8,6 +8,7 @@ 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
|
+
- 'The catalog only names skills the initializing caller may see (skill authorities and the `skills:filter` flow, e.g. feature flags)'
|
|
11
12
|
- 'Catalog summary is bounded at 16 KB with a truncation footer pointing at skill://index.json'
|
|
12
13
|
---
|
|
13
14
|
|
|
@@ -51,6 +52,7 @@ 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
|
|
55
|
+
- The catalog only names skills the initializing caller may see (skill authorities and the `skills:filter` flow, e.g. feature flags)
|
|
54
56
|
- Catalog summary is bounded at 16 KB with a truncation footer pointing at skill://index.json
|
|
55
57
|
|
|
56
58
|
## Related
|
|
@@ -68,6 +68,8 @@ The catalog summary is built by `composeInitializeInstructions(...)` and `buildS
|
|
|
68
68
|
|
|
69
69
|
> **Dynamic skills:** because the composer recomputes the summary on every `initialize` request, skills registered after server boot **are** picked up automatically.
|
|
70
70
|
|
|
71
|
+
> **Per caller:** the summary (and the SEP-2640 `skill://` hints under `sep2640InInstructions`) is composed for the client that initializes, like `skills/list`: it only names skills whose `authorities` that caller satisfies and that the hookable `skills:filter` flow keeps, so a flag-disabled skill's name and description are left out. With nothing gating a skill the instructions are unchanged. Transports use `composeCallerInstructions(scope, { ctx })`, exported from `@frontmcp/sdk` for custom transports.
|
|
72
|
+
|
|
71
73
|
## Skills HTTP Authentication
|
|
72
74
|
|
|
73
75
|
```typescript
|
|
@@ -307,6 +307,39 @@ Register with `init()`:
|
|
|
307
307
|
class MyServer {}
|
|
308
308
|
```
|
|
309
309
|
|
|
310
|
+
### Option-derived providers are registered before nested plugins
|
|
311
|
+
|
|
312
|
+
`dynamicProviders(options)` and `init({ providers })` are registered **before** the plugin's nested `plugins` are built, for both `init(options)` and `init({ inject, useFactory })` (the factory runs first). A nested plugin can inject them:
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
@Plugin({
|
|
316
|
+
name: 'my-plugin',
|
|
317
|
+
plugins: [
|
|
318
|
+
AuditPlugin.init({
|
|
319
|
+
inject: () => [MyServiceToken],
|
|
320
|
+
useFactory: (service: MyService) => ({ channel: service.auditChannel }),
|
|
321
|
+
}),
|
|
322
|
+
],
|
|
323
|
+
})
|
|
324
|
+
export default class MyPlugin extends DynamicPlugin<MyPluginOptions, MyPluginOptionsInput> {
|
|
325
|
+
/* dynamicProviders() returns MyServiceToken as above */
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The reverse does not work: an option-derived provider cannot inject a provider that a nested plugin exports.
|
|
330
|
+
|
|
331
|
+
### Installing the same plugin in several apps
|
|
332
|
+
|
|
333
|
+
Each app that installs a plugin gets its own copy of the plugin's providers, including CONTEXT-scoped ones. Tools, resources and prompts resolve the nearest definition in their own hierarchy (plugin, then app, then server). So `this.myService` in app A uses A's options even when app B installs `MyPlugin.init()` with different options:
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
@App({ id: 'billing', plugins: [MyPlugin.init({ endpoint: 'https://billing.example.com' })], tools: [RefundTool] })
|
|
337
|
+
class BillingApp {}
|
|
338
|
+
|
|
339
|
+
@App({ id: 'ops', plugins: [MyPlugin.init({ endpoint: 'https://ops.example.com' })], tools: [DeployTool] })
|
|
340
|
+
class OpsApp {}
|
|
341
|
+
```
|
|
342
|
+
|
|
310
343
|
## Step 5: Extend Metadata and Execution Context
|
|
311
344
|
|
|
312
345
|
FrontMCP provides two extension mechanisms for plugins: **metadata augmentation** (add fields to decorators) and **context extensions** (add properties to `this` in tools/resources/prompts).
|
|
@@ -458,13 +491,14 @@ plugins/
|
|
|
458
491
|
|
|
459
492
|
## Common Patterns
|
|
460
493
|
|
|
461
|
-
| Pattern | Correct
|
|
462
|
-
| ------------------------------ |
|
|
463
|
-
| Context extension registration | `contextExtensions: [{ property: 'auditLog', token: AuditLoggerToken }]` in metadata
|
|
464
|
-
| Type augmentation | `declare module '@frontmcp/sdk' { interface ExecutionContextBase { ... } }` in a separate file
|
|
465
|
-
| Provider types | `Token<AuditLogger> = Symbol('AuditLogger')` with typed token
|
|
466
|
-
| Plugin scope | `scope: 'app'` (default) for app-scoped behavior
|
|
467
|
-
| Dynamic options | Extend `DynamicPlugin<TOptions, TInput>` with `static dynamicProviders()`
|
|
494
|
+
| Pattern | Correct | Incorrect | Why |
|
|
495
|
+
| ------------------------------ | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
|
496
|
+
| Context extension registration | `contextExtensions: [{ property: 'auditLog', token: AuditLoggerToken }]` in metadata | `Object.defineProperty(ExecutionContextBase.prototype, ...)` manually | SDK handles runtime installation; manual modification causes ordering issues |
|
|
497
|
+
| Type augmentation | `declare module '@frontmcp/sdk' { interface ExecutionContextBase { ... } }` in a separate file | Skipping the augmentation and casting `this` in tools | Without augmentation, TypeScript cannot type-check `this.auditLog` |
|
|
498
|
+
| Provider types | `Token<AuditLogger> = Symbol('AuditLogger')` with typed token | `provide: Symbol('AuditLogger')` without type annotation | Typed tokens enable compile-time DI resolution checking |
|
|
499
|
+
| Plugin scope | `scope: 'app'` (default) for app-scoped behavior | `scope: 'server'` when hooks should only apply to one app | Server scope fires hooks for all apps in a gateway; default to app |
|
|
500
|
+
| Dynamic options | Extend `DynamicPlugin<TOptions, TInput>` with `static dynamicProviders()` | Constructing providers in the constructor body | `dynamicProviders` runs before instantiation, enabling proper DI wiring |
|
|
501
|
+
| Nested plugin needs options | Nested `Plugin.init({ inject: () => [HostToken], useFactory })` injecting a `dynamicProviders` token | Resolving the host's option-derived provider by reading global state | Option-derived providers are registered before nested plugins are built |
|
|
468
502
|
|
|
469
503
|
## Verification Checklist
|
|
470
504
|
|
|
@@ -417,18 +417,23 @@ A refused call throws `ApprovalRequiredError`; the client receives an error resu
|
|
|
417
417
|
Approvals are looked up by the tool's full name, `<owner id>:<tool name>`, so pass that name to
|
|
418
418
|
`this.approval` grant and check methods. The owner is the app that declares the tool, or the
|
|
419
419
|
adapter or plugin that provides it (`my-app:file_write` for a tool declared on app `my-app`,
|
|
420
|
-
`github-api:create_issue` for one its `github-api` adapter provides). Session approvals belong to
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
420
|
+
`github-api:create_issue` for one its `github-api` adapter provides). Session approvals belong to a
|
|
421
|
+
session the server verified (`FrontMcpContext.verifiedSessionId`). A stateless request has none -- it
|
|
422
|
+
carries the shared stateless session id, or sends no `mcp-session-id` and runs under a fresh
|
|
423
|
+
per-request id -- so it is keyed by the authenticated principal (`authInfo.extra.userId`, then
|
|
424
|
+
`authInfo.extra.sub`, then `authInfo.clientId`): a grant made in one stateless request is found by
|
|
425
|
+
the same principal's next request and by no other principal. A stateless call with no principal
|
|
426
|
+
cannot hold a session approval. Releases up to 1.8.1 keyed a request without `mcp-session-id` by
|
|
427
|
+
its per-request id, so the grant was never found again
|
|
428
|
+
([#597](https://github.com/agentfront/frontmcp/issues/597)).
|
|
425
429
|
|
|
426
430
|
Installed on an app, `ApprovalPlugin` gates only that app's tools (including those its adapters
|
|
427
431
|
and plugins provide) against its own store, so two apps can each install it with separate stores.
|
|
428
|
-
Installed on the server, it gates every tool; a tool both gate must pass each store's check.
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
store
|
|
432
|
+
Installed on the server, it gates every tool; a tool both gate must pass each store's check.
|
|
433
|
+
`this.approval` resolves the `ApprovalService` of the nearest `ApprovalPlugin` -- the one the
|
|
434
|
+
tool's own app installed, otherwise the server's -- so with two apps each installing it, a grant
|
|
435
|
+
or check in one app's tool uses that app's store. Releases up to 1.8.1 resolved the store of the
|
|
436
|
+
app registered last ([#600](https://github.com/agentfront/frontmcp/issues/600)).
|
|
432
437
|
|
|
433
438
|
### Using `this.approval` in Tools
|
|
434
439
|
|
|
@@ -496,9 +501,11 @@ response to everyone else.
|
|
|
496
501
|
|
|
497
502
|
The identity is the subject your auth layer puts in `authInfo.extra` (`sub` / `userId`), then
|
|
498
503
|
the client id, then the session. For a request the SDK verified, the client id is the token's
|
|
499
|
-
`sub`, or `anon:<id>` for an anonymous session.
|
|
500
|
-
|
|
501
|
-
|
|
504
|
+
`sub`, or `anon:<id>` for an anonymous session. Only a session the server verified
|
|
505
|
+
(`FrontMcpContext.verifiedSessionId`) is an identity: the session id the stateless HTTP transport
|
|
506
|
+
gives every request, the fresh per-request id of a request without `mcp-session-id`, and an
|
|
507
|
+
`mcp-session-id` the server did not accept are not. A call with no identity at all gets a key of
|
|
508
|
+
its own rather than one shared with every other identity-less caller.
|
|
502
509
|
|
|
503
510
|
Set `keyByIdentity: false` **only** when every caller would get byte-identical output: public
|
|
504
511
|
reference data, a currency table, a static document.
|
|
@@ -753,12 +760,16 @@ The plugin hooks into listing and execution flows for tools, resources, resource
|
|
|
753
760
|
|
|
754
761
|
| Capability | Hidden from | Refused on |
|
|
755
762
|
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
756
|
-
| Tool | `tools/list`
|
|
763
|
+
| Tool | `tools/list`, and the `toolName` completion of `ui://widget/{toolName}.html` | `tools/call` |
|
|
757
764
|
| Resource | `resources/list` | `resources/read`, `completion/complete` |
|
|
758
765
|
| Resource template | `resources/templates/list` | `resources/read` of any URI it matches, `completion/complete` |
|
|
759
766
|
| Prompt | `prompts/list` | `prompts/get`, `completion/complete` |
|
|
760
767
|
| Skill | `skills/search`, `skills/list`, `skill://index.json`, its `skill://<path>/SKILL.md` entry in `resources/list`, `GET /skills`, `/llm.txt`, `/llm_full.txt`, `codecall:searchSkills`, `codecall:searchKnowledge` | `skills/load`, `skill://<path>/SKILL.md` and its files, `GET /skills/{id}` (same answer as a nonexistent skill) |
|
|
761
768
|
|
|
769
|
+
The `toolName` completion of `ui://widget/{toolName}.html` runs the caller's `tools:list-tools` flow, so it offers only the UI tools `tools/list` shows that caller -- a flagged-off tool is left out there too ([#596](https://github.com/agentfront/frontmcp/issues/596)).
|
|
770
|
+
|
|
771
|
+
The skill catalog in the `initialize` instructions (and the SEP-2640 `skill://` hints under `skillsConfig.sep2640InInstructions`) is filtered for the initializing client too, so a flag-disabled skill's name and description never appear there ([#603](https://github.com/agentfront/frontmcp/issues/603)). A skill's `skill://<path>/SKILL.md` entry in `resources/list` is gated by the skill that path serves now -- replace a skill at the same path and the entry takes the new skill's flag ([#606](https://github.com/agentfront/frontmcp/issues/606)).
|
|
772
|
+
|
|
762
773
|
Installed on an `@App`, the gates cover every capability that app provides, including tools, resources and prompts contributed by its adapters (e.g. an OpenAPI adapter) and plugins. Its tool, resource, prompt and completion gates do not run for other apps' capabilities -- install it in `@FrontMcp({ plugins })` to gate every app. Resources and prompts served outside every app (the SEP-2640 `skill://` resources) are gated by every installed copy. Skills are gated through the `skills:filter` flow, which every skill surface runs -- as the calling user on every transport, stdio and in-memory included; custom plugins can hook `Did('filterSkills')` on it the same way, and reuse `filterServableSkills(scope, skills)` from `@frontmcp/sdk` to serve skills from a surface of their own.
|
|
763
774
|
|
|
764
775
|
---
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
"name": "ui-widgets",
|
|
77
|
-
"description": "@Tool({ ui }) \u2014 template formats, servingMode, host-detect resourceMode, CSP, widgetAccessible, MCP Apps spec."
|
|
77
|
+
"description": "@Tool({ ui }) \u2014 template formats, trusted markup (html / escapeStringResults), servingMode, host-detect resourceMode, CSP, widgetAccessible, MCP Apps spec."
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"name": "annotations",
|
|
@@ -337,11 +337,12 @@
|
|
|
337
337
|
"name": "22-tool-with-ui-html-template",
|
|
338
338
|
"level": "intermediate",
|
|
339
339
|
"description": "Tool with an inline HTML function template \u2014 `ui: { template: (ctx) => '<div>\u2026</div>' }` \u2014 for a quick widget that doesn't need a separate `.tsx` file.",
|
|
340
|
-
"tags": ["ui", "ui-widgets", "html-template", "
|
|
340
|
+
"tags": ["ui", "ui-widgets", "html-template", "html-tag", "escapeStringResults", "TemplateContext"],
|
|
341
341
|
"features": [
|
|
342
|
-
"Adding a `ui:` block with a function template
|
|
342
|
+
"Adding a `ui:` block with a function template that returns markup built with the `ctx.helpers.html` tagged template",
|
|
343
343
|
"Annotating `ctx` explicitly to dodge the TS7006 inference gap on the union `ui.template` type",
|
|
344
|
-
"
|
|
344
|
+
"Letting `ctx.helpers.html` escape every interpolated value so tool output can't inject markup into the widget",
|
|
345
|
+
"Opting in to `escapeStringResults: true` so a plain string result is escaped \u2014 the default from FrontMCP 1.9",
|
|
345
346
|
"Reading from `ctx.output` and `ctx.helpers` \u2014 the typed runtime context the template renderer hands you"
|
|
346
347
|
]
|
|
347
348
|
},
|
|
@@ -365,7 +366,7 @@
|
|
|
365
366
|
"features": [
|
|
366
367
|
"Restricting the widget's outbound `fetch` via `ui.csp.connectDomains` (emitted on the resource per #455)",
|
|
367
368
|
"Opting the widget into cross-tool calls with `widgetAccessible: true` and using `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs",
|
|
368
|
-
"
|
|
369
|
+
"Building the markup with `ctx.helpers.html` and embedding initial data into the inline `<script>` via `trustedHtml(jsonEmbed(...))` (`jsonEmbed` escapes `<`, `>` and `&`)",
|
|
369
370
|
"Surfacing in-flight status via `invocationStatus.invoking` / `invoked` so the host UI shows feedback"
|
|
370
371
|
]
|
|
371
372
|
},
|
|
@@ -1069,6 +1070,7 @@
|
|
|
1069
1070
|
"Top-level `instructions` on `@FrontMcp` exposes a global system prompt to MCP clients",
|
|
1070
1071
|
"`skillsConfig.injectInstructions: 'append'` adds the skill catalog summary after the user prompt",
|
|
1071
1072
|
"Dynamic skills are picked up because the composer runs on every initialize request",
|
|
1073
|
+
"The catalog only names skills the initializing caller may see (skill authorities and the `skills:filter` flow, e.g. feature flags)",
|
|
1072
1074
|
"Catalog summary is bounded at 16 KB with a truncation footer pointing at skill://index.json"
|
|
1073
1075
|
]
|
|
1074
1076
|
},
|