@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.
Files changed (77) hide show
  1. package/README.md +107 -155
  2. package/catalog/create-tool/SKILL.md +24 -24
  3. package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +1 -1
  4. package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +0 -1
  5. package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +1 -3
  6. package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +4 -6
  7. package/catalog/create-tool/references/availability.md +10 -10
  8. package/catalog/create-tool/references/decorator-options.md +1 -1
  9. package/catalog/create-tool/references/ui-widgets.md +91 -42
  10. package/catalog/frontmcp-authorities/SKILL.md +5 -0
  11. package/catalog/frontmcp-channels/SKILL.md +17 -16
  12. package/catalog/frontmcp-channels/references/channel-sources.md +4 -4
  13. package/catalog/frontmcp-channels/references/channel-two-way.md +1 -1
  14. package/catalog/frontmcp-config/examples/configure-deployment-targets/distributed-ha-config.md +2 -0
  15. package/catalog/frontmcp-config/examples/configure-session/vercel-kv-session.md +1 -2
  16. package/catalog/frontmcp-config/examples/configure-throttle/distributed-redis-throttle.md +3 -3
  17. package/catalog/frontmcp-config/examples/configure-transport/custom-protocol-flags.md +0 -1
  18. package/catalog/frontmcp-config/examples/configure-transport/distributed-sessions-redis.md +0 -1
  19. package/catalog/frontmcp-config/examples/configure-transport/stateless-serverless.md +2 -3
  20. package/catalog/frontmcp-config/examples/configure-transport-protocol-presets/stateless-api-serverless.md +2 -3
  21. package/catalog/frontmcp-config/references/configure-auth.md +1 -1
  22. package/catalog/frontmcp-config/references/configure-deployment-targets.md +68 -16
  23. package/catalog/frontmcp-config/references/configure-http.md +11 -6
  24. package/catalog/frontmcp-config/references/configure-security-headers.md +18 -13
  25. package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +4 -4
  26. package/catalog/frontmcp-config/references/configure-throttle.md +4 -2
  27. package/catalog/frontmcp-config/references/configure-transport.md +4 -5
  28. package/catalog/frontmcp-deployment/SKILL.md +19 -19
  29. package/catalog/frontmcp-deployment/examples/build-for-browser/react-provider-setup.md +5 -3
  30. package/catalog/frontmcp-deployment/examples/deploy-to-node/docker-compose-with-redis.md +1 -1
  31. package/catalog/frontmcp-deployment/examples/deploy-to-node/pm2-with-nginx.md +1 -1
  32. package/catalog/frontmcp-deployment/examples/deploy-to-vercel/vercel-with-kv.md +2 -0
  33. package/catalog/frontmcp-deployment/references/build-for-browser.md +46 -9
  34. package/catalog/frontmcp-deployment/references/build-for-mcpb.md +15 -8
  35. package/catalog/frontmcp-deployment/references/build-for-sdk.md +11 -10
  36. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +5 -1
  37. package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +11 -10
  38. package/catalog/frontmcp-deployment/references/deploy-to-node.md +11 -11
  39. package/catalog/frontmcp-deployment/references/deploy-to-vercel.md +15 -7
  40. package/catalog/frontmcp-deployment/references/mcp-client-integration.md +12 -7
  41. package/catalog/frontmcp-deployment/references/protocol-versions.md +7 -3
  42. package/catalog/frontmcp-development/examples/create-agent/nested-agents-with-swarm.md +50 -10
  43. package/catalog/frontmcp-development/examples/openapi-adapter/ref-security-and-filtering.md +13 -7
  44. package/catalog/frontmcp-development/references/create-adapter.md +14 -0
  45. package/catalog/frontmcp-development/references/create-agent.md +82 -48
  46. package/catalog/frontmcp-development/references/create-plugin-hooks.md +15 -5
  47. package/catalog/frontmcp-development/references/create-plugin.md +23 -2
  48. package/catalog/frontmcp-development/references/create-skill-with-tools.md +7 -2
  49. package/catalog/frontmcp-development/references/create-skill.md +4 -0
  50. package/catalog/frontmcp-development/references/decorators-guide.md +10 -11
  51. package/catalog/frontmcp-development/references/official-adapters.md +1 -1
  52. package/catalog/frontmcp-development/references/official-plugins.md +138 -28
  53. package/catalog/frontmcp-development/references/openapi-adapter.md +52 -2
  54. package/catalog/frontmcp-guides/references/example-task-manager.md +2 -2
  55. package/catalog/frontmcp-guides/references/example-weather-api.md +2 -2
  56. package/catalog/frontmcp-observability/references/metrics-endpoint.md +3 -1
  57. package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +13 -1
  58. package/catalog/frontmcp-production-readiness/examples/production-node-sdk/package-json-config.md +2 -2
  59. package/catalog/frontmcp-production-readiness/references/common-checklist.md +3 -2
  60. package/catalog/frontmcp-production-readiness/references/distributed-ha.md +54 -24
  61. package/catalog/frontmcp-production-readiness/references/health-readiness-endpoints.md +3 -1
  62. package/catalog/frontmcp-setup/examples/nx-workflow/build-test-affected.md +2 -2
  63. package/catalog/frontmcp-setup/examples/nx-workflow/multi-server-deployment.md +9 -5
  64. package/catalog/frontmcp-setup/examples/nx-workflow/scaffold-and-generate.md +1 -1
  65. package/catalog/frontmcp-setup/examples/project-structure-nx/nx-generator-scaffolding.md +2 -2
  66. package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +28 -15
  67. package/catalog/frontmcp-setup/references/multi-app-composition.md +11 -8
  68. package/catalog/frontmcp-setup/references/nx-workflow.md +68 -28
  69. package/catalog/frontmcp-setup/references/project-structure-nx.md +7 -4
  70. package/catalog/frontmcp-setup/references/setup-project.md +15 -0
  71. package/catalog/frontmcp-setup/references/setup-redis.md +12 -3
  72. package/catalog/frontmcp-setup/references/setup-sqlite.md +21 -14
  73. package/catalog/frontmcp-testing/SKILL.md +28 -23
  74. package/catalog/frontmcp-testing/references/setup-testing.md +39 -2
  75. package/catalog/frontmcp-testing/references/test-auth.md +8 -0
  76. package/catalog/skills-manifest.json +14 -12
  77. package/package.json +1 -1
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: official-plugins
3
- description: Guide to the 6 official plugins for discovery, memory, auth, caching, flags, and monitoring
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 6 official plugins that extend server behavior with cross-cutting concerns: semantic tool discovery, session memory, authorization workflows, result caching, feature gating, and visual monitoring. Install individually or via `@frontmcp/plugins` (meta-package re-exporting cache, codecall, and remember).
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`. 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
+ - `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 ALL tools in the server. With `appIds`, only tools from the specified apps are hidden — tools from other apps remain directly callable.
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 (only discoverable via codecall:search)
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 `visibleInListTools` or search ranking to protect a tool.
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. `alwaysPrompt: true`: refused with state `pending`.
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
- 6. Otherwise refused with state `pending` (or `expired`).
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; a tool several plugins gate must
466
- pass each store's check, and a denial in any of them refuses the call.
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: `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
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. If the adapter is unavailable the gate uses the ref's
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 | Solution |
991
- | --------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
992
- | `this.remember` is undefined | RememberPlugin not registered or missing `.init()` | Add `RememberPlugin.init({ type: 'memory' })` to `plugins` array |
993
- | 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` |
994
- | 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 |
995
- | 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 |
996
- | Approval webhook times out | Callback URL not reachable from the external approval service | Verify `callbackPath` is publicly accessible and matches the webhook configuration |
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 path prefix
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": "^29.0.0",
35
- "ts-jest": "^29.0.0",
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": "^29.0.0",
34
- "ts-jest": "^29.0.0",
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
 
@@ -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
- # After ~30s, its heartbeat expires and sessions are claimed by surviving pods
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
@@ -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": "^29.0.0",
62
- "ts-jest": "^29.0.0",
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'` starts with per-instance counters instead
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