API Reference

MCP Protocol

How airlock implements the Model Context Protocol: endpoints, meta-tools, and tool execution.

Airlock implements the Model Context Protocol (MCP) for AI agent communication.

Overview

MCP is a JSON-RPC based protocol that allows AI agents to discover and call tools. Airlock exposes your API operations as MCP tools.

Endpoints

Airlock exposes a single MCP HTTP endpoint per organization. Both URLs below are served by the same handler; agents can use either:

POST https://mcp.air-lock.ai/org/{slug}
POST https://mcp.air-lock.ai/

The endpoint provides access to all integrations in your organization through meta-tools:

Meta-ToolDescription
list_servicesList all available integrations in the organization
search_toolsSearch for tools across all integrations by keyword
describe_toolsGet detailed descriptions of specific tools
execute_toolExecute a tool on a specific integration
activate_skillActivate a skill to get its instructions and attachments
read_skill_attachmentLoad the full content of a skill attachment on demand
list_agentsList portable agents published by the organization
describe_agentGet the full spec of a portable agent
export_agentRender a portable agent in a host's native format

Used by Claude Desktop, Claude Code, Augment, and other MCP clients.

Protocol versions

Airlock negotiates the protocol per request via the MCP-Protocol-Version header. Supported revisions:

2026-07-28   2025-11-25   2025-06-18   2025-03-26   2024-11-05   2024-10-07

Omitting the header is treated as 2025-03-26. Any other value returns HTTP 400 with JSON-RPC error -32022 and a data.supported list of the accepted values.

The two generations use different request models, and airlock serves both from the same endpoint:

RevisionModel
2026-07-28Stateless. No initialize handshake and no Mcp-Session-Id. Clients call server/discover for capabilities and instructions, and carry their identity and capabilities on every request under params._meta.
2025-* / 2024-*Sessionful. The initialize handshake shown below, plus an Mcp-Session-Id on subsequent requests.

Authentication

Bearer credentials

The endpoint accepts three kinds of credential in Authorization: Bearer <token>:

CredentialPrefixWhere it comes from
OAuth access tokeneyJ… (JWT)The MCP OAuth flow below — what interactive clients use
Personal API tokenalk_pat_…Settings → Account in the Control Room. Acts as you: same role, access grants, and tool filtering as an OAuth session, and stops working when your org membership does
Service tokenalk_svc_…Issued to a service account for headless agents and CI

Personal and service tokens skip the OAuth round trip entirely, which makes them the practical choice for scripts, cron jobs, and CI. Note that personal API tokens are off by default — an admin enables them for the organization under Settings → Security. See Personal API tokens.

MCP OAuth 2.0

Airlock supports the MCP OAuth 2.0 specification with PKCE and RFC 9728 protected-resource discovery:

  1. An unauthenticated request returns 401 with WWW-Authenticate: Bearer resource_metadata="…".
  2. Fetch the protected-resource metadata it names: GET https://mcp.air-lock.ai/.well-known/oauth-protected-resource/org/{slug} (or /.well-known/oauth-protected-resource for the root endpoint). It names the authorization server.
  3. Fetch the authorization-server metadata: GET https://mcp.air-lock.ai/.well-known/oauth-authorization-server.
  4. Register the client: POST https://mcp.air-lock.ai/register.
  5. Authorize: redirect the user to the authorization_endpoint from the metadata — https://control-room.air-lock.ai/mcp/authorize in production — with your PKCE parameters. Note this endpoint is on the Control Room domain, not the MCP domain.
  6. Token exchange: POST https://mcp.air-lock.ai/token with grant_type=authorization_code.
  7. Token refresh: POST https://mcp.air-lock.ai/token with grant_type=refresh_token.

Token Exchange

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code={code}&code_verifier={verifier}&redirect_uri={uri}

Response includes an opaque refresh token for long-lived sessions:

{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 28800,
  "refresh_token": "abc123...",
  "id_token": "eyJ...",
  "scope": "openid profile email"
}

