@frontmcp/skills 1.8.3 → 1.8.5

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.
@@ -26,15 +26,15 @@ On Linux / Windows servers, this tool simply doesn't exist — it's not in `tool
26
26
 
27
27
  ## Axes
28
28
 
29
- | Axis | Values | Source |
30
- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
31
- | `os` | `'darwin'`, `'linux'`, `'win32'` | `process.platform` (since #417 — was previously `platform`) |
32
- | `runtime` | `'node'`, `'browser'`, `'edge'`, `'bun'`, `'deno'` | Detected at boot |
33
- | `deployment` | `'serverless'`, `'standalone'`, `'distributed'`, `'browser'` | Detected from `frontmcp.config` / env |
34
- | `provider` | `'bare'`, `'docker'`, `'vercel'`, `'lambda'`, `'cloudflare'`, `'netlify'`, `'azure'`, `'gcp'`, `'fly'`, `'render'`, `'railway'` | Auto-detected; override with `FRONTMCP_PROVIDER=<name>` |
35
- | `target` | `'cli'`, `'node'`, `'vercel'`, `'lambda'`, `'cloudflare'`, `'browser'`, `'sdk'`, `'mcpb'`, `'distributed'` | Set by `frontmcp build --target <x>`; `'unknown'` in dev |
36
- | `surface` | `'mcp'`, `'cli'`, `'agent'`, `'job'`, `'http-trigger'` | Per-call axis — which entry point is invoking the tool (only `'mcp'` and `'cli'` are tagged today) |
37
- | `env` | `'production'`, `'development'`, `'test'` | `process.env.NODE_ENV` |
29
+ | Axis | Values | Source |
30
+ | ------------ | ------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
31
+ | `os` | `'darwin'`, `'linux'`, `'win32'` | `process.platform` (since #417 — was previously `platform`) |
32
+ | `runtime` | `'node'`, `'browser'`, `'edge'`, `'bun'`, `'deno'` | Detected at boot |
33
+ | `deployment` | `'serverless'`, `'standalone'`, `'distributed'`, `'browser'` | Detected from `frontmcp.config` / env |
34
+ | `provider` | `'bare'`, `'docker'`, `'vercel'`, `'lambda'`, `'cloudflare'`, `'netlify'`, `'azure'`, `'gcp'`, `'fly'`, `'render'`, `'railway'` | Auto-detected; override with `FRONTMCP_PROVIDER=<name>` |
35
+ | `target` | `'cli'`, `'node'`, `'vercel'`, `'lambda'`, `'cloudflare'`, `'browser'`, `'sdk'`, `'mcpb'`, `'distributed'` | Set by `frontmcp build --target <x>`; `'unknown'` in dev |
36
+ | `surface` | `'mcp'`, `'cli'`, `'agent'`, `'job'`, `'http-trigger'` | Per-call axis — which entry point is invoking the tool |
37
+ | `env` | `'production'`, `'development'`, `'test'` | `process.env.NODE_ENV` |
38
38
 
39
39
  ## Semantics
40
40
 
@@ -100,7 +100,7 @@ These are fine for ergonomic branching. For tools that **shouldn't exist at all*
100
100
 
101
101
  This is the safest way to expose internal-only tools that you want an agent / job to call but don't want a user to invoke from a chat UI.
102
102
 
103
- An MCP client (and the in-process client of a CLI build, surface `'cli'`) never sees such a tool: it is absent from `tools/list`, and `tools/call` answers `Tool "rotate_secrets" not found`, exactly as for a tool that doesn't exist. Resources, resource templates, prompts, agents and skills (including the skills HTTP endpoints, which count as `'mcp'`) follow the same rule, and CodeCall applies its caller's surface to the tools it reaches. In-process dispatch (`this.callTool()`, an agent's own tools) carries no surface and is not restricted. `'agent'`, `'job'` and `'http-trigger'` are reserved: nothing tags them yet, so agents, jobs and HTTP triggers (all in-process) pass every `surface` check. The process-wide axes (`os`, `runtime`, ...) answer `EntryUnavailableError` instead.
103
+ An MCP client (and the in-process client of a CLI build, surface `'cli'`) never sees such a tool: it is absent from `tools/list`, and `tools/call` answers `Tool "rotate_secrets" not found`, exactly as for a tool that doesn't exist. Resources, resource templates, prompts, agents and skills (including the skills HTTP endpoints, which count as `'mcp'`) follow the same rule, and CodeCall applies its caller's surface to the tools it reaches. An agent's model calls its tools on `'agent'` (and is only offered those its `surface` allows), a job's or workflow step's `this.callTool()` on `'job'`, and a `@Channel` handling a webhook on `'http-trigger'`. A tool, resource or prompt calling `this.callTool()` is in-process dispatch: that call carries no surface and is not restricted. Code reads its call's surface with `getCallSurface()`. The process-wide axes (`os`, `runtime`, ...) answer `EntryUnavailableError` instead.
104
104
 
105
105
  ## See also
106
106
 
@@ -332,7 +332,8 @@ profile's rule checks nothing or isn't what it looks like, and `AuthoritiesEngin
332
332
  denies such a rule if it runs anyway (even under `not`):
333
333
 
334
334
  - `{}`, `{ roles: {} }`, `{ roles: { all: [] } }`, `allOf: []`, `guards: []`
