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
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:
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.
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").
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:
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:
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_idend-user it was created for).
Manage
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