Skip to main content
Glama
ntxptrevor

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.