Skip to main content

lib/urchin.ex

defmodule Urchin do
  @moduledoc """
  A Model Context Protocol (MCP) server library implementing the `2025-11-25`
  specification over the Streamable HTTP transport.

  See `Urchin.Server` for authoring a server (behaviour or DSL) and
  `Urchin.Transport.StreamableHTTP` for mounting it as a `Plug`. The convenience
  runner below boots a server with a standalone Bandit endpoint.
  """

  @doc """
  Starts a server module behind a standalone Bandit HTTP endpoint.

  Requires the optional `:bandit` dependency. Options are forwarded to
  `Urchin.Endpoint.start_link/1`; common ones are `:port`, `:path`, `:ip` and
  transport options such as `:allowed_origins`.

  ## Example

      {:ok, _pid} = Urchin.start_link(MyServer, port: 4000, path: "/mcp")
  """
  @spec start_link(module(), keyword()) :: {:ok, pid()} | {:error, term()}
  def start_link(server, opts \\ []) when is_atom(server) do
    Urchin.Endpoint.start_link(Keyword.put(opts, :server, server))
  end

  @doc "The latest protocol revision implemented by this library."
  @spec protocol_version() :: String.t()
  def protocol_version, do: Urchin.Protocol.latest_version()

  @doc """
  Sends a notification to every active session on its general (GET) stream.

  Useful for fan-out notifications such as `notifications/tools/list_changed` or
  `notifications/resources/list_changed`. Sessions without a connected GET stream
  buffer the notification for resumption.

  Returns the number of sessions notified.
  """
  @spec broadcast(String.t(), map() | nil) :: non_neg_integer()
  def broadcast(method, params \\ nil) when is_binary(method) do
    Urchin.Session.Registry
    |> Registry.select([{{:_, :"$1", :_}, [], [:"$1"]}])
    |> Enum.reduce(0, fn pid, count ->
      Urchin.Session.notify(pid, method, params)
      count + 1
    end)
  end
end