A Model Context Protocol server core for the BEAM. Protocol handling, a stdio transport, and JSON Schema validation — with the tool catalog and the dispatch function injected by the host.
The package holds no tools, no domain, and no policy. It decides what a well-formed request is and refuses one that is not; what a tool does is the host's business.
def deps do
[{:beam_mcp, "~> 0.1"}]
endInjection without a specification is a claim with nothing behind it, so both are declared.
BeamMCP.ToolCatalog — the host names the tools.
defmodule MyApp.Catalog do
@behaviour BeamMCP.ToolCatalog
@impl true
def all do
[
%BeamMCP.ToolSpec{
name: :get_weather,
command_class: :observe,
mode: :read_only,
description: "Read the current weather for a place.",
input_schema: %{
"type" => "object",
"properties" => %{"place" => %{"type" => "string"}},
"required" => ["place"],
"additionalProperties" => false
}
}
]
end
endThe dispatch callback — the host does the work.
@type dispatch :: (atom(), map(), keyword() -> {:ok, term()} | {:error, term()})BeamMCP.Transport.Stdio.run(
tool_catalog: MyApp.Catalog,
dispatch: &MyApp.Dispatch.call/3,
server_name: "my-app"
):tool_catalog is required. :dispatch is required for tools/call. :server_name defaults
to beam_mcp, and a host that wants its own name in initialize says so.
A tool's schema lives on its ToolSpec. tools/list advertises that schema and
tools/call enforces that schema, so the contract a client is shown and the contract it is
held to cannot drift apart. Argument keys are derived from the schema's properties; values
are passed through unchanged, because turning a string into a domain term is the host's job and
a generic layer that guesses has acquired someone else's domain.
A ToolSpec that omits input_schema is a tool with no arguments: it advertises an open
empty object, so tools/call refuses nothing and dispatch is handed %{} whatever the client
sent.
Validation is a deliberately small subset of JSON Schema — type, properties, required,
additionalProperties, and bounds. It refuses rather than guesses, and it is not a general
validator.
Newline-delimited JSON-RPC over stdio, dual-era: it serves both the current revision and one legacy revision.
2026-07-28 (modern) |
2025-11-25 (legacy) |
|
|---|---|---|
| opens with | any request, or server/discover |
initialize |
| version travels in | _meta on every request |
the initialize params |
| session | none; each request stands alone | yes |
ping |
removed from the revision, refused | answered |
server/discover, tools/list, tools/call, shutdown, exit at both eras; initialize
and notifications/initialized at legacy only.
A request naming a revision the server does not support gets UnsupportedProtocolVersionError
(-32022) listing what it does support. 2024-11-05 is not supported — it predates
the two chosen revisions.
JSON-RPC batching is refused. It was added in 2025-03-26 and removed in 2025-06-18, so
it is required by exactly one revision of five and by neither of ours.
Pre-1.0. The API may change. Known gaps are listed above and in CONVENTIONS.md, which also
records how this package is developed — the gate takes no baseline, a probe's population is
derived the way the checked mechanism derives it, and CI is unproven until a run exists.
Consumers today: Ultraviolet, and Trinity as a candidate under its own evaluation.
Apache-2.0. See LICENSE, and NOTICE for attribution.