# stdio Transport
The 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](tools-resources-prompts.md) for the handler shape.
## Quick Start
### 1. Create an Escript
```erlang
#!/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
```bash
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`
```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:
```erlang
main(_Args) ->
setup_tools(),
barrel_mcp:start_stdio(). %% Blocks here
```
This is ideal for escripts and simple applications.
### Supervised Mode
Use `barrel_mcp:start_stdio_link/0` when you want the server supervised:
```erlang
-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"}}\n
```
### Supported Methods
The stdio transport supports all MCP methods:
- `initialize` / `initialized` - Connection lifecycle
- `tools/list` / `tools/call` - Tool operations
- `resources/list` / `resources/read` - Resource operations
- `prompts/list` / `prompts/get` - Prompt operations
- `ping` - Keep-alive
### Notifications
MCP notifications (methods without `id`) don't receive responses:
```
{"jsonrpc":"2.0","method":"notifications/initialized"}\n
```
## Building Releases
For production use, build an Erlang release instead of an escript.
### Using rebar3 Release
1. Add to `rebar.config`:
```erlang
{relx, [
{release, {my_mcp_server, "1.0.0"}, [my_app, barrel_mcp]},
{mode, prod},
{extended_start_script, true}
]}.
```
2. Create your main module:
```erlang
-module(my_mcp_main).
-export([start/0]).
start() ->
%% Called when release starts
setup_tools(),
barrel_mcp:start_stdio().
```
3. Configure your app to call this on start:
```erlang
%% 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}.
```
4. Build and run:
```bash
rebar3 release
_build/default/rel/my_mcp_server/bin/my_mcp_server foreground
```
## Debugging
### Testing Locally
You can test your stdio server manually:
```bash
# 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:
```erlang
debug(Msg) ->
io:format(standard_error, "[DEBUG] ~s~n", [Msg]).
```
Or use Erlang's logger to a file:
```erlang
%% 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:
```erlang
%% Access them in your code
HomeDir = os:getenv("HOME"),
PathVar = os:getenv("PATH").
```
You can also configure environment in `claude_desktop_config.json`:
```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:
```erlang
%% 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](getting-started.md) - Basic setup
- [Tools, Resources & Prompts](tools-resources-prompts.md) - MCP primitives
- `barrel_mcp_stdio` module documentation