API reference

Remote tools

The remote tool surface over plain HTTP: list the exposed tools and call one. An explicit allowlist; only pure computations over the request body are reachable.

DELETE /v1/mcp

End a session

Accepted for hosts that send it when they disconnect. There is no session state to remove, so it answers 204 and changes nothing.

Auth
API key
Capability
none needed (MCP transport; the per-tool gate decides, not the endpoint)
Success
HTTP 204

Parameters

None.

Request body

None.

Example request

curl

curl -sS -X DELETE "https://api.afaprotocol.com/v1/mcp" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs"

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}

r = requests.delete(f"{API}/v1/mcp", headers=headers, timeout=30)
r.raise_for_status()
print(r.status_code)  # 204, no body

Example response

HTTP 204
(no body)

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.

What would show this is false

A second DELETE also answers 204. Idempotent by construction.

GET /v1/mcp

Not a stream

The server keeps no session and opens no server-initiated stream, so a GET answers 405 and names the methods it does accept.

Auth
API key
Capability
none needed (MCP transport; the per-tool gate decides, not the endpoint)
Success
HTTP 405

Parameters

None.

Request body

None.

Example request

curl

curl -sS -X GET "https://api.afaprotocol.com/v1/mcp" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs"

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}

r = requests.get(f"{API}/v1/mcp", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 405
{
  "allow": [
    "POST",
    "DELETE"
  ],
  "error": "method_not_allowed"
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.

What would show this is false

The Allow header and the body agree. A host that requires a server-opened GET stream cannot use this endpoint; there is no second transport for it.

POST /v1/mcp

The MCP endpoint (streamable HTTP)

One JSON-RPC 2.0 endpoint an MCP host connects to. It answers initialize, ping, tools/list and tools/call; every tool runs the same code the REST route runs, under your key.

Auth
API key (X-API-Key or Authorization: Bearer); any key of the account
Capability
none needed (MCP transport; the per-tool gate decides, not the endpoint)
Success
HTTP 200

Any API key of the account may call initialize and tools/list. Each tool needs the capability of the REST route it wraps; a key without it receives scope_missing naming that capability.

initialize negotiates protocolVersion 2025-06-18 or 2025-03-26. A batch (JSON array) is accepted.

A tool failure comes back as a result with isError true and the reason as text, never as a stack trace.

An Origin header from a host other than the console or localhost is refused with 403, which is the MCP defence against DNS rebinding.

Parameters

None.

Request body

None.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/mcp" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "id": 1,
  "jsonrpc": "2.0",
  "method": "tools/call",
  "params": {
    "arguments": {},
    "name": "afa_health"
  }
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "id": 1,
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
        "arguments": {},
        "name": "afa_health"
    }
}

