Shared result and error normalization for every server dispatch path.
ExMCP.Server.Dispatch (handler-process transports and stdio),
the HTTP message processor, and ExMCP.Protocol.RequestProcessor (DSL
servers) all convert handler return
values into JSON-RPC results. Keeping that conversion here guarantees the
transports agree on tool-result shape, key stringification and, most
importantly, on what is safe to send back to a client.
Client-facing error messages
error_message/2 never runs inspect/1 over an arbitrary term into a
response. Handler-authored detail (a binary reason, or a map/struct with a
message field) is preserved because MCP servers are expected to explain
themselves; anything else is logged with Logger.error/1 and replaced by a
generic message so internal structs, pids, or file paths cannot leak.
Summary
Functions
Returns the JSON-RPC error code a handler error reason should map to.
Builds a client-safe error message from a handler error reason.
Builds a paginated list result such as %{"tools" => [...], "nextCursor" => ...}.
Prepares the complete source collection for a modern tools/list page.
Applies the result envelope required by the request's protocol era.
Recursively converts atom keys to strings.
Builds an MCP tool result that reports a failure through isError.
Normalizes a handle_call_tool/3 result into an MCP tools/call result.
Validates that an extension result was enabled by per-request capabilities.
Functions
Returns the JSON-RPC error code a handler error reason should map to.
Cursor complaints map to invalid params (-32602); everything else uses
default. Codes embedded in the reason are deliberately not honoured:
handlers have historically returned %{"code" => ...} maps whose codes do
not match the transport-level meaning of the failure.
Builds a client-safe error message from a handler error reason.
Detail that the handler clearly authored (a binary, an atom, or a
:message / "message" field) is kept. Everything else is logged and
omitted from the response.
Builds a paginated list result such as %{"tools" => [...], "nextCursor" => ...}.
Prepares the complete source collection for a modern tools/list page.
Custom handlers that paginate tools must call this function before slicing
a page or calculating an opaque cursor. It stringifies protocol keys,
excludes invalid x-mcp-header definitions, removes ExMCP-only execution
metadata, and applies the deterministic modern ordering.
The normal result path applies the same operation defensively, but at that point it cannot repair a cursor that a custom handler calculated from an unfiltered collection.
Applies the result envelope required by the request's protocol era.
Legacy results are returned unchanged. Modern results receive a
resultType discriminator and result metadata identifying the server.
Handler-supplied input_required (or extension) result types are preserved.
Recursively converts atom keys to strings.
Known MCP protocol fields such as :input_schema, :mime_type and
:is_error are mapped to their lower-camel-case wire names so raw Handler
implementations may use idiomatic Elixir keys.
Builds an MCP tool result that reports a failure through isError.
Normalizes a handle_call_tool/3 result into an MCP tools/call result.
Accepts a bare content list, a binary (wrapped as a text content item), or a
map that already carries content.
Options
:wrap_bare_map- whentrue, a map that carries nocontentkey is wrapped as%{"content" => map}instead of being used as the result verbatim. The handler-process transports (ExMCP.Server.Dispatch) have always done this; the HTTP path has not, and both behaviours are relied on by existing servers.
@spec validate_result_capabilities(map(), map()) :: :ok | {:error, ExMCP.Error.ProtocolError.t()}
Validates that an extension result was enabled by per-request capabilities.