Skip to content
agentFast

Tools and MCP

One error vocabulary, MCP servers by config, agents as MCP servers.

In short: tools are the things your agent can do — look up an order, issue a credit, open a ticket. agentFast gives every tool the same small set of failure messages so the agent can reason about what went wrong, and lets you plug in tools other people have already built.

Tools are where agents touch the real world, so they're where most production incidents start. Two decisions shape everything here: errors are values, never exceptions, and the error vocabulary is fixed.

The 12-code vocabulary

Every tool in agentFast fails with one of exactly twelve codes. Never free text:

CodeMeans
NOT_FOUNDThe thing doesn't exist
ALREADY_EXISTSIt does, and that's the problem
INVALID_INPUTSchema validation failed
FORBIDDENPolicy said no
RATE_LIMITEDToo many calls — includes a retry hint
TIMEOUTTook too long
UPSTREAM_ERRORA dependency failed
NETWORK_BLOCKEDEgress denied by the SSRF guard
APPROVAL_PENDINGWaiting on a human
APPROVAL_REJECTEDA human said no — carries their reason
SANDBOX_ERRORCode execution failed inside the sandbox
INTERNAL_ERRORA tool bug. The backstop
TipWhy a fixed vocabulary matters

A model reasons far better about RATE_LIMITED than about Error: 429 Too Many Requests (retry-after: 30). Free-text errors make every tool's failure a novel parsing problem; twelve codes make it a decision the model has actually seen before.

Writing a tool

One decorator handles validation, timeouts and crashes:

from core.tools.context import ToolContext
from core.tools.errors import ToolError
from core.tools.spec import tool

@tool(
    "Look up an order by id: status, items, total and delivery date.",
    group="support",
    timeout_s=10,
)
async def order_lookup(tctx: ToolContext, order_id: str) -> dict:
    order = await tctx.service("helpdesk").get_order(order_id)
    if order is None:
        raise ToolError("NOT_FOUND", f"no order {order_id}")
    return {"order": order}

The description is the first argument and it is not a comment — the model reads it to decide when to call the tool. See Custom tools for the full walkthrough.

Validation failures become INVALID_INPUT, a timeout becomes TIMEOUT, and an unhandled exception becomes INTERNAL_ERROR with the traceback logged rather than surfaced. Tools are not supposed to raise — the wrapper is the backstop, not the mechanism.

Consuming MCP servers

Any MCP server's tools become available by configuration. No code:

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

Their tools are auto-namespaced (github.create_issue), traced like native tools, rate-limited, and overridable for HITL — so an MCP tool that opens pull requests can be marked high-risk exactly like a built-in one:

tool_overrides:
  github.create_pull_request:
    risk: high
    hitl_mode: suspend

Manage them from the CLI or the dashboard's MCP panel:

agentfast mcp add github --command npx --args "-y,@modelcontextprotocol/server-github"
agentfast mcp test github     # connects for real and lists the tools
agentfast mcp remove github

Publishing your agent as an MCP server

The other direction: expose your whole agent so Claude Desktop, Cursor or Claude Code can call it.

mcp_provider:
  expose_agent: true      # the agent itself, as one tool
  expose_tools: true      # its individual tools
  expose_skills: true     # playbooks
  auto_approve: false     # HITL stays enforced across the boundary
agentfast mcp serve
CarefulHITL crosses the boundary

With auto_approve: false, a high-risk tool called through MCP still pauses and waits for a human in your approval queue — the calling client just sees APPROVAL_PENDING. Setting it to true removes that gate for every MCP caller. Be deliberate.

What ships built in

54 tools across five groups — core, shared, support, research and code. Notable guards:

  • SSRF protection on every outbound fetch — private ranges and metadata endpoints are blocked, returning NETWORK_BLOCKED.
  • Read-only SQL enforcement on database tools.
  • A deny-list on run_shell, plus sandboxed execution for the code tools.

These are defaults, not suggestions. A tool that reaches the network goes through the guard whether it's built in or one you added.