Take actions in your product

An agent that operates your product for one end-user through your REST API. A customer types "refund my last invoice"; the agent looks it up and issues the refund through your API, pausing for a human on the write.

Three moves:

  • Your product becomes the agent's tools. Describe REST endpoints once (custom tools). m8tes calls them server-side; the secret never reaches the agent.
  • It acts as one end-user. Pass user_id (Users). Isolation is strict.
  • Writes wait for a human. Custom tools ask for approval until you mark the server trusted.

Wrap your REST API

Each endpoint you describe becomes a tool the agent can call by name:

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, never seen by the agent
    tool_defs=[
        {"name": "get_invoice", "method": "GET", "path": "/invoices/{id}"},
        {"name": "create_refund", "method": "POST", "path": "/refunds"},
    ],
)
print(server.slug)  # "acme-billing"

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). Auth methods and path rules: Custom tools.

Scope it to an end-user

If every customer hits your API with the same key, keep one account-level server and scope at the agent and run (next section). If each customer has their own credentials, create one server per customer:

Python
server = client.mcp_servers.create(
    name="acme billing",
    url="https://api.acme.com/v1",
    auth_type="bearer",
    secret=customer_api_key,   # this customer's own key
    user_id="cust_123",        # visible only under this end-user
    tool_defs=[{"name": "get_invoice", "method": "GET", "path": "/invoices/{id}"}],
)

For your customers' Gmail, Slack, and other catalog apps, use per-end-user OAuth instead (Tools).

Attach it and run it

Reference the server's slug in an agent's tools, then run for a customer:

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

run = client.runs.create(
    agent_id=ops.id,
    message="refund invoice 8842",
    user_id="cust_123",
    stream=False,
)
print(client.runs.poll(run.id).output)

The agent sees the tool as mcp__cmcp-<id>__create_refund; you only ever use the slug. Custom slugs attach to an agent: unlike catalog tools, they can't be passed per task or per run (a run-level tools list containing a custom slug is rejected). Give the agent its custom tools once, at create or update.

Keep a human in the loop

Read-only (GET) tools run without asking, in every permission mode. They can't change anything. Writes ask before each use. To let a write run unattended, mark its server trusted:

Python
# writes you're happy to run unattended (scheduled, webhook, API runs)
client.mcp_servers.update(writes.id, auto_approve=True)

An untrusted write pauses for approval; in an unattended run with no one to approve, it is marked needs-approval and never silently fires. Handle approvals inline with runs.wait(...), or from your inbox or Slack. See Human-in-the-loop.

What's guaranteed (full detail): egress is server-side and IP-pinned (the agent never connects to your service directly; private and metadata addresses are blocked); your secret is encrypted at rest and never reaches the agent; a server is visible only to its owner and, when scoped, its user_id end-user.

End-to-end: refund an invoice with approval

A billing product lets its agent look up invoices on its own but requires a human to approve refunds, all for one customer:

Python
from m8tes import M8tes

client = M8tes()  # reads M8TES_API_KEY

# read-only lookups → GET tools never prompt, so no auto_approve needed
lookups = client.mcp_servers.create(
    name="acme billing reads", url="https://api.acme.com/v1",
    auth_type="bearer", secret="sk-live-...",
    tool_defs=[{"name": "get_invoice", "method": "GET", "path": "/invoices/{id}"}],
    user_id="cust_123",
)

# refunds → untrusted, every call waits for a human
writes = client.mcp_servers.create(
    name="acme billing writes", url="https://api.acme.com/v1",
    auth_type="bearer", secret="sk-live-...",
    tool_defs=[{"name": "create_refund", "method": "POST", "path": "/refunds"}],
    user_id="cust_123",
)

ops = client.agents.create(
    name="billing ops",
    tools=[lookups.slug, writes.slug],
    user_id="cust_123",
)

run = client.runs.create(
    agent_id=ops.id,
    message="invoice 8842 was double-charged. refund it.",
    user_id="cust_123",
    human_in_the_loop=True,
    permission_mode="approval",
    stream=False,
)

run = client.runs.wait(run.id, on_approval=lambda req: "allow")
print(run.output)

The lookup ran on its own; the refund paused for on_approval. Surface req.tool_input in your UI so a human can decide before you return "allow" or "deny".

Next: Your API as MCP tools · Custom tools · Users · Human-in-the-loop · Going live

Was this page helpful?