# Architecture
This document describes the current rust-fs-mcp architecture as implemented in the source tree.
## Goals
rust-fs-mcp is designed around four constraints:
- Keep public fs-mcp tool names and request shapes stable.
- Keep all tool results inside a normalized response envelope.
- Resolve external CLI tools (rg, fd, git) from PATH instead of bundling them with the binary, so the release artifact stays small and reuses the user's installed toolchain.
- Keep state explicit and process-local for configuration, search sessions, and git cwd.
## High-Level Flow
```text
MCP client
|
| line JSON-RPC over stdin/stdout
v
src/main.rs
|
v
protocol::server::run
|
+-- initialize ------------------> server metadata and capabilities
+-- tools/list ------------------> protocol::catalog::tool_catalog
+-- tools/call ------------------> tools::dispatch_tool_call
+-- resources/list --------------> empty list
+-- resources/templates/list ----> empty list
```
A tools/call request enters dispatch_tool_call with the tool name and optional JSON arguments. The dispatcher resolves
args_path references first, calls the matching tool handler, then normalizes the RawResult into the public response
envelope.
## Request Lifecycle
1. src/main.rs calls rust_fs_mcp::server::run.
2. protocol::server reads stdin line by line.
3. Each non-empty line is parsed as JSON-RPC.
4. Notification methods return no response.
5. initialize returns protocol version, capabilities, server info, and server instructions.
6. tools/list returns catalog entries and input schemas.
7. tools/call extracts params.name and params.arguments.
8. tools::dispatch_tool_call resolves args_path, args_offset, and args_length.
9. The concrete tool handler returns RawResult.
10. core::response::normalize_tool_result builds the MCP content, structuredContent, _meta, and isError fields.
## Module Responsibilities
| main | Binary entry point and fatal error handling. |
| lib | Re-exports core, protocol, and tools modules. |
| protocol::server | JSON-RPC line protocol, method routing, initialize response, empty resource handlers. |
| protocol::catalog | Public tool registry, tool descriptions, annotations, and JSON schemas. |
| core::args_ref | Large argument indirection through args_path and optional character slicing. |
| core::batch | Shared batch execution and structured batch result format. |
| core::external | Spawns external CLI tools (rg, fd, git) resolved from PATH and captures stdout/stderr with timeouts. |
| core::config | RuntimeConfig (allowedDirectories), home expansion, lexical path normalization, and path-allowed checking with an internal cache. |
| core::response | RawResult type, content sanitization, display text, response timing, public envelope normalization. |
| tools::mod | Tool name dispatcher and cross-tool argument resolution boundary. |
| tools::fs_tools | File, directory, metadata, exact block edit (file-edit), 1-based line edit (file-edit-lines), image, and HTTP read behavior. |
| tools::search_tools | Search execution, in-memory search sessions, pagination, regex and literal matching. |
| tools::inspect_tools | Compact read-only filesystem inspection collection for coding tasks. |
| tools::git_tools | Repository discovery plus git status/add/commit/diff/show by invoking the git CLI resolved from PATH. |
| tests::tool_matrix | End-to-end catalog and dispatch coverage for the public tool surface. |
## State Model
The server stores runtime state in process memory.
| Runtime config | core::config | OnceLock<RwLock<ConfigState>> with a separate Mutex<HashMap<PathBuf, bool>> path-allowed cache | Process lifetime |
| Search sessions | tools::search_tools | OnceLock<Mutex<HashMap<String, SearchSession>>> | Until search-stop or process exit |
| Git cwd | tools::git_tools | OnceLock<Mutex<Option<PathBuf>>> | Until changed or process exit |
No state is persisted by the server except filesystem and git writes requested by tool calls.
## Configuration Boundary
core::config is the shared path and process-safety boundary.
Path handling:
- ~ is expanded from USERPROFILE or HOME.
- Relative paths are joined to the process current directory.
- Lexical components are normalized.
- allowedDirectories is checked after path resolution.
- If allowedDirectories is empty, local path access is unrestricted.
- target_path also checks the parent directory boundary.
- RUST_FS_MCP_TOOL_PROFILE=fast-coding limits tools/list to fs-inspect while dispatch compatibility remains available.
## Tool Dispatch Boundary
tools::dispatch_tool_call is the only public tool execution entry from the protocol layer.
It performs three steps:
1. Capture the start time.
2. Resolve top-level args_path references with inline override support.
3. Route the resolved Value to the named handler and normalize the result.
Unknown tool names return an error RawResult. They still use the normal response envelope.
## Response Contract
Tool handlers return RawResult. RawResult is intentionally small:
| content | MCP content blocks before normalization. |
| structured | Optional tool-specific structured data. |
| is_error | Tool-level error flag. |
| meta | Reserved per-tool metadata map. |
normalize_tool_result then produces the public contract:
- content: display-oriented text block.
- structuredContent.data.content: sanitized content blocks; file bodies are carried here.
- structuredContent.data.structuredContent: sanitized structured data or null; for reads this is metadata only and no longer duplicates the file body.
- structuredContent.data.text: duplicate of data.content, emitted only when the compact envelope is disabled (RUST_FS_MCP_COMPACT=0).
- structuredContent.durationMs: tool duration.
- structuredContent.error: null or message object.
- structuredContent.schemaVersion: 1.
- structuredContent.status: success or error.
- structuredContent.toolName: original tool name.
- _meta.fsMcpResult: compact status metadata.
- isError: present only when the result is an error.
Text and JSON strings are sanitized before leaving the server.
## Batch Contract
Batch tools use run_batch and create_batch_response. The batch layer preserves:
- 1-based input index.
- Original input object.
- Per-item ok flag.
- Per-item RawResult content, structuredContent, and isError. The default compact envelope drops the per-item content copy.
- failedCount, succeededCount, totalCount, and toolName.
A batch response is marked as a tool error only when every item fails.
## Filesystem Architecture
fs_tools is split into read, write/directory, copy/move/remove/info/edit, shared helpers, and HTTP helpers.
Important contracts:
- Local paths pass through ensure_path_allowed, existing_path, or target_path.
- Writes create parent directories when needed.
- file-edit performs exact string replacement and can enforce expected_replacements.
- file-edit-lines replaces inclusive 1-based line ranges, preserving the file's original line endings.
- Binary files are detected through NUL bytes.
- Image files are returned as image content blocks with base64 data.
- Directory traversal honors depth, maxEntries, includeFiles, excludePatterns, and allowMissing.
- URL reads support http:// with redirect handling and reject https:// until TLS support exists.
- file-lines reads line ranges through native Rust streaming.
- dir-list uses native Rust traversal and falls back to fd from PATH when excludePatterns are supplied.
## Search Architecture
search_tools separates session mode from backend mode.
search-start and search-regex run the same search engine. search-start stores the result lines under a generated session id,
while search-regex returns the batch result directly. search-get pages stored lines by offset and length. search-stop removes
session ids.
Backend selection:
- content search shells out to ripgrep (rg) resolved from PATH.
- files search shells out to fd resolved from PATH.
- The resolver invokes commands by name through std::process::Command, so each tool must be installed and available on PATH.
- Result structured data records the backend label (for example `path-rg` or `path-fd`).
Search behavior:
- searchType files returns file paths only.
- searchType content reads text-like files and applies RegexBuilder.
- literalSearch escapes the pattern before compiling.
- ignoreCase sets case-insensitive matching.
- contextLines emits grep-like context separators.
- includeHidden controls dot-path traversal.
- filePattern uses wildcard matching against file name or displayed path.
- maxResults stops traversal early.
## Git Architecture
git_tools wraps the git CLI resolved from PATH through core::external. Every handler builds an argument vector and runs it
with run_git inside the resolved worktree.
Repository discovery:
- path argument wins when provided; a file path uses its parent directory.
- Otherwise the pinned git-cwd is used.
- The worktree root is confirmed with rev-parse --show-toplevel.
Command behavior:
- git-cwd resolves the worktree, can run git init first, and pins the git-cwd for later calls.
- git-add runs git add -- for the given paths.
- git-commit always injects -c user.name=rust-fs-mcp and -c user.email=rust-fs-mcp@example.invalid so commits succeed without local git config, adds --author when an author object is given, and forwards amend and allow-empty.
- git-status runs git status --porcelain --branch.
- git-diff runs git diff with optional staged, name-only, stat, source/target, and path arguments.
- git-show runs git show on object or object:filePath.
Validation:
- Commit messages must pass an English Conventional Commit header check before git runs.
- run_git returns stdout on exit 0 and an stderr-based error otherwise.
Known git boundaries:
- A git binary must be installed and resolvable on PATH; there is no in-process git object store.
- Behavior and edge cases follow the installed git version, including submodule and rename-detection defaults.
## Inspect Architecture
inspect_tools exposes one composite tool, fs-inspect, for read-only coding lookups. A single call carries a root and a
list of requests, and each request is dispatched by its op.
Request ops:
- count-files counts glob matches under a directory with optional recursion and sample paths.
- search runs a regex or literal scan with optional capture-group field extraction and a file pattern filter.
- json-pick parses a JSON file and returns values at the requested JSON pointers.
- snippet returns context-bounded line ranges around substring matches.
- git-status delegates to git_tools::handle_git_status so filesystem and git state resolve in one round-trip.
Shared behavior:
- A per-call maxSnippetChars budget (default 6000) bounds total evidence text and sets the truncated flag when exceeded.
- Each answer carries id, op, status, value, confidence, evidence, and warnings; the call also returns scannedFiles, bytesRead, snippetChars, and truncated metrics.
- Compiled wildcard patterns are cached in a process-wide map, mirroring the search cache.
- RUST_FS_MCP_TOOL_PROFILE=fast-coding narrows tools/list to fs-inspect only.
## Tool Catalog Architecture
protocol::catalog is source of truth for exposed tool names, descriptions, schemas, and annotations. The dispatcher in
tools::mod must stay aligned with the catalog.
The tool matrix test enforces this alignment by:
1. Calling tools/list and collecting catalog names.
2. Calling every public tool through dispatch_tool_call.
3. Sorting and deduplicating the called names.
4. Comparing called names against the catalog.
Any new public tool must update catalog, dispatcher, and tests together.
## Validation Strategy
Use the narrowest command that covers the changed surface.
```powershell
cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test
cargo build
```
For documentation-only changes, also inspect Markdown heading hierarchy, fenced code block languages, local links, and
trailing whitespace.