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:

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

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.custom without 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

  • Elicitation

    elicitation/create: ask the user to fill a form.

  • Sampling

    sampling/createMessage: ask the client’s model for a completion.

  • Roots

    roots/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

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

A tool failure the client receives as an isError: true result. Build it with error_message or error_with.

pub type ToolError =
  @internal ToolError

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 name(definition: Definition(input, output)) -> String

The tool’s name.

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.

✨ Search Document