Skip to main content
Glama

CI Go Reference Go Report Card License: MIT

IDE intelligence for agents — guardrails for unattended work, coordination for fleets.

Plumb is an MCP server that gives a coding agent the intelligence layer of an IDE — LSP-backed semantics, a tree-sitter code index, and project memory — inside guardrails: atomic, lock-serialised writes with transactional rollback, scoped filesystem and git access, and a daemon that survives its own crashes. And because every agent you run shares that one daemon, plumb is also the coordination layer between them: peers see the writes others made, message each other, and hand off work instead of duplicating it. A single binary; nothing else to install.


Why Plumb

LLM agents usually work by reading whole files into the context window — token-heavy, lossy at scale, blind to symbol semantics, and unsafe to let loose on a real repo. Plumb is built on four pillars, in priority order.

1. Reliability & write-safety

Leaving an agent to edit a codebase for an hour is only viable if writes can't corrupt files and a crash can't wedge your session.

  • Atomic I/O — every write is staged in a temp file and renamed into place. No partial writes, ever. Symlink-aware, CRLF-tolerant.

  • Per-path locking — the daemon serialises concurrent writes to the same file across every session and chat window. No races.

  • Multi-file transactions — apply edits across dozens of files with guaranteed atomic rollback if any step fails.

  • Crash-resilient daemon — plumb serve is a reconnecting proxy. If the daemon crashes or hangs, it respawns one and replays the handshake; the agent never notices. In-flight writes are never silently re-run.

  • Optimistic concurrency — mtime/sha guards catch stale edits before they clobber newer changes.

