BeamMCP.Server (BeamMCP v0.2.0)

Copy Markdown View Source

The protocol core: one message in, one response out, no process and no state of its own.

handle_message/2 takes a decoded JSON-RPC message and the state from new/1, and returns the next state and a response — or nil where the protocol defines no reply. A transport supplies the bytes; this module never touches them.

Two eras

It serves 2026-07-28 and 2025-11-25, and tells them apart the way the specification says a dual-era server should: a request carrying per-request _meta is served statelessly, and an initialize request selects legacy semantics. _meta decides only the statelessness — the revision it names then decides the method table and the result envelope, so a request declaring 2025-11-25 through _meta gets that revision's semantics, not the modern ones. A request naming a revision it does not support gets UnsupportedProtocolVersionError (-32022) listing what it does.

Two methods are matched before that switch and so are served identically at both eras: server/discover, which is the stdio era probe and must answer a client that does not yet know what it is talking to, and initialize, which selects legacy semantics whatever else it carries. Neither result is decorated.

What the host supplies

BeamMCP.Server.new(
  tool_catalog: MyApp.Catalog,        # required, a BeamMCP.ToolCatalog
  dispatch: &MyApp.Dispatch.call/3,   # required for tools/call
  server_name: "my-app"               # optional, defaults to "beam_mcp"
)

The server holds no catalog and executes nothing. It advertises the schema a tool's BeamMCP.ToolSpec carries and enforces that same schema on the call, so what a client is shown and what it is held to cannot drift apart.

Summary

Types

The dispatch contract. A host supplies a function of this shape; the server calls it and never inspects what it does. Ultraviolet's Dispatch.safe_call/3 satisfies it.

Types

dispatch()

@type dispatch() :: (atom(), map(), keyword() -> {:ok, term()} | {:error, term()})

The dispatch contract. A host supplies a function of this shape; the server calls it and never inspects what it does. Ultraviolet's Dispatch.safe_call/3 satisfies it.

state()

@type state() :: %{
  dispatch: (atom(), map(), keyword() -> {:ok, term()} | {:error, term()}),
  dispatch_opts: keyword(),
  initialized?: boolean(),
  server_name: String.t(),
  shutdown?: boolean(),
  tool_catalog: module()
}

Functions

handle_message(state, messages)

@spec handle_message(state(), map() | list()) :: {state(), map() | nil}

new(opts \\ [])

@spec new(keyword()) :: state()

shutdown?(state)

@spec shutdown?(state()) :: boolean()