Skip to content

Tools

A tool lets an agent do something: look up an order, check the weather, refund a payment. Every tool is an HTTP call. You describe the call once, and the model decides when to make it and with what arguments.

Maitento splits this into two files:

  • A connector describes the HTTP endpoint: method, URL, parameters, the shape of the body, and what kind of authentication it needs.
  • A tool is what the agent sees. It points at a connector, tells the model when to use it, fixes any values the model must not choose, supplies the credential, and can require a person’s approval.

One connector can back several tools. Tools are known as external operations (extops) in Maitento: that’s the name of their folder (external-operations/), of their API and of the mt extop-… commands. These docs call them tools.

Three tutorials build them up: tutorial 8, Your first tool calls a public weather API, tutorial 9, Call your code calls your own server with a secret, and tutorial 10, Approvals makes a person approve each refund.

connectors/open-api/open-meteo-forecast.mtr
// The connector: the HTTP call itself. Open-Meteo's forecast API is free and needs no key.
method = "GET"
url = "https://api.open-meteo.com/v1/forecast"
description = "Current weather for a latitude and longitude, from Open-Meteo"
parameters.query = [
{
name = "latitude"
description = "Latitude in decimal degrees, for example 51.5072"
type = "number"
},
{
name = "longitude"
description = "Longitude in decimal degrees, for example -0.1276"
type = "number"
},
{
name = "current"
description = "Comma-separated list of current weather variables"
},
{
name = "temperature_unit"
description = "celsius or fahrenheit"
}
]
external-operations/open-api/current-weather.mtr
// The tool: what the agent sees. Its name is the file's name, its description is the guidance, and its arguments
// are the connector's parameters that nothing else fills in.
connector = @ref("open-meteo-forecast")
// A fixed value: Maitento adds it to every call, and the model never sees this parameter.
bindings.query:current = "temperature_2m,wind_speed_10m,weather_code"
guidance = "Gets the current temperature, the wind speed in km/h and the WMO weather code for a place. Work out the latitude and longitude of the place yourself before you call it. The response says which temperature unit it uses."

Then give the tool to an agent with functions = [ { ref = @ref("current-weather") } ], or to every agent in an interaction with functions.openApi.

PropertyRequiredWhat it does
methodyes"GET", "POST", "PUT", "PATCH" or "DELETE"
urlyesthe full URL; {name} marks a path parameter
descriptionyeswhat the endpoint does (500 characters at most)
parameters.path, parameters.queryno[ { name description required type } ]; type is string, number, integer or boolean
schema.requestnoJSON Schema of the request body. Without one, no body is sent, even for a POST
schema.responsenoJSON Schema the response must match
authno"None" (the default), "Header" or "Maitento": see below
PropertyWhat it does
connector@ref to the connector
guidancewhen and how to use it. The model reads this, so write it for the model
bindings.request:<field>, bindings.query:<name>, bindings.path:<name>fixed values the model never sees or chooses
auth = { header = "X-Api-Key" secret = @ref("my-secret") }sends a secret in a header
requiresApproval = truea person must approve each call first

Fix every value the model has no reason to choose. A fixed value (a binding) is added to every call, and the model never sees the parameter:

  • It saves the model’s context. Every parameter the model sees is described in every request it makes. A fixed one costs nothing.
  • It stops mistakes. Models get ids, GUIDs, units and flags wrong: a misspelt variable, an id from the wrong account, a flag left out. A fixed value is right every time, and the model can’t be talked into changing it.

When the value isn’t the same for every run, such as a customer id, let the run supply it instead: see values from the run.

This is the request your endpoint receives:

  • Method and URL come from the connector, with the path and query parameters filled in.
  • The body is the model’s arguments as JSON, already checked against your schema.request, with your bindings applied. A call whose arguments don’t match the schema never reaches you: the model is told what was wrong.
  • Authentication, from auth:
    • None sends no credential.
    • Header sends the secret’s value in the header you named. For an Authorization header, a value without a space is sent as Bearer <value>.
    • Maitento signs the call with a short-lived Maitento token. It is for connectors that call Maitento’s own API, as the knowledge and file system examples do, and only those: a connector with auth = "Maitento" whose URL, or a redirect, names any other host is refused (api:connectors:address-blocked), so the token never reaches anyone but Maitento.

What your response does:

  • Any 2xx response: its body is given to the model as the tool’s result.
  • A redirect: Maitento follows up to five. The credential goes only to the address you configured, never to another host a redirect points at, and never from https down to http.
  • Any other status: the model gets an error with the status and the body, and may try again or explain the problem.

Answer within 100 seconds. Very large responses are cut short.

Tutorial 9 builds a small orders API in TypeScript, Python and C# that an agent calls.

With requiresApproval = true, the run stops before the call with status PendingApproval. Its pendingApproval says which tool and with what arguments:

{
"id": "8344c6ae-bc78-49c2-98fd-19dd69fde582",
"functionName": "refund-order",
"argumentsJson": "{\"request\":{\"amount\":12.5,\"reason\":\"Blue mug arrived smashed\"},\"additionalInputs\":{\"path.orderId\":\"A-1001\"}}",
"status": "Pending",
"agentName": "Support"
}

Approve it and Maitento makes the call. Reject it with a reason, and the agent is told why and redrafts.

await mt.interactionRuns.approve(run.id, { approvalId: run.pendingApproval!.id });
await mt.interactionRuns.reject(run.id, { approvalId: run.pendingApproval!.id, reason: "Refund the mug only" });

