machine-bridge-mcp 0.1.0 → 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 +12 -2
- package/package.json +7 -1
- package/scripts/sync-worker-version.mjs +41 -0
- package/src/local/cli.mjs +42 -12
- package/src/local/self-test.mjs +14 -1
- package/src/local/service.mjs +5 -2
- package/src/local/state.mjs +89 -2
- package/src/worker/index.ts +22 -4
package/README.md
CHANGED
|
@@ -38,6 +38,14 @@ MCP connection password: mcp_password_...
|
|
|
38
38
|
|
|
39
39
|
Keep the foreground process running for the current session. The installed autostart entry keeps the daemon available after future logins.
|
|
40
40
|
|
|
41
|
+
The command is safe to run repeatedly:
|
|
42
|
+
|
|
43
|
+
```zsh
|
|
44
|
+
npm install -g machine-bridge-mcp@latest && machine-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
On repeat runs, the CLI reuses existing state and secrets unless you request rotation, skips Worker redeploys when the deployed Worker is healthy and unchanged, refreshes the autostart entry, stops any currently loaded autostart daemon before starting the foreground daemon, and refuses to start a second daemon for the same workspace if another foreground instance is already running.
|
|
48
|
+
|
|
41
49
|
## Re-select workspace
|
|
42
50
|
|
|
43
51
|
```zsh
|
|
@@ -80,7 +88,7 @@ machine-mcp service uninstall
|
|
|
80
88
|
machine-mcp --no-autostart
|
|
81
89
|
```
|
|
82
90
|
|
|
83
|
-
Autostart runs the daemon with `--daemon-only --no-print-credentials`, so service logs do not contain the MCP connection password. If you start with `--no-write`, `--no-exec`, or `--full-env`, those policy flags are preserved in the autostart entry.
|
|
91
|
+
Autostart runs the daemon with `--daemon-only --no-print-credentials`, so service logs do not contain the MCP connection password. If you start with `--no-write`, `--no-exec`, or `--full-env`, those policy flags are preserved in the autostart entry. macOS/Linux service definitions restart only on process failure; a normal duplicate-instance exit is not treated as a crash loop.
|
|
84
92
|
|
|
85
93
|
## Secrets rotation
|
|
86
94
|
|
|
@@ -157,7 +165,9 @@ Default state roots:
|
|
|
157
165
|
- Linux with `XDG_STATE_HOME`: `$XDG_STATE_HOME/machine-bridge-mcp`
|
|
158
166
|
- Windows: `%APPDATA%\machine-bridge-mcp`
|
|
159
167
|
|
|
160
|
-
State contains the MCP password and daemon secret. Status/doctor output redacts secrets. The normal foreground `start` command prints the MCP password because users need to paste it into their MCP client.
|
|
168
|
+
State contains the MCP password and daemon secret. Status/doctor output redacts secrets. The normal foreground `start` command prints the MCP password because users need to paste it into their MCP client. State files, temporary Worker secret files, lock files, and log directories are created under the user state root with owner-only permissions where the platform supports POSIX modes.
|
|
169
|
+
|
|
170
|
+
The Worker rejects browser requests with an `Origin` header unless the origin is the Worker itself or a loopback HTTP origin. To allow additional browser-based MCP clients, set `MBM_ALLOWED_ORIGINS` to a comma-separated list of exact origins in `wrangler.jsonc` or Cloudflare Worker settings.
|
|
161
171
|
|
|
162
172
|
Override state root:
|
|
163
173
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "One-command hosted Remote MCP bridge to a local machine daemon.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"bin",
|
|
19
19
|
"src/local",
|
|
20
20
|
"src/worker/index.ts",
|
|
21
|
+
"scripts",
|
|
21
22
|
"wrangler.jsonc",
|
|
22
23
|
"tsconfig.json",
|
|
23
24
|
"README.md",
|
|
@@ -27,6 +28,11 @@
|
|
|
27
28
|
"start": "node bin/machine-mcp.mjs start",
|
|
28
29
|
"doctor": "node bin/machine-mcp.mjs doctor",
|
|
29
30
|
"self-test": "node src/local/self-test.mjs",
|
|
31
|
+
"version:sync": "node scripts/sync-worker-version.mjs",
|
|
32
|
+
"version:check": "node scripts/sync-worker-version.mjs --check",
|
|
33
|
+
"version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts",
|
|
34
|
+
"prepack": "npm run version:check",
|
|
35
|
+
"prepublishOnly": "npm run check && npm run version:check",
|
|
30
36
|
"worker:types": "wrangler types src/worker/worker-configuration.d.ts",
|
|
31
37
|
"typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
|
|
32
38
|
"syntax": "sh -n mbm && node --check bin/machine-mcp.mjs && node --check src/local/cli.mjs && node --check src/local/daemon.mjs && node --check src/local/state.mjs && node --check src/local/shell.mjs && node --check src/local/service.mjs && node --check src/local/self-test.mjs",
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const packagePath = path.join(repoRoot, "package.json");
|
|
9
|
+
const workerPath = path.join(repoRoot, "src", "worker", "index.ts");
|
|
10
|
+
const args = new Set(process.argv.slice(2));
|
|
11
|
+
const checkOnly = args.has("--check");
|
|
12
|
+
|
|
13
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
14
|
+
const expected = String(pkg.version || "").trim();
|
|
15
|
+
if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(expected)) {
|
|
16
|
+
fail(`package.json version is not a valid release version: ${JSON.stringify(expected)}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const source = readFileSync(workerPath, "utf8");
|
|
20
|
+
const pattern = /const SERVER_VERSION = "([^"]+)";/;
|
|
21
|
+
const match = source.match(pattern);
|
|
22
|
+
if (!match) fail("Could not find `const SERVER_VERSION = \"...\";` in src/worker/index.ts");
|
|
23
|
+
|
|
24
|
+
const current = match[1];
|
|
25
|
+
if (current === expected) {
|
|
26
|
+
console.log(`Worker version is in sync: ${expected}`);
|
|
27
|
+
process.exit(0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (checkOnly) {
|
|
31
|
+
fail(`Worker version mismatch: package.json=${expected}, src/worker/index.ts=${current}. Run npm run version:sync.`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const updated = source.replace(pattern, `const SERVER_VERSION = "${expected}";`);
|
|
35
|
+
writeFileSync(workerPath, updated);
|
|
36
|
+
console.log(`Updated Worker version: ${current} -> ${expected}`);
|
|
37
|
+
|
|
38
|
+
function fail(message) {
|
|
39
|
+
console.error(message);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
package/src/local/cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import readline from "node:readline/promises";
|
|
|
6
6
|
import { LocalDaemon } from "./daemon.mjs";
|
|
7
7
|
import { runWrangler } from "./shell.mjs";
|
|
8
8
|
import {
|
|
9
|
+
acquireDaemonLock,
|
|
9
10
|
appName,
|
|
10
11
|
defaultStateRoot,
|
|
11
12
|
ensureOwnerOnlyDir,
|
|
@@ -172,18 +173,35 @@ async function startCommand(args) {
|
|
|
172
173
|
await installAutostartBestEffort({ workspace, stateRoot: state.paths.stateRoot, entryScript: process.argv[1], policy: state.policy });
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
|
|
176
|
-
workerUrl: state.worker.url,
|
|
177
|
-
secret: state.worker.daemonSecret,
|
|
178
|
-
workspace,
|
|
179
|
-
policy: state.policy,
|
|
180
|
-
logger: structuredLogger(args.quiet),
|
|
181
|
-
});
|
|
176
|
+
if (!args.daemonOnly) await stopAutostartBestEffort(Boolean(args.quiet));
|
|
182
177
|
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
178
|
+
const lock = acquireDaemonLock(state);
|
|
179
|
+
if (!lock.acquired) {
|
|
180
|
+
if (!args.quiet) {
|
|
181
|
+
const pid = lock.owner?.pid ? `pid ${lock.owner.pid}` : "unknown pid";
|
|
182
|
+
console.warn(`Local daemon is already running for this workspace (${pid}). Not starting a duplicate.`);
|
|
183
|
+
printConnection(state, { json: Boolean(args.json), noPrintCredentials: Boolean(args.noPrintCredentials) });
|
|
184
|
+
}
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
const daemon = new LocalDaemon({
|
|
190
|
+
workerUrl: state.worker.url,
|
|
191
|
+
secret: state.worker.daemonSecret,
|
|
192
|
+
workspace,
|
|
193
|
+
policy: state.policy,
|
|
194
|
+
logger: structuredLogger(args.quiet),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const waitForConnect = daemon.start();
|
|
198
|
+
await waitForConnectWithNotice(waitForConnect, 20_000);
|
|
199
|
+
printConnection(state, { json: Boolean(args.json), noPrintCredentials: Boolean(args.noPrintCredentials) });
|
|
200
|
+
keepProcessAlive(daemon, lock);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
lock.release();
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
187
205
|
}
|
|
188
206
|
|
|
189
207
|
async function ensureWorker(state, args) {
|
|
@@ -357,14 +375,16 @@ function printConnection(state, { json = false, noPrintCredentials = false } = {
|
|
|
357
375
|
console.log(`State: ${payload.state_path}\n`);
|
|
358
376
|
}
|
|
359
377
|
|
|
360
|
-
function keepProcessAlive(daemon) {
|
|
378
|
+
function keepProcessAlive(daemon, lock) {
|
|
361
379
|
const stop = () => {
|
|
362
380
|
console.log("Stopping daemon...");
|
|
363
381
|
daemon.stop();
|
|
382
|
+
lock?.release?.();
|
|
364
383
|
process.exit(0);
|
|
365
384
|
};
|
|
366
385
|
process.once("SIGINT", stop);
|
|
367
386
|
process.once("SIGTERM", stop);
|
|
387
|
+
process.once("exit", () => lock?.release?.());
|
|
368
388
|
setInterval(() => {}, 2 ** 31 - 1);
|
|
369
389
|
}
|
|
370
390
|
|
|
@@ -449,6 +469,15 @@ async function installAutostartBestEffort({ workspace, stateRoot, entryScript, p
|
|
|
449
469
|
}
|
|
450
470
|
}
|
|
451
471
|
|
|
472
|
+
async function stopAutostartBestEffort(quiet = false) {
|
|
473
|
+
try {
|
|
474
|
+
const { stopAutostart } = await import("./service.mjs");
|
|
475
|
+
await stopAutostart({ logger: structuredLogger(true) });
|
|
476
|
+
} catch (error) {
|
|
477
|
+
if (!quiet) console.warn(`Autostart stop skipped: ${error.message}`);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
452
481
|
async function uninstallCommand(args) {
|
|
453
482
|
const stateRoot = stateRootFromArgs(args);
|
|
454
483
|
const deleteRemote = !args.keepWorker;
|
|
@@ -568,6 +597,7 @@ Start options:
|
|
|
568
597
|
--rotate-secrets Rotate secrets before deploying
|
|
569
598
|
--daemon-only Skip deploy and only connect daemon from existing state
|
|
570
599
|
--no-autostart Do not install login autostart during start
|
|
600
|
+
--no-print-credentials Redact the MCP password in console output
|
|
571
601
|
--no-write Disable write_file (default: write enabled)
|
|
572
602
|
--no-exec Disable exec_command (default: exec enabled)
|
|
573
603
|
--full-env Pass full parent environment to exec_command (default: minimal env)
|
package/src/local/self-test.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { mkdtemp, rm } from "node:fs/promises";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { daemonSelfTest } from "./daemon.mjs";
|
|
5
|
-
import { ensureWorkerSecrets, loadState, previewSecret, redactState, selectedWorkspace, setSelectedWorkspace } from "./state.mjs";
|
|
5
|
+
import { acquireDaemonLock, ensureWorkerSecrets, loadState, previewSecret, redactState, selectedWorkspace, setSelectedWorkspace } from "./state.mjs";
|
|
6
6
|
|
|
7
7
|
await daemonSelfTest();
|
|
8
8
|
await stateSelfTest();
|
|
@@ -16,6 +16,19 @@ async function stateSelfTest() {
|
|
|
16
16
|
if (selectedWorkspace(stateRoot) !== workspace) throw new Error("selected workspace was not persisted");
|
|
17
17
|
const state = loadState(workspace, { stateDir: stateRoot });
|
|
18
18
|
ensureWorkerSecrets(state, { rotateSecrets: true });
|
|
19
|
+
const lock = acquireDaemonLock(state);
|
|
20
|
+
if (!lock.acquired) throw new Error("first daemon lock acquisition failed");
|
|
21
|
+
try {
|
|
22
|
+
const duplicate = acquireDaemonLock(state);
|
|
23
|
+
if (duplicate.acquired) throw new Error("duplicate daemon lock acquisition should fail");
|
|
24
|
+
if (duplicate.owner?.pid !== process.pid) throw new Error("duplicate daemon lock owner was not reported");
|
|
25
|
+
} finally {
|
|
26
|
+
lock.release();
|
|
27
|
+
}
|
|
28
|
+
const relock = acquireDaemonLock(state);
|
|
29
|
+
if (!relock.acquired) throw new Error("daemon lock was not released");
|
|
30
|
+
relock.release();
|
|
31
|
+
|
|
19
32
|
const redacted = redactState(state);
|
|
20
33
|
if (redacted.worker.oauthPassword === state.worker.oauthPassword) throw new Error("oauthPassword was not redacted");
|
|
21
34
|
if (redacted.worker.daemonSecret === state.worker.daemonSecret) throw new Error("daemonSecret was not redacted");
|
package/src/local/service.mjs
CHANGED
|
@@ -132,7 +132,10 @@ function launchdPlist({ args, stdout, stderr }) {
|
|
|
132
132
|
${args.map(arg => ` <string>${escapeXml(arg)}</string>`).join("\n")}
|
|
133
133
|
</array>
|
|
134
134
|
<key>RunAtLoad</key><true/>
|
|
135
|
-
<key>KeepAlive</key
|
|
135
|
+
<key>KeepAlive</key>
|
|
136
|
+
<dict>
|
|
137
|
+
<key>SuccessfulExit</key><false/>
|
|
138
|
+
</dict>
|
|
136
139
|
<key>StandardOutPath</key><string>${escapeXml(stdout)}</string>
|
|
137
140
|
<key>StandardErrorPath</key><string>${escapeXml(stderr)}</string>
|
|
138
141
|
</dict>
|
|
@@ -179,7 +182,7 @@ After=network-online.target
|
|
|
179
182
|
[Service]
|
|
180
183
|
Type=simple
|
|
181
184
|
ExecStart=${execArgs}
|
|
182
|
-
Restart=
|
|
185
|
+
Restart=on-failure
|
|
183
186
|
RestartSec=5
|
|
184
187
|
StandardOutput=append:${spec.stdout}
|
|
185
188
|
StandardError=append:${spec.stderr}
|
package/src/local/state.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, realpathSync, rmSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, writeFileSync, chmodSync, realpathSync, rmSync, unlinkSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -89,7 +89,7 @@ export function loadState(workspace, options = {}) {
|
|
|
89
89
|
ensureOwnerOnlyDir(profileDir);
|
|
90
90
|
let state = {};
|
|
91
91
|
if (existsSync(statePath)) {
|
|
92
|
-
state =
|
|
92
|
+
state = readJsonObjectOrBackup(statePath);
|
|
93
93
|
}
|
|
94
94
|
state.schemaVersion = 1;
|
|
95
95
|
state.workspace = {
|
|
@@ -112,6 +112,93 @@ export function saveState(state) {
|
|
|
112
112
|
ownerOnlyFile(statePath);
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
export function daemonLockPathForState(state) {
|
|
116
|
+
const profileDir = state?.paths?.profileDir;
|
|
117
|
+
if (!profileDir) throw new Error("state profile dir is missing");
|
|
118
|
+
return path.join(profileDir, "daemon.lock");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function acquireDaemonLock(state) {
|
|
122
|
+
const lockPath = daemonLockPathForState(state);
|
|
123
|
+
ensureOwnerOnlyDir(path.dirname(lockPath));
|
|
124
|
+
const token = randomBytes(16).toString("hex");
|
|
125
|
+
const payload = {
|
|
126
|
+
pid: process.pid,
|
|
127
|
+
token,
|
|
128
|
+
workspace: state?.workspace?.path || "",
|
|
129
|
+
startedAt: new Date().toISOString(),
|
|
130
|
+
entryScript: process.argv[1] || "",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
134
|
+
let fd;
|
|
135
|
+
try {
|
|
136
|
+
fd = openSync(lockPath, "wx", 0o600);
|
|
137
|
+
writeFileSync(fd, JSON.stringify(payload, null, 2) + "\n");
|
|
138
|
+
closeSync(fd);
|
|
139
|
+
ownerOnlyFile(lockPath);
|
|
140
|
+
return {
|
|
141
|
+
acquired: true,
|
|
142
|
+
path: lockPath,
|
|
143
|
+
owner: payload,
|
|
144
|
+
release() {
|
|
145
|
+
releaseDaemonLock(lockPath, token);
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (fd !== undefined) {
|
|
150
|
+
try { closeSync(fd); } catch {}
|
|
151
|
+
}
|
|
152
|
+
if (error?.code !== "EEXIST") throw error;
|
|
153
|
+
const owner = readDaemonLockOwner(lockPath);
|
|
154
|
+
if (owner?.pid && isPidAlive(owner.pid)) {
|
|
155
|
+
return { acquired: false, path: lockPath, owner, release() {} };
|
|
156
|
+
}
|
|
157
|
+
try { unlinkSync(lockPath); } catch {}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const owner = readDaemonLockOwner(lockPath);
|
|
161
|
+
return { acquired: false, path: lockPath, owner, release() {} };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function releaseDaemonLock(lockPath, token) {
|
|
165
|
+
const owner = readDaemonLockOwner(lockPath);
|
|
166
|
+
if (owner?.token !== token) return;
|
|
167
|
+
try { unlinkSync(lockPath); } catch {}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function readDaemonLockOwner(lockPath) {
|
|
171
|
+
try {
|
|
172
|
+
const parsed = JSON.parse(readFileSync(lockPath, "utf8"));
|
|
173
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isPidAlive(pid) {
|
|
180
|
+
const parsed = Number(pid);
|
|
181
|
+
if (!Number.isInteger(parsed) || parsed <= 0) return false;
|
|
182
|
+
try {
|
|
183
|
+
process.kill(parsed, 0);
|
|
184
|
+
return true;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
return error?.code === "EPERM";
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function readJsonObjectOrBackup(filePath) {
|
|
191
|
+
try {
|
|
192
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
193
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
194
|
+
} catch {
|
|
195
|
+
const backupPath = `${filePath}.corrupt-${Date.now()}`;
|
|
196
|
+
try { renameSync(filePath, backupPath); } catch {}
|
|
197
|
+
return {};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
115
202
|
export function ensureWorkerSecrets(state, options = {}) {
|
|
116
203
|
state.worker ||= {};
|
|
117
204
|
if (!state.worker.oauthPassword || options.rotateSecrets) state.worker.oauthPassword = randomToken("mcp_password");
|
package/src/worker/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DurableObject } from "cloudflare:workers";
|
|
2
2
|
|
|
3
3
|
const SERVER_NAME = "machine-bridge-mcp";
|
|
4
|
-
const SERVER_VERSION = "0.1.
|
|
4
|
+
const SERVER_VERSION = "0.1.1";
|
|
5
5
|
const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
6
6
|
const JSONRPC_VERSION = "2.0";
|
|
7
7
|
const DEFAULT_MAX_BODY_BYTES = 32 * 1024 * 1024;
|
|
@@ -353,7 +353,10 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
353
353
|
tools: this.allTools().map((tool) => tool.name),
|
|
354
354
|
};
|
|
355
355
|
}
|
|
356
|
-
if (workspaceTools.some((tool) => tool.name === name))
|
|
356
|
+
if (workspaceTools.some((tool) => tool.name === name)) {
|
|
357
|
+
if (!this.daemonToolEnabled(name)) throw new Error(`tool disabled by local daemon policy: ${name}`);
|
|
358
|
+
return this.callDaemonTool(name, args);
|
|
359
|
+
}
|
|
357
360
|
throw new Error(`unknown tool: ${name}`);
|
|
358
361
|
}
|
|
359
362
|
|
|
@@ -399,7 +402,23 @@ export class BridgeRoom extends DurableObject<BridgeEnv> {
|
|
|
399
402
|
}
|
|
400
403
|
|
|
401
404
|
private allTools(): Array<Record<string, unknown>> {
|
|
402
|
-
|
|
405
|
+
const advertised = this.daemonAdvertisedTools();
|
|
406
|
+
const localTools = advertised
|
|
407
|
+
? workspaceTools.filter((tool) => advertised.has(tool.name))
|
|
408
|
+
: workspaceTools;
|
|
409
|
+
return [serverInfoTool, ...localTools].map((tool) => ({ ...tool }));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private daemonToolEnabled(name: string): boolean {
|
|
413
|
+
const advertised = this.daemonAdvertisedTools();
|
|
414
|
+
return !advertised || advertised.has(name);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private daemonAdvertisedTools(): Set<string> | null {
|
|
418
|
+
const socket = this.daemonSockets()[0];
|
|
419
|
+
const attachment = socket ? this.daemonAttachment(socket) : undefined;
|
|
420
|
+
if (!attachment?.tools?.length) return null;
|
|
421
|
+
return new Set(attachment.tools);
|
|
403
422
|
}
|
|
404
423
|
|
|
405
424
|
private daemonSockets(): WebSocket[] {
|
|
@@ -847,7 +866,6 @@ function validateOrigin(request: Request, base: string, configured = ""): boolea
|
|
|
847
866
|
try {
|
|
848
867
|
const parsed = new URL(origin);
|
|
849
868
|
if (origin === base) return true;
|
|
850
|
-
if (parsed.protocol === "https:") return true;
|
|
851
869
|
return parsed.protocol === "http:" && isLoopbackHost(parsed.hostname);
|
|
852
870
|
} catch {
|
|
853
871
|
return false;
|