Skip to content

Latest commit

 

History

History
173 lines (158 loc) · 28.1 KB

File metadata and controls

173 lines (158 loc) · 28.1 KB

What this package will not implement

This page is the package's boundary, written down so that it survives the issue tracker. Each entry below is something beam_mcp will never do, with the reason in a line and the test that enforces it, by path and by name. The tests are the proof; this page is the contract. A request to cross one of these lines is a request for a different package: the host (the application that embeds this package, supplies its catalog and dispatch function, and owns every decision about them) for entries 1, 4, 7, 11 and 12; a signer (a module behind the one callback BeamMCP.Signer names; the reference one is in the separate package beam_mcp_signer, Ed25519 through OTP's :crypto, the key under opts[:private_key]) for entries 2 and 3; a client, a logger, an authorization server or a graph-mining library that this project will not write, for entries 9, 5, 8 and 10; and for entry 6, the specification itself, since only it can define a capability. Such a request is answered by pointing here, not by widening this package.

The page and the tests are held together mechanically, in both directions, by three tests (a citation is a path and, on the same line, the names it holds): test/beam_mcp/will_not_implement_test.exs "every test the page cites exists, by path and by name, and every citation names a test" "every test file carrying a boundary marker is cited on the page" "every file under test/beam_mcp/boundary/ carries the marker". Neither may drift from the other.

How the censuses read the tree

