RunCloud MCP Server

RunCloud MCP Server

Exposes the RunCloud v3 API to AI assistants, allowing natural language querying and management of servers with safety profiles for destructive operations.

Category
Visit Server

README

RunCloud MCP Server

A Model Context Protocol server that exposes the RunCloud v3 API to AI assistants like Claude Code and Claude Desktop. Ask questions about your servers in plain language, and perform management actions with explicit guard rails around anything destructive.

Built on Laravel MCP.

You:    Which services are running on my production server?
Claude: (calls runcloud-services) NGiNX, MariaDB, Redis and Supervisor are running.
        Memcached and Beanstalk are stopped.

This controls live infrastructure. Depending on the configured profile, the tools here can restart services, deploy code, and delete web applications and databases. Read Tool profiles and Safety model before connecting it to a production RunCloud account.

Requirements

  • PHP 8.3+
  • Composer
  • A database (PostgreSQL, MySQL, or SQLite) — only used for API tokens when running the HTTP transport
  • A RunCloud account and API key

Installation

git clone https://github.com/josephmcgarvey/runcloud-mcp-server.git
cd runcloud-mcp-server

composer install
cp .env.example .env
php artisan key:generate
php artisan migrate

Then configure .env:

RUNCLOUD_API_TOKEN=your-token-here
RUNCLOUD_MCP_PROFILE=standard

RUNCLOUD_MCP_PROFILE decides which tools exist at all. Start with readonly while you are getting a feel for it — see Tool profiles.

Getting a RunCloud API key

In the RunCloud dashboard, go to Workspace → Settings → API Key and create one. The same screen shows your rate limits. The key is a bearer token with full access to everything in that workspace, so treat it like a root password.

Verify it works:

php artisan tinker --execute 'app(App\Services\RunCloud\RunCloudClient::class)->get("ping");'
# => ["message" => "pong"]

Connecting

Claude Code (local, recommended)

The simplest setup. Claude Code launches the server over stdio — no HTTP, no auth layer, nothing exposed to the network:

claude mcp add runcloud -- php /absolute/path/to/runcloud-mcp-server/artisan mcp:start runcloud

Use --scope user to make it available in every directory rather than just the current project. Restart Claude Code, then check /mcp to confirm the tools loaded.

Claude Desktop and remote clients (HTTP)

Claude Desktop cannot launch a local process on a remote machine, so it needs the HTTP transport. That endpoint is registered at /mcp and guarded by Laravel Sanctum.

Create a user and issue a token:

php artisan tinker --execute 'App\Models\User::create([
    "name" => "You", "email" => "you@example.com", "password" => "a-strong-password",
]);'

php artisan mcp:token you@example.com --name=claude-desktop

The token is displayed once. Connect a remote Claude Code client with:

claude mcp add --transport http runcloud https://your-app.example.com/mcp \
  --header "Authorization: Bearer YOUR_SANCTUM_TOKEN"

Claude Desktop (OAuth)

Claude Desktop's custom connectors only speak OAuth — there is no field for a static bearer token — so Sanctum alone cannot serve them. Laravel Passport provides the OAuth server, and Mcp::oauthRoutes() publishes discovery and dynamic client registration.

Point Claude Desktop at https://your-app.example.com/mcp under Settings → Connectors → Add custom connector, leaving the OAuth client fields blank — the server supports dynamic registration, so it configures itself. It discovers the authorization server, registers, and sends you through sign-in and an approval screen.

The /mcp route accepts both guards, declared in this order:

->middleware('auth:sanctum,api')

The order matters. Passport's guard aborts the chain when it cannot resolve a token, so listing it first causes valid Sanctum tokens to be rejected. With Sanctum first, personal access tokens authenticate directly and anything Sanctum cannot resolve falls through to Passport. A test pins this behaviour.

Passport renders a consent screen but ships no view for it, and it requires a signed-in session, so this application provides both: a login page at /login and a consent screen at resources/views/oauth/authorize.blade.php. Neither uses Vite — they must render correctly on a fresh deployment, and an unstyled consent screen is a poor place to be asking someone to grant access to infrastructure.

Create an account from the console; there is no self-service registration, because signing in leads to server management tooling:

php artisan tinker --execute 'App\Models\User::create([
    "name" => "You", "email" => "you@example.com", "password" => "a-strong-password",
]);'

This flow is verified working with Claude Desktop against a live deployment: discovery, dynamic client registration, the redirect to sign in, the consent screen, approval, and a connected server.

When the connector will not register

Claude Desktop reports the same message — "Couldn't register with … sign-in service" — whether the server refused registration or was never reached at all, so start by ruling out the network path:

