BeamMCP

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"}]
end

Two contracts

Injection 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
end

The dispatch callback — the host does the work.

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

Running it

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.

One schema, one source

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.

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.

What it speaks

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 withany request, or server/discoverinitialize
version travels in_meta on every requestthe initialize params
sessionnone; each request stands aloneyes
pingremoved from the revision, refusedanswered

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.

Status

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.

Licence

Apache-2.0. See LICENSE, and NOTICE for attribution.