triflux 9.6.0 → 9.7.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/.claude-plugin/plugin.json +1 -1
- package/bin/triflux.mjs +535 -26
- package/hooks/hook-manager.mjs +6 -5
- package/hooks/hook-orchestrator.mjs +3 -2
- package/hooks/hook-registry.json +27 -5
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +85 -0
- package/hooks/pipeline-stop.mjs +9 -14
- 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/run.cjs +79 -62
- package/scripts/setup.mjs +306 -2
|
@@ -23,6 +23,7 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
23
23
|
import { join, dirname } from "node:path";
|
|
24
24
|
import { fileURLToPath } from "node:url";
|
|
25
25
|
import { execFileSync } from "node:child_process";
|
|
26
|
+
import { PLUGIN_ROOT } from "./lib/resolve-root.mjs";
|
|
26
27
|
|
|
27
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
28
29
|
const REGISTRY_PATH =
|
|
@@ -50,10 +51,10 @@ function loadRegistry() {
|
|
|
50
51
|
// ── 경로 변수 치환 ──────────────────────────────────────────
|
|
51
52
|
function resolveCommand(cmd) {
|
|
52
53
|
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
53
|
-
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || __dirname.replace(/[\\/]hooks$/, "");
|
|
54
54
|
|
|
55
55
|
return cmd
|
|
56
|
-
.replace(/\$\{PLUGIN_ROOT\}/g,
|
|
56
|
+
.replace(/\$\{PLUGIN_ROOT\}/g, PLUGIN_ROOT)
|
|
57
|
+
.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, PLUGIN_ROOT)
|
|
57
58
|
.replace(/\$\{HOME\}/g, home)
|
|
58
59
|
.replace(/\$HOME\b/g, home);
|
|
59
60
|
}
|
package/hooks/hook-registry.json
CHANGED
|
@@ -65,6 +65,17 @@
|
|
|
65
65
|
"timeout": 3,
|
|
66
66
|
"blocking": false,
|
|
67
67
|
"description": "파일 수정 추적 → 교차 리뷰 nudge"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"id": "tfx-mcp-config-watcher",
|
|
71
|
+
"source": "triflux",
|
|
72
|
+
"matcher": "Edit|Write",
|
|
73
|
+
"command": "node \"${PLUGIN_ROOT}/hooks/mcp-config-watcher.mjs\"",
|
|
74
|
+
"priority": 1,
|
|
75
|
+
"enabled": true,
|
|
76
|
+
"timeout": 3,
|
|
77
|
+
"blocking": false,
|
|
78
|
+
"description": "감시 대상 MCP 설정 변경 감지 → stdio MCP 자동 치환"
|
|
68
79
|
}
|
|
69
80
|
],
|
|
70
81
|
"PostToolUseFailure": [
|
|
@@ -105,12 +116,23 @@
|
|
|
105
116
|
"blocking": false,
|
|
106
117
|
"description": "triflux 환경 초기화"
|
|
107
118
|
},
|
|
119
|
+
{
|
|
120
|
+
"id": "tfx-mcp-safety-guard",
|
|
121
|
+
"source": "triflux",
|
|
122
|
+
"matcher": "*",
|
|
123
|
+
"command": "node \"${PLUGIN_ROOT}/scripts/mcp-safety-guard.mjs\"",
|
|
124
|
+
"priority": 1,
|
|
125
|
+
"enabled": true,
|
|
126
|
+
"timeout": 3,
|
|
127
|
+
"blocking": false,
|
|
128
|
+
"description": "Gemini stdio MCP 자동 감지 + 제거 (spawn EPERM 방지)"
|
|
129
|
+
},
|
|
108
130
|
{
|
|
109
131
|
"id": "tfx-hub-ensure",
|
|
110
132
|
"source": "triflux",
|
|
111
133
|
"matcher": "*",
|
|
112
134
|
"command": "node \"${PLUGIN_ROOT}/scripts/hub-ensure.mjs\"",
|
|
113
|
-
"priority":
|
|
135
|
+
"priority": 2,
|
|
114
136
|
"enabled": true,
|
|
115
137
|
"timeout": 8,
|
|
116
138
|
"blocking": false,
|
|
@@ -121,7 +143,7 @@
|
|
|
121
143
|
"source": "triflux",
|
|
122
144
|
"matcher": "*",
|
|
123
145
|
"command": "node \"${PLUGIN_ROOT}/scripts/preflight-cache.mjs\"",
|
|
124
|
-
"priority":
|
|
146
|
+
"priority": 3,
|
|
125
147
|
"enabled": true,
|
|
126
148
|
"timeout": 5,
|
|
127
149
|
"blocking": false,
|
|
@@ -133,7 +155,7 @@
|
|
|
133
155
|
"matcher": "*",
|
|
134
156
|
"command": "${HOME}/Desktop/Projects/cli/session-vault/scripts/start_hook.sh",
|
|
135
157
|
"priority": 100,
|
|
136
|
-
"enabled":
|
|
158
|
+
"enabled": false,
|
|
137
159
|
"timeout": 10,
|
|
138
160
|
"blocking": false,
|
|
139
161
|
"description": "세션 볼트 로깅 시작"
|
|
@@ -157,7 +179,7 @@
|
|
|
157
179
|
"matcher": "*",
|
|
158
180
|
"command": "${HOME}/Desktop/Projects/cli/session-vault/scripts/export_hook.sh",
|
|
159
181
|
"priority": 100,
|
|
160
|
-
"enabled":
|
|
182
|
+
"enabled": false,
|
|
161
183
|
"timeout": 30,
|
|
162
184
|
"blocking": false,
|
|
163
185
|
"description": "세션 트랜스크립트 내보내기"
|
|
@@ -168,7 +190,7 @@
|
|
|
168
190
|
"matcher": "*",
|
|
169
191
|
"command": "powershell -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File \"${HOME}/.claude/scripts/mcp-cleanup.ps1\"",
|
|
170
192
|
"priority": 100,
|
|
171
|
-
"enabled":
|
|
193
|
+
"enabled": false,
|
|
172
194
|
"timeout": 8,
|
|
173
195
|
"blocking": false,
|
|
174
196
|
"description": "MCP 고아 프로세스 정리"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const BREADCRUMB_PATH = join(homedir(), ".claude", "scripts", ".tfx-pkg-root");
|
|
7
|
+
|
|
8
|
+
function normalizeCandidate(candidate) {
|
|
9
|
+
if (typeof candidate !== "string") return null;
|
|
10
|
+
const value = candidate.trim();
|
|
11
|
+
if (!value) return null;
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isValidPluginRoot(candidate) {
|
|
16
|
+
const root = normalizeCandidate(candidate);
|
|
17
|
+
if (!root) return false;
|
|
18
|
+
return existsSync(join(root, "hooks", "hook-orchestrator.mjs"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toPluginRootFromUrl(url) {
|
|
22
|
+
if (typeof url !== "string" || !url) return null;
|
|
23
|
+
try {
|
|
24
|
+
const sourceDir = dirname(fileURLToPath(url));
|
|
25
|
+
const hookScoped = sourceDir.match(/^(.*?)[\\/]hooks(?:[\\/].*)?$/);
|
|
26
|
+
if (hookScoped?.[1]) return hookScoped[1];
|
|
27
|
+
return resolve(sourceDir, "..");
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readBreadcrumbRoot() {
|
|
34
|
+
if (!existsSync(BREADCRUMB_PATH)) return null;
|
|
35
|
+
try {
|
|
36
|
+
return normalizeCandidate(readFileSync(BREADCRUMB_PATH, "utf8"));
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolvePluginRoot(callerUrl) {
|
|
43
|
+
const breadcrumbRoot = readBreadcrumbRoot();
|
|
44
|
+
if (isValidPluginRoot(breadcrumbRoot)) return breadcrumbRoot;
|
|
45
|
+
|
|
46
|
+
const envRoot = normalizeCandidate(process.env.CLAUDE_PLUGIN_ROOT);
|
|
47
|
+
if (isValidPluginRoot(envRoot)) return envRoot;
|
|
48
|
+
|
|
49
|
+
const callerFallback = toPluginRootFromUrl(callerUrl);
|
|
50
|
+
if (isValidPluginRoot(callerFallback)) return callerFallback;
|
|
51
|
+
|
|
52
|
+
const moduleFallback = toPluginRootFromUrl(import.meta.url) || process.cwd();
|
|
53
|
+
process.stderr.write(
|
|
54
|
+
`[resolve-root] warning: failed to resolve plugin root from breadcrumb/env/caller; fallback=${moduleFallback}\n`
|
|
55
|
+
);
|
|
56
|
+
return moduleFallback;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const PLUGIN_ROOT = resolvePluginRoot(import.meta.url);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// hooks/mcp-config-watcher.mjs — PostToolUse:Edit|Write 훅
|
|
3
|
+
//
|
|
4
|
+
// 감시 대상 MCP 설정 파일 변경을 감지해 stdio 서버를 즉시 차단/치환한다.
|
|
5
|
+
// 경로가 watched_paths와 매칭되지 않으면 바로 종료해 일반 편집 성능에 영향이 없도록 한다.
|
|
6
|
+
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import {
|
|
9
|
+
isWatchedPath,
|
|
10
|
+
loadRegistry,
|
|
11
|
+
remediate,
|
|
12
|
+
scanForStdioServers,
|
|
13
|
+
} from "../scripts/lib/mcp-guard-engine.mjs";
|
|
14
|
+
|
|
15
|
+
function readStdin() {
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(0, "utf8");
|
|
18
|
+
} catch {
|
|
19
|
+
return "";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildSystemMessage(filePath, stdioServers, result) {
|
|
24
|
+
const lines = [`[mcp-guard] 감시 대상 MCP 설정 변경 감지: ${filePath}`];
|
|
25
|
+
|
|
26
|
+
if (result.modified) {
|
|
27
|
+
const actionLabel = result.replacement ? "자동 치환" : "자동 제거";
|
|
28
|
+
lines.push(`[mcp-guard] stdio MCP ${actionLabel}: ${stdioServers.map((server) => server.name).join(", ")}`);
|
|
29
|
+
|
|
30
|
+
if (result.replacement?.name && result.replacement?.url) {
|
|
31
|
+
lines.push(`[mcp-guard] 대체 서버: ${result.replacement.name} -> ${result.replacement.url}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (result.backupPath) {
|
|
35
|
+
lines.push(`[mcp-guard] 백업: ${result.backupPath}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const warning of result.warnings || []) {
|
|
40
|
+
lines.push(warning);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return lines.length > 0 ? lines.join("\n") : "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function main() {
|
|
47
|
+
const raw = readStdin();
|
|
48
|
+
if (!raw.trim()) process.exit(0);
|
|
49
|
+
|
|
50
|
+
let input;
|
|
51
|
+
try {
|
|
52
|
+
input = JSON.parse(raw);
|
|
53
|
+
} catch {
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const toolName = input.tool_name || "";
|
|
58
|
+
if (toolName !== "Edit" && toolName !== "Write") process.exit(0);
|
|
59
|
+
|
|
60
|
+
const filePath = input.tool_input?.file_path || "";
|
|
61
|
+
if (!filePath || !isWatchedPath(filePath)) process.exit(0);
|
|
62
|
+
|
|
63
|
+
let registry;
|
|
64
|
+
try {
|
|
65
|
+
registry = loadRegistry();
|
|
66
|
+
} catch {
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const stdioServers = scanForStdioServers(filePath);
|
|
71
|
+
if (stdioServers.length === 0) process.exit(0);
|
|
72
|
+
|
|
73
|
+
const result = remediate(filePath, stdioServers, registry.policies);
|
|
74
|
+
const systemMessage = buildSystemMessage(filePath, stdioServers, result);
|
|
75
|
+
|
|
76
|
+
if (systemMessage) {
|
|
77
|
+
process.stdout.write(JSON.stringify({ systemMessage }));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
main();
|
|
83
|
+
} catch {
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
package/hooks/pipeline-stop.mjs
CHANGED
|
@@ -6,20 +6,23 @@
|
|
|
6
6
|
// 파이프라인이 없으면 정상 종료를 허용한다.
|
|
7
7
|
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
|
-
import {
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { PLUGIN_ROOT } from "./lib/resolve-root.mjs";
|
|
11
10
|
|
|
12
11
|
let getPipelineStateDbPath;
|
|
12
|
+
let ensurePipelineTable;
|
|
13
|
+
let listPipelineStates;
|
|
13
14
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
({
|
|
16
|
+
getPipelineStateDbPath,
|
|
17
|
+
ensurePipelineTable,
|
|
18
|
+
listPipelineStates,
|
|
19
|
+
} = await import("../hub/pipeline/state.mjs"));
|
|
16
20
|
} catch {
|
|
17
21
|
// hub/pipeline 모듈 없으면 훅 무동작
|
|
18
22
|
process.exit(0);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
const
|
|
22
|
-
const HUB_DB_PATH = getPipelineStateDbPath(PROJECT_ROOT);
|
|
25
|
+
const HUB_DB_PATH = getPipelineStateDbPath(PLUGIN_ROOT);
|
|
23
26
|
const TERMINAL = new Set(["complete", "failed"]);
|
|
24
27
|
|
|
25
28
|
async function checkActivePipelines() {
|
|
@@ -27,14 +30,6 @@ async function checkActivePipelines() {
|
|
|
27
30
|
|
|
28
31
|
try {
|
|
29
32
|
const { default: Database } = await import("better-sqlite3");
|
|
30
|
-
const { ensurePipelineTable, listPipelineStates } = await import(
|
|
31
|
-
join(
|
|
32
|
-
process.env.CLAUDE_PLUGIN_ROOT || PROJECT_ROOT,
|
|
33
|
-
"hub",
|
|
34
|
-
"pipeline",
|
|
35
|
-
"state.mjs"
|
|
36
|
-
)
|
|
37
|
-
);
|
|
38
33
|
|
|
39
34
|
const db = new Database(HUB_DB_PATH, { readonly: true });
|
|
40
35
|
ensurePipelineTable(db);
|
package/package.json
CHANGED
|
@@ -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
|
+
});
|