Token Refresh

When the access token expires, request new tokens:

POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token={refresh_token}&client_id={client_id}
{
  "access_token": "eyJ...",
  "token_type": "Bearer",
  "expires_in": 28800,
  "refresh_token": "def456...",
  "id_token": "eyJ...",
  "scope": "openid profile email"
}

Refresh tokens are single-use and rotated: every successful refresh returns a new refresh_token that you must store in place of the one you sent. The token you presented stays valid only for a ~10-second grace window (so a retried request converges on the same result) and then stops working. A client that keeps replaying the original token starts failing with invalid_grant seconds later and is forced back through a full sign-in.

ParameterLifetimeNotes
Access token8 hoursJWT, used in Authorization: Bearer header
Refresh token30 daysOpaque token, rotated on every use. Rotation does not extend the session: each successor wraps the same underlying credential from your original sign-in, so the chain stops working 30 days after that sign-in
ID token8 hoursJWT with user claims

JSON-RPC Methods

initialize (legacy handshake)

Initialize the MCP session. This is the pre-2026-07-28 model — clients on 2026-07-28 call server/discover instead and never open a session:

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {
      "name": "my-client",
      "version": "1.0.0"
    }
  }
}

Response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {}
    },
    "serverInfo": {
      "name": "<Your Organization Name> (Organization)",
      "version": "1.0.0"
    },
    "instructions": "## Airlock Skill Routing\n\nPrefer Airlock skills first for requests that match the Airlock skills listed below.\n\n## Available Skills\n\n- **Code Review**: Review pull requests",
    "_airlock": {
      "bootstrap_version": 1,
      "model_catalog_version": 1,
      "skill_source": "preferred",
      "disable_local_skills": false,
      "routing_mode": "airlock_first",
      "airlock_authority_scope": "matching_skills",
      "activation_tool": "activate_skill",
      "attachment_tool": "read_skill_attachment",
      "skills": [
        {
          "id": "skill-1",
          "name": "Code Review",
          "description": "Review pull requests",
          "ownership": "organization",
          "tags": ["engineering"]
        }
      ]
    }
  }
}

The serverInfo.name is your organization's display name followed by (Organization) — for example, Acme (Organization).

For organization-wide airlock MCP connections the bootstrap is delivered as a vendor extension. On the legacy (2025-* / 2024-*) path it rides the initialize response at result._airlock. On the 2026-07-28 path there is no handshake, so the same object is returned under result._meta["airlock/bootstrap"] on server/discover and tools/list. Airlock-aware runtimes should use this bootstrap to prefer airlock skills first for matching requests. Local skills may still remain available when no airlock skill matches.

model_catalog_version is the version of the model catalog this deployment serves — a runtime that caches the catalog can compare it on each handshake and re-fetch when it changes.

2026-07-28 responses also carry result._meta.correlation_id, and result._meta.trace_id when the request included a valid W3C traceparent header.

Skill catalog visibility depends on the client. The skills catalog is delivered in result.instructions and result._airlock.skills, but only clients that forward initialize.result.instructions to the model as system context surface it to the agent. Claude Code and claude.ai do this; the Anthropic Managed Agents runtime does not (verified 2026-04-21). For runtimes that drop result.instructions, instruct the agent in its system prompt to call airlock-management/list_skills (via execute_tool) before activate_skill. See the Skills guide for the workaround and a copy-paste prompt fragment.

tools/list

List available tools:

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}

The organization endpoint exposes a small set of meta-tools, not your individual API operations. tools/list returns the meta-tools, and you discover and run individual operations through them.

Response:

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      { "name": "list_services", "description": "List all connected services (APIs) available in this organization." },
      { "name": "search_tools", "description": "Search for available tools across all connected services by keyword." },
      { "name": "describe_tools", "description": "Get full input schemas for specific tools by their exact namespaced names." },
      { "name": "execute_tool", "description": "Execute a tool on any connected service." },
      { "name": "activate_skill", "description": "Activate a skill to get its instructions and attachments." },
      { "name": "read_skill_attachment", "description": "Read the full content of a skill attachment." },
      { "name": "list_agents", "description": "List portable agents published by this organization." },
      { "name": "describe_agent", "description": "Get the full spec of a portable agent." },
      { "name": "export_agent", "description": "Render a portable agent in a host's native format." }
    ]
  }
}

