Custom Tools

Wrap your own REST API as agent tools. You define typed endpoints; m8tes exposes each as a named tool and calls it server-side (IP-pinned, secret never exposed to the agent).

Use this when the app isn't in the catalog and you already have a REST API.

One agent per customer in your product? See Your API as MCP tools. Single-tenant version: Take actions in your product.

Create a custom tool

Python
from m8tes import M8tes

client = M8tes()

server = client.mcp_servers.create(
    name="acme billing",
    url="https://api.acme.com/v1",
    auth_type="bearer",
    secret="sk-live-...",            # write-only, never returned
    tool_defs=[
        {"name": "get_invoice", "method": "GET", "path": "/invoices/{id}"},
        {"name": "create_refund", "method": "POST", "path": "/refunds"},
    ],
)
print(server.slug)  # "acme-billing"

Each entry in tool_defs becomes a tool the agent sees by name (get_invoice, create_refund). Put {param} in a path to make it an argument (/invoices/{id}); any other arguments the agent passes go to the query string (GET/DELETE) or the JSON body (POST/PUT/PATCH). In the dashboard each tool_def shows as an Action ("Add action" adds one), but the SDK param name stays tool_defs.

Kinds

kind on create:

kindWhat you supplyWhen to use
rest_api (default)tool_defs with method + pathYou have a REST API
mcp_http / mcp_sseA remote MCP URL, empty tool_defsThe vendor hosts an MCP server
scriptPython script_source + tool_defs with name only (no method/path)The job needs logic a single REST call can't express

Remote MCP discovery refuses catalogs above 4 MiB per response, 64 pages, 256 tools, 256 KiB per input schema, or 2 MiB for the stored manifest. These limits apply during setup and later catalog refreshes, not to ordinary tool-call responses.

Script-tool ruleBehavior
ScopeAccount-only; user_id is not accepted
Strict accountsCan create without user_id; fetch by returned id. Unscoped listing still requires strict mode off
SourceWrite-only; reads return script_sha256
SecretsUse the agent's request_tool_key card. Never place secrets in script_source
Tool nameMust be a Python identifier; the runtime calls def <name>

For unattended script tools (scheduled tasks, webhooks, agent-driven setup), pass auto_approve=True on create. Updating script_source, the origin URL, allowlist, or tool defs clears trust. Call client.mcp_servers.approve(server.id) (or update(..., auto_approve=True)) after you review the new source, or the run fail-closes with script_changed ("script or allowlist changed since approval").

Python
server = client.mcp_servers.create(
    name="aircall calls",
    url="https://api.aircall.io",          # origin only, no path
    kind="script",
    auth_type="bearer",
    auto_approve=True,                     # required for headless / agent-driven runs
    script_source=(
        "def fetch_calls(from_date: str) -> dict:\n"
        "    r = http('GET', 'https://api.aircall.io/v1/calls')\n"
        "    return {'status': r['status'], 'body': r['body']}\n"
    ),
    tool_defs=[{"name": "fetch_calls", "description": "List recent calls"}],
)
print(server.script_sha256)  # source is never returned

# After you change the source:
# client.mcp_servers.update(server.id, script_source="...")
# client.mcp_servers.approve(server.id)

The script runs in a sidecar jail with no network except the injected http() helper (and json). Caps: 20 calls and 1 MiB of response per job. Extra origins go in script_allowlist (max 8).

Attach to an agent

Reference the server's slug in the agent's tools list, alongside catalog tools:

Python
client.agents.create(name="ops", tools=["gmail", "acme-billing"])

In the dashboard, open Apps → Add custom tool to create one, then enable it on an agent from its Configure → Custom tools section.

An agent can also create a REST tool mid-run without a setup approval. Remote MCP setup follows the normal approval flow because it contacts the supplied server. The agent cannot supply credentials or mark the tool trusted. You provide any required key; using the new tool follows the run's permission policy.

Authentication

The secret is encrypted at rest and injected by m8tes when it makes the call. Pick the auth_type your API expects:

auth_typeWhat m8tes sendsExtra auth_config
nonenothingnone
bearerAuthorization: Bearer <secret>none
oauth_tokenAuthorization: Bearer <secret>none
custom_header<header>: <secret>{"header_name": "X-API-Key"}
api_key_in_url?<param>=<secret>{"query_param": "api_key"}
Python
client.mcp_servers.create(
    name="search service",
    url="https://search.internal-acme.com",
    auth_type="custom_header",
    auth_config={"header_name": "X-API-Key"},
    secret="key-...",
    tool_defs=[{"name": "search", "method": "GET", "path": "/search"}],
)

Permissions

Read-only tools never ask. A GET tool def runs immediately, in every permission mode. It can't mutate anything, so it is treated like any other read. Plan for this: if a lookup against your API is itself sensitive, don't model it as GET, and don't rely on an approval prompt to gate it.

Write tools ask before each use. For trusted unattended work, set Run without asking each time or pass auto_approve=true to client.mcp_servers.create/update.

An untrusted write pauses for approval. With nobody watching, the run is marked needs-approval.

Trust is per server, not per tool, so the usual shape is two servers against the same API: one holding the GET defs, one holding the writes you want gated.

Security model

  • Egress is server-side and IP-pinned. The agent never connects to your service directly. m8tes resolves and pins the host to a validated public IP per call (no DNS-rebind), disables redirects, and caps the response. Private/metadata addresses are blocked.
  • The secret never reaches the agent. It is injected into the outbound request by m8tes and scrubbed from any echoed response. API responses carry only has_secret, never the value.
  • Strictly scoped. A custom tool is visible only to its owner (and, for multi-tenant setups, the user_id end-user it was created for).

Manage

Python
client.mcp_servers.list(user_id="customer_123")
client.mcp_servers.get(server.id)
client.mcp_servers.update(server.id, status="disabled")  # stop using without deleting
client.mcp_servers.delete(server.id)

Next: Your API as MCP tools · Take actions in your product · Human-in-the-loop · Users

Was this page helpful?