machine-bridge-mcp 0.4.2 → 0.5.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/CHANGELOG.md +24 -0
- package/README.md +17 -9
- package/SECURITY.md +6 -4
- package/docs/ARCHITECTURE.md +12 -10
- package/docs/CLIENTS.md +6 -0
- package/docs/LOGGING.md +80 -0
- package/docs/OPERATIONS.md +25 -19
- package/docs/TESTING.md +7 -4
- package/package.json +4 -4
- package/src/local/cli.mjs +155 -77
- package/src/local/daemon.mjs +58 -41
- package/src/local/log.mjs +56 -15
- package/src/local/service.mjs +40 -21
- package/src/local/state.mjs +45 -20
- package/src/local/stdio.mjs +13 -37
- package/src/local/tools.mjs +28 -13
- package/src/shared/server-metadata.json +17 -0
- package/src/shared/tool-catalog.json +2 -2
- package/src/worker/index.ts +13 -11
package/src/local/state.mjs
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import { closeSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync,
|
|
2
|
+
import { closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, lstatSync, mkdirSync, openSync, readSync, renameSync, writeFileSync, chmodSync, realpathSync, rmSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
|
|
6
7
|
|
|
7
8
|
export const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
8
|
-
export const appName =
|
|
9
|
+
export const appName = String(serverMetadata.name);
|
|
9
10
|
const STATE_MARKER = ".machine-bridge-mcp-state";
|
|
11
|
+
const MAX_STATE_JSON_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
const MAX_LOCK_BYTES = 64 * 1024;
|
|
13
|
+
const MAX_MARKER_BYTES = 4096;
|
|
10
14
|
|
|
11
15
|
export function expandHome(input = "") {
|
|
12
16
|
if (!input || input === "~") return os.homedir();
|
|
@@ -29,7 +33,7 @@ export function defaultStateRoot() {
|
|
|
29
33
|
}
|
|
30
34
|
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
function configPath(stateRoot = defaultStateRoot()) {
|
|
33
37
|
return path.join(expandHome(stateRoot), "config.json");
|
|
34
38
|
}
|
|
35
39
|
|
|
@@ -78,13 +82,10 @@ export function workspaceHash(workspace) {
|
|
|
78
82
|
return createHash("sha256").update(String(workspace)).digest("hex").slice(0, 24);
|
|
79
83
|
}
|
|
80
84
|
|
|
81
|
-
|
|
85
|
+
function profileDirForWorkspace(workspace, stateRoot = defaultStateRoot()) {
|
|
82
86
|
return path.join(expandHome(stateRoot), "profiles", workspaceHash(workspace));
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
export function statePathForWorkspace(workspace, stateRoot = defaultStateRoot()) {
|
|
86
|
-
return path.join(profileDirForWorkspace(workspace, stateRoot), "state.json");
|
|
87
|
-
}
|
|
88
89
|
|
|
89
90
|
export function loadState(workspace, options = {}) {
|
|
90
91
|
const stateRoot = ensureStateRoot(options.stateDir ? expandHome(options.stateDir) : defaultStateRoot());
|
|
@@ -96,7 +97,7 @@ export function loadState(workspace, options = {}) {
|
|
|
96
97
|
ownerOnlyFile(statePath);
|
|
97
98
|
state = readJsonObjectOrBackup(statePath);
|
|
98
99
|
}
|
|
99
|
-
state.schemaVersion =
|
|
100
|
+
state.schemaVersion = 4;
|
|
100
101
|
state.workspace = {
|
|
101
102
|
path: workspace,
|
|
102
103
|
hash: workspaceHash(workspace),
|
|
@@ -189,7 +190,7 @@ function releaseProcessLock(lockPath, token) {
|
|
|
189
190
|
|
|
190
191
|
export function readDaemonLockOwner(lockPath) {
|
|
191
192
|
try {
|
|
192
|
-
const parsed = JSON.parse(
|
|
193
|
+
const parsed = JSON.parse(readBoundedUtf8(lockPath, MAX_LOCK_BYTES, "lock file"));
|
|
193
194
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
|
|
194
195
|
} catch {
|
|
195
196
|
return null;
|
|
@@ -207,9 +208,31 @@ function isPidAlive(pid) {
|
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
|
|
211
|
+
function readBoundedUtf8(filePath, maxBytes, label) {
|
|
212
|
+
const pathInfo = lstatSync(filePath);
|
|
213
|
+
if (pathInfo.isSymbolicLink()) throw new Error(`${label} must not be a symbolic link`);
|
|
214
|
+
const noFollow = Number(fsConstants.O_NOFOLLOW || 0);
|
|
215
|
+
const fd = openSync(filePath, Number(fsConstants.O_RDONLY) | noFollow);
|
|
216
|
+
try {
|
|
217
|
+
const info = fstatSync(fd);
|
|
218
|
+
if (!info.isFile()) throw new Error(`${label} is not a regular file`);
|
|
219
|
+
if (info.size > maxBytes) throw new Error(`${label} exceeds ${maxBytes} bytes`);
|
|
220
|
+
const buffer = Buffer.alloc(info.size);
|
|
221
|
+
let offset = 0;
|
|
222
|
+
while (offset < buffer.length) {
|
|
223
|
+
const count = readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
224
|
+
if (count === 0) break;
|
|
225
|
+
offset += count;
|
|
226
|
+
}
|
|
227
|
+
return buffer.subarray(0, offset).toString("utf8");
|
|
228
|
+
} finally {
|
|
229
|
+
closeSync(fd);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
210
233
|
function readJsonObjectOrBackup(filePath) {
|
|
211
234
|
try {
|
|
212
|
-
const parsed = JSON.parse(
|
|
235
|
+
const parsed = JSON.parse(readBoundedUtf8(filePath, MAX_STATE_JSON_BYTES, "state JSON"));
|
|
213
236
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("JSON root must be an object");
|
|
214
237
|
return parsed;
|
|
215
238
|
} catch {
|
|
@@ -286,7 +309,7 @@ function assertSafeStateRootForRemoval(root) {
|
|
|
286
309
|
}
|
|
287
310
|
|
|
288
311
|
function assertValidStateMarker(marker, options = {}) {
|
|
289
|
-
const content =
|
|
312
|
+
const content = readBoundedUtf8(marker, MAX_MARKER_BYTES, "state marker");
|
|
290
313
|
try {
|
|
291
314
|
const value = JSON.parse(content);
|
|
292
315
|
if (value?.app === appName && value?.schema === 1) return;
|
|
@@ -315,7 +338,7 @@ function stateRootMatchesRecordedWorkspace(root) {
|
|
|
315
338
|
if (!entry.isDirectory()) continue;
|
|
316
339
|
const stateFile = path.join(profiles, entry.name, "state.json");
|
|
317
340
|
if (!existsSync(stateFile)) continue;
|
|
318
|
-
const value = JSON.parse(
|
|
341
|
+
const value = JSON.parse(readBoundedUtf8(stateFile, MAX_STATE_JSON_BYTES, "state JSON"));
|
|
319
342
|
if (typeof value?.workspace?.path !== "string") continue;
|
|
320
343
|
try { if (realpathSync(value.workspace.path) === root) return true; } catch {}
|
|
321
344
|
}
|
|
@@ -327,7 +350,7 @@ function isRecognizableLegacyStateRoot(root) {
|
|
|
327
350
|
const config = path.join(root, "config.json");
|
|
328
351
|
try {
|
|
329
352
|
if (existsSync(config)) {
|
|
330
|
-
const value = JSON.parse(
|
|
353
|
+
const value = JSON.parse(readBoundedUtf8(config, MAX_STATE_JSON_BYTES, "config JSON"));
|
|
331
354
|
if (
|
|
332
355
|
value &&
|
|
333
356
|
typeof value.selectedWorkspace === "string" &&
|
|
@@ -343,7 +366,7 @@ function isRecognizableLegacyStateRoot(root) {
|
|
|
343
366
|
if (!entry.isDirectory() || !/^[a-f0-9]{24}$/.test(entry.name)) continue;
|
|
344
367
|
const stateFile = path.join(profiles, entry.name, "state.json");
|
|
345
368
|
if (!existsSync(stateFile)) continue;
|
|
346
|
-
const value = JSON.parse(
|
|
369
|
+
const value = JSON.parse(readBoundedUtf8(stateFile, MAX_STATE_JSON_BYTES, "state JSON"));
|
|
347
370
|
if (value?.workspace?.hash === entry.name && typeof value?.workspace?.path === "string") return true;
|
|
348
371
|
}
|
|
349
372
|
} catch {}
|
|
@@ -354,11 +377,13 @@ function atomicWriteJson(filePath, value) {
|
|
|
354
377
|
const dir = path.dirname(filePath);
|
|
355
378
|
ensureOwnerOnlyDir(dir);
|
|
356
379
|
cleanupStaleAtomicTemps(dir, path.basename(filePath));
|
|
380
|
+
const serialized = `${JSON.stringify(value, null, 2)}\n`;
|
|
381
|
+
if (Buffer.byteLength(serialized) > MAX_STATE_JSON_BYTES) throw new Error(`state JSON exceeds ${MAX_STATE_JSON_BYTES} bytes`);
|
|
357
382
|
const tempPath = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
358
383
|
let fd;
|
|
359
384
|
try {
|
|
360
385
|
fd = openSync(tempPath, "wx", 0o600);
|
|
361
|
-
writeFileSync(fd,
|
|
386
|
+
writeFileSync(fd, serialized, "utf8");
|
|
362
387
|
fsyncSync(fd);
|
|
363
388
|
closeSync(fd);
|
|
364
389
|
fd = undefined;
|
|
@@ -412,11 +437,11 @@ export function ensureWorkerSecrets(state, options = {}) {
|
|
|
412
437
|
if (!state.worker.name || options.workerName) state.worker.name = options.workerName || defaultWorkerName(state.workspace.hash);
|
|
413
438
|
}
|
|
414
439
|
|
|
415
|
-
|
|
440
|
+
function defaultWorkerName(hash) {
|
|
416
441
|
return `mbm-${String(hash || "default").slice(0, 12)}`;
|
|
417
442
|
}
|
|
418
443
|
|
|
419
|
-
|
|
444
|
+
function randomToken(prefix) {
|
|
420
445
|
return `${prefix}_${randomBytes(32).toString("base64url")}`;
|
|
421
446
|
}
|
|
422
447
|
|
|
@@ -424,9 +449,6 @@ export function sha256(value) {
|
|
|
424
449
|
return createHash("sha256").update(String(value)).digest("hex");
|
|
425
450
|
}
|
|
426
451
|
|
|
427
|
-
export function fileSha256(filePath) {
|
|
428
|
-
return createHash("sha256").update(readFileSync(filePath)).digest("hex");
|
|
429
|
-
}
|
|
430
452
|
|
|
431
453
|
export function ensureOwnerOnlyDir(dir) {
|
|
432
454
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
@@ -434,6 +456,9 @@ export function ensureOwnerOnlyDir(dir) {
|
|
|
434
456
|
}
|
|
435
457
|
|
|
436
458
|
export function ownerOnlyFile(filePath) {
|
|
459
|
+
const info = lstatSync(filePath);
|
|
460
|
+
if (info.isSymbolicLink()) throw new Error(`owner-only file must not be a symbolic link: ${filePath}`);
|
|
461
|
+
if (!info.isFile()) throw new Error(`owner-only path is not a regular file: ${filePath}`);
|
|
437
462
|
try { chmodSync(filePath, 0o600); } catch {}
|
|
438
463
|
}
|
|
439
464
|
|
package/src/local/stdio.mjs
CHANGED
|
@@ -2,11 +2,12 @@ import readline from "node:readline";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { randomBytes } from "node:crypto";
|
|
4
4
|
import { LocalDaemon } from "./daemon.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { classifyOperationalError, createLogger } from "./log.mjs";
|
|
6
6
|
import {
|
|
7
7
|
MCP_INSTRUCTIONS,
|
|
8
8
|
MCP_PROTOCOL_VERSION,
|
|
9
9
|
MCP_SUPPORTED_PROTOCOL_VERSIONS,
|
|
10
|
+
SERVER_NAME,
|
|
10
11
|
rpcError,
|
|
11
12
|
rpcResult,
|
|
12
13
|
toolResult,
|
|
@@ -14,10 +15,11 @@ import {
|
|
|
14
15
|
} from "./tools.mjs";
|
|
15
16
|
|
|
16
17
|
const MAX_LINE_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const SLOW_TOOL_CALL_MS = 30_000;
|
|
17
19
|
const PACKAGE_VERSION = String(JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version);
|
|
18
20
|
|
|
19
|
-
export async function runStdioServer({ workspace, policy,
|
|
20
|
-
const logger =
|
|
21
|
+
export async function runStdioServer({ workspace, policy, logLevel = "info" }) {
|
|
22
|
+
const logger = createLogger({ component: "stdio", level: logLevel, stderrOnly: true, color: false });
|
|
21
23
|
const runtime = new LocalDaemon({ workspace, policy, logger });
|
|
22
24
|
const pending = new Map();
|
|
23
25
|
let negotiatedVersion = MCP_PROTOCOL_VERSION;
|
|
@@ -40,7 +42,7 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
|
|
|
40
42
|
return;
|
|
41
43
|
}
|
|
42
44
|
void handleLine(line).catch((error) => {
|
|
43
|
-
logger.error("stdio request handler failed", {
|
|
45
|
+
logger.error("stdio request handler failed", { error_class: classifyOperationalError(error) });
|
|
44
46
|
});
|
|
45
47
|
});
|
|
46
48
|
|
|
@@ -74,7 +76,7 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
|
|
|
74
76
|
protocolVersion: negotiatedVersion,
|
|
75
77
|
capabilities: { tools: { listChanged: false }, logging: {} },
|
|
76
78
|
serverInfo: {
|
|
77
|
-
name:
|
|
79
|
+
name: SERVER_NAME,
|
|
78
80
|
title: "Machine Bridge MCP",
|
|
79
81
|
version: PACKAGE_VERSION,
|
|
80
82
|
description: "Workspace-scoped local coding tools over MCP stdio or authenticated remote relay.",
|
|
@@ -128,11 +130,15 @@ export async function runStdioServer({ workspace, policy, verbose = false }) {
|
|
|
128
130
|
try {
|
|
129
131
|
const result = await runtime.executeTool(name, args, { callId });
|
|
130
132
|
send(rpcResult(message.id, toolResult(result)));
|
|
131
|
-
|
|
133
|
+
const durationMs = Date.now() - started;
|
|
134
|
+
if (durationMs >= SLOW_TOOL_CALL_MS) logger.info("slow tool call completed", { tool: name, duration_ms: durationMs });
|
|
135
|
+
else logger.debug("tool call completed", { call_id: callId.slice(0, 20), tool: name, duration_ms: durationMs });
|
|
132
136
|
} catch (error) {
|
|
133
137
|
const safeError = runtime.safeErrorMessage(error);
|
|
134
138
|
send(rpcResult(message.id, toolResult({ error: safeError }, true)));
|
|
135
|
-
|
|
139
|
+
const durationMs = Date.now() - started;
|
|
140
|
+
logger.warn("tool call failed", { tool: name, duration_ms: durationMs, error_class: classifyOperationalError(error) });
|
|
141
|
+
logger.debug("tool call failure correlation", { call_id: callId.slice(0, 20) });
|
|
136
142
|
} finally {
|
|
137
143
|
if (key) pending.delete(key);
|
|
138
144
|
runtime.finishCall(callId);
|
|
@@ -162,33 +168,3 @@ function jsonRpcIdKey(value) {
|
|
|
162
168
|
function asObject(value) {
|
|
163
169
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
164
170
|
}
|
|
165
|
-
|
|
166
|
-
function classifyError(error) {
|
|
167
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
168
|
-
if (/cancel/i.test(message)) return "cancelled";
|
|
169
|
-
if (/timed out/i.test(message)) return "timeout";
|
|
170
|
-
if (/outside the configured workspace/i.test(message)) return "path_boundary";
|
|
171
|
-
if (/disabled|requires .* mode/i.test(message)) return "policy_denied";
|
|
172
|
-
if (/not found|ENOENT/i.test(message)) return "not_found";
|
|
173
|
-
if (/permission|EACCES|EPERM/i.test(message)) return "permission_denied";
|
|
174
|
-
if (/maximum|exceeds|max_bytes|too many/i.test(message)) return "limit_exceeded";
|
|
175
|
-
if (/invalid|must|requires|ambiguous|mismatch/i.test(message)) return "invalid_request";
|
|
176
|
-
return "execution_failed";
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function errorMessage(error) {
|
|
180
|
-
return sanitizeLogText(error instanceof Error ? error.message : String(error)).slice(0, 4096) || "tool call failed";
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function stderrLogger(verbose) {
|
|
184
|
-
const write = (level, message, fields) => {
|
|
185
|
-
if (level === "debug" && !verbose) return;
|
|
186
|
-
process.stderr.write(`[${level}] stdio: ${sanitizeLogText(message)}${formatFields(fields)}\n`);
|
|
187
|
-
};
|
|
188
|
-
return {
|
|
189
|
-
info(message, fields) { write("info", message, fields); },
|
|
190
|
-
warn(message, fields) { write("warn", message, fields); },
|
|
191
|
-
error(message, fields) { write("error", message, fields); },
|
|
192
|
-
debug(message, fields) { write("debug", message, fields); },
|
|
193
|
-
};
|
|
194
|
-
}
|
package/src/local/tools.mjs
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import catalog from "../shared/tool-catalog.json" with { type: "json" };
|
|
2
|
+
import serverMetadata from "../shared/server-metadata.json" with { type: "json" };
|
|
2
3
|
|
|
3
|
-
export const
|
|
4
|
-
export const
|
|
4
|
+
export const SERVER_NAME = String(serverMetadata.name);
|
|
5
|
+
export const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
6
|
+
export const MCP_SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(serverMetadata.supportedProtocolVersions.map((value) => String(value)));
|
|
5
7
|
|
|
6
|
-
export const MCP_INSTRUCTIONS =
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
export const MCP_INSTRUCTIONS = Object.freeze(serverMetadata.instructions.map((value) => String(value))).join("\n");
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_POLICY_PROFILE = "full";
|
|
12
|
+
export const DEFAULT_POLICY_REVISION = 2;
|
|
13
|
+
|
|
14
|
+
export const POLICY_PROFILES = Object.freeze({
|
|
15
|
+
review: Object.freeze({ profile: "review", allowWrite: false, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
16
|
+
edit: Object.freeze({ profile: "edit", allowWrite: true, execMode: "off", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
17
|
+
agent: Object.freeze({ profile: "agent", allowWrite: true, execMode: "direct", unrestrictedPaths: false, minimalEnv: true, exposeAbsolutePaths: false }),
|
|
18
|
+
full: Object.freeze({ profile: "full", allowWrite: true, execMode: "shell", unrestrictedPaths: true, minimalEnv: false, exposeAbsolutePaths: true }),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const POLICY_ORIGINS = new Set(["default", "explicit", "custom", "migrated", "legacy-preserved"]);
|
|
22
|
+
|
|
23
|
+
export function policyProfile(name, origin = "explicit") {
|
|
24
|
+
const profile = String(name || "").trim().toLowerCase();
|
|
25
|
+
if (!POLICY_PROFILES[profile]) throw new Error(`unknown policy profile: ${profile}`);
|
|
26
|
+
return normalizePolicy({ ...POLICY_PROFILES[profile], origin, revision: DEFAULT_POLICY_REVISION });
|
|
27
|
+
}
|
|
14
28
|
|
|
15
29
|
export function normalizePolicy(policy = {}) {
|
|
16
30
|
const execMode = ["off", "direct", "shell"].includes(policy.execMode)
|
|
@@ -18,8 +32,12 @@ export function normalizePolicy(policy = {}) {
|
|
|
18
32
|
: policy.allowExec === true
|
|
19
33
|
? "shell"
|
|
20
34
|
: "off";
|
|
35
|
+
const origin = POLICY_ORIGINS.has(policy.origin) ? policy.origin : "custom";
|
|
36
|
+
const revision = Number.isInteger(policy.revision) && policy.revision > 0 ? policy.revision : DEFAULT_POLICY_REVISION;
|
|
21
37
|
return {
|
|
22
38
|
profile: typeof policy.profile === "string" && policy.profile ? policy.profile : "custom",
|
|
39
|
+
origin,
|
|
40
|
+
revision,
|
|
23
41
|
allowWrite: policy.allowWrite === true,
|
|
24
42
|
allowExec: execMode !== "off",
|
|
25
43
|
execMode,
|
|
@@ -44,9 +62,6 @@ export function allToolNames() {
|
|
|
44
62
|
return catalog.map((tool) => tool.name);
|
|
45
63
|
}
|
|
46
64
|
|
|
47
|
-
export function findTool(name) {
|
|
48
|
-
return catalog.find((tool) => tool.name === name) || null;
|
|
49
|
-
}
|
|
50
65
|
|
|
51
66
|
export function toolResult(value, isError = false) {
|
|
52
67
|
const special = specialMcpResult(value);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "machine-bridge-mcp",
|
|
3
|
+
"protocolVersion": "2025-11-25",
|
|
4
|
+
"supportedProtocolVersions": [
|
|
5
|
+
"2025-11-25",
|
|
6
|
+
"2025-06-18",
|
|
7
|
+
"2025-03-26"
|
|
8
|
+
],
|
|
9
|
+
"instructions": [
|
|
10
|
+
"You are connected to a local workspace through machine-bridge-mcp.",
|
|
11
|
+
"Remote mode uses a Cloudflare relay; stdio mode runs on the local machine. File and command operations execute on the user's local runtime, not in the Worker.",
|
|
12
|
+
"Filesystem scope and path display follow the active local policy; the default full profile is unrestricted.",
|
|
13
|
+
"Filename sensitivity is not classified by this server; the MCP host may enforce additional independent rules.",
|
|
14
|
+
"run_process avoids shell parsing but is not an OS sandbox. exec_command is exposed only in shell mode and has the local user's authority.",
|
|
15
|
+
"Inspect before editing, prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run."
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
{
|
|
35
35
|
"name": "list_roots",
|
|
36
36
|
"title": "List workspace roots",
|
|
37
|
-
"description": "List filesystem roots exposed by the local
|
|
37
|
+
"description": "List filesystem roots exposed by the active local policy. The default full profile exposes local filesystem roots and returns absolute paths.",
|
|
38
38
|
"availability": "always",
|
|
39
39
|
"annotations": {
|
|
40
40
|
"readOnlyHint": true,
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
{
|
|
101
101
|
"name": "read_file",
|
|
102
102
|
"title": "Read text file",
|
|
103
|
-
"description": "Read a UTF-8 file or an inclusive 1-based line range.
|
|
103
|
+
"description": "Read a UTF-8 regular file or an inclusive 1-based line range. The server does not block sensitive-looking filenames; access follows the active local policy and operating-system permissions.",
|
|
104
104
|
"availability": "always",
|
|
105
105
|
"annotations": {
|
|
106
106
|
"readOnlyHint": true,
|
package/src/worker/index.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { DurableObject } from "cloudflare:workers";
|
|
2
2
|
import toolCatalog from "../shared/tool-catalog.json";
|
|
3
|
+
import serverMetadata from "../shared/server-metadata.json";
|
|
3
4
|
|
|
4
|
-
const SERVER_NAME =
|
|
5
|
-
const SERVER_VERSION = "0.
|
|
6
|
-
const MCP_PROTOCOL_VERSION =
|
|
7
|
-
const MCP_SUPPORTED_PROTOCOL_VERSIONS =
|
|
5
|
+
const SERVER_NAME = String(serverMetadata.name);
|
|
6
|
+
const SERVER_VERSION = "0.5.0";
|
|
7
|
+
const MCP_PROTOCOL_VERSION = String(serverMetadata.protocolVersion);
|
|
8
|
+
const MCP_SUPPORTED_PROTOCOL_VERSIONS = serverMetadata.supportedProtocolVersions.map((value) => String(value));
|
|
8
9
|
const JSONRPC_VERSION = "2.0";
|
|
9
10
|
const DEFAULT_MAX_BODY_BYTES = 8 * 1024 * 1024;
|
|
10
11
|
const MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
@@ -121,13 +122,8 @@ const allCatalogTools = toolCatalog as ToolDefinition[];
|
|
|
121
122
|
const serverInfoTool = publicTool(allCatalogTools.find((tool) => tool.name === "server_info")!);
|
|
122
123
|
const workspaceTools = allCatalogTools.filter((tool) => tool.name !== "server_info").map(publicTool);
|
|
123
124
|
|
|
124
|
-
const MCP_INSTRUCTIONS =
|
|
125
|
-
|
|
126
|
-
"The Cloudflare Worker authenticates and relays calls; file and command operations execute on the user's local runtime.",
|
|
127
|
-
"Relative paths use the configured workspace. Direct filesystem tools are workspace-scoped unless unrestricted paths are explicitly enabled.",
|
|
128
|
-
"run_process avoids shell parsing but is not an OS sandbox. exec_command is only exposed in shell mode and has the local user's authority.",
|
|
129
|
-
"Prefer read_file line ranges, edit_file, or apply_patch over whole-file replacement, and report commands that were run.",
|
|
130
|
-
].join("\n");
|
|
125
|
+
const MCP_INSTRUCTIONS = serverMetadata.instructions.map((value) => String(value)).join("\n");
|
|
126
|
+
|
|
131
127
|
|
|
132
128
|
function publicTool(tool: ToolDefinition): ToolDefinition {
|
|
133
129
|
const { availability: _availability, ...definition } = tool;
|
|
@@ -1001,8 +997,14 @@ function daemonPolicyAllows(availability: unknown, policy: Record<string, unknow
|
|
|
1001
997
|
function sanitizeDaemonPolicy(value: unknown): Record<string, unknown> {
|
|
1002
998
|
const policy = asObject(value);
|
|
1003
999
|
const execMode = policy.execMode === "shell" || policy.execMode === "direct" ? policy.execMode : "off";
|
|
1000
|
+
const origin = sanitizeMetadataText(policy.origin, 32);
|
|
1001
|
+
const revision = Number.isInteger(policy.revision) && Number(policy.revision) > 0
|
|
1002
|
+
? Math.min(Number(policy.revision), 1_000_000)
|
|
1003
|
+
: 1;
|
|
1004
1004
|
return {
|
|
1005
1005
|
profile: sanitizeMetadataText(policy.profile, 32) ?? "custom",
|
|
1006
|
+
origin: ["default", "explicit", "custom", "migrated", "legacy-preserved"].includes(origin ?? "") ? origin : "custom",
|
|
1007
|
+
revision,
|
|
1006
1008
|
allowWrite: policy.allowWrite === true,
|
|
1007
1009
|
allowExec: execMode !== "off",
|
|
1008
1010
|
execMode,
|