MCP Interface¶
This page explains Moraine's MCP interface as an agent user should understand it. For the exhaustive contract and examples, see the MCP Search Interface Specification.
How MCP Tools Appear¶
MCP clients discover tools by calling tools/list and invoke them through
tools/call. The MCP tool definition includes a name, description,
inputSchema, optional outputSchema, and annotations; tool results may include
plain text content and machine-readable structuredContent. See the
MCP tools specification
for the protocol-level model.
Moraine advertises one read-only server, moraine-mcp, with five tools:
| Tool | Use it for |
|---|---|
search_sessions |
Content search over indexed agent events. |
open |
Expanding an event, turn, or session ID into structured context. |
list_sessions |
Time-window browsing over sessions and active work. |
file_attention |
Every session that touched a file, across every worktree. |
get_ingest_status |
Ingestion health, historical coverage, durable progress, freshness, alerts, and conservative ETA. |
All five tools return a short text summary for clients that display text, and
the same result as JSON in structuredContent for clients that can inspect
structured tool output.
search_sessions, list_sessions, open, and file_attention also attach a
compact data.coverage object. When historical coverage is partial or unknown,
they add a stable coverage_partial or coverage_unknown warning so an agent
does not interpret a missing result as proof that an event never existed.
get_ingest_status¶
Call get_ingest_status with an empty object:
The response keeps process health separate from finite historical coverage and
live freshness. conditions contains health, coverage, freshness, and
readiness entries with three-state values and stable reason codes. The latest
heartbeat may include the frozen startup denominator, durable file/byte
completion, queue capacity, sink pressure, and per-source progress. rate and
eta remain absent until a stable same-instance checkpoint history supports a
bounded estimate. Source names are included; source file paths and raw ingest
errors are not.
Record Model¶
Moraine normalizes every harness into three levels:
| Level | Meaning |
|---|---|
| Session | One agent conversation or trace file. |
| Turn | One user-to-agent cycle within a session. |
| Event | One normalized record, such as user input, assistant response, tool call, tool response, reasoning, compaction, system, or runtime event. |
Search returns events because events are the smallest useful evidence unit. An event points upward to its turn and session, so agents can start with a precise hit and expand only as needed.
IDs are opaque typed strings:
Do not parse these IDs. Pass them back to open exactly as returned.
search_sessions¶
search_sessions finds relevant events across Moraine history.
Input:
{
"query": "mcp open tool oneof top-level schema",
"within_id": null,
"event_types": ["user_input", "assistant_response"],
"harness": null,
"source": null,
"n_hits": 10
}
Fields:
| Field | Meaning |
|---|---|
query |
Required keyword query. Empty strings are rejected. |
within_id |
Optional session:... or turn:... ID to scope the search. Event IDs are not valid scopes. |
event_types |
Optional filter. Searchable event types are user_input, assistant_response, reasoning, tool_call, tool_response, compaction, system, and runtime. |
harness |
Optional exact, case-sensitive normalized harness filter. Supported values are codex, claude-code, cursor, hermes, kiro-cli, kimi-cli, nac, opencode, pi-coding-agent, prime-agent, and qwen-code. |
source |
Optional exact, case-sensitive ingest source filter. The default configured values are claude, macOS-only claude-cowork, codex, cursor, cursor-sqlite, hermes, kimi-cli, kiro, omp, opencode, pi, prime-agent, prime-agent-subagents, and qwen-code; each server's MCP tool instructions list its actual configured source names. |
n_hits |
Optional result limit from 1 to 50. Default is 10. |
The default event type filter is user_input and assistant_response. This
deterministic, message-first default searches what the user asked and what the
assistant concluded without returning raw tool evidence. Request tool_call
or tool_response explicitly through event_types when that evidence is
needed. To inspect the full context around a hit, pass its returned turn or
session handle to open.
Use source when the configured source is the distinction that matters. For
example, the pi and omp sources both use the pi-coding-agent harness, so
source: "omp" selects only OMP sessions while harness: "pi-coding-agent"
selects both. Likewise, harness: "prime-agent" spans root and RLM child
sessions, while source: "prime-agent-subagents" selects only children.
Output data:
| Field | Meaning |
|---|---|
result_count |
Number of hits returned. |
limit |
Applied limit. |
truncated |
Whether more matching records may exist. |
results[] |
Event-ranked hits with rank, score, event metadata, turn metadata, session metadata, snippet, and open handles. |
Each result includes:
{
"rank": 1,
"score": 12.34,
"event": { "id": "event:...", "type": "assistant_response" },
"turn": { "id": "turn:...", "ordinal": 7 },
"session": { "id": "session:...", "title": "..." },
"snippet": { "text": "...", "truncated": false },
"open": {
"event_id": "event:...",
"turn_id": "turn:...",
"session_id": "session:..."
}
}
The snippet is a pointer, not the full record. Open the event when the answer depends on exact wording, command output, payload JSON, or tool arguments.
open¶
open reads an ID returned by search_sessions, list_sessions, or another
open response. Session and turn reads are summary-first so a large history
does not enter the model context unless the agent asks for it.
There are three call shapes:
idalone returns session/turn metadata, compact user input and final response, tools and event types, counts, and traversal handles. Itsturnsoreventsarray is empty andnext_cursoris null.idpluslimitstarts bounded forward expansion.limitis from 1 to the server's configured maximum.{ "cursor": next_cursor }continues the same target with the original page size. Treat the cursor as opaque. If an active session changes between pages, reopen the typed ID and start again.
Follow next_cursor until it is null to recover every compact turn or event
summary. Then open an individual event: ID when exact wording, full tool
arguments/output, or payload JSON is needed. There is intentionally no
unbounded one-call transcript mode.
Encrypted reasoning payloads appear as [encrypted reasoning omitted] in
compact event summaries; open the event directly only when its opaque payload
is needed.
What comes back depends on the ID kind:
| ID kind | Returned context |
|---|---|
event |
Full event content, payload details when available, parent turn/session summary, and traversal IDs. |
turn |
Summary and traversal by default; a bounded page of compact event handles when limit or cursor is used. |
session |
Metadata and first/last-turn traversal by default; a bounded page of compact turn summaries when expanded. |
Use event open for exact evidence, id-only turn/session open for orientation, and expand only when the summary is insufficient.
list_sessions¶
list_sessions browses sessions by time window. Use it when the clue is
temporal, such as "today", "last night", or "active sessions".
Input:
{
"start_datetime": "2026-05-08T09:00:00-04:00",
"end_datetime": "2026-05-08T12:00:00-04:00",
"limit": 20,
"cursor": null,
"mode": null,
"harness": null,
"source": null,
"sort": "desc"
}
start_datetime and end_datetime are required and must include an explicit
timezone. mode can filter session mode: web_search, mcp_internal,
tool_calling, or chat. harness and source use the same exact,
case-sensitive semantics as search_sessions. next_cursor lets clients
continue the same listing; changing any filter invalidates that cursor.
Output data includes compact session records:
{
"rank": 1,
"id": "session:...",
"session": {
"title": "...",
"display_label": "...",
"harness": "codex",
"source": "codex",
"started_at": "2026-05-08T13:00:00.000Z",
"updated_at": "2026-05-08T13:45:00.000Z",
"turn_count": 12,
"event_count": 87,
"mode": "tool_calling"
},
"open": { "session_id": "session:..." }
}
list_sessions intentionally does not return raw transcript text, event snippets,
or payloads. Open a listed session if you need to inspect its turns. The bounded
display_label prefers an explicit title/name, then the first genuine Codex
event_msg/user_message preview (trimmed first line, at most 120 Unicode scalar
values plus an ellipsis), followed by existing title/summary/slug metadata and a
privacy-safe harness/mode/time/turn-count descriptor.
file_attention¶
file_attention answers "show me every session that touched this file, and let
me drill into what was done, when." Given a path, it returns the full
agent-attention history of that file — edits, reads, and aborted attempts —
across every worktree of the project: the main checkout, sibling worktrees,
and agent-isolation worktrees, including work that never landed in git. Unlike
git blame, it shows the debugging session that only read the file and the edit
that was tried and reverted. Matching is by the project-relative path tail, which
is byte-identical across worktree roots, so the roots unify by construction.
Input:
{
"path": "crates/moraine-conversations/src/clickhouse_repo.rs",
"scope": "project",
"granularity": "sessions",
"start_datetime": null,
"end_datetime": null,
"tool": null,
"harness": null,
"source": null,
"mutations_only": false,
"limit": 25
}
path is required. Absolute paths are reduced to a project-relative tail using
the nearest Git boundary or, for a non-Git project, exact containment beneath
the client's launch directory. Relative paths are resolved from that launch
directory, including when the client is routed through the central MCP server,
so deleted or not-yet-created files retain launch-project provenance. Git
checkouts share one identity across linked worktrees. Without Git metadata, the
canonical launch directory is the identity and sessions launched from different
subdirectories remain separate. .moraine.toml selects a backend independently
and is not required for identity. Boundary whitespace, file:// URIs, and directory-style trailing
slashes are rejected rather than silently mapped to a different file. Compound
shell text and multi-path captures are never interpreted as one path or root;
unprovable roots remain unknown.
scope is project (default) or all. project restricts both normalized and
legacy fallback lookup to the launch project's canonical Git-common-directory
or exact working-directory identity independently of --project-only, and
fails closed when neither identity can be established. all deliberately drops that request-level project
narrowing. A configured --project-only server scope remains a hard floor so
returned IDs stay openable. Registered pre-digest roots are migrated to a
durable project mapping, and future normalized roots populate that mapping
automatically. Retained older rows with blank normalized identity can be
recovered only when one top-level scalar structured path agrees exactly with
the recorded working directory and that directory is itself a current or
durable root for this project. A
root pruned before this mapping was installed has no stored Git
identity and cannot be attributed safely; project scope excludes it rather than
widening across projects, and the response warns about this one-time upgrade
limitation. granularity is sessions (default, one rollup per session) or
events (the flat touch-by-touch timeline). tool filters by tool name,
harness and source apply the same exact filters as the other retrieval
tools, and mutations_only excludes common pure-read tools. The default limit
is min(50, mcp.max_results) and the maximum is server-configured.
Output data carries a summary, the distinct worktree roots the tail matched
(so over-match is visible, never silently merged), and either per-session
rollups or an event timeline. Each item exposes typed session: / event: IDs
with open handles:
{
"tail": "crates/.../clickhouse_repo.rs",
"summary": {
"total_touches": 9,
"distinct_sessions": 4,
"distinct_roots": 2,
"distinct_known_roots": 2,
"unknown_root_touches": 0,
"first_touch": "2026-06-10T12:00:00.000Z",
"last_touch": "2026-06-15T09:30:00.000Z",
"ambiguous": true,
"scan_truncated": false
},
"path": "crates/moraine-conversations/src/clickhouse_repo.rs",
"scope": "project",
"granularity": "sessions",
"limit": 25,
"truncated": false,
"roots": [
{ "root": "/Users/me/src/moraine", "touch_count": 7, "session_count": 3 },
{ "root": "/Users/me/src/moraine/worktrees/feat", "touch_count": 2, "session_count": 1 }
],
"sessions": [
{
"rank": 1,
"id": "session:...",
"session": { "harness": "claude-code", "touch_count": 5, "tools": ["Edit", "Read"] },
"open": { "session_id": "session:...", "event_id": "event:...", "turn_id": "turn:..." }
}
]
}
file_attention only locates touches; "what was done, when" is open on a
returned event: / turn: / session: ID, which already returns the full edit,
diff, and surrounding reasoning. turn_id is present when the touch joins to
the conversation trace; event_id and session_id are always present on
displayed rows. A tail with too few path segments (a bare basename) is
inherently ambiguous and returns a warning alongside the surfaced roots. Unknown
roots are counted and warned because they can make an otherwise single known
root ambiguous.
Response Envelope¶
Successful structured responses share this shape:
{
"schema_version": "moraine.mcp.search_sessions.v1",
"tool": "search_sessions",
"request": {},
"data": {},
"warnings": [],
"performance": {
"elapsed_ms": 12
}
}
Tool-level errors return schema_version: "moraine.mcp.error.v1" with an
error object. Common error codes are invalid_request, invalid_id,
not_found, unsupported_event_type, cancelled, deadline_exceeded,
busy, resource_exhausted, backend_failure, and internal_error. Moraine
adds no default query deadline: deadline_exceeded is reserved for an explicit
caller deadline. A full request queue is busy; a ClickHouse memory, disk,
row, or byte limit is resource_exhausted; database or transport failure is
backend_failure.
While the search read model is publishing an active-ingest update,
search_sessions returns internal_error with
error.details.reason = "read_model_refresh", retryable = true, and a
positive retry_after_ms. Wait for that interval, then retry the same request.
How Agents Should Read Results¶
The agent should treat Moraine records as a navigable evidence graph:
- A search hit says "this event probably matters."
open(event)says "this is exactly what happened at that point."- Id-only
open(turn)says "this is a bounded map of the immediate conversational context." - Id-only
open(session)says "this is a bounded map of the session." - Bounded expansion pages say "these are the next compact child handles"; follow their opaque cursor only when more context is needed.
- Traversal IDs let the agent move to neighboring events or turns without re-running a broad search.
This is why the recommended flow is narrow-to-wide. It gives the model enough context to answer accurately without flooding its current context window with whole transcripts.