An MCP tool call that got no answer from its tool.
An imported MCP tool returns {:error, %Imp.MCP.CallFailure{}} when the call
did not reach an answer from the tool: the server declined it, it could not be
sent, or it was sent and no trustworthy answer came back. outcome says which,
so a host decides what to do without matching ExMCP's terms:
:refused— declined before anything ran. The server answered with a JSON-RPC error that rejects the request before any method runs (parse error, invalid request, method not found), or the HTTP layer refused it with a 4xx status other than 401 (403 included). Nothing ran, so repeating the call is safe; whether it will succeed depends on why it was refused (a 408 or 429 may pass later, a 404 will not).:auth_refused— the credential was refused before anything ran: an HTTP 401, or an OAuth flow that failed. Nothing ran; renewing the credential and trying once more may succeed.:not_sent— the request never left: the connection could not be opened, the address could not be resolved, the client was not connected, the client process was already gone, or every connection to an HTTP server stayed busy until the call's timeout (:no_idle_connection).:unknown— the request was, or may yet be, delivered, and whether the tool acted is not known: the caller's timeout, a connection that closed after sending, a 5xx status, a response that could not be read, a stream that broke after delivery, a handler that crashed or that the server stopped waiting for (it may still be running), invalid params, any other JSON-RPC error, an error ExMCP raised itself partway through a call, a cancelled request, and a client that exited during the call. Check before repeating it.
Invalid params (-32602) is :unknown, not :refused, because a server can
send it after its tool ran: MCP names it the code for a bad tool call, and
ExMCP's server passes a tool handler's returned ExMCP.Error.ProtocolError
on as the JSON-RPC error, after the handler ran
(ExMCP.MessageProcessor.MethodHandlers.handle_tool_reply/3). Parse error,
invalid request and method not found are read as refusals on the premise that
a server sends them before it dispatches to a tool, as JSON-RPC defines them;
a tool handler that returns one after acting breaks that premise, and the
code cannot show it. An ExMCP.Error.ProtocolError struct is :unknown whatever its code: ExMCP
builds those itself, including after the first round of a multi-round call
has reached the server.
A timeout is :unknown even when the request was still waiting in the
client: ExMCP's client sends a plain HTTP request from inside its own
process, so a call behind a slow one on the same client waits, and when its
caller gives up the request stays queued and is sent later. Imp lends each
call to an HTTP server a client of its own (pool_size: in
Imp.MCP.Connections), so a call waits for a client rather than inside one,
and a call that never got one is :not_sent.
An answer from the tool itself, including an MCP error result
({:mcp_tool_error, envelope}, where isError is true), is not a
CallFailure: the tool said what happened. Imp.Tool.outcome/1 reads any
tool call's return value, this one included.
reason is what ExMCP returned, unchanged, when the failure came from
ExMCP; a process exit is kept as {:exit, reason}. Three reasons are Imp's
own, from the connection pool in front of ExMCP: :timeout (the caller's
:timeout passed while the request was out), :no_idle_connection (every
connection to the server stayed busy until the timeout), and
:not_connected (the server has no connection left). server_name is the
descriptor's name, tool_name the name the server published, and index
the position of the descriptor in the list given to Imp.MCP.connect/2,
which, unlike a name, identifies it.
Summary
Types
@type outcome() :: :refused | :auth_refused | :not_sent | :unknown
@type t() :: %Imp.MCP.CallFailure{ index: non_neg_integer(), outcome: outcome(), reason: term(), server_name: String.t(), tool_name: String.t() }