Skip to content
ServeurpersoComPublic

About

Personal dev tool that exposes a local shell and filesystem to any Model Context Protocol client. Built on the official TypeScript SDK, with a custom WebSocket server transport.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

mcp.js

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.

Warning

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.mode set to something other than none in config.json
  • HTTPS in front (reverse proxy)
  • an IP allowlist at the firewall level

Without these, assume you handed the host over.

Install

Requires Node.js 18+ and bash in PATH.

git clone https://github.com/ServeurpersoCom/mcp.js
cd mcp.js
npm install

Layout

config.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.

Run

node main.js stdio              # local MCP clients
node main.js streamable-http    # HTTP, port 8083
node main.js websocket          # WebSocket, port 8084

main.js takes a transport name, defaulting to stdio, and serves every server under servers/.

Network exposure

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.

Config

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//config.json 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

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 32

Discovery 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.

Tools

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.

Client setup

llama.cpp Svelte WebUI (Streamable HTTP)

In the MCP settings panel, add the server:

  • URL: http://your-host:8083
  • Header: Authorization: Bearer

OpenAI ChatGPT / Anthropic Claude web (Streamable HTTP)

Declare the server in the app's MCP connector configuration with the same URL and Authorization header.

Claude Desktop (stdio)

{
	"mcpServers": {
		"mcp": {
			"command": "node",
			"args": ["/path/to/mcp.js/main.js", "stdio"]
		}
	}
}

Status

Personal Swiss army knife. Evolves with my needs and what friends and users ask for. Issues and PRs welcome.

License

MIT.

About

Personal dev tool that exposes a local shell and filesystem to any Model Context Protocol client. Built on the official TypeScript SDK, with a custom WebSocket server transport.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages