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).
81 lines
2.5 KiB
Python
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()) |