Hostinger Cloud MCP 2.0 Gateway
by ntxptrevor
README.md
# Hostinger Cloud MCP 2.0 Gateway
A **stateless cloud MCP server** for the full Hostinger API, written natively against
MCP protocol revision **`2026-07-28`** — the release that turned MCP from a bidirectional
stateful protocol into a plain request/response one
([Model Context Protocol](https://blog.modelcontextprotocol.io/posts/2026-07-28/)).
It replaces the previous `hostinger-mcp-gateway`, which wrapped the vendor runtime's
stateful transport and spoke the pre-2.0 handshake.
- **Endpoint:** `POST https://mcp.ntxpllc.cloud/mcp`
- **Tools:** 305 of the vendor's 314 (the 9 filesystem-backed deploy tools are hidden on a headless host)
- **Auth:** built-in OAuth 2.1 authorization server with PKCE, Google Sign-In, and an API-token fallback
- **State:** none. No sessions, no database, no sticky routing.
## What "stateless MCP 2.0" actually means here
| 2025-era MCP | This server (`2026-07-28`) |
|---|---|
| `initialize` / `notifications/initialized` handshake | Removed. Every request self-describes via `_meta`. |
| `Mcp-Session-Id` header pins a client to one instance | Removed. Any request can land on any instance. |
| Capabilities learned during the handshake | `server/discover` RPC (optional for clients, **required** of servers) |
| HTTP GET stream for change notifications | `subscriptions/listen` |
| Gateways parse the body to route | `Mcp-Method` and `Mcp-Name` headers (SEP-2243) |
| Results are bare | Every result carries `resultType`; list/read results carry `ttlMs` + `cacheScope` |
| `logging/setLevel` | Per-request `_meta["io.modelcontextprotocol/logLevel"]` |
| `resources/subscribe` / `unsubscribe` | `subscriptions/listen` |
Because there is no protocol session and no in-process tool state, the container can be
scaled to N replicas behind plain round-robin with no shared store. The only persisted
byte on disk is the HMAC key used to sign OAuth artifacts.
### Backward compatibility
Real clients migrate at different speeds, so `2025-06-18` and `2025-03-26` are still
accepted. On those revisions the server answers `initialize`, `ping` and
`logging/setLevel`, and strips the 2.0-only fields (`resultType`, `ttlMs`, `cacheScope`)
so strict older validators do not choke. Version is negotiated per request from
`_meta["io.modelcontextprotocol/protocolVersion"]`, then the `MCP-Protocol-Version`
header, then `initialize` params.
## Authorization
The Hostinger API token never leaves the server. Two ways in:
1. **OAuth 2.1 + Google Sign-In** — the gateway is its own authorization server, because
Hostinger's `auth.hostinger.com` allowlists redirect URIs for only a few clients and
rejects Perplexity's. Implemented: PKCE (S256, mandatory), dynamic client registration
(RFC 7591), Client ID Metadata Documents, refresh tokens, `iss` on the authorization
response (RFC 9207), `application_type` at registration (SEP-837), issuer-bound
credentials (SEP-2352), and resource-indicator audience binding (RFC 8707).
2. **API-token bearer** — present the Hostinger account token directly, for n8n, cron and scripts.
Every OAuth artifact is a self-contained HMAC-signed JWT: client registrations,
authorization codes, access tokens and refresh tokens. There is no database.
Discovery:
- `GET /.well-known/oauth-protected-resource`
- `GET /.well-known/oauth-authorization-server`
## Deploy
CI builds the image on every push to `main` and publishes it to
`ghcr.io/ntxptrevor/hostinger-mcp2-gateway:latest`. The VPS pulls that image rather than
building from source — building on the VPS through the Docker Manager API reports success
but silently produces no container.
To ship a change: push to `main`, wait for the workflow, then re-run `docker compose up`
for the project (the compose file sets `pull_policy: always`).
Hostinger VPS, Docker Manager, `docker-compose.yml` in this repo. Required environment:
| Variable | Purpose |
|---|---|
| `HOSTINGER_API_TOKEN` | Upstream credential. Required. |
| `PUBLIC_URL` | OAuth issuer + resource identifier. No trailing slash. |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | Identity layer. Redirect URI: `{PUBLIC_URL}/oauth/googlecb` |
| `ALLOWED_EMAILS` | Sign-in allowlist. Empty means any verified Google account. |
Optional: `READ_ONLY=true` hides every write tool, `TOOL_ALLOWLIST` / `TOOL_DENYLIST`
narrow the catalogue, `ALLOW_LOCAL_FS=true` exposes the nine filesystem-backed deploy
tools if you mount a workspace.
Mount `/data` so the token-signing key survives restarts — otherwise every client has to
re-authorize after a redeploy.
## Verify
```bash
npm install && npm test # 49 checks against a stubbed Hostinger API
curl -sS https://mcp.ntxpllc.cloud/health
curl -sS https://mcp.ntxpllc.cloud/mcp \
-H "Authorization: Bearer $HOSTINGER_API_TOKEN" \
-H "Content-Type: application/json" \
-H "MCP-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: server/discover" \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover"}'
```
## Layout
```
src/config.mjs environment, protocol versions, signing key
src/auth.mjs OAuth 2.1 authorization server + request authentication
src/hostinger.mjs tool catalogue and the stateless HTTP executor
src/protocol.mjs MCP dispatcher: 2026-07-28 core + legacy compatibility
src/server.mjs Express surface (MCP endpoint, OAuth endpoints, discovery)
test/smoke.mjs end-to-end tests against a stub upstream
```
The tool catalogue is read from the vendor package
[`hostinger-api-mcp`](https://github.com/hostinger/api-mcp-server) (v1.33.0), but the
vendor's stateful runtime is bypassed: requests are executed directly so the upstream
bearer is supplied per call and concurrent callers cannot interfere.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues