willow-mcp
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| WILLOW_PG_DB | No | Postgres database name | willow |
| SAP_SAFE_ROOT | No | SAFE folder root | ~/.sap/Applications |
| WILLOW_PG_USER | No | Postgres user (Unix socket auth) | $USER |
| WILLOW_STORE_ROOT | No | SQLite store directory | ~/.willow/store |
| SAP_PGP_FINGERPRINT | No | Pinned GPG fingerprint |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| store_putA | Write a JSON record to a named SOIL collection (created on first write).
Pass
|
| store_getA | Read one record from a SOIL collection by its exact ID. Returns the stored record plus _id/_created/_updated/_deviation/_action metadata, or {error: not_found} if the ID is absent or soft-deleted. Read-only; confined to your store_scope. |
| store_listA | Return every live record in one SOIL collection, oldest first, each with _id/_created/_updated metadata. Unfiltered and unpaginated — prefer store_search once a collection grows large, and store_collections to discover collection names. Read-only; confined to your store_scope. |
| store_updateA | Replace an existing record's data in-place — same ID, updated_at bumped,
audit metadata retained. Never creates: an unknown ID returns
{error: not_found} (use store_put to create). |
| store_searchA | Full-text search one SOIL collection: the query is split on whitespace and EVERY token must appear somewhere in a record's JSON (AND logic, substring match). Returns matching records with _id metadata; an empty query returns []. Read-only; confined to your store_scope. |
| store_deleteA | Soft-delete one record by ID: it disappears from get/list/search but is retained on disk for audit and recovery (hard removal is an operator act, deliberately outside the tool surface). Returns {deleted: true|false} — false means the ID wasn't found. Confined to your store_scope. |
| store_purge_collectionA | Soft-delete EVERY record in a collection at once — a bulk store_delete, for clearing out a whole collection (e.g. leftover test/scratch data). Archive, not drop: records are retained (deleted=1) and stay recoverable, they just fall out of get/list/search, matching the store's soft-delete model. The collection's store.db is never removed — hard removal is an operator/filesystem act, deliberately outside the tool surface. Guarded against fat-finger accidents: pass confirm= to proceed. Confined to your store_scope like every other store_* tool. |
| store_search_allA | Token-AND search across every SOIL collection you can see — all of them, or only your manifest's store_scope if one is set. Each hit carries a _collection field naming where it was found. Use when you don't know which collection holds the data; prefer store_search when you do. Read-only. |
| store_collectionsA | List the SOIL collections you can see — every collection under the store,
narrowed to your |
| store_statsA | Per-collection live-record counts for the collections you can see (narrowed to your store_scope) — the numeric companion to store_collections. Counts only live records (soft-deleted ones don't show). Returns each collection with its count (largest first), plus store-wide totals — handy for spotting a polluted or runaway collection before deciding what to purge. |
| lineage_recordA | Record a provenance atom — the "story of this willow" as memory an agent
can query later. Answers the questions agents keep asking: where did this
come from, why is it this way, what was here before. Relationships to other atoms are typed EDGES (stored in
Corrections re-record the same
|
| lineage_linkA | Add one provenance edge without (re)writing a node — e.g. mark an atom
|
| lineage_whyA | Answer "why does X exist / where did X come from" from recorded lineage.
Give a slug id or free text; returns the matching atom's rationale, origin,
authority, and evidence, PLUS its typed edges — what it supersedes (and
whether it is itself still current), what it was derived_from, and what
motivated it — the lineage, not a blob. A plain-language |
| lineage_listA | List recorded lineage atoms (id, title, whether current, tags) — the index
of "what parts of this willow have a recorded story". Pass current_only=True
to hide atoms that a later |
| friction_scanA | Scan a transcript window for the mirror failure mode: the agent has stopped
being other and is reflecting the user back, smoothed, WHILE the user is
escalating. Model-free and deterministic — no LLM, no egress; it NEVER blocks,
it only flags. When a window of agent turns sits below the friction
|
| friction_flags_listB | List recent friction flags recorded by |
| nestor_tool_routeA | Route a natural-language intent to the willow-mcp verb that serves it.
Returns status='served' with the resolved |
| nestor_tool_sealA | Teach the oracle: sanction that natural-language |
| nestor_tool_pendingA | The teach-queue: natural-language intents nestor_tool_route couldn't serve, newest first, deduped by phrasing — what a human should seal (or reject) so the oracle learns them. Empty when every routed intent has a sealed home. |
| session_bindA | Open a cryptographically-bound session (check-in). Provide a 13-field
HMAC-signed Once bound, the tier is LOGGED on your subsequent calls (receipt
|
| session_reconcileA | Close a bound session and reconcile what you DECLARE you did against what the receipt log shows you actually did (check-out; willow-gate seam Phase 4 / H3).
Requires a live session bound to your app_id (call session_bind first); returns {error} if there is none. This RECONCILES and records — it never blocks a handoff, so run session_handoff_write / handoff_write_v4 as usual alongside it. |
| knowledge_ingestA | Add a knowledge atom to the Postgres knowledge base. Check for duplicates first.
|
| nest_scanA | Walk a drop folder, extract + classify its files, and write a canonical SQLite Nest DB. Returns structure only (counts by source status and fragment type) — never file content. dry_run=True (default): classify and report counts WITHOUT writing the DB — inspect what a dump would become before committing it. dry_run=False writes. use_embed uses an Ollama embedding model when present (falls back to regex offline); use_llm escalates the uncertain tail to a text/vision model. Inference stays on this machine by default. It is NOT unconditional: the
seams post to $OLLAMA_HOST, and if that points off-box this tool requires the
operator's standing |
| nest_statusB | Counts for a seeded Nest DB — sources by status, fragments by type, topical categories by size. Structure only; no content. |
| nest_digestA | A one-page Markdown map of a seeded Nest DB — the WALLED view: category breakdown, discovered clusters, secret kinds, and how files were read. Person names, the date timeline, and source filenames are suppressed (they are content, not structure). The full unwalled digest is a local-CLI affordance only; it is never returned over MCP. |
| nest_promoteA | Promote a Nest's STRUCTURE into the knowledge base. Reads structure-only atoms from bridge.build_bridge — counts, curated category names, and redacted secret kinds, never fragment content, filenames, or person names — and ingests each through the same core write knowledge_ingest uses. dry_run=True: return the atoms that WOULD be promoted (safe to inspect — they are structure only) without writing. dry_run=False ingests them.
|
| nest_intake_scanA | Scan drop zone(s), classify new files by filename into tracks, and stage a
review queue. Idempotent (a file already staged is not re-staged) and
non-destructive — nothing is moved until nest_intake_file. |
| nest_intake_queueA | List the pending review queue — files staged by nest_intake_scan awaiting a confirm/override/skip decision, with the track the classifier predicted. |
| nest_intake_fileA | File a staged item: MOVE the file to its predicted track's destination, or
to |
| nest_intake_skipA | Skip a staged item — leave the file where it is and record the skip (removes it from the pending queue; the decision is logged as feedback). |
| nest_intake_flagsA | List open rule-delta flags — patterns the classifier got wrong often enough (CORRECTION_FLAG_THRESHOLD overrides) that it proposes a rules change. The classifier never rewrites its own rules; a human ratifies the delta. |
| gap_logA | Log or bump a "we don't know this yet" entry. Repeated asks of the same topic+question increment asked_count instead of duplicating — asked_count is the backlog's own priority signal for what to fill in next. Returns {id, status, asked_count}. |
| gap_listA | List backlog gaps, most-asked first — the fleet's shared "what we don't
know yet" queue. Filter by |
| gap_resolveA | Mark a gap as being worked or answered — bookkeeping only, does not write to the knowledge base. Use gap_promote to actually land a verified answer and close the gap out. |
| gap_deleteA | Soft-delete a single gap by id — for clearing junk or test entries from the backlog without disturbing its real gaps. Archive, not drop: the gap is retained (deleted=1) and just stops appearing in gap_list. Returns {deleted, id}, or {error: not_found}. |
| gap_purge_topicA | Soft-delete every gap under an exact topic in ONE call — bulk cleanup of a junk/test namespace without hitting gap_delete's per-call rate limit. Promoted gaps (which point at a landed knowledge atom) are left intact. Archive, not drop: purged gaps are retained (deleted=1), just removed from gap_list. Pass confirm= to proceed. Returns {purged, skipped_promoted, topic}. Note: gaps are a FLEET-SHARED backlog, not store_scoped — this purges every
app's gaps under the topic, so it's gated on its own |
| gap_promoteA | Turn a gap into trusted knowledge. Requires an answer, at least one source, and who's vouching for it (confirmed_by) — a human name, an agent id, whatever this fleet uses as an identity, but never empty. Writes through the SAME schema-confirmation gate as a direct knowledge_ingest call: if the 'knowledge' table mapping for this app_id hasn't been confirmed via schema_confirm_mapping, this refuses with unconfirmed_schema exactly like knowledge_ingest would. Requires Postgres — gap_log/list/resolve work SOIL-only, but promotion targets the durable, searchable knowledge base. Gated as its own permission (gap_promote), separate from gap_write, the same way schema_admin is kept separate from knowledge_write — landing something as trusted knowledge is a more consequential act than logging or resolving a gap. |
| knowledge_searchA | Search the fleet Postgres knowledge base by content: the query is
whitespace-split and ALL tokens must appear in an atom (AND logic).
|
| task_submitA | Submit a task to the Kart sandboxed execution queue. Returns task_id for polling. Tasks run network-isolated by default. Egress needs the Local Postgres access (socket mount + PG/POSTGRES env) requires The Kartikeya executor verifies all host gates and the signed envelope again
immediately before shell launch. Missing attribution or envelope, an invalid
signature, expiry, replay, task mutation, unavailable verifier, or a strict
trust-root failure denies network. Signing is available only through the local
interactive Task text is security-scanned at SUBMIT time (defense-in-depth): a task the
Kart scanner would refuse — destructive, exfiltration, secret access, obfusc-
ation, or resource-exhaustion (fork bomb / spin / disk-fill) — is rejected
here before it ever occupies a queue slot, not only when the worker later
picks it up. The worker re-scans at execution regardless; this just denies
earlier and keeps a bomb from sitting |
| task_statusA | Poll one task submitted via task_submit. Reads the fleet Postgres queue and returns the task row — status, result, submitted_by, timestamps — or {error: not_found} for an unknown task_id. Read-only; poll until the worker marks the task complete. |
| task_listA | List tasks still waiting in the Kart queue for one worker |
| kb_atA | Fetch one knowledge-base atom by its exact ID from the fleet Postgres — content, domain, source, and tags. Returns {error: not_found} for an unknown ID. Read-only; the direct-address companion to knowledge_search. |
| knowledge_flagA | Attach a visible integrity flag to an existing KB atom (tags only). Idempotent: re-flagging replaces the prior flag metadata. Original content
is unchanged. Requires |
| knowledge_retractA | Tombstone a KB atom in place — hidden from default search, fetchable by id. Adds |
| kb_promoteA | Move an existing knowledge atom to a different domain — e.g. lift a 'journal' entry into a topical domain once it proves durable. Updates the atom in place; content and ID are unchanged. Returns {id, domain} or {error: not_found}. Requires a confirmed 'knowledge' schema mapping. |
| kb_journalA | Append a free-text journal entry to the knowledge base: a new atom in
domain 'journal', tagged 'journal' plus any |
| kb_ingestA | Promote a ratified agent_seed slice to Postgres KB (source_type: agent_seed). Requires ratified + trusted seed at $WILLOW_HOME/seeds/{agent_id}.json. slice: voice_only | work_context | full (omit to use exposure.json default for kb_ingest). Never promotes persona.cast or context.personal_note.
|
| schema_confirm_mappingA | Confirm a table's schema mapping, unlocking write tools for it (knowledge_ingest,
kb_journal, kb_promote today). preview=True: dry-run — return the proposed mapping AND a rendered |
| kb_startup_continuityA | Fetch the knowledge atoms marked for session-startup continuity —
domain 'continuity' or tagged "continuity" — newest first, up to |
| agent_routeA | Record a routing decision — "this task goes to that agent" — in the fleet's routing_decisions ledger and return a routing_id for correlating the eventual outcome. Does NOT start or notify the target agent: pair with dispatch_send to actually deliver the work, and report completion via agent_dispatch_result. |
| agent_dispatch_resultA | Close the loop on an agent_route call: attach the outcome — |
| dispatch_sendA | Create a dispatch packet assigning work to another agent: writes
meta.json + assignment.md with status 'pending' under
$WILLOW_HOME/dispatch/. |
| dispatch_readA | Read one dispatch packet by ID: its meta (from/to app, role, phase, priority), current status, and the full assignment.md body. Read-only — how a specialist sees its brief before calling dispatch_accept. Returns {error: not_found} for an unknown dispatch_id, or {error: not_party_to_dispatch} if app_id is neither from_app, to_app, reply_to, nor the orchestrator (B-54, issue #242 -- dispatch_read permission alone used to let any holder read any dispatch_id's full content). |
| dispatch_listA | List dispatch packets newest-first, filtered by any combination of
to_app / from_app / status ('' = no filter). Returns packet metadata only,
no assignment bodies — use dispatch_read for one packet's brief.
Read-only. Returns {dispatches, total, unverified, unverified_total} --
|
| dispatch_acceptA | Accept a dispatch packet addressed to you: flips its status pending → working and records your session_id against it. Refuses if the packet is addressed to a different app (wrong_recipient) or is not currently pending (invalid_transition). Read the brief with dispatch_read first; close out with handoff_write_v4 when the work is done. |
| handoff_write_v4A | Close out a dispatch you accepted: writes handoff.json (the structured
|
| handoff_readA | Read the closeout of a completed dispatch: the structured handoff.json findings plus the closeout.md narrative. What the orchestrator reads before verify_handoff, and what a successor agent reads to pick up the thread. Read-only. Returns {error: not_party_to_dispatch} if app_id is neither from_app, to_app, reply_to, nor the orchestrator (B-54, issue #242 -- same packet-party check as dispatch_read). |
| verify_handoffA | Orchestrator-side check of a completed dispatch's handoff: confirms the closeout exists and its declarations hold — checklist resolved, envelope clean, findings present. Run this before agent_clear releases the specialist for its next packet. Read-only. |
| agent_clearA | Release a specialist after its handoff has been verified: clears
|
| session_readA | Read the thin per-session state file for an app/session pair — entry mode, bound dispatch, project/workspace — as written by session_enter. Returns {error: not_found} for an unknown session. Read-only. |
| session_enterA | FIRST CALL of any session. Registers the app/session pair, resolves the
entry mode (human seat vs dispatched specialist — pass |
| session_handoff_writeA | Close a human-entry session with a project-scoped markdown closeout:
|
| specialist_listA | List the specialist registry (config/specialists.json): every agent the orchestrator can route work to, with role metadata and — when include_permissions=true — each one's permission set. Returns {registry, specialists, total}. Read-only; use specialist_get for one agent's full row and persona. |
| specialist_getA | Fetch one specialist's registry row by agent_id, including its compiled persona text and persona file path when available. Returns {error: not_found} for an unknown agent. Read-only — how an orchestrator inspects a specialist before dispatching work to it. |
| exposure_config_getA | Read the standing exposure defaults from $WILLOW_HOME/config/ exposure.json — which preset governs each seed destination when exposure_slice is called without an explicit one. Returns {format, path, exists, config}; exists=false means no config file, so built-in defaults apply. Read-only (AS-8). |
| exposure_sliceB | Resolve and apply an exposure preset for a seed destination (AS-8). destination: session_enter | kb_ingest | agent_seed_mirror | grove | cloud_llm | dispatch. preset: override standing default. fields: custom dotted paths (checkbox IDs). |
| agent_seed_mirrorA | Mirror a ratified home seed into SOIL collection willow_agents_seeds (AS-5). Requires ratified status; when WILLOW_PGP_FINGERPRINT is set the detached .sig must verify. slice: full | voice_only | work_context (omit for exposure.json default). |
| fleet_statusA | Read the canonical fleet roster (fleet.json) — every registered agent and its declared state — annotated with drift diagnostics wherever the fleet Postgres disagrees with the file. Read-only; requires Postgres. Use fleet_health for liveness signals rather than roster membership. |
| frank_readA | Read recent entries from the FRANK governance ledger — the fleet's
hash-chained, Postgres-backed audit chain — newest first, optionally
filtered to one |
| frank_verifyA | Re-hash the entire FRANK governance chain and verify every prev_hash → hash link, detecting tampering, edits, or gaps. Returns the verification verdict, including where the chain breaks if it does. Also checks the chain's head against the externally-held anchor at
|
| frank_appendA | Append one event to the shared FRANK governance ledger: |
| envelope_applyA | Check an authority envelope — an operator-granted, scoped permission —
BEFORE acting under it: verifies #333 double-charging note: |
| fleet_healthA | Fleet health — task queue counts by status, plus live worker heartbeats. A queue depth is only half the picture: |
| context_saveA | Save ephemeral working state under |
| context_getA | Read a saved context by key. Returns {error: not_found} if absent, or {error: expired} (and purges it) if its TTL has passed. |
| context_listA | List your saved context keys with save/expiry times (values omitted — use context_get). Expired entries are skipped and purged. |
| context_expireA | Delete one of your saved contexts immediately, ahead of any TTL. Returns {expired: true} if a record was removed, false if the key didn't exist. Scoped to your app_id — you cannot expire another app's context. |
| integration_listA | List every integration adapter — live and declared stubs — with status, credential source (env/vault, never the value), and, for stubs, what is missing and what earns the implementation. |
| integration_statusA | Offline readiness readout for one integration: live or stub, credential presence, and whether the three-key egress gate would pass for this app right now. Makes no network call — ask this before asking for a lease. |
| integration_callA | Call an external API through a registered integration adapter. Egress needs THREE keys, all held at once: the 'integration_net' capability
in this app's manifest (own line — NOT granted by task_net, integration_call,
or full_access), the operator's standing consent.internet, and an unexpired
egress lease issued via |
| federation_discoverA | Shadow-IT scan: |
| federation_list_serversA | List every operator-ratified downstream MCP server: id, name, launch command/args, the environment-variable NAMES it receives (never values), who ratified it and when. Never includes a discovered-but-unratified server — connecting requires ratification first (see federation_discover). |
| federation_callA | Call one tool on one ratified downstream MCP server. Authorized only at the intersection of two ceilings (docs/design/
federated-mcp-gating.md Decision 2): this app's manifest must grant BOTH
the 'mcp_federation' capability (own line — spawning a server at all) AND
the namespaced The downstream tool's result is scanned by external-guard and sandwich-wrapped if flagged (untrusted output, same treatment willow_web_fetch gives fetched pages) before it comes back. |
| receipts_tailA | Return your own most-recent tool-call receipts, newest first: ts, tool, outcome (ok/denied/rate_limited/error), detail. Scoped to your app_id — a self-audit trail, never another identity's calls. |
| whoamiA | Report who you are and what you may do: your app_id, role, the permission groups your manifest grants, the resolved set of tools you can actually call (group expansion minus any deny_tools), your store_scope, and whether you're a human-only seat. Read-only and self-scoped — your own manifest, never another identity's: in serve mode the app_id comes from your OAuth binding, and in stdio under enforcement you must prove you own the app_id you pass (a valid per-call credential), so whoami can't enumerate another bound agent's config. Ungated otherwise, like diagnostic_summary, so it still answers when your manifest is empty or missing (it says exactly that). |
| diagnostic_summaryA | Self-check: is this willow-mcp install wired correctly? Reports the SOIL store (path/writable/collections), Postgres (reachable + which database + whether willow-mcp's tables are present), schema-mapping confirmation state, your app_id's manifest + resolved permissions, identity bindings, whether a task worker is alive to drain the queue, your egress lease and which of the keys authorizing it this process could forge, whether this install is still wired to a fleet it claims to be severed from, and the config-bearing environment — then a verdict (ok/degraded/broken) with named problems and fixes. Ungated on purpose: it must answer even when your manifest or database is misconfigured. Reveals only your own config, never fleet rows or vault secrets. Severance is asserted, never assumed: name a fleet with WILLOW_MCP_FLEET_HOME
and WILLOW_MCP_FLEET_PG_DB and every shared surface becomes a named problem.
Name none and the check reports |
| commitment_ingestA | Ingest calendar events into the operator's commitment ledger and persist the
result (SOIL collection
This tool NEVER writes the calendar back — a cancel/reschedule is a proposal routed through the SAFE gate, which is deliberately not exposed over MCP (no new authority). |
| commitment_acknowledgeA | Mark a commitment change as seen by the operator — the split-stick halves match again. Recorded as an 'acknowledged' history entry (never erased) and it stops surfacing as a 'mismatch'. Returns unknown_uid if there is no such commitment. |
| commitment_surfaceA | The dew-rule view: what — if anything — deserves the operator's attention right
now. Silent by default; it speaks only when the split-stick halves disagree — a
commitment imminent (starting within
|
| commitment_listA | List the operator's persisted commitments as FACTS only (uid/title/when/who/
state/acknowledged/history length) — never the event body. Read-only. |
| code_graph_indexA | Index a repository's Python + JS/TS files into a local SQLite symbol graph
(symbols, import/inherit edges, per-file stats). Run once before the read
tools. Pure stdlib
|
| code_graph_searchA | Fuzzy symbol search over the graph: exact → prefix → contains →
camelCase/snake_case token split. |
| code_graph_explainA | Explain a symbol: signature, file location, callers (inbound edges), and
callees (outbound). |
| code_graph_walkA | BFS from an anchor symbol along import + inherit edges, collecting context
until a token budget is hit. Deterministic (alphabetical within each hop).
|
| code_graph_suggestA | Suggest the files most relevant to a task description, ranked by keyword overlap with symbol names + file paths. No embeddings, no LLM. Read-only. |
| code_graph_impactA | Blast radius: which files/symbols import from the given files? |
| willow_web_searchA | Open web search via DuckDuckGo HTML (no API key). Returns title, url, snippet, source, and hostname per result. Use for current events, tech news, and queries that need the live open web.
|
| willow_institutional_searchA | Search ~60 named institutional and academic collections — arXiv, PubMed, Crossref, OpenAlex, Library of Congress, Europeana, CourtListener, the Smithsonian — and return citable results. Every hit carries Read
Requires web_net + consent.internet + a live egress lease. |
| willow_web_fetchA | Fetch a URL through external-guard (not native WebFetch). Returns text with sandwich defense when wrap=True. Blocks private, loopback
and other non-public destinations — resolving names, not just matching them
— and re-checks every redirect hop rather than the first URL alone; the
chain taken comes back in |
| fork_createA | Open a fork — a named, bounded unit of work (feature branch + PR
tracking) recorded in the SOIL store. Snapshots selected environment
variables so env_check can later spot drift between where the fork was
opened and where it's being worked. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/rudi193-cmd/willow-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server