curl https://your-app.example.com/.well-known/oauth-authorization-server

JSON with an issuer field means the path is open and the problem is in OAuth. A timeout or connection refused means the request is not arriving, and nothing in this application can fix it. Behind a reverse proxy, check the proxy points at the port the application actually listens on — a proxy that is publicly reachable but forwarding to the wrong port fails exactly this way.

Then confirm APP_URL matches the public hostname. Every endpoint in the discovery document is derived from it, so a stale value sends the client somewhere that does not answer.

Never expose /mcp without authentication. Anyone who can reach it can control your servers. Note that the login page is public by necessity — it is where the OAuth redirect lands — so any account on this application should have a strong password.

Available tools

Forty-five tools, subject to the configured profile. Read and destructive operations are deliberately split into separate tools so that MCP annotations stay accurate.

Read-only

Tool Actions
runcloud-servers list, get, stats (counts), health (live RAM/disk/CPU), hardware
runcloud-webapps list (with search and sorting), get
runcloud-services list services with running state, memory, CPU, version
runcloud-databases list, get, grants
runcloud-database-users list, get
runcloud-domains list, get
runcloud-ssl status, method (basic vs advanced, AutoSSL)
runcloud-system-users list, get
runcloud-git Repository, branch, auto-deploy state, deployment script
runcloud-cron list, get
runcloud-supervisor list, get, status (live process states)
runcloud-webapp-settings PHP/FPM settings and linked databases
runcloud-firewall Server firewall rules, summarised by port

Write

Tool Notes
runcloud-create-webapp Custom/empty app for Laravel, APIs, static sites
runcloud-create-wordpress Full WordPress install; generates both passwords
runcloud-create-database RunCloud cannot rename a database afterwards
runcloud-create-database-user Generates a strong password; returned exactly once
runcloud-add-domain Configures neither DNS nor HTTPS
runcloud-create-cron Takes a normal five-field cron expression
runcloud-create-supervisor Long-running processes such as queue workers
runcloud-create-system-user Generates the SSH/SFTP password; sudo off by default
runcloud-add-firewall-rule Stages a rule; refuses duplicates and malformed addresses

Destructive — require confirm=true

Tool Notes
runcloud-control-service start, stop, restart, reload. Only stop/restart need confirmation
runcloud-database-grant grant needs no confirmation; revoke does
runcloud-database-user-password Resets a password, breaking apps using the old one
runcloud-toggle-cron Flips state; re-reads the job first to avoid inverting the intent
runcloud-update-webapp-settings Merges over current values; reloads the FPM pool
runcloud-change-php-version Applies immediately, no staging step or rollback
runcloud-system-user-password Breaks anything using the old SSH/SFTP password
runcloud-generate-deployment-key Replaces any existing key, breaking deploys using it
runcloud-deploy-firewall Applies staged rules; refuses to leave no SSH route in
runcloud-git-clone Writes into the live application root
runcloud-git-deploy deploy (runs the script) or branch (switches branches)
runcloud-git-deploy-script Replaces the script outright; can enable auto-deploy
runcloud-git-remove Also requires a matching repository
runcloud-install-ssl Let's Encrypt or custom. Prefer the staging environment while testing
runcloud-delete-webapp
runcloud-delete-database deleteUser defaults to false
runcloud-delete-database-user
runcloud-delete-domain Also requires a matching domainName
runcloud-delete-ssl HTTPS stops working
runcloud-delete-cron Also requires a matching label
runcloud-delete-supervisor Also requires a matching label; queued work stops
runcloud-delete-system-user Refuses while the user still owns web apps
runcloud-delete-firewall-rule Also requires a matching ipAddress; refuses the last SSH rule

Tool profiles

Every registered tool's schema is sent to the client on each request, so the tool list has a standing context cost. It is also the outer boundary on what the model can do at all: a tool that is not registered cannot be called, whatever the prompt says.

Set RUNCLOUD_MCP_PROFILE in .env:

Profile Tools Approx. context Allows
readonly 13 ~4,600 tokens Nothing that changes anything
standard (default) 35 ~13,200 tokens Reads, creation, modification, service control
full 45 ~16,100 tokens Everything, including deletions

standard withholds the ten tools that destroy something the API cannot recreate. It still allows restarting services, deploying, and changing settings — those are disruptive but recoverable.

An unrecognised value falls back to standard, so a typo cannot silently widen access.

Deleting servers

There is no tool for it, under any profile. DELETE /servers/{id} exists in the RunCloud API and is deliberately not exposed here: destroying an entire machine is not an operation worth reaching through a language model, and no confirmation argument makes it safe enough to be worth the risk. A test asserts the tool has not been reintroduced.

