ocean-brain 0.13.3 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/README.md +66 -2
  2. package/dist/mcp.js +439 -480
  3. package/package.json +2 -2
  4. package/server/client/dist/assets/{ArrowRight.es-BlNsZQFs.js → ArrowRight.es-nBJws3ws.js} +1 -1
  5. package/server/client/dist/assets/{AuxiliaryPanelHeader-HAttwVby.js → AuxiliaryPanelHeader-BN_2axQu.js} +1 -1
  6. package/server/client/dist/assets/{Calendar-DwQBllfw.js → Calendar-vA4Xa4Qr.js} +1 -1
  7. package/server/client/dist/assets/{Callout-CcYyvHQP.js → Callout-Bo2P960P.js} +1 -1
  8. package/server/client/dist/assets/{CaretUp.es-C8iY5OlB.js → CaretUp.es-DDbqKn78.js} +1 -1
  9. package/server/client/dist/assets/{CheckCircle.es-Dqr3WImn.js → CheckCircle.es-CyY9aLBF.js} +1 -1
  10. package/server/client/dist/assets/{Copy.es-fMv0ETir.js → Copy.es-BwX4tEoU.js} +1 -1
  11. package/server/client/dist/assets/{File.es-CHtXXafR.js → File.es-DeTgvUnI.js} +1 -1
  12. package/server/client/dist/assets/{FileText.es-DPIwDCEe.js → FileText.es-C-bpw8dQ.js} +1 -1
  13. package/server/client/dist/assets/{FlowArrow.es-Dxee98ir.js → FlowArrow.es-CKcMBOmX.js} +1 -1
  14. package/server/client/dist/assets/{Graph-hVj6vAYJ.js → Graph-BNoSRJP3.js} +1 -1
  15. package/server/client/dist/assets/{Graph-CdEu8eAn.js → Graph-DYYxNAWn.js} +1 -1
  16. package/server/client/dist/assets/{Image.es-CjXY1JVf.js → Image.es-d4jWnijM.js} +1 -1
  17. package/server/client/dist/assets/{Info.es-CYxg-Vku.js → Info.es-DVzN2joA.js} +1 -1
  18. package/server/client/dist/assets/Integration-gDDtIJYJ.js +1 -0
  19. package/server/client/dist/assets/{LinkSimple.es-B-P-7Iem.js → LinkSimple.es-BD4aGTc3.js} +1 -1
  20. package/server/client/dist/assets/Note-FL31jb2U.js +19 -0
  21. package/server/client/dist/assets/{NotePropertiesPanel-6P7UfRBR.js → NotePropertiesPanel-os22Ppnu.js} +1 -1
  22. package/server/client/dist/assets/{Plus.es-Dy6onD-Z.js → Plus.es-Cl8zOOZa.js} +1 -1
  23. package/server/client/dist/assets/Reminders-Dt2JuGci.js +1 -0
  24. package/server/client/dist/assets/{Search-AaD1Mt0d.js → Search-CPPTW2PO.js} +2 -2
  25. package/server/client/dist/assets/{SurfaceCard-D5QxOte_.js → SurfaceCard-jL2qzrzF.js} +1 -1
  26. package/server/client/dist/assets/{Tag-B2Qiq1Hv.js → Tag-3DbGmuxa.js} +1 -1
  27. package/server/client/dist/assets/{TagNotes-B27G1ZVg.js → TagNotes-DLwRF8pT.js} +1 -1
  28. package/server/client/dist/assets/Trash.es-fxukjAO6.js +1 -0
  29. package/server/client/dist/assets/{ViewChip-BVkZAXyx.js → ViewChip-CVe_HNyd.js} +1 -1
  30. package/server/client/dist/assets/Views-8ghLRax7.js +1 -0
  31. package/server/client/dist/assets/{abnfDiagram-N423BO3Z-D44O4T7q.js → abnfDiagram-N423BO3Z-CPgGmCbn.js} +1 -1
  32. package/server/client/dist/assets/app-Bcl0U7r9.css +1 -0
  33. package/server/client/dist/assets/app-DzChh-V0.js +151 -0
  34. package/server/client/dist/assets/{arc-Cil9fXuW.js → arc-DqTgRpuF.js} +1 -1
  35. package/server/client/dist/assets/{architectureDiagram-T3A2C74G-Cjq6ayF2.js → architectureDiagram-T3A2C74G-rTYMAQ-J.js} +1 -1
  36. package/server/client/dist/assets/{blockDiagram-VBNYF7ZC-B2Xm5N0i.js → blockDiagram-VBNYF7ZC-BOluytVz.js} +1 -1
  37. package/server/client/dist/assets/{c4Diagram-5PPSVZJV-Dp0y-lCR.js → c4Diagram-5PPSVZJV-C8gbPpcb.js} +1 -1
  38. package/server/client/dist/assets/{calendar-data-wB0ifXdo.js → calendar-data-BQ24n348.js} +1 -1
  39. package/server/client/dist/assets/channel--aI9fJWX.js +1 -0
  40. package/server/client/dist/assets/{chunk-2GRJ4B5K-CWaaNkFx.js → chunk-2GRJ4B5K-CRER5Ka0.js} +1 -1
  41. package/server/client/dist/assets/{chunk-2Q5K7J3B-Ces-xz2S.js → chunk-2Q5K7J3B-hxmJWGu_.js} +1 -1
  42. package/server/client/dist/assets/{chunk-5RXB4S5H-B-4Chlfc.js → chunk-5RXB4S5H-Bpr5Us8Y.js} +1 -1
  43. package/server/client/dist/assets/{chunk-5VM5RSS4-C8_HVwuz.js → chunk-5VM5RSS4-CKUz9fAb.js} +1 -1
  44. package/server/client/dist/assets/{chunk-6Q2QTUOP-xZ8ofRqk.js → chunk-6Q2QTUOP-B8XEOHPN.js} +1 -1
  45. package/server/client/dist/assets/{chunk-GF5L2VYU-Bpa43a7s.js → chunk-GF5L2VYU-BBUD-HJi.js} +1 -1
  46. package/server/client/dist/assets/{chunk-JWPE2WC7-DsyTXbuq.js → chunk-JWPE2WC7-Dq6_B8-d.js} +1 -1
  47. package/server/client/dist/assets/{chunk-KBJHAD2P-BDhjm9-8.js → chunk-KBJHAD2P--2VZEwNJ.js} +1 -1
  48. package/server/client/dist/assets/{chunk-RYQCIY6F-Cn-jdp6I.js → chunk-RYQCIY6F-CKCwnKiC.js} +1 -1
  49. package/server/client/dist/assets/{chunk-XXDRQBXY-BKsc_0wh.js → chunk-XXDRQBXY-VDufR1Oi.js} +1 -1
  50. package/server/client/dist/assets/classDiagram-JCYQIIEL-BsbwE38V.js +1 -0
  51. package/server/client/dist/assets/classDiagram-v2-OCEON4UE-BsbwE38V.js +1 -0
  52. package/server/client/dist/assets/{cose-bilkent-JH36ORCC-jmn3Q4zj.js → cose-bilkent-JH36ORCC-D4nqOx8S.js} +1 -1
  53. package/server/client/dist/assets/{cynefinDiagram-MW4NZA55-BpLstuBJ.js → cynefinDiagram-MW4NZA55-CUuLMs-6.js} +1 -1
  54. package/server/client/dist/assets/{dagre-VZM6K2ZE-B5ft5Yzm.js → dagre-VZM6K2ZE-CUSkhZgS.js} +1 -1
  55. package/server/client/dist/assets/{diagram-7IWD3JNH-B_W9IsWZ.js → diagram-7IWD3JNH-D95eVcKx.js} +4 -4
  56. package/server/client/dist/assets/{diagram-B4RE2ZJO-32chS3GX.js → diagram-B4RE2ZJO-7OH_fb0K.js} +1 -1
  57. package/server/client/dist/assets/{diagram-LBJQPF4R-Bqupdb3-.js → diagram-LBJQPF4R-D9UwEIMd.js} +1 -1
  58. package/server/client/dist/assets/{diagram-Q27KOJAE-BD8jKBsv.js → diagram-Q27KOJAE-Dx7WFDuX.js} +1 -1
  59. package/server/client/dist/assets/{diagram-UB23O5K3-C40jAU8D.js → diagram-UB23O5K3-D2BYwAKT.js} +1 -1
  60. package/server/client/dist/assets/{ebnfDiagram-BXEA7PRR-BQfW9mFA.js → ebnfDiagram-BXEA7PRR-DbeA-fk4.js} +1 -1
  61. package/server/client/dist/assets/{erDiagram-JOGREHBK-ILZnLzz2.js → erDiagram-JOGREHBK-jWW-ICGO.js} +1 -1
  62. package/server/client/dist/assets/{flowDiagram-UKHOOZJN-CDmwr2dI.js → flowDiagram-UKHOOZJN-68iZdxxw.js} +1 -1
  63. package/server/client/dist/assets/{ganttDiagram-PKOTCBZU-DBcrUc49.js → ganttDiagram-PKOTCBZU-CxwY3VXg.js} +1 -1
  64. package/server/client/dist/assets/{gitGraphDiagram-DS77QQ5N-_xO6__FK.js → gitGraphDiagram-DS77QQ5N-Ci6o5MfZ.js} +1 -1
  65. package/server/client/dist/assets/{image.api-ChvlfuZg.js → image.api-CyIIEGkG.js} +1 -1
  66. package/server/client/dist/assets/index-Iy_BgP1R.js +1 -0
  67. package/server/client/dist/assets/{infoDiagram-6WML65LV-CuIIcu-L.js → infoDiagram-6WML65LV-DY7aMdjV.js} +1 -1
  68. package/server/client/dist/assets/integrations-LOdTs1T6.js +1 -0
  69. package/server/client/dist/assets/{ishikawaDiagram-WSZJBQD7-BVtRGTOj.js → ishikawaDiagram-WSZJBQD7-Bh5qKh1d.js} +1 -1
  70. package/server/client/dist/assets/{journeyDiagram-NVQOT4AX-D5-Nzb2N.js → journeyDiagram-NVQOT4AX-B_svXTTT.js} +1 -1
  71. package/server/client/dist/assets/{kanban-definition-27J2QSJJ-DmOhgC8H.js → kanban-definition-27J2QSJJ-CaJslHGg.js} +1 -1
  72. package/server/client/dist/assets/manage-image-DXedfr7U.js +1 -0
  73. package/server/client/dist/assets/{manage-image-detail-Dw3tAW3h.js → manage-image-detail-DmifOPNU.js} +1 -1
  74. package/server/client/dist/assets/{mcp-admin-adapter-D0R8p9az.js → mcp-admin-adapter-CmKsg1vd.js} +1 -1
  75. package/server/client/dist/assets/mcp-d8IzsoRz.js +5 -0
  76. package/server/client/dist/assets/{mermaid.core-C2tL9tq7.js → mermaid.core-BfUQM5vF.js} +4 -4
  77. package/server/client/dist/assets/{mindmap-definition-FAOFIHXS-DuvsPqrh.js → mindmap-definition-FAOFIHXS-DGbkJda2.js} +1 -1
  78. package/server/client/dist/assets/{note-core-YhYEmI_R.js → note-core-DRBsBf9H.js} +9 -9
  79. package/server/client/dist/assets/{note-runtime-B3ats-dC.js → note-runtime-ztZvXRIu.js} +16 -16
  80. package/server/client/dist/assets/{note-ui-BtzPk8gA.js → note-ui-DRnAnwKv.js} +1 -1
  81. package/server/client/dist/assets/{pegDiagram-VL7TDLO6-D64crEZV.js → pegDiagram-VL7TDLO6-Bip6zQct.js} +1 -1
  82. package/server/client/dist/assets/{pieDiagram-7S7Q4E2Y--kl7vopC.js → pieDiagram-7S7Q4E2Y-CjfjGY8Y.js} +1 -1
  83. package/server/client/dist/assets/{placeholder-B5uPsE7-.js → placeholder-G6iEBbDy.js} +1 -1
  84. package/server/client/dist/assets/{properties-B7Fv95qY.js → properties-BPobx_Qk.js} +1 -1
  85. package/server/client/dist/assets/{quadrantDiagram-CIZ2JOQS-DuvJ4M2y.js → quadrantDiagram-CIZ2JOQS-B6F1yUT-.js} +1 -1
  86. package/server/client/dist/assets/{railroadDiagram-AXF67PYL-BMKmCOY7.js → railroadDiagram-AXF67PYL-D-0fLe0F.js} +1 -1
  87. package/server/client/dist/assets/{requirementDiagram-LRYGKXZP-DY1eJcy0.js → requirementDiagram-LRYGKXZP-tm4mnCyJ.js} +1 -1
  88. package/server/client/dist/assets/route-preload.js +2 -2
  89. package/server/client/dist/assets/{sankeyDiagram-W5VNT64P-C3Ka4GKM.js → sankeyDiagram-W5VNT64P-DSBWFurb.js} +1 -1
  90. package/server/client/dist/assets/{search-ddYl3qH-.js → search-DanMKCC8.js} +1 -1
  91. package/server/client/dist/assets/{search-admin-adapter-Daf7e2YQ.js → search-admin-adapter-7sTcWTmv.js} +1 -1
  92. package/server/client/dist/assets/{sequenceDiagram-SI44F4Z6-Bhpwtk2s.js → sequenceDiagram-SI44F4Z6-KrwjjJIV.js} +1 -1
  93. package/server/client/dist/assets/{sizeCapture-X5ZJPWSS-BFta71TW.js → sizeCapture-X5ZJPWSS-DL2cn_Q6.js} +1 -1
  94. package/server/client/dist/assets/{stateDiagram-OKZ733FA-D6GCn1io.js → stateDiagram-OKZ733FA-CKuNDH85.js} +1 -1
  95. package/server/client/dist/assets/{stateDiagram-v2-UEYNNEHI-g4yinGDn.js → stateDiagram-v2-UEYNNEHI-BFaj2wbM.js} +1 -1
  96. package/server/client/dist/assets/{swimlanes-SLNWSIFB-DLFvOjUl.js → swimlanes-SLNWSIFB-5S17nqhK.js} +2 -2
  97. package/server/client/dist/assets/swimlanesDiagram-ULZ7WXOC-dexsHJq2.js +8 -0
  98. package/server/client/dist/assets/{tag.api-DEUROVKQ.js → tag.api-kKZl04yY.js} +1 -1
  99. package/server/client/dist/assets/{time-PxWila4-.js → time-BwwQGbK-.js} +38 -38
  100. package/server/client/dist/assets/{timeline-definition-Z64GVDOM-DTSIekSM.js → timeline-definition-Z64GVDOM-CU1hp_7d.js} +3 -3
  101. package/server/client/dist/assets/{trash-BHida7BG.js → trash-Cepvi7G8.js} +1 -1
  102. package/server/client/dist/assets/url-Bp8sXMb4.js +1 -0
  103. package/server/client/dist/assets/{useNoteMutate-BjvZUmtX.js → useNoteMutate-D-mZKXjl.js} +3 -3
  104. package/server/client/dist/assets/{useReminderMutate-C5n-5LM-.js → useReminderMutate-Cexk1ydX.js} +1 -1
  105. package/server/client/dist/assets/{vennDiagram-T6HMQDX7-CMNqSI2X.js → vennDiagram-T6HMQDX7-MoiVLnil.js} +1 -1
  106. package/server/client/dist/assets/{wardleyDiagram-T6FBY63Y-DV-JOYqB.js → wardleyDiagram-T6FBY63Y-cZkfdI6C.js} +1 -1
  107. package/server/client/dist/assets/{xychartDiagram-ELKLHX3M-BUIniF4m.js → xychartDiagram-ELKLHX3M-B63hxD-r.js} +1 -1
  108. package/server/client/dist/index.html +6 -6
  109. package/server/dist/app.js +2 -0
  110. package/server/dist/features/integration/auth.js +21 -0
  111. package/server/dist/features/integration/manifest.js +87 -0
  112. package/server/dist/features/integration/schema.js +54 -0
  113. package/server/dist/features/integration/service.js +173 -0
  114. package/server/dist/features/mcp-admin/service.js +19 -83
  115. package/server/dist/features/note/graphql/note.field.resolver.js +1 -1
  116. package/server/dist/features/note/graphql/note.query.resolver.js +6 -0
  117. package/server/dist/features/note/graphql/note.type-defs.js +13 -1
  118. package/server/dist/features/note/http/authoring.js +493 -0
  119. package/server/dist/features/note/http/mcp.js +15 -463
  120. package/server/dist/features/note/services/authoring.js +17 -1
  121. package/server/dist/features/note/services/markdown-intent-write.js +1 -0
  122. package/server/dist/features/note/services/markdown-read.js +85 -0
  123. package/server/dist/features/note/services/properties.js +36 -30
  124. package/server/dist/features/note/services/search.js +17 -0
  125. package/server/dist/features/search/graphql/resolver.js +10 -1
  126. package/server/dist/features/search/graphql/type-defs.js +3 -0
  127. package/server/dist/features/tag/graphql/tag.query.resolver.js +10 -0
  128. package/server/dist/features/tag/graphql/tag.type-defs.js +1 -0
  129. package/server/dist/features/view/graphql/mcp.query.resolver.js +22 -0
  130. package/server/dist/features/view/graphql/view.query.resolver.js +2 -0
  131. package/server/dist/features/view/graphql/view.type-defs.js +27 -0
  132. package/server/dist/features/view/services/mcp-read.js +142 -0
  133. package/server/dist/features/view/services/workspace.js +12 -4
  134. package/server/dist/modules/auth-guard.js +24 -1
  135. package/server/dist/modules/integration-token.js +27 -0
  136. package/server/dist/modules/mcp-auth.js +37 -23
  137. package/server/dist/modules/mcp-token.js +6 -25
  138. package/server/dist/routes/api.js +2 -0
  139. package/server/dist/routes/graphql.js +2 -1
  140. package/server/dist/routes/integrations.js +117 -0
  141. package/server/dist/routes/mcp.js +29 -9
  142. package/server/prisma/migrations/20260917120000_0020_plugin_installations/migration.sql +39 -0
  143. package/server/prisma/migrations/20260917150000_0021_integration_connections/migration.sql +29 -0
  144. package/server/prisma/schema.prisma +34 -10
  145. package/server/client/dist/assets/Note-CpzLtGVM.js +0 -19
  146. package/server/client/dist/assets/Reminders-DnuALBL6.js +0 -1
  147. package/server/client/dist/assets/Trash.es-XEcWG_aq.js +0 -1
  148. package/server/client/dist/assets/Views-B5oh8GVM.js +0 -1
  149. package/server/client/dist/assets/app-EwvT7bMw.css +0 -1
  150. package/server/client/dist/assets/app-NgiBDISG.js +0 -151
  151. package/server/client/dist/assets/channel-DZFalh-h.js +0 -1
  152. package/server/client/dist/assets/classDiagram-JCYQIIEL-CkEhFvVV.js +0 -1
  153. package/server/client/dist/assets/classDiagram-v2-OCEON4UE-CkEhFvVV.js +0 -1
  154. package/server/client/dist/assets/index-CIEap4bu.js +0 -1
  155. package/server/client/dist/assets/manage-image-CSJ3Flgc.js +0 -1
  156. package/server/client/dist/assets/mcp-D8footel.js +0 -5
  157. package/server/client/dist/assets/swimlanesDiagram-ULZ7WXOC-Ci4mcvmo.js +0 -8
  158. package/server/client/dist/assets/url-DKUMNVCI.js +0 -1