See it run: docs/demos/ — two-agents-one-file.sh (a stale write is refused, nothing is lost) and daemon-respawn.sh (below — the daemon is killed mid-session; the agent's next edit still succeeds):

daemon-respawn.sh: the daemon is killed mid-session and the agent's next edit still succeeds

2. Multi-agent coordination

One daemon serves every agent you run — which makes it the natural place for agents to see and talk to each other, not merely avoid each other's writes. Locks stop two agents corrupting a file; coordination stops them duplicating a task, rebasing onto a function signature a peer is mid-rewrite of, or shipping a change a peer's in-flight work is about to invalidate.

  • Peer awareness (on by default) — workspace_sessions names every active session and the writes it made, as the daemon recorded them. Recorded activity, not another agent's say-so: an agent about to start a task can see that a peer is already in those files. (Read-only operations never appear, and a write that failed or was refused is kept but marked [failed — no change applied] — so "a peer is working here" and "this landed" are distinguishable at a glance.)

  • An agent-to-agent mailbox (on by default, same workspace) — leave_note / check_messages give sessions a threaded channel: hand a change to the peer already rewriting those files, or ask a peer to measure a behaviour instead of assuming it. Messages ride on ordinary tool results, so a working agent receives them without polling.

  • Advisory intents (opt-in: [collab] intents) — share_intent declares what an agent is working on; a peer whose write touches a claimed path gets a hint at the moment of the would-be collision. Intents are deliberately labelled as unverified claims, kept distinct from the daemon-recorded activity feed, and never block anything.

  • Durable findings (opt-in: [collab] knowledge_handoff) — share_findings turns what an agent just learned into a searchable, secret-scrubbed project memory immediately, so the knowledge outlives the session that produced it.

Coordination is advisory by design — the write-safety above never depends on agents cooperating. Reference: Cross-agent sharing in the tool docs and the [collab] config section.

3. Semantic intelligence

The same primitives your editor has, exposed as structured tools:

  • LSP-backed refactors — rename_symbol, replace_symbol_body, safe_delete_symbol understand scope, types, and references.

  • Real diagnostics inline — actual gopls/pyright output is appended to every write, so the agent learns it broke the build immediately.

  • Symbol search — scoped to your code, no stdlib or dependency noise.

4. Context efficiency & safety controls

  • Read only what you need — symbols or line ranges, not 2,000-line files.

  • Scoped access you control — a per-connection path allowlist (read-only vs read-write roots) plus tiered git gating (destructive and network operations are off by default and need explicit confirmation). See SECURITY.md.

  • One-round-trip bootstrap — session_start returns workspace, branch, recent commits, diagnostics, and project memory.

See the measured, reproducible numbers behind this: docs/use-cases.md — reading one function is 2.9×–33.4× less context than the whole file (the ratio is how much of the file you didn't need), and find_references returns the real call sites where a text search is 60% noise. The page publishes the losses too: read_multiple_files costs 1.31× more payload than reading the files natively (down from 1.32×, but still a loss — see Scenario 10 for why it isn't smaller). Every figure is regenerated by scripts/measure-use-cases.py.


Related MCP server: AI Knowledge Center MCP

Get started

Plumb is a single binary — from zero to your first answer:

1. Install

# Homebrew (macOS + Linux) — recommended
brew install plumbkit/plumb/plumb

# or with Go
go install github.com/plumbkit/plumb/cmd/plumb@latest

# or grab a prebuilt binary: https://github.com/plumbkit/plumb/releases

macOS note: prebuilt binaries are not yet notarised — on first run you may need xattr -d com.apple.quarantine ./plumb, or right-click → Open. Homebrew installs avoid this.

2. Connect your agent

plumb setup claude-code      # also: claude-desktop, codex, gemini, cursor, …

plumb setup writes the MCP config for you — no hand-editing JSON.

3. Open your project and try it

Make sure the language server you need is on your $PATH (gopls for Go, pyright for Python, …), then point your agent at a real question. In Claude Code:

cd your/project
claude "Use plumb to orient in this repo (session_start), then show me
everywhere <Handler> is called and what would break if I changed its signature."

Plumb resolves the workspace and runs session_start for orientation, then answers with real LSP and topology data — actual call sites and blast radius — instead of guessing from file dumps. It's read-only; nothing is modified. (Any connected agent works — just paste the prompt.)

No go.mod/pyproject.toml and not a git repo? Run plumb init once to pin the workspace root (it also seeds .plumb/context.md and project config).

Full walkthrough → docs/getting-started.md.


Language support (honest version)

Plumb negotiates LSP capabilities per language and also ships a built-in tree-sitter index for search and navigation with no language server. Support comes in tiers — we'd rather be precise than claim a big number.

Tier

Languages

What you get

First-class (CI-tested, real-binary integration)

Go (gopls), Python (pyright)

Full LSP: definitions, references, rename, diagnostics, hierarchies + all write tools

Validated

Java (jdtls), Rust (rust-analyzer), Swift (sourcekit-lsp), TypeScript/JS (typescript-language-server), Zig (zls), Kotlin (kotlin-lsp), HTML (vscode-html-language-server)

Full LSP; just put the server on $PATH and it activates automatically (exclude any language with [lsp.<lang>] enabled = false). HTML carries one caveat: that server has no filesystem access, so it answers only from documents already opened

Search & navigation (tree-sitter, no LSP needed)

31+ incl. JS/TS/TSX, Ruby, C, C#, Elixir, Scala, PHP, JSON, CSS, SCSS, XML, Lua, C++, Objective-C, Dart, Bash, SQL, HCL, Dockerfile, TOML, YAML, Markdown

Ranked symbol search, outlines, graph exploration via the Topology index

Real-binary validation has been exercised on macOS and Linux — as of 2026-08-21, all nine adapters pass their integration tests against real server binaries on both. Details, including three toolchain traps that look like adapter bugs, are in docs/adding-an-lsp.md. Windows is tracked but not yet supported — the daemon's Unix-socket architecture needs a port.


How it works

plumb serve is a thin, reconnecting stdio proxy. The real work happens in one shared background daemon, so language servers stay warm across chats.

flowchart TD
    A1["Claude"] --> S1["plumb serve *"]
    A2["Codex"] --> S2["plumb serve *"]
    A3["Gemini"] --> S3["plumb serve *"]
    S1 --> K["plumb.sock"]
    S2 --> K
    S3 --> K
    K --> D["plumb daemon **"]
    D --> SDB[("stats.db ***<br/>global — all projects")]
    D --> G["gopls → /projects/foo"]
    D --> P["pyright → /projects/bar"]
    G --> F1[("/projects/foo/.plumb/ ***<br/>topology.db · memory.db")]
    P --> F2[("/projects/bar/.plumb/ ***<br/>topology.db · memory.db")]

* plumb serve is a reconnecting proxy — if the daemon crashes or hangs it respawns one and replays the handshake, so your session survives without the agent noticing.

** one shared process, reused across every conversation.

*** SQLite. One global stats.db (tool stats + episodic summaries); two per-project indexes under each workspace's .plumb/ — topology.db (the code graph) and memory.db (memory search). Schema details → docs/architecture.md.

Servers stay warm across chats, per-path locks are shared across every connection, and symbol indexes update live after each write. Full architecture → docs/architecture.md.


Monitoring (TUI)

Run plumb with no arguments for a live dashboard — see what your agent is doing in real time: every tool call as it happens, daemon health, per-tool stats, and streaming logs you can follow and filter. The fastest way to catch a runaway loop or confirm an edit landed.


Core capabilities

Plumb exposes 61 tools. The ones you'll use constantly:

session_start · workspace_symbols · get_definition · find_references · rename_symbol · edit_file · transaction_apply · diagnostics

The rest cover filesystem reads/writes, LSP hierarchies, tiered git, an optional local Topology index (ranked search + blast-radius/route analysis, no language server needed), durable per-project memory, and cross-agent coordination (peer sessions, an agent mailbox, opt-in intents and knowledge handoff). Full API reference: docs/tools.md.


Configuration

Global or per-project config.toml, or environment variables. Run plumb config show to see the resolved config with provenance.

[edits]
strict = true                  # require read_file before edit_file
rate_limit_per_minute = 30     # bound runaway agent loops

[git]
allow_destructive = false      # reset/checkout/rebase off by default
allow_push = false             # push/fetch/pull off by default

Full settings reference: docs/configuration.md.


The hard part

Agents can already read code well enough; writing it unsupervised — concurrently, transactionally, recoverably — is what's still unsolved. Plumb is the bet that this is the half worth getting right first. It's early, and the language coverage says so: a small validated core, the rest clearly marked experimental.


Roadmap

Plumb is pre-1.0. The core — write-safety, the resilient daemon, the topology index, and project memory — is in daily use. The road to 1.0 is mostly about proving it beyond the validated core and smoothing distribution. Issues and ideas welcome.

Shipped

  • Concurrency-safe, atomic, transactional writes with rollback

  • Crash-resilient reconnecting daemon

  • Tree-sitter topology index + per-project memory

  • Cross-agent coordination: peer awareness + agent mailbox (default on), intents + knowledge handoff (opt-in)

  • Go and Python LSP adapters validated (real-binary)

Getting to 1.0. Rather than jump from 0.9 straight to 1.0, Plumb ships a series of focused minor releases — 0.10 through 0.19 — each with one coherent theme. 0.19.x is the last 0.x release; 1.0 follows it as a deliberate stability commitment. Native Windows support is intentionally a post-1.0 (1.1) item, not a 1.0 gate. The themed plan:

  • 0.10 — distribution + honest claims (Homebrew, semantic re-rank → GA)

  • 0.11 — validate the experimental LSP adapters on real binaries (zls ✓ validated; Kotlin ✓ validated on JetBrains' kotlin-lsp)

  • 0.12 — Swift on Xcode via Build Server Protocol guidance

  • 0.13 — daemon robustness (git-write crash safety, liveness probe)

  • 0.14 — agent ergonomics + tool surface

  • 0.15 — honesty + full config surface

  • 0.16 — stabilisation + cross-platform proving

  • 0.17 — distribution + discoverability (registries)

  • 0.18 — proof + docs

  • 0.19 — soak + feedback, the last 0.x (rolling patches, not a formal RC)

  • 1.0 — general availability: the stability + validated-core promise

Full detail, rationale, and the post-1.0 items (Windows, tree-sitter cleanup) are in docs/roadmap.md.

Contributing

See CONTRIBUTING.md and AGENTS.md for architecture and code style. We follow Australian English in all prose. By contributing you agree to the Code of Conduct.

License

MIT — see LICENSE.

Available Tools

59 tools
agent_configA

Read and (when enabled) write a small allowlist of plumb config keys on the user's behalf — task commands ([tasks.]), log level, theme, topology excludes, quality analysers. op=describe lists exactly what you may write (always available); op=set writes a batch to the project's .plumb/config.toml, validated and applied all-or-nothing, tagged provenance=agent and one-step revertible (plumb config unset). Writing is OFF unless the user enabled [agent_config_writes]; safety-critical keys (git tiers, workspace roots, strict mode, API keys, the enable knob itself) are never writable. Use it to set up a repo's build/test commands from what you can read in the project.

Inputs: op (required), set, scope.

call_hierarchyA

Show the call hierarchy for a symbol: who calls it (incoming) and what it calls (outgoing). PREFER a name (uri + symbol_name) — plumb resolves the exact identifier position for you, avoiding off-by-one errors; a raw file position (uri + line + character) is the fallback and, when it lands off an identifier, is snapped to the enclosing symbol. Useful for understanding control flow and assessing the impact of changes. When the language server provides no call hierarchy for the file (e.g. zls for Zig), falls back to the topology call graph, annotated source=topology (approximate).

Inputs: uri, line, character, direction, +1 more.

check_messagesA

Read messages other agents have sent you, optionally waiting for one to arrive. Receive half of plumb's mailbox; leave_note is the send half. Full etiquette — addressing, delivery, the exchange cap, cross-project rules: the plumb-chat skill.

Omit wait_seconds (or 0) to return immediately with whatever is waiting. A positive wait_seconds BLOCKS until a message arrives or the wait expires — hand your turn to a peer instead of polling. Capped by [collab] max_wait_seconds, kept below the client's own call timeout.

Each message is handed over exactly ONCE, by this tool or session_start; re-calling will not redeliver it. The block on other tool results only PREVIEWS a message — it marks nothing read, so a client that hides it loses nothing. Every message carries a conversation_id; quote it in leave_note to reply in thread.

Also reports your OWN unread mail — anything you sent that nobody has taken delivery of yet, with its age. Listing is a read; it never consumes the message on the recipient's behalf.

Requires [collab] mailbox = true.

Parameters: wait_seconds — block up to this long for a message (default 0, no wait).

Inputs: wait_seconds.

copy_fileA

Copy a file to a new path, preserving file permissions. Parent directories of to are created if missing. Refuses to overwrite an existing destination unless overwrite=true. Cross-device copies are supported. Notifies the LSP server with FileCreated so diagnostics update immediately. The response shows the diff of what landed at the destination — every line added, plus the destination it replaced when overwrite=true — gated by [edits].show_write_diff. To move or rename a file, use rename_file instead.

Inputs: to, from, dirty_ok, overwrite.

daemon_infoA

Returns metadata about the current MCP session and daemon process: session name (e.g. swift-falcon), session ID, daemon version, the version of the serve proxy this session arrived through (a DIFFERENT binary, which a daemon restart does not update — check it before concluding a proxy-side fix is live), the source commit the binary was built from (with a dirty marker, or an explicit unknown), Go runtime, OS/arch, start timestamp, and uptime, plus the MCP protocol revision negotiated with this client (and, on a mismatch, the revision it offered and the capabilities it advertised), plus live config-store state (generation, last reload time, and whether a restart is needed for a pending restart-bound change), and — when available — this connection's workspace-pin provenance (how, when, and from where the pin was last set). It also reports this session's total tool-call count and its slowest calls (per-call durations from recorded stats). Use this to identify which session you are operating in or to verify the daemon state.

Inputs: no named parameters.

delete_fileA

Delete files and empty directories. Pass file_path for one, or paths for several in a single call (max 100). Refuses to delete directories unless allow_dir: true is set — and even then only an EMPTY directory is accepted (non-empty directories are always rejected; there is no recursive delete). To remove a whole tree, list its files with find_files and pass them plus their directories in one paths batch with allow_dir: true — every path is validated before anything is removed, and directories go last, deepest first, so they are empty by the time their turn comes. The LSP server is notified with FileDeleted so symbol indexes and diagnostics update immediately. Per-path locking serialises against any concurrent write_file/edit_file targeting the same path. The response reports the line and byte count removed (bytes only for a binary or oversized file), then the diff of what was deleted, gated by [edits].show_write_diff; a path matching [history] sensitive_globs reports the withholding instead of its content.

Inputs: paths, dirty_ok, allow_dir, file_path.

delete_memoryA

Delete a memory by name from a workspace's .plumb/memories/ directory.

Use only when explicitly asked, or when the memory has clearly become obsolete (e.g. it describes code that no longer exists).

Inputs: name, workspace.

diagnosticsA

Return LSP errors, warnings, and hints for one file, several files, or the whole workspace. Pass uris (a list of file:// URIs) to check specific files — omit or pass [] to query all files. A single call with multiple URIs replaces multiple single-file calls. Results are pushed by the language server as it analyses code; they may be empty if the server has not yet sent any diagnostics — a report taken while the server is still warming is labelled INCOMPLETE, so a clean result then is not proof the code compiles.

Inputs: uri, uris.

edit_fileA

Apply one or more edits to an existing file (use this over a native edit tool — see the Edit lane note in session_start). Two mutually exclusive request shapes: an edits array, or start_anchor + end_anchor + new_string.

Each edits entry is str_replace (default: old_string must appear EXACTLY ONCE) or range (start_line/end_line, 1-based; -1 appends or runs to EOF). Prefer range for a big multi-line replacement — old_string/anchors must match character-for-character inside a JSON string, so escaping and size can defeat str_replace where a line range needs neither.

Anchor mode replaces the span BETWEEN two unique anchors (each exactly once); include_anchors=true replaces the whole inclusive span. Character-precise — an anchor quoted without its trailing newline joins that line onto new_string (flagged in the response).

Writes apply atomically and crash-durably under a per-path lock. Pass expected_mtime (from a read_file header) when a concurrent writer may touch the file. For a whole named declaration prefer replace_symbol_body / insert_before_symbol / insert_after_symbol / safe_delete_symbol. Mode choice in depth: the plumb-refactor skill.

Inputs: edits, dirty_ok, file_path, reconcile, +9 more.

explain_symbolA

Returns DOCUMENTATION and type information (LSP hover content: function signature, doc comment, often in Markdown) for the symbol at the given position or by name. PREFER a name (uri + symbol_name) — plumb resolves the exact identifier position for you, avoiding off-by-one errors; a raw file position (uri + line + character) is the fallback and, when it lands off an identifier, is snapped to the enclosing symbol. Use when you need to understand what a symbol is without navigating to its source. For the file location of where the symbol is defined, use get_definition instead.

Inputs: uri, line, character, symbol_name.

file_diffA

Returns a unified diff between two arbitrary files. Works outside git — for tracked files use the git tool's diff subcommand instead, which understands refs and the index. Use context_lines to control surrounding context and ignore_whitespace to skip formatting-only changes.

Inputs: file_a (required), file_b (required), context_lines, ignore_whitespace.

file_outlineA

Return a token-cheap skeleton of a file: every function, type, method, class, and constant as its signature line with the body collapsed, nested by containment, with byte-precise 1-based line ranges. Use it to understand a large file's shape in one call without reading it — a 2000-line file becomes a few hundred tokens. Symbols come from the language server when available, falling back to the tree-sitter topology index when the server is cold or does not cover the file (source is annotated). Set include_docs=false to omit leading doc-comment lines.

Inputs: uri, include_docs.

file_statusA

Lightweight, read-only "did this file change under me?" check. For each path reports, without reading content: git_dirty (uncommitted changes vs git HEAD/index — untracked counts as dirty), changed_since_plumb_wrote (the on-disk mtime advanced since plumb last wrote it this session — a peer or external process edited it), last_writer (plumb = plumb wrote it last this session and it is unchanged; external = plumb wrote it but it has since changed on disk; unknown = plumb has not written it this session), plus mtime and size. Use before re-editing a file you read or wrote earlier to confirm your view is still current, instead of a blind re-read; pair changed_since_plumb_wrote / last_writer with a read_file to refresh when it reports drift. Missing files are reported, not an error. This is a status probe, not a content read — it does not satisfy strict mode's read-before-edit requirement.

Inputs: paths.

find_filesA

Workspace-scoped file/directory finder and directory lister. Unlike shell find/fd/ls, results are confined to the active project (no .git/, node_modules/, build output, or anything else .gitignore excludes), every call is recorded in the project's stats, and the pattern semantics are consistent across hosts. pattern is optional — omit it to list everything. Supports glob and regex patterns, extension and type (file/dir/any) filters, depth limits (max_depth=1 lists one level, like ls), sort_by name/size/modified, and include_details for a per-entry [FILE]/[DIR]/[LINK] marker, size, and modified time.

Inputs: path, type, exclude, pattern, +7 more.

find_referencesA

Find all references to a symbol across the entire workspace. Returns file path, line number, and the source line at each reference site. PREFER a name (uri + symbol_name) — plumb resolves the exact identifier position for you, avoiding off-by-one errors; a raw file position (uri + line + character) is the fallback and, when it lands off an identifier, is snapped to the enclosing symbol.

Inputs: uri, line, character, symbol_name, +1 more.

find_replaceA

Grep-equivalent: find text across files with optional replacement. Search and replace text across files in a directory tree.

Defaults to dry_run=true so you can preview the diff before committing. Set dry_run=false to write changes.

When the [edits].show_write_diff config flag is on (the default), the response appends a per-file unified diff — in both preview and applied modes — for up to the first 20 changed files, with a "+N more file(s)" summary beyond that. Set show_write_diff=false to suppress it.

Skips binary files (detected via null-byte sniff of the first 8 KB). Skips files larger than max_file_bytes (50 MiB default). Honours .gitignore. Use 'glob' to limit which files to touch (e.g. ".go", "**/.md"); a glob with a literal directory prefix (e.g. "src/**/*.go") prunes sibling directories from the walk entirely. Files are processed in parallel; output is sorted by path.

For identifier refactors use rename_symbol (scope- and type-aware); find_replace is for plain-text edits (doc strings, license headers, hostnames, version strings, non-code files).

Inputs: glob, path, dry_run, pattern, +7 more.

get_definitionA

Returns the SOURCE LOCATION (file path + line number) of where a symbol is defined. PREFER a name (uri + symbol_name) — plumb resolves the exact identifier position for you, avoiding off-by-one errors; a raw file position (uri + line + character) is the fallback and, when it lands off an identifier, is snapped to the enclosing symbol. Use when you need to navigate to the implementation of a symbol. For documentation or type signatures at the same position, use explain_symbol instead.

Inputs: uri, line, character, symbol_name.

gitA

Run git through one tiered, policy-gated tool (no shell, no agent-supplied command line). Read subcommands (status, log, diff, show, blame, shortlog, branch/tag/stash listing) always run. Write (add, commit, switch, mv, merge, branch/tag create, stash push/pop) needs [git] allow_writes (default on). Destructive (reset, clean, checkout, restore, rebase, revert, cherry-pick, branch/tag delete, stash drop) needs allow_destructive AND confirm:true. Network (push, fetch, pull) needs allow_push AND confirm:true; force-pushing a protected branch or using an ad-hoc URL/remote is always refused.

add and commit are typed: add stages with -A semantics; commit takes message, plus an optional files list for a path-limited commit. Every other subcommand uses args.

Cross-session guard refuses a write/destructive/network op if a DIFFERENT session moved this repo's HEAD/branch since observed (override with confirm:true). expected_head pins the exact HEAD commit those ops must be at.

Full tier table, the cross-session guard, commit attribution, and the narrower plumb tool to prefer over a destructive git call: the plumb-git skill.

Inputs: args, repo, files, confirm, +3 more.

git_initA

Initialise a new git repository at the given path (git init). The directory is created if it does not exist. Set init_plumb: true to also create a .plumb/ workspace marker with a blank context.md, so plumb attaches to the project automatically on the next session.

Inputs: path, init_plumb.

insert_after_symbolA

Insert text immediately after a symbol's declaration.

Useful for adding a new method to a struct (insert after an existing one), or appending a related helper. Provide the full text to insert in 'content' — include leading newline if appropriate.

The response includes a unified diff of the change — a preview in dry-run, the applied change otherwise — unless show_write_diff is disabled.

Works even when the language server is cold or cannot parse the file: it then locates the symbol via a fresh tree-sitter parse (line-granular range, annotated in the output).

Inputs: name_path (required), uri, content, dry_run, +1 more.

insert_before_symbolA

Insert text immediately before a symbol's declaration.

Useful for adding a new function/method before an existing one, or prepending a doc comment. Locates the symbol via the LSP document symbol tree (no manual line counting). Provide the full text to insert in 'content' — include trailing newline if appropriate.

Set include_doc_comment=true to insert before any existing leading doc comment instead of between the comment and the symbol — useful when adding a new function (with its own doc comment) above a function that already has one.

The response includes a unified diff of the change — a preview in dry-run, the applied change otherwise — unless show_write_diff is disabled.

Works even when the language server is cold or cannot parse the file: it then locates the symbol via a fresh tree-sitter parse (line-granular range, annotated in the output).

Inputs: name_path (required), uri, content, dry_run, +2 more.

leave_noteA

Send a message to another agent — a named peer session, or "next" (whoever attaches to this workspace next). Send half of plumb's mailbox; check_messages is the receive half. Full etiquette — addressing, delivery, the exchange cap, cross-project rules: the plumb-chat skill.

Every message belongs to a thread: omit conversation_id to start one (the reply carries its id), or quote an id you were given to reply into that thread (to may then be omitted). A thread is capped at [collab] max_exchanges messages; once spent, replies are refused.

Delivery is by polling only: check_messages or session_start hands it over, exactly once. A peer idle on its human has not seen the message; silence is not refusal, so do not re-send.

Messages are bound to the exact SESSION when it is connected; a disconnected peer, or "next", is delivered by name instead. Cross-project sends need the recipient project's opt-in. Requires [collab] mailbox = true; the body is secret-scrubbed.

Parameters: body (required); to (peer session name or "next" — omitted means "next" on a new thread, the other participant on a reply); conversation_id (reply into an existing thread).

Inputs: body (required), to, conversation_id.

list_memoriesA

List memories saved for a workspace.

Memories are markdown notes stored in /.plumb/memories/.md. They persist project-specific context — conventions, architectural decisions, gotchas — across MCP conversations. Each memory may have YAML frontmatter (name, description) used as a one-line summary in the listing.

If 'workspace' is omitted, the daemon's currently-resolved workspace is used.

Inputs: workspace.

minimal_diff_reviewA

Reviews a diff for signs of over-building — findings NEVER block a write, they are hints. Deterministic, no LLM: it flags a single-use abstraction, a thin forwarding wrapper, a new dependency with a well-known stdlib equivalent, a possible duplicate helper, and a logic change with no accompanying test change. Evidence is asymmetric: a check stays silent unless it can point at concrete evidence and (where defensible) a smaller alternative, so silence is NOT proof a change is minimal. Findings are labelled by confidence: high = proven from the diff text; low = leans on the topology index, which is approximate (its call graph is intra-file — unlike find_references' exact cross-file lookup) and may be a few edits stale. Reviews the working-tree diff vs base_ref (default HEAD); pass files to scope it to your change in a shared worktree. Degrades cleanly outside a git repository.

Inputs: mode, files, base_ref, max_findings, +1 more.

move_symbolA

Move a top-level declaration (function, method, type, const, or var) from one file to another within the SAME directory/package, atomically. The symbol's full source — declaration plus, by default, its leading doc comment (include_doc_comment) — moves from source_uri to destination_uri in one all-or-nothing operation: if the destination write fails the source is rolled back. Locates the symbol via the LSP document-symbol tree, falling back to tree-sitter when the language server is cold.

Scope (v1, conservative): source and destination must be in the SAME directory — plumb does not rewrite references or imports, so a move that would change a symbol's package or import path is REFUSED. destination_uri must already exist unless create_destination=true. Also refuses an ambiguous symbol (disambiguate with name_path), a path outside the workspace, or (Go) mismatched build constraints between source and destination.

Dry-run by default (dry_run=true): previews the unified diff without writing; set dry_run=false to apply. Undo is per-file — reverting a move takes two undo_edit calls. Scope rationale: the plumb-refactor skill.

Inputs: name_path (required), source_uri (required), destination_uri (required), dry_run, +3 more.

mutation_testA

Mutation-test your own assertions: apply an explicit mutant, prove it still COMPILES, run a scoped test set, classify the result, and restore the file — the check that tells a real assertion from a vacuous one. Takes explicit mutants only (file_path + exact-once old_string/new_string, like edit_file); it does not generate them. Three outcomes: KILLED (mutant compiled and a test failed — the assertion is real), SURVIVED (mutant compiled and every test still passed — the assertion is VACUOUS, the finding that matters), and INVALID (the mutant did not apply, did not compile, could not be started, or timed out — it proves nothing and is NEVER reported as a kill; that false kill is why the compile gate exists). Scope the run with test_target ({target}; topology_affected says which package) and test_run (the {run} test-name filter). Commands are the stored, trust-gated [tasks.] slots run_task uses; you cannot pass a command line. They run from the git work-tree holding the mutated file: a file in another worktree of the commands' repository re-roots them there (same relative working_dir). Mutants spanning work-trees, or in a linked worktree they can't move into, are refused, never run on the wrong tree. Restoration is guaranteed on every exit path (including panic and cancellation): the pre-mutation bytes are snapshotted, rewritten under the per-path lock, and SHA-256-verified before the run is reported clean. It REFUSES a file with uncommitted changes (untracked included), no override — a clean file means git checkout recovers it if the daemon dies mid-run. It also refuses to start unless the workspace BUILDS and its tests PASS unmutated: a kill means "green before, red after", so against an already-red suite every mutant reads as killed for a reason unrelated to it. The refusal says which: suite red, timed out, or could not start — only the first is about your code. One mutation run at a time per daemon; a second call is refused rather than queued.

Inputs: mutants, test_run, test_task, test_target, +2 more.

read_fileA

Read the text contents of a file (absolute path, file:// URI, or workspace-relative path). Use start_line/end_line to stream a slice of a large file. Each line is prefixed with a 1-based line number + tab (cat -n style) for exact range math; this gutter is display-only — strip the leading '\t' before reusing a line as an edit_file/find_replace old_string. Binary files are rejected; output is capped at 200 KiB (use line ranges on large files). The header carries the file's mtime (RFC3339Nano) and SHA-256 — pass them back as expected_mtime/expected_sha on edit_file for optimistic-concurrency checks. Pass pattern to search WITHIN the file instead of windowing: it returns each matching line with its 1-based line number (and optional context_lines), so an over-cap file stays searchable in one tool — literal text by default (smart-case: case-insensitive when all lowercase), Go RE2 regex when use_regex; output is bounded by max_matches (default 200) and labelled when truncated. Combine pattern with start_line/end_line to restrict the search to a line window; pattern with limit is rejected.

Inputs: limit, offset, pattern, end_line, +6 more.

read_memoryA

Read a saved memory by name from a workspace's .plumb/memories/ directory.

Returns the full markdown content (including any frontmatter). Use list_memories first to discover what memories exist.

Inputs: name, workspace.

read_multiple_filesA

Read up to 20 files in a single call. Each file's content is returned under a '### ' heading, followed by that file's own read_file header (mtime, sha256, line and byte counts) so it can be edited without re-reading — reads ARE recorded per file, exactly like read_file, so edit_file works under [edits] strict mode with no re-read. Errors for individual files are reported inline — one unreadable file doesn't block the others. Accepts absolute paths, file:// URIs, or workspace-relative paths. Binary files are detected and skipped. Each file is subject to the same 200 KiB cap as read_file. Pass start_line/end_line or pattern (with use_regex/context_lines/max_matches) to slice or search EVERY path in the call uniformly — same semantics as read_file's own parameters, applied per file; there is no per-path override, so a windowed batch read still records EACH file's full mtime/sha in the read tracker (identical to read_file's own ranged-read behaviour — strict mode is mtime-based, not range-based, so a later edit anywhere in the file is still covered). The 20-path cap is unchanged by slicing.

Inputs: paths, pattern, end_line, use_regex, +3 more.

read_symbolA

Read the source body of a named symbol (function, method, type) in one call. Accepts plain name or dotted ReceiverType.MethodName form. Returns all matches when the name is ambiguous. Each body line carries a display-only 1-based file line-number gutter ('\t', cat -n style) — strip it before reusing a line as an edit_file old_string. Falls back to a tree-sitter parse when the language server is cold or absent.

Inputs: uri, name, path.

relevant_memoriesA

Return memories whose frontmatter 'paths:' globs match the given file.

Memories can be auto-attached to specific parts of a project by adding a 'paths:' field to their frontmatter (e.g. 'paths: internal/auth/**, cmd/server/*.go'). This tool surfaces only the memories relevant to a given file — much smaller than list_memories when many memories exist.

Call this when starting work on a file to discover context the LLM should load before editing.

Inputs: path, workspace.

rename_fileA

Move (rename) a file. Parent directories of to are created if missing. Refuses to overwrite an existing destination unless overwrite=true. The LSP server is notified with FileDeleted (source) and FileCreated (destination) so symbol indexes and diagnostics update immediately. A rename changes no content, so the response shows a diff only when the move destroyed an existing destination (that destination's content), gated by [edits].show_write_diff. To duplicate a file without removing the source, use copy_file instead. For LSP-semantic identifier renames across files, use rename_symbol instead.

Inputs: to, from, dirty_ok, overwrite.

rename_sessionA

Renames the current MCP session. Pass the new name as the name parameter — letters (any case), digits, and hyphens, capped at 25 characters, with no leading/trailing or consecutive hyphens. User-provided case is preserved; auto-generated names are lowercase. The name must be free: a session name is the address the mailbox delivers to, so a name another LIVE session already answers to is refused (compared case-insensitively) and "next" is reserved for the next-arrival address. Renaming to the name you already hold is fine.

Inputs: name.

rename_symbolA

Rename a symbol throughout the workspace using LSP semantic refactoring.

The language server identifies every reference across all files and applies a precise edit set atomically. Safer than text find-and-replace: it understands scope, shadowing, and types, so it won't rename unrelated identifiers that share the name.

Prefer symbol_name to identify the symbol; plumb resolves it through the document-symbol tree and queries the language server at the exact identifier position. Raw line/character remains supported and recovers from narrow "no identifier" misses by snapping once to the enclosing symbol. Runs in dry_run mode by default; set dry_run=false to apply. The response appends a per-file unified diff (a preview in dry-run, the applied change otherwise), capped at 20 files, unless show_write_diff is disabled.

If the language server cannot compute the rename (an error, or an empty edit set — common with sourcekit-lsp before the build graph resolves), pass structural_fallback=true to attempt a best-effort identifier-boundary text rename via find_replace (still dry_run by default). The fallback is NOT scope-aware — it renames every whole-word occurrence in same-extension files — so review the preview before applying.

Inputs: new_name (required), uri, line, dry_run, +4 more.

replace_symbol_bodyA

Replace the entire declaration of a symbol with new content.

The replacement spans the symbol's full Range as reported by the LSP — for a function, this is from 'func' keyword through the closing '}'. Provide the complete new declaration (signature + body) in 'content'.

Set include_doc_comment=true to also cover any contiguous doc comment above the symbol — gopls and most LSP servers report the symbol range starting at the declaration keyword, so without this flag the old doc comment is left orphaned. With it on, your 'content' must include the new doc comment too (or the symbol will have none).

Use rename_symbol if you only want to change the symbol's name. Use this tool when changing logic, signature, or both — addressed by name_path, no line/character coordinates to compute like edit_file's range mode.

The response includes a unified diff of the change — a preview in dry-run, the applied change otherwise — unless show_write_diff is disabled.

Works even when the language server is cold or cannot parse the file: it then locates the symbol via a fresh tree-sitter parse (line-granular range, annotated in the output).

Inputs: name_path (required), uri, content, dry_run, +2 more.

run_commandA

Run a named command from the workspace's [[command]] allow-list (build/test/lint/scripts) without leaving plumb. It runs only the exact fixed argv the user configured (no shell, no agent-supplied command line); the optional target fills a single {target} placeholder with one shell-safe argument. A command from a project's .plumb/config.toml must be trusted first (run plumb trust); a command from your global config always runs. The command runs under an OS sandbox (a write jail) when one is available. Output and runtime are bounded. For an ordinary build/lint/test, prefer run_task and its [tasks.] slots — those ship with defaults.

Inputs: name, target.

run_taskA

Run a stored per-language task command — build, lint, test, e2e, verify, or a project-defined slot — configured in [tasks.]. It executes only the command the user saved (no shell, no agent-supplied command line), for this workspace's primary language or the one you name in language. Runs from the workspace root or [tasks.] working_dir, with its env. A project-supplied (.plumb/config.toml) command must be trusted first (run plumb trust); the shipped defaults and global-config commands always run. Output and runtime are bounded. Pairs with topology_affected (which says WHICH tests to run; this runs them).

Inputs: run, slot, target, verbose, +1 more.

safe_delete_symbolA

Delete a symbol's declaration only if it has no remaining references.

Calls LSP textDocument/references first. If any reference outside the declaration itself is found, the deletion is rejected with the list of referencing locations so the caller can decide what to do. This prevents accidental deletion of code that's still in use.

Set include_doc_comment=true to also delete any contiguous doc comment above the symbol — otherwise the comment is left orphaned, pointing at whatever ends up next in the file.

The response includes a unified diff of the deletion — a preview in dry-run, the applied change otherwise — unless show_write_diff is disabled.

Inputs: name_path (required), uri, dry_run, dirty_ok, +1 more.

search_in_filesA

Exact scan of current file contents — literal text by default, regex when use_regex=true. Use search_in_files when you need every occurrence, exact verification, audits, or safe replacement prep. Unlike shell grep/rg, results are confined to the active project (no .git/, node_modules/, build artefacts, or anything else .gitignore excludes), binary files are skipped (null-byte sniff of the first 8 KB), files larger than max_file_bytes (50 MiB default) are skipped before opening, globs with a literal directory prefix (e.g. "src/**/*.go") prune sibling directories from the walk. Smart-case (case-insensitive when the pattern is all lowercase), supports context lines and glob file filters.

Inputs: glob, path, exclude, pattern, +7 more.

search_memoriesA

Search saved memories for a workspace.

When the FTS5 memory index is available and fresh, returns ranked hits (by relevance, with a bonus for user-authored memories) annotated source=memory-fts. Otherwise falls back to a deterministic grep over the markdown files, returning each match with the memory name and line. Smart-case (case-insensitive if 'pattern' is all lowercase) unless 'case_sensitive' is set; 'use_regex' forces the grep path. 'mode' (auto|fts|grep) overrides the choice; default auto.

Memory-only corpus with a deterministic grep fallback — for ranked discovery across code, docs, AND memories in one call, use workspace_search instead. Useful when you don't know which memory contains a piece of context — much faster than reading every memory.

Inputs: mode, pattern, use_regex, workspace, +1 more.

session_startA

Bootstrap tool — call this first at the start of every session. Returns one-shot orientation: workspace path, language, current git branch, first 200 lines of .plumb/context.md, all saved memory names/descriptions, top-5 most-used tools, 5 most recently-modified files, 3 most recent commits, the live git tool policy (whether commits/destructive/push are enabled), and any active LSP errors/warnings. If no workspace is resolved yet, pass an absolute workspace to pin it — clients like Claude Desktop do not report the folder automatically. A subagent that just needs cheap re-orientation should pass detail: "brief" for a ≤1.5 KB summary instead of the full packet. Idempotent — safe to call multiple times.

Inputs: force, scope, detail, purpose, +3 more.

share_findingsA

Hand off what you have just learned to other agents on this workspace as a durable, searchable memory — RIGHT NOW, instead of waiting for the idle summary to fire when your session ends.

Use it after you have mapped a subsystem, pinned down a gotcha, or worked out how something fits together, so a peer working in parallel can pick it up immediately. The finding is written through plumb's generated-memory pipeline: it is secret-scrubbed before storage, stamped with your session and the date as its provenance, and indexed for search. Peers discover it through the ordinary channels — search_memories, workspace_search, relevant_memories, memory hint injection, and the next session_start.

This is AGENT-GENERATED content: it is labelled lower-confidence than a user-written memory and never displaces one in a capped hint slot. It counts against the same [memory] generated_memory_keep retention as an idle episodic summary. Nothing here is an LLM summary — you supply the text.

Requires [collab] knowledge_handoff = true; otherwise the call is refused. Strictly per-workspace.

Parameters: summary — a one- or two-line headline of the finding (required). description — optional longer detail appended below the summary. paths — optional workspace-relative globs the finding is about (e.g. ["internal/tools/ratelimit*"]); stored as frontmatter so relevant_memories and hint injection route it to those files.

Inputs: summary (required), paths, description.

share_intentA

Broadcast what you are working on to other agents active on this workspace RIGHT NOW, so they can steer around your in-progress work instead of colliding with it (e.g. "refactoring the rate limiter — avoid internal/tools/ratelimit*").

This is ADVISORY and a CLAIM, not a lock: it never blocks anyone's write, and what you say you are doing is not the same as what the daemon observes you did (that is workspace_sessions' recent_writes). Peers see your intent in workspace_sessions, and a peer whose write touches a path matching your path_globs gets a bounded advisory hint labelled as an unverified claim.

You have at most ONE live intent — calling this again replaces it. The intent expires after ttl_minutes (default from [collab] intent_ttl_minutes) and is cleared automatically when your session ends. Delivery is by polling and hint injection only; plumb does not push to another agent.

Requires [collab] intents = true; otherwise the call is refused. Strictly per-workspace; the body is secret-scrubbed before storage.

Parameters: body — what you are doing (required, free text). path_globs — optional workspace-relative globs for the area you are working on (e.g. ["internal/tools/ratelimit*"]); drives peer write hints. ttl_minutes — optional expiry override; defaults to [collab] intent_ttl_minutes.

Inputs: body (required), path_globs, ttl_minutes.

structural_queryA

Run a curated structural check over the topology index — find symbols by SHAPE, not name. Complements topology_search (find by name) and search_in_files (find by text) with audits useful for review and refactor prep. Named queries (no raw tree-sitter queries are exposed): "undocumented-exports" (exported functions/methods/types/constants with no doc comment), "long-functions" (functions over min_lines, default 80), "unused-context" (Go functions taking context.Context whose body never references it). Results are approximate (source=topology) and confidence-labelled where the check is heuristic. Returns a clear message when the index is disabled or empty.

Inputs: limit, query, language, min_lines.

topology_affectedA

After you change code, ask this which tests to run instead of running the whole suite. Given changed files or symbols, it answers with PACKAGES to run — one row each with the test count and why the package is implicated, plus the individual test names in the package the change landed in. Where the workspace's test runner takes a positional path (go, python), each row leads with a ready target to hand straight to run_task(slot:"test"), expressed relative to [tasks.].working_dir so it works from the directory that command runs in. Where the runner scopes by name or by a project-specific flag (rust, typescript, swift, zig), the directory is named and no command is guessed. A package is reached either by containing the change, or by importing a package that does (cross-package import edges). Within a reached package every test is counted, because co-location cannot tell which of them exercise the change: that is the recall bias, and it is deliberate — a missed test is worse than an extra. Results are heuristic; verify before relying. max_results bounds the number of PACKAGES, and the changed package is always listed first so a cap cannot drop it. Returns a clear message when topology is disabled.

Inputs: files, symbols, max_results.

topology_exploreA

Bounded BFS neighbourhood around a named symbol in the topology index. NARROW IT FIRST on a large file or an unfamiliar language: include_source="none" returns names only (the default, "signatures", is several times larger), and depth=1 with max_nodes=15 answers "what touches this?" in a fraction of the default budget (depth 2, 50 nodes, 30000 bytes) — raise them once you know what you are looking for. Returns the centre node, neighbour nodes, and connecting edges up to depth/max_nodes/max_bytes. Reports truncation when limits are hit. Source is 'topology' (approximate — use LSP semantic tools for authoritative reference and definition lookups). Returns an error when topology is disabled or the symbol is not in the index.

Inputs: kind, name, path, depth, +4 more.

topology_impactA

Bidirectional BFS blast-radius analysis around a named symbol. Returns two sections: 'depends on' (outward — what the symbol depends on) and 'depended on by' (inward — what depends on this symbol). Primary use: assess blast radius before a refactor. Source is 'topology' (approximate); the topology call graph is intra-file, so for a function/method the inward section is augmented with a 'cross-file callers' block resolved via the language server (source=lsp) when one is available. mode="reachability" switches to entry-point reachability. The default package granularity follows production import edges from package-main roots plus candidate-seeded topology_routes roots; Go _test.go importers are excluded, and unsupported/polyglot workspaces are refused rather than reported as falsely unreachable. Set granularity="function" for the additive Go-only admitted partial static call graph: it uses exact callable roots, production callers, durable derived cross-file edges, and the full reachable closure. Unresolved receiver/dynamic calls, test callers, unsupported languages, and unindexed roots remain outside that lower-bound answer. Each granularity supports the default summary, path_to (one shortest root-to-target chain), and layers (SCC condensation; import cycles for package or recursion cycles for function). All outputs disclose their scope and known limitations, and responses are byte-capped. Returns a clear message when topology is disabled or the symbol is not in the index.

Inputs: kind, mode, name, path, +8 more.

topology_routesA

Pattern-matches entry-point-shaped symbol NAMES and signatures: Go handler funcs (http.HandleFunc, r.GET/POST, mux.Handle), Cobra cmd.Run/RunE, Python decorators (@app.route, @router.get, FastAPI path decorators), and Swift/Vapor idioms (RouteCollection.boot, configure(_:Application), ParsableCommand.run). It does NOT parse route registrations or call sites, so it cannot recover a path-to-handler binding (e.g. "/api/x" -> handlerFn) — it only finds functions whose name or signature looks like a known entry-point idiom. Results are candidates, not confirmed routes: each carries a confidence annotation reflecting the pattern's typical accuracy, not a resolved binding. Returns a clear message when no candidates match or topology is disabled.

Inputs: limit, framework, path_prefix.

topology_statusA

Report the health and statistics of the topology index for this workspace: indexer state, indexed/skipped file counts (with the recorded reason for each skipped file, most recent first), total nodes and edges, database size, last sync time, indexed languages, and the most recent indexing error if any. Returns a clear message when topology indexing is disabled.

Inputs: workspace.

transaction_applyA

Apply str_replace edits across multiple files atomically. Every operation is validated against the on-disk content first; if any old_string is missing or ambiguous, NO files are written. If writes start succeeding but one fails partway, the already-written files are rolled back to their pre-transaction content. Per-path locks prevent interleaving with other write tools. Use for refactors that must land as one unit. Up to 50 operations per call; the response lists each file with a unified diff unless show_write_diff is off.

Inputs: dirty_ok, operations, await_diagnostics, fail_on_new_errors.

type_hierarchyA

Show the type hierarchy for a type: its supertypes (interfaces it implements, embedded types) and subtypes (types that implement or embed it). PREFER a name (uri + symbol_name) — plumb resolves the exact identifier position for you, avoiding off-by-one errors; a raw file position (uri + line + character) is the fallback and, when it lands off an identifier, is snapped to the enclosing symbol. Useful for understanding inheritance and polymorphism.

Inputs: uri, line, character, direction, +1 more.

undo_editA

Revert plumb's most recent write to a file — the safe alternative to git checkout <file>, which discards EVERY uncommitted change in the file. undo_edit restores only what plumb's last edit_file/write_file changed, and refuses by default if the file was modified since (an external or peer edit), so it never silently clobbers someone else's work (pass force:true to override). If the last write created the file, undo removes it. Single-level per file: it undoes the last write; a fresh write re-arms it. Undo history is per session and cleared on a workspace switch. Very large files (pre-write content over 1 MiB) are not snapshotted, so undo is unavailable for them. Both the restore and the removal of a file the write created return a diff of what the undo changed, gated by [edits].show_write_diff.

Inputs: force, file_path.

workspace_sessionsA

Returns same-workspace session awareness: who else is actively connected to this project and what files they recently edited.

you — this session's name.

active_sessions — sessions on this workspace right now (name, client, how long since their last tool call). A single entry whose is_self field is true means you are the only active session — your view of the workspace is authoritative. Multiple entries mean concurrent agents are working here; treat any file a peer recently touched as potentially changed.

recent_writes — the last N write operations (write_file, edit_file, rename_file, git commit, …) by all sessions on this workspace. The file path (when available), session name, operation, and age are shown. Only operations that could modify the workspace are listed: read-only git subcommands (status, log, diff, …) and dry-run previews never appear. A call that failed or was refused is kept but marked '[failed — no change applied]' — evidence the peer is working in that file even though nothing landed on disk. A successful git commit is attributed in full: its line carries the session name, the commit's short SHA and subject, and the repository, so a peer's commit is traceable to the session that authored it. When [collab] peer_awareness is on and the topology index has the file, each entry is annotated with its enclosing package/symbol (best-effort, source=topology).

Use this before editing a file that another session may have recently modified: if it appears in recent_writes, re-read it first.

Parameters: recent_limit — max recent-write entries to return (default 10, max 50).

Workspace boundary: workspace_sessions is scoped to the caller's pinned workspace; it never reveals sessions from a different project.

Inputs: recent_limit.

workspace_symbolsA

Search for symbols (functions, types, variables, constants) by name or substring across the entire workspace — instant, uses the LSP index. Pass uri to restrict the search to that one document instead. Returns names, kinds, and source locations.

Inputs: uri, query.

write_fileA

Create or overwrite a file with the given content. The write is atomic and crash-durable (temp file fsynced, renamed, parent directory fsynced before the call returns — never partially written); parent directories are created automatically and the LSP server is notified so diagnostics and symbols update immediately. Pass expected_mtime or expected_sha (from a read_file header) to reject the write if the file changed since you read it, so a full-content overwrite never silently clobbers a concurrent change. If the call fails with a transport/connection error, the atomic temp+rename guarantees the file is either fully written or untouched — never partially written; re-read to confirm which side of the rename it landed on. Use edit_file for targeted edits to an existing file.

Inputs: content, dirty_ok, file_path, create_dirs, +5 more.

write_historyA

Reviews file write history recorded across plumb write tools. Supports listing recent changes with session, agent, tool, and file filters, and showing full diffs and tool-call metadata by sequence number or call ID.

Inputs: seq, file, mode, tool, +7 more.

write_memoryA

Write or overwrite a memory in a workspace's .plumb/memories/ directory.

The memory is a markdown file at /.plumb/memories/.md. If 'description' or 'paths' is provided, frontmatter is prepended automatically — list_memories will surface the description, and relevant_memories / hint injection use paths globs to attach the memory to files.

Memory names must match [A-Za-z0-9_-]+. Choose specific names that describe the memory's topic (e.g. 'auth-architecture', 'test-conventions', 'gotchas-cache-invalidation').

Inputs: name, paths, content, workspace, +1 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.8/5.0

    Scored across 59 tools

    Disambiguation3/5

    Many tools overlap in search/discovery (find_files, search_in_files, find_replace, workspace_search, topology_search, workspace_symbols), file reading (read_file, read_multiple_files, read_symbol, file_outline), and editing (edit_file, write_file, transaction_apply, symbol insertion tools). Descriptions help distinguish scope and intent, but the volume and similarity create real misselection risk. An agent can usually find the right tool, but not without careful reading.

    Naming Consistency4/5

    All names use snake_case and many carry clear domain prefixes (topology_, file_, workspace_, session_), making the set predictable. However, the pattern is not strictly verb_noun throughout: several tools are noun-first (file_outline, call_hierarchy, topology_status) or use other orderings. The deviations are minor and readable.

    Tool Count1/5

    59 tools is an extreme mismatch for a single MCP server, far beyond the typical 3–15 well-scoped range and crossing the 50+ threshold. Even accounting for plumb's broad code-workspace purpose, the surface is heavily overloaded. Many tools could be consolidated or moved to separate servers.

    Completeness4/5

    The surface covers an unusually broad domain: file operations, git, LSP, topology analysis, memory, collaboration, tasks, diagnostics, and config. Minor gaps exist (e.g., no explicit directory-creation tool, no dedicated memory-update distinct from overwrite), but core lifecycle operations are present and workable.

    Maintenance

    ActivityActive
    ResponsivenessUsually handled

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      B
      maintenance
      Local-first code intelligence and safety layer for AI coding agents. MCP server exposes dependency graph, impact analysis, and AST-compressed repo context, backed by typed local memory, patch-scope safety gates, and git-independent transaction rollback.
      1
      MIT