Skip to content

Developer guide

Build an adapter

An ai4all adapter is a small MCP server. Which transport you implement depends on which circle you belong to: Group A adapters run as local child processes over stdio; Group B adapters answer streamable HTTP. Everything else — schema shape, erasure contract, error codes — is identical.

Group A · stdio

The host spawns your adapter as a child process and speaks newline-delimited JSON-RPC over stdin/stdout. No port is opened, no packet leaves the device. Write logs to stderr — anything on stdout that is not a JSON-RPC message corrupts the stream.

Group B · HTTP stream

Your adapter answers HTTP POST with either a JSON response or an SSE stream. Every request must be independently authorised — the host will not maintain a session for you, and any node may send the next call.

1 · Declare your tools

The schema is the contract the pre-execution firewall enforces. Be strict: name every property, type every property, mark what is required, and set additionalProperties: false. A loose schema is a firewall bypass you wrote yourself.

tool schema · json
{
  "name": "memory_search",
  "description": "Search connected memory networks and return ranked spans.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query":  { "type": "string",  "description": "Natural-language search." },
      "scope":  { "type": "string",  "enum": ["sovereign", "non-sovereign", "both"] },
      "limit":  { "type": "integer", "description": "Maximum spans to return." }
    },
    "required": ["query"],
    "additionalProperties": false
  }
}

2a · Group A adapter (stdio)

adapter.py · python
# Group A — sovereign adapter, spawned as a child process.
# The host writes JSON-RPC to stdin and reads it from stdout.
# Nothing leaves the machine.

import sys, json

TOOLS = [{
    "name": "silmari_active_forget",
    "description": "Cryptographically erase a memory block on this device.",
    "inputSchema": {
        "type": "object",
        "properties": {"block_id": {"type": "string"}},
        "required": ["block_id"],
    },
}]

def respond(rid, result=None, error=None):
    msg = {"jsonrpc": "2.0", "id": rid}
    msg["error" if error else "result"] = error or result
    sys.stdout.write(json.dumps(msg) + "\n")
    sys.stdout.flush()

for line in sys.stdin:
    if not line.strip():
        continue
    req = json.loads(line)
    rid = req.get("id")
    if rid is None:            # notification: never reply
        continue
    if req.get("method") == "tools/list":
        respond(rid, {"tools": TOOLS})
    elif req.get("method") == "tools/call":
        args = req.get("params", {}).get("arguments", {})
        block = args.get("block_id")
        # ... perform the local erasure ...
        respond(rid, {"content": [{"type": "text",
                                   "text": f"erased {block}"}]})
    else:
        respond(rid, error={"code": -32601, "message": "Method not found"})

2b · Group B client (streamable HTTP)

client.go · go
// Group B — cloud adapter over streamable HTTP.
// Stateless: every request carries its own credential. No session to resume.

package groupb

import (
	"bytes"
	"encoding/json"
	"net/http"
	"time"
)

type Request struct {
	JSONRPC string          `json:"jsonrpc"`
	ID      any             `json:"id"`
	Method  string          `json:"method"`
	Params  json.RawMessage `json:"params"`
}

func Call(endpoint, token string, req Request) (*http.Response, error) {
	body, err := json.Marshal(req)
	if err != nil {
		return nil, err
	}

	r, err := http.NewRequest(http.MethodPost, endpoint, bytes.NewReader(body))
	if err != nil {
		return nil, err
	}
	r.Header.Set("Content-Type", "application/json")
	r.Header.Set("Accept", "application/json, text/event-stream")
	// Self-describing credential — not a sticky session id.
	r.Header.Set("Authorization", "Bearer "+token)

	client := &http.Client{Timeout: 1500 * time.Millisecond}
	return client.Do(r)
}

3 · Implement the erasure contract

Every adapter must answer a forget request, and answering it means more than deleting a row. Enumerate what your backend derived from that record — embeddings, summaries, cached spans, graph edges — and remove those too. Then report honestly what you could not reach; a partial erasure that reports success is worse than one that reports failure.

4 · Error codes the host expects

-32700
Parse error — the bytes were not valid JSON.
-32600
Invalid Request — the envelope is malformed (bad jsonrpc, missing id).
-32601
Method not found — the method is unknown to this adapter.
-32602
Invalid params — arguments failed schema assertion.
-32603
Internal error — your adapter failed for its own reasons.
-32000
Blocked by pre-execution firewall (ai4all host-specific).

Test it before you ship it

Paste your tool schema and a representative call into the sandbox. It runs the same envelope validation, schema assertion, and firewall rules the host will run — so a payload that clears the sandbox will clear the host.