@bivy/bivy 0.7.0-staging.93 → 0.7.0-staging.95
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/dist/harness/mcp-config.js +89 -6
- package/dist/harness/mcp-inject.js +31 -8
- package/package.json +1 -1
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// Universal Agent Harness — MCP config rewriting.
|
|
4
4
|
//
|
|
5
5
|
// The one piece of MCP governance that is unavoidably per-agent is *where* the
|
|
6
|
-
// config lives —
|
|
7
|
-
// Windsurf, and most MCP hosts use
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// config lives — and, for a couple of hosts, the shape. Claude Code, Cursor,
|
|
7
|
+
// Windsurf, and most MCP hosts use `{ mcpServers: { name: { command, args, env
|
|
8
|
+
// } } }` (stdio) plus optional remote (url) servers. OpenCode is the JSON
|
|
9
|
+
// outlier: `{ mcp: { name: { type: "local", command: [bin, ...args],
|
|
10
|
+
// environment } } }` (see opencode.ai/config.json). This module rewrites both
|
|
11
|
+
// shapes so every stdio server launches through the Bivy MCP proxy instead of
|
|
12
|
+
// directly — turning each agent's own MCP config into the injection point.
|
|
12
13
|
//
|
|
13
14
|
// Pure functions, no I/O — unit-tested in test/harness-mcp-config.test.ts. The
|
|
14
15
|
// file-location table for each agent is data (see agentMcpConfigTargets) that
|
|
@@ -109,6 +110,88 @@ export function withBivyToolsServer(config, spec, name = "bivy") {
|
|
|
109
110
|
return { config, added: false };
|
|
110
111
|
return { config: { ...config, mcpServers: { ...servers, [name]: spec } }, added: true };
|
|
111
112
|
}
|
|
113
|
+
/** Convert a universal stdio server spec into OpenCode's local-server shape. */
|
|
114
|
+
export function toOpenCodeLocalServer(spec) {
|
|
115
|
+
const command = [spec.command ?? "", ...(spec.args ?? [])].filter((s, i) => i === 0 || s !== undefined);
|
|
116
|
+
// Drop a leading empty command if somehow absent — callers always pass one.
|
|
117
|
+
const argv = command[0] ? command : command.slice(1);
|
|
118
|
+
const out = { type: "local", command: argv };
|
|
119
|
+
if (spec.env && Object.keys(spec.env).length)
|
|
120
|
+
out.environment = { ...spec.env };
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/** True when an OpenCode local server already launches through the Bivy proxy. */
|
|
124
|
+
export function isOpenCodeProxied(spec, launcher) {
|
|
125
|
+
if (!Array.isArray(spec.command) || spec.command.length === 0)
|
|
126
|
+
return false;
|
|
127
|
+
if (spec.command[0] !== launcher.command)
|
|
128
|
+
return false;
|
|
129
|
+
return spec.command.includes(PROXY_MARKER);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Rewrite every local stdio server in `config.mcp` to launch through the proxy:
|
|
133
|
+
*
|
|
134
|
+
* original: { type: "local", command: ["mcp-fs", "--root", "/w"], environment: {...} }
|
|
135
|
+
* rewritten: { type: "local",
|
|
136
|
+
* command: ["bivy", "mcp-proxy", "--bivy-mcp", "--server", "<name>", "--",
|
|
137
|
+
* "mcp-fs", "--root", "/w"],
|
|
138
|
+
* environment: {...} }
|
|
139
|
+
*
|
|
140
|
+
* Remote servers and already-proxied locals are left untouched (reported in
|
|
141
|
+
* `skipped`). Idempotent. Does not mutate the input.
|
|
142
|
+
*/
|
|
143
|
+
export function routeOpenCodeThroughProxy(config, launcher) {
|
|
144
|
+
const rewritten = [];
|
|
145
|
+
const skipped = [];
|
|
146
|
+
const servers = config.mcp ?? {};
|
|
147
|
+
const nextServers = {};
|
|
148
|
+
for (const [name, spec] of Object.entries(servers)) {
|
|
149
|
+
if (!spec || typeof spec !== "object") {
|
|
150
|
+
nextServers[name] = spec;
|
|
151
|
+
skipped.push(name);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!Array.isArray(spec.command) || spec.command.length === 0 || typeof spec.command[0] !== "string" || !spec.command[0]) {
|
|
155
|
+
// Remote/url server, enabled-only stub, or malformed — can't wrap via stdio proxy.
|
|
156
|
+
nextServers[name] = spec;
|
|
157
|
+
skipped.push(name);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (isOpenCodeProxied(spec, launcher)) {
|
|
161
|
+
nextServers[name] = spec;
|
|
162
|
+
skipped.push(name);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const prefix = launcher.argsPrefix ?? [];
|
|
166
|
+
const orig = spec.command;
|
|
167
|
+
nextServers[name] = {
|
|
168
|
+
...spec,
|
|
169
|
+
type: "local",
|
|
170
|
+
command: [launcher.command, ...prefix, PROXY_MARKER, "--server", name, "--", ...orig],
|
|
171
|
+
};
|
|
172
|
+
rewritten.push(name);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
config: { ...config, mcp: nextServers },
|
|
176
|
+
rewritten,
|
|
177
|
+
skipped,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Insert the Bivy tools server under OpenCode's `mcp.<name>` (default "bivy").
|
|
182
|
+
* Idempotent: an existing entry of that name is left untouched.
|
|
183
|
+
*/
|
|
184
|
+
export function withOpenCodeBivyToolsServer(config, spec, name = "bivy") {
|
|
185
|
+
const servers = config.mcp ?? {};
|
|
186
|
+
if (servers[name])
|
|
187
|
+
return { config, added: false };
|
|
188
|
+
return { config: { ...config, mcp: { ...servers, [name]: spec } }, added: true };
|
|
189
|
+
}
|
|
190
|
+
/** Basename check for OpenCode project config files we inject into. */
|
|
191
|
+
export function isOpenCodeConfigFile(filePath) {
|
|
192
|
+
const base = nodePath.basename(filePath).toLowerCase();
|
|
193
|
+
return base === "opencode.json" || base === ".opencode.json" || base === "opencode.jsonc" || base === ".opencode.jsonc";
|
|
194
|
+
}
|
|
112
195
|
/** JSON MCP-config file candidates for an agent, most-specific (safest) first. */
|
|
113
196
|
export function agentMcpConfigTargets(agentId, ctx) {
|
|
114
197
|
const ws = (...parts) => nodePath.join(ctx.workspace, ...parts);
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
// session-scoped (workspace-local files preferred) so a failure or a concurrent
|
|
10
10
|
// session can't corrupt config: we snapshot the exact bytes and restore them.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// JSON configs (Claude, Gemini, generic .mcp.json) use the universal
|
|
13
|
+
// `mcpServers` shape; OpenCode's project `opencode.json` uses its own `mcp`
|
|
14
|
+
// shape (see routeOpenCodeThroughProxy). TOML/YAML (Codex, Goose) go through
|
|
15
|
+
// the format-specific writers. Unit-tested in test/harness-mcp-inject.test.ts.
|
|
15
16
|
import fs from "node:fs";
|
|
16
17
|
import path from "node:path";
|
|
17
|
-
import { agentMcpConfigTargets, bivyToolsServerSpec, routeThroughProxy, withBivyToolsServer, } from "./mcp-config.js";
|
|
18
|
+
import { agentMcpConfigTargets, bivyToolsServerSpec, isOpenCodeConfigFile, routeOpenCodeThroughProxy, routeThroughProxy, toOpenCodeLocalServer, withBivyToolsServer, withOpenCodeBivyToolsServer, } from "./mcp-config.js";
|
|
18
19
|
import { injectTomlMcp, injectYamlMcp, insertTomlServer } from "./mcp-config-formats.js";
|
|
19
20
|
/** The proxy launcher Bivy injects — `bivy mcp-proxy …`. */
|
|
20
21
|
export function bivyProxyLauncher(bivyCommand = "bivy") {
|
|
@@ -23,7 +24,9 @@ export function bivyProxyLauncher(bivyCommand = "bivy") {
|
|
|
23
24
|
/**
|
|
24
25
|
* Inject the proxy into a single JSON config file. Returns a restore thunk
|
|
25
26
|
* (a no-op if the file was absent, unreadable, non-JSON, or had no stdio
|
|
26
|
-
* servers to route).
|
|
27
|
+
* servers to route). OpenCode project configs (`opencode.json`) use the
|
|
28
|
+
* OpenCode `mcp` shape; everything else uses the universal `mcpServers` shape.
|
|
29
|
+
* Never throws.
|
|
27
30
|
*/
|
|
28
31
|
export function injectJsonMcpConfig(filePath, launcher) {
|
|
29
32
|
let original;
|
|
@@ -40,7 +43,9 @@ export function injectJsonMcpConfig(filePath, launcher) {
|
|
|
40
43
|
catch {
|
|
41
44
|
return { injected: false, restore: () => { } };
|
|
42
45
|
}
|
|
43
|
-
const result =
|
|
46
|
+
const result = isOpenCodeConfigFile(filePath)
|
|
47
|
+
? routeOpenCodeThroughProxy(parsed, launcher)
|
|
48
|
+
: routeThroughProxy(parsed, launcher);
|
|
44
49
|
if (result.rewritten.length === 0)
|
|
45
50
|
return { injected: false, restore: () => { } };
|
|
46
51
|
// Preserve the file's indentation feel by re-serializing with 2 spaces; the
|
|
@@ -116,7 +121,9 @@ export function injectMcpConfigFile(filePath, launcher) {
|
|
|
116
121
|
* servers a file already has), this CREATES the config when absent so an agent
|
|
117
122
|
* that ships no MCP config still gets the tool. Handles the most-specific JSON
|
|
118
123
|
* config (session-local for claude/gemini/opencode/generic) and Codex's TOML
|
|
119
|
-
* (`~/.codex/config.toml` — Codex has no project-local option).
|
|
124
|
+
* (`~/.codex/config.toml` — Codex has no project-local option). OpenCode gets
|
|
125
|
+
* its native `{ mcp: { bivy: { type: "local", command: [...] } } }` shape — the
|
|
126
|
+
* universal `mcpServers` key is rejected by OpenCode's schema. restore() deletes
|
|
120
127
|
* a file it created and rewrites the exact original bytes of one it modified.
|
|
121
128
|
* Idempotent (a `bivy` server already present is a no-op, so concurrent sessions
|
|
122
129
|
* sharing a global config don't double up). Best-effort; never throws. Goose YAML
|
|
@@ -131,6 +138,7 @@ export function injectBivyToolsForSession(agentId, ctx, bivyCommand = "bivy") {
|
|
|
131
138
|
return { injected: [], restore: () => { } };
|
|
132
139
|
const spec = bivyToolsServerSpec({ sessionId: ctx.sessionId, endpoint: ctx.endpoint, bivyCommand });
|
|
133
140
|
const ext = path.extname(target).toLowerCase();
|
|
141
|
+
const openCode = agentId === "opencode" || isOpenCodeConfigFile(target);
|
|
134
142
|
const existed = fs.existsSync(target);
|
|
135
143
|
let original;
|
|
136
144
|
if (existed) {
|
|
@@ -142,7 +150,22 @@ export function injectBivyToolsForSession(agentId, ctx, bivyCommand = "bivy") {
|
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
let nextContent;
|
|
145
|
-
if (ext === ".json") {
|
|
153
|
+
if (ext === ".json" && openCode) {
|
|
154
|
+
let parsed = {};
|
|
155
|
+
if (original !== undefined) {
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(original);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return { injected: [], restore: () => { } };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const { config, added } = withOpenCodeBivyToolsServer(parsed, toOpenCodeLocalServer(spec));
|
|
164
|
+
if (!added)
|
|
165
|
+
return { injected: [], restore: () => { } };
|
|
166
|
+
nextContent = `${JSON.stringify(config, null, 2)}\n`;
|
|
167
|
+
}
|
|
168
|
+
else if (ext === ".json") {
|
|
146
169
|
let parsed = {};
|
|
147
170
|
if (original !== undefined) {
|
|
148
171
|
try {
|
package/package.json
CHANGED