MCP and A2A Protocols

LibreFang supports MCP (Model Context Protocol) and A2A (Agent to Agent) protocols.


MCP (Model Context Protocol)

MCP is a standardized protocol for connecting LLMs to external tools and services.

Overview

┌─────────────┐      MCP       ┌─────────────┐
   LibreFang ◄─────────────►  MCP Server
   (Client)  │   JSON-RPC 2.0  │  (Server)   │
└─────────────┘                  └─────────────┘

MCP Server Configuration

[[mcp_servers]]
name = "filesystem"
transport = { type = "stdio", command = "npx", args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] }

[[mcp_servers]]
name = "github"
transport = { type = "stdio", command = "npx", args = ["-y", "@modelcontextprotocol/server-github"] }
env = ["GITHUB_TOKEN"]

The launch details live under a transport table, not at the top level of the entry. env is a list of strings, not a table: a bare "GITHUB_TOKEN" forwards that variable from the daemon's own environment, and "GITHUB_TOKEN=ghp_..." sets it inline. McpServerConfigEntry is deny_unknown_fields, so a top-level command key or a env = { … } table is a hard parse error rather than an ignored key.

Connect the EveryAPI MCP server

The EveryAPI CLI ships an MCP server inside its own binary, reachable as everyapi mcp. LibreFang speaks stdio MCP, so wiring the two together needs no new code on either side — two config stanzas are the whole integration.

1. Register the server in ~/.librefang/config.toml:

[[mcp_servers]]
name = "everyapi"
transport = { type = "stdio", command = "everyapi", args = ["mcp"] }
timeout_secs = 30
env = []

env = [] is correct and not an oversight. The stdio transport clears the child's environment and then forwards a fixed safe set that includes HOME, XDG_CONFIG_HOME, USER, and PATH (plus USERPROFILE / APPDATA on Windows). That is exactly what everyapi mcp needs to find ~/.config/everyapi/credentials.json on its own, so no credential has to be copied into LibreFang's config or vault.

2. Grant the server to an agent in its agent.toml:

mcp_servers = ["everyapi"]

Independent of the provider wiring

This bridge is orthogonal to librefang models connect everyapi. That command wires EveryAPI as an LLM provider so agents can route completions through the gateway. The MCP server instead gives an agent tools for inspecting and operating the gateway account itself. everyapi mcp reads the gateway's own credentials file directly, so the bridge works whether or not models connect everyapi has ever run — and conversely, connecting the provider does not make these tools available. Run everyapi login first either way: the server process starts fine unauthenticated, but every individual tool returns a not-logged-in error until credentials exist.

Optional: add a catalog entry

The stanza above is all that is required. If you also want librefang mcp add everyapi and dashboard catalog visibility, drop this file at ~/.librefang/mcp/catalog/everyapi.toml:

id = "everyapi"
name = "EveryAPI"
description = "Inspect and operate an EveryAPI gateway account — quota, seller channels, edge nodes — through the everyapi CLI's built-in MCP server"
category = "cloud"
icon = "lucide:gauge"
tags = ["everyapi", "gateway", "quota", "billing", "marketplace"]

[transport]
type = "stdio"
command = "everyapi"
args = ["mcp"]

[health_check]
interval_secs = 60
unhealthy_threshold = 3

setup_instructions = """
1. Install the EveryAPI CLI and run `everyapi login`, which writes ~/.config/everyapi/credentials.json.
2. Run `librefang mcp add everyapi`.
3. Add "everyapi" to the agent's mcp_servers list in its agent.toml.

No credential needs to be entered into LibreFang. The MCP server reads the gateway credentials file itself.
"""

[i18n.zh]
name = "EveryAPI"
description = "通过 everyapi CLI 内置的 MCP 服务器检视与操作 EveryAPI 网关账户 —— 配额、卖家渠道、边缘节点。"

