@brutalsystems/muster 0.5.0 → 0.6.0
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 +73 -9
- package/dist/codex-policy.js +39 -13
- package/dist/config.js +3 -0
- package/dist/format.js +4 -0
- package/dist/guard.js +9 -3
- package/dist/hosts/terminal.js +7 -6
- package/dist/hosts/tmux.js +12 -14
- package/dist/mcp-bridge.js +91 -0
- package/dist/mcp.js +207 -0
- package/dist/muster.js +17 -2
- package/dist/run.js +46 -9
- package/dist/task-worker.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -91,7 +91,10 @@ the resolved `terminal` (a launch event, not a live window-status check).
|
|
|
91
91
|
Closing the Terminal window leaves the session running; use `muster stop <id>`
|
|
92
92
|
to stop it. Terminal.app and iTerm2 may require macOS Automation permission.
|
|
93
93
|
Ghostty opens a separate app instance that exits when its last window closes;
|
|
94
|
-
|
|
94
|
+
that instance disables saved-window restoration so previous tabs and windows
|
|
95
|
+
are not duplicated. It uses the [Ghostty macOS launch interface](https://github.com/ghostty-org/ghostty/discussions/9221).
|
|
96
|
+
Terminal.app creates the requested window before activation
|
|
97
|
+
to avoid also opening a default shell window on startup.
|
|
95
98
|
iTerm2 uses its [documented scripting interface](https://iterm2.com/documentation-scripting.html).
|
|
96
99
|
If the launch request to the viewer fails,
|
|
97
100
|
Muster stops the launched session and returns an error.
|
|
@@ -99,9 +102,10 @@ Muster stops the launched session and returns an error.
|
|
|
99
102
|
Auto-selection tries tmux, then pty. Every peer includes `host`, `capabilities`
|
|
100
103
|
and `attach_hint`. Watchability is separate from the runtime's idle/busy state.
|
|
101
104
|
|
|
102
|
-
- **tmux:**
|
|
103
|
-
server.
|
|
104
|
-
|
|
105
|
+
- **tmux:** each agent has its own session on the dedicated `muster` tmux
|
|
106
|
+
server. Opening or exiting one agent does not switch another agent's viewer.
|
|
107
|
+
Watchable and attachable; survives the CLI or MCP server exiting.
|
|
108
|
+
Use the returned attach hint to reconnect to that agent.
|
|
105
109
|
- **pty:** not watchable or attachable. The CLI prints the peer record and stays
|
|
106
110
|
running to own the terminal. Ctrl-C stops it. MCP-owned pty sessions stop when
|
|
107
111
|
the MCP server disconnects. There is no persistent pty daemon.
|
|
@@ -124,6 +128,7 @@ max_concurrent = 4
|
|
|
124
128
|
permissions = "deny"
|
|
125
129
|
sandbox = "read-only"
|
|
126
130
|
allow_dangerous_flags = false
|
|
131
|
+
default_mcp = []
|
|
127
132
|
```
|
|
128
133
|
|
|
129
134
|
Choose `sandbox = "workspace-write"` yourself when agents should edit files.
|
|
@@ -174,8 +179,8 @@ The concurrency cap is shared by separate CLI/MCP processes, including pending
|
|
|
174
179
|
launches. Settings apply to both kinds; tasks are read-only by default.
|
|
175
180
|
|
|
176
181
|
Codex defaults to an explicit sandbox and never-ask approval policy. Muster enumerates
|
|
177
|
-
and explicitly disables inherited MCP servers,
|
|
178
|
-
|
|
182
|
+
and explicitly disables inherited MCP servers, enables only the selected
|
|
183
|
+
operator definitions, then verifies the effective server selection. Hooks, plugins, app connectors, automatic skill-MCP installation
|
|
179
184
|
and external notifications are disabled for the child. Unknown MCP names that
|
|
180
185
|
cannot be addressed safely are refused.
|
|
181
186
|
|
|
@@ -189,9 +194,10 @@ arrive after startup; this v1 is not an enterprise policy-enforcement layer.
|
|
|
189
194
|
Claude permissions and its command sandbox are different mechanisms; neither
|
|
190
195
|
claim implies that every external tool is OS-sandboxed.
|
|
191
196
|
|
|
192
|
-
These child restrictions
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
These child restrictions prevent automatically propagating spawn authority.
|
|
198
|
+
External tools are enabled only through the operator configuration and launch
|
|
199
|
+
selection described below. The child's runtime inbox remains reachable from an
|
|
200
|
+
external Tin Can even when it has no MCP tools of its own.
|
|
195
201
|
No user-level runtime configuration is rewritten, and workspace-trust dialogs
|
|
196
202
|
are never accepted automatically.
|
|
197
203
|
|
|
@@ -202,6 +208,64 @@ requester, runtime, kind and host. Registry and task outputs also live under
|
|
|
202
208
|
closed: verify no Muster operation is running before removing that directory.
|
|
203
209
|
There is no automatic time-based lock theft.
|
|
204
210
|
|
|
211
|
+
## Optional tools for launched agents
|
|
212
|
+
|
|
213
|
+
MCP servers are optional. Muster does not require or install Tin Can (or any
|
|
214
|
+
other MCP server). Register only servers you want launched agents to use in
|
|
215
|
+
`~/.muster/config.toml`. For example, to use an already-installed Tin Can:
|
|
216
|
+
|
|
217
|
+
```toml
|
|
218
|
+
# Optional personal defaults for sessions. Omit to keep launches MCP-free.
|
|
219
|
+
default_mcp = ["tincan"]
|
|
220
|
+
|
|
221
|
+
[mcp_servers.tincan]
|
|
222
|
+
command = "tincan"
|
|
223
|
+
tools = ["peers", "send_peer", "message_log"]
|
|
224
|
+
required = false
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Then either runtime can use it:
|
|
228
|
+
|
|
229
|
+
```sh
|
|
230
|
+
muster run codex --mcp tincan --prompt 'Coordinate with the other agent'
|
|
231
|
+
muster run claude --mcp tincan --prompt 'Coordinate with the other agent'
|
|
232
|
+
muster run codex --no-mcp --prompt 'Work without external tools'
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
`--mcp NAME` is repeatable and **replaces** personal defaults. `--no-mcp` clears
|
|
236
|
+
them and cannot be combined with `--mcp`. MCP `run` takes `mcp: ["tincan"]`;
|
|
237
|
+
`mcp: []` disables all servers. Omitted selection uses `default_mcp` for sessions.
|
|
238
|
+
Tasks have no defaults, but accept an explicit selection.
|
|
239
|
+
|
|
240
|
+
Definitions support either `command` plus optional `args`, `env`, and `env_vars`
|
|
241
|
+
for stdio, or `url` plus optional `bearer_token_env_var` for Streamable HTTP.
|
|
242
|
+
`tools` is a required nonempty list of exact tool names. Use `env_vars` and
|
|
243
|
+
`bearer_token_env_var` to reference credentials in the launch environment;
|
|
244
|
+
credentials are not written to launch logs. HTTP OAuth setup shared across
|
|
245
|
+
runtimes, plugin/app tools, MCP resources and prompts are outside this interface.
|
|
246
|
+
Server and tool names must contain only letters, digits, underscores or hyphens.
|
|
247
|
+
Relative stdio commands run from the requested working directory; prefer an
|
|
248
|
+
absolute executable path or a command on PATH.
|
|
249
|
+
|
|
250
|
+
Before agent launch, Muster initializes each selected server and checks its tool
|
|
251
|
+
catalog. No tool is called during preflight. An explicit `--mcp` selection is
|
|
252
|
+
required: a missing server, missing credential, or missing configured tool fails
|
|
253
|
+
that launch. Defaults are optional unless their definition sets `required = true`;
|
|
254
|
+
an unavailable optional default is skipped with `mcp_warnings`. Each preflight is
|
|
255
|
+
bounded by `startup_timeout_sec` (default 10, maximum 60) and the launch deadline.
|
|
256
|
+
JSON results and `list` report selected logical names in `mcp`; human output shows
|
|
257
|
+
`MCP:` and any warnings. These describe launch configuration, not continuous
|
|
258
|
+
server health.
|
|
259
|
+
|
|
260
|
+
Codex receives isolated definitions, using a fresh runtime name when an inherited
|
|
261
|
+
server has the same name, and its native tool allowlist. Claude receives a private
|
|
262
|
+
Muster stdio connection that filters both tool discovery and calls; newly added
|
|
263
|
+
upstream tools cannot bypass the selection. Only selected tools are preapproved.
|
|
264
|
+
Claude connection specifications are stored with owner-only permissions under
|
|
265
|
+
`~/.muster/mcp`, removed on stop, task completion, or when `list` observes an ended
|
|
266
|
+
session. MCP server access is separate from filesystem sandbox permissions: a
|
|
267
|
+
selected external tool may have its own write or network capabilities.
|
|
268
|
+
|
|
205
269
|
## MCP installation
|
|
206
270
|
|
|
207
271
|
Installed deliberately, in the one session that should hold spawn authority —
|
package/dist/codex-policy.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
+
import TOML from "@iarna/toml";
|
|
4
|
+
import { codexMcpConfig } from "./mcp.js";
|
|
3
5
|
const exec = promisify(execFile);
|
|
4
|
-
export async function codexPolicyArgs(env, cwd, deadline = Date.now() + 8000) {
|
|
6
|
+
export async function codexPolicyArgs(env, cwd, deadline = Date.now() + 8000, selected = []) {
|
|
5
7
|
const args = [
|
|
6
8
|
"--disable",
|
|
7
9
|
"hooks",
|
|
@@ -17,24 +19,48 @@ export async function codexPolicyArgs(env, cwd, deadline = Date.now() + 8000) {
|
|
|
17
19
|
"notify=[]",
|
|
18
20
|
];
|
|
19
21
|
async function list(extra) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
try {
|
|
23
|
+
const { stdout } = await exec("codex", [...extra, "mcp", "list", "--json"], {
|
|
24
|
+
env,
|
|
25
|
+
cwd,
|
|
26
|
+
timeout: Math.max(1, Math.min(4000, deadline - Date.now())),
|
|
27
|
+
maxBuffer: 1024 * 1024,
|
|
28
|
+
});
|
|
29
|
+
const value = JSON.parse(stdout);
|
|
30
|
+
if (!Array.isArray(value))
|
|
31
|
+
throw new Error("Cannot verify effective Codex MCP configuration");
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
28
35
|
throw new Error("Cannot verify effective Codex MCP configuration");
|
|
29
|
-
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
|
-
|
|
38
|
+
const inherited = await list(args);
|
|
39
|
+
const usedNames = new Set(inherited.map((s) => s.name));
|
|
40
|
+
for (const server of inherited) {
|
|
32
41
|
if (typeof server.name !== "string" ||
|
|
33
42
|
!/^[a-zA-Z0-9_-]+$/.test(server.name))
|
|
34
43
|
throw new Error("Cannot safely disable an inherited MCP server with a non-simple name");
|
|
35
44
|
args.push("-c", `mcp_servers.${server.name}.enabled=false`);
|
|
36
45
|
}
|
|
37
|
-
|
|
38
|
-
|
|
46
|
+
const expected = [];
|
|
47
|
+
for (const server of selected) {
|
|
48
|
+
let name = server.name;
|
|
49
|
+
let suffix = 0;
|
|
50
|
+
// Config layers merge nested maps. A fresh key prevents inherited env,
|
|
51
|
+
// credentials or per-tool policy from surviving the requested definition.
|
|
52
|
+
while (usedNames.has(name))
|
|
53
|
+
name = `muster_${server.name}_${++suffix}`;
|
|
54
|
+
usedNames.add(name);
|
|
55
|
+
expected.push(name);
|
|
56
|
+
args.push("-c", `mcp_servers.${name}=${TOML.stringify.value(codexMcpConfig(server))}`);
|
|
57
|
+
}
|
|
58
|
+
const enabled = (await list(args))
|
|
59
|
+
.filter((s) => s.enabled !== false)
|
|
60
|
+
.map((s) => s.name)
|
|
61
|
+
.sort();
|
|
62
|
+
expected.sort();
|
|
63
|
+
if (JSON.stringify(enabled) !== JSON.stringify(expected))
|
|
64
|
+
throw new Error("Inherited Codex MCP servers remain enabled or requested servers are missing; refusing launch");
|
|
39
65
|
return args;
|
|
40
66
|
}
|
package/dist/config.js
CHANGED
|
@@ -3,6 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import TOML from "@iarna/toml";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
+
import { mcpName, mcpServerSchema } from "./mcp.js";
|
|
6
7
|
export const configSchema = z
|
|
7
8
|
.object({
|
|
8
9
|
host: z.enum(["auto", "tmux", "pty", "macos-terminal"]).default("auto"),
|
|
@@ -17,6 +18,8 @@ export const configSchema = z
|
|
|
17
18
|
"danger-full-access",
|
|
18
19
|
])
|
|
19
20
|
.default("read-only"),
|
|
21
|
+
default_mcp: z.array(mcpName).default([]),
|
|
22
|
+
mcp_servers: z.record(mcpName, mcpServerSchema).default({}),
|
|
20
23
|
allow_dangerous_flags: z.boolean().default(false),
|
|
21
24
|
})
|
|
22
25
|
.strict();
|
package/dist/format.js
CHANGED
|
@@ -40,6 +40,10 @@ function recordHuman(record, color) {
|
|
|
40
40
|
lines.push(field("Permissions", clean(record.permissions)));
|
|
41
41
|
if (record.sandbox)
|
|
42
42
|
lines.push(field("Sandbox", clean(record.sandbox)));
|
|
43
|
+
if (record.mcp)
|
|
44
|
+
lines.push(field("MCP", record.mcp.length ? record.mcp.map(clean).join(", ") : "none"));
|
|
45
|
+
for (const warning of record.mcp_warnings ?? [])
|
|
46
|
+
lines.push(field("MCP warning", clean(warning)));
|
|
43
47
|
if (record.host)
|
|
44
48
|
lines.push(field("Host", `${clean(record.host)}${record.host === "pty" ? " (not watchable or attachable)" : ""}`));
|
|
45
49
|
if (record.terminal_opened)
|
package/dist/guard.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
|
+
import { mcpName, claudeMcpConfig } from "./mcp.js";
|
|
3
4
|
export const runSchema = z
|
|
4
5
|
.object({
|
|
5
6
|
runtime: z.enum(["codex", "claude"]),
|
|
@@ -17,6 +18,7 @@ export const runSchema = z
|
|
|
17
18
|
terminal: z.enum(["auto", "terminal", "iterm2", "ghostty"]).optional(),
|
|
18
19
|
permissions: z.enum(["auto", "deny", "bypass"]).optional(),
|
|
19
20
|
sandbox: z.enum(["read-only", "workspace-write", "full-access"]).optional(),
|
|
21
|
+
mcp: z.array(mcpName).optional(),
|
|
20
22
|
args: z.array(z.string()).default([]),
|
|
21
23
|
})
|
|
22
24
|
.strict();
|
|
@@ -53,7 +55,7 @@ function extras(req) {
|
|
|
53
55
|
}
|
|
54
56
|
return result;
|
|
55
57
|
}
|
|
56
|
-
export function launchArgs(req, config) {
|
|
58
|
+
export function launchArgs(req, config, mcp = [], bridgePath) {
|
|
57
59
|
const extra = extras(req);
|
|
58
60
|
const policy = resolvePermissions(req, config);
|
|
59
61
|
if (req.runtime === "codex")
|
|
@@ -89,7 +91,11 @@ export function launchArgs(req, config) {
|
|
|
89
91
|
disableAllHooks: true,
|
|
90
92
|
permissions: {
|
|
91
93
|
defaultMode: mode,
|
|
92
|
-
|
|
94
|
+
allow: mcp.flatMap((s) => s.config.tools.map((t) => `mcp__${s.name}__${t}`)),
|
|
95
|
+
deny: [
|
|
96
|
+
...(readonly ? ["Edit", "Write", "NotebookEdit"] : []),
|
|
97
|
+
...mcp.flatMap((s) => s.excludedTools.map((t) => `mcp__${s.name}__${t}`)),
|
|
98
|
+
],
|
|
93
99
|
additionalDirectories: [],
|
|
94
100
|
},
|
|
95
101
|
sandbox: {
|
|
@@ -112,7 +118,7 @@ export function launchArgs(req, config) {
|
|
|
112
118
|
"",
|
|
113
119
|
"--strict-mcp-config",
|
|
114
120
|
"--mcp-config",
|
|
115
|
-
|
|
121
|
+
JSON.stringify(claudeMcpConfig(mcp, bridgePath)),
|
|
116
122
|
"--settings",
|
|
117
123
|
JSON.stringify(settings),
|
|
118
124
|
...extra,
|
package/dist/hosts/terminal.js
CHANGED
|
@@ -56,22 +56,22 @@ export async function openTerminal(tmux, server, hostRef, deadline, terminal = "
|
|
|
56
56
|
"Ghostty",
|
|
57
57
|
"--args",
|
|
58
58
|
"--quit-after-last-window-closed=true",
|
|
59
|
+
// A separate instance otherwise restores the user's saved tabs/windows.
|
|
60
|
+
"--window-save-state=never",
|
|
59
61
|
"-e",
|
|
60
62
|
tmux,
|
|
61
63
|
"-L",
|
|
62
64
|
server,
|
|
63
65
|
"attach-session",
|
|
64
66
|
"-t",
|
|
65
|
-
"muster",
|
|
66
|
-
";",
|
|
67
|
-
"select-window",
|
|
68
|
-
"-t",
|
|
69
67
|
hostRef,
|
|
70
68
|
], timeout);
|
|
71
69
|
return;
|
|
72
70
|
}
|
|
73
71
|
const quoted = "'" + tmux.replaceAll("'", "'\\''") + "'";
|
|
74
|
-
const shellCommand = `${quoted} -L ${server} attach-session -t
|
|
72
|
+
const shellCommand = `${quoted} -L ${server} attach-session -t ${hostRef}`;
|
|
73
|
+
// Terminal must create the requested window before activation. Activating
|
|
74
|
+
// a cold app first also creates its default shell window.
|
|
75
75
|
await command("/usr/bin/osascript", [
|
|
76
76
|
"-e",
|
|
77
77
|
terminal === "iterm2"
|
|
@@ -83,8 +83,9 @@ export async function openTerminal(tmux, server, hostRef, deadline, terminal = "
|
|
|
83
83
|
end run`
|
|
84
84
|
: `on run argv
|
|
85
85
|
tell application "Terminal"
|
|
86
|
-
|
|
86
|
+
launch
|
|
87
87
|
do script (item 1 of argv)
|
|
88
|
+
activate
|
|
88
89
|
end tell
|
|
89
90
|
end run`,
|
|
90
91
|
shellCommand,
|
package/dist/hosts/tmux.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -52,13 +53,9 @@ export class TmuxHost {
|
|
|
52
53
|
file,
|
|
53
54
|
];
|
|
54
55
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
output = await this.call(["new-window", "-t", "muster", ...common], opts.deadline);
|
|
61
|
-
}
|
|
56
|
+
// Window selection is session-wide: sharing a session makes every
|
|
57
|
+
// attached terminal follow whichever agent was opened most recently.
|
|
58
|
+
const output = await this.call(["new-session", "-s", `muster-${randomUUID()}`, ...common], opts.deadline);
|
|
62
59
|
const [hostRef, pid] = output.trim().split("\t");
|
|
63
60
|
if (!hostRef || !/^@\d+$/.test(hostRef) || !Number(pid))
|
|
64
61
|
throw new Error("tmux returned an invalid window identity");
|
|
@@ -69,8 +66,7 @@ export class TmuxHost {
|
|
|
69
66
|
try {
|
|
70
67
|
const windows = await this.call([
|
|
71
68
|
"list-windows",
|
|
72
|
-
"-
|
|
73
|
-
"muster",
|
|
69
|
+
"-a",
|
|
74
70
|
"-F",
|
|
75
71
|
"#{window_id}\t#{pane_start_command}",
|
|
76
72
|
]);
|
|
@@ -87,14 +83,16 @@ export class TmuxHost {
|
|
|
87
83
|
try {
|
|
88
84
|
return (await this.call([
|
|
89
85
|
"list-windows",
|
|
90
|
-
"-
|
|
91
|
-
"muster",
|
|
86
|
+
"-a",
|
|
92
87
|
"-F",
|
|
93
|
-
"#{window_id}\t#{pane_pid}\t#{window_name}",
|
|
88
|
+
"#{window_id}\t#{pane_pid}\t#{window_name}\t#{session_name}",
|
|
94
89
|
]))
|
|
95
90
|
.trim()
|
|
96
91
|
.split("\n")
|
|
97
|
-
.filter(
|
|
92
|
+
.filter((l) => {
|
|
93
|
+
const session = l.split("\t")[3];
|
|
94
|
+
return (session === "muster" || /^muster-[0-9a-f-]{36}$/.test(session ?? ""));
|
|
95
|
+
})
|
|
98
96
|
.map((l) => {
|
|
99
97
|
const [hostRef, pid, label] = l.split("\t");
|
|
100
98
|
return { hostRef: hostRef, pid: Number(pid), label: label };
|
|
@@ -117,6 +115,6 @@ export class TmuxHost {
|
|
|
117
115
|
}
|
|
118
116
|
}
|
|
119
117
|
attachHint(hostRef) {
|
|
120
|
-
return `tmux -L ${this.server} attach-session -t
|
|
118
|
+
return `tmux -L ${this.server} attach-session -t ${hostRef}`;
|
|
121
119
|
}
|
|
122
120
|
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Claude has permission rules, but no MCP exposure allowlist. This stdio
|
|
2
|
+
// connection exposes and dispatches only the operator-selected tools, including
|
|
3
|
+
// when the upstream server changes its tool catalog after launch.
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
8
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
9
|
+
import { mcpServerSchema, mcpName, mcpTransport } from "./mcp.js";
|
|
10
|
+
async function main() {
|
|
11
|
+
const [path, name] = process.argv.slice(2);
|
|
12
|
+
if (!path || !name || !mcpName.safeParse(name).success)
|
|
13
|
+
throw new Error("Invalid MCP bridge arguments");
|
|
14
|
+
const definitions = JSON.parse(await readFile(path, "utf8"));
|
|
15
|
+
const config = mcpServerSchema.parse(definitions[name]);
|
|
16
|
+
const client = new Client({ name: "muster-tool-bridge", version: "1" }, { capabilities: {} });
|
|
17
|
+
const server = new Server({ name: `muster-${name}`, version: "1" }, { capabilities: { tools: {} } });
|
|
18
|
+
let shutdown;
|
|
19
|
+
const close = () => (shutdown ??= Promise.resolve().then(async () => {
|
|
20
|
+
await client.close().catch(() => { });
|
|
21
|
+
await server.close().catch(() => { });
|
|
22
|
+
}));
|
|
23
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"])
|
|
24
|
+
process.once(signal, () => {
|
|
25
|
+
void close().finally(() => process.exit(0));
|
|
26
|
+
});
|
|
27
|
+
process.stdin.once("end", () => {
|
|
28
|
+
void close();
|
|
29
|
+
});
|
|
30
|
+
process.stdin.once("close", () => {
|
|
31
|
+
void close();
|
|
32
|
+
});
|
|
33
|
+
server.onclose = () => {
|
|
34
|
+
void close();
|
|
35
|
+
};
|
|
36
|
+
client.onclose = () => {
|
|
37
|
+
void close();
|
|
38
|
+
};
|
|
39
|
+
const transport = mcpTransport(config, process.env, process.cwd());
|
|
40
|
+
const timer = setTimeout(() => {
|
|
41
|
+
void close().finally(() => process.exit(1));
|
|
42
|
+
}, config.startup_timeout_sec * 1000);
|
|
43
|
+
try {
|
|
44
|
+
await client.connect(transport);
|
|
45
|
+
if (shutdown) {
|
|
46
|
+
await close();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
await close();
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
}
|
|
57
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
58
|
+
const tools = [];
|
|
59
|
+
let cursor;
|
|
60
|
+
do {
|
|
61
|
+
const page = await client.listTools(cursor ? { cursor } : {});
|
|
62
|
+
tools.push(...page.tools.filter((t) => config.tools.includes(t.name)));
|
|
63
|
+
cursor = page.nextCursor;
|
|
64
|
+
} while (cursor);
|
|
65
|
+
return { tools };
|
|
66
|
+
});
|
|
67
|
+
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
|
|
68
|
+
if (!config.tools.includes(params.name))
|
|
69
|
+
return {
|
|
70
|
+
isError: true,
|
|
71
|
+
content: [
|
|
72
|
+
{ type: "text", text: "Tool not enabled for this Muster launch" },
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
return await client.callTool({
|
|
76
|
+
name: params.name,
|
|
77
|
+
arguments: params.arguments,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
await server.connect(new StdioServerTransport());
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
await close();
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
main().catch(() => {
|
|
89
|
+
process.stderr.write("Muster MCP connection failed; check the selected server configuration.\n");
|
|
90
|
+
process.exitCode = 1;
|
|
91
|
+
});
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
5
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
6
|
+
export const mcpName = z.string().regex(/^[a-zA-Z0-9_-]+$/);
|
|
7
|
+
const environmentName = z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/);
|
|
8
|
+
export const mcpServerSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
command: z.string().min(1).optional(),
|
|
11
|
+
args: z.array(z.string()).default([]),
|
|
12
|
+
url: z
|
|
13
|
+
.string()
|
|
14
|
+
.url()
|
|
15
|
+
.refine((s) => /^https?:\/\//.test(s), "MCP URL must use HTTP(S)")
|
|
16
|
+
.optional(),
|
|
17
|
+
env: z.record(z.string()).default({}),
|
|
18
|
+
env_vars: z.array(environmentName).default([]),
|
|
19
|
+
bearer_token_env_var: environmentName.optional(),
|
|
20
|
+
tools: z.array(mcpName).min(1),
|
|
21
|
+
required: z.boolean().default(false),
|
|
22
|
+
startup_timeout_sec: z.number().positive().max(60).default(10),
|
|
23
|
+
})
|
|
24
|
+
.strict()
|
|
25
|
+
.superRefine((v, ctx) => {
|
|
26
|
+
if (!!v.command === !!v.url)
|
|
27
|
+
ctx.addIssue({
|
|
28
|
+
code: "custom",
|
|
29
|
+
message: "MCP server requires exactly one of command or url",
|
|
30
|
+
});
|
|
31
|
+
if (v.command && v.bearer_token_env_var)
|
|
32
|
+
ctx.addIssue({
|
|
33
|
+
code: "custom",
|
|
34
|
+
message: "bearer_token_env_var requires an HTTP server",
|
|
35
|
+
});
|
|
36
|
+
if (v.url &&
|
|
37
|
+
(v.args.length || Object.keys(v.env).length || v.env_vars.length))
|
|
38
|
+
ctx.addIssue({
|
|
39
|
+
code: "custom",
|
|
40
|
+
message: "args, env and env_vars require a stdio server",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
export function selectMcp(req, config) {
|
|
44
|
+
// Personal defaults apply to sessions, not unattended one-shot tasks.
|
|
45
|
+
const names = req.mcp ?? (req.kind === "session" ? config.default_mcp : []);
|
|
46
|
+
return [...new Set(names)].map((name) => {
|
|
47
|
+
const server = Object.hasOwn(config.mcp_servers, name)
|
|
48
|
+
? config.mcp_servers[name]
|
|
49
|
+
: undefined;
|
|
50
|
+
if (!server)
|
|
51
|
+
throw new Error(`Unknown MCP server: ${name}`);
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
config: server,
|
|
55
|
+
required: req.mcp !== undefined || server.required,
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function mcpEnvironment(servers, base, source) {
|
|
60
|
+
const env = { ...base };
|
|
61
|
+
for (const { config } of servers) {
|
|
62
|
+
for (const key of [
|
|
63
|
+
...config.env_vars,
|
|
64
|
+
...(config.bearer_token_env_var ? [config.bearer_token_env_var] : []),
|
|
65
|
+
]) {
|
|
66
|
+
if (source[key] !== undefined)
|
|
67
|
+
env[key] = source[key];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return env;
|
|
71
|
+
}
|
|
72
|
+
function serverEnv(config, env) {
|
|
73
|
+
const result = { ...config.env };
|
|
74
|
+
for (const key of config.env_vars) {
|
|
75
|
+
if (env[key] === undefined)
|
|
76
|
+
throw new Error(`Missing MCP environment variable: ${key}`);
|
|
77
|
+
result[key] = env[key];
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
function headers(config, env) {
|
|
82
|
+
if (!config.bearer_token_env_var)
|
|
83
|
+
return {};
|
|
84
|
+
const value = env[config.bearer_token_env_var];
|
|
85
|
+
if (!value)
|
|
86
|
+
throw new Error("Missing MCP bearer token environment variable");
|
|
87
|
+
return { Authorization: `Bearer ${value}` };
|
|
88
|
+
}
|
|
89
|
+
export function mcpTransport(config, env, cwd) {
|
|
90
|
+
const transport = config.command
|
|
91
|
+
? new StdioClientTransport({
|
|
92
|
+
command: config.command,
|
|
93
|
+
args: config.args,
|
|
94
|
+
env: { ...env, ...serverEnv(config, env) },
|
|
95
|
+
cwd,
|
|
96
|
+
stderr: "pipe",
|
|
97
|
+
})
|
|
98
|
+
: new StreamableHTTPClientTransport(new URL(config.url), {
|
|
99
|
+
requestInit: { headers: headers(config, env) },
|
|
100
|
+
});
|
|
101
|
+
if (transport instanceof StdioClientTransport)
|
|
102
|
+
transport.stderr?.on("data", () => { });
|
|
103
|
+
return transport;
|
|
104
|
+
}
|
|
105
|
+
export async function prepareMcp(selected, env, cwd, deadline) {
|
|
106
|
+
const servers = [], warnings = [];
|
|
107
|
+
for (const server of selected) {
|
|
108
|
+
const client = new Client({ name: "muster-preflight", version: "1" }, { capabilities: {} });
|
|
109
|
+
let timer;
|
|
110
|
+
let diagnostic = "startup or tool discovery failed (check executable, credentials and server configuration)";
|
|
111
|
+
try {
|
|
112
|
+
const timeout = Math.min(server.config.startup_timeout_sec * 1000, deadline - Date.now());
|
|
113
|
+
if (timeout <= 0)
|
|
114
|
+
throw new Error("deadline");
|
|
115
|
+
const transport = mcpTransport(server.config, env, cwd);
|
|
116
|
+
const discover = async () => {
|
|
117
|
+
await client.connect(transport);
|
|
118
|
+
const names = new Set();
|
|
119
|
+
let cursor;
|
|
120
|
+
do {
|
|
121
|
+
const page = await client.listTools(cursor ? { cursor } : {});
|
|
122
|
+
for (const tool of page.tools) {
|
|
123
|
+
if (!mcpName.safeParse(tool.name).success)
|
|
124
|
+
throw new Error("Unsupported tool name");
|
|
125
|
+
names.add(tool.name);
|
|
126
|
+
}
|
|
127
|
+
cursor = page.nextCursor;
|
|
128
|
+
} while (cursor);
|
|
129
|
+
const missing = server.config.tools.filter((name) => !names.has(name));
|
|
130
|
+
if (missing.length) {
|
|
131
|
+
diagnostic = `configured tools not found: ${missing.join(", ")}`;
|
|
132
|
+
throw new Error("missing tools");
|
|
133
|
+
}
|
|
134
|
+
return [...names].filter((name) => !server.config.tools.includes(name));
|
|
135
|
+
};
|
|
136
|
+
const excludedTools = await Promise.race([
|
|
137
|
+
discover(),
|
|
138
|
+
new Promise((_, reject) => {
|
|
139
|
+
timer = setTimeout(() => reject(new Error("timeout")), timeout);
|
|
140
|
+
}),
|
|
141
|
+
]);
|
|
142
|
+
servers.push({ ...server, excludedTools });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// Server errors may include tokens or environment values; never log them.
|
|
146
|
+
const message = `MCP server ${server.name}: ${diagnostic}`;
|
|
147
|
+
if (server.required)
|
|
148
|
+
throw new Error(message);
|
|
149
|
+
warnings.push(`${message}; skipped optional default`);
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
if (timer)
|
|
153
|
+
clearTimeout(timer);
|
|
154
|
+
await client.close().catch(() => { });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
servers,
|
|
159
|
+
summary: {
|
|
160
|
+
mcp: servers.map((s) => s.name),
|
|
161
|
+
...(warnings.length ? { mcp_warnings: warnings } : {}),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function codexMcpConfig(server) {
|
|
166
|
+
const c = server.config;
|
|
167
|
+
return {
|
|
168
|
+
...(c.command
|
|
169
|
+
? { command: c.command, args: c.args, env: c.env, env_vars: c.env_vars }
|
|
170
|
+
: {
|
|
171
|
+
url: c.url,
|
|
172
|
+
...(c.bearer_token_env_var
|
|
173
|
+
? { bearer_token_env_var: c.bearer_token_env_var }
|
|
174
|
+
: {}),
|
|
175
|
+
}),
|
|
176
|
+
enabled: true,
|
|
177
|
+
required: server.required,
|
|
178
|
+
enabled_tools: c.tools,
|
|
179
|
+
disabled_tools: [],
|
|
180
|
+
startup_timeout_sec: c.startup_timeout_sec,
|
|
181
|
+
default_tools_approval_mode: "approve",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
export function claudeMcpConfig(servers, bridgePath) {
|
|
185
|
+
if (servers.length && !bridgePath)
|
|
186
|
+
throw new Error("Missing MCP bridge configuration");
|
|
187
|
+
return {
|
|
188
|
+
mcpServers: Object.fromEntries(servers.map(({ name, config }) => [
|
|
189
|
+
name,
|
|
190
|
+
{
|
|
191
|
+
type: "stdio",
|
|
192
|
+
command: process.execPath,
|
|
193
|
+
args: [
|
|
194
|
+
fileURLToPath(new URL("../dist/mcp-bridge.js", import.meta.url)),
|
|
195
|
+
bridgePath,
|
|
196
|
+
name,
|
|
197
|
+
],
|
|
198
|
+
env: Object.fromEntries([
|
|
199
|
+
...config.env_vars,
|
|
200
|
+
...(config.bearer_token_env_var
|
|
201
|
+
? [config.bearer_token_env_var]
|
|
202
|
+
: []),
|
|
203
|
+
].map((key) => [key, "${" + key + "}"])),
|
|
204
|
+
},
|
|
205
|
+
])),
|
|
206
|
+
};
|
|
207
|
+
}
|
package/dist/muster.js
CHANGED
|
@@ -23,6 +23,8 @@ run also accepts --format json|human, --permissions auto|deny|bypass,
|
|
|
23
23
|
and --sandbox read-only|workspace-write|full-access before -- RUNTIME_OPTIONS.
|
|
24
24
|
--open displays a tmux session on macOS. --terminal accepts auto (Terminal.app),
|
|
25
25
|
terminal, iterm2, or ghostty, and requires --open.
|
|
26
|
+
--mcp NAME is repeatable and replaces configured session defaults; --no-mcp
|
|
27
|
+
clears them. Tasks use no MCP defaults; select servers explicitly.
|
|
26
28
|
run, list and stop default to JSON. output prints raw task text.
|
|
27
29
|
|
|
28
30
|
pty launches print their record and keep this process running. Ctrl-C stops the
|
|
@@ -54,7 +56,7 @@ async function main() {
|
|
|
54
56
|
});
|
|
55
57
|
});
|
|
56
58
|
if (mcp) {
|
|
57
|
-
const server = new Server({ name: "muster", version: "0.
|
|
59
|
+
const server = new Server({ name: "muster", version: "0.6.0" }, { capabilities: { tools: {} } });
|
|
58
60
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
59
61
|
tools: [
|
|
60
62
|
{
|
|
@@ -94,6 +96,11 @@ async function main() {
|
|
|
94
96
|
enum: ["read-only", "workspace-write", "full-access"],
|
|
95
97
|
description: "Defaults to config (read-only). Full access requires config authorization.",
|
|
96
98
|
},
|
|
99
|
+
mcp: {
|
|
100
|
+
type: "array",
|
|
101
|
+
items: { type: "string", pattern: "^[a-zA-Z0-9_-]+$" },
|
|
102
|
+
description: "Configured MCP server names. Omit for session defaults; [] disables all. Tasks have no defaults.",
|
|
103
|
+
},
|
|
97
104
|
args: { type: "array", items: { type: "string" } },
|
|
98
105
|
},
|
|
99
106
|
required: ["runtime", "prompt"],
|
|
@@ -180,9 +187,17 @@ async function main() {
|
|
|
180
187
|
format: { type: "string" },
|
|
181
188
|
permissions: { type: "string" },
|
|
182
189
|
sandbox: { type: "string" },
|
|
190
|
+
mcp: { type: "string", multiple: true },
|
|
191
|
+
"no-mcp": { type: "boolean" },
|
|
183
192
|
},
|
|
184
193
|
});
|
|
185
|
-
const { format = "json", ...values } = parsed;
|
|
194
|
+
const { format = "json", "no-mcp": noMcp, ...values } = parsed;
|
|
195
|
+
if (noMcp !== undefined && command !== "run")
|
|
196
|
+
throw new Error("--no-mcp applies only to run");
|
|
197
|
+
if (noMcp && values.mcp)
|
|
198
|
+
throw new Error("--mcp and --no-mcp cannot be combined");
|
|
199
|
+
if (noMcp)
|
|
200
|
+
values.mcp = [];
|
|
186
201
|
if (format !== "json" && format !== "human")
|
|
187
202
|
throw new Error("format must be json or human");
|
|
188
203
|
if (command === "output" && parsed.format !== undefined)
|
package/dist/run.js
CHANGED
|
@@ -17,27 +17,30 @@ import { assertClaudePolicy } from "./claude-policy.js";
|
|
|
17
17
|
import { codexPolicyArgs } from "./codex-policy.js";
|
|
18
18
|
import { CodexRpc } from "./codex-rpc.js";
|
|
19
19
|
import { assignNames, resolvePeer } from "./naming.js";
|
|
20
|
+
import { selectMcp, prepareMcp, mcpEnvironment } from "./mcp.js";
|
|
20
21
|
export class Muster {
|
|
21
22
|
config;
|
|
22
23
|
home;
|
|
23
24
|
env;
|
|
24
25
|
drivers;
|
|
26
|
+
sourceEnv;
|
|
25
27
|
registry;
|
|
26
28
|
log;
|
|
27
29
|
owned = new Set();
|
|
28
30
|
closing = false;
|
|
29
31
|
active = new Set();
|
|
30
|
-
constructor(config, home, env, drivers) {
|
|
32
|
+
constructor(config, home, env, drivers, sourceEnv) {
|
|
31
33
|
this.config = config;
|
|
32
34
|
this.home = home;
|
|
33
35
|
this.env = env;
|
|
34
36
|
this.drivers = drivers;
|
|
37
|
+
this.sourceEnv = sourceEnv;
|
|
35
38
|
this.registry = new Registry(home);
|
|
36
39
|
this.log = new LaunchLog(home);
|
|
37
40
|
}
|
|
38
41
|
static async create(opts = {}) {
|
|
39
42
|
const env = launchEnv(opts.env ?? process.env), home = opts.home ?? join(env.HOME ?? homedir(), ".muster");
|
|
40
|
-
return new Muster(await loadConfig(home), home, env, opts.drivers ?? hosts());
|
|
43
|
+
return new Muster(await loadConfig(home), home, env, opts.drivers ?? hosts(), { ...(opts.env ?? process.env) });
|
|
41
44
|
}
|
|
42
45
|
run(input, requester = "cli") {
|
|
43
46
|
if (this.closing)
|
|
@@ -60,7 +63,9 @@ export class Muster {
|
|
|
60
63
|
(req.kind !== "session" || !["auto", "tmux"].includes(requestedHost)))
|
|
61
64
|
throw new Error("--open requires a tmux session on macOS");
|
|
62
65
|
const permissions = resolvePermissions(req, this.config);
|
|
63
|
-
const
|
|
66
|
+
const selectedMcp = selectMcp(req, this.config);
|
|
67
|
+
let argv = launchArgs(req, this.config);
|
|
68
|
+
const driver = req.kind === "session"
|
|
64
69
|
? await selectHost(req.open ? "tmux" : requestedHost, this.drivers)
|
|
65
70
|
: undefined;
|
|
66
71
|
if (req.open &&
|
|
@@ -87,20 +92,35 @@ export class Muster {
|
|
|
87
92
|
requester,
|
|
88
93
|
...(req.open ? { open: true, terminal } : {}),
|
|
89
94
|
args: req.args,
|
|
95
|
+
mcp: selectedMcp.map((s) => s.name),
|
|
90
96
|
...permissions,
|
|
91
97
|
});
|
|
98
|
+
const prepared = await prepareMcp(selectedMcp, mcpEnvironment(selectedMcp, this.env, this.sourceEnv), req.cwd, deadline);
|
|
99
|
+
const runtimeEnv = mcpEnvironment(prepared.servers, this.env, this.sourceEnv);
|
|
100
|
+
entry = await this.registry.update(entry.launchId, prepared.summary);
|
|
101
|
+
let bridgePath;
|
|
102
|
+
if (req.runtime === "claude" && prepared.servers.length) {
|
|
103
|
+
const dir = join(this.home, "mcp");
|
|
104
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
105
|
+
bridgePath = join(dir, entry.launchId + ".json");
|
|
106
|
+
entry = await this.registry.update(entry.launchId, {
|
|
107
|
+
mcpConfigPath: bridgePath,
|
|
108
|
+
});
|
|
109
|
+
await writeFile(bridgePath, JSON.stringify(Object.fromEntries(prepared.servers.map((s) => [s.name, s.config]))), { mode: 0o600 });
|
|
110
|
+
}
|
|
111
|
+
argv = launchArgs(req, this.config, prepared.servers, bridgePath);
|
|
92
112
|
if (req.runtime === "codex") {
|
|
93
|
-
policy = await codexPolicyArgs(
|
|
113
|
+
policy = await codexPolicyArgs(runtimeEnv, req.cwd, deadline, prepared.servers);
|
|
94
114
|
argv.splice(1, 0, ...policy);
|
|
95
115
|
}
|
|
96
116
|
if (this.closing)
|
|
97
117
|
throw new Error("Launch cancelled: Muster is closing");
|
|
98
118
|
if (req.kind === "task")
|
|
99
|
-
return await this.startTask(entry, argv);
|
|
119
|
+
return await this.startTask(entry, argv, runtimeEnv);
|
|
100
120
|
const launched = await driver.launch({
|
|
101
121
|
argv,
|
|
102
122
|
cwd: req.cwd,
|
|
103
|
-
env:
|
|
123
|
+
env: runtimeEnv,
|
|
104
124
|
label: req.prompt.slice(0, 48),
|
|
105
125
|
deadline,
|
|
106
126
|
});
|
|
@@ -115,7 +135,7 @@ export class Muster {
|
|
|
115
135
|
? "no Claude session registry found (check terminal for workspace trust or login prompts)"
|
|
116
136
|
: "no descendant runtime identity found";
|
|
117
137
|
if (req.runtime === "codex")
|
|
118
|
-
rpc = new CodexRpc(this.env, req.cwd
|
|
138
|
+
rpc = new CodexRpc(this.env, req.cwd);
|
|
119
139
|
while (Date.now() < deadline) {
|
|
120
140
|
if (deadline - Date.now() < 25)
|
|
121
141
|
break;
|
|
@@ -175,6 +195,7 @@ export class Muster {
|
|
|
175
195
|
const peer = {
|
|
176
196
|
kind: "session",
|
|
177
197
|
...permissions,
|
|
198
|
+
...prepared.summary,
|
|
178
199
|
name: named.display,
|
|
179
200
|
canonical_id: named.canonicalId,
|
|
180
201
|
runtime,
|
|
@@ -233,19 +254,20 @@ export class Muster {
|
|
|
233
254
|
await rpc?.close();
|
|
234
255
|
}
|
|
235
256
|
}
|
|
236
|
-
async startTask(entry, argv) {
|
|
257
|
+
async startTask(entry, argv, env) {
|
|
237
258
|
const dir = join(this.home, "tasks", entry.id);
|
|
238
259
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
239
260
|
const outputPath = join(dir, "output.txt"), exitPath = join(dir, "exit.json"), startPath = join(dir, "start"), specPath = join(dir, "launch.json");
|
|
240
261
|
await writeFile(specPath, JSON.stringify({
|
|
241
262
|
argv,
|
|
242
263
|
cwd: entry.cwd,
|
|
243
|
-
env
|
|
264
|
+
env,
|
|
244
265
|
outputPath,
|
|
245
266
|
exitPath,
|
|
246
267
|
startPath,
|
|
247
268
|
logHome: this.home,
|
|
248
269
|
launchId: entry.launchId,
|
|
270
|
+
mcpConfigPath: entry.mcpConfigPath,
|
|
249
271
|
}), { mode: 0o600 });
|
|
250
272
|
const child = spawn(process.execPath, [
|
|
251
273
|
fileURLToPath(new URL("../dist/task-worker.js", import.meta.url)),
|
|
@@ -273,6 +295,8 @@ export class Muster {
|
|
|
273
295
|
kind: "task",
|
|
274
296
|
permissions: entry.permissions,
|
|
275
297
|
sandbox: entry.sandbox,
|
|
298
|
+
mcp: entry.mcp ?? [],
|
|
299
|
+
...(entry.mcp_warnings ? { mcp_warnings: entry.mcp_warnings } : {}),
|
|
276
300
|
id: entry.id,
|
|
277
301
|
runtime: entry.runtime,
|
|
278
302
|
state: "running",
|
|
@@ -293,6 +317,15 @@ export class Muster {
|
|
|
293
317
|
}
|
|
294
318
|
}
|
|
295
319
|
async stopEntry(entry) {
|
|
320
|
+
try {
|
|
321
|
+
await this.stopEntryProcesses(entry);
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
if (entry.mcpConfigPath)
|
|
325
|
+
await rm(entry.mcpConfigPath, { force: true });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async stopEntryProcesses(entry) {
|
|
296
329
|
if (!entry.root)
|
|
297
330
|
return;
|
|
298
331
|
if (!(await isSame(entry.root))) {
|
|
@@ -396,6 +429,8 @@ export class Muster {
|
|
|
396
429
|
if (kind && kind !== original.kind)
|
|
397
430
|
continue;
|
|
398
431
|
const e = await this.refreshed(original);
|
|
432
|
+
if (e.mcpConfigPath && !["starting", "running"].includes(e.status))
|
|
433
|
+
await rm(e.mcpConfigPath, { force: true });
|
|
399
434
|
if (e.kind === "session")
|
|
400
435
|
result.push({
|
|
401
436
|
...e.peer,
|
|
@@ -417,6 +452,8 @@ export class Muster {
|
|
|
417
452
|
catch { }
|
|
418
453
|
result.push({
|
|
419
454
|
kind: "task",
|
|
455
|
+
...(e.mcp ? { mcp: e.mcp } : {}),
|
|
456
|
+
...(e.mcp_warnings ? { mcp_warnings: e.mcp_warnings } : {}),
|
|
420
457
|
...(e.permissions
|
|
421
458
|
? { permissions: e.permissions, sandbox: e.sandbox }
|
|
422
459
|
: {}),
|
package/dist/task-worker.js
CHANGED
|
@@ -33,6 +33,8 @@ async function finish(code, signal, error) {
|
|
|
33
33
|
finished = true;
|
|
34
34
|
try {
|
|
35
35
|
await output.close();
|
|
36
|
+
if (spec.mcpConfigPath)
|
|
37
|
+
await rm(spec.mcpConfigPath, { force: true });
|
|
36
38
|
await new LaunchLog(spec.logHome).write({
|
|
37
39
|
event: "task_exit",
|
|
38
40
|
launch_id: spec.launchId,
|