Repository navigation
Expand file tree
/
Copy pathmix.exs
More file actions
171 lines (154 loc) · 7.63 KB
/
Copy pathmix.exs
File metadata and controls
171 lines (154 loc) · 7.63 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# SPDX-FileCopyrightText: 2026 Sudo Apt Holdings LLC
# SPDX-License-Identifier: Apache-2.0
defmodule BeamMCP.MixProject do
use Mix.Project
@version "0.10.1"
@source_url "https://github.com/ScriptKittyOS/beam_mcp"
# The oldest OTP this project supports. Mix has an `elixir:` key but none for OTP, so the
# floor is enforced here, at compile time: a below-floor build fails rather than shipping and
# failing in a way that looks like a defect in this package. The reason travels with the
# number (see `check_otp!/1` and the README), because a floor without a reason gets raised by
# the next person who finds it inconvenient.
@otp_floor 27
@doc "The oldest OTP release this project supports."
def otp_floor, do: @otp_floor
@doc """
Raises unless `release` -- the string `:erlang.system_info(:otp_release)` returns, or that
form -- is at or above `otp_floor/0`. The message names the floor, the version found, and
why the floor is where it is. Pure and public so a test can exercise the raise on any OTP.
"""
def check_otp!(release) do
found = release |> to_string() |> String.trim() |> Integer.parse()
case found do
{major, _rest} when major >= @otp_floor ->
:ok
{major, _rest} ->
Mix.raise("""
beam_mcp requires Erlang/OTP #{@otp_floor} or newer; found OTP #{major}.
OTP 27.0 added the `trace` module -- isolated trace sessions
(`:trace.session_create/3`) -- and the connectome tracer runs inside one of its own,
so that a process a host already traces is traced too and a host's own patterns and
flags are never touched; so OTP 27 is the hard requirement. It is also the
oldest release this project supports: the lowest leg the CI matrix runs the suite
on, so that support is a measurement and not a hope. The suite runs on OTP 27, 28 and 29 in CI
(the floor, the pinned line and the newest pair the compatibility table lists) and on
28 on the maintainers' machines. Releases older than #{@otp_floor} are neither tested
nor supported.
Install OTP #{@otp_floor} or newer (the maintainers' pins are in the repository's
.tool-versions), or pin an older beam_mcp.
""")
:error ->
Mix.raise("beam_mcp could not read the OTP release from #{inspect(release)}")
end
end
def project do
_ = check_otp!(:erlang.system_info(:otp_release))
[
app: :beam_mcp,
version: @version,
# Fixtures under test/support compile only for the test environment: they are real
# modules the connectome builder reads, never a mock, and they never ship.
elixirc_paths: elixirc_paths(Mix.env()),
# OTP 27 (the floor above) needs Elixir 1.17 or newer -- Elixir 1.15/1.16 support OTP 24-26
# only -- so the two stated minimums are coherent: the oldest supported pair is Elixir 1.17
# on OTP 27. The README states the same pair, pinned equal by the census test.
elixir: "~> 1.17",
elixirc_options: [warnings_as_errors: true],
start_permanent: Mix.env() == :prod,
deps: deps(),
description: description(),
package: package(),
name: "BeamMCP",
source_url: @source_url,
docs: [
main: "readme",
extras: [
"README.md",
"CHANGELOG.md",
"docs/connectome.md",
"docs/connectome-canonical.md",
"docs/connectome-observed.md",
"docs/connectome-diff.md",
"docs/connectome-reach.md",
"docs/will-not-implement.md",
"docs/threat-model.md",
"docs/crypto-posture.md",
"docs/fips.md",
"docs/provenance.md",
"docs/api-stability.md",
"docs/architecture.md",
"docs/assurance-case.md",
"docs/roadmap.md",
"docs/security-review.md",
"UPGRADING.md",
"docs/governance.md",
"docs/succession.md",
"SECURITY.md"
],
# Grouped by namespace, not by a list of names: a module added under either prefix
# lands in its group without an edit here. The core modules are the ungrouped rest.
groups_for_modules: [
Transports: ~r/^BeamMCP\.Transport\./,
Connectome: ~r/^BeamMCP\.Connectome\./
],
groups_for_extras: [
Connectome: ~r{^docs/connectome},
Policy:
~r{^(docs/(will-not-implement|threat-model|crypto-posture|fips|provenance|api-stability|architecture|assurance-case|roadmap|security-review|governance|succession)|SECURITY|UPGRADING)}
]
]
]
end
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]
def application do
# OTP's `tools` application carries `:xref`, which the declared-connectome builder reads
# call edges with. Optional, in the one spelling Mix honours -- `tools: :optional` inside
# extra_applications. A separate `optional_applications:` key is accepted by Mix and
# ignored, and the first version of this line shipped tools as REQUIRED in the .app;
# a test reads the .app the build writes. `crypto` is the digest's application, required:
# through 0.5.0 it was absent here and an HTTP host had it only through plug and bandit,
# both optional -- a stdio-only release would have had no :crypto.hash/2 at all.
[extra_applications: [:logger, :crypto, tools: :optional]]
end
defp deps do
[
{:jason, "~> 1.4"},
# The dispatch path emits `[:beam_mcp, :dispatch, :start | :stop | :exception]` through
# this, the ecosystem's emission point. Required, not optional: an optional emission would
# branch the hot path on whether a module is loaded, and that second path is one nobody
# exercises. Apache-2.0; no dependencies of its own.
{:telemetry, "~> 1.0"},
# Plug is the transport contract; Bandit is the server the host runs it on. Both are
# optional: a host using only the stdio transport must not be made to pull an HTTP
# server in, and `optional: true` keeps them out of that host's dependency tree.
{:plug, "~> 1.16", optional: true},
# 1.13 is a security floor, not a feature one: below it Bandit carries EEF-CVE-2026-105659
# (HTTP/2 send deadlines reset by every WINDOW_UPDATE) and EEF-CVE-2026-105660 (an HTTP/1
# request target choosing conn.host and conn.port). A host that pulls Bandit in beside this
# transport resolves to a fixed one; a host that does not is unaffected (optional).
{:bandit, "~> 1.13", optional: true},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.31", only: :dev, runtime: false},
# Property tests. Test-time only; it does not enter the published package.
{:stream_data, "~> 1.4", only: [:dev, :test], runtime: false}
]
end
defp description do
"Model Context Protocol server core for the BEAM: protocol handling, stdio and stateless " <>
"Streamable HTTP transports, and JSON Schema validation, with the tool catalog and " <>
"dispatch injected by the host."
end
defp package do
[
licenses: ["Apache-2.0"],
links: %{"GitHub" => @source_url},
# Globs, not directories: Hex walks a directory in readdir order (`File.ls!`, unsorted --
# ext4's per-filesystem hash order, tmpfs's another), so a directory here gave one tarball on
# this machine and a different one on tmpfs with the same 39 files (a review lane measured
# it); `Path.wildcard` sorts, so a glob gives one entry order everywhere. The set is the same.
files:
~w(lib/**/*.ex docs/*.md docs/public-api.txt mix.exs README.md CHANGELOG.md UPGRADING.md SECURITY.md LICENSE NOTICE LICENSES/*)
]
end
end