The catalog directory is synced from the upstream registry, but the sync only prunes files it installed itself, so a hand-placed entry survives. It declares no required_env, so librefang mcp add everyapi reports Ready immediately rather than prompting for a key.

Tool inventory

The server exposes 15 tools. Seven read, eight write.

Read-onlyWhat it returns
everyapi_statusAccount name, remaining and used quota in USD, request count, top-up URL
everyapi_topupThe wallet URL where credits are purchased — a string only, it moves no money
everyapi_seller_listThe caller's mounted seller channels
everyapi_seller_eligibilityWhich marketplace-seller gates the account has passed
everyapi_edge_listRegistered BYO-GPU edge nodes
everyapi_edge_statusHealth and serving state of one edge node
everyapi_admin_marketplace_statusDeployment-wide marketplace settings (admin)
WriteEffect and friction
everyapi_seller_add_keyMounts a seller channel from plaintext upstream API keys passed as arguments. No confirm gate.
everyapi_seller_withdrawMoves money out of the seller balance. Requires confirm: "yes".
everyapi_admin_marketplace_setChanges deployment-wide marketplace settings. Requires confirm: "yes".
everyapi_edge_removeDestroys an edge-node registration. Requires confirm: "yes".
everyapi_seller_add_oauth_codex_startBegins an OAuth channel mount; needs a human to paste a code back.
everyapi_seller_add_oauth_codex_pollPolls that flow to completion.
everyapi_seller_add_oauth_claude_startBegins the Claude OAuth channel mount.
everyapi_seller_add_oauth_claude_completeFinishes it with the pasted code.

Tool names are double-prefixed

LibreFang namespaces every MCP tool as mcp_{server}_{tool}, and EveryAPI's tools are already named everyapi_*. The two compose literally, with no de-duplication:

everyapi_status  mcp_everyapi_everyapi_status

The first everyapi is the server name from your [[mcp_servers]] stanza; the second belongs to the upstream tool. This matters because approval patterns match the full namespaced name.

Restricting an agent to the read-only tools

require_approval accepts glob patterns and is matched against the namespaced name, so four patterns cover all eight writes and none of the seven reads:

# ~/.librefang/config.toml
[approval]
require_approval = [
  "mcp_everyapi_everyapi_seller_add_*",        # add_key + the 4 OAuth tools
  "mcp_everyapi_everyapi_seller_withdraw",
  "mcp_everyapi_everyapi_admin_marketplace_set",
  "mcp_everyapi_everyapi_edge_remove",
]

seller_list and seller_eligibility are deliberately not caught: the wildcard is anchored on seller_add_, which is a distinct prefix.

Four honest limits on what this buys you. The last three are bypasses: settings elsewhere in config.toml that silently switch the globs off.

require_approval pauses, it does not deny. A matched call suspends the turn and raises an approval request; a human then approves or rejects it. For an unattended cron agent that means a write tool stalls rather than being refused, and the outcome on timeout is whatever approval.timeout_fallback says (deny by default, but skip and escalate are also available). If you want a hard block rather than a pause, use a channel deny rule or leave the write tools' server off that agent's mcp_servers list entirely.

trusted_senders does not bypass an MCP tool. The approval manager waives the gate for a trusted sender only when the tool is not high-risk, and classify_risk grades every mcp_* name as high risk precisely because it cannot enumerate what third-party server code does. So mcp_everyapi_everyapi_seller_withdraw stays gated even for a trusted sender. Note this was not always true: the classifier matched a closed list of built-in names, so MCP tools fell through to Low and the bypass returned before require_approval was read at all.

A channel allowed_tools rule still is a bypass. An allowed_tools entry that matches the tool returns "no approval needed" before the require_approval list is consulted, so a channel allow-list is not a safe place to name an EveryAPI write tool.

