Backoffice is a remote MCP (Model Context Protocol) server that gives AI assistants like Claude command-line access on a remote machine via shell, patch_file, env_set, env_delete, memory_read, memory_write, memory_append, memory_patch, and get_instructions tools. Instead of relying on individual MCPs for every service, the AI can just use existing CLIs through Backoffice. It can also persist data on disk, run cron jobs, and do whatever other processing you might want to do on the data your AI assistant gives it.
It's designed to be deployed on an isolated, ephemeral machine (e.g. Railway). Clients connect over HTTP at /mcp, authenticate via OAuth (or a static bearer token), and can then run bash commands on the host.
- An MCP client (e.g. Claude.ai) connects to
/mcpand authenticates - The server registers MCP tools:
shell,patch_file,env_set,env_delete,memory_read,memory_write,memory_append,memory_patch, andget_instructions shellruns a bash command and returns stdout, stderr, and the exit code. Working directory and environment persist across calls.patch_fileapplies structured line patches to files.env_set/env_deletepersist environment variables (credentials, API keys) that are automatically injected into everyshellcall.memory_read/memory_write/memory_append/memory_patchgive the AI persistent memory across conversations (/data/MEMORY.md).get_instructionsreturns the full system instructions for the MCP server.- OAuth state is in-memory only β tokens are lost on restart/redeploy
When a user asks you to set up Backoffice, follow these steps:
bun installbrew install railwayThen tell the user to run railway login β this opens a browser and requires human interaction. Once logged in, deploy:
railway upRailway auto-sets RAILWAY_PUBLIC_DOMAIN. For other hosts (Fly.io, etc.), the user needs to set PUBLIC_BASE_URL to their public origin.
Tell the user to go to Claude.ai β Settings β Connectors β Add custom connector and set the MCP URL to https://<their-railway-domain>.up.railway.app/mcp. This is a manual UI step.
By default Railway spins up a fresh container on every deploy. To persist data (installed CLIs, config files, etc.) tell the user to add a Volume in their Railway service settings and mount it at /data. This path is required β the AI is instructed to use /data for memory and credentials.
- Runtime: Bun
- Language: TypeScript (strict mode)
- Framework: None β plain
Bun.serve()HTTP server - Key dependency:
@modelcontextprotocol/sdkfor MCP protocol + OAuth
src/
index.ts # Entry point β HTTP server, routing, session management
mcp.ts # MCP server factory, CORS helpers
INSTRUCTIONS.md # MCP instructions sent to the AI on connect
tools/ # MCP tool modules (exec, fs, patch, env, memory, instructions)
oauth/
index.ts # Re-exports
runtime.ts # OAuth endpoint handlers (authorize, token, register)
memoryProvider.ts # In-memory OAuth client/token store
fileProvider.ts # File-backed OAuth store (used when OAUTH_STATE_FILE is set)
eventStore.ts # In-memory SSE event store for resumable streams
bun install # Install dependencies
bun run start # Start the server (default: port 3000)
bun run typecheck # Type-check without emitting
bun run lint # Lint with ESLint
bun run lint:fix # Lint and auto-fix
bun run format # Format with Prettier
bun run format:check # Check formatting| Variable | Required | Description |
|---|---|---|
PORT |
No | Server port (default: 3000) |
PUBLIC_BASE_URL |
No | Public origin for OAuth metadata. Set when not on Railway. |
RAILWAY_PUBLIC_DOMAIN |
No | Auto-set by Railway. Used as public origin if PUBLIC_BASE_URL is unset. |
AUTH_PASSPHRASE |
No | Passphrase required on the OAuth authorize screen. Auto-generated on startup if not set. Printed to stdout. |
USE_MCP_TOKEN_AUTH |
No | Set to 1 to use static bearer token auth instead of OAuth. Useful for local dev or non-browser MCP clients. |
MCP_TOKEN |
No | Static bearer token (only used when USE_MCP_TOKEN_AUTH=1). Auto-generated to .mcp-token if unset. |
ALLOWED_REDIRECT_URI_DOMAINS |
No | Comma-separated list of domains OAuth clients are allowed to register redirect URIs for. Default: claude.ai. Set to claude.ai,localhost to also allow local clients. |
bun install
USE_MCP_TOKEN_AUTH=1 bun run startThe server prints the bearer token to stdout. Use it as Authorization: Bearer <token> against http://localhost:3000/mcp.
- No unnecessary comments β code should be self-explanatory
- Explicit
undefined/nullchecks instead of loose truthiness - Use
node:prefix for Node built-ins (node:crypto,node:fs) - Zod for all runtime input validation
- OAuth state is persisted to
/data/oauth-state.jsonby default; setOAUTH_RESET_ON_RESTART=1for in-memory only