A census is a test over the source rather than over behaviour. The censuses under test/beam_mcp/boundary/ (entries 2, 3, 4, 6, 7, 8, 9, 10, 11 and 12) share one reader (test/support/beam_mcp/boundary.ex): every non-comment line of every lib/**/*.ex file, the same files mix compile reads, so an untracked module is seen; doc strings are read too, so a census that must allow prose says so by pattern. That lib/**/*.ex is the whole application is itself pinned, in the source and in the built artefact (no Erlang sources, no other compile path, Mix's own compilers and no other, no macro, quote or compile-time read under lib/, and every module the built application lists a BeamMCP. one, the .app list agreeing with the ebin): test/beam_mcp/boundary/population_test.exs "nothing compiles into the application from outside lib/: no Erlang sources, no other elixirc path" "every module the built application lists is a BeamMCP module, and the list is the ebin". Two older censuses cited below read the tracked files instead (git): entry 1's sign census and the README's word census under entry 4; a module not yet added to git is outside their count, which every file in a pull request is inside. Each census was shown red, before it was committed, by planting its violation under lib/ and restoring. A census reads text, so its reach is the reach of its pattern; where a pattern allows a shape, the entry says which.

Three censuses read the artefact instead of the text (this one, entry 11's, and the population census's module list), and this one is what the others rest on. :xref over the beams compiled from lib/ lists every module and every function the package calls, whatever the call was spelled: an alias, a pipe, a capture, apply under any name, a call without parentheses on a named module, all resolve to the same edge in the compiled form. It is run with Erlang's built-in functions included: :xref omits calls to BIFs by default, and some of the functions that matter most on :erlang (apply/3, binary_to_term/1, list_to_atom/1, binary_to_atom/2) are BIFs the default listing does not see (measured; spawn/2 and open_port/2 it does). The lists are pinned exactly: the modules the package calls (:xref itself among them: the declared connectome reads beams with it); on the modules through which code, names, secrets, the operating system, the disk, another process or another node could be reached (:erlang, :code, Code, System, Application, :application, :trace, :crypto, :ets, :xref, :io_lib, :logger, Logger, :digraph, :atomics, :telemetry, Jason, Process, GenServer, Supervisor, Plug.Conn, Plug.Exception, IO), the functions; the one atom the package makes from a binary, by the function that makes it; and every atom in the compiled forms that names a module (an Elixir.-prefixed atom is a module name by construction, installed here or not; an Erlang-style one, if this VM can load it): whatever it was written for, a module handed to a supervisor as a child spec's {m, f, a}, or to anything else as data, is named there whether or not it is ever called, so that set is pinned too: the called modules plus eight that arrive through attributes, export lists, option names, a tuple tag; and one, json, that is a local function name OTP 28 turned into a module's name: the loud collision this census is built to have (shell would trip it the same way). The eight are exact for the OTP the gate runs; an older OTP without a json module reads one fewer. The population is the build's ebin, not Mix's .app file, which is regenerated on a one-second mtime and can miss a module compiled in the same second. A new library, an evaluator, a socket, a shell, a spawn to another node, a key store, an environment read, a file read or an atom decoded from the wire fails here until it is named: test/beam_mcp/boundary/package_reach_test.exs "the modules the package calls are exactly the listed ones" "on the modules that could reach code, names, secrets, the OS or another node, the functions called are exactly the listed ones" "the one atom made from a binary is made in Server.declared_atoms/1" "every atom in the compiled forms that names a module is a called module or one of the ten named as data". What runs at compile time (a module body, an attribute's expression) leaves no call in the beam and is outside every artefact census; the text holds that line instead, by name. The census's patterns are the list; this paragraph names their classes, not their spellings. The classes: macro and guard definitions, quote, unquote, unquote_splicing; the Elixir and Erlang evaluators and compilers (Code except Code.ensure_*, :elixir, :elixir_*, :compile, :erl_eval, :erl_parse, :erl_scan, EEx, Mix); the four :code loaders (load_*, atomic_load, prepare_loading, finish_loading); the readers of the environment and the disk (:os., File., :file., :prim_file., :filelib., Path.wildcard, :init., and twelve named readers on System and Application); a quoted atom carrying a \x or \u escape (the two escapes that can spell a letter), any word sigil at all (one string sigil is allowed by its exact line; every other ~w, whatever its delimiter, lines, escapes or modifier, and the macro called by name, is refused), and the Erlang names above in quotes; an atom built by interpolation is the joined-strings edge below, read by nothing; and every import, alias or require that would bring any of these in under another name, across lines. Three allowances by exact line: the package's own version read from mix.exs, the tracer's threat model naming the loader it does not call, and the HTTP transport's one word sigil, which makes strings. Beside the census, the compiler's warnings_as_errors (in mix.exs) refuses a needlessly quoted atom outright, a bar this package's build has and a stranger's might not. A reach under a name not in those classes, run in a module body, is held by nothing but a reviewer's eye; the reader that would see a module body by what it does is a compiler tracer, and it is not built. Macros invoked from Elixir and the dependencies (use GenServer, defstruct, Logger.error) expand under lib/ as anywhere and are the dependency list's, held by their names only; the one Elixir macro that reads the disk at expansion, EEx.function_from_file, is barred by its name.

The dependency set is pinned beside it by name, as declared and as locked (a path: dependency never reaches the lock; a source or checksum is not read): test/beam_mcp/boundary/population_test.exs "the dependencies mix.exs declares are exactly the listed ones" "the dependencies the lock file holds are exactly the listed ones". The text census for the same acts stays beside it, for the line it names: test/beam_mcp/boundary/no_dynamic_evaluation_test.exs "no line under lib/ evaluates code or builds a name at runtime, beyond the argument-key atoms".

