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"},
    ],
    user_id="customer_123",
)
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"}],
    user_id="customer_123",
)
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"], user_id="customer_123")

In the dashboard, open Apps → Custom tools → 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.

Every tool reports who made it and who can use it. created_by_teammate_id is the agent that set it up (null when a person did), and teammate_ids lists the agents it is attached to. The dashboard shows both as Made by and Used by.

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"}
oauth2Authorization: Bearer <access token> from a sign-in, refreshed for younone
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"}],
    user_id="customer_123",
)

Sign-in (OAuth) MCP servers

Most hosted MCP servers want an OAuth sign-in, not a key. Create them with auth_type="oauth2" and no secret. m8tes finds the sign-in, registers itself with the vendor, and stores and refreshes the tokens.

Python
srv = client.mcp_servers.create(
    name="beehiiv",
    url="https://mcp.beehiiv.com/mcp",
    kind="mcp_http",
    auth_type="oauth2",
    user_id="customer_123",
)
link = client.mcp_servers.start_oauth(srv.id, user_id="customer_123")
print(link.authorization_url)  # the owner signs in here, then lands on m8tes Apps

The owner returns to the m8tes Apps page, which completes the sign-in and discovers the tools. sign_in_host names where they sign in, and has_secret turns true once they have. Clear it with update(srv.id, secret=None) to sign out. An agent that creates one of these gets the sign-in detected automatically and asks the owner to sign in.

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, user_id="customer_123")
client.mcp_servers.update(server.id, status="disabled", user_id="customer_123")  # stop using without deleting
client.mcp_servers.delete(server.id, user_id="customer_123")

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

Was this page helpful?