paddock
by JohnXu22786
README.md
[简体中文](README.zh.md)
# 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
```bash
# 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):
```json
{
"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:
```bash
# 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](docs/INTEGRATION.md).
- Requirements: Node.js ≥ 18.17 (check with `node --version`).
### Installing in DSH
```bash
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:
```bash
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`)
```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](examples/paddock.config.example.json).
### Environment variables
| Variable | Description |
| --- | --- |
| `PADDOCK_CONFIG` | config file path (takes precedence over the default path) |
| `PADDOCK_ZONES` | paddock directories: a JSON array string (`["/a","/b"]`) or a single path |
| `PADDOCK_READ_ONLY` | `1` / `true` / `yes` / `on` count as enabled |
| `PADDOCK_VERBOSE` | same as above (`1` / `true` / `yes` / `on`) |
### limits semantics
| Key | Default | Effect |
| --- | --- | --- |
| `peekBytes` | 1 MiB | upper 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) |
| `readBytes` | 16 MiB | upper bound for `pdk_read_bytes` base64 reads |
| `sliceBytes` | 64 KiB | upper bound for a single `pdk_slice` slice |
| `grepBytes` | 256 KiB | per-file scan limit for `pdk_grep` (`deep: true` can bypass; lines over 1 MiB are skipped wholesale) |
| `grepHits` | 200 | hit-count limit for `pdk_grep` |
| `findHits` | 1000 | result-count limit for `pdk_find` |
| `treeDepth` | 6 | default depth limit for `pdk_tree` |
| `treeEntries` | 20000 | total 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
| Tool | Parameters | Description |
| --- | --- | --- |
| `pdk_zones` | — | list paddocks (given paths + resolved real paths) |
| `pdk_ls` | `path`, `sortBy?` | list directory: entry type (file/dir/link), size, sorting, counts and totals |
| `pdk_tree` | `path`, `depth?`, `exclude?`, `includeHidden?` | recursive directory tree (JSON); overly deep directories are marked `cut: true`, node-limit overruns are marked `truncated` |
| `pdk_peek` | `path`, `head?`, `tail?` | read text files; head/tail stream the first/last N lines; binary and over-limit files are rejected |
| `pdk_read_many` | `paths[]` | batch text reads (1–64 files); a single file failure is folded without aborting |
| `pdk_slice` | `path`, `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_bytes` | `path` | base64 read of binaries + MIME guessing (png/jpg/mp3/…) |
| `pdk_find` | `path`, `pattern`, `exclude?`, `includeHidden?` | recursive filename glob search |
| `pdk_grep` | `path`, `needle`, `mode?`, `caseSensitive?`, `glob?`, `exclude?`, `includeHidden?`, `deep?` | content search: regex (case-insensitive by default) or fixed string; line numbers + truncated snippets |
| `pdk_meta` | `path` | metadata: type/size/permissions/times; symlinks report their target |
### Write tools
| Tool | Parameters | Description |
| --- | --- | --- |
| `pdk_write` | `path`, `content` | write/overwrite (UTF-8), parent directories created automatically |
| `pdk_rewrite` | `path`, `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_mkdir` | `path` | recursive directory creation, idempotent |
| `pdk_move` | `source`, `destination` | move/rename; refuses to overwrite an existing destination; cross-device copies + deletes automatically |
| `pdk_copy` | `source`, `destination` | copy files/directories; symlinks are not followed (links are recreated as-is) |
| `pdk_remove` | `path`, `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:
```json
{
"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](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](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
```bash
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](LICENSE).
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing