# `Snodo.OTP.Proxy`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.4.3/integrations/otp/lib/snodo/otp/proxy.ex#L1)

A local GenServer that stands in for a GenServer behind an MCP endpoint.
Application code keeps `GenServer.call/3` and `GenServer.cast/2`; each
becomes a `tools/call` chosen by the contract.

Requests go out one at a time, in the order the proxy receives them, and
each waits for its result before the next is sent. That keeps the ordering
OTP gives a sender and a GenServer: casts arrive in order, and a call sees
every cast sent before it. The remote GenServer handles one message at a
time anyway; the cost is that requests do not overlap on the wire.

Failure semantics follow OTP where the wire allows it:

  * The caller's own `GenServer.call` timeout decides how long the caller
    waits, and a result that arrives after the caller gave up is dropped,
    as a late reply is in OTP. The request itself is not cancelled: it
    holds the single in-flight slot until the remote side answers or
    `:request_timeout` expires, so later requests wait behind it.
  * A call the remote side timed out (the adapter's `:timeout`, or the
    proxy's `:request_timeout`, default 60,000 ms) is answered
    `{:error, :timeout}`, so a caller with an `:infinity` timeout is not
    left waiting.
  * A call to a remote target that is not there (`GenServer target
    unavailable`) stops the proxy with `:noproc`, so a `Process.monitor/1`
    on the proxy delivers `DOWN` and a supervisor can restart it. A cast
    to it is dropped, as a cast to a dead name is in OTP. With
    `on_unavailable: :error` the call is answered `{:error, :noproc}`
    instead and the proxy keeps running.
  * At most `:max_queue` requests (default 1,000) wait behind the one in
    flight. A call beyond that is answered `{:error, :queue_full}` and a
    cast is dropped.
  * A request whose process crashes, for example on arguments JSON cannot
    encode, is answered `{:error, {:request_failed, reason}}`, and later
    requests still run.
  * A reply `decode_reply/2` does not match is answered
    `{:error, {:decode_failed, name, json}}`. `encode_call/1` and
    `encode_cast/1` run on the caller's message in the proxy, as a
    GenServer callback does, so a raise there stops the proxy.
  * A connection failure stops the proxy with `{:nodedown, url}`. An HTTP
    error status, such as 401 or 404, is answered `{:error, error}` and the
    proxy keeps running.
  * A message the contract does not know stops the proxy with
    `{:function_clause, message}`, which is what an unmatched callback
    clause does to a GenServer.

Options: `:url` and `:contract` (required), `:name`, `:headers`,
`:request_timeout`, `:max_queue`, `:on_unavailable` (`:stop`, the default,
or `:error`), and `:connect`, a keyword list merged over the
defaults passed to `Snodo.Client.connect/2` (`protocol: "2026-07-28"`,
`timeout: 15_000`), for example `connect: [token_provider: provider]`.

What it cannot give: a pid for the remote process, links to it, or any
DOWN that is not provoked by a call. Those need a subscription stream.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `start_link`

---

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