warpline exposes its surface over an MCP stdio server (JSON-RPC 2.0). Launch it
with the warpline-mcp executable, or as a module:
warpline-mcp # installed entry point
python -m warpline.mcp # module formThe server speaks line-delimited JSON-RPC on stdin/stdout and supports the
2024-11-05 and 2025-03-26 MCP protocol versions. Three methods are handled:
| Method | Purpose |
|---|---|
initialize |
Handshake. Returns serverInfo {name: "warpline", version}, the negotiated protocolVersion, and instructions. |
tools/list |
List every tool with its inputSchema, outputSchema, and metadata. |
tools/call |
Invoke a tool by name with arguments. |
There are six frozen federation contracts, plus verify_record
(verification-freshness) and project_status (a read-only binding/health probe)
— eight tools in all. Each is registered under two names — an endorsed
name and a short shim — that return identical schema and data. So tools/list
reports sixteen entries; they collapse to eight tools.
| Endorsed name | Shim | Schema | Mutating? |
|---|---|---|---|
warpline_change_list |
changed |
warpline.change_list.v1 |
no |
warpline_entity_timeline_get |
timeline |
warpline.entity_timeline.v1 |
no |
warpline_entity_churn_count_get |
churn |
warpline.entity_churn_count.v1 |
no |
warpline_impact_radius_get |
blast_radius |
warpline.impact_radius.v1 |
no |
warpline_reverify_worklist_get |
reverify |
warpline.reverify_worklist.v1 |
no |
warpline_edge_snapshot_capture |
capture_snapshot |
warpline.edge_snapshot.v1 |
yes (local only) |
warpline_verification_record |
verify_record |
warpline.verification_record.v1 |
yes (local only) |
warpline_project_status_get |
project_status |
warpline.project_status.v1 |
no (read-only) |
All tools require repo (a path string). Most read tools are marked
read_only: true but may initialize .weft/warpline/ state on first touch (they
open the store, which creates/migrates it). The exception is
warpline_project_status_get: it is the one genuinely read-only tool —
writes_local_state: false, mutates_paths: [] — and reports an absent store as
absent rather than initializing it. warpline_edge_snapshot_capture and
warpline_verification_record are the two mutating tools — both write only to
.weft/warpline/.
Every outbound tool returns the same frozen envelope. Only data, next_actions,
and the relevant enrichment keys differ between tools.
{
"schema": "warpline.<contract>.v1",
"ok": true,
"query": {
"repo": "...", "tool": "...", "arguments": {},
"filters": {}, "sort": {"by": "...", "order": "asc"},
"page": {"limit": 50, "cursor": null}
},
"data": { },
"warnings": [],
"next_actions": {},
"enrichment": {"sei": "...", "edges": "...", "work": "...",
"risk": "...", "governance": "...", "requirements": "..."},
"meta": {
"producer": {"tool": "warpline", "version": "1.1.1"},
"local_only": true,
"peer_side_effects": []
}
}enrichmentis a closed vocabulary:present(peer present, fact attached),absent(peer present, no fact),unavailable(peer unreachable), plusstale | partial | skippedforedges. None is ever a transport error or an implied clean state. See Degrade behavior.meta.local_onlyis alwaystrue;meta.peer_side_effectsis always[].- Over JSON-RPC, a successful
tools/callreturns the envelope in bothresult.structuredContent(the object) andresult.content[0].text(the same object, JSON-serialized).
Recoverable errors are returned as a JSON-RPC error whose data is a
warpline.error.v1 object:
{
"jsonrpc": "2.0", "id": 1,
"error": {
"code": -32602, "message": "invalid params",
"data": {
"schema": "warpline.error.v1",
"error_code": "invalid_rev_range",
"rejected_field": "rev_range",
"retryability": "retry_with_changes",
"hint": "Pass a git revision range resolvable from repo, e.g. HEAD~1..HEAD.",
"details": {"message": "..."}
}
}
}Switch on error_code, not on message text. Both vocabularies are closed:
error_code |
retryability |
Meaning |
|---|---|---|
missing_required_field |
retry_with_changes |
A required argument was absent. |
invalid_repo |
retry_with_changes |
repo is not a readable git repository. |
invalid_rev_range |
retry_with_changes |
The rev_range did not resolve. |
invalid_entity_ref |
retry_with_changes |
The entity_ref had an unknown shape/kind. |
invalid_changed_refs |
retry_with_changes |
changed_refs was not a list of {kind, value}. |
invalid_depth |
retry_with_changes |
depth was not an integer in 0–5. |
invalid_filter |
retry_with_changes |
A filters key was not recognised. |
invalid_sort |
retry_with_changes |
sort_by/sort_order was not recognised. |
peer_unavailable |
retry_safe |
A federation peer was unreachable. |
snapshot_unavailable |
retry_with_changes |
A graph-enriched answer was requested with no snapshot. |
internal_error |
fatal |
A warpline defect. Inspect logs before retrying. |
retryability is one of retry_safe, retry_with_changes, fatal.
JSON-RPC transport codes used: -32700 parse error, -32600 invalid request,
-32601 unknown method/tool, -32602 invalid params (recoverable
warpline.error.v1), -32603 internal error.
Inputs that name an entity accept a {kind, value} ref (a bare string is treated
as kind: auto). Accepted kind values:
auto | locator | sei | path | qualname | warpline_entity_key_id
Every entity in a response carries both locator and sei (sei: null when
unresolved). Key on sei (preferred) or locator. warpline_entity_key_id is a
warpline-internal row id echoed for convenience — not a federation key. See
SEI.
warpline.change_list.v1 — changed entities for a repo and rev range; returns
ready-to-call next_actions. Call this first.
Input
| Field | Type | Notes |
|---|---|---|
repo |
string | (required) |
rev_range |
string | git revision range, e.g. HEAD~1..HEAD. Omit for all recorded changes. |
base_ref, head_ref |
string | accepted in the schema. |
filters |
object | reserved. |
sort_by, sort_order |
string | reserved. |
limit |
integer | default 50. |
cursor |
string | null | reserved (single page; next_cursor is always null). |
include_next_actions |
boolean | — |
data
{
"items": [
{
"change_id": "warpline:change:3",
"entity": {"locator": "...", "sei": null, "warpline_entity_key_id": 1, "path": "..."},
"change_kind": "added | modified | removed | moved",
"actor": "...", "commit": "...", "changed_at": "<iso8601>"
}
],
"changed_refs": [{"kind": "sei | locator", "value": "..."}],
"page": {"limit": 50, "next_cursor": null, "has_more": false}
}next_actions names warpline_reverify_worklist_get and
warpline_impact_radius_get, each with ready-to-call arguments (the resolved
changed_entity_key_ids, changed_refs, and depth: 2). enrichment.sei is
present if any item resolved a SEI, else absent.
warpline.entity_timeline.v1 — ordered change history for one entity.
Input
| Field | Type | Notes |
|---|---|---|
repo |
string | (required) |
entity_ref |
object | {kind, value}. |
entity |
string | bare-string alternative to entity_ref (kind auto). |
filters, sort_by, sort_order, limit, cursor |
— | limit default 50. |
data
{
"entity": {"locator": "...", "sei": "... | null",
"sei_resolution": "resolved | unresolved | unknown"},
"items": [{"change_kind": "...", "actor": "...", "commit": "...",
"changed_at": "<iso8601>", "path": "..."}],
"page": {"limit": 50, "next_cursor": null, "has_more": false}
}sei_resolution is warpline's local honesty signal about its own resolution state
— not a lineage claim. enrichment.governance is present only when a rename
feed was supplied (timeline stitched across renames), else unavailable.
warpline.entity_churn_count.v1 — per-entity change-event counts over an optional
window. A never-observed entity returns churn_count: 0 (not omitted, not an
error).
Input
| Field | Type | Notes |
|---|---|---|
repo |
string | (required) |
entity_refs |
array of {kind, value} |
(required) |
window |
object | {since, until, rev_range}, all optional. |
sort_by |
string | churn_count (default) or sei. |
sort_order |
string | desc (default) or asc. |
limit |
integer | default 100. |
cursor |
string | null | reserved. |
data
{
"items": [
{"entity": {"sei": "... | null", "locator": "..."},
"churn_count": 2,
"first_changed_at": "<iso8601 | null>",
"last_changed_at": "<iso8601 | null>",
"last_actor": "... | null"}
],
"window": {"since": null, "until": null, "rev_range": null},
"page": {"limit": 100, "next_cursor": null, "has_more": false}
}warpline.impact_radius.v1 — downstream affected entities over the latest dated
snapshot, with mandatory completeness + staleness.
Input
| Field | Type | Notes |
|---|---|---|
repo |
string | (required) |
rev_range |
string | seed from a rev range's change events. |
changed_refs |
array of {kind, value} |
seed entities (SEIs preferred). |
changed_entity_key_ids |
array of integers | seed entities by internal key id. |
depth |
integer | 0–5, default 2. |
filters, sort_by, sort_order, limit, cursor |
— | limit default 100. |
Seeds are the union of changed_entity_key_ids, the resolved changed_refs, and
the change events in rev_range.
data
{
"completeness": "FULL | DELTA | NO_SNAPSHOT | SKIPPED",
"staleness": {"snapshot_commit": "... | null", "commits_behind": 0},
"changed": [{"entity": {"locator": "...", "sei": "..."}}],
"affected": [
{"entity": {"locator": "...", "sei": "..."},
"depth": 1,
"via_edges": [{"from": "...", "to": "...", "kind": "calls | references", "confidence": "..."}]}
],
"page": {"limit": 100, "next_cursor": null, "has_more": false}
}completeness and staleness are mandatory in every answer. A non-FULL
completeness adds a warnings[] entry. enrichment.edges mirrors completeness
(present/partial/absent/skipped). An empty affected under NO_SNAPSHOT
means "no graph to traverse," never "nothing affected." See
Blast-radius.
warpline.reverify_worklist.v1 — the worklist to recheck before claiming
completion. Same input as impact_radius, plus group_by and
include_federation.
data
{
"completeness": "FULL | DELTA | NO_SNAPSHOT | SKIPPED",
"staleness": {"snapshot_commit": "... | null", "commits_behind": 0},
"items": [
{
"entity": {"locator": "...", "sei": "..."},
"priority": "P1 | P2 | P3 | unknown",
"reason": "changed | downstream",
"depth": 0,
"why": [ /* via_edges path for downstream items */ ],
"suggested_verification": [{"kind": "test | inspection", "command": "..."}],
"enrichment": {"work": [], "risk": [], "governance": [], "requirements": []}
}
],
"next_actions": {"filigree": [ /* candidate work items */ ]},
"page": {"limit": 100, "next_cursor": null, "has_more": false}
}Changed entities are always present (reason: changed); downstream items are added
when a snapshot exists. priority comes from sibling work enrichment, not from
warpline — unknown when there is no work seam. next_actions.filigree[] holds
candidate work items; warpline files nothing. enrichment.work is present
when work links were seen, absent when the filigree peer was consulted but had
none, unavailable when there was no work client.
When include_federation=true, the filigree work client reads the dashboard HTTP
API at FILIGREE_API_URL (default http://localhost:8724) for ADR-029 entity
association reverse lookups and issue records. Point FILIGREE_API_URL at a
non-default dashboard when filigree is not running locally. If that API is absent
or unreachable, the response stays local-only and records work enrichment as
unavailable with the filigree member reason unreachable; this is not a
confident empty result.
warpline.edge_snapshot.v1 — the only mutating tool. Captures loomweave's
dated edges into .weft/warpline/. Never mutates a sibling repo.
Input
| Field | Type | Notes |
|---|---|---|
repo |
string | (required) |
commit |
string | commit to stamp the snapshot at (default HEAD). |
mode |
string | full (default) or changed_only. |
changed_refs |
array of {kind, value} |
for changed_only mode. |
if_stale_after |
string | null | — |
max_entities |
integer | — |
dry_run |
boolean | report what would be captured without writing. |
idempotency_key |
string | null | — |
The loomweave executable is server/project config, not a tool argument — set
WARPLINE_LOOMWEAVE_COMMAND (default loomweave).
data
{
"snapshot_id": 1,
"commit_sha": "...",
"source": "loomweave",
"source_version": "... | no_index",
"completeness": "FULL | DELTA | SKIPPED",
"entities": 0,
"edges": 0,
"failed_entities": [{"locator": "...", "reason": "..."}],
"idempotency": "created | already_current | dry_run"
}With loomweave absent, completeness is SKIPPED and source_version is
no_index — an honest "no edges captured." DELTA means some entities failed to
capture (listed in failed_entities); the snapshot is usable but a floor.
enrichment.sei is unavailable when loomweave was unreachable (the SEI authority
could not be consulted), else absent.
warpline.verification_record.v1 — 2nd mutating tool. Records a gate-pass
verification event for a commit into .weft/warpline/. Never mutates a sibling repo.
Advisory; warpline never gates. Idempotent on (repo, commit, kind, source=warpline).
Input
| Field | Type | Notes |
|---|---|---|
repo |
string | (required) |
commit |
string | (required) commit ref — resolved to object SHA before storage; symbolic refs are never persisted. |
kind |
string | (required) free-form non-empty provenance label, e.g. test_pass, ci_pass, gate_pass. |
actor |
string | null | optional — who recorded the event. |
data
{
"commit_sha": "...",
"kind": "test_pass",
"verified_at": "2026-06-25T10:00:00+00:00",
"actor": "ci",
"source": "warpline",
"idempotency": "recorded | already_recorded"
}idempotency: already_recorded means the row already existed (a second call for
the same (repo, commit, kind) tuple is a no-op — exactly one row is stored).
All enrichment keys are at their default state (sei: absent, edges: absent,
work: unavailable, risk: unavailable, governance: unavailable,
requirements: unavailable) — no graph-layer dependency.
warpline.project_status.v1 — read-only binding/health probe. Reports
whether THIS warpline build can read and serve the snapshot store for repo
(data.binding_ok). warpline is repo-per-call — bound to nothing at launch — so
this is a can-service-R check, not a launch-time binding: given repo=R the
server reads the schema version from inside R's snapshot store
(data.store.schema_version, null when absent/unreadable), so a stale binary
that cannot read its store is caught — unlike mere directory existence, which
such a binary would still see.
Strictly read-only: it is the one tool with writes_local_state: false /
mutates_paths: []. It creates and migrates no snapshot state — an absent store
reports absent (no DB is created; with a capture_snapshot next-action), and a
present store's warpline.db is left byte-for-byte unchanged. (Opening a present
WAL-mode store read-only may spawn gitignored -wal/-shm SQLite coordination
sidecars — these are not snapshot state; mode=ro is chosen over immutable=1
so the probe always reads the latest committed schema version.)
Input
| Field | Type | Notes |
|---|---|---|
repo |
string | (required) — absolute path of the repo to probe. |
data
{
"resolved_root": "/abs/path/to/repo",
"store": {
"present": true,
"readable": true,
"schema_version": 4,
"snapshot_rev": "c0ffee",
"change_event_count": 2221
},
"store_status": "ok",
"binding_ok": true
}binding_ok is true iff the store is present, readable, and at a schema
version this build serves. The three not-bound outcomes (store_status is a
closed vocab: ok | store_absent | store_unreadable | schema_ahead):
| Outcome | store_status |
store.present |
store.readable |
store.schema_version |
binding_ok |
|---|---|---|---|---|---|
| Store readable + serveable | ok |
true |
true |
<int> |
true |
Never capture_snapshot-ed |
store_absent |
false |
false |
null |
false |
| Corrupt / unparseable store | store_unreadable |
true |
false |
null |
false |
| Written by a newer build (schema beyond this binary — the stale-binary case) | schema_ahead |
true |
false |
null |
false |
When not bound, the human-readable reason rides on warnings[0] (e.g. naming the
on-disk schema vs the highest this build serves). Federation consumers (e.g.
Lacuna's MCP-attachment harness) assert on data.binding_ok plus
data.store.schema_version is not null — the non-tautological store-read signal.