335
- - a misspelled field (`role:`), a profile name inside `allOf`/`anyOf`, an `operator` other than `'AND'`/`'OR'`
335
+ - a misspelled field (`role:`), a profile name no profile has (`authorities: 'admn'`), a profile name inside
336
+ `allOf`/`anyOf`, an `operator` other than `'AND'`/`'OR'`
336
337
  - an ABAC condition with no `value`, or one the operator can't use: `exists` needs `true`/`false`;
337
338
  `in`/`notIn` a non-empty list; `gt`/`gte`/`lt`/`lte` a number; `startsWith`/`endsWith`/`matches`
338
339
  a string (a `{ fromInput }` / `{ fromClaims }` reference works for all but `exists`)
@@ -401,7 +402,7 @@ the caller the agent runs for, also with `execution.useToolFlow: false`.
401
402
 
402
403
  | Problem | Cause | Solution |
403
404
  | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
404
- | `profile 'admin' is not registered` | Profile used in decorator but not in `authorities.profiles` config | Add the profile to the `profiles` field in `@FrontMcp({ authorities })` |
405
+ | Startup: `Invalid authorities rule: … names an unknown profile "admin"` | Profile used in decorator but not in `authorities.profiles` config | Fix the name, or add the profile to the `profiles` field in `@FrontMcp({ authorities })` |
405
406
  | All users denied despite correct roles | `claimsMapping.roles` path does not match the actual JWT claim path | Decode a real JWT and verify the dot-path resolves to the roles array |
406
407
  | `authorities` field not recognized on decorator | `@frontmcp/auth` not imported (metadata augmentation not active) | Add `import '@frontmcp/auth'` (or `import type ... from '@frontmcp/auth'`) anywhere in your project to activate the metadata augmentation. There is no `@frontmcp/auth/authorities` subpath. |
407
408
  | ABAC condition always fails | `{ fromInput: 'tenantId' }` but tool input field is named `tenant_id` | The `fromInput` key must exactly match the tool's input schema field name |
@@ -62,7 +62,7 @@ export class MyServer {}
62
62
 
63
63
  ### Single Profile (String)
64
64
 
65
- The simplest form. The named profile's policy is evaluated. If the profile is not registered, the request is denied with `"profile 'name' is not registered"`.
65
+ The simplest form. The named profile's policy is evaluated. A name no registered profile has stops the server at startup with `Invalid authorities rule: … names an unknown profile "name"`, alone or in a list of profiles.
66
66
 
67
67
  ```typescript
68
68
  @Tool({ name: 'delete_user', authorities: 'admin' })
@@ -37,6 +37,8 @@ class CIAlertChannel extends ChannelContext {
37
37
  }
38
38
  ```
39
39
 
40
+ The path is a `POST` route on FrontMCP's HTTP server (`bootstrap()` / `createHandler()`), guarded like a custom `http.routes` entry: `throttle.ipFilter` runs first, the path may not be a FrontMCP path (MCP endpoint, `/oauth/*`, `/.well-known/*`, `/health`, `/metrics`), and one channel per path (either mistake fails startup). The route does not authenticate the sender: verify signatures (e.g. `X-Hub-Signature-256`) in `onEvent()`. `createDirect()`, stdio and `createFetchHandler()` serve no webhook route.
41
+
40
42
  ## App Event Source
41
43
 
42
44
  Subscribes to the in-process `ChannelEventBus`. Your application code emits events, and the channel transforms them into notifications.
