AIUpdated 9 min

Whitelist vs. blocklist for MCP shells: aki-mcp-sv's security architecture

An MCP server exposing shell access to the internet has to choose between whitelist and blocklist. Comparing aki-mcp-sv with Desktop Commander, mcp-remote, and Anthropic's official server-filesystem, plus an analysis of OAuth 2.1 without DCR.

An MCP (Model Context Protocol) server that wants to let Claude web or ChatGPT run shell commands on a real machine has to answer one architecture question first: gate commands with a whitelist or a blocklist? aki-mcp-sv, Lac Viet Anh's open-source MCP server, chose whitelist. This article explains why that choice fits a server that self-hosts to the internet, and places it alongside other existing remote MCP patterns.

Four groups of MCP servers, and which one aki-mcp-sv belongs to

Zooming out, most existing MCP servers fall into three groups that don't solve the same problem as aki-mcp-sv, and aki-mcp-sv is the fourth group:

  • Local-only, not remote: Desktop Commander (blocklist, runs stdio for Claude Desktop) and Anthropic's official server-filesystem (no shell tool at all, only reads/writes files inside allowed directories) — neither was designed to be exposed to the internet.
  • Bridge/proxy, not a server: mcp-remote (geelen) is a client-side tool that relays stdio to HTTP/SSE for a remote MCP server that already exists elsewhere; it doesn't grant any filesystem or shell access on its own.
  • Cloud-hosted SaaS: Composio, Smithery-hosted MCP run on the provider's infrastructure, acting as a middleman for third-party API calls (GitHub, Slack...), never touching your personal machine's filesystem/shell.
  • Self-hosted, self-remote (aki-mcp-sv): runs on your own machine, self-exposes via Tailscale Funnel, you keep full control of the infrastructure instead of handing it to a third party.

Where whitelist beats blocklist

Desktop Commander blocks shell with a blocklist (blockedCommands): it lists forbidden commands, allowing everything else by default. A blocklist is inherently leaky, unable to enumerate every dangerous command and its variants. Desktop Commander's own guide says outright: never expose it to the internet.

aki-mcp-sv targets a different situation: opening access for Claude and ChatGPT on the web, over the open internet via Tailscale Funnel. The opposite choice, a whitelist with deny-by-default, delivers four properties: fail-safe (unknown commands are blocked automatically), minimal attack surface, subcommand-level granularity (git is scoped to status/log/diff/show only), and read-only by default.

That "read-only by default" property wasn't just theoretical: 1.1.0 still kept find and sort in the default allowlist, even though each command's own flags break read-only (find -delete/-exec, sort -o <path>), and execFile offers no defense since the danger sits in the binary's own argv, not in a shell. Version 1.2.0 closed this hole (issue #2) by dropping both commands from the default set entirely rather than patching individual flags, staying true to the whitelist philosophy: curate the surface, don't patch each case. The search arm's find_path/search_content cover the read-only lookup those commands were reached for.

OAuth 2.1: Claude and Gemini paste in, ChatGPT and Grok self-register

Note

claude.ai defaults to attempting client self-registration (Dynamic Client Registration - DCR) before connecting. aki-mcp-sv doesn't advertise that endpoint to Claude; client_id/client_secret are generated once when running npm start, and the user pastes them manually into the Advanced settings field, exactly the pre-registered client credentials mechanism that Anthropic's own documentation recognizes as a valid way to bypass DCR. Gemini uses this same paste-in mechanism: it reuses Claude's exact confidential client (same Client ID/Secret), it doesn't self-register.

ChatGPT and Grok do the opposite: both self-register via POST /register (RFC 7591) as public clients (token_endpoint_auth_method: none), each with its own pre-allowlisted redirect URI (chatgpt.com, grok.com/connectors-oauth-exchange-code/). All three flows, whether pasted-in or self-registered, still have to clear the passphrase screen and PKCE before getting a token; this is where it differs from mcp-remote, where the entire OAuth 2.1 + PKCE + DCR flow happens only on the client side to talk to a different remote server, not as that server's own self-protection mechanism.

Two layers actually block unauthorized access: a 10-character passphrase at /authorize (~50 bits of entropy, no bare Approve button since /authorize is a public endpoint), plus PKCE S256 so access tokens only go to the client holding the matching code_verifier.

Execution mechanism: execFile, never a real shell

shell-mcp.js executes commands via execFile, never through a real shell, so command-chaining characters like semicolons, double-ampersands, and pipes are blocked at the execution layer rather than by string filtering. gatekeeper.js is the sole public gateway; the real mcp-hub only listens on loopback, so its unauthenticated admin API (/api/*) is never exposed to the internet.

Limitations

Warning

No refresh token rotation for Claude, acceptable since this is a confidential client (has a client_secret). Restarting npm start loses the entire granted session since tokens only live in RAM. No rate-limiting on /authorize, acceptable given the ~50-bit entropy of the passphrase makes brute-force infeasible. Gemini authenticates successfully but, in live testing on 2026-08-09, doesn't yet drive MCP tools reliably (connection is healthy, tool calls aren't trustworthy); Claude, ChatGPT, and Grok are the reliable clients today.