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:
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:
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:
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:
# 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:
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