Personal dev tool that exposes a local shell and filesystem to any Model Context Protocol client. Built on the official TypeScript SDK (@modelcontextprotocol/sdk, v1.29+), with a custom WebSocket server transport added on top since the SDK only ships stdio and Streamable HTTP on the server side.
Runs anywhere Node.js and bash run: Linux, macOS, and Windows via WSL or Git Bash.
- llama.cpp Svelte WebUI (native MCP client)
- OpenAI ChatGPT (web, custom MCP server)
- Anthropic Claude (web, custom MCP server)
Four tools (bash_tool, view, create_file, str_replace), three
transports (stdio, Streamable HTTP, WebSocket), every server aggregated
behind one endpoint, zero framework.
This server gives the connected LLM whatever shell privileges the Node process itself has. Launch it as root, the LLM runs as root. Launch it under your daily user on your personal desktop, the LLM can read your SSH keys, browser data and home directory, and wipe it all in one command. Running it as root on your personal machine is a bad idea unless you are fully aware of what you are doing.
Recommended deployment targets, from cheapest to most isolated:
- Podman in userland, rootless, with a dedicated UID
- A throwaway VM you can wipe
- A dedicated Raspberry Pi sitting on a spare network segment
Bonus use case: cyber audit. Point a frontier model at this server inside a container and watch it try to break out. If it escapes, you learn something useful about both the model and your container configuration.
Never expose the HTTP or WebSocket transport on a port reachable from the public internet without all three of:
auth.modeset to something other thannoneinconfig.json- HTTPS in front (reverse proxy)
- an IP allowlist at the firewall level
Without these, assume you handed the host over.
Requires Node.js 18+ and bash in PATH.
git clone https://github.com/ServeurpersoCom/mcp.js
cd mcp.js
npm installconfig.json transport, credentials, and the identity served
core/ protocol, auth, and the three transports
core/transports/ stdio, Streamable HTTP, WebSocket
servers/bash/ the bash server: config.json, tools.json, lib/
main.js binds every server to one transport
Every server under servers/ is loaded at startup and their tools and
prompts are served together, so a client connects once and sees them
all. Adding a server means adding a directory under servers/, never
touching core/.
Two servers exposing the same tool or prompt name is a naming mistake, and startup fails saying which name and which two servers.
node main.js stdio # local MCP clients
node main.js streamable-http # HTTP, port 8083
node main.js websocket # WebSocket, port 8084main.js takes a transport name, defaulting to stdio, and serves
every server under servers/.
Default bind address is 0.0.0.0 so a llama.cpp instance on your
desktop can reach a sandbox running on a Raspberry Pi across the LAN
without any config tweak. If you run everything on a single machine,
set host to 127.0.0.1 in config.json. Do not expose the ports on
the public internet unless you know exactly what you are doing, and
even then, re-read the Warning section first.
Settings come from two files with disjoint roles. The root config.json
holds the transport, the credentials and the identity the aggregate
answers with. A servers/ holds that server's prompts
and its own settings block, nothing else.
Root:
{
"auth": {
"mode": "none",
"clientId": "",
"clientSecret": "",
"password": "",
"staticToken": ""
},
"streamable_http": { "host": "0.0.0.0", "port": 8083 },
"websocket": { "host": "0.0.0.0", "port": 8084 },
"mcp": {
"protocolVersion": "2025-06-18",
"serverName": "mcp-bash",
"serverVersion": "1.0.0",
"serverIcons": [
{ "src": "https://example.com/favicon-light.png", "mimeType": "image/png", "theme": "light" },
{ "src": "https://example.com/favicon-dark.png", "mimeType": "image/png", "theme": "dark" }
]
}
}The bash server:
{
"prompts": [],
"bash": {
"timeout": 300,
"outputLimitBytes": 4096,
"fileLimitBytes": 33554432
}
}One endpoint serves them all, so ports and identity belong to the root
and a server never carries them. prompts holds the templates the
server publishes over prompts/list and prompts/get, gathered from
every server. serverIcons is optional and rides verbatim into the
initialize response.
auth.mode selects the scheme for the HTTP and WebSocket transports.
Stdio is never authenticated, it speaks over local pipes only.
| Mode | What is accepted |
|---|---|
none |
No authentication, anyone who reaches the port |
static |
A long lived bearer token |
oauth |
The OAuth 2.1 flow only |
oauth+static |
Either of the two |
static suits clients that cannot run an interactive flow. Every
request carries Authorization: Bearer , matched in
constant time. The WebSocket transport only ever understands this
scheme, so it rejects every connection under oauth.
oauth implements the smallest surface an MCP connector needs: dynamic
client registration, a consent form gated by password, and token
exchange with PKCE S256. clientId, clientSecret and password must
all be set. Issued tokens live in memory, so a restart makes connectors
register and authorize again.
The server aborts at startup on an unknown mode, on static with an
empty staticToken, or on an OAuth mode missing one of its three
secrets.
Generate solid secrets:
openssl rand -hex 32Discovery follows RFC 9728: a request without a valid token gets a 401
carrying a WWW-Authenticate header that points at
/.well-known/oauth-protected-resource on the host the request came
in on. Serve that document, and the matching
/.well-known/oauth-authorization-server, from your reverse proxy.
| Name | Action |
|---|---|
bash_tool |
Run a bash command with timeout and output truncation |
view |
Read a file with line numbers and optional range, or list a directory |
create_file |
Create a file with auto mkdir and base64 safe writes |
str_replace |
Replace a unique string in a file, rejects ambiguous cases |
All four take a mandatory description argument so the LLM states its
intent on every call. Shows up in logs, useful for audit.
In the MCP settings panel, add the server:
- URL:
http://your-host:8083 - Header:
Authorization: Bearer
Declare the server in the app's MCP connector configuration with the same URL and Authorization header.
{
"mcpServers": {
"mcp": {
"command": "node",
"args": ["/path/to/mcp.js/main.js", "stdio"]
}
}
}Personal Swiss army knife. Evolves with my needs and what friends and users ask for. Issues and PRs welcome.
MIT.