Files
lore-engine/docs/05-mcp-tools.md
Kaysser Kayyali ba314bc664 docs(adr): 0009 — reified Relation edges; recall + tool-dispatch reframes
Three findings from the Cognee-API review:

ADR 0009 (the big one): edges with time/confidence are reified
:Relation nodes, promoted v1.1 -> v1. Cognee's graph_model can't
carry valid_from/valid_until/confidence on a native edge (an edge
is a nested DataPoint field; the Edge object only has weight +
relationship_type). So any edge the time model, consistency engine,
disputed-edge machinery, and retcon policy operate on is a Relation
node. Structural edges (is_type, template-wiring) stay native.
Propagated: 11-extensibility (Relation now v1, +disputed/retcon
fields), 04-consistency (Category A + B Cypher match through Relation
nodes, materialize is_disputed/disputed_with), 00-overview count,
CONTEXT.md (+Relation term), slice 1/3/6 notes.

Finding 1: cognee.recall is not 'low-precision' — it returns scored
multi-source RecallResponse objects (incl cypher/triplet/temporal
kinds), session-aware. It's the fallback because results are
un-typed/un-cited/un-time-bounded, not low-precision. Reframed in
07-reasoning-harness + 05-mcp-tools.

Finding 3: 'register our 45 tools with Cognee's dispatch' was false.
Cognee ships cognee-mcp (a fixed 14-tool surface) — a reference
server, not a registry we extend. Lore Engine runs its own MCP
server (45 tools), calls Cognee's Python API in-process. Reframed
in 00-overview + 22-cognee-boundary.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 23:20:26 -04:00

15 KiB
Raw Blame History

05 — MCP Tool Catalog

The complete tool surface the LLM uses to reason about the world. Each tool has one job. Higher-level patterns are compositions, not bigger tools.

Base tools (none are "inherited as-is" — Cognee ships no MCP tool catalog; all 45 are Lore Engine handlers. semantic_search delegates to cognee.recall; the rest are Lore Engine originals):

  • semantic_search — vector search over chunks (wraps cognee.recall)
  • graph_traverse — n-hop traversal
  • get_context — full context for a single entity
  • get_person_profile — entity summary
  • query_as_npc — NPC-scoped query
  • log_encounter — write an encounter (world-builder write tool)
  • get_unresolved — list provisional entities (built in slice 2)
  • get_contradictions — list contradictions (built in slice 2)

New tools are grouped by function. Signatures use TypeScript-ish notation for clarity; the actual JSON-RPC schemas live in the MCP server source.


Group 1: Identity & disambiguation

lookup(query, type?)

The disambiguating entry point. "The dagger" → which one?

Parameters:

  • query (string) — name, alias, or partial name
  • type (string, optional) — restrict to one label (Person, Item, etc.)

Returns: Array of matching nodes with name, type, ID, aliases, and a match_confidence score. The LLM picks one (or asks the user).

Why this exists: Without it, the LLM has to guess entity IDs, which it cannot do reliably. This is the single highest-leverage tool for reducing hallucination.


entity_context(name, at_time?)

One-hop summary of an entity. "Who is Aldric?" → answer in one call.

Parameters:

  • name (string) — entity name
  • at_time (string, optional) — canonical time, e.g. 3rd_age.year_345. If omitted, returns the entity's "current" state (uses current reserved token).

Returns:

{
  "entity": { "type": "Person", "name": "Aldric Raventhorne", "id": "uuid-..." },
  "at_time": "3rd_age.year_345",
  "factions": [{"name": "House Vyr", "valid_from": "...", "valid_until": "..."}],
  "locations": [{"name": "Thornwall Keep", ...}],
  "cultures": [...],
  "titles": [...],
  "languages": [...],
  "magic_systems": [...],
  "deities": [...],
  "items_possessed": [...],
  "alive": true,
  "lifespan": {"from": "3rd_age.year_300", "until": "3rd_age.year_360"}
}

Why this exists: Most questions start with "who is X?" or "what is X?" This is the cheapest possible answer. The LLM can always drill deeper with expand_context.


Group 2: Time-aware queries

was_true_at(relation, subject, object, at_time)

The most common time-aware query. "Were House Vyr and the Crimson Pact allied in 340 TA?"

Parameters:

  • relation (string) — edge type, e.g. ALLIED_WITH, RULED, POSSESSES
  • subject (string) — subject name
  • object (string) — object name
  • at_time (string) — canonical time

Returns:

{
  "was_true": true,
  "valid_from": "3rd_age.year_312",
  "valid_until": "3rd_age.year_345",
  "sources": ["chronicles-vyr.md", "pact-treaties.md"],
  "confidence": 0.92
}

Or "was_true": false with no edge found. The LLM is told the result is from the canonical sources; if confidence < 0.5, it should qualify the claim.


true_during(relation, subject, object, era)

"During the Third Age, when was House Vyr at war with anyone?"

Parameters:

  • relation (string) — edge type
  • subject (string)
  • object (string, optional) — if omitted, returns all objects of that relation in the time window
  • era (string) — canonical era, e.g. 3rd_age

