laravel-forge-readonly-mcp
Provides read-only access to the Laravel Forge API, enabling AI assistants to query server, site, deployment, SSL, log, and other infrastructure details without making changes.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@laravel-forge-readonly-mcpshow my servers and their PHP versions"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
laravel-forge-readonly-mcp
A read-only MCP server for the Laravel Forge API.
Point an AI assistant at your Forge infrastructure to answer questions — which server hosts a site, when it last deployed, what's in the error log, which certificates are about to expire — without giving it the ability to change anything.
It cannot deploy, restart, provision, restore, or delete. Not because it's asked not to: there
is no tool for it and no code path for it. The HTTP client only issues GET, and the verb isn't
a parameter anywhere.
Built on Forge's current API (
https://forge.laravel.com/api). The older/api/v1is deprecated and Forge discontinues it on 31 August 2026.
Install
npm install -g laravel-forge-readonly-mcpCreate a token in Forge → account dashboard → API. Recommended scopes are below.
Claude Code
claude mcp add forge -e FORGE_API_TOKEN=your-token -- npx -y laravel-forge-readonly-mcpClaude Desktop / any MCP client
{
"mcpServers": {
"forge": {
"command": "npx",
"args": ["-y", "laravel-forge-readonly-mcp"],
"env": {
"FORGE_API_TOKEN": "your-token",
"FORGE_ORGANIZATION": "your-org-slug"
}
}
}
}FORGE_ORGANIZATION is optional but worth setting if you have one organisation: tools can then
omit the slug, and the assistant doesn't need a lookup call first.
Related MCP server: Forge MCP Server
Options
Flag | Environment variable | Default |
|
| required |
|
| none — pass |
|
|
|
|
|
|
|
| off — see below |
Credentials come only from flags or the environment, never from a tool argument, so nothing the model says can change which token, base URL or default organisation is used.
Tools
19 named tools for the common reads, plus one escape hatch. Forge publishes 133 GET
endpoints; mapping them 1:1 would cost 30–50k tokens of JSON schema in your context window
before you asked anything, so the frequent paths get real tools and forge_get covers the rest
against the same allowlist.
Tool | What it answers |
| Which orgs can this token see? |
| Provider, region, size, PHP version, IPs, provisioning state |
| Recent Forge activity on a server — the closest thing to an audit trail |
| Installed PHP versions and the default — find servers on an EOL version |
| Every site across all servers: domain, repo, PHP, deploy state |
| Domains, aliases and SSL expiry |
| Deployment history: commit, author, status, duration |
| The commands each deploy runs |
| Application, Nginx access or Nginx error log |
| Cron jobs for a server or one site, with last-run state |
| Daemons and queue workers, and whether they're running |
| Is Horizon/Octane/Reverb/Pulse/Inertia/scheduler on? In maintenance mode? |
| History of commands run from the Commands panel |
| Database schemas, or users and their grants (never contents) |
| Is this server actually being backed up? |
| Any other documented Forge GET — monitors, firewall rules, Nginx templates, recipes, teams, providers… |
List tools take page_size, cursor, sort, include and filter. Cursors are never followed
automatically — see rate limiting.
Token scopes: the part worth reading
Forge has genuine view-only scopes, so a token can be minted that is incapable of writing. These five reach 76 of the 120 endpoints this server exposes (plus 13 more that need no scope at all) with zero write capability:
organization:view server:view user:view team:view recipe:viewTwo other view scopes are not worth granting. credential:view unlocks exactly one endpoint
this server exposes (a VPC lookup) — its other three are cloud-provider credentials, which are
withheld — so it buys nothing while widening what a leaked
token could read. resources:view currently gates no endpoint at all; add it when Forge ships the
managed-database and object-storage API.
The wrinkle: 30 reads sit behind manage scopes, so getting the read also buys the write.
Two are worth it, and the rest mostly aren't:
Scope | Reads it unlocks | Writes it also grants | Suggested |
| 8 — all domains and certificates | add / modify / delete a domain | Add — this is SSL and domain visibility |
| 4 — application and Nginx logs, server logs | clearing logs, nothing else | Add — smallest blast radius, biggest payoff |
| 6 | redirect rules, webhooks, healthcheck | Optional |
| 2 — Nginx config | rewrite the Nginx config | Skip unless needed |
| 2 — firewall rules | change the firewall | Skip unless needed |
| 2 | basic-auth rules | Skip |
| 1 — deployment log | deploy the site | Don't |
| 2 — backup instances | restore a backup | Don't |
| 1 — process log | create daemons, enable Horizon/Octane/Pulse | Don't |
| 2 | delete storage providers | Don't |
Suggested token — seven scopes:
organization:view server:view user:view team:view recipe:view site:meta server:manage-logsThat reaches 101 of the 120 endpoints this server exposes, and the token cannot deploy, restore a backup, change a firewall, or rewrite an Nginx config. Its only write powers are clearing a log and modifying a domain.
For a token with literally no write capability, drop site:meta and server:manage-logs:
89 endpoints, but no logs and no domain or certificate visibility.
Every scope-gated endpoint stays available in this package regardless of what you grant — your token's scopes decide, not us. A 403 says which scope was missing, and whether that scope would also hand over write access:
Forge denied access to /orgs/acme/servers/1/sites/2/nginx (403). This endpoint requires the
"site:manage-nginx" scope, which your token does not have. Note that "site:manage-nginx" also
grants the matching write permissions — this package deliberately does not require you to hold
it, and will simply not serve this read if you would rather keep the token read-only.What it deliberately won't read
A GET being read-only doesn't make it safe to put in front of a language model. Thirteen
of Forge's reads are withheld by default, for two different reasons.
They'd leak a secret:
…/sites/{site}/environment— Forge documents this as "Get .env content": database passwords,APP_KEY, every third-party API key the app holdsComposer and npm registry credentials
Cloud provider credentials (
/server-credentials)SSH keys and deploy keys
They'd leak a write capability:
…/deployments/deploy-hookreturns, in Forge's words, "the deployment trigger URL" — a URL that triggers a deploy with no authentication. Handing that string to a model defeats the entire point of a read-only server, however much the verb wasGET.
That last one is why this package doesn't implement read-only as "allow all GETs". Refusals explain themselves rather than looking like a bug:
/orgs/acme/servers/1/sites/2/deployments/deploy-hook is a read this server deliberately
withholds: the deployment trigger URL — an unauthenticated deploy capability, so reading it
would hand out the ability to deploy. Exposing it would grant a write capability, which
defeats the purpose of a read-only server.…and one field, wherever it appears
Withholding an endpoint is worthless if the thing it guards is duplicated into a payload you do serve. Forge returns the deploy trigger URL inline on ordinary site reads:
{ "attributes": { "deployment_url": "https://forge.laravel.com/servers/1/sites/2/deploy/http?token=…" } }That's the same unauthenticated deploy capability as the deploy-hook endpoint, arriving through
get_site and list_sites. So every response is also filtered field-by-field, and
deployment_url comes back as:
[withheld: deploy trigger URL — an unauthenticated capability to deploy the site, not a read]The key is preserved so you can see something was withheld, and a null value passes through
untouched — that legitimately means quick-deploy is off. The general lesson, learned by probing
the live API rather than reading its docs: for a read-only surface over someone else's API the
unit of danger is a field, not an endpoint. Fields travel.
--unsafe-allow-secrets re-enables all thirteen endpoints and this field. It's off by default and
named to be hard to turn on by accident. If you enable it, those values will appear in model
context and in transcripts and logs wherever they're kept.
How read-only is enforced
Four independent layers, so no single mistake is sufficient:
No write tool exists. Only reads are implemented, so there is nothing to disable.
The client can only
GET. The verb is a literal in one place and is never threaded through from configuration or arguments.Every path is matched against an allowlist of Forge's documented GET templates before a request is built. This is what makes
forge_getsafe: the model picks a path, but only from a set already reviewed. Denials are checked before allowances, because an excluded literal path (…/deployments/deploy-hook) collides with a legitimate template (…/deployments/{deployment}) — in a deny-by-default boundary the denial must win.Every response is filtered field-by-field on the way out, because the endpoint allowlist alone let the deploy trigger URL through inline on ordinary site reads.
Your token's scopes, if you mint it view-only — defence in depth rather than the primary control.
The allowlist is generated from Forge's OpenAPI spec (npm run generate:paths), so a typo
can't silently create a hole. npm run check:drift — run in CI on every PR and weekly — fails if
a new GET appears unreviewed, if one we rely on disappears, if a read moves behind a
write-granting scope, or, most importantly, if a withheld path is renamed such that its
exclusion silently stops protecting anything.
Rate limiting
Forge allows 60 requests per minute per token. This package keeps a local sliding window and
also honours X-RateLimit-* and Retry-After, retrying a 429 twice before giving up with advice
to narrow the request.
Cursors are never followed automatically. A single "check every site" call must not be able
to spend the whole minute, so pagination is explicit: read meta.next_cursor and pass it back as
cursor.
Development
npm install
npm run lint # tsc --noEmit
npm test # unit tests (no network)
npm run build
node scripts/smoke.mjs # drives the built binary over stdio; no network needed
npm run generate:paths # regenerate the path table from Forge's spec
npm run check:drift # fail if the package has drifted from Forge's APIFORGE_SPEC_FILE=/path/to/spec.json makes the generator and drift check read a local spec
instead of fetching.
Using this behind an MCP gateway
Tool names are intentionally un-prefixed (list_servers, not forge_list_servers), because
aggregating gateways add their own namespace and a self-prefixed name becomes
forge__forge_list_servers downstream.
Annotations split by kind: this package sets readOnlyHint and idempotentHint (facts about the
tool, true everywhere) plus a plain title. A gateway is free to override the top-level title
with its own house convention.
main and bin are the same built entry, so a gateway can require.resolve() the package and
run it under its own Node rather than needing a global install.
Licence
MIT. Not affiliated with or endorsed by Laravel. "Laravel" and "Laravel Forge" are trademarks of Taylor Otwell; this is an independent client for their public API.
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/brynj-digital/laravel-forge-readonly-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server