zenith-mcp
v2026.1.14
Published
Zenith-MCP filesystem server
Readme
Zenith-MCP
The MCP filesystem server built for serious AI-assisted development. Not just read/write — Zenith gives your AI agent genuine code intelligence: AST-aware editing, impact-graph refactoring, semantic search, and a version-controlled symbol database, all inside a hardened security sandbox.
Why Zenith over other MCP filesystem servers?
Most MCP filesystem servers are thin wrappers around fs.readFile and fs.writeFile. Zenith is different at every layer:
| Capability | Typical MCP server | Zenith-MCP |
|---|---|---|
| Edit precision | Full-file overwrite or text replacement | 3-mode surgical editing: content-match, block-boundary, or AST symbol — no line numbers needed |
| Code understanding | None | Tree-sitter AST parsing for 20+ languages, lazy-loaded WASM, LRU-cached |
| Search intelligence | Grep or basic glob | BM25 pre-filter + ripgrep + BM25 post-rank, structural similarity, definition lookup |
| Cross-file refactoring | Not available | Impact-graph traversal (callers/callees), outlier detection, atomic multi-file apply with rollback |
| Edit safety | Write and hope | All-or-nothing in-memory validation → atomic temp-file swap → SQLite stash on failure |
| Symbol versioning | Not available | Every symbol edit auto-snapshots the original; point-in-time rollback via stashRestore |
| Security model | CWD or basic prefix check | validatePath with symlink resolution + re-check, exclusive-write wx flag, sensitive-file blocking, per-session isolation |
| Transport | stdio only | stdio and HTTP (Streamable HTTP + legacy SSE, bearer auth, per-session context, idle reaping) |
What the code actually does
🔬 True code-awareness via Tree-sitter
Zenith ships WASM grammars for JavaScript, TypeScript, TSX, Python, Go, Rust, Java, C, C++, C#, Kotlin, PHP, Ruby, Swift, Bash, CSS, JSON, YAML, SQL, Markdown, and more. Grammars are lazy-loaded on first use and permanently cached — zero startup penalty.
This unlocks capabilities that are simply impossible with text search:
edit_filesymbol mode — "Replace theAuthService.loginmethod" without knowing its line number. Zenith finds the AST bounds and replaces exactly the right block, then re-indents the new code to match the file.read_text_filesymbol mode — Read just the body ofBM25Index.scoreacross a 10 000-line file.search_filesstructural mode — Find all functions with the same AST shape (Jaccard similarity over 3-gram node fingerprints) to detect copy-paste patterns or candidates for a common abstraction.search_filesdefinition mode — Locate every file that definesAuthService.loginusing the parse tree, not a fragile regex.- Syntax gate — After any edit,
checkSyntaxErrors()walks the new AST and warns ifERRORor missing nodes were introduced.
🧠 Intelligent two-stage search
Searching a 50 000-file monorepo without drowning the LLM context is a hard problem. Zenith solves it with a pipeline:
- BM25 pre-filter — builds an in-memory BM25 corpus (file paths weighted 3×, first 8 KB of content) and selects the top-100 candidate files.
- ripgrep — blazing-fast regex search scoped to those 100 files (with
.gitignoreawareness). Falls back to a pure-JS implementation whenrgisn't available. - BM25 post-rank — if results exceed 50 lines, BM25 re-ranks individual result lines and fills the character budget with the most relevant hits.
The BM25 implementation is zero-dependency, inline (~120 lines), and uses entropy weighting (high-entropy terms are downweighted) plus sigmoid TF saturation for better precision than vanilla BM25.
⚡ All-or-nothing atomic edits with stash recovery
Edit safety is not an afterthought. Every edit_file call:
- Validates all edits against an in-memory copy of the file — exact match, then trimmed-whitespace match, then indent-stripped match within a ±50-line window.
- If any edit fails, zero edits are applied and the file is untouched.
- Failed edits are stashed to SQLite with their full payload. The AI retries via
stashRestore applywith only the correction needed — unchanged edits rehydrate from the stash automatically. - Successful edits write to a temp file, verify the byte size, then
fs.rename()for an atomic swap.
Block edits (block_start / block_end) and symbol edits (AST-located) follow the same pipeline.
🔗 Cross-file impact refactoring
refactor_batch implements a full refactoring workflow:
query— traverses the per-project SQLite symbol graph (edgestable) to find all callers (forward) or callees (reverse) of a symbol, up to 5 levels deep.load— fetches each symbol's body plus N lines of context. Runs outlier detection: computesgetSymbolStructure()for each occurrence (param shape, return type, decorators, modifiers, parent scope) and flags deviations from the modal pattern.apply— parses a diff-style payload, gates on acknowledged outliers and syntax validity, then applies edits atomically per file. Successful patterns are cached forreapply.reapply— applies a cached edit pattern to new targets without repeating the full workflow.
🗄️ Per-project symbol database
Each git repository gets a .mcp/symbols.db (auto-gitignored) with:
symbols— definitions and references from the full Tree-sitter parseedges— caller → callee links for impact traversalversions— point-in-time snapshots of every symbol body touched byedit_fileorrefactor_batch, with a configurable TTL
Rollback is a single tool call: stashRestore restore symbol:"AuthService.login".
🔒 Security-first design
validatePath()— expands~, normalizes, prefix-checks against allowed directories, callsfs.realpath()to resolve symlinks, then re-checks the resolved path. Throws before anyfscall if the check fails.- Exclusive writes — new file creation uses the
wxflag. Pre-existing symlinks at the target path cannot be exploited. - Sensitive file blocking —
isSensitive()usesminimatchglobs to block.env,*.pem,*.key,*credentials*,*secret*, and more from appearing in search results and symbol indexing. - Per-session isolation (HTTP) — each HTTP client gets its own
FilesystemContext. MCP root negotiations for one session never affect another.
Features
- Read/write files — text, media, and batch reads with budget-aware truncation and optional compression
- Surgical editing — content-match, block-replace, and symbol-aware edits with dry-run preview
- Intelligent search — content search with BM25 ranking, file discovery, symbol search, structural similarity, definition lookup, and single-file grep/symbol search
- Cross-file refactoring — impact analysis, batch symbol loading, and coordinated multi-file edits with rollback
- Code awareness — Tree-sitter AST parsing for 20+ languages (lazy-loaded WASM grammars)
- Symbol indexing & versioning — per-project SQLite index with impact graphs and automatic version snapshots
- Stash & restore — retry failed edits, restore symbol versions, and manage project roots
- Dynamic directory access control via MCP Roots
- Dual transport — stdio (local) and HTTP (remote with Streamable HTTP + legacy SSE)
Quick Start
stdio (Local)
Standard MCP stdio transport for local clients like Claude Desktop or VS Code.
npx zenith-mcp /path/to/dir1 /path/to/dir2HTTP (Remote)
Express-based HTTP server supporting both Streamable HTTP and legacy SSE transports.
ZENITH_MCP_API_KEY=secret npx zenith-mcp-http /path/to/dir1 --port=3100 --host=0.0.0.0HTTP Endpoints:
POST /mcp— Streamable HTTP (initialize + messages)GET /mcp— Streamable HTTP SSE notification streamDELETE /mcp— Streamable HTTP session teardownGET /sse— Legacy SSE transportPOST /messages— Legacy SSE message endpointGET /health— Health check
Sessions are isolated per client and reaped after 30 minutes of idle time (configurable via SESSION_TTL_MS). All HTTP requests require Authorization: Bearer <API_KEY>.
Directory Access Control
Directories can be specified via command-line arguments or dynamically via MCP Roots.
Method 1: Command-line Arguments
zenith-mcp /path/to/dir1 /path/to/dir2Method 2: MCP Roots (Recommended)
MCP clients that support Roots can dynamically update allowed directories at runtime via roots/list_changed notifications. Roots completely replace server-side directories when provided.
Important: If the server starts without CLI directories AND the client doesn't support roots (or provides empty roots), initialization will fail.
Why no fallback? Allowed directories are a strict security sandbox — they determine what the AI can read and write. The server intentionally does not fall back to
process.cwd()or auto-detected git roots, because that could accidentally expose sensitive files. A separate "project root" resolver (used only for the symbol index and stash database) does have fallbacks (git → cwd → registered roots → global), but that layer never grants filesystem access.
How It Works
- Server Startup — uses CLI directories as the baseline
- Client Initialization — if the client supports roots, the server requests
roots/listand replaces allowed directories - Runtime Updates —
notifications/roots/list_changedtriggers a refresh - Access Control — all filesystem operations are restricted to allowed directories; symlinks are resolved and validated
Tools
read_text_file
Read a text file with multiple modes.
mode:
standardpath(string)maxChars(number, optional, default 50000, up to 400000)head(number, optional) — first N linestail(number, optional) — last N linesoffset(number, optional) — start line (0-based), combine withheadshowLineNumbers(boolean, optional)compression(boolean, optional) — compress whitespace via structured compression
mode:
greppath(string)grep(string) — regex to match lines (case-insensitive)grepContext(number, optional, default 0, max 30) — context lines around matchesmaxChars(number, optional)showLineNumbers(boolean, optional)
mode:
windowpath(string)aroundLine(number, optional) — center window on this linecontext(number, optional, default 30) — window radiusranges(array of{startLine, endLine}, optional) — explicit line rangesmaxChars(number, optional)showLineNumbers(boolean, optional)
mode:
symbolpath(string)symbol(string) — symbol name, dot-qualified for methods (e.g.AuthService.login)nearLine(number, optional) — disambiguate multiple matchesexpandLines(number, optional, default 0, max 50) — extra context around symbolmaxChars(number, optional)
read_media_file
Read an image or audio file. Returns base64 data with MIME type.
path(string)- Supported:
.png,.jpg,.jpeg,.gif,.webp,.bmp,.svg,.mp3,.wav,.ogg,.flac
read_multiple_files
Read up to 50 files concurrently with dynamic character budget balancing.
paths(string[])maxCharsPerFile(number, optional)compression(boolean, optional, default true) — compress whitespaceshowLineNumbers(boolean, optional, default false)- Failed reads won't stop the entire operation
write_file
Create, overwrite, or append to a file. Auto-creates parent directories. Atomic writes with temp-file + rename.
path(string)content(string)failIfExists(boolean, optional) — fail if file already existsappend(boolean, optional) — append instead of overwriting; smart-resumes overlapping tails
edit_file
Surgical file editing with three modes. Supports dry-run preview. Failed edits are stashed for retry.
mode:
contentoldContent(string) — exact text to find (uses exact, trimmed, and indent-stripped matching)newContent(string)
mode:
blockblock_start(string) — trimmed first line of the block to replaceblock_end(string) — trimmed last line of the blockreplacement_block(string)
mode:
symbolsymbol(string) — symbol name, dot-qualified for methodsnewText(string)nearLine(number, optional)
All modes support dryRun to preview changes without writing.
directory
Directory exploration with two modes.
mode:
list— list directory contentspath(string, optional)depth(number, optional, default 1, max 10) — recursion depthincludeSizes(boolean, optional, default false)sortBy(enum"name" | "size", optional, default"name") — requiresincludeSizeslistAllowed(boolean, optional, default false) — list allowed root directories instead
mode:
tree— recursive directory tree with optional symbol metadatapath(string)excludePatterns(string[], optional) — glob patterns to excludeshowSymbols(boolean, optional, default false) — show symbol counts per fileshowSymbolNames(boolean, optional, default false) — show symbol names per file
search_files
Multi-mode search with ripgrep + BM25 ranking and JS fallback.
mode:
content— text/regex search (always case-insensitive)path(string)contentQuery(string) — text or regex to search forpattern(string, optional) — glob to limit filescontextLines(number, optional, default 0)literalSearch(boolean, optional, default false)countOnly(boolean, optional, default false)includeHidden(boolean, optional, default false)maxResults(number, optional, default 50)
mode:
files— file discoverypath(string)pattern(string, optional)namePattern(string, optional)pathContains(string, optional)extensions(string[], optional)includeMetadata(boolean, optional, default false)includeHidden(boolean, optional, default false)maxResults(number, optional, default 100)
mode:
symbol— find symbols by name substring, or list all symbols when omittedpath(string)symbolQuery(string, optional) — omit to list all symbolssymbolKind(enum, optional, default"any")pattern(string, optional)maxResults(number, optional, default 50)
mode:
structural— find structurally similar symbols (AST fingerprinting)path(string)structuralQuery(string) — symbol name to find similar definitions ofsymbolKind(enum, optional, default"any")maxResults(number, optional, default 20)
mode:
definition— find files defining a specific symbolpath(string)definesSymbol(string) — dot-qualified supportednamePattern(string, optional)pathContains(string, optional)extensions(string[], optional)maxResults(number, optional, default 100)
search_file
Single-file search by regex or symbol name. Read-only.
path(string) — file to searchgrep(string, optional) — case-insensitive regex to match linesgrepContext(number, optional, default 0, max 30) — context lines around matchessymbol(string, optional) — symbol name, dot-qualified for methods (e.g.AuthService.login)nearLine(number, optional) — disambiguate multiple symbol matchesexpandLines(number, optional, default 0, max 50) — extra context around symbolmaxChars(number, optional, default 50000, up to 400000)
file_manager
Directory and file management operations.
- mode:
mkdir—path - mode:
delete—path(file only, irreversible) - mode:
move—source,destination - mode:
info—path(returns size, created, modified, accessed, type, permissions)
stashRestore
Retry failed edits, restore versions, browse stash, and manage project roots.
mode:
apply— retry a stashed edit or writestashId(number)corrections(array, optional) — disambiguation for failed editsnewPath(string, optional) — redirect a failed writedryRun(boolean, optional)
mode:
restore— rollback a symbol version or clear a stash entrystashId(number, optional)symbol(string, optional)version(number, optional)file(string, optional)dryRun(boolean, optional)
mode:
list— show all stash entriestype(enum"edit" | "write", optional)
mode:
read— view a stash entry's contentsstashId(number)
mode:
init— register a non-git directory as a project rootprojectRoot(string)projectName(string, optional)
mode:
history— list version snapshots for a symbolsymbol(string)file(string, optional)
refactor_batch
Apply one edit pattern across multiple similar symbols, with outlier detection and rollback.
mode:
query— impact analysis (callers or callees)target(string) — symbol namefileScope(string, optional)direction(enum"forward" | "reverse", default"forward")depth(number, default 1, max 5)
mode:
load— load symbol bodies with contextselection(array) — indices from prior query or explicit{symbol, file}pairscontextLines(number, optional, default 5, max 30)loadMore(boolean, optional, default false)
mode:
apply— apply edited diff to selected occurrencespayload(string) — edited diff with symbol headersdryRun(boolean, optional)
mode:
reapply— reuse a cached payload on new targetssymbolGroup(string)newTargets(array) — names or{symbol, file}pairsdryRun(boolean, optional)
Tool Annotations
| Tool | readOnlyHint | idempotentHint | destructiveHint | Notes |
|-----------------------|--------------|----------------|-----------------|-------------------------------------------------|
| read_text_file | true | — | — | Pure read |
| read_media_file | true | — | — | Pure read |
| read_multiple_files | true | — | — | Pure read |
| directory | true | — | — | Pure read |
| search_files | true | — | — | Pure read |
| search_file | true | — | — | Pure read (single-file) |
| write_file | false | false | true | Overwrites existing files |
| edit_file | false | false | true | Re-applying edits can fail or double-apply |
| file_manager | false | false | true | Mixed: mkdir is idempotent, delete/move are not |
| stashRestore | false | false | true | Restores and applies are stateful |
| refactor_batch | false | false | true | Multi-file writes |
Usage with Claude Desktop
Add this to your claude_desktop_config.json:
NPX (stdio)
{
"mcpServers": {
"zenith": {
"command": "npx",
"args": [
"-y",
"zenith-mcp",
"/Users/username/Desktop"
]
}
}
}HTTP
{
"mcpServers": {
"zenith": {
"url": "http://localhost:3100/mcp",
"headers": {
"Authorization": "Bearer your-api-key"
}
}
}
}Usage with VS Code
Method 1: User Configuration
Open the Command Palette (Ctrl + Shift + P) and run MCP: Open User Configuration.
Method 2: Workspace Configuration
Add the configuration to .vscode/mcp.json in your workspace.
NPX Example
{
"servers": {
"zenith": {
"command": "npx",
"args": [
"-y",
"zenith-mcp",
"${workspaceFolder}"
]
}
}
}Environment Variables
| Variable | Description |
|----------|-------------|
| ZENITH_MCP_API_KEY / MCP_BRIDGE_API_KEY / COMMANDER_API_KEY | API key for HTTP mode (required) |
| SESSION_TTL_MS | HTTP session idle timeout in ms (default: 1800000) |
| CHAR_BUDGET | Global character budget for reads (default: 400000) |
| SEARCH_CHAR_BUDGET | Character budget for search results (default: 15000) |
| DEFAULT_EXCLUDES | Comma-separated default exclude patterns |
| SENSITIVE_PATTERNS | Comma-separated sensitive file glob patterns |
| REFACTOR_MAX_CHARS | Max characters for refactor_batch (default: 30000) |
| REFACTOR_MAX_CONTEXT | Max context lines for refactor_batch (default: 30) |
| REFACTOR_VERSION_TTL_HOURS | Version snapshot TTL in hours (default: 24) |
| TOON_PROJECT_DIR | Path to the toon compression project (default: /home/tanner/Projects/toon) |
| ZENITH_MCP_ADAPTERS_ENABLED | Comma-separated adapter names to enable (overrides config file) |
| ZENITH_MCP_ADAPTER_BACKUP_DIR | Backup directory for adapter config file changes |
Adapter Configuration
Zenith-MCP can auto-configure MCP client config files for 16 platforms.
Supported Adapters
| Adapter | Config Format | Platform | |---------|---------------|----------| | Claude Desktop | JSON | macOS, Windows | | OpenCode | TOML | Linux, macOS | | VS Code Copilot | JSON | All | | Cline | JSON | All | | Codex CLI | JSON | All | | Codex Desktop | JSON5 | All | | Continue.dev | JSON | All | | Gemini CLI | JSON | All | | GitHub Copilot | JSON | All | | JetBrains | YAML | All | | OpenClaw | JSON | All | | Raycast | JSON | macOS | | Roo Code | JSON | All | | Warp | YAML | macOS, Linux | | Zed | JSON | All | | Antigravity | JSON | All |
Adapter CLI
# List all available adapters
npx zenith-mcp-config --list
# Enable adapters (comma-separated)
npx zenith-mcp-config --enable claude_desktop,opencode
# Check status
npx zenith-mcp-config --status
# Set backup directory (for config file backups before modification)
npx zenith-mcp-config --backup-dir ~/.zenith-mcp/backupsSettings are persisted at ~/.zenith-mcp/adapter-config.json and can be overridden via environment variables ZENITH_MCP_ADAPTERS_ENABLED and ZENITH_MCP_ADAPTER_BACKUP_DIR.
Server Configuration
Zenith-MCP includes a config management system for managing external MCP server registrations.
Admin CLI
# List configured servers and their tools
npx zenith-mcp-config-admin list
# Show detailed status
npx zenith-mcp-config-admin status
# Register a new server
npx zenith-mcp-config-admin install my-server npx -y my-mcp-server
# Scan configured servers
npx zenith-mcp-config-admin scanConfig is stored at ~/.zenith-mcp/zenith-mcp/servers.yaml:
servers:
my-server:
command: npx
args: ["-y", "my-mcp-server"]
transport: stdio
enabled: true
tools: {}
toolFilters:
allow: []
deny: []
retrieval:
enabled: false
topK: 15
scorer: bmxfThe retrieval section controls the optional tool retrieval pipeline. When enabled, Zenith dynamically filters the tool set presented to clients based on workspace context and conversation history, using a 6-tier scoring fallback (BMXF blend → env-only → keyword → static categories → frequency prior → universal).
Build
npm install
npm run buildThe repository uses a hybrid layout: dist/core/, dist/tools/, dist/cli/, and dist/server/ remain committed JavaScript source, while src/ contains the TypeScript adapters, config, and retrieval code compiled by npm run build into dist/adapters/, dist/config/, and dist/retrieval/. These compiled directories are gitignored — only the hand-authored dist/ subdirectories are version-controlled.
License
MIT License. See the LICENSE file in the project repository.
