Back to home

JohnXu22786

fs-mcp

paddock: constrained local filesystem MCP server for agents — file read/write/search confined to configurable zones, zero runtime dependencies, installable as a dsh plugin bundle.

Stars
0
Language
JavaScript
Created
Aug 16, 2026
Updated
Aug 16, 2026

Introduction

简体中文

paddock — Local filesystem workbench (MCP stdio server)

paddock gives agents a complete, constrained set of local file operations: text read/write, directory management, filename search, content search, and metadata queries — all operations are confined to configurable "paddock" directories, and out-of-bounds access is always rejected.

  • Zero runtime dependencies: pure Node.js standard library, no npm install needed
  • MCP protocol: line-delimited JSON-RPC 2.0 over stdio, loadable by any MCP client
  • dsh-ready: ships a Cordis bridge plugin, one-line bundle integration with the harness
  • Read-only mode: --read-only blocks all write operations in one shot
  • Large files & binaries: streaming head/tail, byte slicing, base64 reads, binary sniffing
  • Search: a self-built glob engine (* ** ? {a,b} [abc]) plus regex/fixed-string content search

Quick start

# No dependency installation needed, run directly
node src/entry.js "C:/Users/me/projects"

Once started, the MCP session begins: JSON-RPC messages are read line by line from stdin, written line by line to stdout, and logs go only to stderr.

Connect from any MCP client (example with a standard MCP client config):

{
  "mcpServers": {
    "paddock": {
      "command": "node",
      "args": ["/path/to/fs-mcp/src/entry.js", "C:/Users/me/projects"]
    }
  }
}

You can also run a smoke session directly to verify:

# Write an initialize request to stdin and observe the response
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' | node src/entry.js "C:/Users/me/projects"

Installation

  • As an MCP server: copy this directory, run node src/entry.js <paddock dirs…>, no build step.
  • As a dsh plugin: see docs/INTEGRATION.md.
  • Requirements: Node.js ≥ 18.17 (check with node --version).

Installing in DSH

dsh plugin --profile demo add github:JohnXu22786/fs-mcp
  • demo is a dsh profile: it is created automatically on first use, and the package is added to dsh.profile.bundles;
  • The cordis.patch.yml inside the package defines the paddock/bridge plugin, which starts this MCP server within the dsh process and registers all 16 tools into ctx.tools after the handshake — no manual configuration needed;
  • Removal:
dsh plugin --profile demo remove paddock

Configuration

Configuration is merged as "defaults < config file < environment variables < command-line arguments"; paddock directories are unioned across all three sources.

Command line

node src/entry.js [options] [paddock dirs…]

  --config <path>   config file (defaults to paddock.config.json in the working directory, loaded only if present)
  --read-only       read-only mode: blocks all write operations
  --zones <path>    additional paddock directories (repeatable)
  --verbose         debug logging (stderr)
  --version / --help

Positional arguments are treated as paddock directories. At least one paddock is required, otherwise startup fails (exit code 2).

Config file (paddock.config.json)

{
  "zones": ["C:/Users/me/projects", "C:/Users/me/data"],
  "readOnly": false,
  "limits": {
    "peekBytes": 1048576,
    "readBytes": 16777216,
    "sliceBytes": 65536,
    "grepBytes": 262144,
    "grepHits": 200,
    "findHits": 1000,
    "treeDepth": 6
  },
  "behavior": {
    "includeHidden": false
  }
}

A full example is in examples/paddock.config.example.json.

Environment variables

VariableDescription
PADDOCK_CONFIGconfig file path (takes precedence over the default path)
PADDOCK_ZONESpaddock directories: a JSON array string (["/a","/b"]) or a single path
PADDOCK_READ_ONLY1 / true / yes / on count as enabled
PADDOCK_VERBOSEsame as above (1 / true / yes / on)

limits semantics

KeyDefaultEffect
peekBytes1 MiBupper bound for pdk_peek full reads (head/tail are not limited by this, but an over-long single line is capped by the internal 8 MiB line-buffer limit)
readBytes16 MiBupper bound for pdk_read_bytes base64 reads
sliceBytes64 KiBupper bound for a single pdk_slice slice
grepBytes256 KiBper-file scan limit for pdk_grep (deep: true can bypass; lines over 1 MiB are skipped wholesale)
grepHits200hit-count limit for pdk_grep
findHits1000result-count limit for pdk_find
treeDepth6default depth limit for pdk_tree
treeEntries20000total node limit for pdk_tree (truncated with a truncated marker when exceeded)

