@frontmcp/skills 1.8.5 → 1.8.6
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/examples/23-tool-with-ui-filesource-tsx.md +6 -3
- package/catalog/create-tool/examples/27-tool-with-examples-metadata.md +3 -2
- 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 +31 -2
- 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-throttle-guard-config.md +19 -4
- package/catalog/frontmcp-config/references/configure-throttle.md +25 -7
- 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 +1 -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/deploy-to-vercel-config.md +15 -3
- package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +1 -1
- 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/official-plugins.md +58 -7
- 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-setup/references/setup-redis.md +37 -7
- package/catalog/frontmcp-testing/references/setup-testing.md +2 -1
- package/catalog/frontmcp-testing/references/test-e2e-handler.md +1 -0
- package/catalog/skills-manifest.json +6 -3
- package/package.json +1 -1
|
@@ -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({
|
|
@@ -107,6 +109,7 @@ export default function SalesChartWidget({ output }: Props) {
|
|
|
107
109
|
## Why these defaults matter
|
|
108
110
|
|
|
109
111
|
- **`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.
|
|
112
|
+
- **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
113
|
- **`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
114
|
- **`hydrate: false`** — default. React SSR output is static HTML; the bridge IIFE handles any interactivity. Enabling hydration creates React error #418 in Claude's iframe sandbox where the client-side render diverges from the SSR render.
|
|
112
115
|
- **`*.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.
|
|
@@ -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?`
|
|
@@ -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
|
|
@@ -73,7 +73,7 @@ Or use the FileSource form — it sidesteps the issue.
|
|
|
73
73
|
| `preferredHeight` | — | `number` (px) or CSS string (`'50vh'`). Initial widget height; auto-resize grows/shrinks from this baseline. |
|
|
74
74
|
| `minHeight` / `maxHeight` | — | `number` (px) or CSS string. Clamp the widget height; auto-resize never reports outside this range. |
|
|
75
75
|
| `aspectRatio` | — | CSS `aspect-ratio` (`'16 / 9'` or `1.5`). Hosts that honor it size by ratio instead of measured height. |
|
|
76
|
-
| `autoResize` | `true` |
|
|
76
|
+
| `autoResize` | `true` | Report the document height (margins included) to the host after the handshake. Set `false` to opt out (CSS still applies). |
|
|
77
77
|
| `csp` | — | `{ connectDomains?, resourceDomains? }` — emitted on the resource content's `_meta.ui.csp` (#455). Claude honors CSP only here. |
|
|
78
78
|
| `contentSecurity` | strict | `{ allowUnsafeLinks?, allowInlineScripts?, bypassSanitization? }` — keep defaults. |
|
|
79
79
|
| `escapeStringResults` | unset | `true` escapes plain string results of a template function; `html` / `trustedHtml` stay markup. Default in 1.9. |
|
|
@@ -140,6 +140,12 @@ ui: {
|
|
|
140
140
|
|
|
141
141
|
See [`rules/widget-paths-anchor-with-import-meta-url.md`](../rules/widget-paths-anchor-with-import-meta-url.md).
|
|
142
142
|
|
|
143
|
+
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):
|
|
144
|
+
|
|
145
|
+
- `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).
|
|
146
|
+
- Only widget files are copied, not other local files a widget imports.
|
|
147
|
+
- 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.
|
|
148
|
+
|
|
143
149
|
## `@frontmcp/ui` prerequisite (#443)
|
|
144
150
|
|
|
145
151
|
`.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,6 +157,16 @@ npm install @frontmcp/ui
|
|
|
151
157
|
|
|
152
158
|
Match the version to `@frontmcp/sdk`. Without it, server-side bundling fails with a friendly error pointing at this requirement.
|
|
153
159
|
|
|
160
|
+
## `esbuild` prerequisite (#649)
|
|
161
|
+
|
|
162
|
+
`@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:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
npm install esbuild # in "dependencies", not "devDependencies"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Projects created with `frontmcp create` already have it through the `frontmcp` package. `@frontmcp/uipack` declares it as an optional peer dependency (`>=0.27.0 <1`). Without it, the call fails with an error naming the widget.
|
|
169
|
+
|
|
154
170
|
## Widget bridge — `window.FrontMcpBridge`
|
|
155
171
|
|
|
156
172
|
When the widget needs to read tool data or invoke other tools, the bridge IIFE is injected automatically. Set `widgetAccessible: true` to enable `callTool`:
|
|
@@ -176,11 +192,22 @@ ui: {
|
|
|
176
192
|
| `getToolInput()` / `getToolOutput()` / `getStructuredContent()` | Read the tool data |
|
|
177
193
|
| `getWidgetState()` / `setWidgetState(state)` | Persisted per-widget state |
|
|
178
194
|
| `getHostContext()` / `getTheme()` / `getDisplayMode()` | Host context |
|
|
195
|
+
| `onContextChange(cb)` | Subscribe to host context changes (handshake included) |
|
|
179
196
|
| `hasCapability(cap)` | Probe adapter capabilities |
|
|
180
197
|
| `onToolResponseMetadata(cb)` | Subscribe to `ui/html` arrival (inline mode) |
|
|
181
198
|
|
|
182
199
|
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
200
|
|
|
201
|
+
### Theme
|
|
202
|
+
|
|
203
|
+
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="…">`:
|
|
204
|
+
|
|
205
|
+
- The frame's canvas and form controls match the host (a dark host no longer gets an opaque white frame).
|
|
206
|
+
- Style dark mode with `[data-theme='dark'] …` selectors.
|
|
207
|
+
- `getTheme()` returns the same value; `onContextChange` listeners fire for the handshake context too.
|
|
208
|
+
- Nothing is written when the host sends no theme (the OS fallback `getTheme()` starts with is never applied).
|
|
209
|
+
- A widget styled for light only keeps it with `:root { color-scheme: light }` — author CSS wins over the meta tag.
|
|
210
|
+
|
|
184
211
|
## Host considerations
|
|
185
212
|
|
|
186
213
|
| Host | Notes |
|
|
@@ -209,7 +236,9 @@ What FrontMCP does with it:
|
|
|
209
236
|
|
|
210
237
|
- **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
238
|
- **`_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
|
|
239
|
+
- **Runtime auto-resize** — when `autoResize !== false` and `ResizeObserver` is available, the bridge observes `<html>`, `<body>` and `#root` and reports the page height to the host (debounced via `requestAnimationFrame`), also firing a `widget:resize` event you can listen for. Call `window.FrontMcpBridge.setSize({ height, width, aspectRatio })` to report manually.
|
|
240
|
+
- **What is measured** — the whole document: `<html>` at `height: fit-content`, plus any content overflowing a fixed-height `<body>`, clamped by a px `max-height` on `<html>`. Body margins and margins collapsed through the body (an `<h2>` or `<ul>` at the edge) are counted, and the height shrinks when content does. `preferredHeight` / `minHeight` / `maxHeight` act as the floor and ceiling.
|
|
241
|
+
- **When it is sent** — reports wait for the bridge to initialize. In an ext-apps host the first report goes out once the `ui/initialize` handshake completes (a request sent earlier would be rejected); a report the host rejects is sent again on the next observation, even for the same height. A manual `setSize` called before the handshake is held and delivered right after it (only the latest size).
|
|
213
242
|
|
|
214
243
|
Per-host behavior:
|
|
215
244
|
|
|
@@ -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 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 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, 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
|
|
|
@@ -11,13 +11,23 @@ description: Complete GuardConfig interface reference for rate limiting, concurr
|
|
|
11
11
|
interface GuardConfig {
|
|
12
12
|
enabled: boolean;
|
|
13
13
|
|
|
14
|
-
// Storage for distributed rate limiting
|
|
14
|
+
// Storage for distributed rate limiting -- a StorageConfig from @frontmcp/utils,
|
|
15
|
+
// NOT the top-level `redis` shape (a block without `type` is auto-detected
|
|
16
|
+
// from REDIS_URL / REDIS_HOST and otherwise runs in memory)
|
|
15
17
|
storage?: {
|
|
16
|
-
type
|
|
17
|
-
redis?:
|
|
18
|
+
type?: 'memory' | 'redis' | 'vercel-kv' | 'upstash' | 'auto';
|
|
19
|
+
redis?:
|
|
20
|
+
| { config: { host: string; port?: number; password?: string; db?: number; tls?: boolean } }
|
|
21
|
+
| { url: string };
|
|
22
|
+
vercelKv?: { url?: string; token?: string };
|
|
23
|
+
upstash?: { url?: string; token?: string };
|
|
24
|
+
// What to do when the backend is unreachable at startup:
|
|
25
|
+
// 'error' (default in production) -- startup fails with GuardStorageUnavailableError (rate limits fail closed)
|
|
26
|
+
// 'memory' (default otherwise) -- start with per-instance counters
|
|
27
|
+
fallback?: 'error' | 'memory';
|
|
18
28
|
};
|
|
19
29
|
|
|
20
|
-
keyPrefix?: string; // default: 'mcp:guard:'
|
|
30
|
+
keyPrefix?: string; // default: 'mcp:guard:' -- a trailing ':' is dropped, keys read 'mcp:guard:<entity>:...'
|
|
21
31
|
|
|
22
32
|
// Server-wide limits
|
|
23
33
|
global?: RateLimitConfig;
|
|
@@ -57,6 +67,11 @@ interface IpFilterConfig {
|
|
|
57
67
|
}
|
|
58
68
|
```
|
|
59
69
|
|
|
70
|
+
## Storage Failure and Key Format
|
|
71
|
+
|
|
72
|
+
- **Fails closed.** When `storage` cannot be reached at startup, the server does not start: startup rejects with `GuardStorageUnavailableError` (code `GUARD_STORAGE_UNAVAILABLE`), whose message names `throttle.storage`. That is the default in production. Set `storage.fallback: 'memory'` to start with per-instance counters instead. (The top-level `redis` and `transport.persistence` differ: they fall back to memory with an error log.)
|
|
73
|
+
- **Keys.** `<keyPrefix><entity>:<partition>:<kind>:...`, e.g. `mcp:guard:export_tickets:global:rl:1790722980000`. Before 1.8.6 the default prefix wrote `mcp:guard::export_tickets:...`; old and new instances do not read each other's counters, so limits briefly split during a rolling deploy. A custom `keyPrefix` without a trailing `:` keeps its keys.
|
|
74
|
+
|
|
60
75
|
## Partition Strategies
|
|
61
76
|
|
|
62
77
|
- **`'global'`**: Single counter shared by all clients. Protects total server capacity.
|
|
@@ -244,6 +244,22 @@ throttle: {
|
|
|
244
244
|
}
|
|
245
245
|
```
|
|
246
246
|
|
|
247
|
+
`storage` is a `StorageConfig` from `@frontmcp/utils` -- `type` picks the backend, and its options go under the matching key (`redis: { config }` or `redis: { url }`, `vercelKv: { url, token }`, `upstash: { url, token }`). It is NOT the top-level `redis` shape: `{ provider: 'redis', host, port }` has no `type`, so it is auto-detected from `REDIS_URL` / `REDIS_HOST` and otherwise runs in memory.
|
|
248
|
+
|
|
249
|
+
**Rate limits fail closed.** If the store is unreachable at startup, the server does not start: startup rejects with `GuardStorageUnavailableError` (`throttle.storage (redis) is unavailable: ...`), the default in production. To start with per-instance counters instead, opt in:
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
storage: {
|
|
253
|
+
type: 'redis',
|
|
254
|
+
redis: { url: process.env['REDIS_URL'] },
|
|
255
|
+
fallback: 'memory',
|
|
256
|
+
},
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
The top-level `redis` and `transport.persistence` behave differently: they fall back to in-memory storage with an error log.
|
|
260
|
+
|
|
261
|
+
Keys are `<keyPrefix><entity>:<partition>:<kind>:...` (`mcp:guard:export_tickets:global:rl:...`); a trailing `:` on `keyPrefix` is dropped. Before 1.8.6 the default prefix wrote `mcp:guard::...`, so counters briefly split between versions during a rolling deploy.
|
|
262
|
+
|
|
247
263
|
## Verification
|
|
248
264
|
|
|
249
265
|
```bash
|
|
@@ -297,13 +313,15 @@ done
|
|
|
297
313
|
|
|
298
314
|
## Troubleshooting
|
|
299
315
|
|
|
300
|
-
| Problem
|
|
301
|
-
|
|
|
302
|
-
| Rate limits not enforced across instances
|
|
303
|
-
|
|
|
304
|
-
|
|
|
305
|
-
|
|
|
306
|
-
|
|
|
316
|
+
| Problem | Cause | Solution |
|
|
317
|
+
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
318
|
+
| Rate limits not enforced across instances | In-memory storage used with multiple server replicas | Configure `storage: { type: 'redis' }` in the throttle block to share counters |
|
|
319
|
+
| Startup fails with `GuardStorageUnavailableError` | The `throttle.storage` backend is unreachable; rate limits fail closed | Bring the store up, or set `throttle.storage.fallback: 'memory'` to start with per-instance counters |
|
|
320
|
+
| Redis-backed limits still per-instance | `storage` written in the top-level `redis` shape (`{ provider: 'redis', host }`) with no `type`, so it was auto-detected as memory | Use `{ type: 'redis', redis: { config: { host, port } } }` |
|
|
321
|
+
| All requests rejected with 403 | `ipFilter.defaultAction` set to `'deny'` without any `allowList` entries, or the runtime reports no client IP (a custom fetch wrapper that drops the second handler argument on Deno/Bun) | Add the allowed IP ranges to `allowList`, pass the platform's second argument through to the handler, or change `defaultAction` to `'allow'` |
|
|
322
|
+
| Tools timing out unexpectedly | `defaultTimeout.executeMs` too low for the tool's normal execution time | Increase the global default or set a per-tool `timeout.executeMs` override |
|
|
323
|
+
| `X-Forwarded-For` header ignored | No trusted proxy declared. `ipFilter.trustProxy` / `trustedProxyDepth` are accepted by the schema but NOT read -- client-IP extraction happens in the SDK context layer, before guard config is reachable | Set the `FRONTMCP_TRUST_PROXY=true` and `FRONTMCP_TRUSTED_PROXY_DEPTH` environment variables instead |
|
|
324
|
+
| Rate limit resets not aligned with expectations | `windowMs` misunderstood as a sliding window when it is a fixed window | The window is fixed; all counters reset at the end of each `windowMs` interval |
|
|
307
325
|
|
|
308
326
|
## Examples
|
|
309
327
|
|
|
@@ -53,7 +53,7 @@ export default class MyServer {}
|
|
|
53
53
|
// buildCommand/installCommand are detected from your lockfile.
|
|
54
54
|
{
|
|
55
55
|
"version": 2,
|
|
56
|
-
"buildCommand": "yarn build",
|
|
56
|
+
"buildCommand": "yarn frontmcp build --target vercel",
|
|
57
57
|
"installCommand": "yarn install"
|
|
58
58
|
}
|
|
59
59
|
```
|
|
@@ -58,7 +58,7 @@ vercel env add LOG_LEVEL info
|
|
|
58
58
|
// Do not add functions/rewrites referencing api/frontmcp.* (no such file).
|
|
59
59
|
{
|
|
60
60
|
"version": 2,
|
|
61
|
-
"buildCommand": "yarn build",
|
|
61
|
+
"buildCommand": "yarn frontmcp build --target vercel",
|
|
62
62
|
"installCommand": "yarn install"
|
|
63
63
|
}
|
|
64
64
|
```
|
package/catalog/frontmcp-deployment/examples/deploy-to-vercel-config/minimal-vercel-config.md
CHANGED
|
@@ -11,6 +11,7 @@ tags:
|
|
|
11
11
|
- minimal
|
|
12
12
|
features:
|
|
13
13
|
- The exact shape of the auto-generated `vercel.json` — three keys, nothing else
|
|
14
|
+
- That `buildCommand` builds the vercel target (`<exec> frontmcp build --target vercel`), not the `build` script
|
|
14
15
|
- That routing and function configuration live in `.vercel/output/`, not `vercel.json`
|
|
15
16
|
- That hand-authoring `api/frontmcp.ts` references in `vercel.json` is unnecessary and breaks deploys
|
|
16
17
|
---
|
|
@@ -25,7 +26,7 @@ features:
|
|
|
25
26
|
// vercel.json — yarn project (yarn.lock present)
|
|
26
27
|
{
|
|
27
28
|
"version": 2,
|
|
28
|
-
"buildCommand": "yarn build",
|
|
29
|
+
"buildCommand": "yarn frontmcp build --target vercel",
|
|
29
30
|
"installCommand": "yarn install"
|
|
30
31
|
}
|
|
31
32
|
```
|
|
@@ -34,7 +35,7 @@ features:
|
|
|
34
35
|
// vercel.json — pnpm project (pnpm-lock.yaml present)
|
|
35
36
|
{
|
|
36
37
|
"version": 2,
|
|
37
|
-
"buildCommand": "pnpm
|
|
38
|
+
"buildCommand": "pnpm exec frontmcp build --target vercel",
|
|
38
39
|
"installCommand": "pnpm install"
|
|
39
40
|
}
|
|
40
41
|
```
|
|
@@ -43,11 +44,13 @@ features:
|
|
|
43
44
|
// vercel.json — npm project (package-lock.json present)
|
|
44
45
|
{
|
|
45
46
|
"version": 2,
|
|
46
|
-
"buildCommand": "
|
|
47
|
+
"buildCommand": "npx frontmcp build --target vercel",
|
|
47
48
|
"installCommand": "npm install"
|
|
48
49
|
}
|
|
49
50
|
```
|
|
50
51
|
|
|
52
|
+
`buildCommand` runs the vercel target through the project's package manager (bun projects get `bunx frontmcp build --target vercel`). It is never `<pm> run build`: the `build` script is `frontmcp build`, which builds the config's deployments and never writes `.vercel/output`.
|
|
53
|
+
|
|
51
54
|
The actual function and routes live under `.vercel/output/`:
|
|
52
55
|
|
|
53
56
|
```text
|
|
@@ -64,6 +67,7 @@ The actual function and routes live under `.vercel/output/`:
|
|
|
64
67
|
## What This Demonstrates
|
|
65
68
|
|
|
66
69
|
- The exact shape of the auto-generated `vercel.json` — three keys, nothing else
|
|
70
|
+
- That `buildCommand` builds the vercel target (`<exec> frontmcp build --target vercel`), not the `build` script
|
|
67
71
|
- That routing and function configuration live in `.vercel/output/`, not `vercel.json`
|
|
68
72
|
- That hand-authoring `api/frontmcp.ts` references in `vercel.json` is unnecessary and breaks deploys
|
|
69
73
|
|
|
@@ -25,11 +25,12 @@ The Vercel adapter emits a minimal `vercel.json` (version + buildCommand + insta
|
|
|
25
25
|
|
|
26
26
|
```json
|
|
27
27
|
// vercel.json — extends the auto-generated minimum with regions + headers.
|
|
28
|
-
// The
|
|
29
|
-
//
|
|
28
|
+
// The build never overwrites an existing vercel.json, so keep buildCommand
|
|
29
|
+
// building the vercel target (`<exec> frontmcp build --target vercel`) —
|
|
30
|
+
// `yarn build` would run the config's deployments and deploy nothing.
|
|
30
31
|
{
|
|
31
32
|
"version": 2,
|
|
32
|
-
"buildCommand": "yarn build",
|
|
33
|
+
"buildCommand": "yarn frontmcp build --target vercel",
|
|
33
34
|
"installCommand": "yarn install",
|
|
34
35
|
"regions": ["iad1"],
|
|
35
36
|
"headers": [
|
|
@@ -16,12 +16,23 @@ After `frontmcp build --target vercel`, your `vercel.json` looks like:
|
|
|
16
16
|
```json
|
|
17
17
|
{
|
|
18
18
|
"version": 2,
|
|
19
|
-
"buildCommand": "yarn build",
|
|
19
|
+
"buildCommand": "yarn frontmcp build --target vercel",
|
|
20
20
|
"installCommand": "yarn install"
|
|
21
21
|
}
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
The
|
|
24
|
+
The package manager is detected from your lockfile, and `buildCommand` runs the **vercel target** through it:
|
|
25
|
+
|
|
26
|
+
| Lockfile | `buildCommand` | `installCommand` |
|
|
27
|
+
| --------------------------- | ------------------------------------------ | ---------------- |
|
|
28
|
+
| `package-lock.json` or none | `npx frontmcp build --target vercel` | `npm install` |
|
|
29
|
+
| `yarn.lock` | `yarn frontmcp build --target vercel` | `yarn install` |
|
|
30
|
+
| `pnpm-lock.yaml` | `pnpm exec frontmcp build --target vercel` | `pnpm install` |
|
|
31
|
+
| `bun.lock` / `bun.lockb` | `bunx frontmcp build --target vercel` | `bun install` |
|
|
32
|
+
|
|
33
|
+
`frontmcp create --target vercel` scaffolds the same file (plus a `$schema` line). The build never overwrites an existing `vercel.json`.
|
|
34
|
+
|
|
35
|
+
**Never use `<pm> run build` as the `buildCommand`.** The project's `build` script is `frontmcp build`, which builds the deployments in `frontmcp.config` (typically `node`) and never writes `.vercel/output` -- Vercel then has nothing to deploy. Files generated before 1.8.6 used `yarn build` / `npm run build`; replace them with the command above.
|
|
25
36
|
|
|
26
37
|
## Customizing
|
|
27
38
|
|
|
@@ -30,7 +41,7 @@ You can hand-edit `vercel.json` AFTER the build to add fields Vercel supports
|
|
|
30
41
|
```json
|
|
31
42
|
{
|
|
32
43
|
"version": 2,
|
|
33
|
-
"buildCommand": "yarn build",
|
|
44
|
+
"buildCommand": "yarn frontmcp build --target vercel",
|
|
34
45
|
"installCommand": "yarn install",
|
|
35
46
|
"regions": ["iad1"]
|
|
36
47
|
}
|
|
@@ -43,6 +54,7 @@ To add response headers, prefer setting them inside your FrontMCP server (where
|
|
|
43
54
|
- Do **not** add a `rewrites` block routing to `/api/frontmcp` — there is no `api/` directory in the Vercel build output.
|
|
44
55
|
- Do **not** add a `functions: { "api/frontmcp.ts": ... }` map — the function lives at `.vercel/output/functions/index.func/handler.cjs`, configured by `.vc-config.json` (runtime, handler) generated by the adapter.
|
|
45
56
|
- Do **not** set `framework` to a value the FrontMCP build doesn't match. Leaving it unset is safest.
|
|
57
|
+
- Do **not** set `buildCommand` to `<pm> run build` / `yarn build` -- that builds the config's deployments, not the vercel target.
|
|
46
58
|
|
|
47
59
|
## Function Runtime Config
|
|
48
60
|
|
|
@@ -58,7 +58,7 @@ This produces a Vercel Build Output API v3 structure:
|
|
|
58
58
|
vercel.json # version, buildCommand, installCommand
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
The adapter detects your package manager from the lockfile and writes the matching `
|
|
61
|
+
The adapter detects your package manager from the lockfile and writes the matching `installCommand` and a `buildCommand` that builds the **vercel target** through it: `npx frontmcp build --target vercel` (npm), `yarn frontmcp build --target vercel`, `pnpm exec frontmcp build --target vercel`, or `bunx frontmcp build --target vercel`. An existing `vercel.json` is left as it is — including one from `frontmcp create --target vercel`, which writes the same commands. Never set `buildCommand` to `<pm> run build`: the `build` script is `frontmcp build`, which builds the config's deployments (typically `node`) and never writes `.vercel/output`. No `api/` directory is involved.
|
|
62
62
|
|
|
63
63
|
## Step 2: Configure the Server for Vercel KV
|
|
64
64
|
|
|
@@ -122,6 +122,15 @@ before the first `elicit()`/`sample()`/`listRoots()` call.
|
|
|
122
122
|
and a 10-minute expiry — a tampered or replayed blob is discarded and the
|
|
123
123
|
exchange restarts.
|
|
124
124
|
|
|
125
|
+
**Multi-instance: set `VAULT_SECRET`.** The signing key is `VAULT_SECRET`, else
|
|
126
|
+
`JWT_SECRET`, else a random per-process key. With the per-process key, a round
|
|
127
|
+
that lands on another instance (or arrives after a restart) fails verification
|
|
128
|
+
and the tool asks its first question again. Set `VAULT_SECRET` (or `JWT_SECRET`)
|
|
129
|
+
to the same value on every instance. In production, `redis` or
|
|
130
|
+
`transport.persistence` without either secret logs a startup warning; each
|
|
131
|
+
rejection logs `mcp-20260728: rejected requestState` with `reason: 'bad-signature'`
|
|
132
|
+
and a `hint` naming `VAULT_SECRET`.
|
|
133
|
+
|
|
125
134
|
The client MUST declare the matching capability, or the server answers `-32021`. (An unversioned call the server only defaulted to 2026-07-28 comes from a client that never declared it; `elicit()` answers that one with `ElicitationNotSupportedError`, as for a legacy client without a session.)
|
|
126
135
|
|
|
127
136
|
```json
|
|
@@ -9,6 +9,7 @@ features:
|
|
|
9
9
|
- 'Using `toolPatterns` glob patterns to cache groups of tools without per-tool configuration'
|
|
10
10
|
- 'Per-tool `cache` metadata with custom `ttl` (seconds) and `slideWindow` for TTL refresh on hits'
|
|
11
11
|
- 'Using `cache: true` for simple default-TTL caching'
|
|
12
|
+
- "A cache hit returns the same `content`/`structuredContent` as the miss, marked by `_meta.cache: 'hit'` on the result"
|
|
12
13
|
- "Gating a tool with `featureFlag: 'beta-search'` -- the tool is hidden from `list_tools` when the flag is off"
|
|
13
14
|
- 'Accessing `this.featureFlags.isEnabled()` inside a tool for runtime flag checks'
|
|
14
15
|
---
|
|
@@ -75,6 +76,9 @@ class GetWeatherTool extends ToolContext {
|
|
|
75
76
|
return { city: input.city, temperature: weather.temp, condition: weather.condition };
|
|
76
77
|
}
|
|
77
78
|
}
|
|
79
|
+
|
|
80
|
+
// A second identical call is a hit: execute() does not run, `structuredContent` is the same
|
|
81
|
+
// `{ city, temperature, condition }` as the first call, and `result._meta.cache === 'hit'`.
|
|
78
82
|
```
|
|
79
83
|
|
|
80
84
|
```typescript
|
|
@@ -106,6 +110,7 @@ class BetaSearchTool extends ToolContext {
|
|
|
106
110
|
- Using `toolPatterns` glob patterns to cache groups of tools without per-tool configuration
|
|
107
111
|
- Per-tool `cache` metadata with custom `ttl` (seconds) and `slideWindow` for TTL refresh on hits
|
|
108
112
|
- Using `cache: true` for simple default-TTL caching
|
|
113
|
+
- A cache hit returns the same `content`/`structuredContent` as the miss, marked by `_meta.cache: 'hit'` on the result
|
|
109
114
|
- Gating a tool with `featureFlag: 'beta-search'` -- the tool is hidden from `list_tools` when the flag is off
|
|
110
115
|
- Accessing `this.featureFlags.isEnabled()` inside a tool for runtime flag checks
|
|
111
116
|
|
package/catalog/frontmcp-development/examples/official-plugins/remember-plugin-session-memory.md
CHANGED
|
@@ -6,8 +6,8 @@ description: 'Demonstrates installing the Remember plugin and using `this.rememb
|
|
|
6
6
|
tags: [development, session, plugins, remember, plugin, memory]
|
|
7
7
|
features:
|
|
8
8
|
- "Installing `RememberPlugin` with `type: 'memory'` for development"
|
|
9
|
-
- 'Enabling `tools: { enabled: true }` to expose LLM-callable memory tools (`remember_this`, `recall`, etc.)'
|
|
10
|
-
- 'Using `this.remember.set()` with default `session` scope and explicit `user` scope'
|
|
9
|
+
- 'Enabling `tools: { enabled: true }` to expose LLM-callable memory tools (`remember_this`, `recall`, etc.), whose `scope` defaults to `session`'
|
|
10
|
+
- 'Using `this.remember.set()` with default `session` scope and explicit `user` scope (refused for an anonymous caller)'
|
|
11
11
|
- 'Using `this.remember.get()` with a `defaultValue` fallback'
|
|
12
12
|
- 'Using `this.remember.knows()` to check key existence without retrieving the value'
|
|
13
13
|
---
|
|
@@ -54,6 +54,7 @@ import { Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
|
54
54
|
class PreferencesTool extends ToolContext {
|
|
55
55
|
async execute(input: { theme: string; language: string }) {
|
|
56
56
|
await this.remember.set('theme', input.theme);
|
|
57
|
+
// User scope needs a signed-in caller: an anonymous one is refused with RememberIdentityError
|
|
57
58
|
await this.remember.set('language', input.language, { scope: 'user' });
|
|
58
59
|
|
|
59
60
|
return { saved: true, theme: input.theme, language: input.language };
|
|
@@ -75,7 +76,8 @@ import { Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
|
75
76
|
class GreetingTool extends ToolContext {
|
|
76
77
|
async execute(input: { name: string }) {
|
|
77
78
|
const theme = await this.remember.get('theme', { defaultValue: 'light' });
|
|
78
|
-
|
|
79
|
+
// Read from the scope it was stored in
|
|
80
|
+
const language = await this.remember.get('language', { scope: 'user', defaultValue: 'en' });
|
|
79
81
|
const hasOnboarded = await this.remember.knows('onboarding_complete');
|
|
80
82
|
|
|
81
83
|
return {
|
|
@@ -91,8 +93,8 @@ class GreetingTool extends ToolContext {
|
|
|
91
93
|
## What This Demonstrates
|
|
92
94
|
|
|
93
95
|
- Installing `RememberPlugin` with `type: 'memory'` for development
|
|
94
|
-
- Enabling `tools: { enabled: true }` to expose LLM-callable memory tools (`remember_this`, `recall`, etc.)
|
|
95
|
-
- Using `this.remember.set()` with default `session` scope and explicit `user` scope
|
|
96
|
+
- Enabling `tools: { enabled: true }` to expose LLM-callable memory tools (`remember_this`, `recall`, etc.), whose `scope` defaults to `session`
|
|
97
|
+
- Using `this.remember.set()` with default `session` scope and explicit `user` scope (refused for an anonymous caller)
|
|
96
98
|
- Using `this.remember.get()` with a `defaultValue` fallback
|
|
97
99
|
- Using `this.remember.knows()` to check key existence without retrieving the value
|
|
98
100
|
|
|
@@ -131,7 +131,7 @@ The sandboxed VM runs AgentScript (a restricted JavaScript subset). Presets cont
|
|
|
131
131
|
CodeCall contributes 4 tools to your server:
|
|
132
132
|
|
|
133
133
|
- `codecall:search` -- Semantic search over all registered tools using TF-IDF scoring with synonym expansion. Input: `{ queries: string[] }` (array of atomic action phrases, max 10). Decompose complex requests into simple actions (e.g., "delete users and send email" becomes `queries: ["delete user", "send email"]`). Returns ranked tool names, descriptions, and relevance scores.
|
|
134
|
-
- `codecall:describe` -- Returns full input/output JSON schemas for one or more tools. Input: `{ toolNames: string[] }` (tool names from search results). Use after search to understand tool interfaces before execution. If `notFound` array is non-empty in the response, re-search with corrected queries.
|
|
134
|
+
- `codecall:describe` -- Returns full input/output JSON schemas for one or more tools. Input: `{ toolNames: string[] }` (tool names from search results). Use after search to understand tool interfaces before execution. If `notFound` array is non-empty in the response, re-search with corrected queries. Results are cached for 60 seconds per server and per caller.
|
|
135
135
|
- `codecall:execute` -- Runs an AgentScript program in the sandboxed VM. Input: `{ script: string }` (AgentScript code). Use `callTool(name, args)` to invoke tools within scripts. The script can call multiple tools, branch on results, and compose outputs.
|
|
136
136
|
- `codecall:invoke` -- Direct single-tool invocation (available when `directCalls` is enabled). Bypasses the VM for simple one-shot calls.
|
|
137
137
|
|
|
@@ -244,7 +244,9 @@ class GlobalStoreServer {}
|
|
|
244
244
|
|
|
245
245
|
### Storage Types
|
|
246
246
|
|
|
247
|
-
- `memory` -- In-process Map. Fastest, no persistence. Good for development.
|
|
247
|
+
- `memory` -- In-process Map. Fastest, no persistence. Good for development. One store per server: servers built in the
|
|
248
|
+
same process (even from the same app class or `RememberPlugin.init()` result) never see each other's memory, `global`
|
|
249
|
+
scope included.
|
|
248
250
|
- `redis` -- Dedicated Redis connection. Plugin manages the client lifecycle.
|
|
249
251
|
- `redis-client` -- Bring your own ioredis client instance.
|
|
250
252
|
- `vercel-kv` -- Vercel KV (Redis-compatible). Uses `@vercel/kv` package.
|
|
@@ -285,7 +287,8 @@ class MyTool extends ToolContext {
|
|
|
285
287
|
- `session` -- Default scope. With a verified session, valid only for that session and cleared
|
|
286
288
|
when it ends. Without one (stateless transport, MCP 2026-07-28), it belongs to the authenticated
|
|
287
289
|
principal and lasts across that principal's requests until its TTL, not per request.
|
|
288
|
-
- `user` -- Persists for the user across sessions. Tied to user identity
|
|
290
|
+
- `user` -- Persists for the signed-in user across sessions. Tied to user identity; refused for an
|
|
291
|
+
anonymous caller, whose `anon:<id>` subject names no user.
|
|
289
292
|
- `tool` -- Scoped to a specific tool plus the same identity as `session` (the verified session,
|
|
290
293
|
else the authenticated principal). Isolated per tool.
|
|
291
294
|
- `global` -- Shared across all sessions and users. Use carefully.
|
|
@@ -296,7 +299,10 @@ sends. A stateless HTTP transport (shared `__stateless__` id), MCP 2026-07-28 (n
|
|
|
296
299
|
unverified `mcp-session-id` carry no session identity: `session` and `tool` scope fall back to the
|
|
297
300
|
authenticated principal, and an unauthenticated request without a verified session is refused
|
|
298
301
|
with a `RememberIdentityError` rather than given a namespace shared with other clients. `user`
|
|
299
|
-
scope is refused with no authenticated user
|
|
302
|
+
scope is refused with no authenticated user, and an anonymous subject (`anon:<id>`, which the SDK
|
|
303
|
+
makes up for one session or for each request without one) counts as none. `RememberIdentityError`
|
|
304
|
+
is a public MCP error (code `REMEMBER_IDENTITY_REQUIRED`), so the client reads the refusal as
|
|
305
|
+
written, in production too. If the data really is shared, use `scope: 'global'`.
|
|
300
306
|
|
|
301
307
|
**Set `REMEMBER_SECRET` on every instance that shares a store.** All scopes, `session` and
|
|
302
308
|
`tool` included, derive their encryption key from that secret plus the scope identity. A
|
|
@@ -340,6 +346,14 @@ clear the legacy prefixes manually if you want the storage back.
|
|
|
340
346
|
- `forget` -- Remove a stored value by key
|
|
341
347
|
- `list_memories` -- List all stored keys, optionally filtered by pattern
|
|
342
348
|
|
|
349
|
+
All four take an optional `scope` (default `session`) and describe it to the model the same way:
|
|
350
|
+
`session` is this session, or without one (stateless HTTP, MCP 2026-07-28) the signed-in caller
|
|
351
|
+
across its requests; `user` is the signed-in caller across all of its sessions; `tool` is the tool
|
|
352
|
+
running the call, for the same caller as `session` -- each memory tool has its own, so `recall`
|
|
353
|
+
does not see what `remember_this` stored in `tool` scope; `global` is shared by every caller. An
|
|
354
|
+
anonymous caller cannot use `user`, nor `session` or `tool` without a session. No scope lasts
|
|
355
|
+
"until disconnect" or "forever": entries last until forgotten or until their `ttl` runs out.
|
|
356
|
+
|
|
343
357
|
---
|
|
344
358
|
|
|
345
359
|
## 3. Approval Plugin (`@frontmcp/plugin-approval`)
|
|
@@ -424,7 +438,12 @@ authInfo.extra.approvalContext = { type: 'project', identifier: resolvedProjectI
|
|
|
424
438
|
`maxTtlMs`, however it was stored.
|
|
425
439
|
6. Otherwise refused with state `pending` (or `expired`).
|
|
426
440
|
|
|
427
|
-
A refused call throws `ApprovalRequiredError`; the client receives an error result
|
|
441
|
+
A refused call throws `ApprovalRequiredError`; the client receives an error result whose text is
|
|
442
|
+
exactly the tool's `approvalMessage` (or the default `Tool "<full name>" requires approval to
|
|
443
|
+
execute. Allow?`, or `Tool "<full name>" execution denied.` for a denial) and whose `_meta.code` is
|
|
444
|
+
`APPROVAL_REQUIRED`. The approval errors extend `PublicMcpError`, so this holds in production too:
|
|
445
|
+
the message is never replaced by `Internal FrontMCP error` and never carries a stack trace (releases
|
|
446
|
+
up to 1.8.5 wrapped refusals as internal server errors).
|
|
428
447
|
|
|
429
448
|
Approvals are looked up by the tool's full name, `<owner id>:<tool name>`, so pass that name to
|
|
430
449
|
`this.approval` grant and check methods. The owner is the app that declares the tool, or the
|
|
@@ -482,6 +501,22 @@ class DangerousActionTool extends ToolContext {
|
|
|
482
501
|
}
|
|
483
502
|
```
|
|
484
503
|
|
|
504
|
+
Each grant records its grantor in `grantedBy`. Without one, it is the signed-in caller whose tool
|
|
505
|
+
made the grant: `userGrantor(<user id>)`, i.e. `{ source: 'user', identifier: '<user id>', method:
|
|
506
|
+
'interactive' }`. Without a signed-in user (no principal, or an anonymous `anon:` subject) it is
|
|
507
|
+
`{ source: 'user' }` with no identifier. `revokeApproval()` records `revokedBy` the same way
|
|
508
|
+
(`userRevoker(<user id>)`). Releases up to 1.8.5 recorded both as `'policy'`. Pass `grantedBy` to
|
|
509
|
+
record anything else; `userGrantor`'s third argument is an options object, not the method:
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
import { policyGrantor, userGrantor } from '@frontmcp/plugin-approval';
|
|
513
|
+
|
|
514
|
+
await this.approval.grantSessionApproval('my-app:file_write', { grantedBy: policyGrantor('safe-list') });
|
|
515
|
+
await this.approval.grantUserApproval('my-app:file_write', {
|
|
516
|
+
grantedBy: userGrantor('user-123', 'Jane Doe', { method: 'interactive' }),
|
|
517
|
+
});
|
|
518
|
+
```
|
|
519
|
+
|
|
485
520
|
### Per-Tool Approval Metadata
|
|
486
521
|
|
|
487
522
|
```typescript
|
|
@@ -581,7 +616,8 @@ class GlobalCacheServer {}
|
|
|
581
616
|
|
|
582
617
|
### Storage Types
|
|
583
618
|
|
|
584
|
-
- `memory` -- In-process Map with automatic eviction. No external dependencies.
|
|
619
|
+
- `memory` -- In-process Map with automatic eviction. No external dependencies. One store per server: servers built in
|
|
620
|
+
the same process (even from the same app class or `CachePlugin.init()` result) never share entries.
|
|
585
621
|
- `redis` -- Dedicated Redis connection with native TTL support. Plugin manages the client.
|
|
586
622
|
- `redis-client` -- Bring your own ioredis client instance.
|
|
587
623
|
- `global-store` -- Reuses the Redis connection from `@FrontMcp({ redis: {...} })`.
|
|
@@ -626,7 +662,7 @@ CachePlugin.init({
|
|
|
626
662
|
});
|
|
627
663
|
```
|
|
628
664
|
|
|
629
|
-
A tool is cached if it matches any pattern OR has `cache: true` (or a cache object) in its metadata.
|
|
665
|
+
A tool is cached if it matches any pattern OR has `cache: true` (or a cache object) in its metadata. `cache: { ttl: 0 }` (or a negative TTL) turns caching off for the tool, even when it matches a pattern; up to 1.8.5 it cached the first result with no expiry.
|
|
630
666
|
|
|
631
667
|
### Cache Bypass
|
|
632
668
|
|
|
@@ -646,6 +682,21 @@ identity. Two calls share an entry only when all three match.
|
|
|
646
682
|
Set `keyByIdentity: false` to drop identity from the key, and only for output that is identical for every caller. See
|
|
647
683
|
"Cache keys include the caller's identity" above.
|
|
648
684
|
|
|
685
|
+
### What a Cache Hit Returns
|
|
686
|
+
|
|
687
|
+
A hit skips `execute()` and answers with the cached output unchanged: its `content` and `structuredContent` are the
|
|
688
|
+
same as the call that filled the cache. The hit is marked on the result's own `_meta` (`result._meta.cache === 'hit'`),
|
|
689
|
+
never inside the data, so a tool without an `outputSchema` does not see `_meta` in its `structuredContent` or text.
|
|
690
|
+
A miss has no `cache` key in `_meta`. A plugin hook that wants to add result metadata the same way sets the
|
|
691
|
+
`tools:call-tool` flow state's `resultMeta`:
|
|
692
|
+
|
|
693
|
+
```typescript
|
|
694
|
+
@ToolHook.Did('execute')
|
|
695
|
+
tagResult(flowCtx: FlowCtxOf<'tools:call-tool'>) {
|
|
696
|
+
flowCtx.state.set('resultMeta', { ...flowCtx.state.resultMeta, traced: true });
|
|
697
|
+
}
|
|
698
|
+
```
|
|
699
|
+
|
|
649
700
|
---
|
|
650
701
|
|
|
651
702
|
## 5. Feature Flags Plugin (`@frontmcp/plugin-feature-flags`)
|
package/catalog/frontmcp-production-readiness/examples/production-vercel/vercel-edge-config.md
CHANGED
|
@@ -28,6 +28,7 @@ Checklist for verifying the Vercel Build Output API v3 artifact and edge config
|
|
|
28
28
|
- [ ] `.vercel/output/functions/index.func/handler.cjs` exists — this is the actual function bundle
|
|
29
29
|
- [ ] `.vercel/output/functions/index.func/.vc-config.json` declares `runtime: nodejs24.x` (the default written by the build adapter) and `handler: "handler.cjs"`
|
|
30
30
|
- [ ] No hand-written `vercel.json` with the obsolete `{ "builds": [...], "routes": [...] }` shape — modern adapter emits `{ "version": 2, "buildCommand": ..., "installCommand": ... }` and routes through Build Output API
|
|
31
|
+
- [ ] `vercel.json` `buildCommand` builds the vercel target — `npx|yarn|pnpm exec|bunx frontmcp build --target vercel` — not `<pm> run build` (the `build` script runs `frontmcp build`, which builds the config's deployments and never writes `.vercel/output`; configs generated before 1.8.6 used `yarn build` / `npm run build`)
|
|
31
32
|
- [ ] No hand-written `src/lambda.ts` / `api/mcp.ts` with a fictional `createVercelHandler(...)` import — the build adapter generates `index.js` that requires your decorated `@FrontMcp` class
|
|
32
33
|
|
|
33
34
|
## Runtime config (`@FrontMcp` decorator)
|
|
@@ -15,6 +15,7 @@ These checks apply to ALL deployment targets. Run them first, then proceed to yo
|
|
|
15
15
|
- [ ] Authentication is enabled (`auth` config in `@FrontMcp` or `@frontmcp/auth`)
|
|
16
16
|
- [ ] API keys/tokens are loaded from environment variables, never hardcoded
|
|
17
17
|
- [ ] Session storage uses Redis or platform-native store (not in-memory) for multi-instance
|
|
18
|
+
- [ ] `MCP_SESSION_SECRET` is the same on every instance — a session id minted under another secret is answered 404 (the client re-initializes), in every auth mode including `public`
|
|
18
19
|
- [ ] Session TTL is configured appropriately (not infinite)
|
|
19
20
|
- [ ] Tool-level authorization is enforced where needed (ApprovalPlugin or custom)
|
|
20
21
|
- [ ] OAuth redirect URIs are restricted to known domains
|
|
@@ -54,12 +55,15 @@ These checks apply to ALL deployment targets. Run them first, then proceed to yo
|
|
|
54
55
|
- [ ] Production secrets are managed via secret manager (AWS SSM, Vault, etc.)
|
|
55
56
|
- [ ] API keys have minimum required permissions
|
|
56
57
|
- [ ] Secrets are rotated on a schedule
|
|
58
|
+
- [ ] Multi-instance: `VAULT_SECRET` (or `JWT_SECRET`) is set to the same value on every instance — it signs MCP 2026-07-28 `requestState`, and without it a multi-round tool (`elicit()` / `sample()`) whose next round lands on another instance starts over. Startup logs show no `requestState is signed with a per-process key` warning
|
|
57
59
|
|
|
58
60
|
### Rate Limiting
|
|
59
61
|
|
|
60
62
|
- [ ] Rate limiting is configured for public-facing endpoints
|
|
61
63
|
- [ ] Per-client/per-IP limits are set
|
|
62
64
|
- [ ] Throttle configuration uses `@FrontMcp({ throttle: {...} })`
|
|
65
|
+
- [ ] Multi-instance: `throttle.storage` uses the storage shape `{ type: 'redis', redis: { config: { host, port } } }` (not the top-level `redis` shape, which silently falls back to auto-detection)
|
|
66
|
+
- [ ] Decided what happens when the throttle Redis is down at startup: the default fails closed (`GuardStorageUnavailableError`); `throttle.storage.fallback: 'memory'` starts with per-instance counters instead
|
|
63
67
|
- [ ] Large payload limits are set to prevent memory exhaustion
|
|
64
68
|
|
|
65
69
|
### Dependencies
|
|
@@ -307,6 +307,33 @@ redis-cli -h localhost -p 6379 keys "mcp:*"
|
|
|
307
307
|
|
|
308
308
|
You should see session keys like `mcp:session:<session-id>`.
|
|
309
309
|
|
|
310
|
+
## Step 8 -- Multiple Instances and Redis Outages
|
|
311
|
+
|
|
312
|
+
### Share the secrets, not just Redis
|
|
313
|
+
|
|
314
|
+
Every instance behind the load balancer needs the **same** values:
|
|
315
|
+
|
|
316
|
+
- `MCP_SESSION_SECRET` -- session ids are encrypted with it. An id minted under a different secret is answered with HTTP 404 and the client re-initializes (every auth mode, including `public`, where the id is the caller's only credential). An anonymous session minted by one instance is honored by any instance with the same secret.
|
|
317
|
+
- `VAULT_SECRET` (or `JWT_SECRET`) -- signs MCP 2026-07-28 `requestState`. Without either, each instance uses a random per-process key and a multi-round tool (`elicit()` / `sample()`) whose next round lands elsewhere asks its first question again. In production, `redis` or `transport.persistence` without either secret logs a startup warning; each rejected round logs `mcp-20260728: rejected requestState` with `reason: 'bad-signature'` and a `hint` naming `VAULT_SECRET`.
|
|
318
|
+
|
|
319
|
+
### What happens when Redis is down at startup
|
|
320
|
+
|
|
321
|
+
- `redis` and `transport.persistence` fall back to in-memory storage and log the failure (`[TransportService] Failed to connect to redis - session persistence disabled`); the server starts.
|
|
322
|
+
- `throttle.storage` fails closed: startup aborts with `GuardStorageUnavailableError` (`throttle.storage (redis) is unavailable: …`), the default in production. Opt in to per-instance counters explicitly:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
throttle: {
|
|
326
|
+
enabled: true,
|
|
327
|
+
storage: {
|
|
328
|
+
type: 'redis',
|
|
329
|
+
redis: { config: { host: process.env['REDIS_HOST'] ?? 'localhost', port: 6379 } },
|
|
330
|
+
fallback: 'memory', // per-instance counters while Redis is down
|
|
331
|
+
},
|
|
332
|
+
},
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`throttle.storage` takes the `@frontmcp/utils` storage shape (`{ type: 'redis', redis: { config } }` or `{ type: 'redis', redis: { url } }`), not the top-level `redis` shape.
|
|
336
|
+
|
|
310
337
|
## Common Patterns
|
|
311
338
|
|
|
312
339
|
| Pattern | Recommended | Less explicit | Why |
|
|
@@ -340,13 +367,16 @@ You should see session keys like `mcp:session:<session-id>`.
|
|
|
340
367
|
|
|
341
368
|
## Troubleshooting
|
|
342
369
|
|
|
343
|
-
| Problem
|
|
344
|
-
|
|
|
345
|
-
| `ECONNREFUSED 127.0.0.1:6379`
|
|
346
|
-
| `NOAUTH Authentication required`
|
|
347
|
-
| `ERR max number of clients reached`
|
|
348
|
-
| Vercel KV `401 Unauthorized`
|
|
349
|
-
| Sessions lost after container restart
|
|
370
|
+
| Problem | Cause | Solution |
|
|
371
|
+
| ------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
372
|
+
| `ECONNREFUSED 127.0.0.1:6379` | Redis is not running or Docker container is stopped | Start the container with `docker compose up -d redis` or check the Redis service status |
|
|
373
|
+
| `NOAUTH Authentication required` | Password is set on Redis but not provided in config | Add `password` to the `redis` config or set `REDIS_PASSWORD` environment variable |
|
|
374
|
+
| `ERR max number of clients reached` | Too many open connections from the application | Set `maxRetriesPerRequest` or use connection pooling; check for connection leaks |
|
|
375
|
+
| Vercel KV `401 Unauthorized` | Missing or invalid KV tokens in the environment | Verify `KV_REST_API_URL` and `KV_REST_API_TOKEN` in the Vercel dashboard and redeploy |
|
|
376
|
+
| Sessions lost after container restart | Redis running without append-only persistence | Add `--appendonly yes` to the Redis command in docker-compose or use a managed Redis with persistence enabled |
|
|
377
|
+
| Startup fails with `GuardStorageUnavailableError` | `throttle.storage` Redis is unreachable; rate limits fail closed | Bring Redis up, or set `throttle.storage.fallback: 'memory'` to start with per-instance counters |
|
|
378
|
+
| Clients get 404 for a session they just used (load-balanced) | Instances run with different `MCP_SESSION_SECRET` values | Set the same `MCP_SESSION_SECRET` on every instance |
|
|
379
|
+
| A 2026-07-28 tool repeats its first question; log shows `rejected requestState` / `bad-signature` | `VAULT_SECRET`/`JWT_SECRET` unset or different per instance | Set `VAULT_SECRET` to the same value on every instance |
|
|
350
380
|
|
|
351
381
|
## Examples
|
|
352
382
|
|
|
@@ -275,6 +275,7 @@ test.use({
|
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
test('server exposes expected tools', async ({ mcp }) => {
|
|
278
|
+
// Every page: `list()` follows `nextCursor`, so this holds past 40 tools too.
|
|
278
279
|
const tools = await mcp.tools.list();
|
|
279
280
|
expect(tools).toContainTool('create_record');
|
|
280
281
|
expect(tools).toContainTool('delete_record');
|
|
@@ -586,7 +587,7 @@ node scripts/fix-unused-imports.mjs feature/my-branch
|
|
|
586
587
|
### E2E Tests
|
|
587
588
|
|
|
588
589
|
- [ ] Fixture-based tests use `test.use({ server, port })` for server lifecycle
|
|
589
|
-
- [ ] Tools appear in `tools/list` response via `toContainTool()` matcher
|
|
590
|
+
- [ ] Tools appear in `tools/list` response via `toContainTool()` matcher (`mcp.tools.list()` and the other `list()` calls return every page, so no cursor handling is needed)
|
|
590
591
|
- [ ] Tool calls return expected results via `toBeSuccessful()` matcher
|
|
591
592
|
- [ ] Authenticated tests use `TestTokenFactory` and verify rejection without token
|
|
592
593
|
|
|
@@ -12,6 +12,7 @@ Real API references:
|
|
|
12
12
|
- `TestServer.start({ command, port })` returns a `TestServer` instance with `info.baseUrl` and `stop()` — `libs/testing/src/server/test-server.ts:101`. There is no `TestServer.create(ServerClass)`.
|
|
13
13
|
- Build a client with `await McpTestClient.create({ baseUrl }).withTransport('streamable-http').buildAndConnect()` — `libs/testing/src/client/mcp-test-client.builder.ts`.
|
|
14
14
|
- The public client API is namespaced: `client.tools.list()`, `client.tools.call(name, args)`, `client.resources.list()`, `client.resources.read(uri)`, `client.prompts.list()`, `client.prompts.get(name, args)`, `client.disconnect()` — `libs/testing/src/client/mcp-test-client.ts:306-402`.
|
|
15
|
+
- `tools.list()`, `resources.list()`, `resources.listTemplates()` and `prompts.list()` return **every page**: they follow `nextCursor` until the server stops returning one (a FrontMCP server pages at 40 by default), and throw if a cursor repeats or paging passes 1000 pages. Assert on the full list; there is no cursor to pass.
|
|
15
16
|
|
|
16
17
|
```typescript
|
|
17
18
|
// server.e2e.spec.ts
|
|
@@ -943,7 +943,8 @@
|
|
|
943
943
|
"Configuring `storage: { type: 'redis' }` so rate limit counters are shared across instances",
|
|
944
944
|
"Using `keyPrefix` to namespace guard keys in a shared Redis instance",
|
|
945
945
|
"Combining `partitionBy: 'ip'` for global limits with `partitionBy: 'session'` per tool",
|
|
946
|
-
"In-memory counters are per-process and would allow N times the intended rate with N instances"
|
|
946
|
+
"In-memory counters are per-process and would allow N times the intended rate with N instances",
|
|
947
|
+
"Startup fails closed with `GuardStorageUnavailableError` when Redis is down, unless `fallback: 'memory'`"
|
|
947
948
|
]
|
|
948
949
|
},
|
|
949
950
|
{
|
|
@@ -1412,6 +1413,7 @@
|
|
|
1412
1413
|
"tags": ["deployment", "vercel", "serverless", "config", "minimal"],
|
|
1413
1414
|
"features": [
|
|
1414
1415
|
"The exact shape of the auto-generated `vercel.json` \u2014 three keys, nothing else",
|
|
1416
|
+
"That `buildCommand` builds the vercel target (`<exec> frontmcp build --target vercel`), not the `build` script",
|
|
1415
1417
|
"That routing and function configuration live in `.vercel/output/`, not `vercel.json`",
|
|
1416
1418
|
"That hand-authoring `api/frontmcp.ts` references in `vercel.json` is unnecessary and breaks deploys"
|
|
1417
1419
|
]
|
|
@@ -2071,6 +2073,7 @@
|
|
|
2071
2073
|
"Using `toolPatterns` glob patterns to cache groups of tools without per-tool configuration",
|
|
2072
2074
|
"Per-tool `cache` metadata with custom `ttl` (seconds) and `slideWindow` for TTL refresh on hits",
|
|
2073
2075
|
"Using `cache: true` for simple default-TTL caching",
|
|
2076
|
+
"A cache hit returns the same `content`/`structuredContent` as the miss, marked by `_meta.cache: 'hit'` on the result",
|
|
2074
2077
|
"Gating a tool with `featureFlag: 'beta-search'` -- the tool is hidden from `list_tools` when the flag is off",
|
|
2075
2078
|
"Accessing `this.featureFlags.isEnabled()` inside a tool for runtime flag checks"
|
|
2076
2079
|
]
|
|
@@ -2099,8 +2102,8 @@
|
|
|
2099
2102
|
"tags": ["development", "session", "plugins", "remember", "plugin", "memory"],
|
|
2100
2103
|
"features": [
|
|
2101
2104
|
"Installing `RememberPlugin` with `type: 'memory'` for development",
|
|
2102
|
-
"Enabling `tools: { enabled: true }` to expose LLM-callable memory tools (`remember_this`, `recall`, etc.)",
|
|
2103
|
-
"Using `this.remember.set()` with default `session` scope and explicit `user` scope",
|
|
2105
|
+
"Enabling `tools: { enabled: true }` to expose LLM-callable memory tools (`remember_this`, `recall`, etc.), whose `scope` defaults to `session`",
|
|
2106
|
+
"Using `this.remember.set()` with default `session` scope and explicit `user` scope (refused for an anonymous caller)",
|
|
2104
2107
|
"Using `this.remember.get()` with a `defaultValue` fallback",
|
|
2105
2108
|
"Using `this.remember.knows()` to check key existence without retrieving the value"
|
|
2106
2109
|
]
|