🔥 Agencies & studios Run unlimited client servers for a flat $99/mo — zero per-seat or bandwidth tax. Railyard for agencies →
How an MCP server for deployments works, from Claude's request to your server ← Blog Guides & tutorials • 6 min read

How an MCP server for deployments works, from Claude's request to your server

Almokhtar Bekkour

Almokhtar Bekkour, Founder, Systems Architect

August 15, 2026 · 6 min read

“Deploy the API and tell me when it’s healthy” sounds like one action. Behind it are about a dozen HTTP requests, two kinds of credentials, a permission check that runs on every call, and one human decision point for anything that can’t be undone. This post follows the whole path, request by request, with the JSON as it actually goes over the wire.

The examples use Railyard’s MCP server at https://app.railyard.run/api/mcp. The mechanics apply to any remote MCP server; the specific design choices (scopes, approvals, role caps) are Railyard’s.

The cast

  • The client. Claude on the web, desktop or mobile, Claude Code, Cursor, or anything else that speaks the Model Context Protocol. It’s the one deciding which tool to call.
  • The MCP server. One HTTPS endpoint. It turns a tool call like deploy_app into the same action you’d take in the dashboard, with the same rules.
  • The authorization server. Railyard’s own OAuth endpoints. It issues the token the client sends on every request.
  • You. You approve access once in your browser, and you approve the handful of actions that remove or overwrite data.

Step 1: The client finds out it needs to sign in

The first thing a client does is send a request without a token. The server answers 401 and points at a metadata document:

HTTP/2 401
www-authenticate: Bearer resource_metadata="https://app.railyard.run/.well-known/oauth-protected-resource"

That document names the resource and the authorization server that protects it:

{ "resource": "https://app.railyard.run/api/mcp", "authorization_servers": ["https://app.railyard.run"] }

The client then reads /.well-known/oauth-authorization-server, which lists the authorize, token and registration endpoints, the supported scopes (read, deploy, write) and the only code challenge method it accepts, S256. Nothing is configured by hand. The client discovers everything from the URL you gave it.

The authorization post covers the OAuth half in depth. In short, the client registers itself, opens a Railyard consent page in your browser, and exchanges a single-use code for an access token, using PKCE so an intercepted code is worthless.

Step 2: You approve, and the scope gets capped

The consent page asks two things: which team, and how much access. The choice is read, deploy or write, and it can never exceed your own role on that team. A viewer can grant read, a deployer up to deploy, an admin or owner up to write. The cap isn’t only applied once at consent. It’s checked again on every call against your current role, so if someone demotes you next week, every agent you connected is demoted with you.

If your membership is limited to certain apps, the token only ever sees those apps. Every other app returns “not found”, exactly as it would in the dashboard.

Step 3: The client learns what it can do

With a token in hand, the client asks for the tool list:

{ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }

Each tool comes back with a name, a description, a typed input schema and annotations:

{
  "name": "rollback_app",
  "title": "Roll back app",
  "description": "Redeploy an app's earlier successful deploy. By default it also restores the variables that deploy ran with.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "app": { "type": "string", "description": "App name, as returned by list_apps" },
      "deploy_id": { "type": "integer", "description": "Id of the successful deploy to go back to, from list_deploys" },
      "restore_config": { "type": "boolean", "description": "Also restore that deploy's variables (default true)" }
    },
    "required": ["app", "deploy_id"]
  },
  "annotations": { "title": "Roll back app", "readOnlyHint": false, "destructiveHint": true, "idempotentHint": false, "openWorldHint": false }
}

The annotations aren’t decoration. Claude runs read-only tools without asking, and asks you before a destructive one. That’s why they have to be honest: deploy_app is marked destructive because it replaces the running release, even though a deploy is routine. The tool design post explains how each of the 42 tools is classified.

The list is the same for every connection and comes back in a fixed order. That lets clients cache it, and newer clients are told explicitly how long they may keep it.

Step 4: A tool call

Say you asked why the API is slow. Claude picks app_traffic:

{ "jsonrpc": "2.0", "id": 3, "method": "tools/call",
  "params": { "name": "app_traffic", "arguments": { "app": "api", "range": "1h" } } }

On the server, four checks run before anything else:

  1. Is the token valid and not revoked? If not, the answer is 401 again.
  2. Does the token’s effective scope allow this tool? app_traffic needs read, so everyone passes. A run_command with a deploy token gets a tool error that names the scope it needs.
  3. Can the team make changes right now? A suspended or past-due team can still read everything; anything that changes state is refused with a plain sentence explaining why.
  4. Are the required arguments present and well-formed? A missing app or a deploy_id of "abc" gets an actionable error (“deploy_id must be a whole number”) instead of a stack trace.

Then the tool runs, using the same domain logic the dashboard and REST API use. There’s no second implementation of “deploy” or “roll back” that could drift. The result comes back twice: as text for the model to read, and as structuredContent for clients that want typed data:

{ "content": [{ "type": "text", "text": "{\n  \"range\": \"1h\", ..." }],
  "structuredContent": { "range": "1h", "summary": { "...": "..." }, "previous": { "...": "..." }, "paths": [] } }

Responses are bounded on purpose: log reads stop at 500 lines, SQL at 200 rows, lists at 50 entries. An agent that asks for “everything” gets a useful slice, not a payload that eats its context window.

Step 5: The actions that wait for a person

Five tools never act on their own: delete_app, unset_env, remove_domain, restore_backup and reboot_server. When Claude calls one, the server records an approval request and answers immediately:

Waiting for approval: a team admin must approve “Remove shop.example.com from storefront” in Railyard (Approvals). Nothing has changed yet.

An admin sees the request on the Approvals page and approves or rejects it. Approval runs the action inside the same transaction that records the decision. If the action fails (say, the server is offline when the reboot is approved), the whole thing rolls back, the request stays pending, and the admin sees why.

Agents can’t approve anything, including their own requests or deploys waiting for sign-off. That holds no matter what the token’s scope is.

Step 6: Everything lands in the audit log

Every tool call that runs, and every call refused for lack of access, writes an activity entry: which tool, which token, who connected it, and the arguments, minus the ones that could hold secrets. Variable values, SQL text and shell commands never reach that entry. Deploys an agent starts show up in the deploy history like any other, with their trigger.

list_activity gives the agent itself read access to that trail. That’s how “who changed DATABASE_POOL yesterday?” gets a real answer.

Why it’s built this way

Three principles drove the design.

The token is the boundary, not the prompt. Instructions in a prompt can be argued with; a read token can’t be talked into a write. Every safety property above is enforced on the server, per call.

The agent gets your powers, never more. Scopes, role caps, app limits and the suspended-team rule are the same checks a person hits in the dashboard. An agent is just another way to click the same buttons.

Irreversible means human. A bad deploy rolls back and a restart is just a restart, so an agent can do those on its own. Deleting data, restoring over today’s database, or taking a server down for a reboot can’t be cleanly undone, so those always wait.

Try it

In Claude, open Customize → Connectors, search for Railyard, and choose Connect. In Claude Code:

claude mcp add --transport http railyard https://app.railyard.run/api/mcp

Then ask it something real: “Which of my apps failed their last deploy, and why?” The full tool reference is in the MCP docs.

Connect Claude to your own servers.

Add Railyard from Claude's connectors directory and approve access on one page. Your agent can deploy, roll back, read logs and query data on servers you already own.

Free plan, no card, and 7 days of Pro for every new team. One flat price per team.

Theme
Colors