@@ -525,9 +525,41 @@ const validateNotePropertiesPatchValues = async (patch, tx = models) => {
525
525
  }
526
526
  return normalizedPatch;
527
527
  };
528
- const listNoteProperties = async (noteId) => {
528
+ const setNotePropertyValues = async (tx, id, patch) => {
529
+ const normalizedPatch = normalizePatch(patch);
530
+ for (const item of normalizedPatch.set) {
531
+ const definition = await tx.propertyDefinition.findUnique({ where: { key: item.key } });
532
+ if (!definition) {
533
+ throw new InvalidNotePropertyInputError(
534
+ `Property ${item.key} is not defined. Create it in property settings first.`
535
+ );
536
+ }
537
+ if (definition.valueType !== item.valueType) {
538
+ throw new InvalidNotePropertyInputError(
539
+ `Property ${item.key} already uses ${definition.valueType} values.`
540
+ );
541
+ }
542
+ const valueData = await buildTypedValueData(item, definition.id, tx);
543
+ await tx.noteProperty.upsert({
544
+ where: {
545
+ noteId_propertyDefinitionId: {
546
+ noteId: id,
547
+ propertyDefinitionId: definition.id
548
+ }
549
+ },
550
+ create: {
551
+ noteId: id,
552
+ propertyDefinitionId: definition.id,
553
+ ...valueData
554
+ },
555
+ update: valueData
556
+ });
557
+ }
558
+ };
559
+ const listNoteProperties = async (noteId, keys) => {
560
+ if (keys?.length === 0) return [];
529
561
  const properties = await models.noteProperty.findMany({
530
- where: { noteId },
562
+ where: { noteId, ...keys ? { definition: { key: { in: keys.map(normalizePropertyKey) } } } : {} },
531
563
  include: { definition: true, option: true },
532
564
  orderBy: [{ definition: { key: "asc" } }]
533
565
  });
@@ -813,34 +845,7 @@ const updateNotePropertiesWithVersionGuardAndSnapshot = async ({
813
845
  optionColor: property.option?.color ?? null
814
846
  }))
815
847
  };