One census under the same reader pins a seam rather than barring an act. Entry 12 says the host that wants a multi-round-trip request owns its state above this core; where that is, is the :server option of both transports: a module, BeamMCP.Server by default, through which the transports reach new/1, handle_message/2 and (stdio) shutdown?/1 and never by name, so a wrapper the host passes is what answers. Read three ways over the whole of lib/, no path or module listed: the text, the artefact's call edges, and the artefact's atom sites (where the core's name may appear in another module exactly once per transport, as the default value): test/beam_mcp/boundary/no_server_literal_test.exs "no line under lib/ calls BeamMCP.Server by name, but the core's own moduledoc example" "from the artefact: no module under lib/ but BeamMCP.Server itself calls a function of it" "from the artefact: BeamMCP.Server appears in another module's compiled forms exactly once per transport, as the default value". The core itself is unchanged by the seam: entry 12's two tests hold as they stood.

The entries

# The package will never Why Enforced by
1 compute or populate a sign. :allow, :deny, :hold and :ungoverned are a consumer's; the package writes :unset (no sign has been supplied to it) into every edge's sign slot, on both graphs, and treats no sign as suppression. A sign is a verdict. The package exports topology and lets the verdict be somebody else's, so that nothing in it can be mistaken for approval; and a sign it cannot attribute to a decider is not a reason to leave an edge out of a record. test/beam_mcp/connectome/census_test.exs "no code line under lib/ writes or names a sign other than :unset" "no code line under lib/ filters, hides or downgrades an edge on the basis of its sign"; test/beam_mcp/readme_claims_test.exs "the package writes only :unset into the sign slot, on both graphs"
2 hold a key. No line under lib/ generates, loads, decodes or stores key material. The one cryptographic function the package calls is :crypto.hash/2, a digest with no key in it: one site, over canonical bytes, the algorithm a variable bound from the caller's option (SHA-256 by default, SHA-384 or SHA-512 by choice, the envelope naming which) and never a literal, and the count is pinned so that a second site has to say what it hashes. A package that holds a key can be asked to use it. The canonical bytes exist so that a consumer can sign them without importing this package. test/beam_mcp/boundary/no_key_holding_test.exs "no line under lib/ names key material or calls a crypto function other than :crypto.hash/2" ":crypto.hash/2 is called at one site, over canonical bytes, with the algorithm a variable"; test/beam_mcp/boundary/package_reach_test.exs "on the modules that could reach code, names, secrets, the OS or another node, the functions called are exactly the listed ones"
3 make a signature of its own. No signing or MAC primitive is called under lib/, and no key is held. The one sign/2 defined there is BeamMCP.Signer.None, a no-op answering {:error, :no_signer}; the one call of a signer is BeamMCP.Connectome.Canonical.signature/3, which hands the canonical bytes to a host-supplied module implementing BeamMCP.Signer (exactly one callback, sign(canonical_bytes, opts), two arguments with those names), and places what comes back beside them. The key and the primitive are in the separate package beam_mcp_signer (Ed25519 through OTP's :crypto, the key under opts[:private_key]), which a host attaches; this package does not depend on it. Signing bytes publishes nothing about who decides what; a richer callback would, so the census pins the shape and any widening is a visible act. The sign field an edge carries is the host's verdict slot (entry 1), a value and not an act. test/beam_mcp/boundary/no_signature_test.exs "no line under lib/ calls a signing or MAC primitive" "exactly one def sign under lib/: the no-op, spelled as pinned, in its own file" "the behaviour has exactly one callback, sign/2, with the pinned argument names and return" "exactly one call of a signer under lib/: signature/3's, over encode/2's bytes"; test/beam_mcp/boundary/package_reach_test.exs "the modules the package calls are exactly the listed ones"
4 decide authority. It writes no verdict (entry 1: the sign slot is only ever :unset) and names no receipt (a signed record that a call happened), no approval (a decision that a call may proceed), no risk tier (a ranking of calls by consequence), no egress and no mask (the withholding or rewriting of what leaves the system): any word containing receipt, approv, egress, tier or mask (a "frontier" in prose would trip it, loudly, and be read). Every one of these is a decision about the host's tools, and the package holds none of them; the moment it held one, its topology could be mistaken for a verdict. The words "verdict" and "authority" are not barred: under lib/ they name the host's slot in the edge's docs and the diff's own class, dead authority. They are terms the package defines, not acts it performs. The acts are barred in the spellings code uses as well as prose. test/beam_mcp/boundary/no_authority_test.exs "no line under lib/ names a receipt, an approval, a risk tier, egress or a mask, in any spelling"; test/beam_mcp/connectome/census_test.exs "no code line under lib/ writes or names a sign other than :unset"; test/beam_mcp/readme_claims_test.exs "deliberately out: no line under lib/ names a receipt, an approval, a risk tier or egress"
5 put a payload byte into the observed graph. Edge identity only: never an argument, a result, a header, an error message or a stack frame's contents. A wiring diagram that carries payloads is a log, and a log of tool calls is the most sensitive artefact a host produces. The observed graph is safe to export because it cannot contain what was said. test/beam_mcp/connectome/observed_test.exs "a marker in a nested argument map and in a uri argument is absent from rows, bytes, sidecar and latency" "a marker in the error a dispatch returns is absent" "a marker in an exception a dispatch raises is absent, and the edge was still recorded" "the :stop event itself carries no argument, result or header bytes" "dispatch_opts never enter: a secret handed to every dispatch is in no row, byte or summary" "request headers carrying the marker reach neither the events nor the rows" "a throw and an exit from the dispatch are exceptions of their kind, with marker-free frames; a crafted error_info is dropped from the frames"; test/beam_mcp/connectome/tracer_test.exs "a registered name is identity: a secret in a name is published in the bytes, the message beside it is not"; test/beam_mcp/connectome/diff_test.exs "an observed graph the collector built from a call carrying a marker diffs to bytes with no marker"; test/beam_mcp/readme_claims_test.exs "the observed graph carries edge identity only, never a payload byte"
6 claim an MCP capability the specification does not define. No topology or reachability capability on the wire; connectome:// is the package's own URI scheme, not a claimed capability. Capabilities are negotiated with clients that read the specification, not this README. An invented key is a promise no client can act on. The advertised keys are held to ServerCapabilities as each revision's schema defines it: a copy of the two key sets taken from the schema files on 2026-09-15 and cited in the test (tasks in 2025-11-25; extensions in 2026-07-28). The entry bars keys the specification does not define, at the top level and one level under each capability whose sub-keys the schema names (tools: listChanged, the only capability advertised today; resources, prompts and tasks are held the day they are advertised; completions, logging, experimental and extensions are open objects and nothing is read under them; a third level is not read); a key it does define and this package does not implement is a different question, answered by the README. test/beam_mcp/boundary/no_invented_capability_test.exs "server/discover advertises only keys the 2026-07-28 schema defines" "the initialize result advertises only keys the 2025-11-25 schema defines" "no line under lib/ names a topology or reachability capability on the wire"
7 issue or honour a session identifier. Over HTTP no response carries Mcp-Session-Id, a request carrying one gets the same status and body as one that does not (held on four methods, including an unknown one), and no line under lib/ reads or writes one; the name is barred in every delimiter and casing (mcp-session-id, McpSessionId, session_id), with one allowance: the hyphenated header name written directly after the words "no `", the transport's own denial. Every request stands alone; the 2026-07-28 transport removed sessions. Refusing unestablished callers on a transport where that matters is the host's job, stated in the README. test/beam_mcp/boundary/no_session_test.exs "no response carries an mcp-session-id header, and a request carrying one is answered as if it did not" "no code line under lib/ reads or writes a session identifier"
8 carry OAuth. No authorization flow, discovery document, token endpoint or bearer handling under lib/: the authorization header is not even read there; it reaches the host's hook untouched. The transport offers :authorize and :authorize_body hooks and performs no cryptography. Verifying is the host's work; making it possible is the transport's. A package that performs no cryptography (entries 2 and 3) cannot honestly offer an OAuth server. test/beam_mcp/boundary/no_oauth_no_client_test.exs "no OAuth under lib/"
9 be a client. No module under lib/ names itself a client, opens an outbound connection, or sends an initialize request. The artefact holds the outbound half whatever the spelling: none of gen_tcp, ssl, socket, gen_udp, ssh, httpc, inets, os, peer, net_kernel, rpc, Port, File or any HTTP client is among the modules the package calls, and of :erlang only spawn/1 (the local one) is; the text census names the same by word. A message to a process registered on another node (send/2, GenServer.call/2 to a {name, node}) is the one outbound act the compiled form cannot tell from a local one; every initialize under lib/ is a clause head that receives one, or the list of methods the modern era removed. A server that also calls out has two threat models, and a page like this one for each; this package keeps one. test/beam_mcp/boundary/no_oauth_no_client_test.exs "no client under lib/: no client module, no outbound connection, initialize only ever received"; test/beam_mcp/boundary/package_reach_test.exs "the modules the package calls are exactly the listed ones"
10 enumerate all paths or match motifs in the connectome. all_paths is refused by name, always; it is not capped; its one definition under lib/ is the refusal, and no function under lib/ carries motif, isomorph, subgraph, path or walk in its name, except the bare names path and walk (reach's own witness builder and dominator pass), allowed by name alone, in any module and at any arity. The number of paths is exponential in the graph; a cap would be a promise to answer "some of them", which is worse than no answer. Motif matching is refused for the same reason. The reachability questions the package does answer are on the reach page. test/beam_mcp/connectome/reach_test.exs "all-paths enumeration is refused by name, not attempted"; test/beam_mcp/boundary/no_path_enumeration_test.exs "the only definition of all_paths under lib/ is the refusal, and no motif matcher is defined"
11 hold a tool, a domain, or a concrete catalog. No module under lib/ implements BeamMCP.Catalog (by @behaviour, or by defining or delegating capabilities/0, which is all the package asks of a catalog) and nothing under lib/ builds a %BeamMCP.ToolSpec{}, in any spelling: in the compiled form of every module under lib/, the atom BeamMCP.ToolSpec occurs only inside a map pattern: a literal, an alias, a struct/2, a Map.put of __struct__, a map update of __struct__, a map key, a %__MODULE__{} in its own module or a variable bound to the module all leave the atom somewhere else, and the compiler-generated sites are not skipped but pinned (the struct's own __struct__/0,1 and Catalog's callback info, exactly), so an unquote of a hand-built syntax tree marked generated is one site too many. What this does not see is a tool derived from a tool the package was handed: a matched struct updated field by field carries no atom of its own; nothing under lib/ does that today, and it is a reviewer's line. The struct is defined there, matched there, and never constructed there. The catalog is reached through three callees, capabilities/0 at five sites, read_resource/1 at one and get_prompt/2 at one: in the compiled form every call through a module known only at runtime, with parentheses, is one of those seven; the one read is reached only after the catalog's own capabilities/0 has listed the uri or a template that matches it, and the one render only after it has listed the prompt and the tools validator has passed the arguments (a call through a function value is a closure, the host's dispatch and hooks or the package's own, and not a module call); and the one spelling the compiled form cannot tell from a field access (m.capabilities without parentheses, a deprecated form) is held by the list of every name the package reaches by dot syntax, pinned exactly: its own fields, conn.method, and a rescued exception's __struct__ (names, not module–name pairs: a runtime module whose export shares a field's name, m.nodes with m = :erlang, is inside the list). The catalog and the dispatch are injected by the host. The README's opening sentence: the package holds no tools, no domain, and no policy; a commodity protocol layer with nothing of its own to protect or to sell. test/beam_mcp/boundary/no_catalog_test.exs "no module under lib/ implements BeamMCP.Catalog" "no line under lib/ constructs a tool" "the catalog is called through three callees: capabilities/0 at five sites, read_resource/1 at one, get_prompt/2 at one" "every name the package reaches by dot syntax is on its pinned list"
12 run a multi-round-trip request. The 2026-07-28 revision lets a server answer tools/call, resources/read or prompts/get with an InputRequiredResult and finish on a later request carrying inputResponses and a requestState of the server's own. This package answers every request completely or refuses it: the one resultType written under lib/ is "complete", at one site, and neither continuation parameter is read anywhere: a request carrying them is served as if it carried neither, since nothing here ever asked for input. An input-required round trip is a conversation with state between two messages (what was asked, what came back, what the server had decided so far), and this core has no process and no state of its own by design: one message in, one response out. The requestState the revision offers as the server's opaque continuation would carry the server's own decision state for a client to hand back, unlike the pagination cursor, which names a position any client may name by other means and carries no decision. A host that wants the round trip owns exactly the state it needs to run it, above this core. Named by the owner as the one item of the 2026-07-28 surface nobody had placed; placed here, out, so nobody later improves it in by accident. test/beam_mcp/boundary/no_mrtr_test.exs "the only resultType written under lib/ is complete, at one site" "no line under lib/ reads inputResponses or requestState, and none names InputRequiredResult"; test/beam_mcp/mrtr_wire_test.exs "on the core, at both eras, the answer with the continuation parameters is the bare answer" "through the HTTP transport the answer with the continuation parameters is the bare answer"

What a census does not prove

Every entry above is proven by a test over this tree; none rests on reading alone. But a census reads text, and text has edges worth stating:

  • The censuses bar the acts by their written names (:crypto.sign, :public_key., :httpc., Mcp-Session-Id, and so on), whatever delimiter the name is written in (entry 7 allows one: backticks, prose). An act under a name the patterns do not list (in this package's own code or in a library) would not be seen. For key material, which has no name of its own, the patterns bar the ways it would arrive: an environment read, a _KEY constant, a decoder, a generator. The libraries this package can reach are the ones in mix.lock (the development tools included): jason and telemetry, the optional plug and bandit, and what they bring, which includes plug_crypto, a signing and key-derivation library; it is barred by name in entries 2 and 3. Nothing in this repository audits what a dependency does; the lock file is the list, and a reader who wants the guarantee reads it.
  • A name assembled at runtime would defeat every text census above. The acts that assemble one (apply, Module.concat, an evaluator, a loader, an atom made from a binary) are calls, and the artefact census sees every runtime call by its compiled target, so those are held whatever they are spelled; what runs at compile time is held by the text (the reader paragraph). What has no call in it (a barred word spelled as two strings joined, "mcp-" <> "session-id", a header name that is data and not code) is outside every census on this page and is not pinned; a reviewer reads for it, as for the compile-time reach above: those two are what a reviewer's eye holds here. The text reader drops a line beginning # as a comment unless it carries a #{: an interpolation inside a string is code and is read. Two things under lib/ are called without a written name by design and are the host's code: its catalog module (capabilities/0 at five sites, read_resource/1 at one and get_prompt/2 at one: three callees, and the only named calls through a runtime module) and the functions it hands in as options (the dispatch, the :authorize hooks).
  • A digest is not a signature until a key goes into it. Entry 2's census bars every keyed primitive by name, but :crypto.hash/2 over bytes that happen to contain a secret is a construction no census can tell from a hash; the one site is over canonical bytes, and a second would have to say what it hashes.
  • Entry 7's wire half is asserted over the HTTP transport, the only transport with headers, on requests the transport serves; a session carried under some other header name would be a session identifier by another name, which the source half sees only if the name says so. Over stdio there is no header to carry one; the source half covers both transports.
  • Entry 9's "sends no initialize" is a statement about this package's source. A host that wraps it and sends one is a client of something else, and out of this page's reach.

Related

  • The README's Deliberately out paragraph, which points here.
  • The connectome pages: the vocabulary defines the graph, its nodes, edges and the sign; the canonical page defines the bytes a signer would sign; the diff page defines the four classes, dead authority among them; the reach page states which questions the package answers and that all_paths is refused; the observed page states the tracer's threat model and the payload rule.