-
{key}
+
{key}
{typeof value === "object" ? JSON.stringify(value) : String(value)}
@@ -248,6 +363,8 @@ export function GraphInspectorPanel({
);
}
+/* ─── styles ─────────────────────────────────────────────────────── */
+
const inputStyle: CSSProperties = {
width: "100%",
background: "rgba(4, 10, 18, 0.5)",
@@ -284,19 +401,12 @@ const secondaryActionButtonStyle: CSSProperties = {
const predictionCardStyle: CSSProperties = {
textAlign: "left",
- padding: 12,
+ padding: "10px 12px",
background: "rgba(88, 166, 255, 0.08)",
border: "1px solid rgba(88, 166, 255, 0.12)",
borderRadius: 10,
cursor: "pointer",
-};
-
-const pathStepStyle: CSSProperties = {
- color: "#e6edf3",
- fontSize: 13,
- padding: "8px 10px",
- background: "rgba(255, 255, 255, 0.03)",
- borderRadius: 8,
+ width: "100%",
};
const propertyCardStyle: CSSProperties = {
@@ -338,3 +448,58 @@ const sectionTitleStyle: CSSProperties = {
textTransform: "uppercase",
letterSpacing: "0.08em",
};
+
+const pathFlowContainerStyle: CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ gap: 0,
+ flexWrap: "wrap",
+ rowGap: 8,
+};
+
+const pathNodeChipStyle: CSSProperties = {
+ display: "inline-flex",
+ alignItems: "center",
+ gap: 6,
+ padding: "5px 10px",
+ borderRadius: 999,
+ background: "rgba(88,166,255,0.1)",
+ border: "1px solid rgba(88,166,255,0.22)",
+ color: "#e6edf3",
+ fontSize: 12,
+ fontWeight: 600,
+ maxWidth: 160,
+};
+
+const pathNodeIndexStyle: CSSProperties = {
+ display: "inline-flex",
+ alignItems: "center",
+ justifyContent: "center",
+ width: 16,
+ height: 16,
+ borderRadius: "50%",
+ background: "rgba(88,166,255,0.22)",
+ color: "#79c0ff",
+ fontSize: 9,
+ fontWeight: 800,
+ flexShrink: 0,
+};
+
+const pathEdgeConnectorStyle: CSSProperties = {
+ display: "inline-flex",
+ alignItems: "center",
+ gap: 2,
+ flexShrink: 0,
+};
+
+const pathEdgeLabelStyle: CSSProperties = {
+ fontSize: 9,
+ fontWeight: 700,
+ color: "#6a7f97",
+ letterSpacing: "0.04em",
+ textTransform: "uppercase",
+ maxWidth: 70,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+};
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx b/explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx
rename to explorer/src/workspaces/GraphWorkspace/GraphLoadingOverlay.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/GraphRuntimeStage.tsx b/explorer/src/workspaces/GraphWorkspace/GraphRuntimeStage.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/GraphRuntimeStage.tsx
rename to explorer/src/workspaces/GraphWorkspace/GraphRuntimeStage.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
similarity index 97%
rename from semantica-explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
rename to explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
index a01dd32c..0c04357d 100644
--- a/semantica-explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
+++ b/explorer/src/workspaces/GraphWorkspace/GraphWorkspace.tsx
@@ -1,6 +1,7 @@
-import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { batchMergeEdges, batchMergeNodes, graph } from "../../store/graphStore";
+import { logEvent } from "../../store/registryStore";
import type { EdgeAttributes, NodeAttributes } from "../../store/graphStore";
import { curveGroupForPair } from "../../store/edgePairKeys.js";
import { InspectorPanel, MetricChip, SurfaceCard } from "../../ui/primitives";
@@ -630,6 +631,7 @@ export function GraphWorkspace() {
const [searchResults, setSearchResults] = useState
([]);
const [searchError, setSearchError] = useState("");
const [predictionType, setPredictionType] = useState("");
+ const [isRunningPredictions, setIsRunningPredictions] = useState(false);
const [predictions, setPredictions] = useState([]);
const [pathTargetId, setPathTargetId] = useState("");
const [pathResult, setPathResult] = useState(null);
@@ -814,6 +816,7 @@ export function GraphWorkspace() {
const handleRunPredictions = useCallback(async () => {
if (!selectedNodeId) return;
+ setIsRunningPredictions(true);
try {
const response = await fetch("/api/enrich/links", {
method: "POST",
@@ -833,6 +836,8 @@ export function GraphWorkspace() {
} catch (predictionError) {
console.error("[GraphWorkspace] prediction failed", predictionError);
setPredictions([]);
+ } finally {
+ setIsRunningPredictions(false);
}
}, [predictionType, selectedNodeId]);
@@ -899,6 +904,7 @@ export function GraphWorkspace() {
attributes: buildRealtimeNodeAttributes(payload),
},
]);
+ logEvent("add-node", `Added node ${payload.label ?? payload.id}${payload.nodeType ? ` (${payload.nodeType})` : ""} via realtime ws`, { nodeId: payload.id, nodeType: payload.nodeType });
sceneRef.current?.getRuntime()?.requestRender();
}
if (eventType === "ADD_EDGE") {
@@ -911,6 +917,7 @@ export function GraphWorkspace() {
attributes: buildRealtimeEdgeAttributes(payload),
},
]);
+ logEvent("add-edge", `Added edge ${payload.edgeType ?? payload.id} (${payload.source_id} → ${payload.target_id}) via realtime ws`, { edgeId: payload.id, edgeType: payload.edgeType, source: payload.source_id, target: payload.target_id });
sceneRef.current?.getRuntime()?.requestRender();
}
} catch (socketError) {
@@ -1293,10 +1300,34 @@ export function GraphWorkspace() {
disabled: showLoadingOverlay || !searchQuery.trim(),
onClick: () => void handleSearch(),
},
+ {
+ id: "zoom-in",
+ label: "+ Zoom In",
+ title: "Zoom in (or scroll up on the canvas)",
+ onClick: () => {
+ const runtime = sceneRef.current?.getRuntime();
+ if (runtime?.renderer === "sigma") {
+ const camera = (runtime.scene as import("sigma").default).getCamera();
+ camera.animatedZoom({ duration: 200 });
+ }
+ },
+ },
+ {
+ id: "zoom-out",
+ label: "- Zoom Out",
+ title: "Zoom out (or scroll down on the canvas)",
+ onClick: () => {
+ const runtime = sceneRef.current?.getRuntime();
+ if (runtime?.renderer === "sigma") {
+ const camera = (runtime.scene as import("sigma").default).getCamera();
+ camera.animatedUnzoom({ duration: 200 });
+ }
+ },
+ },
{
id: "fit-view",
label: "Fit View",
- title: "Reset the camera to the current view",
+ title: "Reset the camera to fit the whole graph",
onClick: () => sceneRef.current?.fitView(),
},
{
@@ -1581,6 +1612,7 @@ export function GraphWorkspace() {
predictionType={predictionType}
onPredictionTypeChange={setPredictionType}
onRunPredictions={() => void handleRunPredictions()}
+ isRunningPredictions={isRunningPredictions}
pathTargetId={pathTargetId}
onPathTargetChange={setPathTargetId}
onTracePath={() => void handleTracePath()}
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/GraphWorkspaceShell.tsx b/explorer/src/workspaces/GraphWorkspace/GraphWorkspaceShell.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/GraphWorkspaceShell.tsx
rename to explorer/src/workspaces/GraphWorkspace/GraphWorkspaceShell.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx b/explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx
rename to explorer/src/workspaces/GraphWorkspace/SigmaSceneAdapter.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx b/explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx
rename to explorer/src/workspaces/GraphWorkspace/TimelinePanel.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts b/explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts
rename to explorer/src/workspaces/GraphWorkspace/behaviors/clickSelectionBehavior.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts b/explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts
rename to explorer/src/workspaces/GraphWorkspace/behaviors/fitViewBehavior.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts b/explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts
rename to explorer/src/workspaces/GraphWorkspace/behaviors/focusCameraBehavior.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts b/explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts
rename to explorer/src/workspaces/GraphWorkspace/behaviors/hoverActivationBehavior.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts b/explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts
rename to explorer/src/workspaces/GraphWorkspace/behaviors/pathHighlightBehavior.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts b/explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts
rename to explorer/src/workspaces/GraphWorkspace/behaviors/searchFocusBehavior.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/behaviors/types.ts b/explorer/src/workspaces/GraphWorkspace/behaviors/types.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/behaviors/types.ts
rename to explorer/src/workspaces/GraphWorkspace/behaviors/types.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts b/explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts
rename to explorer/src/workspaces/GraphWorkspace/behaviors/viewModeSwitchBehavior.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts b/explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts
rename to explorer/src/workspaces/GraphWorkspace/graphAnalytics.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/graphConfig.ts b/explorer/src/workspaces/GraphWorkspace/graphConfig.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/graphConfig.ts
rename to explorer/src/workspaces/GraphWorkspace/graphConfig.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/graphLoading.ts b/explorer/src/workspaces/GraphWorkspace/graphLoading.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/graphLoading.ts
rename to explorer/src/workspaces/GraphWorkspace/graphLoading.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts b/explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts
rename to explorer/src/workspaces/GraphWorkspace/graphSceneLayers.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/graphSceneState.ts b/explorer/src/workspaces/GraphWorkspace/graphSceneState.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/graphSceneState.ts
rename to explorer/src/workspaces/GraphWorkspace/graphSceneState.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/graphTheme.ts b/explorer/src/workspaces/GraphWorkspace/graphTheme.ts
similarity index 86%
rename from semantica-explorer/src/workspaces/GraphWorkspace/graphTheme.ts
rename to explorer/src/workspaces/GraphWorkspace/graphTheme.ts
index bce23092..ea033613 100644
--- a/semantica-explorer/src/workspaces/GraphWorkspace/graphTheme.ts
+++ b/explorer/src/workspaces/GraphWorkspace/graphTheme.ts
@@ -265,16 +265,16 @@ export const GRAPH_THEME: GraphTheme = {
],
overview: {
nodeBase: "#0B1320",
- nodeCore: "#435D7A",
+ nodeCore: "#5A7A9E",
nodeMuted: "#121927",
- nodeBorder: "#64758C",
- nodeTintMix: 0.03,
- nodeCoreMix: 0.52,
+ nodeBorder: "#7A92AE",
+ nodeTintMix: 0.14,
+ nodeCoreMix: 0.72,
nodeShellAlpha: 0.97,
nodeCoreAlpha: 1,
- edgeBackbone: "rgba(83, 111, 148, 0.04)",
- edgeStructure: "rgba(72, 90, 118, 0.009)",
- edgeInspection: "rgba(98, 120, 148, 0.026)",
+ edgeBackbone: "rgba(100, 148, 210, 0.38)",
+ edgeStructure: "rgba(88, 140, 200, 0.28)",
+ edgeInspection: "rgba(110, 165, 230, 0.48)",
},
accent: {
selected: "#F2D288",
@@ -285,12 +285,12 @@ export const GRAPH_THEME: GraphTheme = {
inferred: "#D07B4D",
},
muted: {
- fallback: "rgba(96, 112, 136, 0.1)",
- nodeAlpha: 0.085,
- edgeOverview: "rgba(82, 100, 124, 0.009)",
- edgeStructure: "rgba(92, 112, 138, 0.02)",
- edgeInspection: "rgba(124, 148, 176, 0.066)",
- edgeFocus: "rgba(160, 186, 218, 0.16)",
+ fallback: "rgba(96, 112, 136, 0.18)",
+ nodeAlpha: 0.12,
+ edgeOverview: "rgba(82, 100, 124, 0.12)",
+ edgeStructure: "rgba(92, 112, 138, 0.18)",
+ edgeInspection: "rgba(124, 148, 176, 0.26)",
+ edgeFocus: "rgba(160, 186, 218, 0.42)",
},
background: {
canvas: "#07101A",
@@ -305,36 +305,36 @@ export const GRAPH_THEME: GraphTheme = {
zoomTiers: {
overview: {
maxRatio: Number.POSITIVE_INFINITY,
- nodeScale: 0.66,
- labelThreshold: 0.985,
- labelBudget: 10,
- edgePriorityThreshold: 0.72,
+ nodeScale: 0.88,
+ labelThreshold: 0.92,
+ labelBudget: 28,
+ edgePriorityThreshold: 0.55,
arrowPriorityThreshold: Number.POSITIVE_INFINITY,
- edgeSizeScale: 0.34,
+ edgeSizeScale: 0.62,
showBadges: false,
showCurves: false,
showContextualArrows: false,
},
structure: {
maxRatio: 1.2,
- nodeScale: 0.98,
- labelThreshold: 0.88,
- labelBudget: 36,
- edgePriorityThreshold: 0.4,
- arrowPriorityThreshold: 0.75,
- edgeSizeScale: 0.92,
+ nodeScale: 1.02,
+ labelThreshold: 0.82,
+ labelBudget: 60,
+ edgePriorityThreshold: 0.3,
+ arrowPriorityThreshold: 0.65,
+ edgeSizeScale: 1.05,
showBadges: true,
showCurves: true,
showContextualArrows: true,
},
inspection: {
maxRatio: 0.5,
- nodeScale: 1,
- labelThreshold: 0.7,
- labelBudget: 80,
+ nodeScale: 1.08,
+ labelThreshold: 0.6,
+ labelBudget: 120,
edgePriorityThreshold: 0,
- arrowPriorityThreshold: 0.58,
- edgeSizeScale: 1.04,
+ arrowPriorityThreshold: 0.45,
+ edgeSizeScale: 1.18,
showBadges: true,
showCurves: true,
showContextualArrows: true,
@@ -398,13 +398,13 @@ export const GRAPH_THEME: GraphTheme = {
inspection: { base: 1.2, emphasis: 1.7, muted: 0.6 },
},
states: {
- default: { color: "base", sizeMultiplier: 0.72, minSize: 0.68, forceLabel: false, zIndex: 0, borderBoost: -0.42 },
- hovered: { color: "hovered", sizeMultiplier: 1.18, minSize: 12.5, forceLabel: true, zIndex: 4, borderBoost: 0.22 },
- selected: { color: "selected", sizeMultiplier: 1.06, minSize: 10.5, forceLabel: true, zIndex: 3, borderBoost: 0.2 },
- neighbor: { color: "base", sizeMultiplier: 0.84, minSize: 4.8, forceLabel: true, zIndex: 2, borderBoost: -0.08 },
- path: { color: "path", sizeMultiplier: 1.01, minSize: 6.2, forceLabel: true, zIndex: 2, borderBoost: 0.08 },
- inactive: { color: "muted", sizeMultiplier: 0.32, minSize: 0.46, forceLabel: false, zIndex: 0, borderBoost: -0.42 },
- muted: { color: "muted", sizeMultiplier: 0.32, minSize: 0.46, forceLabel: false, zIndex: 0, borderBoost: -0.42 },
+ default: { color: "base", sizeMultiplier: 0.92, minSize: 3.5, forceLabel: false, zIndex: 0, borderBoost: -0.18 },
+ hovered: { color: "hovered", sizeMultiplier: 1.28, minSize: 13.5, forceLabel: true, zIndex: 4, borderBoost: 0.28 },
+ selected: { color: "selected", sizeMultiplier: 1.14, minSize: 11.5, forceLabel: true, zIndex: 3, borderBoost: 0.24 },
+ neighbor: { color: "base", sizeMultiplier: 0.96, minSize: 5.5, forceLabel: true, zIndex: 2, borderBoost: 0.04 },
+ path: { color: "path", sizeMultiplier: 1.08, minSize: 7.0, forceLabel: true, zIndex: 2, borderBoost: 0.12 },
+ inactive: { color: "muted", sizeMultiplier: 0.48, minSize: 1.8, forceLabel: false, zIndex: 0, borderBoost: -0.28 },
+ muted: { color: "muted", sizeMultiplier: 0.48, minSize: 1.8, forceLabel: false, zIndex: 0, borderBoost: -0.28 },
},
variants: {
default: { sizeMultiplier: 1, borderBoost: 0, haloBoost: 0, badgeVisibleFrom: "inspection" },
@@ -437,14 +437,14 @@ export const GRAPH_THEME: GraphTheme = {
},
edges: {
states: {
- default: { color: "structure", sizeMultiplier: 0.74, minSize: 0.18, zIndex: 0, forceArrow: false, hide: false },
- backbone: { color: "backbone", sizeMultiplier: 0.72, minSize: 0.18, zIndex: 1, forceArrow: false, hide: false },
- hovered: { color: "hover", sizeMultiplier: 1.42, minSize: 1.45, zIndex: 3, forceArrow: true, hide: false },
- selected: { color: "hover", sizeMultiplier: 1.42, minSize: 1.45, zIndex: 3, forceArrow: true, hide: false },
- neighbor: { color: "focus", sizeMultiplier: 0.92, minSize: 0.5, zIndex: 1, forceArrow: false, hide: false },
- path: { color: "path", sizeMultiplier: 1.5, minSize: 1.8, zIndex: 4, forceArrow: true, hide: false },
- inactive: { color: "muted", sizeMultiplier: 1, minSize: 0.16, zIndex: 0, forceArrow: false, hide: true },
- muted: { color: "muted", sizeMultiplier: 1, minSize: 0.16, zIndex: 0, forceArrow: false, hide: true },
+ default: { color: "structure", sizeMultiplier: 0.96, minSize: 0.9, zIndex: 0, forceArrow: false, hide: false },
+ backbone: { color: "backbone", sizeMultiplier: 1.0, minSize: 1.0, zIndex: 1, forceArrow: false, hide: false },
+ hovered: { color: "hover", sizeMultiplier: 1.6, minSize: 2.2, zIndex: 3, forceArrow: true, hide: false },
+ selected: { color: "hover", sizeMultiplier: 1.6, minSize: 2.2, zIndex: 3, forceArrow: true, hide: false },
+ neighbor: { color: "focus", sizeMultiplier: 1.1, minSize: 1.2, zIndex: 1, forceArrow: false, hide: false },
+ path: { color: "path", sizeMultiplier: 1.7, minSize: 2.4, zIndex: 4, forceArrow: true, hide: false },
+ inactive: { color: "muted", sizeMultiplier: 0.6, minSize: 0.5, zIndex: 0, forceArrow: false, hide: false },
+ muted: { color: "muted", sizeMultiplier: 0.6, minSize: 0.5, zIndex: 0, forceArrow: false, hide: false },
},
variants: {
line: { baseType: "line", arrowPolicy: "hidden", curveStrength: 0, sizeMultiplier: 1, glowAlpha: 0 },
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx b/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx
rename to explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPlugin.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx b/explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx
rename to explorer/src/workspaces/GraphWorkspace/plugins/explorationEffectsPluginPhaseC.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/plugins/index.ts b/explorer/src/workspaces/GraphWorkspace/plugins/index.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/plugins/index.ts
rename to explorer/src/workspaces/GraphWorkspace/plugins/index.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx b/explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx
rename to explorer/src/workspaces/GraphWorkspace/plugins/legendPlugin.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx b/explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx
rename to explorer/src/workspaces/GraphWorkspace/plugins/neighborhoodPanelPlugin.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx b/explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx
rename to explorer/src/workspaces/GraphWorkspace/plugins/temporalOverlayPlugin.tsx
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/plugins/types.ts b/explorer/src/workspaces/GraphWorkspace/plugins/types.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/plugins/types.ts
rename to explorer/src/workspaces/GraphWorkspace/plugins/types.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/scene.ts b/explorer/src/workspaces/GraphWorkspace/scene.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/scene.ts
rename to explorer/src/workspaces/GraphWorkspace/scene.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts b/explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts
rename to explorer/src/workspaces/GraphWorkspace/sigmaNativeRendering.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/types.ts b/explorer/src/workspaces/GraphWorkspace/types.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/types.ts
rename to explorer/src/workspaces/GraphWorkspace/types.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/useGraphData.ts b/explorer/src/workspaces/GraphWorkspace/useGraphData.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/useGraphData.ts
rename to explorer/src/workspaces/GraphWorkspace/useGraphData.ts
diff --git a/semantica-explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts b/explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts
rename to explorer/src/workspaces/GraphWorkspace/useLoadGraph.ts
diff --git a/semantica-explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx b/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx
similarity index 96%
rename from semantica-explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx
rename to explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx
index abf32aba..44b0df58 100644
--- a/semantica-explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx
+++ b/explorer/src/workspaces/ImportExportWorkspace/ImportExportWorkspace.tsx
@@ -4,6 +4,7 @@
import { useState, useCallback } from "react";
import { useDropzone } from "react-dropzone";
import { UploadCloud, Download, FileJson, FileText, CheckCircle2, AlertCircle, Loader2 } from "lucide-react";
+import { logEvent } from "../../store/registryStore";
const THEME_CSS = `
.glass-panel {
@@ -107,6 +108,11 @@ export function ImportExportWorkspace() {
const data = await res.json();
showToast("success", `Imported ${data.nodes_imported} nodes and ${data.edges_imported} edges!`);
+ logEvent("import", `Imported ${data.nodes_imported} nodes · ${data.edges_imported} edges from ${file.name}`, {
+ file: file.name,
+ nodesImported: data.nodes_imported,
+ edgesImported: data.edges_imported,
+ });
setFile(null);
} catch (err: any) {
showToast("error", err.message || "An error occurred during import");
@@ -142,6 +148,7 @@ export function ImportExportWorkspace() {
document.body.removeChild(a);
showToast("success", "Export complete! Your download should begin shortly.");
+ logEvent("export", `Exported graph as ${exportFormat.toUpperCase()}`, { format: exportFormat });
} catch (err: any) {
showToast("error", err.message || "An error occurred during export");
} finally {
diff --git a/semantica-explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx b/explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx
rename to explorer/src/workspaces/LineageWorkspace/LineageDiagram.tsx
diff --git a/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx b/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx
new file mode 100644
index 00000000..60866143
--- /dev/null
+++ b/explorer/src/workspaces/ManageWorkspace/KGOverviewTab.tsx
@@ -0,0 +1,339 @@
+/**
+ * src/workspaces/ManageWorkspace/KGOverviewTab.tsx
+ *
+ * Quick-view dashboard for the Knowledge Graph: node/edge counts,
+ * type distributions, and top connected nodes.
+ */
+import { useState, useEffect, useCallback } from "react";
+import { Network, RefreshCw, Loader2 } from "lucide-react";
+
+interface KGStats {
+ node_count: number;
+ edge_count: number;
+ node_types?: Record;
+ edge_types?: Record;
+ [key: string]: unknown;
+}
+
+interface NodeItem {
+ id: string;
+ type: string;
+ content: string;
+ properties?: Record;
+}
+
+interface NodeListResponse {
+ nodes: NodeItem[];
+ total: number;
+}
+
+function TypeBar({ label, count, total, color }: { label: string; count: number; total: number; color: string }) {
+ const pct = total > 0 ? Math.round((count / total) * 100) : 0;
+ return (
+
+
+ {label}
+
+
+
+ {count.toLocaleString()}
+ {pct}%
+
+
+ );
+}
+
+const NODE_COLORS = ["#3E79F2", "#149287", "#2F9F61", "#555FD6", "#8A56D8", "#B65473", "#C9922E", "#4aa3ff", "#f2b66d"];
+const EDGE_COLORS = ["#4cc38a", "#79c0ff", "#d2a8ff", "#f2b66d", "#ff7b72", "#58a6ff", "#4aa3ff", "#8A56D8"];
+
+function buildTypeMap(nodes: NodeItem[], key: keyof NodeItem): Record {
+ const map: Record = {};
+ for (const node of nodes) {
+ const val = String(node[key] ?? "unknown");
+ map[val] = (map[val] ?? 0) + 1;
+ }
+ return map;
+}
+
+export function KGOverviewTab() {
+ const [stats, setStats] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState("");
+ const [topNodes, setTopNodes] = useState<{ node: NodeItem; neighborCount: number }[]>([]);
+ const [nodeTypeMap, setNodeTypeMap] = useState>({});
+
+ const fetchOverview = useCallback(async () => {
+ setLoading(true);
+ setError("");
+ try {
+ const [statsRes, nodesRes] = await Promise.all([
+ fetch("/api/graph/stats"),
+ fetch("/api/graph/nodes?limit=500"),
+ ]);
+
+ if (statsRes.ok) {
+ const statsData: KGStats = await statsRes.json();
+ setStats(statsData);
+ }
+
+ if (nodesRes.ok) {
+ const nodesData: NodeListResponse = await nodesRes.json();
+ const nodes = nodesData.nodes ?? [];
+ setNodeTypeMap(buildTypeMap(nodes, "type"));
+
+ // Simulate neighbor counts via edges fetch for top-N
+ const edgesRes = await fetch("/api/graph/edges?limit=2000");
+ if (edgesRes.ok) {
+ const edgesData = await edgesRes.json();
+ const edges: { source: string; target: string }[] = edgesData.edges ?? [];
+ const degreeMap: Record = {};
+ for (const edge of edges) {
+ degreeMap[edge.source] = (degreeMap[edge.source] ?? 0) + 1;
+ degreeMap[edge.target] = (degreeMap[edge.target] ?? 0) + 1;
+ }
+ const sorted = nodes
+ .map((n) => ({ node: n, neighborCount: degreeMap[n.id] ?? 0 }))
+ .sort((a, b) => b.neighborCount - a.neighborCount)
+ .slice(0, 10);
+ setTopNodes(sorted);
+ }
+ }
+ } catch {
+ setError("Failed to load graph overview. Ensure the server is running.");
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ void fetchOverview();
+ }, [fetchOverview]);
+
+ const nodeTypeEntries = Object.entries(nodeTypeMap).sort((a, b) => b[1] - a[1]);
+ const edgeTypeEntries = stats?.edge_types
+ ? Object.entries(stats.edge_types).sort((a, b) => b[1] - a[1])
+ : [];
+
+ const totalNodes = stats?.node_count ?? 0;
+ const totalEdges = stats?.edge_count ?? 0;
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
KG Overview
+
Quick view of the Knowledge Graph structure and health
+
+
+
void fetchOverview()} disabled={loading} style={refreshBtnStyle}>
+ {loading ? : }
+ Refresh
+
+
+
+ {error ? (
+
+ {error}
+
+ ) : null}
+
+
+ {/* Stats chips */}
+
+ {[
+ { label: "Nodes", value: totalNodes.toLocaleString(), color: "#4aa3ff", sub: `${nodeTypeEntries.length} types` },
+ { label: "Edges", value: totalEdges.toLocaleString(), color: "#4cc38a", sub: `${edgeTypeEntries.length} relationship types` },
+ { label: "Density", value: totalNodes > 1 ? ((totalEdges / (totalNodes * (totalNodes - 1))) * 100).toFixed(3) + "%" : "—", color: "#d2a8ff", sub: "graph density" },
+ ].map(({ label, value, color, sub }) => (
+
+
{label}
+
{loading ? "—" : value}
+
{sub}
+
+ ))}
+
+
+ {/* Type breakdowns */}
+
+ {/* Node types */}
+
+
Node Type Breakdown
+ {loading ? (
+
+ {[80, 65, 45, 35, 25].map((w, i) => (
+
+ ))}
+
+ ) : nodeTypeEntries.length === 0 ? (
+
No data — load the graph first.
+ ) : (
+ nodeTypeEntries.slice(0, 8).map(([type, count], i) => (
+
+ ))
+ )}
+
+
+ {/* Edge types */}
+
+
Edge Type Breakdown
+ {loading ? (
+
+ {[70, 55, 48, 30, 20].map((w, i) => (
+
+ ))}
+
+ ) : edgeTypeEntries.length === 0 ? (
+
Edge type breakdown requires the stats endpoint to return edge_types.
+ ) : (
+ edgeTypeEntries.slice(0, 8).map(([type, count], i) => (
+
+ ))
+ )}
+
+
+
+ {/* Top connected nodes */}
+ {topNodes.length > 0 ? (
+
+
Top Connected Nodes (by degree)
+
+ {topNodes.map(({ node, neighborCount }, rank) => (
+
+
#{rank + 1}
+
+
+ {node.content || node.id}
+
+
{node.type}
+
+
+ {neighborCount} conn.
+
+
+ ))}
+
+
+ ) : null}
+
+
+ );
+}
+
+/* ─── styles ─────────────────────────────────────────────────────── */
+
+const shellStyle: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ width: "100%",
+ height: "100%",
+ background: "#0d1117",
+ overflow: "hidden",
+};
+
+const headerStyle: React.CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ padding: "20px 24px 16px",
+ borderBottom: "1px solid rgba(88,166,255,0.1)",
+ flexShrink: 0,
+};
+
+const refreshBtnStyle: React.CSSProperties = {
+ display: "inline-flex",
+ alignItems: "center",
+ gap: 6,
+ padding: "6px 12px",
+ borderRadius: 8,
+ border: "1px solid rgba(127,208,255,0.16)",
+ background: "rgba(74,163,255,0.08)",
+ color: "#8fa8c6",
+ fontSize: 12,
+ fontWeight: 600,
+ cursor: "pointer",
+};
+
+const scrollBodyStyle: React.CSSProperties = {
+ flex: 1,
+ overflowY: "auto",
+ padding: "20px 24px",
+ display: "flex",
+ flexDirection: "column",
+ gap: 16,
+};
+
+const statsRowStyle: React.CSSProperties = {
+ display: "grid",
+ gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
+ gap: 12,
+};
+
+const statCardStyle: React.CSSProperties = {
+ padding: "18px 20px",
+ borderRadius: 16,
+ background: "linear-gradient(135deg, rgba(13,17,23,0.8), rgba(22,27,34,0.5))",
+ border: "1px solid rgba(127,208,255,0.1)",
+ boxShadow: "inset 0 1px 0 rgba(255,255,255,0.04)",
+};
+
+const sectionRowStyle: React.CSSProperties = {
+ display: "grid",
+ gridTemplateColumns: "1fr 1fr",
+ gap: 12,
+};
+
+const breakdownCardStyle: React.CSSProperties = {
+ padding: "16px 18px",
+ borderRadius: 14,
+ background: "linear-gradient(135deg, rgba(13,17,23,0.7), rgba(22,27,34,0.4))",
+ border: "1px solid rgba(255,255,255,0.06)",
+ display: "flex",
+ flexDirection: "column",
+ gap: 8,
+};
+
+const sectionTitleStyle: React.CSSProperties = {
+ color: "#8b949e",
+ fontSize: 11,
+ fontWeight: 700,
+ textTransform: "uppercase",
+ letterSpacing: "0.07em",
+ marginBottom: 4,
+};
+
+const topNodeRowStyle: React.CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ gap: 10,
+ padding: "8px 10px",
+ borderRadius: 10,
+ background: "rgba(255,255,255,0.025)",
+ border: "1px solid rgba(255,255,255,0.05)",
+};
+
+const skeletonWrapStyle: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ gap: 8,
+ marginTop: 4,
+};
+
+const skeletonBarStyle: React.CSSProperties = {
+ height: 12,
+ borderRadius: 999,
+ background: "rgba(255,255,255,0.05)",
+ animation: "skeleton-pulse 1.4s ease-in-out infinite",
+};
diff --git a/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx b/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx
new file mode 100644
index 00000000..4bb2e629
--- /dev/null
+++ b/explorer/src/workspaces/ManageWorkspace/OntologySummaryTab.tsx
@@ -0,0 +1,346 @@
+/**
+ * src/workspaces/ManageWorkspace/OntologySummaryTab.tsx
+ *
+ * A compact read-only view of all loaded SKOS ConceptSchemes and their
+ * top-level concepts. Clicking a concept deep-links to the Vocabulary Browser.
+ */
+import { useState } from "react";
+import { BookOpen, ChevronRight, ChevronDown, ExternalLink } from "lucide-react";
+import { useVocabularies, useConceptHierarchy } from "../VocabularyWorkspace/queries";
+import type { ConceptNode, VocabularyScheme } from "../VocabularyWorkspace/types";
+
+function countConcepts(nodes: ConceptNode[]): number {
+ return nodes.reduce((acc, node) => {
+ return acc + 1 + countConcepts(node.children ?? []);
+ }, 0);
+}
+
+function ConceptRow({
+ concept,
+ depth,
+ onSelect,
+}: {
+ concept: ConceptNode;
+ depth: number;
+ onSelect: (concept: ConceptNode) => void;
+}) {
+ const [expanded, setExpanded] = useState(false);
+ const children = concept.children ?? [];
+ const hasChildren = children.length > 0;
+
+ return (
+ <>
+ { (e.currentTarget as HTMLDivElement).style.background = "rgba(74,163,255,0.07)"; }}
+ onMouseLeave={(e) => { (e.currentTarget as HTMLDivElement).style.background = "transparent"; }}
+ >
+ {hasChildren ? (
+ setExpanded((v) => !v)}
+ style={{ background: "transparent", border: "none", color: "#8b949e", cursor: "pointer", padding: 0, display: "flex", alignItems: "center" }}
+ >
+ {expanded ? : }
+
+ ) : (
+
+ )}
+ onSelect(concept)}
+ style={{ flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
+ >
+ {concept.pref_label || concept.uri}
+
+ {children.length > 0 ? (
+ {children.length}
+ ) : null}
+
+ {expanded && hasChildren
+ ? children.map((child) => (
+
+ ))
+ : null}
+ >
+ );
+}
+
+function SchemePanel({
+ scheme,
+ onSelectConcept,
+}: {
+ scheme: VocabularyScheme;
+ onSelectConcept: (concept: ConceptNode) => void;
+}) {
+ const [expanded, setExpanded] = useState(true);
+ const { data: hierarchy = [], isLoading } = useConceptHierarchy(scheme.uri);
+ const totalConcepts = countConcepts(hierarchy);
+
+ return (
+
+ {/* Scheme header */}
+
setExpanded((v) => !v)}
+ style={schemeHeaderStyle}
+ >
+
+ {expanded ? : }
+ {scheme.label}
+
+
+ {isLoading ? "…" : `${totalConcepts} concept${totalConcepts !== 1 ? "s" : ""}`}
+
+
+
+ {/* Concept tree */}
+ {expanded ? (
+
+ {isLoading ? (
+
Loading concepts…
+ ) : hierarchy.length === 0 ? (
+
+ No concepts found in this scheme.
+
+ ) : (
+ hierarchy.map((concept) => (
+
+ ))
+ )}
+
+ ) : null}
+
+ );
+}
+
+export function OntologySummaryTab({
+ onOpenVocabularyBrowser,
+}: {
+ onOpenVocabularyBrowser?: () => void;
+}) {
+ const { data: schemes = [], isLoading } = useVocabularies();
+ const [selectedConcept, setSelectedConcept] = useState(null);
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
Ontology Summary
+
+ {isLoading
+ ? "Loading schemes…"
+ : `${schemes.length} vocabulary scheme${schemes.length !== 1 ? "s" : ""} loaded`}
+
+
+
+ {onOpenVocabularyBrowser ? (
+
+
+ Open Full Browser
+
+ ) : null}
+
+
+
+ {/* Scheme tree column */}
+
+ {isLoading ? (
+
+ {[90, 75, 60].map((w, i) => (
+
+ ))}
+
+ ) : schemes.length === 0 ? (
+
+
+
No vocabulary schemes loaded
+
+ Import a .ttl or .rdf file via the Vocabulary Browser to see your ontology here.
+
+
+ ) : (
+
+ {schemes.map((scheme) => (
+
+ ))}
+
+ )}
+
+
+ {/* Concept detail panel */}
+ {selectedConcept ? (
+
+
+
+ Concept Detail
+
+
setSelectedConcept(null)} style={{ background: "transparent", border: "none", color: "#8b949e", cursor: "pointer", fontSize: 16 }}>×
+
+
+
+ {selectedConcept.pref_label}
+
+ {selectedConcept.notation ? (
+
Notation: {selectedConcept.notation}
+ ) : null}
+
+ {selectedConcept.uri}
+
+
+ {selectedConcept.description ? (
+
+
Description
+
{selectedConcept.description}
+
+ ) : null}
+
+ {selectedConcept.alt_labels?.length ? (
+
+
Alternative Labels
+
+ {selectedConcept.alt_labels.map((label) => (
+ {label}
+ ))}
+
+
+ ) : null}
+
+ {(selectedConcept.children?.length ?? 0) > 0 ? (
+
+
Narrower Concepts ({selectedConcept.children!.length})
+
+ {selectedConcept.children!.slice(0, 8).map((child) => (
+
setSelectedConcept(child)}
+ style={{ color: "#79c0ff", fontSize: 12, cursor: "pointer", padding: "3px 0" }}
+ >
+ → {child.pref_label}
+
+ ))}
+ {selectedConcept.children!.length > 8 ? (
+
+{selectedConcept.children!.length - 8} more
+ ) : null}
+
+
+ ) : null}
+
+ ) : null}
+
+
+ );
+}
+
+/* ─── styles ─────────────────────────────────────────────────────── */
+
+const shellStyle: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ width: "100%",
+ height: "100%",
+ background: "#0d1117",
+ overflow: "hidden",
+};
+
+const headerStyle: React.CSSProperties = {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ padding: "20px 24px 16px",
+ borderBottom: "1px solid rgba(88,166,255,0.1)",
+ flexShrink: 0,
+};
+
+const openBrowserBtnStyle: React.CSSProperties = {
+ display: "inline-flex",
+ alignItems: "center",
+ gap: 6,
+ padding: "6px 12px",
+ borderRadius: 8,
+ border: "1px solid rgba(210,168,255,0.22)",
+ background: "rgba(210,168,255,0.08)",
+ color: "#d2a8ff",
+ fontSize: 12,
+ fontWeight: 600,
+ cursor: "pointer",
+};
+
+const treeColumnStyle: React.CSSProperties = {
+ flex: 1,
+ overflowY: "auto",
+ borderRight: "1px solid rgba(255,255,255,0.06)",
+};
+
+const schemeCardStyle: React.CSSProperties = {
+ borderRadius: 10,
+ border: "1px solid rgba(210,168,255,0.1)",
+ background: "rgba(255,255,255,0.02)",
+ overflow: "hidden",
+};
+
+const schemeHeaderStyle: React.CSSProperties = {
+ width: "100%",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "space-between",
+ padding: "10px 14px",
+ background: "transparent",
+ border: "none",
+ cursor: "pointer",
+ borderBottom: "1px solid rgba(255,255,255,0.05)",
+};
+
+const detailPanelStyle: React.CSSProperties = {
+ width: 300,
+ padding: "20px",
+ overflowY: "auto",
+ borderLeft: "1px solid rgba(255,255,255,0.06)",
+ flexShrink: 0,
+};
+
+const detailSectionStyle: React.CSSProperties = {
+ marginTop: 14,
+ paddingTop: 12,
+ borderTop: "1px solid rgba(255,255,255,0.06)",
+};
+
+const detailLabelStyle: React.CSSProperties = {
+ color: "#8b949e",
+ fontSize: 10,
+ fontWeight: 700,
+ textTransform: "uppercase",
+ letterSpacing: "0.07em",
+ marginBottom: 6,
+};
+
+const altLabelChipStyle: React.CSSProperties = {
+ padding: "3px 8px",
+ borderRadius: 999,
+ background: "rgba(255,255,255,0.05)",
+ border: "1px solid rgba(255,255,255,0.08)",
+ color: "#8fa8c6",
+ fontSize: 11,
+};
+
+const emptyStateStyle: React.CSSProperties = {
+ display: "flex",
+ flexDirection: "column",
+ alignItems: "center",
+ justifyContent: "center",
+ padding: 40,
+ height: "100%",
+};
diff --git a/semantica-explorer/src/workspaces/ReasoningWorkspace.tsx b/explorer/src/workspaces/ReasoningWorkspace.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/ReasoningWorkspace.tsx
rename to explorer/src/workspaces/ReasoningWorkspace.tsx
diff --git a/semantica-explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx b/explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx
rename to explorer/src/workspaces/SparqlWorkspace/SparqlWorkspace.tsx
diff --git a/semantica-explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx b/explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx
rename to explorer/src/workspaces/VocabularyWorkspace/ConceptTree.tsx
diff --git a/semantica-explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx b/explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx
rename to explorer/src/workspaces/VocabularyWorkspace/ImportDropzone.tsx
diff --git a/semantica-explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx b/explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx
rename to explorer/src/workspaces/VocabularyWorkspace/PropertyPanel.tsx
diff --git a/semantica-explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx b/explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx
rename to explorer/src/workspaces/VocabularyWorkspace/Sidebar.tsx
diff --git a/semantica-explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx b/explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx
similarity index 100%
rename from semantica-explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx
rename to explorer/src/workspaces/VocabularyWorkspace/VocabularyWorkspace.tsx
diff --git a/semantica-explorer/src/workspaces/VocabularyWorkspace/queries.ts b/explorer/src/workspaces/VocabularyWorkspace/queries.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/VocabularyWorkspace/queries.ts
rename to explorer/src/workspaces/VocabularyWorkspace/queries.ts
diff --git a/semantica-explorer/src/workspaces/VocabularyWorkspace/types.ts b/explorer/src/workspaces/VocabularyWorkspace/types.ts
similarity index 100%
rename from semantica-explorer/src/workspaces/VocabularyWorkspace/types.ts
rename to explorer/src/workspaces/VocabularyWorkspace/types.ts
diff --git a/semantica-explorer/tests/graphStore.multi-edge.test.mjs b/explorer/tests/graphStore.multi-edge.test.mjs
similarity index 100%
rename from semantica-explorer/tests/graphStore.multi-edge.test.mjs
rename to explorer/tests/graphStore.multi-edge.test.mjs
diff --git a/semantica-explorer/tsconfig.app.json b/explorer/tsconfig.app.json
similarity index 100%
rename from semantica-explorer/tsconfig.app.json
rename to explorer/tsconfig.app.json
diff --git a/semantica-explorer/tsconfig.json b/explorer/tsconfig.json
similarity index 100%
rename from semantica-explorer/tsconfig.json
rename to explorer/tsconfig.json
diff --git a/semantica-explorer/tsconfig.node.json b/explorer/tsconfig.node.json
similarity index 100%
rename from semantica-explorer/tsconfig.node.json
rename to explorer/tsconfig.node.json
diff --git a/semantica-explorer/vite.config.ts b/explorer/vite.config.ts
similarity index 89%
rename from semantica-explorer/vite.config.ts
rename to explorer/vite.config.ts
index f741d247..2f87b55c 100644
--- a/semantica-explorer/vite.config.ts
+++ b/explorer/vite.config.ts
@@ -1,13 +1,15 @@
import { defineConfig } from 'vite'
-import react, { reactCompilerPreset } from '@vitejs/plugin-react'
-import babel from '@rolldown/plugin-babel'
+import react from '@vitejs/plugin-react'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
plugins: [
- react(),
- babel({ presets: [reactCompilerPreset()] })
+ react({
+ babel: {
+ plugins: ['babel-plugin-react-compiler'],
+ },
+ }),
],
base: '/',
diff --git a/mcp/README.md b/mcp/README.md
new file mode 100644
index 00000000..bdbf25e1
--- /dev/null
+++ b/mcp/README.md
@@ -0,0 +1,242 @@
+# Semantica MCP Server
+
+A fully modular [Model Context Protocol](https://modelcontextprotocol.io/) server for the Semantica knowledge graph.
+Connects Claude Code, Cursor, Windsurf, Cline, Continue, VS Code (GitHub Copilot), and any other MCP-compatible AI tool directly to your Semantica graph.
+
+---
+
+## Quick start
+
+```bash
+# From the repo root
+pip install -e ".[mcp]"
+
+# Test the server (type a JSON-RPC request, press Enter)
+python -m mcp
+```
+
+Or point your AI tool at it (see per-tool configs below).
+
+---
+
+## Transport
+
+**stdio** — the server reads newline-delimited JSON-RPC 2.0 from `stdin` and writes responses to `stdout`.
+Log/debug output goes to `stderr` only.
+
+```
+python -m mcp [--debug]
+```
+
+---
+
+## Tools (17 total)
+
+### Extraction
+
+| Tool | Description |
+|---|---|
+| `extract_entities` | Named entity recognition (NER) — people, places, orgs, concepts |
+| `extract_relations` | Relation extraction + (subject, predicate, object) triplets |
+| `extract_all` | Full pipeline: NER + coreference + relations + events + triplets |
+
+### Decision Intelligence
+
+| Tool | Description |
+|---|---|
+| `record_decision` | Record a decision with context, confidence, causal links |
+| `query_decisions` | Query decisions by natural language or structured filters |
+| `find_precedents` | Find past decisions similar to a scenario (hybrid similarity) |
+| `get_causal_chain` | Trace upstream/downstream causal chain from a decision |
+| `analyze_decision_impact` | Analyse downstream influence of a decision |
+
+### Knowledge Graph
+
+| Tool | Description |
+|---|---|
+| `add_entity` | Add a node/entity to the graph |
+| `add_relationship` | Add a directed edge between two entities |
+| `search_graph` | Search nodes by label or ID substring |
+| `get_graph_summary` | Node/edge counts, decision count, type breakdown |
+| `get_graph_analytics` | PageRank, betweenness, degree centrality, community detection |
+
+### Reasoning
+
+| Tool | Description |
+|---|---|
+| `run_reasoning` | Forward-chaining IF/THEN rules over facts |
+| `abductive_reasoning` | Generate plausible hypotheses for observations |
+
+### Export & Provenance
+
+| Tool | Description |
+|---|---|
+| `export_graph` | Export graph to JSON, CSV, GraphML, Parquet, Turtle, N-Triples, RDF/XML, JSON-LD |
+| `get_provenance` | Audit history and source lineage for a node |
+
+---
+
+## Resources (4 total)
+
+| URI | Description |
+|---|---|
+| `semantica://graph/summary` | Live node/edge counts and type breakdown |
+| `semantica://decisions/list` | Most recent 50 decisions |
+| `semantica://schema/info` | Schema version, node/edge types, tool names |
+| `semantica://ontology/schema` | Full ontology schema |
+
+---
+
+## Per-tool configuration
+
+### Claude Code (`~/.claude/settings.json`)
+
+```json
+{
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "mcp"],
+ "cwd": "/path/to/semantica"
+ }
+ }
+}
+```
+
+Or use the plugin bundle:
+```bash
+claude mcp add semantica python -m mcp --cwd /path/to/semantica
+```
+
+---
+
+### Cursor (`~/.cursor/mcp.json`)
+
+```json
+{
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "mcp"],
+ "cwd": "/path/to/semantica"
+ }
+ }
+}
+```
+
+---
+
+### Windsurf (`~/.codeium/windsurf/mcp_config.json`)
+
+```json
+{
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "mcp"],
+ "cwd": "/path/to/semantica"
+ }
+ }
+}
+```
+
+---
+
+### Cline (VS Code extension settings)
+
+In your VS Code `settings.json`:
+
+```json
+{
+ "cline.mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "mcp"],
+ "cwd": "/path/to/semantica"
+ }
+ }
+}
+```
+
+---
+
+### Continue (`~/.continue/config.json`)
+
+```json
+{
+ "mcpServers": [
+ {
+ "name": "semantica",
+ "command": "python",
+ "args": ["-m", "mcp"],
+ "cwd": "/path/to/semantica"
+ }
+ ]
+}
+```
+
+---
+
+### VS Code (GitHub Copilot) — `.vscode/mcp.json`
+
+```json
+{
+ "servers": {
+ "semantica": {
+ "type": "stdio",
+ "command": "python",
+ "args": ["-m", "mcp"],
+ "cwd": "${workspaceFolder}"
+ }
+ }
+}
+```
+
+---
+
+### Amazon Q Developer
+
+Add to your Q Developer MCP config:
+
+```json
+{
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "mcp"],
+ "cwd": "/path/to/semantica"
+ }
+ }
+}
+```
+
+---
+
+## Environment variables
+
+| Variable | Default | Description |
+|---|---|---|
+| `SEMANTICA_KG_PATH` | *(in-memory)* | Path to persist/load the graph (JSON file) |
+
+---
+
+## Package structure
+
+```
+mcp/
+├── __init__.py # Package entry, re-exports SemanticaMCPServer + main
+├── __main__.py # python -m mcp entry point
+├── server.py # SemanticaMCPServer class + stdio event loop
+├── session.py # Lazy ContextGraph singleton (get_graph / reset_graph)
+├── schemas.py # JSON Schema definitions for all tool inputs
+├── tools/
+│ ├── __init__.py # Assembles TOOL_DEFINITIONS list
+│ ├── extraction.py # NER, relation extraction, full pipeline
+│ ├── decisions.py # Record, query, precedents, causal chain, impact
+│ ├── graph.py # Add entity/relationship, search, summary, analytics
+│ ├── reasoning.py # Forward-chaining rules, abductive hypotheses
+│ └── export.py # Graph export (multi-format) + provenance
+└── resources/
+ ├── __init__.py # Re-exports RESOURCE_DEFINITIONS + handle_resource_read
+ └── registry.py # URI → handler map for the 4 semantica:// resources
+```
diff --git a/mcp/__init__.py b/mcp/__init__.py
new file mode 100644
index 00000000..5867cde4
--- /dev/null
+++ b/mcp/__init__.py
@@ -0,0 +1,27 @@
+"""
+Semantica MCP Server Package
+
+A full Model Context Protocol (MCP) server for Semantica — exposes knowledge graph
+construction, semantic extraction, decision intelligence, reasoning, analytics,
+and export capabilities as MCP tools and resources.
+
+Run the server:
+ python -m mcp.server # from repo root
+ python -m semantica.mcp_server # alias inside installed package
+
+Configure in Claude Desktop, Windsurf, Cline, Continue, VS Code:
+ {
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "mcp.server"],
+ "cwd": "/path/to/semantica"
+ }
+ }
+ }
+"""
+
+from .server import SemanticaMCPServer, main
+
+__all__ = ["SemanticaMCPServer", "main"]
+__version__ = "0.4.0"
diff --git a/mcp/__main__.py b/mcp/__main__.py
new file mode 100644
index 00000000..f219b7d3
--- /dev/null
+++ b/mcp/__main__.py
@@ -0,0 +1,5 @@
+"""Entry point: python -m mcp.server"""
+from mcp.server import main
+
+if __name__ == "__main__":
+ main()
diff --git a/mcp/resources/__init__.py b/mcp/resources/__init__.py
new file mode 100644
index 00000000..434c03d5
--- /dev/null
+++ b/mcp/resources/__init__.py
@@ -0,0 +1,8 @@
+"""
+MCP resource registry — static and dynamic resources exposed via resources/list
+and resources/read.
+"""
+
+from .registry import RESOURCE_DEFINITIONS, handle_resource_read
+
+__all__ = ["RESOURCE_DEFINITIONS", "handle_resource_read"]
diff --git a/mcp/resources/registry.py b/mcp/resources/registry.py
new file mode 100644
index 00000000..052ee8c4
--- /dev/null
+++ b/mcp/resources/registry.py
@@ -0,0 +1,153 @@
+"""
+Resource handlers for Semantica MCP resources.
+
+Each resource maps a semantica:// URI to a callable that returns
+{"uri": ..., "mimeType": ..., "text": ...}.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+
+from mcp.session import get_graph
+
+log = logging.getLogger("semantica.mcp.resources")
+
+
+def _read_graph_summary(uri: str) -> dict:
+ try:
+ graph = get_graph()
+ all_nodes = list(graph.find_nodes())
+ node_types: dict[str, int] = {}
+ for n in all_nodes:
+ t = str(n.get("type", "Unknown"))
+ node_types[t] = node_types.get(t, 0) + 1
+ edge_count = 0
+ if hasattr(graph, "edge_count"):
+ try:
+ edge_count = graph.edge_count()
+ except Exception as exc:
+ log.debug("Unable to read graph edge_count(); defaulting to 0: %s", exc)
+ data = {
+ "node_count": len(all_nodes),
+ "edge_count": edge_count,
+ "node_types": node_types,
+ }
+ except Exception as exc:
+ data = {"error": str(exc)}
+ return {"uri": uri, "mimeType": "application/json", "text": json.dumps(data, indent=2)}
+
+
+def _read_decisions_list(uri: str) -> dict:
+ try:
+ graph = get_graph()
+ nodes = list(graph.find_nodes(node_type="decision"))
+ decisions = [
+ {
+ "id": n.get("id"),
+ "category": n.get("category"),
+ "outcome": n.get("outcome"),
+ "scenario": str(n.get("scenario", ""))[:120],
+ }
+ for n in nodes[:50]
+ ]
+ data = {"decisions": decisions, "count": len(decisions)}
+ except Exception as exc:
+ data = {"error": str(exc), "decisions": []}
+ return {"uri": uri, "mimeType": "application/json", "text": json.dumps(data, indent=2)}
+
+
+def _read_schema_info(uri: str) -> dict:
+ info = {
+ "version": "0.4.0",
+ "node_types": [
+ "Entity", "decision", "Decision", "Event", "Concept",
+ "Person", "Organisation", "Location",
+ ],
+ "edge_types": [
+ "RELATED_TO", "CAUSED_BY", "LEADS_TO", "PART_OF",
+ "INSTANCE_OF", "SIMILAR_TO",
+ ],
+ "tools": [
+ "extract_entities", "extract_relations", "extract_all",
+ "record_decision", "query_decisions", "find_precedents",
+ "get_causal_chain", "analyze_decision_impact",
+ "add_entity", "add_relationship", "search_graph",
+ "get_graph_summary", "get_graph_analytics",
+ "run_reasoning", "abductive_reasoning",
+ "export_graph", "get_provenance",
+ ],
+ }
+ return {"uri": uri, "mimeType": "application/json", "text": json.dumps(info, indent=2)}
+
+
+def _read_ontology_schema(uri: str) -> dict:
+ try:
+ graph = get_graph()
+ try:
+ from semantica.ontology import OntologyManager
+ mgr = OntologyManager(graph_store=graph)
+ schema = mgr.get_schema()
+ text = json.dumps(schema, indent=2) if isinstance(schema, dict) else str(schema)
+ except (ImportError, AttributeError):
+ text = json.dumps({"message": "Ontology manager not available"}, indent=2)
+ except Exception as exc:
+ text = json.dumps({"error": str(exc)}, indent=2)
+ return {"uri": uri, "mimeType": "application/json", "text": text}
+
+
+# Map URI → handler
+_HANDLERS: dict[str, object] = {
+ "semantica://graph/summary": _read_graph_summary,
+ "semantica://decisions/list": _read_decisions_list,
+ "semantica://schema/info": _read_schema_info,
+ "semantica://ontology/schema": _read_ontology_schema,
+}
+
+RESOURCE_DEFINITIONS = [
+ {
+ "uri": "semantica://graph/summary",
+ "name": "Graph Summary",
+ "description": "High-level summary of the current knowledge graph: node/edge counts and type breakdown.",
+ "mimeType": "application/json",
+ },
+ {
+ "uri": "semantica://decisions/list",
+ "name": "Decision List",
+ "description": "Most recent decisions recorded in the knowledge graph (up to 50).",
+ "mimeType": "application/json",
+ },
+ {
+ "uri": "semantica://schema/info",
+ "name": "Schema Info",
+ "description": "Semantica schema version, supported node/edge types, and available tool names.",
+ "mimeType": "application/json",
+ },
+ {
+ "uri": "semantica://ontology/schema",
+ "name": "Ontology Schema",
+ "description": "Full ontology schema from the OntologyManager (concept hierarchy and constraints).",
+ "mimeType": "application/json",
+ },
+]
+
+
+def handle_resource_read(uri: str) -> dict:
+ """Dispatch a resources/read request to the appropriate handler."""
+ handler = _HANDLERS.get(uri)
+ if handler is None:
+ return {
+ "uri": uri,
+ "mimeType": "application/json",
+ "text": json.dumps({"error": f"Unknown resource URI: {uri}"}),
+ }
+ try:
+ return handler(uri) # type: ignore[call-arg]
+ except Exception as exc:
+ log.exception("resource_read failed for %s", uri)
+ return {
+ "uri": uri,
+ "mimeType": "application/json",
+ "text": json.dumps({"error": str(exc)}),
+ }
diff --git a/mcp/schemas.py b/mcp/schemas.py
new file mode 100644
index 00000000..5a9f8c7c
--- /dev/null
+++ b/mcp/schemas.py
@@ -0,0 +1,292 @@
+"""
+Input schema definitions for all MCP tools.
+
+Each entry is the JSON Schema object placed in the tool's ``inputSchema``
+field. Keeping them here avoids duplication across tool modules.
+"""
+
+EXTRACTION_TEXT = {
+ "type": "object",
+ "properties": {
+ "text": {
+ "type": "string",
+ "description": "Input text to process",
+ }
+ },
+ "required": ["text"],
+}
+
+EXTRACT_ENTITIES = EXTRACTION_TEXT
+
+EXTRACT_RELATIONS = EXTRACTION_TEXT
+
+EXTRACT_ALL = {
+ "type": "object",
+ "properties": {
+ "text": {"type": "string", "description": "Input text to process"},
+ "include_events": {
+ "type": "boolean",
+ "description": "Also extract events (default: true)",
+ },
+ "include_triplets": {
+ "type": "boolean",
+ "description": "Also extract (subject, predicate, object) triplets (default: true)",
+ },
+ },
+ "required": ["text"],
+}
+
+RECORD_DECISION = {
+ "type": "object",
+ "properties": {
+ "category": {
+ "type": "string",
+ "description": "Decision category, e.g. 'loan_approval', 'deployment'",
+ },
+ "scenario": {
+ "type": "string",
+ "description": "Natural-language description of the situation",
+ },
+ "reasoning": {
+ "type": "string",
+ "description": "Explanation of why this decision was made",
+ },
+ "outcome": {
+ "type": "string",
+ "description": "Decision result, e.g. 'approved', 'rejected', 'deferred'",
+ },
+ "confidence": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 1,
+ "description": "Confidence score between 0 and 1",
+ },
+ "decision_maker": {
+ "type": "string",
+ "description": "Who or what made the decision (default: mcp_client)",
+ },
+ "valid_from": {
+ "type": "string",
+ "description": "ISO 8601 validity start date (optional)",
+ },
+ "valid_until": {
+ "type": "string",
+ "description": "ISO 8601 validity end date (optional)",
+ },
+ },
+ "required": ["category", "scenario", "reasoning", "outcome", "confidence"],
+}
+
+QUERY_DECISIONS = {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "Natural language query (optional)",
+ },
+ "category": {
+ "type": "string",
+ "description": "Filter by exact category (optional)",
+ },
+ "outcome": {
+ "type": "string",
+ "description": "Filter by outcome value (optional)",
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 200,
+ "description": "Maximum number of results (default: 10)",
+ },
+ },
+}
+
+FIND_PRECEDENTS = {
+ "type": "object",
+ "properties": {
+ "scenario": {
+ "type": "string",
+ "description": "Scenario description to find similar past decisions for",
+ },
+ "max_results": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 50,
+ "description": "Maximum number of precedents to return (default: 5)",
+ },
+ },
+ "required": ["scenario"],
+}
+
+GET_CAUSAL_CHAIN = {
+ "type": "object",
+ "properties": {
+ "decision_id": {
+ "type": "string",
+ "description": "ID of the decision to trace",
+ },
+ "direction": {
+ "type": "string",
+ "enum": ["upstream", "downstream", "both"],
+ "description": "Trace direction (default: downstream)",
+ },
+ "max_depth": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 20,
+ "description": "Maximum chain depth (default: 5)",
+ },
+ },
+ "required": ["decision_id"],
+}
+
+ANALYZE_DECISION_IMPACT = {
+ "type": "object",
+ "properties": {
+ "decision_id": {
+ "type": "string",
+ "description": "ID of the decision to analyse",
+ },
+ },
+ "required": ["decision_id"],
+}
+
+ADD_ENTITY = {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "Unique node identifier",
+ },
+ "label": {
+ "type": "string",
+ "description": "Human-readable label (defaults to id)",
+ },
+ "type": {
+ "type": "string",
+ "description": "Node type, e.g. 'Person', 'Organisation', 'Concept'",
+ },
+ "metadata": {
+ "type": "object",
+ "description": "Additional key-value properties",
+ },
+ },
+ "required": ["id"],
+}
+
+ADD_RELATIONSHIP = {
+ "type": "object",
+ "properties": {
+ "source": {
+ "type": "string",
+ "description": "Source node ID",
+ },
+ "target": {
+ "type": "string",
+ "description": "Target node ID",
+ },
+ "type": {
+ "type": "string",
+ "description": "Relationship type, e.g. 'WORKS_AT', 'CAUSED_BY'",
+ },
+ "metadata": {
+ "type": "object",
+ "description": "Additional edge properties",
+ },
+ },
+ "required": ["source", "target"],
+}
+
+SEARCH_GRAPH = {
+ "type": "object",
+ "properties": {
+ "query": {
+ "type": "string",
+ "description": "Search term or phrase",
+ },
+ "node_type": {
+ "type": "string",
+ "description": "Filter by node type (optional)",
+ },
+ "limit": {
+ "type": "integer",
+ "description": "Max results (default: 20)",
+ },
+ },
+ "required": ["query"],
+}
+
+RUN_REASONING = {
+ "type": "object",
+ "properties": {
+ "facts": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Fact strings, e.g. ['Person(John)', 'Employee(John)']",
+ },
+ "rules": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "IF/THEN rule strings, e.g. ['IF Employee(?x) THEN Worker(?x)']",
+ },
+ },
+ "required": ["facts", "rules"],
+}
+
+ABDUCTIVE_REASONING = {
+ "type": "object",
+ "properties": {
+ "observations": {
+ "type": "array",
+ "items": {"type": "string"},
+ "description": "Observed facts to explain",
+ },
+ "max_hypotheses": {
+ "type": "integer",
+ "description": "Max hypotheses to generate (default: 5)",
+ },
+ },
+ "required": ["observations"],
+}
+
+EXPORT_GRAPH = {
+ "type": "object",
+ "properties": {
+ "format": {
+ "type": "string",
+ "enum": ["turtle", "ttl", "nt", "xml", "json-ld", "json", "csv"],
+ "description": "Export format (default: json-ld)",
+ },
+ },
+}
+
+GET_PROVENANCE = {
+ "type": "object",
+ "properties": {
+ "entity_id": {
+ "type": "string",
+ "description": "Entity or node ID to get provenance for",
+ },
+ },
+ "required": ["entity_id"],
+}
+
+GET_ANALYTICS = {
+ "type": "object",
+ "properties": {
+ "metrics": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": ["pagerank", "betweenness", "communities", "degree", "all"],
+ },
+ "description": "Analytics to compute (default: ['all'])",
+ },
+ "top_n": {
+ "type": "integer",
+ "description": "Top N nodes to return per metric (default: 10)",
+ },
+ },
+}
+
+EMPTY = {"type": "object", "properties": {}}
diff --git a/mcp/server.py b/mcp/server.py
new file mode 100644
index 00000000..371343a5
--- /dev/null
+++ b/mcp/server.py
@@ -0,0 +1,223 @@
+"""
+Semantica MCP Server — JSON-RPC 2.0 over stdio.
+
+Implements the Model Context Protocol so any MCP-compatible AI tool
+(Claude Code, Cursor, Windsurf, Cline, Continue, VS Code Copilot, etc.)
+can interact with the Semantica knowledge graph.
+
+Run:
+ python -m mcp # via __main__.py
+ python -m mcp.server # direct
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import sys
+from typing import Any
+
+from mcp.resources import RESOURCE_DEFINITIONS, handle_resource_read
+from mcp.tools import TOOL_DEFINITIONS
+
+log = logging.getLogger("semantica.mcp.server")
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+def _ok(request_id: Any, result: Any) -> dict:
+ return {"jsonrpc": "2.0", "id": request_id, "result": result}
+
+
+def _err(request_id: Any, code: int, message: str, data: Any = None) -> dict:
+ error: dict = {"code": code, "message": message}
+ if data is not None:
+ error["data"] = data
+ return {"jsonrpc": "2.0", "id": request_id, "error": error}
+
+
+# JSON-RPC error codes
+_PARSE_ERROR = -32700
+_METHOD_NOT_FOUND = -32601
+_INVALID_PARAMS = -32602
+_INTERNAL_ERROR = -32603
+
+# ---------------------------------------------------------------------------
+# Tool dispatch index
+# ---------------------------------------------------------------------------
+
+_TOOL_INDEX: dict[str, dict] = {t["name"]: t for t in TOOL_DEFINITIONS}
+
+
+# ---------------------------------------------------------------------------
+# Request handlers
+# ---------------------------------------------------------------------------
+
+def _handle_initialize(req_id: Any, params: dict) -> dict:
+ return _ok(req_id, {
+ "protocolVersion": "2024-11-05",
+ "capabilities": {
+ "tools": {},
+ "resources": {},
+ },
+ "serverInfo": {
+ "name": "semantica-mcp",
+ "version": "0.4.0",
+ },
+ })
+
+
+def _handle_tools_list(req_id: Any, _params: dict) -> dict:
+ tools = [
+ {
+ "name": t["name"],
+ "description": t["description"],
+ "inputSchema": t["inputSchema"],
+ }
+ for t in TOOL_DEFINITIONS
+ ]
+ return _ok(req_id, {"tools": tools})
+
+
+def _handle_tools_call(req_id: Any, params: dict) -> dict:
+ name = params.get("name", "")
+ args = params.get("arguments", {}) or {}
+
+ tool = _TOOL_INDEX.get(name)
+ if tool is None:
+ return _err(req_id, _METHOD_NOT_FOUND, f"Unknown tool: {name}")
+
+ try:
+ result = tool["_handler"](args)
+ except Exception as exc:
+ log.exception("Tool %s raised an exception", name)
+ return _err(req_id, _INTERNAL_ERROR, str(exc))
+
+ # MCP spec: content must be a list of content items
+ return _ok(req_id, {
+ "content": [{"type": "text", "text": json.dumps(result, ensure_ascii=False)}],
+ "isError": "error" in result,
+ })
+
+
+def _handle_resources_list(req_id: Any, _params: dict) -> dict:
+ return _ok(req_id, {"resources": RESOURCE_DEFINITIONS})
+
+
+def _handle_resources_read(req_id: Any, params: dict) -> dict:
+ uri = params.get("uri", "").strip()
+ if not uri:
+ return _err(req_id, _INVALID_PARAMS, "uri is required")
+ resource = handle_resource_read(uri)
+ return _ok(req_id, {
+ "contents": [
+ {
+ "uri": resource["uri"],
+ "mimeType": resource.get("mimeType", "application/json"),
+ "text": resource.get("text", ""),
+ }
+ ]
+ })
+
+
+def _handle_ping(req_id: Any, _params: dict) -> dict:
+ return _ok(req_id, {})
+
+
+# ---------------------------------------------------------------------------
+# Dispatch table
+# ---------------------------------------------------------------------------
+
+_DISPATCH = {
+ "initialize": _handle_initialize,
+ "tools/list": _handle_tools_list,
+ "tools/call": _handle_tools_call,
+ "resources/list": _handle_resources_list,
+ "resources/read": _handle_resources_read,
+ "ping": _handle_ping,
+}
+
+
+# ---------------------------------------------------------------------------
+# Main server class
+# ---------------------------------------------------------------------------
+
+class SemanticaMCPServer:
+ """Semantica MCP server — reads JSON-RPC requests from stdin, writes to stdout."""
+
+ def __init__(self, *, debug: bool = False) -> None:
+ level = logging.DEBUG if debug else logging.WARNING
+ logging.basicConfig(stream=sys.stderr, level=level,
+ format="%(name)s %(levelname)s %(message)s")
+
+ # ------------------------------------------------------------------
+ def dispatch(self, request: dict) -> dict | None:
+ """Process one JSON-RPC request and return a response dict (or None for notifications)."""
+ req_id = request.get("id") # None for notifications
+ method = request.get("method", "")
+ params = request.get("params") or {}
+
+ handler = _DISPATCH.get(method)
+ if handler is None:
+ if req_id is None:
+ return None # Notification — ignore unknown methods silently
+ return _err(req_id, _METHOD_NOT_FOUND, f"Method not found: {method}")
+
+ try:
+ return handler(req_id, params)
+ except Exception as exc:
+ log.exception("Unhandled error in method %s", method)
+ if req_id is None:
+ return None
+ return _err(req_id, _INTERNAL_ERROR, str(exc))
+
+ # ------------------------------------------------------------------
+ def run(self) -> None:
+ """Start the stdio event loop."""
+ log.info("Semantica MCP server starting (stdio)")
+ for raw_line in sys.stdin:
+ raw_line = raw_line.strip()
+ if not raw_line:
+ continue
+ try:
+ request = json.loads(raw_line)
+ except json.JSONDecodeError as exc:
+ response = _err(None, _PARSE_ERROR, f"Parse error: {exc}")
+ _write(response)
+ continue
+
+ if isinstance(request, list):
+ # Batch request
+ responses = []
+ for req in request:
+ resp = self.dispatch(req)
+ if resp is not None:
+ responses.append(resp)
+ if responses:
+ _write(responses)
+ else:
+ resp = self.dispatch(request)
+ if resp is not None:
+ _write(resp)
+
+
+def _write(obj: Any) -> None:
+ sys.stdout.write(json.dumps(obj, ensure_ascii=False) + "\n")
+ sys.stdout.flush()
+
+
+# ---------------------------------------------------------------------------
+# Entry point
+# ---------------------------------------------------------------------------
+
+def main() -> None:
+ import argparse
+ parser = argparse.ArgumentParser(description="Semantica MCP Server")
+ parser.add_argument("--debug", action="store_true", help="Enable debug logging")
+ args = parser.parse_args()
+ SemanticaMCPServer(debug=args.debug).run()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/mcp/session.py b/mcp/session.py
new file mode 100644
index 00000000..6569d1ad
--- /dev/null
+++ b/mcp/session.py
@@ -0,0 +1,47 @@
+"""
+Shared graph session — lazy singleton across all tool handlers.
+
+The graph is initialised once on first access and shared for the
+lifetime of the MCP server process. Set SEMANTICA_KG_PATH to
+automatically load a persisted graph on start.
+"""
+
+from __future__ import annotations
+
+import logging
+import os
+from typing import Any, Optional
+
+log = logging.getLogger("semantica.mcp.session")
+
+_graph: Optional[Any] = None
+
+
+def get_graph() -> Any:
+ """
+ Return the shared ContextGraph instance, creating it on first call.
+
+ The graph is created with advanced_analytics=True so all centrality,
+ community-detection, and embedding features are available.
+ """
+ global _graph
+ if _graph is None:
+ from semantica.context import ContextGraph
+
+ _graph = ContextGraph(advanced_analytics=True)
+
+ kg_path = os.environ.get("SEMANTICA_KG_PATH", "").strip()
+ if kg_path and os.path.exists(kg_path):
+ try:
+ _graph.load(kg_path)
+ log.info("Graph loaded from %s", kg_path)
+ except Exception as exc:
+ log.warning("Could not load graph from %s: %s", kg_path, exc)
+
+ return _graph
+
+
+def reset_graph() -> None:
+ """Reset the singleton (mainly useful in tests)."""
+ global _graph
+ _graph = None
diff --git a/mcp/tools/__init__.py b/mcp/tools/__init__.py
new file mode 100644
index 00000000..333cb133
--- /dev/null
+++ b/mcp/tools/__init__.py
@@ -0,0 +1,22 @@
+"""
+MCP tool registry — imports all tool handlers and assembles TOOL_DEFINITIONS.
+
+Each module under mcp/tools/ registers its handlers here.
+"""
+
+from .decisions import DECISION_TOOLS
+from .export import EXPORT_TOOLS
+from .extraction import EXTRACTION_TOOLS
+from .graph import GRAPH_TOOLS
+from .reasoning import REASONING_TOOLS
+
+# Ordered list — exposed to the MCP client via tools/list
+TOOL_DEFINITIONS = (
+ EXTRACTION_TOOLS
+ + DECISION_TOOLS
+ + GRAPH_TOOLS
+ + REASONING_TOOLS
+ + EXPORT_TOOLS
+)
+
+__all__ = ["TOOL_DEFINITIONS"]
diff --git a/mcp/tools/decisions.py b/mcp/tools/decisions.py
new file mode 100644
index 00000000..dc894efc
--- /dev/null
+++ b/mcp/tools/decisions.py
@@ -0,0 +1,166 @@
+"""
+Decision intelligence tools — record, query, precedents, causal chain, impact.
+"""
+
+from __future__ import annotations
+
+import logging
+
+from mcp.schemas import (
+ ANALYZE_DECISION_IMPACT,
+ FIND_PRECEDENTS,
+ GET_CAUSAL_CHAIN,
+ QUERY_DECISIONS,
+ RECORD_DECISION,
+)
+from mcp.session import get_graph
+
+log = logging.getLogger("semantica.mcp.tools.decisions")
+
+
+def handle_record_decision(args: dict) -> dict:
+ """Record a decision with full context into the knowledge graph."""
+ required = ["category", "scenario", "reasoning", "outcome", "confidence"]
+ missing = [f for f in required if f not in args]
+ if missing:
+ return {"error": f"Missing required fields: {', '.join(missing)}"}
+ try:
+ graph = get_graph()
+ decision_id = graph.record_decision(
+ category=str(args["category"]),
+ scenario=str(args["scenario"]),
+ reasoning=str(args["reasoning"]),
+ outcome=str(args["outcome"]),
+ confidence=float(args["confidence"]),
+ entities=args.get("entities", []),
+ decision_maker=args.get("decision_maker", "mcp_client"),
+ valid_from=args.get("valid_from"),
+ valid_until=args.get("valid_until"),
+ )
+ return {
+ "decision_id": decision_id,
+ "status": "recorded",
+ "category": args["category"],
+ "outcome": args["outcome"],
+ }
+ except Exception as exc:
+ log.exception("record_decision failed")
+ return {"error": str(exc)}
+
+
+def handle_query_decisions(args: dict) -> dict:
+ """Query recorded decisions by natural language or structured filters."""
+ query = args.get("query", "").strip()
+ category = args.get("category", "").strip()
+ outcome_filter = args.get("outcome", "").strip()
+ limit = int(args.get("limit", 10))
+ try:
+ graph = get_graph()
+ if query:
+ results = graph.find_similar_decisions(query, max_results=limit)
+ decisions = results if isinstance(results, list) else list(results)
+ else:
+ nodes = graph.find_nodes(node_type="decision")
+ decisions = list(nodes)[:limit * 5] # over-fetch for filtering
+ if category:
+ decisions = [d for d in decisions if d.get("category") == category]
+ if outcome_filter:
+ decisions = [d for d in decisions if d.get("outcome") == outcome_filter]
+ decisions = decisions[:limit]
+ return {"decisions": decisions, "count": len(decisions)}
+ except Exception as exc:
+ log.exception("query_decisions failed")
+ return {"error": str(exc), "decisions": []}
+
+
+def handle_find_precedents(args: dict) -> dict:
+ """Find past decisions similar to a given scenario using hybrid similarity search."""
+ scenario = args.get("scenario", "").strip()
+ if not scenario:
+ return {"error": "scenario is required", "precedents": []}
+ max_results = int(args.get("max_results", 5))
+ try:
+ graph = get_graph()
+ precedents = graph.find_similar_decisions(scenario, max_results=max_results)
+ results = precedents if isinstance(precedents, list) else list(precedents)
+ return {"precedents": results, "count": len(results)}
+ except Exception as exc:
+ log.exception("find_precedents failed")
+ return {"error": str(exc), "precedents": []}
+
+
+def handle_get_causal_chain(args: dict) -> dict:
+ """Trace the upstream or downstream causal chain from a decision."""
+ decision_id = args.get("decision_id", "").strip()
+ if not decision_id:
+ return {"error": "decision_id is required", "chain": []}
+ direction = args.get("direction", "downstream")
+ max_depth = int(args.get("max_depth", 5))
+ try:
+ graph = get_graph()
+ try:
+ from semantica.context.causal_analyzer import CausalChainAnalyzer
+ analyzer = CausalChainAnalyzer(graph_store=graph)
+ chain = analyzer.get_causal_chain(
+ decision_id, direction=direction, max_depth=max_depth
+ )
+ except (ImportError, AttributeError):
+ chain = graph.get_causal_chain(decision_id) if hasattr(graph, "get_causal_chain") else []
+ result = chain if isinstance(chain, list) else list(chain)
+ return {"chain": result, "count": len(result), "direction": direction}
+ except Exception as exc:
+ log.exception("get_causal_chain failed")
+ return {"error": str(exc), "chain": []}
+
+
+def handle_analyze_decision_impact(args: dict) -> dict:
+ """Analyse the downstream impact of a decision on the graph."""
+ decision_id = args.get("decision_id", "").strip()
+ if not decision_id:
+ return {"error": "decision_id is required"}
+ try:
+ graph = get_graph()
+ if hasattr(graph, "analyze_decision_impact"):
+ impact = graph.analyze_decision_impact(decision_id)
+ elif hasattr(graph, "analyze_decision_influence"):
+ impact = graph.analyze_decision_influence(decision_id)
+ else:
+ impact = {"message": "impact analysis not available on this graph instance"}
+ return {"decision_id": decision_id, "impact": impact}
+ except Exception as exc:
+ log.exception("analyze_decision_impact failed")
+ return {"error": str(exc)}
+
+
+DECISION_TOOLS = [
+ {
+ "name": "record_decision",
+ "description": "Record a decision with full context, causal links, and metadata into the Semantica knowledge graph.",
+ "inputSchema": RECORD_DECISION,
+ "_handler": handle_record_decision,
+ },
+ {
+ "name": "query_decisions",
+ "description": "Query recorded decisions by natural language, category, or outcome filter.",
+ "inputSchema": QUERY_DECISIONS,
+ "_handler": handle_query_decisions,
+ },
+ {
+ "name": "find_precedents",
+ "description": "Find past decisions similar to a given scenario using hybrid similarity search.",
+ "inputSchema": FIND_PRECEDENTS,
+ "_handler": handle_find_precedents,
+ },
+ {
+ "name": "get_causal_chain",
+ "description": "Trace the causal chain upstream or downstream from a recorded decision.",
+ "inputSchema": GET_CAUSAL_CHAIN,
+ "_handler": handle_get_causal_chain,
+ },
+ {
+ "name": "analyze_decision_impact",
+ "description": "Analyse the downstream impact and influence of a decision across the knowledge graph.",
+ "inputSchema": ANALYZE_DECISION_IMPACT,
+ "_handler": handle_analyze_decision_impact,
+ },
+]
diff --git a/mcp/tools/export.py b/mcp/tools/export.py
new file mode 100644
index 00000000..f435bf18
--- /dev/null
+++ b/mcp/tools/export.py
@@ -0,0 +1,150 @@
+"""
+Export tools — graph export (JSON/RDF/CSV/GraphML/Parquet) and provenance.
+"""
+
+from __future__ import annotations
+
+import logging
+
+from mcp.schemas import EXPORT_GRAPH, GET_PROVENANCE
+from mcp.session import get_graph
+
+log = logging.getLogger("semantica.mcp.tools.export")
+
+_FORMAT_ALIASES: dict[str, str] = {
+ "ttl": "turtle",
+ "turtle": "turtle",
+ "nt": "nt",
+ "xml": "xml",
+ "json-ld": "json-ld",
+ "jsonld": "json-ld",
+}
+
+
+def handle_export_graph(args: dict) -> dict:
+ """Export the knowledge graph to a structured format."""
+ fmt = str(args.get("format", "json")).lower().strip()
+ include_metadata = bool(args.get("include_metadata", True))
+ try:
+ graph = get_graph()
+
+ if fmt == "json":
+ nodes = list(graph.find_nodes())
+ edges: list = []
+ if hasattr(graph, "find_edges"):
+ try:
+ edges = list(graph.find_edges())
+ except Exception as exc:
+ log.debug("Failed to collect edges during JSON export; continuing with empty edges: %s", exc)
+ payload: dict = {"nodes": nodes, "edges": edges}
+ if include_metadata:
+ payload["meta"] = {
+ "node_count": len(nodes),
+ "edge_count": len(edges),
+ "format": "json",
+ }
+ return {"format": "json", "data": payload}
+
+ if fmt in ("csv",):
+ nodes = list(graph.find_nodes())
+ rows = []
+ for n in nodes:
+ rows.append(",".join([
+ str(n.get("id", "")),
+ str(n.get("label", "")),
+ str(n.get("type", "")),
+ ]))
+ header = "id,label,type"
+ return {"format": "csv", "data": header + "\n" + "\n".join(rows)}
+
+ if fmt in ("graphml",):
+ try:
+ from semantica.export import GraphMLExporter
+ exporter = GraphMLExporter()
+ data = exporter.export(graph)
+ return {"format": "graphml", "data": data}
+ except Exception as exc:
+ return {"error": f"GraphML export failed: {exc}"}
+
+ if fmt in ("parquet",):
+ try:
+ from semantica.export import ParquetExporter
+ exporter = ParquetExporter()
+ data = exporter.export(graph, include_metadata)
+ return {"format": "parquet", "data": str(data)}
+ except Exception as exc:
+ return {"error": f"Parquet export failed: {exc}"}
+
+ # RDF formats
+ rdf_fmt = _FORMAT_ALIASES.get(fmt)
+ if rdf_fmt:
+ try:
+ from semantica.export import RDFExporter
+ rdf_str = RDFExporter().export_to_rdf(graph, format=rdf_fmt)
+ return {"format": rdf_fmt, "data": rdf_str}
+ except Exception as exc:
+ return {"error": f"RDF export failed: {exc}"}
+
+ return {"error": f"Unsupported format '{fmt}'. Supported: json, csv, graphml, parquet, turtle, nt, xml, json-ld"}
+
+ except Exception as exc:
+ log.exception("export_graph failed")
+ return {"error": str(exc)}
+
+
+def handle_get_provenance(args: dict) -> dict:
+ """Retrieve the provenance / audit history for a node."""
+ node_id = args.get("node_id", "").strip()
+ if not node_id:
+ return {"error": "node_id is required", "provenance": []}
+ include_metadata = bool(args.get("include_metadata", True))
+ try:
+ graph = get_graph()
+
+ # Try ProvenanceTracker first
+ try:
+ from semantica.kg import ProvenanceTracker
+ tracker = ProvenanceTracker()
+ records = tracker.get_provenance(node_id)
+ result = records if isinstance(records, list) else list(records)
+ except (ImportError, AttributeError):
+ # Fallback: look for provenance on the node itself
+ nodes = list(graph.find_nodes())
+ matched = [n for n in nodes if n.get("id") == node_id]
+ if matched:
+ node = matched[0]
+ prov = node.get("provenance") or node.get("source") or node.get("metadata", {})
+ result = [prov] if prov else []
+ else:
+ result = []
+
+ payload: dict = {"node_id": node_id, "provenance": result, "count": len(result)}
+ if include_metadata and result:
+ payload["sources"] = list({
+ str(r.get("source", r.get("origin", "")))
+ for r in result
+ if isinstance(r, dict)
+ })
+ return payload
+ except Exception as exc:
+ log.exception("get_provenance failed")
+ return {"error": str(exc), "provenance": []}
+
+
+EXPORT_TOOLS = [
+ {
+ "name": "export_graph",
+ "description": (
+ "Export the Semantica knowledge graph to JSON, CSV, GraphML, Parquet, "
+ "Turtle (RDF), N-Triples, RDF/XML, or JSON-LD."
+ ),
+ "inputSchema": EXPORT_GRAPH,
+ "_handler": handle_export_graph,
+ },
+ {
+ "name": "get_provenance",
+ "description": "Retrieve the provenance and audit history for a specific node in the knowledge graph.",
+ "inputSchema": GET_PROVENANCE,
+ "_handler": handle_get_provenance,
+ },
+]
diff --git a/mcp/tools/extraction.py b/mcp/tools/extraction.py
new file mode 100644
index 00000000..7ccd385b
--- /dev/null
+++ b/mcp/tools/extraction.py
@@ -0,0 +1,168 @@
+"""
+Extraction tools — NER, relation extraction, event detection, triplets.
+"""
+
+from __future__ import annotations
+
+import logging
+from typing import Any
+
+from mcp.schemas import EXTRACT_ALL, EXTRACT_ENTITIES, EXTRACT_RELATIONS
+
+log = logging.getLogger("semantica.mcp.tools.extraction")
+
+
+def _clear_cache() -> None:
+ try:
+ from semantica.semantic_extract.cache import _result_cache
+ _result_cache.clear()
+ except Exception:
+ log.debug("Could not clear semantic_extract cache; continuing", exc_info=True)
+
+
+def handle_extract_entities(args: dict) -> dict:
+ """Extract named entities from text using Semantica NER."""
+ text = args.get("text", "").strip()
+ if not text:
+ return {"error": "text is required", "entities": []}
+ _clear_cache()
+ try:
+ from semantica.semantic_extract import NamedEntityRecognizer
+ entities = NamedEntityRecognizer().extract(text) or []
+ return {
+ "entities": [
+ {
+ "label": getattr(e, "label", str(e)),
+ "type": getattr(e, "type", None),
+ "start": getattr(e, "start", None),
+ "end": getattr(e, "end", None),
+ "confidence": getattr(e, "confidence", None),
+ }
+ for e in entities
+ ],
+ "count": len(entities),
+ }
+ except Exception as exc:
+ log.exception("extract_entities failed")
+ return {"error": str(exc), "entities": []}
+
+
+def handle_extract_relations(args: dict) -> dict:
+ """Extract relations and triplets from text."""
+ text = args.get("text", "").strip()
+ if not text:
+ return {"error": "text is required", "relations": [], "triplets": []}
+ _clear_cache()
+ try:
+ from semantica.semantic_extract import NamedEntityRecognizer, RelationExtractor, TripletExtractor
+ entities = NamedEntityRecognizer().extract(text) or []
+ relations = RelationExtractor().extract(text, entities) or []
+ triplets = TripletExtractor().extract(text) or []
+ return {
+ "relations": [
+ {
+ "source": getattr(r, "source", None),
+ "type": getattr(r, "type", None),
+ "target": getattr(r, "target", None),
+ "confidence": getattr(r, "confidence", None),
+ }
+ for r in relations
+ ],
+ "triplets": [
+ {
+ "subject": getattr(t, "subject", None),
+ "predicate": getattr(t, "predicate", None),
+ "object": getattr(t, "object", None),
+ }
+ for t in triplets
+ ],
+ "relation_count": len(relations),
+ "triplet_count": len(triplets),
+ }
+ except Exception as exc:
+ log.exception("extract_relations failed")
+ return {"error": str(exc), "relations": [], "triplets": []}
+
+
+def handle_extract_all(args: dict) -> dict:
+ """Run the full extraction pipeline: NER + relations + events + triplets."""
+ text = args.get("text", "").strip()
+ if not text:
+ return {"error": "text is required"}
+ include_events = args.get("include_events", True)
+ include_triplets = args.get("include_triplets", True)
+ _clear_cache()
+ result: dict[str, Any] = {}
+ try:
+ from semantica.semantic_extract import (
+ CoreferenceResolver,
+ EventDetector,
+ NamedEntityRecognizer,
+ RelationExtractor,
+ TripletExtractor,
+ )
+
+ entities = NamedEntityRecognizer().extract(text) or []
+ result["entities"] = [
+ {"label": getattr(e, "label", str(e)), "type": getattr(e, "type", None)}
+ for e in entities
+ ]
+
+ resolved = CoreferenceResolver().resolve(text)
+ relations = RelationExtractor().extract(resolved, entities) or []
+ result["relations"] = [
+ {"source": getattr(r, "source", None),
+ "type": getattr(r, "type", None),
+ "target": getattr(r, "target", None)}
+ for r in relations
+ ]
+
+ if include_events:
+ events = EventDetector().extract(text) or []
+ result["events"] = [
+ {"type": getattr(ev, "type", None),
+ "trigger": getattr(ev, "trigger", str(ev))}
+ for ev in events
+ ]
+
+ if include_triplets:
+ triplets = TripletExtractor().extract(resolved) or []
+ result["triplets"] = [
+ {"subject": getattr(t, "subject", None),
+ "predicate": getattr(t, "predicate", None),
+ "object": getattr(t, "object", None)}
+ for t in triplets
+ ]
+
+ result["summary"] = {
+ "entities": len(result.get("entities", [])),
+ "relations": len(result.get("relations", [])),
+ "events": len(result.get("events", [])),
+ "triplets": len(result.get("triplets", [])),
+ }
+ return result
+ except Exception as exc:
+ log.exception("extract_all failed")
+ return {"error": str(exc)}
+
+
+EXTRACTION_TOOLS = [
+ {
+ "name": "extract_entities",
+ "description": "Extract named entities (people, places, organisations, concepts) from text.",
+ "inputSchema": EXTRACT_ENTITIES,
+ "_handler": handle_extract_entities,
+ },
+ {
+ "name": "extract_relations",
+ "description": "Extract relations and (subject, predicate, object) triplets from text.",
+ "inputSchema": EXTRACT_RELATIONS,
+ "_handler": handle_extract_relations,
+ },
+ {
+ "name": "extract_all",
+ "description": "Run the full Semantica extraction pipeline: NER, coreference resolution, relation extraction, event detection, and triplet generation.",
+ "inputSchema": EXTRACT_ALL,
+ "_handler": handle_extract_all,
+ },
+]
diff --git a/mcp/tools/graph.py b/mcp/tools/graph.py
new file mode 100644
index 00000000..5508025b
--- /dev/null
+++ b/mcp/tools/graph.py
@@ -0,0 +1,187 @@
+"""
+Graph tools — add entities/relationships, search, analytics, summary.
+"""
+
+from __future__ import annotations
+
+import logging
+
+from mcp.schemas import ADD_ENTITY, ADD_RELATIONSHIP, EMPTY, GET_ANALYTICS, SEARCH_GRAPH
+from mcp.session import get_graph
+
+log = logging.getLogger("semantica.mcp.tools.graph")
+
+
+def handle_add_entity(args: dict) -> dict:
+ """Add a node/entity to the Semantica knowledge graph."""
+ node_id = args.get("id", "").strip()
+ if not node_id:
+ return {"error": "id is required"}
+ try:
+ graph = get_graph()
+ graph.add_node(
+ node_id=node_id,
+ label=args.get("label", node_id),
+ node_type=args.get("type", "Entity"),
+ metadata=args.get("metadata", {}),
+ )
+ return {"status": "added", "id": node_id, "type": args.get("type", "Entity")}
+ except Exception as exc:
+ log.exception("add_entity failed")
+ return {"error": str(exc)}
+
+
+def handle_add_relationship(args: dict) -> dict:
+ """Add a directed relationship (edge) between two entities."""
+ source = args.get("source", "").strip()
+ target = args.get("target", "").strip()
+ if not source or not target:
+ return {"error": "source and target are required"}
+ rel_type = args.get("type", "RELATED_TO")
+ try:
+ graph = get_graph()
+ graph.add_edge(
+ source_id=source,
+ target_id=target,
+ edge_type=rel_type,
+ metadata=args.get("metadata", {}),
+ )
+ return {"status": "added", "source": source, "target": target, "type": rel_type}
+ except Exception as exc:
+ log.exception("add_relationship failed")
+ return {"error": str(exc)}
+
+
+def handle_search_graph(args: dict) -> dict:
+ """Search nodes in the knowledge graph by label or metadata."""
+ query = args.get("query", "").strip()
+ if not query:
+ return {"error": "query is required", "results": []}
+ node_type = args.get("node_type", "").strip() or None
+ limit = int(args.get("limit", 20))
+ try:
+ graph = get_graph()
+ if node_type:
+ nodes = list(graph.find_nodes(node_type=node_type))
+ else:
+ nodes = list(graph.find_nodes())
+ q = query.lower()
+ matched = [
+ n for n in nodes
+ if q in str(n.get("label", "")).lower()
+ or q in str(n.get("id", "")).lower()
+ ][:limit]
+ return {"results": matched, "count": len(matched), "query": query}
+ except Exception as exc:
+ log.exception("search_graph failed")
+ return {"error": str(exc), "results": []}
+
+
+def handle_get_graph_summary(args: dict) -> dict: # noqa: ARG001
+ """Return a high-level summary of the current knowledge graph."""
+ try:
+ graph = get_graph()
+ all_nodes = list(graph.find_nodes())
+ decisions = [n for n in all_nodes if n.get("type") in ("decision", "Decision")]
+ node_types: dict[str, int] = {}
+ for n in all_nodes:
+ t = str(n.get("type", "Unknown"))
+ node_types[t] = node_types.get(t, 0) + 1
+ edge_count = 0
+ if hasattr(graph, "edge_count"):
+ try:
+ edge_count = graph.edge_count()
+ except Exception:
+ log.exception("graph.edge_count failed; defaulting edge_count to 0")
+ return {
+ "node_count": len(all_nodes),
+ "edge_count": edge_count,
+ "decision_count": len(decisions),
+ "node_types": node_types,
+ "graph_ready": True,
+ }
+ except Exception as exc:
+ log.exception("get_graph_summary failed")
+ return {"error": str(exc), "graph_ready": False}
+
+
+def handle_get_graph_analytics(args: dict) -> dict:
+ """Compute centrality, community detection, and other graph metrics."""
+ requested = args.get("metrics", ["all"])
+ top_n = int(args.get("top_n", 10))
+ compute_all = "all" in requested
+ result: dict = {}
+ try:
+ graph = get_graph()
+ from semantica.kg import CentralityCalculator, CommunityDetector
+
+ if compute_all or "pagerank" in requested:
+ try:
+ pr = CentralityCalculator().calculate_pagerank(graph)
+ items = pr.items() if hasattr(pr, "items") else []
+ result["pagerank"] = sorted(items, key=lambda x: x[1], reverse=True)[:top_n]
+ except Exception as exc:
+ result["pagerank_error"] = str(exc)
+
+ if compute_all or "betweenness" in requested:
+ try:
+ bc = CentralityCalculator().calculate_betweenness_centrality(graph)
+ items = bc.items() if hasattr(bc, "items") else []
+ result["betweenness"] = sorted(items, key=lambda x: x[1], reverse=True)[:top_n]
+ except Exception as exc:
+ result["betweenness_error"] = str(exc)
+
+ if compute_all or "communities" in requested:
+ try:
+ comms = CommunityDetector().detect_communities(graph)
+ result["community_count"] = len(comms) if isinstance(comms, (list, dict)) else 0
+ result["communities"] = comms if isinstance(comms, list) else []
+ except Exception as exc:
+ result["communities_error"] = str(exc)
+
+ if compute_all or "degree" in requested:
+ try:
+ deg = CentralityCalculator().calculate_degree_centrality(graph)
+ items = deg.items() if hasattr(deg, "items") else []
+ result["degree"] = sorted(items, key=lambda x: x[1], reverse=True)[:top_n]
+ except Exception as exc:
+ result["degree_error"] = str(exc)
+
+ return result
+ except Exception as exc:
+ log.exception("get_graph_analytics failed")
+ return {"error": str(exc)}
+
+
+GRAPH_TOOLS = [
+ {
+ "name": "add_entity",
+ "description": "Add a node or entity (person, place, concept, organisation) to the knowledge graph.",
+ "inputSchema": ADD_ENTITY,
+ "_handler": handle_add_entity,
+ },
+ {
+ "name": "add_relationship",
+ "description": "Add a directed relationship (edge) between two entities in the knowledge graph.",
+ "inputSchema": ADD_RELATIONSHIP,
+ "_handler": handle_add_relationship,
+ },
+ {
+ "name": "search_graph",
+ "description": "Search nodes in the knowledge graph by label or ID substring.",
+ "inputSchema": SEARCH_GRAPH,
+ "_handler": handle_search_graph,
+ },
+ {
+ "name": "get_graph_summary",
+ "description": "Return a high-level summary of the knowledge graph: node count, edge count, decision count, node type breakdown.",
+ "inputSchema": EMPTY,
+ "_handler": handle_get_graph_summary,
+ },
+ {
+ "name": "get_graph_analytics",
+ "description": "Compute PageRank centrality, betweenness centrality, degree centrality, and community detection over the knowledge graph.",
+ "inputSchema": GET_ANALYTICS,
+ "_handler": handle_get_graph_analytics,
+ },
+]
diff --git a/mcp/tools/reasoning.py b/mcp/tools/reasoning.py
new file mode 100644
index 00000000..98e888eb
--- /dev/null
+++ b/mcp/tools/reasoning.py
@@ -0,0 +1,73 @@
+"""
+Reasoning tools — forward chaining, abductive reasoning.
+"""
+
+from __future__ import annotations
+
+import logging
+
+from mcp.schemas import ABDUCTIVE_REASONING, RUN_REASONING
+
+log = logging.getLogger("semantica.mcp.tools.reasoning")
+
+
+def handle_run_reasoning(args: dict) -> dict:
+ """Run forward-chaining IF/THEN rules over facts to derive new knowledge."""
+ facts = args.get("facts", [])
+ rules = args.get("rules", [])
+ if not facts:
+ return {"error": "facts list is required", "derived_facts": []}
+ if not rules:
+ return {"error": "rules list is required", "derived_facts": []}
+ try:
+ from semantica.reasoning import Reasoner
+ reasoner = Reasoner()
+ for rule in rules:
+ reasoner.add_rule(str(rule))
+ derived = reasoner.infer_facts(facts)
+ result = derived if isinstance(derived, list) else list(derived)
+ return {
+ "derived_facts": result,
+ "count": len(result),
+ "input_facts": len(facts),
+ "rules_applied": len(rules),
+ }
+ except Exception as exc:
+ log.exception("run_reasoning failed")
+ return {"error": str(exc), "derived_facts": []}
+
+
+def handle_abductive_reasoning(args: dict) -> dict:
+ """Generate plausible hypotheses that explain a set of observations."""
+ observations = args.get("observations", [])
+ if not observations:
+ return {"error": "observations list is required", "hypotheses": []}
+ max_hypotheses = int(args.get("max_hypotheses", 5))
+ try:
+ from semantica.reasoning import AbductiveReasoner
+ reasoner = AbductiveReasoner()
+ hypotheses = reasoner.generate_hypotheses(observations)
+ result = hypotheses if isinstance(hypotheses, list) else list(hypotheses)
+ return {
+ "hypotheses": result[:max_hypotheses],
+ "count": min(len(result), max_hypotheses),
+ }
+ except Exception as exc:
+ log.exception("abductive_reasoning failed")
+ return {"error": str(exc), "hypotheses": []}
+
+
+REASONING_TOOLS = [
+ {
+ "name": "run_reasoning",
+ "description": "Run forward-chaining IF/THEN rules over a set of facts to derive new facts. E.g. facts=['Person(John)'], rules=['IF Person(?x) THEN Mortal(?x)'] → derives 'Mortal(John)'.",
+ "inputSchema": RUN_REASONING,
+ "_handler": handle_run_reasoning,
+ },
+ {
+ "name": "abductive_reasoning",
+ "description": "Generate plausible hypotheses that best explain a set of observed facts.",
+ "inputSchema": ABDUCTIVE_REASONING,
+ "_handler": handle_abductive_reasoning,
+ },
+]
diff --git a/plugins/.claude-plugin/README.md b/plugins/.claude-plugin/README.md
index 8aca646c..2b6beaad 100644
--- a/plugins/.claude-plugin/README.md
+++ b/plugins/.claude-plugin/README.md
@@ -2,13 +2,21 @@
Semantica ships a shared plugin bundle under `plugins/` with skills, agents, and hooks for knowledge graphs, context graphs, decision intelligence, reasoning, explainability, provenance, ontology, and export workflows.
-This README is for community users who want to install or reuse the plugin package across Claude, Cursor, and Codex.
+This README covers installation across every supported platform.
## Supported Platforms
-- Claude Code
-- Cursor
-- Codex
+| Platform | Method | Config file |
+|---|---|---|
+| Claude Code | Native plugin bundle | `plugins/.claude-plugin/plugin.json` |
+| Cursor | Native plugin bundle | `plugins/.cursor-plugin/plugin.json` |
+| Codex CLI | Native plugin bundle | `plugins/.codex-plugin/plugin.json` |
+| Windsurf | MCP server + plugin bundle | `plugins/.windsurf-plugin/plugin.json` |
+| Cline (VS Code) | MCP server + plugin bundle | `plugins/.cline-plugin/plugin.json` |
+| Continue | MCP server | `plugins/.continue-plugin/plugin.json` |
+| VS Code | MCP server | `plugins/.vscode-plugin/plugin.json` |
+| Claude Desktop | MCP server | — (see MCP section below) |
+| Any MCP client | MCP server | `python -m semantica.mcp_server` |
## Prerequisites
@@ -23,12 +31,16 @@ cd semantica
```text
plugins/
- skills/
- agents/
- hooks/
- .claude-plugin/
- .cursor-plugin/
- .codex-plugin/
+ skills/ ← 17 domain skills
+ agents/ ← 3 specialized agents
+ hooks/ ← hooks.json
+ .claude-plugin/ ← Claude Code manifest
+ .cursor-plugin/ ← Cursor manifest
+ .codex-plugin/ ← Codex CLI manifest
+ .windsurf-plugin/← Windsurf manifest + MCP config
+ .cline-plugin/ ← Cline manifest + MCP config
+ .continue-plugin/← Continue manifest + MCP config
+ .vscode-plugin/ ← VS Code manifest + MCP config
```
## Plugin Contents
@@ -128,6 +140,114 @@ After installing on any platform, these are good smoke tests:
4. `/semantica:explain decision `
5. `/semantica:validate graph`
+## MCP Server (Windsurf · Cline · Continue · VS Code · Claude Desktop · Any tool)
+
+Semantica includes a full MCP server (`semantica/mcp_server.py`) that exposes 12 tools and 3 resources over stdio — compatible with any MCP-aware tool.
+
+### Start the server
+
+```bash
+python -m semantica.mcp_server
+```
+
+### Configure in your tool
+
+**Claude Desktop** — `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
+
+```json
+{
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ }
+}
+```
+
+**Windsurf** — `~/.codeium/windsurf/mcp_config.json`:
+
+```json
+{
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ }
+}
+```
+
+**Cline** — Cline MCP settings panel → Add server:
+
+```json
+{
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+}
+```
+
+**Continue** — `~/.continue/config.json`:
+
+```json
+{
+ "mcpServers": [
+ {
+ "name": "semantica",
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ ]
+}
+```
+
+**VS Code** — `settings.json`:
+
+```json
+{
+ "mcp.servers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ }
+}
+```
+
+### Available MCP tools
+
+| Tool | Description |
+|---|---|
+| `extract_entities` | Named entity recognition from text |
+| `extract_relations` | Relation and triplet extraction from text |
+| `record_decision` | Record a decision with full context and metadata |
+| `query_decisions` | Query recorded decisions by natural language or category |
+| `find_precedents` | Find past decisions similar to a scenario |
+| `get_causal_chain` | Trace upstream/downstream causal chain from a decision |
+| `add_entity` | Add a node/entity to the knowledge graph |
+| `add_relationship` | Add a directed edge between two entities |
+| `run_reasoning` | Run IF/THEN rules over facts to derive new facts |
+| `get_graph_analytics` | PageRank centrality and community detection |
+| `export_graph` | Export graph as Turtle, JSON-LD, N-Triples, or JSON |
+| `get_graph_summary` | Node count, decision count, graph status |
+
+### Available MCP resources
+
+| URI | Description |
+|---|---|
+| `semantica://graph/summary` | High-level graph statistics |
+| `semantica://decisions/list` | All recorded decisions |
+| `semantica://schema/info` | Server info and capability list |
+
+### Environment variables
+
+| Variable | Description |
+|---|---|
+| `SEMANTICA_KG_PATH` | Path to a persisted graph to load on start |
+| `SEMANTICA_LOG_LEVEL` | Log level: DEBUG, INFO, WARNING (default: WARNING) |
+
## Community Notes
- Keep plugin name/version/keywords updated in each manifest before publishing.
diff --git a/plugins/.cline-plugin/README.md b/plugins/.cline-plugin/README.md
new file mode 100644
index 00000000..c097b82b
--- /dev/null
+++ b/plugins/.cline-plugin/README.md
@@ -0,0 +1,24 @@
+# Semantica — Cline Plugin
+
+Adds all 17 Semantica skills, 3 agents, and hook configuration to Cline (VS Code extension).
+
+## MCP Server Setup (recommended)
+
+In Cline settings, add a new MCP server:
+
+```json
+{
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"],
+ "env": {}
+ }
+}
+```
+
+Cline will discover all 12 Semantica tools automatically on connection.
+
+## Requirements
+
+- Python 3.8+
+- `pip install semantica`
diff --git a/plugins/.cline-plugin/marketplace.json b/plugins/.cline-plugin/marketplace.json
new file mode 100644
index 00000000..335fb8fc
--- /dev/null
+++ b/plugins/.cline-plugin/marketplace.json
@@ -0,0 +1,17 @@
+{
+ "name": "semantica-cline",
+ "plugins": [
+ {
+ "name": "semantica",
+ "description": "Semantica plugin for Cline: knowledge graph skills, reasoning, extraction, and visualization.",
+ "source": "./",
+ "category": "Productivity",
+ "tags": [
+ "knowledge-graph",
+ "reasoning",
+ "semantica",
+ "cline"
+ ]
+ }
+ ]
+}
diff --git a/plugins/.cline-plugin/plugin.json b/plugins/.cline-plugin/plugin.json
new file mode 100644
index 00000000..81c004c4
--- /dev/null
+++ b/plugins/.cline-plugin/plugin.json
@@ -0,0 +1,35 @@
+{
+ "name": "semantica-cline",
+ "displayName": "Semantica Cline Plugin",
+ "description": "Semantica plugin for Cline: knowledge graph skills, decision intelligence, reasoning, extraction, and visualization.",
+ "version": "0.1.0",
+ "author": {
+ "name": "Semantica Contributors"
+ },
+ "homepage": "https://github.com/Hawksight-AI/semantica",
+ "repository": "https://github.com/Hawksight-AI/semantica",
+ "license": "MIT",
+ "keywords": [
+ "semantica",
+ "knowledge graph",
+ "cline",
+ "context graphs",
+ "decision intelligence",
+ "explainability",
+ "causal analysis",
+ "provenance",
+ "ontology",
+ "graph analytics",
+ "semantic extraction",
+ "visualization",
+ "reasoning",
+ "mcp"
+ ],
+ "skills": "../skills",
+ "agents": "../agents",
+ "hooks": "../hooks/hooks.json",
+ "mcp": {
+ "server": "python -m semantica.mcp_server",
+ "transport": "stdio"
+ }
+}
diff --git a/plugins/.continue-plugin/README.md b/plugins/.continue-plugin/README.md
new file mode 100644
index 00000000..b1a4ca2d
--- /dev/null
+++ b/plugins/.continue-plugin/README.md
@@ -0,0 +1,26 @@
+# Semantica — Continue Plugin
+
+Adds Semantica as an MCP server and context provider to [Continue.dev](https://continue.dev).
+
+## MCP Server Setup
+
+Add to `~/.continue/config.json`:
+
+```json
+{
+ "mcpServers": [
+ {
+ "name": "semantica",
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ ]
+}
+```
+
+Continue will show all Semantica tools in the `@semantica` context provider dropdown.
+
+## Requirements
+
+- Python 3.8+
+- `pip install semantica`
diff --git a/plugins/.continue-plugin/marketplace.json b/plugins/.continue-plugin/marketplace.json
new file mode 100644
index 00000000..0ff7e099
--- /dev/null
+++ b/plugins/.continue-plugin/marketplace.json
@@ -0,0 +1,17 @@
+{
+ "name": "semantica-continue",
+ "plugins": [
+ {
+ "name": "semantica",
+ "description": "Semantica plugin for Continue.dev: knowledge graph context provider, reasoning, and extraction.",
+ "source": "./",
+ "category": "Productivity",
+ "tags": [
+ "knowledge-graph",
+ "reasoning",
+ "semantica",
+ "continue"
+ ]
+ }
+ ]
+}
diff --git a/plugins/.continue-plugin/plugin.json b/plugins/.continue-plugin/plugin.json
new file mode 100644
index 00000000..da93dc38
--- /dev/null
+++ b/plugins/.continue-plugin/plugin.json
@@ -0,0 +1,33 @@
+{
+ "name": "semantica-continue",
+ "displayName": "Semantica Continue Plugin",
+ "description": "Semantica plugin for Continue.dev: knowledge graph context provider, decision intelligence, reasoning, and semantic extraction.",
+ "version": "0.1.0",
+ "author": {
+ "name": "Semantica Contributors"
+ },
+ "homepage": "https://github.com/Hawksight-AI/semantica",
+ "repository": "https://github.com/Hawksight-AI/semantica",
+ "license": "MIT",
+ "keywords": [
+ "semantica",
+ "knowledge graph",
+ "continue",
+ "context provider",
+ "decision intelligence",
+ "explainability",
+ "causal analysis",
+ "provenance",
+ "ontology",
+ "semantic extraction",
+ "reasoning",
+ "mcp"
+ ],
+ "skills": "../skills",
+ "agents": "../agents",
+ "hooks": "../hooks/hooks.json",
+ "mcp": {
+ "server": "python -m semantica.mcp_server",
+ "transport": "stdio"
+ }
+}
diff --git a/plugins/.vscode-plugin/README.md b/plugins/.vscode-plugin/README.md
new file mode 100644
index 00000000..5a0b7531
--- /dev/null
+++ b/plugins/.vscode-plugin/README.md
@@ -0,0 +1,36 @@
+# Semantica — VS Code Plugin
+
+Adds Semantica as an MCP server to VS Code (via GitHub Copilot Chat or any MCP-aware extension).
+
+## MCP Server Setup
+
+Add to your VS Code `settings.json`:
+
+```json
+{
+ "github.copilot.chat.mcp.servers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ }
+}
+```
+
+Or if using the VS Code MCP extension directly:
+
+```json
+{
+ "mcp.servers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ }
+}
+```
+
+## Requirements
+
+- Python 3.8+
+- `pip install semantica`
diff --git a/plugins/.vscode-plugin/marketplace.json b/plugins/.vscode-plugin/marketplace.json
new file mode 100644
index 00000000..76f4b2a2
--- /dev/null
+++ b/plugins/.vscode-plugin/marketplace.json
@@ -0,0 +1,17 @@
+{
+ "name": "semantica-vscode",
+ "plugins": [
+ {
+ "name": "semantica",
+ "description": "Semantica plugin for VS Code: knowledge graph skills, reasoning, extraction, and visualization.",
+ "source": "./",
+ "category": "Productivity",
+ "tags": [
+ "knowledge-graph",
+ "reasoning",
+ "semantica",
+ "vscode"
+ ]
+ }
+ ]
+}
diff --git a/plugins/.vscode-plugin/plugin.json b/plugins/.vscode-plugin/plugin.json
new file mode 100644
index 00000000..a39c097e
--- /dev/null
+++ b/plugins/.vscode-plugin/plugin.json
@@ -0,0 +1,35 @@
+{
+ "name": "semantica-vscode",
+ "displayName": "Semantica VS Code Plugin",
+ "description": "Semantica plugin for VS Code: knowledge graph skills, decision intelligence, reasoning, extraction, and visualization via MCP server.",
+ "version": "0.1.0",
+ "author": {
+ "name": "Semantica Contributors"
+ },
+ "homepage": "https://github.com/Hawksight-AI/semantica",
+ "repository": "https://github.com/Hawksight-AI/semantica",
+ "license": "MIT",
+ "keywords": [
+ "semantica",
+ "knowledge graph",
+ "vscode",
+ "context graphs",
+ "decision intelligence",
+ "explainability",
+ "causal analysis",
+ "provenance",
+ "ontology",
+ "graph analytics",
+ "semantic extraction",
+ "visualization",
+ "reasoning",
+ "mcp"
+ ],
+ "skills": "../skills",
+ "agents": "../agents",
+ "hooks": "../hooks/hooks.json",
+ "mcp": {
+ "server": "python -m semantica.mcp_server",
+ "transport": "stdio"
+ }
+}
diff --git a/plugins/.windsurf-plugin/README.md b/plugins/.windsurf-plugin/README.md
new file mode 100644
index 00000000..4718e807
--- /dev/null
+++ b/plugins/.windsurf-plugin/README.md
@@ -0,0 +1,29 @@
+# Semantica — Windsurf Plugin
+
+Adds all 17 Semantica skills, 3 agents, and hook configuration to Windsurf.
+
+## MCP Server Setup (recommended)
+
+Add to your Windsurf MCP config (`~/.codeium/windsurf/mcp_config.json`):
+
+```json
+{
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ }
+}
+```
+
+Windsurf will then have access to all 12 Semantica tools (extract, record_decision, query_decisions, find_precedents, get_causal_chain, add_entity, add_relationship, run_reasoning, get_graph_analytics, export_graph, and more) directly in the AI panel.
+
+## Skills
+
+All 17 skills under `plugins/skills/` are available as slash commands once the plugin is loaded.
+
+## Requirements
+
+- Python 3.8+
+- `pip install semantica`
diff --git a/plugins/.windsurf-plugin/marketplace.json b/plugins/.windsurf-plugin/marketplace.json
new file mode 100644
index 00000000..e5ea82db
--- /dev/null
+++ b/plugins/.windsurf-plugin/marketplace.json
@@ -0,0 +1,17 @@
+{
+ "name": "semantica-windsurf",
+ "plugins": [
+ {
+ "name": "semantica",
+ "description": "Semantica plugin for Windsurf: knowledge graph skills, reasoning, extraction, and visualization.",
+ "source": "./",
+ "category": "Productivity",
+ "tags": [
+ "knowledge-graph",
+ "reasoning",
+ "semantica",
+ "windsurf"
+ ]
+ }
+ ]
+}
diff --git a/plugins/.windsurf-plugin/plugin.json b/plugins/.windsurf-plugin/plugin.json
new file mode 100644
index 00000000..cbf45713
--- /dev/null
+++ b/plugins/.windsurf-plugin/plugin.json
@@ -0,0 +1,35 @@
+{
+ "name": "semantica-windsurf",
+ "displayName": "Semantica Windsurf Plugin",
+ "description": "Semantica plugin for Windsurf: knowledge graph skills, decision intelligence, reasoning, extraction, and visualization.",
+ "version": "0.1.0",
+ "author": {
+ "name": "Semantica Contributors"
+ },
+ "homepage": "https://github.com/Hawksight-AI/semantica",
+ "repository": "https://github.com/Hawksight-AI/semantica",
+ "license": "MIT",
+ "keywords": [
+ "semantica",
+ "knowledge graph",
+ "windsurf",
+ "context graphs",
+ "decision intelligence",
+ "explainability",
+ "causal analysis",
+ "provenance",
+ "ontology",
+ "graph analytics",
+ "semantic extraction",
+ "visualization",
+ "reasoning",
+ "mcp"
+ ],
+ "skills": "../skills",
+ "agents": "../agents",
+ "hooks": "../hooks/hooks.json",
+ "mcp": {
+ "server": "python -m semantica.mcp_server",
+ "transport": "stdio"
+ }
+}
diff --git a/semantica-explorer/README.md b/semantica-explorer/README.md
deleted file mode 100644
index c3c21da2..00000000
--- a/semantica-explorer/README.md
+++ /dev/null
@@ -1,75 +0,0 @@
-# React + TypeScript + Vite
-
-This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
-
-Currently, two official plugins are available:
-
-- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
-- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
-
-## React Compiler
-
-The React Compiler is enabled on this template. See [this documentation](https://react.dev/learn/react-compiler) for more information.
-
-Note: This will impact Vite dev & build performances.
-
-## Expanding the ESLint configuration
-
-If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
-
-```js
-export default defineConfig([
- globalIgnores(['dist']),
- {
- files: ['**/*.{ts,tsx}'],
- extends: [
- // Other configs...
-
- // Remove tseslint.configs.recommended and replace with this
- tseslint.configs.recommendedTypeChecked,
- // Alternatively, use this for stricter rules
- tseslint.configs.strictTypeChecked,
- // Optionally, add this for stylistic rules
- tseslint.configs.stylisticTypeChecked,
-
- // Other configs...
- ],
- languageOptions: {
- parserOptions: {
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
- tsconfigRootDir: import.meta.dirname,
- },
- // other options...
- },
- },
-])
-```
-
-You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
-
-```js
-// eslint.config.js
-import reactX from 'eslint-plugin-react-x'
-import reactDom from 'eslint-plugin-react-dom'
-
-export default defineConfig([
- globalIgnores(['dist']),
- {
- files: ['**/*.{ts,tsx}'],
- extends: [
- // Other configs...
- // Enable lint rules for React
- reactX.configs['recommended-typescript'],
- // Enable lint rules for React DOM
- reactDom.configs.recommended,
- ],
- languageOptions: {
- parserOptions: {
- project: ['./tsconfig.node.json', './tsconfig.app.json'],
- tsconfigRootDir: import.meta.dirname,
- },
- // other options...
- },
- },
-])
-```
diff --git a/semantica-explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx b/semantica-explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx
deleted file mode 100644
index 07b38f82..00000000
--- a/semantica-explorer/src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-/**
- * src/workspaces/DecisionWorkspace/DecisionWorkspace.tsx
- */
-import { useState, useEffect } from "react";
-
-const THEME_CSS = `
- .glass-panel {
- background: linear-gradient(135deg, rgba(13,17,23,0.75), rgba(22,27,34,0.6));
- backdrop-filter: blur(16px) saturate(1.2);
- -webkit-backdrop-filter: blur(16px) saturate(1.2);
- border: 1px solid rgba(88,166,255,0.2);
- box-shadow: 0 8px 32px rgba(0,0,0,0.5), inset 1px 1px 0 rgba(255,255,255,0.05);
- }
-`;
-
-
-
-function CausalChainNode({ hop, title, desc }: { hop: number, title: string, desc: string }) {
- return (
-
- );
-}
-
-export function DecisionWorkspace() {
- const [decisions, setDecisions] = useState([]);
- const [selectedDecision, setSelectedDecision] = useState(null);
- const [chain, setChain] = useState([]);
- const [loading, setLoading] = useState(false);
-
- useEffect(() => {
- fetch("/api/decisions")
- .then(res => res.json())
- .then(data => {
- setDecisions(data);
- if (data.length > 0) handleSelectDecision(data[0]);
- })
- .catch(console.error);
- }, []);
-
- const handleSelectDecision = async (d: any) => {
- setSelectedDecision(d);
- setLoading(true);
- try {
- const res = await fetch(`/api/decisions/${d.decision_id}/chain`);
- const data = await res.json();
- setChain(data.chain || []);
- } catch (e) {
- console.error(e);
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
- {/* Left Column: Decisions List */}
-
-
- Decision Tree
-
-
- {decisions.map(d => (
-
handleSelectDecision(d)}
- style={{
- textAlign: "left", padding: "12px 16px", borderRadius: 8, cursor: "pointer",
- background: selectedDecision?.decision_id === d.decision_id ? "rgba(88,166,255,0.15)" : "transparent",
- border: `1px solid ${selectedDecision?.decision_id === d.decision_id ? "#58a6ff" : "rgba(255,255,255,0.1)"}`,
- color: selectedDecision?.decision_id === d.decision_id ? "#ffffff" : "#c9d1d9",
- transition: "all 0.2s"
- }}
- >
- {d.decision_id}
- {d.category || 'Uncategorized'}
-
- ))}
-
-
-
- {/* Right Column: Causal Chains */}
-
-
-
- {selectedDecision ? (
- <>
-
{selectedDecision.decision_id}
-
Outcome: {selectedDecision.outcome}
-
-
-
Causal Chain
- {loading ? (
-
Loading chain...
- ) : chain.length > 0 ? (
- chain.map((c, i) => (
-
- ))
- ) : (
-
No causal chain found.
- )}
-
- >
- ) : (
-
Select a decision to view details
- )}
-
-
- );
-}
diff --git a/semantica/mcp_server.py b/semantica/mcp_server.py
new file mode 100644
index 00000000..ee1fb805
--- /dev/null
+++ b/semantica/mcp_server.py
@@ -0,0 +1,606 @@
+"""
+Semantica MCP Server
+
+Exposes Semantica's knowledge graph, decision intelligence, semantic extraction,
+reasoning, and analytics capabilities as an MCP (Model Context Protocol) server
+over stdio — compatible with Claude Desktop, Windsurf, Cline, Continue, VS Code,
+Roo Code, and any other MCP-aware tool.
+
+Usage
+-----
+Configure in your tool's MCP settings:
+
+ Claude Desktop / Windsurf / Cline / Continue / VS Code:
+ {
+ "mcpServers": {
+ "semantica": {
+ "command": "python",
+ "args": ["-m", "semantica.mcp_server"]
+ }
+ }
+ }
+
+Run directly:
+ python -m semantica.mcp_server
+
+Environment variables:
+ SEMANTICA_KG_PATH — path to a persisted graph to load on start (optional)
+ SEMANTICA_LOG_LEVEL — log level: DEBUG, INFO, WARNING (default: WARNING)
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import os
+import sys
+from typing import Any
+
+# ── logging ────────────────────────────────────────────────────────────────
+_log_level = getattr(logging, os.environ.get("SEMANTICA_LOG_LEVEL", "WARNING").upper(), logging.WARNING)
+logging.basicConfig(stream=sys.stderr, level=_log_level,
+ format="%(asctime)s [semantica-mcp] %(levelname)s %(message)s")
+log = logging.getLogger("semantica.mcp_server")
+
+# ── lazy graph session ──────────────────────────────────────────────────────
+_graph: Any = None
+
+
+def _get_graph():
+ global _graph
+ if _graph is None:
+ from semantica.context import ContextGraph
+ _graph = ContextGraph(advanced_analytics=True)
+ kg_path = os.environ.get("SEMANTICA_KG_PATH")
+ if kg_path and os.path.exists(kg_path):
+ try:
+ _graph.load(kg_path)
+ log.info("Loaded graph from %s", kg_path)
+ except Exception as exc:
+ log.warning("Could not load graph from %s: %s", kg_path, exc)
+ return _graph
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# Tool implementations
+# ══════════════════════════════════════════════════════════════════════════════
+
+def _tool_extract_entities(args: dict) -> dict:
+ """Extract named entities from text."""
+ text = args.get("text", "")
+ if not text:
+ return {"error": "text is required"}
+ from semantica.semantic_extract import NamedEntityRecognizer
+ from semantica.semantic_extract.cache import _result_cache
+ _result_cache.clear()
+ entities = NamedEntityRecognizer().extract(text)
+ return {
+ "entities": [
+ {"label": getattr(e, "label", str(e)),
+ "type": getattr(e, "type", None),
+ "start": getattr(e, "start", None),
+ "end": getattr(e, "end", None)}
+ for e in (entities or [])
+ ]
+ }
+
+
+def _tool_extract_relations(args: dict) -> dict:
+ """Extract relations and triplets from text."""
+ text = args.get("text", "")
+ if not text:
+ return {"error": "text is required"}
+ from semantica.semantic_extract import RelationExtractor, TripletExtractor
+ from semantica.semantic_extract.cache import _result_cache
+ _result_cache.clear()
+ relations = RelationExtractor().extract(text)
+ triplets = TripletExtractor().extract(text)
+ return {
+ "relations": [
+ {"source": getattr(r, "source", None),
+ "type": getattr(r, "type", None),
+ "target": getattr(r, "target", None)}
+ for r in (relations or [])
+ ],
+ "triplets": [
+ {"subject": getattr(t, "subject", None),
+ "predicate": getattr(t, "predicate", None),
+ "object": getattr(t, "object", None)}
+ for t in (triplets or [])
+ ],
+ }
+
+
+def _tool_record_decision(args: dict) -> dict:
+ """Record a decision with full context into the graph."""
+ required = ["category", "scenario", "reasoning", "outcome", "confidence"]
+ for field in required:
+ if field not in args:
+ return {"error": f"missing required field: {field}"}
+ graph = _get_graph()
+ decision_id = graph.record_decision(
+ category=args["category"],
+ scenario=args["scenario"],
+ reasoning=args["reasoning"],
+ outcome=args["outcome"],
+ confidence=float(args["confidence"]),
+ entities=args.get("entities", []),
+ decision_maker=args.get("decision_maker", "mcp_client"),
+ valid_from=args.get("valid_from"),
+ valid_until=args.get("valid_until"),
+ )
+ return {"decision_id": decision_id, "status": "recorded"}
+
+
+def _tool_query_decisions(args: dict) -> dict:
+ """Query decisions by natural language or structured filters."""
+ query = args.get("query", "")
+ category = args.get("category")
+ limit = int(args.get("limit", 10))
+ graph = _get_graph()
+ try:
+ if query:
+ results = graph.find_similar_decisions(query, max_results=limit)
+ elif category:
+ nodes = graph.find_nodes(node_type="decision")
+ results = [n for n in nodes if n.get("category") == category][:limit]
+ else:
+ results = graph.find_nodes(node_type="decision")[:limit]
+ return {"decisions": results if isinstance(results, list) else list(results)}
+ except Exception as exc:
+ return {"error": str(exc), "decisions": []}
+
+
+def _tool_find_precedents(args: dict) -> dict:
+ """Find past decisions similar to a given scenario."""
+ scenario = args.get("scenario", "")
+ if not scenario:
+ return {"error": "scenario is required"}
+ max_results = int(args.get("max_results", 5))
+ graph = _get_graph()
+ try:
+ precedents = graph.find_similar_decisions(scenario, max_results=max_results)
+ return {"precedents": precedents if isinstance(precedents, list) else list(precedents)}
+ except Exception as exc:
+ return {"error": str(exc), "precedents": []}
+
+
+def _tool_get_causal_chain(args: dict) -> dict:
+ """Get the causal chain for a decision."""
+ decision_id = args.get("decision_id", "")
+ if not decision_id:
+ return {"error": "decision_id is required"}
+ direction = args.get("direction", "downstream")
+ max_depth = int(args.get("max_depth", 5))
+ graph = _get_graph()
+ try:
+ from semantica.context.causal_analyzer import CausalChainAnalyzer
+ analyzer = CausalChainAnalyzer(graph_store=graph)
+ chain = analyzer.get_causal_chain(decision_id, direction=direction, max_depth=max_depth)
+ return {"chain": chain if isinstance(chain, list) else list(chain)}
+ except Exception as exc:
+ return {"error": str(exc), "chain": []}
+
+
+def _tool_add_entity(args: dict) -> dict:
+ """Add a node/entity to the knowledge graph."""
+ node_id = args.get("id", "")
+ label = args.get("label", node_id)
+ node_type = args.get("type", "Entity")
+ if not node_id:
+ return {"error": "id is required"}
+ graph = _get_graph()
+ graph.add_node(node_id=node_id, label=label, node_type=node_type,
+ metadata=args.get("metadata", {}))
+ return {"status": "added", "id": node_id}
+
+
+def _tool_add_relationship(args: dict) -> dict:
+ """Add a relationship (edge) between two entities."""
+ source = args.get("source", "")
+ target = args.get("target", "")
+ rel_type = args.get("type", "RELATED_TO")
+ if not source or not target:
+ return {"error": "source and target are required"}
+ graph = _get_graph()
+ graph.add_edge(source_id=source, target_id=target, edge_type=rel_type,
+ metadata=args.get("metadata", {}))
+ return {"status": "added", "source": source, "target": target, "type": rel_type}
+
+
+def _tool_run_reasoning(args: dict) -> dict:
+ """Run forward-chaining reasoning rules over a set of facts."""
+ facts = args.get("facts", [])
+ rules = args.get("rules", [])
+ if not facts or not rules:
+ return {"error": "facts and rules are required"}
+ from semantica.reasoning import Reasoner
+ reasoner = Reasoner()
+ for rule in rules:
+ reasoner.add_rule(rule)
+ derived = reasoner.infer_facts(facts)
+ return {"derived_facts": derived if isinstance(derived, list) else list(derived)}
+
+
+def _tool_get_graph_analytics(args: dict) -> dict:
+ """Compute graph analytics: centrality, community detection, metrics."""
+ graph = _get_graph()
+ try:
+ from semantica.kg import CentralityCalculator, CommunityDetector
+ centrality = CentralityCalculator().calculate_pagerank(graph)
+ communities = CommunityDetector().detect_communities(graph)
+ node_count = len(list(graph.find_nodes()))
+ edge_count = getattr(graph, "edge_count", lambda: 0)()
+ return {
+ "node_count": node_count,
+ "edge_count": edge_count,
+ "top_nodes_by_pagerank": sorted(
+ centrality.items() if hasattr(centrality, "items") else [],
+ key=lambda x: x[1], reverse=True
+ )[:10],
+ "community_count": len(communities) if isinstance(communities, (list, dict)) else 0,
+ }
+ except Exception as exc:
+ return {"error": str(exc)}
+
+
+def _tool_export_graph(args: dict) -> dict:
+ """Export the current knowledge graph to a serialised format."""
+ fmt = args.get("format", "json-ld")
+ graph = _get_graph()
+ try:
+ from semantica.export import RDFExporter, JSONExporter
+ if fmt in ("turtle", "ttl", "nt", "xml", "json-ld"):
+ result = RDFExporter().export_to_rdf(graph, format=fmt)
+ else:
+ result = JSONExporter().export(graph)
+ return {"format": fmt, "data": result}
+ except Exception as exc:
+ return {"error": str(exc)}
+
+
+def _tool_get_graph_summary(args: dict) -> dict:
+ """Return a high-level summary of the current graph."""
+ graph = _get_graph()
+ try:
+ node_count = len(list(graph.find_nodes()))
+ decisions = graph.find_nodes(node_type="decision")
+ return {
+ "node_count": node_count,
+ "decision_count": len(list(decisions)),
+ "graph_ready": True,
+ }
+ except Exception as exc:
+ return {"error": str(exc), "graph_ready": False}
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# MCP protocol tables
+# ══════════════════════════════════════════════════════════════════════════════
+
+TOOLS = [
+ {
+ "name": "extract_entities",
+ "description": "Extract named entities (people, places, organisations, concepts) from text using Semantica NER.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "text": {"type": "string", "description": "Input text to extract entities from"}
+ },
+ "required": ["text"],
+ },
+ "_handler": _tool_extract_entities,
+ },
+ {
+ "name": "extract_relations",
+ "description": "Extract relations and (subject, predicate, object) triplets from text.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "text": {"type": "string", "description": "Input text to extract relations from"}
+ },
+ "required": ["text"],
+ },
+ "_handler": _tool_extract_relations,
+ },
+ {
+ "name": "record_decision",
+ "description": "Record a decision into the Semantica knowledge graph with full context, causal links, and metadata.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "category": {"type": "string", "description": "Decision category, e.g. 'loan_approval'"},
+ "scenario": {"type": "string", "description": "Natural-language situation description"},
+ "reasoning": {"type": "string", "description": "Why this decision was made"},
+ "outcome": {"type": "string", "description": "Decision outcome, e.g. 'approved'"},
+ "confidence": {"type": "number", "description": "Confidence score 0–1"},
+ "decision_maker":{"type": "string", "description": "Who/what made the decision"},
+ "valid_from": {"type": "string", "description": "ISO date validity start (optional)"},
+ "valid_until": {"type": "string", "description": "ISO date validity end (optional)"},
+ },
+ "required": ["category", "scenario", "reasoning", "outcome", "confidence"],
+ },
+ "_handler": _tool_record_decision,
+ },
+ {
+ "name": "query_decisions",
+ "description": "Query recorded decisions by natural language, category, or get all recent decisions.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "query": {"type": "string", "description": "Natural language query (optional)"},
+ "category": {"type": "string", "description": "Filter by category (optional)"},
+ "limit": {"type": "integer", "description": "Max results (default 10)"},
+ },
+ },
+ "_handler": _tool_query_decisions,
+ },
+ {
+ "name": "find_precedents",
+ "description": "Find past decisions similar to a given scenario using hybrid similarity search.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "scenario": {"type": "string", "description": "Scenario description to find precedents for"},
+ "max_results": {"type": "integer", "description": "Max results (default 5)"},
+ },
+ "required": ["scenario"],
+ },
+ "_handler": _tool_find_precedents,
+ },
+ {
+ "name": "get_causal_chain",
+ "description": "Trace the causal chain upstream or downstream from a decision.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "decision_id": {"type": "string", "description": "Decision ID to trace"},
+ "direction": {"type": "string", "enum": ["upstream", "downstream"], "description": "Trace direction"},
+ "max_depth": {"type": "integer", "description": "Max chain depth (default 5)"},
+ },
+ "required": ["decision_id"],
+ },
+ "_handler": _tool_get_causal_chain,
+ },
+ {
+ "name": "add_entity",
+ "description": "Add a node/entity to the Semantica knowledge graph.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "id": {"type": "string", "description": "Unique node ID"},
+ "label": {"type": "string", "description": "Human-readable label"},
+ "type": {"type": "string", "description": "Node type, e.g. 'Person', 'Organisation'"},
+ "metadata": {"type": "object", "description": "Additional properties"},
+ },
+ "required": ["id"],
+ },
+ "_handler": _tool_add_entity,
+ },
+ {
+ "name": "add_relationship",
+ "description": "Add a directed relationship (edge) between two entities in the knowledge graph.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "source": {"type": "string", "description": "Source node ID"},
+ "target": {"type": "string", "description": "Target node ID"},
+ "type": {"type": "string", "description": "Relationship type, e.g. 'WORKS_AT'"},
+ "metadata": {"type": "object", "description": "Additional edge properties"},
+ },
+ "required": ["source", "target"],
+ },
+ "_handler": _tool_add_relationship,
+ },
+ {
+ "name": "run_reasoning",
+ "description": "Run forward-chaining IF/THEN rules over a set of facts to derive new facts.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "facts": {
+ "type": "array", "items": {"type": "string"},
+ "description": "List of fact strings, e.g. ['Person(John)', 'Employee(John)']",
+ },
+ "rules": {
+ "type": "array", "items": {"type": "string"},
+ "description": "IF/THEN rule strings, e.g. ['IF Employee(?x) THEN WorkerBee(?x)']",
+ },
+ },
+ "required": ["facts", "rules"],
+ },
+ "_handler": _tool_run_reasoning,
+ },
+ {
+ "name": "get_graph_analytics",
+ "description": "Compute PageRank centrality and community detection over the knowledge graph.",
+ "inputSchema": {"type": "object", "properties": {}},
+ "_handler": _tool_get_graph_analytics,
+ },
+ {
+ "name": "export_graph",
+ "description": "Export the current knowledge graph. Formats: turtle, ttl, nt, xml, json-ld, json.",
+ "inputSchema": {
+ "type": "object",
+ "properties": {
+ "format": {
+ "type": "string",
+ "enum": ["turtle", "ttl", "nt", "xml", "json-ld", "json"],
+ "description": "Export format (default: json-ld)",
+ }
+ },
+ },
+ "_handler": _tool_export_graph,
+ },
+ {
+ "name": "get_graph_summary",
+ "description": "Return a high-level summary of the current knowledge graph: node count, decision count, status.",
+ "inputSchema": {"type": "object", "properties": {}},
+ "_handler": _tool_get_graph_summary,
+ },
+]
+
+RESOURCES = [
+ {
+ "uri": "semantica://graph/summary",
+ "name": "Graph Summary",
+ "description": "High-level statistics about the current knowledge graph",
+ "mimeType": "application/json",
+ },
+ {
+ "uri": "semantica://decisions/list",
+ "name": "Decisions",
+ "description": "List of all recorded decisions in the graph",
+ "mimeType": "application/json",
+ },
+ {
+ "uri": "semantica://schema/info",
+ "name": "Schema Info",
+ "description": "Semantica server info and available capabilities",
+ "mimeType": "application/json",
+ },
+]
+
+
+def _read_resource(uri: str) -> dict:
+ if uri == "semantica://graph/summary":
+ return _tool_get_graph_summary({})
+ if uri == "semantica://decisions/list":
+ return _tool_query_decisions({"limit": 50})
+ if uri == "semantica://schema/info":
+ return {
+ "name": "Semantica",
+ "version": "0.4.0",
+ "tools": [t["name"] for t in TOOLS],
+ "resources": [r["uri"] for r in RESOURCES],
+ }
+ return {"error": f"Unknown resource URI: {uri}"}
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# JSON-RPC / MCP protocol handler
+# ══════════════════════════════════════════════════════════════════════════════
+
+SERVER_INFO = {
+ "name": "semantica",
+ "version": "0.4.0",
+}
+
+CAPABILITIES = {
+ "tools": {"listChanged": False},
+ "resources": {"listChanged": False, "subscribe": False},
+}
+
+
+def _handle(req: dict) -> dict | None:
+ """Dispatch a single JSON-RPC request; return None for notifications."""
+ method = req.get("method", "")
+ params = req.get("params") or {}
+ req_id = req.get("id")
+
+ def ok(result):
+ return {"jsonrpc": "2.0", "id": req_id, "result": result}
+
+ def err(code, message):
+ return {"jsonrpc": "2.0", "id": req_id, "error": {"code": code, "message": message}}
+
+ # Notifications (no id) — acknowledge silently
+ if req_id is None and method.startswith("notifications/"):
+ return None
+
+ if method == "initialize":
+ return ok({
+ "protocolVersion": "2024-11-05",
+ "capabilities": CAPABILITIES,
+ "serverInfo": SERVER_INFO,
+ })
+
+ if method == "notifications/initialized":
+ return None
+
+ if method == "ping":
+ return ok({})
+
+ if method == "tools/list":
+ tools_out = [
+ {"name": t["name"], "description": t["description"], "inputSchema": t["inputSchema"]}
+ for t in TOOLS
+ ]
+ return ok({"tools": tools_out})
+
+ if method == "tools/call":
+ name = params.get("name", "")
+ arguments = params.get("arguments") or {}
+ handler = next((t["_handler"] for t in TOOLS if t["name"] == name), None)
+ if handler is None:
+ return err(-32601, f"Unknown tool: {name}")
+ try:
+ result = handler(arguments)
+ text = json.dumps(result, ensure_ascii=False, indent=2)
+ return ok({"content": [{"type": "text", "text": text}]})
+ except Exception as exc:
+ log.exception("Tool %s raised", name)
+ return err(-32603, str(exc))
+
+ if method == "resources/list":
+ return ok({"resources": RESOURCES})
+
+ if method == "resources/read":
+ uri = params.get("uri", "")
+ data = _read_resource(uri)
+ text = json.dumps(data, ensure_ascii=False, indent=2)
+ return ok({"contents": [{"uri": uri, "mimeType": "application/json", "text": text}]})
+
+ if method == "prompts/list":
+ return ok({"prompts": []})
+
+ return err(-32601, f"Method not found: {method}")
+
+
+# ══════════════════════════════════════════════════════════════════════════════
+# stdio event loop
+# ══════════════════════════════════════════════════════════════════════════════
+
+def _run_stdio():
+ log.info("Semantica MCP server starting on stdio")
+ # Use binary stdin/stdout for reliable newline handling on Windows
+ stdin = sys.stdin.buffer
+ stdout = sys.stdout.buffer
+
+ while True:
+ try:
+ line = stdin.readline()
+ if not line:
+ break
+ line = line.strip()
+ if not line:
+ continue
+ try:
+ req = json.loads(line)
+ except json.JSONDecodeError as exc:
+ resp = {"jsonrpc": "2.0", "id": None,
+ "error": {"code": -32700, "message": f"Parse error: {exc}"}}
+ stdout.write(json.dumps(resp).encode() + b"\n")
+ stdout.flush()
+ continue
+
+ resp = _handle(req)
+ if resp is not None:
+ stdout.write(json.dumps(resp, ensure_ascii=False).encode() + b"\n")
+ stdout.flush()
+ except EOFError:
+ break
+ except KeyboardInterrupt:
+ break
+ except Exception as exc:
+ log.exception("Unhandled error in MCP loop: %s", exc)
+
+ log.info("Semantica MCP server stopped")
+
+
+def main():
+ _run_stdio()
+
+
+if __name__ == "__main__":
+ main()