triflux 9.6.0 → 9.7.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/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +414 -25
- package/hooks/hook-registry.json +24 -2
- package/hooks/mcp-config-watcher.mjs +85 -0
- package/package.json +1 -1
- package/scripts/__tests__/mcp-guard-engine.test.mjs +118 -0
- package/scripts/lib/mcp-guard-engine.mjs +940 -0
- package/scripts/mcp-safety-guard.mjs +44 -0
- package/scripts/setup.mjs +58 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { describe, it, afterEach } from "node:test";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
isWatchedPath,
|
|
10
|
+
loadRegistry,
|
|
11
|
+
remediate,
|
|
12
|
+
resolveHubUrl,
|
|
13
|
+
scanForStdioServers,
|
|
14
|
+
} from "../lib/mcp-guard-engine.mjs";
|
|
15
|
+
|
|
16
|
+
const TEST_DIR = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const PROJECT_ROOT = resolve(TEST_DIR, "..", "..");
|
|
18
|
+
const originalHome = {
|
|
19
|
+
HOME: process.env.HOME,
|
|
20
|
+
USERPROFILE: process.env.USERPROFILE,
|
|
21
|
+
TFX_HUB_PORT: process.env.TFX_HUB_PORT,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
function createHomeDir(prefix = "mcp-guard-") {
|
|
25
|
+
const base = join(tmpdir(), `${prefix}${Date.now()}-${Math.random().toString(16).slice(2)}`);
|
|
26
|
+
mkdirSync(base, { recursive: true });
|
|
27
|
+
mkdirSync(join(base, ".gemini"), { recursive: true });
|
|
28
|
+
mkdirSync(join(base, ".claude", "cache", "tfx-hub"), { recursive: true });
|
|
29
|
+
mkdirSync(join(base, ".codex"), { recursive: true });
|
|
30
|
+
return base;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function withHome(homeDir) {
|
|
34
|
+
process.env.HOME = homeDir;
|
|
35
|
+
process.env.USERPROFILE = homeDir;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
if (originalHome.HOME === undefined) delete process.env.HOME;
|
|
40
|
+
else process.env.HOME = originalHome.HOME;
|
|
41
|
+
|
|
42
|
+
if (originalHome.USERPROFILE === undefined) delete process.env.USERPROFILE;
|
|
43
|
+
else process.env.USERPROFILE = originalHome.USERPROFILE;
|
|
44
|
+
|
|
45
|
+
if (originalHome.TFX_HUB_PORT === undefined) delete process.env.TFX_HUB_PORT;
|
|
46
|
+
else process.env.TFX_HUB_PORT = originalHome.TFX_HUB_PORT;
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("mcp guard engine", () => {
|
|
50
|
+
it("loads the MCP registry", () => {
|
|
51
|
+
const registry = loadRegistry();
|
|
52
|
+
assert.equal(registry.version, 1);
|
|
53
|
+
assert.equal(registry.servers["tfx-hub"].url, "http://127.0.0.1:27888/mcp");
|
|
54
|
+
assert.equal(registry.policies.watched_paths.length, 5);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("matches watched paths for Gemini and local .mcp.json", () => {
|
|
58
|
+
const homeDir = createHomeDir();
|
|
59
|
+
withHome(homeDir);
|
|
60
|
+
|
|
61
|
+
assert.equal(isWatchedPath(join(homeDir, ".gemini", "settings.json")), true);
|
|
62
|
+
assert.equal(isWatchedPath(join(PROJECT_ROOT, "nested", ".mcp.json")), true);
|
|
63
|
+
assert.equal(isWatchedPath(join(PROJECT_ROOT, "nested", "settings.yaml")), false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("detects stdio MCP servers from JSON config", () => {
|
|
67
|
+
const homeDir = createHomeDir();
|
|
68
|
+
withHome(homeDir);
|
|
69
|
+
|
|
70
|
+
const settingsPath = join(homeDir, ".gemini", "settings.json");
|
|
71
|
+
writeFileSync(settingsPath, JSON.stringify({
|
|
72
|
+
mcpServers: {
|
|
73
|
+
"unsafe-stdio": { command: "node", args: ["server.js"] },
|
|
74
|
+
"safe-url": { url: "http://127.0.0.1:27888/mcp" },
|
|
75
|
+
},
|
|
76
|
+
}, null, 2));
|
|
77
|
+
|
|
78
|
+
const found = scanForStdioServers(settingsPath);
|
|
79
|
+
assert.deepEqual(found.map((server) => server.name), ["unsafe-stdio"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("replaces stdio MCP entries with tfx-hub and writes a backup", () => {
|
|
83
|
+
const homeDir = createHomeDir();
|
|
84
|
+
withHome(homeDir);
|
|
85
|
+
|
|
86
|
+
const pidPath = join(homeDir, ".claude", "cache", "tfx-hub", "hub.pid");
|
|
87
|
+
writeFileSync(pidPath, JSON.stringify({ host: "127.0.0.1", port: 30123 }), "utf8");
|
|
88
|
+
|
|
89
|
+
const settingsPath = join(homeDir, ".gemini", "settings.json");
|
|
90
|
+
writeFileSync(settingsPath, JSON.stringify({
|
|
91
|
+
mcpServers: {
|
|
92
|
+
"unsafe-stdio": { command: "node", args: ["server.js"] },
|
|
93
|
+
},
|
|
94
|
+
}, null, 2));
|
|
95
|
+
|
|
96
|
+
const result = remediate(settingsPath, scanForStdioServers(settingsPath), { stdio_action: "replace-with-hub" });
|
|
97
|
+
const updated = JSON.parse(readFileSync(settingsPath, "utf8"));
|
|
98
|
+
|
|
99
|
+
assert.equal(result.modified, true);
|
|
100
|
+
assert.equal(existsSync(`${settingsPath}.bak`), true);
|
|
101
|
+
assert.deepEqual(result.removedServers, ["unsafe-stdio"]);
|
|
102
|
+
assert.equal(updated.mcpServers["tfx-hub"].url, "http://127.0.0.1:30123/mcp");
|
|
103
|
+
assert.equal(Object.hasOwn(updated.mcpServers, "unsafe-stdio"), false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("uses hub.pid port before registry fallback when resolving Hub URL", () => {
|
|
107
|
+
const homeDir = createHomeDir();
|
|
108
|
+
withHome(homeDir);
|
|
109
|
+
|
|
110
|
+
writeFileSync(
|
|
111
|
+
join(homeDir, ".claude", "cache", "tfx-hub", "hub.pid"),
|
|
112
|
+
JSON.stringify({ host: "127.0.0.1", port: 29991 }),
|
|
113
|
+
"utf8",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
assert.equal(resolveHubUrl(), "http://127.0.0.1:29991/mcp");
|
|
117
|
+
});
|
|
118
|
+
});
|