A redrafted call is a new approval, with a new pendingApproval.id, and each approval can be answered once. The SDKs’ run() helpers call your onWaiting callback (on_waiting in Python, OnWaiting in C#) each time the run starts waiting for a person, so a script that approves from onWaiting is asked again for every redraft. See it in tutorial 10, Approvals.

Approvals happen only in interactions. A tool that needs approval, called from an app, is not run: the call fails with an error that says so, because an app has nobody to ask.

Sometimes a value belongs to the run, such as a customer id or a knowledge base id, and the model must never choose it. Declare it on the interaction with inputs.functions.binding. The value is passed as a run input and written straight into the tool call:

interactions/one-shot/ask-handbook.mtr
agent = {
name = "HandbookAssistant"
agent = @ref("handbook-assistant")
}
task = "An employee asks: %%question%%"
inputs.prompts = [
{
name = "question"
description = "The employee's question"
required = true
}
]
functions.openApi = [
{
name = "search-handbook"
ref = @ref("search-handbook")
}
]
// A run input, partitionId, goes straight into the tool's request body.
// The model never sees it or chooses it: it only writes the query.
inputs.functions.binding = [
{
function = @ref("search-handbook")
name = "partitionId"
target = "partitionId"
description = "The knowledge base to search"
required = true
type = "request"
}
]
FieldWhat it does
namethe run input that carries the value, passed like any other input (partitionId above)
functionthe tool whose calls get the value
agentthe agent whose tool it is, when the tool comes from that agent’s functions. Leave it out when the interaction hands the tool out itself, with functions.openApi, as above. A binding for an agent’s tool with no agent is refused when you deploy
typewhat target names: request (a field of the request body, the default), query or path (a parameter)
targetthe field or parameter’s name. Separate the names of a nested body field with /
required, default, descriptionas for any input: see below

A binding with a default (or an inputs.functions.auth input with a default secret) uses it when a run leaves the input out or passes it empty, even when the input is required. A required input with no default must be given, or the run is refused with api:interactions:input-required.

Like a fixed value, the parameter is never shown to the model, so the agent can’t change it, even if someone asks it to. See it in tutorial 8 (a query parameter with a default), tutorial 13 (the knowledge base to search) and tutorial 15 (one file system id for three tools).

A tool’s auth names one secret. When the secret depends on the run, such as one API key per shop or per customer, let the run choose it with inputs.functions.auth:

interactions/one-shot/support-any-shop.mtr
// The support desk for any shop: each run says which shop's key the get-order tool sends.
agent = {
name = "Support"
agent = @ref("support-agent")
}
task = "A customer writes: %%message%%"
inputs.prompts = [
{
name = "message"
description = "What the customer wrote"
required = true
}
]
// The run passes the id of a secret, and Maitento sends that secret's value in X-Api-Key instead of the one the tool
// names. The agent never sees it.
inputs.functions.auth = [
{
name = "shopKey"
description = "The id of the secret that holds the shop's API key"
function = @ref("get-order")
agent = @ref("support-agent")
header = "X-Api-Key"
required = true
}
]
FieldWhat it does
namethe run input. Its value is the secret’s id: not its name, and never its value
function, agentwhich tool, and whose, as for a binding
headerthe header the secret’s value is sent in (Authorization when left out)
requiredwhether a run must choose one
secretthe default, as @ref("…"): the secret used when a run doesn’t choose one
  • The run’s secret replaces the tool’s own auth for that run. A run that chooses none sends the input’s default secret; with no default, an optional input leaves the tool’s own auth in place.
  • A name is refused when the run starts. The run fails to start with api:interactions:input-invalid (requires valid GUID for secret ID shopKey - but value is: second-shop-api-key). Look the secret up by name and pass its id.
  • An id that isn’t one of your secrets starts the run, but the tool call fails: the agent is told Could not find secret … for connector … as the tool’s result, and nothing is sent.
  • The agent never sees which secret it uses, so a customer of one shop can’t talk it into reading another’s data.

See it in tutorial 9, Call your code, where one support agent serves two shops.

  • Public addresses only: Maitento calls your endpoint from inside its own network, so it only calls addresses on the public internet. A connector whose URL is localhost, a loopback, private or link-local address (127.0.0.1, 169.254.169.254) or an internal name (orders-api, *.internal, *.local) is refused when you deploy it. A name that resolves to such an address is refused when the tool is called, and so is a redirect to one. The tool’s result is then an error with the code api:connectors:address-blocked:

    {"error": "Maitento won't call orders.internal.example.com: the address isn't on the public internet. Tools can only call public addresses, never localhost, private networks or internal names.", "code": "api:connectors:address-blocked"}

    Any port is fine. Maitento’s own hosts are refused too, except Maitento’s API from a connector with auth = "Maitento". To try a tool against a server on your own machine, give it a public address with a tunnel, as tutorial 9 does.

  • A Header connector needs its secret. A tool on a connector with auth = "Header" gets its secret from the tool’s own auth, from its binding in the interaction, or from a run input (inputs.functions.auth). An interaction that gives a tool none of these is refused when you deploy it, with the code api:connectors:secret-missing. If the secret is an optional run input and a run isn’t given it, the agent still sees the tool, and calling it returns an error with the same code.

  • Optional parameters: the model is shown every parameter as required. In the schema, make a field that may be missing accept null, and list only the fields your endpoint truly needs under required.

  • Turning a version off: connectors and tools are versioned. If a version misbehaves, disable it (mt connector-version-disable, mt extop-version-disable) and everything pinned to it stops calling it.

  • MCP: connectors to MCP servers can be created but can’t be called yet. Use HTTP (OpenAPI) connectors.