Claude.ai MCP Server (Self-hosted)

by leszczynskikarol

Not rated
GitHub

About

Self-hosted MCP server for Claude.ai. Give Claude direct access to your AWS, SSH, local shell, GitHub, PostgreSQL and PM2 — without Claude Code. Your keys never leave your machine.

Details

Author
leszczynskikarol
Categories
Developer Tools, Other, Infrastructure

Setup

Install Claude.ai MCP Server (Self-hosted) in your MCP client (Claude Desktop, Cursor, Windsurf, and others).

Repository: https://github.com/leszczynskikarol/mcp-server

Follow the installation instructions in the repository README, then restart your MCP client.

Self-hosted MCP server for Claude.ai. Give Claude direct access to your AWS, SSH, local shell, GitHub, PostgreSQL and PM2 — without Claude Code. Your keys never leave your machine.

Give Claude direct access to your AWS account, SSH into your servers, run shell commands on your laptop, query your databases, and manage PM2 processes — all from a Claude chat.No Claude Code subscription needed. Your keys never leave your machine.

All 10 tools live in your Claude.ai sidebar - no Desktop install, no separate client, just a custom connector.

A small Node.js MCP server you run on your own machine (laptop, desktop, VPS) and connect toClaude.ai(or any MCP client) as a Custom Connector. It exposes a configurable set of tools that let Claude do real work on your infrastructure:

- Run AWS CLI commands using your local profile
- SSH into your servers using your.pemkeys
- Execute shell commands on your local machine
- Write files directly on your local disk (full UTF-8, no shell quoting)
- Hit the GitHub REST API with your Personal Access Token
- Query PostgreSQL databases on remote hosts via SSH
- Inspect PM2 processes on remote servers
- Iterate over very long documents (book editing) that exceed the context window

Claude.ai ──HTTPS──► nginx + cert ──HTTP──► frps ──tunnel──► frpc + node (cloud) on a VPS :8080 (vhost) (your PC) │ ┌───────────────┬───────────────┬─────────┼─────────┐ ▼ ▼ ▼ ▼ ▼ AWS CLI ssh -i .pem cmd.exe psql via pm2 list (local) user@host git/npm SSH (remote)

SSH keys, GitHub PAT and AWS credentialsnever leave your local machine. The tunnel only carries MCP requests and their results.

The MCP ecosystem mostly does two things today:
- MCP servers as npm packagesthat run viastdioand require Claude Desktop.
- Hosted MCP servicesbehind someone else's auth and API limits.

This project is thethird option: your own MCP server, your keys, your servers, accessible from web Claude.ai (where you already work). It's a single ~1000-lineserver.jsfile that you can read end-to-end in 20 minutes and extend in 5.

- ✅No Claude Code subscription needed— works with regular Claude.ai (web)
- ✅OAuth 2.1 with PKCE— proper Claude.ai integration, not a hacky workaround
- ✅Your keys stay yours— SSH keys, GitHub PATs, AWS creds never leave your machine
- ✅Self-hostable— Windows, Linux, Mac, anything that runs Node 18+
- ✅Configurable via JSON— add a new server or new key without touching code
- ✅One file to read— no framework magic, no hidden config
- ✅Hardened OAuth— PKCE S256,client_secretenforcement, refresh token rotation, RFC 7009 revocation, dynamic IP allowlist with auto-enroll, anti-clickjacking

Real examples from a real Claude.ai chat using this MCP server.

"SSH into my matury server and show me: disk usage (df -h), memory usage (free -h), and uptime. Use a single command and present the output nicely."

Claude composes a single SSH command, parses the multi-section output, and renders disk, memory and uptime as a clean snapshot with key numbers highlighted.

"Using AWS CLI, list all my EC2 instances in eu-central-1 with their IDs, types, and state. Format the result as a clean table."

Claude callsaws_cliwith adescribe-instances --query ...filter, then parses the JSON and renders it as a markdown table with running/stopped status indicators.

"Run a SQL query on my 'panel' PostgreSQL database to count the total users, then on database 'smart_edu' count rows in the users table, and tell me how my system is doing."

Two tools work together here:ssh_execto enumerate databases when the first guess misses, thenpostgres_queryagainst the right one.

Claude lists every database on the host, identifies the ones with auserstable, and runsCOUNT()queries in parallel.

- Node.js 18+(tested on 22, 24, 25)
- SSH.pemkeyslocally (for hosts you want to control)
- AWS CLI configured locally(aws configure) — only if you use theaws_clitool
- A public HTTPS endpoint— only if you want to expose this to Claude.ai

