Skip to content

MCP tools

selvedge-server exposes eight MCP tools. Seven are read-only and idempotent; one (log_change) is the writer. Every tool ships with full per-parameter descriptions, an output schema, and tool-level annotations — so MCP-aware agents and directories can gate, surface, and pick between them appropriately.

ToolPurposeRead/WriteIdempotent
log_changeRecord a change eventWrite (append)No (each call mints a new event)
diffHistory for an entity / prefixReadYes
blameMost recent change + contextReadYes
historyFiltered history across entitiesReadYes
changesetAll events under a slugReadYes
searchFull-text searchReadYes
prior_attemptsPrior attempts on an entity + inferred outcomeReadYes
stale_decisionsDecisions due for a revisit — expired, past their date, or condition-matchedReadYes

None are openWorldHint: true. None touch the network.


Record a change event. The only writer.

NameTypeDescription
entity_pathstring (required)The thing that changed: users.email, src/auth.py::login, env/STRIPE_SECRET_KEY, deps/stripe. See entity paths.
entity_typestringcolumn / table / file / function / class / endpoint / dependency / env_var / index / schema / config / other. Coerced to "other" if unrecognized.
change_typestring (required)add / remove / modify / rename / retype / create / delete / index_add / index_remove / migrate / revert / reject / supersede. Validated against the enum — typos are rejected. revert (“tried and rolled back”) and supersede (re-open a reverted decision) were added in v0.3.9.1; reject (v0.3.11) records “considered and decided against” without writing the change — the counterpart to revert for paths never taken.
diffstringThe diff text. Optional but recommended.
reasoningstringThe why. Captured from the agent’s own context. Run through the quality validator — empty / too-short / generic-placeholder values produce warnings (advisory).
agentstringThe calling agent name (claude-code, cursor, copilot, etc.).
session_idstringOptional session correlation ID.
git_commitstringCommit hash. Usually unset at log time and backfilled by the post-commit hook.
projectstringProject name. Defaults to the project root’s basename.
changeset_idstringA slug grouping related changes under one feature/task. Indexed.
rename_fromstringThe entity’s previous path, for renames. Set it with change_type="rename" and put the new path in entity_path. Selvedge then writes the dual-event pattern — a rename on the old path and a create on the new path with metadata.renamed_from set — so blame / diff / prior_attempts on the new path keep the history. A rename_from without change_type="rename" is rejected. Rename is a parameter, not a separate tool.
revisit_afterstringA revisit date for the decision — an ISO-8601 date (2026-12-01) or a relative offset from the event’s timestamp (90d, 6mo), normalized with the same grammar as --since. Consumed by stale_decisions / selvedge stale. New in v0.3.8.
constraintstringv0.3.9.1. The testable principle behind the decision (card data in our own DB = PCI scope), kept as its own queryable field instead of buried in reasoning.
stale_whenstringv0.3.9.1. The evidence that would invalidate the decision (payment provider changed). stale_decisions keyword-matches it against later change events and flags review_suggested — surfacing only.
expires_whenstringv0.3.11. A machine-checkable expiry condition in a closed four-shape grammar: library:NAME>=VERSION, entity:PATH:changes, date:ISO, manual:LABEL. Validated at write time — values outside the grammar are rejected, not stored. stale_decisions evaluates it locally (no network, no LLM) and flags expired with the pattern that fired.
supersedesstringv0.3.9.1. The id of the prior event this change overrides. Only valid with change_type="supersede"; leave empty to auto-link the entity’s most recent removal (remove / delete / index_remove / revert / reject — so after a standalone rejection it re-opens the rejection). The store stays append-only — the old verdict is never edited, just derived as superseded.

