Self-hosted MCP server for Claude
Allows running AWS CLI commands using a local AWS profile, enabling management of AWS resources such as EC2 instances, S3 buckets, and more.
Allows making REST API requests to GitHub using a Personal Access Token, enabling repository management, issue tracking, and other GitHub operations.
Allows inspecting PM2 process manager status and logs on remote servers, enabling monitoring of Node.js applications.
Allows running psql queries on remote PostgreSQL databases via SSH, enabling database inspection and querying.
mcp-server
Self-hosted MCP server for Claude.ai
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.
What is this?
A small Node.js MCP server you run on your own machine (laptop, desktop, VPS) and connect to Claude.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
.pemkeysExecute 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
Architecture:
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 credentials never leave your local machine. The tunnel only carries MCP requests and their results.
Why self-host this?
The MCP ecosystem mostly does two things today:
MCP servers as npm packages that run via
stdioand require Claude Desktop.Hosted MCP services behind someone else's auth and API limits.
This project is the third option: your own MCP server, your keys, your servers, accessible from web Claude.ai (where you already work). It's a single ~1000-line server.js file that you can read end-to-end in 20 minutes and extend in 5.
Killer features:
✅ 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
Tools
Tool | What it does |
| Run any |
| SSH into any host using a key defined in |
| Run any shell command on the local machine ( |
| Write text content to a local file (overwrite or append). Full UTF-8, no shell quoting issues — preferred over |
| Make REST API requests to GitHub using your Personal Access Token |
| Run |
| Show |
| Stream a file from a remote host to the local filesystem (uses the persistent SSH pool). Essential when working from Claude.ai web — the cloud sandbox has no native scp |
| Stream a local file to a remote host. Optional POSIX mode flag (e.g. |
| Split a large text file into ~3000-word chunks |
| Read one chunk from a directory created by |
| Manage JSON notes for iterative work on long documents |
See it in action
Real examples from a real Claude.ai chat using this MCP server.
Server health snapshot over SSH
"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.
List EC2 instances in any region
"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 calls aws_cli with a describe-instances --query ... filter, then parses the JSON and renders it as a markdown table with running/stopped status indicators.
Query PostgreSQL databases over SSH
"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_exec to enumerate databases when the first guess misses, then postgres_query against the right one.

Claude lists every database on the host, identifies the ones with a users table, and runs COUNT(*) queries in parallel.
Requirements
Node.js 18+ (tested on 22, 24, 25)
SSH
.pemkeys locally (for hosts you want to control)AWS CLI configured locally (
aws configure) — only if you use theaws_clitoolA public HTTPS endpoint — only if you want to expose this to Claude.ai
Quick start
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.jsYou should see:
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 :4500That's it for local. To use this from Claude.ai (web), you need to expose it over HTTPS — see Exposing publicly below.
Configuration
.env (secrets — never commit)
# 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=2592000hosts.json (server list — never commit)
{
"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"
}
}Key paths can use:
Absolute paths:
D:/keys/server.pemorD:\\keys\\server.pemTilde expansion:
~/keys/server.pem(resolved to$HOME/%USERPROFILE%)Forward slashes work on Windows too
Exposing publicly (Claude.ai)
Claude.ai requires HTTPS. The recommended setup uses FRP (Fast Reverse Proxy) + nginx + Let's Encrypt on a small VPS.
1. DNS record
mcp.your-domain.com A <VPS_IP> TTL 3002. nginx vhost on VPS
/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.com3. FRP server (frps) on VPS
/etc/frp/frps.toml:
bindPort = 7000
vhostHTTPPort = 8080
auth.method = "token"
auth.token = "<shared token>"4. FRP client (frpc) on your local machine
frpc-mcp.toml:
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"]5. Add the connector in Claude.ai
Open Settings → Connectors → Add custom connector
URL:
https://mcp.your-domain.com/mcp(with/mcpsuffix!)OAuth Client ID/Secret: leave empty
Click Connect → a login form appears → enter
MCP_USERandMCP_PASSfrom your.envIn 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 /24 subnet to the allowlist for 30 days. This is intentional — see Security.
Recommended Claude.ai preferences
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.md is a curated set of user preferences that closes those holes. Paste them into Settings -> Profile -> Personal preferences in Claude.ai. They're battle-tested against the exact failure modes that prompted writing some of the tools in this server (especially write_file).
Covers, among other things:
File-tool hierarchy: always
write_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-escapedAnti-loop limits -- stop after 3 failures on the same problem
Non-ASCII character handling on Windows (
cmd.execode page gotcha)
Running on Windows
The recommended autostart on Windows uses Task Scheduler + a small .bat with 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.
One-shot install
After .env and hosts.json are in place, run as administrator:
install-task.batThis is a small batch wrapper around install-task.ps1. The PowerShell script:
Generates
start-mcp-hidden.vbs(so thecmd.exewindow stays hidden).Generates
mcp-task.generated.xmlwith%USERDOMAIN%\%USERNAME%filled in — nothing hard-coded.Registers a Task Scheduler entry named MCP Server that runs at every logon with
HighestAvailableprivilege.
The action of the task is wscript.exe "...\start-mcp-hidden.vbs", which silently launches start-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 loopUseful commands:
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 logTo restart after editing server.js:
taskkill /F /IM node.exe /TThe :loop in start-mcp.bat will restart node within 5 seconds.
FRP tunnel autostart
The tunnel is separate from the MCP server. The simplest setup is a start-mcp.bat (different file in a different directory) that only launches frpc:
@echo off
cd /d C:\Users\YourUser\frp\frp_0.61.1_windows_amd64
start "FRP tunnel mcp" /min frpc.exe -c frpc-mcp.tomlAdd it to Task Scheduler the same way (At logon, highest privileges), or shove it in shell:startup.
Without autostart
If you just want to run node manually for development:
cd /d D:\mcp-server
node server.jsCtrl+C to stop.
Extending
Adding a new host
Edit hosts.json:
{
"hosts": {
"production": {},
"new-server": {
"ip": "5.6.7.8",
"user": "ubuntu",
"key": "main",
"description": "New server"
}
}
}The server reads hosts.json at 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 connector so it sees the new host in the host parameter dropdown.
Adding a new SSH key
{
"keys": {
"main": "/path/to/main.pem",
"client-x": "~/keys/client-x.pem"
}
}Adding a new tool
In server.js:
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 /T to let the restart loop pick up the change. Disconnect and reconnect the connector in Claude.ai to see the new tool.
Security
For a full list of what the server enforces, see SECURITY.md. The headline features:
OAuth 2.1 with PKCE (S256 only) and Dynamic Client Registration
client_idmatch on/oauth/token— code can only be redeemed by its issuing client (RFC 6749 §4.1.3)client_secretenforcement on/oauth/tokenand/oauth/revoke— secrets issued at registration are actually checkedRefresh 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 state in
oauth-state.json— node restart no longer forces re-authorization in Claude.aiToken revocation endpoint at
/oauth/revoke(RFC 7009)Dynamic IP allowlist with 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_IPSAnti-clickjacking on the OAuth login form via
helmet:X-Frame-Options: DENYandContent-Security-Policy: frame-ancestors 'none'Rate limit on
/oauth/*: 30 requests / 15 minutes / IPPrototype pollution prevention in
book_note(__proto__,constructor,prototypekeys rejected)Token values redacted in logs — only
client_idand the first 8 chars of any token are written
For AI assistants (Claude, Cursor, Cline, Aider, etc.)
This repo includes CLAUDE.md with 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 from
CLAUDE.mdinto Settings → Profile → Personal preferences in Claude.ai. That's the only mechanism in the web UI that loads instructions at session start.(Optional) Copy
CLAUDE.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.
Recommended deployment posture
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=true is permissive and will be rejected by express-rate-limit because it would allow any client to spoof X-Forwarded-For and bypass rate limiting. Use loopback (or a comma-separated list of trusted proxy IPs) instead.
Hardening you can apply on top
Read-only AWS profile for
aws_cliif you don't need mutations — create dedicated IAM credentials withReadOnlyAccessCommand whitelist for
aws_cli/local_exec/ssh_execif you trust Claude less than the AWS consolePin SSH host keys — remove
StrictHostKeyChecking=nofromssh_execand pre-populate~/.ssh/known_hostsAudit log to a file — Task Scheduler setup writes to
logs/mcp.log; persistent rotation is up to youPer-tool ACL — restrict which client (e.g. work vs personal Claude.ai account) can call which tool with a custom dispatcher in front of
server.toolCheck
.gitignore— must include.env,hosts.json,oauth-state.json,logs/,mcp-task.generated.xml,start-mcp-hidden.vbs
Troubleshooting
Symptom | Cause | Fix |
| URL without | Use |
| Node not running, FRP tunnel down, or your IP not on the allowlist | Check |
| No vhost for the subdomain | Create vhost + run certbot |
| frpc not running or node crashed | Check |
| New IP, not yet enrolled | Should self-heal — Claude.ai will re-run OAuth and add the |
|
| Change |
| Another node running on port 4500 |
|
| Missing |
|
| Missing |
|
| Key not defined in | Add it to the |
| Wrong user for the host | In |
| NVM on remote host — non-interactive shell doesn't load nvm.sh | Tool handles this automatically (base64 + nvm.sh sourcing). Make sure NVM is in |
Claude sees connector but no tools | Toggle is off, or old chat session |
|
Changed tools, Claude shows old ones | MCP schema cache | Settings → Connectors → Disconnect → Connect |
Task Scheduler shows | The task itself is | Wait 5 seconds (restart loop), or run |
Project files
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.logCompatibility
MCP Client | Status |
Claude.ai (web) | ✅ Primary target — fully tested |
Claude Desktop | ✅ Tested - same connector URL works (custom connectors with OAuth) |
Custom MCP client (with OAuth 2.1) | ✅ Standard implementation |
Custom MCP client (without OAuth) | ❌ Requires modification — OAuth is mandatory in current code |
Roadmap
Ideas for future versions (PRs welcome):
Persistent OAuth state so node restart doesn't kill connections(done in 1.1.0)Rate limiting on(done in 1.1.0)/oauth/*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)
License
MIT © 2026 Karol Leszczynski
Contributing
Pull requests welcome! See CONTRIBUTING.md for guidelines.
For questions or ideas, use GitHub Discussions instead of issues.
🇵🇱 Polski README: README.pl.md
This server cannot be installed
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/LeszczynskiKarol/mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server