Airlock's virtual airlock/check_status tool (used to poll the outcome of an approval-gated call) is also available through these meta-tools.

Clients that negotiate the MCP Apps UI extension (io.modelcontextprotocol/ui) additionally see three tools — airlock_approval_card, airlock_ai_policy_sign_card, and airlock_widget.

The first two are render-only: they display a card inside the conversation but never perform the action they show, because approvals and policy signatures always happen in your authenticated Control Room session. airlock_widget is different — it actually executes the upstream tool it renders, through the same pipeline as execute_tool (grants, security blocks, policy, budget, and audit all apply), then displays the result in the airlock broker frame.

tools/call

Individual operations are not listed by tools/list and are not invoked directly by operationId. Instead, discover them with search_tools / describe_tools, then run them through execute_tool. Tools are referenced by their namespaced name in the form service-slug/tool-name (e.g. github/create_issue).

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "execute_tool",
    "arguments": {
      "tool": "github/list_issues",
      "arguments": {
        "limit": 10
      }
    }
  }
}

Response (success):

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[{\"id\": 1, \"title\": \"Fix login bug\"}, ...]"
      }
    ]
  }
}

Response (approval required — legacy format):

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"status\": \"PENDING_APPROVAL\", \"requestId\": \"abc123\", \"taskId\": \"task-abc123\", \"tool\": \"github/create_issue\", \"approvalUrl\": \"https://control-room.air-lock.ai/requests/abc123\", \"message\": \"This action requires human approval. The user has been notified and can approve at: https://control-room.air-lock.ai/requests/abc123. To check the result, use execute_tool with tool \\\"airlock/check_status\\\" and request_id \\\"abc123\\\".\"}"
      }
    ]
  }
}

Tip: The approval response shown above is a structured JSON object with the requestId, a taskId, the gated tool, the approvalUrl, and a human-readable message. To observe the decision, the agent calls execute_tool with tool airlock/check_status and the request_id (a poll interval of ~15s is suggested).

The exact payload depends on what your client negotiated. Clients that implement SEP-1036 URL elicitation receive "status": "AWAITING_USER_ACTION" with an elicitation object (mode: "url", elicitationId, url) instead of "status": "PENDING_APPROVAL" with approvalUrl. Clients that support the MCP Apps UI extension receive PENDING_APPROVAL plus a ui object pointing at the airlock_approval_card tool. Treat both PENDING_APPROVAL and AWAITING_USER_ACTION as "awaiting a human", and read the link from approvalUrl or elicitation.url — whichever is present.

MCP Tasks

On the 2026-07-28 path, airlock implements the MCP Tasks extension (SEP-2663). A client that opts in receives approval-gated calls as tasks and resolves them with the standard protocol methods instead of airlock's airlock/check_status tool.

Opting in requires two things, both per request: the 2026-07-28 protocol version, and the tasks extension declared in params._meta. The examples above use the legacy handshake, where the declaration is ignored and you always get the text payload.

// POST with header: MCP-Protocol-Version: 2026-07-28
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "execute_tool",
    "arguments": {
      "tool": "github/create_issue",
      "arguments": { "title": "Fix login bug" }
    },
    "_meta": {
      "io.modelcontextprotocol/clientCapabilities": {
        "extensions": { "io.modelcontextprotocol/tasks": {} }
      }
    }
  }
}

An approval-gated call then returns a CreateTaskResult instead of the payloads above:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "resultType": "task",
    "taskId": "task-abc123",
    "status": "working",
    "statusMessage": "Waiting for approval — review and approve at: https://control-room.air-lock.ai/requests/abc123",
    "createdAt": "2026-07-26T10:00:00.000Z",
    "lastUpdatedAt": "2026-07-26T10:00:00.000Z",
    "ttlMs": 86400000,
    "pollIntervalMs": 15000
  }
}