entity_path is canonicalized on write (strip leading ./, collapse //, normalize separators to /, trim — case preserved on purpose) at a single storage chokepoint shared by the MCP and CLI write paths, so src/auth.py::login and ./src/auth.py::login resolve to the same entity. entity_path is also shape-checked per entity_type (e.g. a function path without ::) — a soft warning in the warnings array, never a rejection.

{
"id": "uuid",
"timestamp": "2026-04-22T15:31:02Z",
"status": "ok" | "error",
"error": "",
"warnings": ["reasoning too short", "..."]
}

Every key is always populated — empty string / empty list when not applicable. This keeps the outputSchema clean and lets agents type-check without branching.


History for an entity or entity prefix.

NameTypeDescription
entity_pathstring (required)Exact match (users.email) or prefix (users returns all users.*).
limitintDefault 20.
[
{ "id": "...", "timestamp": "...", "entity_path": "...", "change_type": "...",
"diff": "...", "reasoning": "...", "agent": "...", "git_commit": "...",
"project": "...", "changeset_id": "..." },
...
]

Newest-first (abbreviated — each event also carries entity_type, session_id). LIKE queries properly escape _, %, and \.


Most recent change + context for an exact entity. The query everyone runs first.

NameTypeDescription
entity_pathstring (required)Exact match only — no prefix expansion (use diff for that).
{
"id": "...",
"timestamp": "...",
"entity_path": "...",
"entity_type": "...",
"change_type": "...",
"diff": "...",
"reasoning": "...",
"agent": "...",
"session_id": "...",
"git_commit": "...",
"project": "...",
"changeset_id": "...",
"metadata": {},
"revisit_after": "",
"expires_when": "",
"error": ""
}

On miss (no history found), every field is empty and error carries the “no history found” message. The protocol-level isError is false — empty history isn’t a protocol failure; the in-payload error key is the documented signal.


Filtered history across all entities.

NameTypeDescription
sincestring15m / 24h / 7d / 5mo / 1y / ISO 8601. Unparseable → error.
entity_pathstringExact or prefix.
projectstringExact match.
changeset_idstringExact match.
limitintDefault 50.

Array of event objects, newest-first. Same shape as diff (abbreviated above — each event also carries entity_type, session_id).


All events grouped under a named feature/task slug.

NameTypeDescription
changeset_idstring (required)The slug. Returns events oldest-first to reconstruct chronology.

Array of event objects, oldest-first. Empty array if the changeset has no events (returned with error: "..." per the same convention as blame).


Full-text search across reasoning + diff + entity_path.

NameTypeDescription
querystring (required)Free-text query. Wildcards _ and % are escaped — they match literally, not as SQL LIKE metacharacters.
limitintDefault 20.

Array of event objects, newest-first. Match score is implicit (newest first within matches); a future version may add --score ranking.


Prior change attempts on an entity, each with an inferred outcome. Call this before editing an entity — if the same change was tried before and reverted, you get the prior reasoning and why it was rejected, so you can change your plan instead of repeating it. New in v0.3.7; this is Selvedge’s wedge.

NameTypeDescription
entity_pathstringThe entity you’re about to change. Exact + prefix match (users also covers users.email). Provide this or description.
descriptionstringFree-text description, when you don’t have an exact path. Matched as a substring against prior reasoning / diffs / paths. entity_path wins if both are given.
fuzzystringv0.3.9.1. Semantic query — also returns attempts on entities whose prior reasoning is similar to this text, catching renames (card_token → payment_token) that exact/substring lookups miss. Rows are labeled match_type="fuzzy" with a similarity score. Needs the selvedge[semantic] extra and a selvedge index run; otherwise falls back to substring with a leading note row.
min_confidencestringproximity_high (default) returns the high-signal rows: attempts closed by an explicit revert/reject (confidence exact, v0.3.11 — always clears this floor, standalone rejections included) plus the clear tried-then-reverted proximity cases. proximity_low widens to the noisy tail (still-active changes, far-apart reverts).
window_minutesintProximity window for the add→remove revert heuristic — since v0.3.11 the tiebreaker for implicit removals only. Within it ⇒ proximity_high, beyond ⇒ proximity_low; attempts closed by an explicit revert/reject are exact regardless of the window. Default 10080 (7 days).
limitintDefault 20.

Array of event objects (newest-first), each with the trail fields:

[
{ "...event fields...": "...",
"outcome": "reverted" | "rejected" | "reopened" | "active",
"confidence": "exact" | "proximity_high" | "proximity_low",
"outcome_reasoning": "why it was reverted or rejected, or \"\" while still active",
"superseded_by": "id of the supersede that re-opened it, or \"\"",
"supersede_reasoning": "why it was re-opened, or \"\"",
"current_status": "active" | "reverted" | "reopened",
"match_type": "exact" | "substring" | "fuzzy",
"similarity": 0.0 }
]

Stated outcomes first, inference second (v0.3.11). An attempt closed by an explicit revert or reject event reports confidence: "exact" — the outcome is written in the log, not guessed. A standalone reject (a path never taken, so there is no earlier attempt to close) surfaces as its own row with outcome: "rejected", whose reasoning is the record. The add→remove proximity heuristic remains as the tiebreaker for implicit removals, and supersede links (v0.3.9.1) extend the trail — it reads tried → reverted → re-opened. Treat reverted and rejected as “don’t repeat this without a supersede”. The output is templated and deterministic — no LLM call in core — and the tool is pull-only: it never writes and never pushes; you decide when to ask.

Conservative by design. min_confidence defaults to proximity_high, so an empty list — nothing clearly tried-and-rejected — is the normal, preferred answer over a speculative false positive; exact rows always clear that default floor. You get one shot at the agent’s trust budget. If you filter results on confidence, accept exact wherever you accepted proximity_high — rows moved between those values in v0.3.11.


Decisions now due for a revisit — expired, past their revisit date, or with a triggered stale condition. New in v0.3.8 as the date-based half of active memory; v0.3.9.1 added condition matching, and v0.3.11 lit up the expires_when evaluator.

Three deterministic surfacing rules, named by each row’s flag:

  • expired (v0.3.11) — the decision’s expires_when condition fired, evaluated from local state only: date:ISO against the clock, entity:PATH:changes against the event log, library:NAME>=VERSION against installed distribution metadata. expired_pattern names the grammar shape that fired. A library: condition whose dependency isn’t locally observable surfaces as manual_review instead of a guess, and manual:LABEL never auto-fires.
  • revisit_due — revisit_after has passed and the entity is still live, so an old-but-correct decision nobody touches never nags.
  • review_suggested (v0.3.9.1) — the decision’s stale_when text shares keywords with a later change event: the named invalidation evidence may have happened. Surfacing only — nothing is un-retired automatically; follow up with a supersede.
NameTypeDescription
entity_pathstringFilter to one entity (exact + prefix match). Omit to scan all dated decisions.
projectstringExact match.
agentstringExact match.
limitintDefault 20. Date-due rows first, most-overdue leading.

Array of event objects, each with the surfacing fields on top:

[
{ "...event fields...": "...",
"flag": "expired" | "manual_review" | "revisit_due" | "review_suggested",
"revisit_due": "2026-...Z",
"days_overdue": 12,
"active_use_signals": ["queried"],
"matched_terms": [],
"matched_event_id": "",
"expires_status": "",
"expired_pattern": "",
"expires_detail": "",
"stale_reason": "past its revisit date and still active — the entity was queried (blame/diff/prior_attempts) after the decision." }
]

Every field always populated, never null — empty string / empty list where a rule didn’t apply.

Active-use weighting — pure age never surfaces. A revisit_due decision only comes back if its entity is still live: it was queried (blame / diff / prior_attempts) at or after the decision was logged, or its changeset_id saw later sibling activity. active_use_signals lists which signals fired. A dated decision nobody has touched is filtered out — that’s the noise defense against old-but-correct decisions. The output is templated and deterministic — no LLM call, no network — and the tool is read-only.


Every tool advertises:

log_change readOnly=false destructive=false idempotent=false openWorld=false
diff readOnly=true destructive=false idempotent=true openWorld=false
blame readOnly=true destructive=false idempotent=true openWorld=false
history readOnly=true destructive=false idempotent=true openWorld=false
changeset readOnly=true destructive=false idempotent=true openWorld=false
search readOnly=true destructive=false idempotent=true openWorld=false
prior_attempts readOnly=true destructive=false idempotent=true openWorld=false
stale_decisions readOnly=true destructive=false idempotent=true openWorld=false

log_change is append-only — destructive: false even though it writes — but not idempotent, since each call mints a new event with a fresh UUID and timestamp.


Each parameter on every tool ships with a description in inputSchema.properties, populated via Annotated[T, Field(description=...)] on the function signature. Agents picking which tool to call read these directly at tool-call time, so they’re a DX surface, not just directory metadata.

This was a major v0.3.3 fix — earlier versions left the rich descriptions in the function body where agents couldn’t see them. Coverage is 100% — every parameter on all eight tools.

  • Entity paths → — what counts as an entity, how prefix matching works.
  • CLI reference → — the same data, queried from the command line.
  • Comparison → — Selvedge’s MCP-tools approach vs. the git-hook + LLM-inference approach.