ExMCP Troubleshooting Guide
View SourceThis guide covers common issues and their solutions when working with ExMCP.
STDIO Transport Issues
"Unexpected end of JSON input" Error
Problem: When using STDIO transport with MCP Inspector or other clients, you see:
Error from MCP server: SyntaxError: Unexpected end of JSON inputCause: The MCP STDIO protocol requires that ONLY valid JSON-RPC messages appear on stdout. Any other output (logs, Mix.install messages, etc.) contaminates the stream.
Status: ✅ FIXED - ExMCP now handles non-JSON lines gracefully during startup.
Common Sources of Contamination:
Mix.installoutput (e.g., "==> ex_mcp", "Compiling...")- Logger messages going to stdout instead of stderr
- Horde registry/supervisor startup logs
- Any
IO.putscalls without specifying:stderr
What We Fixed:
- STDIO server now ignores non-JSON lines instead of sending error responses
- Implemented protocol version negotiation between client and server
- Added configurable startup delay for Mix.install output
- Improved logging configuration for STDIO transport
Solutions:
For Scripts Using Mix.install
Configure logging BEFORE calling Mix.install:
#!/usr/bin/env elixir
# CRITICAL: Configure before Mix.install
Application.put_env(:ex_mcp, :stdio_mode, true)
Application.put_env(:ex_mcp, :stdio_startup_delay, 500) # ms
# Suppress all logging
System.put_env("ELIXIR_LOG_LEVEL", "emergency")
Application.put_env(:logger, :level, :emergency)
Mix.install([
{:ex_mcp, "~> 0.1"}
], verbose: false)
# Your server code here...For Production Servers
- Use Releases: Build a release that doesn't need Mix.install (recommended)
- Use StdioLauncher: Helper module that handles startup properly
ExMCP.StdioLauncher.start(MyServer, [ {:ex_mcp, "~> 0.1"} ])
Note: While ExMCP now gracefully handles non-JSON output during startup, Mix.install may still produce some stdout output that cannot be completely suppressed. For absolute zero contamination in production, use compiled releases.
Debugging Output
Always send debug output to stderr:
# Good
IO.puts(:stderr, "Debug message")
# Bad - contaminates stdout
IO.puts("Debug message")Server Hangs After Starting
Problem: The STDIO server starts but doesn't respond to requests.
Cause: The server isn't properly entering STDIO transport mode.
Solution: Ensure you're using transport: :stdio when starting:
MyServer.start_link(transport: :stdio)DSL Issues
Tools Not Appearing
Problem: Defined tools don't show up when client lists them.
Cause: Missing or incorrect callback implementation.
Solution: Ensure you implement the handle_tool_call/3 callback:
@impl true
def handle_tool_call(tool_name, args, state) do
# Handle the tool call
{:ok, result, state}
end"Unknown Key" Warnings
Problem: Client shows warnings about __unknown_key__ in responses.
Cause: This was a bug in older versions where protocol data was being atomized incorrectly.
Solution: Update to the latest version of ExMCP. Protocol data is now kept as strings.
HTTP Transport Issues
Port Already in Use
Problem: Starting HTTP server fails with "address already in use".
Solution:
- Check if another server is running on the port
- Use a different port:
MyServer.start_link(transport: :http, port: 8080) - Kill the existing process using the port
CORS Errors
Problem: Browser clients get CORS errors when connecting.
Solution: CORS is enabled by default. If still having issues:
MyServer.start_link(
transport: :http,
cors_enabled: true
)General Debugging Tips
Enable Debug Logging
For non-STDIO transports, enable debug logging:
Logger.configure(level: :debug)Check Server State
Use the native transport for debugging:
{:ok, server} = MyServer.start_link(transport: :native)
:sys.get_state(server)Test with Simple Client
Use the ExMCP client to test your server:
{:ok, client} = ExMCP.Client.connect(url: "stdio://path/to/server.exs")
{:ok, response} = ExMCP.Client.call_tool(client, "tool_name", %{arg: "value"})Common Mistakes
- Using atoms for protocol keys: Protocol data should use string keys
- Not implementing callbacks: Ensure all required callbacks are implemented
- Logging to stdout in STDIO mode: Always use
:stderrfor STDIO servers - Not handling errors: Always return proper error tuples from handlers
Getting Help
If you're still having issues:
- Check the examples in
examples/getting_started/ - Review the test files for usage patterns
- Open an issue on GitHub with:
- ExMCP version
- Elixir/OTP versions
- Minimal reproduction code
- Full error messages