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"}
  ]
end

The 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
end

There 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
end

Where to next