Self-hosted MCP server for tight developer-model collaboration. Exposes your shell, filesystem, and HTTP client to Claude Desktop and Claude Code via the Model Context Protocol. Runs on Docker. Auditable. Secure by default.
-
Clone the repo:
git clone https://github.com/rdemeritt/pagurus cd pagurus -
Copy and configure environment:
cp .env.example .env chmod 600 .env # Edit .env with your settings (see Configuration below) -
Generate an API key:
pnpm install pnpm pagurus keygen
Copy the printed key into
PAGURUS_API_KEYSin.env. -
Start the server:
docker compose up -d
-
Verify it's running:
curl http://127.0.0.1:8080/healthz # → {"status":"ok","version":"0.1.0"}
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"pagurus": {
"url": "http://127.0.0.1:8080/mcp",
"headers": {
"Authorization": "Bearer pag_live_YOUR_KEY_HERE"
}
}
}
}Restart Claude Desktop. You'll now see Pagurus tools in the compose panel.
claude mcp add --transport http pagurus http://127.0.0.1:8080/mcp \
--header "Authorization: Bearer pag_live_YOUR_KEY_HERE"| Tool | Description | Default | Env Var |
|---|---|---|---|
fs.read |
Read file contents (sandboxed) | Enabled | PAGURUS_FS_ROOT, PAGURUS_FS_DENYLIST |
fs.write |
Write/overwrite files | Enabled | PAGURUS_FS_WRITE |
fs.list |
List directory contents | Enabled | PAGURUS_FS_ROOT |
http.fetch |
Fetch URLs with SSRF protection | Enabled | PAGURUS_HTTP_ALLOWLIST |
shell.exec |
Execute shell commands | Disabled | PAGURUS_SHELL_ENABLED, PAGURUS_SHELL_ALLOWLIST |
See ADR-003 for tool design rationale.
Pagurus is a Model Context Protocol (MCP) server that sits between Claude (Desktop or Code) and your local filesystem, HTTP APIs, and shell commands. It acts as a security boundary, enforcing authentication, authorization, and sandboxing at every layer.
sequenceDiagram
participant Claude as Claude Desktop/<br/>Code
participant Proxy as Reverse Proxy<br/>(Traefik, Caddy, etc.)
participant Pagurus as Pagurus Container
participant Auth as Auth Middleware
participant Tools as Tool Packs
Claude->>Proxy: POST /mcp<br/>+ Authorization header
Proxy->>Pagurus: HTTP or HTTPS
Pagurus->>Auth: Extract & validate<br/>Bearer token
Auth->>Auth: SHA-256 fingerprint<br/>+ timingSafeEqual
alt Token invalid
Auth-->>Claude: 401 Unauthorized
else Token valid
Auth->>Tools: Route to tool<br/>e.g., fs.read
Tools->>Tools: Jail path / validate<br/>SSRF check / check allowlist
Tools-->>Pagurus: Tool result
Pagurus-->>Claude: MCP response
end
graph TD
A["Pagurus Server<br/>(Hono + MCP SDK)"]
A1["GET /healthz<br/>GET /readyz<br/>POST /mcp"]
A --> A1
B["Auth Middleware"]
C["DNS-Rebinding Defense"]
D["MCP Streamable HTTP Transport"]
A1 --> B
B --> C
C --> D
E["Tool Packs"]
F["fs Pack"]
G["http Pack"]
H["shell Pack"]
D --> E
E --> F
E --> G
E --> H
F1["Path Jailing<br/>Denylist<br/>Max Size"]
F --> F1
G1["DNS-Rebinding<br/>SSRF Validation<br/>Allow-list<br/>Private IP Block"]
G --> G1
H1["Command Allow-list<br/>Hard Deny-list<br/>Concurrency Cap<br/>Process Group Kill"]
H --> H1
I["Config<br/>env vars"]
I -.-> A
I -.-> B
I -.-> E
When Claude calls a tool through Pagurus, here's what happens:
-
Claude sends a request — POST to
/mcpwith JSON-RPCtools/callandAuthorization: Bearer <KEY>header. -
Reverse proxy receives it — The proxy terminates TLS and forwards to Pagurus over HTTP (internal network).
-
Hono server accepts POST /mcp — Routes through:
- Health endpoint exemption —
/healthzand/readyzskip all auth - Auth middleware — Extracts Bearer token, compares using
timingSafeEqualagainst API keys to prevent timing attacks; setskeyFingerprinton context; returns 401 if no match
- Health endpoint exemption —
-
DNS-rebinding defense — Validates
OriginandHostheaders againstPAGURUS_EXTERNAL_URL; blocks mismatches with 403. -
MCP transport deserializes — Converts JSON-RPC to MCP handler call.
-
Tool handler executes — Depending on the tool:
- fs.read — Jails path (resolves realpath, checks for
..escape), checks denylist, reads with size cap - http.fetch — Validates hostname against allow-list, checks for private IPs via DNS lookup, enforces SSRF rules, strips sensitive headers
- shell.exec — Checks command allowlist AND hard deny-list, jails cwd, spawns with
shell: false, kills process group on timeout
- fs.read — Jails path (resolves realpath, checks for
-
Response returned — Tool returns result as MCP
tools/callresponse; Pagurus serializes and sends back to Claude. -
Audit log — Shell execution logs command, args, exit code, duration, truncation status, and key fingerprint for compliance.
Purpose: List files and directories in the workspace.
Example:
{
"method": "tools/call",
"params": {
"name": "fs.list",
"arguments": {
"path": "."
}
}
}Configuration:
PAGURUS_FS_ROOT— Absolute path to expose (required)
Security: Paths are jailed; .. sequences cannot escape the root. No symlink attacks possible (realpath validation).
Purpose: Read a file from the workspace as text or base64.
Example:
{
"method": "tools/call",
"params": {
"name": "fs.read",
"arguments": {
"path": "src/config.ts",
"encoding": "utf8"
}
}
}Configuration:
PAGURUS_FS_ROOT— Workspace root (required)PAGURUS_FS_DENYLIST— Glob patterns to deny (default:.env,.env.*,**/*.key,**/*.pem,**/*.p12)PAGURUS_FS_MAX_READ_BYTES— Max bytes (default: 1 MiB, max: 100 MiB)
Security: Files matching denylist are blocked. Reads are bounded (TOCTOU-safe via fd operations). Binary files auto-detect and return as base64.
Purpose: Write or overwrite a file in the workspace.
Example:
{
"method": "tools/call",
"params": {
"name": "fs.write",
"arguments": {
"path": "output.txt",
"content": "hello world",
"encoding": "utf8"
}
}
}Configuration:
PAGURUS_FS_WRITE— Enable writes (default:true)PAGURUS_FS_ROOT— Workspace root (required)PAGURUS_FS_DENYLIST— Same asfs.readPAGURUS_FS_MAX_READ_BYTES— Size cap applies
Security: Writes use atomic rename (temp file + rename); denylist is enforced. Disabled by default; set PAGURUS_FS_WRITE=false for read-only mode.
Purpose: Fetch a URL from an operator-approved allow-list.
Example:
{
"method": "tools/call",
"params": {
"name": "http.fetch",
"arguments": {
"url": "https://api.github.com/repos/rdemeritt/pagurus",
"method": "GET",
"headers": {
"Accept": "application/json"
},
"timeout_ms": 10000
}
}
}Configuration:
PAGURUS_HTTP_ALLOWLIST— Comma-separated hostnames (e.g.,api.github.com,*.api.example.com). Empty = deny all (fail-closed).PAGURUS_HTTP_ALLOW_PRIVATE— Allow private IPs (default:false). Never settruein production.
Security:
- DNS is resolved at connection time (prevents rebinding attacks).
- Private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 127.0.0.0/8, fe80::/10, fc00::/7, etc.) are blocked unless explicitly allowed.
- Auth headers are stripped on redirect to different origins.
- Response body capped at 1 MB; truncated responses flagged.
Purpose: Execute an allow-listed command in the workspace.
Example:
{
"method": "tools/call",
"params": {
"name": "shell.exec",
"arguments": {
"command": "git",
"args": ["status"],
"cwd": "myrepo",
"timeout_ms": 5000
}
}
}Configuration:
PAGURUS_SHELL_ENABLED— Enable this tool (default:false). High-risk feature.PAGURUS_SHELL_ALLOWLIST— Comma-separated command basenames (e.g.,git,ls,cat,grep). Empty = no commands allowed.
Security:
- Shells (
sh,bash,zsh, etc.) and interpreters (node,python,ruby, etc.) are hard-blocked regardless of allowlist. - Dangerous commands (
rm,chmod,sudo,kill, etc.) are always blocked. - Process spawned with
shell: false(no shell injection possible). - Entire process group is killed on timeout (no orphans).
- Concurrency capped at 4 concurrent calls.
- All executions are audit-logged with command, args, exit code, duration, and key fingerprint.
Pagurus implements defense-in-depth across multiple layers:
flowchart TD
A["Request<br/>from Claude"]
B["Layer 1: TLS<br/>Traefik terminates"]
C["Layer 2: Auth<br/>Bearer token +<br/>timingSafeEqual"]
D["Layer 3: DNS-Rebinding<br/>Origin/Host validation"]
E["Layer 4: Tool-Level<br/>Sandboxing"]
A --> B
B --> C
C --> D
D --> E
F["fs: Path jailing<br/>+ denylist<br/>+ size bounds"]
G["http: SSRF defense<br/>+ allow-list<br/>+ private IP block"]
H["shell: Hard deny<br/>+ allow-list<br/>+ process kill"]
E --> F
E --> G
E --> H
F --> I["Sandboxed<br/>Response"]
G --> I
H --> I
Layer 1: TLS Transport
- All traffic to Pagurus is encrypted. A reverse proxy terminates TLS; internal communication uses your Docker network.
Layer 2: Bearer Token Authentication
- API keys are cryptographically compared using
timingSafeEqualto prevent timing attacks. - Keys are plaintext in
.env— keep the file private (chmod 600). - No session tokens or OAuth yet (v0.2 planned).
Layer 3: DNS-Rebinding Defense
PAGURUS_EXTERNAL_URLis required; bothOriginandHostheaders are validated against it.- Requests to
localhostor127.0.0.1bypass this check (development convenience). - Prevents DNS rebinding attacks that would trick the server into accepting attacker-controlled hosts.
Layer 4: Tool-Level Sandboxing
- fs: Paths are jail-validated using realpath + relative path checks; symlinks cannot escape root. Denylist (glob patterns) blocks sensitive files (
.env,*.key,*.pem). Reads are size-bounded and TOCTOU-safe. - http: Hostname allow-list is fail-closed (empty = deny all). Private IP ranges are blocked by default. SSRF is validated at connection time via DNS; undici's agent hook enforces checks. Headers are scrubbed; cookies and auth headers are stripped.
- shell: Hard deny-list blocks shells and interpreters regardless of allow-list. Spawned with
shell: false(no injection). Process group killed on timeout. All executions audit-logged.
For details on authentication design, see ADR-002. For tool threat model, see ADR-003.
For known limitations and how to report security vulnerabilities, see SECURITY.md.
All configuration via environment variables in .env:
| Variable | Default | Description |
|---|---|---|
PAGURUS_BIND_HOST |
127.0.0.1 |
Hostname to bind to. 0.0.0.0 only on private networks. |
PAGURUS_BIND_PORT |
8080 |
Port to listen on. |
PAGURUS_EXTERNAL_URL |
— | REQUIRED Public URL of this instance (e.g., https://pagurus.yourdomain.com). Used for DNS rebinding defense. |
PAGURUS_API_KEYS |
— | REQUIRED Comma-separated API keys. Generate with pnpm pagurus keygen. |
PAGURUS_FS_ROOT |
— | REQUIRED Absolute path on host to expose as workspace root. |
PAGURUS_FS_WRITE |
true |
Allow fs.write tool. Set false for read-only. |
PAGURUS_FS_DENYLIST |
.env,.env.*,**/*.key,**/*.pem,**/*.p12 |
Glob patterns to deny access (relative to FS_ROOT). |
PAGURUS_FS_MAX_READ_BYTES |
1048576 (1 MiB) |
Max file size for reads. Max: 100 MiB. |
PAGURUS_HTTP_ALLOWLIST |
— | Comma-separated hostname allow-list (e.g., api.github.com,*.api.example.com). Empty = deny all. |
PAGURUS_HTTP_ALLOW_PRIVATE |
false |
Allow private IP ranges in HTTP requests. Never set true in production. |
PAGURUS_SHELL_ENABLED |
false |
Enable shell.exec tool. Warning: highest-risk feature. |
PAGURUS_SHELL_ALLOWLIST |
— | Comma-separated allowed command basenames (e.g., git,ls,cat,grep). Shells always blocked. |
See .env.example for all options with detailed descriptions.
The base docker-compose.yml binds to 127.0.0.1:8080. To expose Pagurus over HTTPS, put a reverse proxy in front (Traefik, Caddy, Nginx, etc.).
See DEPLOYMENT.md for platform-specific examples (HermitHost, Coolify, standalone Traefik, Caddy, Nginx, etc.).
- OAuth 2.0 authentication (multi-user)
- Multi-tenant isolation
- Notes pack (create/update/search local markdown notes)
- Admin dashboard for key + tool management
- Rate limiting per API key
- Request logging + audit trail
- Custom tool packs (user-defined code execution sandboxes)
- WebSocket transport for streaming responses
- Secrets rotation
We welcome contributions. Please read CONTRIBUTING.md before opening a PR.
Development setup:
pnpm install
pnpm test # Run tests
pnpm lint # Lint code
pnpm typecheck # Type checking
pnpm dev # Local dev server (watch mode)This project adopts the Contributor Covenant. All participants are expected to uphold this code.
Found a security vulnerability? Please read SECURITY.md for responsible disclosure guidelines.
MIT License — see LICENSE for details. Copyright 2026 Pagurus Contributors.
- Model Context Protocol — open standard for AI tool integration
- HermitHost — self-hosted platform that inspired this
- MCP SDK team for excellent documentation and tooling