# `Phantom.Router`
[🔗](https://github.com/dbernheisel/phantom_mcp/blob/main/lib/phantom/router.ex#L1)

A DSL for defining MCP servers.
This module provides functions that define tools, resources, and prompts.

See `Phantom` for usage examples.

## Telemetry

Telemetry is provided with these events:

- `[:phantom, :dispatch, :start]` with meta: `~w[method params request session]a`
- `[:phantom, :dispatch, :stop]` with meta: `~w[method params request result session]a`
- `[:phantom, :dispatch, :exception]` with meta: `~w[method kind reason stacktrace params request session]a`

# `connect`

```elixir
@callback connect(Phantom.Session.t(), term()) ::
  {:ok, Phantom.Session.t()}
  | {:unauthorized | 401,
     www_authenticate_header :: Phantom.Plug.www_authenticate()}
  | {:forbidden | 403, message :: String.t()}
  | {:error, any()}
```

When the connection is opening, this callback will be invoked.
You will receive the session and the adapter's context, for example, when using `Phantom.Plug`, you'll get the
`Plug.Conn`.

This is critical for authentication and authorization.

- `{:ok, session}` - The session is authenticated and authorized.
- `{:unauthorized | 401, www_authenticate_header}` - The session is not authenticated. The `www_authenticate_header` should
reveal to the client how to authenticate. This can either be a string to represent a built header, or a map
that is passed into `Phantom.Plug.www_authenticate/1` to build the header.
- `{:forbidden | 403, message}` - The session is not authorized. For example, the user is authenticated,
but lacks the account permissions to access the MCP server.
- `{:error, message}` - The connection should be rejected for any other reason.

# `disconnect`

```elixir
@callback disconnect(Phantom.Session.t()) :: {:ok, Phantom.Session.t()} | any()
```

When the connection is closing, this callback will be invoked.

The return value is largely ignored. This is a lifecycle event that will likely happen very often.
This could be helpful if you wanted to emit a side-effect when the connection closes or
modify the session. Consider hooking into the `[:phantom, :plug, :request, :disconnect]`
telemetry event instead. The telemetry event will receive the modified session if implemented.

# `instructions`

```elixir
@callback instructions(Phantom.Session.t()) :: {:ok, String.t()}
```

Return the instructions for the MCP server for a given session. This is retrieved when the client
is initializing with the server.

By default, it will return the compiled `:instructions` provided to `use Phantom.Router`, however
if you need the instructions to be dynamic based on the session, you may implement this
callback and return `{:ok, "my instructions"}`. Any other shape will result in no instructions.

# `list_resources`

```elixir
@callback list_resources(String.t() | nil, Phantom.Session.t()) ::
  {:reply, Phantom.Resource.list_response(), Phantom.Session.t()}
  | {:noreply, Phantom.Session.t()}
  | {:error, any(), Phantom.Session.t()}
```

List resources available to the client.

This will expect the response to use `Phantom.Resource.list/2` as the result.
You may also want to leverage `resource_for/3` and `Phantom.Resource.resource_link/3`
to construct the response. See `m:Phantom#module-defining-resources` for an exmaple.

Remember to check for allowed resources according to `session.allowed_resource_templates`

# `server_info`

```elixir
@callback server_info(Phantom.Session.t()) ::
  {:ok,
   %{
     :name =&gt; String.t(),
     :version =&gt; String.t(),
     optional(:icons) =&gt; [map()],
     optional(:websiteUrl) =&gt; String.t()
   }}
  | {:error, any()}
```

Return the server information for the MCP server for a given session. This is retrieved when the client
is initializing with the server.

By default, it will return the static `:name` and `:vsn` provided to `use Phantom.Router`, however
if you need the instructions to be dynamic based on the session, you may implement this
callback and return `{:ok, %{name: "my name", version: "my version"}`. Any other shape will result
in no server information.

You may also include optional `:icons` (a list of serialized icon maps) and `:websiteUrl` (a URL string)
in the returned map. These are part of the MCP 2025-11-25 specification for `Implementation`.

# `terminate`

```elixir
@callback terminate(Phantom.Session.t()) :: {:ok, any()} | {:error, any()}
```

When the session is terminating, this callback will be invoked. Termination is when the client
indicates they are finished with the MCP session.

The callback will be invoked and should return `{:ok, _}` or `{:error, _}` to indicate
success or not in terminating the session. Consider hooking into the
`[:phantom, :plug, :request, :terminate]` telemetry event for side-effects.

# `prompt`
*macro* 

See `Phantom.Router.prompt/3`

# `prompt`
*macro* 

Define a prompt that can be retrieved by the MCP client.

## Examples

    prompt :summarize,
      description: "A text prompt",
      completion_function: :summarize_complete,
      arguments: [
        %{
          name: "text",
          description: "The text to summarize",
        },
        %{
          name: "resource",
          description: "The resource to summarize",
        }
      ]
    )

    # ...

    require Phantom.Prompt, as: Prompt
    def summarize(args, _request, session) do
      {:reply, Prompt.response([
        assistant: Prompt.text("You're great"),
        user: Prompt.text("No you're great!")
      ], session}
    end

    def summarize_complete("text", _typed_value, session) do
      {:reply, ["many values"], session}
    end

    def summarize_complete("resource", _typed_value, session) do
      # list of IDs
      {:reply, ["123"], session}
    end

# `read_resource`

```elixir
@spec read_resource(Phantom.Session.t(), module(), URI.t()) ::
  {:ok, uri_string :: String.t(),
   Phantom.Resource.blob_content() | Phantom.Resource.text_content()}
  | {:error, error_response :: map()}
```

Reads the resource given its URI, primarily for embedded resources.

This is available on your router as: `MyApp.MCP.Router.read_resource/3` that
accepts the session, resource_name, and path params.

For example:

    iex> MyApp.MCP.Router.read_resource(session, :my_resource, id: 321)
    {:ok, "myapp:///resources/123", %{
      blob: "abc123"
      uri: "myapp:///resources/123",
      mimeType: "audio/wav",
      name: "Some audio",
      title: "Super audio"
    }}

# `resource`
*macro* 

See `Phantom.Router.resource/4`

# `resource`
*macro* 

Define a resource that can be read by the MCP client.

## Examples

    resource "app:///studies/:id", MyApp.MCP, :read_study,
      description: "A study",
      mime_type: "application/json"

    # ...

    require Phantom.Resource, as: Resource
    def read_study(%{"id" => id}, _request, session) do
      {:reply, Response.response(
        Response.text("IO.puts \"Hi\"")
      ), session}
    end

# `resource_uri`

Constructs a response map for the given resource with the provided parameters. This
function is provided to your MCP Router that accepts the session instead.

For example

```elixir
iex> MyApp.MCP.Router.resource_uri(session, :my_resource, id: 123)
{:ok, "myapp:///my-resource/123"}

iex> MyApp.MCP.Router.resource_uri(session, :my_resource, foo: "error")
{:error, :invalid_params}

iex> MyApp.MCP.Router.resource_uri(session, :unknown, id: 123)
{:error, :router_not_found}
```

# `tool`
*macro* 

Define a tool that can be called by the MCP client.

## Input schema DSL

Use a `do` block to define input fields with an Ecto-like syntax. This
generates JSON Schema for clients and validates incoming arguments at
dispatch time. See `Phantom.Tool.JSONSchema` for the full list of field
types, options, and validators.

    tool :search, description: "Search for stuff" do
      field :query, :string, required: true
      field :limit, :integer, default: 10
      field :tags, {:array, :string}
    end

Nested objects are supported with a `do` block on `:map` fields:

    tool :search, description: "Search" do
      field :query, :string, required: true
      field :filters, :map do
        field :category, :string, in: ~w[books movies music]
        field :min_price, :number, minimum: 0
      end
    end

## Map-based input schema

For full control over the JSON Schema (without server-side validation),
pass `:input_schema` directly:

    tool :echo,
      description: "Echo a message",
      input_schema: %{
        required: [:message],
        properties: %{
          message: %{type: "string", description: "message to echo"}
        }
      }

## External handler module

    tool :search, MyApp.MCP, description: "Search" do
      field :query, :string, required: true
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