git clone https://github.com/LeszczynskiKarol/mcp-server.git cd mcp-server npm install cp .env.example .env # fill in MCP_PASS and MCP_BASE_URL cp hosts.example.json hosts.json # add your servers node server.js
Loaded N hosts and M keys from ./hosts.json MCP server: my-mcp-server Port: 4500 Static IP allowlist: (none) Auto-enroll: enabled (TTL 30 days) Trust proxy: false MCP listening on :4500

That's it for local. To use this from Claude.ai (web), you need to expose it over HTTPS — seeExposing publiclybelow.

# REQUIRED MCP_USER=admin MCP_PASS=<long password, min 20 chars> MCP_BASE_URL=https://your-domain.com # OPTIONAL — GitHub integration GITHUB_TOKEN=github_pat_xxxxxxxxxxxxxxxx GITHUB_OWNER=YourGitHubUsername # OPTIONAL — server tuning PORT=4500 TOKEN_TTL_SECONDS=2592000 # 30 days AUTH_CODE_TTL_SECONDS=600 # 10 minutes CLIENT_TTL_SECONDS=7776000 # 90 days (unused-client cleanup) EXEC_BUFFER_MB=10 EXEC_TIMEOUT_SECONDS=120 # per-command timeout MCP_SERVER_NAME=my-mcp-server HOSTS_CONFIG=./hosts.json OAUTH_STATE_FILE=./oauth-state.json # OPTIONAL — IP allowlist (security) # Comma-separated static IPs/CIDRs that are always allowed. # Leave empty if you only want auto-enroll via OAuth login. MCP_ALLOWED_IPS= # Trust X-Forwarded-For — use "loopback" when behind FRP/nginx on the same box. # Other valid values: comma-separated list of trusted proxy IPs/CIDRs, "false" # (default), or "true" (rejected in production — would let any client spoof XFF). MCP_TRUST_PROXY=loopback # Auto-enroll the requesting /24 subnet to allowlist after a successful OAuth login MCP_AUTO_ENROLL=true # How long an auto-enrolled subnet stays on the allowlist (default 30 days) MCP_ENROLL_TTL_SECONDS=2592000
{ "hosts": { "production": { "ip": "1.2.3.4", "user": "ubuntu", "key": "main", "description": "Main production server" }, "staging": { "ip": "5.6.7.8", "user": "ubuntu", "key": "main", "description": "Staging environment" } }, "keys": { "main": "/path/to/main.pem" } }

- Absolute paths:D:/keys/server.pemorD:\\keys\\server.pem
- Tilde expansion:~/keys/server.pem(resolved to$HOME/%USERPROFILE%)
- Forward slashes work on Windows too

Claude.ai requires HTTPS. The recommended setup usesFRP(Fast Reverse Proxy) +nginx+Let's Encrypton a small VPS.

mcp.your-domain.com A <VPS_IP> TTL 300

/etc/nginx/sites-available/mcp.your-domain.com:

