fa-mcp-sdk 0.4.142 → 0.11.2

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 (200) hide show
  1. package/README.md +5 -0
  2. package/cli-template/.dockerignore +16 -0
  3. package/cli-template/.gitlab-ci.yml +135 -0
  4. package/cli-template/AGENTS.md +1 -0
  5. package/cli-template/CHANGELOG.md +64 -0
  6. package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +27 -4
  7. package/cli-template/FA-MCP-SDK-DOC/02-1-tools-and-api.md +195 -0
  8. package/cli-template/FA-MCP-SDK-DOC/02-2-prompts-and-resources.md +172 -9
  9. package/cli-template/FA-MCP-SDK-DOC/03-configuration.md +170 -12
  10. package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +158 -8
  11. package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +67 -6
  12. package/cli-template/FA-MCP-SDK-DOC/07-testing-and-operations.md +31 -15
  13. package/cli-template/FA-MCP-SDK-DOC/10-mcp-apps.md +1 -1
  14. package/cli-template/FA-MCP-SDK-DOC/11-public-contract.md +342 -0
  15. package/cli-template/README.md +37 -0
  16. package/cli-template/deploy/docker/.env.example +10 -0
  17. package/cli-template/deploy/docker/Dockerfile +44 -0
  18. package/cli-template/deploy/docker/Dockerfile.local +29 -0
  19. package/cli-template/deploy/docker/README.md +94 -0
  20. package/cli-template/deploy/docker/config/local.docker.yaml +14 -0
  21. package/cli-template/deploy/docker/docker-compose.yml +31 -0
  22. package/cli-template/deploy/gitlab-runner/.env.example +16 -0
  23. package/cli-template/deploy/gitlab-runner/README.md +65 -0
  24. package/cli-template/deploy/gitlab-runner/config/config.toml.template +26 -0
  25. package/cli-template/deploy/gitlab-runner/docker-compose.yml +39 -0
  26. package/cli-template/deploy/gitlab-runner/entrypoint.sh +27 -0
  27. package/cli-template/deploy/gitlab-runner/start.sh +47 -0
  28. package/cli-template/gitignore +96 -95
  29. package/cli-template/package.json +1 -1
  30. package/config/_local.yaml +73 -11
  31. package/config/custom-environment-variables.yaml +102 -0
  32. package/config/default.yaml +164 -11
  33. package/config/local.yaml +20 -19
  34. package/dist/core/_types_/config.d.ts +119 -0
  35. package/dist/core/_types_/config.d.ts.map +1 -1
  36. package/dist/core/_types_/types.d.ts +137 -4
  37. package/dist/core/_types_/types.d.ts.map +1 -1
  38. package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
  39. package/dist/core/agent-tester/agent-tester-router.js +25 -11
  40. package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
  41. package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
  42. package/dist/core/agent-tester/services/TesterMcpClientService.js +6 -4
  43. package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
  44. package/dist/core/auth/admin-auth.js +4 -4
  45. package/dist/core/auth/admin-auth.js.map +1 -1
  46. package/dist/core/auth/agent-tester-auth.d.ts +1 -1
  47. package/dist/core/auth/agent-tester-auth.d.ts.map +1 -1
  48. package/dist/core/auth/agent-tester-auth.js +8 -4
  49. package/dist/core/auth/agent-tester-auth.js.map +1 -1
  50. package/dist/core/auth/auth-profile.d.ts +38 -0
  51. package/dist/core/auth/auth-profile.d.ts.map +1 -0
  52. package/dist/core/auth/auth-profile.js +101 -0
  53. package/dist/core/auth/auth-profile.js.map +1 -0
  54. package/dist/core/auth/jwt-v2.d.ts +27 -0
  55. package/dist/core/auth/jwt-v2.d.ts.map +1 -0
  56. package/dist/core/auth/jwt-v2.js +180 -0
  57. package/dist/core/auth/jwt-v2.js.map +1 -0
  58. package/dist/core/auth/jwt.d.ts +27 -13
  59. package/dist/core/auth/jwt.d.ts.map +1 -1
  60. package/dist/core/auth/jwt.js +36 -13
  61. package/dist/core/auth/jwt.js.map +1 -1
  62. package/dist/core/auth/key-resolver.d.ts +74 -0
  63. package/dist/core/auth/key-resolver.d.ts.map +1 -0
  64. package/dist/core/auth/key-resolver.js +330 -0
  65. package/dist/core/auth/key-resolver.js.map +1 -0
  66. package/dist/core/auth/middleware.d.ts.map +1 -1
  67. package/dist/core/auth/middleware.js +66 -0
  68. package/dist/core/auth/middleware.js.map +1 -1
  69. package/dist/core/auth/multi-auth.d.ts +1 -1
  70. package/dist/core/auth/multi-auth.d.ts.map +1 -1
  71. package/dist/core/auth/multi-auth.js +7 -7
  72. package/dist/core/auth/multi-auth.js.map +1 -1
  73. package/dist/core/auth/token-generator/server.js +4 -4
  74. package/dist/core/auth/token-generator/server.js.map +1 -1
  75. package/dist/core/auth/types.d.ts +5 -0
  76. package/dist/core/auth/types.d.ts.map +1 -1
  77. package/dist/core/db/pg-db.d.ts +7 -0
  78. package/dist/core/db/pg-db.d.ts.map +1 -1
  79. package/dist/core/db/pg-db.js +54 -3
  80. package/dist/core/db/pg-db.js.map +1 -1
  81. package/dist/core/errors/BaseMcpError.d.ts +21 -1
  82. package/dist/core/errors/BaseMcpError.d.ts.map +1 -1
  83. package/dist/core/errors/BaseMcpError.js +20 -1
  84. package/dist/core/errors/BaseMcpError.js.map +1 -1
  85. package/dist/core/errors/ValidationError.d.ts +5 -0
  86. package/dist/core/errors/ValidationError.d.ts.map +1 -1
  87. package/dist/core/errors/ValidationError.js +6 -1
  88. package/dist/core/errors/ValidationError.js.map +1 -1
  89. package/dist/core/errors/errors.d.ts +31 -3
  90. package/dist/core/errors/errors.d.ts.map +1 -1
  91. package/dist/core/errors/errors.js +86 -6
  92. package/dist/core/errors/errors.js.map +1 -1
  93. package/dist/core/errors/specific-errors.d.ts +54 -0
  94. package/dist/core/errors/specific-errors.d.ts.map +1 -0
  95. package/dist/core/errors/specific-errors.js +82 -0
  96. package/dist/core/errors/specific-errors.js.map +1 -0
  97. package/dist/core/index.d.ts +10 -2
  98. package/dist/core/index.d.ts.map +1 -1
  99. package/dist/core/index.js +9 -1
  100. package/dist/core/index.js.map +1 -1
  101. package/dist/core/init-mcp-server.d.ts.map +1 -1
  102. package/dist/core/init-mcp-server.js +39 -0
  103. package/dist/core/init-mcp-server.js.map +1 -1
  104. package/dist/core/mcp/create-mcp-server.d.ts +12 -6
  105. package/dist/core/mcp/create-mcp-server.d.ts.map +1 -1
  106. package/dist/core/mcp/create-mcp-server.js +592 -33
  107. package/dist/core/mcp/create-mcp-server.js.map +1 -1
  108. package/dist/core/mcp/debug-trace.d.ts +3 -1
  109. package/dist/core/mcp/debug-trace.d.ts.map +1 -1
  110. package/dist/core/mcp/debug-trace.js +17 -2
  111. package/dist/core/mcp/debug-trace.js.map +1 -1
  112. package/dist/core/mcp/deprecation.d.ts +31 -0
  113. package/dist/core/mcp/deprecation.d.ts.map +1 -0
  114. package/dist/core/mcp/deprecation.js +96 -0
  115. package/dist/core/mcp/deprecation.js.map +1 -0
  116. package/dist/core/mcp/mcp-logging.d.ts +32 -0
  117. package/dist/core/mcp/mcp-logging.d.ts.map +1 -0
  118. package/dist/core/mcp/mcp-logging.js +97 -0
  119. package/dist/core/mcp/mcp-logging.js.map +1 -0
  120. package/dist/core/mcp/pagination.d.ts +13 -0
  121. package/dist/core/mcp/pagination.d.ts.map +1 -0
  122. package/dist/core/mcp/pagination.js +50 -0
  123. package/dist/core/mcp/pagination.js.map +1 -0
  124. package/dist/core/mcp/prompts.d.ts +5 -1
  125. package/dist/core/mcp/prompts.d.ts.map +1 -1
  126. package/dist/core/mcp/prompts.js +3 -1
  127. package/dist/core/mcp/prompts.js.map +1 -1
  128. package/dist/core/mcp/resources.d.ts +9 -0
  129. package/dist/core/mcp/resources.d.ts.map +1 -1
  130. package/dist/core/mcp/resources.js +158 -11
  131. package/dist/core/mcp/resources.js.map +1 -1
  132. package/dist/core/mcp/server-stdio.d.ts +7 -1
  133. package/dist/core/mcp/server-stdio.d.ts.map +1 -1
  134. package/dist/core/mcp/server-stdio.js +8 -3
  135. package/dist/core/mcp/server-stdio.js.map +1 -1
  136. package/dist/core/mcp/task-store.d.ts +97 -0
  137. package/dist/core/mcp/task-store.d.ts.map +1 -0
  138. package/dist/core/mcp/task-store.js +175 -0
  139. package/dist/core/mcp/task-store.js.map +1 -0
  140. package/dist/core/mcp/tool-limits.d.ts +22 -0
  141. package/dist/core/mcp/tool-limits.d.ts.map +1 -0
  142. package/dist/core/mcp/tool-limits.js +115 -0
  143. package/dist/core/mcp/tool-limits.js.map +1 -0
  144. package/dist/core/mcp/validate-tool-args.d.ts +16 -0
  145. package/dist/core/mcp/validate-tool-args.d.ts.map +1 -0
  146. package/dist/core/mcp/validate-tool-args.js +67 -0
  147. package/dist/core/mcp/validate-tool-args.js.map +1 -0
  148. package/dist/core/mcp/validate-tool-names.d.ts +11 -0
  149. package/dist/core/mcp/validate-tool-names.d.ts.map +1 -0
  150. package/dist/core/mcp/validate-tool-names.js +23 -0
  151. package/dist/core/mcp/validate-tool-names.js.map +1 -0
  152. package/dist/core/metrics/metrics.d.ts +45 -0
  153. package/dist/core/metrics/metrics.d.ts.map +1 -0
  154. package/dist/core/metrics/metrics.js +119 -0
  155. package/dist/core/metrics/metrics.js.map +1 -0
  156. package/dist/core/utils/mask-sensitive.d.ts +44 -0
  157. package/dist/core/utils/mask-sensitive.d.ts.map +1 -0
  158. package/dist/core/utils/mask-sensitive.js +64 -0
  159. package/dist/core/utils/mask-sensitive.js.map +1 -0
  160. package/dist/core/utils/testing/McpHttpClient.d.ts +8 -33
  161. package/dist/core/utils/testing/McpHttpClient.d.ts.map +1 -1
  162. package/dist/core/utils/testing/McpHttpClient.js +8 -74
  163. package/dist/core/utils/testing/McpHttpClient.js.map +1 -1
  164. package/dist/core/utils/testing/McpStreamableHttpClient.d.ts +24 -30
  165. package/dist/core/utils/testing/McpStreamableHttpClient.d.ts.map +1 -1
  166. package/dist/core/utils/testing/McpStreamableHttpClient.js +36 -198
  167. package/dist/core/utils/testing/McpStreamableHttpClient.js.map +1 -1
  168. package/dist/core/utils/utils.d.ts.map +1 -1
  169. package/dist/core/utils/utils.js +2 -0
  170. package/dist/core/utils/utils.js.map +1 -1
  171. package/dist/core/web/admin-router.js +3 -3
  172. package/dist/core/web/admin-router.js.map +1 -1
  173. package/dist/core/web/cors.d.ts +9 -1
  174. package/dist/core/web/cors.d.ts.map +1 -1
  175. package/dist/core/web/cors.js +26 -5
  176. package/dist/core/web/cors.js.map +1 -1
  177. package/dist/core/web/event-store.d.ts +33 -0
  178. package/dist/core/web/event-store.d.ts.map +1 -0
  179. package/dist/core/web/event-store.js +65 -0
  180. package/dist/core/web/event-store.js.map +1 -0
  181. package/dist/core/web/oauth-router.d.ts +37 -0
  182. package/dist/core/web/oauth-router.d.ts.map +1 -0
  183. package/dist/core/web/oauth-router.js +207 -0
  184. package/dist/core/web/oauth-router.js.map +1 -0
  185. package/dist/core/web/request-id.d.ts +44 -0
  186. package/dist/core/web/request-id.d.ts.map +1 -0
  187. package/dist/core/web/request-id.js +82 -0
  188. package/dist/core/web/request-id.js.map +1 -0
  189. package/dist/core/web/server-http.d.ts.map +1 -1
  190. package/dist/core/web/server-http.js +322 -182
  191. package/dist/core/web/server-http.js.map +1 -1
  192. package/package.json +15 -2
  193. package/scripts/claude-2-agents-symlink.js +10 -1
  194. package/scripts/generate-jwt.js +129 -51
  195. package/src/template/custom-resources.ts +14 -0
  196. package/src/template/prompts/custom-prompts.ts +4 -0
  197. package/src/template/tools/handle-tool-call.ts +59 -3
  198. package/src/template/tools/tools.ts +92 -31
  199. package/src/tests/mcp/test-http.js +1 -1
  200. package/src/tests/mcp/test-sse.js +1 -1