Safety model

Five independent layers, because a language model calling infrastructure APIs will eventually misread an instruction.

Profiles. The narrowest layer, and the only one that does not depend on the model behaving reasonably: a withheld tool is absent from the schema entirely. See Tool profiles.

Annotations. Tools are marked readOnlyHint or destructiveHint so the client can warn before running them. This is only a hint — a model can still call the tool — which is why the other layers exist.

Explicit confirmation. Destructive tools hard-refuse unless confirm=true is present in the payload, and the server instructions tell the model never to set it on its own initiative. Refusals return an error explaining what would have been affected, so the model must come back to you.

Name verification. Deleting a server, database, or database user also requires passing its exact name. If the name does not match what the API reports, the call is refused before anything is deleted. This catches a hallucinated or stale ID.

Credential redaction. RunCloud embeds Git deploy keys (pullKey1, pullKey2) directly in web application responses. These are stripped in RunCloudClient::decode() — at the transport boundary rather than per tool — so every current and future tool is covered. The redaction recurses, because RunCloud's response envelope is inconsistent: list endpoints wrap results in data while single-resource and delete endpoints return bare objects.

Database passwords are generated server-side rather than accepted from the model, which produces low-entropy passwords when asked to invent one. Generated passwords are 40 characters and alphanumeric, so they survive .env files and DSN connection strings without escaping.

Architecture

There is no intermediate REST API. The Laravel application is the MCP server:

Claude  ──JSON-RPC──▶  Laravel MCP  ──▶  MCP Tools  ──▶  RunCloudClient  ──HTTPS──▶  RunCloud v3
                       routes/ai.php     app/Mcp/Tools    app/Services/RunCloud

RunCloudClient owns authentication, retries, pagination clamping, error translation, and redaction. Tools own endpoint paths and input schemas.

Adding a resource means writing a tool that calls the client and registering it in App\Mcp\Servers\RunCloudServer.

Postman collection

postman/ contains a collection and environment covering 57 RunCloud v3 endpoints, useful for exercising the API directly when a tool misbehaves and you need to see the raw response.

Import both files, set apiToken in the environment, and run 00 · Connectivity > Ping.

Requests are annotated with the API's sharper edges — the inconsistent response envelope, the PATCH/PUT collision on the Git script endpoint, the DELETE that requires a JSON body, and the cron toggle that flips rather than sets. Destructive requests are isolated in their own folder.

Testing

php artisan test

The suite covers the client (auth headers, pagination clamping, error translation, redaction) and every tool, including that each destructive guard refuses for the right reason. HTTP calls are faked, so the tests never touch a real RunCloud account.

One test scans the server instructions and tool descriptions for runcloud-* references and fails if any does not resolve to a registered tool — prose that steers the model by tool name should not drift away from the code.

Deployment

This is an ordinary Laravel application and deploys anywhere Laravel runs, including Laravel Cloud. Set RUNCLOUD_API_TOKEN and APP_URL, run migrations, and ensure /mcp sits behind authentication.

Passport keys are not in the repository. They live in storage/oauth-*.key, which is gitignored because a private key must never be committed. Every environment needs its own, so your deploy pipeline must run:

php artisan passport:keys

Alternatively, supply them as PASSPORT_PRIVATE_KEY and PASSPORT_PUBLIC_KEY environment variables. Without keys, Passport throws LogicException: Invalid key supplied and every request to /mcp returns 500 rather than a 401.

APP_URL must match the public hostname. OAuth discovery advertises endpoints derived from it, and clients reject an authorization server served over plain HTTP. Behind a TLS-terminating proxy the application trusts X-Forwarded-* headers so generated URLs use https.

Coverage

Currently implemented: servers, web applications (create custom and WordPress, list, get, delete, settings), services, databases, database users, domains, basic SSL, system users, Git deployment, cron jobs, Supervisor, and the server firewall.

Not yet implemented: cloning and alias apps, advanced per-domain SSL, backups and snapshots, server provisioning, the per-application WAF, third-party integrations, atomic deployments, and script installers. These follow the same pattern — a tool class calling RunCloudClient — and the client and test scaffolding are reusable as-is.

API key permissions

RunCloud scopes API keys per resource type. A key that can list servers may still be refused on Git, returning 403. If a tool reports that the key lacks permission, enable the relevant scope under Workspace → Settings → API Key rather than assuming the tool is broken.

Security

The RunCloud API key grants full control over every server in the workspace. Do not commit it; .env is gitignored and only .env.example is tracked.

If you find a security issue, please open an issue or contact the maintainer rather than filing a public exploit.

License

MIT.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured