Noizu.MCP.Test runs a real client session against your server over an
in-memory transport — full protocol semantics (handshake, capabilities,
validation, notifications) with no I/O. Connections are isolated per test:
async: true is safe.
defmodule MyApp.MCPTest do
use ExUnit.Case, async: true
import Noizu.MCP.Test
setup do
%{client: connect(MyApp.MCP)}
end
test "search returns hits", %{client: client} do
assert {:ok, result} = call_tool(client, "search_docs", %{"query" => "installation"})
assert result.is_error == false
assert [%{type: :text, text: text}] = result.content
assert text =~ "hits"
end
endconnect/2 starts the server's supervision tree on demand (no need to add
it to your test app's supervisor) and performs the initialize handshake.
Options include protocol_version: and client capabilities: overrides
for negotiation tests.
The wrappers
Feature wrappers mirror the client API and return decoded structs:
call_tool/4, list_tools/2, list_resources/2,
list_resource_templates/2, read_resource/3, subscribe/2,
unsubscribe/2, list_prompts/2, get_prompt/4, complete/4,
set_log_level/2.
Lower-level escape hatches:
{:ok, result} = request(client, "ping") # any method, decoded result
id = send_request(client, "tools/call", %{...}) # fire without waiting
{:ok, result} = await(client, id) # ... collect later
notify(client, "notifications/cancelled", %{"requestId" => id})
cancel(client, id, "reason") # sugar for the above
deliver_raw(client, ~s({"jsonrpc": "2.0"...})) # malformed-input testsNotifications and progress
test "subscription fan-out", %{client: client} do
assert {:ok, _} = request(client, "resources/subscribe", %{"uri" => "config://app"})
MyApp.MCP.notify_resource_updated("config://app")
params = assert_notification(client, "notifications/resources/updated")
assert params["uri"] == "config://app"
end
test "progress", %{client: client} do
{:ok, _} = call_tool(client, "long_task", %{}, progress_token: "t1")
params = assert_progress(client)
assert params["progressToken"] == "t1"
end
test "silence", %{client: client} do
{:ok, _} = call_tool(client, "quick", %{})
refute_notification(client, "notifications/progress")
endMatchers buffer out-of-order traffic per session, so interleaved notifications don't flake.
Testing sampling / elicitation / roots
Tools that call Ctx.sample/elicit/list_roots need a client that advertises
those capabilities — give connect/2 a handler:
defmodule StubHandler do
@behaviour Noizu.MCP.Client.Handler
@impl true
def handle_sampling(_params, _state),
do: {:ok, %{"role" => "assistant", "content" => %{"type" => "text", "text" => "stub"}, "model" => "stub"}}
@impl true
def handle_elicitation(_params, _state), do: {:ok, :accept, %{"confirm" => true}}
end
client = connect(MyApp.MCP, handler: StubHandler)
assert {:ok, result} = call_tool(client, "consult_llm", %{"question" => "?"})Testing toolkit and hidden tools
Toolkit tools (use Noizu.MCP.Server.Toolkit + @mcp) test exactly like
classic ones — call_tool/4 by wire name. For hidden items, assert both
halves of the contract: excluded from listings, still callable:
test "hidden tools are unlisted but callable", %{client: client} do
{:ok, tools} = list_tools(client)
refute "internal_tool" in Enum.map(tools, & &1.name)
assert {:ok, result} = call_tool(client, "internal_tool", %{})
assert result.is_error == false
end
test "catalog reveals hidden tools", %{client: client} do
{:ok, result} = call_tool(client, "catalog", %{"type" => "tools"})
entry = Enum.find(result.structured["tools"], &(&1["name"] == "internal_tool"))
assert entry["hidden"] == true
endThe same pattern covers hidden prompts (list_prompts/2 + get_prompt/4)
and hidden resources (list_resources/2 + read_resource/3). For
session-gated visibility, flip the gate and assert the list_changed
notification:
{:ok, _} = call_tool(client, "unlock", %{}) # sets the session assign
assert_notification(client, "notifications/tools/list_changed")
{:ok, tools} = list_tools(client) # now includes gated toolsConformance
The library's own suite validates wire output against the vendored official
JSON schema (priv/spec/2025-11-25/schema.json). If you extend the
protocol surface by hand (request/3 with custom result shapes), consider
doing the same — see test/noizu/mcp/conformance_test.exs in the source
repo for the pattern.