relay/tool
Typed MCP tools: a definition, its handler, and what the handler sees.
define(name, input_codec, output_codec) admits a tool from two
Blueprint codecs, and define_content(name, input_codec) admits one that
returns only content blocks. Both panic with the tool name on a
definition mistake, such as an empty name or a non-object input schema,
because tool definitions are written in source code; try_define and
try_define_content return a typed DefineError for definitions built
from runtime data. The with_* setters add a title, a description, the
four behavior hints, icons, _meta, required client capabilities and an
explicit input schema.
The same Definition serves the server and the client: bind a handler
with handle, handle_with_error_renderer or handle_call and pass the
tools to relay/server.new; call it with relay/client.call.
declaration returns what tools/list publishes, and input_codec,
output_codec and name let an adapter reuse the contract.
import json/blueprint/codec
import relay/tool
pub fn greet() -> tool.Tool(Nil) {
let input = {
use name <- codec.field("name", codec.string(), get: fn(name) { name })
codec.success(name)
}
tool.define("greet", input, codec.string())
|> tool.with_description("Greets the user by name")
|> tool.with_read_only_hint(True)
|> tool.handle(fn(name) { Ok("Hello, " <> name <> "!") })
}
handle hides the handler’s error behind the generic message
Tool execution failed.. handle_with_error_renderer publishes the
ToolError a renderer builds, and handle_call receives the Call:
application context, input responses, progress reporting, a
cancellation signal, the invocation id, the JSON-RPC request id and the
correlation.
Ids and retries
MCP 2026-07-28 has no session and no idempotency key, so nothing in
the protocol identifies a retried call. A handler has three ids:
idempotency_key(call)is the key the client chose for one logical request (relay/client.with_idempotency_key), carried in_metaunderio.github.gleam-dream/idempotency-key. A retry that carries the same key is the client’s promise that it is the same request. Relay accepts 1 to 128 visible ASCII characters and refuses a request with any other value as invalid params. The key is not authenticated: key idempotent work by the principal the server authenticated (such as therelay/authorization.Grant’s) together with the key, so one client cannot replay or block another’s work.invocation_id(call)is Relay’s own: unique among the invocations this node has run since it started, and new for every request, retries included. It joins the call’s telemetry.request_id(call)is the JSON-RPCidexactly as the client sent it. The client chooses it, and the protocol only requires that it differ from that client’s other requests in flight. It may repeat across clients, and a retry carries the same id only when the client reuses it on purpose. Relay’s client sends a fresh id per request.
Recognise a retry by the idempotency key, not the request id, and never treat any of these values or the correlation as proof of who sent the request.
Types
One invocation as a handle_call handler sees it. Read it with
context, input_responses, report_progress, cancelled,
invocation_id, request_id, idempotency_key, correlation and
client_info.
pub type Call(context) =
@internal Call(context)
The client’s name and version, from the request metadata.
pub type ClientInfo {
ClientInfo(name: String, version: String)
}
Constructors
-
ClientInfo(name: String, version: String)
A tool as tools/list publishes it: local declarations come from
declaration and tool_declaration, remote ones from
relay/client.list_tools. The schemas are JSON Schema documents as exact
Blueprint values; input_contract loads the input schema for
validation. Read fields by label: a later revision may add fields.
pub type Declaration {
Declaration(
name: String,
title: option.Option(String),
description: option.Option(String),
input_schema: value.Value,
output_schema: option.Option(value.Value),
annotations: ToolAnnotations,
icons: List(content.Icon),
meta: List(#(String, value.Value)),
)
}
Constructors
-
Declaration( name: String, title: option.Option(String), description: option.Option(String), input_schema: value.Value, output_schema: option.Option(value.Value), annotations: ToolAnnotations, icons: List(content.Icon), meta: List(#(String, value.Value)), )
Why try_define refused a definition. define panics with the same
description.
pub type DefineError {
EmptyName
NameTooLong(name: String, max: Int, actual: Int)
InvalidNameCharacter(name: String, character: String)
MissingInputSchema(name: String)
InputSchemaNotObject(name: String, kind: String)
MissingOutputSchema(name: String)
OutputSchemaNotObject(name: String, kind: String)
}
Constructors
-
EmptyName -
NameTooLong(name: String, max: Int, actual: Int) -
InvalidNameCharacter(name: String, character: String) -
MissingInputSchema(name: String)The input codec has no schema, for example a
codec.customwithout one. -
InputSchemaNotObject(name: String, kind: String)MCP tool arguments are a JSON object; the input schema’s root is not.
-
MissingOutputSchema(name: String) -
OutputSchemaNotObject(name: String, kind: String)
An admitted tool contract: its name, metadata, input codec and output. The same value drives listing, registration and typed client calls.
pub type Definition(input, output) =
@internal Definition(input, output)
A server-initiated method a tool or prompt may ask the client to answer.
pub type InputMethod {
Elicitation
Sampling
Roots
}
Constructors
-
Elicitationelicitation/create: ask the user to fill a form. -
Samplingsampling/createMessage: ask the client’s model for a completion. -
Rootsroots/list: ask for the client’s filesystem roots.
One request in an input round. params is the method’s params object
as defined by the pinned revision.
pub type InputRequest {
InputRequest(method: InputMethod, params: value.Value)
}
Constructors
-
InputRequest(method: InputMethod, params: value.Value)
What a handle_call handler returns: complete,
complete_with_content or request_input.
pub type Reply(output) =
@internal Reply(output)
A JSON-RPC request id, which MCP allows to be a string or an integer.
StringId("1") and IntegerId(1) are different ids.
pub type RequestId {
StringId(String)
IntegerId(Int)
}
Constructors
-
StringId(String) -
IntegerId(Int)
A definition bound to a handler, ready for relay/server.new.
pub type Tool(context) =
@internal Tool(context)
The annotations of a tool declaration. None means the server did not
publish the hint. Read fields by label.
pub type ToolAnnotations {
ToolAnnotations(
title: option.Option(String),
read_only_hint: option.Option(Bool),
destructive_hint: option.Option(Bool),
idempotent_hint: option.Option(Bool),
open_world_hint: option.Option(Bool),
)
}
Constructors
-
ToolAnnotations( title: option.Option(String), read_only_hint: option.Option(Bool), destructive_hint: option.Option(Bool), idempotent_hint: option.Option(Bool), open_world_hint: option.Option(Bool), )
Values
pub fn cancelled(call: Call(context)) -> process.Selector(Nil)
Fires once when the call is cancelled: the client disconnected, sent a cancellation, or the invocation timed out. Select on it while the handler waits, and stop work it started elsewhere: Relay waits up to the runtime’s cancellation grace period for the handler to return, then kills it.
pub fn client_info(
call: Call(context),
) -> option.Option(ClientInfo)
The client’s name and version, when the request declared them.
pub fn complete(output: output) -> Reply(output)
A finished call. A structured tool also gets a text block that mirrors the encoded value, for clients that read only content.
pub fn complete_with_content(
output: output,
blocks: List(content.ContentBlock),
) -> Reply(output)
A finished call with explicit content blocks instead of the text mirror.
pub fn complete_with_meta(
output: output,
meta: List(#(String, value.Value)),
) -> Reply(output)
A finished call whose generated content blocks carry _meta. Works
for structured and content-only definitions, preserving their normal
rendering. The supplied members replace each generated block’s metadata.
Useful for application request ids and links to asynchronous work.
pub fn context(call: Call(context)) -> context
The application context the transport built for this request.
pub fn correlation(
call: Call(context),
) -> correlation.Correlation
This request’s correlation: the transport’s (relay/http.with_correlation),
else the one the client sent (relay/client.with_correlation), else a
fresh one. Pass it to the packages the handler calls. It is client
input: use it to join telemetry, never to authorize.
pub fn declaration(
definition: Definition(input, output),
) -> Declaration
What tools/list publishes for this definition.
pub fn define(
name: String,
input: codec.Codec(input),
output: codec.Codec(output),
) -> Definition(input, output)
Admits a tool whose result is a structured value encoded by output.
Panics with the tool name when the name is invalid or a codec has no
schema; use try_define for definitions built at runtime.
pub fn define_content(
name: String,
input: codec.Codec(input),
) -> Definition(input, List(content.ContentBlock))
Admits a tool whose result is only content blocks. The output type is
List(ContentBlock). Panics like define.
pub fn describe_define_error(error: DefineError) -> String
A one-line description of a definition error.
pub fn error_message(text: String) -> ToolError
A tool failure with one text block.
pub fn error_with(
blocks: List(content.ContentBlock),
structured: option.Option(value.Value),
) -> ToolError
A tool failure with its own content and optional structured value.
pub fn handle(
definition: Definition(input, output),
handler: fn(input) -> Result(output, error),
) -> Tool(context)
Binds a handler. An Error reaches the client as the generic message
Tool execution failed., so the handler’s private error never leaks.
pub fn handle_call(
definition: Definition(input, output),
handler: fn(Call(context), input) -> Result(
Reply(output),
ToolError,
),
) -> Tool(context)
Binds a handler that receives the Call and returns a Reply: a result,
a result with explicit content, or another input round.
pub fn handle_with_error_renderer(
definition: Definition(input, output),
handler: fn(input) -> Result(output, error),
render: fn(error) -> ToolError,
) -> Tool(context)
Binds a handler and publishes the ToolError that render builds from
its error, before registration erases the error type.
pub fn idempotency_key(
call: Call(context),
) -> option.Option(String)
The idempotency key the client sent for this request, if any: 1 to 128 visible ASCII characters. A retry with the same key is the client’s promise that it is the same request. It is not authenticated, so key idempotent work by the authenticated principal and this key together.
pub fn input_codec(
definition: Definition(input, output),
) -> codec.Codec(input)
The codec that encodes call arguments and decodes them in the handler.
pub fn input_contract(
declaration: Declaration,
) -> Result(contract.Contract, contract.DocumentError)
Loads the declaration’s input schema as a Blueprint contract, to validate arguments before a call. Fails for a schema outside the profile Blueprint validates.
pub fn input_method_name(method: InputMethod) -> String
The wire name of an input method, such as "elicitation/create".
pub fn input_responses(
call: Call(context),
) -> dict.Dict(String, value.Value)
The client’s responses to the previous input round, by request key; empty on the first round.
pub fn invocation_id(call: Call(context)) -> Int
Relay’s id for this invocation, the invocation_id of its telemetry:
unique among the invocations this node has run since it started. A
retried request is a new invocation with a new id.
pub fn output_codec(
definition: Definition(input, output),
) -> option.Option(codec.Codec(output))
The codec of the structured result, or None for a content-only tool.
pub fn report_progress(
call: Call(context),
progress: Float,
total: option.Option(Float),
message: option.Option(String),
) -> Nil
Sends a progress notification when the client asked for progress.
progress must increase from one report to the next; Relay drops a
report that does not. total and message are optional.
pub fn request_id(call: Call(context)) -> RequestId
The JSON-RPC id of this request, exactly as the client sent it. It is
chosen by the client and is not authenticated: it may repeat across
clients, and a retry carries the same id only when the client reuses it.
To recognise a retry, use idempotency_key instead.
pub fn request_input(
requests: dict.Dict(String, InputRequest),
) -> Reply(output)
Pauses the call until the client answers every request; the client calls
again with the responses, which the handler reads with
input_responses. Requests for methods the client did not declare are
left out.
pub fn tool_declaration(tool: Tool(context)) -> Declaration
What tools/list publishes for a bound tool.
pub fn try_define(
name: String,
input: codec.Codec(input),
output: codec.Codec(output),
) -> Result(Definition(input, output), DefineError)
define for definitions built from runtime data: returns the error
instead of panicking.
pub fn try_define_content(
name: String,
input: codec.Codec(input),
) -> Result(
Definition(input, List(content.ContentBlock)),
DefineError,
)
define_content for definitions built from runtime data.
pub fn with_description(
definition: Definition(input, output),
description: String,
) -> Definition(input, output)
Sets the description a client or a model reads to choose the tool.
pub fn with_destructive_hint(
definition: Definition(input, output),
hint: Bool,
) -> Definition(input, output)
Publishes destructiveHint: whether the tool may destroy data.
pub fn with_icons(
definition: Definition(input, output),
icons: List(content.Icon),
) -> Definition(input, output)
Replaces the icons a client may show for the tool.
pub fn with_idempotent_hint(
definition: Definition(input, output),
hint: Bool,
) -> Definition(input, output)
Publishes idempotentHint: whether repeating a call has no further
effect.
pub fn with_input_schema(
definition: Definition(input, output),
input_schema: value.Value,
) -> Definition(input, output)
Publishes this JSON Schema document as the input schema instead of the one derived from the input codec; the codec still decodes every call. Panics when the schema is not a JSON object.
pub fn with_meta(
definition: Definition(input, output),
meta: List(#(String, value.Value)),
) -> Definition(input, output)
Replaces the declaration’s _meta members.
pub fn with_open_world_hint(
definition: Definition(input, output),
hint: Bool,
) -> Definition(input, output)
Publishes openWorldHint: whether the tool reaches outside systems.
pub fn with_read_only_hint(
definition: Definition(input, output),
hint: Bool,
) -> Definition(input, output)
Publishes readOnlyHint: whether the tool leaves its environment
unchanged. Unset hints are omitted.
pub fn with_required_client_capabilities(
definition: Definition(input, output),
capabilities: List(String),
) -> Definition(input, output)
Names client capabilities, such as "elicitation", that a call needs. A
request whose metadata does not declare them fails with the MCP
missing-capability error before the handler runs.
pub fn with_title(
definition: Definition(input, output),
title: String,
) -> Definition(input, output)
Sets the human-readable title clients show instead of the name.