stdio Transport
View SourceThe stdio transport enables MCP communication over stdin/stdout, which is the transport used by Claude Desktop and other MCP clients that spawn server processes.
Overview
Unlike HTTP transport, stdio transport:
- Uses newline-delimited JSON-RPC messages.
- Runs as a child process spawned by the MCP client.
- Is ideal for local integrations (no network overhead).
- Is the primary transport for Claude Desktop.
The same registries (tools, resources, prompts, resource
templates, completions, tasks) work over stdio. Tool handlers may
be arity 1 or arity 2 ((Args, Ctx)) — the emit_progress and
cancellation hooks in Ctx interleave on stdout in stdio just
like they do on the SSE channel for HTTP. See the
Tools guide for the handler shape.
Quick Start
1. Create an Escript
#!/usr/bin/env escript
%%! -pa _build/default/lib/*/ebin
-module(my_mcp_server).
-mode(compile).
main(_Args) ->
%% Start the application
application:ensure_all_started(barrel_mcp),
barrel_mcp_registry:wait_for_ready(),
%% Register your tools
barrel_mcp:reg_tool(<<"hello">>, my_mcp_server, hello, #{
description => <<"Say hello">>,
input_schema => #{
<<"type">> => <<"object">>,
<<"properties">> => #{
<<"name">> => #{
<<"type">> => <<"string">>,
<<"description">> => <<"Name to greet">>
}
}
}
}),
%% Start stdio server (blocks until stdin closes)
barrel_mcp:start_stdio().
hello(Args) ->
Name = maps:get(<<"name">>, Args, <<"World">>),
<<"Hello, ", Name/binary, "!">>.2. Make it Executable
chmod +x my_mcp_server
3. Configure Claude Desktop
Edit your claude_desktop_config.json:
macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
Windows: %APPDATA%\Claude\claude_desktop_config.json
Linux: ~/.config/claude/claude_desktop_config.json
{
"mcpServers": {
"my-erlang-server": {
"command": "/absolute/path/to/my_mcp_server",
"args": []
}
}
}4. Restart Claude Desktop
After saving the config, restart Claude Desktop. Your MCP server will be available.
Blocking vs Supervised Mode
Blocking Mode
Use barrel_mcp:start_stdio/0 when you want the server to run in the current process:
main(_Args) ->
setup_tools(),
barrel_mcp:start_stdio(). %% Blocks hereThis is ideal for escripts and simple applications.
Supervised Mode
Use barrel_mcp:start_stdio_link/0 when you want the server supervised:
-module(my_app_sup).
-behaviour(supervisor).
-export([init/1]).
init([]) ->
%% Ensure tools are registered first
setup_tools(),
Children = [
#{id => mcp_stdio,
start => {barrel_mcp, start_stdio_link, []},
restart => permanent,
type => worker}
],
{ok, {#{strategy => one_for_one}, Children}}.Protocol Details
Message Format
Each message is a single line of JSON (newline-delimited):
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05"}}\nSupported Methods
The stdio transport supports all MCP methods:
initialize/initialized- Connection lifecycletools/list/tools/call- Tool operationsresources/list/resources/read- Resource operationsprompts/list/prompts/get- Prompt operationsping- Keep-alive
Notifications
MCP notifications (methods without id) don't receive responses:
{"jsonrpc":"2.0","method":"notifications/initialized"}\nBuilding Releases
For production use, build an Erlang release instead of an escript.
Using rebar3 Release
- Add to
rebar.config:
{relx, [
{release, {my_mcp_server, "1.0.0"}, [my_app, barrel_mcp]},
{mode, prod},
{extended_start_script, true}
]}.- Create your main module:
-module(my_mcp_main).
-export([start/0]).
start() ->
%% Called when release starts
setup_tools(),
barrel_mcp:start_stdio().- Configure your app to call this on start:
%% In your application module
start(_Type, _Args) ->
%% Start your supervisor
{ok, Sup} = my_app_sup:start_link(),
%% If running in MCP mode, start stdio
case application:get_env(my_app, mcp_mode, false) of
true -> spawn(fun my_mcp_main:start/0);
false -> ok
end,
{ok, Sup}.- Build and run:
rebar3 release
_build/default/rel/my_mcp_server/bin/my_mcp_server foreground
Debugging
Testing Locally
You can test your stdio server manually:
# Start your server
./my_mcp_server
# Then type JSON-RPC messages (each on one line):
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"hello","arguments":{"name":"Erlang"}}}
Logging
Since stdout is used for MCP responses, use stderr for debugging:
debug(Msg) ->
io:format(standard_error, "[DEBUG] ~s~n", [Msg]).Or use Erlang's logger to a file:
%% Configure in your app startup
logger:add_handler(file_handler, logger_std_h, #{
config => #{file => "/tmp/mcp_server.log"}
}).Common Issues
Server not appearing in Claude Desktop:
- Check config file path and JSON syntax
- Use absolute path to executable
- Restart Claude Desktop after config changes
"Command not found" errors:
- Ensure the executable has the shebang line
- Check file permissions (
chmod +x) - Use absolute paths in config
No responses:
- Ensure all tools are registered before
start_stdio/0 - Check stderr for errors
Environment Variables
Claude Desktop passes environment variables to your server:
%% Access them in your code
HomeDir = os:getenv("HOME"),
PathVar = os:getenv("PATH").You can also configure environment in claude_desktop_config.json:
{
"mcpServers": {
"my-server": {
"command": "/path/to/my_mcp_server",
"args": [],
"env": {
"MY_CONFIG": "/path/to/config.json",
"DEBUG": "true"
}
}
}
}Working Directory
The working directory is typically the user's home directory or where Claude Desktop was launched. To ensure consistent behavior:
%% Set a known working directory
file:set_cwd("/path/to/my/app"),
%% Or use absolute paths for all file operations
ConfigPath = filename:join([os:getenv("HOME"), ".config", "myapp"]).See Also
- Getting Started - Basic setup
- Tools, Resources & Prompts - MCP primitives
barrel_mcp_stdiomodule documentation