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.
- package/README.md +66 -2
- package/dist/mcp.js +439 -480
- package/package.json +2 -2
- package/server/client/dist/assets/{ArrowRight.es-BlNsZQFs.js → ArrowRight.es-nBJws3ws.js} +1 -1
- package/server/client/dist/assets/{AuxiliaryPanelHeader-HAttwVby.js → AuxiliaryPanelHeader-BN_2axQu.js} +1 -1
- package/server/client/dist/assets/{Calendar-DwQBllfw.js → Calendar-vA4Xa4Qr.js} +1 -1
- package/server/client/dist/assets/{Callout-CcYyvHQP.js → Callout-Bo2P960P.js} +1 -1
- package/server/client/dist/assets/{CaretUp.es-C8iY5OlB.js → CaretUp.es-DDbqKn78.js} +1 -1
- package/server/client/dist/assets/{CheckCircle.es-Dqr3WImn.js → CheckCircle.es-CyY9aLBF.js} +1 -1
- package/server/client/dist/assets/{Copy.es-fMv0ETir.js → Copy.es-BwX4tEoU.js} +1 -1
- package/server/client/dist/assets/{File.es-CHtXXafR.js → File.es-DeTgvUnI.js} +1 -1
- package/server/client/dist/assets/{FileText.es-DPIwDCEe.js → FileText.es-C-bpw8dQ.js} +1 -1
- package/server/client/dist/assets/{FlowArrow.es-Dxee98ir.js → FlowArrow.es-CKcMBOmX.js} +1 -1
- package/server/client/dist/assets/{Graph-hVj6vAYJ.js → Graph-BNoSRJP3.js} +1 -1
- package/server/client/dist/assets/{Graph-CdEu8eAn.js → Graph-DYYxNAWn.js} +1 -1
- package/server/client/dist/assets/{Image.es-CjXY1JVf.js → Image.es-d4jWnijM.js} +1 -1
- package/server/client/dist/assets/{Info.es-CYxg-Vku.js → Info.es-DVzN2joA.js} +1 -1
- package/server/client/dist/assets/Integration-gDDtIJYJ.js +1 -0
- package/server/client/dist/assets/{LinkSimple.es-B-P-7Iem.js → LinkSimple.es-BD4aGTc3.js} +1 -1
- package/server/client/dist/assets/Note-FL31jb2U.js +19 -0
- package/server/client/dist/assets/{NotePropertiesPanel-6P7UfRBR.js → NotePropertiesPanel-os22Ppnu.js} +1 -1
- package/server/client/dist/assets/{Plus.es-Dy6onD-Z.js → Plus.es-Cl8zOOZa.js} +1 -1
- package/server/client/dist/assets/Reminders-Dt2JuGci.js +1 -0
- package/server/client/dist/assets/{Search-AaD1Mt0d.js → Search-CPPTW2PO.js} +2 -2
- package/server/client/dist/assets/{SurfaceCard-D5QxOte_.js → SurfaceCard-jL2qzrzF.js} +1 -1
- package/server/client/dist/assets/{Tag-B2Qiq1Hv.js → Tag-3DbGmuxa.js} +1 -1
- package/server/client/dist/assets/{TagNotes-B27G1ZVg.js → TagNotes-DLwRF8pT.js} +1 -1
- package/server/client/dist/assets/Trash.es-fxukjAO6.js +1 -0
- package/server/client/dist/assets/{ViewChip-BVkZAXyx.js → ViewChip-CVe_HNyd.js} +1 -1
- package/server/client/dist/assets/Views-8ghLRax7.js +1 -0
- package/server/client/dist/assets/{abnfDiagram-N423BO3Z-D44O4T7q.js → abnfDiagram-N423BO3Z-CPgGmCbn.js} +1 -1
- package/server/client/dist/assets/app-Bcl0U7r9.css +1 -0
- package/server/client/dist/assets/app-DzChh-V0.js +151 -0
- package/server/client/dist/assets/{arc-Cil9fXuW.js → arc-DqTgRpuF.js} +1 -1
- package/server/client/dist/assets/{architectureDiagram-T3A2C74G-Cjq6ayF2.js → architectureDiagram-T3A2C74G-rTYMAQ-J.js} +1 -1
- package/server/client/dist/assets/{blockDiagram-VBNYF7ZC-B2Xm5N0i.js → blockDiagram-VBNYF7ZC-BOluytVz.js} +1 -1
- package/server/client/dist/assets/{c4Diagram-5PPSVZJV-Dp0y-lCR.js → c4Diagram-5PPSVZJV-C8gbPpcb.js} +1 -1
- package/server/client/dist/assets/{calendar-data-wB0ifXdo.js → calendar-data-BQ24n348.js} +1 -1
- package/server/client/dist/assets/channel--aI9fJWX.js +1 -0
- package/server/client/dist/assets/{chunk-2GRJ4B5K-CWaaNkFx.js → chunk-2GRJ4B5K-CRER5Ka0.js} +1 -1
- package/server/client/dist/assets/{chunk-2Q5K7J3B-Ces-xz2S.js → chunk-2Q5K7J3B-hxmJWGu_.js} +1 -1
- package/server/client/dist/assets/{chunk-5RXB4S5H-B-4Chlfc.js → chunk-5RXB4S5H-Bpr5Us8Y.js} +1 -1
- package/server/client/dist/assets/{chunk-5VM5RSS4-C8_HVwuz.js → chunk-5VM5RSS4-CKUz9fAb.js} +1 -1
- package/server/client/dist/assets/{chunk-6Q2QTUOP-xZ8ofRqk.js → chunk-6Q2QTUOP-B8XEOHPN.js} +1 -1
- package/server/client/dist/assets/{chunk-GF5L2VYU-Bpa43a7s.js → chunk-GF5L2VYU-BBUD-HJi.js} +1 -1
- package/server/client/dist/assets/{chunk-JWPE2WC7-DsyTXbuq.js → chunk-JWPE2WC7-Dq6_B8-d.js} +1 -1
- package/server/client/dist/assets/{chunk-KBJHAD2P-BDhjm9-8.js → chunk-KBJHAD2P--2VZEwNJ.js} +1 -1
- package/server/client/dist/assets/{chunk-RYQCIY6F-Cn-jdp6I.js → chunk-RYQCIY6F-CKCwnKiC.js} +1 -1
- package/server/client/dist/assets/{chunk-XXDRQBXY-BKsc_0wh.js → chunk-XXDRQBXY-VDufR1Oi.js} +1 -1
- package/server/client/dist/assets/classDiagram-JCYQIIEL-BsbwE38V.js +1 -0
- package/server/client/dist/assets/classDiagram-v2-OCEON4UE-BsbwE38V.js +1 -0
- package/server/client/dist/assets/{cose-bilkent-JH36ORCC-jmn3Q4zj.js → cose-bilkent-JH36ORCC-D4nqOx8S.js} +1 -1
- package/server/client/dist/assets/{cynefinDiagram-MW4NZA55-BpLstuBJ.js → cynefinDiagram-MW4NZA55-CUuLMs-6.js} +1 -1
- package/server/client/dist/assets/{dagre-VZM6K2ZE-B5ft5Yzm.js → dagre-VZM6K2ZE-CUSkhZgS.js} +1 -1
- package/server/client/dist/assets/{diagram-7IWD3JNH-B_W9IsWZ.js → diagram-7IWD3JNH-D95eVcKx.js} +4 -4
- package/server/client/dist/assets/{diagram-B4RE2ZJO-32chS3GX.js → diagram-B4RE2ZJO-7OH_fb0K.js} +1 -1
- package/server/client/dist/assets/{diagram-LBJQPF4R-Bqupdb3-.js → diagram-LBJQPF4R-D9UwEIMd.js} +1 -1
- package/server/client/dist/assets/{diagram-Q27KOJAE-BD8jKBsv.js → diagram-Q27KOJAE-Dx7WFDuX.js} +1 -1
- package/server/client/dist/assets/{diagram-UB23O5K3-C40jAU8D.js → diagram-UB23O5K3-D2BYwAKT.js} +1 -1
- package/server/client/dist/assets/{ebnfDiagram-BXEA7PRR-BQfW9mFA.js → ebnfDiagram-BXEA7PRR-DbeA-fk4.js} +1 -1
- package/server/client/dist/assets/{erDiagram-JOGREHBK-ILZnLzz2.js → erDiagram-JOGREHBK-jWW-ICGO.js} +1 -1
- package/server/client/dist/assets/{flowDiagram-UKHOOZJN-CDmwr2dI.js → flowDiagram-UKHOOZJN-68iZdxxw.js} +1 -1
- package/server/client/dist/assets/{ganttDiagram-PKOTCBZU-DBcrUc49.js → ganttDiagram-PKOTCBZU-CxwY3VXg.js} +1 -1
- package/server/client/dist/assets/{gitGraphDiagram-DS77QQ5N-_xO6__FK.js → gitGraphDiagram-DS77QQ5N-Ci6o5MfZ.js} +1 -1
- package/server/client/dist/assets/{image.api-ChvlfuZg.js → image.api-CyIIEGkG.js} +1 -1
- package/server/client/dist/assets/index-Iy_BgP1R.js +1 -0
- package/server/client/dist/assets/{infoDiagram-6WML65LV-CuIIcu-L.js → infoDiagram-6WML65LV-DY7aMdjV.js} +1 -1
- package/server/client/dist/assets/integrations-LOdTs1T6.js +1 -0
- package/server/client/dist/assets/{ishikawaDiagram-WSZJBQD7-BVtRGTOj.js → ishikawaDiagram-WSZJBQD7-Bh5qKh1d.js} +1 -1
- package/server/client/dist/assets/{journeyDiagram-NVQOT4AX-D5-Nzb2N.js → journeyDiagram-NVQOT4AX-B_svXTTT.js} +1 -1
- package/server/client/dist/assets/{kanban-definition-27J2QSJJ-DmOhgC8H.js → kanban-definition-27J2QSJJ-CaJslHGg.js} +1 -1
- package/server/client/dist/assets/manage-image-DXedfr7U.js +1 -0
- package/server/client/dist/assets/{manage-image-detail-Dw3tAW3h.js → manage-image-detail-DmifOPNU.js} +1 -1
- package/server/client/dist/assets/{mcp-admin-adapter-D0R8p9az.js → mcp-admin-adapter-CmKsg1vd.js} +1 -1
- package/server/client/dist/assets/mcp-d8IzsoRz.js +5 -0
- package/server/client/dist/assets/{mermaid.core-C2tL9tq7.js → mermaid.core-BfUQM5vF.js} +4 -4
- package/server/client/dist/assets/{mindmap-definition-FAOFIHXS-DuvsPqrh.js → mindmap-definition-FAOFIHXS-DGbkJda2.js} +1 -1
- package/server/client/dist/assets/{note-core-YhYEmI_R.js → note-core-DRBsBf9H.js} +9 -9
- package/server/client/dist/assets/{note-runtime-B3ats-dC.js → note-runtime-ztZvXRIu.js} +16 -16
- package/server/client/dist/assets/{note-ui-BtzPk8gA.js → note-ui-DRnAnwKv.js} +1 -1
- package/server/client/dist/assets/{pegDiagram-VL7TDLO6-D64crEZV.js → pegDiagram-VL7TDLO6-Bip6zQct.js} +1 -1
- package/server/client/dist/assets/{pieDiagram-7S7Q4E2Y--kl7vopC.js → pieDiagram-7S7Q4E2Y-CjfjGY8Y.js} +1 -1
- package/server/client/dist/assets/{placeholder-B5uPsE7-.js → placeholder-G6iEBbDy.js} +1 -1
- package/server/client/dist/assets/{properties-B7Fv95qY.js → properties-BPobx_Qk.js} +1 -1
- package/server/client/dist/assets/{quadrantDiagram-CIZ2JOQS-DuvJ4M2y.js → quadrantDiagram-CIZ2JOQS-B6F1yUT-.js} +1 -1
- package/server/client/dist/assets/{railroadDiagram-AXF67PYL-BMKmCOY7.js → railroadDiagram-AXF67PYL-D-0fLe0F.js} +1 -1
- package/server/client/dist/assets/{requirementDiagram-LRYGKXZP-DY1eJcy0.js → requirementDiagram-LRYGKXZP-tm4mnCyJ.js} +1 -1
- package/server/client/dist/assets/route-preload.js +2 -2
- package/server/client/dist/assets/{sankeyDiagram-W5VNT64P-C3Ka4GKM.js → sankeyDiagram-W5VNT64P-DSBWFurb.js} +1 -1
- package/server/client/dist/assets/{search-ddYl3qH-.js → search-DanMKCC8.js} +1 -1
- package/server/client/dist/assets/{search-admin-adapter-Daf7e2YQ.js → search-admin-adapter-7sTcWTmv.js} +1 -1
- package/server/client/dist/assets/{sequenceDiagram-SI44F4Z6-Bhpwtk2s.js → sequenceDiagram-SI44F4Z6-KrwjjJIV.js} +1 -1
- package/server/client/dist/assets/{sizeCapture-X5ZJPWSS-BFta71TW.js → sizeCapture-X5ZJPWSS-DL2cn_Q6.js} +1 -1
- package/server/client/dist/assets/{stateDiagram-OKZ733FA-D6GCn1io.js → stateDiagram-OKZ733FA-CKuNDH85.js} +1 -1
- package/server/client/dist/assets/{stateDiagram-v2-UEYNNEHI-g4yinGDn.js → stateDiagram-v2-UEYNNEHI-BFaj2wbM.js} +1 -1
- package/server/client/dist/assets/{swimlanes-SLNWSIFB-DLFvOjUl.js → swimlanes-SLNWSIFB-5S17nqhK.js} +2 -2
- package/server/client/dist/assets/swimlanesDiagram-ULZ7WXOC-dexsHJq2.js +8 -0
- package/server/client/dist/assets/{tag.api-DEUROVKQ.js → tag.api-kKZl04yY.js} +1 -1
- package/server/client/dist/assets/{time-PxWila4-.js → time-BwwQGbK-.js} +38 -38
- package/server/client/dist/assets/{timeline-definition-Z64GVDOM-DTSIekSM.js → timeline-definition-Z64GVDOM-CU1hp_7d.js} +3 -3
- package/server/client/dist/assets/{trash-BHida7BG.js → trash-Cepvi7G8.js} +1 -1
- package/server/client/dist/assets/url-Bp8sXMb4.js +1 -0
- package/server/client/dist/assets/{useNoteMutate-BjvZUmtX.js → useNoteMutate-D-mZKXjl.js} +3 -3
- package/server/client/dist/assets/{useReminderMutate-C5n-5LM-.js → useReminderMutate-Cexk1ydX.js} +1 -1
- package/server/client/dist/assets/{vennDiagram-T6HMQDX7-CMNqSI2X.js → vennDiagram-T6HMQDX7-MoiVLnil.js} +1 -1
- package/server/client/dist/assets/{wardleyDiagram-T6FBY63Y-DV-JOYqB.js → wardleyDiagram-T6FBY63Y-cZkfdI6C.js} +1 -1
- package/server/client/dist/assets/{xychartDiagram-ELKLHX3M-BUIniF4m.js → xychartDiagram-ELKLHX3M-B63hxD-r.js} +1 -1
- package/server/client/dist/index.html +6 -6
- package/server/dist/app.js +2 -0
- package/server/dist/features/integration/auth.js +21 -0
- package/server/dist/features/integration/manifest.js +87 -0
- package/server/dist/features/integration/schema.js +54 -0
- package/server/dist/features/integration/service.js +173 -0
- package/server/dist/features/mcp-admin/service.js +19 -83
- package/server/dist/features/note/graphql/note.field.resolver.js +1 -1
- package/server/dist/features/note/graphql/note.query.resolver.js +6 -0
- package/server/dist/features/note/graphql/note.type-defs.js +13 -1
- package/server/dist/features/note/http/authoring.js +493 -0
- package/server/dist/features/note/http/mcp.js +15 -463
- package/server/dist/features/note/services/authoring.js +17 -1
- package/server/dist/features/note/services/markdown-intent-write.js +1 -0
- package/server/dist/features/note/services/markdown-read.js +85 -0
- package/server/dist/features/note/services/properties.js +36 -30
- package/server/dist/features/note/services/search.js +17 -0
- package/server/dist/features/search/graphql/resolver.js +10 -1
- package/server/dist/features/search/graphql/type-defs.js +3 -0
- package/server/dist/features/tag/graphql/tag.query.resolver.js +10 -0
- package/server/dist/features/tag/graphql/tag.type-defs.js +1 -0
- package/server/dist/features/view/graphql/mcp.query.resolver.js +22 -0
- package/server/dist/features/view/graphql/view.query.resolver.js +2 -0
- package/server/dist/features/view/graphql/view.type-defs.js +27 -0
- package/server/dist/features/view/services/mcp-read.js +142 -0
- package/server/dist/features/view/services/workspace.js +12 -4
- package/server/dist/modules/auth-guard.js +24 -1
- package/server/dist/modules/integration-token.js +27 -0
- package/server/dist/modules/mcp-auth.js +37 -23
- package/server/dist/modules/mcp-token.js +6 -25
- package/server/dist/routes/api.js +2 -0
- package/server/dist/routes/graphql.js +2 -1
- package/server/dist/routes/integrations.js +117 -0
- package/server/dist/routes/mcp.js +29 -9
- package/server/prisma/migrations/20260917120000_0020_plugin_installations/migration.sql +39 -0
- package/server/prisma/migrations/20260917150000_0021_integration_connections/migration.sql +29 -0
- package/server/prisma/schema.prisma +34 -10
- package/server/client/dist/assets/Note-CpzLtGVM.js +0 -19
- package/server/client/dist/assets/Reminders-DnuALBL6.js +0 -1
- package/server/client/dist/assets/Trash.es-XEcWG_aq.js +0 -1
- package/server/client/dist/assets/Views-B5oh8GVM.js +0 -1
- package/server/client/dist/assets/app-EwvT7bMw.css +0 -1
- package/server/client/dist/assets/app-NgiBDISG.js +0 -151
- package/server/client/dist/assets/channel-DZFalh-h.js +0 -1
- package/server/client/dist/assets/classDiagram-JCYQIIEL-CkEhFvVV.js +0 -1
- package/server/client/dist/assets/classDiagram-v2-OCEON4UE-CkEhFvVV.js +0 -1
- package/server/client/dist/assets/index-CIEap4bu.js +0 -1
- package/server/client/dist/assets/manage-image-CSJ3Flgc.js +0 -1
- package/server/client/dist/assets/mcp-D8footel.js +0 -5
- package/server/client/dist/assets/swimlanesDiagram-ULZ7WXOC-Ci4mcvmo.js +0 -8
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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(
|
|
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
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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",
|