@adaptivemcp/mcp-binary 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # @adaptivemcp/mcp-binary
2
+
3
+ The **only sanctioned shell-out layer** in Adaptive MCP (see `docs/doubts.md` §12b / D10).
4
+
5
+ It wraps a CLI binary into an MCP stdio server, mapping the binary's CLI onto
6
+ MCP tools. Binaries like **rtk** (which has no native MCP server) become
7
+ chainable MCP servers this way, then attach to `@adaptivemcp/middleware` like
8
+ any other integration — instead of living at the host layer or shelling out
9
+ inside core.
10
+
11
+ ```ts
12
+ import { createRtkWrapper } from "@adaptivemcp/mcp-binary";
13
+
14
+ const rtk = createRtkWrapper(); // resolves the user's existing rtk install
15
+ if (rtk.available) {
16
+ await rtk.start(); // stdio MCP server exposing `rtk_exec`
17
+ }
18
+ ```
19
+
20
+ ## What "MCP chaining" means here
21
+
22
+ Adaptive MCP does **not** chain binaries. It chains **MCP servers**.
23
+
24
+ - Each MCP server (yours, headroom's, rtk's wrapper) is a first-class node in a
25
+ `MiddlewareChain`.
26
+ - A middleware runs `beforeCall` / `afterCall` around a tool invocation. The
27
+ `afterCall` of one middleware sees the output produced by the previous one.
28
+ - The binary wrapper is just *one* node: it turns a CLI subprocess into an MCP
29
+ tool so it can participate in the chain. The chain itself is protocol-level,
30
+ not process-level.
31
+
32
+ So when you add rtk to the chain, you are not injecting rtk into every other
33
+ tool. You are adding a middleware that, for the tools you route through it, runs
34
+ a command via rtk and compresses the result. Other MCP tools are untouched
35
+ unless you explicitly route them through the rtk middleware.
36
+
37
+ ## When / how rtk and headroom run
38
+
39
+ | | headroom | rtk (via this wrapper) |
40
+ | --- | --- | --- |
41
+ | Native MCP server? | Yes | No — wrapped here |
42
+ | Runs as | MCP server in the chain | CLI subprocess spawned by the wrapper |
43
+ | Trigger | `afterCall` compresses a tool's string output | `rtk_exec` tool runs a command through `rtk gain` |
44
+ | Effect on other tools | Only tools routed through the headroom middleware | Only tools routed through the rtk middleware |
45
+ | Telemetry | `headroom` view in `tools-metadata.yaml` | `rtk_exec` calls recorded like any MCP call |
46
+
47
+ **headroom** is a normal MCP server. Its `HeadroomMiddleware.afterCall` runs
48
+ after a tool returns and compresses the string output (D9: passthrough on
49
+ failure). It never spawns a process.
50
+
51
+ **rtk** has no MCP server, so `createRtkWrapper()` wraps the `rtk` CLI into one.
52
+ The wrapper exposes a single `rtk_exec` tool. When a tool in the chain calls
53
+ `rtk_exec`, the wrapper spawns `rtk gain <command>`, captures stdout, and
54
+ returns it. That is the *only* place in Adaptive MCP that spawns a process.
55
+
56
+ ## Layering: this wrapper is NOT the rtk hook
57
+
58
+ rtk's own value comes from its **PreToolUse hook** (installed by `rtk init -g`),
59
+ which rewrites an agent's Bash calls (`git status` → `rtk git status`) *before*
60
+ they execute. That hook operates at the **agent's shell layer**.
61
+
62
+ This MCP wrapper is a **different layer**: it runs rtk as a normal CLI
63
+ subprocess *inside the Adaptive MCP middleware chain*, compressing the output of
64
+ a tool call that already returned.
65
+
66
+ ```
67
+ Agent Bash layer: rtk hook rewrites `git status` → `rtk git status` (rtk's own feature)
68
+ │
69
+ ▼
70
+ MCP tool-output layer: tool returns → rtk_exec middleware → `rtk gain` → compressed result
71
+ ```
72
+
73
+ The two **compose** rather than conflict: the hook compresses what the agent
74
+ sends to the shell; this wrapper compresses what a tool's MCP result carries
75
+ back. Adaptive MCP does not install, enable, or disable the rtk hook — it only
76
+ reuses the rtk binary if it is already present.
77
+
78
+ ## Graceful coexistence when rtk is already installed
79
+
80
+ If you already use rtk, Adaptive MCP is **not a nuisance** to your other MCPs:
81
+
82
+ 1. **We reuse your install.** `resolveRtkCommand()` looks up your existing `rtk`
83
+ on `PATH` (or an explicit `command` path). We never download or install a
84
+ second copy, and we never touch your shell config or hook.
85
+ 2. **We verify it's the right package.** A name collision exists: the Rust
86
+ *Type Kit* crate on crates.io is also called `rtk`. `resolveRtkCommand()`
87
+ probes `rtk --version` and only accepts a binary that prints `rtk <semver>`
88
+ (rtk-ai). If the `rtk` on `PATH` is the wrong package, the wrapper reports
89
+ `wrong-package` and degrades gracefully instead of silently misbehaving.
90
+ 3. **We degrade, not crash.** If rtk is missing (or the wrong package), the
91
+ wrapper still constructs and registers its tools, but each `rtk_exec` call
92
+ returns a clear setup message (`isError: true`) instead of spawning a missing
93
+ binary. Check `server.available` to know the state up front.
94
+ 4. **No double-compression surprise.** Because the hook and the wrapper are on
95
+ different layers, routing a tool through `rtk_exec` does not "double up" with
96
+ the hook — the hook affects what the agent runs in the shell; the wrapper
97
+ affects what a tool's MCP result carries back. They are independent knobs.
98
+
99
+ ```ts
100
+ import { createRtkWrapper, resolveRtkCommand } from "@adaptivemcp/mcp-binary";
101
+
102
+ const resolution = resolveRtkCommand(); // { status: "found" | "missing" | "wrong-package" }
103
+ const rtk = createRtkWrapper(); // uses the resolved command automatically
104
+
105
+ if (!rtk.available) {
106
+ // resolution.detail explains why (missing, or wrong `rtk` package on PATH)
107
+ console.warn(resolution.detail);
108
+ }
109
+ ```
110
+
111
+ ## Telemetry integration
112
+
113
+ Every MCP call — including `rtk_exec` and headroom-compressed results — flows
114
+ through the same pipeline as any other tool:
115
+
116
+ ```
117
+ tool call → TelemetryRecorder → MemoryStore (SQLite)
118
+ → Evaluator (insights) → ExtensionController
119
+ → tools-metadata.yaml (dev.adaptivemcp/tools-metadata)
120
+ ```
121
+
122
+ `AdaptiveRuntime.observeCompleted(output)` accepts the (possibly compressed)
123
+ `output` and folds it into `ToolStats`. `MiddlewareChain.contributeView()`
124
+ surfaces per-middleware state under the `middleware:` map in
125
+ `tools-metadata.yaml` — e.g. `middleware: { headroom: { hash, savingsPercent } }`.
126
+ rtk's *own* opt-in telemetry (GDPR-gated, off by default) is separate and
127
+ untouched; Adaptive MCP only records the MCP-level calls it makes.
128
+
129
+ ## API
130
+
131
+ - `createRtkWrapper(options?: RtkWrapperOptions): BinaryMcpServer` — wraps the
132
+ rtk CLI. `options.command` overrides discovery; `options.timeoutMs` bounds
133
+ each spawn; `options.version` sets the advertised server version.
134
+ - `resolveRtkCommand(command?: string): RtkResolution` — locates and verifies a
135
+ usable rtk-ai binary. Returns `{ status, command?, detail? }`.
136
+ - `BinaryMcpServer` — generic CLI→MCP wrapper. `available` getter, `name` /
137
+ `version` getters, `toolNames()`, `start()`, `close()`.
138
+ - `RTK_TOOLS` — the reference `BinaryToolSpec[]` (`rtk_exec` → `rtk gain`).
139
+
140
+ > **Secure by default.** Discovery is read-only: it probes `rtk --version` and
141
+ > never writes to the filesystem, mutates `PATH`, or installs software. If you
142
+ > need rtk, install it yourself from <https://rtk-ai.app> (Homebrew, the official
143
+ > checksum-hardened installer, or `cargo install`).
@@ -0,0 +1,16 @@
1
+ /**
2
+ * @adaptivemcp/mcp-binary
3
+ *
4
+ * The **only** sanctioned shell-out layer in Adaptive MCP (doubts.md §12b / D10).
5
+ *
6
+ * It wraps a CLI binary into an MCP stdio server, mapping the binary's CLI onto
7
+ * MCP tools. Binaries like **rtk** (which has no native MCP server) become
8
+ * chainable MCP servers this way, then attach to `@adaptivemcp/middleware` like
9
+ * any other integration — instead of living at the host layer or shelling out
10
+ * inside core. `RtkWrapper` is the reference binary config.
11
+ */
12
+ export { BinaryMcpServer } from "./server.js";
13
+ export type { BinaryMcpServerOptions, BinaryToolSpec } from "./server.js";
14
+ export { createRtkWrapper, RTK_TOOLS, resolveRtkCommand } from "./rtk.js";
15
+ export type { RtkWrapperOptions, RtkResolution } from "./rtk.js";
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,YAAY,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAC1E,YAAY,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @adaptivemcp/mcp-binary
3
+ *
4
+ * The **only** sanctioned shell-out layer in Adaptive MCP (doubts.md §12b / D10).
5
+ *
6
+ * It wraps a CLI binary into an MCP stdio server, mapping the binary's CLI onto
7
+ * MCP tools. Binaries like **rtk** (which has no native MCP server) become
8
+ * chainable MCP servers this way, then attach to `@adaptivemcp/middleware` like
9
+ * any other integration — instead of living at the host layer or shelling out
10
+ * inside core. `RtkWrapper` is the reference binary config.
11
+ */
12
+ export { BinaryMcpServer } from "./server.js";
13
+ export { createRtkWrapper, RTK_TOOLS, resolveRtkCommand } from "./rtk.js";
14
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC"}
package/dist/rtk.d.ts ADDED
@@ -0,0 +1,78 @@
1
+ import { BinaryMcpServer, type BinaryToolSpec } from "./server.js";
2
+ /**
3
+ * Reference binary config for **rtk** (rtk-ai.app).
4
+ *
5
+ * rtk has no native MCP server (doubts.md §12b, issue #1442 is "gauging
6
+ * interest" only), so it is wrapped here into an MCP server. The wrapper exposes
7
+ * a single `rtk_exec` tool that runs an arbitrary shell command through rtk's
8
+ * output compression. Once rtk ships a native MCP server, this wrapper becomes
9
+ * redundant and rtk can be chained directly like headroom.
10
+ *
11
+ * **Layering — this is NOT the rtk hook.** rtk's own value comes from its
12
+ * PreToolUse hook (installed by `rtk init -g`), which rewrites an agent's Bash
13
+ * calls (`git status` → `rtk git status`) *before* they execute. That hook
14
+ * operates at the agent's shell layer. This MCP wrapper is a *different* layer:
15
+ * it runs rtk as a normal CLI subprocess inside the Adaptive MCP middleware
16
+ * chain, compressing the output of a tool call that already returned. The two
17
+ * compose — the hook compresses what the agent sends to the shell, this wrapper
18
+ * compresses what a tool's MCP result carries back. See `docs/doubts.md` §12b.
19
+ */
20
+ export declare const RTK_TOOLS: BinaryToolSpec[];
21
+ export interface RtkWrapperOptions {
22
+ /**
23
+ * Path or name of the rtk binary. Defaults to `"rtk"`.
24
+ *
25
+ * If omitted, `resolveRtkCommand()` is used to locate the user's existing
26
+ * install (graceful coexistence — we prefer the binary the user already has
27
+ * and configured over bundling or re-installing one).
28
+ */
29
+ command?: string;
30
+ version?: string;
31
+ timeoutMs?: number;
32
+ }
33
+ /**
34
+ * The result of locating the rtk binary on the host.
35
+ *
36
+ * `status` tells the caller how to behave:
37
+ * - `"found"`: a working rtk-ai binary is available — wrap it directly.
38
+ * - `"missing"`: no rtk on PATH — the caller should degrade gracefully
39
+ * (e.g. skip the rtk middleware, or surface a setup hint) rather than
40
+ * spawn a binary that does not exist.
41
+ * - `"wrong-package"`: a binary named `rtk` exists but it is the unrelated
42
+ * Rust *Type Kit* crate from crates.io (its `rtk gain` fails). The caller
43
+ * must NOT treat this as rtk-ai; it should warn and skip.
44
+ */
45
+ export interface RtkResolution {
46
+ status: "found" | "missing" | "wrong-package";
47
+ /** The command/path to use, when `status === "found"`. */
48
+ command?: string;
49
+ /** Human-readable detail for logs / `contributeView`. */
50
+ detail?: string;
51
+ }
52
+ /**
53
+ * Locate a usable rtk-ai binary on the host.
54
+ *
55
+ * 1. If `command` is an absolute/relative path, verify it exists and that
56
+ * `rtk --version` reports an `rtk <semver>` line (the rtk-ai binary),
57
+ * not the Rust Type Kit crate.
58
+ * 2. Otherwise search `PATH` for `rtk`, then probe it the same way.
59
+ *
60
+ * This is the sanctioned, read-only discovery step. It never downloads or
61
+ * installs anything — Adaptive MCP must not mutate the host's toolchain. If the
62
+ * user already has rtk (from Homebrew, the official installer, or cargo), we
63
+ * reuse it; if not, we report `missing` and let the caller degrade.
64
+ */
65
+ export declare function resolveRtkCommand(command?: string): RtkResolution;
66
+ /**
67
+ * Build an `BinaryMcpServer` that wraps the rtk CLI. This is the sanctioned
68
+ * shell-out layer (D4/D10): rtk becomes a chainable MCP server instead of
69
+ * living at the host layer or shelling out inside core.
70
+ *
71
+ * Discovery is graceful: when `command` is omitted we resolve the user's
72
+ * existing rtk install via `resolveRtkCommand()`. If none is found (or the
73
+ * `rtk` name collides with another package), the returned server is marked
74
+ * `unavailable` and its `rtk_exec` tool will report a clear setup message
75
+ * instead of spawning a missing binary. Call `server.available` to check.
76
+ */
77
+ export declare function createRtkWrapper(options?: RtkWrapperOptions): BinaryMcpServer;
78
+ //# sourceMappingURL=rtk.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rtk.d.ts","sourceRoot":"","sources":["../src/rtk.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAA+B,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAEhG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,SAAS,EAAE,cAAc,EAWrC,CAAC;AAEF,MAAM,WAAW,iBAAiB;IAChC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,eAAe,CAAC;IAC9C,0DAA0D;IAC1D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,aAAa,CAoCjE;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,eAAe,CAWjF"}
package/dist/rtk.js ADDED
@@ -0,0 +1,107 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { z } from "zod";
3
+ import { BinaryMcpServer } from "./server.js";
4
+ /**
5
+ * Reference binary config for **rtk** (rtk-ai.app).
6
+ *
7
+ * rtk has no native MCP server (doubts.md §12b, issue #1442 is "gauging
8
+ * interest" only), so it is wrapped here into an MCP server. The wrapper exposes
9
+ * a single `rtk_exec` tool that runs an arbitrary shell command through rtk's
10
+ * output compression. Once rtk ships a native MCP server, this wrapper becomes
11
+ * redundant and rtk can be chained directly like headroom.
12
+ *
13
+ * **Layering — this is NOT the rtk hook.** rtk's own value comes from its
14
+ * PreToolUse hook (installed by `rtk init -g`), which rewrites an agent's Bash
15
+ * calls (`git status` → `rtk git status`) *before* they execute. That hook
16
+ * operates at the agent's shell layer. This MCP wrapper is a *different* layer:
17
+ * it runs rtk as a normal CLI subprocess inside the Adaptive MCP middleware
18
+ * chain, compressing the output of a tool call that already returned. The two
19
+ * compose — the hook compresses what the agent sends to the shell, this wrapper
20
+ * compresses what a tool's MCP result carries back. See `docs/doubts.md` §12b.
21
+ */
22
+ export const RTK_TOOLS = [
23
+ {
24
+ name: "rtk_exec",
25
+ description: "Run a shell command through rtk's CLI-output compression. Returns the compressed command output.",
26
+ subcommand: "gain",
27
+ inputSchema: {
28
+ command: z.string().describe("The shell command to run and compress."),
29
+ },
30
+ argStyle: "positionals",
31
+ },
32
+ ];
33
+ /**
34
+ * Locate a usable rtk-ai binary on the host.
35
+ *
36
+ * 1. If `command` is an absolute/relative path, verify it exists and that
37
+ * `rtk --version` reports an `rtk <semver>` line (the rtk-ai binary),
38
+ * not the Rust Type Kit crate.
39
+ * 2. Otherwise search `PATH` for `rtk`, then probe it the same way.
40
+ *
41
+ * This is the sanctioned, read-only discovery step. It never downloads or
42
+ * installs anything — Adaptive MCP must not mutate the host's toolchain. If the
43
+ * user already has rtk (from Homebrew, the official installer, or cargo), we
44
+ * reuse it; if not, we report `missing` and let the caller degrade.
45
+ */
46
+ export function resolveRtkCommand(command) {
47
+ const candidates = command ? [command] : ["rtk"];
48
+ for (const candidate of candidates) {
49
+ // Probe: rtk-ai prints "rtk <version>"; the Rust Type Kit crate does not.
50
+ // `execFileSync` resolves PATH names itself, so a bare "rtk" works.
51
+ try {
52
+ const out = execFileSync(candidate, ["--version"], {
53
+ encoding: "utf8",
54
+ timeout: 5000,
55
+ windowsHide: true,
56
+ });
57
+ if (/^rtk\s+\d+\.\d+/.test(out.trim())) {
58
+ return { status: "found", command: candidate, detail: out.trim() };
59
+ }
60
+ // A binary named `rtk` exists but is not rtk-ai (name collision).
61
+ return {
62
+ status: "wrong-package",
63
+ detail: `Found a binary named 'rtk' at ${candidate}, but it is not rtk-ai (its --version is "${out.trim()}"). This is likely the unrelated Rust 'Type Kit' crate. Install rtk-ai from https://rtk-ai.app instead.`,
64
+ };
65
+ }
66
+ catch {
67
+ // Probe failed: not on PATH, not executable, or the wrong package that
68
+ // errors on --version. If an explicit path was given, report it as a
69
+ // collision; otherwise keep looking (PATH name collision).
70
+ if (command) {
71
+ return {
72
+ status: "wrong-package",
73
+ detail: `The binary at '${command}' did not respond to 'rtk --version' like rtk-ai. Verify it is the rtk-ai binary (https://rtk-ai.app).`,
74
+ };
75
+ }
76
+ continue;
77
+ }
78
+ }
79
+ return {
80
+ status: "missing",
81
+ detail: "rtk-ai not found on PATH. Install it from https://rtk-ai.app (or set RtkWrapperOptions.command to an explicit path). The rtk middleware will be skipped.",
82
+ };
83
+ }
84
+ /**
85
+ * Build an `BinaryMcpServer` that wraps the rtk CLI. This is the sanctioned
86
+ * shell-out layer (D4/D10): rtk becomes a chainable MCP server instead of
87
+ * living at the host layer or shelling out inside core.
88
+ *
89
+ * Discovery is graceful: when `command` is omitted we resolve the user's
90
+ * existing rtk install via `resolveRtkCommand()`. If none is found (or the
91
+ * `rtk` name collides with another package), the returned server is marked
92
+ * `unavailable` and its `rtk_exec` tool will report a clear setup message
93
+ * instead of spawning a missing binary. Call `server.available` to check.
94
+ */
95
+ export function createRtkWrapper(options = {}) {
96
+ const resolution = resolveRtkCommand(options.command);
97
+ const opts = {
98
+ name: "adaptive-rtk-wrapper",
99
+ version: options.version ?? "0.1.0",
100
+ command: resolution.command ?? "rtk",
101
+ tools: RTK_TOOLS,
102
+ timeoutMs: options.timeoutMs,
103
+ unavailable: resolution.status !== "found" ? resolution.detail : undefined,
104
+ };
105
+ return new BinaryMcpServer(opts);
106
+ }
107
+ //# sourceMappingURL=rtk.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rtk.js","sourceRoot":"","sources":["../src/rtk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,eAAe,EAAoD,MAAM,aAAa,CAAC;AAEhG;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAqB;IACzC;QACE,IAAI,EAAE,UAAU;QAChB,WAAW,EACT,kGAAkG;QACpG,UAAU,EAAE,MAAM;QAClB,WAAW,EAAE;YACX,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,wCAAwC,CAAC;SACvE;QACD,QAAQ,EAAE,aAAa;KACxB;CACF,CAAC;AAmCF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAgB;IAChD,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IACjD,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE,CAAC;QACnC,0EAA0E;QAC1E,oEAAoE;QACpE,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,YAAY,CAAC,SAAS,EAAE,CAAC,WAAW,CAAC,EAAE;gBACjD,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,IAAI;gBACb,WAAW,EAAE,IAAI;aAClB,CAAC,CAAC;YACH,IAAI,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;gBACvC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC;YACrE,CAAC;YACD,kEAAkE;YAClE,OAAO;gBACL,MAAM,EAAE,eAAe;gBACvB,MAAM,EAAE,iCAAiC,SAAS,6CAA6C,GAAG,CAAC,IAAI,EAAE,yGAAyG;aACnN,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,uEAAuE;YACvE,qEAAqE;YACrE,2DAA2D;YAC3D,IAAI,OAAO,EAAE,CAAC;gBACZ,OAAO;oBACL,MAAM,EAAE,eAAe;oBACvB,MAAM,EAAE,kBAAkB,OAAO,wGAAwG;iBAC1I,CAAC;YACJ,CAAC;YACD,SAAS;QACX,CAAC;IACH,CAAC;IACD,OAAO;QACL,MAAM,EAAE,SAAS;QACjB,MAAM,EAAE,0JAA0J;KACnK,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAA6B,EAAE;IAC9D,MAAM,UAAU,GAAG,iBAAiB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACtD,MAAM,IAAI,GAA2B;QACnC,IAAI,EAAE,sBAAsB;QAC5B,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,OAAO;QACnC,OAAO,EAAE,UAAU,CAAC,OAAO,IAAI,KAAK;QACpC,KAAK,EAAE,SAAS;QAChB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,WAAW,EAAE,UAAU,CAAC,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;KAC3E,CAAC;IACF,OAAO,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,75 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * A single CLI subcommand exposed as an MCP tool.
4
+ *
5
+ * The wrapper shells out to `<command> <subcommand> [args...]` and returns the
6
+ * combined stdout/stderr as the tool result. This is the **only** place in
7
+ * Adaptive MCP permitted to spawn processes (doubts.md §12b / D10) — it is the
8
+ * sanctioned shell-out layer that lets binaries like rtk become chainable MCP
9
+ * servers instead of living at the host layer or shelling out inside core.
10
+ */
11
+ export interface BinaryToolSpec {
12
+ /** MCP tool name, e.g. `rtk_exec`. */
13
+ name: string;
14
+ /** Human description shown in `tools/list`. */
15
+ description: string;
16
+ /** The subcommand passed to the binary (e.g. `gain`, `compress`). */
17
+ subcommand: string;
18
+ /**
19
+ * Zod-validated input schema for the tool. The parsed args are forwarded to
20
+ * the binary as CLI flags/positionals. Use `argStyle` to control formatting.
21
+ */
22
+ inputSchema: z.ZodRawShape;
23
+ /** How to render parsed args on the command line. Defaults to `--key value`. */
24
+ argStyle?: "flags" | "positionals";
25
+ }
26
+ export interface BinaryMcpServerOptions {
27
+ /** Server identity advertised in `initialize`. */
28
+ name: string;
29
+ version?: string;
30
+ /** The binary to invoke, e.g. `"rtk"` or `"/usr/local/bin/rtk"`. */
31
+ command: string;
32
+ /** Base args always prepended (e.g. `["--json"]`). */
33
+ baseArgs?: string[];
34
+ /** Tools to expose. */
35
+ tools: BinaryToolSpec[];
36
+ /** Shell out timeout in ms (per invocation). Defaults to 30s. */
37
+ timeoutMs?: number;
38
+ /**
39
+ * When set, the binary could not be located/verified (e.g. rtk not installed,
40
+ * or the `rtk` name collides with another package). The server still registers
41
+ * its tools, but each invocation returns this message instead of spawning a
42
+ * missing binary. Lets the chain degrade gracefully rather than erroring.
43
+ */
44
+ unavailable?: string;
45
+ }
46
+ /**
47
+ * Wraps a CLI binary into an MCP stdio server. Each `BinaryToolSpec` becomes an
48
+ * MCP tool that runs `<command> <subcommand> <args>` and returns the output.
49
+ *
50
+ * This is intentionally generic (D10): rtk is the reference binary, but any
51
+ * CLI tool whose output benefits from in-loop transformation can be wrapped the
52
+ * same way and then chained through `@adaptivemcp/middleware`.
53
+ */
54
+ export declare class BinaryMcpServer {
55
+ private server;
56
+ private opts;
57
+ constructor(options: BinaryMcpServerOptions);
58
+ private registerTool;
59
+ /** Whether the wrapped binary was located and verified at construction. */
60
+ get available(): boolean;
61
+ /** Server identity advertised in `initialize`. */
62
+ get name(): string;
63
+ /** Server version advertised in `initialize`. */
64
+ get version(): string;
65
+ /** Names of the tools registered on this server. */
66
+ toolNames(): string[];
67
+ /** Spawn the binary, write `input` (if any) to stdin, resolve with stdout. */
68
+ private run;
69
+ private renderArgs;
70
+ /** Attach to stdio and start listening. */
71
+ start(): Promise<void>;
72
+ /** Stop the server. */
73
+ close(): Promise<void>;
74
+ }
75
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC7B,sCAAsC;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,qEAAqE;IACrE,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,EAAE,OAAO,GAAG,aAAa,CAAC;CACpC;AAED,MAAM,WAAW,sBAAsB;IACrC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAC;IAChB,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,uBAAuB;IACvB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB,iEAAiE;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,IAAI,CAAyB;gBAEzB,OAAO,EAAE,sBAAsB;IAW3C,OAAO,CAAC,YAAY;IAuBpB,2EAA2E;IAC3E,IAAI,SAAS,IAAI,OAAO,CAEvB;IAED,kDAAkD;IAClD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,iDAAiD;IACjD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,oDAAoD;IACpD,SAAS,IAAI,MAAM,EAAE;IAIrB,8EAA8E;IAC9E,OAAO,CAAC,GAAG;IA+BX,OAAO,CAAC,UAAU;IAYlB,2CAA2C;IACrC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAK5B,uBAAuB;IACjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAG7B"}
package/dist/server.js ADDED
@@ -0,0 +1,110 @@
1
+ import { spawn } from "node:child_process";
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ /**
5
+ * Wraps a CLI binary into an MCP stdio server. Each `BinaryToolSpec` becomes an
6
+ * MCP tool that runs `<command> <subcommand> <args>` and returns the output.
7
+ *
8
+ * This is intentionally generic (D10): rtk is the reference binary, but any
9
+ * CLI tool whose output benefits from in-loop transformation can be wrapped the
10
+ * same way and then chained through `@adaptivemcp/middleware`.
11
+ */
12
+ export class BinaryMcpServer {
13
+ server;
14
+ opts;
15
+ constructor(options) {
16
+ this.opts = options;
17
+ this.server = new McpServer({ name: options.name, version: options.version ?? "0.1.0" }, { capabilities: {} });
18
+ for (const tool of options.tools) {
19
+ this.registerTool(tool);
20
+ }
21
+ }
22
+ registerTool(tool) {
23
+ this.server.registerTool(tool.name, {
24
+ title: tool.name,
25
+ description: tool.description,
26
+ inputSchema: tool.inputSchema,
27
+ }, async (args) => {
28
+ if (this.opts.unavailable) {
29
+ return {
30
+ content: [{ type: "text", text: this.opts.unavailable }],
31
+ isError: true,
32
+ };
33
+ }
34
+ const out = await this.run(tool.subcommand, args, tool.argStyle ?? "flags");
35
+ return {
36
+ content: [{ type: "text", text: out }],
37
+ };
38
+ });
39
+ }
40
+ /** Whether the wrapped binary was located and verified at construction. */
41
+ get available() {
42
+ return !this.opts.unavailable;
43
+ }
44
+ /** Server identity advertised in `initialize`. */
45
+ get name() {
46
+ return this.opts.name;
47
+ }
48
+ /** Server version advertised in `initialize`. */
49
+ get version() {
50
+ return this.opts.version ?? "0.1.0";
51
+ }
52
+ /** Names of the tools registered on this server. */
53
+ toolNames() {
54
+ return this.opts.tools.map((t) => t.name);
55
+ }
56
+ /** Spawn the binary, write `input` (if any) to stdin, resolve with stdout. */
57
+ run(subcommand, args, style) {
58
+ const cliArgs = [
59
+ ...(this.opts.baseArgs ?? []),
60
+ subcommand,
61
+ ...this.renderArgs(args, style),
62
+ ];
63
+ return new Promise((resolve, reject) => {
64
+ const child = spawn(this.opts.command, cliArgs, {
65
+ stdio: ["pipe", "pipe", "pipe"],
66
+ });
67
+ let stdout = "";
68
+ let stderr = "";
69
+ const timer = setTimeout(() => {
70
+ child.kill("SIGKILL");
71
+ reject(new Error(`binary ${this.opts.command} timed out after ${this.opts.timeoutMs ?? 30000}ms`));
72
+ }, this.opts.timeoutMs ?? 30000);
73
+ child.stdout.on("data", (d) => (stdout += d.toString()));
74
+ child.stderr.on("data", (d) => (stderr += d.toString()));
75
+ child.on("error", (err) => {
76
+ clearTimeout(timer);
77
+ reject(err);
78
+ });
79
+ child.on("close", (code) => {
80
+ clearTimeout(timer);
81
+ if (code === 0)
82
+ resolve(stdout.trim());
83
+ else
84
+ reject(new Error(`${this.opts.command} exited ${code}: ${stderr.trim() || stdout.trim()}`));
85
+ });
86
+ });
87
+ }
88
+ renderArgs(args, style) {
89
+ if (style === "positionals") {
90
+ return Object.values(args).map(String);
91
+ }
92
+ const out = [];
93
+ for (const [key, value] of Object.entries(args)) {
94
+ if (value === undefined || value === null)
95
+ continue;
96
+ out.push(`--${key}`, String(value));
97
+ }
98
+ return out;
99
+ }
100
+ /** Attach to stdio and start listening. */
101
+ async start() {
102
+ const transport = new StdioServerTransport();
103
+ await this.server.connect(transport);
104
+ }
105
+ /** Stop the server. */
106
+ async close() {
107
+ await this.server.close();
108
+ }
109
+ }
110
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@adaptivemcp/mcp-binary",
3
+ "version": "0.1.1",
4
+ "description": "Generic CLI-binary -> MCP-server stdio wrapper (the only sanctioned shell-out layer).",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "test": "vitest run --config ../../vitest.config.ts"
22
+ },
23
+ "dependencies": {
24
+ "@adaptivemcp/spec": "workspace:*",
25
+ "@modelcontextprotocol/sdk": "^1.30.1",
26
+ "zod": "^4.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "typescript": "^5.6.0",
30
+ "vitest": "^2.1.0"
31
+ }
32
+ }