@frontmcp/skills 1.2.1 → 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.
Files changed (131) hide show
  1. package/README.md +38 -29
  2. package/catalog/TEMPLATE.md +26 -0
  3. package/catalog/create-tool/SKILL.md +318 -0
  4. package/catalog/create-tool/examples/01-basic-class-tool.md +112 -0
  5. package/catalog/create-tool/examples/02-basic-function-tool.md +80 -0
  6. package/catalog/create-tool/examples/03-tool-with-zod-shape-output.md +78 -0
  7. package/catalog/create-tool/examples/04-tool-with-zod-schema-output.md +97 -0
  8. package/catalog/create-tool/examples/05-tool-with-primitive-output.md +93 -0
  9. package/catalog/create-tool/examples/06-tool-with-media-output.md +109 -0
  10. package/catalog/create-tool/examples/08-tool-with-provider-injection.md +110 -0
  11. package/catalog/create-tool/examples/09-tool-with-multiple-providers.md +107 -0
  12. package/catalog/create-tool/examples/11-tool-with-fetch.md +94 -0
  13. package/catalog/create-tool/examples/12-tool-with-fetch-and-retries.md +115 -0
  14. package/catalog/create-tool/examples/13-tool-with-single-auth-provider.md +85 -0
  15. package/catalog/create-tool/examples/14-tool-with-multiple-auth-providers.md +105 -0
  16. package/catalog/create-tool/examples/15-tool-with-credential-vault.md +115 -0
  17. package/catalog/create-tool/examples/16-tool-with-rate-limit.md +71 -0
  18. package/catalog/create-tool/examples/17-tool-with-concurrency-and-timeout.md +101 -0
  19. package/catalog/create-tool/examples/18-tool-with-progress-and-notify.md +96 -0
  20. package/catalog/create-tool/examples/19-tool-with-elicitation.md +102 -0
  21. package/catalog/create-tool/examples/20-tool-with-annotations.md +125 -0
  22. package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +107 -0
  23. package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +93 -0
  24. package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +112 -0
  25. package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +127 -0
  26. package/catalog/create-tool/examples/25-tool-handing-off-to-job.md +143 -0
  27. package/catalog/create-tool/examples/26-tool-with-resource-link-output.md +94 -0
  28. package/catalog/create-tool/examples/27-tool-with-examples-metadata.md +90 -0
  29. package/catalog/create-tool/references/annotations.md +96 -0
  30. package/catalog/create-tool/references/auth-providers.md +167 -0
  31. package/catalog/create-tool/references/availability.md +106 -0
  32. package/catalog/create-tool/references/decorator-options.md +95 -0
  33. package/catalog/create-tool/references/derived-types.md +102 -0
  34. package/catalog/create-tool/references/elicitation.md +128 -0
  35. package/catalog/create-tool/references/error-handling.md +128 -0
  36. package/catalog/create-tool/references/execution-context.md +158 -0
  37. package/catalog/create-tool/references/file-layout.md +96 -0
  38. package/catalog/create-tool/references/function-style-builder.md +118 -0
  39. package/catalog/create-tool/references/input-schema.md +141 -0
  40. package/catalog/create-tool/references/output-schema.md +175 -0
  41. package/catalog/create-tool/references/quick-start.md +124 -0
  42. package/catalog/create-tool/references/registration.md +132 -0
  43. package/catalog/create-tool/references/remote-and-esm.md +68 -0
  44. package/catalog/create-tool/references/testing.md +59 -0
  45. package/catalog/create-tool/references/throttling.md +109 -0
  46. package/catalog/create-tool/references/ui-widgets.md +198 -0
  47. package/catalog/create-tool/rules/always-define-output-schema.md +77 -0
  48. package/catalog/create-tool/rules/derive-execute-types.md +57 -0
  49. package/catalog/create-tool/rules/input-schema-is-raw-shape.md +76 -0
  50. package/catalog/create-tool/rules/no-toolcontext-generics.md +50 -0
  51. package/catalog/create-tool/rules/no-try-catch-around-execute.md +79 -0
  52. package/catalog/create-tool/rules/register-in-app.md +76 -0
  53. package/catalog/create-tool/rules/snake-case-tool-names.md +45 -0
  54. package/catalog/create-tool/rules/use-this-fail-for-business-errors.md +75 -0
  55. package/catalog/create-tool/rules/widget-paths-anchor-with-import-meta-url.md +76 -0
  56. package/catalog/create-tool/rules/widget-resource-mode-host-detect.md +61 -0
  57. package/catalog/frontmcp-auth-ui/SKILL.md +146 -0
  58. package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/login-slot.md +97 -0
  59. package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/multi-step-auth-extra.md +133 -0
  60. package/catalog/frontmcp-auth-ui/references/custom-auth-ui.md +162 -0
  61. package/catalog/frontmcp-authorities/SKILL.md +55 -18
  62. package/catalog/frontmcp-authorities/references/authority-profiles.md +25 -1
  63. package/catalog/frontmcp-authorities/references/custom-evaluators.md +1 -1
  64. package/catalog/frontmcp-authorities/references/rbac-abac-rebac.md +9 -0
  65. package/catalog/frontmcp-channels/SKILL.md +7 -1
  66. package/catalog/frontmcp-config/SKILL.md +14 -7
  67. package/catalog/frontmcp-config/examples/configure-auth/local-credential-vault.md +94 -0
  68. package/catalog/frontmcp-config/examples/configure-auth/local-secure-store.md +138 -0
  69. package/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md +45 -23
  70. package/catalog/frontmcp-config/examples/configure-auth-modes/local-behind-tunnel.md +73 -0
  71. package/catalog/frontmcp-config/examples/configure-auth-modes/local-consent-enforcement.md +87 -0
  72. package/catalog/frontmcp-config/examples/configure-auth-modes/local-dcr-control.md +67 -0
  73. package/catalog/frontmcp-config/examples/configure-auth-modes/local-minimal.md +62 -0
  74. package/catalog/frontmcp-config/examples/configure-auth-modes/local-multi-provider-orchestration.md +93 -0
  75. package/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md +18 -20
  76. package/catalog/frontmcp-config/examples/configure-auth-modes/local-single-operator.md +66 -0
  77. package/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md +37 -23
  78. package/catalog/frontmcp-config/examples/configure-http/custom-http-routes.md +98 -0
  79. package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-redis.md +17 -9
  80. package/catalog/frontmcp-config/references/configure-auth-modes.md +86 -23
  81. package/catalog/frontmcp-config/references/configure-auth.md +296 -50
  82. package/catalog/frontmcp-config/references/configure-deployment-targets.md +84 -1
  83. package/catalog/frontmcp-config/references/configure-http.md +203 -14
  84. package/catalog/frontmcp-config/references/configure-session.md +14 -7
  85. package/catalog/frontmcp-deployment/SKILL.md +17 -15
  86. package/catalog/frontmcp-deployment/references/build-for-mcpb.md +1 -1
  87. package/catalog/frontmcp-deployment/references/deploy-manifest-yaml.md +308 -0
  88. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md +174 -0
  89. package/catalog/frontmcp-deployment/references/mcp-client-integration.md +145 -2
  90. package/catalog/frontmcp-development/SKILL.md +36 -50
  91. package/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md +14 -0
  92. package/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md +85 -9
  93. package/catalog/frontmcp-development/references/create-job.md +45 -11
  94. package/catalog/frontmcp-development/references/create-provider.md +80 -8
  95. package/catalog/frontmcp-development/references/create-skill-with-tools.md +31 -0
  96. package/catalog/frontmcp-development/references/create-skill.md +45 -0
  97. package/catalog/frontmcp-development/references/decorators-guide.md +15 -15
  98. package/catalog/frontmcp-extensibility/SKILL.md +1 -1
  99. package/catalog/frontmcp-extensibility/examples/skill-audit-log/verify-chain.md +8 -6
  100. package/catalog/frontmcp-extensibility/references/skill-audit-log.md +7 -2
  101. package/catalog/frontmcp-guides/SKILL.md +8 -8
  102. package/catalog/frontmcp-observability/SKILL.md +16 -8
  103. package/catalog/frontmcp-observability/examples/metrics-endpoint/enable-metrics-endpoint.md +77 -0
  104. package/catalog/frontmcp-observability/references/metrics-endpoint.md +161 -0
  105. package/catalog/frontmcp-production-readiness/SKILL.md +1 -1
  106. package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +3 -2
  107. package/catalog/frontmcp-setup/SKILL.md +12 -12
  108. package/catalog/frontmcp-setup/examples/frontmcp-skills-usage/install-and-search-skills.md +19 -1
  109. package/catalog/frontmcp-setup/examples/multi-app-composition/per-app-auth-and-isolation.md +7 -4
  110. package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +260 -19
  111. package/catalog/frontmcp-setup/references/multi-app-composition.md +6 -5
  112. package/catalog/frontmcp-setup/references/setup-project.md +29 -0
  113. package/catalog/frontmcp-setup/references/setup-sqlite.md +68 -9
  114. package/catalog/frontmcp-testing/SKILL.md +26 -18
  115. package/catalog/frontmcp-testing/references/test-auth.md +24 -0
  116. package/catalog/skills-manifest.json +676 -146
  117. package/package.json +1 -1
  118. package/src/manifest.d.ts +72 -1
  119. package/src/manifest.js +4 -1
  120. package/src/manifest.js.map +1 -1
  121. package/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md +0 -61
  122. package/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md +0 -84
  123. package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +0 -92
  124. package/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md +0 -92
  125. package/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md +0 -59
  126. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md +0 -101
  127. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md +0 -62
  128. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md +0 -101
  129. package/catalog/frontmcp-development/references/create-tool-annotations.md +0 -48
  130. package/catalog/frontmcp-development/references/create-tool-output-schema-types.md +0 -71
  131. package/catalog/frontmcp-development/references/create-tool.md +0 -728
