@lenne.tech/nest-server 12.2.0 → 12.3.1

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 (160) hide show
  1. package/.claude/rules/better-auth.md +31 -4
  2. package/.claude/rules/configurable-features.md +8 -7
  3. package/.claude/rules/package-management.md +18 -2
  4. package/.claude/rules/persistence.md +2 -2
  5. package/.claude/rules/testing.md +5 -5
  6. package/.claude/rules/versioning.md +13 -2
  7. package/CLAUDE.md +5 -0
  8. package/FRAMEWORK-API.md +4 -3
  9. package/README.md +2 -2
  10. package/dist/config.env.js +2 -2
  11. package/dist/config.env.js.map +1 -1
  12. package/dist/core/common/helpers/graphql-ws-connection.helper.d.ts +4 -0
  13. package/dist/core/common/helpers/graphql-ws-connection.helper.js +17 -3
  14. package/dist/core/common/helpers/graphql-ws-connection.helper.js.map +1 -1
  15. package/dist/core/common/interceptors/check-security.interceptor.js +19 -2
  16. package/dist/core/common/interceptors/check-security.interceptor.js.map +1 -1
  17. package/dist/core/common/interfaces/server-options.interface.d.ts +8 -0
  18. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +3 -0
  19. package/dist/core/modules/ai/core-ai-mcp.controller.js +29 -0
  20. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  21. package/dist/core/modules/ai/core-ai.constants.d.ts +3 -0
  22. package/dist/core/modules/ai/core-ai.constants.js +3 -0
  23. package/dist/core/modules/ai/core-ai.constants.js.map +1 -1
  24. package/dist/core/modules/ai/core-ai.controller.d.ts +9 -0
  25. package/dist/core/modules/ai/core-ai.controller.js +48 -1
  26. package/dist/core/modules/ai/core-ai.controller.js.map +1 -1
  27. package/dist/core/modules/ai/core-ai.module.d.ts +2 -0
  28. package/dist/core/modules/ai/core-ai.module.js +9 -1
  29. package/dist/core/modules/ai/core-ai.module.js.map +1 -1
  30. package/dist/core/modules/ai/core-ai.resolver.d.ts +6 -0
  31. package/dist/core/modules/ai/core-ai.resolver.js +32 -0
  32. package/dist/core/modules/ai/core-ai.resolver.js.map +1 -1
  33. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.d.ts +65 -1
  34. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js +6 -3
  35. package/dist/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.js.map +1 -1
  36. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js +48 -1
  37. package/dist/core/modules/ai/helpers/ai-mcp-oauth.helper.js.map +1 -1
  38. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.d.ts +9 -0
  39. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js +54 -0
  40. package/dist/core/modules/ai/helpers/ai-tool-authorization.helper.js.map +1 -0
  41. package/dist/core/modules/ai/index.d.ts +8 -0
  42. package/dist/core/modules/ai/index.js +7 -0
  43. package/dist/core/modules/ai/index.js.map +1 -1
  44. package/dist/core/modules/ai/inputs/core-ai-settings.input.d.ts +3 -0
  45. package/dist/core/modules/ai/inputs/core-ai-settings.input.js +31 -0
  46. package/dist/core/modules/ai/inputs/core-ai-settings.input.js.map +1 -0
  47. package/dist/core/modules/ai/interfaces/ai-tool.interface.d.ts +2 -0
  48. package/dist/core/modules/ai/models/core-ai-settings.model.d.ts +4 -0
  49. package/dist/core/modules/ai/models/core-ai-settings.model.js +30 -0
  50. package/dist/core/modules/ai/models/core-ai-settings.model.js.map +1 -0
  51. package/dist/core/modules/ai/models/core-ai-status.model.d.ts +3 -0
  52. package/dist/core/modules/ai/models/core-ai-status.model.js +25 -0
  53. package/dist/core/modules/ai/models/core-ai-status.model.js.map +1 -0
  54. package/dist/core/modules/ai/models/core-ai-user-setting.model.d.ts +8 -0
  55. package/dist/core/modules/ai/models/core-ai-user-setting.model.js +45 -0
  56. package/dist/core/modules/ai/models/core-ai-user-setting.model.js.map +1 -0
  57. package/dist/core/modules/ai/services/core-ai-conversation.service.d.ts +4 -1
  58. package/dist/core/modules/ai/services/core-ai-conversation.service.js +16 -4
  59. package/dist/core/modules/ai/services/core-ai-conversation.service.js.map +1 -1
  60. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js +6 -6
  61. package/dist/core/modules/ai/services/core-ai-mcp-oauth.service.js.map +1 -1
  62. package/dist/core/modules/ai/services/core-ai-mcp.service.d.ts +27 -2
  63. package/dist/core/modules/ai/services/core-ai-mcp.service.js +118 -11
  64. package/dist/core/modules/ai/services/core-ai-mcp.service.js.map +1 -1
  65. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js +9 -6
  66. package/dist/core/modules/ai/services/core-ai-prompt-builder.service.js.map +1 -1
  67. package/dist/core/modules/ai/services/core-ai-user-setting.service.d.ts +17 -0
  68. package/dist/core/modules/ai/services/core-ai-user-setting.service.js +72 -0
  69. package/dist/core/modules/ai/services/core-ai-user-setting.service.js.map +1 -0
  70. package/dist/core/modules/ai/services/core-ai.service.d.ts +8 -3
  71. package/dist/core/modules/ai/services/core-ai.service.js +68 -29
  72. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  73. package/dist/core/modules/better-auth/better-auth.config.js +10 -4
  74. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -1
  75. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +2 -2
  76. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  77. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.d.ts +8 -0
  78. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js +64 -0
  79. package/dist/core/modules/better-auth/core-better-auth-path-guard.middleware.js.map +1 -0
  80. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -1
  81. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  82. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +1 -1
  83. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +5 -3
  84. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  85. package/dist/core/modules/better-auth/core-better-auth.module.js +2 -0
  86. package/dist/core/modules/better-auth/core-better-auth.module.js.map +1 -1
  87. package/dist/core/modules/better-auth/index.d.ts +1 -0
  88. package/dist/core/modules/better-auth/index.js +1 -0
  89. package/dist/core/modules/better-auth/index.js.map +1 -1
  90. package/dist/core/modules/error-code/error-codes.d.ts +18 -0
  91. package/dist/core/modules/error-code/error-codes.js +16 -0
  92. package/dist/core/modules/error-code/error-codes.js.map +1 -1
  93. package/dist/core/modules/sync/core-sync.service.d.ts +1 -0
  94. package/dist/core/modules/sync/core-sync.service.js +31 -2
  95. package/dist/core/modules/sync/core-sync.service.js.map +1 -1
  96. package/dist/core/modules/tenant/core-tenant.guard.js +10 -0
  97. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  98. package/dist/core/modules/tenant/core-tenant.service.d.ts +21 -1
  99. package/dist/core/modules/tenant/core-tenant.service.js +153 -1
  100. package/dist/core/modules/tenant/core-tenant.service.js.map +1 -1
  101. package/dist/server/modules/error-code/error-codes.d.ts +2 -0
  102. package/dist/test/test.helper.d.ts +1 -0
  103. package/dist/test/test.helper.js +9 -2
  104. package/dist/test/test.helper.js.map +1 -1
  105. package/docs/REQUEST-LIFECYCLE.md +5 -4
  106. package/docs/error-codes.md +14 -0
  107. package/docs/offline-sync-protocol.md +17 -8
  108. package/migration-guides/12.0.x-to-12.1.x.md +0 -1
  109. package/migration-guides/12.2.x-to-12.3.x.md +440 -0
  110. package/migration-guides/12.3.0-to-12.3.1.md +136 -0
  111. package/package.json +26 -26
  112. package/src/config.env.ts +6 -2
  113. package/src/core/common/helpers/graphql-ws-connection.helper.ts +32 -4
  114. package/src/core/common/interceptors/check-security.interceptor.ts +26 -2
  115. package/src/core/common/interfaces/server-options.interface.ts +84 -8
  116. package/src/core/common/persistence/sync-model.ts +1 -1
  117. package/src/core/modules/ai/INTEGRATION-CHECKLIST.md +36 -15
  118. package/src/core/modules/ai/README.md +178 -25
  119. package/src/core/modules/ai/core-ai-mcp.controller.ts +48 -1
  120. package/src/core/modules/ai/core-ai.constants.ts +10 -0
  121. package/src/core/modules/ai/core-ai.controller.ts +55 -1
  122. package/src/core/modules/ai/core-ai.module.ts +14 -0
  123. package/src/core/modules/ai/core-ai.resolver.ts +36 -0
  124. package/src/core/modules/ai/helpers/ai-mcp-oauth-consent.helper.ts +35 -4
  125. package/src/core/modules/ai/helpers/ai-mcp-oauth.helper.ts +68 -1
  126. package/src/core/modules/ai/helpers/ai-tool-authorization.helper.ts +99 -0
  127. package/src/core/modules/ai/index.ts +19 -0
  128. package/src/core/modules/ai/inputs/core-ai-settings.input.ts +24 -0
  129. package/src/core/modules/ai/interfaces/ai-hook.interface.ts +1 -1
  130. package/src/core/modules/ai/interfaces/ai-tool.interface.ts +38 -26
  131. package/src/core/modules/ai/models/core-ai-mode.model.ts +1 -1
  132. package/src/core/modules/ai/models/core-ai-settings.model.ts +23 -0
  133. package/src/core/modules/ai/models/core-ai-status.model.ts +16 -0
  134. package/src/core/modules/ai/models/core-ai-tool-policy.model.ts +1 -1
  135. package/src/core/modules/ai/models/core-ai-user-setting.model.ts +46 -0
  136. package/src/core/modules/ai/services/core-ai-conversation.service.ts +31 -6
  137. package/src/core/modules/ai/services/core-ai-mcp-oauth.service.ts +12 -9
  138. package/src/core/modules/ai/services/core-ai-mcp.service.ts +206 -13
  139. package/src/core/modules/ai/services/core-ai-prompt-builder.service.ts +13 -6
  140. package/src/core/modules/ai/services/core-ai-user-setting.service.ts +85 -0
  141. package/src/core/modules/ai/services/core-ai.service.ts +134 -35
  142. package/src/core/modules/api-token/README.md +4 -2
  143. package/src/core/modules/better-auth/CUSTOMIZATION.md +1 -1
  144. package/src/core/modules/better-auth/INTEGRATION-CHECKLIST.md +12 -2
  145. package/src/core/modules/better-auth/README.md +32 -1
  146. package/src/core/modules/better-auth/better-auth.config.ts +21 -5
  147. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +5 -2
  148. package/src/core/modules/better-auth/core-better-auth-path-guard.middleware.ts +85 -0
  149. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +5 -2
  150. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +14 -7
  151. package/src/core/modules/better-auth/core-better-auth.module.ts +12 -2
  152. package/src/core/modules/better-auth/index.ts +1 -0
  153. package/src/core/modules/error-code/error-codes.ts +18 -0
  154. package/src/core/modules/sync/README.md +11 -1
  155. package/src/core/modules/sync/core-sync.service.ts +48 -3
  156. package/src/core/modules/tenant/INTEGRATION-CHECKLIST.md +12 -1
  157. package/src/core/modules/tenant/README.md +49 -3
  158. package/src/core/modules/tenant/core-tenant.guard.ts +18 -2
  159. package/src/core/modules/tenant/core-tenant.service.ts +287 -2
  160. package/src/test/test.helper.ts +17 -3