server { server_name mcp.your-domain.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # SSE / long-lived MCP connections proxy_http_version 1.1; proxy_set_header Connection ""; proxy_buffering off; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } listen 80; }
sudo ln -s /etc/nginx/sites-available/mcp.your-domain.com /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx sudo certbot --nginx -d mcp.your-domain.com
bindPort = 7000 vhostHTTPPort = 8080 auth.method = "token" auth.token = "<shared token>"

4. FRP client (frpc) on your local machine

serverAddr = "<VPS_IP>" serverPort = 7000 auth.method = "token" auth.token = "<shared token from frps>" [[proxies]] name = "mcp" type = "http" localPort = 4500 customDomains = ["mcp.your-domain.com"]

- OpenSettings → Connectors → Add custom connector
- URL:https://mcp.your-domain.com/mcp(with/mcpsuffix!)
- OAuth Client ID/Secret:leave empty
- ClickConnect→ a login form appears → enterMCP_USERandMCP_PASSfrom your.env
- In a chat:+→ Connectors → toggle this MCP on →start a new conversation(tools attach at chat start)

The first request from a new IP will trigger an OAuth re-login, which adds your/24subnet to the allowlist for 30 days. This is intentional — seeSecurity.

Even with the right tools installed, Claude will sometimes waste tokens on common failure modes: re-reading files after writing them, copying files into the sandbox "to edit them", lying that a file is on disk when only an artifact was created.

CLAUDE_PREFERENCES.mdis a curated set of user preferences that closes those holes. Paste them intoSettings -> Profile -> Personal preferencesinClaude.ai. They're battle-tested against the exact failure modes that prompted writing some of the tools in this server (especiallywrite_file).

- File-tool hierarchy: alwayswrite_filefor new files, PowerShell for surgical edits, never base64-chunks through cmd.exe
- "Trust the write" -- don't re-read a file just to confirm it saved
- Sandbox isolation -- the bash sandbox has no access to your local disk; don't bridge it via public file hosts (uguu.se, transfer.sh, etc.)
- Artifacts vs. MCP file writes -- "Created a file" tiles in the chat UI are NOT saved to your disk
- Raw content rule --write_filecontent must be raw, not JSON-escaped
- Anti-loop limits -- stop after 3 failures on the same problem
- Non-ASCII character handling on Windows (cmd.execode page gotcha)

The recommended autostart on Windows usesTask Scheduler+ a small.batwith a restart loop. PM2 used to be the recommendation but is currently incompatible with Node 25's named-pipe handling (EPERM \\.\pipe\rpc.sock), so the project has switched to plain Task Scheduler.

After.envandhosts.jsonare in place, run as administrator:

This is a small batch wrapper aroundinstall-task.ps1. The PowerShell script:
- Generatesstart-mcp-hidden.vbs(so thecmd.exewindow stays hidden).
- Generatesmcp-task.generated.xmlwith%USERDOMAIN%\%USERNAME%filled in — nothing hard-coded.
- Registers a Task Scheduler entry namedMCP Serverthat runs at every logon withHighestAvailableprivilege.

The action of the task iswscript.exe "...\start-mcp-hidden.vbs", which silently launchesstart-mcp.bat. That batch keeps node alive with a restart loop:

@echo off cd /d D:\mcp-server if not exist logs mkdir logs :loop node server.js >> logs\mcp.log 2>&1 echo [%date% %time%] node exited, restarting in 5s >> logs\mcp.log timeout /t 5 /nobreak >nul goto loop
schtasks /run /tn "MCP Server" :: start now schtasks /query /tn "MCP Server" /v /fo LIST :: status schtasks /delete /tn "MCP Server" /f :: uninstall tasklist | findstr node.exe :: check that node is alive type D:\mcp-server\logs\mcp.log :: read the log

The:loopinstart-mcp.batwill restart node within 5 seconds.

The tunnel is separate from the MCP server. The simplest setup is astart-mcp.bat(different file in a different directory) that only launchesfrpc:

@echo off cd /d C:\Users\YourUser\frp\frp_0.61.1_windows_amd64 start "FRP tunnel mcp" /min frpc.exe -c frpc-mcp.toml

Add it to Task Scheduler the same way (At logon, highest privileges), or shove it inshell:startup.

If you just want to run node manually for development:

{ "hosts": { "production": {}, "new-server": { "ip": "5.6.7.8", "user": "ubuntu", "key": "main", "description": "New server" } } }

The server readshosts.jsonat boot, so kill node (taskkill /F /IM node.exe /T) and the restart loop will pick up the new config in 5 seconds.In Claude.ai, disconnect and reconnect the connectorso it sees the new host in thehostparameter dropdown.

{ "keys": { "main": "/path/to/main.pem", "client-x": "~/keys/client-x.pem" } }
server.tool( "your_tool_name", "Clear description of when Claude should use this tool", { param: z.string().describe("what this parameter does"), }, async ({ param }) => { // your logic here return { content: [{ type: "text", text: "result" }] }; }, );

After saving,taskkill /F /IM node.exe /Tto let the restart loop pick up the change.Disconnect and reconnect the connector in Claude.aito see the new tool.

For a full list of what the server enforces, seeSECURITY.md. The headline features:

- OAuth 2.1 with PKCE(S256 only) and Dynamic Client Registration
- client_idmatchon/oauth/token— code can only be redeemed by its issuing client (RFC 6749 §4.1.3)
- client_secretenforcementon/oauth/tokenand/oauth/revoke— secrets issued at registration are actually checked
- Refresh token validation and rotation— the old refresh token is invalidated on every use, a fresh pair is issued, and only the owning client can rotate
- Persistent OAuth stateinoauth-state.json— node restart no longer forces re-authorization in Claude.ai
- Token revocation endpointat/oauth/revoke(RFC 7009)
- Dynamic IP allowlistwith auto-enroll — the/24subnet of every successful OAuth login is allowlisted for 30 days. Unknown IPs get 401 +WWW-Authenticate, so Claude.ai silently re-runs OAuth and the new subnet is added. Static IPs/CIDRs can be configured viaMCP_ALLOWED_IPS
- Anti-clickjackingon the OAuth login form viahelmet:X-Frame-Options: DENYandContent-Security-Policy: frame-ancestors 'none'
- Rate limiton/oauth/: 30 requests / 15 minutes / IP
- Prototype pollution preventioninbook_note(__proto__,constructor,prototypekeys rejected)
- Token values redacted in logs— onlyclient_idand the first 8 chars of any token are written

For AI assistants (Claude, Cursor, Cline, Aider, etc.)

This repo includesCLAUDE.mdwith critical instructions for any AI assistant working on this codebase.Read it first.It documents:

- The single correct way to edit files on the Windows host (write_filetool)
- Anti-patterns that have wasted real tokens (sandbox-as-bridge, base64 chunks, uploading files to public hosts to transfer them back to the owner's disk)
- Anti-loop rules: 3 failures → stop and propose alternatives
- Server restart workflow that breaks MCP sessions

If you're a human deploying this MCP server for your own Claude.ai account:
- Copy the rules fromCLAUDE.mdinto Settings → Profile → Personal preferences in Claude.ai. That's the only mechanism in the web UI that loads instructions at session start.
- (Optional) CopyCLAUDE.mdinto every repo you'll edit via this server. AI tools running outside claude.ai (Claude Code, Cursor, Cline) will read it automatically.
- Future Anthropic Agent Skills support in claude.ai web may eventually load~/.claude/skills/
.mdautomatically. Until then, preferences + per-repo CLAUDE.md is the working pattern.

Behind the FRP + nginx topology described above:

MCP_TRUST_PROXY=loopback # frpc connects to node over 127.0.0.1 MCP_AUTO_ENROLL=true # let Claude.ai's egress IP enroll itself on first login MCP_ALLOWED_IPS= # leave empty unless you have a fixed office/VPN IP MCP_PASS=<random 20+ chars>

MCP_TRUST_PROXY=trueis permissive and will berejectedbyexpress-rate-limitbecause it would allow any client to spoofX-Forwarded-Forand bypass rate limiting. Useloopback(or a comma-separated list of trusted proxy IPs) instead.
- Read-only AWS profileforaws_cliif you don't need mutations — create dedicated IAM credentials withReadOnlyAccess
- Command whitelistforaws_cli/local_exec/ssh_execif you trust Claude less than the AWS console
- Pin SSH host keys— removeStrictHostKeyChecking=nofromssh_execand pre-populate~/.ssh/known_hosts
- Audit log to a file— Task Scheduler setup writes tologs/mcp.log; persistent rotation is up to you
- Per-tool ACL— restrict which client (e.g. work vs personal Claude.ai account) can call which tool with a custom dispatcher in front ofserver.tool
- Check.gitignore— must include.env,hosts.json,oauth-state.json,logs/,mcp-task.generated.xml,start-mcp-hidden.vbs

mcp-server/ ├── server.js # MCP server (Express + StreamableHTTP + OAuth) ├── package.json ├── .env # (gitignored) secrets ├── .env.example ├── hosts.json # (gitignored) server list ├── hosts.example.json ├── oauth-state.json # (gitignored) persisted OAuth state ├── .gitignore ├── README.md # English (this file) ├── README.pl.md # Polish translation ├── LICENSE ├── CHANGELOG.md ├── CONTRIBUTING.md ├── SECURITY.md ├── setup.bat # quick start for Windows ├── setup.sh # quick start for Linux/Mac ├── start-mcp.bat # node restart loop (Windows autostart) ├── install-task.bat # wrapper that runs install-task.ps1 ├── install-task.ps1 # registers the "MCP Server" scheduled task └── logs/ # (gitignored) mcp.log

Ideas for future versions (PRs welcome):

- Persistent OAuth state so node restart doesn't kill connections(done in 1.1.0)
- Rate limiting on/oauth/*(done in 1.1.0)
- Token revocation endpoint(done in 1.1.0)
- Audit log to file (audit.logwith rotation)
- Per-tool ACL (which client can use which tool)
- Read-only mode for AWS / SSH (whitelist of safe commands)
- Docker Compose for one-command deployment
- More tools: S3 upload, CloudWatch logs, Sentry, Stripe
- Multi-user authentication (OIDC integration: Google / GitHub login)

Pull requests welcome! SeeCONTRIBUTING.mdfor guidelines.

For questions or ideas, useGitHub Discussionsinstead of issues.

This is a web browser that enables your coding agent, such as Claude Code, to visit websites on your behalf and assist you in identifying bugs or creating UI test cases.

🤖 ⚡️ MCP server ( MacOS) — control remote desktops via VNC

Manage your Bedrock server with natural language commands using the Bedrock Server Manager API.

Connect to, configure, and monitor Cisco network devices like routers and switches via SSH.

Production-ready MCP server for AI-driven console automation and monitoring. 40+ tools for session management, SSH, testing, and background jobs.

Give your AI agents access to production without the risks of sharing SSH keys.

An intelligent MCP server that automates the reproduction of GitHub issues for AWS CDK projects.

No reviews yet — be the first

Sign in to leave a review

Use Google, GitHub, or an email account so ratings stay tied to real people.

Email sign in

No reviews posted yet.