stdio
Unexpected end of JSON input
The stdio transport requires stdout to contain only newline-delimited JSON-RPC.
Avoid IO.puts/1, normal Logger output, or noisy startup scripts on stdout.
Use stderr for diagnostics:
IO.puts(:stderr, "debug")For scripts with Mix.install/2, configure logging before installing deps:
Application.put_env(:ex_mcp, :stdio_mode, true)
Application.put_env(:logger, :level, :emergency)
Mix.install([{:ex_mcp, "~> 1.0.0-rc.8"}], verbose: false)Server hangs after starting
Start stdio servers with:
MyServer.start_link(transport: :stdio)For clients, command must be a list:
ExMCP.Client.start_link(transport: :stdio, command: ["node", "server.js"])HTTP
Connection refused
Check the URL and endpoint path. If the path is included in url, ExMCP uses
that as the default endpoint:
ExMCP.Client.start_link(transport: :http, url: "http://localhost:4000/mcp")Or provide it explicitly:
ExMCP.Client.start_link(
transport: :http,
url: "http://localhost:4000",
endpoint: "/mcp"
)CORS errors
For Phoenix/Plug servers, configure CORS in your Plug pipeline or pass
cors_enabled: true to ExMCP.HttpPlug.
SSE stream does not start
First identify the negotiated protocol era:
- On legacy MCP, confirm the standalone GET stream is enabled and the client
uses
use_sse: true. - On MCP 2026-07-28,
use_ssedoes not control streaming. Ordinary requests andsubscriptions/listenown their SSE response on the POST that created them. Check that the proxy preservesContent-Type: text/event-stream, does not buffer or transform the response, and has an idle timeout longer than the configured keepalive interval.
Increase stream_handshake_timeout for slow deployments. See the
Streamable HTTP comparison for the full
era-specific lifecycle.
Protocol modes and MCP 2026-07-28
server/discover fails or the client does not fall back
Set the intended compatibility policy explicitly:
ExMCP.Client.start_link(
transport: :http,
url: "https://example.com/mcp",
protocol_mode: :prefer_modern
):prefer_modern falls back to legacy initialize only when a live peer gives
positive evidence that it is legacy-compatible. A timeout, authentication
failure, transport break, cached-modern failure, recognized modern error, or
unsupported modern revision is surfaced instead of being silently downgraded.
Use :prefer_legacy for a deliberate rollback, or reset_era_cache: true once
after an operator-controlled endpoint upgrade. Strict :modern_only and
:legacy_only modes never fall back.
Error -32022: unsupported protocol version
The request's modern
_meta["io.modelcontextprotocol/protocolVersion"], the
MCP-Protocol-Version HTTP header, and the server's enabled protocol mode must
agree. Inspect the error's data.requested and data.supported values, then
check both client and server protocol_mode settings. Do not use
protocol_version: "2025-11-25" to select an era; that option is only the
legacy revision preference.
Invalid request metadata
Every MCP 2026-07-28 request must include a _meta object with:
{
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": {}
}io.modelcontextprotocol/clientInfo is optional, but when present it must
contain non-empty name and version strings. ExMCP adds these fields for its
own clients; this error usually indicates a custom peer, manually constructed
JSON-RPC message, or middleware that rewrote params._meta.
Error -32020: HTTP header mismatch
Modern HTTP requests must carry exactly one MCP-Protocol-Version and
Mcp-Method header matching the JSON-RPC body. tools/call,
resources/read, and prompts/get also require a matching Mcp-Name.
Annotated tool arguments may require Mcp-Param-* headers.
ExMCP derives and replaces these headers automatically. If the error occurs with an ExMCP client, inspect reverse-proxy behavior: duplicate headers must not be collapsed by choosing one value, and routing headers must not be cached, normalized to a different value, or injected by middleware.
Result is rejected for missing resultType, ttlMs, or cacheScope
Every modern result needs resultType: "complete" or
resultType: "input_required". A complete cacheable result must also contain
a non-negative integer ttlMs and cacheScope equal to "public" or
"private". Non-complete results must not contain cache hints. Legacy result
maps do not gain these fields merely because the transport is HTTP.
When the server uses ExMCP's normal Handler or DSL dispatch, return the usual
{:ok, result, state} / ToolResult.* shape and let
ExMCP.Server.ResultNormalizer add resultType plus conservative cache
defaults (ttlMs: 0, cacheScope: "private"). Suspend an operation with
ExMCP.Server.DSL.Result.input_required/2 or the documented
{:input_required, ...} handler tuple. If a custom peer constructs raw wire
results or bypasses ExMCP dispatch, it must add and validate the modern fields
itself. See Modern result cache hints.
GET or DELETE returns 405
This is expected on a :modern_only MCP endpoint. MCP 2026-07-28 has no
standalone GET stream or session DELETE. Open a subscriptions/listen POST for
long-lived notifications and cancel by closing its response stream. GET,
DELETE, Mcp-Session-Id, and Last-Event-ID are retained only for enabled
legacy Streamable HTTP connections.
BEAM-Local
Client cannot connect
transport: :beam requires a live server PID:
{:ok, server} = MyServer.start_link(transport: :beam) # DSL provides start_link; raw handlers use HandlerServer
Process.alive?(server)
{:ok, client} = ExMCP.Client.start_link(transport: :beam, server: server)Do not use transport: :native; it was removed in the 1.0 API cleanup.
DSL
Tools do not appear
Use ExMCP.Server.Handler and ExMCP.Server.DSL together, and make sure the
server starts through a supported transport:
defmodule MyServer do
use ExMCP.Server.Handler
use ExMCP.Server.DSL
tool "ping", "Health check" do
run fn _args, state ->
{:ok, %{content: [%{type: "text", text: "pong"}]}, state}
end
end
endDebugging
Enable debug logging for non-stdio transports:
Logger.configure(level: :debug)Inspect local server state when using BEAM-local tests:
:sys.get_state(server)Run focused tests:
mix test test/ex_mcp/client_beam_transport_test.exs
mix test test/ex_mcp/server/transport_test.exs