Skip to main content

guides/resources_and_prompts.md

# Resources, Templates & Prompts

## Static resources

A resource is a module with a fixed URI and a `read/2`:

```elixir
defmodule MyApp.Resources.Config do
  use Noizu.MCP.Server.Resource,
    uri: "config://app",
    name: "App Config",
    description: "Application configuration",
    mime_type: "application/json",
    subscribable: true

  @impl true
  def read("config://app", _ctx), do: {:ok, Jason.encode!(MyApp.Config.current())}
end
```

Register it with `resource MyApp.Resources.Config` on the server. `read/2`
returns:

- `{:ok, binary}` — text contents with the declared MIME type
- `{:ok, {:blob, binary}}` — binary contents (base64-encoded on the wire)
- `{:ok, %Noizu.MCP.Types.ResourceContents{}}` or a list of them — full control
- `{:error, Noizu.MCP.Error.resource_not_found(uri)}` — the spec's `-32002`

## Subscriptions

Declare `subscribable: true` and clients can `resources/subscribe`. When the
underlying data changes, notify subscribers from anywhere in your app —
the server module exports a fan-out helper:

```elixir
MyApp.MCP.notify_resource_updated("config://app")
```

Every session subscribed to that URI receives
`notifications/resources/updated`. Similarly, when the *set* of
tools/resources/prompts changes:

```elixir
MyApp.MCP.notify_changed(:tools)      # or :resources, :prompts
```

## Resource templates (RFC 6570)

Templates expose URI families. Matched variables arrive as an atom-keyed map:

```elixir
defmodule MyApp.Resources.TableSchema do
  use Noizu.MCP.Server.ResourceTemplate,
    uri_template: "db://{table}/schema",
    name: "Table Schema",
    mime_type: "application/json"

  @impl true
  def read(_uri, %{table: table}, _ctx) do
    case MyApp.Repo.table_schema(table) do
      {:ok, schema} -> {:ok, Jason.encode!(schema)}
      :error -> {:error, Noizu.MCP.Error.resource_not_found("db://#{table}/schema")}
    end
  end

  # Optional: argument completion for editors/clients
  @impl true
  def complete(:table, prefix, _ctx),
    do: {:ok, Enum.filter(MyApp.Repo.table_names(), &String.starts_with?(&1, prefix))}

  # Optional: enumerate concrete instances in resources/list
  @impl true
  def list(_ctx) do
    {:ok,
     for table <- MyApp.Repo.table_names() do
       %Noizu.MCP.Types.Resource{uri: "db://#{table}/schema", name: "#{table} schema"}
     end}
  end
end
```

Register with `resource_template MyApp.Resources.TableSchema`.

## Prompts

```elixir
defmodule MyApp.Prompts.CodeReview do
  use Noizu.MCP.Server.Prompt,
    name: "code_review",
    description: "Review code for quality issues"

  arguments do
    arg :code, required: true, description: "The code to review"
    arg :style, description: "Review style", complete: ["strict", "friendly"]
  end

  @impl true
  def get(%{"code" => code} = args, _ctx) do
    style = args["style"] || "strict"

    {:ok,
     [
       Noizu.MCP.Types.PromptMessage.user("Review this code (style: #{style}):"),
       Noizu.MCP.Types.PromptMessage.user(code)
     ]}
  end
end
```

- Prompt arguments arrive **string-keyed** (they are free-form spec-side).
- `PromptMessage.user/1` and `assistant/1` accept a string or
  `Noizu.MCP.Types.Content` structs.
- Return `{:ok, messages}` or `{:ok, messages, description: "..."}` to
  override the description per call.

## Hidden resources, templates & prompts

`hidden: true` works on resources, resource templates, and prompts exactly as
it does on tools — the item is omitted from `resources/list` /
`resources/templates/list` / `prompts/list` but stays fully readable/gettable
by URI or name:

```elixir
use Noizu.MCP.Server.Resource, uri: "internal://secrets", hidden: true
use Noizu.MCP.Server.Prompt, name: "internal_prompt", hidden: true

# or per registration:
resource MyApp.Resources.Config, hidden: true
prompt MyApp.Prompts.CodeReview, hidden: true
```

Hand-written list callbacks can opt hidden items back in: the registry
helpers behind the generated defaults
(`Noizu.MCP.Server.Features.Resources.list_registered/5`,
`list_registered_templates/3`, and
`Noizu.MCP.Server.Features.Prompts.list_registered/3`) take
`include_hidden: true`. The full hidden-items story — overrides, the catalog
discovery tool, session-gated visibility — lives in
[Toolkits, Categories & Hidden Tools](toolkits_and_discovery.md).

## Completion

`completion/complete` requests are routed automatically:

- `complete: ["a", "b"]` on an `arg` — static prefix-filtered suggestions
- `def complete(arg_name, prefix, ctx)` on a prompt or resource-template
  module — dynamic; return `{:ok, values}` or
  `{:ok, values, has_more: bool, total: n}`

Responses are capped at 100 values per the spec.

## Pagination

All `list` endpoints paginate automatically with opaque cursors when a
DSL-registered collection is large, and hand-written `handle_list_*`
callbacks receive `cursor` and return `{:ok, items, next_cursor | nil}` —
see [Tools & Schemas](tools.md) for the behaviour-only pattern.