@frontmcp/skills 1.8.6 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -155
- package/catalog/create-tool/SKILL.md +24 -24
- package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +1 -1
- package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +0 -1
- package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +1 -3
- package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +4 -6
- package/catalog/create-tool/references/availability.md +10 -10
- package/catalog/create-tool/references/decorator-options.md +1 -1
- package/catalog/create-tool/references/ui-widgets.md +91 -42
- package/catalog/frontmcp-authorities/SKILL.md +5 -0
- package/catalog/frontmcp-channels/SKILL.md +17 -16
- package/catalog/frontmcp-channels/references/channel-sources.md +4 -4
- package/catalog/frontmcp-channels/references/channel-two-way.md +1 -1
- package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md +2 -0
- package/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md +1 -2
- package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +3 -3
- package/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md +0 -1
- package/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md +0 -1
- package/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md +2 -3
- package/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md +2 -3
- package/catalog/frontmcp-config/references/configure-auth.md +1 -1
- package/catalog/frontmcp-config/references/configure-deployment-targets.md +68 -16
- package/catalog/frontmcp-config/references/configure-http.md +11 -6
- package/catalog/frontmcp-config/references/configure-security-headers.md +18 -13
- package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +4 -4
- package/catalog/frontmcp-config/references/configure-throttle.md +4 -2
- package/catalog/frontmcp-config/references/configure-transport.md +4 -5
- package/catalog/frontmcp-deployment/SKILL.md +19 -19
- package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +5 -3
- package/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md +1 -1
- package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +2 -0
- package/catalog/frontmcp-deployment/references/build-for-browser.md +46 -9
- package/catalog/frontmcp-deployment/references/build-for-mcpb.md +15 -8
- package/catalog/frontmcp-deployment/references/build-for-sdk.md +11 -10
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +5 -1
- package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +11 -10
- package/catalog/frontmcp-deployment/references/deploy-to-node.md +11 -11
- package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +15 -7
- package/catalog/frontmcp-deployment/references/mcp-client-integration.md +12 -7
- package/catalog/frontmcp-deployment/references/protocol-versions.md +7 -3
- package/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md +50 -10
- package/catalog/frontmcp-development/examples/openapi-adapter/ref-security-and-filtering.md +13 -7
- package/catalog/frontmcp-development/references/create-adapter.md +14 -0
- package/catalog/frontmcp-development/references/create-agent.md +82 -48
- package/catalog/frontmcp-development/references/create-plugin-hooks.md +15 -5
- package/catalog/frontmcp-development/references/create-plugin.md +23 -2
- package/catalog/frontmcp-development/references/create-skill-with-tools.md +7 -2
- package/catalog/frontmcp-development/references/create-skill.md +4 -0
- package/catalog/frontmcp-development/references/decorators-guide.md +10 -11
- package/catalog/frontmcp-development/references/official-adapters.md +1 -1
- package/catalog/frontmcp-development/references/official-plugins.md +138 -28
- package/catalog/frontmcp-development/references/openapi-adapter.md +52 -2
- package/catalog/frontmcp-guides/references/example-task-manager.md +2 -2
- package/catalog/frontmcp-guides/references/example-weather-api.md +2 -2
- package/catalog/frontmcp-observability/references/metrics-endpoint.md +3 -1
- package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +13 -1
- package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md +2 -2
- package/catalog/frontmcp-production-readiness/references/common-checklist.md +3 -2
- package/catalog/frontmcp-production-readiness/references/distributed-ha.md +54 -24
- package/catalog/frontmcp-production-readiness/references/health-readiness-endpoints.md +3 -1
- package/catalog/frontmcp-setup/examples/nx-workflow/build-test-affected.md +2 -2
- package/catalog/frontmcp-setup/examples/nx-workflow/multi-server-deployment.md +9 -5
- package/catalog/frontmcp-setup/examples/nx-workflow/scaffold-and-generate.md +1 -1
- package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +2 -2
- package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +28 -15
- package/catalog/frontmcp-setup/references/multi-app-composition.md +11 -8
- package/catalog/frontmcp-setup/references/nx-workflow.md +68 -28
- package/catalog/frontmcp-setup/references/project-structure-nx.md +7 -4
- package/catalog/frontmcp-setup/references/setup-project.md +15 -0
- package/catalog/frontmcp-setup/references/setup-redis.md +12 -3
- package/catalog/frontmcp-setup/references/setup-sqlite.md +21 -14
- package/catalog/frontmcp-testing/SKILL.md +28 -23
- package/catalog/frontmcp-testing/references/setup-testing.md +39 -2
- package/catalog/frontmcp-testing/references/test-auth.md +8 -0
- package/catalog/skills-manifest.json +14 -12
- package/package.json +1 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: official-plugins
|
|
3
|
-
description: Guide to the
|
|
3
|
+
description: Guide to the 7 official plugins for discovery, memory, auth, caching, flags, monitoring, and WebMCP
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Official FrontMCP Plugins
|
|
7
7
|
|
|
8
|
-
FrontMCP ships
|
|
8
|
+
FrontMCP ships 7 official plugins that extend server behavior with cross-cutting concerns: semantic tool discovery, session memory, authorization workflows, result caching, feature gating, visual monitoring, and exposing in-browser tools to browser agents (WebMCP). Install individually or via `@frontmcp/plugins` (meta-package re-exporting cache, codecall, and remember).
|
|
9
9
|
|
|
10
10
|
> **Note:** The Dashboard plugin (`@frontmcp/plugin-dashboard`) is currently in **beta** and may not work correctly in all environments. It is not recommended for production use at this time.
|
|
11
11
|
|
|
@@ -16,6 +16,7 @@ FrontMCP ships 6 official plugins that extend server behavior with cross-cutting
|
|
|
16
16
|
- Installing and configuring any official FrontMCP plugin (CodeCall, Remember, Approval, Cache, Feature Flags)
|
|
17
17
|
- Adding session memory, tool caching, or authorization workflows to an existing server
|
|
18
18
|
- Integrating feature flag services (LaunchDarkly, Split.io, Unleash) to gate tools at runtime
|
|
19
|
+
- Exposing the tools of a FrontMCP server running in the browser to in-browser agents through WebMCP (`document.modelContext`)
|
|
19
20
|
|
|
20
21
|
### Recommended
|
|
21
22
|
|
|
@@ -98,7 +99,7 @@ class MyServer {}
|
|
|
98
99
|
|
|
99
100
|
### Modes
|
|
100
101
|
|
|
101
|
-
- `codecall_only` -- Hides all tools from `list_tools` except CodeCall meta-tools. All other tools are discovered only via `codecall:search
|
|
102
|
+
- `codecall_only` -- Hides all tools from `list_tools` except CodeCall meta-tools. All other tools are discovered only via `codecall:search` and reached only through CodeCall: a client's direct `tools/call` of a hidden tool is refused. Best when the server has a large number of tools and you want the AI to search-then-execute. When `appIds` is set, only tools from those apps are hidden — tools from other apps remain visible.
|
|
102
103
|
- `codecall_opt_in` -- Shows all tools in `list_tools` normally. Tools opt-in to CodeCall execution via metadata. Useful when only some tools benefit from orchestrated execution.
|
|
103
104
|
- `metadata_driven` -- Per-tool `metadata.codecall` controls visibility and CodeCall availability independently. Most granular control.
|
|
104
105
|
|
|
@@ -115,7 +116,19 @@ CodeCallPlugin.init({
|
|
|
115
116
|
});
|
|
116
117
|
```
|
|
117
118
|
|
|
118
|
-
Without `appIds`, `codecall_only` mode hides
|
|
119
|
+
Without `appIds`, `codecall_only` mode hides every tool the plugin judges: all tools of the server when it is installed on the server, or its own app's tools and those of apps without a CodeCall plugin of their own when it is installed on an app. With `appIds`, only tools from the specified apps are hidden — tools from other apps remain directly callable. An app with its own CodeCall plugin is judged by that plugin alone, in `list_tools` and on a direct `tools/call` alike; up to 1.8.7 another app's `codecall_only` plugin hid its tools from `list_tools` while its own plugin still let clients call them.
|
|
120
|
+
|
|
121
|
+
### Hidden Tools Are Not Directly Callable
|
|
122
|
+
|
|
123
|
+
A tool CodeCall hides from `list_tools` (in any mode: every non-meta tool in `codecall_only` unless it sets
|
|
124
|
+
`visibleInListTools: true`, any tool with `visibleInListTools: false` otherwise) is reachable only through CodeCall. A
|
|
125
|
+
client's direct `tools/call` of it -- MCP, an MCP Apps widget, an in-page WebMCP agent, `DirectMcpServer.callTool()` --
|
|
126
|
+
is answered exactly like a call of an unknown tool (`Tool "<name>" not found`), before its input is validated. Still
|
|
127
|
+
allowed: CodeCall's own calls (`codecall:execute`, `codecall:invoke`), server-side composition (`this.callTool()` from
|
|
128
|
+
a tool, agent or job), and the server's own system tools (such as `sendElicitationResult`). Give a tool that clients
|
|
129
|
+
or widgets call directly `codecall: { visibleInListTools: true }`. Up to 1.8.7 the tool was only missing from the
|
|
130
|
+
listing, and a client that knew its name ran it directly, past `includeTools`, `enabledInCodeCall`, the blocked
|
|
131
|
+
namespaces and `directCalls`.
|
|
119
132
|
|
|
120
133
|
### VM Presets
|
|
121
134
|
|
|
@@ -143,7 +156,7 @@ Control how individual tools interact with CodeCall:
|
|
|
143
156
|
@Tool({
|
|
144
157
|
name: 'my_tool',
|
|
145
158
|
codecall: {
|
|
146
|
-
visibleInListTools: false, // Hide from list_tools
|
|
159
|
+
visibleInListTools: false, // Hide from list_tools; reached only through CodeCall (direct tools/call refused)
|
|
147
160
|
enabledInCodeCall: true, // Available for execution via codecall:execute
|
|
148
161
|
tags: ['data', 'query'], // Extra indexing hints for semantic search
|
|
149
162
|
},
|
|
@@ -174,7 +187,7 @@ CodeCallPlugin.init({
|
|
|
174
187
|
- `tool.appId` names the owning app for the tools its adapters and plugins provide too, so `includeTools: (tool) => tool.appId !== 'admin'` withholds every tool of app `admin`.
|
|
175
188
|
- `codecall:searchSkills` and `codecall:searchKnowledge` run the SDK's `skills:filter` flow, so a skill a plugin withholds there (a flag-disabled skill, for one) is absent from both.
|
|
176
189
|
- `directCalls.allowedTools` and `directCalls.filter` only narrow the base policy; listing a withheld tool does not make it callable. Unlisted tools are refused.
|
|
177
|
-
- Hiding a tool from search is not the control; the refusal at execution is. Do not rely on
|
|
190
|
+
- Hiding a tool from search is not the control; the refusal at execution is. Do not rely on search ranking to protect a tool. Hiding it from `list_tools` (`visibleInListTools: false`) does also refuse a client's direct `tools/call` of it, but CodeCall's own surfaces then apply this policy.
|
|
178
191
|
- `includeTools` and `directCalls.filter` receive the same object, with the tool's `annotations` and declared `metadata` (`tool.metadata?.annotations` is the same object as `tool.annotations`). It is a deep read-only copy, so a filter cannot change what the next decision reads.
|
|
179
192
|
- Namespace bindings (`mail.send({...})` for a tool named `mail.send`) are AgentScript wrappers over `callTool()` inside the sandbox: they count toward `vm.maxSteps` and pass the rate limit and suspicious-sequence checks exactly like `callTool('mail.send', {...})`. A binding with no argument sends `{}`.
|
|
180
193
|
- `codecall:execute` results never include a `stack`, in any environment. In `runtime_error`, `syntax_error` and `tool_error` messages, stack frames are dropped and absolute paths (POSIX, Windows, UNC, `file:` URLs, quoted paths) become `[path]`; other URLs are kept.
|
|
@@ -277,11 +290,20 @@ class MyTool extends ToolContext {
|
|
|
277
290
|
// List keys matching pattern
|
|
278
291
|
const keys = await this.remember.list({ pattern: 'user:*' });
|
|
279
292
|
|
|
293
|
+
// Update a value, keeping its metadata (and its expiry, unless a new `ttl` is given)
|
|
294
|
+
await this.remember.update('theme', 'light');
|
|
295
|
+
|
|
280
296
|
return { content: [{ type: 'text', text: `Theme: ${theme}` }] };
|
|
281
297
|
}
|
|
282
298
|
}
|
|
283
299
|
```
|
|
284
300
|
|
|
301
|
+
`update(key, value, { ttl? })` returns `false` for a key that does not exist. Without a `ttl` the entry keeps its
|
|
302
|
+
current expiry, and `knows()` and `list()` stop reporting it once that passes, the same as `get()`; up to 1.8.7 an
|
|
303
|
+
entry updated without a `ttl` stayed in `knows()` and `list()` after it expired. `knows()` and `list()` read each entry
|
|
304
|
+
and check its own expiry, so they report exactly the keys `get()` returns a value for, even while the store still holds
|
|
305
|
+
an expired key for up to a second.
|
|
306
|
+
|
|
285
307
|
### Memory Scopes
|
|
286
308
|
|
|
287
309
|
- `session` -- Default scope. With a verified session, valid only for that session and cleared
|
|
@@ -346,6 +368,10 @@ clear the legacy prefixes manually if you want the storage back.
|
|
|
346
368
|
- `forget` -- Remove a stored value by key
|
|
347
369
|
- `list_memories` -- List all stored keys, optionally filtered by pattern
|
|
348
370
|
|
|
371
|
+
`tools.prefix` renames them (`prefix: 'memory_'` gives `memory_recall`, ...; each description names the prefixed siblings) and
|
|
372
|
+
`tools.allowedScopes` rejects any other `scope` (including the default `session` when a call omits it) with a public `REMEMBER_SCOPE_NOT_ALLOWED` error that lists the allowed scopes. With `enabled` unset or `false` none is registered.
|
|
373
|
+
With `RememberPlugin.init({ inject, useFactory })`, `tools` is read from the options the factory returns, at startup.
|
|
374
|
+
|
|
349
375
|
All four take an optional `scope` (default `session`) and describe it to the model the same way:
|
|
350
376
|
`session` is this session, or without one (stateless HTTP, MCP 2026-07-28) the signed-in caller
|
|
351
377
|
across its requests; `user` is the signed-in caller across all of its sessions; `tool` is the tool
|
|
@@ -431,12 +457,20 @@ authInfo.extra.approvalContext = { type: 'project', identifier: resolvedProjectI
|
|
|
431
457
|
2. A recorded **denial** for the caller (session, user, time-limited or context scope): refused
|
|
432
458
|
with state `denied`. A denial outranks pre-approved contexts and any approval.
|
|
433
459
|
3. The session context is one of `preApprovedContexts`: the tool runs.
|
|
434
|
-
4.
|
|
435
|
-
5. An approval for the caller that the tool's policy accepts: the tool runs. The caller's session,
|
|
460
|
+
4. An approval for the caller that the tool's policy accepts: the tool runs. The caller's session,
|
|
436
461
|
user, time-limited and context approvals all count (a context approval only when the session
|
|
437
462
|
carries that context); its scope must be in `allowedScopes`, and it must be younger than
|
|
438
|
-
`maxTtlMs`, however it was stored.
|
|
439
|
-
|
|
463
|
+
`maxTtlMs`, however it was stored. With `alwaysPrompt: true` the approval is used up by this
|
|
464
|
+
call (only one of two concurrent calls gets it), so the next call needs a new one.
|
|
465
|
+
5. Otherwise refused with state `pending` (or `expired`).
|
|
466
|
+
|
|
467
|
+
Releases up to 1.8.7 refused every call of an `alwaysPrompt` tool, approved or not. The built-in store
|
|
468
|
+
uses up an approval with the storage's atomic `deleteIfEquals()` (memory, Redis, Upstash, Vercel KV), so
|
|
469
|
+
a denial or new approval recorded in the meantime is kept; a backend without it (Cloudflare KV, the
|
|
470
|
+
filesystem, SQLite) has the approval deleted directly. A custom `ApprovalStore` should implement
|
|
471
|
+
`consumeApproval()` (delete exactly that record, and only while it is still stored, in one step; resolve
|
|
472
|
+
`true` only for the call that deleted it); without it the gate revokes the caller's approvals of the tool
|
|
473
|
+
instead.
|
|
440
474
|
|
|
441
475
|
A refused call throws `ApprovalRequiredError`; the client receives an error result whose text is
|
|
442
476
|
exactly the tool's `approvalMessage` (or the default `Tool "<full name>" requires approval to
|
|
@@ -462,8 +496,10 @@ Installed on an app, `ApprovalPlugin` gates that app's tools (including those it
|
|
|
462
496
|
plugins provide) against its own store, so two apps can each install it with separate stores. It
|
|
463
497
|
also gates, against its store, the `approval` tools of apps with no approval plugin of their own,
|
|
464
498
|
so such a tool never runs ungated because the plugin sits on another app (releases up to 1.8.2 ran
|
|
465
|
-
them for anyone). Installed on the server, it gates every tool
|
|
466
|
-
|
|
499
|
+
them for anyone). Installed on the server, it gates every tool. A tool several plugins gate (one on
|
|
500
|
+
the server, one on its app) is decided once over all their stores: an approval in any of them lets
|
|
501
|
+
it run -- one approval is enough, e.g. a grant through `this.approval` in the tool's app -- and a
|
|
502
|
+
denial in any of them refuses the call. Releases up to 1.8.7 required an approval in each store.
|
|
467
503
|
`this.approval` resolves the `ApprovalService` of the nearest `ApprovalPlugin` -- the one the
|
|
468
504
|
tool's own app installed, otherwise the server's -- so with two apps each installing it, a grant
|
|
469
505
|
or check in one app's tool uses that app's store. Releases up to 1.8.1 resolved the store of the
|
|
@@ -502,10 +538,12 @@ class DangerousActionTool extends ToolContext {
|
|
|
502
538
|
```
|
|
503
539
|
|
|
504
540
|
Each grant records its grantor in `grantedBy`. Without one, it is the signed-in caller whose tool
|
|
505
|
-
made the grant: `
|
|
506
|
-
|
|
507
|
-
`
|
|
508
|
-
|
|
541
|
+
made the grant: `{ source: 'user', identifier: '<user id>', method: 'implicit' }` (a tool that grants
|
|
542
|
+
through `this.approval` asked no one, so it is not an `'interactive'` answer; a tool that did ask passes
|
|
543
|
+
`grantedBy: userGrantor(<user id>)`). Without a signed-in user (no principal, or an anonymous `anon:`
|
|
544
|
+
subject) it is `{ source: 'user', method: 'implicit' }` with no identifier. `revokeApproval()` records
|
|
545
|
+
`revokedBy` the same way and `getRevocations(toolId)` reads it back (kept 24 hours). Releases up to 1.8.5
|
|
546
|
+
recorded both as `'policy'`; 1.8.6 recorded `'interactive'` and kept no `revokedBy`. Pass `grantedBy` to
|
|
509
547
|
record anything else; `userGrantor`'s third argument is an options object, not the method:
|
|
510
548
|
|
|
511
549
|
```typescript
|
|
@@ -627,7 +665,7 @@ class GlobalCacheServer {}
|
|
|
627
665
|
Enable caching on individual tools via the `cache` metadata field:
|
|
628
666
|
|
|
629
667
|
```typescript
|
|
630
|
-
// Enable caching with default TTL
|
|
668
|
+
// Enable caching with default TTL (no sliding window)
|
|
631
669
|
@Tool({ name: 'get_weather', cache: true })
|
|
632
670
|
class GetWeatherTool extends ToolContext {
|
|
633
671
|
/* ... */
|
|
@@ -664,6 +702,13 @@ CachePlugin.init({
|
|
|
664
702
|
|
|
665
703
|
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.
|
|
666
704
|
|
|
705
|
+
Only `slideWindow: true` refreshes the TTL on a hit (`ttl`, or the plugin's `defaultTTL` when the tool sets none).
|
|
706
|
+
`cache: true` means the plugin defaults and never slides: an entry expires its TTL after it was written, however often it
|
|
707
|
+
is read. Up to 1.8.7, `cache: true` slid on every hit and `{ slideWindow: true }` without a `ttl` never slid.
|
|
708
|
+
|
|
709
|
+
A result the tool returns with `isError: true` (a `CallToolResult` reporting a failure) is never cached, so the next
|
|
710
|
+
call runs the tool again; up to 1.8.7 the failure was served from the cache until the TTL ran out.
|
|
711
|
+
|
|
667
712
|
### Cache Bypass
|
|
668
713
|
|
|
669
714
|
Send the bypass header to skip caching for a specific request:
|
|
@@ -673,6 +718,13 @@ x-frontmcp-disable-cache: true
|
|
|
673
718
|
```
|
|
674
719
|
|
|
675
720
|
The header name is configurable via `bypassHeader` in the plugin options. Default: `'x-frontmcp-disable-cache'`.
|
|
721
|
+
It must start with `x-frontmcp-` (case-insensitive): the plugin reads it from the request context, which keeps only a
|
|
722
|
+
request's `x-frontmcp-*` headers. Any other name (`'x-no-cache'`) throws `CachePluginConfigurationError` when the
|
|
723
|
+
plugin is created; up to 1.8.7 it was accepted and silently ignored.
|
|
724
|
+
|
|
725
|
+
```typescript
|
|
726
|
+
CachePlugin.init({ type: 'memory', bypassHeader: 'x-frontmcp-no-cache' }); // client sends `x-frontmcp-no-cache: 1`
|
|
727
|
+
```
|
|
676
728
|
|
|
677
729
|
### Cache Key
|
|
678
730
|
|
|
@@ -711,7 +763,10 @@ A disabled flag filters the entry out of `tools/list`, `resources/list`, `prompt
|
|
|
711
763
|
|
|
712
764
|
That matters because a listing is not an access control. Clients cache listings and hold
|
|
713
765
|
resource URIs and prompt names from earlier sessions, so anything gated only at list time
|
|
714
|
-
stays reachable by name.
|
|
766
|
+
stays reachable by name. The refusal is a public `FeatureFlagDisabledError` (`FEATURE_FLAG_DISABLED`, 403)
|
|
767
|
+
that names the capability and the flag. `FeatureFlagPlugin.init()` with no (or an unknown) `adapter` throws a
|
|
768
|
+
`FeatureFlagConfigurationError` at startup, and so does `adapter: 'custom'` without an `adapterInstance` that has
|
|
769
|
+
`isEnabled()`, `getVariant()` and `evaluateFlags()` (it used to start and answer every request with a 500). If the adapter is unavailable the gate uses the ref's
|
|
715
770
|
`defaultValue`, and a bare string ref (no default) fails closed.
|
|
716
771
|
|
|
717
772
|
### Installation
|
|
@@ -789,7 +844,7 @@ class CustomFlagServer {}
|
|
|
789
844
|
- `splitio` -- Split.io integration. Requires `@splitsoftware/splitio` package.
|
|
790
845
|
- `launchdarkly` -- LaunchDarkly integration. Requires `launchdarkly-node-server-sdk` package.
|
|
791
846
|
- `unleash` -- Unleash integration. Requires `unleash-client` package.
|
|
792
|
-
- `custom` -- Provide your own adapter instance implementing the `FeatureFlagAdapter` interface.
|
|
847
|
+
- `custom` -- Provide your own adapter instance (`adapterInstance`, required) implementing the `FeatureFlagAdapter` interface.
|
|
793
848
|
|
|
794
849
|
### Using `this.featureFlags` in Tools
|
|
795
850
|
|
|
@@ -803,15 +858,24 @@ class BetaFeatureTool extends ToolContext {
|
|
|
803
858
|
return { content: [{ type: 'text', text: 'Feature not available' }] };
|
|
804
859
|
}
|
|
805
860
|
|
|
861
|
+
// Fail open: `true` when the adapter throws or has no answer for the flag
|
|
862
|
+
const engine = (await this.featureFlags.isEnabled('search-v2', true)) ? 'v2' : 'v1';
|
|
863
|
+
|
|
806
864
|
// Get variant value (for multivariate flags)
|
|
807
865
|
const variant = await this.featureFlags.getVariant('experiment-flag');
|
|
808
866
|
// variant may be 'control', 'treatment-a', 'treatment-b', etc.
|
|
809
867
|
|
|
810
|
-
return { content: [{ type: 'text', text: `Running variant: ${variant}` }] };
|
|
868
|
+
return { content: [{ type: 'text', text: `Running variant: ${variant} on search ${engine}` }] };
|
|
811
869
|
}
|
|
812
870
|
}
|
|
813
871
|
```
|
|
814
872
|
|
|
873
|
+
`isEnabled(key, defaultValue?)` answers `defaultValue` (else the plugin's `defaultValue`, else `false`) when the
|
|
874
|
+
adapter throws or has no answer for the flag -- a key the `static` adapter was not given, or one a custom adapter's
|
|
875
|
+
`evaluateFlags()` omits -- the same rule the gates apply to a ref's `defaultValue`. A flag the adapter answers keeps
|
|
876
|
+
its answer, `false` included. Split.io, LaunchDarkly and Unleash answer every key with the service's own default. Up to
|
|
877
|
+
1.8.7 the default applied only when the adapter threw, so `isEnabled('unknown-flag', true)` was `false`.
|
|
878
|
+
|
|
815
879
|
### Per-Tool Feature Flag Gating
|
|
816
880
|
|
|
817
881
|
Tools gated by a feature flag are automatically hidden from `list_tools` and blocked from execution when the flag is off:
|
|
@@ -823,7 +887,7 @@ class BetaTool extends ToolContext {
|
|
|
823
887
|
/* ... */
|
|
824
888
|
}
|
|
825
889
|
|
|
826
|
-
// Object with default value -- if flag evaluation fails, use the default
|
|
890
|
+
// Object with default value -- if flag evaluation fails or the flag is unknown, use the default
|
|
827
891
|
@Tool({
|
|
828
892
|
name: 'experimental_tool',
|
|
829
893
|
featureFlag: { key: 'experimental-flag', defaultValue: false },
|
|
@@ -953,6 +1017,51 @@ All official plugins use the static `init()` pattern inherited from `DynamicPlug
|
|
|
953
1017
|
class ProductionServer {}
|
|
954
1018
|
```
|
|
955
1019
|
|
|
1020
|
+
## 7. WebMCP Plugin (`@frontmcp/plugin-webmcp`)
|
|
1021
|
+
|
|
1022
|
+
Exposes the tools of a FrontMCP server that runs **in the page** (`create()` from `@frontmcp/sdk` / `@frontmcp/react`) to in-browser agents through [WebMCP](https://webmachinelearning.github.io/webmcp/) — `document.modelContext`, which Gemini in Chrome, the Model Context Tool Inspector extension and DevTools' WebMCP pane use. WebMCP is in origin trial in Chrome/Edge (149–162); for local development enable `chrome://flags/#enable-webmcp-testing`.
|
|
1023
|
+
|
|
1024
|
+
### Installation
|
|
1025
|
+
|
|
1026
|
+
```typescript
|
|
1027
|
+
import { WebMcpPlugin } from '@frontmcp/plugin-webmcp';
|
|
1028
|
+
import { create } from '@frontmcp/sdk';
|
|
1029
|
+
|
|
1030
|
+
const server = await create({
|
|
1031
|
+
info: { name: 'shop', version: '1.0.0' },
|
|
1032
|
+
tools: [SearchProducts, AddToCart],
|
|
1033
|
+
plugins: [WebMcpPlugin.init({ prefix: 'shop.' })], // always .init(), with or without options
|
|
1034
|
+
});
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
The plugin is a transport adapter: it lists tools through the `tools:list-tools` flow and runs every agent call through `tools:call-tool`, both on the `'webmcp'` call surface — so hooks, authorities, quota and `availableWhen` apply. It keeps the registrations in sync with the tool registry (including `server.registerTool()` and React `useDynamicTool` tools) and unregisters everything on `server.dispose()`. Without `document.modelContext` (Node, unsupported browsers) it does nothing.
|
|
1038
|
+
|
|
1039
|
+
### Options
|
|
1040
|
+
|
|
1041
|
+
| Option | Default | Description |
|
|
1042
|
+
| -------------- | ------------------------- | -------------------------------------------------------------------------- |
|
|
1043
|
+
| `prefix` | `''` | Prepended to every exposed name |
|
|
1044
|
+
| `include` | all | `(tool) => boolean`, runs after `availableWhen` and authorities |
|
|
1045
|
+
| `exposedTo` | — | Other origins (e.g. an iframe's parent) the tools are offered to |
|
|
1046
|
+
| `authContext` | anonymous `webmcp` caller | `DirectAuthContext` or a function returning it, resolved per list and call |
|
|
1047
|
+
| `modelContext` | `document.modelContext` | A polyfill or test double |
|
|
1048
|
+
|
|
1049
|
+
### Choosing what agents see
|
|
1050
|
+
|
|
1051
|
+
```typescript
|
|
1052
|
+
@Tool({ name: 'fill_checkout_form', availableWhen: { surface: ['webmcp'] } }) // browser agents only
|
|
1053
|
+
@Tool({ name: 'admin_reset', availableWhen: { surface: ['mcp'] } }) // never exposed through WebMCP
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
### Translation rules
|
|
1057
|
+
|
|
1058
|
+
- Names: `prefix + name`, characters outside `[A-Za-z0-9_.-]` become `_` (`app:tool` → `app_tool`), max 128, collisions get `_2`, `_3`, …
|
|
1059
|
+
- Annotations: `readOnlyHint` → `readOnlyHint`; explicit `destructiveHint: true` → `consequentialHint`; explicit `openWorldHint: true` → `untrustedContentHint`.
|
|
1060
|
+
- Results: `{ content, structuredContent? }` without `_meta`; an `isError` result or server error rejects with its message.
|
|
1061
|
+
- Tools only — resources and prompts stay MCP-only; elicitation is unavailable to WebMCP callers.
|
|
1062
|
+
|
|
1063
|
+
For other browsers, load a polyfill that installs `document.modelContext` (e.g. `@mcp-b/global`) before `create()`, or pass one as `modelContext`.
|
|
1064
|
+
|
|
956
1065
|
## Common Patterns
|
|
957
1066
|
|
|
958
1067
|
| Pattern | Correct | Incorrect | Why |
|
|
@@ -987,13 +1096,14 @@ class ProductionServer {}
|
|
|
987
1096
|
|
|
988
1097
|
## Troubleshooting
|
|
989
1098
|
|
|
990
|
-
| Problem | Cause
|
|
991
|
-
| --------------------------------- |
|
|
992
|
-
| `this.remember` is undefined | RememberPlugin not registered or missing `.init()`
|
|
993
|
-
| Cache not working for a tool | Tool name does not match any `toolPatterns` glob and `cache` metadata is not set
|
|
994
|
-
| Feature flag always returns false | Using `'static'` adapter with flag not in the `flags` map
|
|
995
|
-
| Dashboard returns 404 | Plugin is in beta and auto-disabled in production (`NODE_ENV=production`)
|
|
996
|
-
| Approval webhook times out | Callback URL not reachable from the external approval service
|
|
1099
|
+
| Problem | Cause | Solution |
|
|
1100
|
+
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
1101
|
+
| `this.remember` is undefined | RememberPlugin not registered or missing `.init()` | Add `RememberPlugin.init({ type: 'memory' })` to `plugins` array |
|
|
1102
|
+
| Cache not working for a tool | Tool name does not match any `toolPatterns` glob and `cache` metadata is not set | Add `cache: true` to `@Tool` decorator or add matching pattern to `toolPatterns` |
|
|
1103
|
+
| Feature flag always returns false | Using `'static'` adapter with flag not in the `flags` map | Add the flag key to `flags: { 'my-flag': true }` or check adapter connection |
|
|
1104
|
+
| Dashboard returns 404 | Plugin is in beta and auto-disabled in production (`NODE_ENV=production`) | Dashboard is unstable — avoid in production. For dev: set `enabled: true` explicitly |
|
|
1105
|
+
| Approval webhook times out | Callback URL not reachable from the external approval service | Verify `callbackPath` is publicly accessible and matches the webhook configuration |
|
|
1106
|
+
| WebMCP tools never appear | No `document.modelContext` (flag off, no origin-trial token, non-HTTPS, no polyfill), or `WebMcpPlugin` without `.init()` | Enable `chrome://flags/#enable-webmcp-testing` or load `@mcp-b/global`; check `isWebMcpSupported()`; use `WebMcpPlugin.init()` |
|
|
997
1107
|
|
|
998
1108
|
## Examples
|
|
999
1109
|
|
|
@@ -201,12 +201,60 @@ class IntegrationHub {}
|
|
|
201
201
|
// Tools: github:createIssue, jira:createTicket, slack:postMessage, etc.
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
+
## Options From Providers (`useFactory`)
|
|
205
|
+
|
|
206
|
+
Build the options at startup from injected providers. The factory returns `OpenApiAdapterOptions` (or a promise of them); the adapter is built from them under the `name` given to `init()`:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
OpenapiAdapter.init({
|
|
210
|
+
name: 'billing',
|
|
211
|
+
inject: () => [BillingConfig] as const,
|
|
212
|
+
useFactory: (config: BillingConfig) => ({ name: 'billing', url: config.specUrl, baseUrl: config.baseUrl }),
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Until 1.8.7 this form failed startup with `Cannot read properties of undefined (reading 'name')` plus an unhandled rejection that could end the Node process.
|
|
217
|
+
|
|
204
218
|
## Filtering Operations
|
|
205
219
|
|
|
206
|
-
Control which API operations become MCP tools:
|
|
220
|
+
Control which API operations become MCP tools. Every `generateOptions` field is passed to `mcp-from-openapi`'s generator, and an operation becomes a tool only when it passes every filter you set:
|
|
207
221
|
|
|
208
222
|
```typescript
|
|
209
|
-
// Filter by
|
|
223
|
+
// Filter by OpenAPI tag
|
|
224
|
+
OpenapiAdapter.init({
|
|
225
|
+
name: 'billing-api',
|
|
226
|
+
url: 'https://api.example.com/openapi.json',
|
|
227
|
+
generateOptions: {
|
|
228
|
+
includeTags: ['invoices', 'customers'], // carries one of these tags
|
|
229
|
+
excludeTags: ['internal'], // carries none of these
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Filter by path glob (`*` = within a segment, `**` = across segments, `?` = one character)
|
|
234
|
+
OpenapiAdapter.init({
|
|
235
|
+
name: 'billing-api',
|
|
236
|
+
url: 'https://api.example.com/openapi.json',
|
|
237
|
+
generateOptions: {
|
|
238
|
+
includePaths: ['/invoices/**', '/customers/*'],
|
|
239
|
+
excludePaths: ['/invoices/*/admin/**'],
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Read-only tools only (annotations say readOnlyHint: true — GET/HEAD/OPTIONS/TRACE unless overridden)
|
|
244
|
+
OpenapiAdapter.init({
|
|
245
|
+
name: 'billing-api',
|
|
246
|
+
url: 'https://api.example.com/openapi.json',
|
|
247
|
+
generateOptions: { readOnlyOnly: true },
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Filter by HTTP method (lower-case names)
|
|
251
|
+
OpenapiAdapter.init({
|
|
252
|
+
name: 'billing-api',
|
|
253
|
+
url: 'https://api.example.com/openapi.json',
|
|
254
|
+
generateOptions: { excludeMethods: ['delete', 'put'] }, // or includeMethods: ['get']
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// Custom filter (runs after every other filter)
|
|
210
258
|
OpenapiAdapter.init({
|
|
211
259
|
name: 'billing-api',
|
|
212
260
|
url: 'https://api.example.com/openapi.json',
|
|
@@ -234,6 +282,8 @@ OpenapiAdapter.init({
|
|
|
234
282
|
});
|
|
235
283
|
```
|
|
236
284
|
|
|
285
|
+
The adapter's own defaults are `preferredStatusCodes: [200, 201, 202, 204]`, `includeDeprecated: false` and `includeAllResponses: true`; every other generator option (`maxToolNameLength`, `descriptionStrategy`, `target`, …) takes the value you set.
|
|
286
|
+
|
|
237
287
|
## Input Transforms
|
|
238
288
|
|
|
239
289
|
Hide inputs from AI/users and inject values server-side:
|
|
@@ -31,8 +31,8 @@ An authenticated task management MCP server with CRUD tools, a Redis-backed prov
|
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@frontmcp/testing": "^1.0.0",
|
|
34
|
-
"jest": "^
|
|
35
|
-
"ts-jest": "^29.
|
|
34
|
+
"jest": "^30.0.0",
|
|
35
|
+
"ts-jest": "^29.4.0",
|
|
36
36
|
"typescript": "^5.4.0",
|
|
37
37
|
},
|
|
38
38
|
}
|
|
@@ -30,8 +30,8 @@ A complete beginner MCP server that exposes a weather lookup tool and a static r
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@frontmcp/testing": "^1.0.0",
|
|
33
|
-
"jest": "^
|
|
34
|
-
"ts-jest": "^29.
|
|
33
|
+
"jest": "^30.0.0",
|
|
34
|
+
"ts-jest": "^29.4.0",
|
|
35
35
|
"typescript": "^5.4.0",
|
|
36
36
|
},
|
|
37
37
|
}
|
|
@@ -29,6 +29,8 @@ class Server {}
|
|
|
29
29
|
|
|
30
30
|
Then scrape: `curl http://localhost:3000/metrics` (the default port is `PORT`, else 3000) — Content-Type is the canonical Prometheus `text/plain; version=0.0.4; charset=utf-8`.
|
|
31
31
|
|
|
32
|
+
`FrontMcpInstance.createFetchHandler(config)` (the Web-standard `(Request) => Response` handler) answers `GET /metrics` too — same body, auth and headers as the Express listener (it needs `@frontmcp/observability` installed, like the Express endpoint).
|
|
33
|
+
|
|
32
34
|
## Configuration
|
|
33
35
|
|
|
34
36
|
```typescript
|
|
@@ -105,7 +107,7 @@ Keep label values bounded (status codes, enum members, tool names) — unbounded
|
|
|
105
107
|
|
|
106
108
|
## Path conflict guard
|
|
107
109
|
|
|
108
|
-
`metrics.path` MUST NOT collide with MCP transport paths (`/mcp`, `/sse`, `/messages`). The service constructor throws `MetricsPathConflictError` at startup if it detects an overlap.
|
|
110
|
+
`metrics.path` MUST NOT collide with MCP transport paths (`/mcp`, `/sse`, `/messages`). The service constructor throws `MetricsPathConflictError` at startup if it detects an overlap. `createFetchHandler()` also throws `MetricsPathConflictError` when `metrics.path` equals its MCP entry path (`http.entryPath`, `/` when unset), since the metrics route would otherwise answer the MCP `GET` that opens the event stream.
|
|
109
111
|
|
|
110
112
|
## Common Patterns
|
|
111
113
|
|
package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md
CHANGED
|
@@ -51,6 +51,7 @@ class MainApp {}
|
|
|
51
51
|
provider: 'redis',
|
|
52
52
|
host: process.env['REDIS_HOST'] || 'redis',
|
|
53
53
|
port: 6379,
|
|
54
|
+
defaultTtlMs: 30 * 60_000, // slides while the owning pod serves the session; default 1 hour
|
|
54
55
|
},
|
|
55
56
|
},
|
|
56
57
|
},
|
|
@@ -134,6 +135,12 @@ spec:
|
|
|
134
135
|
value: distributed
|
|
135
136
|
- name: REDIS_HOST
|
|
136
137
|
value: redis
|
|
138
|
+
# Same secret on every pod: session ids are encrypted with it.
|
|
139
|
+
- name: MCP_SESSION_SECRET
|
|
140
|
+
valueFrom:
|
|
141
|
+
secretKeyRef:
|
|
142
|
+
name: mcp-server
|
|
143
|
+
key: session-secret
|
|
137
144
|
ports:
|
|
138
145
|
- containerPort: 3000
|
|
139
146
|
livenessProbe:
|
|
@@ -210,9 +217,14 @@ kubectl exec -it deploy/redis -- redis-cli KEYS "mcp:ha:heartbeat:*"
|
|
|
210
217
|
# 2) "mcp:ha:heartbeat:mcp-server-7b8f9-def34"
|
|
211
218
|
# 3) "mcp:ha:heartbeat:mcp-server-7b8f9-ghi56"
|
|
212
219
|
|
|
220
|
+
# Every pod listens on its relay channel: a request that reaches a pod which
|
|
221
|
+
# does not own the session is relayed to the owner and answered from there.
|
|
222
|
+
kubectl exec -it deploy/redis -- redis-cli PUBSUB CHANNELS "mcp:ha:notify:*"
|
|
223
|
+
|
|
213
224
|
# Kill a pod and watch takeover
|
|
214
225
|
kubectl delete pod mcp-server-7b8f9-abc12
|
|
215
|
-
#
|
|
226
|
+
# Until its heartbeat expires (~30s) its sessions answer 503 + Retry-After;
|
|
227
|
+
# then surviving pods take them over and serve them.
|
|
216
228
|
```
|
|
217
229
|
|
|
218
230
|
## What This Demonstrates
|
package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md
CHANGED
|
@@ -58,8 +58,8 @@ Shows the correct package.json configuration for publishing a FrontMCP SDK packa
|
|
|
58
58
|
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@frontmcp/testing": "^1.0.0",
|
|
61
|
-
"jest": "^
|
|
62
|
-
"ts-jest": "^29.
|
|
61
|
+
"jest": "^30.0.0",
|
|
62
|
+
"ts-jest": "^29.4.0",
|
|
63
63
|
"typescript": "^5.4.0",
|
|
64
64
|
"zod": "^4.0.0",
|
|
65
65
|
},
|
|
@@ -36,7 +36,8 @@ These checks apply to ALL deployment targets. Run them first, then proceed to yo
|
|
|
36
36
|
- [ ] `security.dnsRebindingProtection.allowedHosts` (or `FRONTMCP_ALLOWED_HOSTS`) names the public
|
|
37
37
|
hostname(s) — on a routable bind the derived default is **not** enforced, and FrontMCP logs a
|
|
38
38
|
warning saying so
|
|
39
|
-
- [ ] Startup logs show no `DNS-rebinding protection is not enforcing a Host allow-list` warning
|
|
39
|
+
- [ ] Startup logs show no `DNS-rebinding protection is not enforcing a Host allow-list` warning, and
|
|
40
|
+
the production audit reports `[Security] DNS_REBINDING_PROTECTED` (not `DNS_REBINDING_NOT_ENFORCED`)
|
|
40
41
|
- [ ] Include the port when the public URL uses a non-default one (`api.example.com:8443`)
|
|
41
42
|
- [ ] `allowedOrigins` is set when a browser client connects, so a foreign `Origin` is refused
|
|
42
43
|
|
|
@@ -63,7 +64,7 @@ These checks apply to ALL deployment targets. Run them first, then proceed to yo
|
|
|
63
64
|
- [ ] Per-client/per-IP limits are set
|
|
64
65
|
- [ ] Throttle configuration uses `@FrontMcp({ throttle: {...} })`
|
|
65
66
|
- [ ] 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'`
|
|
67
|
+
- [ ] Decided what happens when the throttle Redis is down, at startup or mid-run: the default fails closed (`GuardStorageUnavailableError`); `throttle.storage.fallback: 'memory'` uses per-instance counters instead
|
|
67
68
|
- [ ] Large payload limits are set to prevent memory exhaustion
|
|
68
69
|
|
|
69
70
|
### Dependencies
|