Tool interface

16 tools; write operations carry a mutating marker (gated by read-only mode). All path arguments must be absolute paths inside a paddock; relative paths are resolved against the process working directory and validated the same way.

Read-only tools

ToolParametersDescription
pdk_zoneslist paddocks (given paths + resolved real paths)
pdk_lspath, sortBy?list directory: entry type (file/dir/link), size, sorting, counts and totals
pdk_treepath, depth?, exclude?, includeHidden?recursive directory tree (JSON); overly deep directories are marked cut: true, node-limit overruns are marked truncated
pdk_peekpath, head?, tail?read text files; head/tail stream the first/last N lines; binary and over-limit files are rejected
pdk_read_manypaths[]batch text reads (1–64 files); a single file failure is folded without aborting
pdk_slicepath, offset?, length?read large files by byte range; binary content returned as base64; eof: true when the slice reaches the end of the file
pdk_read_bytespathbase64 read of binaries + MIME guessing (png/jpg/mp3/…)
pdk_findpath, pattern, exclude?, includeHidden?recursive filename glob search
pdk_greppath, needle, mode?, caseSensitive?, glob?, exclude?, includeHidden?, deep?content search: regex (case-insensitive by default) or fixed string; line numbers + truncated snippets
pdk_metapathmetadata: type/size/permissions/times; symlinks report their target

Write tools

ToolParametersDescription
pdk_writepath, contentwrite/overwrite (UTF-8), parent directories created automatically
pdk_rewritepath, edits[], dryRun?precise replacement: {oldText, newText, all?} applied in order; fails on any miss; dryRun previews -/+ changes; newText is treated literally ($ sequences are never expanded)
pdk_mkdirpathrecursive directory creation, idempotent
pdk_movesource, destinationmove/rename; refuses to overwrite an existing destination; cross-device copies + deletes automatically
pdk_copysource, destinationcopy files/directories; symlinks are not followed (links are recreated as-is)
pdk_removepath, recursive?delete; non-empty directories need recursive: true; paddock roots can never be deleted

Error model

Tool-level failures do not produce protocol errors; they return structured isError: true results:

{
  "isError": true,
  "content": [{ "type": "text", "text": "[not-found] File does not exist: C:/…" }],
  "structuredContent": { "error": { "code": "not-found", "message": "File does not exist: C:/…" } }
}

Error codes: config readonly validation fence (out of bounds) not-found conflict limit binary wrong-kind io internal (fallback). network / timeout / jsonrpc appear only in the bridge client (the communication layer with the server subprocess). Error messages carry actionable follow-up suggestions (e.g. large files are pointed to pdk_slice).


Security model

  • Paddock boundary: every path passes "paddock" validation before any IO (see docs/SECURITY.md).
  • Symlinks: existing paths are realpath-resolved then validated; new paths validate by resolving the "nearest existing ancestor" and re-checking — links inside a paddock that point outside can never become a springboard.
  • Component-level checks: string-prefix misjudgments such as zone vs zone_extra are impossible; win32 comparisons fold case.
  • Windows hardening: rejects invalid path segments, trailing dots/spaces, and reserved device names (NUL/CON/COM1…).
  • Read-only mode: with --read-only or config readOnly: true, all 6 write tools are gated.
  • Delete protection: paddock root directories cannot be removed by pdk_remove.
  • Large files/binaries: text-read limits, slice reads, base64, NUL-byte sniffing — the model is never slammed with gigantic or binary content.

dsh integration

Pick either of the two ways; details in docs/INTEGRATION.md:

  1. Bundle bridge (recommended, zero dependencies): this package's dsh.bundle.patch points to cordis.patch.yml, and dsh merges the paddock/bridge plugin line into the config tree; the plugin starts the server subprocess inside the harness process and, after the handshake, registers all 16 tools into ctx.tools — model-side tool names look like mcp__paddock__pdk_peek.
  2. Official MCP client: connect directly to node src/entry.js <paddock> with the @deepseek-ai/dsh-mcp-client config line.

Development

npm test        # node --test: 100+ cases (4 symlink cases skip on win32 as they need developer mode)
npm run smoke   # sequential end-to-end smoke test (real subprocess + stdio session, 16 checks)
node src/entry.js --help

Test coverage: paddock escapes/symlink escapes/ancestor-chain protection, read-write round-trips, large-file and binary policies, glob and content search, three-source config merging, protocol engine, end-to-end subprocess sessions.

License

MIT, see LICENSE.