Every agent container connects to a per-group MCP server over a unix socket. The server is started by gated before the container spawns, and torn down after the container exits. To give agents a new capability — look up a user, query a database, call an internal API — you add a tool to this server. The agent sees it in its MCP manifest and can call it with use_mcp_tool.
ipc/ipc.go defines GatedFns — a struct of function fields that the gateway wires before starting the MCP server. Add your function:
// ipc/ipc.go
type GatedFns struct {
SendMessage func(jid, content string) error
SendReply func(jid, replyTo, content string) error
SpawnGroup func(name string, parentFolder string) (string, error)
// ... existing fields ...
LookupUser func(jid string) (string, error) // add this
}
In the same file, find ServeMCP and add the tool registration alongside the existing tools:
// ipc/ipc.go (inside ServeMCP)
s.AddTool(
mcp.NewTool("lookup_user",
mcp.WithDescription("Look up display name and profile for a user JID"),
mcp.WithString("jid",
mcp.Required(),
mcp.Description("The user's JID, e.g. telegram:123456789"),
),
),
func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
jid := req.Params.Arguments["jid"].(string)
result, err := fns.LookupUser(jid)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(result), nil
},
)
When gated calls ipc.ServeMCP, it passes a GatedFns value. Add your implementation there:
// gated/main.go
fns := ipc.GatedFns{
SendMessage: func(jid, content string) error { /* ... */ },
// ... existing wiring ...
LookupUser: func(jid string) (string, error) {
return store.LookupUserProfile(db, jid)
},
}
The implementation can call anything the gateway has access to: the SQLite store, an HTTP API, an in-memory cache. It runs in the gateway process, not in the container.
No changes to the agent container. The tool appears in the MCP manifest automatically on next invocation:
// agent code (Claude Code CLI, any tool call syntax)
const result = await use_mcp_tool("arizuko", "lookup_user", {
jid: "telegram:123456789"
})
// result: "Alice (alice_tg) — joined 2024-03-01"
If you want to restrict the new tool to specific groups, add a grant rule:
# groups/main/.grants
* # tier 0: all tools including lookup_user
# groups/main/public/.grants
send_reply # tier 2: send_reply only — lookup_user not visible
The buildMCPServer function in ipc/ipc.go filters the tool list against the group's grants before sending the manifest. An agent in a restricted group literally cannot see the tool in its manifest. Grant rules determine capability, not just allowed calls.
The socat bridge in settings.json is all the wiring the container needs:
{
"mcpServers": {
"arizuko": {
"command": "socat",
"args": ["STDIO", "UNIX-CONNECT:/workspace/ipc/router.sock"]
}
}
}
The unix socket path is volume-mounted into the container. The agent connects, gets the filtered manifest, and calls tools. The gateway handles the call on the other side of the socket. There is no separate sidecar process to configure — the MCP server is the gateway, accessed through the socket.
Because grant rules filter the manifest before it is sent, agents that cannot call a tool cannot see it. There is no second enforcement layer. The manifest is the policy.