# A2AServer Server-side implementation of Google's A2A (Agent-to-Agent) protocol. Exposes MCP tools as A2A skills over JSON-RPC 2.0. Tasks persist in SQLite via Sqler (`:a2a_db`). ## Protocol Endpoints Routed by `MCPServer.Router`: | Method | Path | Auth | Handler | |--------|------|------|---------| | GET | `/.well-known/agent.json` | Public | `public_agent_card/0` | | POST | `/a2a` | Cookie | `handle_send/2`, `handle_get/2`, `handle_cancel/2` | CSRF is skipped for `/a2a` (JSON-RPC, authenticated separately via cookie). ## Agent Card (Discovery) Two levels of discovery depending on authentication: **Public** (`GET /.well-known/agent.json`) — returns a card with only safe tools: ``` get_greeting, current_time, is_admin, get_counter, add_numbers ``` **Authenticated** — returns all tools the user has permission to access, filtered by `MCPServer.tools_for_permissions/1`. Both return the same card structure: ```json { "name": "elixir-mcp-agent", "description": "An Elixir MCP server exposed via the A2A protocol", "url": "https://localhost/a2a", "version": "1.0.0", "capabilities": { "streaming": false, "pushNotifications": false, "stateTransitionHistory": true }, "authentication": {"schemes": ["cookie"]}, "defaultInputModes": ["text"], "defaultOutputModes": ["text"], "skills": [ {"id": "add_numbers", "name": "add_numbers", "description": "...", "tags": [], "examples": []} ] } ``` Skills are generated from MCP tool definitions — each tool's `name` and `description` map directly to a skill entry. ## Task Lifecycle ``` submitted → working → completed → failed → canceled ``` ### `handle_send(params, user)` Creates a task, executes the requested tool, and returns the result. This is synchronous — the task moves to `completed` or `failed` before the response is sent. 1. Parse message parts — extract text parts and look for a data part with `tool` and `arguments` 2. If no tool specified, default to the first registered MCP tool 3. Insert task row with state `"working"` and the message in history 4. Execute tool via `MCPServer.Manager.call_tool_as(tool_name, args, user)` 5. On success: store artifacts, set state to `"completed"` 6. On error: store error artifact, set state to `"failed"` 7. Return the full task response **Message format expected:** ```json { "message": { "role": "user", "parts": [ {"type": "text", "text": "Human-readable request"}, {"type": "data", "data": {"tool": "add_numbers", "arguments": {"a": 3, "b": 7}}} ] } } ``` The data part is optional. If absent, the first registered tool is called with `{"text": }` as arguments. ### `handle_get(params, user)` Retrieves a task by ID. Returns the task with current state, history, and artifacts. Returns JSON-RPC error `-32602` if ID is missing, `-32001` if task not found. ### `handle_cancel(params, user)` Sets a task's state to `"canceled"` via optimistic-locking update. Returns JSON-RPC error `-32602` if ID is missing, `-32001` if task not found. ## Storage Schema Table `a2a_tasks` on `:a2a_db`: | Column | Type | Description | |--------|------|-------------| | `id` | INTEGER PK | Millisecond timestamp (Sqler convention) | | `updated_at` | INTEGER | Millisecond timestamp, used for optimistic locking | | `user_id` | INTEGER NOT NULL | Owner, indexed | | `state` | TEXT | `submitted`, `working`, `completed`, `failed`, `canceled` | | `history` | TEXT | JSON array of messages (the conversation) | | `artifacts` | TEXT | JSON array of result artifacts | ## Task Response Format All handlers return tasks in this shape: ```json { "id": "1709395200000", "status": { "state": "completed", "timestamp": "2026-03-02T12:00:00.000Z" }, "history": [ {"role": "user", "parts": [{"type": "text", "text": "Add 3 and 7"}]} ], "artifacts": [ { "name": "add_numbers-result", "parts": [{"type": "text", "text": "10"}] } ] } ``` Timestamps are converted from Sqler millisecond integers to ISO 8601. ## Functions | Function | Purpose | |----------|---------| | `setup_database(db)` | Create `a2a_tasks` table and indexes (called at startup) | | `public_agent_card()` | Agent card with safe-only tools (unauthenticated) | | `agent_card(permissions)` | Agent card filtered by user permissions | | `handle_send(params, user)` | Create and execute a task | | `handle_get(params, user)` | Retrieve a task by ID | | `handle_cancel(params, user)` | Cancel a task by ID | ## Source `lib/a2a_server.ex`