MCP Gateway
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MCP GatewayShow the last 5 anomaly alerts for plant A12"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Gateway — Phase 1 (KLIP, read-only)
A single secure service that lets authorized Energi-Up staff query KLIP in natural language through Claude, without opening the application. Strictly read-only.
Deployment target (PRD Q3, now closed): <gateway-hostname> -> <gateway-public-ip>
(ECS-MCP, ap-southeast-5). Note the hostname is mcp-gw, not the mcp.example.com
the v0.9 documents assumed; the PRD/TSD should be updated to match.
Documents: PRD v0.9 · TSD v0.9 · Implementation guide · Design review · Deployment runbook
1. Pinned versions (T-1)
The MCP spec revision and SDK version are pinned here. Where the SDK's own docs disagree with the design documents, the SDK wins — it defines the exact API.
Component | Pinned | Notes |
| 1.30.0 (exact, no caret) | Published 27 Jul 2026. Supplies the OAuth AS, the Streamable HTTP transport and |
MCP protocol revision | 2025-11-25 implemented; wire shape compatible with | Revision |
Node.js | 22 LTS ( | |
TypeScript | 5.9.3, | |
express | 5.2.1 | Raised from the TSD's 4.x: the SDK depends on |
zod | 4.4.3 | Raised from the TSD's 3.x: the SDK's type definitions target zod 4. Note |
jose | 6.2.9 | RS256 gateway tokens. |
| 2.1.0 | Substituted for |
axios | 1.19.0 | KLIP client, behind the method guard. |
pg | 8.23.0 | |
PostgreSQL | 16 (container) | |
nginx | nginx.org mainline | Config in |
Related MCP server: Snowflake MCP Server
2. Layout
src/
core/ config, logger, db, audit, cache, rateLimit, semaphore, migrate, errors
adapters/klip/ routes(APPENDIX A) · fields(APPENDIX A) · client(guard) · session · paginate · normalize
tools/klip/ 9 tool definitions + shared parameter plumbing
mcp/ server, envelope, runner
auth/ keys, hub(OIDC RP), users, clients, tokens, provider, loginPage
http/ app, consent(Hub + break-glass), health, origin, clientIp
migrations/ idempotent SQL (001 schema, 002 Hub OIDC)
deploy/ nginx config, backup sidecar
test/ 120 tests + mock KLIP and mock Hub fixturesLayering rule (T-3): tools → adapters → core. Nothing imports http/ except the
entrypoint. All business normalization lives in adapters/klip/normalize.ts, which
imports nothing, so it is unit-testable in isolation.
3. Authentication — Downstream Hub OIDC
Pilot users sign in with Downstream Hub (OIDC). The gateway remains the
authorization server that Claude talks to; the Hub is one step inside its own
/authorize flow.
We deliberately do not use the SDK's ProxyOAuthServerProvider. Proxying would
hand Claude a Hub token, which would break the RFC 8707 audience binding, give
Claude broader Hub scopes than klip:read, and move token issuance out of our
control so the S8 kill switch could no longer invalidate live sessions.
Claude ──/authorize──▶ gateway ──302──▶ Downstream Hub ──302──▶ /authorize/hub/callback
│ │
│ validate id_token (sig/iss/aud/nonce) │
│ check the pilot ALLOWLIST │
◀──────────────────────────────────────────────┘
└──302 code──▶ Claude ──/token──▶ gateway token (klip:read)Authentication is not authorization. The Hub proves who someone is; the
users table decides whether they may use the connector. Phase 1 uses one shared
KLIP service account, so every admitted user can read everything MCP_READONLY
can read (review H8) — pilot membership is the data-access control. A Hub account
that is not on the list gets a 403, and the <= 15 cap is enforced by user:add.
Two PKCE exchanges run in the flow; do not conflate them. Claude's own
code_challenge protects the Claude→gateway leg (handled by the SDK); a separate
verifier the gateway keeps server-side protects the gateway→Hub leg.
Which Hub client? The gateway's own — not KLIP's
Register a new OIDC client for the MCP Gateway. Do not reuse KLIP's Hub registration, even though KLIP is already registered in the testing DWS Hub.
The Hub only touches one of the two trust boundaries:
Boundary | Credential | Hub involved? |
Claude → gateway (which human is asking) | gateway's own Hub client + the pilot allowlist | yes |
gateway → KLIP (reading data) |
| no — PRD §7 excludes it explicitly |
Reusing KLIP's client breaks the first boundary in a concrete way. The gateway
validates the ID token's aud against its own HUB_CLIENT_ID; sharing KLIP's
client id means an ID token minted during a KLIP login would be accepted by the
gateway, which is the confused-deputy shape. Separate clients are what make the
two relying parties distinguishable. It also keeps the redirect-URI allowlists,
client secrets, rotation schedules, Hub SSO audit entries, and disable switches
independent — turning the connector's Hub client off must not take KLIP login down.
None of this needs any change on the KLIP side. K1–K4 are unaffected.
Two Hub instances, two registrations
Register the gateway separately in each Hub and pair them with the matching KLIP:
Stage | KLIP_ENV | HUB_ISSUER | Client |
4–6 (build, staging UAT) |
| testing DWS Hub | gateway client in the testing Hub |
7+ (production cutover) |
| production Hub | a separate gateway client in the production Hub |
The pairing is enforced at boot, because getting it wrong in one direction is dangerous rather than untidy:
KLIP_ENV=production+ a testingHUB_ISSUER→ the gateway refuses to start. Anyone able to create a test-Hub account would otherwise reach real commercial data.KLIP_ENV=staging+ the productionHUB_ISSUER→ warns and continues.
hub:check prints the pairing it detected, so the cutover is verifiable:
pairing: KLIP staging <-> Hub testingWhat DWS Hub requires (it is not vanilla OIDC)
Per Docs/SSO-TARGET-APP-INTEGRATION.md. Four of these differ from the defaults an
OIDC client library assumes, and three would fail outright:
DWS Hub | |
Client type | public, PKCE S256 — |
Discovery |
|
Token body | JSON; form-encoded returns |
Scopes |
|
| mandatory in the token request and byte-exact |
Hence HUB_DISCOVERY_URL is configured explicitly, HUB_CLIENT_SECRET is optional,
and HUB_TOKEN_BODY defaults to json with a one-shot fallback to form on
unsupported_grant_type (logging which worked, so it can be pinned).
Setting it up
Register the gateway as an OIDC client in the Hub with redirect URI
<PUBLIC_URL>/authorize/hub/callback. It is a public client — do not ask for a secret.Put
HUB_ISSUER,HUB_DISCOVERY_URLandHUB_CLIENT_IDin/opt/mcp/.env.Verify before any pilot user tries:
docker compose exec -T gateway node dist/cli.js hub:checkThis prints the redirect URI to register, runs discovery, and warns if the Hub does not advertise S256 PKCE. The gateway also probes discovery at boot and reports it in
/healthzashub_oidc.Add pilot users (no passwords — the Hub authenticates them):
docker compose exec -T gateway node dist/cli.js user:add someone@example.com "Their Name"
The break-glass account
Exactly one local password account is permitted, for use when the Hub is down or misconfigured:
docker compose exec -T gateway node dist/cli.js user:add-break-glass it-emergency@example.comIt is hidden behind a disclosure on the login page, forces a password change at
first use, and every sign-in through it is audited with break_glass: true at high
severity. A Hub-authenticated user cannot use the password path at all, so turning
the Hub off is not a way to fall back to a password nobody set.
Set BREAK_GLASS_ENABLED=false once the Hub path is proven in production to remove
the password surface entirely.
Optional group gate — not available on DWS Hub
HUB_REQUIRED_GROUP adds a second check against a groups claim. DWS Hub does not
issue one (it advertises only openid profile email), so setting it would refuse
every user. hub:check warns if you do. The pilot allowlist in the users table
remains the authorization control.
IdP-initiated login
The Hub can push a user straight to a callback from its dashboard tile. That cannot work for a connector: the callback exists to complete an authorization request Claude started, so arriving without one leaves nothing to issue a code against. The gateway detects it and says "start from Claude instead" rather than failing as "sign-in expired".
4. Quick start (local)
npm ci
docker run -d --name mcpgw-devdb -e POSTGRES_DB=gateway -e POSTGRES_USER=gateway \
-e POSTGRES_PASSWORD=devpassword -p 127.0.0.1:55432:5432 postgres:16-alpine
cp .env.example .env.dev # then edit: PUBLIC_URL=http://localhost:8787, DATABASE_URL=...55432...
npx tsx test/fixtures/mockKlip.ts 5099 & # mock KLIP, behaves like the real one
set -a; . ./.env.dev; set +a
npm run migrate
npx tsx src/index.tsCreate a pilot user (works with a TTY or piped input):
printf 'a-strong-password\na-strong-password\n' | npx tsx src/cli.ts user:add you@example.com "Your Name"5. Tests
npm testSuite | Covers |
| The Incoterm × status × null matrix, kg→MT, rounding order, negative outstanding, WIB timestamps |
| Exhaustive method/path table for T-6, traversal and origin escapes |
| T-5 envelope, truncation |
| A bounded fetch publishes |
| All 9 tools against mock KLIP; no non-GET ever reaches KLIP; 401 re-login; |
| RFC 8707 audience binding: only this server's canonical resource is accepted |
| S5 redaction — aggressive on strings, inert on numbers |
| Hub OIDC against a mock provider: discovery issuer mismatch, foreign signing key, wrong issuer/audience, expired token, missing and replayed nonce, no-email claim |
|
|
| Production KLIP behind a testing Hub refuses to boot; the normal pairings do not |
| Token-endpoint auth method chosen from discovery, including post-only and public-client Hubs |
| DWS Hub modelled exactly: |
The mock Hub is a working mini-OIDC provider — real discovery document, real JWKS, real RS256 ID tokens, and PKCE verification on the authorization code — with every knob needed to forge a bad token, because the negative cases are the point.
The mock KLIP fixture deliberately reproduces the real system's quirks: kilograms
labelled MT, mixed-language statuses, an incoterm outside the standard four, null
quantities, an over-delivered contract, a limit silently clamped to 100, and a
contract remark carrying a prompt-injection payload.
6. Deploy
Full host-specific procedure, with the real IP, hostname and security-group table: deploy/RUNBOOK.md.
# on ECS-MCP
cd /opt/mcp && git pull
docker compose build gateway && docker compose up -d
curl -fsS http://127.0.0.1:8787/healthzAdmin CLI — note this runs inside the container, because the host installs Docker only and has no Node.js:
docker compose exec -T gateway node dist/cli.js user:list
docker compose exec -T gateway node dist/cli.js audit:summary --days 7
docker compose exec -T gateway node dist/cli.js audit:export --from 2026-08-01 --to 2026-09-01 --out /tmp/audit.csv
docker compose exec -T gateway node dist/cli.js routes:verify # probes KLIP, reports Appendix A gapsKill switch (S8) — target under 5 minutes:
docker compose exec -T gateway node dist/cli.js tokens:revoke-all --reason "incident 2026-xx"
docker compose stop gatewayBreak-glass if the app container is unhealthy:
docker compose exec -T db psql -U gateway -d gateway -c "UPDATE oauth_tokens SET revoked_at=now() WHERE revoked_at IS NULL;"7. Appendix A is a hard gate
src/adapters/klip/routes.ts and src/adapters/klip/fields.ts hold every KLIP path,
query-parameter name, page-size ceiling, response field name and enum value the
adapter depends on. Every entry is currently unverified.
The gate is executable, not clerical: with KLIP_ENV=production the process
refuses to start while any route is unverified. Run routes:verify against
staging, record the results, set verified: true per route and enums.verified = true.
Two fields matter more than the rest:
maxLimit— the largestlimitKLIP actually accepts. If it silently clamps to 100, aKLIP_PAGE_SIZEof 1000 turns one page into ten and breaks the latency target.enums.*— the canonical status and incoterm values. Anything unmapped is excluded from totals with a data-quality note, never defaulted.
8. Deviations from TSD v0.9
Each one comes from the design review and is commented at its call site.
# | Change | Why |
B3 | Origin rejected only when present and invalid | The spec requires 403 only for a present-and-invalid Origin. Claude calls the connector server-to-server and may send none; rejecting absent would 403 every tool call. |
B4 |
| RFC 8707 audience binding. Added |
B5 | Stateless transport; identity from the token per request | Revision 2026-07-28 removed protocol sessions. T-4 is satisfied by construction — there is no session to hijack. |
B6 | Kill switch runs | The host has no Node.js, so |
B7 | nginx is the anti-abuse floor; per-user limit keyed on the OAuth | All traffic arrives from Anthropic's shared egress range, so IP-keyed limiting puts the whole pilot in one bucket. Added |
H1 | OAuth built on the SDK's | The SDK ships the AS, including revocation and default rate limiting. Only storage and user authentication are ours. |
H2 | Downstream Hub OIDC is the login path, with one break-glass local account | Brought forward from Phase 2. The Hub authenticates; the users table stays the pilot allowlist. |
H3 | nginx allowlist scaffolding for Anthropic's | Anthropic publishes stable egress ranges and recommends allowlisting; |
H4 | Truncated results publish | Four distinct paths to a confidently wrong number. |
H5 | Short TTL cache; pages 2..N fetched concurrently; page size clamped to | Ten sequential round-trips cannot meet P95 ≤ 5 s. |
H6 | Added a 9th tool, | Without it a mistyped plant name returns an empty set that reads as "nothing is outstanding". |
H7 |
| The specified hardcoded "KLIP production" string would have made every staging UAT answer claim to be production. |
H9 |
| Retention by partition drop is the only thing the append-only trigger permits; U5's export had no implementation; client IPs would all have logged as 127.0.0.1. |
H10 | Backup sidecar, container healthcheck, | Specified in the TSD but never implemented in the guide, so it would not have existed at go-live. |
— | Unknown tool parameters rejected via | PRD 8.1 requires rejection; a plain |
— | Tools return | Typed data rather than JSON embedded in prose — fewer transcription errors, which is what M1 measures. |
9. Still outstanding
Appendix A reconciliation (P1) — blocks production, enforced at startup.
KLIP-side K1–K4 — the
MCP_READONLYrole,svc-mcpaccount, security-group rule.Off-host backup sync and a tested restore — the sidecar writes locally only.
Real corporate egress CIDRs in
deploy/nginx/mcp.conf, then enable the two commentedreturn 403lines.Load test at the 30-concurrent-user capacity NFR.
Hub client registration — the gateway's own, in the testing DWS Hub first:
HUB_ISSUER, client id and secret, with redirect URI<PUBLIC_URL>/authorize/hub/callbackregistered Hub-side. Confirm the Hub's email and groups claim names, then runhub:check. A second, separate registration in the production Hub is needed at Stage 7.Decide whether to set
HUB_REQUIRED_GROUP, and whether to turnBREAK_GLASS_ENABLED=falseafter the Hub path is proven in production.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- FlicenseAqualityBmaintenanceEnables read-only querying of the gong-nl-db Postgres database through natural language via Claude Desktop.9
- AlicenseNot gradedqualityDmaintenanceEnables natural language queries against Snowflake Gold-layer tables through Claude Desktop, allowing users to ask business questions in plain English without SQL knowledge.MIT
- AlicenseNot gradedqualityCmaintenanceGives Claude live access to your Observe tenant, enabling natural language queries about errors, logs, and metrics without writing OPAL pipelines.17MIT
- FlicenseNot gradedqualityBmaintenanceEnables natural language interaction with Kintone data via Claude, allowing listing apps, field definitions, querying and modifying records.
Related MCP Connectors
WHOOP recovery, strain, sleep and workouts in Claude via official WHOOP OAuth. Free, open source.
Connect Claude to Fathom meeting recordings, transcripts, and summaries
Garmin data in Claude & ChatGPT via the Garmin Health API. OAuth sign-in, no password sharing.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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/dwsitproject-hub/MCP-Gateway'
If you have feedback or need assistance with the MCP directory API, please join our Discord server