Skip to content

MCP Server

Connect agent harnesses — Claude Code, Claude Desktop, Cursor, or any MCP client — directly to your graph. kg serve exposes it over the Model Context Protocol with four read-only, schema-aware tools. No LLM is involved in serving: extraction needs a model, querying doesn't.

The four tools

Tool What it does
describe_ontology The schema the loaded ontology declares and what the graph actually holds (per-label counts). Agents call this first.
search_entities Case-insensitive substring search on the name property — resolves a vague mention into a concrete node.
get_neighbors One-hop neighborhood of a named entity, both directions, with the relationship type and out/in direction.
query_graph Hand-written read-only openCypher (MATCH/WHERE/RETURN/ORDER BY/LIMIT); write clauses are rejected.

Tool descriptions are generated at runtime from the loaded ontology — swap the YAML and the agent sees a different schema, with zero code changes. That's the repo's core claim carried through to the agent layer.

Quick connect (stdio)

uv sync --extra mcp        # optional dependency; default install stays lean
kg serve --check           # DB reachable? graph? data? (exit 0/1, no mcp needed)
kg serve --print-config    # ready-to-paste harness config JSON

--print-config prints something like:

{
  "mcpServers": {
    "kg-generic": {
      "command": "/path/to/.venv/bin/python",
      "args": ["-m", "kg.cli", "serve", "--env", "/path/to/.env"]
    }
  }
}

The command is the venv's own interpreter and the .env path is absolute, so the harness can't accidentally resolve either against the wrong environment. Paste the mcpServers block into your harness config and restart the harness — it should list the four tools.

claude mcp add kg-generic -- /path/to/.venv/bin/python -m kg.cli serve

(Or add the mcpServers block from --print-config to .mcp.json / your project settings.)

Paste the block into claude_desktop_config.json (Settings → Developer → Edit Config) and restart the app.

Any MCP client that speaks stdio: use the command + args from --print-config as the spawn command.

Two graphs, two servers

Each .env profile serves its own ontology and graph. Give each a name and register both:

kg serve --print-config --env .env                # kg-generic  -> kg_graph
kg serve --print-config --env .env.constitution   # kg-constitution_india -> kg_graph_const

Both blocks can live in the same harness config; each server only ever sees its own graph, and each gets tool descriptions built from its own ontology.

Operational notes

  • Ontology YAML edits need a harness restart. Descriptions are built once at startup. Ingested data, however, appears without a restart — counts refresh on a ~60s TTL.
  • If the server won't start, run kg info / kg serve --check. Over stdio the harness swallows tracebacks; these two print the real error (bad YAML, unreachable DB, missing graph) to your terminal.
  • Serving an empty graph is fine. kg serve starts, describe_ontology reports the declared schema with zero counts and a hint pointing at kg ingest. Run the ingest whenever ready; the server picks the data up without a restart. Use --require-data if you'd rather it refuse to start.

The read-only boundary

The server connects as the kg_reader role — created by docker/init-age.sql with USAGE/SELECT only. This is the actual guard against hostile Cypher from an agent: the write-clause check in query_graph is a friendly error message, not a security boundary. Configure via .env:

POSTGRES_READER_USER=kg_reader
POSTGRES_READER_PASSWORD=change_me

Unset, the server falls back to the write credentials (it works, but any query_graph caller could write).

Non-default graph names

The init script grants on the default kg_graph schema. A graph created under a different AGE_GRAPH_NAME gets its own schema and needs a one-time:

GRANT USAGE ON SCHEMA <graph> TO kg_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA <graph> TO kg_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA <graph> GRANT SELECT ON TABLES TO kg_reader;

HTTP deployment (optional)

Some setups prefer an HTTP endpoint over per-client stdio processes:

docker compose --profile mcp up -d --build   # Postgres + the MCP server

The mcp service is profile-gated — plain docker compose up -d still starts only Postgres. It serves the bundled ontology named by MCP_ONTOLOGY (default generic.yaml) at http://127.0.0.1:8080/mcp.

No authentication

This endpoint exposes the entire graph to anyone who can reach the port, with no auth. It is bound to 127.0.0.1 on purpose. Before exposing it beyond your machine, put at least a bearer-token check (ASGI middleware) in front — deliberately not implemented here. Auth is tracked as future work.

Note

The HTTP server needs no Ollama / hosted LLM — just Python and Postgres. Extraction is the only model-dependent step, and it runs on the host via kg ingest, not inside this container.

Limits

This is schema-aware read access, not GraphRAG retrieval: there's no retrieval/QA, no text-to-Cypher, no ranking over text chunks. The agent sees the ontology and queries the graph itself.