Files
lore-engine-poc-v3/scripts/05_mcp_server.py
Lore Engine Dev fbf6c9668e slice 5.7: LORE_GRAPH_BACKEND env var in MCP entry scripts
Both MCP entry scripts (05_mcp_server.py for stdio and
06_mcp_http_server.py for Streamable HTTP) now select their
graph backend at startup through a shared loader
(scripts/mcp_server_entry.load_graph):

  * LORE_GRAPH_BACKEND=pickle (default) — load the
    .graph.pkl built by 01_ingest.py.
  * LORE_GRAPH_BACKEND=neo4j — connect to Neo4j at
    $LORE_NEO4J_URI (default bolt://127.0.0.1:7687) and
    load the mirrored graph.
  * Anything else — clear error and exit 4.

Exit codes:

  * 0: graph loaded (only happens if the caller ignores
    the sys.exit() call below and treats load_graph() as
    non-throwing — for the supported backends, load_graph
    returns normally).
  * 1: pickle path missing.
  * 2: neo4j_graph not importable.
  * 3: neo4j unreachable.
  * 4: unknown backend value.

Neo4jGraph.__init__ now eagerly calls verify_connectivity()
so the loader fails loudly at startup rather than on the
first query — the driver pool opens sockets lazily otherwise,
and the first session.run would be too late for the
entry scripts to log a clear error.

Refactors:

  * scripts/05_mcp_server.py: removed inline _load_graph(),
    now imports from scripts.mcp_server_entry.
  * scripts/06_mcp_http_server.py: same.
  * lore_engine_poc/neo4j_graph.py: Neo4jGraph.__init__
    eagerly verifies connectivity.

Tests:

  * tests/test_mcp/test_backend_switch.py — 5 docker-gated
    tests (pickle default, neo4j up, neo4j down exits 3,
    garbage backend exits 4, trivial registry works with
    both backends).

Suite: 624 -> 629 passed (+5 backend-switch tests, all 559
baseline + 38 Neo4j + consistency + ingest + backend-switch
tests preserved).
2026-06-18 23:10:08 -04:00

81 lines
2.5 KiB
Python

#!/usr/bin/env python3
"""05_mcp_server — stdio MCP server for the Lore Engine POC.
Run:
python3 scripts/05_mcp_server.py
Speaks newline-delimited JSON-RPC 2.0 on stdin/stdout. Wire format:
in: {"jsonrpc":"2.0","id":1,"method":"initialize","params":{...}}
out: {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05",...}}
The server loads the graph at startup. The backend is selected by
``$LORE_GRAPH_BACKEND`` (default ``pickle`` — load the
``.graph.pkl`` built by ``01_ingest.py``; ``neo4j`` — connect to
the Neo4j container at ``$LORE_NEO4J_URI``). See
``scripts/mcp_server_entry.py`` for the loader.
Why stdlib only
---------------
We deliberately avoid ``fastmcp`` / ``mcp`` pip dependencies — the
Lore Engine POC manifest only declares ``cognee``. Adding a
package without bumping the manifest is a supply-chain smell, and
the JSON-RPC 2.0 protocol is small enough to write directly.
Lines of plumbing in this file: ~30. The actual dispatch lives
in :class:`lore_engine_poc.mcp_server.MCPServer`, which is unit-
tested in ``tests/test_mcp/test_server.py``.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from lore_engine_poc.mcp_server import MCPServer # noqa: E402
from lore_engine_poc.mcp_tools import TOOL_REGISTRY # noqa: E402
from scripts.mcp_server_entry import load_graph # noqa: E402
def main() -> int:
graph = load_graph()
server = MCPServer(graph, TOOL_REGISTRY)
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except json.JSONDecodeError as exc:
err = {
"jsonrpc": "2.0",
"id": None,
"error": {"code": -32700, "message": f"Parse error: {exc}"},
}
sys.stdout.write(json.dumps(err) + "\n")
sys.stdout.flush()
continue
if not isinstance(msg, dict):
err = {
"jsonrpc": "2.0",
"id": None,
"error": {"code": -32600, "message": "Invalid Request: expected JSON object"},
}
sys.stdout.write(json.dumps(err) + "\n")
sys.stdout.flush()
continue
response = server.handle_message(msg)
if response is not None:
sys.stdout.write(json.dumps(response, default=str) + "\n")
sys.stdout.flush()
return 0
if __name__ == "__main__":
raise SystemExit(main())