Exposing your tools over MCP
An agent can only do what your API lets it. You expose that surface as an MCP server — a small HTTPS service that advertises a set of tools and runs them on request. Delegate discovers the tools your server offers and presents each to the agent.
Write the server
Use the official mcp Python package
with your existing framework. A tool is a function with a typed signature; the
package derives the schema the model sees from it. A minimal server for the
photo example:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("photos")
@mcp.tool()
def search_photos(
season: str | None = None, people: list[str] | None = None
) -> dict:
"""Search this customer's photo library.
Returns photos matching the filters, each with an id, a thumbnail URL,
a taken-at date, and a precision.
"""
customer = current_customer() # from the authenticated token
return photo_service.search(customer, season=season, people=people or [])
Two things about that current_customer() call are the whole point of this
page, and are covered under scoping below.
What makes a good tool
The agent's quality is capped by your tools, not by the model. Three things matter more than they look:
-
The description is load-bearing. It is the text the model reads to decide whether and how to use the tool.
"""Search this customer's photo library"""is doing real work; a vague description produces vague behaviour. -
Resolve ambiguous inputs on your side, not the model's. "Winter" is a hemisphere question; "last summer" is a date-math question. If your search takes a
seasonand resolves it against the customer's location, the model hands over a word and you return the right photos. If it doesn't, the model does the arithmetic and gets it subtly wrong. -
Return structure, not prose. Ids, dates with an explicit precision, match reasons. The agent turns those into an answer; your tool shouldn't try to write the answer itself.
Scoping is the credential
Delegate presents a per-customer token to your server on every call — the one you issued for that customer when you provisioned them. Your server derives which customer's data to touch from that token, and from nothing else.
Do not accept a customer id as a tool argument. If you did, you could not tell an id the agent was legitimately given from one a model invented — they arrive in the same JSON. Deriving it from the authenticated token closes that gap: the model has no way to author a token, so it has no way to reach another customer's data. This is the property that lets you prove your own isolation by reading your own auth middleware.
def current_customer():
token = request.headers["Authorization"].removeprefix("Bearer ")
return verify_and_resolve(token) # your code, your boundary
Mutations need their own gate
A search tool is safe to let an agent call freely. A tool that changes something — sets a date, deletes a photo — is not, because the text the agent reasons over includes data you don't control (a filename, a caption) and could carry an instruction. Two rules:
- Never let the agent perform a mutation the customer hasn't approved. Have
the agent propose changes (Delegate's
planmode is built for this), show the customer a diff, and only apply what they accept. - Enforce that on your side, not with a prompt. The write tool should reject any change that doesn't carry an approval your UI issued for that specific record and value. "The agent was told not to" is not a control.
See the security model for why this is non-negotiable rather than cautious.
Register the server
Once your server is live, register it with your tenant. First add your server's
host to the tenant's egress allow-list — Delegate will not call a host that
isn't on it. You manage this yourself as a tenant admin
(PUT /v1/custom-tools/{tenant_id}/egress-hosts), so the whole setup is
self-service. Then register the server:
POST /v1/custom-tools/{tenant_id}
{
"name": "photos",
"display_name": "Photo tools",
"description": "Search and manage a customer's photos.",
"definition": {
"adapter": "mcp",
"url": "https://mcp.your-app.com/mcp",
"name_prefix": "photos"
}
}
name_prefix (optional) keeps your tool names clear of Delegate's built-ins and
of any other server — a server offering a tool called run would otherwise
collide, and the prefixed photos_run doesn't.
You can also do both steps from the Organization → Integrations screen: set the allow-list and register tools without touching the API. Discovery — reading what your server actually offers — happens per agent, and provisioning runs it for you, so there's nothing more to do here.