Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
856 lines
24 KiB
Markdown
856 lines
24 KiB
Markdown
# MCP & A2A Integration Guide
|
|
|
|
OpenFang implements both the **Model Context Protocol (MCP)** and **Agent-to-Agent (A2A)** protocol, enabling deep interoperability with external tools, IDEs, and other agent frameworks.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
- [Part 1: MCP (Model Context Protocol)](#part-1-mcp-model-context-protocol)
|
|
- [Overview](#mcp-overview)
|
|
- [MCP Client -- Connecting to External Servers](#mcp-client)
|
|
- [MCP Server -- Exposing OpenFang via MCP](#mcp-server)
|
|
- [Configuration Examples](#mcp-configuration-examples)
|
|
- [API Endpoints](#mcp-api-endpoints)
|
|
- [Part 2: A2A (Agent-to-Agent Protocol)](#part-2-a2a-agent-to-agent-protocol)
|
|
- [Overview](#a2a-overview)
|
|
- [Agent Card](#agent-card)
|
|
- [A2A Server](#a2a-server)
|
|
- [A2A Client](#a2a-client)
|
|
- [Task Lifecycle](#task-lifecycle)
|
|
- [API Endpoints](#a2a-api-endpoints)
|
|
- [Configuration](#a2a-configuration)
|
|
- [Security](#security)
|
|
|
|
---
|
|
|
|
## Part 1: MCP (Model Context Protocol)
|
|
|
|
### MCP Overview
|
|
|
|
The Model Context Protocol (MCP) is a JSON-RPC 2.0 based protocol that standardizes how LLM applications discover and invoke tools. OpenFang supports MCP in both directions:
|
|
|
|
- **As a client**: OpenFang connects to external MCP servers (GitHub, filesystem, databases, Puppeteer, etc.) and makes their tools available to all agents.
|
|
- **As a server**: OpenFang exposes its own agents as MCP tools, so IDEs like Cursor, VS Code, and Claude Desktop can call OpenFang agents directly.
|
|
|
|
OpenFang implements MCP protocol version `2024-11-05`.
|
|
|
|
**Source files:**
|
|
- Client: `crates/openfang-runtime/src/mcp.rs`
|
|
- Server handler: `crates/openfang-runtime/src/mcp_server.rs`
|
|
- CLI server: `crates/openfang-cli/src/mcp.rs`
|
|
- Config types: `crates/openfang-types/src/config.rs` (`McpServerConfigEntry`, `McpTransportEntry`)
|
|
|
|
---
|
|
|
|
### MCP Client
|
|
|
|
The MCP client (`McpConnection` in `openfang-runtime`) allows OpenFang to connect to any MCP-compatible server and use its tools as if they were built-in.
|
|
|
|
#### Configuration
|
|
|
|
MCP servers are configured in `config.toml` using the `[[mcp_servers]]` array:
|
|
|
|
```toml
|
|
[[mcp_servers]]
|
|
name = "github"
|
|
timeout_secs = 30
|
|
env = ["GITHUB_PERSONAL_ACCESS_TOKEN"]
|
|
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-github"]
|
|
```
|
|
|
|
Each entry maps to a `McpServerConfigEntry` struct:
|
|
|
|
| Field | Type | Default | Description |
|
|
|-------|------|---------|-------------|
|
|
| `name` | `String` | required | Display name, used in tool namespacing |
|
|
| `transport` | `McpTransportEntry` | required | How to connect (stdio or SSE) |
|
|
| `timeout_secs` | `u64` | `30` | JSON-RPC request timeout |
|
|
| `env` | `Vec<String>` | `[]` | Env vars to pass through to the subprocess |
|
|
|
|
#### Transport Types
|
|
|
|
OpenFang supports two MCP transports, defined by `McpTransport`:
|
|
|
|
**Stdio** -- Spawns a subprocess and communicates via stdin/stdout with newline-delimited JSON-RPC:
|
|
|
|
```toml
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-github"]
|
|
```
|
|
|
|
**SSE** -- Connects to a remote HTTP endpoint and sends JSON-RPC via POST:
|
|
|
|
```toml
|
|
[mcp_servers.transport]
|
|
type = "sse"
|
|
url = "https://mcp.example.com/api"
|
|
```
|
|
|
|
#### Tool Namespacing
|
|
|
|
All tools discovered from MCP servers are namespaced using the pattern `mcp_{server}_{tool}` to prevent collisions with built-in tools or tools from other servers. Names are normalized to lowercase with hyphens replaced by underscores.
|
|
|
|
Examples:
|
|
- Server `github`, tool `create_issue` becomes `mcp_github_create_issue`
|
|
- Server `my-server`, tool `do_thing` becomes `mcp_my_server_do_thing`
|
|
|
|
Helper functions (exported from `openfang_runtime::mcp`):
|
|
- `format_mcp_tool_name(server, tool)` -- builds the namespaced name
|
|
- `is_mcp_tool(name)` -- checks if a tool name starts with `mcp_`
|
|
- `extract_mcp_server(tool_name)` -- extracts the server name from a namespaced tool
|
|
|
|
#### Auto-Connection on Kernel Boot
|
|
|
|
When the kernel starts (`start_background_agents()`), it checks `config.mcp_servers`. If any are configured, it spawns a background task that calls `connect_mcp_servers()`. This method:
|
|
|
|
1. Iterates each `McpServerConfigEntry` in the config
|
|
2. Converts the config-level `McpTransportEntry` into a runtime `McpTransport`
|
|
3. Calls `McpConnection::connect()` which:
|
|
- Spawns the subprocess (stdio) or creates an HTTP client (SSE)
|
|
- Sends the `initialize` handshake with client info
|
|
- Sends the `notifications/initialized` notification
|
|
- Calls `tools/list` to discover all available tools
|
|
- Namespaces each tool with `mcp_{server}_{tool}`
|
|
4. Caches discovered `ToolDefinition` entries in `kernel.mcp_tools`
|
|
5. Stores the live `McpConnection` in `kernel.mcp_connections`
|
|
|
|
After connection, the kernel logs the total number of MCP tools available.
|
|
|
|
#### Tool Discovery and Listing
|
|
|
|
MCP tools are merged into the agent's available tool set via `available_tools()`:
|
|
|
|
```
|
|
built-in tools (23) + skill tools + MCP tools = full tool list
|
|
```
|
|
|
|
When an agent calls an MCP tool during its loop, the tool runner recognizes the `mcp_` prefix, finds the appropriate `McpConnection`, strips the namespace prefix, and forwards the `tools/call` request to the external MCP server.
|
|
|
|
#### Connection Lifecycle
|
|
|
|
The `McpConnection` struct manages the lifetime of the connection:
|
|
|
|
```rust
|
|
pub struct McpConnection {
|
|
config: McpServerConfig,
|
|
tools: Vec<ToolDefinition>,
|
|
transport: McpTransportHandle, // Stdio or SSE
|
|
next_id: u64, // JSON-RPC request counter
|
|
}
|
|
```
|
|
|
|
When the connection is dropped, stdio subprocesses are automatically killed via `Drop`:
|
|
|
|
```rust
|
|
impl Drop for McpConnection {
|
|
fn drop(&mut self) {
|
|
if let McpTransportHandle::Stdio { ref mut child, .. } = self.transport {
|
|
let _ = child.start_kill();
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### MCP Server
|
|
|
|
OpenFang can also act as an MCP server, exposing its agents as callable tools to external MCP clients.
|
|
|
|
#### How It Works
|
|
|
|
Each OpenFang agent becomes an MCP tool named `openfang_agent_{name}` (with hyphens replaced by underscores). The tool accepts a single `message` string parameter and returns the agent's response.
|
|
|
|
For example, an agent named `code-reviewer` becomes the MCP tool `openfang_agent_code_reviewer`.
|
|
|
|
#### CLI: `openfang mcp`
|
|
|
|
The primary way to run the MCP server is the `openfang mcp` command, which starts a stdio-based MCP server:
|
|
|
|
```bash
|
|
openfang mcp
|
|
```
|
|
|
|
This command:
|
|
1. Checks if an OpenFang daemon is running (via `find_daemon()`)
|
|
2. If found, proxies all tool calls to the daemon via its HTTP API
|
|
3. If no daemon is running, boots an in-process kernel as a fallback
|
|
4. Reads Content-Length framed JSON-RPC messages from stdin
|
|
5. Writes Content-Length framed JSON-RPC responses to stdout
|
|
|
|
The MCP server uses `McpBackend` which supports two modes:
|
|
- `McpBackend::Daemon` -- forwards requests to a running OpenFang daemon via HTTP
|
|
- `McpBackend::InProcess` -- boots a full kernel when no daemon is available
|
|
|
|
#### HTTP MCP Endpoint
|
|
|
|
OpenFang also exposes an MCP endpoint over HTTP at `POST /mcp`. Unlike the stdio server (which only exposes agents), the HTTP endpoint exposes the full tool set (built-in + skills + MCP tools) and executes tools via the kernel's `execute_tool()` pipeline. This means the HTTP MCP endpoint supports:
|
|
|
|
- All 23 built-in tools (file_read, web_fetch, etc.)
|
|
- All installed skill tools
|
|
- All connected MCP server tools
|
|
|
|
#### Supported JSON-RPC Methods
|
|
|
|
| Method | Description |
|
|
|--------|-------------|
|
|
| `initialize` | Handshake; returns server capabilities and info |
|
|
| `notifications/initialized` | Client confirmation; no response |
|
|
| `tools/list` | Returns all available tools with names, descriptions, and input schemas |
|
|
| `tools/call` | Executes a tool and returns the result |
|
|
|
|
Unknown methods receive a `-32601` (Method not found) error.
|
|
|
|
#### Protocol Details
|
|
|
|
**Message Framing** (stdio mode):
|
|
|
|
```
|
|
Content-Length: 123\r\n
|
|
\r\n
|
|
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
|
|
```
|
|
|
|
Messages are limited to 10 MB (`MAX_MCP_MESSAGE_SIZE`). Oversized messages are drained and rejected.
|
|
|
|
**Initialize Handshake:**
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "initialize",
|
|
"params": {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": {},
|
|
"clientInfo": { "name": "cursor", "version": "1.0" }
|
|
}
|
|
}
|
|
```
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"result": {
|
|
"protocolVersion": "2024-11-05",
|
|
"capabilities": { "tools": {} },
|
|
"serverInfo": { "name": "openfang", "version": "0.1.0" }
|
|
}
|
|
}
|
|
```
|
|
|
|
**Tool Call:**
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 3,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": "openfang_agent_code_reviewer",
|
|
"arguments": {
|
|
"message": "Review this Python function for security issues..."
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Response:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 3,
|
|
"result": {
|
|
"content": [{
|
|
"type": "text",
|
|
"text": "I found 3 potential security issues..."
|
|
}]
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Connecting from IDEs
|
|
|
|
**Cursor / VS Code (with MCP extension):**
|
|
|
|
Add to your MCP configuration file (e.g., `.cursor/mcp.json` or VS Code MCP settings):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"openfang": {
|
|
"command": "openfang",
|
|
"args": ["mcp"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Claude Desktop:**
|
|
|
|
Add to `claude_desktop_config.json`:
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"openfang": {
|
|
"command": "openfang",
|
|
"args": ["mcp"],
|
|
"env": {}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
After configuration, all OpenFang agents appear as tools in the IDE. For example, you can ask Claude Desktop to "use the openfang code-reviewer agent to review this file."
|
|
|
|
---
|
|
|
|
### MCP Configuration Examples
|
|
|
|
#### GitHub Server (file + issue + PR tools)
|
|
|
|
```toml
|
|
[[mcp_servers]]
|
|
name = "github"
|
|
timeout_secs = 30
|
|
env = ["GITHUB_PERSONAL_ACCESS_TOKEN"]
|
|
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-github"]
|
|
```
|
|
|
|
#### Filesystem Server
|
|
|
|
```toml
|
|
[[mcp_servers]]
|
|
name = "filesystem"
|
|
timeout_secs = 10
|
|
env = []
|
|
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
|
|
```
|
|
|
|
#### PostgreSQL Server
|
|
|
|
```toml
|
|
[[mcp_servers]]
|
|
name = "postgres"
|
|
timeout_secs = 30
|
|
env = ["DATABASE_URL"]
|
|
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-postgres"]
|
|
```
|
|
|
|
#### Puppeteer (Browser Automation)
|
|
|
|
```toml
|
|
[[mcp_servers]]
|
|
name = "puppeteer"
|
|
timeout_secs = 60
|
|
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-puppeteer"]
|
|
```
|
|
|
|
#### Remote SSE Server
|
|
|
|
```toml
|
|
[[mcp_servers]]
|
|
name = "remote-tools"
|
|
timeout_secs = 30
|
|
|
|
[mcp_servers.transport]
|
|
type = "sse"
|
|
url = "https://tools.example.com/mcp"
|
|
```
|
|
|
|
#### Multiple Servers
|
|
|
|
```toml
|
|
[[mcp_servers]]
|
|
name = "github"
|
|
env = ["GITHUB_PERSONAL_ACCESS_TOKEN"]
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-github"]
|
|
|
|
[[mcp_servers]]
|
|
name = "filesystem"
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-filesystem", "/home/user/projects"]
|
|
|
|
[[mcp_servers]]
|
|
name = "postgres"
|
|
env = ["DATABASE_URL"]
|
|
[mcp_servers.transport]
|
|
type = "stdio"
|
|
command = "npx"
|
|
args = ["-y", "@modelcontextprotocol/server-postgres"]
|
|
```
|
|
|
|
---
|
|
|
|
### MCP API Endpoints
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| `GET` | `/api/mcp/servers` | List configured and connected MCP servers with their tools |
|
|
| `POST` | `/mcp` | Handle MCP JSON-RPC requests over HTTP (full tool execution) |
|
|
|
|
**GET /api/mcp/servers** response:
|
|
|
|
```json
|
|
{
|
|
"configured": [
|
|
{
|
|
"name": "github",
|
|
"transport": { "type": "stdio", "command": "npx", "args": [...] },
|
|
"timeout_secs": 30,
|
|
"env": ["GITHUB_PERSONAL_ACCESS_TOKEN"]
|
|
}
|
|
],
|
|
"connected": [
|
|
{
|
|
"name": "github",
|
|
"tools_count": 12,
|
|
"tools": [
|
|
{ "name": "mcp_github_create_issue", "description": "[MCP:github] Create a GitHub issue" },
|
|
{ "name": "mcp_github_search_repos", "description": "[MCP:github] Search repositories" }
|
|
],
|
|
"connected": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Part 2: A2A (Agent-to-Agent Protocol)
|
|
|
|
### A2A Overview
|
|
|
|
The Agent-to-Agent (A2A) protocol, originally specified by Google, enables cross-framework agent interoperability. It allows agents built with different frameworks to discover each other's capabilities and exchange tasks.
|
|
|
|
OpenFang implements A2A in both directions:
|
|
|
|
- **As a server**: Publishes Agent Cards describing each agent's capabilities, accepts task submissions, and tracks task lifecycle.
|
|
- **As a client**: Discovers external A2A agents at boot time, sends tasks to them, and polls for results.
|
|
|
|
**Source files:**
|
|
- Protocol types and logic: `crates/openfang-runtime/src/a2a.rs`
|
|
- API routes: `crates/openfang-api/src/routes.rs`
|
|
- Config types: `crates/openfang-types/src/config.rs` (`A2aConfig`, `ExternalAgent`)
|
|
|
|
---
|
|
|
|
### Agent Card
|
|
|
|
An Agent Card is a JSON document that describes an agent's identity, capabilities, and supported interaction modes. It is served at the well-known path `/.well-known/agent.json` per the A2A specification.
|
|
|
|
The `AgentCard` struct:
|
|
|
|
```rust
|
|
pub struct AgentCard {
|
|
pub name: String,
|
|
pub description: String,
|
|
pub url: String, // endpoint URL (e.g., "http://host/a2a")
|
|
pub version: String, // protocol version
|
|
pub capabilities: AgentCapabilities,
|
|
pub skills: Vec<AgentSkill>, // A2A skill descriptors
|
|
pub default_input_modes: Vec<String>, // e.g., ["text"]
|
|
pub default_output_modes: Vec<String>, // e.g., ["text"]
|
|
}
|
|
```
|
|
|
|
**AgentCapabilities:**
|
|
|
|
```rust
|
|
pub struct AgentCapabilities {
|
|
pub streaming: bool, // true -- OpenFang supports streaming
|
|
pub push_notifications: bool, // false -- not currently implemented
|
|
pub state_transition_history: bool, // true -- task status history available
|
|
}
|
|
```
|
|
|
|
**AgentSkill** (not the same as OpenFang skills -- these are A2A capability descriptors):
|
|
|
|
```rust
|
|
pub struct AgentSkill {
|
|
pub id: String, // matches the OpenFang tool name
|
|
pub name: String, // human-readable (underscores replaced with spaces)
|
|
pub description: String,
|
|
pub tags: Vec<String>,
|
|
pub examples: Vec<String>,
|
|
}
|
|
```
|
|
|
|
Agent Cards are built from OpenFang agent manifests via `build_agent_card()`. Each tool in the agent's capability list becomes an A2A skill descriptor. Example card:
|
|
|
|
```json
|
|
{
|
|
"name": "code-reviewer",
|
|
"description": "Reviews code for bugs, security issues, and style",
|
|
"url": "http://127.0.0.1:50051/a2a",
|
|
"version": "0.1.0",
|
|
"capabilities": {
|
|
"streaming": true,
|
|
"pushNotifications": false,
|
|
"stateTransitionHistory": true
|
|
},
|
|
"skills": [
|
|
{
|
|
"id": "file_read",
|
|
"name": "file read",
|
|
"description": "Can use the file_read tool",
|
|
"tags": ["tool"],
|
|
"examples": []
|
|
}
|
|
],
|
|
"defaultInputModes": ["text"],
|
|
"defaultOutputModes": ["text"]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### A2A Server
|
|
|
|
OpenFang serves A2A requests through the REST API. The server-side implementation involves:
|
|
|
|
1. **Agent Card publication** at `/.well-known/agent.json`
|
|
2. **Agent listing** at `/a2a/agents`
|
|
3. **Task submission and tracking** via the `A2aTaskStore`
|
|
|
|
#### A2aTaskStore
|
|
|
|
The `A2aTaskStore` is an in-memory, bounded store for tracking A2A task lifecycle:
|
|
|
|
```rust
|
|
pub struct A2aTaskStore {
|
|
tasks: Mutex<HashMap<String, A2aTask>>,
|
|
max_tasks: usize, // default: 1000
|
|
}
|
|
```
|
|
|
|
Key properties:
|
|
- **Bounded**: When the store reaches `max_tasks`, it evicts the oldest completed/failed/cancelled task (FIFO)
|
|
- **Thread-safe**: Uses `Mutex<HashMap>` for concurrent access
|
|
- **Kernel field**: Stored as `kernel.a2a_task_store`
|
|
|
|
Methods on `A2aTaskStore`:
|
|
- `insert(task)` -- add a new task, evicting old ones if at capacity
|
|
- `get(task_id)` -- retrieve a task by ID
|
|
- `update_status(task_id, status)` -- change a task's status
|
|
- `complete(task_id, response, artifacts)` -- mark as completed with response
|
|
- `fail(task_id, error_message)` -- mark as failed with error
|
|
- `cancel(task_id)` -- mark as cancelled
|
|
|
|
#### Task Submission Flow
|
|
|
|
When `POST /a2a/tasks/send` is called:
|
|
|
|
1. Extract the message text from the A2A request format (parts with type "text")
|
|
2. Find the target agent (currently uses the first registered agent)
|
|
3. Create an `A2aTask` with status `Working` and insert into the task store
|
|
4. Send the message to the agent via `kernel.send_message()`
|
|
5. On success: complete the task with the agent's response
|
|
6. On failure: fail the task with the error message
|
|
7. Return the final task state
|
|
|
|
---
|
|
|
|
### A2A Client
|
|
|
|
The `A2aClient` struct discovers and interacts with external A2A agents:
|
|
|
|
```rust
|
|
pub struct A2aClient {
|
|
client: reqwest::Client, // 30-second timeout
|
|
}
|
|
```
|
|
|
|
**Methods:**
|
|
|
|
- `discover(url)` -- fetches `{url}/.well-known/agent.json` and parses the Agent Card
|
|
- `send_task(url, message, session_id)` -- sends a JSON-RPC task submission
|
|
- `get_task(url, task_id)` -- polls for task status
|
|
|
|
#### Auto-Discovery at Boot
|
|
|
|
When the kernel starts and A2A is enabled with external agents configured, it spawns a background task that calls `discover_external_agents()`. This function:
|
|
|
|
1. Creates an `A2aClient`
|
|
2. Iterates each configured `ExternalAgent`
|
|
3. Fetches each agent's card from `{url}/.well-known/agent.json`
|
|
4. Logs successful discoveries (name, URL, skill count)
|
|
5. Stores discovered `(name, AgentCard)` pairs in `kernel.a2a_external_agents`
|
|
|
|
Failed discoveries are logged as warnings but do not prevent boot.
|
|
|
|
#### Sending Tasks to External Agents
|
|
|
|
```rust
|
|
let client = A2aClient::new();
|
|
let task = client.send_task(
|
|
"https://other-agent.example.com/a2a",
|
|
"Analyze this dataset for anomalies",
|
|
Some("session-123"),
|
|
).await?;
|
|
println!("Task {}: {:?}", task.id, task.status);
|
|
```
|
|
|
|
The client sends a JSON-RPC request:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tasks/send",
|
|
"params": {
|
|
"message": {
|
|
"role": "user",
|
|
"parts": [{ "type": "text", "text": "Analyze this dataset..." }]
|
|
},
|
|
"sessionId": "session-123"
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### Task Lifecycle
|
|
|
|
An `A2aTask` tracks the full lifecycle of a cross-agent interaction:
|
|
|
|
```rust
|
|
pub struct A2aTask {
|
|
pub id: String,
|
|
pub session_id: Option<String>,
|
|
pub status: A2aTaskStatus,
|
|
pub messages: Vec<A2aMessage>,
|
|
pub artifacts: Vec<A2aArtifact>,
|
|
}
|
|
```
|
|
|
|
#### Task States
|
|
|
|
| Status | Description |
|
|
|--------|-------------|
|
|
| `Submitted` | Task received but not yet started |
|
|
| `Working` | Task is being actively processed by the agent |
|
|
| `InputRequired` | Agent needs more information from the caller |
|
|
| `Completed` | Task finished successfully |
|
|
| `Cancelled` | Task was cancelled by the caller |
|
|
| `Failed` | Task encountered an error |
|
|
|
|
#### Message Format
|
|
|
|
Messages use an A2A-specific format with typed content parts:
|
|
|
|
```rust
|
|
pub struct A2aMessage {
|
|
pub role: String, // "user" or "agent"
|
|
pub parts: Vec<A2aPart>,
|
|
}
|
|
|
|
pub enum A2aPart {
|
|
Text { text: String },
|
|
File { name: String, mime_type: String, data: String }, // base64
|
|
Data { mime_type: String, data: serde_json::Value },
|
|
}
|
|
```
|
|
|
|
#### Artifacts
|
|
|
|
Tasks can produce artifacts (files, structured data) alongside messages:
|
|
|
|
```rust
|
|
pub struct A2aArtifact {
|
|
pub name: String,
|
|
pub parts: Vec<A2aPart>,
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
### A2A API Endpoints
|
|
|
|
| Method | Path | Auth | Description |
|
|
|--------|------|------|-------------|
|
|
| `GET` | `/.well-known/agent.json` | Public | Agent Card for the primary agent |
|
|
| `GET` | `/a2a/agents` | Public | List all agent cards |
|
|
| `POST` | `/a2a/tasks/send` | Public | Submit a task to an agent |
|
|
| `GET` | `/a2a/tasks/{id}` | Public | Get task status and messages |
|
|
| `POST` | `/a2a/tasks/{id}/cancel` | Public | Cancel a running task |
|
|
|
|
#### GET /.well-known/agent.json
|
|
|
|
Returns the Agent Card for the first registered agent. If no agents are spawned, returns a placeholder card.
|
|
|
|
#### GET /a2a/agents
|
|
|
|
Lists all registered agents as Agent Cards:
|
|
|
|
```json
|
|
{
|
|
"agents": [
|
|
{
|
|
"name": "code-reviewer",
|
|
"description": "Reviews code for bugs and security issues",
|
|
"url": "http://127.0.0.1:50051/a2a",
|
|
"version": "0.1.0",
|
|
"capabilities": { "streaming": true, "pushNotifications": false, "stateTransitionHistory": true },
|
|
"skills": [...],
|
|
"defaultInputModes": ["text"],
|
|
"defaultOutputModes": ["text"]
|
|
}
|
|
],
|
|
"total": 1
|
|
}
|
|
```
|
|
|
|
#### POST /a2a/tasks/send
|
|
|
|
Submit a task. Request body follows JSON-RPC 2.0 format:
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tasks/send",
|
|
"params": {
|
|
"message": {
|
|
"role": "user",
|
|
"parts": [{ "type": "text", "text": "Review this code for security issues" }]
|
|
},
|
|
"sessionId": "optional-session-id"
|
|
}
|
|
}
|
|
```
|
|
|
|
Response (completed task):
|
|
|
|
```json
|
|
{
|
|
"id": "550e8400-e29b-41d4-a716-446655440000",
|
|
"sessionId": "optional-session-id",
|
|
"status": "completed",
|
|
"messages": [
|
|
{
|
|
"role": "user",
|
|
"parts": [{ "type": "text", "text": "Review this code for security issues" }]
|
|
},
|
|
{
|
|
"role": "agent",
|
|
"parts": [{ "type": "text", "text": "I found 2 potential issues..." }]
|
|
}
|
|
],
|
|
"artifacts": []
|
|
}
|
|
```
|
|
|
|
#### GET /a2a/tasks/{id}
|
|
|
|
Poll for task status. Returns `404` if the task is not found or has been evicted.
|
|
|
|
#### POST /a2a/tasks/{id}/cancel
|
|
|
|
Cancel a running task. Sets its status to `Cancelled`. Returns `404` if the task is not found.
|
|
|
|
---
|
|
|
|
### A2A Configuration
|
|
|
|
A2A is configured in `config.toml` under the `[a2a]` section:
|
|
|
|
```toml
|
|
[a2a]
|
|
enabled = true
|
|
listen_path = "/a2a"
|
|
|
|
[[a2a.external_agents]]
|
|
name = "research-agent"
|
|
url = "https://research.example.com"
|
|
|
|
[[a2a.external_agents]]
|
|
name = "data-analyst"
|
|
url = "https://data.example.com"
|
|
```
|
|
|
|
The `A2aConfig` struct:
|
|
|
|
| Field | Type | Default | Description |
|
|
|-------|------|---------|-------------|
|
|
| `enabled` | `bool` | `false` | Whether A2A endpoints are active |
|
|
| `listen_path` | `String` | `"/a2a"` | Base path for A2A endpoints |
|
|
| `external_agents` | `Vec<ExternalAgent>` | `[]` | External agents to discover at boot |
|
|
|
|
Each `ExternalAgent`:
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `name` | `String` | Display name for this external agent |
|
|
| `url` | `String` | Base URL where the agent's card is published |
|
|
|
|
If `a2a` is `None` (not present in config), all A2A features are disabled. The A2A endpoints are always registered in the router but the discovery and task store functionality requires `enabled = true`.
|
|
|
|
---
|
|
|
|
## Security
|
|
|
|
### MCP Security
|
|
|
|
**Subprocess Sandboxing**: Stdio MCP servers run with `env_clear()` -- the subprocess environment is completely cleared. Only explicitly whitelisted environment variables (listed in the `env` field) plus `PATH` are passed through. This prevents leaking secrets to untrusted MCP server processes.
|
|
|
|
**Path Traversal Prevention**: The command path for stdio MCP servers is validated to reject `..` sequences.
|
|
|
|
**SSRF Protection**: SSE transport URLs are checked against known metadata endpoints (169.254.169.254, metadata.google) to prevent SSRF attacks.
|
|
|
|
**Request Timeout**: All MCP requests have a configurable timeout (default 30 seconds) to prevent hung connections.
|
|
|
|
**Message Size Limit**: The stdio MCP server enforces a 10 MB maximum message size to prevent out-of-memory attacks. Oversized messages are drained and rejected.
|
|
|
|
### A2A Security
|
|
|
|
**Rate Limiting**: A2A endpoints go through the same GCRA rate limiter as all other API endpoints.
|
|
|
|
**API Authentication**: When `api_key` is set in the kernel config, all API endpoints (including A2A) require a `Authorization: Bearer <key>` header. The exception is `/.well-known/agent.json` and the health endpoint which are typically public.
|
|
|
|
**Task Store Bounds**: The `A2aTaskStore` is bounded (default 1000 tasks) with FIFO eviction of completed/failed/cancelled tasks, preventing memory exhaustion from task accumulation.
|
|
|
|
**External Agent Discovery**: The `A2aClient` uses a 30-second timeout and sends a `User-Agent: OpenFang/0.1 A2A` header. Failed discoveries are logged but do not block kernel boot.
|
|
|
|
### Kernel-Level Protection
|
|
|
|
Both MCP and A2A tool execution flows through the same security pipeline as all other tool calls:
|
|
- Capability-based access control (agents only get tools they are authorized for)
|
|
- Tool result truncation (50K character hard cap)
|
|
- Universal 60-second tool execution timeout
|
|
- Loop guard detection (blocks repetitive tool call patterns)
|
|
- Taint tracking on data flowing between tools
|