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.
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
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No 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.
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
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No 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.
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
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 401 | no key, or a key that expired or was revoked | |
| 403 | Origin header not allowed | |
| 200 | -32601 | unknown JSON-RPC method (JSON-RPC error object) |
| 200 | -32700 | body 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.
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
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No credential, an expired session, or a revoked or expired key. |
| 422 | validation error | A required field is missing or a value has the wrong type. |
| 422 | missing_tool / invalid_arguments | tool is not a non-empty string, or arguments is not an object. |
| 404 | unknown_tool | The name is not on the allowlist. Same body for every such name. |
| 400 | tool_error | The 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.
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
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_token / invalid_or_expired_api_key | No 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.