Relay
Relay is a Gleam MCP server and client for Erlang. It implements the frozen MCP 2026-07-28 revision over stdio and Streamable HTTP.
Capabilities
- Typed tool definitions reused for server declarations, handlers, and client calls. Calls distinguish success, tool refusal, and a request for more input.
- Server resources and URI templates, prompts, completion, and text, image, audio, and resource content.
- HTTP and stdio servers, plus HTTP, stdio, and in-process client connections.
- Client discovery, bounded listings, calls, resource reads, prompt retrieval, completion, and notification subscriptions.
- Live tool registration and notifications to active subscribers when the catalog changes.
- Per-request context and optional bearer resource admission. The application supplies the verifier and chooses how context becomes business policy.
- Deadlines, cancellation, headers, correlation, and optional idempotency keys. Client failures retain evidence about whether a request may have been sent.
When Relay fits
Use Relay when a Gleam application needs to expose typed operations to MCP peers, or call an existing MCP server while retaining native Gleam values and explicit failure information. Common setups are:
- An HTTP MCP endpoint exposing application operations to an agent or other remote client.
- An endpoint mounted in an existing application router, with each request supplying tenant or caller context and an application-owned verifier when bearer protection is needed.
- A local tool process launched over stdio, or an application that calls an MCP peer over HTTP or stdio.
Install
Add Relay and JSON Blueprint from Hex; add Sinal or HTTP Gun directly when your application imports their modules or types.
gleam add relay json_blueprint@2
Use Gleam >= 1.18.0 with the Erlang target. Commit gleam.toml and
manifest.toml so builds use the same resolved versions.
Server flow and ownership
On the server, the transport admits a request to a runtime actor. The actor serializes protocol state through a pure reducer and starts isolated workers for application handlers. HTTP can build request context and ask a verifier before dispatch; stdio trusts its parent process.
flowchart LR
App[Application definitions and handlers] --> Catalog[Service catalog]
Peer[MCP peer] --> Transport[HTTP or stdio transport]
Catalog --> Runtime[Runtime actor]
Transport --> Runtime
Runtime <-->|events and effects| Reducer[Pure reducer]
Runtime --> Worker[Isolated handler worker]
Worker --> Runtime
Serve and call a typed tool
This runnable example defines one typed tool and reuses its definition on both sides of a local HTTP round trip.
import gleam/int
import gleam/io
import gleam/string
import json/blueprint/codec
import relay/client
import relay/http
import relay/server
import relay/tool
/// One `Definition` holds the input and output codecs used by both the server
/// handler and the client call.
pub fn greet() -> tool.Definition(String, String) {
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)
}
/// Binds an ephemeral loopback port, calls the tool and closes everything.
/// Application code handles each `Result` and closes resources it created when
/// a later operation fails.
pub fn serve_and_call() -> Nil {
let service =
server.new([tool.handle(greet(), fn(name) { Ok("Hello, " <> name <> "!") })])
let assert Ok(mcp) = http.start(http.new(service))
let url = "http://127.0.0.1:" <> int.to_string(http.port(mcp)) <> "/"
let assert Ok(config) = client.http(url)
let assert Ok(peer) = client.connect(config)
let assert Ok(client.Succeeded("Hello, Ada!", _)) =
client.call(peer, greet(), "Ada")
client.close(peer)
http.stop(mcp)
}
Mount in an application
Use http.handler when the host router should own HTTP listening. The complete example supplies
the imports and Tenant type. It reads a tenant header as example input; derive
trusted tenant authority through your application’s authentication and policy. This buffered interface cannot stream progress or observe client disconnects and returns 406 for subscriptions/listen; mount http.mist_handler when streaming and disconnect cancellation matter. Call http.stop when the mounted endpoint shuts down.
pub fn mount(service: server.Server(Tenant)) -> http.Handler(Tenant) {
let assert Ok(mcp) =
http.new_with_context(service, fn(req) {
case request.get_header(req, "x-tenant") {
Ok(id) -> Ok(Tenant(id))
Error(Nil) ->
Error(response.new(400) |> response.set_body(bytes_tree.new()))
}
})
|> http.with_allowed_hosts(["mcp.example.com", "127.0.0.1"])
|> http.handler
mcp
}
For bearer-protected endpoints, see the protection recipe. Relay checks the verifier’s attested resource and scopes and formats challenges and metadata; the verifier owns token authenticity, issuer, and expiry checks.
Limits to plan for
- Only MCP
2026-07-28is implemented. Legacy2025-11-25sessions, MCP client OAuth flows, tasks, resource-read continuation, andlogging/setLevelare not implemented. - Tool input schemas use Blueprint’s finite admitted profile. Unsupported schemas are reported explicitly; Relay does not claim to accept every valid JSON Schema.
- Stdio provides no bearer authorization. Run it under a trusted parent process.
- A client error with
MaybeSentevidence means the operation may have run. Relay never retries automatically and does not provide durable idempotency receipts; the application owns retry and reconciliation policy. - The built-in resource-template matcher supports one simple
{name}variable per segment. Useresources.template_with_matcherfor other patterns.
Learn more
- Runnable usage recipes
- Public API modules:
tool,server,http,stdio,client, andauthorization - Design and contracts
- Protocol and conformance evidence, including its limits