The task fields are flat — there is no task wrapper object. Because a task result has no content blocks, the approval URL rides statusMessage while the task is working.

The declaration is per request. A tasks/get that omits it is answered with -32021 Missing Required Client Capability, so send the same _meta block on every poll. Clients that do not declare the extension keep the legacy format above, byte for byte.

MethodBehavior
tasks/getReturns the task fields flat, under resultType: "complete". A completed task inlines the original tool result under result; a failed one carries error.
tasks/cancelCooperative — always acknowledged with an empty result for a known task. Airlock honors it by withdrawing a still-pending approval, unless execution has already been claimed. Observe the real outcome with tasks/get.
tasks/updateAcknowledged with an empty result.

tasks/result and tasks/list were removed when Tasks left the core spec and are not served.

Approval statuses map onto task statuses as: pending → working, approved → completed (with the tool's output), rejected → cancelled, expired → failed.

Tool Mapping

OpenAPI operations map to MCP tools:

OpenAPIMCP Tool
operationId: list_usersTool name: list_users
No operationIdName derived from method + path — GET /users/{id} becomes get_users_id
description (falls back to summary)Tool description
Request body schemaTool inputSchema
Path parametersAdded to inputSchema.properties
Query parametersAdded to inputSchema.properties

Tool names are truncated to 64 characters. Use the truncated name in policy rules — it is the name both the runtime and the policy engine match on.

Error Handling

Airlock returns failures in two different shapes, and a client needs to check both.

Tool errors

A tool that is denied by policy, blocked, unavailable under your access grants, or that fails upstream returns a normal JSON-RPC result with isError: true and the reason in the text content block — not a JSON-RPC error:

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      { "type": "text", "text": "Denied by policy: create_issue requires approval" }
    ],
    "isError": true
  }
}

Always check result.isError, not just the presence of error. A client that only inspects error will silently treat a policy-denied call as a success.

The AI use policy gate is a notable case — the call was understood and deliberately not performed:

{
  "content": [{ "type": "text", "text": "AI Use Policy acknowledgement required. … No tool call was made." }],
  "isError": true,
  "structuredContent": {
    "status": "POLICY_SIGNATURE_REQUIRED",
    "signUrl": "https://control-room.air-lock.ai/policy"
  }
}

structuredContent is only present for clients that negotiated the MCP Apps UI extension or SEP-1036 URL elicitation. A plain client receives just the text block with isError: true, with the sign-in URL inside the message text.

No upstream request was made. Surface the sign URL to the user and retry after they sign.

Protocol errors

Malformed JSON, unknown methods, invalid params, and unsupported protocol versions return a JSON-RPC error object:

{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32022,
    "message": "Unsupported protocol version",
    "data": {
      "supported": ["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05", "2024-10-07"],
      "requested": "2027-01-01"
    }
  }
}

Error Codes

CodeMeaning
-32700Parse error
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error
-32020Header mismatch — the Mcp-Method or Mcp-Name request header disagrees with the JSON-RPC body (2026-07-28 only); data.mismatch says which
-32021Missing required client capability — e.g. a tasks/* call from a request that did not declare io.modelcontextprotocol/tasks; data.requiredCapabilities lists what to declare
-32022Unsupported MCP-Protocol-Version; data.supported lists the accepted revisions

Rate Limiting

Airlock does not enforce a request-count cap on the MCP endpoint — request counts are tracked for analytics only (surfaced on the Usage and Analytics pages). Runtime spend is instead bounded per integration by token budgets: when an integration's hard token limit is crossed, its tool calls return an error until the budget resets or is raised.

An abuse-protection rate limit still applies: requests that trip it receive an HTTP 429 Too Many Requests response with a Retry-After header indicating how long to wait before retrying.