Skip to main content

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 season and 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 plan mode 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.