MCP Setup Guide

Patchloom includes an MCP (Model Context Protocol) server for structured tool calls. MCP-capable AI agents can call Patchloom tools directly with JSON parameters, with no shell command construction, no quoting issues, and no --apply flag needed.

Official MCP Registry

Patchloom publishes to the official MCP Registry as io.github.patchloom/patchloom. The listing points clients at the crates.io and npm packages and starts the stdio server with mcp-server.

Repo metadata lives in server.json at the repository root. Ownership markers (required by the registry) are:

  • crates.io: a visible mcp-name: io.github.patchloom/patchloom line in the crate README (HTML comments are stripped on crates.io, so the token must be plain markdown text).
  • npm: mcpName in the published package.json (injected during the release publish-npm job; cargo-dist does not emit it).

On each release, after crates.io and npm publish succeed, the Release workflow calls Publish MCP Registry, which stamps the version into server.json, confirms those markers, authenticates with GitHub OIDC, and runs mcp-publisher publish. You can re-run it manually via workflow_dispatch with an explicit version (for example after a failed publish).

Smithery (local MCPB)

Smithery can list local stdio servers via an MCPB bundle (URL publish is for hosted Streamable HTTP endpoints; Patchloom's primary agent path is local stdio).

The bundle lives under mcpb/ and runs:

  1. patchloom mcp-server if the binary is on PATH, else
  2. npx -y patchloom@<version> mcp-server (Node.js 18+).
make pack-mcpb   # writes target/mcpb/patchloom-<version>.mcpb (override with VERSION=x.y.z)
# One-time auth + CI secret:
#   smithery auth login
#   export SMITHERY_API_KEY=$(smithery auth whoami --full | sed -n 's/^SMITHERY_API_KEY=//p')
#   gh secret set SMITHERY_API_KEY --repo patchloom/patchloom
bash scripts/publish-smithery.sh   # REST upload (reliable for stdio MCPB)

CI: .github/workflows/publish-smithery.yml packs after each GitHub Release and publishes when the SMITHERY_API_KEY secret is set (soft-skip otherwise).

Glama directory

Glama indexes open-source MCP servers for discovery, quality scores, and optional hosted connectors.

Listing (live): glama.ai/mcp/servers/patchloom/patchloom (search API id pk95432szu). Repo ownership metadata is in root glama.json (maintainers GitHub usernames).

Canonical short description (match server.json / MCP Registry, max 100 chars):

Agent-safe structured edits: JSON/YAML/TOML/md/AST, dry-run, batch/tx. Not a filesystem MCP.

If the Glama profile still shows the older “AI agent file editing via MCP…” blurb, open the listing as a maintainer and set the description to the text above (GitHub sync does not always replace an existing Glama description).

First-time submit (already done for Patchloom)

  1. Sign in at glama.ai (GitHub OAuth is supported).
  2. Open MCP Servers and click Add MCP Server.
  3. Submit GitHub URL https://github.com/patchloom/patchloom with the description above.
  4. Confirm search: GET https://glama.ai/api/mcp/v1/servers?query=patchloom returns a non-empty servers list.

There is no public unauthenticated submit API.

Verify MCP support

The MCP server is included by default in all pre-built binaries and in recommended installs (Homebrew, Scoop, npm npx / global, crates.io). winget usually tracks the latest GitHub Release after Microsoft publishes the version PR; Chocolatey often lags while versions wait for moderation. Prefer Scoop or a GitHub Release binary when you need a known-current MCP server. Verify it works:

patchloom mcp-server --help

Configure your agent

Set command to an absolute path to the binary, or to patchloom if the CLI is already on PATH (Homebrew, Scoop, cargo install, installer).

Examples:

PlatformTypical command value
Unix (Homebrew / cargo)patchloom or /opt/homebrew/bin/patchloom
Windows (Scoop)patchloom or C:\\Users\\you\\scoop\\shims\\patchloom.exe
Windows (portable zip)C:\\path\\to\\patchloom.exe

JSON configs need escaped backslashes on Windows paths (C:\\\\Users\\\\...\\\\patchloom.exe). Prefer the bare name patchloom when the shim is on PATH.

Grok (config.toml)

Add to ~/.grok/config.toml:

[mcp_servers.patchloom]
command = "patchloom"
args = ["mcp-server"]
env = { PATCHLOOM_MCP_SURFACE = "core" }

Claude Desktop (JSON)

Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):