@@ -69,6 +71,8 @@ scope.channelEventBus.emit('app:error', {
69
71
 
70
72
  Automatically pushes when registered agents finish execution. Optionally filter by agent IDs.
71
73
 
74
+ An event is published once an `invoke_<agent>` call has finished: `status: 'success'` only when the agent's output also passed its `outputSchema` (output that fails it is an `'error'`), and a call waiting for the client's answer to an elicitation publishes nothing until it finishes.
75
+
72
76
  ```typescript
73
77
  @Channel({
74
78
  name: 'agent-done',
@@ -96,9 +100,11 @@ class AgentDoneChannel extends ChannelContext {
96
100
  }
97
101
  ```
98
102
 
103
+ The event is `{ agentId, agentName, status: 'success' | 'error', durationMs, output?, error?, runId?, sessionId }`, published after every `invoke_<agent>` call and delivered only to the session that called the agent.
104
+
99
105
  ## Job Completion Source
100
106
 
101
- Pushes when background jobs or workflows complete. Optionally filter by job names.
107
+ Pushes when background jobs or workflows complete. Optionally filter by job names. The event is `{ jobName, jobId, status: 'success' | 'error', durationMs?, output?, error?, attempt, sessionId }` (for a workflow, `jobName` is its name). It goes only to the session that ran the job; a run with no session is delivered to no one.
102
108
 
103
109
  ```typescript
104
110
  @Channel({
@@ -7,7 +7,7 @@ tags: [config, auth, local, tunnel, proxy, issuer, auth-modes]
7
7
  features:
8
8
  - 'Relying on request-host-derived OAuth discovery, which works behind a tunnel or under an http.entryPath without extra config'
9
9
  - 'Setting `local.issuer` to a full public HTTPS URL so the token `iss` matches what clients reach through the proxy'
10
- - 'Knowing `FRONTMCP_PUBLIC_HOST` overrides only the discovery host (scheme/port still come from the HTTP config or local.issuer)'
10
+ - 'Knowing the issuer order: `local.issuer`, then `FRONTMCP_PUBLIC_URL`, then `FRONTMCP_PUBLIC_HOST` (host only), then the request'
11
11
  ---
12
12
 
13
13
  # Local Mode Behind a Tunnel
@@ -20,12 +20,13 @@ Expose a local-mode server through a tunnel or TLS proxy by aligning the token i
20
20
  // src/server.ts
21
21
  // OAuth discovery (.well-known/*) is derived from the incoming request host at
22
22
  // runtime and advertises /oauth/* at the root, so it works behind a tunnel or
23
- // reverse proxy with no extra config. `local.issuer` only aligns the boot-time
24
- // `iss` claim with the public HTTPS URL clients reach.
23
+ // reverse proxy with no extra config. The issuer is the same in discovery, on
24
+ // authorization responses (RFC 9207 `iss`) and in tokens: `local.issuer`, else
25
+ // FRONTMCP_PUBLIC_URL, else FRONTMCP_PUBLIC_HOST, else the request's origin.
25
26
  //
26
- // `FRONTMCP_PUBLIC_HOST=mcp.example.com` would set only the discovery HOST
27
- // (scheme stays http, port stays the HTTP port) — use `local.issuer` when you
28
- // need a different scheme/port, as below.
27
+ // `FRONTMCP_PUBLIC_HOST=mcp.example.com` would set only the HOST (scheme stays
28
+ // http, port stays the HTTP port) — use `local.issuer` when you need a
29
+ // different scheme/port, as below.
29
30
  import { App, FrontMcp, Tool, ToolContext, z } from '@frontmcp/sdk';
30
31
 
31
32
  @Tool({
@@ -65,7 +66,7 @@ class Server {}
65
66
 
66
67
  - Relying on request-host-derived OAuth discovery, which works behind a tunnel or under an http.entryPath without extra config
67
68
  - Setting `local.issuer` to a full public HTTPS URL so the token `iss` matches what clients reach through the proxy
68
- - Knowing `FRONTMCP_PUBLIC_HOST` overrides only the discovery host (scheme/port still come from the HTTP config or local.issuer)
69
+ - Knowing the issuer order: `local.issuer`, then `FRONTMCP_PUBLIC_URL`, then `FRONTMCP_PUBLIC_HOST` (host only), then the request
69
70
 
70
71
  ## Related
71
72
 
@@ -39,6 +39,8 @@ auth: {
39
39
 
40
40
  Tokens are compared in constant time over SHA-256 digests, so neither the value nor its length leaks by timing. A match yields a session whose `sub` is `static:<12 hex chars>` — a non-reversible digest prefix of the matching token, so audit logs can tell configured tokens apart without the secret appearing anywhere. Anything else, including a missing credential, is a `401` with a `WWW-Authenticate: Bearer realm="…"` challenge.
41
41
 
42
+ Public, anonymous-transparent and static callers get their claims from `anonymousCallerClaims(options, anonymousId)` in `@frontmcp/auth` (one shape whether the caller starts a session, resumes one, or sends a sessionless MCP 2026-07-28 request). Options: `issuer` (required, the `iss` claim), `scopes` (default `['anonymous']`, space-joined into `scope`), and `subject`. Without `subject` the result is `{ sub: 'anon:<anonymousId>', name: 'Anonymous' }`; with it (static mode passes `static:<12 hex chars>`) `sub` is that subject and `name` is `'Static token'`. Pass a unique `anonymousId` per caller (FrontMCP uses the session `uuid`, the same one when the session starts and on every resume, so an anonymous caller keeps one `sub` for the whole session; or a fresh UUID for a sessionless request) so anonymous callers never share a `sub`-keyed partition. Up to 1.8.4 a new anonymous session's first request got a different `sub` from the rest of the session.
43
+
42
44
  **Use when:** one shared secret is the right granularity and standing up OAuth 2.1 is not. Rotate by deploying with both the old and new token in `tokens`, then dropping the old one.
43
45
 
44
46
  **Do not use when:** you need per-user identity, revocation, or progressive auth — use `local` or `remote`.
@@ -88,7 +90,7 @@ Local mode also accepts `allowDefaultPublic` (default `false` — set `true` to
88
90
 
89
91
  > **Client registration (security):** `requireRegisteredClients` defaults to `true` (local/remote): every client must be registered (DCR / `dcr.clients`) or a CIMD client-id URL, so `redirect_uri` is exact-matched (OAuth 2.1) — this prevents auth-code interception via an attacker-chosen redirect. Set it to `false` only for local development. The server grants only the scopes in `allowedScopes` (default: the OpenID scopes). Confidential clients (`token_endpoint_auth_method: client_secret_basic`/`client_secret_post`) are authenticated with a constant-time `client_secret` check on both the code-exchange and refresh grants (Basic header or body param).
90
92
 
91
- > **Public origin (security):** pin `FRONTMCP_PUBLIC_URL` in production. The issuer / resource / OAuth-discovery URLs and the transparent expected audience derive from it rather than from request headers; `X-Forwarded-Host`/`X-Forwarded-Proto` are ignored unless `FRONTMCP_TRUST_PROXY=1` (a trusted proxy that strips client-supplied forwarded headers).
93
+ > **Public origin (security):** pin `FRONTMCP_PUBLIC_URL` in production. The issuer / resource / OAuth-discovery URLs and the transparent expected audience derive from it rather than from request headers; `X-Forwarded-Host`/`X-Forwarded-Proto` are ignored unless `FRONTMCP_TRUST_PROXY=1` (a trusted proxy that strips client-supplied forwarded headers). The Web fetch handler (`createFetchHandler`, Workers) takes the address from the request URL, never from a `Host` header that disagrees with it.
92
94
 
93
95
  **Progressive / incremental authorization** (opt-in via `incrementalAuth`): when enabled, the minted token carries an `authorized_apps` claim and a `tools/call` for an app NOT in that claim resolves to a `CallToolResult` with `isError: true` and `_meta.code === 'AUTHORIZATION_REQUIRED'` (fields: `authorization_required: true`, `app`, `tool`, `auth_url`, `required_scopes`, `session_mode`, `supports_incremental`). The client declares the initial grant on `/oauth/authorize?…&apps=crm` (omit `apps` to grant all apps) and expands it later by **following the `auth_url` from the failed call** — that URL carries a framework-signed, single-use `ticket` naming the target app and the prior grant, and the new token's claim is the **union** of the prior apps plus the target (the user identity and already-granted apps are preserved; upstream tokens stay server-side). Do NOT hand-assemble `…&mode=incremental&app=slack`: since 1.7.2 (GHSA-2c4g-9c8x-6m8g) an authorize without a valid ticket is an ordinary login and runs the full credential gate, and `/oauth/callback` ignores an `incremental=true` parameter entirely. Single use is enforced with a conditional write against the configured session storage, so a **multi-instance deployment needs shared storage** (Redis, or any adapter supporting `ifNotExists`) for that guarantee to hold across instances; a backend without conditional writes (Cloudflare KV) degrades to an in-memory guard that holds within one instance only and warns at first use. Without an `incrementalAuth` block, no claim is minted and there is **no** app-level gating (allow-all preserved). `consent` (tool-level) and `incrementalAuth` (app-level) are independent.
94
96
 
@@ -94,7 +94,9 @@ Local mode runs a built-in OAuth 2.1 authorization server and signs its own JWT
94
94
  class Server {}
95
95
  ```
96
96
 
97
- - `local.issuer` -- the `iss` claim set in generated tokens (defaults to a request-host-derived URL if omitted).
97
+ - `local.issuer` -- the issuer named everywhere: discovery's `issuer` and `authorization_servers`, the RFC 9207 `iss` on every authorization response (errors included), and the tokens' `iss`. Without it: `FRONTMCP_PUBLIC_URL` (plus the entry path) when pinned, else the `FRONTMCP_PUBLIC_HOST` boot-time issuer (`http://<host>:<port>`), else the request's origin -- the same on the Node server and under `createFetchHandler()`.
98
+ - The protected resource metadata's `scopes_supported` is what the mode grants: `allowedScopes` (local/remote), `anonymousScopes` (public), `scopes` (static), `requiredScopes` then `scopes` (transparent), plus `authProviders` scopes outside local/remote mode.
99
+ - An MCP 2026-07-28 request has no session: anonymous and static-key callers get none minted, so `MCP_SESSION_SECRET` is needed only for session clients; `this.context.verifiedSessionId` is `undefined` there.
98
100
 
99
101
  Token signing uses **HS256, a symmetric secret** read from the `JWT_SECRET` environment variable -- there is **no RSA/EC key pair** and no key store. Generate a stable secret (`JWT_SECRET=$(openssl rand -hex 32)`); if it is unset, FrontMCP falls back to a random per-process secret and all tokens are invalidated on restart.
100
102
 
@@ -57,6 +57,25 @@ Not all FrontMCP features are available in browser environments:
57
57
  | Crypto (`@frontmcp/utils`) | Yes | Uses WebCrypto API |
58
58
  | Direct client (`connect()`) | Yes | In-memory connection |
59
59
 
60
+ ### Request context in the browser
61
+
62
+ A browser has no `AsyncLocalStorage`. Unless the runtime provides TC39 `AsyncContext`
63
+ (`getAsyncContextMode()` from `@frontmcp/utils` is then `'native'`), the browser build runs
64
+ requests one at a time (`'serialized'`) so a request never reads another request's session, auth
65
+ info or running tool:
66
+
67
+ - `DirectMcpServer` calls, `connect()` clients and `createFetchHandler` requests take turns. A
68
+ request keeps its turn until it returns and everything it started has unwound; background jobs,
69
+ workflows and tasks take their own turn afterwards.
70
+ - A request waiting on its client (elicitation, `roots/list`) steps aside while it waits.
71
+ - Concurrent tool calls inside one request (`Promise.all`) are refused with
72
+ `AsyncContextOverlapError` once they overlap. Run them one after another.
73
+ - A workflow runs its ready steps one at a time, whatever its `maxConcurrency`, so each step runs
74
+ once instead of overlapping and being retried.
75
+ - A tool must not call its own server through a `DirectClient`/`DirectMcpServer` (it waits for its
76
+ own turn); use `this.scope` flows. A request that waits more than 10s for its turn logs why.
77
+ - Timers and un-awaited promises must not read request context.
78
+
60
79
  ## Usage with @frontmcp/react
61
80
 
62
81
  The browser build is commonly paired with `@frontmcp/react` for React applications. `FrontMcpProvider` takes a pre-created `DirectMcpServer` (via the SDK's `create()` factory) — not a `serverUrl`. Hooks for listing/invoking are `useListTools` / `useCallTool`:
@@ -159,6 +178,8 @@ ls dist/browser/
159
178
  | CORS errors on tool calls | MCP server missing CORS headers | Configure CORS middleware on the MCP server |
160
179
  | Bundle too large | All server-side code included | Use `--target browser` and a dedicated client entry file |
161
180
  | `@frontmcp/utils` fs throws | File system ops called in browser | Remove fs calls; use API endpoints or in-memory alternatives |
181
+ | `AsyncContextOverlapError` | Concurrent tool calls inside one request | Await the calls one after another (no `AsyncContext` in browser) |
182
+ | A call never returns | A tool calls its own server via a client | Call other tools through `this.scope` flows |
162
183
 
163
184
  ## Examples
164
185
 
@@ -34,6 +34,16 @@ leaves out actions the caller can never run. When the server configures
34
34
  `authorities`, bundle rules are evaluated with the server's engine (its
35
35
  `claimsMapping`, resolvers and custom evaluators).
36
36
 
37
+ A bundle skill's `SKILL.md` is listed in `skill://index.json` at its name
38
+ (`skill://Invoices/SKILL.md`, as SEP-2640 requires) and is also served at the
39
+ same URI with its id (`skill://invoices/SKILL.md`), the id the meta-tools and
40
+ `skills/list` report, with the same gating. A skill whose id is another
41
+ skill's name is refused, so an id names one skill: a bundle with one is not
42
+ applied and the previous bundle stays active. The previous bundle's skills do
43
+ not count, so a new skill can take the name of a skill the bundle drops or
44
+ renames (`registerSkillContent`'s `supersedes` option, which the bundle sync
45
+ fills in).
46
+
37
47
  For the conceptual picture, see [Skills-Only Deployment](https://docs.agentfront.dev/frontmcp/features/skills-only-deployment).
38
48
  For the production-ready decorator build, see [`deploy-to-cloudflare.md`](./deploy-to-cloudflare.md).
39
49
 
@@ -159,10 +159,10 @@ To keep bindings out of `process.env` entirely, add `nodejs_compat_do_not_popula
159
159
 
160
160
  `NODE_ENV = "production"` in `[vars]` makes this a production deployment, where FrontMCP refuses its development fallbacks:
161
161
 
162
- | Secret | Required when | Failure without it |
163
- | -------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
164
- | `MCP_SESSION_SECRET` | always in production — `session:verify` encrypts session IDs with it | `500 {"error":"server_misconfigured","code":"SESSION_SECRET_REQUIRED"}` |
165
- | `JWT_SECRET` | `auth.mode` is `local` or `remote` (these mint tokens) | the server refuses to start; requests answer `500 {"error":"server_misconfigured","code":"JWT_SECRET_REQUIRED"}` |
162
+ | Secret | Required when | Failure without it |
163
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
164
+ | `MCP_SESSION_SECRET` | in production, for session clients (Durable Object sessions, protocol before 2026-07-28) — `session:verify` encrypts their session IDs with it; 2026-07-28 requests need none | `500 {"error":"server_misconfigured","code":"SESSION_SECRET_REQUIRED"}` |
165
+ | `JWT_SECRET` | `auth.mode` is `local` or `remote` (these mint tokens) | the server refuses to start; requests answer `500 {"error":"server_misconfigured","code":"JWT_SECRET_REQUIRED"}` |
166
166
 
167
167
  ```bash
168
168
  npx wrangler secret put MCP_SESSION_SECRET # openssl rand -hex 32
@@ -172,12 +172,23 @@ npx wrangler secret put MCP_SESSION_SECRET # openssl rand -hex 32
172
172
  npx wrangler secret put JWT_SECRET # openssl rand -hex 32
173
173
  ```
174
174
 
175
+ **When the server fails to start.** The Worker builds the server on its first request. A failed build is answered, never thrown to the platform, and the error's message is not echoed:
176
+
177
+ - A configuration fault (missing or weak secret, a startup check, a config the schema refuses): `500 {"error":"server_misconfigured","code":"…"}` — codes `SESSION_SECRET_REQUIRED`, `JWT_SECRET_REQUIRED`, `JWT_SECRET_INVALID`, `UNENFORCED_METADATA`, `AUTH_CONFIGURATION_ERROR`, `CONFIG_INVALID`.
178
+ - Anything else (a remote that refused the connection, a package that failed to load): `503 {"error":"server_unavailable","code":"SERVER_START_FAILED"}` with `Retry-After`.
179
+
180
+ The failure is kept until a retry delay passes (1 s, doubling up to 60 s); the first request after it builds again. The cause is logged once per attempt (`wrangler tail`). Same for `createEdgeMcp()`, `createFetchHandler()` on an edge isolate, and each session Durable Object.
181
+
175
182
  Because `[vars]` reach `process.env`, `npx wrangler dev` sees the same `NODE_ENV=production` the deployment does, so a missing secret fails locally rather than only after a successful deploy.
176
183
 
177
184
  ### Background tasks
178
185
 
179
186
  Background tasks need a store that outlives a single request and is shared between isolates, which an edge runtime cannot provide in-process. FrontMCP disables them automatically when no distributed store is configured, and the worker serves normally without them — no `tasks: { enabled: false }` opt-out is needed. `tasks: { enabled: true }` without `tasks.redis` fails the build rather than the deployed worker.
180
187
 
188
+ ### Startup checks
189
+
190
+ The server is built on its first request, but the checks the config's metadata settles run when the module evaluates, so `createEdgeMcp` throws and the worker fails to deploy: an `approval` or `featureFlag` field no plugin that reaches the entry enforces (`UnenforcedMetadataError`), or `authorities` without the `authorities` option (`AuthConfigurationError`). A plugin installed on an app reaches only that app's entries, unless one of its hooks is `appliesTo: 'uncovered-apps'` as the built-in approval and feature-flag plugins' are. A tool declared inside an `@Agent` is reached only by that agent's plugins (none with `execution.useToolFlow: false`), and an agent's plugins reach nothing else. The remaining checks run when the first request builds the server.
191
+
181
192
  ## Step 5: Deploy
182
193
 
183
194
  ```bash
@@ -262,13 +273,18 @@ class_name = "FrontMcpSession"
262
273
  [[migrations]]
263
274
  tag = "v1"
264
275
  new_classes = ["FrontMcpSession"]
265
- # MCP_SESSION_SECRET is required on production isolates. Set it as a SECRET, not
276
+ # MCP_SESSION_SECRET is required on production isolates that serve session clients
277
+ # (Durable Object sessions, protocol before 2026-07-28). Set it as a SECRET, not
266
278
  # a var — `[vars]` is committed plaintext:
267
279
  # npx wrangler secret put MCP_SESSION_SECRET # openssl rand -hex 32
268
280
  ```
269
281
 
270
282
  One DO per session holds a persistent transport so the `GET` notification stream stays open and `tools/call` notifications reach it. It runs the **same `http:request` flow** (auth/session:verify/router/audit/metrics + hooks) as the stateless path — so transparent auth returns `401` + `WWW-Authenticate` on the worker too.
271
283
 
284
+ A session belongs to the caller that opened it: the `Mcp-Session-Id` only addresses the DO, so the flow's `checkPersistentSessionOwner` stage binds the session to the caller of its first request (verified issuer + subject, else its token) and answers anyone else with `404 Session not found` on `POST`, `GET` and `DELETE`, before every protocol handler (2026-07-28 included). The owner is kept in the DO's `state.storage`, so a DO rebuilt after eviction still refuses strangers. On a public server (anonymous callers, no token) the unguessable session id is the only credential. The owner ends its session with `DELETE`.
285
+
286
+ OAuth sign-in works on the worker too, including `/oauth/provider/:providerId/callback` (a federated provider's redirect and the federated consent submission): path parameters are matched like Express.
287
+
272
288
  ## Storage Options
273
289
 
274
290
  | Storage | Use Case | Notes |
@@ -122,7 +122,7 @@ before the first `elicit()`/`sample()`/`listRoots()` call.
122
122
  and a 10-minute expiry — a tampered or replayed blob is discarded and the
123
123
  exchange restarts.
124
124
 
125
- The client MUST declare the matching capability, or the server answers `-32021`:
125
+ The client MUST declare the matching capability, or the server answers `-32021`. (An unversioned call the server only defaulted to 2026-07-28 comes from a client that never declared it; `elicit()` answers that one with `ElicitationNotSupportedError`, as for a legacy client without a session.)
126
126
 
127
127
  ```json
128
128
  "io.modelcontextprotocol/clientCapabilities": { "elicitation": { "form": {} } }
@@ -127,6 +127,10 @@ llm: {
127
127
  },
128
128
  ```
129
129
 
130
+ ## Invocation and Hooks
131
+
132
+ Calling `invoke_<agent>` runs `tools:call-tool` for the agent's tool (the agent's `authorities`, `rateLimit`, `concurrency`, `timeout` and plugin fields apply there), then `agents:call-agent`, which runs the agent. Hooks on agent invocation run in that flow: `AgentCallHook` in a plugin, or `@AgentCallHook.Will(...)` / `.Did(...)` methods on the agent class. `this.context` and `CONTEXT`-scoped providers are available inside the agent, including a custom `execute()`.
133
+
130
134
  ## Custom execute() vs Default Agent Loop
131
135
 
132
136
  By default, calling `execute()` runs the full agent loop: the LLM receives the input plus system instructions, decides which inner tools to call, processes results, and iterates until it produces a final answer.
@@ -170,6 +170,8 @@ class StrictWorkflowSkill extends SkillContext {}
170
170
  | `'warn'` | Logs a warning for missing tools but continues. Use during development when tools may not all be available yet. |
171
171
  | `'ignore'` | Silently ignores missing tools. Use for optional tool references or cross-server skills. |
172
172
 
173
+ 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
+
173
175
  ## Instruction Sources
174
176
 
175
177
  Skills support three ways to provide instructions.
@@ -145,7 +145,7 @@ export default skill({
145
145
 
146
146
  ### URL Reference
147
147
 
148
- Load instructions from a remote URL. Fetched at build time when the skill is loaded.
148
+ Load instructions from a remote URL.
149
149
 
150
150
  ```typescript
151
151
  @Skill({
@@ -156,6 +156,8 @@ Load instructions from a remote URL. Fetched at build time when the skill is loa
156
156
  class ApiStandardsSkill extends SkillContext {}
157
157
  ```
158
158
 
159
+ > **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
+
159
161
  ## SkillContext: loadInstructions() and build()
160
162
 
161
163
  The `SkillContext` class resolves instructions regardless of the source type. When the framework serves a skill, it calls `build()` which internally calls `loadInstructions()`.
@@ -178,6 +178,7 @@ CodeCallPlugin.init({
178
178
  - `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
179
  - 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
180
  - `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.
181
+ - `illegal_access` messages name the script's own lines (`FORBIDDEN_LOOP (line 3): …`), whatever the enclave's transform printed; a line that is none of the script's is left out. `tool_error` results carry no `toolInput` (deprecated in the schema, never set).
181
182
 
182
183
  ### Power Features
183
184
 
@@ -795,7 +796,7 @@ The `toolName` completion of `ui://widget/{toolName}.html` runs the caller's `to
795
796
 
796
797
  The skill catalog in the `initialize` instructions (and the SEP-2640 `skill://` hints under `skillsConfig.sep2640InInstructions`) is filtered for the initializing client too, so a flag-disabled skill's name and description never appear there ([#603](https://github.com/agentfront/frontmcp/issues/603)). A skill's `skill://<path>/SKILL.md` entry in `resources/list` is gated by the skill that path serves now -- replace a skill at the same path and the entry takes the new skill's flag ([#606](https://github.com/agentfront/frontmcp/issues/606)).
797
798
 
798
- Installed on an `@App`, the gates cover every capability that app provides, including tools, resources and prompts contributed by its adapters (e.g. an OpenAPI adapter) and plugins. Its tool, resource, prompt and completion gates also cover the flagged capabilities of apps with no feature-flag plugin of their own (as its list filters already did), but not those of an app that installs its own -- install it in `@FrontMcp({ plugins })` to gate every app with one adapter. Releases up to 1.8.2 hid another app's flagged-off tool from `tools/list` but still ran it when called by name. Resources and prompts served outside every app (the SEP-2640 `skill://` resources) are gated by every installed copy. Skills are gated through the `skills:filter` flow, which every skill surface runs -- as the calling user on every transport, stdio and in-memory included; custom plugins can hook `Did('filterSkills')` on it the same way, and reuse `filterServableSkills(scope, skills)` from `@frontmcp/sdk` to serve skills from a surface of their own.
799
+ Installed on an `@App`, the gates cover every capability that app provides, including tools, resources and prompts contributed by its adapters (e.g. an OpenAPI adapter) and plugins. Its tool, resource, prompt and completion gates also cover the flagged capabilities of apps with no feature-flag plugin of their own, but not those of an app that installs its own -- install it in `@FrontMcp({ plugins })` to gate every app with one adapter. Listings follow the same copy of the plugin as the gates (a plugin on each of two apps decides each app's entries, listed and served, with that app's flags); up to 1.8.3 every copy filtered every app's listings. Releases up to 1.8.2 hid another app's flagged-off tool from `tools/list` but still ran it when called by name. Resources and prompts served outside every app (the SEP-2640 `skill://` resources) are gated by every installed copy. Skills are gated through the `skills:filter` flow, which every skill surface runs -- as the calling user on every transport, stdio and in-memory included; custom plugins can hook `Did('filterSkills')` on it the same way, and reuse `filterServableSkills(scope, skills)` from `@frontmcp/sdk` to serve skills from a surface of their own.
799
800
 
800
801
  ---
801
802
 
@@ -128,16 +128,16 @@ OpenapiAdapter.init({
128
128
 
129
129
  With no `authProviderMapper`, `securityResolver` or `staticAuth`, the adapter sends **no** credentials: operations that require auth fail with `Authentication required for tool '…'` and a `SECURITY WARNING` is logged at startup. The caller's MCP token (`ctx.authInfo.token`) is never forwarded implicitly — not by default, and not when an `authProviderMapper` function returns `undefined` — because passing it to another API is token passthrough, which the MCP specification forbids. `passthroughCallerToken: true` is the explicit opt-in, used only after every other credential source came up empty.
130
130
 
131
- | Risk Level | Strategy | Description |
132
- | ---------- | ------------------------------------------ | ---------------------------------------------------- |
133
- | LOW | `authProviderMapper` or `securityResolver` | Auth from user context, not exposed to clients |
134
- | MEDIUM | `staticAuth`, `additionalHeaders`, or none | Static credentials, or no credentials at all |
135
- | HIGH | `includeSecurityInInput: true` | Auth fields exposed to MCP clients (not recommended) |
136
- | HIGH | `passthroughCallerToken: true` | The MCP client's own token is sent to the API |
131
+ | Risk Level | Strategy | Description |
132
+ | ---------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------- |
133
+ | LOW | `authProviderMapper` or `securityResolver` | Auth from user context, not exposed to clients |
134
+ | MEDIUM | `staticAuth`, `additionalHeaders`, or none | Static credentials, or no credentials at all |
135
+ | HIGH | `includeSecurityInInput` (`true` or a list of schemes), or `securitySchemesInInput` | Auth fields exposed to MCP clients (not recommended) |
136
+ | HIGH | `passthroughCallerToken: true` | The MCP client's own token is sent to the API |
137
137
 
138
138
  `passthroughCallerToken: true` scores HIGH alongside an `authProviderMapper` too (the token is sent when no mapper function returns a credential); only a `securityResolver` or a non-empty `staticAuth` leaves it unused.
139
139
 
140
- Resolution order: `securityResolver` → `authProviderMapper` → `staticAuth` (fills every credential no mapper function returned; a mapped value wins) → `passthroughCallerToken`. A security scheme with no `authProviderMapper` entry is refused at startup unless `staticAuth` or `passthroughCallerToken` covers it (the latter sends the caller's token for it and logs a `SECURITY WARNING`).
140
+ Resolution order: `securityResolver` → `authProviderMapper` → `staticAuth` (fills every credential no mapper function returned; a mapped value wins) → `passthroughCallerToken`. A security scheme with no `authProviderMapper` entry is refused at startup unless `staticAuth` covers it, `additionalHeaders` carries its credential, `headersMapper` may set it (a header or cookie scheme, checked on each request), or `passthroughCallerToken` does for an HTTP bearer scheme (it sends the caller's token for it and logs a `SECURITY WARNING`; the caller's token never fills an API key, basic, OAuth2 or OpenID Connect scheme). An operation that requires auth is sent only with a credential for one of its own schemes, from a credential option, the tool input (`securitySchemesInInput`, or `includeSecurityInInput`: `true` for every scheme, a list for the schemes it names, like `securitySchemesInInput`; the schemes a list leaves out still need a credential source), `additionalHeaders` or `headersMapper`; otherwise it fails with `Authentication required for tool '…'`. A tool-input credential is used for a scheme only when no other source supplies one (a server credential always wins).
141
141
 
142
142
  ## Spec Polling
143
143
 
@@ -18,7 +18,7 @@ These checks apply to ALL deployment targets. Run them first, then proceed to yo
18
18
  - [ ] Session TTL is configured appropriately (not infinite)
19
19
  - [ ] Tool-level authorization is enforced where needed (ApprovalPlugin or custom)
20
20
  - [ ] OAuth redirect URIs are restricted to known domains
21
- - [ ] `FRONTMCP_PUBLIC_URL` is pinned to the canonical origin — issuer / resource / OAuth-discovery URLs and the transparent-mode expected audience derive from it, not from request headers. `X-Forwarded-Host`/`X-Forwarded-Proto` are ignored by default; only set `FRONTMCP_TRUST_PROXY=1` behind a proxy that strips client-supplied forwarded headers
21
+ - [ ] `FRONTMCP_PUBLIC_URL` is pinned to the canonical origin — issuer / resource / OAuth-discovery URLs and the transparent-mode expected audience derive from it, not from request headers. `X-Forwarded-Host`/`X-Forwarded-Proto` are ignored by default; only set `FRONTMCP_TRUST_PROXY=1` behind a proxy that strips client-supplied forwarded headers. The Web fetch handler (`createFetchHandler`, Workers) takes the address from the request URL, never from a `Host` header that disagrees with it
22
22
  - [ ] `auth.requireRegisteredClients` left at its default `true` (local/remote) so unknown clients can't present an attacker-chosen `redirect_uri` (auth-code interception). Clients register via DCR / `dcr.clients` / CIMD; a production remote-mode server has neither DCR nor `dcr.clients`, so its MCP clients must use CIMD
23
23
  - [ ] `auth.allowedScopes` lists the scopes the server may grant (local/remote); anything else a client asks for is dropped
24
24
  - [ ] Transparent mode sets `auth.expectedAudience` (bind tokens to this resource) and validates issuer (`providerConfig.verifyIssuer`, default on)
@@ -23,6 +23,7 @@ Target-specific checklist for publishing FrontMCP as a browser-compatible SDK.
23
23
  - [ ] All file operations removed or polyfilled
24
24
  - [ ] Fetch API used instead of Node http/https modules
25
25
  - [ ] Works in major browsers (Chrome, Firefox, Safari, Edge)
26
+ - [ ] Without `AsyncContext` requests run one at a time: tools don't start concurrent tool calls inside one request, don't call their own server through a client, and don't read request context from timers
26
27
 
27
28
  ## Security
28
29
 
@@ -573,7 +573,7 @@
573
573
  "features": [
574
574
  "Relying on request-host-derived OAuth discovery, which works behind a tunnel or under an http.entryPath without extra config",
575
575
  "Setting `local.issuer` to a full public HTTPS URL so the token `iss` matches what clients reach through the proxy",
576
- "Knowing `FRONTMCP_PUBLIC_HOST` overrides only the discovery host (scheme/port still come from the HTTP config or local.issuer)"
576
+ "Knowing the issuer order: `local.issuer`, then `FRONTMCP_PUBLIC_URL`, then `FRONTMCP_PUBLIC_HOST` (host only), then the request"
577
577
  ]
578
578
  },
579
579
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/skills",
3
- "version": "1.8.3",
3
+ "version": "1.8.5",
4
4
  "description": "Curated skills catalog for FrontMCP projects",
5
5
  "author": "AgentFront <info@agentfront.dev>",
6
6
  "homepage": "https://docs.agentfront.dev",