A hand-tagged agent auto-approves everything. Agents spawned through activate_hand() carry a hand: tag and are treated as curated trusted packages: their approval submissions return AutoApproved without ever reaching a human, and execution proceeds straight through. That covers mcp_everyapi_everyapi_seller_add_key and mcp_everyapi_everyapi_seller_withdraw along with everything else. If a hand agent should be able to reach the EveryAPI tools at all, treat the globs as documentation rather than enforcement, and keep the write-capable server off that agent.

Approvals are cached per session by default. approval.cache_approvals_per_session defaults to true, so the first human approval of a tool name inside a session auto-approves every later call of that same tool in that session. For seller_add_key — the one write tool EveryAPI itself does not gate behind a confirm token — that means one "yes" covers an unbounded number of subsequent key uploads until the session ends or the daemon restarts. Set cache_approvals_per_session = false if you want a decision per call.

Per-user RBAC is the one gate none of these can waive. A user-policy decision of NeedsApproval sets force_human, which is evaluated ahead of the trusted-sender check, suppresses the hand-agent auto-approval carve-out, and skips the per-session cache. It is the only mechanism above that all three bypasses respect, so it is what to reach for when the sender is trusted, the agent is hand-tagged, or the session is long-lived.

Flagship use case: check the balance before an expensive job

The intended pattern is a cron agent that calls mcp_everyapi_everyapi_status and aborts if the gateway is nearly out of credit, rather than discovering it mid-run through a wall of 402s. The read tools are all safe to leave ungated, so this needs no approval interaction at all.

The honest caveat: everyapi_status returns rendered text, not JSON. The agent receives something shaped like this:

Account: alice (alice@example.com)
Quota:    $12.34 remaining   $87.66 used
Requests: 1420
Top-up:   https://api.everyapi.ai/wallet

There is no structured field to read. An agent that wants to branch on the remaining balance must parse a float out of that prose, which is exactly the kind of step models get wrong at the margins — a $1,234.00 thousands separator or a future wording change breaks it silently. Prefer a coarse threshold ("if it looks below ten dollars, stop and notify") over arithmetic that assumes an exact parse, and have the agent surface the raw line to a human when it cannot read a number out of it confidently.

MCP Tool Naming

MCP tool naming format:

mcp_{server}_{tool}

For example:

  • mcp_filesystem_read_file
  • mcp_github_create_issue

Built-in MCP Servers

LibreFang includes built-in MCP servers:

ServerTools
filesystemread_file, write_file, list_directory
githubcreate_issue, get_pr, search_repos
postgresquery, execute, list_tables

MCP Client

Connect to external servers as an MCP client:

[[mcp_servers]]
name = "custom"
transport = { type = "stdio", command = "python", args = ["./mcp_server.py"] }

Developing MCP Servers

from mcp.server import Server
from mcp.types import Tool, TextContent

app = Server("my-server")

@app.list_tools()
async def list_tools():
    return [
        Tool(
            name="my_tool",
            description="My custom tool",
            inputSchema={"type": "object", "properties": {}}
        )
    ]

@app.call_tool()
async def call_tool(name: str, arguments: dict):
    return [TextContent(type="text", text="result")]

MCP Taint Policy

Outbound payloads to MCP tools are scanned for sensitive data (tokens, API keys, well-known secret prefixes, PII) by an outbound taint detector. By default every detector rule fires; specific rules can be muted per tool or per path when a particular MCP server legitimately needs to receive that data class.

# config.toml — global mute by tool-name pattern
[[mcp_taint_policy]]
tool = "mcp__github__create_pr"
skip_rules = ["AuthorizationLiteral"]   # let GitHub MCP forward bearer tokens

Available rule ids (the TaintRuleId enum):

RuleDetects
AuthorizationLiteralAuthorization: Bearer ... / Authorization: Basic ... headers in payloads
KeyValueSecretpassword=..., token=..., api_key=... patterns
WellKnownPrefixsk-, ghp_, xoxb-, etc. — known token prefixes
OpaqueTokenLong high-entropy strings without a known prefix
PiiEmail / PiiPhone / PiiCreditCard / PiiSsnPersonal info classes
SensitiveKeyNameJSON keys named password, secret, private_key, etc.

