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.
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.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。