@@ -28,20 +28,47 @@ export const AGENT_PROMPT = `You are a database management assistant.
28
28
  Add in `src/prompts/custom-prompts.ts`:
29
29
 
30
30
  ```typescript
31
- import { IPromptData, IGetPromptRequest } from 'fa-mcp-sdk';
31
+ import { IPromptData, IGetPromptRequest, IPromptArgument } from 'fa-mcp-sdk';
32
32
 
33
33
  export const customPrompts: IPromptData[] = [
34
34
  { name: 'greeting', description: 'Greeting message', arguments: [],
35
35
  content: 'Hello! How can I help?' },
36
36
 
37
- { name: 'context_prompt', description: 'Context-aware', arguments: [],
38
- content: (req: IGetPromptRequest) => `Context: ${JSON.stringify(req.params.arguments)}` },
37
+ // Standard §10.5 parameterised prompt. The `arguments[]` array is advertised in
38
+ // prompts/list; the values arrive as `request.params.arguments` (string map) on
39
+ // prompts/get. The content function receives them as the second argument.
40
+ {
41
+ name: 'context_prompt',
42
+ description: 'Context-aware prompt with explicit arguments',
43
+ arguments: [
44
+ { name: 'topic', description: 'Subject area to focus on', required: true },
45
+ { name: 'audience', description: 'Audience level (junior / senior)', required: false },
46
+ ] satisfies IPromptArgument[],
47
+ content: (_req, args) =>
48
+ `Focus on ${args?.topic ?? 'the codebase'} for a ${args?.audience ?? 'mixed'} audience.`,
49
+ },
39
50
 
40
51
  { name: 'admin_only', description: 'Admin instructions', arguments: [],
41
52
  content: 'Admin-only content', requireAuth: true },
53
+
54
+ // Standard §10.5 (MAY) — optional UI metadata. `title` is a human-facing label (falls back to
55
+ // `name`); `icons` is an `IIcon[]` (`{ src; mimeType?; sizes? }`, `src` = absolute URL or data: URI).
56
+ // Both only affect display in the client UI and pass through prompts/list unchanged.
57
+ {
58
+ name: 'release_notes',
59
+ title: 'Release notes',
60
+ icons: [{ src: 'https://cdn.example.com/notes.png', mimeType: 'image/png', sizes: '48x48' }],
61
+ description: 'Release change summary',
62
+ arguments: [],
63
+ content: 'Summary of changes for the current release.',
64
+ },
42
65
  ];
43
66
  ```
44
67
 
68
+ > **Compatibility.** The old single-argument signature
69
+ > `(req: IGetPromptRequest) => string` still works — the second `args` parameter is
70
+ > optional. Only update prompts that need access to the values.
71
+
45
72
  Pass to server:
46
73
  ```typescript
47
74
  const serverData: McpServerData = { ..., customPrompts };
@@ -114,12 +141,21 @@ export const customPrompts = async (ctx: ITransportContext): Promise<IPromptData
114
141
 
115
142
  ### Standard Resources
116
143
 
117
- | URI | Description |
118
- |-----|-------------|
119
- | `project://id` | Service identifier (`appConfig.name`) |
120
- | `project://name` | Display name (`appConfig.productName`) |
121
- | `doc://readme` | README.md content |
122
- | `use://http-headers` | Used HTTP headers (from `usedHttpHeaders`) |
144
+ | URI | MIME | Description |
145
+ |-----|------|-------------|
146
+ | `project://id` | `text/plain` | Service identifier (`appConfig.name`) |
147
+ | `project://name` | `text/plain` | Display name (`appConfig.productName`) |
148
+ | `project://version` | `text/plain` | Server version (`appConfig.version`) mirror of `GET /health.version` and `serverInfo.version` (standard §4 SHOULD) |
149
+ | `doc://readme` | `text/markdown` | README.md content |
150
+ | `use://http-headers` | `application/json` | Used HTTP headers (from `usedHttpHeaders`) |
151
+ | `use://auth` | `application/json` | Enabled auth schemes / methods / expected JWT claims (standard §11.2 SHOULD) |
152
+ | `<appConfig.name>://agent/brief` | `text/markdown` | Mirror of `agent_brief` prompt (Avatar profile §11.2) |
153
+ | `<appConfig.name>://agent/prompt` | `text/markdown` | Mirror of `agent_prompt` prompt (Avatar profile §11.2) |
154
+
155
+ > The `<appConfig.name>://agent/*` URIs are built automatically from `appConfig.name`
156
+ > (e.g. `mcp-jira://agent/brief`). If a project's `customResources` list contains a
157
+ > resource with the same URI, the project-supplied entry wins — handy when the service
158
+ > needs to publish a different brief through the resources endpoint than through prompts.
123
159
 
124
160
  ### Custom Resources
125
161
 
@@ -142,6 +178,15 @@ export const customResources: IResourceData[] = [
142
178
 
143
179
  { uri: 'custom://secrets', name: 'Secrets', description: 'Protected',
144
180
  mimeType: 'application/json', content: {}, requireAuth: true },
181
+
182
+ // Standard §11.3 (MAY) — optional UI metadata. `title` is a human-facing label; `icons` is an
183
+ // `IIcon[]` (same shape as prompts). `size` (bytes) is optional: on resources/list the SDK
184
+ // computes it from the content (UTF-8 byte length for text/objects, buffer length for blobs) when
185
+ // not set; lazy (function) content omits `size`. An author-supplied `size` is preserved.
186
+ { uri: 'custom://logo', name: 'logo', title: 'Brand logo', description: 'SVG logo',
187
+ mimeType: 'image/svg+xml', size: 1234,
188
+ icons: [{ src: 'https://cdn.example.com/logo.svg', mimeType: 'image/svg+xml' }],
189
+ content: '<svg …>' },
145
190
  ];
146
191
  ```
147
192
 
@@ -150,6 +195,35 @@ Pass to server:
150
195
  const serverData: McpServerData = { ..., customResources };
151
196
  ```
152
197
 
198
+ ### Binary Resources (`blob`)
199
+
200
+ A resource whose payload is not text (image, PDF, archive, …) declares `content` as
201
+ `IResourceBinaryContent` instead of a string. `resources/read` then returns the bytes as base64
202
+ `contents[0].blob` (with the resource's `mimeType`) and omits `text` — exactly one of `text` /
203
+ `blob` is present per standard §11.4 / §12.2.
204
+
205
+ ```typescript
206
+ import { readFileSync } from 'node:fs';
207
+ import { IResourceData } from 'fa-mcp-sdk';
208
+
209
+ export const customResources: IResourceData[] = [
210
+ // Raw bytes — the SDK base64-encodes the Buffer for you:
211
+ { uri: 'custom://logo.png', name: 'Logo', description: 'Brand logo',
212
+ mimeType: 'image/png', content: { blob: readFileSync('assets/logo.png') } },
213
+
214
+ // Already-base64 string — pass it through with base64: true:
215
+ { uri: 'custom://icon.png', name: 'Icon', description: 'App icon',
216
+ mimeType: 'image/png', content: { blob: PNG_BASE64, base64: true } },
217
+
218
+ // A function may return binary content too (sync or async):
219
+ { uri: 'custom://report.pdf', name: 'Report', description: 'Generated PDF',
220
+ mimeType: 'application/pdf', content: async () => ({ blob: await buildPdf() }) },
221
+ ];
222
+ ```
223
+
224
+ `{ blob: string }` is assumed to be base64 unless you set `base64: false` (then the SDK encodes the
225
+ string's raw bytes). Clients decode `contents[0].blob` from base64 to recover the original file.
226
+
153
227
  ### Dynamic Resources (Function)
154
228
 
155
229
  For dynamic resource lists based on transport type, headers, or user:
@@ -203,3 +277,92 @@ Both prompts and resources support `requireAuth: true`:
203
277
  - Requires valid authentication to access
204
278
  - Unauthenticated requests get error
205
279
  - Works with any configured auth method (JWT, Basic, etc.)
280
+
281
+ ## Optional MAY capabilities — templates & subscribe (standard §11.5)
282
+
283
+ Disabled by default. Opt-in via `config/default.yaml`:
284
+
285
+ ```yaml
286
+ mcp:
287
+ resources:
288
+ subscribeEnabled: false # MAY §11.5 — turn on only when resources change at runtime
289
+ templatesEnabled: false # MAY §11.5 — turn on when you publish customResourceTemplates
290
+ ```
291
+
292
+ ### `resources/templates/list`
293
+
294
+ When `templatesEnabled: true`, register `customResourceTemplates` on `McpServerData`:
295
+
296
+ ```typescript
297
+ import { IResourceTemplateInfo, McpServerData } from 'fa-mcp-sdk';
298
+
299
+ const customResourceTemplates: IResourceTemplateInfo[] = [
300
+ {
301
+ uriTemplate: 'issue://{key}', // RFC 6570
302
+ name: 'jira-issue',
303
+ title: 'Jira issue by key',
304
+ description: 'Single Jira issue addressable by ticket key.',
305
+ mimeType: 'application/json',
306
+ },
307
+ ];
308
+
309
+ const serverData: McpServerData = { ..., customResourceTemplates };
310
+ ```
311
+
312
+ If you do not register any templates the server still answers `resources/templates/list`
313
+ with an empty array — clients can probe the capability safely.
314
+
315
+ ### `resources/subscribe` + change notifications
316
+
317
+ When `subscribeEnabled: true`, the server advertises `subscribe` and `listChanged` in its
318
+ `resources` capability. To notify subscribers when content changes call
319
+ `notifyResourceUpdated(server, uri)`:
320
+
321
+ ```typescript
322
+ import { notifyResourceUpdated } from 'fa-mcp-sdk';
323
+
324
+ // Each HTTP session owns its own Server instance — track the server reference at the
325
+ // point where you have it (e.g. inside a custom-resources content function).
326
+ await notifyResourceUpdated(server, 'project://version');
327
+ ```
328
+
329
+ The helper emits `notifications/resources/updated` only to clients that previously called
330
+ `resources/subscribe` for the given URI on that `Server`.
331
+
332
+ ## Optional MAY capability — argument completion (standard §8.2)
333
+
334
+ `completion/complete` lets a client ask the server to suggest values for a prompt or resource
335
+ argument (for example, the valid project ids for a `project` argument). Disabled by default; the
336
+ capability is advertised only when **both** the config flag is on **and** a `completionProvider`
337
+ is supplied on `McpServerData` — otherwise `completion/complete` returns `-32601`.
338
+
339
+ ```yaml
340
+ mcp:
341
+ completions:
342
+ enabled: true # MAY §8.2 — also requires a completionProvider (see below)
343
+ ```
344
+
345
+ ```typescript
346
+ import { McpServerData } from 'fa-mcp-sdk';
347
+
348
+ const completionProvider: McpServerData['completionProvider'] = async ({ ref, argument }) => {
349
+ // ref: { type: 'ref/prompt' | 'ref/resource'; name?; uri? }
350
+ // argument: { name; value } — value is what the user has typed so far
351
+ if (ref.type === 'ref/prompt' && argument.name === 'project') {
352
+ const all = await listProjectIds();
353
+ return all.filter((id) => id.startsWith(argument.value));
354
+ }
355
+ return [];
356
+ };
357
+
358
+ const serverData: McpServerData = { ..., completionProvider };
359
+ ```
360
+
361
+ The SDK caps the response at 100 values and sets `completion.hasMore` / `completion.total`
362
+ accordingly.
363
+
364
+ ## Pagination (standard §8.4)
365
+
366
+ `prompts/list` and `resources/list` use the same cursor-based pagination as `tools/list`:
367
+ opaque base64(offset), stable sort by `name` / `uri`. The page size comes from
368
+ `mcp.pagination.pageSize` (default 100). See [03-configuration → "Pagination"](./03-configuration.md#pagination).
@@ -113,9 +113,46 @@ mcp:
113
113
  rateLimit:
114
114
  maxRequests: 100
115
115
  windowMs: 60000
116
+ # Standard §14 — 'subject' counts per JWT `sub`/`user` with IP fallback; 'ip' = legacy.
117
+ scope: subject
118
+ # Max in-flight tools/call per subject. Excess → -32003 / HTTP 429 + Retry-After.
119
+ maxConcurrentPerSubject: 16
120
+ # Hard ceilings enforced by the HTTP transport (standard §14). Concrete servers MAY raise
121
+ # or lower these per-environment without patching the SDK.
122
+ limits:
123
+ # Max accepted JSON / urlencoded request body, bytes. Above the limit:
124
+ # JSON-RPC code -32005, HTTP 413 Payload Too Large.
125
+ maxPayloadBytes: 1048576 # 1 MiB
126
+ # Max serialized tool result, bytes. Above the limit, the SDK truncates the payload
127
+ # and marks `structuredContent.truncated: true` + appends "…[truncated]" to text content.
128
+ maxToolResultBytes: 10485760 # 10 MiB
129
+ # Per-tool execution timeout, milliseconds. Above the limit:
130
+ # JSON-RPC code -32004, HTTP 504 Gateway Timeout.
131
+ toolTimeoutMs: 30000 # 30 seconds
116
132
  tools:
117
133
  answerAs: text # text | structuredContent
118
134
  hideAnnotations: false # true — strip `annotations` from tool listings
135
+ # Standard §8.4 — server-side pagination for tools/list, prompts/list, resources/list.
136
+ pagination:
137
+ pageSize: 100 # items per page (cursor is opaque base64(offset))
138
+ # Standard §11.5 — optional MAY resource capabilities. Off by default.
139
+ resources:
140
+ subscribeEnabled: false # advertise `subscribe` + `listChanged`; emit notifications/resources/updated
141
+ templatesEnabled: false # advertise + serve resources/templates/list
142
+ # Standard §8.7 (MAY) — task-augmented execution (long-running / pollable tool calls). Off by
143
+ # default. When enabled, advertises the `tasks` capability and serves tasks/list|get|result|cancel.
144
+ # Long-running tools opt in per-tool via `execution.taskSupport`. Default store is in-memory only.
145
+ tasks:
146
+ enabled: false # advertise `tasks` capability and accept the lifecycle methods
147
+ defaultTtlMs: 3600000 # finished-task retention from creation (clamped to [minTtlMs, maxTtlMs])
148
+ minTtlMs: 0 # lower bound a client-requested ttl is clamped to
149
+ maxTtlMs: 86400000 # hard retention ceiling (24 h)
150
+ pollIntervalMs: 1000 # suggested client poll interval, surfaced in every task object
151
+ maxTasks: 1000 # retained tasks cap; oldest finished evicted first
152
+ # Standard §6 (MAY) — Streamable HTTP SSE resumability via Last-Event-ID. Off by default.
153
+ sse:
154
+ resumability: false # wire in-memory EventStore into the transport for replay on reconnect
155
+ maxStoredEvents: 1000 # ring-buffer size: recent events retained per process for replay
119
156
 
120
157
  swagger:
121
158
  servers:
@@ -135,10 +172,21 @@ uiColor:
135
172
  primary: '#0f65dc'
136
173
 
137
174
  webServer:
138
- host: '0.0.0.0'
175
+ # Bind address. Default: '127.0.0.1' — loopback only (safer default, standard §6).
176
+ # Set to '0.0.0.0' explicitly when running inside a container / behind a reverse proxy.
177
+ host: '127.0.0.1'
139
178
  port: {{port}}
140
- # array of hosts that CORS skips
141
- originHosts: ['localhost', '0.0.0.0']
179
+ # Array of hosts whose `Origin` header bypasses the CORS guard.
180
+ # CORS now actively rejects unlisted origins with HTTP 403 + JSON-RPC error.
181
+ # In production an empty list aborts startup.
182
+ originHosts: ['localhost']
183
+ # Express `trust proxy`. Set true | 'loopback' | <number> when behind an HTTPS reverse
184
+ # proxy so /.well-known/openid-configuration derives `issuer` from X-Forwarded-* headers.
185
+ trustProxy: false
186
+ # Standard §7.1 — secrets in URL forbidden. POST /ct with JSON body is the only safe form;
187
+ # GET /ct?t=<token> is disabled by default. Opt-in via allowQueryToken=true (non-prod only).
188
+ tokenCheck:
189
+ allowQueryToken: false
142
190
  # Authentication is configured here only when accessing the MCP server
143
191
  # Authentication in services that enable tools, resources, and prompts
144
192
  # is implemented more deeply. To do this, you need to use the information passed in HTTP headers
@@ -156,17 +204,15 @@ webServer:
156
204
  permanentServerTokens: [ ] # Add your server tokens here: ['token1', 'token2']
157
205
 
158
206
  # ========================================================================
159
- # JWT TOKEN — standard signed JWT (HS256)
160
- # Tokens issued by this SDK are standard 3-segment JWTs `header.payload.signature`.
161
- # The verifier also temporarily accepts pre-migration legacy tokens
162
- # (`<expire_ms>.<hex>` AES-256-CTR format) for backward compatibility.
163
- # CPU cost: Medium - signature verification + JSON parsing
164
- #
165
- # To enable this authentication, you need to set auth.enabled = true and set
166
- # encryptKey to at least 8 characters (used as the HS256 signing secret).
207
+ # JWT TOKEN — four operating modes (since SDK 0.7.0)
208
+ # - legacyAesCtr (default) HS256 + AES-CTR fallback. 0.6.x parity.
209
+ # - embedded — ES256/RS256 with built-in IdP. Dev / demo.
210
+ # - localKey — ES256/RS256 verify with public key on disk.
211
+ # - remoteJwks — verify against external IdP's JWKS endpoint.
167
212
  # ========================================================================
168
213
  jwtToken:
169
- # HS256 signing secret used to sign/verify tokens for this MCP (minimum 8 chars)
214
+ mode: legacyAesCtr # see above
215
+ # HS256 signing secret used ONLY by legacyAesCtr mode (minimum 8 chars)
170
216
  encryptKey: '***'
171
217
  # If webServer.auth.enabled and the parameter true, the service name and the service specified in the token will be checked
172
218
  checkMCPName: true
@@ -174,8 +220,22 @@ webServer:
174
220
  # the client IP will be checked against the allowed list in the token
175
221
  isCheckIP: false
176
222
  # Optional JWT `iss` claim. When non-empty, the generator stamps it and the verifier requires it.
223
+ # legacyAesCtr only — in non-legacy modes use expectedIssuer below.
177
224
  issuer: ''
178
225
 
226
+ # -------- Modes embedded / localKey / remoteJwks --------
227
+ algorithm: ES256 # ES256 | RS256
228
+ keyStoragePath: './keys' # embedded: autogenerated keypair (private.pem + public.pem)
229
+ publicKeyPath: '' # localKey: PEM public key path
230
+ privateKeyPath: '' # localKey: optional — enables local issuance
231
+ jwksUri: '' # remoteJwks: external JWKS endpoint
232
+ expectedIssuer: '' # required for embedded/localKey/remoteJwks (standard §7.2)
233
+ expectedAudience: '' # defaults to appConfig.name
234
+ jwksCacheTtl: 600 # JWKS cache, seconds
235
+ jwksCooldown: 30 # min interval between repeat fetches when kid missing
236
+ clockSkew: 30 # allowed exp/nbf drift, seconds (max enforced: 60)
237
+ defaultTtl: 1800 # default TTL for /oauth/token-issued tokens
238
+
179
239
  # ========================================================================
180
240
  # Basic Authentication - Base64 encoded username:password
181
241
  # CPU cost: Medium - Base64 decoding + string comparison
@@ -387,3 +447,101 @@ db:
387
447
  password: <password>
388
448
  usedExtensions: [] # e.g. [pgvector]
389
449
  ```
450
+
451
+
452
+ ## Pagination (`mcp.pagination`)
453
+
454
+ Standard §8.4 — server-side pagination for `tools/list`, `prompts/list`, and
455
+ `resources/list`. The SDK sorts items stably by `name` / `uri`, slices the list, and
456
+ returns `nextCursor` (opaque base64 of the next offset) when more entries follow.
457
+
458
+ | Key | Default | Notes |
459
+ |-----|---------|-------|
460
+ | `mcp.pagination.pageSize` | `100` | Items per page; lower this for low-context clients (e.g. terminal MCP clients) or raise it for power users. |
461
+
462
+ Override per environment via `MCP_PAGINATION_PAGE_SIZE`. Invalid cursors return JSON-RPC
463
+ `-32602` with `error.data.field: 'cursor'`.
464
+
465
+ ```bash
466
+ # clients without pagination support still see the first page — fully spec-compliant
467
+ curl -s ... -d '{"jsonrpc":"2.0","method":"tools/list","id":1}'
468
+
469
+ # clients that opt in
470
+ curl -s ... -d '{"jsonrpc":"2.0","method":"tools/list","id":1,"params":{"cursor":"NTA="}}'
471
+ ```
472
+
473
+ ## Resource MAY capabilities (`mcp.resources`)
474
+
475
+ Standard §11.5 — opt-in templates and subscriptions. Defaults keep the server in the
476
+ "static resources only" mode that low-context clients expect.
477
+
478
+ | Key | Default | Notes |
479
+ |-----|---------|-------|
480
+ | `mcp.resources.subscribeEnabled` | `false` | Advertise `subscribe` + `listChanged` and register `resources/subscribe`. Project code emits change events via `notifyResourceUpdated(server, uri)`. |
481
+ | `mcp.resources.templatesEnabled` | `false` | Advertise + serve `resources/templates/list`. Templates come from `McpServerData.customResourceTemplates`. |
482
+
483
+ See [02-2-prompts-and-resources → "Optional MAY capabilities"](./02-2-prompts-and-resources.md#optional-may-capabilities-templates--subscribe-standard-115)
484
+ for end-to-end examples.
485
+
486
+ ## SSE stream resumability (`mcp.sse`)
487
+
488
+ Standard §6 (MAY) — opt-in replay of missed Streamable HTTP SSE events after a reconnect. Off by
489
+ default; when off the transport behaves exactly as before. Only relevant for the HTTP transport.
490
+
491
+ | Key | Default | Notes |
492
+ |-----|---------|-------|
493
+ | `mcp.sse.resumability` | `false` | When `true`, an in-memory `InMemoryEventStore` is wired into the Streamable HTTP transport. A client reconnecting to `GET /mcp` with a `Last-Event-ID` header replays the events it missed. Env `MCP_SSE_RESUMABILITY`. |
494
+ | `mcp.sse.maxStoredEvents` | `1000` | Ring-buffer size — how many recent events are retained per process for replay. Env `MCP_SSE_MAX_STORED_EVENTS`. |
495
+
496
+ The store is a per-process ring buffer: it does not survive a restart and is not shared across
497
+ instances. For multi-replica deployments either pin reconnects to the same instance (sticky sessions
498
+ by `Mcp-Session-Id`) or implement a shared `EventStore`. Events evicted past `maxStoredEvents` are not
499
+ replayed — the client simply resumes from the current moment, without error.
500
+
501
+ ## HTTP Transport Hardening (`mcp.limits`)
502
+
503
+ Standard §14 mandates explicit ceilings on request body, tool result and tool execution time. The
504
+ SDK enforces all three from `mcp.limits` — see the snippet under "config/default.yaml" above.
505
+
506
+ | Key | Default | What happens above the limit |
507
+ |-----|---------|------------------------------|
508
+ | `mcp.limits.maxPayloadBytes` | 1 MiB | JSON-RPC `-32005` + HTTP **413 Payload Too Large**. The Express `entity.too.large` error is translated automatically — clients never see the default HTML error page. |
509
+ | `mcp.limits.maxToolResultBytes` | 10 MiB | Response is truncated. `structuredContent.truncated: true` is set on structured payloads; `…[truncated]` marker is appended to oversized text content. Standard §12.2. |
510
+ | `mcp.limits.toolTimeoutMs` | 30 000 ms | JSON-RPC `-32004` + HTTP **504 Gateway Timeout** on `/mcp`. The pending tool promise is left running (Node can't synchronously abort user code); your tool SHOULD also self-cancel if it watches the elapsed time. |
511
+
512
+ Override per-environment in `config/{development,production,local}.yaml` or via env vars
513
+ (`MCP_LIMITS_MAX_PAYLOAD_BYTES`, `MCP_LIMITS_MAX_TOOL_RESULT_BYTES`, `MCP_LIMITS_TOOL_TIMEOUT_MS`).
514
+
515
+ ## Health, Readiness, CORS
516
+
517
+ | Endpoint / Setting | Behaviour | Standard |
518
+ |--------------------|-----------|----------|
519
+ | `GET /health` | Returns `{ status, version, uptime, details }`. HTTP **503** when `status === 'unhealthy'`, **200** otherwise. | §16.1 |
520
+ | `GET /ready` | No auth. Returns `{ status, checks: { db, cache, jwks } }`. Each check is `'ok' \| 'error' \| 'skipped'` — never leaks credentials or connection strings. HTTP **503** when any check fails, **200** when all green. | §16.2 / §16.3 |
521
+ | `webServer.host` | Default `'127.0.0.1'` (loopback). Containers / k8s pods / public-facing deployments MUST set `'0.0.0.0'` explicitly. | §6 |
522
+ | `webServer.originHosts` | Empty list in production aborts `initMcpServer()`. Unlisted `Origin` headers receive HTTP **403** + JSON-RPC error (no longer silently allowed). | §6 |
523
+
524
+ ## MCP-Specific JSON-RPC Error Codes (Appendix B)
525
+
526
+ | Code | Class | HTTP | When |
527
+ |------|-------|------|------|
528
+ | `-32002` | `ResourceNotFoundError` | 404 | Session / resource not found (legacy SSE `/messages`, missing JWKS key, etc.) |
529
+ | `-32003` | `RateLimitedError` | 429 | Per-client rate limit exceeded. Response carries the `Retry-After` HTTP header AND `error.data.retryAfter` (seconds). |
530
+ | `-32004` | `TimeoutError` | 504 | Tool execution exceeded `mcp.limits.toolTimeoutMs`. |
531
+ | `-32005` | `PayloadTooLargeError` | 413 | Request body exceeded `mcp.limits.maxPayloadBytes`. |
532
+
533
+ Import the classes (and the `MCP_ERROR_CODES` map) from the SDK root:
534
+
535
+ ```typescript
536
+ import {
537
+ PayloadTooLargeError, TimeoutError, RateLimitedError, ResourceNotFoundError,
538
+ MCP_ERROR_CODES,
539
+ createJsonRpcErrorResponse, // accepts (err, requestId?, extraData?)
540
+ IMcpErrorData, // { requestId?, field?, reason?, retryAfter?, [k]: unknown }
541
+ } from 'fa-mcp-sdk';
542
+ ```
543
+
544
+ All four extend `BaseMcpError`. The `createJsonRpcErrorResponse` helper emits the canonical
545
+ `error.data` shape from Appendix B.3 — `{ requestId?, field?, reason?, retryAfter?, … }`.
546
+ Stack traces and internal paths are NEVER included in `error.data` (standard §13.3).
547
+
@@ -12,16 +12,83 @@ interface AuthResult {
12
12
  username?: string;
13
13
  isTokenDecrypted?: boolean;
14
14
  payload?: any;
15
+ /**
16
+ * Standard §7.4 — authenticated but not authorized. Triggers HTTP 403
17
+ * (NO WWW-Authenticate challenge). Set by custom validators or scope checks.
18
+ */
19
+ forbidden?: boolean;
15
20
  }
16
21
  ```
17
22
 
23
+ ## JWT Modes (since SDK 0.7.0)
24
+
25
+ `webServer.auth.jwtToken.mode` selects the JWT engine:
26
+
27
+ | Mode | Algorithm | Issues tokens | Verifies tokens | Discovery | Use case |
28
+ |------|-----------|---------------|-----------------|-----------|----------|
29
+ | `legacyAesCtr` (default) | HS256 + legacy AES-CTR | yes (HS256) | yes | — | Backward-compatible / 0.6.x parity |
30
+ | `embedded` | ES256 / RS256 | yes (autogen keys) | yes | full OIDC + JWKS + `/oauth/token` | Dev / demo |
31
+ | `localKey` | ES256 / RS256 | when `privateKeyPath` set | yes | OIDC + JWKS | Isolated server with PEM keys |
32
+ | `remoteJwks` | ES256 / RS256 | NO (`501`) | yes (remote JWKS) | protected-resource only | Corporate IdP (Keycloak, Okta, Azure AD, …) |
33
+
34
+ Pre-flight checks (`init-mcp-server.ts`) reject misconfigured non-legacy modes at start:
35
+
36
+ - `remoteJwks` without `jwksUri` → throws
37
+ - `localKey` without `publicKeyPath` → throws
38
+ - non-legacy without `expectedIssuer` → throws (standard §7.2)
39
+ - `clockSkew > 60s` → throws (standard Прил. A.1)
40
+ - `production` + `legacyAesCtr` + `auth.enabled=true` → warn (asymmetric required by standard)
41
+
42
+ ```yaml
43
+ # config/local.yaml — corporate IdP
44
+ webServer:
45
+ trustProxy: true # behind HTTPS reverse proxy
46
+ auth:
47
+ enabled: true
48
+ jwtToken:
49
+ mode: remoteJwks
50
+ jwksUri: 'https://idp.corp/.well-known/jwks.json'
51
+ expectedIssuer: 'https://idp.corp'
52
+ expectedAudience: '${SERVICE_NAME}'
53
+ jwksCacheTtl: 600
54
+ jwksCooldown: 30
55
+ clockSkew: 30
56
+ ```
57
+
58
+ Discovery endpoints mounted automatically when `mode != 'legacyAesCtr'`:
59
+
60
+ - `GET /.well-known/oauth-protected-resource` (any non-legacy)
61
+ - `GET /.well-known/openid-configuration` (`embedded` / `localKey`)
62
+ - `GET /.well-known/jwks.json` (`embedded` / `localKey`)
63
+ - `POST /oauth/token` (`embedded` + `localKey` with private key, `grant_type=password`)
64
+
65
+ On every 401 the server sets:
66
+
67
+ ```
68
+ WWW-Authenticate: Bearer realm="<appConfig.name>",
69
+ resource_metadata="<base>/.well-known/oauth-protected-resource"
70
+ ```
71
+
72
+ If the token was decoded but rejected (expired, bad scope), the header additionally carries
73
+ `error="invalid_token", error_description="…"` per RFC 6750.
74
+
18
75
  ## Token Operations
19
76
 
20
77
  ```typescript
21
- import { generateToken } from 'fa-mcp-sdk';
78
+ import { generateToken, checkJwtToken } from 'fa-mcp-sdk';
79
+
80
+ // Since 0.7.0 — generateToken / checkJwtToken are async (dispatch by jwtToken.mode).
81
+ const token = await generateToken('john_doe', 3600, { role: 'admin' }); // 1 hour
82
+ const r = await checkJwtToken({ token });
83
+ ```
84
+
85
+ Synchronous fallbacks (legacy mode only — never throw on `mode = legacyAesCtr`):
86
+
87
+ ```typescript
88
+ import { generateTokenLegacy, checkJwtTokenLegacy } from 'fa-mcp-sdk';
22
89
 
23
- // Generate JWT (liveTimeSec = seconds until expiry)
24
- const token = generateToken('john_doe', 3600, { role: 'admin' }); // 1 hour
90
+ const token = generateTokenLegacy('john_doe', 3600, { role: 'admin' });
91
+ const r = checkJwtTokenLegacy({ token });
25
92
  ```
26
93
 
27
94
  ## Test Authentication
@@ -29,8 +96,9 @@ const token = generateToken('john_doe', 3600, { role: 'admin' }); // 1 hour
29
96
  ```typescript
30
97
  import { getAuthHeadersForTests, McpHttpClient, appConfig } from 'fa-mcp-sdk';
31
98
 
32
- // Auto-generates auth headers based on config (permanent basic → JWT priority)
33
- const headers = getAuthHeadersForTests();
99
+ // Since 0.7.0 getAuthHeadersForTests is async. Uses canLocallyIssueJwt() so JWT-based
100
+ // headers work in every mode that can sign locally (legacy / embedded / localKey).
101
+ const headers = await getAuthHeadersForTests();
34
102
 
35
103
  // Usage
36
104
  const response = await fetch(`http://localhost:${appConfig.webServer.port}/mcp`, {
@@ -41,7 +109,62 @@ const response = await fetch(`http://localhost:${appConfig.webServer.port}/mcp`,
41
109
 
42
110
  // With test client
43
111
  const client = new McpHttpClient('http://localhost:3000');
44
- const result = await client.callTool('tool', args, getAuthHeadersForTests());
112
+ const result = await client.callTool('tool', args, await getAuthHeadersForTests());
113
+ ```
114
+
115
+ ## Scope Enforcement (since SDK 0.7.0)
116
+
117
+ Standard §7.5 — protect specific tools / prompts / resources with `requiredScopes`. The auth
118
+ middleware checks the token's `scope` claim (space-separated) against the declared list and
119
+ returns HTTP 403 (or JSON-RPC `-32004` for tools) when scopes are missing.
120
+
121
+ ```typescript
122
+ // Resource with required scope
123
+ {
124
+ uri: 'admin://users',
125
+ name: 'users',
126
+ description: 'Admin user list',
127
+ mimeType: 'application/json',
128
+ requireAuth: true,
129
+ requiredScopes: ['mcp:admin'],
130
+ content: async () => ({ ... }),
131
+ }
132
+
133
+ // Prompt with required scope
134
+ {
135
+ name: 'admin_brief',
136
+ description: 'Privileged brief',
137
+ arguments: [],
138
+ content: '…',
139
+ requiredScopes: ['mcp:admin'],
140
+ }
141
+
142
+ // Tool with required scope (via _meta — SDK Tool type has no native scope field)
143
+ {
144
+ name: 'delete_user',
145
+ description: 'Delete a user account',
146
+ inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
147
+ _meta: { requiredScopes: ['mcp:admin'] },
148
+ }
149
+ ```
150
+
151
+ A token issued via `POST /oauth/token` with `scope=mcp:admin` (or any IdP token carrying that
152
+ scope) passes; everything else hits 403. The full server-side scope map is published via the
153
+ built-in `use://auth` resource — clients can introspect it programmatically.
154
+
155
+ ## Forbidden vs Unauthorized
156
+
157
+ Custom validators can now signal 403 explicitly via `AuthResult.forbidden`:
158
+
159
+ ```typescript
160
+ const validator: CustomAuthValidator = async (req) => {
161
+ const token = req.headers['x-api-key'];
162
+ if (!token) return { success: false, error: 'No API key' }; // → 401
163
+ if (await tokenIsRevoked(token)) {
164
+ return { success: false, forbidden: true, error: 'Token revoked' }; // → 403, no challenge
165
+ }
166
+ return { success: true, authType: 'custom' };
167
+ };
45
168
  ```
46
169
 
47
170
  ## Admin Panel Authentication
@@ -296,12 +419,31 @@ curl -H "Authorization: Basic $(echo -n 'admin:password' | base64)" http://local
296
419
  curl -H "X-API-Key: custom-key" http://localhost:3000/mcp
297
420
  ```
298
421
 
422
+ ## Token Check Endpoint (`/ct`)
423
+
424
+ Standard §7.1 forbids secrets in URL query strings. Since SDK 0.7.0 `GET /ct?t=<token>` is
425
+ disabled by default and returns HTTP 405. Use `POST /ct` with JSON body instead:
426
+
427
+ ```bash
428
+ curl -X POST http://localhost:3000/ct \
429
+ -H "Content-Type: application/json" \
430
+ -d '{"t": "<your-token>"}'
431
+ ```
432
+
433
+ Opt-in for the legacy form (non-production only — flag is ignored when `NODE_ENV=production`):
434
+
435
+ ```yaml
436
+ webServer:
437
+ tokenCheck:
438
+ allowQueryToken: true
439
+ ```
440
+
299
441
  ## CLI Token Generator
300
442
 
301
443
  Generate JWT tokens from the command line without starting the server:
302
444
 
303
445
  ```bash
304
- node scripts/generate-jwt.js -u <username> -ttl <duration> [-s <service>] [-p <params>]
446
+ node scripts/generate-jwt.js -u <username> -ttl <duration> [-s <service>] [-p <params>] [--key <path>]
305
447
  ```
306
448
 
307
449
  | Option | ENV | Description |
@@ -310,8 +452,16 @@ node scripts/generate-jwt.js -u <username> -ttl <duration> [-s <service>] [-p <p
310
452
  | `-ttl` | `JWT_TTL` | Token lifetime: `<N>s` \| `<N>m` \| `<N>d` \| `<N>y` (required) |
311
453
  | `-s`, `--service-name` | `JWT_PAYLOAD_SERVICE_NAME` | Service name (optional) |
312
454
  | `-p`, `--params` | `JWT_PAYLOAD_PARAMS` | Extra payload `key=value;key=value` (optional) |
455
+ | `--key`, `--private-key` | — | Override private key path (only for embedded / localKey modes) |
456
+
457
+ Behaviour by `webServer.auth.jwtToken.mode`:
313
458
 
314
- The HS256 signing secret is read from config `webServer.auth.jwtToken.encryptKey` (via `config/local.yaml` or ENV `WS_TOKEN_ENCRYPT_KEY`). Generated tokens are standard 3-segment JWTs.
459
+ | Mode | What the script does |
460
+ |------|----------------------|
461
+ | `legacyAesCtr` | HS256 with `encryptKey` (legacy). |
462
+ | `embedded` | ES256/RS256 with keys from `keyStoragePath/private.pem`. |
463
+ | `localKey` | ES256/RS256 with `privateKeyPath` (must be configured or passed via `--key`). |
464
+ | `remoteJwks` | Exits with error — tokens must be obtained from the external IdP. |
315
465
 
316
466
  **Examples:**
317
467