This document is for people building, testing, packaging, or debugging the MCP server from source. User-facing installation and MCP client configuration live in README.md.
Requirements
- Elixir 1.15+
- Erlang/OTP 26+
- macOS for the currently supported local release artifact
Run commands in mcp_server/ unless noted otherwise.
Local Development
Fetch dependencies:
mix deps.get
Run the server from source in stdio mode:
mix mcp.run
This is equivalent to mix run --no-halt with stdio attached and is
useful for local iteration before building a release.
Build A Release
MIX_ENV=prod mix release --overwrite
The executable lands at:
_build/prod/rel/ptc_runner_mcp/bin/ptc_runner_mcp
Smoke test:
_build/prod/rel/ptc_runner_mcp/bin/ptc_runner_mcp version
The MCP initialize response advertises serverInfo.name as
ptc_lisp, plus the package version and build metadata. When built
from a git checkout, serverInfo.version uses SemVer build metadata
such as 0.1.0+abc123def456, and serverInfo.build includes
compile-time git_commit and git_dirty fields. CI or packaging
scripts can override these with PTC_RUNNER_MCP_GIT_COMMIT and
PTC_RUNNER_MCP_GIT_DIRTY.
Test And Check
Targeted test example:
mix test test/ptc_runner_mcp/release_env_test.exs
Standard local checks:
mix format --check-formatted
mix compile --warnings-as-errors
mix credo --strict
mix test
The repo may also define pre-commit hooks that run a scoped subset of these checks.
Release Distribution And Remote IEx
The release defaults RELEASE_DISTRIBUTION=none. That is the right
default for stdio MCP clients because they often spawn one server
subprocess per configured client or probe. A fixed distributed Erlang
node name would make those subprocesses collide.
Remote IEx debugging is still available when you opt in explicitly. Start the release with distribution enabled and a unique node name:
RELEASE_DISTRIBUTION=sname \
RELEASE_NODE=ptc_runner_mcp_debug_1 \
_build/prod/rel/ptc_runner_mcp/bin/ptc_runner_mcp start
Attach from another terminal with the same settings:
RELEASE_DISTRIBUTION=sname \
RELEASE_NODE=ptc_runner_mcp_debug_1 \
_build/prod/rel/ptc_runner_mcp/bin/ptc_runner_mcp remote
Use a different RELEASE_NODE for every concurrent debug process.
The release also includes bin/ptc_lisp_repl, a human-facing PTC-Lisp
REPL wrapper that connects to the running node without requiring Erlang
or Elixir to be installed on the host:
RELEASE_DISTRIBUTION=sname \
RELEASE_NODE=ptc_runner_mcp_debug_1 \
_build/prod/rel/ptc_runner_mcp/bin/ptc_lisp_repl --display envelope
From remote IEx, the same REPL can be started manually:
PtcRunnerMcp.Repl.start(display: :envelope)The REPL routes through lisp_eval or, when stateful sessions are
enabled, lisp_session_eval. That means it sees the same configured
upstream MCP tools, response profile, limits, and error feedback as
MCP clients. :display text shows terminal-oriented output,
:display envelope shows the full pretty JSON MCP tool response
envelope, and :display json shows the same envelope as compact JSON.
For local development without building a release, run the same REPL
implementation from the mcp_server/ Mix project:
mix mcp.repl --display envelope
mix mcp.repl --upstreams-config ./upstreams.json --display envelope
mix mcp.repl --stateless --eval "(+ 1 2)"
For a Docker debug container:
export RELEASE_COOKIE="$(openssl rand -base64 48)"
docker run --rm -it \
--name ptc-mcp-debug \
-p 7332:7332 \
-e RELEASE_DISTRIBUTION=name \
-e RELEASE_NODE=ptc_runner_mcp@127.0.0.1 \
-e RELEASE_COOKIE="$RELEASE_COOKIE" \
-e PTC_RUNNER_MCP_HTTP_AUTH_TOKEN="$(openssl rand -base64 32)" \
ghcr.io/andreasronge/ptc-runner-mcp:TAG
docker exec -it ptc-mcp-debug \
/opt/ptc_runner_mcp/bin/ptc_lisp_repl --display envelope
The docker exec command runs the wrapper inside the image and uses the
runtime bundled in /opt/ptc_runner_mcp; the Docker host does not need
Erlang or Elixir installed.
The Erlang distribution cookie grants full VM RPC access, not only
PTC-Lisp access. Keep it high entropy, local to the debug session, and
never expose EPMD or BEAM distribution ports publicly. The Docker example
above does not publish those ports; prefer docker exec for local
debugging.
Release Lifecycle Commands
Useful commands:
ptc_runner_mcp start # foreground server
ptc_runner_mcp version # print release version
ptc_runner_mcp eval "..." # one-shot VM expression
These commands require a distributed node and therefore only work for
processes started with RELEASE_DISTRIBUTION=sname or
RELEASE_DISTRIBUTION=name:
ptc_runner_mcp remote
ptc_runner_mcp rpc "..."
ptc_runner_mcp pid
ptc_runner_mcp stop
ptc_runner_mcp restart
For ordinary stdio operation, stop the server by closing stdin or sending SIGINT/SIGTERM from the owning process.
Manual JSON-RPC Smoke Test
cat <<'EOF' | _build/prod/rel/ptc_runner_mcp/bin/ptc_runner_mcp start
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"hello","version":"0.0.0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"lisp_eval","arguments":{"program":"(+ 1 2)"}}}
EOF
For a fuller raw protocol walkthrough, see
docs/guides/mcp-getting-started.md.
Diagnostics
Useful local flags:
ptc_runner_mcp start --debug-tool
ptc_runner_mcp start --trace-dir /tmp/ptc-traces
ptc_runner_mcp start --log-level debug
Be careful with debug logs and full trace payloads: they may include
programs, context, and result data. The detailed diagnostics reference
lives in docs/mcp-debug.md, and all flags are
listed in
docs/mcp-server-configuration.md.
Packaging Notes
The intended release channel is a standalone ptc_runner_mcp archive.
The first supported target is Apple Silicon macOS, with additional
targets added as CI coverage and packaging are proven.
Expected artifacts:
- one archive per OS/architecture pair;
- snapshot prereleases from
main; - versioned releases from tags;
SHA256SUMSgenerated in CI after packaging.