@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
@@ -71,47 +71,301 @@ class MyApp {}
71
71
  ```
72
72
 
73
73
  - `provider` -- the authorization server URL. FrontMCP fetches JWKS from `{provider}/.well-known/jwks.json`.
74
- - `expectedAudience` -- the `aud` claim value that tokens must contain.
74
+ - `expectedAudience` -- the `aud` claim value that tokens must contain. Supported in **transparent, local, AND remote** modes (it lives on the shared/orchestrated auth options), so set it wherever FrontMCP verifies bearer tokens — not transparent-only.
75
+ - `allowAnonymous` (default `false`) -- when `true`, requests without a token get an anonymous session (scoped by `anonymousScopes`, default `['anonymous']`) instead of a 401.
75
76
 
76
77
  Use transparent mode when clients already have tokens from your identity provider and the server only needs to verify them.
77
78
 
78
79
  ## Mode 3: Local
79
80
 
80
- Local mode lets the FrontMCP server sign its own JWT tokens. This is useful for internal services or environments where an external identity provider is not available.
81
+ Local mode runs a built-in OAuth 2.1 authorization server and signs its own JWT tokens. This is useful for internal services or environments where an external identity provider is not available.
81
82
 
82
83
  ```typescript
83
- @App({
84
+ @FrontMcp({
85
+ info: { name: 'internal-api', version: '1.0.0' },
84
86
  auth: {
85
87
  mode: 'local',
86
88
  local: {
87
- issuer: 'my-server',
89
+ issuer: 'https://mcp.internal.example.com', // optional; auto-derived from request host otherwise
88
90
  },
89
91
  },
90
92
  })
91
- class MyApp {}
93
+ class Server {}
94
+ ```
95
+
96
+ - `local.issuer` -- the `iss` claim set in generated tokens (defaults to a request-host-derived URL if omitted).
97
+
98
+ Token signing uses **HS256, a symmetric secret** read from the `JWT_SECRET` environment variable -- there is **no RSA/EC key pair** and no key store. Generate a stable secret (`JWT_SECRET=$(openssl rand -hex 32)`); if it is unset, FrontMCP falls back to a random per-process secret and all tokens are invalidated on restart.
99
+
100
+ Key local-mode options:
101
+
102
+ - `tokenStorage` -- where authorization codes / refresh tokens / federated sessions persist. Defaults to `'memory'` (lost on restart). Use `{ sqlite: { path } }` for single-node persistence or `{ redis: { ... } }` for multi-instance. This is honored in local mode.
103
+ - `requireEmail` (default `true`) -- when `false`, the login callback mints a code without prompting for an email, deriving a stable `sub` from `anonymousSubject` (default `'local-operator'`). Use for single-operator setups (e.g. Claude Code).
104
+ - `consent` -- enables an **interactive tool-selection screen** during login AND **call-time enforcement**. When `consent.enabled` is `true`, `/oauth/callback` renders a consent screen (after authentication) listing the available tools; the user's checked tools are GET-submitted back to `/oauth/callback`, embedded in the token's `consent` claim, and enforced on every `tools/call` — a call to an unselected tool is rejected with `TOOL_NOT_CONSENTED` (JSON-RPC `-32003`). Honored flags: `groupByApp` (default `true`), `showDescriptions` (default `true`), `allowSelectAll` (default `true`), `requireSelection` (default `true` — rejects an empty submit), `customMessage`, `rememberConsent` (default `true`), `excludedTools` (never offered, always available), `defaultSelectedTools` (pre-checked). Federated logins show the screen after the last provider links. Tokens minted without consent (disabled, or via the test factory) carry no claim and stay all-tools-allowed. `rememberConsent` persists each user's per-client selection (keyed by `consent:{userSub}:{clientId}`, sharing the configured `tokenStorage` backend) and reuses it on the next login: the screen is skipped when no new tool appeared, or re-shown pre-filled when a NEW tool was added (a newly-added tool is never silently granted). Set `rememberConsent: false` to always re-show the screen.
105
+ - `login` -- customize the built-in login page: `title` / `subtitle` / `logoUri`, declarative `fields` (each `{ type: 'text'|'password'|'email'|'select'|'hidden'; label?; required?; placeholder?; options? }`), a full HTML `render(ctx)` override, and a `subject` strategy (`{ fromField, strategy: 'per-session'|'per-account' }`). Omitting `login` keeps the default email/name page.
106
+ - `authenticate(input, ctx)` -- custom verification run at the login callback **before** a token is minted. `input.fields` carries the submitted login fields (reserved OAuth params excluded); `ctx` is `{ get, fetch, logger, clientId?, clientName? }`. Return `{ ok: true, sub?, claims? }` to mint a token (custom `claims` are embedded in the JWT; reserved claims like `sub`/`iss`/`exp`/`scope` are stripped) or `{ ok: false, message, retryField? }` to re-render the login page with the error (no code issued). When set, the email requirement no longer applies.
107
+ - `providers` -- declarative upstream OAuth providers (GitHub, Slack, Jira, …) to orchestrate. When set, FrontMCP federates them at `/oauth/authorize`, stores each provider's tokens **encrypted server-side**, and exposes them to tools via `this.orchestration.getToken(id)`. See [Multi-provider orchestration](#multi-provider-orchestration-providers--federatedauth) below.
108
+ - `federatedAuth` -- gates JWT issuance during federated login: `minProviders` (default `1` when `providers` are set — "no JWT until ≥1 linked"), `requiredProviders` (ids that must all be linked), and `stateValidation` (`'strict'` default).
109
+ - `dcr` -- declarative control surface for the built-in Dynamic Client Registration endpoint (`POST /oauth/register`) and the `/oauth/authorize` client allowlist. All fields optional; omitting `dcr` preserves today's behavior (DCR on in dev, off in prod). See [Dynamic Client Registration](#dynamic-client-registration-dcr) below.
110
+ - `anonymousScopes` (default `['anonymous']`) -- scopes assigned to anonymous sessions (used when `allowDefaultPublic` lets an unauthenticated request through).
111
+ - `allowDefaultPublic` (default `false`) -- when `true`, requests without a token are allowed through as anonymous instead of being rejected with a 401. Keep `false` to require auth on every request.
112
+ - `expectedAudience` -- the `aud` claim value tokens must carry. Also valid in local/remote mode (not just transparent); set it when FrontMCP must reject tokens minted for a different audience.
113
+
114
+ ### Multi-provider orchestration (`providers` + `federatedAuth`)
115
+
116
+ Declare upstream providers to make multi-provider orchestration a turnkey local default. No JWT is minted until the `minProviders` threshold (and every `requiredProviders` id) is met; linked providers' tokens are read by tools via `this.orchestration`.
117
+
118
+ ```typescript
119
+ @FrontMcp({
120
+ info: { name: 'internal-api', version: '1.0.0' },
121
+ auth: {
122
+ mode: 'local',
123
+ providers: [
124
+ {
125
+ id: 'github',
126
+ // `authorizeUrl`/`tokenUrl` are accepted aliases for
127
+ // `authorizationEndpoint`/`tokenEndpoint`.
128
+ authorizeUrl: 'https://github.com/login/oauth/authorize',
129
+ tokenUrl: 'https://github.com/login/oauth/access_token',
130
+ clientId: process.env.GITHUB_CLIENT_ID!,
131
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
132
+ scopes: ['read:user', 'repo'],
133
+ },
134
+ { id: 'slack', authorizeUrl: '…', tokenUrl: '…', clientId: '…', scopes: ['users:read'] },
135
+ ],
136
+ federatedAuth: { minProviders: 1, requiredProviders: ['github'] },
137
+ },
138
+ })
139
+ class Server {}
140
+ ```
141
+
142
+ `UpstreamProviderOptions` fields: `id` (required; used in `getToken(id)`), `authorizationEndpoint`/`authorizeUrl` (required), `tokenEndpoint`/`tokenUrl` (required), `clientId` (required), `clientSecret?`, `scopes?`, `name?`, `userInfoEndpoint?`, `jwksUri?`. The per-provider callback URL is auto-computed as `${issuer}/oauth/provider/${id}/callback` — register that URL with each provider.
143
+
144
+ Tools read downstream tokens through the `this.orchestration` context extension (available in `local`/`remote` mode):
145
+
146
+ ```typescript
147
+ @Tool({ name: 'github_repos' })
148
+ class GitHubReposTool extends ToolContext {
149
+ async execute() {
150
+ if (!this.orchestration.isAuthenticated) return { error: 'not authenticated' };
151
+ const token = await this.orchestration.getToken('github'); // throws if not linked
152
+ const maybe = await this.orchestration.tryGetToken('slack'); // null if skipped
153
+ const res = await fetch('https://api.github.com/user/repos', {
154
+ headers: { Authorization: `Bearer ${token}` },
155
+ });
156
+ return { repos: await res.json() };
157
+ }
158
+ }
159
+ ```
160
+
161
+ Tokens stay server-side and AES-256-GCM-encrypted at rest — never placed in the JWT or exposed to the model. No login PII is stored (only provider tokens + non-PII provider ids).
162
+
163
+ ### Dynamic Client Registration (`dcr`)
164
+
165
+ The built-in `POST /oauth/register` (RFC 7591) endpoint lets clients self-register. By default it is **enabled in development and disabled in production** (`NODE_ENV=production` short-circuits it), holds clients in an in-memory map, and enforces no allowlist. The optional `dcr` block makes that a declarative control surface. **Omitting `dcr` preserves the default behavior exactly.**
166
+
167
+ ```typescript
168
+ @FrontMcp({
169
+ info: { name: 'internal-api', version: '1.0.0' },
170
+ auth: {
171
+ mode: 'local',
172
+ dcr: {
173
+ enabled: false, // force off regardless of NODE_ENV (register → 404; metadata omits registration_endpoint)
174
+ allowedRedirectUris: ['https://app.example.com/callback', 'http://localhost:*/callback'], // exact or `*` glob
175
+ allowedClientIds: ['dashboard'], // only these client_ids may use /oauth/authorize
176
+ initialAccessToken: process.env.DCR_INITIAL_ACCESS_TOKEN, // require `Authorization: Bearer <token>` on register
177
+ clients: [
178
+ // pre-registered trusted clients — accepted WITHOUT a DCR round-trip
179
+ { clientId: 'dashboard', redirectUris: ['https://app.example.com/callback'], clientName: 'Internal Dashboard' },
180
+ ],
181
+ },
182
+ },
183
+ })
184
+ class Server {}
185
+ ```
186
+
187
+ `LocalDcrConfig` fields:
188
+
189
+ - `enabled` -- when `false`, `/oauth/register` responds `404` and `registration_endpoint` is omitted from `/.well-known/oauth-authorization-server`. When unset, defaults to on in dev / off in prod.
190
+ - `allowedRedirectUris` -- exact or simple-`*`-glob allowlist. A `redirect_uri` not on it is rejected at **register** (`400 invalid_redirect_uri`) and at **authorize** (error page — the unlisted URI is never redirected to, an open-redirect guard).
191
+ - `allowedClientIds` -- only these `client_id`s may be used at `/oauth/authorize`. CIMD URL client ids are validated by the CIMD layer and are exempt.
192
+ - `initialAccessToken` -- when set, `/oauth/register` requires a matching `Authorization: Bearer <token>` (constant-time compared); otherwise `401 invalid_token`.
193
+ - `clients` -- pre-registered trusted clients (`{ clientId, redirectUris, clientName?, clientSecret?, tokenEndpointAuthMethod?, grantTypes?, responseTypes?, scope? }`) seeded at startup; accepted by the authorize/token flows **without** a DCR round-trip. Lets you disable DCR and still ship known clients.
194
+
195
+ A successful registration responds `201 Created`. This `dcr` block governs the **local Authorization Server only** — it is unrelated to the upstream-provider `providerConfig.dcrEnabled` / `registrationEndpoint` fields (which register THIS server against an upstream IdP).
196
+
197
+ ### Custom login + verification (`login` / `authenticate`)
198
+
199
+ ```typescript
200
+ @FrontMcp({
201
+ info: { name: 'internal-api', version: '1.0.0' },
202
+ auth: {
203
+ mode: 'local',
204
+ login: {
205
+ title: 'Sign in with your API key',
206
+ fields: { apiKey: { type: 'password', label: 'API Key', required: true } },
207
+ subject: { fromField: 'apiKey', strategy: 'per-account' }, // stable sub per key
208
+ },
209
+ authenticate: async (input, ctx) => {
210
+ const res = await ctx.fetch('https://api.example.com/verify', {
211
+ method: 'POST',
212
+ headers: { authorization: `Bearer ${input.fields.apiKey}` },
213
+ });
214
+ if (!res.ok) return { ok: false, message: 'Invalid API key', retryField: 'apiKey' };
215
+ const { accountId, tier } = (await res.json()) as { accountId: string; tier: string };
216
+ return { ok: true, sub: accountId, claims: { tenantId: accountId, plan: tier } };
217
+ },
218
+ },
219
+ })
220
+ class Server {}
221
+ ```
222
+
223
+ ### Per-session credential vault (`this.credentials`)
224
+
225
+ An `{ ok: true }` result may return `credentials: Array<{ key; secret; metadata? }>`. FrontMCP **persists** these into a built-in, **AES-256-GCM-encrypted**, per-session vault keyed by the authenticated `sub`, and exposes them to tools via `this.credentials`. The vault is enabled automatically in `local` (and `remote`) modes — no wiring required. (This is distinct from `this.authProviders`, which is opt-in scaffolding for downstream OAuth providers — see below.)
226
+
227
+ ```typescript
228
+ auth: {
229
+ mode: 'local',
230
+ login: { fields: { apiKey: { type: 'password', label: 'API Key', required: true } } },
231
+ authenticate: async (input) => {
232
+ const apiKey = input.fields['apiKey'];
233
+ if (!(await isValid(apiKey))) return { ok: false, message: 'Invalid API key' };
234
+ // Persisted, encrypted, keyed by the minted `sub`:
235
+ return { ok: true, credentials: [{ key: 'acme', secret: apiKey, metadata: { baseUrl: 'https://acme.example' } }] };
236
+ },
237
+ }
238
+ ```
239
+
240
+ Read them from any tool:
241
+
242
+ ```typescript
243
+ @Tool({ name: 'call_acme' })
244
+ class CallAcmeTool extends ToolContext {
245
+ async execute(input: Input): Promise<Output> {
246
+ const cred = await this.credentials.get('acme'); // { secret, metadata } | undefined
247
+ if (!cred) {
248
+ const res = await this.credentials.requireConnect({ key: 'acme' });
249
+ if (!res.connected) return { connectUrl: res.resumeUrl }; // framework-signed /oauth/connect URL
250
+ }
251
+ const keys = await this.credentials.list();
252
+ // use cred.secret / cred.metadata …
253
+ }
254
+ }
255
+ ```
256
+
257
+ `this.credentials` API:
258
+
259
+ - `get(key)` -> `Promise<{ secret, metadata } | undefined>` — decrypt a credential for the request's subject.
260
+ - `list()` -> `Promise<string[]>` — credential keys in the current session vault.
261
+ - `requireConnect({ key, context? })` -> returns the credential when connected, else a **framework-signed**, short-lived `/oauth/connect?token=…` resume URL. Opening it renders a single-field add-credential page (reusing `login.fields`); on submit FrontMCP re-invokes `authenticate()` with a `resume: { sub, key, context }` context and **additively** stores the returned credential into the existing session vault. A rotated-away (dead) session is refused.
262
+
263
+ Security: per-record AES key derived (HKDF) from a fresh per-session `vaultId` + a pepper from `VAULT_SECRET ?? JWT_SECRET` (random per-process fallback, logged with a warning — credentials then do not survive a restart, never plaintext). A disconnect + reconnect rotates the `vaultId`, so the reconnected session sees an **empty vault** and old ciphertext is undecryptable. The vault is backed by the same `auth.tokenStorage` backend (memory / Redis / SQLite). No login PII is stored.
264
+
265
+ ### Session secure-secret store (`this.secureStore`)
266
+
267
+ `this.credentials` is OAuth-credential-centric (resume URLs, per-authorize vault rotation). For **arbitrary user-typed secrets** (an API key a tool prompts for, a webhook signing secret, a per-session draft token) use `this.secureStore` — a general, typed, session-scoped key→secret store whose backing is **pluggable per deployment**. It is enabled automatically in `local` (and `remote`) modes. Stop re-implementing AES-GCM + HKDF + scope + persistence by hand.
268
+
269
+ Select the backing with `auth.secureStore` (mirrors `tokenStorage`, plus a namespace `scope`):
270
+
271
+ ```typescript
272
+ @FrontMcp({
273
+ apps: [MyApp],
274
+ auth: {
275
+ mode: 'local',
276
+ // 'memory' | { sqlite } | { redis } | { backend } | { scope?, ttlMs?, encryption? }
277
+ secureStore: { sqlite: { path: './.frontmcp/secrets.sqlite' }, scope: 'user' },
278
+ },
279
+ })
280
+ export default class Server {}
92
281
  ```
93
282
 
94
- - `local.issuer` -- the `iss` claim set in generated tokens (defaults to server URL if omitted).
283
+ Read/write from any tool (or the auth UI):
284
+
285
+ ```typescript
286
+ @Tool({ name: 'save_api_key' })
287
+ class SaveApiKeyTool extends ToolContext {
288
+ async execute(input: { apiKey: string }) {
289
+ await this.secureStore.set('stg.api-key', input.apiKey); // JSON-serialized
290
+ const key = await this.secureStore.get<string>('stg.api-key');
291
+ const all = await this.secureStore.list();
292
+ // await this.secureStore.delete('stg.api-key');
293
+ return { saved: key !== undefined, keys: all };
294
+ }
295
+ }
296
+ ```
95
297
 
96
- The server generates a signing key pair on startup (or loads one from the configured key store). Clients obtain tokens through a server-provided endpoint.
298
+ Backings:
299
+
300
+ - `'memory'` (default) -- in-memory, **AES-256-GCM**-encrypted via `VaultEncryption` (lost on restart).
301
+ - `{ sqlite: { path } }` / `{ redis: { ... } }` -- persistent; same `VaultEncryption` over the shared `StorageAdapter` (reuses `createTokenStorageAdapter`). When the backing matches `tokenStorage`, the same adapter/connection is shared.
302
+ - `{ backend }` -- a **custom** backing implementing the four-method `SecureStoreBackend` (`get`/`set`/`delete`/`list`), e.g. an OS keychain. Used as-is — **no native dependency is bundled** by the framework (see below).
303
+
304
+ Scope (`scope`, identity is hashed into the namespace, never stored raw):
305
+
306
+ - `user` (default) -- keyed by the authenticated `sub`; anonymous requests read empty / skip writes.
307
+ - `session` -- keyed by the transport `sessionId`.
308
+ - `global` -- one server-wide namespace.
309
+
310
+ `this.secureStore` API: `get<T>(key)` -> `Promise<T | undefined>`, `set<T>(key, value, { ttlMs? })`, `delete(key)` -> `Promise<boolean>`, `list()` -> `Promise<string[]>`. Object configs also accept `ttlMs` (default write TTL) and `encryption.pepper` (overrides `VAULT_SECRET ?? JWT_SECRET`).
311
+
312
+ **OS keychain is pluggable, not bundled.** FrontMCP does not ship `keytar`/`wincred`/`libsecret`. To back the store with an OS keychain, pass `secureStore: { backend: myKeychainBackend }` where `myKeychainBackend` implements `SecureStoreBackend` (you load the native peer-dep). A backend deals only in `(namespace, key) → string`; the accessor resolves the namespace and JSON-serializes values. Backends that cannot honor a TTL (an OS keychain) ignore the `ttlMs` argument; OS keychains rely on the OS for at-rest encryption rather than `VaultEncryption`.
97
313
 
98
314
  ## Mode 4: Remote
99
315
 
100
- Remote mode performs a full OAuth 2.1 authorization flow with an external provider. Clients are redirected to the provider for authentication and return with an authorization code.
316
+ Remote mode runs a local OAuth 2.1 server that **proxies user authentication to a
317
+ single mandatory upstream IdP**. `GET /oauth/authorize` redirects **straight to
318
+ the upstream IdP** — there is no FrontMCP login page and no provider-selection
319
+ page. The IdP returns to `/oauth/provider/{id}/callback`; FrontMCP exchanges the
320
+ code, stores the upstream tokens encrypted (server-side), derives the session
321
+ identity (`sub`/`email`/`name`) from the **upstream user**, and mints its own
322
+ HS256 session token for the MCP client.
101
323
 
102
324
  ```typescript
103
- @App({
325
+ @FrontMcp({
326
+ info: { name: 'srv', version: '1.0.0' },
327
+ apps: [MyApp],
104
328
  auth: {
105
329
  mode: 'remote',
106
330
  provider: 'https://auth.example.com',
107
- clientId: 'xxx',
331
+ clientId: 'xxx', // pre-registered (DCR not yet wired)
332
+ clientSecret: process.env['OAUTH_CLIENT_SECRET'],
333
+ scopes: ['openid', 'profile', 'email'],
334
+ providerConfig: { id: 'idp' }, // stable provider id (defaults to provider host)
108
335
  },
109
336
  })
110
- class MyApp {}
337
+ class Server {}
111
338
  ```
112
339
 
113
- - `provider` -- the OAuth 2.1 authorization server URL.
114
- - `clientId` -- the OAuth client identifier registered with the provider.
340
+ - `provider` -- the upstream OAuth 2.1 authorization server base URL. Endpoints are
341
+ derived from it (`/authorize`, `/token`, `/userinfo`, `/.well-known/jwks.json`)
342
+ unless overridden via `providerConfig.{authEndpoint,tokenEndpoint,userInfoEndpoint,jwksUri}`.
343
+ - `clientId` -- the OAuth client identifier **pre-registered** with the provider.
344
+ - `providerConfig.id` -- stable id used by tools: `this.orchestration.getToken('idp')`.
345
+
346
+ Tools read the upstream IdP token (never exposed to the LLM) via the orchestration
347
+ accessor:
348
+
349
+ ```typescript
350
+ @Tool({ name: 'whoami' })
351
+ class Whoami extends ToolContext {
352
+ async execute() {
353
+ const token = await this.orchestration.tryGetToken('idp');
354
+ if (!token) return { error: 'Re-authenticate' }; // upstream auto-refresh not yet wired
355
+ return await (
356
+ await this.fetch('https://auth.example.com/userinfo', {
357
+ headers: { Authorization: `Bearer ${token}` },
358
+ })
359
+ ).json();
360
+ }
361
+ }
362
+ ```
363
+
364
+ **Deferred (not yet wired):** upstream **Dynamic Client Registration**
365
+ (`providerConfig.dcrEnabled` / `registrationEndpoint`) — provide a pre-registered
366
+ `clientId`; and upstream **token auto-refresh** — once the upstream access token
367
+ expires the user must re-authenticate (FrontMCP's own session token still
368
+ refreshes via the `refresh_token` grant).
115
369
 
116
370
  ## OAuth Local Dev Flow
117
371
 
@@ -158,29 +412,17 @@ class PublicApi {}
158
412
  class AdminApi {}
159
413
  ```
160
414
 
161
- ## Credential Vault
162
-
163
- The credential vault stores downstream API tokens obtained during the OAuth flow. Use it when your MCP tools need to call external APIs on behalf of the authenticated user. Credential vault is managed through the auth provider's OAuth flow — downstream tokens are stored automatically when users authorize external services.
415
+ ## Credential Vault (`this.authProviders`)
164
416
 
165
- ```typescript
166
- @App({
167
- name: 'MyApp',
168
- auth: {
169
- mode: 'remote',
170
- provider: 'https://auth.example.com',
171
- clientId: 'mcp-client-id',
172
- },
173
- })
174
- class MyApp {}
175
- ```
417
+ The credential vault and its `this.authProviders` accessor are intended to surface downstream API tokens to tools. The accessor and the underlying vault primitives exist in `@frontmcp/auth`, but they are **scaffolding** — the vault must be explicitly configured/registered with providers before `this.authProviders` resolves. It is **not** turnkey-wired for `local` mode (or auto-populated by simply setting `auth.mode`), so do not assume credentials appear automatically from configuring an auth mode.
176
418
 
177
- Tools access downstream credentials via the `this.authProviders` context extension:
419
+ When the vault is configured with registered providers, tools read credentials via the `this.authProviders` context extension:
178
420
 
179
421
  ```typescript
180
422
  @Tool({ name: 'create_github_issue' })
181
423
  class CreateGithubIssueTool extends ToolContext {
182
424
  async execute(input: { title: string; body: string }) {
183
- // Access downstream credentials via the authProviders context extension
425
+ // Resolves only when the AuthProviders vault is configured with providers.
184
426
  const github = await this.authProviders.get('github');
185
427
  const headers = await this.authProviders.headers('github');
186
428
  // Use headers to call GitHub API
@@ -188,22 +430,24 @@ class CreateGithubIssueTool extends ToolContext {
188
430
  }
189
431
  ```
190
432
 
191
- The `authProviders` accessor (from `@frontmcp/auth`) provides:
433
+ The `authProviders` accessor (from `@frontmcp/auth`) exposes:
192
434
 
193
435
  - `get(provider)` -- get the credential/token for a provider.
194
436
  - `headers(provider)` -- get pre-formatted auth headers for HTTP requests.
195
437
  - `has(provider)` -- check if a provider is configured.
196
438
  - `refresh(provider)` -- force refresh the credential.
197
439
 
440
+ If the vault is not configured, accessing `this.authProviders` throws (`AuthProvidersNotConfiguredError`).
441
+
198
442
  ## Common Patterns
199
443
 
200
- | Pattern | Correct | Incorrect | Why |
201
- | --------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------- | ----------------------------------------------------------------------------- |
202
- | Session store in production | Use Redis or Vercel KV session store | Use default in-memory session store | Sessions are lost on restart; in-memory does not survive process recycling |
203
- | Secret management | Load `clientId`, vault secrets, and Redis passwords from environment variables | Hardcode secrets in source code | Hardcoded secrets leak into version control and are difficult to rotate |
204
- | Audience validation | Always set `expectedAudience` in transparent/remote mode | Omit the audience field | Without audience validation, tokens issued for any audience would be accepted |
205
- | Auth mode for development | Use `public` mode or local OAuth mock for dev environments | Use `remote` mode pointing at production IdP during development | Avoids accidental production token usage and simplifies local iteration |
206
- | Vault encryption secret | Generate a strong random secret and store in env var `VAULT_SECRET` | Use a short or predictable string for vault encryption | Weak secrets compromise all stored downstream credentials |
444
+ | Pattern | Correct | Incorrect | Why |
445
+ | --------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
446
+ | Session store in production | Use Redis or Vercel KV session store | Use default in-memory session store | Sessions are lost on restart; in-memory does not survive process recycling |
447
+ | Secret management | Load `clientId`, vault secrets, and Redis passwords from environment variables | Hardcode secrets in source code | Hardcoded secrets leak into version control and are difficult to rotate |
448
+ | Audience validation | Always set `expectedAudience` in transparent/remote mode | Omit the audience field | Without audience validation, tokens issued for any audience would be accepted |
449
+ | Auth mode for development | Use `public` mode or local OAuth mock for dev environments | Use `remote` mode pointing at production IdP during development | Avoids accidental production token usage and simplifies local iteration |
450
+ | Local-mode signing secret | Set a stable `JWT_SECRET` (e.g. `openssl rand -hex 32`) in env | Leave `JWT_SECRET` unset in a long-lived deployment | Unset means a random per-process HS256 secret; every restart invalidates all issued tokens |
207
451
 
208
452
  ## Verification Checklist
209
453
 
@@ -217,7 +461,7 @@ The `authProviders` accessor (from `@frontmcp/auth`) provides:
217
461
  **Security**
218
462
 
219
463
  - [ ] No secrets are hardcoded in source files -- all loaded from environment variables
220
- - [ ] Vault encryption secret is a strong random value
464
+ - [ ] `JWT_SECRET` is set to a stable strong random value when using `local`/`remote` mode (otherwise tokens are invalidated on every restart)
221
465
  - [ ] Production deployments use Redis or Vercel KV for session storage, not in-memory
222
466
 
223
467
  **Runtime**
@@ -229,21 +473,23 @@ The `authProviders` accessor (from `@frontmcp/auth`) provides:
229
473
 
230
474
  ## Troubleshooting
231
475
 
232
- | Problem | Cause | Solution |
233
- | --------------------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
234
- | `JWKS fetch failed` error on startup | The `provider` URL is unreachable or does not serve `/.well-known/jwks.json` | Verify the provider URL is correct and accessible from the server; check network/firewall rules |
235
- | Tokens rejected with `invalid audience` | The `expectedAudience` value does not match the `aud` claim in the token | Align the `expectedAudience` config with the audience value your identity provider sets in tokens |
236
- | Sessions lost after server restart | Using the default in-memory session store in production | Switch to Redis or Vercel KV session store via `configure-session` reference |
237
- | `VAULT_SECRET is not defined` error | The vault encryption secret environment variable is missing | Set `VAULT_SECRET` in your environment or `.env` file before starting the server |
238
- | OAuth redirect fails in local dev | `remote` mode requires HTTPS and reachable callback URLs | Set `NODE_ENV=development` to relax HTTPS requirements, or use a local OAuth mock server |
476
+ | Problem | Cause | Solution |
477
+ | --------------------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ |
478
+ | `JWKS fetch failed` error on startup | The `provider` URL is unreachable or does not serve `/.well-known/jwks.json` | Verify the provider URL is correct and accessible from the server; check network/firewall rules |
479
+ | Tokens rejected with `invalid audience` | The `expectedAudience` value does not match the `aud` claim in the token | Align the `expectedAudience` config with the audience value your identity provider sets in tokens |
480
+ | Sessions lost after server restart | Using the default in-memory session store in production | Switch to Redis or Vercel KV session store via `configure-session` reference |
481
+ | Local-mode tokens invalid after restart | `JWT_SECRET` unset (random per-process secret) and/or `tokenStorage: 'memory'` | Set a stable `JWT_SECRET`; use `tokenStorage: { sqlite: { path } }` or `{ redis }` to persist codes/refresh tokens |
482
+ | OAuth redirect fails in local dev | `remote` mode requires HTTPS and reachable callback URLs | Set `NODE_ENV=development` to relax HTTPS requirements, or use a local OAuth mock server |
239
483
 
240
484
  ## Examples
241
485
 
242
- | Example | Level | Description |
243
- | ---------------------------------------------------------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
244
- | [`multi-app-auth`](../examples/configure-auth/multi-app-auth.md) | Advanced | Configure a single FrontMCP server with multiple apps, each using a different auth mode -- public for open endpoints and remote for admin endpoints. |
245
- | [`public-mode-setup`](../examples/configure-auth/public-mode-setup.md) | Basic | Set up a FrontMCP server with public (unauthenticated) access and anonymous scopes. |
246
- | [`remote-oauth-with-vault`](../examples/configure-auth/remote-oauth-with-vault.md) | Intermediate | Configure a FrontMCP server with remote OAuth 2.1 authentication and use the credential vault to call downstream APIs on behalf of the authenticated user. |
486
+ | Example | Level | Description |
487
+ | ---------------------------------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
488
+ | [`multi-app-auth`](../examples/configure-auth/multi-app-auth.md) | Advanced | Configure a single FrontMCP server with multiple apps, each using a different auth mode -- public for open endpoints and remote for admin endpoints. |
489
+ | [`public-mode-setup`](../examples/configure-auth/public-mode-setup.md) | Basic | Set up a FrontMCP server with public (unauthenticated) access and anonymous scopes. |
490
+ | [`remote-oauth-with-vault`](../examples/configure-auth/remote-oauth-with-vault.md) | Intermediate | Configure a FrontMCP server with local OAuth orchestration of an upstream provider and read the downstream provider token in a tool via this.orchestration.getToken. |
491
+ | [`local-credential-vault`](../examples/configure-auth/local-credential-vault.md) | Intermediate | 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. |
492
+ | [`local-secure-store`](../examples/configure-auth/local-secure-store.md) | Intermediate | 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. |
247
493
 
248
494
  > See all examples in [`examples/configure-auth/`](../examples/configure-auth/)
249
495
 
@@ -157,14 +157,97 @@ frontmcp build --target vercel
157
157
  | `takeoverGracePeriodMs` | number | 5000 | Grace period before takeover |
158
158
  | `redisKeyPrefix` | string | `mcp:ha:` | Redis key prefix |
159
159
 
160
+ ### Project-Defined CLI Commands (`cli.commands`)
161
+
162
+ Register project-specific verbs that ship alongside the built-in
163
+ `frontmcp` commands. Each verb spawns a runner module (TS or JS) as a
164
+ child process — the project's own code never runs in the CLI process.
165
+
166
+ ```typescript
167
+ export default defineConfig({
168
+ name: 'my-server',
169
+ deployments: [{ target: 'node' }],
170
+ cli: {
171
+ commands: {
172
+ deploy: {
173
+ entry: './scripts/deploy.ts',
174
+ description: 'Push the current build to staging',
175
+ arguments: [{ name: 'env', required: true }],
176
+ options: [{ flags: '-n, --dry-run' }, { flags: '-c, --concurrency <num>', default: 4 }],
177
+ },
178
+ 'db-migrate': { entry: './scripts/migrate.ts' },
179
+ },
180
+ },
181
+ });
182
+ ```
183
+
184
+ | Field | Type | Description |
185
+ | ------------- | -------------------------- | --------------------------------------------------- |
186
+ | `entry` | string | Path to runner (TS/JS), relative to project root |
187
+ | `description` | string | One-line description (shown in `--help`) |
188
+ | `arguments` | `ProjectCommandArgument[]` | Positional args (`{ name, required?, variadic? }`) |
189
+ | `options` | `ProjectCommandOption[]` | Named options (`{ flags, description?, default? }`) |
190
+ | `hidden` | boolean | Hide from `--help` (verb still invokable) |
191
+
192
+ Verb names must match `/^[a-zA-Z][a-zA-Z0-9:_-]*$/` and may not collide
193
+ with a built-in (`dev`, `build`, `test`, `start`, `skills`, etc.). Use a
194
+ namespaced prefix to avoid collisions: `project:init`, `db-migrate`.
195
+
196
+ Runner selection:
197
+
198
+ - `.ts` / `.tsx` / `.mts` / `.cts` → `node --import tsx <entry>`
199
+ - `.js` / `.mjs` / `.cjs` → `node <entry>`
200
+
201
+ The runner gets the parsed positionals as argv plus a
202
+ `FRONTMCP_PROJECT_COMMAND` env var holding a JSON payload of
203
+ `{ verb, positionals, options, cwd }`.
204
+
205
+ List every registered verb (built-in + project) with:
206
+
207
+ ```bash
208
+ frontmcp --list-commands
209
+ ```
210
+
160
211
  ## File Resolution Order
161
212
 
213
+ Per-invocation precedence (issue #400):
214
+
215
+ 1. Explicit `--config <path>` flag.
216
+ 2. `FRONTMCP_CONFIG` env var.
217
+ 3. Upward walk from `cwd` to the nearest ancestor containing a `frontmcp.config.*` (caps at 10 levels — monorepo nested apps work without `cd <repo-root>`).
218
+ 4. Fallback: `package.json` (derives name, default node target).
219
+
220
+ Within a directory:
221
+
162
222
  1. `frontmcp.config.ts`
163
223
  2. `frontmcp.config.js`
164
224
  3. `frontmcp.config.json`
165
225
  4. `frontmcp.config.mjs`
166
226
  5. `frontmcp.config.cjs`
167
- 6. Fallback: `package.json` (derives name, default node target)
227
+
228
+ ## Override precedence (issue #400)
229
+
230
+ For every CLI option that's also expressible in the config:
231
+
232
+ ```
233
+ explicit CLI flag > FRONTMCP_<NAME> env var > frontmcp.config field > built-in default
234
+ ```
235
+
236
+ ## Per-command consumption (issue #400)
237
+
238
+ The config is consumed by every `frontmcp` command, not just `build`:
239
+
240
+ | Command | Config fields consumed |
241
+ | --------------------------------- | --------------------------------------------------------------------------------------------------- |
242
+ | `build` | `name`, `version`, `entry`, `deployments`, `build`, `nodeVersion` |
243
+ | `dev` | `entry`, `transport.http.port`, `env.shared` ⊕ `env.dev` |
244
+ | `test` | `test.timeoutMs` / `test.runInBand` / `test.coverage` / `test.testMatch`, `env.shared` ⊕ `env.test` |
245
+ | `inspector` | `transport.default`, `transport.http.port`, `transport.stdio` |
246
+ | `pm start` / `socket` / `service` | `name`, `entry`, `transport.http.port`, `transport.http.socketPath`, `env.shared` ⊕ `env.ship` |
247
+ | `skills install` / `export` | `skills.provider`, `skills.bundle`, `skills.install`, `skills.exportTarget` |
248
+ | `eject-mcp-config <client>` | `clients.<client>`, `name`, `transport`, `env.ship` |
249
+
250
+ See `transport`, `env`, `clients`, `test`, `skills` field reference in [docs/frontmcp/deployment/frontmcp-config](https://docs.agentfront.dev/frontmcp/deployment/frontmcp-config).
168
251
 
169
252
  ## JSON Schema for IDE Support
170
253