{
  "mcpServers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

VS Code (.vscode/mcp.json)

Create .vscode/mcp.json in your workspace root:

{
  "servers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

Cursor (.cursor/mcp.json)

Create .cursor/mcp.json in your workspace root:

{
  "servers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

Or use the Patchloom VS Code extension to configure MCP automatically via the Patchloom: Configure MCP command.

Generic stdio MCP

Any MCP client that supports stdio transport can connect by spawning patchloom mcp-server as a subprocess. The server communicates via JSON-RPC over stdin/stdout.

Available tools

Patchloom exposes two registration paths for MCP tools (see src/cmd/mcp/surface.rs for the inventory and policy):

PathRuleExamples
Registry (default)1:1 with a plan write Operation; schema from the variantdoc_set, create_file, fix_whitespace, most md_* writers
Custom (justified exception)Multi-file scan, multi-op/batch/plan, readonly query, AST analyze, patch/metasearch_files, replace_text, batch_*, execute_plan, doc_get / doc_query, all ast_*

Prefer the registry for new simple write tools. Do not force custom tools into the registry when that would lose multi-file, batch, or read UX.

Custom tools are inventoried in CUSTOM_MCP_TOOLS_CORE (always registered) and CUSTOM_MCP_TOOLS_AST (only when the ast feature is enabled). Default builds expose 58 tools (registry + custom). Builds without ast omit the AST tools so list_tools stays honest about what is callable.

ToolDescription
doc_setSet a value by selector path in a JSON, YAML, or TOML file
doc_deleteDelete a value by selector path from a structured file
doc_mergeDeep-merge an object into a document (optional selector, e.g. 0 for multi-doc YAML)
doc_appendAppend a value to an array
doc_prependPrepend a value to an array
doc_ensureSet a value only if the selector path does not exist
doc_delete_whereDelete array elements matching a predicate field (field=value)
doc_updateUpdate values at selector paths that use predicates or wildcards (items[name=a].v, items[*].enabled). Not a predicate field; doc_delete_where is the predicate tool.
doc_moveMove a value from one selector path to another
doc_getRead a value by selector path (read-only)
doc_queryQuery a structured file: has, keys, len, select, or flatten (read-only). Selector optional for keys/len (defaults to .).
doc_diffCompare two structured files (read-only)
search_filesSearch text files for a pattern, including literal, case-insensitive, count, file-only (files_with_matches / files_without_match), multiline, invert-match, and assert-count modes. Binary and invalid UTF-8 files are skipped (read-only)
list_filesBounded directory inventory with the same ignore/exclude/glob rules as search (read-only). Prefer this over a second generic filesystem MCP for list/tree. Optional max_depth prunes the walk per root (does not enter deeper dirs). max_results (default 500) still counts all in-depth matches then truncates and sets truncated / total_matched
git_statusShow uncommitted file changes vs git HEAD (read-only)
server_infoReturn server identity and workspace root: cwd, surface (full|core), tool_count, package version, and MCP protocol_version (read-only). Use cwd before relative path ops
read_fileRead file contents with optional line range
replace_textReplace text in a text file (literal or regex). Binary and invalid UTF-8 files are skipped
apply_fragmentFreeform fragment at a required after/before/old anchor; strips Morph-style // ... existing code ... markers (no cloud merge). Prefer when the agent has a snippet plus a known placement
md_upsert_bulletAdd a bullet under a markdown heading
md_table_appendAppend a row to a markdown table
md_replace_sectionReplace a section by heading (through next same-or-higher heading)
md_insert_after_headingInsert content immediately after a heading line (before body)
md_insert_after_sectionInsert content after the full section body (sibling section)
md_insert_before_headingInsert content before a markdown heading
md_move_sectionMove a heading section through next same-or-higher heading (same-file or cross-file)
md_dedupe_headingsRemove later whole sections with a duplicate heading (second body discarded)
md_lintLint an AGENTS.md file; returns {ok, path, issue_count, issues} (CLI lint-agents --json parity). Branch on ok; isError stays false when issues are present
fix_whitespaceFix whitespace and line endings in a text file. Binary and invalid UTF-8 files are skipped
create_fileCreate a new file with content
append_fileAppend content to an existing file
prepend_filePrepend content to an existing file
delete_fileDelete a file
move_fileMove or rename a file (binary-safe)
apply_patchApply a unified diff, Codex Begin Patch, or SEARCH/REPLACE / DiffFenced document (unique unless replace_all). Empty-hunk +++ /dev/null unlinks. A hunked delete applies minus first; leftover rewrites. Stale is ambiguous and does not unlink
batch_replaceReplace the same text across multiple files atomically
batch_tidyFix whitespace in multiple files atomically
execute_planExecute a full multi-op transaction plan atomically (recommended for complex/multi-file edits). Supports inline plan or plan_path. MCP strips format/validate; CLI tx still runs those lifecycle steps.
ast_listList symbol definitions (functions, classes, structs, enums, methods) in a file or directory (20 languages). Filter by kind.
ast_readRead a specific symbol's source code by name from a file.
ast_renameRename identifiers across files using AST-aware renaming (skips strings and comments).
ast_validateValidate syntax of source files. Returns parse errors with line numbers.
ast_searchStructural search using AST queries. Supports S-expression syntax and code patterns with meta-variables.
ast_refsFind all references to a symbol across files. Distinguishes definitions from references.
ast_depsExtract import/dependency statements from source files (Rust, Python, JS/TS, Go, Java, C/C++, Ruby, PHP).
ast_mapGenerate a ranked repository map using PageRank over the symbol reference graph. Token-budget-aware output.
ast_diffStructural diff between two versions of a file. Shows added, removed, and modified symbols.
ast_impactTransitive impact analysis: trace the reference graph to find all dependents of a symbol.
ast_replaceReplace text only within a specific symbol's body using AST scoping.
ast_insertInsert code before/after a symbol or inside a container (module, class, impl block).
ast_wrapWrap a symbol in a container (module, class, namespace, impl block, or custom wrapper).
ast_importsList, add, remove, or deduplicate import statements in source files.
ast_reorderReorder symbols by strategy: alphabetical, reverse, kind-first, or custom order.
ast_groupMove symbols into a new or existing module block within the same file.
ast_moveMove symbols between files with configurable insertion position. Optional update_imports plus old_module_path / new_module_path rewrites consumer imports.
ast_extract_to_fileExtract a symbol to a new file, optionally unwrapping module blocks. Optional update_imports plus old_module_path / new_module_path rewrites consumer imports.
ast_splitSplit a file by distributing symbols across multiple target files.

How MCP mode differs from CLI mode

AspectCLI modeMCP mode
Apply behaviorRequires --apply flagAuto-applies (writes are the default)
Input formatShell argumentsStructured JSON parameters
Path securityNo restrictionPaths must stay within working directory
Error formatstderr textMCP error response with structured content
DiscoveryAgent reads AGENTS.mdAgent discovers tools via MCP protocol
Lifecycle stepsCLI tx runs format/validateMCP execute_plan strips format/validate

Multi-step plans and concurrency guidance (important for agents)

For any work involving more than one edit (especially on the same file or related files), prefer the execute_plan tool over issuing many individual tools:

  • One execute_plan call = atomic execution of a mixed plan (doc.set + md.replace_section + create + replace + ...).
  • MCP execute_plan strips format and validate lifecycle steps from the submitted plan. Those steps do not run (they are CLI tx / project-config only). Use .patchloom.toml if the workspace needs lifecycle shells.
  • Top-level MCP strict overrides the plan only when the caller sends it. Omitting it leaves plan.strict unchanged (so {"plan":{"strict":false}} is honored).
  • write_policy on the plan is still accepted.

Example inline plan (JSON):

{
  "version": 1,
  "strict": true,
  "operations": [
    { "op": "doc.set", "path": "package.json", "selector": "version", "value": "2.0.0" },
    { "op": "md.replace_section", "path": "AGENTS.md", "heading": "## Commands", "content": "Run make check.\n" },
    { "op": "file.create", "path": "REPORT.md", "content": "# Summary\n" }
  ]
}

Critical rules for agents (to avoid lost updates and races):

  • Do not issue concurrent write tools against the same path(s) unless using execute_plan.
  • Serialize writes per path. Parallelize only across completely disjoint paths.
  • Per-call "ok" does not mean the combined result is coherent if you interleave writers yourself.
  • Use one execute_plan for any logical multi-edit task.

These semantics are also documented in the tool instructions returned by the MCP server and in patchloom agent-rules --mode mcp.

Tool surface (full vs core)

By default the server registers the full tool inventory (registry + custom handlers, including AST when built with ast). That default stays for compatibility.

Recommended for coding agents (Grok, Claude, Cursor, dual native tools + MCP): start with the core pack so tool schemas stay small. Upgrade to full when you need AST or advanced standalone tools.

export PATCHLOOM_MCP_SURFACE=core
patchloom mcp-server
ValueEffect
unset or fullFull inventory (default; backward compatible)
coreOnly: read_file, search_files, list_files, replace_text, batch_replace, doc_get, doc_set, doc_query, md_replace_section, execute_plan, server_info
anything elseServer fails to start with a clear error

server_info includes "surface": "core"|"full", "tool_count", package "version", MCP "protocol_version" (same as the initialize handshake), and when surface is full a "recommendation" string pointing coding agents at core. Invalid PATCHLOOM_MCP_SURFACE values are rejected (do not silently fall back).

Handshake instructions match the active surface: core mode lists only the core tools (it does not advertise full-inventory names such as create_file or ast_*). Instructions also include a short canonical name map (CLI / plan / MCP) and an explore rule (prefer list_files / search_files / read_file over shell cat/find/ls and over a second generic filesystem MCP when Patchloom MCP is connected).

Note: execute_plan remains on core, so multi-op plans can still run create/delete/AST-style plan ops (and plan apply.fragment) when the host sends a full plan. Standalone tools such as apply_fragment, create_file, and ast_* are full-inventory only. The env flag reduces the tool schema surface for small agents; it is not a capability sandbox for the plan catalog.

Short system-prompt rules (not the full ~50KB dump):

patchloom agent-rules --surface core

Coding-agent install recipes (core by default)

Coding agents should use the core pack so tool schemas stay small. Full inventory remains the product default when the env is unset (compatibility). Set PATCHLOOM_MCP_SURFACE=core in the client config for Grok, Claude, Cursor, and Codex-style hosts.

Grok / config.toml

[mcp_servers.patchloom]
command = "patchloom"
args = ["mcp-server"]
env = { PATCHLOOM_MCP_SURFACE = "core" }

Cursor (~/.cursor/mcp.json or project .cursor/mcp.json)

{
  "mcpServers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

Claude Desktop / Claude Code style MCP config

{
  "mcpServers": {
    "patchloom": {
      "command": "patchloom",
      "args": ["mcp-server"],
      "env": {
        "PATCHLOOM_MCP_SURFACE": "core"
      }
    }
  }
}

Codex / generic stdio MCP

{
  "name": "patchloom",
  "command": "patchloom",
  "args": ["mcp-server"],
  "env": {
    "PATCHLOOM_MCP_SURFACE": "core"
  }
}

Do not also install a generic filesystem MCP only for list/read when Patchloom MCP is connected: use list_files, search_files, and read_file instead. Use full surface (unset env or full) when you need standalone AST tools or advanced md/doc tools as first-class MCP tools (execute_plan on core can still run plan ops).

Example fragment for copy-paste: mcp-core.example.json.

Design notes: mcp-surface-tiers.md.

Debugging and logging

The MCP server can log every tool call to a JSONL file for debugging and performance analysis. Each line records the tool name, duration, and success/failure status.

Enable logging with the --log flag:

patchloom mcp-server --log /tmp/patchloom-mcp.log

Or set the PATCHLOOM_MCP_LOG environment variable (the --log flag takes precedence):

export PATCHLOOM_MCP_LOG=/tmp/patchloom-mcp.log
patchloom mcp-server

Each line is a JSON object:

{"ts":1749123456789,"tool":"replace_text","duration_ms":3,"ok":true}
{"ts":1749123456800,"tool":"doc_set","duration_ms":5,"ok":false,"error":"file not found"}
FieldTypeDescription
tsnumberUnix timestamp in milliseconds
toolstringTool name that was called
duration_msnumberExecution time in milliseconds
okbooleanWhether the call succeeded
errorstringError message (only present on failure)

Configuring logging in your MCP client

Grok (config.toml) -- pass the env var to the MCP server subprocess:

[mcp_servers.patchloom]
command = "/path/to/patchloom"
args = ["mcp-server"]
env = { PATCHLOOM_MCP_LOG = "/tmp/patchloom-mcp.log" }

Or use --log in the args:

[mcp_servers.patchloom]
command = "/path/to/patchloom"
args = ["mcp-server", "--log", "/tmp/patchloom-mcp.log"]

Claude Desktop / VS Code / Cursor (JSON) -- use --log in the args:

{
  "mcpServers": {
    "patchloom": {
      "command": "/path/to/patchloom",
      "args": ["mcp-server", "--log", "/tmp/patchloom-mcp.log"]
    }
  }
}

Or pass the env var (Claude Desktop supports env in server config):

{
  "mcpServers": {
    "patchloom": {
      "command": "/path/to/patchloom",
      "args": ["mcp-server"],
      "env": { "PATCHLOOM_MCP_LOG": "/tmp/patchloom-mcp.log" }
    }
  }
}

Security model

The MCP server enforces path containment: every path must resolve inside the working directory where patchloom mcp-server was started. Relative paths are preferred. Absolute paths that resolve inside that workspace are allowed (agents often concatenate server_info.cwd with a relative path). ../ traversal, absolute paths outside the workspace, and symlinks that escape the workspace are rejected. This prevents an agent from editing files outside the project.

Each individual tool validates every path before execution.

Streamable HTTP transport

By default, the MCP server uses stdio transport (ideal for local IDE/agent integration). With --http, the server switches to Streamable HTTP transport, allowing remote MCP clients to connect over the network.

Streamable HTTP has no authentication and no token. The default bind is loopback (127.0.0.1). Binding a non-loopback address (0.0.0.0, a LAN IP, or a public IP) is refused unless you pass --allow-unauthenticated.

Basic HTTP

# Default: listen on 127.0.0.1:8080 (loopback)
patchloom mcp-server --http

# Custom port on loopback
patchloom mcp-server --http --port 3000

# Explicit loopback bind
patchloom mcp-server --http --host 127.0.0.1

The MCP endpoint is served at /mcp (e.g., http://127.0.0.1:8080/mcp).

Do not copy-paste --host 0.0.0.0 as a default. All-interfaces HTTP is unauthenticated (there is no token) and requires an explicit opt-in:

# All interfaces: unauthenticated, opt-in required
patchloom mcp-server --http --host 0.0.0.0 --allow-unauthenticated

HTTPS with native TLS

TLS encrypts the connection but still does not authenticate clients. Non-loopback binds still need --allow-unauthenticated. There is no bearer token.

patchloom mcp-server --http --host 0.0.0.0 --port 443 \
  --tls-cert cert.pem --tls-key key.pem --allow-unauthenticated

Both --tls-cert and --tls-key must be provided together. The server uses rustls (no OpenSSL dependency).

HTTP transport flags

FlagDefaultDescription
--httpoffUse Streamable HTTP transport instead of stdio
--host127.0.0.1Bind address (requires --http). Non-loopback binds require --allow-unauthenticated
--port8080Bind port (requires --http). Use 0 for an OS-assigned ephemeral port (printed in the startup banner)
--allow-unauthenticatedoffPermit HTTP on a non-loopback bind. Streamable HTTP has no token or other client auth
--tls-certnoneTLS certificate PEM file; enables HTTPS (requires --http and --tls-key)
--tls-keynoneTLS private key PEM file (requires --http and --tls-cert)

Connecting a remote MCP client

Use any MCP client that supports Streamable HTTP transport. Example with the rmcp Rust client:

#![allow(unused)]
fn main() {
// Requires `rmcp` 3.x with client + HTTP transport features (see crates.io).
use rmcp::ServiceExt;
use rmcp::transport::StreamableHttpClientTransport;

let transport = StreamableHttpClientTransport::from_uri("http://localhost:8080/mcp");
let client = ().serve(transport).await?;
// Negotiate initialize; tool calls go through client.peer().call_tool(...).
}

Logging with HTTP transport

The --log flag works identically with HTTP transport:

patchloom mcp-server --http --log /tmp/mcp.log

Graceful shutdown

The HTTP server shuts down gracefully on Ctrl+C (SIGINT): active SSE streams are terminated, in-flight requests complete, and the server exits cleanly.

Example tool call

An MCP-capable agent sends:

{
  "method": "tools/call",
  "params": {
    "name": "doc_set",
    "arguments": {
      "path": "config.yaml",
      "selector": "database.port",
      "value": 5432
    }
  }
}

Patchloom parses the YAML, changes database.port to 5432, preserves all comments and formatting, and writes the file. The agent receives a success response with no further action needed.