r = requests.post(f"{API}/v1/mcp", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "id": 1,
  "jsonrpc": "2.0",
  "result": {
    "content": [
      {
        "text": "{\"status\": \"ok\", \"service\": \"afaprotocol\", \"version\": \"0.1.0\", \"storage\": \"postgres\"}",
        "type": "text"
      }
    ],
    "isError": false,
    "structuredContent": {
      "service": "afaprotocol",
      "status": "ok",
      "storage": "postgres",
      "version": "0.1.0"
    }
  }
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
401no key, or a key that expired or was revoked
403Origin header not allowed
200-32601unknown JSON-RPC method (JSON-RPC error object)
200-32700body is not JSON (JSON-RPC error object)

What would show this is false

Call tools/list with a key scoped to events:read: every tool is listed. Call afa_record_event with the same key: refused, naming events:write.

POST /v1/mcp/call

Call one remote tool

Dispatches one allowlisted tool with the arguments you supply and returns its result.

Auth
session cookie or API key
Capability
none needed (MCP transport; the per-tool gate decides, not the endpoint)
Success
HTTP 200

aa_intent_delta takes before and after envelopes. aa_interference_scan takes envelopes (a list) and an optional window_seconds, and returns the exclusive-scope conflicts among them.

Parameters

None.

Request body

A JSON object with no fixed fields. Follow the example.

Example request

curl

curl -sS -X POST "https://api.afaprotocol.com/v1/mcp/call" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs" \
  -H "Content-Type: application/json" \
  -d '{
  "arguments": {
    "after": {
      "action_class": "tool-call",
      "actor": "orchestrator-1",
      "envelope_id": "env-7d3c1a9e5b2f4068",
      "operator_plan": [
        "read config",
        "deploy staging",
        "notify channel"
      ],
      "scope": [
        "repo:read",
        "deploy:staging",
        "network:egress"
      ],
      "temporal": "now"
    },
    "before": {
      "action_class": "tool-call",
      "actor": "orchestrator-1",
      "envelope_id": "env-7d3c1a9e5b2f4068",
      "operator_plan": [
        "read config",
        "deploy staging"
      ],
      "scope": [
        "repo:read",
        "deploy:staging"
      ],
      "temporal": "now"
    }
  },
  "tool": "aa_intent_delta"
}'

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}
payload = {
    "arguments": {
        "after": {
            "action_class": "tool-call",
            "actor": "orchestrator-1",
            "envelope_id": "env-7d3c1a9e5b2f4068",
            "operator_plan": [
                "read config",
                "deploy staging",
                "notify channel"
            ],
            "scope": [
                "repo:read",
                "deploy:staging",
                "network:egress"
            ],
            "temporal": "now"
        },
        "before": {
            "action_class": "tool-call",
            "actor": "orchestrator-1",
            "envelope_id": "env-7d3c1a9e5b2f4068",
            "operator_plan": [
                "read config",
                "deploy staging"
            ],
            "scope": [
                "repo:read",
                "deploy:staging"
            ],
            "temporal": "now"
        }
    },
    "tool": "aa_intent_delta"
}

r = requests.post(f"{API}/v1/mcp/call", headers=headers, json=payload, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "result": {
    "action_class_changed": null,
    "actor_changed": false,
    "added_scope": [
      "network:egress"
    ],
    "envelope_ids": {
      "after": "env-7d3c1a9e5b2f4068",
      "before": "env-7d3c1a9e5b2f4068"
    },
    "operator_plan_changed": true,
    "operator_plan_diff": {
      "added": [
        "notify channel"
      ],
      "removed": [],
      "reordered": false
    },
    "prompt_hash_changed": false,
    "removed_scope": [],
    "temporal_changed": null
  },
  "tool": "aa_intent_delta"
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.
422validation errorA required field is missing or a value has the wrong type.
422missing_tool / invalid_argumentstool is not a non-empty string, or arguments is not an object.
404unknown_toolThe name is not on the allowlist. Same body for every such name.
400tool_errorThe tool rejected the arguments; reason says why.

What would show this is false

Swap before and after: added_scope and removed_scope trade places. The result depends on the two envelopes you sent and nothing else.

GET /v1/mcp/tools

List the exposed remote tools

Lists the tools reachable over HTTP, each with a one-line description.

Auth
session cookie or API key
Capability
none needed (MCP transport; listing carries no capability)
Success
HTTP 200

An explicit allowlist. Only tools whose result is a function of the request body are exposed; nothing that reads or writes server-side state appears here.

Parameters

None.

Request body

None.

Example request

curl

curl -sS -X GET "https://api.afaprotocol.com/v1/mcp/tools" \
  -H "X-API-Key: afa-beta-EXAMPLE-e4qs"

Python

import requests

API = "https://api.afaprotocol.com"
headers = {"X-API-Key": "afa-beta-EXAMPLE-e4qs"}

r = requests.get(f"{API}/v1/mcp/tools", headers=headers, timeout=30)
r.raise_for_status()
print(r.json())

Example response

HTTP 200
{
  "count": 2,
  "tools": [
    {
      "description": "Compute a structured diff between two authority envelopes supplied in the request: added and removed scope, actor and action changes, and plan changes.",
      "name": "aa_intent_delta"
    },
    {
      "description": "Detect concurrent exclusive-scope conflicts across a set of authority envelopes supplied in the request, within a time window.",
      "name": "aa_interference_scan"
    }
  ]
}

Errors

StatusCodeMeaning
401missing_token / invalid_or_expired_api_keyNo credential, an expired session, or a revoked or expired key.

What would show this is false

Call a name that is not listed: 404 unknown_tool with the same body for every unlisted name, so the refusal does not reveal what else exists.