Meridian Bank MCP Server
Meridian Bank MCP Server
A production-grade remote Model Context Protocol (MCP) server for a fictional digital bank ("Meridian"). It exposes the tools an AI ops/support copilot would use to service customers, spanning the full risk spectrum from read-only lookups to money movement and destructive admin actions.
It is a realistic customer artifact built to validate LLM Shield's guardrails + agentic IDP. It stands on its own as a real OAuth 2.1-protected MCP server.
Transport: Streamable HTTP (
POST /mcp), stateless.Auth: OAuth 2.1 — the server is a resource server (RFC 9728 protected-resource metadata, RFC 8707 audience-bound tokens) with a co-located minimal authorization server (RFC 8414 metadata, PKCE S256, authorization_code + refresh_token + client_credentials, Ed25519-signed JWTs). Conforms to the MCP 2025-11-25 auth spec.
Storage: PostgreSQL via Drizzle ORM, seeded with a realistic dataset.
SDK:
@modelcontextprotocol/sdkv1.x (TypeScript).
See docs/specs and docs/plans for the design and plan.
Tool catalog
Tool | Risk | Required scope |
| read (PII in output) |
|
| reversible write |
|
| money movement |
|
| money movement |
|
| destructive / admin |
|
Money movement is atomic and idempotent (row-locked source, unique idempotency_key,
no negative balances). Every mutation writes an audit_log row. Insufficient scope is
returned as an MCP tool error (isError), not an HTTP 403, because Streamable HTTP
multiplexes all tool calls over one POST /mcp.
Run locally
Prerequisites: Node 22+, Docker (for Postgres).
# 1. Postgres
docker run -d --name meridian-pg -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=meridian -p 5433:5432 postgres:16
# 2. Env
cp .env.example .env.local # edit if needed; SEED_ON_BOOT=true for demo data
# 3. Install + run (migrates, seeds, listens on :8080)
npm install
npm run dev # or: npm run build && npm startScripts: npm run build, npm start, npm run dev, npm test, npm run db:generate,
npm run db:migrate, npm run db:seed.
Dev operator credentials (seeded; NOT for production)
Username | Password | Role |
|
| admin |
|
| ops |
|
| support |
Try it
Machine-to-machine (client_credentials):
B=http://localhost:8080
TOKEN=$(curl -s -X POST $B/token \
-d grant_type=client_credentials \
-d client_id=meridian-copilot -d client_secret=dev-client-secret-change-me \
-d 'scope=banking:read payments:write' -d resource=$B/mcp | jq -r .access_token)
# discovery
curl -s $B/.well-known/oauth-protected-resource/mcp | jq
curl -s $B/.well-known/oauth-authorization-server | jq
# unauthorized → 401 + WWW-Authenticate
curl -i -X POST $B/mcp -H 'content-type: application/json' -d '{}'Full end-to-end smoke (MCP handshake, scope enforcement, transfer, idempotency):
OAUTH_CLIENT_SECRET=dev-client-secret-change-me npx tsx scripts/mcp-smoke.ts
npx tsx scripts/authcode-smoke.ts # browser-style authorization_code + PKCEMCP Inspector (interactive): point it at http://localhost:8080/mcp (Streamable HTTP).
It will run OAuth discovery; complete the operator login/consent with a dev credential above.
Deploy to Railway
The server is deploy-ready (Dockerfile + railway.json, /healthz health check).
railway login
railway init # or link an existing project
railway add -d postgres # provision managed Postgres
# In the service Variables, set:
# DATABASE_URL = ${{Postgres.DATABASE_URL}}
# PUBLIC_URL = https://<your-service>.up.railway.app (set after the domain exists)
# AUTH_SIGNING_KEY = <a stable Ed25519 private JWK> (so tokens survive restarts)
# COOKIE_SECRET, OAUTH_CLIENT_SECRET = <secrets>
# SEED_ON_BOOT = true (first deploy, to seed demo data)
railway up # build + deploy from the Dockerfile
railway domain # generate the public URLThe MCP endpoint is then https://<your-service>.up.railway.app/mcp. Set PUBLIC_URL to
that origin so the token audience and metadata are correct, then redeploy.
Note:
railway.com/mcpis Railway's own site; your server lives at your Railway-assigned domain (or a custom domain you attach), not onrailway.comitself.
Putting LLM Shield in front (later)
Meridian is designed to sit behind Shield's MCP proxy (a Policy Enforcement Point). When that phase starts:
Isolate Meridian on the network so Shield's proxy is the only path to it.
AUTH_ISSUERis env-driven, so Shield's OAuth authorization server can replace the co-located AS without code changes.
Configuration
See .env.example for all variables. Key ones: PUBLIC_URL (drives the
token audience PUBLIC_URL/mcp), AUTH_ISSUER, AUTH_SIGNING_KEY, DATABASE_URL,
OAUTH_CLIENT_*, SEED_ON_BOOT.