Custom MCP Connector for the Posts Database
Julien Béranger
+ Claude Sonnet 5
Why
The posts table used to live on Neon, reachable through Neon's own hosted MCP server. It's now on a self-hosted Postgres instance on the VPS at db.w3hc.org, driven day to day by a custom pnpm posts CLI.
Scheduled Claude sessions run inside Anthropic's cloud sandboxes, and those sandboxes only have outbound HTTPS through a managed proxy — no raw TCP. Postgres's wire protocol is raw TCP on port 5432, so a direct connection string, even a correct one with valid credentials, never reaches the box. This has nothing to do with firewall rules or pg_hba config on the VPS side; the block is on the client's egress path.
The fix is a small server that already sits where it can reach Postgres over TCP, and that itself speaks HTTPS outward. That's exactly the role a remote MCP server plays — it's the same shape as Neon's own hosted connector, just pointed at this VPS instead.
Scope
Three tools, mirroring what the pnpm posts CLI already does, not a generic SQL-execution tool:
| Tool | Purpose | Maps to |
|---|---|---|
posts_latest | Fetch the most recent post whose slug matches a prefix (e.g. eth-daily-) | pnpm posts list (filtered) |
posts_upsert | Insert or update a post by slug | pnpm posts add |
posts_list | List recent posts, optionally filtered by slug prefix | pnpm posts list |
Deliberately leaving out raw SQL execution and delete. A narrow, purpose-built surface is safer to expose over the internet than a general query tool, and it's enough for the daily-post workflow this is built for. Delete can stay CLI-only if it's rarely used.
Architecture
Claude session (cloud sandbox, HTTPS-only egress)
│ HTTPS + bearer token
▼
Reverse proxy on the VPS (existing TLS termination)
│ localhost
▼
Node.js MCP server (Streamable HTTP transport, stateless)
│ TCP, SSL verify-full, existing role
▼
Postgres on db.w3hc.org:5432The MCP server is a small Node.js process, built with the MCP TypeScript SDK, using the Streamable HTTP transport — the recommended transport for remote servers (stateless JSON, no long-lived session to manage, easier to run behind a reverse proxy than SSE). It reuses the same connection approach as the existing CLI: postgresql://website@db.w3hc.org:5432/website?sslmode=verify-full&sslrootcert=system, ideally through node-postgres with a small connection pool, and the same website role, so it inherits the CONNECTION LIMIT and pg_hba restrictions already in place — no new database privileges to design.
It sits behind whatever already terminates TLS for db.w3hc.org today, on its own subdomain or path (e.g. mcp.w3hc.org or db.w3hc.org/mcp), and only ever listens on localhost — the reverse proxy is the only thing exposed publicly.
Authentication
A single bearer token, checked with a constant-time comparison (crypto.timingSafeEqual in Node), passed as Authorization: Bearer <token>. No OAuth flow needed for a single-consumer server like this. The token is generated once, stored on the VPS as an environment variable for the server process, and stored on the Claude side as the connector's credential when it's added.
Resource footprint
This is the part worth being concrete about, since it's the open question:
- Idle: a Node process holding a small Postgres connection pool (2-3 connections) sits at roughly 40-80 MB of resident memory and effectively zero CPU. That's comparable to a small
systemdservice, not a database or a build process. - Under load: call volume here is a handful of requests a day — one scheduled run, occasional interactive use. Each request is a fast, indexed read or a single-row upsert; CPU cost per call is negligible, on the order of milliseconds.
- Comparison: this is lighter than Postgres itself, which is already running on the same box, and far lighter than the nightly
pg_dumpalland restic backup jobs mentioned in the VPS write-up.
In short: no, this isn't resource-consuming at the scale this is used. The realistic cost isn't compute, it's the setup and maintenance — running one more systemd unit, one more thing to keep patched, one more secret to rotate. For a VPS already running Postgres and serving the blog, one more small Node process is well within existing headroom.
Build steps
- Scaffold:
npm init, add@modelcontextprotocol/sdk, Zod for input schemas,pgfor the database client. Follow the MCP quickstart for the base server shape. - Port the CLI logic: the
init/add/listfunctions frompnpm postsbecome the implementation behindposts_upsertandposts_list; addposts_latestas a thin query on top (WHERE slug LIKE $1 ORDER BY date DESC LIMIT 1). - Wire up Streamable HTTP: stateless mode, bearer-token middleware in front of the MCP handler.
- Test locally with MCP Inspector (
npx @modelcontextprotocol/inspector) againstlocalhostbefore it's anywhere near the internet. - Deploy:
systemdunit (Restart=on-failure, runs as an unprivileged user), reverse-proxied with TLS, firewalled to only accept local connections from the proxy. - Register the connector: once it's live over HTTPS, add it as a custom MCP connector, using the server's URL and the bearer token as its credential.
- Swap the scheduled task: point the Ethereum Daily job's read/upsert calls at the new tools instead of the retired Neon ones.
Open questions
- Subdomain vs. path for the server's public URL — whichever is less friction alongside the existing reverse-proxy config.
- Whether
posts_listneeds pagination now or can stay a fixed small limit until the table grows enough to matter. - Token rotation cadence — a manual rotation every few months is probably enough at this call volume.