Skip to content
agentFast
The production layer

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:

| Code | Means | |---|---| | NOT_FOUND | The thing doesn't exist | | ALREADY_EXISTS | It does, and that's the problem | | INVALID_INPUT | Schema validation failed | | FORBIDDEN | Policy said no | | RATE_LIMITED | Too many calls — includes a retry hint | | TIMEOUT | Took too long | | UPSTREAM_ERROR | A dependency failed | | NETWORK_BLOCKED | Egress denied by the SSRF guard | | APPROVAL_PENDING | Waiting on a human | | APPROVAL_REJECTED | A human said no — carries their reason | | SANDBOX_ERROR | Code execution failed inside the sandbox | | INTERNAL_ERROR | A 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 import ToolContext, ToolError, 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.