🔥 Agencies & studios Run unlimited client servers for a flat $99/mo — zero per-seat or bandwidth tax. Railyard for agencies →
Designing MCP tools an AI agent can't misuse in production ← Blog Security & BYOC architecture • 5 min read

Designing MCP tools an AI agent can't misuse in production

Almokhtar Bekkour

Almokhtar Bekkour, Founder, Systems Architect

August 9, 2026 · 5 min read

Handing an AI agent the keys to production is mostly a tool-design problem. Models are good at picking the right tool and filling in arguments. They are less good at noticing that the arguments came from a log line someone else wrote, or that “clean up the old staging apps” matched more apps than you meant. The server has to make the dangerous mistakes impossible, not just unlikely.

These are the rules Railyard’s MCP server follows across its 42 tools, and why.

Rule 1: One tool, one kind of effect

The simplest MCP server for an existing API is a single api_request tool taking a method and a path. It’s also the worst one. The client can’t tell a GET from a DELETE before the call, so it can’t decide when to ask you first.

Every Railyard tool has exactly one kind of effect:

EffectExamplesWhat it means
Readlist_apps, deploy_logs, app_traffic, list_activityChanges nothing
Additivecreate_app, add_domain, create_backup, create_environmentAdds something new; existing state is untouched
Destructivedeploy_app, rollback_app, restart_app, set_env, scale_appReplaces or interrupts something that exists
Approvaldelete_app, unset_env, remove_domain, restore_backup, reboot_serverCan’t be cleanly undone, so it waits for a person

That’s why there’s a list_deploys and a cancel_deploy, instead of one deploys tool with an action argument.

Rule 2: Annotations tell the truth, even when it’s inconvenient

MCP tools carry hints: readOnlyHint, destructiveHint, idempotentHint, openWorldHint. Clients use them to decide what runs without asking. Claude runs read-only tools straight away and asks before destructive ones.

The temptation is to mark everyday actions as non-destructive so the agent stops asking. We didn’t. deploy_app is destructive, because it replaces the running release. So is restart_app, because it interrupts requests in flight, and so is set_env, because it overwrites a value. The result is one confirmation per deploy, which is the right amount of friction for production.

The other hints carry information too:

  • idempotentHint: true on set_env, scale_app and set_maintenance. Calling them twice with the same arguments ends in the same state, so a client can retry safely after a network hiccup.
  • openWorldHint: false everywhere. Every tool acts only inside your Railyard team, never on arbitrary third-party systems.

A test runs over every tool on every build. It fails if any tool is missing a title or a hint, or is marked both read-only and destructive.

Rule 3: Scopes follow risk, not convenience

Three scopes, each a superset of the last:

  • read sees apps, servers, deploys, logs, traffic, alerts, domains, backups and the activity trail.
  • deploy adds deploying, rolling back, cancelling, restarting, scaling workers and maintenance mode. These are the actions a release engineer needs, and all of them are recoverable.
  • write adds creating apps and environments, launching templates, changing variables and domains, starting backups and running commands.

Some read-looking tools sit in write on purpose. get_env returns variable values. query_database reads your data. list_tables maps your schema. run_command can print every secret in the container. “Can only read” isn’t the same as “harmless”, so anything that exposes data or secrets needs the scope you’d trust with them.

Rule 4: An agent never outranks the person who connected it

A scope on its own isn’t enough. If a viewer could grant an agent write, the agent would have more power than the person who connected it. So every token’s effective scope is capped by its creator’s current role:

  • A viewer’s connections are read-only.
  • A deployer’s stop at deploy.
  • Admins and owners can grant write.

The cap is computed on each call, not frozen at connection time. Demote someone and every agent they connected is demoted in the same instant. Members limited to certain apps produce tokens that only see those apps.

Rule 5: Irreversible actions need a human, every time

Five actions create an approval request instead of running:

  • delete_app: data and history are gone.
  • unset_env: a running app can break silently on next deploy.
  • remove_domain: the site goes dark on that hostname.
  • restore_backup: replaces today’s data with an older copy.
  • reboot_server: every app on the box goes down.

The agent gets an immediate, plain answer (“Waiting for approval… Nothing has changed yet”), so it can tell you what’s pending instead of retrying. An admin approves on the Approvals page. The action runs inside the same transaction that records the approval. If it fails, the transaction rolls back and the request stays pending with the reason shown. Agents can never approve anything, including deploys waiting for sign-off.

This rule applies regardless of scope. Even a write token can’t delete an app without a person.

Rule 6: Fail with a sentence, not a stack trace

A model can only recover from an error it understands. Every refusal is written for the model to act on:

  • “No app named “api-prod” in Acme. Call list_apps for the team’s app names.”
  • “deploy_id must be a whole number, got “latest”.”
  • “This API token can’t run command — create a write token.”
  • “web is a web process; web processes scale with autoscaling.”
  • “Payment is overdue, so this team is read-only until the invoice is paid.”

Each one names the fix, or the tool that gets the information the model is missing.

Rule 7: Bounded answers

An agent that asks for “all the logs” shouldn’t get a 40 MB answer that pushes everything else out of its context window. Every list and read has a ceiling, and the ceilings are documented:

  • runtime and deploy logs: up to 500 lines
  • SQL results: 200 rows
  • deploys and activity: 50 entries
  • template search: 50 results
  • command output: the last 20,000 characters

Defaults are smaller still, so the common case is cheap.

Rule 8: Keep secrets out of the trail

Every call is audited: tool, token, who connected it, the arguments. But some arguments are secrets. Variable values, SQL text and shell commands are dropped from the MCP audit entry before it’s written. Sealed variables stay masked in get_env, and set_env refuses to change them; they can only be changed in the dashboard.

Rule 9: One implementation of every action

deploy_app goes through the same deploy logic as the dashboard’s Deploy button. So the deploy approval rule, the deploy window, the per-hour rate limit and the “a deploy is already running” check apply to agents automatically. The same goes for maintenance mode, rollbacks, promotions and backups. A separate implementation for agents would drift from the one people use, and drift is where the holes are.

What this adds up to

The agent can do almost everything you can, at most what your role allows. It can’t do what can’t be undone without you, and every call leaves a record. You get the speed of “roll back the shop to yesterday’s release” in one sentence, without wondering what else it might do.

The full list of tools, scopes and approvals is in the MCP docs. To see the whole request path end to end, read how an MCP server for deployments works.

Give an agent production access, safely.

Read, deploy and write scopes, capped by each person's role, with human approval for anything that removes data. Connect Claude to Railyard in one click.

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

Theme
Colors