Packages
backplane_mcp_protocol
1.10.5
1.10.5
1.10.4
1.10.3
1.10.2
1.10.1
1.10.0
1.9.0
1.8.3
1.8.2
1.8.1
1.8.0
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.3
retired
1.6.0
1.5.0
1.4.0
1.3.0
1.2.0
1.1.0
1.0.1
1.0.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
Model Context Protocol (MCP) implementation in Elixir with Phoenix integration
Current section
Files
Jump to
Current section
Files
backplane_mcp_protocol
README.md
README.md
# Backplane.McpProtocol MCP
[](https://hex.pm/packages/backplane_mcp_protocol)
[](https://hexdocs.pm/backplane_mcp_protocol)
[](https://hex.pm/packages/backplane_mcp_protocol)
Model Context Protocol (MCP) implementation in Elixir.
## Overview
Backplane.McpProtocol is the MCP protocol app used by Backplane and is also
published on Hex. It provides client and server implementations for the
[Model Context Protocol](https://spec.modelcontextprotocol.io/) under the
Backplane.McpProtocol namespace. The package supports the modern
`2026-07-28` protocol over Streamable HTTP and stdio while preserving the
legacy initialization and session behavior required by older protocol versions.
## Installation
```elixir
def deps do
[
{:backplane_mcp_protocol, "~> 1.10.5"}
]
end
```
Inside the Backplane umbrella, use `{:backplane_mcp_protocol, in_umbrella: true}`
instead.
## Quick Start
### Server
```elixir
# Define a tool as a Component (compile-time registration)
defmodule MyApp.Echo do
@moduledoc "Echoes everything the user says to the LLM"
use Backplane.McpProtocol.Server.Component, type: :tool
alias Backplane.McpProtocol.Server.Response
schema do
field :text, :string, required: true, max_length: 150, description: "the text to be echoed"
end
@impl true
def execute(%{text: text}, frame) do
{:reply, Response.text(Response.tool(), text), frame}
end
end
defmodule MyApp.MCPServer do
use Backplane.McpProtocol.Server,
name: "My Server",
version: "1.0.0",
capabilities: [:tools]
# Static component registration — dispatches to MyApp.Echo.execute/2
component MyApp.Echo
@impl true
def init(_client_info, frame) do
# Legacy sessions can also register tools dynamically via the Frame:
# frame = register_tool(frame, "dynamic_tool", description: "...", input_schema: %{...})
{:ok, frame}
end
# Use init_request/2 instead for request-local modern setup.
end
# Add to your application supervisor
children = [
{MyApp.MCPServer, transport: :streamable_http}
]
# Add to your Phoenix router (if using HTTP)
forward "/mcp", Backplane.McpProtocol.Server.Transport.StreamableHTTP.Plug, server: MyApp.MCPServer
# Or if using only Plug router
forward "/mcp", to: Backplane.McpProtocol.Server.Transport.StreamableHTTP.Plug, init_opts: [server: MyApp.MCPServer]
```
Now you can achieve your MCP server on `http://localhost:<port>/mcp`
### Client
```elixir
# Add to your application supervisor
children = [
{Backplane.McpProtocol.Client,
name: MyApp.MCPClient,
transport: {:streamable_http, base_url: "http://localhost:4000"},
client_info: %{"name" => "MyApp", "version" => "1.0.0"},
protocol_version: :auto}
]
# Use the client
{:ok, result} = Backplane.McpProtocol.Client.call_tool(MyApp.MCPClient, "echo", %{text: "this will be echoed!"})
```
`:auto` is the default. It probes with modern `server/discover`, negotiates
`2026-07-28` when available, and falls back to legacy initialization only when
the transport provides protocol-defined evidence of a legacy peer. For
Streamable HTTP, a valid JSON-RPC `-32601 Method not found` response to the
current `server/discover` request is legacy evidence whether it arrives with
HTTP 200 or HTTP 400. Explicit version pins never downgrade. Pin a version
string when cross-era fallback is not wanted:
```elixir
protocol_version: "2025-06-18"
```
Modern HTTP requests are stateless, POST-only, and do not create an MCP
session. Legacy versions continue to use their existing initialization,
session, GET notification stream, and DELETE cleanup behavior.
### Streamable HTTP endpoint and dynamic headers
Use `url:` when the client already has the exact MCP endpoint, including any
non-default path:
```elixir
transport:
{:streamable_http,
url: "https://mcp.example.com/custom/mcp",
headers: %{"x-static" => "configured"},
headers_provider: fn ->
{:ok, %{"authorization" => "Bearer #{resolve_current_token()}"}}
end}
```
`url:` is mutually exclusive with `base_url:` plus `mcp_path:`. Supplying both
forms raises `ArgumentError`, as does supplying neither. With the composed form,
`mcp_path:` defaults to `/mcp`:
```elixir
transport:
{:streamable_http,
base_url: "https://mcp.example.com",
mcp_path: "/mcp"}
```
`headers_provider:` must be a zero-arity function returning either
`{:ok, headers_map}` or `{:error, reason}`. The map must contain binary header
names and binary values. Lists and keyword lists are not valid provider return
values. Header names are normalized case-insensitively before dynamic values
override static values, duplicate normalized names are rejected, and names or
values containing invalid header syntax or CR/LF are rejected.
The provider is invoked for every outbound HTTP operation: POST requests,
legacy GET streams, legacy session DELETE, and modern request-scoped streams.
Malformed provider results return `:invalid_headers_provider_result`; provider
exceptions, throws, and exits become `:headers_provider_failed`; an explicit
`{:error, reason}` is returned as a transport error. Static headers are
validated before the provider is called.
The provider executes in the transport request path, not in the original
caller's process. Automatic propagation of caller-local Logger metadata such as
a request ID is therefore not available through this zero-arity seam.
## Documentation
For detailed guides and examples, see the files in `pages/`, including the
client, server, API reference, and authorization guides.
## Verification
From `apps/backplane_mcp_protocol`, run the package and release checks with the
umbrella dependency directory:
```bash
MIX_ENV=test MIX_DEPS_PATH=../../deps mix test
MIX_ENV=dev MIX_DEPS_PATH=../../deps mix docs
MIX_ENV=dev MIX_DEPS_PATH=../../deps mix hex.build --unpack
```
Run the frozen official conformance package in a second terminal after starting
the server harness:
```bash
MIX_ENV=test MIX_DEPS_PATH=../../deps mix run --no-halt test/conformance/server_runner.exs -- 4105
npx -y @modelcontextprotocol/conformance@0.2.0-alpha.11 server --url http://127.0.0.1:4105/mcp --requirements 2026-07-28
MIX_ENV=test MIX_DEPS_PATH=../../deps mix compile
npx -y @modelcontextprotocol/conformance@0.2.0-alpha.11 client --command "ERL_LIBS=../../_build/test/lib elixir test/conformance/client_runner.exs --" --requirements 2026-07-28
```
The package revision and scored requirement counts are recorded in the
[conformance pin](https://github.com/gsmlg-opt/backplane/blob/main/apps/backplane_mcp_protocol/test/conformance/PIN.md).
## Examples
The app includes Elixir implementation examples using `plug` and `phoenix` apps:
1. [upcase-server](/priv/dev/upcase/README.md): `plug` based MCP server using streamable_http
2. [echo-elixir](/priv/dev/echo-elixir/README.md): `phoenix` based MCP server using sse
3. [ascii-server](/priv/dev/ascii/README.md): `phoenix_live_view` based MCP server using streamable_http and UI
## License
LGPL-v3 License. See [LICENSE](./LICENSE) for details.