noizu_mcp implements the Model Context Protocol
— both the server side (expose tools, resources, and prompts to AI
applications) and the client side (consume MCP servers from Elixir). It
targets spec revision 2025-11-25 and negotiates down to 2025-06-18.
Installation
# mix.exs
defp deps do
[
{:noizu_mcp, "~> 0.1"},
# Optional — only if you serve or consume Streamable HTTP:
{:plug, "~> 1.16"},
{:bandit, "~> 1.5"},
{:req, "~> 0.5"}
]
endThe HTTP dependencies are optional: a stdio-only server needs none of them,
an HTTP server needs plug (and bandit if it runs standalone), and an
HTTP client needs req.
Your first server
A server is a module that registers components. A tool is a module with a
schema and a call/2 function:
defmodule MyApp.Tools.GetWeather do
use Noizu.MCP.Server.Tool,
name: "get_weather",
description: "Get current weather for a location",
annotations: [read_only_hint: true]
input do
field :location, :string, required: true, description: "City name or zip code"
field :units, :enum, values: [:celsius, :fahrenheit], default: :celsius
end
@impl true
def call(%{location: location, units: units}, _ctx) do
{:ok, "21.5°#{if units == :celsius, do: "C", else: "F"} and clear in #{location}"}
end
end
defmodule MyApp.MCP do
use Noizu.MCP.Server,
name: "myapp",
version: "1.0.0",
instructions: "Weather tools for MyApp."
tool MyApp.Tools.GetWeather
endThere is nothing else to declare: the tools capability (and every other
capability) is derived from what you register.
One module per tool scales down poorly for bundles of small tools —
Noizu.MCP.Server.Toolkit defines several tools in one module via @mcp
function annotations, registered with a single tool MyApp.Toolkit line. See
Toolkits, Categories & Hidden Tools.
Running it
Over stdio — add it to your supervision tree and start the VM with the transport attached:
# application.ex
children = [
{MyApp.MCP, transport: :stdio}
]claude mcp add myapp -- mix run --no-halt
Over Streamable HTTP — mount the plug in Phoenix or run it on Bandit:
# Phoenix router
forward "/mcp", Noizu.MCP.Transport.StreamableHTTP.Plug, server: MyApp.MCP
# or standalone — supervise the server, then the listener
children = [
MyApp.MCP,
{Bandit, plug: {Noizu.MCP.Transport.StreamableHTTP.Plug, server: MyApp.MCP}, port: 4040}
]claude mcp add --transport http myapp http://localhost:4040/mcp
Testing it
Noizu.MCP.Test connects an in-memory client straight to your server —
no transport, async: true safe:
defmodule MyApp.MCPTest do
use ExUnit.Case, async: true
import Noizu.MCP.Test
test "get_weather" do
client = connect(MyApp.MCP)
assert {:ok, result} = call_tool(client, "get_weather", %{"location" => "NYC"})
assert [%{type: :text, text: text}] = result.content
assert text =~ "NYC"
end
endWhere to next
- Tools & Schemas — the field DSL, validation, return contracts
- Resources & Prompts — resources, templates, subscriptions, prompts, completion
- The Handler Context — progress, logging, cancellation, sampling/elicitation
- Consuming Servers — the client API
- Streamable HTTP and stdio — transport deployment guides
- Authentication — OAuth 2.1 on both sides
- Testing — the full test toolkit