WorkingAgents needs to proxy tools from external MCP servers – Stripe, GitHub, a customer’s internal API, a specialist AI model – and make them available to agents through the same permission and audit layer as built-in tools. This document describes how that works.
The Architecture
External MCP server connections are stored in SQLite, managed through the same MCP tools / REST API as everything else, and each connection is a first-class entity in the permission system.
Data Model
CREATE TABLE mcp_connections (
id INTEGER PRIMARY KEY, -- ms timestamp
updated_at INTEGER NOT NULL,
name TEXT NOT NULL UNIQUE,
transport TEXT NOT NULL, -- "http" | "stdio" | "sse" | "websocket"
url TEXT, -- for http/sse/websocket transports
command TEXT, -- for stdio transport
args_json TEXT, -- JSON array of command args
env_json TEXT, -- JSON map of env vars
auth_json TEXT, -- encrypted: type, token, etc.
permission_key INTEGER NOT NULL, -- the key required to use this server's tools
status TEXT DEFAULT 'active', -- active | disabled
last_seen_at INTEGER, -- when connection was last healthy
tool_count INTEGER DEFAULT 0 -- cached from last discovery
);
Each connection row represents one external MCP server. The permission_key is a runtime-assignable integer that gates access – exactly like every other module in the system.
Permission Model
Each external server gets a dedicated permission key assigned at connection time. The convention is to use integers in the 200_001+ range for external connections. The admin grants this key to specific users or roles:
mcp_connection_add name=stripe url=https://mcp.stripe.com auth_token=sk_live_... permission_key=200001 transport=http
access_control_grant user=alice permissions=[200001]
access_control_grant role=finance permissions=[200001]
Alice and all finance-role users can now call Stripe tools. Bob cannot. If Stripe’s server misbehaves, mcp_connection_disable name=stripe removes all access immediately for all users – no config files, no restart.
How MCPConnectionRegistry Works
MCPConnectionRegistry is a GenServer that owns the mcp_connections SQLite table. It is the sole process that reads and writes that database.
On startup: Loads all status = 'active' rows, starts a Hermes.Client.Supervisor for each under a DynamicSupervisor, discovers the tool list from each server, and caches it in memory.
On tool list: When a user requests tools/list, MyMCPServer calls MCPConnectionRegistry.tools_for_permissions(permissions). The registry returns only tools whose connection’s permission_key is present in the user’s permission map. A user without the Stripe permission does not see Stripe tools.
On tool call: When an agent calls a tool name that isn’t a built-in, MyMCPServer falls through to MCPConnectionRegistry.call_tool(name, args, permissions). The registry finds which connection owns that tool name, checks the permission, and proxies the call through the live hermes_mcp client.
On add/disable/remove: The registry updates the database and immediately starts or stops the corresponding hermes_mcp client. No restart required.
Adding a New External Connection
HTTP server with bearer auth
mcp_connection_add name=stripe transport=http url=https://mcp.stripe.com auth_token=sk_live_... permission_key=200001
The connection is live immediately. Tools are discovered and cached. Grant the permission key to relevant users.
stdio server
mcp_connection_add name=github transport=stdio command=npx args=[-y, @modelcontextprotocol/server-github] env={GITHUB_PERSONAL_ACCESS_TOKEN: ghp_...} permission_key=200002
WorkingAgents spawns the process, connects via stdin/stdout, and discovers the tool list.
Audit Trail
Every proxied tool call goes through the same telemetry path as built-in tools. The audit log records: user, tool name, external server name, timestamp, result status. An admin can query which users called Stripe tools in the last 24 hours and what the result was.
Available MCP Tools
| Tool | Description |
|---|---|
mcp_connection_add |
Register a new external server |
mcp_connection_list |
List registered servers with status |
mcp_connection_enable |
Re-enable a disabled connection |
mcp_connection_disable |
Disable a connection (tools removed immediately) |
mcp_connection_remove |
Delete a connection permanently |
Implementation
Three modules implement this:
MCPConnection – pure functional module. Owns the mcp_connections table schema, CRUD operations, and AES-256-CTR encryption/decryption of the auth_json credential field.
MCPConnectionRegistry – GenServer. Sole owner of the database. On startup connects to all active servers. Manages a DynamicSupervisor that runs one Hermes.Client.Supervisor per active connection. Exposes tools_for_permissions/1 and call_tool/3 to the MCP server layer.
Permissions.MCPConnection – AccessControlled wrapper. Gates admin operations (add, list, enable, disable, remove) behind permission key 140_001. Individual connections each have their own permission key for end-user access.
The client library is hermes_mcp, already in the dependency tree. The transport and client process for each connection are started dynamically via Hermes.Client.Supervisor, registered in a dedicated Elixir Registry with via-tuple names so they can be looked up and terminated by connection name.
Behavior at Runtime
| Action | Effect |
|---|---|
mcp_connection_add |
Row inserted, client started, tools cached |
mcp_connection_disable |
Row updated, client stopped, tools removed from all lists |
mcp_connection_enable |
Row updated, client restarted, tools re-cached |
mcp_connection_remove |
Row deleted, client stopped |
| User permission revoked | User no longer sees or can call those tools (next request) |
| External server down | Client retries, tools remain cached, calls fail gracefully |