Returns: Array of intervals with valid_from, valid_until, object (or subject), sources.


state_at(entity, at_time)

"What was Valdorn like in 340 TA?" — full snapshot.

Parameters:

  • entity (string)
  • at_time (string)

Returns: A comprehensive state object: ruling faction(s), controlling faction(s), notable persons present, ongoing events, current contradictions, magic systems in use, deities worshipped, languages spoken, items of note.

Why this exists: This is the answer to "what was the world like at time T?" It composes entity_context with entities_present and event_chain filtered by time.


entities_present(location, at_time, type?)

"Who was in Mardsville in 340 TA?"

Parameters:

  • location (string)
  • at_time (string)
  • type (string, optional) — restrict to Person, Faction, Creature, Item

Returns: Array of entities whose LOCATED_IN (or CONTROLS, for factions) edge was valid at at_time.


timeline(entity, relation_type?, from?, to?)

"What did Aldric do, in order?"

Parameters:

  • entity (string)
  • relation_type (string, optional) — filter to one edge type
  • from (string, optional) — start time
  • to (string, optional) — end time

Returns: Chronologically sorted array of events/relations involving the entity, with the source document for each. The LLM can hand this back to the user as a "biography."


Group 3: Lineage & hierarchy

list_lineage(person, depth?)

"What bloodline is Aldric part of, and who else?"

Parameters:

  • person (string)
  • depth (integer, optional, default=2) — how many PARENT_OF/DESCENDED_FROM hops to traverse

Returns:

{
  "lineage": { "name": "House Vyr (bloodline)", "founding_ancestor": "..." },
  "members": [{"name": "Aldric", "relation": "self"}, ...],
  "cadet_branches": [...],
  "depth_covered": 2
}

list_offspring(person)

"Who are Aldric's children?" Direct children, no recursion. Cheaper than list_lineage for simple cases.


ancestors_of(person, generations?)

"Who were Aldric's grandparents and great-grandparents?" Walks PARENT_OF upward, returns the chain.


descendants_of(person, generations?)

The inverse — walks PARENT_OF downward.


location_hierarchy(location, direction?)

"What is Thornwall Keep part of?" (up) or "What is part of Valdorn?" (down).

Parameters:

  • location (string)
  • direction (string) — up (parent regions/kingdoms) or down (sub-locations)

Returns: The geographic / political hierarchy above or below the location, with RULES/CONTROLS annotations.


Group 4: Causal & event chains

event_chain(event, depth?)

"What caused the Sundering, and what did it cause?"

Parameters:

  • event (string)
  • depth (integer, optional, default=2) — CAUSED/PRECEDED/CONCURRENT_WITH traversal depth

Returns: A graph structure with the event, its causes (depth 1+), its effects (depth 1+), and any concurrent events. Critical for "why did X happen" and "what were the consequences of X."


events_during(era, location?, type?)

"What battles happened in the Northern Reaches during the Third Age?"

Parameters:

  • era (string)
  • location (string, optional)
  • type (string, optional) — Event label or a sub-classification if you create them

Returns: Array of events, sorted by in-fiction date.


Group 5: Knowledge & lore

lore_about(entity, type?, limit?)

"What do the chronicles say about Aldric?"

Parameters:

  • entity (string)
  • type (string, optional) — LoreSource source_type filter: prose, timeline, family_tree, etc.
  • limit (integer, optional, default=10)

Returns: Array of LoreSource documents that mention the entity, with the relevant chunks and a relevance score.


cite(claim)

"Where does the engine get that from?" — given a claim (a string), return the source documents and the specific chunks that support it.

Parameters:

  • claim (string) — natural language claim

Returns: Array of source chunks with similarity scores. The LLM can then say "according to..." and name the source.

Why this exists: Source attribution is a first-class feature. The LLM must always be able to back up its claims. This is the inverse of semantic_search — instead of "find me chunks that match X," it's "for this claim, where did it come from?"


Group 6: Consistency (see 04-consistency.md for full details)

Tool Purpose
get_contradictions(subject?, severity?, limit?) List flagged contradictions
get_anachronisms(entity?, limit?) List flagged anachronisms
get_ontology_violations(rule_id?, severity?, limit?) List ontology rule violations
get_orphans(reason?, limit?) List entities with missing structural data
flag_for_review(node_id, reason) LLM marks a node suspicious
explain_violation(node_id) Returns the rule, edges, and sources behind a violation
run_consistency_check(scope?) Force a check over entity/era/all
latest_run() Most recent ConsistencyRun summary
add_ontology_rule(id, cypher, description, severity) World-builder only
list_ontology_rules() Browse rules

Group 7: Generation & narrative (advanced)

summarize_chain(entity, depth, style?)

"Summarize the chain of events that produced Aldric's reign."

Walks the event_chain from a starting point, condenses the result into a narrative paragraph or bullet list, and returns it with citations.

Parameters:

  • entity (string)
  • depth (integer, default=3)
  • style (string) — bullet, paragraph, chronicle, whispers (in-character first-person)

