@frontmcp/skills 1.8.7 → 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/examples/21-tool-with-availability-constraints.md +1 -1
- package/catalog/create-tool/references/availability.md +10 -10
- package/catalog/create-tool/references/ui-widgets.md +30 -8
- 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-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 +54 -20
- package/catalog/frontmcp-config/references/configure-http.md +5 -2
- package/catalog/frontmcp-config/references/configure-throttle-guard-config.md +1 -1
- 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/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/references/build-for-browser.md +38 -9
- package/catalog/frontmcp-deployment/references/build-for-mcpb.md +15 -9
- package/catalog/frontmcp-deployment/references/build-for-sdk.md +2 -1
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare.md +3 -1
- package/catalog/frontmcp-deployment/references/deploy-to-lambda.md +2 -2
- package/catalog/frontmcp-deployment/references/deploy-to-node.md +11 -11
- 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 -49
- package/catalog/frontmcp-development/references/create-plugin-hooks.md +15 -5
- package/catalog/frontmcp-development/references/create-plugin.md +8 -4
- 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 +127 -24
- package/catalog/frontmcp-development/references/openapi-adapter.md +52 -2
- package/catalog/frontmcp-observability/references/metrics-endpoint.md +3 -1
- package/catalog/frontmcp-production-readiness/examples/distributed-ha/ha-kubernetes-3-replicas.md +12 -1
- package/catalog/frontmcp-production-readiness/references/common-checklist.md +2 -1
- package/catalog/frontmcp-production-readiness/references/distributed-ha.md +48 -28
- 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 +1 -1
- 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 +53 -21
- 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 +12 -8
- package/catalog/frontmcp-testing/SKILL.md +16 -12
- package/catalog/frontmcp-testing/references/setup-testing.md +14 -1
- package/catalog/skills-manifest.json +10 -8
- package/package.json +1 -1
|
@@ -62,6 +62,9 @@ These are the flow names with pre-built hook decorator exports in `@frontmcp/sdk
|
|
|
62
62
|
| `resources:read-resource` | Resource reading | `ResourceHook` |
|
|
63
63
|
| `resources:list-resources` | Resource listing | `ListResourcesHook` |
|
|
64
64
|
| `resources:list-resource-templates` | Resource template listing | `ListResourceTemplatesHook` |
|
|
65
|
+
| `prompts:get-prompt` | Prompt retrieval | `PromptHook` |
|
|
66
|
+
| `prompts:list-prompts` | Prompt listing | `ListPromptsHook` |
|
|
67
|
+
| `completion:complete` | Argument completion | `CompletionHook` |
|
|
65
68
|
| `agents:call-agent` | Agent invocation | `AgentCallHook` |
|
|
66
69
|
| `channels:send-notification` | Channel notification send | `ChannelSendHook` |
|
|
67
70
|
| `channels:list` | Channel listing | `ChannelListHook` |
|
|
@@ -152,10 +155,13 @@ import {
|
|
|
152
155
|
AgentCallHook, // FlowHooksOf('agents:call-agent')
|
|
153
156
|
ChannelListHook, // FlowHooksOf('channels:list')
|
|
154
157
|
ChannelSendHook, // FlowHooksOf('channels:send-notification')
|
|
158
|
+
CompletionHook, // FlowHooksOf('completion:complete')
|
|
155
159
|
HttpHook, // FlowHooksOf('http:request')
|
|
160
|
+
ListPromptsHook, // FlowHooksOf('prompts:list-prompts')
|
|
156
161
|
ListResourcesHook, // FlowHooksOf('resources:list-resources')
|
|
157
162
|
ListResourceTemplatesHook, // FlowHooksOf('resources:list-resource-templates')
|
|
158
163
|
ListToolsHook, // FlowHooksOf('tools:list-tools')
|
|
164
|
+
PromptHook, // FlowHooksOf('prompts:get-prompt')
|
|
159
165
|
ResourceHook, // FlowHooksOf('resources:read-resource')
|
|
160
166
|
ToolHook, // FlowHooksOf('tools:call-tool')
|
|
161
167
|
} from '@frontmcp/sdk';
|
|
@@ -167,7 +173,7 @@ Usage:
|
|
|
167
173
|
const { Will, Did, Around, Stage } = ToolHook;
|
|
168
174
|
```
|
|
169
175
|
|
|
170
|
-
> **Note:** Other
|
|
176
|
+
> **Note:** Other flows (e.g., `skills:filter`, transport flows) can be hooked by passing the flow name to `FlowHooksOf('flow:name')`. Prefer the pre-built exports above when one is available: exporting them is also what puts a flow's types in the published package, which is why `FlowHooksOf('prompts:get-prompt')`, `'prompts:list-prompts'` and `'completion:complete'` did not typecheck in consumer projects up to 1.8.7.
|
|
171
177
|
|
|
172
178
|
## call-tool Flow Stages
|
|
173
179
|
|
|
@@ -310,7 +316,9 @@ export class MyApp {}
|
|
|
310
316
|
|
|
311
317
|
Plugins are initialized in array order. Hook priority determines execution order within the same stage.
|
|
312
318
|
|
|
313
|
-
Hooks declared on an app's providers, on its plugins (including plugins nested inside them), and on those plugins' providers run only for that app's tools, resources and prompts (`tools:call-tool`, `resources:read-resource`, `prompts:get-prompt`, `completion:complete`), including the ones its adapters and plugins provide, such as the tools an OpenAPI adapter generates. Plugins registered on the server (`@FrontMcp({ plugins })`) apply to every app. Resources and prompts the server serves outside every app, such as the SEP-2640 `skill://` resources, run every app's hooks.
|
|
319
|
+
Hooks declared on an app's providers, on its plugins (including plugins nested inside them), and on those plugins' providers run only for that app's tools, resources and prompts (`tools:call-tool`, `resources:read-resource`, `prompts:get-prompt`, `completion:complete`), including the ones its adapters and plugins provide, such as the tools an OpenAPI adapter generates. Plugins and providers registered on the server (`@FrontMcp({ plugins, providers })`) apply to every app. Resources and prompts the server serves outside every app, such as the SEP-2640 `skill://` resources, run every app's hooks.
|
|
320
|
+
|
|
321
|
+
A hook declared on a `CONTEXT`-scoped provider (`@Provider({ scope: ProviderScope.CONTEXT })`, a class provider) runs on the instance built for the request or session -- the same instance the request's tools get from `this.get()`. Up to 1.8.7, hooks on server-level and `CONTEXT`-scoped providers were never registered.
|
|
314
322
|
|
|
315
323
|
## Using Hooks Inside a @Tool Class
|
|
316
324
|
|
|
@@ -377,11 +385,12 @@ class ProcessOrderTool extends ToolContext {
|
|
|
377
385
|
### Available Stages for Tool Hooks
|
|
378
386
|
|
|
379
387
|
```
|
|
380
|
-
parseInput → findTool → checkToolAuthorization →
|
|
381
|
-
→
|
|
388
|
+
parseInput → ensureRemoteCapabilities → findTool → checkToolAuthorization → checkEntryAuthorities
|
|
389
|
+
→ createTaskIfRequested → createToolCallContext → checkToolCredentials → acquireQuota → acquireSemaphore
|
|
390
|
+
→ validateInput → execute → validateOutput → releaseSemaphore → releaseQuota → applyUI → finalize
|
|
382
391
|
```
|
|
383
392
|
|
|
384
|
-
|
|
393
|
+
A tool-class hook runs on the tool instance, which `createToolCallContext` builds. So it can hook `Did`/`Stage` on `createToolCallContext` and any hook on a later stage. A hook on an earlier stage, `Will`/`Around` on `createToolCallContext`, or a list-flow hook (`ListToolsHook`; listing builds no instance) fails startup with `InvalidHookFlowError` -- put those on a plugin or a provider. The same holds for `@Resource` (`createResourceContext`), `@Prompt` (`createPromptContext`) and `@Agent` (`createAgentContext`) classes. `@Job` classes cannot declare hooks at all (jobs do not run through a hookable flow); hook `tools:call-tool` for the `execute_job` tool instead. Up to 1.8.7 these hooks were accepted and silently never ran.
|
|
385
394
|
|
|
386
395
|
## Common Patterns
|
|
387
396
|
|
|
@@ -415,6 +424,7 @@ Any stage can have `@Will`, `@Did`, `@Stage`, or `@Around` hooks.
|
|
|
415
424
|
| Problem | Cause | Solution |
|
|
416
425
|
| --------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------- |
|
|
417
426
|
| Hook never fires | Plugin not registered in `plugins` array | Add plugin class to `@App` or `@FrontMcp` `plugins` array |
|
|
427
|
+
| `InvalidHookFlowError` at startup | Entry-class hook that could never run | Move early-stage, list-flow and `@Job` hooks to a plugin or a provider |
|
|
418
428
|
| Hook fires for wrong flow | Used wrong flow name in `FlowHooksOf` | Verify flow name matches (e.g., `'tools:call-tool'` not `'tool:call'`) |
|
|
419
429
|
| `@Around` skips the stage entirely | `next()` not called inside the around handler | Always `await next()` to execute the wrapped stage |
|
|
420
430
|
| Multiple hooks execute in wrong order | Priorities not set or conflicting | Set explicit `priority` values; lower numbers execute first |
|
|
@@ -5,7 +5,7 @@ description: Build plugins with providers, context extensions, lifecycle hooks,
|
|
|
5
5
|
|
|
6
6
|
# Create a FrontMCP Plugin
|
|
7
7
|
|
|
8
|
-
This skill covers building custom plugins for FrontMCP and using all
|
|
8
|
+
This skill covers building custom plugins for FrontMCP and using all 7 official plugins. Plugins are modular units that extend server behavior through providers, context extensions, lifecycle hooks, and contributed tools/resources/prompts.
|
|
9
9
|
|
|
10
10
|
## When to Use This Skill
|
|
11
11
|
|
|
@@ -332,7 +332,9 @@ The reverse does not work: an option-derived provider cannot inject a provider t
|
|
|
332
332
|
|
|
333
333
|
### Options named like plugin metadata, and option-derived tools
|
|
334
334
|
|
|
335
|
-
`init(options)` spreads the options into the plugin's metadata, so an option named like a list-valued metadata key (`tools`, `resources`, `prompts`, `skills`, `adapters`, `plugins`, `exports`) used to be read as that list: `RememberPlugin.init({ tools: { enabled: true } })` crashed at startup. A non-array value under one of those keys is now an option and stays out of the metadata; an array still contributes.
|
|
335
|
+
`init(options)` spreads the options into the plugin's metadata, so an option named like a list-valued metadata key (`tools`, `resources`, `prompts`, `skills`, `adapters`, `plugins`, `exports`, `contextExtensions`, `enforcesMetadata`) used to be read as that list: `RememberPlugin.init({ tools: { enabled: true } })` crashed at startup. A non-array value under one of those keys is now an option and stays out of the metadata; an array still contributes.
|
|
336
|
+
|
|
337
|
+
`providers` follows the same rule: an array adds providers to the plugin; any other value (`MyPlugin.init({ providers: { region: 'eu' } })`) is the plugin's own option and reaches the instance instead of throwing `(extraProviders ?? []) is not iterable` at module load. When the options type declares `providers`, `init()` types the key as that option.
|
|
336
338
|
|
|
337
339
|
To register tools only when an option asks for it, declare `static dynamicTools(options)`, the counterpart of `dynamicProviders`. Its tools are added to those from `@Plugin({ tools })` and from an array `tools` option:
|
|
338
340
|
|
|
@@ -343,7 +345,7 @@ export default class MemoryPlugin extends DynamicPlugin<MemoryOptions, MemoryOpt
|
|
|
343
345
|
}
|
|
344
346
|
```
|
|
345
347
|
|
|
346
|
-
`dynamicTools` runs
|
|
348
|
+
`dynamicTools`, like `dynamicProviders`, runs on the options the plugin is built with: those given to `init(options)`, or the ones an `init({ inject, useFactory })` factory returns at startup. `RememberPlugin.init({ inject, useFactory: () => ({ type: 'memory', tools: { enabled: true } }) })` therefore registers the memory tools.
|
|
347
349
|
|
|
348
350
|
### Installing the same plugin in several apps
|
|
349
351
|
|
|
@@ -357,6 +359,8 @@ class BillingApp {}
|
|
|
357
359
|
class OpsApp {}
|
|
358
360
|
```
|
|
359
361
|
|
|
362
|
+
An app that does not install the plugin does not get its providers: there, `this.get(Token)` throws and `this.tryGet(Token)` returns `undefined`. Install the plugin on the server (`@FrontMcp({ plugins })`) to share it with every app. Up to 1.8.7 the providers a plugin derives from its options (`dynamicProviders(options)`, `init({ providers })`) leaked to every other app on the server.
|
|
363
|
+
|
|
360
364
|
## Step 5: Extend Metadata and Execution Context
|
|
361
365
|
|
|
362
366
|
FrontMCP provides two extension mechanisms for plugins: **metadata augmentation** (add fields to decorators) and **context extensions** (add properties to `this` in tools/resources/prompts).
|
|
@@ -475,7 +479,7 @@ export { MyServiceToken } from './my-plugin.symbols';
|
|
|
475
479
|
|
|
476
480
|
## Official Plugins
|
|
477
481
|
|
|
478
|
-
For official plugin installation, configuration, and examples, see the **official-plugins** skill. FrontMCP provides
|
|
482
|
+
For official plugin installation, configuration, and examples, see the **official-plugins** skill. FrontMCP provides 7 official plugins: CodeCall, Remember, Approval, Cache, Feature Flags, Dashboard, and WebMCP. Install individually or via `@frontmcp/plugins` (meta-package).
|
|
479
483
|
|
|
480
484
|
## Recommended Folder Structure
|
|
481
485
|
|
|
@@ -82,7 +82,9 @@ The `tools` array in `@Skill` metadata supports three ways to reference tools th
|
|
|
82
82
|
|
|
83
83
|
### 1. Class Reference
|
|
84
84
|
|
|
85
|
-
Pass the tool class directly. The framework resolves the tool name and validates it exists in the registry.
|
|
85
|
+
Pass the tool class directly (or `{ tool: ToolClass, purpose?, required? }`). The framework resolves the tool name (the tool's `id` when it declares one, else its `name`) and validates it exists in the registry. The class is only a reference: register the tool in the `tools` of the `@App` or `@FrontMcp` too. A class that isn't a `@Tool` stops the server from starting with `Invalid tool class '<ClassName>'`.
|
|
86
|
+
|
|
87
|
+
Up to 1.8.7 a class reference itself stopped the server from starting (the class lost its `@Tool` name while the skill's options were validated); on those versions, reference the tool by name.
|
|
86
88
|
|
|
87
89
|
```typescript
|
|
88
90
|
@Skill({
|
|
@@ -170,6 +172,8 @@ class StrictWorkflowSkill extends SkillContext {}
|
|
|
170
172
|
| `'warn'` | Logs a warning for missing tools but continues. Use during development when tools may not all be available yet. |
|
|
171
173
|
| `'ignore'` | Silently ignores missing tools. Use for optional tool references or cross-server skills. |
|
|
172
174
|
|
|
175
|
+
In `'strict'` mode the server refuses to start, with `SkillValidationError: Skill '<name>' failed tool validation: missing tools [...]`. Up to 1.8.7 it started anyway, silently.
|
|
176
|
+
|
|
173
177
|
When a caller loads the skill (`skills/load`, the `skills:load` flow, `GET /skills/{id}`, `/llm_full.txt`), a referenced tool that `availableWhen.surface` doesn't offer that caller (an agent-only tool, for an MCP client) is reported as missing, without its input schema, just as `tools/list` leaves it out. The same skill loaded by an agent lists it as available.
|
|
174
178
|
|
|
175
179
|
## Instruction Sources
|
|
@@ -652,7 +656,7 @@ class AuditServer {}
|
|
|
652
656
|
|
|
653
657
|
## CodeCall Compatibility
|
|
654
658
|
|
|
655
|
-
When the `CodeCallPlugin` is active in `codecall_only` mode, all tools registered on the server are hidden from `list_tools`. The AI client only sees the three CodeCall meta-tools (`codecall:search`, `codecall:describe`, `codecall:execute`). This means skill instructions that reference tool names directly (e.g., "Use the `build_project` tool") become misleading -- the AI cannot call those tools
|
|
659
|
+
When the `CodeCallPlugin` is active in `codecall_only` mode, all tools registered on the server are hidden from `list_tools`. The AI client only sees the three CodeCall meta-tools (`codecall:search`, `codecall:describe`, `codecall:execute`). This means skill instructions that reference tool names directly (e.g., "Use the `build_project` tool") become misleading -- the AI cannot call those tools: they do not appear in the tool listing, and a direct `tools/call` of a hidden tool is refused as for an unknown tool.
|
|
656
660
|
|
|
657
661
|
### When This Matters
|
|
658
662
|
|
|
@@ -755,6 +759,7 @@ class DeployServiceSkill extends SkillContext {}
|
|
|
755
759
|
| -------------------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
|
756
760
|
| Skill not appearing in `/llm.txt` | `visibility` is set to `'mcp'` | Change to `'both'` or `'http'` to include HTTP discovery |
|
|
757
761
|
| `toolValidation: 'strict'` throws at startup | A referenced tool is not registered in the scope | Register all referenced tools in the `tools` array of `@App` or `@FrontMcp` |
|
|
762
|
+
| `Invalid tool class` at startup | A class in `tools` isn't decorated with `@Tool` | Reference a `@Tool` class, or the tool's name |
|
|
758
763
|
| `skillDir()` fails to load | `SKILL.md` file missing or frontmatter is invalid YAML | Ensure the directory contains a `SKILL.md` with valid `---` delimited YAML frontmatter |
|
|
759
764
|
| Instructions are empty at runtime | `{ file: './path.md' }` path is relative to wrong directory | Use a path relative to the skill file's location, not the project root |
|
|
760
765
|
| Parameters not visible to AI client | `parameters` defined as a plain object instead of an array | Use array format: `[{ name, description, type, required }]` |
|
|
@@ -53,6 +53,8 @@ Create a class extending `SkillContext` and decorate it with `@Skill`. The decor
|
|
|
53
53
|
| `allowedTools` | `string` | No | Space-delimited pre-approved tool names (Agent Skills spec) |
|
|
54
54
|
| `resources` | `SkillResources` | No | Bundled dirs: `{ scripts?, references?, assets? }` |
|
|
55
55
|
|
|
56
|
+
With `toolValidation: 'strict'`, the server refuses to start when a referenced tool isn't registered (`SkillValidationError`).
|
|
57
|
+
|
|
56
58
|
### Basic Example
|
|
57
59
|
|
|
58
60
|
```typescript
|
|
@@ -158,6 +160,8 @@ class ApiStandardsSkill extends SkillContext {}
|
|
|
158
160
|
|
|
159
161
|
> **When file and URL instructions are read:** when the server starts, for every skill — the server indexes each skill for `skills/search` and checks its tools then, so a URL is fetched at every start whether or not a client reads the skill. A read that fails is logged (`Failed to load skill <name>: …`) and tried again the first time the skill is loaded; the content is kept once a read succeeds.
|
|
160
162
|
|
|
163
|
+
> **Search index:** `skills/search` ranks with TF-IDF from the optional peer `vectoriadb`, loaded the first time a search runs — not at server start. Listing and loading skills (`skills/list`, `skills/load`, `skill://` resources) never need it, so a server whose skills are never searched runs without it installed, and a server started inside a Jest test needs no `--experimental-vm-modules`. The first search without the package fails with an error naming it.
|
|
164
|
+
|
|
161
165
|
## SkillContext: loadInstructions() and build()
|
|
162
166
|
|
|
163
167
|
The `SkillContext` class resolves instructions regardless of the source type. When the framework serves a skill, it calls `build()` which internally calls `loadInstructions()`.
|
|
@@ -87,7 +87,7 @@ FrontMCP uses a hierarchical decorator system. The nesting order is:
|
|
|
87
87
|
| `pagination?` | List operation pagination (`tools/list` endpoint) |
|
|
88
88
|
| `fetch?` | What `this.fetch()` adds upstream: `forwardCallerTokenTo` / `forwardCustomHeadersTo` origin allow-lists (default: none), `autoInjectTracingHeaders`, `requestTimeout` |
|
|
89
89
|
| `ui?` | UI rendering config (CDN overrides for widget imports) |
|
|
90
|
-
| `extApps?` | Widget-to-host MCP Apps communication (host capabilities, session validation)
|
|
90
|
+
| `extApps?` | Widget-to-host MCP Apps communication (host capabilities, session validation). `ui/callServerTool` returns the tool's data without its page; `ui/log` answers `result: {}` |
|
|
91
91
|
| `loader?` | Default npm/ESM package loader for `App.esm()` / `App.remote()` apps |
|
|
92
92
|
|
|
93
93
|
> **Throttle vs per-tool guards:** Server-level `throttle` is a `GuardConfig` object with `global`, `defaultRateLimit`, `defaultConcurrency`, `defaultTimeout` sub-fields that set server-wide defaults. Tool-level `rateLimit`, `concurrency`, `timeout` fields (on `@Tool`) override these defaults per tool.
|
|
@@ -123,7 +123,7 @@ class MyServer {}
|
|
|
123
123
|
| `tools?` | Array of tool classes or function-built tools |
|
|
124
124
|
| `resources?` | Array of resource classes or function-built resources |
|
|
125
125
|
| `prompts?` | Array of prompt classes or function-built prompts |
|
|
126
|
-
| `agents?` | Array of agent classes (each exposed as `
|
|
126
|
+
| `agents?` | Array of agent classes (each exposed as an `invoke_<id>` tool) |
|
|
127
127
|
| `skills?` | Array of skill definitions |
|
|
128
128
|
| `plugins?` | App-scoped plugins |
|
|
129
129
|
| `providers?` | App-scoped DI providers |
|
|
@@ -331,10 +331,11 @@ class UserProfileResource extends ResourceContext {
|
|
|
331
331
|
| `llm` | LLM configuration (model, provider, temperature, etc.) |
|
|
332
332
|
| `inputSchema?` | Zod raw shape for agent input |
|
|
333
333
|
| `outputSchema?` | Zod schema for structured output |
|
|
334
|
-
| `tools?` | Tools
|
|
335
|
-
| `agents?` |
|
|
336
|
-
| `exports?` |
|
|
337
|
-
| `swarm?` |
|
|
334
|
+
| `tools?` | Tools offered to this agent's model |
|
|
335
|
+
| `agents?` | Nested agents, offered to its model as `invoke_<id>` |
|
|
336
|
+
| `exports?` | `{ resources?, prompts?, providers? }` shared with app |
|
|
337
|
+
| `swarm?` | Which other agents it can call, and how deep |
|
|
338
|
+
| `execution?` | Loop limits, `inheritParentTools`, `inheritPlugins` |
|
|
338
339
|
|
|
339
340
|
```typescript
|
|
340
341
|
import { Agent, AgentContext, z } from '@frontmcp/sdk';
|
|
@@ -348,11 +349,7 @@ import { Agent, AgentContext, z } from '@frontmcp/sdk';
|
|
|
348
349
|
},
|
|
349
350
|
tools: [WebSearchTool, SummarizeTool],
|
|
350
351
|
})
|
|
351
|
-
class ResearchAgent extends AgentContext {
|
|
352
|
-
async execute(input: { topic: string }) {
|
|
353
|
-
return this.run(`Research and summarize: ${input.topic}`);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
352
|
+
class ResearchAgent extends AgentContext {} // the default execute() runs the LLM loop
|
|
356
353
|
```
|
|
357
354
|
|
|
358
355
|
---
|
|
@@ -384,6 +381,8 @@ class ResearchAgent extends AgentContext {
|
|
|
384
381
|
| `allowedTools?` | Space-delimited pre-approved tool names (Agent Skills spec) |
|
|
385
382
|
| `resources?` | Bundled dirs: `{ scripts?, references?, assets? }` (Agent Skills spec) |
|
|
386
383
|
|
|
384
|
+
`toolValidation: 'strict'` makes the server refuse to start when a referenced tool isn't registered.
|
|
385
|
+
|
|
387
386
|
```typescript
|
|
388
387
|
import { Skill } from '@frontmcp/sdk';
|
|
389
388
|
|
|
@@ -5,7 +5,7 @@ description: Overview of all official FrontMCP adapters that convert external de
|
|
|
5
5
|
|
|
6
6
|
# Official Adapters
|
|
7
7
|
|
|
8
|
-
Adapters convert external definitions (OpenAPI specs, Lambda functions, etc.) into MCP tools, resources, and prompts automatically. They are registered in the `adapters` array of `@App
|
|
8
|
+
Adapters convert external definitions (OpenAPI specs, Lambda functions, etc.) into MCP tools, resources, and prompts automatically. They are registered in the `adapters` array of `@App` (that app's entries) or of `@FrontMcp` (entries every app serves).
|
|
9
9
|
|
|
10
10
|
## When to Use This Skill
|
|
11
11
|
|
|
@@ -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
|
|
@@ -348,6 +370,7 @@ clear the legacy prefixes manually if you want the storage back.
|
|
|
348
370
|
|
|
349
371
|
`tools.prefix` renames them (`prefix: 'memory_'` gives `memory_recall`, ...; each description names the prefixed siblings) and
|
|
350
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.
|
|
351
374
|
|
|
352
375
|
All four take an optional `scope` (default `session`) and describe it to the model the same way:
|
|
353
376
|
`session` is this session, or without one (stateless HTTP, MCP 2026-07-28) the signed-in caller
|
|
@@ -434,12 +457,20 @@ authInfo.extra.approvalContext = { type: 'project', identifier: resolvedProjectI
|
|
|
434
457
|
2. A recorded **denial** for the caller (session, user, time-limited or context scope): refused
|
|
435
458
|
with state `denied`. A denial outranks pre-approved contexts and any approval.
|
|
436
459
|
3. The session context is one of `preApprovedContexts`: the tool runs.
|
|
437
|
-
4.
|
|
438
|
-
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,
|
|
439
461
|
user, time-limited and context approvals all count (a context approval only when the session
|
|
440
462
|
carries that context); its scope must be in `allowedScopes`, and it must be younger than
|
|
441
|
-
`maxTtlMs`, however it was stored.
|
|
442
|
-
|
|
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.
|
|
443
474
|
|
|
444
475
|
A refused call throws `ApprovalRequiredError`; the client receives an error result whose text is
|
|
445
476
|
exactly the tool's `approvalMessage` (or the default `Tool "<full name>" requires approval to
|
|
@@ -465,8 +496,10 @@ Installed on an app, `ApprovalPlugin` gates that app's tools (including those it
|
|
|
465
496
|
plugins provide) against its own store, so two apps can each install it with separate stores. It
|
|
466
497
|
also gates, against its store, the `approval` tools of apps with no approval plugin of their own,
|
|
467
498
|
so such a tool never runs ungated because the plugin sits on another app (releases up to 1.8.2 ran
|
|
468
|
-
them for anyone). Installed on the server, it gates every tool
|
|
469
|
-
|
|
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.
|
|
470
503
|
`this.approval` resolves the `ApprovalService` of the nearest `ApprovalPlugin` -- the one the
|
|
471
504
|
tool's own app installed, otherwise the server's -- so with two apps each installing it, a grant
|
|
472
505
|
or check in one app's tool uses that app's store. Releases up to 1.8.1 resolved the store of the
|
|
@@ -632,7 +665,7 @@ class GlobalCacheServer {}
|
|
|
632
665
|
Enable caching on individual tools via the `cache` metadata field:
|
|
633
666
|
|
|
634
667
|
```typescript
|
|
635
|
-
// Enable caching with default TTL
|
|
668
|
+
// Enable caching with default TTL (no sliding window)
|
|
636
669
|
@Tool({ name: 'get_weather', cache: true })
|
|
637
670
|
class GetWeatherTool extends ToolContext {
|
|
638
671
|
/* ... */
|
|
@@ -669,6 +702,13 @@ CachePlugin.init({
|
|
|
669
702
|
|
|
670
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.
|
|
671
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
|
+
|
|
672
712
|
### Cache Bypass
|
|
673
713
|
|
|
674
714
|
Send the bypass header to skip caching for a specific request:
|
|
@@ -678,6 +718,13 @@ x-frontmcp-disable-cache: true
|
|
|
678
718
|
```
|
|
679
719
|
|
|
680
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
|
+
```
|
|
681
728
|
|
|
682
729
|
### Cache Key
|
|
683
730
|
|
|
@@ -718,7 +765,8 @@ That matters because a listing is not an access control. Clients cache listings
|
|
|
718
765
|
resource URIs and prompt names from earlier sessions, so anything gated only at list time
|
|
719
766
|
stays reachable by name. The refusal is a public `FeatureFlagDisabledError` (`FEATURE_FLAG_DISABLED`, 403)
|
|
720
767
|
that names the capability and the flag. `FeatureFlagPlugin.init()` with no (or an unknown) `adapter` throws a
|
|
721
|
-
`FeatureFlagConfigurationError` at startup
|
|
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
|
|
722
770
|
`defaultValue`, and a bare string ref (no default) fails closed.
|
|
723
771
|
|
|
724
772
|
### Installation
|
|
@@ -796,7 +844,7 @@ class CustomFlagServer {}
|
|
|
796
844
|
- `splitio` -- Split.io integration. Requires `@splitsoftware/splitio` package.
|
|
797
845
|
- `launchdarkly` -- LaunchDarkly integration. Requires `launchdarkly-node-server-sdk` package.
|
|
798
846
|
- `unleash` -- Unleash integration. Requires `unleash-client` package.
|
|
799
|
-
- `custom` -- Provide your own adapter instance implementing the `FeatureFlagAdapter` interface.
|
|
847
|
+
- `custom` -- Provide your own adapter instance (`adapterInstance`, required) implementing the `FeatureFlagAdapter` interface.
|
|
800
848
|
|
|
801
849
|
### Using `this.featureFlags` in Tools
|
|
802
850
|
|
|
@@ -810,15 +858,24 @@ class BetaFeatureTool extends ToolContext {
|
|
|
810
858
|
return { content: [{ type: 'text', text: 'Feature not available' }] };
|
|
811
859
|
}
|
|
812
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
|
+
|
|
813
864
|
// Get variant value (for multivariate flags)
|
|
814
865
|
const variant = await this.featureFlags.getVariant('experiment-flag');
|
|
815
866
|
// variant may be 'control', 'treatment-a', 'treatment-b', etc.
|
|
816
867
|
|
|
817
|
-
return { content: [{ type: 'text', text: `Running variant: ${variant}` }] };
|
|
868
|
+
return { content: [{ type: 'text', text: `Running variant: ${variant} on search ${engine}` }] };
|
|
818
869
|
}
|
|
819
870
|
}
|
|
820
871
|
```
|
|
821
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
|
+
|
|
822
879
|
### Per-Tool Feature Flag Gating
|
|
823
880
|
|
|
824
881
|
Tools gated by a feature flag are automatically hidden from `list_tools` and blocked from execution when the flag is off:
|
|
@@ -830,7 +887,7 @@ class BetaTool extends ToolContext {
|
|
|
830
887
|
/* ... */
|
|
831
888
|
}
|
|
832
889
|
|
|
833
|
-
// 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
|
|
834
891
|
@Tool({
|
|
835
892
|
name: 'experimental_tool',
|
|
836
893
|
featureFlag: { key: 'experimental-flag', defaultValue: false },
|
|
@@ -960,6 +1017,51 @@ All official plugins use the static `init()` pattern inherited from `DynamicPlug
|
|
|
960
1017
|
class ProductionServer {}
|
|
961
1018
|
```
|
|
962
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
|
+
|
|
963
1065
|
## Common Patterns
|
|
964
1066
|
|
|
965
1067
|
| Pattern | Correct | Incorrect | Why |
|
|
@@ -994,13 +1096,14 @@ class ProductionServer {}
|
|
|
994
1096
|
|
|
995
1097
|
## Troubleshooting
|
|
996
1098
|
|
|
997
|
-
| Problem | Cause
|
|
998
|
-
| --------------------------------- |
|
|
999
|
-
| `this.remember` is undefined | RememberPlugin not registered or missing `.init()`
|
|
1000
|
-
| Cache not working for a tool | Tool name does not match any `toolPatterns` glob and `cache` metadata is not set
|
|
1001
|
-
| Feature flag always returns false | Using `'static'` adapter with flag not in the `flags` map
|
|
1002
|
-
| Dashboard returns 404 | Plugin is in beta and auto-disabled in production (`NODE_ENV=production`)
|
|
1003
|
-
| 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()` |
|
|
1004
1107
|
|
|
1005
1108
|
## Examples
|
|
1006
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:
|