Skip to content

· 2 min read · Zain Akram

Prompt-driven deployments: putting OAuth in front of Coolify for MCP

How a small Node service turned a static-token deployment API into an OAuth-protected MCP server that Claude can use safely, with consent, scopes, sessions and an audit trail.

  • MCP
  • DevOps
  • OAuth
  • Node.js

We wanted to say "redeploy staging for the listing service and show me the logs" to Claude and have it happen. Coolify, our self-hosted deployment platform, has a perfectly good API. It also has exactly one auth mechanism: long-lived static tokens. That is fine for scripts and unacceptable for an AI client acting on behalf of different people.

The Model Context Protocol expects remote servers to speak OAuth 2.0. So the job was to build the missing piece: an OAuth shim in front of Coolify that exposes a scoped MCP tool surface.

The shape of it

A Node and Express service with three responsibilities:

  1. Authorization server. Implements the OAuth endpoints MCP clients expect (metadata discovery, dynamic client registration, authorize, token, revoke), with PKCE required.
  2. Consent and sessions. The authorize step shows a consent screen listing the projects and actions being granted. Sessions and grants live in Supabase; Coolify tokens never leave the server.
  3. MCP server. Exposes tools like list_projects, deploy, redeploy, get_logs and get_status, each checked against the grant's scopes before touching Coolify.

Scopes map to real actions

Scopes are named after what they allow, not after resources: deploy:staging, deploy:production, logs:read, status:read. A grant for staging deploys cannot be escalated to production by a clever prompt, because the check happens in the shim, after the model has already decided what to call.

Production deploys additionally require a confirmation round-trip: the tool returns a summary and a one-time token, and the client must call confirm_deploy with it. Humans see this as "Claude asked me to confirm", which is exactly right.

Rate limits and audit

Every tool call is rate limited per client and per user, and written to an audit table with the actor, the grant, the arguments and the Coolify response. When something goes wrong at 2am, the log answers "who asked for what" in one query.

Things that bit us

  • Discovery metadata matters. MCP clients look for /.well-known/oauth-authorization-server. Get one field wrong and you get a vague "could not connect" with no further detail.
  • Dynamic client registration is required by most clients. We allow it, but registered clients start with no scopes until a human approves them on the consent screen.
  • Idempotency. Models retry. deploy takes an idempotency key derived from the request so a retry does not trigger a second deploy.

Was it worth it?

The pipelines now support 25+ deploys a week, a meaningful share of them started from a chat prompt. More importantly, security signed off, because the model never holds a credential, every action is scoped and logged, and the dangerous ones ask first. That pattern, a thin OAuth and policy layer between the model and any real system, is now how I approach every "let the agent do things" request.