# `Raxol.MCP.Server`
[🔗](https://github.com/DROOdotFOO/raxol/blob/v2.7.0/lib/raxol/mcp/server.ex#L1)

Transport-agnostic MCP server.

Receives decoded JSON-RPC messages, dispatches to the Registry for tool/resource
operations, and returns response maps. Transports (stdio, SSE) call
`handle_message/2` and write the response back over their I/O channel.

## Supported Methods

- `initialize` -- MCP handshake, returns server capabilities
- `notifications/initialized` -- client acknowledgement (no reply)
- `ping` -- health check
- `tools/list` -- list the registered tools this caller may call
- `tools/call` -- invoke a tool
- `resources/list` -- list registered resources
- `resources/read` -- read a resource
- `prompts/list` -- list registered prompts
- `prompts/get` -- render a prompt with arguments
- `logging/setLevel` -- set server log level
- `completion/complete` -- auto-complete tool arguments

## Notifications

The server can push notifications to connected transports. Transports
subscribe via `subscribe/2` and receive `{:mcp_notification, map()}` messages.

## Elicitation

When `Raxol.MCP.Authorizer` returns `{:ask, prompt}`, what happens depends on
whether the client can be asked. A client that advertised the `elicitation`
capability at `initialize` (and has a subscribed transport) is sent a real
`elicitation/create` request; anything else gets the machine-readable
`authorization_required` deny.

The shape matters, because the transport seam is synchronous. `tools/call`
returns `nil` **immediately** and the call is parked:

    client                     server                    transport
    tools/call  ------------->  ASK -> park, return nil ->  (unblocked)
               <-------------  elicitation/create (push)
    response   ------------->  resume, return nil
               <-------------  tools/call response (push)

Returning `nil` is what keeps this from deadlocking: the transport's
`handle_message` does not block, so it stays free to READ the client's
answer. Both the prompt and the eventual response ride the subscriber
channel, so no transport change was needed.

A parked call is always closed, exactly once, by one of four paths: an
approval (runs the tool), a decline/cancel/unapproved accept, an error
response, or a timeout (`:elicitation_timeout_ms`, default 60s). Every path
but approval resolves to the same deny -- silence is not consent.

## Sensitive tools

A tool annotated `destructiveHint: true` or `sensitive: true` (see
`Raxol.MCP.ToolDef.sensitive?/1`) must not be served without an authorizer.
The server REFUSES TO BOOT when one is already registered, and denies at
`tools/call` for one registered afterwards -- the boot check alone would be
bypassable by registering late.

# `authorizer_source`

```elixir
@type authorizer_source() :: :configured | :default
```

Where this server's `:authorizer` came from.

`:configured` -- a caller chose it. `:default` -- a framework fallback stood in
because nobody chose one. The distinction exists for
`authorization_configured?/1`; see that function for why the two cannot be told
apart by the authorizer's value.

# `conn_id`

```elixir
@type conn_id() :: term()
```

Identifies one client connection. `:default` is the implicit single
connection used by stdio and by direct callers; a network transport mints an
opaque per-connection value.

# `pending_elicitation`

```elixir
@type pending_elicitation() :: %{
  request_id: term(),
  tool: String.t(),
  arguments: map(),
  timer: reference(),
  owner: conn_id()
}
```

A `tools/call` parked awaiting the client's elicitation answer. `request_id`
is the ORIGINAL call's id -- the one the client is still waiting on, and
`owner` is the connection that made it. Only `owner` may answer, and the
answer goes only to `owner`.

# `t`

```elixir
@type t() :: %Raxol.MCP.Server{
  authorizer: Raxol.MCP.Authorizer.t() | nil,
  authorizer_source: authorizer_source(),
  client_capabilities: %{required(conn_id()) =&gt; map()},
  elicitation_timeout_ms: pos_integer(),
  initialized: boolean(),
  log_level:
    :debug
    | :info
    | :notice
    | :warning
    | :error
    | :critical
    | :alert
    | :emergency,
  pending_elicitations: %{required(String.t()) =&gt; pending_elicitation()},
  read_authorizer: Raxol.MCP.Authorizer.t() | nil,
  registry: GenServer.server(),
  resource_subscriptions: %{required(String.t()) =&gt; boolean()},
  subscribers: %{required(conn_id()) =&gt; pid()},
  transport: transport()
}
```

# `transport`

```elixir
@type transport() :: :stdio | :sse | :unknown
```

Which transport carries this server's clients, as DECLARED at start.

Server-derived rather than client-asserted, which is what makes it usable in
an authorization decision: a policy can tell a stdio caller (which inherits
the OS process boundary) from a network one.

# `authorization_configured?`

```elixir
@spec authorization_configured?(GenServer.server()) :: boolean()
```

Whether authorization on this server was CHOSEN by a caller. Network transport
boot guards use this to fail closed (see `Raxol.MCP.Deployment`). Returns
`false` if the server is unreachable.

Deliberately not `authorizer != nil`. A framework may supply a restrictive
fallback -- a deny-everything allowlist is a sensible default for an
unconfigured production server -- and the value alone cannot be told apart from
a policy an operator wrote. Treating the fallback as configured would satisfy
this gate with a default, which removes exactly the forcing function the gate
exists to be: nobody had to decide that a network transport should serve.

So a server started with `authorizer_source: :default` answers `false` however
strict its authorizer is. `:default` is also the DEFAULT source: for a
predicate whose whole purpose is "nobody had to decide that a network
transport should serve", an embedder who forgets the option must fail closed,
not be taken at its word. `Raxol.Application` passes `:configured`
explicitly when `mcp_authorizer_source/0` says an operator named a key.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `handle_message`

```elixir
@spec handle_message(GenServer.server(), map()) :: {:reply, map() | nil}
```

Handle a decoded JSON-RPC message.

Returns `{:reply, response_map}` for requests or `{:reply, nil}` for
notifications (no response needed).

# `handle_message`

```elixir
@spec handle_message(GenServer.server(), map(), conn_id(), timeout()) ::
  {:reply, map() | nil}
```

Handle a decoded JSON-RPC message attributed to a specific connection.

`conn_id` is what binds an elicitation to the client that triggered it: only
the connection that parked a `tools/call` may answer its prompt, and the
prompt and result are delivered to that connection alone. A transport that
cannot tell its clients apart must NOT invent ids -- passing an unidentified
connection is safe (it can still make ordinary requests, and simply cannot
elicit), whereas reusing one client's id for another hands over its approvals.

# `notify`

```elixir
@spec notify(GenServer.server(), String.t(), map()) :: :ok
```

Send a notification to all subscribed transports.

# `read_authorization_configured?`

```elixir
@spec read_authorization_configured?(GenServer.server()) :: boolean()
```

Whether the READ seam has an authorizer. The network boot guards use this to
refuse a server that would serve `resources/read` -- live model state -- to
any client that connects. Returns `false` if the server is unreachable.

Presence, not source: unlike `authorization_configured?/1` there is no
framework fallback here worth telling apart. A read authorizer exists only
because someone configured one, and `Raxol.MCP.Deployment`'s tool-seam gate
is what carries the "nobody decided" argument.

# `start_link`

```elixir
@spec start_link(keyword()) :: GenServer.on_start()
```

Start the server, linked to the calling process.

# `subscribe`

```elixir
@spec subscribe(GenServer.server(), pid()) :: :ok
```

Subscribe a transport process to server notifications.

The subscriber receives `{:mcp_notification, notification_map}` messages.
Automatically unsubscribes when the subscriber process exits.

# `subscribe`

```elixir
@spec subscribe(GenServer.server(), pid(), conn_id()) :: :ok
```

Subscribe a transport process as a named connection.

The id is what later elicitation prompts and results are addressed to, so it
must be the same value the transport passes to `handle_message/3` for that
client's requests.

---

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