MCP Reference
Drive Agiler from Claude, Cursor, and other MCP-aware agents — manage projects, files, SQL, backups, and more without leaving the chat.
Introduction
The Agiler MCP server lets AI agents — Claude Code, Claude Desktop, Cursor, and any other client that speaks the Model Context Protocol — read and write everything in your Agiler account: projects, files, SQL, backups, domains, environment variables, rules, logs, usage. It’s the same surface area as the REST API and the CLI, exposed as MCP tools so an agent can reason about your hosting and act on it in a single conversation.
If you’re new to MCP itself, the protocol’s introduction is the best starting point — it explains the JSON-RPC framing, the difference between tools and resources, and how clients negotiate capabilities.
Endpoint
https://mcp.agiler.io/v1
The transport is streamable HTTP — JSON-RPC 2.0 over POST, with server→client notifications delivered as Server-Sent Events on the same connection. Sessions are stateful: the server issues an Mcp-Session-Id header on the initial initialize request that the client must echo on every subsequent call.
Session timeout defaults to 15 minutes of inactivity. Re-initializing produces a fresh session ID; the previous one is silently dropped.
The server identifies itself as {"name":"agiler","version":"v1"} in the initialize response. It advertises tools with listChanged support, which the server uses to revoke tools mid-session when a token’s scopes change (see Scopes).
Authentication
Every request must carry a bearer token:
Authorization: Bearer ak_...
The token is an Agiler API key — the same kind the CLI and REST API accept. Generate one in the dashboard under Settings → API Keys. Token scopes are fixed at issue time and enforced per request: the server re-verifies the token against the public API on every JSON-RPC call, so revocation propagates within one round trip rather than waiting for a session to expire.
A request with no token, an invalid token, or a token that has been revoked returns 401 Unauthorized before any MCP message processing happens.
Setup
Point your MCP-capable agent at the endpoint above with your API key in the Authorization header. Configuration shape varies by client — three of the most common are below.
Claude Code — add to ~/.claude/mcp.json (global) or .mcp.json in the project root:
{
"mcpServers": {
"agiler": {
"type": "http",
"url": "https://mcp.agiler.io/v1",
"headers": {
"Authorization": "Bearer ak_..."
}
}
}
}
Then run claude mcp list to confirm the server is reachable and claude mcp tools agiler to enumerate the tools your token’s scopes unlock.
Claude Desktop — open Settings → Developer → Edit Config and add the same mcpServers entry as above. Restart the app so the new server is loaded.
Cursor — Settings → MCP → Add new MCP server, pick the HTTP transport, paste the endpoint, and set the Authorization header.
Generic — any client implementing the streamable-HTTP transport. The minimum handshake is one POST to /v1 with a JSON-RPC initialize request, then subsequent tools/list and tools/call requests carrying the Mcp-Session-Id header returned in the initialize response.
A quick smoke test, no MCP client required:
SESSION=$(curl -sD - https://mcp.agiler.io/v1 \
-H "Authorization: Bearer $AGILER_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
| awk '/Mcp-Session-Id/ {print $2}' | tr -d '\r')
curl -s https://mcp.agiler.io/v1 \
-H "Authorization: Bearer $AGILER_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "Mcp-Session-Id: $SESSION" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whoami","arguments":{}}}'
Scopes
Tools are gated by the token’s scopes. The base tools (whoami, regions_list, runtimes_list, rules_catalog, scopes_list) are always available to any authenticated user; everything else requires at least one scope from the table below.
| Scope | Tools gated |
|---|---|
projects:read | workspaces_list, workspaces_get, projects_list, projects_get |
projects:write | workspaces_create, projects_create, projects_update, projects_delete |
projects.files:read | files_list, files_get |
projects.files:write | files_write, files_write_many, files_move, files_copy, files_delete |
projects.sql:execute | sql_run, sql_list, sql_get, sql_cancel_or_delete |
projects.domains:read | domains_list |
projects.domains:write | domains_create, domains_update, domains_update_primary, domains_delete |
projects.variables:read | variables_list |
projects.variables:write | variables_create, variables_update, variables_delete |
projects.rules:read | rules_list, rules_get |
projects.rules:write | rules_create, rules_update, rules_delete |
projects.backups:read | backups_list, backups_get_policy |
projects.backups:write | backups_create, backups_update_policy, backups_delete, backups_restore |
projects.logs:read | logs_list |
projects.usage:read | usage_get |
Inheritance. Granting a broader scope implies the narrower ones, so most tokens only need a handful of explicit scopes:
projects:writeimpliesprojects:readand everyprojects.*scope (both:readand:write).projects:readimplies everyprojects.*:readscope.- A specific
:write(e.g.projects.files:write) implies its corresponding:read.
Use scopes_list to see this table from inside an agent, and whoami to inspect the scopes the current token actually carries. If a tool the agent expects is missing from tools/list, the token is probably under-scoped — re-issue it with the right scope and reconnect.
Mid-session revocation. If the token’s scopes change while a session is active (a scope is revoked, or the whole token is rotated), the affected tools are removed from the server on their next call and the client receives a notifications/tools/list_changed notification. The conversational agent can then re-list tools and continue with whatever’s still authorized — no hard disconnect.
Tools
Run tools/list to see exactly which tools your token unlocks. The full inventory, grouped by scope domain:
Base (no scope required):
| Tool | What it does |
|---|---|
whoami | Return the authenticated user’s profile plus effective_scopes |
regions_list | Regions where projects can be hosted |
runtimes_list | Available runtime stacks (e.g. php85, php83) |
rules_catalog | Valid fact / operator / action codes and ready-to-use rule templates |
scopes_list | Every MCP scope, the tools each scope gates, and a one-line summary |
Workspaces & projects (projects:read / projects:write):
| Tool | What it does |
|---|---|
workspaces_list | List workspaces the caller can access |
workspaces_get | Fetch a single workspace |
workspaces_create | Create a new workspace |
projects_list | List projects, optionally scoped to a workspace |
projects_get | Fetch a single project (accepts UUID or slug) |
projects_create | Create a project; bootstraps default rule, domain, and backup policy |
projects_update | Update name, runtime, workspace_id, active |
projects_delete | Permanently delete a project; requires confirm_name |
Files (projects.files:read / projects.files:write):
| Tool | What it does |
|---|---|
files_list | List files / directories under a project |
files_get | Fetch a single file’s contents + metadata (size, etag, modified_at) |
files_write | Write or overwrite one file; supports if_match (CAS) and if_none_match="*" (create-only) |
files_write_many | Batch write up to 100 files in one call; non-atomic — per-row results[] |
files_move | Atomic move; returns the new entry’s etag |
files_copy | Atomic copy; returns the new entry’s etag |
files_delete | Delete a single file |
SQL (projects.sql:execute):
| Tool | What it does |
|---|---|
sql_run | Run one SQL statement; requires explicit read_only and supports async=true |
sql_list | List recent statements (slim projection: id, status, sql_preview) |
sql_get | Fetch a statement plus a page of result rows (default 100, max 1000 per page) |
sql_cancel_or_delete | Cancel if running; once terminal, delete the record |
Domains (projects.domains:read / projects.domains:write):
| Tool | What it does |
|---|---|
domains_list | List all domains attached to a project |
domains_create | Attach a new domain; optionally mark it primary |
domains_update | Rename a domain and/or update its primary flag |
domains_update_primary | Set or clear the primary flag (legacy single-purpose; prefer domains_update) |
domains_delete | Detach a domain; auto-promotes another to primary if needed |
Variables (projects.variables:read / projects.variables:write):
| Tool | What it does |
|---|---|
variables_list | List env variables; value is omitted when sensitive=true |
variables_create | Create a variable; name must match ^[A-Z_][A-Z0-9_]*$ |
variables_update | Update name, value, or sensitive flag |
variables_delete | Delete a variable |
Rules (projects.rules:read / projects.rules:write):
| Tool | What it does |
|---|---|
rules_list | List rules in priority order |
rules_get | Fetch a single rule |
rules_create | Create a rule (call rules_catalog first for the value domain) |
rules_update | Update fields of an existing rule |
rules_delete | Delete a rule |
Backups (projects.backups:read / projects.backups:write):
| Tool | What it does |
|---|---|
backups_list | List backups, most-recent-first |
backups_get_policy | Read frequency_days / retention_days |
backups_create | Trigger an immediate manual backup |
backups_update_policy | Adjust cadence (0–30 days) and retention (1–365 days) |
backups_delete | Delete a backup record |
backups_restore | Restore a project; requires confirm_backup_created_at; supports drain_requests |
Observability (projects.logs:read / projects.usage:read):
| Tool | What it does |
|---|---|
logs_list | Tail request and application logs; since / until / q filters |
usage_get | Bucketed metrics (requests, bandwidth, compute, storage) with per-bucket and total summaries |
Every tool returns the canonical envelope described in Response shapes; every tool documents its arguments and side effects through its MCP description, which surfaces in clients that show tool documentation (Claude Desktop’s tool panel, Cursor’s MCP inspector).
Response shapes
All tools return a JSON object inside MCP’s tools/call structuredContent field. The envelope always carries a status (the underlying HTTP status the public API returned, surfaced verbatim) and one of four payload shapes:
List response — for any tool that paginates:
{
"status": 200,
"items": [ /* … */ ],
"pagination": {
"has_more": true,
"next_cursor": "abc123…"
},
"headers": { /* X-RateLimit-*, ETag, Last-Modified when present */ }
}
pagination.next_cursor is the canonical paging signal — pass it back as the cursor argument on the next call until has_more is false. The Link header that the underlying REST API returns is dropped here to avoid two competing cursor sources.
Single-resource response:
{
"status": 200,
"body": { /* the resource */ },
"headers": { /* … */ }
}
body is the same JSON object the REST API returns for the corresponding GET / POST / PATCH request.
No-content response — for DELETE and other writes that don’t return a body:
{
"status": 204,
"ok": true,
"headers": { /* present only when upstream returned metadata */ }
}
Batch response — only files_write_many returns this shape, since it fans out to N independent PUTs:
{
"status": 200,
"summary": { "ok": 12, "failed": 1 },
"results": [
{ "path": "wp-content/style.css", "ok": true, "status": 200, "etag": "\"…\"", "modified_at": "2026-05-25T…" },
{ "path": "wp-content/locked.php", "ok": false, "status": 412, "error": { "code": "precondition_failed", "message": "…" } }
]
}
The aggregate status is always 200; check summary or iterate results for per-row outcomes.
Errors
Tool failures come back as JSON-RPC errors with a structured payload the client can render to the user:
{
"code": "validation_error",
"message": "name must not be empty",
"field": "name"
}
Common codes:
| Code | Meaning |
|---|---|
validation_error | One of the arguments is missing, malformed, or out of range — field points at the offending input |
not_found | The referenced resource doesn’t exist or isn’t visible to the caller |
already_exists | A uniqueness constraint was violated; the envelope often includes the existing resource’s id |
precondition_failed | if_match / if_none_match rejected the write — the envelope includes current_etag and current_modified_at |
project_provisioning | Project is still being created; honor Retry-After and re-check projects_get.body.status |
project_in_maintenance | Project is offline for upgrade; honor Retry-After |
The same HTTP status codes used by the REST API surface in status so an agent can react with the same logic it’d use against the API directly. 429 Too Many Requests is the one to watch for — the headers field includes Retry-After and X-RateLimit-Reset so the agent can back off cleanly.
Patterns
A few conventions show up repeatedly across the tool surface. Knowing them once means knowing them everywhere.
Pagination. Every list tool accepts optional cursor and limit arguments. The first call may omit both; the response’s pagination.next_cursor feeds the next call until pagination.has_more is false. limit is clamped server-side; agents that pass an out-of-range value get a single, consistent error envelope rather than a silent downgrade.
Idempotency. Destructive tools require a second argument that acknowledges the live resource state, not a remembered identifier:
projects_deleterequiresconfirm_nameequal to the project’s currentbody.name. The server re-fetches the project and rejects withvalidation_errorif it doesn’t match — protection against acting on a stale id.backups_restorerequiresconfirm_backup_created_atequal to the backup’s currentbody.created_at. Same protection.
A second call after the resource is gone returns 404 not_found rather than re-deleting something new with the same name.
Compare-and-swap. File writes support optimistic concurrency:
if_match: "<etag>"— write only if the file’s current ETag matches; returns412 precondition_failedotherwise. Pair withfiles_get(or theetagreturned by a previousfiles_write/files_write_many/files_move/files_copy) to chain edits without a round-trip.if_none_match: "*"— create-only; returns412if the file already exists.
On 412, the error envelope includes the current etag and modified_at so the agent can decide whether to retry, merge, or abort without re-fetching.
Async SQL. Long-running statements can be dispatched with async: true:
{ "name": "sql_run", "arguments": {
"project": "<uuid>",
"sql": "ALTER TABLE wp_posts ADD INDEX idx_status_date (post_status, post_date);",
"read_only": false,
"async": true
}}
The call returns 202 pending with the statement’s id; poll sql_get until status transitions to success, error, or cancelled. Synchronous statements are capped at 180 s and 1000 rows inline.
Project status. A project’s body.status is one of pending (still provisioning), ok (ready), or maintenance (offline for upgrade work). File, SQL, and backup operations against a non-ok project return 503 with code: project_provisioning or project_in_maintenance — check projects_get and honor Retry-After before retrying.
Tool deregistration. When a tool the agent has been calling loses its required scope mid-session, the tool is silently removed from the server on its next invocation and the next tools/call returns an error directing the client to reconnect. Agents that subscribe to notifications/tools/list_changed get an immediate signal and can re-list tools without an explicit error.
Feedback
Bug reports, feature requests, and questions about the MCP server are welcome via Contact Us.
© 2026 Agiler. All rights reserved.
The WordPress® trademark is the intellectual property of the WordPress Foundation, and the Woo® and WooCommerce® trademarks are the intellectual property of WooCommerce, Inc. Uses of the WordPress®, Woo®, and WooCommerce® names in this website are for identification purposes only and do not imply an endorsement by WordPress Foundation or WooCommerce, Inc. Agiler is not endorsed or owned by, or affiliated with, the WordPress Foundation or WooCommerce, Inc.