816
- for (const item of normalizedPatch.set) {
817
- const definition = await tx.propertyDefinition.findUnique({ where: { key: item.key } });
818
- if (!definition) {
819
- throw new InvalidNotePropertyInputError(
820
- `Property ${item.key} is not defined. Create it in property settings first.`
821
- );
822
- }
823
- if (definition.valueType !== item.valueType) {
824
- throw new InvalidNotePropertyInputError(
825
- `Property ${item.key} already uses ${definition.valueType} values.`
826
- );
827
- }
828
- const valueData = await buildTypedValueData(item, definition.id, tx);
829
- await tx.noteProperty.upsert({
830
- where: {
831
- noteId_propertyDefinitionId: {
832
- noteId: id,
833
- propertyDefinitionId: definition.id
834
- }
835
- },
836
- create: {
837
- noteId: id,
838
- propertyDefinitionId: definition.id,
839
- ...valueData
840
- },
841
- update: valueData
842
- });
843
- }
848
+ await setNotePropertyValues(tx, id, normalizedPatch);
844
849
  if (normalizedPatch.deleteKeys.length > 0) {
845
850
  const definitions = await tx.propertyDefinition.findMany({
846
851
  where: { key: { in: normalizedPatch.deleteKeys } },
@@ -909,6 +914,7 @@ export {
909
914
  resolveNotePropertiesPatchValueTypes,
910
915
  serializeNoteProperties,
911
916
  serializeNoteProperty,
917
+ setNotePropertyValues,
912
918
  updateNotePropertiesWithVersionGuard,
913
919
  updateNotePropertiesWithVersionGuardAndSnapshot,
914
920
  updateNotePropertyDefinition,
@@ -144,6 +144,22 @@ const NOTE_CONTENT_PREVIEW_MAX_LENGTH = 100;
144
144
  const buildNoteContentPreview = (content) => {
145
145
  return extractVisibleSearchTextFromContent(content).slice(0, NOTE_CONTENT_PREVIEW_MAX_LENGTH);
146
146
  };
147
+ const buildNoteSearchExcerpt = (note, query) => {
148
+ const terms = parseNoteSearchQuery(query).included;
149
+ const text = extractVisibleSearchTextFromContent(note.content);
150
+ for (const [source, value] of [
151
+ ["body", text],
152
+ ["title", note.title]
153
+ ]) {
154
+ const positions = terms.map((term) => new RegExp(term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "iu").exec(value)?.index ?? -1).filter((index) => index >= 0);
155
+ if (!positions.length) continue;
156
+ const matchStart = Math.min(...positions);
157
+ const start = Math.max(0, matchStart - 80);
158
+ const end = Math.min(value.length, Math.max(start + 240, matchStart + 80));
159
+ return { text: value.slice(start, end), source, start, end };
160
+ }
161
+ return null;
162
+ };
147
163
  const matchesNoteSearchQuery = (note, query) => {
148
164
  const parsedQuery = typeof query === "string" ? parseNoteSearchQuery(query) : query;
149
165
  if (!parsedQuery.hasFilters) {
@@ -171,6 +187,7 @@ export {
171
187
  NOTE_SEARCH_PASS_THROUGH_NODE_TYPES,
172
188
  NOTE_SEARCH_TEXT_SCHEMA_VERSION,
173
189
  buildNoteContentPreview,
190
+ buildNoteSearchExcerpt,
174
191
  buildNoteSearchProjection,
175
192
  buildNoteSearchText,
176
193
  extractVisibleSearchTextFromContent,
@@ -1,13 +1,22 @@
1
+ import { buildNoteSearchExcerpt } from "../../../features/note/services/search.js";
1
2
  import { searchNotesHybrid } from "../hybrid-search.js";
2
3
  import { searchRelatedNotes } from "../related-notes.js";
3
4
  const createSearchNotesResolver = (search = searchNotesHybrid) => {
4
5
  return async (_, { query, pagination, mode = "HYBRID" }) => {
5
- return search({
6
+ const result = await search({
6
7
  query,
7
8
  limit: Math.min(50, Math.max(0, pagination.limit)),
8
9
  offset: Math.max(0, pagination.offset),
9
10
  mode: mode.toLowerCase()
10
11
  });
12
+ const notes = new Map(result.notes.map((note) => [note.id, note]));
13
+ return {
14
+ ...result,
15
+ matches: result.matches.map((match) => {
16
+ const note = notes.get(match.noteId);
17
+ return { ...match, excerpt: match.lexical && note ? buildNoteSearchExcerpt(note, query) : null };
18
+ })
19
+ };
11
20
  };
12
21
  };
13
22
  const createSearchRelatedNotesResolver = (findRelatedNotes = searchRelatedNotes) => {
@@ -10,8 +10,11 @@ const searchTypeDefs = gql`
10
10
  noteId: ID!
11
11
  lexical: Boolean!
12
12
  semantic: Boolean!
13
+ excerpt: SearchNoteExcerpt
13
14
  }
14
15
 
16
+ type SearchNoteExcerpt { text: String!, source: String!, start: Int!, end: Int! }
17
+
15
18
  type SearchRelatedNote {
16
19
  id: ID!
17
20
  title: String!
@@ -1,4 +1,5 @@
1
1
  import models from "../../../models.js";
2
+ import { normalizeTagName } from "../services/organization.js";
2
3
  const TAG_SORT_FIELDS = /* @__PURE__ */ new Set(["referenceCount", "name"]);
3
4
  const normalizeSortBy = (sortBy) => {
4
5
  return TAG_SORT_FIELDS.has(sortBy) ? sortBy : "referenceCount";
@@ -13,6 +14,15 @@ const getTagOrderBy = (sortBy, sortOrder) => {
13
14
  return { [sortBy]: sortOrder };
14
15
  };
15
16
  const tagQueryResolvers = {
17
+ tagsByNames: async (_, { names }) => {
18
+ if (names.length > 100) {
19
+ throw new Error("At most 100 tag names may be resolved at once.");
20
+ }
21
+ return models.tag.findMany({
22
+ where: { name: { in: [...new Set(names.map(normalizeTagName))] } },
23
+ orderBy: { name: "asc" }
24
+ });
25
+ },
16
26
  allTags: async (_, {
17
27
  searchFilter,
18
28
  pagination
@@ -27,6 +27,7 @@ const tagType = gql`
27
27
  const tagQuery = gql`
28
28
  type Query {
29
29
  allTags(searchFilter: SearchFilterInput, pagination: PaginationInput): Tags!
30
+ tagsByNames(names: [String!]!): [Tag!]!
30
31
  }
31
32
  `;
32
33
  const tagMutation = gql`
@@ -0,0 +1,22 @@
1
+ import { GraphQLError } from "graphql";
2
+ import { InvalidNotePropertyInputError } from "../../../features/note/services/properties.js";
3
+ import { listViewSections, readViewSection } from "../services/mcp-read.js";
4
+ import { getNotesByQuery } from "../services/workspace.js";
5
+ const queryResult = async (read) => {
6
+ try {
7
+ return await read();
8
+ } catch (error) {
9
+ if (error instanceof InvalidNotePropertyInputError) {
10
+ throw new GraphQLError(error.message, { extensions: { code: "INVALID_NOTE_PROPERTY_INPUT" } });
11
+ }
12
+ throw error;
13
+ }
14
+ };
15
+ const mcpViewQueryResolvers = {
16
+ notesByQuery: (_, { input, pagination }) => queryResult(() => getNotesByQuery(input, pagination)),
17
+ viewSections: (_, { query, pagination }) => queryResult(() => listViewSections(query, pagination)),
18
+ readViewSection: (_, input) => queryResult(() => readViewSection(input))
19
+ };
20
+ export {
21
+ mcpViewQueryResolvers
22
+ };
@@ -8,6 +8,7 @@ import {
8
8
  getViewSectionNotes,
9
9
  getViewWorkspace
10
10
  } from "../../../features/view/services/workspace.js";
11
+ import { mcpViewQueryResolvers } from "./mcp.query.resolver.js";
11
12
  const createViewQueryResolvers = (deps = {
12
13
  getNotesByProperties,
13
14
  getViewSectionBoardColumn,
@@ -16,6 +17,7 @@ const createViewQueryResolvers = (deps = {
16
17
  getViewSectionNotes,
17
18
  getViewWorkspace
18
19
  }) => ({
20
+ ...mcpViewQueryResolvers,
19
21
  viewWorkspace: async () => {
20
22
  return deps.getViewWorkspace();
21
23
  },
@@ -1,5 +1,29 @@
1
1
  import { gql } from "../../../modules/graphql.js";
2
2
  const viewType = gql`
3
+ type NotesQueryConditions {
4
+ tagNames: [String!]!
5
+ mode: TagMatchMode!
6
+ propertyFilters: [ViewPropertyFilter!]!
7
+ sortBy: ViewSortBy!
8
+ sortOrder: ViewSortOrder!
9
+ }
10
+
11
+ type NotesQueryResult {
12
+ query: NotesQueryConditions!
13
+ totalCount: Int!
14
+ notes: [Note!]!
15
+ }
16
+
17
+ type ViewSectionListing { section: ViewSection!, tabTitle: String! }
18
+ type ViewSectionListings { totalCount: Int!, sections: [ViewSectionListing!]! }
19
+ type ViewReadRow { note: Note!, groupValue: String, calendarDate: String }
20
+ type ViewGroupProperty { key: String!, name: String!, options: [NotePropertyOption!]! }
21
+ type ViewReadResult {
22
+ section: ViewSection!
23
+ totalCount: Int!
24
+ rows: [ViewReadRow!]!
25
+ groupProperty: ViewGroupProperty
26
+ }
3
27
  type ViewWorkspace {
4
28
  activeTabId: ID
5
29
  tabs: [ViewTab!]!
@@ -140,6 +164,9 @@ const viewType = gql`
140
164
  `;
141
165
  const viewQuery = gql`
142
166
  type Query {
167
+ notesByQuery(input: NotesByPropertiesInput!, pagination: PaginationInput): NotesQueryResult!
168
+ viewSections(query: String, pagination: PaginationInput): ViewSectionListings!
169
+ readViewSection(id: ID!, pagination: PaginationInput, propertyKeys: [String!], groupValue: String, dateRange: DateRangeInput): ViewReadResult!
143
170
  viewWorkspace: ViewWorkspace!
144
171
  viewSection(id: ID!): ViewSection
145
172
  viewSectionNotes(
@@ -0,0 +1,142 @@
1
+ import { InvalidNotePropertyInputError } from "../../../features/note/services/properties.js";
2
+ import models from "../../../models.js";
3
+ import {
4
+ buildViewBoardColumnWhere,
5
+ buildViewSectionCalendarWhere,
6
+ buildViewSectionWhere,
7
+ getViewSectionById,
8
+ normalizeViewCalendarDateRange,
9
+ normalizeViewNotesPagination,
10
+ serializeViewSection
11
+ } from "./workspace.js";
12
+ const listViewSections = async (query = "", pagination) => {
13
+ const { limit, offset } = normalizeViewNotesPagination(pagination);
14
+ const where = query.trim() ? { OR: [{ title: { contains: query.trim() } }, { tab: { title: { contains: query.trim() } } }] } : {};
15
+ const [totalCount, sections] = await Promise.all([
16
+ models.viewSection.count({ where }),
17
+ models.viewSection.findMany({
18
+ where,
19
+ include: { tab: true, tags: { orderBy: { name: "asc" } } },
20
+ orderBy: [{ tab: { order: "asc" } }, { tabId: "asc" }, { order: "asc" }, { id: "asc" }],
21
+ take: limit,
22
+ skip: offset
23
+ })
24
+ ]);
25
+ return {
26
+ totalCount,
27
+ sections: sections.map((section) => ({ section: serializeViewSection(section), tabTitle: section.tab.title }))
28
+ };
29
+ };
30
+ const readViewSection = async ({
31
+ id,
32
+ pagination,
33
+ groupValue,
34
+ dateRange,
35
+ propertyKeys
36
+ }) => {
37
+ const section = await getViewSectionById(id);
38
+ if (!section) throw new InvalidNotePropertyInputError("View section not found. Use list_views to find its ID.");
39
+ if (groupValue !== void 0 && section.displayType !== "board") {
40
+ throw new InvalidNotePropertyInputError("groupValue is only valid for a board.");
41
+ }
42
+ if (dateRange && section.displayType !== "calendar") {
43
+ throw new InvalidNotePropertyInputError("dateRange is only valid for a calendar.");
44
+ }
45
+ const { limit, offset } = normalizeViewNotesPagination(pagination);
46
+ const selectedPropertyKeys = propertyKeys ?? (section.displayType === "table" ? section.displayOptions.tablePropertyKeys : []);
47
+ const withPropertyKeys = (note) => ({ ...note, selectedPropertyKeys });
48
+ if (section.displayType === "calendar") {
49
+ if (!dateRange)
50
+ throw new InvalidNotePropertyInputError(
51
+ "Calendar views require dateRange (start inclusive, end exclusive; at most 32 days)."
52
+ );
53
+ const range = normalizeViewCalendarDateRange(dateRange);
54
+ const field = section.displayOptions.calendarDateField;
55
+ if (field === "property") {
56
+ const key2 = section.displayOptions.calendarDatePropertyKey;
57
+ const definition = key2 ? await models.propertyDefinition.findUnique({ where: { key: key2 } }) : null;
58
+ if (!definition || definition.valueType !== "date") {
59
+ throw new InvalidNotePropertyInputError("The calendar date property is unavailable.");
60
+ }
61
+ const where3 = {
62
+ propertyDefinitionId: definition.id,
63
+ dateValue: { gte: range.start, lt: range.end },
64
+ note: buildViewSectionWhere(section)
65
+ };
66
+ const [totalCount3, values] = await Promise.all([
67
+ models.noteProperty.count({ where: where3 }),
68
+ models.noteProperty.findMany({
69
+ where: where3,
70
+ include: { note: true },
71
+ orderBy: [{ dateValue: "asc" }, { noteId: "asc" }],
72
+ take: limit,
73
+ skip: offset
74
+ })
75
+ ]);
76
+ return {
77
+ section,
78
+ totalCount: totalCount3,
79
+ rows: values.map((value) => ({
80
+ note: withPropertyKeys(value.note),
81
+ calendarDate: value.dateValue?.toISOString().slice(0, 10)
82
+ })),
83
+ groupProperty: null
84
+ };
85
+ }
86
+ const where2 = buildViewSectionCalendarWhere(section, range);
87
+ const [totalCount2, notes2] = await Promise.all([
88
+ models.note.count({ where: where2 }),
89
+ models.note.findMany({ where: where2, orderBy: [{ [field]: "asc" }, { id: "asc" }], take: limit, skip: offset })
90
+ ]);
91
+ return {
92
+ section,
93
+ totalCount: totalCount2,
94
+ rows: notes2.map((note) => ({ note: withPropertyKeys(note), calendarDate: note[field].toISOString() })),
95
+ groupProperty: null
96
+ };
97
+ }
98
+ const key = section.displayOptions.boardGroupByPropertyKey;
99
+ const groupProperty = section.displayType === "board" && key ? await models.propertyDefinition.findUnique({
100
+ where: { key },
101
+ include: { options: { orderBy: [{ order: "asc" }, { id: "asc" }] } }
102
+ }) : null;
103
+ let where = buildViewSectionWhere(section);
104
+ if (section.displayType === "board") {
105
+ if (!groupProperty || groupProperty.valueType !== "select") {
106
+ throw new InvalidNotePropertyInputError("The board grouping property is unavailable.");
107
+ }
108
+ if (groupValue !== void 0) {
109
+ const option = groupValue === null ? null : groupProperty.options.find(
110
+ (candidate) => candidate.value === groupValue.trim().toLowerCase().replace(/\s+/g, "-")
111
+ );
112
+ if (groupValue !== null && !option)
113
+ throw new InvalidNotePropertyInputError(`Property ${key} option ${groupValue} is not defined.`);
114
+ where = buildViewBoardColumnWhere(section, groupProperty.id, option?.id ?? null);
115
+ }
116
+ }
117
+ const [totalCount, notes] = await Promise.all([
118
+ models.note.count({ where }),
119
+ models.note.findMany({
120
+ where,
121
+ orderBy: [{ [section.sortBy]: section.sortOrder }, { id: "asc" }],
122
+ take: limit,
123
+ skip: offset,
124
+ include: {
125
+ properties: { where: { propertyDefinitionId: groupProperty?.id ?? -1 }, include: { option: true } }
126
+ }
127
+ })
128
+ ]);
129
+ return {
130
+ section,
131
+ totalCount,
132
+ rows: notes.map((note) => ({
133
+ note: withPropertyKeys(note),
134
+ groupValue: note.properties[0]?.option?.value ?? null
135
+ })),
136
+ groupProperty
137
+ };
138
+ };
139
+ export {
140
+ listViewSections,
141
+ readViewSection
142
+ };
@@ -762,11 +762,8 @@ const getViewSectionBoardColumn = async (id, optionValue, pagination) => {
762
762
  ]);
763
763
  return { totalCount, notes };
764
764
  };
765
- const getNotesByPropertiesWithDb = async (db, input, pagination) => {
765
+ const getNotesByQueryWithDb = async (db, input, pagination) => {
766
766
  const normalizedQuery = normalizeViewNotesQueryInput(input);
767
- if (normalizedQuery.propertyFilters.length === 0) {
768
- throw new InvalidNotePropertyInputError("At least one property filter is required.");
769
- }
770
767
  const query = {
771
768
  ...normalizedQuery,
772
769
  propertyFilters: await hydratePropertyFilters(db, normalizedQuery.propertyFilters, {
@@ -785,10 +782,18 @@ const getNotesByPropertiesWithDb = async (db, input, pagination) => {
785
782
  })
786
783
  ]);
787
784
  return {
785
+ query,
788
786
  totalCount,
789
787
  notes
790
788
  };
791
789
  };
790
+ const getNotesByPropertiesWithDb = async (db, input, pagination) => {
791
+ if (!input.propertyFilters?.length) {
792
+ throw new InvalidNotePropertyInputError("At least one property filter is required.");
793
+ }
794
+ return getNotesByQueryWithDb(db, input, pagination);
795
+ };
796
+ const getNotesByQuery = (input, pagination) => getNotesByQueryWithDb(models, input, pagination);
792
797
  const getNotesByProperties = async (input, pagination) => {
793
798
  return getNotesByPropertiesWithDb(models, input, pagination);
794
799
  };
@@ -1008,6 +1013,8 @@ export {
1008
1013
  deleteViewTab,
1009
1014
  getNotesByProperties,
1010
1015
  getNotesByPropertiesWithDb,
1016
+ getNotesByQuery,
1017
+ getNotesByQueryWithDb,
1011
1018
  getViewSectionBoardColumn,
1012
1019
  getViewSectionById,
1013
1020
  getViewSectionCalendarNotes,
@@ -1028,6 +1035,7 @@ export {
1028
1035
  pickNextActiveViewTabId,
1029
1036
  reorderViewSections,
1030
1037
  reorderViewTabs,
1038
+ serializeViewSection,
1031
1039
  setActiveViewTab,
1032
1040
  updateViewSection,
1033
1041
  updateViewTab
@@ -19,7 +19,30 @@ const issueCsrfToken = (authConfig, reply) => {
19
19
  };
20
20
  const createCsrfProtection = (authConfig) => {
21
21
  if (authConfig.mode !== "password") {
22
- return (_request, _reply, done) => done();
22
+ return (request, reply, done) => {
23
+ if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS") {
24
+ done();
25
+ return;
26
+ }
27
+ const origin = request.headers.origin;
28
+ if (origin) {
29
+ let sameHost = false;
30
+ try {
31
+ const url = new URL(origin);
32
+ sameHost = ["http:", "https:"].includes(url.protocol) && url.host === request.headers.host;
33
+ } catch {
34
+ sameHost = false;
35
+ }
36
+ if (!sameHost) {
37
+ void reply.status(403).send({
38
+ code: "FORBIDDEN_ORIGIN",
39
+ message: "Owner writes require the Ocean Brain page origin."
40
+ });
41
+ return;
42
+ }
43
+ }
44
+ done();
45
+ };
23
46
  }
24
47
  return (request, reply, done) => {
25
48
  if (request.method === "GET" || request.method === "HEAD" || request.method === "OPTIONS") {
@@ -0,0 +1,27 @@
1
+ import crypto from "crypto";
2
+ const TOKEN_BYTE_LENGTH = 32;
3
+ const HASH_ALGORITHM = "sha256";
4
+ const toTokenHash = (token) => {
5
+ return crypto.createHash(HASH_ALGORITHM).update(token, "utf8").digest("hex");
6
+ };
7
+ const issueIntegrationToken = () => {
8
+ const plaintext = crypto.randomBytes(TOKEN_BYTE_LENGTH).toString("base64url");
9
+ const hash = toTokenHash(plaintext);
10
+ return {
11
+ plaintext,
12
+ hash
13
+ };
14
+ };
15
+ const verifyIntegrationToken = (storedHash, presentedToken) => {
16
+ const presentedHash = toTokenHash(presentedToken);
17
+ const storedBuffer = Buffer.from(storedHash, "utf8");
18
+ const presentedBuffer = Buffer.from(presentedHash, "utf8");
19
+ if (storedBuffer.length !== presentedBuffer.length) {
20
+ return false;
21
+ }
22
+ return crypto.timingSafeEqual(storedBuffer, presentedBuffer);
23
+ };
24
+ export {
25
+ issueIntegrationToken,
26
+ verifyIntegrationToken
27
+ };
@@ -43,7 +43,7 @@ const readBearerToken = (authorizationHeader) => {
43
43
  }
44
44
  return authorizationHeader.slice("Bearer ".length).trim() || void 0;
45
45
  };
46
- const createMcpAuthMiddleware = (_authConfig, mcpAdminAuth) => {
46
+ const createMcpAuthMiddleware = (_authConfig, mcpAdminAuth, permission = "notes:read") => {
47
47
  return async (req, reply) => {
48
48
  const status = await mcpAdminAuth.getStatus();
49
49
  if (!status.enabled) {
@@ -76,30 +76,20 @@ const createMcpAuthMiddleware = (_authConfig, mcpAdminAuth) => {
76
76
  });
77
77
  return;
78
78
  }
79
- const versionInfo = getOceanBrainVersionInfo();
80
- const mcpCompatibilityVersion = readMcpCompatibilityVersion(req);
81
- const mcpClientVersion = readMcpClientVersion(req);
82
- if (!mcpCompatibilityVersion || !isMcpVersionCompatible(versionInfo.mcp.compatibilityVersion, mcpCompatibilityVersion)) {
83
- await reply.status(426).headers(JSON_HEADERS).send({
84
- code: "MCP_VERSION_INCOMPATIBLE",
85
- message: createMcpVersionCompatibilityMessage({
86
- serverVersion: versionInfo.version,
87
- requiredMcpCompatibilityVersion: versionInfo.mcp.compatibilityRequirement,
88
- mcpCompatibilityVersion,
89
- mcpClientVersion
90
- }),
91
- serverVersion: versionInfo.version,
92
- mcpVersion: mcpClientVersion ?? mcpCompatibilityVersion ?? null,
93
- mcpClientVersion: mcpClientVersion ?? null,
94
- mcpCompatibilityVersion: mcpCompatibilityVersion ?? null,
95
- requiredMcpVersion: versionInfo.mcpVersionRequirement,
96
- requiredMcpCompatibilityVersion: versionInfo.mcp.compatibilityRequirement,
97
- releaseUrl: versionInfo.releaseUrl
98
- });
79
+ if (!validation.permissions.includes(permission)) {
80
+ await reply.status(403).send({ code: "INTEGRATION_PERMISSION_DENIED", message: `Permission required: ${permission}` });
81
+ return;
99
82
  }
83
+ req.integration = {
84
+ connectionId: "mcp",
85
+ integrationId: "ocean-brain.mcp",
86
+ native: true,
87
+ permissions: validation.permissions
88
+ };
89
+ await enforceMcpCompatibility(req, reply);
100
90
  };
101
91
  };
102
- const createReadOnlyMcpValidationRule = () => {
92
+ const createReadOnlyMcpValidationRule = (message = "MCP endpoint is read-only") => {
103
93
  return (context) => {
104
94
  return {
105
95
  OperationDefinition(node) {
@@ -107,7 +97,7 @@ const createReadOnlyMcpValidationRule = () => {
107
97
  return;
108
98
  }
109
99
  context.reportError(
110
- new GraphQLError("MCP endpoint is read-only", {
100
+ new GraphQLError(message, {
111
101
  nodes: [node],
112
102
  extensions: { code: "FORBIDDEN" }
113
103
  })
@@ -116,9 +106,33 @@ const createReadOnlyMcpValidationRule = () => {
116
106
  };
117
107
  };
118
108
  };
109
+ const enforceMcpCompatibility = async (req, reply) => {
110
+ const versionInfo = getOceanBrainVersionInfo();
111
+ const mcpCompatibilityVersion = readMcpCompatibilityVersion(req);
112
+ const mcpClientVersion = readMcpClientVersion(req);
113
+ if (!mcpCompatibilityVersion || !isMcpVersionCompatible(versionInfo.mcp.compatibilityVersion, mcpCompatibilityVersion)) {
114
+ await reply.status(426).headers(JSON_HEADERS).send({
115
+ code: "MCP_VERSION_INCOMPATIBLE",
116
+ message: createMcpVersionCompatibilityMessage({
117
+ serverVersion: versionInfo.version,
118
+ requiredMcpCompatibilityVersion: versionInfo.mcp.compatibilityRequirement,
119
+ mcpCompatibilityVersion,
120
+ mcpClientVersion
121
+ }),
122
+ serverVersion: versionInfo.version,
123
+ mcpVersion: mcpClientVersion ?? mcpCompatibilityVersion ?? null,
124
+ mcpClientVersion: mcpClientVersion ?? null,
125
+ mcpCompatibilityVersion: mcpCompatibilityVersion ?? null,
126
+ requiredMcpVersion: versionInfo.mcpVersionRequirement,
127
+ requiredMcpCompatibilityVersion: versionInfo.mcp.compatibilityRequirement,
128
+ releaseUrl: versionInfo.releaseUrl
129
+ });
130
+ }
131
+ };
119
132
  export {
120
133
  OCEAN_BRAIN_MCP_VERSION_HEADER,
121
134
  createMcpAuthMiddleware,
122
135
  createReadOnlyMcpValidationRule,
136
+ enforceMcpCompatibility,
123
137
  isMcpVersionCompatible
124
138
  };
@@ -1,27 +1,8 @@
1
- import crypto from "crypto";
2
- const TOKEN_BYTE_LENGTH = 32;
3
- const HASH_ALGORITHM = "sha256";
4
- const toTokenHash = (token) => {
5
- return crypto.createHash(HASH_ALGORITHM).update(token, "utf8").digest("hex");
6
- };
7
- const issueMcpToken = () => {
8
- const plaintext = crypto.randomBytes(TOKEN_BYTE_LENGTH).toString("base64url");
9
- const hash = toTokenHash(plaintext);
10
- return {
11
- plaintext,
12
- hash
13
- };
14
- };
15
- const verifyMcpToken = (storedHash, presentedToken) => {
16
- const presentedHash = toTokenHash(presentedToken);
17
- const storedBuffer = Buffer.from(storedHash, "utf8");
18
- const presentedBuffer = Buffer.from(presentedHash, "utf8");
19
- if (storedBuffer.length !== presentedBuffer.length) {
20
- return false;
21
- }
22
- return crypto.timingSafeEqual(storedBuffer, presentedBuffer);
23
- };
1
+ import {
2
+ issueIntegrationToken,
3
+ verifyIntegrationToken
4
+ } from "./integration-token.js";
24
5
  export {
25
- issueMcpToken,
26
- verifyMcpToken
6
+ issueIntegrationToken as issueMcpToken,
7
+ verifyIntegrationToken as verifyMcpToken
27
8
  };
@@ -16,6 +16,7 @@ import {
16
16
  import { createCsrfProtection, requireSessionForWrite } from "../modules/auth-guard.js";
17
17
  import { createAuthAttemptRateLimit, createSessionAccessRateLimit } from "../modules/rate-limit.js";
18
18
  import { createServerEventsHandler } from "../modules/server-events-handler.js";
19
+ import { createIntegrationRouter } from "./integrations.js";
19
20
  import { createMcpRouter } from "./mcp.js";
20
21
  const createApiRouter = (authConfig, mcpAdminService) => {
21
22
  return async (app) => {
@@ -23,6 +24,7 @@ const createApiRouter = (authConfig, mcpAdminService) => {
23
24
  const requireSession = requireSessionForWrite(authConfig);
24
25
  const sessionAccessRateLimit = app.rateLimit(createSessionAccessRateLimit());
25
26
  app.register(createMcpRouter(authConfig, mcpAdminService), { prefix: "/mcp" });
27
+ app.register(createIntegrationRouter(authConfig));
26
28
  app.get("/auth/session", createSessionStatusHandler(authConfig));
27
29
  app.post(
28
30
  "/auth/login",