@frontmcp/skills 1.3.0 → 1.4.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 +38 -29
- package/catalog/TEMPLATE.md +26 -0
- package/catalog/create-tool/SKILL.md +318 -0
- package/catalog/create-tool/examples/01-basic-class-tool.md +112 -0
- package/catalog/create-tool/examples/02-basic-function-tool.md +80 -0
- package/catalog/create-tool/examples/03-tool-with-zod-shape-output.md +78 -0
- package/catalog/create-tool/examples/04-tool-with-zod-schema-output.md +97 -0
- package/catalog/create-tool/examples/05-tool-with-primitive-output.md +93 -0
- package/catalog/create-tool/examples/06-tool-with-media-output.md +109 -0
- package/catalog/create-tool/examples/08-tool-with-provider-injection.md +110 -0
- package/catalog/create-tool/examples/09-tool-with-multiple-providers.md +107 -0
- package/catalog/create-tool/examples/11-tool-with-fetch.md +94 -0
- package/catalog/create-tool/examples/12-tool-with-fetch-and-retries.md +115 -0
- package/catalog/create-tool/examples/13-tool-with-single-auth-provider.md +85 -0
- package/catalog/create-tool/examples/14-tool-with-multiple-auth-providers.md +105 -0
- package/catalog/create-tool/examples/15-tool-with-credential-vault.md +115 -0
- package/catalog/create-tool/examples/16-tool-with-rate-limit.md +71 -0
- package/catalog/create-tool/examples/17-tool-with-concurrency-and-timeout.md +101 -0
- package/catalog/create-tool/examples/18-tool-with-progress-and-notify.md +96 -0
- package/catalog/create-tool/examples/19-tool-with-elicitation.md +102 -0
- package/catalog/create-tool/examples/20-tool-with-annotations.md +125 -0
- package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +107 -0
- package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +93 -0
- package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +112 -0
- package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +127 -0
- package/catalog/create-tool/examples/25-tool-handing-off-to-job.md +143 -0
- package/catalog/create-tool/examples/26-tool-with-resource-link-output.md +94 -0
- package/catalog/create-tool/examples/27-tool-with-examples-metadata.md +90 -0
- package/catalog/create-tool/references/annotations.md +96 -0
- package/catalog/create-tool/references/auth-providers.md +167 -0
- package/catalog/create-tool/references/availability.md +106 -0
- package/catalog/create-tool/references/decorator-options.md +95 -0
- package/catalog/create-tool/references/derived-types.md +102 -0
- package/catalog/create-tool/references/elicitation.md +128 -0
- package/catalog/create-tool/references/error-handling.md +128 -0
- package/catalog/create-tool/references/execution-context.md +158 -0
- package/catalog/create-tool/references/file-layout.md +96 -0
- package/catalog/create-tool/references/function-style-builder.md +118 -0
- package/catalog/create-tool/references/input-schema.md +141 -0
- package/catalog/create-tool/references/output-schema.md +175 -0
- package/catalog/create-tool/references/quick-start.md +124 -0
- package/catalog/create-tool/references/registration.md +132 -0
- package/catalog/create-tool/references/remote-and-esm.md +68 -0
- package/catalog/create-tool/references/testing.md +59 -0
- package/catalog/create-tool/references/throttling.md +109 -0
- package/catalog/create-tool/references/ui-widgets.md +198 -0
- package/catalog/create-tool/rules/always-define-output-schema.md +77 -0
- package/catalog/create-tool/rules/derive-execute-types.md +57 -0
- package/catalog/create-tool/rules/input-schema-is-raw-shape.md +76 -0
- package/catalog/create-tool/rules/no-toolcontext-generics.md +50 -0
- package/catalog/create-tool/rules/no-try-catch-around-execute.md +79 -0
- package/catalog/create-tool/rules/register-in-app.md +76 -0
- package/catalog/create-tool/rules/snake-case-tool-names.md +45 -0
- package/catalog/create-tool/rules/use-this-fail-for-business-errors.md +75 -0
- package/catalog/create-tool/rules/widget-paths-anchor-with-import-meta-url.md +76 -0
- package/catalog/create-tool/rules/widget-resource-mode-host-detect.md +61 -0
- package/catalog/frontmcp-auth-ui/SKILL.md +146 -0
- package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/login-slot.md +97 -0
- package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/multi-step-auth-extra.md +133 -0
- package/catalog/frontmcp-auth-ui/references/custom-auth-ui.md +162 -0
- package/catalog/frontmcp-authorities/SKILL.md +55 -18
- package/catalog/frontmcp-authorities/references/authority-profiles.md +25 -1
- package/catalog/frontmcp-authorities/references/custom-evaluators.md +1 -1
- package/catalog/frontmcp-authorities/references/rbac-abac-rebac.md +9 -0
- package/catalog/frontmcp-channels/SKILL.md +7 -1
- package/catalog/frontmcp-config/SKILL.md +9 -2
- package/catalog/frontmcp-config/examples/configure-auth/local-credential-vault.md +94 -0
- package/catalog/frontmcp-config/examples/configure-auth/local-secure-store.md +138 -0
- package/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md +45 -23
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-behind-tunnel.md +73 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-consent-enforcement.md +87 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-dcr-control.md +67 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-minimal.md +62 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-multi-provider-orchestration.md +93 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md +18 -20
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-single-operator.md +66 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md +37 -23
- package/catalog/frontmcp-config/examples/configure-http/custom-http-routes.md +98 -0
- package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-redis.md +17 -9
- package/catalog/frontmcp-config/references/configure-auth-modes.md +86 -23
- package/catalog/frontmcp-config/references/configure-auth.md +296 -50
- package/catalog/frontmcp-config/references/configure-http.md +149 -15
- package/catalog/frontmcp-deployment/SKILL.md +15 -13
- package/catalog/frontmcp-deployment/references/deploy-manifest-yaml.md +308 -0
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md +174 -0
- package/catalog/frontmcp-deployment/references/mcp-client-integration.md +38 -2
- package/catalog/frontmcp-development/SKILL.md +30 -44
- package/catalog/frontmcp-development/references/decorators-guide.md +15 -15
- package/catalog/frontmcp-extensibility/SKILL.md +1 -1
- package/catalog/frontmcp-extensibility/examples/skill-audit-log/verify-chain.md +8 -6
- package/catalog/frontmcp-extensibility/references/skill-audit-log.md +7 -2
- package/catalog/frontmcp-guides/SKILL.md +1 -1
- package/catalog/frontmcp-observability/SKILL.md +1 -1
- package/catalog/frontmcp-production-readiness/SKILL.md +1 -1
- package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +3 -2
- package/catalog/frontmcp-setup/SKILL.md +1 -1
- package/catalog/frontmcp-setup/examples/multi-app-composition/per-app-auth-and-isolation.md +7 -4
- package/catalog/frontmcp-setup/references/multi-app-composition.md +6 -5
- package/catalog/frontmcp-testing/SKILL.md +9 -1
- package/catalog/frontmcp-testing/references/test-auth.md +24 -0
- package/catalog/skills-manifest.json +653 -149
- package/package.json +1 -1
- package/src/manifest.d.ts +72 -1
- package/src/manifest.js +4 -1
- package/src/manifest.js.map +1 -1
- package/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md +0 -80
- package/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md +0 -132
- package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +0 -110
- package/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md +0 -92
- package/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md +0 -59
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md +0 -101
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md +0 -62
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md +0 -101
- package/catalog/frontmcp-development/references/create-tool-annotations.md +0 -48
- package/catalog/frontmcp-development/references/create-tool-output-schema-types.md +0 -71
- package/catalog/frontmcp-development/references/create-tool.md +0 -806
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: custom-auth-ui
|
|
3
|
+
description: Replace FrontMCP's built-in OAuth pages with custom React components using the auth.ui slot→file map and auth.extras name→handler map (no decorator, no class) plus the @frontmcp/ui/auth hooks.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Custom Authorization UI (`auth.ui` / `auth.extras`)
|
|
7
|
+
|
|
8
|
+
FrontMCP's `local` and `remote` OAuth modes serve built-in HTML for the login, consent, incremental, federated, and error steps. The `auth.ui` **slot→file map** replaces any of those slots with **your own React component** while the framework keeps owning everything security-sensitive — CSRF, the Content-Security-Policy, anti-clickjacking headers, the pending-authorization state, the submit target, and the OAuth redirect. **You write only the UI.**
|
|
9
|
+
|
|
10
|
+
There is **no decorator and no class** — a slot is just a `.tsx` path, and an extra is just a handler function. If no `auth.ui` slot is configured, the built-in pages are served unchanged — the feature is purely additive and opt-in per slot.
|
|
11
|
+
|
|
12
|
+
## Two halves
|
|
13
|
+
|
|
14
|
+
| Half | Package | Responsibility |
|
|
15
|
+
| ---------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
16
|
+
| **Server** | `@frontmcp/sdk` (`auth.ui` / `auth.extras`) | Transpiles your component's `.tsx` once (server-side, single-file transform — deps stay external), inlines it as an ES module + an import-map, injects the flow state, mints/verifies CSRF, sets CSP. It never bundles or renders your component server-side. |
|
|
17
|
+
| **Client** | `@frontmcp/ui/auth` | The framework-free contract (`AuthFlowState`, wire constants) **plus** hooks (`useAuthFlow`, …), `<AuthPageWrapper>`, and `mountAuthPage` that read the state, render the component in the browser, and drive submits. Loaded in the browser **from esm.sh** via the import-map. |
|
|
18
|
+
|
|
19
|
+
At request time the server serializes an `AuthFlowState` into `window.__FRONTMCP_AUTH__`, serves a page with an **empty** `#frontmcp-auth-root` mount node, an **`<script type="importmap">`** mapping `react` / `react-dom` / `@frontmcp/ui/auth` to **esm.sh** (the `@frontmcp/ui/auth` entry gets `?external=react,react-dom` for a single React), and an inline **`<script type="module">`** with your transpiled component + a `mountAuthPage(<YourComponent>)` tail. The browser loads the deps from esm.sh, runs the module, and `mountAuthPage` renders your component **client-side** into the empty node — there is **no bundling and no server-side React**, exactly how a `@Tool({ ui: { file } })` widget loads. `@frontmcp/ui` is browser-only — the SDK passes `@frontmcp/ui/auth` to the renderer only as an import-map specifier string and never imports it. Your component reads the state via the hooks and submits back to the OAuth callback.
|
|
20
|
+
|
|
21
|
+
## Server: `auth.ui` — a slot→file map
|
|
22
|
+
|
|
23
|
+
Map each slot to a sibling `.tsx`/`.jsx` source (default export); the SDK transpiles it once with esbuild and inlines it as an ES module (deps loaded from esm.sh via an import-map; `mountAuthPage` appended for you), exactly like a `@Tool({ ui: { file } })` widget. The path is **relative and auto-anchored to the config file's directory** — no `fileURLToPath`:
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { App } from '@frontmcp/sdk'; // or @FrontMcp
|
|
27
|
+
|
|
28
|
+
@App({
|
|
29
|
+
auth: {
|
|
30
|
+
mode: 'local',
|
|
31
|
+
// slot → RELATIVE .tsx, auto-anchored to THIS config file's directory.
|
|
32
|
+
ui: { login: './auth/login.tsx' },
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
export default class Server {}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Absolute paths pass through unchanged. If the declaring file's directory can't be captured (exotic loader), the framework falls back to `process.cwd()` with a warning — use an absolute path if that's wrong.
|
|
39
|
+
|
|
40
|
+
### Slots
|
|
41
|
+
|
|
42
|
+
| Slot | Replaces the built-in… | Key fields the component receives |
|
|
43
|
+
| ------------- | ------------------------------------------- | ----------------------------------------- |
|
|
44
|
+
| `login` | Local sign-in page | `clientName`, `scopes`, `redirectUri` |
|
|
45
|
+
| `consent` | Tool-consent screen | `tools[]` (selectable tool cards) |
|
|
46
|
+
| `incremental` | Single-app incremental authorization screen | `extras.appId` / `extras.appName` |
|
|
47
|
+
| `federated` | Multi-provider selection | `providers[]` (selectable provider cards) |
|
|
48
|
+
| `error` | OAuth error page | `error` text |
|
|
49
|
+
|
|
50
|
+
Map only the slots you want to customize — the rest keep their built-in pages.
|
|
51
|
+
|
|
52
|
+
### Per-app scoping (`splitByApp`)
|
|
53
|
+
|
|
54
|
+
`auth.ui` / `auth.extras` are **scoped to the auth config they live on** (like `consent`). Under `splitByApp`, each `@App({ auth: { mode, ui, extras } })` gets its OWN custom auth UI (paths anchored to that `@App` file); the parent multi-app scope uses the top-level `@FrontMcp({ auth })` (paths anchored to the server file).
|
|
55
|
+
|
|
56
|
+
## Server: `auth.extras` — validated extra fields
|
|
57
|
+
|
|
58
|
+
An `auth.extras[name]` entry is a **server handler function** that adds a server-validated side endpoint your page can POST to **mid-flow**, without finishing the authorization. Each accepted submission is appended to a per-`(pending-auth, extra)` accumulator the framework keeps; the response carries the full accumulator map back so the page refreshes without a reload.
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { type AuthExtraContext } from '@frontmcp/sdk';
|
|
62
|
+
|
|
63
|
+
export async function addEnv(input: Record<string, unknown>, ctx: AuthExtraContext) {
|
|
64
|
+
const key = typeof input['key'] === 'string' ? input['key'].trim() : '';
|
|
65
|
+
if (!key) return { ok: false as const, error: 'key is required' };
|
|
66
|
+
if (ctx.current.some((it) => (it as { key?: string }).key === key)) {
|
|
67
|
+
return { ok: false as const, error: `"${key}" was already added` };
|
|
68
|
+
}
|
|
69
|
+
const value = typeof input['value'] === 'string' ? input['value'] : '';
|
|
70
|
+
// `addedItems` here is the list of NEW items to APPEND on success.
|
|
71
|
+
return { ok: true as const, addedItems: [{ key, value }] };
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The handler returns `{ ok, error?, addedItems?, sideEffects? }` (sync or async). The `ctx` is minimal and PII-free: `{ name, pendingAuthId?, current }` (`current` is what's already accepted for this extra). Declare it under `auth`: `@FrontMcp({ auth: { mode: 'local', extras: { 'envs:add': addEnv } } })`.
|
|
76
|
+
|
|
77
|
+
## The `AuthFlowState` a component receives
|
|
78
|
+
|
|
79
|
+
| Field | Type | Notes |
|
|
80
|
+
| --------------- | ------------------------- | ------------------------------------------------------------------------ |
|
|
81
|
+
| `slot` | `AuthSlot` | Which page slot is rendering. |
|
|
82
|
+
| `pendingAuthId` | `string?` | Correlation id; round-tripped on every submit. Absent only for `error`. |
|
|
83
|
+
| `clientName` | `string?` | OAuth client display name (**not** an end user). |
|
|
84
|
+
| `clientId` | `string?` | OAuth `client_id`. |
|
|
85
|
+
| `scopes` | `string[]` | Requested OAuth scopes. |
|
|
86
|
+
| `redirectUri` | `string?` | Validated `redirect_uri` the code is sent to. |
|
|
87
|
+
| `resource` | `string?` | RFC 8707 resource indicator, when supplied. |
|
|
88
|
+
| `error` | `string?` | Error text (error slot, or a re-rendered failed submit). |
|
|
89
|
+
| `csrfToken` | `string?` | Server-minted anti-CSRF token. The hooks echo it for you. |
|
|
90
|
+
| `providers` | `AuthProvider[]` | Selectable providers on the `federated` slot. |
|
|
91
|
+
| `tools` | `AuthTool[]` | Selectable tools on the `consent` slot. |
|
|
92
|
+
| `extras` | `Record<string, unknown>` | Free-form, slot-specific extras (e.g. logo URI, incremental target app). |
|
|
93
|
+
|
|
94
|
+
**No PII is in the contract.** Anything the user types (email, name, …) travels in your own form fields, never in `AuthFlowState`.
|
|
95
|
+
|
|
96
|
+
## Client: `@frontmcp/ui/auth`
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install @frontmcp/ui react react-dom
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`react` / `react-dom` are peer dependencies of `@frontmcp/ui`.
|
|
103
|
+
|
|
104
|
+
| Import | Use |
|
|
105
|
+
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
106
|
+
| `@frontmcp/ui/auth` | The framework-free contract types + wire constants (no React) **and** React hooks (`useAuthFlow`, …) + `<AuthPageWrapper>` + `mountAuthPage`. Single source of truth for the client API; imports only `react`/`react-dom` — no MUI. |
|
|
107
|
+
| `@frontmcp/ui/auth/vanilla` | Framework-free (browser) helpers: `getAuthFlow`, `submitFinish`, `submitExtra`, `getAddedItems` (plus the contract types). |
|
|
108
|
+
|
|
109
|
+
### React hooks + wrapper + client mount
|
|
110
|
+
|
|
111
|
+
- **`useAuthFlow()`** — the flow-state fields above plus a `submitFinish` handler. `<form onSubmit={submitFinish}>` `preventDefault`s, serializes the form, attaches `pending_auth_id` + `csrf` + the slot marker, posts to the callback, and follows the OAuth redirect.
|
|
112
|
+
- **`useExtraField(name)`** — `{ onSubmit, result, pending }` for an `auth.extras` form. On success it merges the returned `addedItems` back into context.
|
|
113
|
+
- **`useAddedItems(name)`** — the server-side accumulator for a named extra, reactively.
|
|
114
|
+
- **`<AuthPageWrapper>`** — outer chrome that reads the injected state once, provides it via context, and (by default) renders the enclosing `<form>` with the `pending_auth_id` + `csrf` hidden fields so a no-JS submit still works. Pass `renderForm={false}` to supply your own forms.
|
|
115
|
+
- **`mountAuthPage(Component, options?)`** — the client entrypoint; wraps `Component` in `<AuthPageWrapper>` and renders it (`createRoot(...).render(...)`) into the empty `#frontmcp-auth-root` node in the browser. Pure client render — no server markup to hydrate. **The SDK appends `mountAuthPage(<your default export>)` to the inlined module for you**, so a `file`-based component just needs a default export (loaded in the browser from esm.sh via the import-map).
|
|
116
|
+
|
|
117
|
+
### Vanilla (no framework)
|
|
118
|
+
|
|
119
|
+
`@frontmcp/ui/auth/vanilla` exposes the same flow without React: `getAuthFlow()` (read `window.__FRONTMCP_AUTH__`; `tryGetAuthFlow()` is tolerant), `getAddedItems(name)`, `submitFinish(formOrData?)`, and `submitExtra(name, formOrData?)`.
|
|
120
|
+
|
|
121
|
+
## Routes the server adds
|
|
122
|
+
|
|
123
|
+
When at least one `auth.extras` handler is configured (otherwise it 404s / falls through, so defaults are untouched):
|
|
124
|
+
|
|
125
|
+
| Route | Method | Purpose |
|
|
126
|
+
| ----------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
127
|
+
| `/oauth/ui/extra` | `POST` | Routes an `auth.extras[name]` submission (the `action` field names the extra) to its handler; returns `{ ok, error?, addedItems?, sideEffects? }`. CSRF-verified (400 on mismatch). |
|
|
128
|
+
|
|
129
|
+
There is **no `/oauth/ui/:slot.js` route** — the component is transpiled server-side and **inlined** into the authorize/callback page as a `<script type="module">` (deps from esm.sh via the import-map), so there is no separately-served bundle. The pages themselves are served by the existing `/oauth/authorize` + `/oauth/callback` flows; an `auth.ui` slot swaps the served page body (import-map + injected state + the inline transpiled module + empty mount) — the component renders client-side. The finish submit still posts to `/oauth/callback`.
|
|
130
|
+
|
|
131
|
+
## Security — the framework owns it
|
|
132
|
+
|
|
133
|
+
- **CSRF**: the server mints a per-pending-authorization token, stores it (echoed into `csrfToken`), and verifies it on the finish submit and every `auth.extras` POST with a constant-time compare. Your component never generates or checks it.
|
|
134
|
+
- **CSP + anti-clickjacking**: the auth-UI HTML ships with a strict CSP — `default-src 'self'; script-src 'self' 'unsafe-inline' https://esm.sh; connect-src 'self' https://esm.sh; style-src 'self' 'unsafe-inline' https://esm.sh; img-src 'self' data: https:; frame-ancestors 'none'; base-uri 'self'; form-action 'self'` — plus `X-Frame-Options: DENY`, `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer`. It allows `https://esm.sh` (deps) + `'unsafe-inline'` (the JSON-escaped state script) but **NOT `'unsafe-eval'`** — the transform is server-side, never in the browser.
|
|
135
|
+
- **No PII**: the injected state carries OAuth client identifiers + control fields only.
|
|
136
|
+
- **Fail-safe**: a component that can't be transpiled (missing / invalid `.tsx`) is logged (error cached so a broken file isn't retried each request) and **falls back to the built-in page** — a broken custom page can't take the server down.
|
|
137
|
+
|
|
138
|
+
## Local dev / offline (esm.sh caveat)
|
|
139
|
+
|
|
140
|
+
The browser loads `react`, `react-dom`, and `@frontmcp/ui/auth` from **esm.sh**. `react`/`react-dom` are always there, but `@frontmcp/ui/auth` resolves only once **published to npm**. In an unpublished monorepo, an in-browser render can't fetch it — though the SERVER still emits the full page (import-map + injected state + inline transpiled module), so HTTP-level checks pass; only the browser DOM render is affected. To render before publishing, map the specifier to a locally-served ESM URL via `@FrontMcp({ ui: { cdnOverrides } })`:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
@FrontMcp({
|
|
144
|
+
ui: { cdnOverrides: { '@frontmcp/ui/auth': 'http://localhost:5173/ui-auth.mjs' } },
|
|
145
|
+
auth: { mode: 'local', ui: { login: './auth/login.tsx' } },
|
|
146
|
+
})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
A non-esm.sh override URL is left as-is (no `?external=react`). Once published, no overrides are needed.
|
|
150
|
+
|
|
151
|
+
## Examples
|
|
152
|
+
|
|
153
|
+
| Example | Level | Description |
|
|
154
|
+
| ------------------------------------------------------------------------------ | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
155
|
+
| [`login-slot`](../examples/custom-auth-ui/login-slot.md) | Intermediate | Replace the built-in login page with a custom React component via auth.ui: { login: './login.tsx' } and useAuthFlow, while the framework keeps owning CSRF and CSP. |
|
|
156
|
+
| [`multi-step-auth-extra`](../examples/custom-auth-ui/multi-step-auth-extra.md) | Advanced | Add a server-validated multi-step field to a custom login page with auth.extras: { 'envs:add': fn }, useExtraField, and useAddedItems — accepted rows accumulate server-side and reflect back without a reload. |
|
|
157
|
+
|
|
158
|
+
## See also
|
|
159
|
+
|
|
160
|
+
- Docs: [Custom Authorization UI (`auth.ui`)](https://docs.agentfront.dev/frontmcp/authentication/custom-ui)
|
|
161
|
+
- `frontmcp-config` → `configure-auth` — the declarative `login` / `authenticate` config (tweak the built-in page's fields without React)
|
|
162
|
+
- `create-tool` → `ui-widgets` — the `@Tool({ ui: { file } })` widget pipeline this mirrors
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frontmcp-authorities
|
|
3
|
-
description: 'Use when implementing authorization
|
|
3
|
+
description: 'Use when implementing authorization and access control for FrontMCP tools, resources, prompts, or skills, deciding who may invoke what. Covers the RBAC, ABAC, and ReBAC models and when to choose each; JWT claims mapping per identity provider (Auth0, Keycloak, Okta, Cognito, Frontegg); reusable named authority profiles; and custom authority evaluators for domain-specific policy. This is about who-can-do-what (permissions, roles, scopes), distinct from configuring auth modes and login (see frontmcp-config) and custom login UI (see frontmcp-auth-ui). Triggers: authorization, access control, RBAC, ABAC, ReBAC, permissions, roles, scopes, policy enforcement, JWT claims, restrict who can call a tool.'
|
|
4
4
|
tags: [authorization, rbac, abac, rebac, security, permissions, roles, access-control, authorities, jwt]
|
|
5
5
|
category: development
|
|
6
6
|
targets: [all]
|
|
@@ -20,9 +20,10 @@ Built-in RBAC/ABAC/ReBAC authorization system for FrontMCP entry types. Each flo
|
|
|
20
20
|
|
|
21
21
|
### Must Use
|
|
22
22
|
|
|
23
|
-
- Adding role-based or permission-based access control to tools, resources, or
|
|
23
|
+
- Adding role-based or permission-based access control to tools, resources, prompts, or skills
|
|
24
24
|
- Restricting MCP entry visibility based on the caller's JWT claims
|
|
25
25
|
- Enforcing tenant isolation (ABAC) or relationship checks (ReBAC) on entries
|
|
26
|
+
- Gating which skills a caller can discover and load (`@Skill({ authorities })`)
|
|
26
27
|
|
|
27
28
|
### Recommended
|
|
28
29
|
|
|
@@ -73,7 +74,7 @@ Add the `authorities` field to your `@FrontMcp` decorator. No plugin import need
|
|
|
73
74
|
import { FrontMcp } from '@frontmcp/sdk';
|
|
74
75
|
|
|
75
76
|
@FrontMcp({
|
|
76
|
-
name: 'my-server',
|
|
77
|
+
info: { name: 'my-server', version: '1.0.0' },
|
|
77
78
|
authorities: {
|
|
78
79
|
// configured in next steps
|
|
79
80
|
},
|
|
@@ -284,23 +285,59 @@ authorities: {
|
|
|
284
285
|
}
|
|
285
286
|
```
|
|
286
287
|
|
|
288
|
+
### Skill Authorities (`@Skill({ authorities })`)
|
|
289
|
+
|
|
290
|
+
`authorities` on `@Skill` is enforced exactly like the other entry types, across
|
|
291
|
+
**every** surface a skill is served from:
|
|
292
|
+
|
|
293
|
+
- **Deny on load/read** — loading a gated skill the caller can't access throws
|
|
294
|
+
`AuthorityDeniedError` (MCP code `-32003`), the same as a denied `tools/call`.
|
|
295
|
+
Covers `skills/load` (MCP), `skill://<path>/SKILL.md` and `skill://<path>/<file>`
|
|
296
|
+
reads (SEP-2640), and `GET /skills/{id}` (HTTP).
|
|
297
|
+
- **Filter on discovery** — gated skills the caller can't access are removed from
|
|
298
|
+
`skills/search` / `skills/list` (MCP), the `skill://index.json` discovery index and
|
|
299
|
+
skill-path autocomplete (SEP-2640), and `GET /skills` (HTTP).
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
@Skill({ name: 'review-pr', description: '…', instructions: '…' }) // open to all
|
|
303
|
+
@Skill({ name: 'internal-runbook', description: '…', instructions: '…',
|
|
304
|
+
authorities: 'admin' }) // admins only
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Two limitations to design around:
|
|
308
|
+
|
|
309
|
+
- **List-time filtering is role/permission/claims-only.** Discovery runs without
|
|
310
|
+
request input, so `{ fromInput: '…' }` ABAC/ReBAC policies can't be evaluated when
|
|
311
|
+
filtering and will hide the skill from discovery. Use role/permission/claims
|
|
312
|
+
authorities for discoverable skills; input-dependent policies still enforce at
|
|
313
|
+
load time. (Same limitation applies to tools/resources/prompts.)
|
|
314
|
+
- **HTTP skills discovery is fail-closed.** The Skills HTTP API uses a binary
|
|
315
|
+
api-key/bearer gate with no claims, so gated skills are hidden from `GET /skills`
|
|
316
|
+
and denied on `GET /skills/{id}` regardless of the bearer. Serve gated skills over
|
|
317
|
+
an MCP transport for claims-based access. Ungated skills are unaffected.
|
|
318
|
+
|
|
319
|
+
Boot-time fail-fast covers skills too: a `@Skill` with `authorities` but no configured
|
|
320
|
+
authorities engine fails server startup with `AuthConfigurationError`, exactly like a
|
|
321
|
+
tool/resource/prompt/agent.
|
|
322
|
+
|
|
287
323
|
## Scenario Routing Table
|
|
288
324
|
|
|
289
|
-
| Scenario | Approach
|
|
290
|
-
| ------------------------------------- |
|
|
291
|
-
| Simple role gate (admin-only tool) | `authorities: 'admin'` profile
|
|
292
|
-
|
|
|
293
|
-
|
|
|
294
|
-
|
|
|
295
|
-
|
|
|
296
|
-
|
|
|
297
|
-
|
|
|
298
|
-
|
|
|
299
|
-
|
|
|
300
|
-
|
|
|
301
|
-
|
|
|
302
|
-
|
|
|
303
|
-
|
|
|
325
|
+
| Scenario | Approach | Reference |
|
|
326
|
+
| ------------------------------------- | ----------------------------------------------------------------- | ---------------------------------- |
|
|
327
|
+
| Simple role gate (admin-only tool) | `authorities: 'admin'` profile | `references/authority-profiles.md` |
|
|
328
|
+
| Gate skill discovery + load | `@Skill({ authorities: 'admin' })` (role/permission/claims-based) | `references/authority-profiles.md` |
|
|
329
|
+
| Permission-based access | `authorities: { permissions: { all: ['x'] } }` | `references/rbac-abac-rebac.md` |
|
|
330
|
+
| Tenant isolation | ABAC with `{ fromInput: 'tenantId' }` | `references/rbac-abac-rebac.md` |
|
|
331
|
+
| Resource ownership check | ReBAC with relationship resolver | `references/rbac-abac-rebac.md` |
|
|
332
|
+
| IP allowlist or custom logic | Custom evaluator via `custom.*` | `references/custom-evaluators.md` |
|
|
333
|
+
| Different IdP (Auth0/Keycloak/Okta) | Configure `claimsMapping` | `references/claims-mapping.md` |
|
|
334
|
+
| Admin OR (editor AND same-tenant) | `anyOf` / `allOf` combinators | `references/authority-profiles.md` |
|
|
335
|
+
| Custom pre/post authority logic | Hook with `Will`/`Did`/`Around` on `checkEntryAuthorities` stage | `references/custom-evaluators.md` |
|
|
336
|
+
| Replace built-in check with OPA/Cedar | `Around('checkEntryAuthorities')` hook | `references/custom-evaluators.md` |
|
|
337
|
+
| Audit authority decisions | `Did('checkEntryAuthorities')` hook for logging/metrics | `references/custom-evaluators.md` |
|
|
338
|
+
| Tenant allowlist in Redis/DB | Async custom evaluator with `custom.*` field | `references/custom-evaluators.md` |
|
|
339
|
+
| Subscription check before tool runs | Async custom evaluator or `Will('checkEntryAuthorities')` hook | `references/custom-evaluators.md` |
|
|
340
|
+
| Feature flag gate on a tool | Async custom evaluator checking flag service | `references/custom-evaluators.md` |
|
|
304
341
|
|
|
305
342
|
## Common Patterns
|
|
306
343
|
|
|
@@ -15,7 +15,7 @@ Profiles are registered in the `profiles` field of the `authorities` config. Eac
|
|
|
15
15
|
import { FrontMcp } from '@frontmcp/sdk';
|
|
16
16
|
|
|
17
17
|
@FrontMcp({
|
|
18
|
-
name: 'my-server',
|
|
18
|
+
info: { name: 'my-server', version: '1.0.0' },
|
|
19
19
|
authorities: {
|
|
20
20
|
claimsMapping: {
|
|
21
21
|
roles: 'realm_access.roles',
|
|
@@ -95,6 +95,15 @@ export default class AdminReportPrompt extends PromptContext {
|
|
|
95
95
|
// only admin can use this prompt
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
+
|
|
99
|
+
// Skills are gated the same way. Non-admins can neither discover nor load this:
|
|
100
|
+
@Skill({
|
|
101
|
+
name: 'internal-runbook',
|
|
102
|
+
description: 'Restricted operational runbook',
|
|
103
|
+
instructions: { file: './internal-runbook.md' },
|
|
104
|
+
authorities: 'admin',
|
|
105
|
+
})
|
|
106
|
+
export class InternalRunbookSkill {}
|
|
98
107
|
```
|
|
99
108
|
|
|
100
109
|
### Multiple Profiles (String Array)
|
|
@@ -244,9 +253,24 @@ The authorities system does not only enforce on execution. The built-in `filterB
|
|
|
244
253
|
- `tools/list` only returns tools the current user is authorized to call
|
|
245
254
|
- `resources/list` only returns resources the current user can read
|
|
246
255
|
- `prompts/list` only returns prompts the current user can get
|
|
256
|
+
- Skills are filtered on every discovery surface: `skills/search` / `skills/list`, the SEP-2640 `skill://index.json` index + skill-path autocomplete, and `GET /skills`. Loading a gated skill the caller can't access (via `skills/load`, a `skill://…` read, or `GET /skills/{id}`) is denied with `AuthorityDeniedError` (`-32003`).
|
|
247
257
|
|
|
248
258
|
This filtering happens automatically. No additional configuration is needed. Entries without an `authorities` field are always visible.
|
|
249
259
|
|
|
260
|
+
**List-time evaluation has no request input.** Because discovery runs before any
|
|
261
|
+
arguments exist, input-dependent policies — ABAC conditions using `{ fromInput: '…' }`
|
|
262
|
+
or ReBAC `resourceId: { fromInput: '…' }` — cannot be evaluated when filtering and
|
|
263
|
+
will exclude the entry from the list (it is still enforced correctly at execution/load
|
|
264
|
+
time, where input is available). For entries (especially **skills**) that must remain
|
|
265
|
+
discoverable, gate them with role/permission/claims-based authorities such as
|
|
266
|
+
`authorities: 'admin'` or `{ roles: { any: ['admin'] } }`.
|
|
267
|
+
|
|
268
|
+
**HTTP skills discovery is fail-closed.** The Skills HTTP API authenticates with a
|
|
269
|
+
binary api-key/bearer gate that surfaces no JWT claims, so authority-gated skills are
|
|
270
|
+
hidden from `GET /skills` and denied on `GET /skills/{id}` regardless of the bearer.
|
|
271
|
+
Serve gated skills over an MCP transport (full claims context) for claims-based access;
|
|
272
|
+
skills without `authorities` are served over HTTP unchanged.
|
|
273
|
+
|
|
250
274
|
## Profile Design Guidelines
|
|
251
275
|
|
|
252
276
|
| Guideline | Example |
|
|
@@ -66,7 +66,7 @@ import { ipAllowListEvaluator } from './evaluators/ip-allow-list';
|
|
|
66
66
|
import { timeWindowEvaluator } from './evaluators/time-window';
|
|
67
67
|
|
|
68
68
|
@FrontMcp({
|
|
69
|
-
name: 'my-server',
|
|
69
|
+
info: { name: 'my-server', version: '1.0.0' },
|
|
70
70
|
authorities: {
|
|
71
71
|
claimsMapping: { roles: 'roles', permissions: 'permissions' },
|
|
72
72
|
profiles: { admin: { roles: { any: ['admin'] } } },
|
|
@@ -173,6 +173,15 @@ Instead of hardcoding values, reference runtime data from tool input or JWT clai
|
|
|
173
173
|
{ path: 'claims.department', op: 'eq', value: { fromClaims: 'manager.department' } }
|
|
174
174
|
```
|
|
175
175
|
|
|
176
|
+
> **`fromInput` is for tools and agents only.** Only **tools** and **agents**
|
|
177
|
+
> pass request input into the authorities evaluation. `@Resource` and `@Prompt`
|
|
178
|
+
> do not receive tool-style input, so a `fromInput` reference (in an ABAC
|
|
179
|
+
> condition or a ReBAC `resourceId`) has nothing to resolve against on those
|
|
180
|
+
> entries — gate resources and prompts with role / permission / `fromClaims`
|
|
181
|
+
> policies instead. (Separately, list-time discovery filtering runs without any
|
|
182
|
+
> input at all, so `fromInput` policies are also excluded from list results for
|
|
183
|
+
> everyone — reserve them for call-time enforcement.)
|
|
184
|
+
|
|
176
185
|
### ABAC Examples
|
|
177
186
|
|
|
178
187
|
**Tenant isolation:**
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frontmcp-channels
|
|
3
|
-
description: 'Use when
|
|
3
|
+
description: 'Use when pushing real-time notifications or events into Claude Code (or another MCP client) sessions, or building two-way chat bridges. Covers channel source types: incoming webhooks (such as GitHub), app error events, agent-completion and job-completion alerts, service connectors, file watchers, and replay buffers; plus two-way conversational bridges connecting WhatsApp, Telegram, Slack, and Discord to a Claude Code session. Triggers: push notifications, real-time alerts, webhook channel, chat bridge, WhatsApp / Telegram / Slack / Discord, agent completion alert, job status notification, error forwarding, server-to-client messaging. The skill for CHANNELS and NOTIFICATIONS.'
|
|
4
|
+
when_to_use: |
|
|
5
|
+
Trigger when creating or editing a *.channel.ts file, or building a channel
|
|
6
|
+
that pushes real-time notifications into a Claude Code session: webhook
|
|
7
|
+
sources, app / agent / job event alerts, service connectors, file watchers,
|
|
8
|
+
or a two-way chat bridge (WhatsApp, Telegram, Slack, Discord).
|
|
9
|
+
paths: '**/*.channel.ts'
|
|
4
10
|
tags: [channels, notifications, claude-code, webhooks, messaging, real-time, two-way]
|
|
5
11
|
category: development
|
|
6
12
|
targets: [all]
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frontmcp-config
|
|
3
|
-
description: 'Use when
|
|
3
|
+
description: 'Use when configuring a FrontMCP server through frontmcp.config or the @FrontMcp options. Covers auth modes (public, transparent, local, remote), OAuth plus credential vault and secureStore, CORS, HTTP port / entry-path prefix / unix socket, security headers (CSP, HSTS, X-Frame-Options, X-Content-Type-Options), rate limiting / throttling / concurrency / timeout / IP filtering (GuardConfig), session storage (Redis, Vercel KV), client transport protocols (SSE, Streamable HTTP, stateless, protocol presets), elicitation, multi-target build config, and skillsConfig (HTTP catalog, caching, audit log, instruction injection). Triggers: configure auth, set up CORS, add rate limiting, throttle requests, manage sessions, choose transport, set HTTP options, configure JWT or OAuth. The skill for server CONFIGURATION.'
|
|
4
|
+
when_to_use: |
|
|
5
|
+
Trigger when creating or editing frontmcp.config.ts/js or the @FrontMcp({...})
|
|
6
|
+
options: configuring auth modes, OAuth + credential vault, CORS, HTTP port /
|
|
7
|
+
entry path / unix socket, security headers, rate limiting / throttling /
|
|
8
|
+
GuardConfig, session storage (Redis, Vercel KV), client transport / protocol
|
|
9
|
+
presets, elicitation, multi-target builds, or skillsConfig.
|
|
10
|
+
paths: '**/frontmcp.config.*'
|
|
4
11
|
tags: [router, config, transport, http, auth, session, redis, sqlite, throttle, guide]
|
|
5
12
|
category: config
|
|
6
13
|
targets: [all]
|
|
@@ -162,7 +169,7 @@ Each reference has matching examples under [`examples/<reference>/`](./examples/
|
|
|
162
169
|
| Example | Level | Description |
|
|
163
170
|
| --------------------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------- |
|
|
164
171
|
| [`local-self-signed-tokens`](./examples/configure-auth-modes/local-self-signed-tokens.md) | Intermediate | Configure a server that signs its own JWT tokens with consent and incremental auth enabled. |
|
|
165
|
-
| [`remote-enterprise-oauth`](./examples/configure-auth-modes/remote-enterprise-oauth.md) | Advanced |
|
|
172
|
+
| [`remote-enterprise-oauth`](./examples/configure-auth-modes/remote-enterprise-oauth.md) | Advanced | Proxy auth to one mandatory upstream IdP, mint a FrontMCP session, read the upstream token. |
|
|
166
173
|
| [`transparent-jwt-validation`](./examples/configure-auth-modes/transparent-jwt-validation.md) | Basic | Validate externally-issued JWTs without managing token lifecycle on the server. |
|
|
167
174
|
|
|
168
175
|
### `configure-auth`
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: local-credential-vault
|
|
3
|
+
reference: configure-auth
|
|
4
|
+
level: intermediate
|
|
5
|
+
description: 'Persist a per-session credential from a local authenticate() verifier into the built-in encrypted vault and read it from a tool via this.credentials.'
|
|
6
|
+
tags: [config, auth, local, vault, credentials, this-credentials]
|
|
7
|
+
features:
|
|
8
|
+
- 'Returning `credentials: [{ key, secret, metadata? }]` from `authenticate()` so FrontMCP persists them encrypted, keyed by the minted `sub`'
|
|
9
|
+
- 'Reading a per-session credential from a tool via `this.credentials.get(key)` (no `this.authProviders` wiring needed)'
|
|
10
|
+
- 'Prompting the agent to connect a missing credential mid-session with `this.credentials.requireConnect({ key })` (framework-signed `/oauth/connect` URL)'
|
|
11
|
+
- 'Understanding per-session rotation: a reconnect yields an empty vault and old ciphertext is undecryptable'
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Local Mode with the Per-Session Credential Vault (`this.credentials`)
|
|
15
|
+
|
|
16
|
+
Persist a per-session credential from a local authenticate() verifier into the built-in encrypted vault and read it from a tool via this.credentials.
|
|
17
|
+
|
|
18
|
+
The vault is AES-256-GCM-encrypted, keyed by the authenticated `sub`, and enabled
|
|
19
|
+
automatically in `local` (and `remote`) modes — there is nothing to register.
|
|
20
|
+
|
|
21
|
+
## Code
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// src/server.ts
|
|
25
|
+
import { App, FrontMcp, Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
26
|
+
|
|
27
|
+
@Tool({
|
|
28
|
+
name: 'call_acme',
|
|
29
|
+
description: 'Call the Acme API using the session credential',
|
|
30
|
+
inputSchema: {},
|
|
31
|
+
outputSchema: { connected: z.boolean(), connectUrl: z.string().optional() },
|
|
32
|
+
})
|
|
33
|
+
class CallAcmeTool extends ToolContext {
|
|
34
|
+
async execute() {
|
|
35
|
+
// Read the credential the verifier persisted at login.
|
|
36
|
+
const cred = await this.credentials.get('acme'); // { secret, metadata } | undefined
|
|
37
|
+
if (!cred) {
|
|
38
|
+
// Not connected — hand the agent a framework-signed resume URL so the
|
|
39
|
+
// user can connect it mid-session (verified server-side, short-lived).
|
|
40
|
+
const res = await this.credentials.requireConnect({ key: 'acme' });
|
|
41
|
+
if (!res.connected) return { connected: false, connectUrl: res.resumeUrl };
|
|
42
|
+
}
|
|
43
|
+
// const headers = { Authorization: `Bearer ${cred!.secret}` };
|
|
44
|
+
// … call Acme with cred.secret / cred.metadata …
|
|
45
|
+
return { connected: true };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@App({ name: 'acme-tools', tools: [CallAcmeTool] })
|
|
50
|
+
class AcmeApp {}
|
|
51
|
+
|
|
52
|
+
@FrontMcp({
|
|
53
|
+
info: { name: 'acme-server', version: '1.0.0' },
|
|
54
|
+
apps: [AcmeApp],
|
|
55
|
+
auth: {
|
|
56
|
+
mode: 'local',
|
|
57
|
+
login: {
|
|
58
|
+
title: 'Connect Acme',
|
|
59
|
+
fields: { apiKey: { type: 'password', label: 'Acme API Key', required: true } },
|
|
60
|
+
subject: { fromField: 'apiKey', strategy: 'per-account' },
|
|
61
|
+
},
|
|
62
|
+
authenticate: async (input) => {
|
|
63
|
+
// Mid-session connect re-invokes authenticate() with a `resume` context.
|
|
64
|
+
if (input.resume) {
|
|
65
|
+
const value = input.fields['apiKey'];
|
|
66
|
+
if (!value) return { ok: false, message: 'A value is required', retryField: 'apiKey' };
|
|
67
|
+
return { ok: true, credentials: [{ key: input.resume.key, secret: value }] };
|
|
68
|
+
}
|
|
69
|
+
// Initial login: validate and persist the credential into the vault.
|
|
70
|
+
const apiKey = input.fields['apiKey'];
|
|
71
|
+
if (!apiKey?.startsWith('sk-')) return { ok: false, message: 'Invalid API key', retryField: 'apiKey' };
|
|
72
|
+
return {
|
|
73
|
+
ok: true,
|
|
74
|
+
credentials: [{ key: 'acme', secret: apiKey, metadata: { baseUrl: 'https://acme.example' } }],
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
class Server {}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## What This Demonstrates
|
|
83
|
+
|
|
84
|
+
- Returning `credentials: [{ key, secret, metadata? }]` from `authenticate()` so FrontMCP persists them encrypted, keyed by the minted `sub`
|
|
85
|
+
- Reading a per-session credential from a tool via `this.credentials.get(key)` (no `this.authProviders` wiring needed)
|
|
86
|
+
- Prompting the agent to connect a missing credential mid-session with `this.credentials.requireConnect({ key })` (framework-signed `/oauth/connect` URL)
|
|
87
|
+
- Understanding per-session rotation: a reconnect yields an empty vault and old ciphertext is undecryptable
|
|
88
|
+
|
|
89
|
+
## Related
|
|
90
|
+
|
|
91
|
+
- See `configure-auth` for the full `this.credentials` API and the
|
|
92
|
+
`authenticate()` verifier contract.
|
|
93
|
+
- See `remote-oauth-with-vault` for the separate `this.authProviders` downstream
|
|
94
|
+
OAuth-provider vault.
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: local-secure-store
|
|
3
|
+
reference: configure-auth
|
|
4
|
+
level: intermediate
|
|
5
|
+
description: 'Configure the general session secure-secret store (this.secureStore) with a pluggable backing (memory / sqlite / redis / custom OS-keychain) and read/write user-typed secrets from a tool.'
|
|
6
|
+
tags: [config, auth, local, secure-store, secrets, this-secure-store, keychain]
|
|
7
|
+
features:
|
|
8
|
+
- 'Selecting the secure-store backing via `auth.secureStore` (memory / sqlite / redis / custom backend) plus a namespace `scope`'
|
|
9
|
+
- 'Reading/writing arbitrary user-typed secrets from a tool via `this.secureStore.set/get/list/delete` (JSON-serialized, scoped to the session/subject)'
|
|
10
|
+
- 'Backing the store with an OS keychain by supplying a `SecureStoreBackend` — no native dependency is bundled by the framework'
|
|
11
|
+
- 'Understanding scope: `user` (keyed by sub, default), `session` (keyed by sessionId), `global` (server-wide)'
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
# Local Mode with the Session Secure-Secret Store (`this.secureStore`)
|
|
15
|
+
|
|
16
|
+
Configure the general session secure-secret store (this.secureStore) with a pluggable backing (memory / sqlite / redis / custom OS-keychain) and read/write user-typed secrets from a tool.
|
|
17
|
+
|
|
18
|
+
The store is distinct from `this.credentials` (which is OAuth-credential-centric):
|
|
19
|
+
use `this.secureStore` for arbitrary secrets like an API key a tool prompts for.
|
|
20
|
+
The built-in backings encrypt at rest with AES-256-GCM, and an OS keychain can be
|
|
21
|
+
plugged in without the framework bundling any native dependency.
|
|
22
|
+
|
|
23
|
+
## Code
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
// src/server.ts
|
|
27
|
+
import { App, FrontMcp, Tool, ToolContext, z, type SecureStoreConfig } from '@frontmcp/sdk';
|
|
28
|
+
|
|
29
|
+
@Tool({
|
|
30
|
+
name: 'set_api_key',
|
|
31
|
+
description: 'Store a user-typed API key in the session secure store',
|
|
32
|
+
inputSchema: { apiKey: z.string() },
|
|
33
|
+
outputSchema: { saved: z.boolean(), keys: z.array(z.string()) },
|
|
34
|
+
})
|
|
35
|
+
class SetApiKeyTool extends ToolContext {
|
|
36
|
+
async execute(input: { apiKey: string }) {
|
|
37
|
+
// Values are JSON-serialized and encrypted at rest (built-in backings),
|
|
38
|
+
// scoped to the current `sub` (user scope, the default).
|
|
39
|
+
await this.secureStore.set('stg.api-key', input.apiKey);
|
|
40
|
+
const keys = await this.secureStore.list();
|
|
41
|
+
return { saved: true, keys };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@Tool({
|
|
46
|
+
name: 'read_api_key',
|
|
47
|
+
description: 'Read the stored API key (presence only — never returns the raw secret)',
|
|
48
|
+
inputSchema: {},
|
|
49
|
+
outputSchema: { present: z.boolean() },
|
|
50
|
+
})
|
|
51
|
+
class ReadApiKeyTool extends ToolContext {
|
|
52
|
+
async execute() {
|
|
53
|
+
const apiKey = await this.secureStore.get<string>('stg.api-key');
|
|
54
|
+
// Never return the raw secret to the model — only presence.
|
|
55
|
+
return { present: apiKey !== undefined };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@App({ name: 'Secrets', tools: [SetApiKeyTool, ReadApiKeyTool] })
|
|
60
|
+
class SecretsApp {}
|
|
61
|
+
|
|
62
|
+
// Pick the backing per deployment. Default ('memory') is encrypted in-process.
|
|
63
|
+
const secureStore: SecureStoreConfig = {
|
|
64
|
+
sqlite: { path: './.frontmcp/secrets.sqlite' }, // survives restart
|
|
65
|
+
scope: 'user', // 'user' (default) | 'session' | 'global'
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
@FrontMcp({
|
|
69
|
+
info: { name: 'secure-store-demo', version: '0.1.0' },
|
|
70
|
+
apps: [SecretsApp],
|
|
71
|
+
auth: {
|
|
72
|
+
mode: 'local',
|
|
73
|
+
secureStore,
|
|
74
|
+
login: {
|
|
75
|
+
title: 'Sign in',
|
|
76
|
+
fields: { apiKey: { type: 'password', label: 'API Key', required: true } },
|
|
77
|
+
subject: { fromField: 'apiKey', strategy: 'per-account' },
|
|
78
|
+
},
|
|
79
|
+
authenticate: async (input) => {
|
|
80
|
+
if (!input.fields['apiKey']) return { ok: false, message: 'API key required', retryField: 'apiKey' };
|
|
81
|
+
return { ok: true };
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
export default class Server {}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## What This Demonstrates
|
|
89
|
+
|
|
90
|
+
- Selecting the secure-store backing via `auth.secureStore` (memory / sqlite / redis / custom backend) plus a namespace `scope`
|
|
91
|
+
- Reading/writing arbitrary user-typed secrets from a tool via `this.secureStore.set/get/list/delete` (JSON-serialized, scoped to the session/subject)
|
|
92
|
+
- Backing the store with an OS keychain by supplying a `SecureStoreBackend` — no native dependency is bundled by the framework
|
|
93
|
+
- Understanding scope: `user` (keyed by sub, default), `session` (keyed by sessionId), `global` (server-wide)
|
|
94
|
+
|
|
95
|
+
## Optional: back the store with an OS keychain (pluggable, not bundled)
|
|
96
|
+
|
|
97
|
+
FrontMCP does **not** ship `keytar`/`wincred`/`libsecret`. Supply an object
|
|
98
|
+
implementing `SecureStoreBackend` and the framework uses it as-is (no framework
|
|
99
|
+
crypto — an OS keychain is encrypted by the OS):
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
import type { SecureStoreBackend } from '@frontmcp/sdk';
|
|
103
|
+
|
|
104
|
+
// import keytar from 'keytar'; // YOU add the native peer-dep
|
|
105
|
+
|
|
106
|
+
const keychainBackend: SecureStoreBackend = {
|
|
107
|
+
async get(namespace, key) {
|
|
108
|
+
return (await keytar.getPassword(`frontmcp:${namespace}`, key)) ?? null;
|
|
109
|
+
},
|
|
110
|
+
async set(namespace, key, value /*, ttlMs */) {
|
|
111
|
+
await keytar.setPassword(`frontmcp:${namespace}`, key, value); // ttlMs ignored
|
|
112
|
+
},
|
|
113
|
+
async delete(namespace, key) {
|
|
114
|
+
return keytar.deletePassword(`frontmcp:${namespace}`, key);
|
|
115
|
+
},
|
|
116
|
+
async list(/* namespace */) {
|
|
117
|
+
const creds = await keytar.findCredentials('frontmcp');
|
|
118
|
+
return creds.map((c) => c.account);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// auth: { mode: 'local', secureStore: { backend: keychainBackend, scope: 'global' } }
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Notes
|
|
126
|
+
|
|
127
|
+
- **Backings**: `'memory'` (default, encrypted in-process), `{ sqlite: { path } }`,
|
|
128
|
+
`{ redis: { ... } }`, or `{ backend }` (custom). The persistent built-ins reuse
|
|
129
|
+
the same `StorageAdapter`/`VaultEncryption` as `tokenStorage`; when the backing
|
|
130
|
+
matches `tokenStorage` the same connection is shared.
|
|
131
|
+
- **Scope**: `user` keys by `sub` (anonymous requests read empty / skip writes),
|
|
132
|
+
`session` keys by `sessionId`, `global` shares one server-wide namespace. The
|
|
133
|
+
identity is hashed into the namespace — never stored raw.
|
|
134
|
+
- **API**: `get<T>(key)`, `set<T>(key, value, { ttlMs? })`, `delete(key)`,
|
|
135
|
+
`list()`. Object configs also accept `ttlMs` and `encryption.pepper`
|
|
136
|
+
(overrides `VAULT_SECRET ?? JWT_SECRET`).
|
|
137
|
+
- **Never return raw secrets to the model** — expose presence/redacted previews
|
|
138
|
+
only, as `read_api_key` does above.
|