@@ -396,6 +396,16 @@ factory.registerBuilder('my-provider', (conn) => new MyProvider(conn));
396
396
  `conversationId` to `aiPrompt` to load prior turns and append the new ones.
397
397
  Manage via `createAiConversation` / `findAiConversations` / `getAiConversation`
398
398
  / `deleteAiConversation`.
399
+ A prompt only continues the caller's OWN conversation, on REST, the stream and
400
+ GraphQL alike. An id the caller did not create — another user's, or one that does
401
+ not exist — counts as no conversation: the run reads no history, writes nothing,
402
+ uses no conversation-scoped tool grants, and its response carries no
403
+ `conversationId` (12.3.0; before, the turns were written into the other user's
404
+ conversation). Admins are no exception here: they may READ every conversation
405
+ (`getAiConversation`, `findAiConversations` with `all`), never continue one.
406
+ `CoreAiConversationService.appendMessage(id, message, ownerId)` checks the owner
407
+ itself and writes nothing for anyone else; a project that calls it directly passes
408
+ the id of the user the turn belongs to.
399
409
  - **Audit** (`ai.audit: true`): every run is persisted to `aiInteractions`
400
410
  (admin-readable via `findAiInteractions` / `getAiInteraction`).
401
411
  - **Streaming**: `POST /ai/stream` returns Server-Sent Events (`action`, `token`,
@@ -418,26 +428,32 @@ export class TransferFundsTool extends AiTool {
418
428
  readonly mutating = true; // governed by the confirmation policy
419
429
  readonly destructive = true; // always requires confirmation
420
430
 
421
- // PLAN-MODE pre-flight, without mutating: lets the whole plan be rejected before
422
- // any step runs. It does NOT run in auto mode or over MCP — so it must never be
423
- // the only place a permission is checked.
431
+ // Runs before EVERY execution (12.3.0+): auto mode, plan mode (for the whole plan
432
+ // before any step runs, and again per step) and MCP — on the arguments that will
433
+ // run, after the PreToolUse hooks. Never mutates. A throw counts as a refusal.
424
434
  async authorize(args, context) {
425
435
  const account = await this.accountService.get(args.fromId, context.serviceOptions).catch(() => null);
426
- return { allowed: !!account, reason: account ? undefined : 'No access to source account' };
436
+ return account
437
+ ? { allowed: true, data: account } // reaches execute() as context.authorization.data
438
+ : { allowed: false, reason: 'No access to source account' };
427
439
  }
428
440
 
429
441
  async execute(args, context) {
430
- // The REAL gate, on every path: route through the service with the caller's
431
- // serviceOptions so `@Restricted`, `securityCheck()` and tenant scoping apply.
432
- // Do not rely on authorize() having run.
433
- const account = await this.accountService.getForUser(args.fromId, context.serviceOptions);
442
+ // Still route the write through the service with the caller's serviceOptions, so
443
+ // `@Restricted`, `securityCheck()` and tenant scoping apply: authorize() decides
444
+ // whether to try, the service decides what the user may change.
445
+ const account =
446
+ context.authorization?.data ?? (await this.accountService.getForUser(args.fromId, context.serviceOptions));
434
447
  return this.accountService.transfer(account, args.toId, args.amount, context.serviceOptions);
435
448
  }
436
449
  }
437
450
  ```
438
451
 
439
- > **Do not put a permission check only in `authorize()`.** It runs in plan mode
440
- > only — auto mode (the default) and MCP go straight to `execute()`.
452
+ > **`authorize()` runs on every path since 12.3.0.** Until 12.2 it ran in plan mode
453
+ > only — auto mode (the default) and MCP went straight to `execute()`. A refusal skips
454
+ > the call: the model receives `TOOL_NOT_AVAILABLE` with the `reason` (keep it free of
455
+ > anything the user may not know), an MCP client an error result. The refusal is not
456
+ > recorded as a learning signal.
441
457
 
442
458
  ## Confirmation for changes
443
459
 
@@ -447,6 +463,8 @@ export class TransferFundsTool extends AiTool {
447
463
  admin set `enforced: true`.
448
464
  - When confirmation is needed the response has `requiresConfirmation: true` +
449
465
  `pendingActions`; re-send the prompt with `confirm: true` to proceed.
466
+ - Over MCP the same flags apply through MCP elicitation (12.3.0+) — see
467
+ [Authorization and confirmation over MCP](#authorization-and-confirmation-over-mcp-1230).
450
468
 
451
469
  ## Client metadata & prompt enrichment
452
470
 
@@ -455,6 +473,16 @@ export class TransferFundsTool extends AiTool {
455
473
  - The system prompt is enriched with the user's roles + available tools and optional
456
474
  `ai.documentation` (override `CoreAiPromptBuilderService.getDocumentation()` for RAG).
457
475
 
476
+ **What bounds the size.** `input.context` and `input.metadata` are capped at 4000 characters
477
+ each for the prompt — that cap protects the context window, not the CPU. The work is bounded
478
+ earlier, by the body parser: a request over `bodyParser.json.limit` (100 kB unless configured)
479
+ is refused with 413 before it is parsed. A project that raises the limit for `dataUrl`
480
+ attachments should raise it only as far as its images need; serializing a client block for the
481
+ cap then costs about half of what parsing the same body already did (measured 2026-11-03:
482
+ 2 MB → ~10 ms parse, ~5 ms serialize). The same holds for tool results: `ai.maxToolResultChars`
483
+ keeps them out of the context window, while a tool's own query decides how much data it loads —
484
+ page large result sets in the tool rather than relying on the cap.
485
+
458
486
  ## Self-optimizing prompts (editable templates + governed learning)
459
487
 
460
488
  The system prompt is assembled from **keyed fragments** by `CoreAiPromptBuilderService`,
@@ -506,8 +534,8 @@ so it can recover within the run.
506
534
  > **Security:** learned hints and template overrides only ever **add textual guidance** —
507
535
  > they can never relax the permission model. Tool role-filtering,
508
536
  > `CrudService`/`@Restricted` and `secretFields` are enforced backend-side regardless of
509
- > the prompt. (`authorize()` is _not_ part of that unconditional set — it runs in plan
510
- > mode only; see [Deferred tool schemas](#deferred-tool-schemas-large-registries) below.)
537
+ > the prompt. Since 12.3.0 a tool's `authorize()` belongs to that set too: it runs before
538
+ > every execution, in every mode and over MCP.
511
539
 
512
540
  ```typescript
513
541
  ai: {
@@ -533,6 +561,37 @@ When a user's session history would overflow, the **oldest non-system turns are
533
561
  `ai.maxToolResultChars` (default `12000`) — so long-running conversations never exceed the
534
562
  model's limit.
535
563
 
564
+ **Native tool definitions count against the budget** (12.3.0+): a connection with
565
+ `supportsNativeTools: true` carries every description and schema in the `tools` payload, beside
566
+ the messages, and that payload occupies the same window. **A crowded window is reported**: when
567
+ the system prompt and the tool definitions take more than half of the input budget, the
568
+ framework warns once per connection and process, naming the connection, the numbers and whether
569
+ the window is the connection's own or the `ai.contextWindow` fallback. Before, the only trace was
570
+ a debug line, and the visible symptom was an assistant that answered generically because the
571
+ user's message had been cut.
572
+
573
+ **Guarding the prompt size in a project test:** measure against `ai.contextWindow` (the
574
+ fallback) and say so in the test — it guards the connections without a window of their own,
575
+ the worst case, not the window a pinned or detected connection has. Raising its limit removes
576
+ that guard; shrink the catalog instead (`deferToolSchemas`, `deferToolSummaryChars`, fewer
577
+ tools per role).
578
+
579
+ ## Tool catalog for native connections (12.3.0+)
580
+
581
+ A connection with native tool calling receives every tool's description and parameter schema
582
+ in the `tools` payload. Its system-prompt catalog therefore lists **names only** — repeating
583
+ the descriptions (and, without `deferToolSchemas`, the schemas) paid for every tool twice.
584
+ Emulated connections and plan mode keep the full catalog: there it is the model's only source.
585
+
586
+ ```typescript
587
+ ai: {
588
+ nativeToolCatalog: 'full', // restores descriptions (and schemas) in the native catalog; default 'names'
589
+ }
590
+ ```
591
+
592
+ Use `'full'` only for a model that demonstrably picks tools better with the descriptions in the
593
+ prompt; the cost is the duplicated text on every call.
594
+
536
595
  ## Deferred tool schemas (large registries)
537
596
 
538
597
  The system prompt normally carries every tool's full JSON parameter schema. With a large
@@ -558,9 +617,10 @@ context, set `deferToolSummaryChars` alongside it (roughly 200–400 works well)
558
617
 
559
618
  > **Scoped to the emulated tool protocol, not to the provider.** In auto mode a connection
560
619
  > with `supportsNativeTools: true` receives every full description and JSON schema through
561
- > the native `tools` payload anyway, so truncation is skipped there and no `search_tools`
562
- > banner is emitted — telling the model a description was cut while handing it the full text
563
- > in the same request would only buy a wasted round-trip against `maxIterations`.
620
+ > the native `tools` payload anyway, and its catalog lists names only (see above), so
621
+ > `deferToolSummaryChars` has no effect there and no `search_tools` banner is emitted —
622
+ > telling the model a description was cut while handing it the full text in the same request
623
+ > would only buy a wasted round-trip against `maxIterations`.
564
624
  >
565
625
  > **Plan mode always uses the emulated protocol**, so a native connection IS truncated there.
566
626
  > Because plan mode returns a complete plan in one call without executing anything, the model
@@ -577,10 +637,8 @@ context, set `deferToolSummaryChars` alongside it (roughly 200–400 works well)
577
637
  > confirmation gate reads a tool's `mutating`/`destructive` **flags**, never its description
578
638
  > — so a truncated "requires confirmation" sentence cannot disable it.
579
639
  >
580
- > Note `AiTool.authorize()` is **not** part of that chain in every mode: it runs in **plan
581
- > mode only**. In auto mode (the default) and over MCP, tools go straight to `execute()` — so
582
- > data-level checks (ownership, tenant scope) must live **inside `execute()`**, routed through
583
- > `CrudService` with `context.serviceOptions`, not in `authorize()` alone.
640
+ > `AiTool.authorize()` is part of that chain on every path since 12.3.0 (auto mode, plan
641
+ > mode, MCP), and it reads the call's arguments, never the description.
584
642
 
585
643
  ## Token budgets & usage
586
644
 
@@ -641,6 +699,21 @@ them:
641
699
 
642
700
  Without multi-tenancy, tool roles are compared with `user.roles` exactly as before.
643
701
 
702
+ ## Is the assistant available? (`GET /ai/status`, 12.3.1+)
703
+
704
+ A frontend asks before anybody signs in, to decide whether it shows the assistant at all:
705
+
706
+ | Server | Answer |
707
+ | ----------------------------- | -------------------------------- |
708
+ | `ai` configured, `ai.mcp` off | `200 { "mcp": false }` |
709
+ | `ai` configured, `ai.mcp` on | `200 { "mcp": true }` |
710
+ | no `ai` (or `enabled: false`) | `404` — the route does not exist |
711
+
712
+ The route is public (`@Roles(S_EVERYONE)`): no session, token or cookie is needed, and the body names no user data.
713
+ `404` is the signal "AI is off" on purpose — a server without the module, also one older than 12.3.1, answers it the
714
+ same way, so a client needs no second rule. Every other `/ai` route keeps its roles. A project controller extending
715
+ `CoreAiController` inherits the route; `mcp` comes from what `CoreAiModule.forRoot()` registered (`AI_MCP_ENABLED`).
716
+
644
717
  ## MCP server (`ai.mcp: true`)
645
718
 
646
719
  The `AiToolRegistry` also feeds a real **MCP server** at `POST/GET/DELETE /ai/mcp`
@@ -671,6 +744,53 @@ the server log; the response carries only a stable `#LTNS_0901` code.
671
744
  - Answering a foreign id `404` rather than `403` is deliberate — a 403 would confirm
672
745
  that the id exists, and the id travels in a response header and through client logs.
673
746
 
747
+ ### Authorization and confirmation over MCP (12.3.0+)
748
+
749
+ `tools/call` applies the same gates as the chat orchestrator, in this order:
750
+
751
+ 1. **Role filter** — the tool must be in the user's set (`AiToolRegistry.forUser()`).
752
+ 2. **Tool policies** — the admin's `aiToolPolicies` rules for this call: `deny` answers an
753
+ error result, `ask` asks in step 4 whatever else is configured.
754
+ 3. **`authorize()`** — the tool's data-level check on the call's arguments. A refusal, a
755
+ thrown error or an `allowed` that is not `true` answers an error result; nothing runs.
756
+ 4. **Confirmation** — a `destructive` tool always, a `mutating` tool when
757
+ `ai.confirmation.mutating` says so (`enforced`, else `default`; MCP has no per-request
758
+ `requireConfirmation`), as long as MCP asks this user (below). The server asks through
759
+ **MCP elicitation** (a yes/no form the client shows) and runs the tool only on an
760
+ explicit yes. A no, a cancelled dialog, an error or a timeout (5 minutes) means "not
761
+ run". A client without the `elicitation` capability gets an error result instead of an
762
+ unconfirmed run.
763
+ 5. **`execute()`** — with `context.authorization` from step 3.
764
+
765
+ **Whether MCP asks** is a server default each user may switch for themselves, like a
766
+ "bypass permissions" mode:
767
+
768
+ | `ai.mcp.confirmation` | Asks a user who has not chosen | A user's own switch |
769
+ | ------------------------------------ | ------------------------------ | -------------------------------------------- |
770
+ | unset, `true`, `{}` | yes | may switch it off |
771
+ | `false` | no | may switch it on |
772
+ | `{ default: true, enforced: true }` | yes | ignored; a change answers 403 (`#LTNS_0609`) |
773
+ | `{ default: false, enforced: true }` | no | ignored; a change answers 403 |
774
+
775
+ The user's switch is `PUT /ai/settings` with `{ mcpConfirmation: false }` (GraphQL
776
+ `updateAiSettings`), read back with `GET /ai/settings` (`aiSettings`), which answers the
777
+ effective value and `mcpConfirmationEnforced` so a UI can show the switch disabled. It is
778
+ stored per user (`aiUserSettings`, one record per user) and touches nobody else's calls.
779
+ Only `false` switches the default off and only `enforced: true` enforces — a misspelt value
780
+ never relaxes the gate. `authorize()` and the admin's `deny` / `ask` rules apply whatever
781
+ the switch says.
782
+
783
+ `tools/list` gives a `destructive` tool `annotations: { destructiveHint: true, readOnlyHint: false }`
784
+ and a `mutating` one `{ destructiveHint: false, readOnlyHint: false }` — hints for the client's own
785
+ UI, not a security boundary. A tool with neither flag carries no annotations: it may still write,
786
+ so it is never advertised as read-only, and clients keep the spec's cautious defaults.
787
+
788
+ The confirmation keeps the model from acting without the user; it cannot stop a hostile
789
+ client, which holds the user's credential and could call the API directly. The boundary
790
+ against that stays the role filter, `authorize()` and the `CrudService` inside `execute()`.
791
+ The question is English (an MCP request carries no language); override
792
+ `CoreAiMcpService.confirmationPrompt()` to localize it.
793
+
674
794
  ### Multi-replica / sticky sessions
675
795
 
676
796
  An MCP session owns a **live Streamable-HTTP transport** — an open response stream
@@ -696,6 +816,16 @@ and sends operators looking in the wrong place.
696
816
 
697
817
  Without Redis nothing changes: unknown ids answer `404` exactly as before.
698
818
 
819
+ **No sticky sessions available?** Set `ai.mcp.stateless: true` (12.3.0+). Every POST then builds its own
820
+ server and transport for the authenticated user (the SDK's stateless mode, `sessionIdGenerator: undefined`),
821
+ no `mcp-session-id` is issued, a session id a client still sends is ignored, and GET / DELETE answer `405`
822
+ with `Allow: POST`. Any replica can answer any request, and the user's rights and tenant are read per request
823
+ by construction. What is lost is what a session carries: server-initiated messages, the SSE stream, and
824
+ server-initiated requests — **MCP elicitation among them**. A tool that needs confirmation (above) is
825
+ therefore refused in this mode unless the user switched the question off (`PUT /ai/settings`) or
826
+ `ai.mcp.confirmation` is `false`. A server that only offers read tools is not affected.
827
+ `testHelper.mcpSession()` follows the setting (no session id expected or sent; `sessionId` is `''`).
828
+
699
829
  ### OAuth 2.1 (`ai.mcp.oauth: true`)
700
830
 
701
831
  For generic MCP clients that auto-discover + register:
@@ -767,7 +897,13 @@ user decides whether an MCP client may act for them:
767
897
  - **Allow** issues the code only with the page's CSRF token, signed with the OAuth secret and bound to
768
898
  the user, the client, the redirect URI and the PKCE challenge (10 minutes); a stale token shows the
769
899
  form again, a forged or tampered one too, and nothing is issued. **Deny** sends the client back with
770
- `access_denied`.
900
+ `access_denied` — with the same token since 12.3.0; without it the form is shown again.
901
+ - **A malformed request** (a missing `code_challenge`, a wrong `response_type`, …) is answered with `400`
902
+ and `{ error, error_description }`, never redirected to the client (12.3.0). The redirect URI is whatever
903
+ the client's registrant wrote, so the SDK's error redirect made `/authorize` an open redirect on this
904
+ domain (RFC 9700 §4.11.2). Registration itself stays open — claude.ai, ChatGPT, Cursor and Claude Code
905
+ register their own URIs. Only what happens once the request reached the provider (the user's Allow or
906
+ Deny, and refusals raised in `authorizeConsent()`) still redirects.
771
907
 
772
908
  The page carries its own `Content-Security-Policy` (script and style by nonce, `frame-ancestors 'none'`,
773
909
  no `form-action`, which browsers would apply to the redirect to the client), `X-Frame-Options: DENY`,
@@ -792,8 +928,25 @@ the page offers no sign-in. Its sign-in goes through the IAM sign-in controller,
792
928
  hands Better-Auth the request so Better-Auth's own login-CSRF protection runs — otherwise a hostile page
793
929
  could sign the visitor in to the attacker's account before they approve a client.
794
930
 
795
- **Your own page.** Override `renderConsentPage(view)` in a subclass to restyle it (keep every value
796
- escaped), or `authorizeConsent(client, params, res)` for a consent UI of your own — same shape: no session
931
+ **Your own wording and look (12.3.0+).** Override `renderConsentPage(view)` and call
932
+ `renderMcpOAuthConsentPage(view, { texts, styles })`: `texts` replaces single texts per locale (merged over
933
+ the built-in ones, still escaped), `styles` is CSS appended inside the page's nonce'd `<style>` element. The
934
+ markup, the scripts and the security headers stay the framework's, so the page keeps receiving their fixes.
935
+ Fill both from code or configuration only, never from user or tenant input: a `<` in `styles` is neutralized
936
+ so it cannot end the `<style>` element, and the page's CSP blocks what CSS could load from elsewhere, but CSS
937
+ still decides what the page shows.
938
+
939
+ ```typescript
940
+ protected override renderConsentPage(view: IMcpOAuthConsentView): string {
941
+ return renderMcpOAuthConsentPage(view, {
942
+ styles: ':root{--accent:#0b5}',
943
+ texts: { de: { intro: '{client} möchte in Ihrem Namen auf {app} zugreifen.' } },
944
+ });
945
+ }
946
+ ```
947
+
948
+ **Your own page.** Override `renderConsentPage(view)` in a subclass to restyle it beyond that (keep every
949
+ value escaped), or `authorizeConsent(client, params, res)` for a consent UI of your own — same shape: no session
797
950
  → sign in, GET → show which client, POST with a CSRF token → `saveAuthorizationCode()` and redirect;
798
951
  refuse with `AccessDeniedError` from `this.loadOAuthErrors()`. Register the subclass via
799
952
  `CoreModule.forRoot(env, { ai: { mcpOAuthService } })`, so the MCP controller and the endpoints share it.
@@ -802,9 +955,9 @@ production secret check runs again before the endpoints are mounted (without a s
802
955
  environment outside `local` / `ci` / `e2e` refuses to boot and to sign tokens at all — deployed `develop` and `test`
803
956
  stages included).
804
957
 
805
- Known property of the SDK: `/authorize` redirects its error answers to the client's registered
806
- redirect URI, and any client may register any https URI — an open redirect in the sense of RFC 9700
807
- §4.11.2, carrying only an OAuth error code.
958
+ Until 12.3.0 the SDK's error redirect made `/authorize` an open redirect in the sense of RFC 9700
959
+ §4.11.2, since any client may register any https URI; the framework now answers those errors itself
960
+ (see "A malformed request" under the consent page above).
808
961
 
809
962
  **Refusals are OAuth errors (11.43.0).** The SDK's handlers answer an error with its OAuth code only
810
963
  when it is one of the SDK's own `OAuthError` classes; anything else becomes `500 server_error`. The
@@ -48,7 +48,8 @@ import { AiToolUser } from './tools/ai-tool.registry.js';
48
48
  * sessions behind a load balancer. When Redis is configured, session ids are additionally
49
49
  * registered in a shared registry (replica + owning user), which turns a mis-routed request
50
50
  * into an explicit 409 naming the owning replica instead of a misleading "unknown session" 404.
51
- * Without Redis the behavior is unchanged.
51
+ * Without Redis the behavior is unchanged. `ai.mcp.stateless` removes the requirement altogether:
52
+ * every request then builds its own server and transport, and any replica can answer it.
52
53
  *
53
54
  * The registry entry records the OWNING USER, and only that user is ever told about the 409.
54
55
  * Otherwise the cross-replica path would undo the local one: an authenticated caller probing
@@ -130,6 +131,11 @@ export class CoreAiMcpController implements OnModuleDestroy {
130
131
  return;
131
132
  }
132
133
 
134
+ if (this.statelessEnabled()) {
135
+ await this.handleStatelessPost(req, res, user);
136
+ return;
137
+ }
138
+
133
139
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
134
140
  let entry = sessionId ? this.transports.get(sessionId) : undefined;
135
141
 
@@ -342,6 +348,42 @@ export class CoreAiMcpController implements OnModuleDestroy {
342
348
  return body?.method === 'initialize';
343
349
  }
344
350
 
351
+ /**
352
+ * Whether `/ai/mcp` runs without sessions (`ai.mcp.stateless`).
353
+ */
354
+ protected statelessEnabled(): boolean {
355
+ const mcp = ConfigService.get<{ stateless?: boolean }>('ai.mcp');
356
+ return typeof mcp === 'object' && mcp?.stateless === true;
357
+ }
358
+
359
+ /**
360
+ * One request, one server: the stateless mode of the SDK transport (`sessionIdGenerator: undefined`).
361
+ *
362
+ * Nothing outlives the response, so nothing has to reach the same replica again — the reason this
363
+ * mode exists. Rights are the user's as of THIS request by construction. A `mcp-session-id` a client
364
+ * still sends (from before a switch to this mode) is ignored rather than answered 404: there is no
365
+ * session it could refer to, and a 404 would only send the client into a reconnect.
366
+ */
367
+ protected async handleStatelessPost(req: Request, res: Response, user: AiToolUser): Promise<void> {
368
+ let StreamableHTTPServerTransport: any;
369
+ try {
370
+ ({ StreamableHTTPServerTransport } = await import('@modelcontextprotocol/sdk/server/streamableHttp.js'));
371
+ } catch (err) {
372
+ // Same lazy import and 503 reasoning as the session path in handlePost().
373
+ return this.mcpUnavailable(res, err as Error);
374
+ }
375
+ const transport: any = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
376
+ const server = await this.mcpService.createServer(user);
377
+ // Released with the response — an aborted request included, so a client that hangs up cannot
378
+ // leave a server behind.
379
+ res.on('close', () => {
380
+ Promise.resolve(transport.close?.()).catch(() => undefined);
381
+ Promise.resolve(server.close?.()).catch(() => undefined);
382
+ });
383
+ await server.connect(transport);
384
+ await transport.handleRequest(req, res, req.body);
385
+ }
386
+
345
387
  /**
346
388
  * Whether the OAuth 2.1 layer is enabled via `ai.mcp.oauth`.
347
389
  */
@@ -359,6 +401,11 @@ export class CoreAiMcpController implements OnModuleDestroy {
359
401
  this.unauthorized(res);
360
402
  return;
361
403
  }
404
+ // Without sessions there is no stream to open and nothing to end (MCP spec: 405 for GET then).
405
+ if (this.statelessEnabled()) {
406
+ res.status(405).set('Allow', 'POST').json({ error: 'Method not allowed: this MCP server is stateless' });
407
+ return;
408
+ }
362
409
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
363
410
  const held = sessionId ? this.transports.get(sessionId) : undefined;
364
411
  // Same ownership rule as the POST path: another user's session is not ours to serve.
@@ -91,6 +91,12 @@ export const AI_TOOL_POLICY_CLASS = 'AI_TOOL_POLICY_CLASS';
91
91
  /** Mongoose injection token. */
92
92
  export const AI_TOOL_POLICY_MODEL = 'AiToolPolicy';
93
93
 
94
+ // ---- user-setting ----
95
+ /** DI token for the model constructor (used by CrudService mapping). */
96
+ export const AI_USER_SETTING_CLASS = 'AI_USER_SETTING_CLASS';
97
+ /** Mongoose injection token. */
98
+ export const AI_USER_SETTING_MODEL = 'AiUserSetting';
99
+
94
100
  // ---- MCP OAuth (stored natively on MongoDB; these models give them their tables on a SQL source) ----
95
101
  /** Mongoose model name of the registered OAuth clients (`aiMcpOAuthClients`). */
96
102
  export const AI_MCP_OAUTH_CLIENT_MODEL = 'AiMcpOAuthClient';
@@ -100,3 +106,7 @@ export const AI_MCP_OAUTH_CODE_MODEL = 'AiMcpOAuthCode';
100
106
  export const AI_MCP_OAUTH_REFRESH_TOKEN_MODEL = 'AiMcpOAuthRefreshToken';
101
107
  /** How long an authorization code may be exchanged — checked on every read, not left to a TTL index. */
102
108
  export const AI_MCP_OAUTH_CODE_TTL_SECONDS = 600;
109
+
110
+ // ---- module options ----
111
+ /** Whether `CoreAiModule.forRoot()` registered the MCP endpoint (`ai.mcp`) — what `GET /ai/status` answers. */
112
+ export const AI_MCP_ENABLED = 'AI_MCP_ENABLED';
@@ -1,4 +1,4 @@
1
- import { Body, Controller, Delete, Get, Param, Post, Put, Query, Res } from '@nestjs/common';
1
+ import { Body, Controller, Delete, Get, Inject, Optional, Param, Post, Put, Query, Res } from '@nestjs/common';
2
2
  import type { Response } from 'express';
3
3
 
4
4
  import { RESTServiceOptions } from '../../common/decorators/rest-service-options.decorator.js';
@@ -6,6 +6,7 @@ import { Roles } from '../../common/decorators/roles.decorator.js';
6
6
  import { RoleEnum } from '../../common/enums/role.enum.js';
7
7
  import type { ServiceOptions } from '../../common/interfaces/service-options.interface.js';
8
8
  import { RequestContext } from '../../common/services/request-context.service.js';
9
+ import { AI_MCP_ENABLED } from './core-ai.constants.js';
9
10
  import { CoreAiBudgetLimitCreateInput } from './inputs/core-ai-budget-limit-create.input.js';
10
11
  import { CoreAiBudgetLimitInput } from './inputs/core-ai-budget-limit.input.js';
11
12
  import { CoreAiConnectionCreateInput } from './inputs/core-ai-connection-create.input.js';
@@ -19,6 +20,7 @@ import { CoreAiPromptUpdateInput } from './inputs/core-ai-prompt-update.input.js
19
20
  import { CoreAiSlotCreateInput } from './inputs/core-ai-slot-create.input.js';
20
21
  import { CoreAiSlotUpdateInput } from './inputs/core-ai-slot-update.input.js';
21
22
  import { CoreAiPromptInput } from './inputs/core-ai-prompt.input.js';
23
+ import { CoreAiSettingsInput } from './inputs/core-ai-settings.input.js';
22
24
  import { CoreAiAvailableConnection } from './models/core-ai-available-connection.model.js';
23
25
  import { CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
24
26
  import { CoreAiConnectionPreference } from './models/core-ai-connection-preference.model.js';
@@ -29,6 +31,8 @@ import { CoreAiPromptHint } from './models/core-ai-prompt-hint.model.js';
29
31
  import { CoreAiPrompt } from './models/core-ai-prompt.model.js';
30
32
  import { CoreAiSlot } from './models/core-ai-slot.model.js';
31
33
  import { CoreAiResponse } from './models/core-ai-response.model.js';
34
+ import { CoreAiSettings } from './models/core-ai-settings.model.js';
35
+ import { CoreAiStatus } from './models/core-ai-status.model.js';
32
36
  import { CoreAiUsageInfo } from './models/core-ai-usage-info.model.js';
33
37
  import { CoreAiBudgetService } from './services/core-ai-budget.service.js';
34
38
  import { CoreAiConnectionPreferenceService } from './services/core-ai-connection-preference.service.js';
@@ -37,6 +41,7 @@ import { CoreAiConnectionService } from './services/core-ai-connection.service.j
37
41
  import { CoreAiConversationService } from './services/core-ai-conversation.service.js';
38
42
  import { CoreAiInteractionService } from './services/core-ai-interaction.service.js';
39
43
  import { CoreAiPlaceholderRegistry } from './services/core-ai-placeholder.registry.js';
44
+ import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
40
45
  import { CoreAiPromptHintService } from './services/core-ai-prompt-hint.service.js';
41
46
  import { CoreAiPromptService } from './services/core-ai-prompt.service.js';
42
47
  import { CoreAiSlotService } from './services/core-ai-slot.service.js';
@@ -45,6 +50,7 @@ import { CoreAiService } from './services/core-ai.service.js';
45
50
  /**
46
51
  * REST controller for AI prompts and connection management.
47
52
  *
53
+ * - `GET /ai/status`: everyone, also without a session.
48
54
  * - `POST /ai/prompt`: any authenticated user.
49
55
  * - `/ai/connections*`: admin only.
50
56
  *
@@ -67,6 +73,29 @@ export class CoreAiController {
67
73
  protected readonly placeholderRegistry: CoreAiPlaceholderRegistry,
68
74
  ) {}
69
75
 
76
+ /**
77
+ * Personal AI settings (12.3.0+). Injected as a property, not through the constructor, so a project subclass
78
+ * written before it keeps compiling and still gets the settings endpoints.
79
+ */
80
+ @Inject(CoreAiUserSettingService)
81
+ protected readonly userSettingService: CoreAiUserSettingService;
82
+
83
+ /** Whether the MCP endpoint is registered (`ai.mcp`, 12.3.1+). A property for the same reason as the settings. */
84
+ @Optional()
85
+ @Inject(AI_MCP_ENABLED)
86
+ protected readonly mcpEnabled?: boolean;
87
+
88
+ /**
89
+ * What the AI module offers on this server (12.3.1+). Public on purpose: a frontend asks before anybody signs in,
90
+ * to decide whether it shows the assistant at all. The route exists only while the module is registered, so a
91
+ * `404` is the answer "AI is off"; the body names no user data.
92
+ */
93
+ @Get('status')
94
+ @Roles(RoleEnum.S_EVERYONE)
95
+ async status(): Promise<CoreAiStatus> {
96
+ return { mcp: this.mcpEnabled === true };
97
+ }
98
+
70
99
  /**
71
100
  * Send a prompt to the AI assistant.
72
101
  */
@@ -362,6 +391,31 @@ export class CoreAiController {
362
391
  return this.budgetService.getUsageInfo(serviceOptions?.currentUser?.id, RequestContext.getTenantId());
363
392
  }
364
393
 
394
+ // ===================================================================================================================
395
+ // Personal settings
396
+ // ===================================================================================================================
397
+
398
+ /**
399
+ * The current user's AI settings as they take effect.
400
+ */
401
+ @Get('settings')
402
+ @Roles(RoleEnum.S_USER)
403
+ async settings(@RESTServiceOptions() serviceOptions: ServiceOptions): Promise<CoreAiSettings> {
404
+ return this.userSettingService.getSettings(serviceOptions?.currentUser?.id);
405
+ }
406
+
407
+ /**
408
+ * Change the current user's own AI settings (403 for a setting the server enforces).
409
+ */
410
+ @Put('settings')
411
+ @Roles(RoleEnum.S_USER)
412
+ async updateSettings(
413
+ @RESTServiceOptions() serviceOptions: ServiceOptions,
414
+ @Body() input: CoreAiSettingsInput,
415
+ ): Promise<CoreAiSettings> {
416
+ return this.userSettingService.updateSettings(serviceOptions?.currentUser?.id, input);
417
+ }
418
+
365
419
  /**
366
420
  * Find AI budget limits (admin).
367
421
  */
@@ -8,9 +8,12 @@ import { CoreAiMcpOAuthService } from './services/core-ai-mcp-oauth.service.js';
8
8
  import { CoreAiMcpService } from './services/core-ai-mcp.service.js';
9
9
  import { CorePersistenceModule } from '../../common/persistence/core-persistence.module.js';
10
10
  import {
11
+ AI_MCP_ENABLED,
11
12
  AI_MCP_OAUTH_CLIENT_MODEL,
12
13
  AI_MCP_OAUTH_CODE_MODEL,
13
14
  AI_MCP_OAUTH_REFRESH_TOKEN_MODEL,
15
+ AI_USER_SETTING_CLASS,
16
+ AI_USER_SETTING_MODEL,
14
17
  } from './core-ai.constants.js';
15
18
  import { AiBudgetLimitSchema, CoreAiBudgetLimit } from './models/core-ai-budget-limit.model.js';
16
19
  import {
@@ -31,6 +34,7 @@ import { AiSlotSchema, CoreAiSlot } from './models/core-ai-slot.model.js';
31
34
  import { AiModeSchema, CoreAiMode } from './models/core-ai-mode.model.js';
32
35
  import { AiToolGrantSchema, CoreAiToolGrant } from './models/core-ai-tool-grant.model.js';
33
36
  import { AiToolPolicySchema, CoreAiToolPolicy } from './models/core-ai-tool-policy.model.js';
37
+ import { AiUserSettingSchema, CoreAiUserSetting } from './models/core-ai-user-setting.model.js';
34
38
  import { LlmProviderFactory } from './providers/llm-provider.factory.js';
35
39
  import { AiCryptoService } from './services/ai-crypto.service.js';
36
40
  import {
@@ -79,6 +83,7 @@ import {
79
83
  CoreAiToolPolicyService,
80
84
  } from './services/core-ai-tool-policy.service.js';
81
85
  import { AI_MODE_CLASS, AI_MODE_MODEL, CoreAiModeService } from './services/core-ai-mode.service.js';
86
+ import { CoreAiUserSettingService } from './services/core-ai-user-setting.service.js';
82
87
  import { CoreAiMcpClientService } from './services/core-ai-mcp-client.service.js';
83
88
  import { CoreAiService } from './services/core-ai.service.js';
84
89
  import { AiHookRegistry } from './hooks/ai-hook.registry.js';
@@ -149,6 +154,9 @@ export interface CoreAiModuleOptions {
149
154
  /** Custom scoped tool-policy store (extends CoreAiToolPolicyService). */
150
155
  toolPolicyService?: Type<CoreAiToolPolicyService>;
151
156
 
157
+ /** Custom personal-settings store (extends CoreAiUserSettingService). */
158
+ userSettingService?: Type<CoreAiUserSettingService>;
159
+
152
160
  /** Custom named-mode store (extends CoreAiModeService). */
153
161
  modeService?: Type<CoreAiModeService>;
154
162
 
@@ -208,6 +216,7 @@ export class CoreAiModule {
208
216
  { model: CoreAiToolGrant, name: AI_TOOL_GRANT_MODEL, schema: AiToolGrantSchema },
209
217
  { model: CoreAiToolPolicy, name: AI_TOOL_POLICY_MODEL, schema: AiToolPolicySchema },
210
218
  { model: CoreAiMode, name: AI_MODE_MODEL, schema: AiModeSchema },
219
+ { model: CoreAiUserSetting, name: AI_USER_SETTING_MODEL, schema: AiUserSettingSchema },
211
220
  // Stored natively on MongoDB; the models give them their tables on a SQL source
212
221
  { name: AI_MCP_OAUTH_CLIENT_MODEL, schema: AiMcpOAuthClientSchema },
213
222
  { name: AI_MCP_OAUTH_CODE_MODEL, schema: AiMcpOAuthCodeSchema },
@@ -215,6 +224,7 @@ export class CoreAiModule {
215
224
  ]);
216
225
  const ToolGrantServiceClass = options.toolGrantService || CoreAiToolGrantService;
217
226
  const ToolPolicyServiceClass = options.toolPolicyService || CoreAiToolPolicyService;
227
+ const UserSettingServiceClass = options.userSettingService || CoreAiUserSettingService;
218
228
  const ModeServiceClass = options.modeService || CoreAiModeService;
219
229
  const McpClientServiceClass = options.mcpClientService || CoreAiMcpClientService;
220
230
  const McpOAuthServiceClass = options.mcpOAuthService || CoreAiMcpOAuthService;
@@ -250,6 +260,7 @@ export class CoreAiModule {
250
260
  CoreAiService,
251
261
  CoreAiToolGrantService,
252
262
  CoreAiToolPolicyService,
263
+ CoreAiUserSettingService,
253
264
  CoreAiModeService,
254
265
  CoreAiMcpClientService,
255
266
  LlmProviderFactory,
@@ -264,6 +275,7 @@ export class CoreAiModule {
264
275
  AskUserQuestionAiTool,
265
276
  SearchToolsAiTool,
266
277
  LlmProviderFactory,
278
+ { provide: AI_MCP_ENABLED, useValue: options.mcpEnabled === true },
267
279
  { provide: AI_BUDGET_LIMIT_CLASS, useValue: CoreAiBudgetLimit },
268
280
  { provide: AI_CONNECTION_CLASS, useValue: CoreAiConnection },
269
281
  { provide: AI_CONNECTION_PREFERENCE_CLASS, useValue: CoreAiConnectionPreference },
@@ -275,6 +287,7 @@ export class CoreAiModule {
275
287
  { provide: AI_TOOL_GRANT_CLASS, useValue: CoreAiToolGrant },
276
288
  { provide: AI_TOOL_POLICY_CLASS, useValue: CoreAiToolPolicy },
277
289
  { provide: AI_MODE_CLASS, useValue: CoreAiMode },
290
+ { provide: AI_USER_SETTING_CLASS, useValue: CoreAiUserSetting },
278
291
  { provide: CoreAiBudgetService, useClass: BudgetServiceClass },
279
292
  { provide: CoreAiConversationService, useClass: ConversationServiceClass },
280
293
  { provide: CoreAiInteractionService, useClass: InteractionServiceClass },
@@ -293,6 +306,7 @@ export class CoreAiModule {
293
306
  { provide: CoreAiService, useClass: ServiceClass },
294
307
  { provide: CoreAiToolGrantService, useClass: ToolGrantServiceClass },
295
308
  { provide: CoreAiToolPolicyService, useClass: ToolPolicyServiceClass },
309
+ { provide: CoreAiUserSettingService, useClass: UserSettingServiceClass },
296
310
  { provide: CoreAiModeService, useClass: ModeServiceClass },
297
311
  { provide: CoreAiMcpClientService, useClass: McpClientServiceClass },
298
312
  { provide: CoreAiMcpOAuthService, useClass: McpOAuthServiceClass },