Returns: A formatted text block plus a citation map. The LLM uses this as a base for its response, then can rewrite in any voice.

Caveat: This tool calls an LLM internally to produce the prose. It's the one place in the engine that does narrative generation. The LLM is told to use the returned text as raw material, not as a final answer.


narrate_arc(start_event, end_event, perspective?)

"Tell me the story of the Border Wars, from the Valdorni perspective."

Composes event_chain, entities_present, and summarize_chain into an arc-narrative.

Parameters:

  • start_event (string)
  • end_event (string)
  • perspective (string, optional) — a Person, Faction, or Culture whose WITNESSED / PARTICIPATED_IN edges filter the events

Returns: A multi-paragraph narrative, perspective-filtered, with a timeline of citations at the end.

Why this exists: This is the "narrative mode" tool. The LLM can hand the result to the user as a short story seed, or use it as the spine of a longer piece.


Group 8: World-building (not for the LLM during inference)

These are tools for the human world-builder, exposed via the MCP server but not in the LLM's primary tool list. The LLM can use them but rarely needs to.

Tool Purpose
add_entity(label, name, properties) Create a new entity
add_relation(from, relation, to, valid_from?, valid_until?) Create a time-bound edge
add_lore_source(title, source_type, content, author?) Ingest a new document
merge_entities(id_a, id_b) Merge two entities that refer to the same thing
set_alias(entity_id, alias) Add an alias
define_era(name, parent_era?, start, end) Add a new era
define_calendar(name, months) Add a new calendar
define_date(slug, label, era, year, month?, day?) Add a new Date node
delete_node(id, reason) Soft-delete a node

These exist so the MCP server is the only write surface for the graph. The LLM doesn't need them but the world-builder does, and having one canonical surface is critical for consistency.


Composition patterns (the recipes)

The LLM doesn't use these tools in isolation. It uses patterns. Five patterns cover 90% of the world-reasoning questions:

Pattern 1: "Who/what is X?"

entity_context(X) → if insufficient, expand_context(X, hops=2)

Pattern 2: "Did X happen at time T?" / "Was X true at time T?"

was_true_at(RELATION, subject, object, T)

Pattern 3: "What was the world like at time T?" / "What was happening in Y at T?"

state_at(Y, T) + entities_present(Y, T)

Pattern 4: "How is X connected to Y?" / "What's the relationship?"

expand_context(X, hops=3, relations=[...]) → filter for Y

Pattern 5: "Why did X happen?" / "What are the consequences of X?"

event_chain(X, depth=3) + cite(claims_from_chain)

These five are the recipes the reasoning harness (07-reasoning-harness.md) spells out for the LLM.


Starter ontology rules (out of the box)

The first 10 rules that ship with the engine:

  1. no-overlapping-rulers — A location cannot have two RULES edges active at the same time.
  2. no-overlapping-spouses — A person cannot have two SPOUSE_OF edges active at the same time.
  3. no-anachronism-participation — A person cannot PARTICIPATED_IN an event outside their lifespan.
  4. no-anachronism-rule — A faction cannot RULES a location before its founding or after its dissolution.
  5. no-orphan-events — Every Event must have OCCURRED_AT and OCCURRED_DURING.
  6. no-orphan-locations — Every Location must have PART_OF (a parent region, even if it's Unmapped Lands).
  7. lineage-continuity — Every Lineage must have a founding_ancestor and at least one MEMBER_OF Person.
  8. magic-system-coherence — A Spell cannot exist in a MagicSystem that has no PRACTICES in the relevant era.
  9. deity-worship-coherence — A Person cannot WORSHIPS a Deity that does not exist in their era.
  10. item-lineage — An Item with INHERITED_BY edges must have a CREATED edge (it was made by someone).

The user can disable any rule by ID, and add new ones via add_ontology_rule.


Tool count: 45 total (8 base + 37 domain)

The full catalog: 8 base tools (one wraps cognee.recall; the rest are Lore Engine originals) + 37 domain tools across Groups 18 = 45 MCP tools — all Lore Engine handlers, exposed through the Lore Engine's own MCP server (Cognee's cognee-mcp is a separate, fixed 14-tool surface; we don't register into it). The server calls Cognee's Python API (remember/recall/add_data_points) in-process. A handful of tools delegate to cognee.recall as a fallback — not because recall is low-precision (it returns scored, multi-source results) but because its results are un-typed, un-cited, and un-time-bounded. That's well past the empirical LLM tool-use ceiling (~25 in a single system prompt), so the Phase 6 reasoning-harness validation measures usage and collapses the long tail. The LLM uses 58 of them 90% of the time; the long tail exists for edge cases the LLM will sometimes need and shouldn't have to fall back to free-text generation for.

This is on the high end of what an LLM can effectively use in a single context. We mitigate by:

  • The reasoning harness documents which 8 to use first.
  • Tools are grouped by function in the system prompt.
  • The lookup tool + active context reduce the need to remember entity IDs.

If the LLM gets tool-confused in practice, the next move is to collapse entities_present into state_at, and narrate_arc into summarize_chain with a parameter. We start big; we collapse based on observed usage.