@@ -0,0 +1,75 @@
1
+ ---
2
+ name: use-this-fail-for-business-errors
3
+ constraint: '`this.fail(new SomeMcpError(...))` for business-logic errors — never raw `throw new Error(...)`.'
4
+ severity: required
5
+ ---
6
+
7
+ # Rule: `this.fail` for business errors, not raw `throw`
8
+
9
+ ## The rule
10
+
11
+ For errors the user / agent should see (not-found, permission-denied, invalid-input, conflict, etc.), use `this.fail(new SomeMcpError(...))`. Never `throw new Error(...)` — the raw `Error` message gets REDACTED before reaching the client.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ import { PublicMcpError, ResourceNotFoundError } from '@frontmcp/sdk';
17
+
18
+ async execute(input: { id: string }) {
19
+ const record = await this.findRecord(input.id);
20
+ if (!record) {
21
+ this.fail(new ResourceNotFoundError(`record:${input.id}`)); // -32002, message reaches client
22
+ }
23
+
24
+ if (record.tenantId !== this.auth.claims['tenantId']) {
25
+ this.fail(new PublicMcpError('Access denied')); // generic public message
26
+ }
27
+
28
+ // …
29
+ }
30
+ ```
31
+
32
+ ## Bad
33
+
34
+ ```typescript
35
+ // ❌ raw Error — message REDACTED to "Internal error" before reaching client
36
+ async execute(input: { id: string }) {
37
+ const record = await this.findRecord(input.id);
38
+ if (!record) {
39
+ throw new Error(`record:${input.id} not found`); // client sees "Internal error"
40
+ }
41
+ }
42
+ ```
43
+
44
+ ## Why
45
+
46
+ | You throw | Client sees |
47
+ | -------------------------------------- | ------------------------------------------------------------ |
48
+ | `new PublicMcpError('Quota exceeded')` | `{ code: -32603, message: 'Quota exceeded' }` |
49
+ | `new ResourceNotFoundError('user:42')` | `{ code: -32002, message: 'user:42' }` |
50
+ | `new Error('Quota exceeded')` | `{ code: -32603, message: 'Internal error' }` ← **redacted** |
51
+
52
+ Raw `Error`s are treated as potentially-sensitive infrastructure errors. The framework REDACTS the message before the client sees it (to avoid leaking stack traces, internal IDs, env vars, etc.). `PublicMcpError` (and subclasses) explicitly opt the message into the response.
53
+
54
+ ## Common error classes
55
+
56
+ | Class | Error code | HTTP | Use for |
57
+ | -------------------------------- | ---------------------------------------- | ----------- | -------------------------------------------------------------------------- |
58
+ | `PublicMcpError` | — | — | Base. Subclass for domain-specific public errors |
59
+ | `ResourceNotFoundError` | `'RESOURCE_NOT_FOUND'` (-32002 JSON-RPC) | 404 | "Thing doesn't exist" |
60
+ | `InvalidInputError` | `'INVALID_INPUT'` | 400 | Cross-field / business-rule input validation (Zod handles per-field shape) |
61
+ | `UnauthorizedError` | `'UNAUTHORIZED'` (-32001 JSON-RPC) | 401 | Missing credentials |
62
+ | `RateLimitError` | `'RATE_LIMIT_EXCEEDED'` | 429 | Rate-limit fired |
63
+ | Custom `PublicMcpError` subclass | your choice | your choice | Domain-specific errors with structured `data` |
64
+
65
+ ## When to throw raw `Error` (or just let it propagate)
66
+
67
+ For **infrastructure errors** that genuinely should be redacted — network failure, DB unavailable, file-system error. Don't catch them; just let them propagate. The framework wraps them in `InternalMcpError` with the message redacted, and logs the original for ops.
68
+
69
+ ## Verification
70
+
71
+ ```bash
72
+ # Find raw `throw new Error(...)` inside tool execute() bodies
73
+ grep -rE 'throw new Error\(' src/**/*.tool.ts
74
+ # Should match few or none — and any matches should be in non-execute code paths
75
+ ```
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: widget-paths-anchor-with-import-meta-url
3
+ constraint: '`.tsx` widget paths in `ui.template: { file }` are anchored via `fileURLToPath(new URL(...))`, never bare relative.'
4
+ severity: required
5
+ ---
6
+
7
+ # Rule: anchor widget paths with `import.meta.url`
8
+
9
+ ## The rule
10
+
11
+ Relative `FileSource` paths in `ui.template: { file }` resolve against `process.cwd()` — **not** the tool source's directory (issue #444). A bare relative path silently breaks the moment the server is launched from a different working directory. Always anchor the path to the tool source.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const widgetPath = fileURLToPath(new URL('./sales-chart.widget.tsx', import.meta.url));
19
+
20
+ @Tool({
21
+ name: 'sales_chart',
22
+ // …
23
+ ui: { template: { file: widgetPath } },
24
+ })
25
+ ```
26
+
27
+ ## Bad
28
+
29
+ ```typescript
30
+ // ❌ bare relative path — resolves against process.cwd()
31
+ @Tool({
32
+ name: 'sales_chart',
33
+ ui: { template: { file: './sales-chart.widget.tsx' } },
34
+ })
35
+ // → works locally when running from src/apps/main/tools/
36
+ // → fails with ENOENT when running from the repo root, from dist/, etc.
37
+ ```
38
+
39
+ ## Why
40
+
41
+ - **`process.cwd()` is whoever launched the process.** `yarn dev` from the repo root, `node dist/main.js` from `/opt/app`, a containerized run from `/`, a serverless cold start from `/var/task`, an Nx executor from `apps/<thing>/` — all different cwds.
42
+ - **Tool sources move around at build time.** ESM build output is often in `dist/`; `.tool.ts` becomes `.tool.js`. The relative reference's resolution chain is fragile to that.
43
+ - **`fileURLToPath(new URL('./x', import.meta.url))` is invariant.** It anchors to the **source file** that contains the URL literal — same answer at dev, build, and runtime.
44
+
45
+ ## CommonJS projects (`__dirname`)
46
+
47
+ `import.meta.url` is **ESM-only**. In a CommonJS project (`package.json` `"type": "commonjs"`, or `tsconfig` `"module": "commonjs"`) `import.meta` is unavailable and the build fails. Anchor with `__dirname` instead — the CJS equivalent, equally invariant to `process.cwd()`:
48
+
49
+ ```typescript
50
+ import { join } from 'node:path';
51
+
52
+ const widgetPath = join(__dirname, 'sales-chart.widget.tsx');
53
+
54
+ @Tool({
55
+ name: 'sales_chart',
56
+ ui: { template: { file: widgetPath } },
57
+ })
58
+ ```
59
+
60
+ Pick the anchor that matches your module system — both resolve to the tool source's directory regardless of cwd. The rule is only that the path must **never** be a bare relative string.
61
+
62
+ ## Also: name the widget `*.widget.tsx`
63
+
64
+ The scaffolded `tsconfig.json` excludes `**/*.widget.tsx` from the server typecheck (issue #445). Naming widgets `sales-chart.widget.tsx` keeps server `tsc --noEmit` happy without dragging React types into the server config.
65
+
66
+ ## Verification
67
+
68
+ ```bash
69
+ # Find any bare-relative `file:` literals in ui templates — should return 0 hits
70
+ grep -rE "file:\s*'\.\.?/[^']*\.tsx'" src/**/*.tool.ts
71
+ ```
72
+
73
+ ## See also
74
+
75
+ - [`references/ui-widgets.md`](../references/ui-widgets.md)
76
+ - [`examples/23-tool-with-ui-filesource-tsx`](../examples/23-tool-with-ui-filesource-tsx.md)
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: widget-resource-mode-host-detect
3
+ constraint: 'Leave `ui.resourceMode` unset — the framework host-detects (`inline` for Claude, `cdn` for others).'
4
+ severity: recommended
5
+ ---
6
+
7
+ # Rule: leave `ui.resourceMode` unset by default
8
+
9
+ ## The rule
10
+
11
+ `ui.resourceMode` defaults to a host-detected value (issue #456): `'inline'` for Claude (React bundled into the widget so it renders under Claude's sandbox CSP), `'cdn'` for OpenAI / ChatGPT / Cursor / MCP Inspector (smaller payload from esm.sh). Leave the field unset unless you have a specific reason to override.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ @Tool({
17
+ // …
18
+ ui: {
19
+ template: { file: widgetPath },
20
+ // resourceMode intentionally UNSET — framework picks per host
21
+ },
22
+ })
23
+ ```
24
+
25
+ ## Bad (without justification)
26
+
27
+ ```typescript
28
+ // ❌ pinning to 'cdn' — breaks Claude (widget hangs on "Loading widget…")
29
+ ui: { template: { file: widgetPath }, resourceMode: 'cdn' }
30
+
31
+ // ❌ pinning to 'inline' — fine for Claude, but always-larger payload for OpenAI / ChatGPT
32
+ ui: { template: { file: widgetPath }, resourceMode: 'inline' }
33
+ ```
34
+
35
+ ## When to override (with justification)
36
+
37
+ - **Force `'inline'`** when the widget must work in a network-blocked environment beyond Claude (some kiosks, air-gapped demos, etc.). Pay the larger payload cost intentionally.
38
+ - **Force `'cdn'`** when you specifically know the widget will only be served to CDN-permissive clients AND you want minimum payload. Rare — usually the framework's choice is correct.
39
+ - **Set per call** at the tool layer is the wrong place — `resourceMode` is part of static widget compilation. Per-call decisions belong in `servingMode` instead.
40
+
41
+ ## Why
42
+
43
+ - **Claude's iframe blocks external scripts.** Default `'cdn'` emits an esm.sh import map for React; Claude's CSP blocks it; the widget hangs forever on the FrontMCP "Loading widget…" placeholder. `'inline'` bundles React into the widget's `<script type="module">`.
44
+ - **OpenAI / ChatGPT / Cursor are CDN-permissive.** `'cdn'` is smaller (no inlined React) — better cold-render performance.
45
+ - **The host can be detected at per-call render time.** `renderToolTemplate` reads `platformType` from the request and picks the right mode automatically. Per-tool overrides defeat the detection.
46
+
47
+ ## Static-mode caveat
48
+
49
+ For `servingMode: 'static'` tools (pre-compiled at server startup, before any client connects), there's no platformType to detect against. Static widgets default to `'cdn'` regardless. If a static-mode tool needs to render in Claude, set `resourceMode: 'inline'` explicitly.
50
+
51
+ ## Verification
52
+
53
+ ```bash
54
+ # Find ui blocks with explicit resourceMode — review each for justification
55
+ grep -rE "resourceMode:\s*'(cdn|inline)'" src/**/*.tool.ts
56
+ ```
57
+
58
+ ## See also
59
+
60
+ - [`references/ui-widgets.md`](../references/ui-widgets.md)
61
+ - [`examples/23-tool-with-ui-filesource-tsx`](../examples/23-tool-with-ui-filesource-tsx.md)
@@ -0,0 +1,146 @@
1
+ ---
2
+ name: frontmcp-auth-ui
3
+ description: 'Use when customizing, branding, or replacing the built-in FrontMCP OAuth pages (the login, consent, federated-select, incremental-authorization, and error pages) with your own React components. Covers the auth.ui slot-to-file map and auth.extras name-to-handler map on the auth config (no decorator, no class); the @frontmcp/ui/auth React hooks, the AuthPageWrapper component, and mountAuthPage (client-rendered via an esm.sh import-map plus a per-file server-side transform, with no bundling and no SSR); and the framework-owned CSRF and CSP. Triggers: custom login page, brand the consent screen, replace the OAuth UI, custom authorization UI, style the auth pages, multi-step login. The skill for CUSTOM AUTHORIZATION UI (distinct from auth config in frontmcp-config and permissions in frontmcp-authorities).'
4
+ tags: [auth, auth-ui, login, consent, custom-ui, react, oauth, guide]
5
+ category: config
6
+ targets: [all]
7
+ bundle: [full]
8
+ priority: 5
9
+ visibility: both
10
+ license: Apache-2.0
11
+ metadata:
12
+ docs: https://docs.agentfront.dev/frontmcp/authentication/custom-ui
13
+ ---
14
+
15
+ # FrontMCP Custom Authorization UI (`auth.ui`)
16
+
17
+ Entry point for replacing FrontMCP's built-in OAuth pages (login, consent, federated, incremental, error) with your own React components. Custom UI is a simple **slot→file map** (`auth.ui`) plus an extras **name→handler map** (`auth.extras`) on the auth config — there is no decorator and no class. The `references/custom-auth-ui.md` reference has the full API; the `examples/` show a single login slot and a multi-step extras form.
18
+
19
+ ## When to Use This Skill
20
+
21
+ ### Must Use
22
+
23
+ - Branding or fully replacing the `local`/`remote` mode **login** page with a custom React component
24
+ - Building a custom **consent** screen, **federated** provider picker, **incremental** authorization, or **error** page
25
+ - Adding a server-validated multi-step field to an authorization page (e.g. "add another item") via `auth.extras`
26
+
27
+ ### Recommended
28
+
29
+ - Understanding which half is the server (`auth.ui` / `auth.extras` in `@frontmcp/sdk`) and which is the client (`@frontmcp/ui/auth`)
30
+ - Looking up the `AuthFlowState` fields a slot component receives, or the `/oauth/ui/extra` route
31
+ - Confirming that the framework (not your component) owns CSRF + CSP
32
+
33
+ ### Skip When
34
+
35
+ - You only need to add/rename **fields** on the built-in login page or run a custom verifier — use the declarative `login` / `authenticate` config in `frontmcp-config` → `configure-auth` instead (no React, no build step)
36
+ - You are customizing a **tool widget** (not an auth page) — use `create-tool` → `ui-widgets`
37
+ - You don't need a custom page at all — configuring no `auth.ui` keeps the built-in pages
38
+
39
+ > **Decision:** Use this skill when you want to render your OWN component for an auth slot. Use `configure-auth`'s declarative `login` config when tweaking the built-in page's fields is enough.
40
+
41
+ ## Prerequisites
42
+
43
+ - A FrontMCP server in `local` or `remote` auth mode (see `frontmcp-config` → `configure-auth`)
44
+ - `@frontmcp/ui`, `react`, and `react-dom` installed (`react`/`react-dom` are peer deps of `@frontmcp/ui`)
45
+
46
+ ## Steps
47
+
48
+ 1. Write a React component (default export) for the slot, reading the injected state via `@frontmcp/ui/auth` hooks (see `references/custom-auth-ui.md`)
49
+ 2. Map the slot to its `.tsx` path under `auth.ui` — a RELATIVE path auto-anchored to the config file's directory (no `fileURLToPath`)
50
+ 3. (Optional) Add an `auth.extras[name]` handler function for any mid-flow validated field
51
+ 4. Declare both **under `auth`**: `@FrontMcp({ auth: { mode: 'local', ui: { login: './…' }, extras: { … } } })` (per-app under `splitByApp` — put them on the `@App({ auth })` that owns the pages)
52
+ 5. Verify using the Verification Checklist below
53
+
54
+ ## Scenario Routing Table
55
+
56
+ | Scenario | Reference | Description |
57
+ | -------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------ |
58
+ | Replace any built-in auth page with a React component | `custom-auth-ui` | `auth.ui: { slot: './file.tsx' }`, the hooks, `<AuthPageWrapper>`, `mountAuthPage` |
59
+ | Add a server-validated mid-flow field (multi-step form) | `custom-auth-ui` | `auth.extras: { name: handler }`, the accumulator, `useExtraField` / `useAddedItems` |
60
+ | Look up the flow-state fields a component receives | `custom-auth-ui` | `AuthFlowState` field table (no PII) |
61
+ | Understand the served route and the framework-owned CSRF + CSP | `custom-auth-ui` | `/oauth/ui/extra`, the esm.sh import-map + inline module, security ownership |
62
+
63
+ ## The map form
64
+
65
+ Custom UI is a **slot→file map** on the auth config. Point each slot at a sibling `.tsx`/`.jsx` source (default export); the SDK transpiles it once server-side and inlines it as an ES module, with deps loaded from esm.sh via an import-map and `mountAuthPage` appended for you — exactly as `@Tool({ ui })` leads with the `FileSource` `{ file }` form:
66
+
67
+ ```ts
68
+ // src/server.ts
69
+ import { App } from '@frontmcp/sdk'; // or @FrontMcp
70
+
71
+ @App({
72
+ auth: {
73
+ mode: 'local',
74
+ // slot → RELATIVE .tsx, auto-anchored to THIS config file's directory.
75
+ ui: { login: './auth/login.tsx' },
76
+ // extra name → handler fn (no class).
77
+ extras: { 'envs:add': async (input, ctx) => ({ ok: true, addedItems: [{ key: String(input.key) }] }) },
78
+ },
79
+ })
80
+ export default class Server {}
81
+ ```
82
+
83
+ A relative `auth.ui` path resolves against the directory of the file that declares the `@App`/`@FrontMcp` config — captured automatically at decoration time. **No `fileURLToPath` needed.** Absolute paths pass through; on capture failure the framework falls back to `process.cwd()` with a warning.
84
+
85
+ ## Common Patterns
86
+
87
+ | Pattern | Correct | Incorrect | Why |
88
+ | ------------------ | ---------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
89
+ | Slot registration | `auth: { ui: { login: './login.tsx' } }` | `auth: { ui: [LoginAuthUi] }` (array of classes) | Custom UI is a slot→file MAP now — no decorator, no class |
90
+ | Path anchoring | Relative `'./login.tsx'` (auto-anchored to the config file) | `fileURLToPath(new URL('./login.tsx', import.meta.url))` | The framework captures the config file's dir for you — manual anchoring is no longer needed |
91
+ | Extra registration | `auth: { extras: { 'envs:add': handlerFn } }` | `auth: { extras: [AddEnvExtra] }` (annotated class) | Extras are an extra-name → handler-function MAP now |
92
+ | CSRF | Let the hooks round-trip `csrfToken` | Generating / checking a token in your component | The server mints + verifies CSRF; your component must not |
93
+ | User identity | User-typed fields live in your own `<form>` inputs | Putting email/name into `AuthFlowState` | `AuthFlowState` is PII-free by contract — it carries OAuth client ids + control fields only |
94
+ | Wrapper form | Wrap UI in `<AuthPageWrapper>` (renders the finish `<form>` + hidden fields) | Hand-rolling `pending_auth_id` / `csrf` hidden inputs | The wrapper injects the control fields so a no-JS submit still works |
95
+
96
+ ## Verification Checklist
97
+
98
+ - [ ] `GET /oauth/authorize` returns a page with an EMPTY `#frontmcp-auth-root` (not the built-in page) — the component's rendered markup is NOT in the HTTP response
99
+ - [ ] The page has a `<script type="module">` with the TRANSPILED component (`React.createElement`, NOT a bundle/IIFE/`react-dom/server`) + an `import { mountAuthPage } from '@frontmcp/ui/auth'` tail
100
+ - [ ] The page has a `<script type="importmap">` mapping `react` + `@frontmcp/ui/auth` → `https://esm.sh/...`, with `?external=react,react-dom` on the `@frontmcp/*` URLs (single React)
101
+ - [ ] The HTML injects `window.__FRONTMCP_AUTH__` with the flow state (and no PII — no `email`/`name` field)
102
+ - [ ] Response carries the auth CSP headers (`frame-ancestors 'none'`, `https://esm.sh` allowed, NO `'unsafe-eval'`) and `X-Frame-Options: DENY`
103
+ - [ ] There is **no** `/oauth/ui/<slot>.js` route (it 404s — the module is inlined, not served separately)
104
+ - [ ] (If using `auth.extras`) a valid `POST /oauth/ui/extra` returns `{ ok: true, addedItems }`, an invalid one returns 400, and a bad `csrf` returns 400
105
+ - [ ] Removing the slot from `auth.ui` falls back to the built-in page unchanged
106
+
107
+ ## Troubleshooting
108
+
109
+ | Problem | Cause | Solution |
110
+ | ---------------------------------------------------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
111
+ | Built-in page still shows | Slot not in `auth.ui`, or the `.tsx` failed to transpile | Confirm the slot is in `auth: { ui: { … } }` on the scope that owns the pages; check logs — a transpile error falls back to the built-in page |
112
+ | Blank page / `@frontmcp/ui/auth` 404s in the browser | `@frontmcp/ui/auth` isn't on esm.sh (unpublished monorepo) | Publish `@frontmcp/ui`, OR map it to a locally-served ESM URL via `@FrontMcp({ ui: { cdnOverrides } })` (see `references/custom-auth-ui.md` → Local dev / offline) |
113
+ | `ENOENT` / component not found at runtime | Path doesn't resolve from the config file's dir | Use a path relative to the config file (auto-anchored), or an absolute path |
114
+ | Hooks throw "must be used inside …" | Component rendered without `<AuthPageWrapper>` | Mount via `mountAuthPage(Component)` (it wraps for you) or wrap manually in `<AuthPageWrapper>` |
115
+ | extras POST returns 400 (csrf) | The submitted `csrf` ≠ the server-minted token | Let the hooks send it — `useExtraField` / `submitExtra` attach `pending_auth_id` + `csrf` automatically |
116
+ | Custom page can't import `@frontmcp/ui/auth` | Package (or `react`/`react-dom`) not installed | `npm install @frontmcp/ui react react-dom` |
117
+
118
+ ## Examples
119
+
120
+ Each reference has matching examples under [`examples/<reference>/`](./examples/):
121
+
122
+ ### `custom-auth-ui`
123
+
124
+ | Example | Level | Description |
125
+ | ----------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------ |
126
+ | [`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`. |
127
+ | [`multi-step-auth-extra`](./examples/custom-auth-ui/multi-step-auth-extra.md) | Advanced | Add a server-validated multi-step field with `auth.extras: { 'envs:add': fn }`, `useExtraField`, and `useAddedItems`. |
128
+
129
+ ## Accessing This Skill
130
+
131
+ Skills are distributed as plain SKILL.md files plus a sibling `references/`
132
+ and `examples/` tree, so consumers can pick whichever access mode fits:
133
+
134
+ | Mode | How it works |
135
+ | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
136
+ | **Filesystem** | Read `libs/skills/catalog/frontmcp-auth-ui/` directly from a clone of the catalog repo, or from a published `@frontmcp/skills` install. SKILL.md is the entry point. |
137
+ | **`frontmcp` CLI** | `frontmcp skills list`, `frontmcp skills read frontmcp-auth-ui`, `frontmcp skills read frontmcp-auth-ui:references/<file>.md`, `frontmcp skills install frontmcp-auth-ui` — no server required. |
138
+ | **MCP `skill://`** | When a developer mounts this skill into their own FrontMCP server (`@FrontMcp({ skills: [...] })`), the SDK exposes it via SEP-2640 resources: `skill://frontmcp-auth-ui/SKILL.md`, `skill://frontmcp-auth-ui/references/{file}.md`, etc. The server's `skill://index.json` returns the SEP-2640 discovery document for everything mounted on it. |
139
+
140
+ The catalog itself is **not** an MCP server. The `skill://` URIs only resolve
141
+ when a server has been configured to host this skill.
142
+
143
+ ## Reference
144
+
145
+ - [Custom Authorization UI (`auth.ui`)](https://docs.agentfront.dev/frontmcp/authentication/custom-ui)
146
+ - Related skills: `frontmcp-config` (→ `configure-auth` for the declarative `login` config), `create-tool` (→ `ui-widgets` for tool widgets)
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: login-slot
3
+ reference: custom-auth-ui
4
+ level: intermediate
5
+ description: "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."
6
+ tags: [auth, auth-ui, login, custom-ui, react, client-rendered]
7
+ features:
8
+ - "Mapping a slot to a `.tsx` file with `auth.ui: { login: './login.tsx' }` (the supported render path)"
9
+ - 'Using a RELATIVE path auto-anchored to the config file — no `fileURLToPath`, no decorator, no class'
10
+ - 'Reading the injected `AuthFlowState` via `useAuthFlow()` and submitting with `<form onSubmit={submitFinish}>`'
11
+ - 'Letting `<AuthPageWrapper>` render the enclosing finish `<form>` with the `pending_auth_id` + `csrf` hidden fields'
12
+ - 'The SDK transpiling the `.tsx` server-side and inlining it as an ES module (deps from esm.sh via an import-map) + appending the `mountAuthPage` call automatically'
13
+ ---
14
+
15
+ # Custom Login Slot with `auth.ui`
16
+
17
+ 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.
18
+
19
+ The component reads the server-injected flow state through `useAuthFlow()` and renders
20
+ its own sign-in form. The framework injects `window.__FRONTMCP_AUTH__`, mints the CSRF
21
+ token, sets the CSP, transpiles the `.tsx` server-side, and inlines it into the authorize
22
+ page as a `<script type="module">` with an esm.sh import-map (no bundling, no SSR) — the
23
+ developer writes only the UI.
24
+
25
+ ## Code
26
+
27
+ ```tsx
28
+ // src/auth/login.tsx — the custom component (default export)
29
+ import React from 'react';
30
+
31
+ import { AuthPageWrapper, useAuthFlow } from '@frontmcp/ui/auth';
32
+
33
+ export default function LoginPage(): React.ReactElement {
34
+ // useAuthFlow() returns the injected state + a submitFinish handler. Everything
35
+ // OAuth (pending id, csrf, submit target, slot markers) is already wired.
36
+ const { clientName, scopes, error, submitFinish } = useAuthFlow();
37
+
38
+ return (
39
+ // AuthPageWrapper renders the enclosing <form> (with pending_auth_id + csrf
40
+ // hidden fields) so a no-JS submit works; submitFinish drives the JS path.
41
+ <AuthPageWrapper>
42
+ <h1>Sign in to {clientName ?? 'the application'}</h1>
43
+ {scopes.length > 0 && <p>Requested access: {scopes.join(', ')}</p>}
44
+ {error && <p className="error">{error}</p>}
45
+
46
+ <form onSubmit={submitFinish}>
47
+ <label>
48
+ Email
49
+ <input type="email" name="email" placeholder="you@example.com" required />
50
+ </label>
51
+ <button type="submit">Continue</button>
52
+ </form>
53
+ </AuthPageWrapper>
54
+ );
55
+ }
56
+
57
+ // No need to call mountAuthPage here: the SDK appends `mountAuthPage(LoginPage)` to the
58
+ // inlined module automatically, so a `file`-based component only needs a default export.
59
+ ```
60
+
61
+ ```ts
62
+ // src/server.ts — map the slot UNDER `auth` (per-app under splitByApp)
63
+ import { FrontMcp } from '@frontmcp/sdk';
64
+
65
+ @FrontMcp({
66
+ info: { name: 'MyServer', version: '1.0.0' },
67
+ auth: {
68
+ mode: 'local',
69
+ // Custom auth UI is a slot→file map, scoped to this auth config. The relative
70
+ // path is auto-anchored to THIS server file's directory — no fileURLToPath.
71
+ ui: { login: './auth/login.tsx' },
72
+ // Single-operator dev convenience: email optional so a no-JS submit can mint a code.
73
+ requireEmail: false,
74
+ },
75
+ })
76
+ export default class Server {}
77
+ ```
78
+
79
+ ## What This Demonstrates
80
+
81
+ - Mapping a slot to a `.tsx` file with `auth.ui: { login: './login.tsx' }` (the supported render path)
82
+ - Using a RELATIVE path auto-anchored to the config file — no `fileURLToPath`, no decorator, no class
83
+ - Reading the injected `AuthFlowState` via `useAuthFlow()` and submitting with `<form onSubmit={submitFinish}>`
84
+ - Letting `<AuthPageWrapper>` render the enclosing finish `<form>` with the `pending_auth_id` + `csrf` hidden fields
85
+ - The SDK transpiling the `.tsx` server-side and inlining it as an ES module (deps from esm.sh via an import-map) + appending the `mountAuthPage` call automatically
86
+
87
+ ## Notes
88
+
89
+ - **File path**: the path is relative to the file that declares the `@FrontMcp`/`@App` config and
90
+ is auto-anchored at decoration time — no manual `fileURLToPath`. Absolute paths also work.
91
+ - **Build exclusion**: keep the `.tsx` out of the server's own typecheck (the SDK transpiles it
92
+ separately) — e.g. place it where the scaffolded `tsconfig.json`'s `exclude` keeps it out.
93
+ - **Fallback**: if the component can't be transpiled (missing / invalid `.tsx`), the framework
94
+ logs it and serves the built-in login page instead — a broken custom page can't take the server down.
95
+ - **esm.sh deps**: the browser loads `@frontmcp/ui/auth` from esm.sh via the import-map; in an
96
+ unpublished monorepo, map it to a local URL via `@FrontMcp({ ui: { cdnOverrides } })` (see the reference).
97
+ - **No PII**: the user's email lives in your `<form>`'s `email` input, never in `AuthFlowState`.
@@ -0,0 +1,133 @@
1
+ ---
2
+ name: multi-step-auth-extra
3
+ reference: custom-auth-ui
4
+ level: advanced
5
+ description: "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."
6
+ tags: [auth, auth-ui, auth-extras, useExtraField, useAddedItems, multi-step, react]
7
+ features:
8
+ - "Declaring a server-validated field as an `auth.extras['envs:add']` handler fn returning `{ ok, error?, addedItems? }`"
9
+ - 'Rejecting bad input and duplicates using `ctx.current` (the items already accepted for this extra in this flow)'
10
+ - "Binding the add-row form to `useExtraField('envs:add').onSubmit` (POSTs to `/oauth/ui/extra` with `pending_auth_id` + `csrf` attached)"
11
+ - "Reflecting the server-side accumulator reactively with `useAddedItems('envs:add')` after each successful add"
12
+ - 'Declaring both the `auth.ui` slot and the `auth.extras` handler under `auth`: `@FrontMcp({ auth: { mode, ui, extras } })`'
13
+ ---
14
+
15
+ # Multi-Step Field with `auth.extras`
16
+
17
+ 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.
18
+
19
+ An `auth.extras[name]` handler adds a side endpoint the page POSTs to mid-flow (without finishing
20
+ the authorization). Each accepted submission is appended to a per-`(pending-auth, extra)`
21
+ accumulator the framework keeps; the response carries the full accumulator back so the page
22
+ refreshes via `useAddedItems` without a reload. CSRF is verified server-side on every POST.
23
+
24
+ ## Code
25
+
26
+ ```ts
27
+ // src/auth/extras.ts — the extra handler (a plain function, no class)
28
+ import { type AuthExtraContext } from '@frontmcp/sdk';
29
+
30
+ // Validates an "add environment variable" submission and accumulates accepted rows.
31
+ export async function addEnv(input: Record<string, unknown>, ctx: AuthExtraContext) {
32
+ const key = typeof input['key'] === 'string' ? input['key'].trim() : '';
33
+ if (!key) return { ok: false as const, error: 'key is required' };
34
+ // ctx.current = rows already accepted for this extra in this pending auth.
35
+ if (ctx.current.some((it) => (it as { key?: string }).key === key)) {
36
+ return { ok: false as const, error: `"${key}" was already added` };
37
+ }
38
+ const value = typeof input['value'] === 'string' ? input['value'] : '';
39
+ // `addedItems` is the list of NEW rows to APPEND on success; the framework
40
+ // returns the FULL accumulator map back to the client.
41
+ return { ok: true as const, addedItems: [{ key, value }] };
42
+ }
43
+ ```
44
+
45
+ ```tsx
46
+ // src/auth/login.tsx — the custom component (default export)
47
+ import React from 'react';
48
+
49
+ import { AuthPageWrapper, useAddedItems, useAuthFlow, useExtraField } from '@frontmcp/ui/auth';
50
+
51
+ export default function LoginPage(): React.ReactElement {
52
+ const { clientName, error, submitFinish } = useAuthFlow();
53
+ // Reactive view of the server-side accumulator for the 'envs:add' extra.
54
+ const envs = useAddedItems<{ key: string; value: string }>('envs:add');
55
+ // onSubmit POSTs to /oauth/ui/extra with pending_auth_id + csrf attached.
56
+ const addEnv = useExtraField('envs:add');
57
+
58
+ return (
59
+ <AuthPageWrapper>
60
+ <h1>Connect {clientName ?? 'the application'}</h1>
61
+ {error && <p className="error">{error}</p>}
62
+
63
+ <ul data-testid="env-list">
64
+ {envs.map((e) => (
65
+ <li key={e.key}>
66
+ {e.key}={e.value}
67
+ </li>
68
+ ))}
69
+ </ul>
70
+
71
+ {/* validated extra field — routes to auth.extras['envs:add'] */}
72
+ <form onSubmit={addEnv.onSubmit}>
73
+ <input name="key" placeholder="KEY" />
74
+ <input name="value" placeholder="value" />
75
+ <button disabled={addEnv.pending}>Add</button>
76
+ {addEnv.result && !addEnv.result.ok && <span className="error">{addEnv.result.error}</span>}
77
+ </form>
78
+
79
+ {/* finish — posts pending_auth_id + csrf and follows the OAuth redirect */}
80
+ <form onSubmit={submitFinish}>
81
+ <input name="email" type="email" />
82
+ <button type="submit">Authorize</button>
83
+ </form>
84
+ </AuthPageWrapper>
85
+ );
86
+ }
87
+
88
+ // The SDK appends `mountAuthPage(LoginPage)` to the inlined module automatically — a
89
+ // `file`-based component only needs a default export.
90
+ ```
91
+
92
+ ```ts
93
+ // src/server.ts — declare BOTH the slot and the extra UNDER `auth`
94
+ import { FrontMcp } from '@frontmcp/sdk';
95
+
96
+ import { addEnv } from './auth/extras';
97
+
98
+ @FrontMcp({
99
+ info: { name: 'MyServer', version: '1.0.0' },
100
+ // `ui` / `extras` are scoped to this auth config (per-app under splitByApp).
101
+ // The login path is relative + auto-anchored to THIS file — no fileURLToPath.
102
+ auth: {
103
+ mode: 'local',
104
+ requireEmail: false,
105
+ ui: { login: './auth/login.tsx' },
106
+ extras: { 'envs:add': addEnv },
107
+ },
108
+ })
109
+ export default class Server {}
110
+ ```
111
+
112
+ ## What This Demonstrates
113
+
114
+ - Declaring a server-validated field as an `auth.extras['envs:add']` handler fn returning `{ ok, error?, addedItems? }`
115
+ - Rejecting bad input and duplicates using `ctx.current` (the items already accepted for this extra in this flow)
116
+ - Binding the add-row form to `useExtraField('envs:add').onSubmit` (POSTs to `/oauth/ui/extra` with `pending_auth_id` + `csrf` attached)
117
+ - Reflecting the server-side accumulator reactively with `useAddedItems('envs:add')` after each successful add
118
+ - Declaring both the `auth.ui` slot and the `auth.extras` handler under `auth`: `@FrontMcp({ auth: { mode, ui, extras } })`
119
+
120
+ ## Notes
121
+
122
+ - **Wire shape**: a successful `POST /oauth/ui/extra` returns `{ ok: true, addedItems: { 'envs:add': [...] } }`
123
+ (the FULL accumulator map keyed by extra name). `useExtraField` merges it into context so
124
+ `useAddedItems` re-renders.
125
+ - **Validation errors**: a rejected submit returns HTTP 400 with `{ ok: false, error }`; surface
126
+ `addEnv.result.error` in the form.
127
+ - **CSRF**: the framework verifies the `csrf` token (minted at SSR time) on every extra POST and
128
+ on the finish submit — a mismatch is rejected 400. The hooks attach it for you; never generate
129
+ or check it in your component.
130
+ - **PII-free `ctx`**: the handler's `ctx` is `{ name, pendingAuthId?, current }` only — no user
131
+ identity is passed to it.
132
+ - **Persistence**: the accumulator is in-memory and keyed by the opaque pending-auth id (10-minute
133
+ TTL), so it survives across re-renders within one authorization but not across server restarts.