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 sessionless streamable HTTP — every JSON-RPC 2.0 request is an independent POST. Modern clients use protocol version 2026-07-28, attach protocol/client metadata to every request, and call server/discover instead of performing an initialize handshake. A response is either one JSON object or a request-scoped Server-Sent Events stream.
The server never issues or consumes an Mcp-Session-Id. Standalone GET and DELETE requests return 405 Method Not Allowed; Last-Event-ID and legacy session headers are ignored because streams are not resumable. Older initialize requests are still accepted for compatibility, but each creates only a temporary request-scoped server and does not return a session ID.
The server identifies itself as {"name":"agiler","version":"v1"} through discovery. It advertises tools with listChanged: false. Because the authorized catalog depends on the freshly verified token, every tools/list result has ttlMs: 0 and cacheScope: "private".
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 Profile → API Keys. The server re-verifies the token and rebuilds its authorized tool catalog on every JSON-RPC request, so token or scope revocation takes effect on the very next request.
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 — use protocol version 2026-07-28. Every POST must include MCP-Protocol-Version and Mcp-Method; tools/call also requires Mcp-Name. The protocol version in the HTTP header must match params._meta.io.modelcontextprotocol/protocolVersion in the JSON-RPC body. Each request also carries client information and capabilities in _meta; there is no session header or initialization state to retain.
A quick smoke test, no MCP client required:
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-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: server/discover" \
-d '{"jsonrpc":"2.0","id":1,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
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-Protocol-Version: 2026-07-28" \
-H "Mcp-Method: tools/call" \
-H "Mcp-Name: whoami" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whoami","arguments":{},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"curl","version":"1"},"io.modelcontextprotocol/clientCapabilities":{}}}}'
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.wp:execute | wp_run, wp_list, wp_get, wp_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 — grant or issue the required scope, then list tools again.
Immediate revocation. There is no session-scoped catalog. If a scope is revoked, the next request rebuilds the tool set from the newly verified scopes. A stale invocation of the removed tool is rejected, while unscoped tools and tools covered by the remaining scopes continue to work. Clients should re-run tools/list after an authorization error; the private zero-TTL response is safe to refresh on every use.
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 or delete a SQL statement |
WordPress (wp-cli) (projects.wp:execute):
| Tool | What it does |
|---|---|
wp_run | Run one wp-cli command — arguments without the leading wp — and supports async=true |
wp_list | List recent commands (slim projection: id, status, command_preview) |
wp_get | Fetch a command plus a page of output lines (default 100, max 1000 per page) |
wp_cancel_or_delete | Cancel or delete a wp-cli command |
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. An upstream REST API 429 Too Many Requests appears in the tool error envelope with its Retry-After and X-RateLimit-* metadata.
The MCP endpoint also has its own admission limits. Those checks run before tool execution and return an HTTP 429 directly with Retry-After: 1. Request-rate rejections also include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset. Retry after at least the advertised delay, preferably with jitter; repeated immediate retries remain subject to the same one-second window. A request body over the configured per-request cap returns HTTP 413 Request Entity Too Large instead — reduce or split the request rather than retrying it unchanged.
Admission limits
Admission is non-blocking: a request that cannot obtain rate, concurrency, or aggregate-body capacity is rejected immediately rather than waiting in an in-process queue. The default limits are per MCP server process:
| Configuration | Default |
|---|---|
max_request_body_bytes | 67,108,864 (64 MiB) |
max_inflight_body_bytes | 1,073,741,824 (1 GiB) |
global_rate_limit_per_second | 500 |
peer_rate_limit_per_second | 100 |
user_rate_limit_per_second | 30 |
max_concurrent_requests | 256 |
max_concurrent_requests_per_peer | 32 |
max_concurrent_requests_per_user | 16 |
In server configuration, zero selects the default and negative values are invalid. max_inflight_body_bytes must be at least max_request_body_bytes; peer and user concurrency limits cannot exceed the global concurrency limit.
Global and socket-peer checks run before bearer-token verification. The peer key comes only from the direct socket’s RemoteAddr; forwarded-address headers are intentionally not trusted. If every replica sees one load balancer as its direct peer, that traffic shares the peer quota. Authenticated-user limits use the verified user ID, so two API keys owned by the same user share one quota.
Known Content-Length requests reserve their declared length from the aggregate body budget. Chunked or otherwise unknown-length requests reserve the full per-request cap until the request completes. This budget counts raw request bytes, not decoded JSON or total heap use; decoded strings and tool arguments can use several times the raw size.
Limits are per process, so adding replicas increases fleet-wide capacity. Enforce a fleet-wide ceiling at the load balancer or gateway when needed.
When rolling out or tuning different values, monitor rejection scope/rate, token-verification latency and errors, heap usage, and goroutine count together. Raising the raw body budget alone does not guarantee equivalent heap headroom.
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.
Authorization refresh. Tool catalogs are request-scoped. When a required scope disappears, a stale tools/call is rejected and the next tools/list omits the tool. Re-list tools and continue with the remaining catalog; no server session needs to be reconnected or terminated.
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.