Files
KaifAhmad1andClaude Sonnet 4.6 7b31304e1e feat(mcp): add modular MCP server package at repo root
Adds a fully self-contained `mcp/` package that exposes Semantica as a
Model Context Protocol server over stdio (JSON-RPC 2.0).

17 tools across 5 domains:
- Extraction: extract_entities, extract_relations, extract_all
- Decision intelligence: record_decision, query_decisions, find_precedents,
  get_causal_chain, analyze_decision_impact
- Knowledge graph: add_entity, add_relationship, search_graph,
  get_graph_summary, get_graph_analytics
- Reasoning: run_reasoning, abductive_reasoning
- Export & provenance: export_graph (JSON/CSV/GraphML/Parquet/RDF), get_provenance

4 resources: semantica://graph/summary, semantica://decisions/list,
semantica://schema/info, semantica://ontology/schema

Package layout:
  mcp/__init__.py + __main__.py  — entry points (python -m mcp)
  mcp/server.py                  — SemanticaMCPServer + stdio event loop
  mcp/session.py                 — lazy ContextGraph singleton
  mcp/schemas.py                 — JSON Schema for all 17 tool inputs
  mcp/tools/{extraction,decisions,graph,reasoning,export}.py
  mcp/resources/registry.py      — URI → handler map
  mcp/README.md                  — per-tool setup (Claude Code, Cursor, Windsurf,
                                   Cline, Continue, VS Code, Amazon Q)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-13 17:38:23 +05:30

167 lines
6.4 KiB
Python

"""
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,
},
]