ExAgent.MCP.Protocol (ExAgent v2.0.1)

View Source

Pure JSON-RPC 2.0 encoding/decoding for the Model Context Protocol, plus the mapping from an MCP tool spec to an ExAgent.Tool.

Splitting the protocol out (no ports, no processes) keeps it trivially unit-testable. ExAgent.MCP.Client owns the transport and calls into here.

Summary

Functions

Decode one complete JSON-RPC line.

Encode a JSON-RPC notification (no id, no reply expected).

Encode a JSON-RPC request (with an id) as a newline-terminated binary.

Extract the concatenated text from an MCP tools/call result.

Map an MCP tools/list entry to an ExAgent.Tool whose call forwards to the server via call_fun. The tool's result text is extracted from the MCP content blocks; an MCP isError: true becomes {:error, _}.

Types

decoded()

@type decoded() ::
  {:response, id :: term(), result :: map()}
  | {:error_response, id :: term(), error :: map()}
  | {:notification, method :: String.t(), params :: map()}
  | :ignore

Functions

decode(line)

@spec decode(binary()) :: decoded()

Decode one complete JSON-RPC line.

  • a response with a result → {:response, id, result}
  • a response with an error → {:error_response, id, error}
  • a notification (no id) → {:notification, method, params}
  • anything unparseable / unrelated → :ignore

encode_notification(method, params)

@spec encode_notification(String.t(), map()) :: iodata()

Encode a JSON-RPC notification (no id, no reply expected).

encode_request(id, method, params)

@spec encode_request(term(), String.t(), map()) :: iodata()

Encode a JSON-RPC request (with an id) as a newline-terminated binary.

result_to_text(result)

@spec result_to_text(map()) :: {:ok, String.t()} | {:error, term()}

Extract the concatenated text from an MCP tools/call result.

to_tool(spec, call_fun, execution_binding \\ :unbound)

@spec to_tool(
  map(),
  (String.t(), map() -> {:ok, String.t()} | {:error, term()}),
  map() | :unbound
) ::
  ExAgent.Tool.t()

Map an MCP tools/list entry to an ExAgent.Tool whose call forwards to the server via call_fun. The tool's result text is extracted from the MCP content blocks; an MCP isError: true becomes {:error, _}.

inputSchema takes precedence over input_schema by key presence. Explicit false or invalid values are preserved for the common Tool validation boundary; only an absent schema uses the default object schema.

The optional third argument is a validated private execution descriptor built by Client from trusted host options, never from server tool metadata. Without it the tool is marked :unbound: ordinary calls work, but durable fingerprints reject. Identity never changes the model-facing schema or description.