API: callers can also use check_outbound_text_violation_with_skip(payload, sink, skip_rules) directly to skip rules per call. The original check_outbound_text_violation delegates with an empty skip set, so existing callers keep their current behaviour.


A2A (Agent to Agent)

The A2A protocol enables communication between LibreFang agents.

Overview

┌─────────────┐      A2A       ┌─────────────┐
   Agent A ◄─────────────►   Agent B
└─────────────┘   JSON over    └─────────────┘
                  HTTP/WebSocket

Agent Card

Each Agent publishes an Agent Card:

{
  "name": "researcher",
  "description": "Deep research agent",
  "url": "http://localhost:4545/api/a2a",
  "version": "1.0.0",
  "capabilities": {
    "streaming": true,
    "pushNotifications": false
  },
  "skills": [
    { "id": "research", "name": "Research" }
  ]
}

Client Endpoints (Send Tasks to External A2A Agents)

These endpoints allow LibreFang to act as a client, delegating tasks to external A2A agents:

EndpointMethodDescription
/api/a2a/agentsGETList discovered external A2A agents
/api/a2a/discoverPOSTDiscover an external A2A agent at a URL
/api/a2a/sendPOSTSend a task to an external A2A agent
/api/a2a/tasks/{id}/statusGETCheck the status of a sent task

Server Endpoints (Accept External Tasks)

These endpoints are exposed by LibreFang, allowing other A2A agents to send tasks to this instance:

EndpointMethodDescription
/.well-known/agent.jsonGETAgent Card (capability declaration)
/api/a2a/tasksPOSTAccept tasks from external agents
/api/a2a/tasks/{id}GETGet task status
/api/a2a/tasks/{id}/messagesGETGet task message history

Discover and Send Tasks to External Agents

# Discover an external agent
curl -X POST http://localhost:4545/api/a2a/discover \
  -H "Content-Type: application/json" \
  -d '{"url": "http://other-agent:4545"}'

# Send a task to an external agent
curl -X POST http://localhost:4545/api/a2a/send \
  -H "Content-Type: application/json" \
  -d '{
    "agent_url": "http://other-agent:4545",
    "message": "Research AI trends and summarize"
  }'

# Check task status
curl http://localhost:4545/api/a2a/tasks/task-123/status

Accept Tasks from External Agents

# External agent sends a task to this instance
curl -X POST http://localhost:4545/api/a2a/tasks \
  -H "Content-Type: application/json" \
  -d '{
    "id": "task-456",
    "message": {
      "role": "user",
      "parts": [{ "type": "text", "text": "Research AI trends" }]
    }
  }'

# Poll for results
curl http://localhost:4545/api/a2a/tasks/task-456

# Or use SSE for streaming
curl -N http://localhost:4545/api/a2a/tasks/task-456/events

Comparison

FeatureMCPA2A
PurposeLLM to ToolsAgent to Agent
ProtocolJSON-RPC 2.0HTTP/WebSocket
DirectionUnidirectionalBidirectional
ExamplesFilesystem, GitHubAgent collaboration

Use Cases

MCP Use Cases

  • Filesystem operations
  • Database queries
  • GitHub API calls
  • Custom tool integration

A2A Use Cases

  • Multi-agent collaboration
  • Task delegation
  • Cross-instance communication

Configuration Example

Complete MCP Configuration

# MCP Servers
[[mcp_servers]]
name = "filesystem"
transport = { type = "stdio", command = "npx", args = ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"] }

[[mcp_servers]]
name = "github"
transport = { type = "stdio", command = "npx", args = ["-y", "@modelcontextprotocol/server-github"] }

# A2A Configuration
[a2a]
enabled = true
listen_path = "/a2a"

CLI Commands

# List MCP servers
librefang mcp list

# Test an MCP server
librefang mcp test filesystem

# Start MCP mode
librefang mcp