# Noizu MCP Cheatsheet
## Server
{: .col-2}
### Define a server
```elixir
defmodule MyApp.MCP do
use Noizu.MCP.Server,
name: "myapp",
version: "1.0.0",
instructions: "..."
tool MyApp.Tools.Search
tool MyApp.Tools.Search, name: "alias"
resource MyApp.Resources.Config
resource_template MyApp.Resources.Table
prompt MyApp.Prompts.Review
end
```
Capabilities are derived — never declared.
### Run it
```elixir
# stdio
children = [{MyApp.MCP, transport: :stdio}]
# HTTP (Phoenix)
forward "/mcp", Noizu.MCP.Transport.StreamableHTTP.Plug,
server: MyApp.MCP
# HTTP (standalone)
children = [
MyApp.MCP,
{Bandit, plug: {Noizu.MCP.Transport.StreamableHTTP.Plug,
server: MyApp.MCP}, port: 4040}
]
```
### Tool
```elixir
defmodule MyApp.Tools.Search do
use Noizu.MCP.Server.Tool,
name: "search",
description: "...",
annotations: [read_only_hint: true]
input do
field :q, :string, required: true, min_length: 2
field :limit, :integer, min: 1, max: 50, default: 10
field :scope, :enum, values: [:all, :docs], default: :all
field :filters, :object do
field :tags, {:array, :string}
end
end
output do
field :count, :integer, required: true
end
@impl true
def call(%{q: q, limit: limit}, ctx) do
Noizu.MCP.Ctx.report_progress(ctx, 0.5)
{:ok, %{count: 7}}
end
end
```
Args arrive atom-keyed, defaults applied, enums cast.
### Tool return values
```elixir
{:ok, "text"} # text block
{:ok, %{key: "val"}} # structuredContent
{:ok, [%Content{...}]} # explicit blocks
{:ok, %ToolResult{...}} # verbatim
{:error, "msg"} # isError result
{:error, %Noizu.MCP.Error{}} # protocol error
raise "boom" # sanitized isError
```
### Raw schema escape hatch
```elixir
input_schema %{
"type" => "object",
"properties" => %{"q" => %{"type" => "string"}},
"required" => ["q"]
}
# call/2 then receives string keys
input_schema """
{"type": "object", "required": ["q"],
"properties": {"q": {"type": "string"}}}
"""
# JSON text — decoded at compile time
```
### Toolkit (many tools, one module)
```elixir
defmodule MyApp.Toolkit do
use Noizu.MCP.Server.Toolkit,
category: "Utility" # default category
@mcp name: "files.read", category: "Files",
description: "Read a file",
input: [path: [type: :string, required: true]]
def read_file(%{path: path}, _ctx),
do: {:ok, File.read!(path)}
@mcp visible: false # hidden, still callable
@mcp input: """
{"type": "object",
"properties": {"q": {"type": "string"}}}
"""
def lookup(args, _ctx), do: {:ok, args["q"] || ""}
end
```
Arity 0–2 (`args, ctx` trimmed); multiple `@mcp` lines
merge (later wins); data-form `input:` ⇒ atom keys,
map/JSON text ⇒ string keys. `category` rides in
`_meta.category`.
### Hidden tools & discovery
```elixir
use Noizu.MCP.Server.Tool, hidden: true, ...
# registration overrides (kit-wide for toolkits):
tool MyApp.Toolkit # all @mcp tools
tool MyApp.Tools.X, hidden: true
tool MyApp.Tools.Y, visible: false # alias
tool MyApp.Toolkit, category: "Admin"
# built-in discovery tool (lists hidden too):
tool Noizu.MCP.Server.Tools.Catalog, hidden: true
```
Hidden items skip `tools/list` but stay callable by
name. Catalog args: `type`, `query`, `category`,
`include_hidden`.
### Resource / template / prompt
```elixir
use Noizu.MCP.Server.Resource,
uri: "config://app", name: "Config",
mime_type: "application/json", subscribable: true
def read("config://app", _ctx), do: {:ok, json}
# binary: {:ok, {:blob, bytes}}
use Noizu.MCP.Server.ResourceTemplate,
uri_template: "db://{table}/schema", name: "Schema"
def read(_uri, %{table: t}, _ctx), do: {:ok, ...}
def complete(:table, prefix, _ctx), do: {:ok, [...]}
def list(_ctx), do: {:ok, [%Types.Resource{...}]}
use Noizu.MCP.Server.Prompt, name: "review"
arguments do
arg :code, required: true
arg :style, complete: ["strict", "friendly"]
end
def get(%{"code" => code}, _ctx),
do: {:ok, [Types.PromptMessage.user(code)]}
```
### Notify from anywhere
```elixir
MyApp.MCP.notify_resource_updated("config://app")
MyApp.MCP.notify_changed(:tools) # :resources :prompts
```
## Handler context (`Noizu.MCP.Ctx`)
{: .col-2}
### Outbound
```elixir
Ctx.report_progress(ctx, 0.5, total: 1.0, message: "...")
Ctx.info(ctx, "cache miss") # debug..emergency
Ctx.log(ctx, :warning, %{...}, logger: "myapp")
```
### State & cancellation
```elixir
Ctx.assign(ctx, :key, v) # this handler / init/2
Ctx.put_session(ctx, :key, v) # future requests
ctx.assigns.auth_claims # verified OAuth claims
Ctx.cancelled?(ctx) # poll in long loops
```
### Call the client back
```elixir
{:ok, r} = Ctx.sample(ctx, %{"messages" => [...],
"maxTokens" => 200}, timeout: 30_000)
case Ctx.elicit(ctx, "Proceed?", schema, timeout: 60_000) do
{:ok, {:accept, fields}} -> ...
{:ok, :decline} -> ...
{:ok, :cancel} -> ...
end
{:ok, roots} = Ctx.list_roots(ctx)
```
## Client
{: .col-2}
### Connect
```elixir
{Noizu.MCP.Client,
name: MyApp.FS,
transport: {:stdio, command: "npx", args: [...]},
# transport: {:streamable_http, url: "https://...",
# auth: {Noizu.MCP.Auth.Static, token: t}},
handler: MyApp.Handler,
client_info: %{name: "myapp", version: "1.0"}}
:ok = Client.await_ready(MyApp.FS, 15_000)
```
### Call
```elixir
{:ok, tools} = Client.list_tools(c)
{:ok, r} = Client.call_tool(c, "name", %{"k" => "v"},
timeout: 60_000, progress: fn p -> ... end)
{:ok, contents} = Client.read_resource(c, uri)
:ok = Client.subscribe_resource(c, uri)
{:ok, %{messages: m}} = Client.get_prompt(c, "name", %{})
{:ok, %{values: v}} = Client.complete(c, {:prompt, "name"}, "arg", "pre")
:ok = Client.set_log_level(c, :warning)
:ok = Client.set_roots(c, [%Types.Root{uri: "file:///w"}])
```
### Async
```elixir
ref = Client.async(c, "tools/call", %{...})
{:ok, r} = Client.await(c, ref, 5_000)
Client.cancel(c, ref, "reason")
```
### Handler (sampling / elicitation)
```elixir
@behaviour Noizu.MCP.Client.Handler
def handle_sampling(params, _s),
do: {:ok, %{"role" => "assistant", "model" => "m",
"content" => %{"type" => "text", "text" => "..."}}}
def handle_elicitation(_params, _s),
do: {:ok, :accept, %{"confirm" => true}}
# | {:ok, :decline} | {:ok, :cancel}
def handle_notification(method, params, _s), do: :ok
```
## Testing
{: .col-2}
### Connect & call
```elixir
import Noizu.MCP.Test
client = connect(MyApp.MCP) # async-safe
client = connect(MyApp.MCP, handler: StubHandler)
{:ok, r} = call_tool(client, "search", %{"q" => "x"})
{:ok, r} = request(client, "ping")
```
### Notifications
```elixir
params = assert_notification(client, "notifications/resources/updated")
params = assert_progress(client)
refute_notification(client, "notifications/progress")
```
### Cancellation race
```elixir
id = send_request(client, "tools/call", %{...})
cancel(client, id, "changed my mind")
```
## OAuth 2.1
{: .col-2}
### Server (resource server)
```elixir
forward "/mcp", Noizu.MCP.Transport.StreamableHTTP.Plug,
server: MyApp.MCP,
auth: [verifier: {MyVerifier, []},
resource_metadata: "https://.../.well-known/oauth-protected-resource"]
# behaviour:
def verify(token, _conn_info, _opts) do
{:ok, claims}
# | {:error, :invalid_token} → 401
# | {:error, :insufficient_scope, %{scope: "s"}} → 403
end
```
### Client
```elixir
auth: {Noizu.MCP.Auth.Static, token: token}
auth: {Noizu.MCP.Auth.OAuth,
client_id: "...", redirect_uri: "http://localhost:8914/cb",
scope: "mcp", authorize_user: &MyApp.Browser.run/1}
# authorize_user.(url) → {:ok, %{"code" => c, "state" => s}}
```