@triflux/core 10.1.0 → 10.25.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/hooks/agent-route-guard.mjs +14 -10
- package/hooks/cross-review-tracker.mjs +21 -8
- package/hooks/error-context.mjs +20 -8
- package/hooks/hook-adaptive-collector.mjs +18 -16
- package/hooks/hook-manager.mjs +93 -32
- package/hooks/hook-orchestrator.mjs +267 -35
- package/hooks/hook-registry.json +343 -256
- package/hooks/hooks.json +49 -1
- package/hooks/keyword-rules.json +183 -23
- package/hooks/lib/resolve-root.mjs +1 -1
- package/hooks/mcp-config-watcher.mjs +6 -2
- package/hooks/permission-safe-allow.mjs +233 -0
- package/hooks/pipeline-stop.mjs +369 -22
- package/hooks/post-tool-tips.mjs +306 -0
- package/hooks/pre-compact-snapshot.mjs +181 -0
- package/hooks/safety-guard.mjs +254 -33
- package/hooks/session-end-cleanup.mjs +345 -0
- package/hooks/session-start-fast.mjs +214 -0
- package/hooks/subagent-tracker.mjs +267 -0
- package/hooks/subagent-verifier.mjs +9 -4
- package/hub/account-broker.mjs +1036 -66
- package/hub/adaptive-diagnostic.mjs +75 -48
- package/hub/adaptive-inject.mjs +96 -58
- package/hub/adaptive-memory.mjs +156 -42
- package/hub/adaptive.mjs +60 -31
- package/hub/assign-callbacks.mjs +163 -126
- package/hub/bridge.mjs +831 -290
- package/hub/cli-adapter-base.mjs +321 -97
- package/hub/codex-adapter.mjs +179 -93
- package/hub/codex-compat.mjs +4 -4
- package/hub/codex-preflight.mjs +113 -37
- package/hub/delegator/contracts.mjs +19 -23
- package/hub/delegator/index.mjs +14 -14
- package/hub/delegator/service.mjs +88 -64
- package/hub/delegator/tool-definitions.mjs +35 -35
- package/hub/dynamic-routing-engine.mjs +511 -0
- package/hub/fullcycle.mjs +34 -18
- package/hub/gemini-adapter.mjs +77 -95
- package/hub/hitl.mjs +202 -143
- package/hub/intent.mjs +167 -38
- package/hub/lib/bash-path.mjs +73 -0
- package/hub/lib/cache-guard.mjs +43 -17
- package/hub/lib/env-detect.mjs +145 -0
- package/hub/lib/hosts-compat.mjs +319 -0
- package/hub/lib/mcp-response-cache.mjs +239 -0
- package/hub/lib/memory-store.mjs +285 -111
- package/hub/lib/path-utils.mjs +165 -0
- package/hub/lib/phase-manager.mjs +493 -0
- package/hub/lib/process-utils.mjs +797 -104
- package/hub/lib/prompt-tmp.mjs +33 -0
- package/hub/lib/risk-tier.mjs +53 -0
- package/hub/lib/spawn-trace.mjs +561 -0
- package/hub/lib/ssh-command.mjs +44 -24
- package/hub/lib/ssh-retry.mjs +5 -1
- package/hub/lib/staged-file-detect.mjs +43 -0
- package/hub/lib/state-snapshot.mjs +293 -0
- package/hub/lib/tfx-route-args.mjs +309 -0
- package/hub/lib/timeline-adapter.mjs +190 -0
- package/hub/lib/trace-recorder.mjs +153 -0
- package/hub/lib/uuidv7.mjs +4 -3
- package/hub/memory-doctor.mjs +266 -106
- package/hub/middleware/quota-middleware.mjs +70 -0
- package/hub/middleware/request-logger.mjs +61 -34
- package/hub/paths.mjs +9 -9
- package/hub/pipeline/gates/confidence.mjs +34 -15
- package/hub/pipeline/gates/consensus.mjs +27 -15
- package/hub/pipeline/gates/index.mjs +7 -3
- package/hub/pipeline/gates/selfcheck.mjs +57 -19
- package/hub/pipeline/index.mjs +77 -42
- package/hub/pipeline/state.mjs +10 -10
- package/hub/pipeline/transitions.mjs +40 -23
- package/hub/platform.mjs +65 -48
- package/hub/quality/deslop.mjs +70 -49
- package/hub/reflexion.mjs +87 -13
- package/hub/research.mjs +33 -25
- package/hub/router.mjs +941 -791
- package/hub/routing/complexity.mjs +132 -29
- package/hub/routing/index.mjs +17 -12
- package/hub/routing/q-learning.mjs +76 -28
- package/hub/routing-snapshot.mjs +261 -0
- package/hub/session-fingerprint.mjs +127 -61
- package/hub/state.mjs +86 -44
- package/hub/team/retry-state-machine.mjs +267 -0
- package/hub/token-mode.mjs +72 -71
- package/hub/workers/worker-utils.mjs +22 -16
- package/hud/colors.mjs +27 -9
- package/hud/constants.mjs +184 -27
- package/hud/context-monitor.mjs +133 -44
- package/hud/hud-qos-status.mjs +153 -50
- package/hud/mission-board.mjs +56 -0
- package/hud/providers/claude.mjs +398 -114
- package/hud/providers/codex.mjs +96 -21
- package/hud/providers/gemini.mjs +184 -51
- package/hud/renderers.mjs +413 -123
- package/hud/terminal.mjs +101 -31
- package/hud/utils.mjs +81 -39
- package/mesh/index.mjs +69 -0
- package/mesh/mesh-budget.mjs +137 -0
- package/mesh/mesh-heartbeat.mjs +100 -0
- package/mesh/mesh-protocol.mjs +96 -0
- package/mesh/mesh-queue.mjs +163 -0
- package/mesh/mesh-registry.mjs +78 -0
- package/mesh/mesh-router.mjs +77 -0
- package/package.json +5 -1
- package/scripts/lib/agent-json.mjs +27 -0
- package/scripts/lib/async.mjs +174 -0
- package/scripts/lib/claudemd-scanner.mjs +6 -1
- package/scripts/lib/cli-agy.mjs +62 -0
- package/scripts/lib/cli-claude.mjs +78 -0
- package/scripts/lib/cli-codex.mjs +199 -0
- package/scripts/lib/cli-gemini.mjs +67 -0
- package/scripts/lib/codex-recovery.sh +50 -0
- package/scripts/lib/context.mjs +3 -3
- package/scripts/lib/cross-review-utils.mjs +13 -5
- package/scripts/lib/doctor-env-checks.mjs +121 -0
- package/scripts/lib/dynamic-route-cli.mjs +107 -0
- package/scripts/lib/env-probe.mjs +58 -30
- package/scripts/lib/env.mjs +14 -0
- package/scripts/lib/gemini-profiles.mjs +44 -10
- package/scripts/lib/handoff.mjs +33 -17
- package/scripts/lib/hook-utils.mjs +8 -6
- package/scripts/lib/hub.mjs +48 -0
- package/scripts/lib/keyword-rules.mjs +48 -19
- package/scripts/lib/logger.mjs +24 -24
- package/scripts/lib/mcp-filter.mjs +391 -247
- package/scripts/lib/mcp-gateway-health-check.mjs +159 -0
- package/scripts/lib/mcp-gateway-servers.mjs +70 -0
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +35 -0
- package/scripts/lib/mcp-guard-engine.mjs +1281 -115
- package/scripts/lib/mcp-health.mjs +773 -0
- package/scripts/lib/mcp-manifest.mjs +24 -14
- package/scripts/lib/mcp-server-catalog.mjs +355 -118
- package/scripts/lib/pid.mjs +63 -0
- package/scripts/lib/process-utils.mjs +26 -0
- package/scripts/lib/psmux-info.mjs +70 -10
- package/scripts/lib/quota.mjs +47 -0
- package/scripts/lib/remote-spawn-transfer.mjs +44 -14
- package/scripts/lib/skill-state.mjs +220 -0
- package/scripts/lib/skill-template.mjs +32 -7
- package/scripts/lib/team.mjs +75 -0
- package/scripts/lib/timeout.mjs +66 -0
- package/scripts/lib/tmp.mjs +26 -0
- package/scripts/lib/toml.mjs +113 -0
|
@@ -1,12 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
copyFileSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readdirSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
2
10
|
import { homedir } from "node:os";
|
|
3
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
basename,
|
|
13
|
+
dirname,
|
|
14
|
+
isAbsolute,
|
|
15
|
+
join,
|
|
16
|
+
relative,
|
|
17
|
+
resolve,
|
|
18
|
+
} from "node:path";
|
|
4
19
|
import { fileURLToPath } from "node:url";
|
|
5
20
|
|
|
6
21
|
const PROJECT_ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
|
7
|
-
const
|
|
22
|
+
const DEFAULT_REGISTRY_PATH = join(PROJECT_ROOT, "config", "mcp-registry.json");
|
|
8
23
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
9
24
|
const DEFAULT_HUB_PATH = "/mcp";
|
|
25
|
+
const SERVER_POLICIES = new Set(["hosted", "gateway-sse", "stdio"]);
|
|
26
|
+
const GATEWAY_PORTS = Object.freeze({
|
|
27
|
+
context7: 8100,
|
|
28
|
+
"brave-search": 8101,
|
|
29
|
+
exa: 8102,
|
|
30
|
+
tavily: 8103,
|
|
31
|
+
jira: 8104,
|
|
32
|
+
serena: 8105,
|
|
33
|
+
notion: 8106,
|
|
34
|
+
"notion-guest": 8107,
|
|
35
|
+
});
|
|
10
36
|
const DEFAULT_REGISTRY = Object.freeze({
|
|
11
37
|
$schema: "mcp-registry-schema",
|
|
12
38
|
version: 1,
|
|
@@ -15,24 +41,37 @@ const DEFAULT_REGISTRY = Object.freeze({
|
|
|
15
41
|
transport: "hub-url",
|
|
16
42
|
hub_base: "http://127.0.0.1:27888",
|
|
17
43
|
},
|
|
44
|
+
policy_notes: {
|
|
45
|
+
transport:
|
|
46
|
+
'Server transport accepts "hub-url" for triflux Hub URL flow, "http" for direct Streamable HTTP MCP endpoints, or "stdio" for upstream-stdio-only MCP servers (rare; only for servers that have no hosted HTTP endpoint, e.g. brave-search). stdio servers require command, args, and may include env.',
|
|
47
|
+
headers:
|
|
48
|
+
'Optional headers are allowed only for HTTP-compatible transports. Each header value must be a descriptor: {"value":"literal"} for non-secret static values, {"env":"ENV_VAR_NAME"} for secrets resolved at sync/runtime, or {"env":"ENV_VAR_NAME","prefix":"Bearer "} for common authorization formats.',
|
|
49
|
+
secret_safety:
|
|
50
|
+
"Resolved secret values must not be written back to this registry file. Missing env vars warn during sync and do not emit empty secret headers.",
|
|
51
|
+
},
|
|
18
52
|
servers: {
|
|
19
53
|
"tfx-hub": {
|
|
54
|
+
policy: "hosted",
|
|
20
55
|
transport: "hub-url",
|
|
21
56
|
url: "http://127.0.0.1:27888/mcp",
|
|
22
57
|
safe: true,
|
|
23
|
-
targets: ["claude", "gemini", "codex"],
|
|
58
|
+
targets: ["claude", "gemini", "codex", "antigravity"],
|
|
24
59
|
description: "triflux Hub MCP 서버",
|
|
25
60
|
},
|
|
26
61
|
},
|
|
27
62
|
policies: {
|
|
28
63
|
stdio_action: "replace-with-hub",
|
|
29
64
|
unknown_server_action: "warn",
|
|
65
|
+
sync_denylist: [],
|
|
30
66
|
watched_paths: [
|
|
31
67
|
"~/.gemini/settings.json",
|
|
32
68
|
"~/.codex/config.toml",
|
|
69
|
+
"~/.claude.json",
|
|
33
70
|
"~/.claude/settings.json",
|
|
34
71
|
"~/.claude/settings.local.json",
|
|
72
|
+
".claude/mcp.json",
|
|
35
73
|
".mcp.json",
|
|
74
|
+
"~/.gemini/config/mcp_config.json",
|
|
36
75
|
],
|
|
37
76
|
},
|
|
38
77
|
});
|
|
@@ -43,13 +82,39 @@ function cloneDefaultRegistry() {
|
|
|
43
82
|
|
|
44
83
|
function expandHome(filePath) {
|
|
45
84
|
if (typeof filePath !== "string") return "";
|
|
46
|
-
if (!filePath.startsWith("~/") && !filePath.startsWith("~\\"))
|
|
85
|
+
if (!filePath.startsWith("~/") && !filePath.startsWith("~\\"))
|
|
86
|
+
return filePath;
|
|
47
87
|
return join(homedir(), filePath.slice(2));
|
|
48
88
|
}
|
|
49
89
|
|
|
50
90
|
function resolveFilePath(filePath) {
|
|
51
91
|
const expanded = expandHome(filePath);
|
|
52
|
-
return isAbsolute(expanded)
|
|
92
|
+
return isAbsolute(expanded)
|
|
93
|
+
? resolve(expanded)
|
|
94
|
+
: resolve(process.cwd(), expanded);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function defaultProjectsRoot() {
|
|
98
|
+
return process.env.TFX_MCP_PROJECTS_ROOT
|
|
99
|
+
? resolve(expandHome(process.env.TFX_MCP_PROJECTS_ROOT))
|
|
100
|
+
: join(homedir(), "Projects");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function globToRegExp(glob) {
|
|
104
|
+
const source = String(glob || "")
|
|
105
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
106
|
+
.replace(/\*/g, ".*")
|
|
107
|
+
.replace(/\?/g, ".");
|
|
108
|
+
return new RegExp(`^${source}$`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isExcludedProjectPath(relativePath, excludePatterns = []) {
|
|
112
|
+
const normalized = relativePath.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
113
|
+
const basenameValue = basename(normalized);
|
|
114
|
+
return excludePatterns.some((pattern) => {
|
|
115
|
+
const re = globToRegExp(pattern);
|
|
116
|
+
return re.test(normalized) || re.test(basenameValue);
|
|
117
|
+
});
|
|
53
118
|
}
|
|
54
119
|
|
|
55
120
|
function normalizeForMatch(filePath) {
|
|
@@ -60,6 +125,14 @@ function pathBasename(filePath) {
|
|
|
60
125
|
return basename(filePath.replace(/\\/g, "/")).toLowerCase();
|
|
61
126
|
}
|
|
62
127
|
|
|
128
|
+
function isClaudeProjectMcpConfig(filePath) {
|
|
129
|
+
return normalizeForMatch(filePath).endsWith("/.claude/mcp.json");
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isClaudeUserConfig(filePath) {
|
|
133
|
+
return normalizeForMatch(filePath).endsWith("/.claude.json");
|
|
134
|
+
}
|
|
135
|
+
|
|
63
136
|
function readJsonFile(filePath) {
|
|
64
137
|
return JSON.parse(readFileSync(filePath, "utf8"));
|
|
65
138
|
}
|
|
@@ -69,6 +142,12 @@ function writeJsonFile(filePath, data) {
|
|
|
69
142
|
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf8");
|
|
70
143
|
}
|
|
71
144
|
|
|
145
|
+
function registryPath() {
|
|
146
|
+
return process.env.TFX_MCP_REGISTRY_PATH
|
|
147
|
+
? resolve(process.env.TFX_MCP_REGISTRY_PATH)
|
|
148
|
+
: DEFAULT_REGISTRY_PATH;
|
|
149
|
+
}
|
|
150
|
+
|
|
72
151
|
function ensureBackup(filePath) {
|
|
73
152
|
const backupPath = `${filePath}.bak`;
|
|
74
153
|
copyFileSync(filePath, backupPath);
|
|
@@ -77,7 +156,15 @@ function ensureBackup(filePath) {
|
|
|
77
156
|
|
|
78
157
|
function isJsonMcpConfig(filePath) {
|
|
79
158
|
const name = pathBasename(filePath);
|
|
80
|
-
return
|
|
159
|
+
return (
|
|
160
|
+
isAntigravityConfig(filePath) ||
|
|
161
|
+
isClaudeUserConfig(filePath) ||
|
|
162
|
+
name === "settings.json" ||
|
|
163
|
+
name === "settings.local.json" ||
|
|
164
|
+
name === "mcp_config.json" ||
|
|
165
|
+
name === ".mcp.json" ||
|
|
166
|
+
isClaudeProjectMcpConfig(filePath)
|
|
167
|
+
);
|
|
81
168
|
}
|
|
82
169
|
|
|
83
170
|
function isCodexConfig(filePath) {
|
|
@@ -85,14 +172,59 @@ function isCodexConfig(filePath) {
|
|
|
85
172
|
return normalized.endsWith("/.codex/config.toml");
|
|
86
173
|
}
|
|
87
174
|
|
|
175
|
+
function isAntigravityConfig(filePath) {
|
|
176
|
+
const normalized = normalizeForMatch(filePath);
|
|
177
|
+
return normalized.endsWith("/.gemini/config/mcp_config.json");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function plaintextSecretHeaderClient(filePath) {
|
|
181
|
+
const client = detectClient(filePath);
|
|
182
|
+
return client === "gemini" || client === "antigravity" ? client : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function plaintextSecretPermissionHint(filePath) {
|
|
186
|
+
const normalized = normalizeForMatch(filePath);
|
|
187
|
+
if (normalized.endsWith("/.gemini/settings.json")) {
|
|
188
|
+
return "~/.gemini/settings.json";
|
|
189
|
+
}
|
|
190
|
+
if (normalized.endsWith("/.gemini/config/mcp_config.json")) {
|
|
191
|
+
return "~/.gemini/config/mcp_config.json";
|
|
192
|
+
}
|
|
193
|
+
return filePath;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function shouldLockJsonConfigPermissions(filePath) {
|
|
197
|
+
return Boolean(plaintextSecretHeaderClient(filePath));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function lockJsonConfigPermissions(filePath) {
|
|
201
|
+
if (!shouldLockJsonConfigPermissions(filePath)) return;
|
|
202
|
+
chmodSync(filePath, 0o600);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isProtectedCodexConfigMutationEnv(env = process.env) {
|
|
206
|
+
return env.NODE_ENV === "test" || env.CI === "true" || env.TFX_TEST === "1";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function shouldSkipCodexConfigMutation() {
|
|
210
|
+
return (
|
|
211
|
+
process.env.TFX_CODEX_CONFIG_SYNC !== "1" &&
|
|
212
|
+
isProtectedCodexConfigMutationEnv()
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
88
216
|
function detectClient(filePath) {
|
|
89
217
|
const normalized = normalizeForMatch(filePath);
|
|
218
|
+
if (normalized.endsWith("/.gemini/config/mcp_config.json"))
|
|
219
|
+
return "antigravity";
|
|
90
220
|
if (normalized.endsWith("/.gemini/settings.json")) return "gemini";
|
|
91
221
|
if (normalized.endsWith("/.codex/config.toml")) return "codex";
|
|
92
222
|
if (
|
|
93
|
-
normalized.endsWith("/.claude
|
|
94
|
-
|
|
95
|
-
|
|
223
|
+
normalized.endsWith("/.claude.json") ||
|
|
224
|
+
normalized.endsWith("/.claude/settings.json") ||
|
|
225
|
+
normalized.endsWith("/.claude/settings.local.json") ||
|
|
226
|
+
normalized.endsWith("/.claude/mcp.json") ||
|
|
227
|
+
normalized.endsWith("/.mcp.json")
|
|
96
228
|
) {
|
|
97
229
|
return "claude";
|
|
98
230
|
}
|
|
@@ -101,19 +233,31 @@ function detectClient(filePath) {
|
|
|
101
233
|
|
|
102
234
|
function detectLabel(filePath) {
|
|
103
235
|
const normalized = normalizeForMatch(filePath);
|
|
236
|
+
if (normalized.endsWith("/.gemini/config/mcp_config.json"))
|
|
237
|
+
return "Antigravity";
|
|
104
238
|
if (normalized.endsWith("/.gemini/settings.json")) return "Gemini";
|
|
239
|
+
if (normalized.endsWith("/.gemini/config/mcp_config.json"))
|
|
240
|
+
return "Antigravity";
|
|
105
241
|
if (normalized.endsWith("/.codex/config.toml")) return "Codex";
|
|
242
|
+
if (normalized.endsWith("/.claude.json")) return "Claude User MCP";
|
|
106
243
|
if (normalized.endsWith("/.claude/settings.json")) return "Claude User";
|
|
107
|
-
if (normalized.endsWith("/.claude/settings.local.json"))
|
|
244
|
+
if (normalized.endsWith("/.claude/settings.local.json"))
|
|
245
|
+
return "Claude Local";
|
|
246
|
+
if (normalized.endsWith("/.claude/mcp.json")) return "Claude Project MCP";
|
|
108
247
|
if (normalized.endsWith("/.mcp.json")) return "Project MCP";
|
|
109
248
|
return basename(filePath);
|
|
110
249
|
}
|
|
111
250
|
|
|
112
251
|
function isPrimaryConfigTarget(filePath) {
|
|
113
252
|
const normalized = normalizeForMatch(filePath);
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
253
|
+
return (
|
|
254
|
+
normalized.endsWith("/.gemini/settings.json") ||
|
|
255
|
+
normalized.endsWith("/.gemini/config/mcp_config.json") ||
|
|
256
|
+
normalized.endsWith("/.codex/config.toml") ||
|
|
257
|
+
normalized.endsWith("/.claude.json") ||
|
|
258
|
+
normalized.endsWith("/.claude/mcp.json") ||
|
|
259
|
+
normalized.endsWith("/.mcp.json")
|
|
260
|
+
);
|
|
117
261
|
}
|
|
118
262
|
|
|
119
263
|
function normalizeUrl(value) {
|
|
@@ -139,12 +283,95 @@ function parseTomlScalar(rawValue) {
|
|
|
139
283
|
if (value === "true") return true;
|
|
140
284
|
if (value === "false") return false;
|
|
141
285
|
if (/^-?\d[\d_]*$/.test(value)) return Number(value.replace(/_/g, ""));
|
|
286
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
287
|
+
return parseTomlArray(value);
|
|
288
|
+
}
|
|
289
|
+
if (value.startsWith("{") && value.endsWith("}")) {
|
|
290
|
+
return parseTomlInlineTable(value);
|
|
291
|
+
}
|
|
142
292
|
if (value.startsWith('"') && value.endsWith('"')) {
|
|
143
293
|
return value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
144
294
|
}
|
|
145
295
|
return value;
|
|
146
296
|
}
|
|
147
297
|
|
|
298
|
+
function parseTomlArray(rawValue) {
|
|
299
|
+
const source = String(rawValue || "").trim();
|
|
300
|
+
const body = source.slice(1, -1).trim();
|
|
301
|
+
if (!body) return [];
|
|
302
|
+
|
|
303
|
+
const parts = [];
|
|
304
|
+
let current = "";
|
|
305
|
+
let inString = false;
|
|
306
|
+
let escaped = false;
|
|
307
|
+
for (const char of body) {
|
|
308
|
+
if (escaped) {
|
|
309
|
+
current += char;
|
|
310
|
+
escaped = false;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
if (char === "\\" && inString) {
|
|
314
|
+
current += char;
|
|
315
|
+
escaped = true;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (char === '"') inString = !inString;
|
|
319
|
+
if (char === "," && !inString) {
|
|
320
|
+
parts.push(current.trim());
|
|
321
|
+
current = "";
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
current += char;
|
|
325
|
+
}
|
|
326
|
+
if (current.trim()) parts.push(current.trim());
|
|
327
|
+
|
|
328
|
+
return parts.map((part) => parseTomlScalar(part));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseTomlInlineTable(rawValue) {
|
|
332
|
+
const source = String(rawValue || "").trim();
|
|
333
|
+
const body = source.slice(1, -1).trim();
|
|
334
|
+
if (!body) return {};
|
|
335
|
+
|
|
336
|
+
const parts = [];
|
|
337
|
+
let current = "";
|
|
338
|
+
let inString = false;
|
|
339
|
+
let escaped = false;
|
|
340
|
+
for (const char of body) {
|
|
341
|
+
if (escaped) {
|
|
342
|
+
current += char;
|
|
343
|
+
escaped = false;
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (char === "\\" && inString) {
|
|
347
|
+
current += char;
|
|
348
|
+
escaped = true;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (char === '"') inString = !inString;
|
|
352
|
+
if (char === "," && !inString) {
|
|
353
|
+
parts.push(current.trim());
|
|
354
|
+
current = "";
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
current += char;
|
|
358
|
+
}
|
|
359
|
+
if (current.trim()) parts.push(current.trim());
|
|
360
|
+
|
|
361
|
+
const table = {};
|
|
362
|
+
for (const part of parts) {
|
|
363
|
+
const match = part.match(
|
|
364
|
+
/^\s*(?:"((?:\\"|[^"])*)"|([A-Za-z0-9_-]+))\s*=\s*(.+?)\s*$/,
|
|
365
|
+
);
|
|
366
|
+
if (!match) continue;
|
|
367
|
+
const key = (match[1] || match[2] || "")
|
|
368
|
+
.replace(/\\"/g, '"')
|
|
369
|
+
.replace(/\\\\/g, "\\");
|
|
370
|
+
table[key] = parseTomlScalar(match[3]);
|
|
371
|
+
}
|
|
372
|
+
return table;
|
|
373
|
+
}
|
|
374
|
+
|
|
148
375
|
function parseCodexMcpServers(raw) {
|
|
149
376
|
const lines = String(raw || "").split(/\r?\n/);
|
|
150
377
|
const servers = {};
|
|
@@ -173,6 +400,231 @@ function parseCodexMcpServers(raw) {
|
|
|
173
400
|
return servers;
|
|
174
401
|
}
|
|
175
402
|
|
|
403
|
+
function isValidHeaderName(name) {
|
|
404
|
+
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(String(name || ""));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function isValidEnvName(name) {
|
|
408
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(String(name || ""));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function normalizeHeaderDescriptors(headers = {}) {
|
|
412
|
+
const normalized = {};
|
|
413
|
+
if (!headers || typeof headers !== "object" || Array.isArray(headers)) {
|
|
414
|
+
return normalized;
|
|
415
|
+
}
|
|
416
|
+
for (const [name, descriptor] of Object.entries(headers)) {
|
|
417
|
+
if (!isValidHeaderName(name)) continue;
|
|
418
|
+
if (
|
|
419
|
+
descriptor &&
|
|
420
|
+
typeof descriptor === "object" &&
|
|
421
|
+
!Array.isArray(descriptor)
|
|
422
|
+
) {
|
|
423
|
+
if (
|
|
424
|
+
Object.hasOwn(descriptor, "value") &&
|
|
425
|
+
typeof descriptor.value === "string"
|
|
426
|
+
) {
|
|
427
|
+
normalized[name] = { value: descriptor.value };
|
|
428
|
+
} else if (
|
|
429
|
+
Object.hasOwn(descriptor, "env") &&
|
|
430
|
+
typeof descriptor.env === "string" &&
|
|
431
|
+
descriptor.env.trim()
|
|
432
|
+
) {
|
|
433
|
+
normalized[name] = { env: descriptor.env.trim() };
|
|
434
|
+
if (typeof descriptor.prefix === "string") {
|
|
435
|
+
normalized[name].prefix = descriptor.prefix;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return normalized;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function normalizeEnvDescriptors(env = {}) {
|
|
444
|
+
const normalized = {};
|
|
445
|
+
if (!env || typeof env !== "object" || Array.isArray(env)) {
|
|
446
|
+
return normalized;
|
|
447
|
+
}
|
|
448
|
+
for (const [name, descriptor] of Object.entries(env)) {
|
|
449
|
+
if (!isValidEnvName(name)) continue;
|
|
450
|
+
if (
|
|
451
|
+
descriptor &&
|
|
452
|
+
typeof descriptor === "object" &&
|
|
453
|
+
!Array.isArray(descriptor) &&
|
|
454
|
+
Object.hasOwn(descriptor, "env") &&
|
|
455
|
+
typeof descriptor.env === "string" &&
|
|
456
|
+
descriptor.env.trim()
|
|
457
|
+
) {
|
|
458
|
+
normalized[name] = { env: descriptor.env.trim() };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return normalized;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function resolveEnvDescriptors(name, serverConfig) {
|
|
465
|
+
const descriptors = normalizeEnvDescriptors(serverConfig?.env || {});
|
|
466
|
+
const env = {};
|
|
467
|
+
const warnings = [];
|
|
468
|
+
|
|
469
|
+
for (const [envKey, descriptor] of Object.entries(descriptors)) {
|
|
470
|
+
const envName = descriptor.env;
|
|
471
|
+
const envValue = process.env[envName];
|
|
472
|
+
if (typeof envValue !== "string" || envValue.length === 0) {
|
|
473
|
+
warnings.push(
|
|
474
|
+
`[mcp-guard] ${name}.${envKey} env skipped: env ${envName} is not set`,
|
|
475
|
+
);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
env[envKey] = envValue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return { descriptors, env, warnings };
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function resolveHeaderDescriptors(name, serverConfig, filePath) {
|
|
485
|
+
const descriptors = normalizeHeaderDescriptors(serverConfig?.headers || {});
|
|
486
|
+
const headers = {};
|
|
487
|
+
const warnings = [];
|
|
488
|
+
const codex = {
|
|
489
|
+
bearer_token_env_var: null,
|
|
490
|
+
env_http_headers: {},
|
|
491
|
+
http_headers: {},
|
|
492
|
+
};
|
|
493
|
+
const codexTarget = isCodexConfig(filePath);
|
|
494
|
+
|
|
495
|
+
for (const [headerName, descriptor] of Object.entries(descriptors)) {
|
|
496
|
+
if (Object.hasOwn(descriptor, "value")) {
|
|
497
|
+
headers[headerName] = descriptor.value;
|
|
498
|
+
if (codexTarget) codex.http_headers[headerName] = descriptor.value;
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const envName = descriptor.env;
|
|
503
|
+
const envValue = process.env[envName];
|
|
504
|
+
const prefix = descriptor.prefix || "";
|
|
505
|
+
if (typeof envValue !== "string" || envValue.length === 0) {
|
|
506
|
+
warnings.push(
|
|
507
|
+
`[mcp-guard] ${name}.${headerName} header skipped: env ${envName} is not set`,
|
|
508
|
+
);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
headers[headerName] = `${prefix}${envValue}`;
|
|
513
|
+
|
|
514
|
+
const plaintextClient = plaintextSecretHeaderClient(filePath);
|
|
515
|
+
if (plaintextClient) {
|
|
516
|
+
warnings.push(
|
|
517
|
+
`[tfx mcp sync] warn: ${plaintextClient} server '${name}' headers contain plaintext secret resolved from $${envName}. Ensure ${plaintextSecretPermissionHint(filePath)} permissions stay 600.`,
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (!codexTarget) continue;
|
|
522
|
+
|
|
523
|
+
if (
|
|
524
|
+
headerName.toLowerCase() === "authorization" &&
|
|
525
|
+
/^Bearer\s+$/i.test(prefix)
|
|
526
|
+
) {
|
|
527
|
+
codex.bearer_token_env_var = envName;
|
|
528
|
+
} else if (!prefix) {
|
|
529
|
+
codex.env_http_headers[headerName] = envName;
|
|
530
|
+
} else {
|
|
531
|
+
codex.http_headers[headerName] = `${prefix}${envValue}`;
|
|
532
|
+
warnings.push(
|
|
533
|
+
`[mcp-guard] ${name}.${headerName} header uses resolved literal in Codex TOML because prefixed env headers are unsupported`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
return {
|
|
539
|
+
descriptors,
|
|
540
|
+
headers,
|
|
541
|
+
warnings,
|
|
542
|
+
codex: {
|
|
543
|
+
...(codex.bearer_token_env_var
|
|
544
|
+
? { bearer_token_env_var: codex.bearer_token_env_var }
|
|
545
|
+
: {}),
|
|
546
|
+
env_http_headers: codex.env_http_headers,
|
|
547
|
+
http_headers: codex.http_headers,
|
|
548
|
+
},
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function hasOwnEntries(value) {
|
|
553
|
+
return value && typeof value === "object" && Object.keys(value).length > 0;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function redactHeaders(headers = {}) {
|
|
557
|
+
const redacted = {};
|
|
558
|
+
for (const key of Object.keys(headers || {})) {
|
|
559
|
+
redacted[key] = "***REDACTED***";
|
|
560
|
+
}
|
|
561
|
+
return redacted;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function redactServerConfig(config = {}) {
|
|
565
|
+
if (!config || typeof config !== "object") return config;
|
|
566
|
+
return {
|
|
567
|
+
...config,
|
|
568
|
+
...(config.headers ? { headers: redactHeaders(config.headers) } : {}),
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function descriptorsFromCodexConfig(config = {}) {
|
|
573
|
+
const descriptors = {};
|
|
574
|
+
if (typeof config.bearer_token_env_var === "string") {
|
|
575
|
+
descriptors.Authorization = {
|
|
576
|
+
env: config.bearer_token_env_var,
|
|
577
|
+
prefix: "Bearer ",
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
if (config.env_http_headers && typeof config.env_http_headers === "object") {
|
|
581
|
+
for (const [name, envName] of Object.entries(config.env_http_headers)) {
|
|
582
|
+
if (typeof envName === "string") descriptors[name] = { env: envName };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (config.http_headers && typeof config.http_headers === "object") {
|
|
586
|
+
for (const [name, value] of Object.entries(config.http_headers)) {
|
|
587
|
+
if (typeof value === "string") descriptors[name] = { value };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return descriptors;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function descriptorsFromJsonConfig(config = {}) {
|
|
594
|
+
const descriptors = {};
|
|
595
|
+
if (!config?.headers || typeof config.headers !== "object") {
|
|
596
|
+
return descriptors;
|
|
597
|
+
}
|
|
598
|
+
for (const [name, value] of Object.entries(config.headers)) {
|
|
599
|
+
if (typeof value === "string") descriptors[name] = { value };
|
|
600
|
+
}
|
|
601
|
+
return descriptors;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function headerNames(headers = {}) {
|
|
605
|
+
return Object.keys(headers || {}).sort();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function sameHeaderDescriptors(left = {}, right = {}) {
|
|
609
|
+
const leftNames = headerNames(left);
|
|
610
|
+
const rightNames = headerNames(right);
|
|
611
|
+
if (JSON.stringify(leftNames) !== JSON.stringify(rightNames)) return false;
|
|
612
|
+
return leftNames.every(
|
|
613
|
+
(name) => JSON.stringify(left[name]) === JSON.stringify(right[name]),
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function headerStatus(expected = {}, actual = {}) {
|
|
618
|
+
const expectedNames = headerNames(expected);
|
|
619
|
+
const actualNames = headerNames(actual);
|
|
620
|
+
if (expectedNames.length === 0 && actualNames.length === 0) return "ok";
|
|
621
|
+
if (expectedNames.some((name) => !Object.hasOwn(actual, name))) {
|
|
622
|
+
return "missing";
|
|
623
|
+
}
|
|
624
|
+
if (!sameHeaderDescriptors(expected, actual)) return "stale";
|
|
625
|
+
return "ok";
|
|
626
|
+
}
|
|
627
|
+
|
|
176
628
|
function removeTomlSection(raw, sectionName) {
|
|
177
629
|
const lines = String(raw || "").split(/\r?\n/);
|
|
178
630
|
const output = [];
|
|
@@ -192,30 +644,113 @@ function removeTomlSection(raw, sectionName) {
|
|
|
192
644
|
if (!skipping) output.push(line);
|
|
193
645
|
}
|
|
194
646
|
|
|
195
|
-
const cleaned = output
|
|
647
|
+
const cleaned = output
|
|
648
|
+
.join("\n")
|
|
649
|
+
.replace(/\n{3,}$/g, "\n\n")
|
|
650
|
+
.replace(/^\n+/, "");
|
|
196
651
|
return cleaned.length > 0 ? cleaned.replace(/\n{3,}/g, "\n\n") : "";
|
|
197
652
|
}
|
|
198
653
|
|
|
199
|
-
function
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
654
|
+
function formatTomlInlineTable(data = {}) {
|
|
655
|
+
const entries = Object.entries(data);
|
|
656
|
+
if (entries.length === 0) return null;
|
|
657
|
+
return `{ ${entries
|
|
658
|
+
.map(
|
|
659
|
+
([key, value]) => `${formatTomlString(key)} = ${formatTomlString(value)}`,
|
|
660
|
+
)
|
|
661
|
+
.join(", ")} }`;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function formatTomlArray(values = []) {
|
|
665
|
+
return `[${values.map((value) => formatTomlString(value)).join(", ")}]`;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function upsertTomlServer(raw, name, config, codex = {}) {
|
|
669
|
+
const lines = String(raw || "").split(/\r?\n/);
|
|
670
|
+
const header = `[mcp_servers.${name}]`;
|
|
671
|
+
const managedKeys = new Set([
|
|
672
|
+
"url",
|
|
673
|
+
"transport",
|
|
674
|
+
"command",
|
|
675
|
+
"args",
|
|
676
|
+
"bearer_token_env_var",
|
|
677
|
+
"http_headers",
|
|
678
|
+
"env_http_headers",
|
|
679
|
+
"env",
|
|
680
|
+
]);
|
|
681
|
+
const nextManagedLines = [];
|
|
682
|
+
if (typeof config.url === "string") {
|
|
683
|
+
nextManagedLines.push(`url = ${formatTomlString(config.url)}`);
|
|
684
|
+
}
|
|
685
|
+
if (typeof config.transport === "string") {
|
|
686
|
+
nextManagedLines.push(`transport = ${formatTomlString(config.transport)}`);
|
|
687
|
+
}
|
|
688
|
+
if (typeof config.command === "string") {
|
|
689
|
+
nextManagedLines.push(`command = ${formatTomlString(config.command)}`);
|
|
690
|
+
}
|
|
691
|
+
if (Array.isArray(config.args)) {
|
|
692
|
+
nextManagedLines.push(`args = ${formatTomlArray(config.args)}`);
|
|
693
|
+
}
|
|
694
|
+
const env = formatTomlInlineTable(config.env || {});
|
|
695
|
+
if (env) nextManagedLines.push(`env = ${env}`);
|
|
696
|
+
if (codex.bearer_token_env_var) {
|
|
697
|
+
nextManagedLines.push(
|
|
698
|
+
`bearer_token_env_var = ${formatTomlString(codex.bearer_token_env_var)}`,
|
|
699
|
+
);
|
|
700
|
+
}
|
|
701
|
+
const httpHeaders = formatTomlInlineTable(codex.http_headers || {});
|
|
702
|
+
if (httpHeaders) nextManagedLines.push(`http_headers = ${httpHeaders}`);
|
|
703
|
+
const envHttpHeaders = formatTomlInlineTable(codex.env_http_headers || {});
|
|
704
|
+
if (envHttpHeaders) {
|
|
705
|
+
nextManagedLines.push(`env_http_headers = ${envHttpHeaders}`);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const sectionStart = lines.findIndex((line) => line.trim() === header);
|
|
709
|
+
if (sectionStart < 0) {
|
|
710
|
+
const section = [header, ...nextManagedLines].join("\n");
|
|
711
|
+
const withoutTrailing = String(raw || "").trimEnd();
|
|
712
|
+
return withoutTrailing.length > 0
|
|
713
|
+
? `${withoutTrailing}\n\n${section}\n`
|
|
714
|
+
: `${section}\n`;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
const output = [];
|
|
718
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
719
|
+
output.push(lines[index]);
|
|
720
|
+
if (index !== sectionStart) continue;
|
|
721
|
+
|
|
722
|
+
index += 1;
|
|
723
|
+
const preserved = [];
|
|
724
|
+
while (index < lines.length && !/^\s*\[/.test(lines[index])) {
|
|
725
|
+
const keyMatch = lines[index].match(/^\s*([A-Za-z0-9_]+)\s*=/);
|
|
726
|
+
if (!keyMatch || !managedKeys.has(keyMatch[1])) {
|
|
727
|
+
preserved.push(lines[index]);
|
|
728
|
+
}
|
|
729
|
+
index += 1;
|
|
730
|
+
}
|
|
731
|
+
output.push(...nextManagedLines, ...preserved.filter(Boolean));
|
|
732
|
+
index -= 1;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return output.join("\n");
|
|
208
736
|
}
|
|
209
737
|
|
|
210
738
|
function getHubServerEntry(registry) {
|
|
211
739
|
const entries = Object.entries(registry?.servers || {});
|
|
212
740
|
if (entries.length === 0) {
|
|
213
|
-
return [
|
|
741
|
+
return [
|
|
742
|
+
"tfx-hub",
|
|
743
|
+
{
|
|
744
|
+
url: `${registry?.defaults?.hub_base || "http://127.0.0.1:27888"}${DEFAULT_HUB_PATH}`,
|
|
745
|
+
},
|
|
746
|
+
];
|
|
214
747
|
}
|
|
215
748
|
|
|
216
|
-
return
|
|
217
|
-
|
|
218
|
-
|
|
749
|
+
return (
|
|
750
|
+
entries.find(([name]) => name === "tfx-hub") ||
|
|
751
|
+
entries.find(([, config]) => config?.transport === "hub-url") ||
|
|
752
|
+
entries[0]
|
|
753
|
+
);
|
|
219
754
|
}
|
|
220
755
|
|
|
221
756
|
function _makeHubRuntimeConfig() {
|
|
@@ -224,30 +759,157 @@ function _makeHubRuntimeConfig() {
|
|
|
224
759
|
|
|
225
760
|
function serverTargets(serverConfig) {
|
|
226
761
|
if (Array.isArray(serverConfig?.targets) && serverConfig.targets.length > 0) {
|
|
227
|
-
return [
|
|
762
|
+
return [
|
|
763
|
+
...new Set(
|
|
764
|
+
serverConfig.targets
|
|
765
|
+
.map((value) => String(value).trim())
|
|
766
|
+
.filter(Boolean),
|
|
767
|
+
),
|
|
768
|
+
];
|
|
228
769
|
}
|
|
229
|
-
return ["claude", "gemini", "codex"];
|
|
770
|
+
return ["claude", "gemini", "codex", "antigravity"];
|
|
230
771
|
}
|
|
231
772
|
|
|
232
773
|
function serverAppliesToClient(serverConfig, client) {
|
|
233
774
|
return serverTargets(serverConfig).includes(client);
|
|
234
775
|
}
|
|
235
776
|
|
|
236
|
-
function
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
777
|
+
function serverPolicy(serverConfig = {}, registry = {}) {
|
|
778
|
+
if (SERVER_POLICIES.has(serverConfig.policy)) return serverConfig.policy;
|
|
779
|
+
const transport = serverConfig.transport || registry?.defaults?.transport;
|
|
780
|
+
if (transport === "stdio") return "stdio";
|
|
781
|
+
return "hosted";
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function gatewaySseUrl(name, serverConfig = {}) {
|
|
785
|
+
if (typeof serverConfig.url === "string" && serverConfig.url.trim()) {
|
|
786
|
+
return normalizeUrl(serverConfig.url);
|
|
787
|
+
}
|
|
788
|
+
const port =
|
|
789
|
+
Number(serverConfig.gateway_port || serverConfig.port || 0) ||
|
|
790
|
+
GATEWAY_PORTS[name];
|
|
791
|
+
if (!Number.isFinite(port) || port <= 0) return "";
|
|
792
|
+
return `http://127.0.0.1:${port}/sse`;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function syncDenylistEntries(policy = {}) {
|
|
796
|
+
if (!Array.isArray(policy?.sync_denylist)) return new Set();
|
|
797
|
+
return new Set(
|
|
798
|
+
policy.sync_denylist
|
|
799
|
+
.map((entry) =>
|
|
800
|
+
String(entry || "")
|
|
801
|
+
.trim()
|
|
802
|
+
.toLowerCase(),
|
|
803
|
+
)
|
|
804
|
+
.filter(Boolean),
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function syncDenylistKey(client, serverName) {
|
|
809
|
+
return `${String(client || "")
|
|
810
|
+
.trim()
|
|
811
|
+
.toLowerCase()}:${String(serverName || "")
|
|
812
|
+
.trim()
|
|
813
|
+
.toLowerCase()}`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export function buildDesiredServerRecord(name, serverConfig, filePath) {
|
|
817
|
+
const policy = serverPolicy(serverConfig);
|
|
818
|
+
if (policy === "stdio") {
|
|
819
|
+
const resolvedEnv = resolveEnvDescriptors(name, serverConfig);
|
|
820
|
+
const envConfig = hasOwnEntries(resolvedEnv.env)
|
|
821
|
+
? { env: resolvedEnv.env }
|
|
822
|
+
: {};
|
|
823
|
+
return {
|
|
824
|
+
name,
|
|
825
|
+
config: {
|
|
826
|
+
command: serverConfig.command,
|
|
827
|
+
args: Array.isArray(serverConfig.args) ? [...serverConfig.args] : [],
|
|
828
|
+
...envConfig,
|
|
829
|
+
},
|
|
830
|
+
envDescriptors: resolvedEnv.descriptors,
|
|
831
|
+
codex: {},
|
|
832
|
+
warnings: resolvedEnv.warnings,
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const url =
|
|
837
|
+
policy === "gateway-sse"
|
|
838
|
+
? gatewaySseUrl(name, serverConfig)
|
|
839
|
+
: serverConfig?.transport === "hub-url"
|
|
840
|
+
? resolveHubUrl()
|
|
841
|
+
: normalizeUrl(serverConfig?.url || "");
|
|
240
842
|
const basenameValue = pathBasename(filePath);
|
|
843
|
+
const resolvedHeaders = resolveHeaderDescriptors(
|
|
844
|
+
name,
|
|
845
|
+
serverConfig,
|
|
846
|
+
filePath,
|
|
847
|
+
);
|
|
848
|
+
const headerConfig = hasOwnEntries(resolvedHeaders.headers)
|
|
849
|
+
? { headers: resolvedHeaders.headers }
|
|
850
|
+
: {};
|
|
851
|
+
|
|
852
|
+
if (policy === "gateway-sse") {
|
|
853
|
+
if (isCodexConfig(filePath)) {
|
|
854
|
+
return {
|
|
855
|
+
name,
|
|
856
|
+
config: { url, transport: "sse" },
|
|
857
|
+
headerDescriptors: {},
|
|
858
|
+
codex: {},
|
|
859
|
+
warnings: [],
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return {
|
|
864
|
+
name,
|
|
865
|
+
config: { type: "sse", url },
|
|
866
|
+
headerDescriptors: {},
|
|
867
|
+
codex: {},
|
|
868
|
+
warnings: [],
|
|
869
|
+
};
|
|
870
|
+
}
|
|
241
871
|
|
|
242
|
-
if (
|
|
243
|
-
|
|
872
|
+
if (
|
|
873
|
+
basenameValue === ".mcp.json" ||
|
|
874
|
+
isClaudeProjectMcpConfig(filePath) ||
|
|
875
|
+
isClaudeUserConfig(filePath)
|
|
876
|
+
) {
|
|
877
|
+
return {
|
|
878
|
+
name,
|
|
879
|
+
config: { type: "http", url, ...headerConfig },
|
|
880
|
+
headerDescriptors: resolvedHeaders.descriptors,
|
|
881
|
+
codex: resolvedHeaders.codex,
|
|
882
|
+
warnings: resolvedHeaders.warnings,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (isAntigravityConfig(filePath)) {
|
|
887
|
+
return {
|
|
888
|
+
name,
|
|
889
|
+
config: { url, type: "http", ...headerConfig },
|
|
890
|
+
headerDescriptors: resolvedHeaders.descriptors,
|
|
891
|
+
codex: resolvedHeaders.codex,
|
|
892
|
+
warnings: resolvedHeaders.warnings,
|
|
893
|
+
};
|
|
244
894
|
}
|
|
245
895
|
|
|
246
896
|
if (isCodexConfig(filePath)) {
|
|
247
|
-
return {
|
|
897
|
+
return {
|
|
898
|
+
name,
|
|
899
|
+
config: { url, ...headerConfig },
|
|
900
|
+
headerDescriptors: resolvedHeaders.descriptors,
|
|
901
|
+
codex: resolvedHeaders.codex,
|
|
902
|
+
warnings: resolvedHeaders.warnings,
|
|
903
|
+
};
|
|
248
904
|
}
|
|
249
905
|
|
|
250
|
-
return {
|
|
906
|
+
return {
|
|
907
|
+
name,
|
|
908
|
+
config: { url, ...headerConfig },
|
|
909
|
+
headerDescriptors: resolvedHeaders.descriptors,
|
|
910
|
+
codex: resolvedHeaders.codex,
|
|
911
|
+
warnings: resolvedHeaders.warnings,
|
|
912
|
+
};
|
|
251
913
|
}
|
|
252
914
|
|
|
253
915
|
function scanJsonConfig(filePath) {
|
|
@@ -266,22 +928,54 @@ function scanJsonConfig(filePath) {
|
|
|
266
928
|
try {
|
|
267
929
|
const parsed = readJsonFile(filePath);
|
|
268
930
|
const mcpServers = parsed?.mcpServers;
|
|
269
|
-
const servers =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
931
|
+
const servers =
|
|
932
|
+
!mcpServers || typeof mcpServers !== "object"
|
|
933
|
+
? []
|
|
934
|
+
: Object.entries(mcpServers)
|
|
935
|
+
.filter(
|
|
936
|
+
([name, config]) =>
|
|
937
|
+
typeof name === "string" &&
|
|
938
|
+
config &&
|
|
939
|
+
typeof config === "object",
|
|
940
|
+
)
|
|
941
|
+
.map(([name, config]) => ({
|
|
942
|
+
name: name.trim(),
|
|
943
|
+
url:
|
|
944
|
+
typeof config.url === "string" ? normalizeUrl(config.url) : "",
|
|
945
|
+
command: typeof config.command === "string" ? config.command : "",
|
|
946
|
+
args: Array.isArray(config.args)
|
|
947
|
+
? config.args.filter((value) => typeof value === "string")
|
|
948
|
+
: [],
|
|
949
|
+
env:
|
|
950
|
+
config.env &&
|
|
951
|
+
typeof config.env === "object" &&
|
|
952
|
+
!Array.isArray(config.env)
|
|
953
|
+
? Object.fromEntries(
|
|
954
|
+
Object.entries(config.env).filter(
|
|
955
|
+
([, value]) => typeof value === "string",
|
|
956
|
+
),
|
|
957
|
+
)
|
|
958
|
+
: {},
|
|
959
|
+
type: typeof config.type === "string" ? config.type : "",
|
|
960
|
+
headers:
|
|
961
|
+
config.headers &&
|
|
962
|
+
typeof config.headers === "object" &&
|
|
963
|
+
!Array.isArray(config.headers)
|
|
964
|
+
? Object.fromEntries(
|
|
965
|
+
Object.entries(config.headers).filter(
|
|
966
|
+
([, value]) => typeof value === "string",
|
|
967
|
+
),
|
|
968
|
+
)
|
|
969
|
+
: {},
|
|
970
|
+
headerDescriptors: descriptorsFromJsonConfig(config),
|
|
971
|
+
transport:
|
|
972
|
+
typeof config.url === "string" && config.url
|
|
973
|
+
? "url"
|
|
974
|
+
: typeof config.command === "string" && config.command
|
|
975
|
+
? "stdio"
|
|
976
|
+
: "unknown",
|
|
977
|
+
raw: config,
|
|
978
|
+
}));
|
|
285
979
|
|
|
286
980
|
return {
|
|
287
981
|
filePath,
|
|
@@ -325,11 +1019,30 @@ function scanCodexConfig(filePath) {
|
|
|
325
1019
|
name,
|
|
326
1020
|
url: typeof config.url === "string" ? normalizeUrl(config.url) : "",
|
|
327
1021
|
command: typeof config.command === "string" ? config.command : "",
|
|
328
|
-
|
|
329
|
-
? "
|
|
330
|
-
:
|
|
331
|
-
|
|
332
|
-
|
|
1022
|
+
args: Array.isArray(config.args)
|
|
1023
|
+
? config.args.filter((value) => typeof value === "string")
|
|
1024
|
+
: [],
|
|
1025
|
+
env:
|
|
1026
|
+
config.env &&
|
|
1027
|
+
typeof config.env === "object" &&
|
|
1028
|
+
!Array.isArray(config.env)
|
|
1029
|
+
? Object.fromEntries(
|
|
1030
|
+
Object.entries(config.env).filter(
|
|
1031
|
+
([, value]) => typeof value === "string",
|
|
1032
|
+
),
|
|
1033
|
+
)
|
|
1034
|
+
: {},
|
|
1035
|
+
headers:
|
|
1036
|
+
config.http_headers && typeof config.http_headers === "object"
|
|
1037
|
+
? config.http_headers
|
|
1038
|
+
: {},
|
|
1039
|
+
headerDescriptors: descriptorsFromCodexConfig(config),
|
|
1040
|
+
transport:
|
|
1041
|
+
typeof config.url === "string" && config.url
|
|
1042
|
+
? "url"
|
|
1043
|
+
: typeof config.command === "string" && config.command
|
|
1044
|
+
? "stdio"
|
|
1045
|
+
: "unknown",
|
|
333
1046
|
raw: config,
|
|
334
1047
|
}));
|
|
335
1048
|
|
|
@@ -368,7 +1081,11 @@ function updateJsonConfig(filePath, updates = [], removals = []) {
|
|
|
368
1081
|
parsed = {};
|
|
369
1082
|
}
|
|
370
1083
|
|
|
371
|
-
if (
|
|
1084
|
+
if (
|
|
1085
|
+
!parsed.mcpServers ||
|
|
1086
|
+
typeof parsed.mcpServers !== "object" ||
|
|
1087
|
+
Array.isArray(parsed.mcpServers)
|
|
1088
|
+
) {
|
|
372
1089
|
parsed.mcpServers = {};
|
|
373
1090
|
}
|
|
374
1091
|
|
|
@@ -383,8 +1100,34 @@ function updateJsonConfig(filePath, updates = [], removals = []) {
|
|
|
383
1100
|
|
|
384
1101
|
for (const update of updates) {
|
|
385
1102
|
const current = parsed.mcpServers[update.name];
|
|
386
|
-
const nextConfig = {
|
|
387
|
-
|
|
1103
|
+
const nextConfig = {
|
|
1104
|
+
...(current && typeof current === "object" ? current : {}),
|
|
1105
|
+
...update.config,
|
|
1106
|
+
};
|
|
1107
|
+
if (Object.hasOwn(update.config, "command")) {
|
|
1108
|
+
delete nextConfig.url;
|
|
1109
|
+
delete nextConfig.type;
|
|
1110
|
+
delete nextConfig.headers;
|
|
1111
|
+
}
|
|
1112
|
+
if (Object.hasOwn(update.config, "url")) {
|
|
1113
|
+
delete nextConfig.command;
|
|
1114
|
+
delete nextConfig.args;
|
|
1115
|
+
delete nextConfig.env;
|
|
1116
|
+
}
|
|
1117
|
+
if (!Object.hasOwn(update.config, "type")) {
|
|
1118
|
+
delete nextConfig.type;
|
|
1119
|
+
}
|
|
1120
|
+
if (!Object.hasOwn(update.config, "transport")) {
|
|
1121
|
+
delete nextConfig.transport;
|
|
1122
|
+
}
|
|
1123
|
+
if (!Object.hasOwn(update.config, "headers")) {
|
|
1124
|
+
delete nextConfig.headers;
|
|
1125
|
+
}
|
|
1126
|
+
if (!Object.hasOwn(update.config, "env")) {
|
|
1127
|
+
delete nextConfig.env;
|
|
1128
|
+
}
|
|
1129
|
+
const changed =
|
|
1130
|
+
JSON.stringify(current || null) !== JSON.stringify(nextConfig);
|
|
388
1131
|
if (changed) {
|
|
389
1132
|
parsed.mcpServers[update.name] = nextConfig;
|
|
390
1133
|
modified = true;
|
|
@@ -396,11 +1139,21 @@ function updateJsonConfig(filePath, updates = [], removals = []) {
|
|
|
396
1139
|
}
|
|
397
1140
|
|
|
398
1141
|
writeJsonFile(resolvedPath, parsed);
|
|
1142
|
+
lockJsonConfigPermissions(resolvedPath);
|
|
399
1143
|
return { modified: true, filePath: resolvedPath };
|
|
400
1144
|
}
|
|
401
1145
|
|
|
402
1146
|
function updateCodexConfig(filePath, updates = [], removals = []) {
|
|
403
1147
|
const resolvedPath = resolveFilePath(filePath);
|
|
1148
|
+
if (shouldSkipCodexConfigMutation()) {
|
|
1149
|
+
return {
|
|
1150
|
+
modified: false,
|
|
1151
|
+
filePath: resolvedPath,
|
|
1152
|
+
skipped: true,
|
|
1153
|
+
reason: "protected-env",
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
404
1157
|
let raw = existsSync(resolvedPath) ? readFileSync(resolvedPath, "utf8") : "";
|
|
405
1158
|
|
|
406
1159
|
for (const name of removals) {
|
|
@@ -408,11 +1161,13 @@ function updateCodexConfig(filePath, updates = [], removals = []) {
|
|
|
408
1161
|
}
|
|
409
1162
|
|
|
410
1163
|
for (const update of updates) {
|
|
411
|
-
raw =
|
|
1164
|
+
raw = upsertTomlServer(raw, update.name, update.config, update.codex || {});
|
|
412
1165
|
}
|
|
413
1166
|
|
|
414
1167
|
const finalRaw = raw.trim().length > 0 ? `${raw.trimEnd()}\n` : "";
|
|
415
|
-
const previousRaw = existsSync(resolvedPath)
|
|
1168
|
+
const previousRaw = existsSync(resolvedPath)
|
|
1169
|
+
? readFileSync(resolvedPath, "utf8")
|
|
1170
|
+
: "";
|
|
416
1171
|
if (finalRaw === previousRaw) {
|
|
417
1172
|
return { modified: false, filePath: resolvedPath };
|
|
418
1173
|
}
|
|
@@ -439,7 +1194,7 @@ function scanConfig(filePath) {
|
|
|
439
1194
|
}
|
|
440
1195
|
|
|
441
1196
|
export function getRegistryPath() {
|
|
442
|
-
return
|
|
1197
|
+
return registryPath();
|
|
443
1198
|
}
|
|
444
1199
|
|
|
445
1200
|
export function createDefaultRegistry() {
|
|
@@ -460,7 +1215,11 @@ export function validateRegistry(registry) {
|
|
|
460
1215
|
errors.push("registry.defaults must be an object");
|
|
461
1216
|
}
|
|
462
1217
|
|
|
463
|
-
if (
|
|
1218
|
+
if (
|
|
1219
|
+
!registry.servers ||
|
|
1220
|
+
typeof registry.servers !== "object" ||
|
|
1221
|
+
Array.isArray(registry.servers)
|
|
1222
|
+
) {
|
|
464
1223
|
errors.push("registry.servers must be an object");
|
|
465
1224
|
} else {
|
|
466
1225
|
for (const [name, server] of Object.entries(registry.servers)) {
|
|
@@ -472,23 +1231,233 @@ export function validateRegistry(registry) {
|
|
|
472
1231
|
errors.push(`registry.servers.${name} must be an object`);
|
|
473
1232
|
continue;
|
|
474
1233
|
}
|
|
475
|
-
|
|
1234
|
+
const policy = serverPolicy(server, registry);
|
|
1235
|
+
if (
|
|
1236
|
+
typeof server.policy !== "string" ||
|
|
1237
|
+
!SERVER_POLICIES.has(server.policy)
|
|
1238
|
+
) {
|
|
1239
|
+
errors.push(
|
|
1240
|
+
`registry.servers.${name}.policy must be hosted, gateway-sse, or stdio`,
|
|
1241
|
+
);
|
|
1242
|
+
}
|
|
1243
|
+
const transport = server.transport || registry.defaults?.transport;
|
|
1244
|
+
if (!["hub-url", "http", "stdio"].includes(transport)) {
|
|
1245
|
+
errors.push(
|
|
1246
|
+
`registry.servers.${name}.transport must be hub-url, http, or stdio`,
|
|
1247
|
+
);
|
|
1248
|
+
}
|
|
1249
|
+
if (
|
|
1250
|
+
policy === "hosted" &&
|
|
1251
|
+
(typeof server.url !== "string" || !server.url.trim())
|
|
1252
|
+
) {
|
|
476
1253
|
errors.push(`registry.servers.${name}.url must be a non-empty string`);
|
|
477
1254
|
}
|
|
1255
|
+
if (policy === "gateway-sse") {
|
|
1256
|
+
const port = Number(server.gateway_port || server.port || 0);
|
|
1257
|
+
const hasGatewayUrl =
|
|
1258
|
+
typeof server.url === "string" && server.url.trim().length > 0;
|
|
1259
|
+
if (
|
|
1260
|
+
!hasGatewayUrl &&
|
|
1261
|
+
(!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
1262
|
+
) {
|
|
1263
|
+
errors.push(
|
|
1264
|
+
`registry.servers.${name}.gateway_port must be a valid TCP port for gateway-sse`,
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (policy === "stdio" && server.url !== undefined) {
|
|
1269
|
+
errors.push(`registry.servers.${name}.url is not used for stdio`);
|
|
1270
|
+
}
|
|
1271
|
+
if (policy === "stdio") {
|
|
1272
|
+
if (typeof server.command !== "string" || !server.command.trim()) {
|
|
1273
|
+
errors.push(
|
|
1274
|
+
`registry.servers.${name}.command must be a non-empty string for stdio`,
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
if (
|
|
1278
|
+
!Array.isArray(server.args) ||
|
|
1279
|
+
server.args.some((arg) => typeof arg !== "string")
|
|
1280
|
+
) {
|
|
1281
|
+
errors.push(
|
|
1282
|
+
`registry.servers.${name}.args must be an array of strings for stdio`,
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
if (policy !== "stdio" && server.command !== undefined) {
|
|
1287
|
+
errors.push(
|
|
1288
|
+
`registry.servers.${name}.command is allowed only for stdio transport`,
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
if (policy !== "stdio" && server.args !== undefined) {
|
|
1292
|
+
errors.push(
|
|
1293
|
+
`registry.servers.${name}.args is allowed only for stdio transport`,
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
if (server.env !== undefined) {
|
|
1297
|
+
if (policy !== "stdio") {
|
|
1298
|
+
errors.push(
|
|
1299
|
+
`registry.servers.${name}.env is allowed only for stdio transport`,
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
if (
|
|
1303
|
+
!server.env ||
|
|
1304
|
+
typeof server.env !== "object" ||
|
|
1305
|
+
Array.isArray(server.env)
|
|
1306
|
+
) {
|
|
1307
|
+
errors.push(`registry.servers.${name}.env must be an object`);
|
|
1308
|
+
} else {
|
|
1309
|
+
for (const [envKey, descriptor] of Object.entries(server.env)) {
|
|
1310
|
+
if (!isValidEnvName(envKey)) {
|
|
1311
|
+
errors.push(
|
|
1312
|
+
`registry.servers.${name}.env contains invalid env name ${envKey}`,
|
|
1313
|
+
);
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
if (
|
|
1317
|
+
!descriptor ||
|
|
1318
|
+
typeof descriptor !== "object" ||
|
|
1319
|
+
Array.isArray(descriptor)
|
|
1320
|
+
) {
|
|
1321
|
+
errors.push(
|
|
1322
|
+
`registry.servers.${name}.env.${envKey} must be a descriptor object`,
|
|
1323
|
+
);
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
const keys = Object.keys(descriptor);
|
|
1327
|
+
if (
|
|
1328
|
+
keys.length !== 1 ||
|
|
1329
|
+
!Object.hasOwn(descriptor, "env") ||
|
|
1330
|
+
typeof descriptor.env !== "string" ||
|
|
1331
|
+
!descriptor.env.trim()
|
|
1332
|
+
) {
|
|
1333
|
+
errors.push(
|
|
1334
|
+
`registry.servers.${name}.env.${envKey}.env must be a non-empty string`,
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
if (server.headers !== undefined) {
|
|
1341
|
+
if (policy !== "hosted") {
|
|
1342
|
+
errors.push(
|
|
1343
|
+
`registry.servers.${name}.headers is allowed only for hosted policy`,
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
if (
|
|
1347
|
+
!server.headers ||
|
|
1348
|
+
typeof server.headers !== "object" ||
|
|
1349
|
+
Array.isArray(server.headers)
|
|
1350
|
+
) {
|
|
1351
|
+
errors.push(`registry.servers.${name}.headers must be an object`);
|
|
1352
|
+
} else {
|
|
1353
|
+
for (const [headerName, descriptor] of Object.entries(
|
|
1354
|
+
server.headers,
|
|
1355
|
+
)) {
|
|
1356
|
+
if (!isValidHeaderName(headerName)) {
|
|
1357
|
+
errors.push(
|
|
1358
|
+
`registry.servers.${name}.headers contains invalid header name ${headerName}`,
|
|
1359
|
+
);
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
if (
|
|
1363
|
+
!descriptor ||
|
|
1364
|
+
typeof descriptor !== "object" ||
|
|
1365
|
+
Array.isArray(descriptor)
|
|
1366
|
+
) {
|
|
1367
|
+
errors.push(
|
|
1368
|
+
`registry.servers.${name}.headers.${headerName} must be a descriptor object`,
|
|
1369
|
+
);
|
|
1370
|
+
continue;
|
|
1371
|
+
}
|
|
1372
|
+
const keys = Object.keys(descriptor);
|
|
1373
|
+
const hasValue = Object.hasOwn(descriptor, "value");
|
|
1374
|
+
const hasEnv = Object.hasOwn(descriptor, "env");
|
|
1375
|
+
if (hasValue && hasEnv) {
|
|
1376
|
+
errors.push(
|
|
1377
|
+
`registry.servers.${name}.headers.${headerName} must use either value or env`,
|
|
1378
|
+
);
|
|
1379
|
+
} else if (hasValue) {
|
|
1380
|
+
if (
|
|
1381
|
+
keys.length !== 1 ||
|
|
1382
|
+
typeof descriptor.value !== "string" ||
|
|
1383
|
+
descriptor.value.length === 0
|
|
1384
|
+
) {
|
|
1385
|
+
errors.push(
|
|
1386
|
+
`registry.servers.${name}.headers.${headerName}.value must be a non-empty string`,
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
} else if (hasEnv) {
|
|
1390
|
+
if (
|
|
1391
|
+
!["env", "prefix"].every((key) => keys.includes(key)) &&
|
|
1392
|
+
keys.some((key) => !["env", "prefix"].includes(key))
|
|
1393
|
+
) {
|
|
1394
|
+
errors.push(
|
|
1395
|
+
`registry.servers.${name}.headers.${headerName} supports only env and optional prefix`,
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
if (
|
|
1399
|
+
typeof descriptor.env !== "string" ||
|
|
1400
|
+
!descriptor.env.trim()
|
|
1401
|
+
) {
|
|
1402
|
+
errors.push(
|
|
1403
|
+
`registry.servers.${name}.headers.${headerName}.env must be a non-empty string`,
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
if (
|
|
1407
|
+
descriptor.prefix !== undefined &&
|
|
1408
|
+
typeof descriptor.prefix !== "string"
|
|
1409
|
+
) {
|
|
1410
|
+
errors.push(
|
|
1411
|
+
`registry.servers.${name}.headers.${headerName}.prefix must be a string`,
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
} else {
|
|
1415
|
+
errors.push(
|
|
1416
|
+
`registry.servers.${name}.headers.${headerName} must use value or env`,
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
478
1422
|
if (server.targets !== undefined && !Array.isArray(server.targets)) {
|
|
479
1423
|
errors.push(`registry.servers.${name}.targets must be an array`);
|
|
480
1424
|
}
|
|
481
1425
|
}
|
|
482
1426
|
}
|
|
483
1427
|
|
|
484
|
-
if (
|
|
1428
|
+
if (
|
|
1429
|
+
!registry.policies ||
|
|
1430
|
+
typeof registry.policies !== "object" ||
|
|
1431
|
+
Array.isArray(registry.policies)
|
|
1432
|
+
) {
|
|
485
1433
|
errors.push("registry.policies must be an object");
|
|
486
1434
|
} else {
|
|
487
1435
|
if (!Array.isArray(registry.policies.watched_paths)) {
|
|
488
1436
|
errors.push("registry.policies.watched_paths must be an array");
|
|
489
1437
|
}
|
|
490
|
-
if (
|
|
491
|
-
|
|
1438
|
+
if (
|
|
1439
|
+
registry.policies.sync_denylist !== undefined &&
|
|
1440
|
+
!Array.isArray(registry.policies.sync_denylist)
|
|
1441
|
+
) {
|
|
1442
|
+
errors.push("registry.policies.sync_denylist must be an array");
|
|
1443
|
+
}
|
|
1444
|
+
if (Array.isArray(registry.policies.sync_denylist)) {
|
|
1445
|
+
for (const entry of registry.policies.sync_denylist) {
|
|
1446
|
+
if (typeof entry !== "string" || !entry.includes(":")) {
|
|
1447
|
+
errors.push(
|
|
1448
|
+
"registry.policies.sync_denylist entries must use client:server format",
|
|
1449
|
+
);
|
|
1450
|
+
break;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
if (
|
|
1455
|
+
registry.policies.stdio_action &&
|
|
1456
|
+
!["replace-with-hub", "warn"].includes(registry.policies.stdio_action)
|
|
1457
|
+
) {
|
|
1458
|
+
errors.push(
|
|
1459
|
+
"registry.policies.stdio_action must be replace-with-hub or warn",
|
|
1460
|
+
);
|
|
492
1461
|
}
|
|
493
1462
|
}
|
|
494
1463
|
|
|
@@ -496,9 +1465,9 @@ export function validateRegistry(registry) {
|
|
|
496
1465
|
}
|
|
497
1466
|
|
|
498
1467
|
export function inspectRegistry() {
|
|
499
|
-
if (!existsSync(
|
|
1468
|
+
if (!existsSync(registryPath())) {
|
|
500
1469
|
return {
|
|
501
|
-
path:
|
|
1470
|
+
path: registryPath(),
|
|
502
1471
|
exists: false,
|
|
503
1472
|
valid: false,
|
|
504
1473
|
errors: ["registry file missing"],
|
|
@@ -507,10 +1476,10 @@ export function inspectRegistry() {
|
|
|
507
1476
|
}
|
|
508
1477
|
|
|
509
1478
|
try {
|
|
510
|
-
const registry = readJsonFile(
|
|
1479
|
+
const registry = readJsonFile(registryPath());
|
|
511
1480
|
const errors = validateRegistry(registry);
|
|
512
1481
|
return {
|
|
513
|
-
path:
|
|
1482
|
+
path: registryPath(),
|
|
514
1483
|
exists: true,
|
|
515
1484
|
valid: errors.length === 0,
|
|
516
1485
|
errors,
|
|
@@ -518,7 +1487,7 @@ export function inspectRegistry() {
|
|
|
518
1487
|
};
|
|
519
1488
|
} catch (error) {
|
|
520
1489
|
return {
|
|
521
|
-
path:
|
|
1490
|
+
path: registryPath(),
|
|
522
1491
|
exists: true,
|
|
523
1492
|
valid: false,
|
|
524
1493
|
errors: [error.message],
|
|
@@ -560,7 +1529,7 @@ export function saveRegistry(registry) {
|
|
|
560
1529
|
if (errors.length > 0) {
|
|
561
1530
|
throw new Error(`MCP registry invalid: ${errors.join("; ")}`);
|
|
562
1531
|
}
|
|
563
|
-
writeJsonFile(
|
|
1532
|
+
writeJsonFile(registryPath(), registry);
|
|
564
1533
|
return registry;
|
|
565
1534
|
}
|
|
566
1535
|
|
|
@@ -577,8 +1546,75 @@ export function listManagedConfigTargets(registry = loadRegistryOrDefault()) {
|
|
|
577
1546
|
});
|
|
578
1547
|
}
|
|
579
1548
|
|
|
1549
|
+
export function discoverProjectMcpTargets(options = {}) {
|
|
1550
|
+
const root = resolve(expandHome(options.root || defaultProjectsRoot()));
|
|
1551
|
+
const maxDepth = Number.isFinite(Number(options.maxDepth))
|
|
1552
|
+
? Number(options.maxDepth)
|
|
1553
|
+
: 4;
|
|
1554
|
+
const excludePatterns = Array.isArray(options.exclude)
|
|
1555
|
+
? options.exclude.filter(Boolean).map(String)
|
|
1556
|
+
: [];
|
|
1557
|
+
const targets = [];
|
|
1558
|
+
const seen = new Set();
|
|
1559
|
+
|
|
1560
|
+
function addTarget(filePath) {
|
|
1561
|
+
const normalized = normalizeForMatch(filePath);
|
|
1562
|
+
if (seen.has(normalized)) return;
|
|
1563
|
+
seen.add(normalized);
|
|
1564
|
+
targets.push({
|
|
1565
|
+
watchedPath: filePath,
|
|
1566
|
+
filePath,
|
|
1567
|
+
client: detectClient(filePath),
|
|
1568
|
+
label: detectLabel(filePath),
|
|
1569
|
+
exists: existsSync(filePath),
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function walk(dirPath, depth) {
|
|
1574
|
+
if (depth > maxDepth) return;
|
|
1575
|
+
let entries;
|
|
1576
|
+
try {
|
|
1577
|
+
entries = readdirSync(dirPath, { withFileTypes: true });
|
|
1578
|
+
} catch {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
for (const entry of entries) {
|
|
1583
|
+
const entryPath = join(dirPath, entry.name);
|
|
1584
|
+
const relativePath = relative(root, entryPath).replace(/\\/g, "/");
|
|
1585
|
+
if (isExcludedProjectPath(relativePath, excludePatterns)) continue;
|
|
1586
|
+
|
|
1587
|
+
if (entry.isDirectory()) {
|
|
1588
|
+
if (depth < maxDepth) walk(entryPath, depth + 1);
|
|
1589
|
+
continue;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
if (!entry.isFile() || depth + 1 > maxDepth) continue;
|
|
1593
|
+
const normalized = entryPath.replace(/\\/g, "/");
|
|
1594
|
+
if (
|
|
1595
|
+
entry.name === ".mcp.json" ||
|
|
1596
|
+
normalized.endsWith("/.claude/mcp.json")
|
|
1597
|
+
) {
|
|
1598
|
+
addTarget(resolve(entryPath));
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
walk(root, 0);
|
|
1604
|
+
return {
|
|
1605
|
+
root,
|
|
1606
|
+
maxDepth,
|
|
1607
|
+
exclude: excludePatterns,
|
|
1608
|
+
targets: targets.sort((left, right) =>
|
|
1609
|
+
left.filePath.localeCompare(right.filePath),
|
|
1610
|
+
),
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
580
1614
|
export function listPrimaryConfigTargets(registry = loadRegistryOrDefault()) {
|
|
581
|
-
return listManagedConfigTargets(registry).filter((target) =>
|
|
1615
|
+
return listManagedConfigTargets(registry).filter((target) =>
|
|
1616
|
+
isPrimaryConfigTarget(target.filePath),
|
|
1617
|
+
);
|
|
582
1618
|
}
|
|
583
1619
|
|
|
584
1620
|
export function scanManagedConfigs(registry = loadRegistryOrDefault()) {
|
|
@@ -590,17 +1626,35 @@ export function scanManagedConfigs(registry = loadRegistryOrDefault()) {
|
|
|
590
1626
|
|
|
591
1627
|
export function inspectRegistryStatus(registry = loadRegistryOrDefault()) {
|
|
592
1628
|
const configs = scanManagedConfigs(registry);
|
|
593
|
-
const primaryTargets = new Set(
|
|
1629
|
+
const primaryTargets = new Set(
|
|
1630
|
+
listPrimaryConfigTargets(registry).map((target) =>
|
|
1631
|
+
normalizeForMatch(target.filePath),
|
|
1632
|
+
),
|
|
1633
|
+
);
|
|
594
1634
|
const rows = [];
|
|
595
1635
|
|
|
596
1636
|
for (const config of configs) {
|
|
597
1637
|
const isPrimary = primaryTargets.has(normalizeForMatch(config.filePath));
|
|
598
|
-
const managedServers = Object.entries(registry.servers || {})
|
|
599
|
-
|
|
1638
|
+
const managedServers = Object.entries(registry.servers || {}).filter(
|
|
1639
|
+
([, serverConfig]) =>
|
|
1640
|
+
isPrimary && serverAppliesToClient(serverConfig, config.client),
|
|
1641
|
+
);
|
|
600
1642
|
|
|
601
1643
|
for (const [name, serverConfig] of managedServers) {
|
|
602
|
-
const
|
|
603
|
-
|
|
1644
|
+
const desired = buildDesiredServerRecord(
|
|
1645
|
+
name,
|
|
1646
|
+
serverConfig,
|
|
1647
|
+
config.filePath,
|
|
1648
|
+
);
|
|
1649
|
+
const expectedUrl = desired.config.url || "";
|
|
1650
|
+
const expectedCommand = desired.config.command || "";
|
|
1651
|
+
const expectedHeaders = isCodexConfig(config.filePath)
|
|
1652
|
+
? desired.headerDescriptors || {}
|
|
1653
|
+
: descriptorsFromJsonConfig(desired.config);
|
|
1654
|
+
const actual =
|
|
1655
|
+
config.servers.find((server) => server.name === name) || null;
|
|
1656
|
+
const actualHeaders = actual?.headerDescriptors || {};
|
|
1657
|
+
const currentHeaderStatus = headerStatus(expectedHeaders, actualHeaders);
|
|
604
1658
|
let status = "missing";
|
|
605
1659
|
|
|
606
1660
|
if (!config.exists) {
|
|
@@ -609,10 +1663,17 @@ export function inspectRegistryStatus(registry = loadRegistryOrDefault()) {
|
|
|
609
1663
|
status = "invalid-config";
|
|
610
1664
|
} else if (!actual) {
|
|
611
1665
|
status = "missing";
|
|
1666
|
+
} else if (expectedCommand) {
|
|
1667
|
+
status =
|
|
1668
|
+
actual.command === expectedCommand &&
|
|
1669
|
+
JSON.stringify(actual.args || []) ===
|
|
1670
|
+
JSON.stringify(desired.config.args || [])
|
|
1671
|
+
? "present"
|
|
1672
|
+
: "mismatch";
|
|
612
1673
|
} else if (!actual.url) {
|
|
613
1674
|
status = actual.transport === "stdio" ? "stdio" : "invalid";
|
|
614
1675
|
} else if (normalizeUrl(actual.url) === normalizeUrl(expectedUrl)) {
|
|
615
|
-
status = "present";
|
|
1676
|
+
status = currentHeaderStatus === "ok" ? "present" : "mismatch";
|
|
616
1677
|
} else {
|
|
617
1678
|
status = "mismatch";
|
|
618
1679
|
}
|
|
@@ -624,8 +1685,13 @@ export function inspectRegistryStatus(registry = loadRegistryOrDefault()) {
|
|
|
624
1685
|
label: config.label,
|
|
625
1686
|
filePath: config.filePath,
|
|
626
1687
|
expectedUrl,
|
|
1688
|
+
expectedCommand,
|
|
627
1689
|
actualUrl: actual?.url || "",
|
|
1690
|
+
actualCommand: actual?.command || "",
|
|
628
1691
|
status,
|
|
1692
|
+
headerStatus: currentHeaderStatus,
|
|
1693
|
+
expectedHeaderNames: headerNames(expectedHeaders),
|
|
1694
|
+
actualHeaderNames: headerNames(actualHeaders),
|
|
629
1695
|
});
|
|
630
1696
|
}
|
|
631
1697
|
|
|
@@ -658,7 +1724,9 @@ export function scanForStdioServers(filePath) {
|
|
|
658
1724
|
|
|
659
1725
|
export function remediate(filePath, stdioServers, policy = {}) {
|
|
660
1726
|
const resolvedPath = resolveFilePath(filePath);
|
|
661
|
-
const offenders = Array.isArray(stdioServers)
|
|
1727
|
+
const offenders = Array.isArray(stdioServers)
|
|
1728
|
+
? stdioServers.filter((server) => server?.name)
|
|
1729
|
+
: [];
|
|
662
1730
|
const action = policy?.stdio_action || "warn";
|
|
663
1731
|
|
|
664
1732
|
if (offenders.length === 0) {
|
|
@@ -722,10 +1790,19 @@ export function remediate(filePath, stdioServers, policy = {}) {
|
|
|
722
1790
|
let replacement = null;
|
|
723
1791
|
|
|
724
1792
|
if (action === "replace-with-hub") {
|
|
725
|
-
const registry = loadRegistryOrDefault();
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
1793
|
+
const registry = policy.registry || loadRegistryOrDefault();
|
|
1794
|
+
const matchingOffender = offenders.find((server) =>
|
|
1795
|
+
Object.hasOwn(registry.servers || {}, server.name),
|
|
1796
|
+
);
|
|
1797
|
+
const [hubServerName, hubServerConfig] = matchingOffender
|
|
1798
|
+
? [matchingOffender.name, registry.servers[matchingOffender.name]]
|
|
1799
|
+
: getHubServerEntry(registry);
|
|
1800
|
+
const desired = buildDesiredServerRecord(
|
|
1801
|
+
hubServerName,
|
|
1802
|
+
hubServerConfig,
|
|
1803
|
+
resolvedPath,
|
|
1804
|
+
);
|
|
1805
|
+
replacement = { name: desired.name, ...redactServerConfig(desired.config) };
|
|
729
1806
|
updates.push(desired);
|
|
730
1807
|
}
|
|
731
1808
|
|
|
@@ -736,15 +1813,19 @@ export function remediate(filePath, stdioServers, policy = {}) {
|
|
|
736
1813
|
backupPath,
|
|
737
1814
|
removedServers: removals,
|
|
738
1815
|
replacement,
|
|
739
|
-
warnings: [],
|
|
1816
|
+
warnings: updates.flatMap((update) => update.warnings || []),
|
|
740
1817
|
};
|
|
741
1818
|
}
|
|
742
1819
|
|
|
743
1820
|
export function resolveHubUrl() {
|
|
744
1821
|
const registryState = inspectRegistry();
|
|
745
|
-
const registry = registryState.valid
|
|
1822
|
+
const registry = registryState.valid
|
|
1823
|
+
? registryState.registry
|
|
1824
|
+
: cloneDefaultRegistry();
|
|
746
1825
|
const [, hubServer] = getHubServerEntry(registry);
|
|
747
|
-
const fallbackRaw =
|
|
1826
|
+
const fallbackRaw =
|
|
1827
|
+
hubServer?.url ||
|
|
1828
|
+
`${registry?.defaults?.hub_base || "http://127.0.0.1:27888"}${DEFAULT_HUB_PATH}`;
|
|
748
1829
|
|
|
749
1830
|
let fallback;
|
|
750
1831
|
try {
|
|
@@ -754,22 +1835,25 @@ export function resolveHubUrl() {
|
|
|
754
1835
|
}
|
|
755
1836
|
|
|
756
1837
|
const envPortRaw = Number(process.env.TFX_HUB_PORT || "");
|
|
757
|
-
const envPort =
|
|
1838
|
+
const envPort =
|
|
1839
|
+
Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : null;
|
|
758
1840
|
const target = {
|
|
759
1841
|
protocol: fallback.protocol || "http:",
|
|
760
1842
|
host: fallback.hostname || "127.0.0.1",
|
|
761
1843
|
port: envPort || Number(fallback.port || 27888),
|
|
762
|
-
pathname:
|
|
1844
|
+
pathname:
|
|
1845
|
+
fallback.pathname && fallback.pathname !== "/"
|
|
1846
|
+
? fallback.pathname
|
|
1847
|
+
: DEFAULT_HUB_PATH,
|
|
763
1848
|
};
|
|
764
1849
|
|
|
1850
|
+
// PR #158 정책: port = TFX_HUB_PORT env (없으면 registry/default 27888) single source.
|
|
1851
|
+
// hub.pid 는 loopback host 힌트 전용. 과거 pid port cascade 는 오염된 port 영속화의
|
|
1852
|
+
// 원인이었고 hub-ensure.resolveHubTarget 에서 이미 제거됨. 여기도 일관 적용.
|
|
765
1853
|
const hubPidPath = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
766
1854
|
if (existsSync(hubPidPath)) {
|
|
767
1855
|
try {
|
|
768
1856
|
const info = readJsonFile(hubPidPath);
|
|
769
|
-
if (!envPort) {
|
|
770
|
-
const pidPort = Number(info?.port);
|
|
771
|
-
if (Number.isFinite(pidPort) && pidPort > 0) target.port = pidPort;
|
|
772
|
-
}
|
|
773
1857
|
if (typeof info?.host === "string") {
|
|
774
1858
|
const host = info.host.trim();
|
|
775
1859
|
if (LOOPBACK_HOSTS.has(host)) target.host = host;
|
|
@@ -785,7 +1869,9 @@ export function resolveHubUrl() {
|
|
|
785
1869
|
|
|
786
1870
|
export function isWatchedPath(filePath) {
|
|
787
1871
|
const registryState = inspectRegistry();
|
|
788
|
-
const registry = registryState.valid
|
|
1872
|
+
const registry = registryState.valid
|
|
1873
|
+
? registryState.registry
|
|
1874
|
+
: cloneDefaultRegistry();
|
|
789
1875
|
const candidate = normalizeForMatch(filePath);
|
|
790
1876
|
|
|
791
1877
|
return (registry?.policies?.watched_paths || []).some((watchedPath) => {
|
|
@@ -802,7 +1888,10 @@ export function isWatchedPath(filePath) {
|
|
|
802
1888
|
return pathBasename(candidate) === trimmed.toLowerCase();
|
|
803
1889
|
}
|
|
804
1890
|
|
|
805
|
-
const suffix = trimmed
|
|
1891
|
+
const suffix = trimmed
|
|
1892
|
+
.replace(/^[.][\\/]/, "")
|
|
1893
|
+
.replace(/\\/g, "/")
|
|
1894
|
+
.toLowerCase();
|
|
806
1895
|
return candidate.endsWith(`/${suffix}`);
|
|
807
1896
|
});
|
|
808
1897
|
}
|
|
@@ -814,16 +1903,34 @@ export function addRegistryServer(name, url, options = {}) {
|
|
|
814
1903
|
if (!normalizedUrl) throw new Error("server url is required");
|
|
815
1904
|
|
|
816
1905
|
const registryState = inspectRegistry();
|
|
817
|
-
const registry = registryState.valid
|
|
818
|
-
|
|
1906
|
+
const registry = registryState.valid
|
|
1907
|
+
? loadRegistry()
|
|
1908
|
+
: cloneDefaultRegistry();
|
|
1909
|
+
const transport =
|
|
1910
|
+
options.transport || (trimmedName === "tfx-hub" ? "hub-url" : "http");
|
|
1911
|
+
const policy =
|
|
1912
|
+
options.policy ||
|
|
1913
|
+
(transport === "stdio"
|
|
1914
|
+
? "stdio"
|
|
1915
|
+
: options.gateway_port || options.port
|
|
1916
|
+
? "gateway-sse"
|
|
1917
|
+
: "hosted");
|
|
819
1918
|
|
|
820
1919
|
registry.servers[trimmedName] = {
|
|
1920
|
+
policy,
|
|
821
1921
|
transport,
|
|
822
1922
|
url: normalizedUrl,
|
|
823
1923
|
safe: options.safe ?? true,
|
|
824
|
-
targets:
|
|
825
|
-
|
|
826
|
-
|
|
1924
|
+
targets:
|
|
1925
|
+
Array.isArray(options.targets) && options.targets.length > 0
|
|
1926
|
+
? [
|
|
1927
|
+
...new Set(
|
|
1928
|
+
options.targets
|
|
1929
|
+
.map((value) => String(value).trim())
|
|
1930
|
+
.filter(Boolean),
|
|
1931
|
+
),
|
|
1932
|
+
]
|
|
1933
|
+
: ["claude", "gemini", "codex", "antigravity"],
|
|
827
1934
|
description: options.description || `${trimmedName} MCP 서버`,
|
|
828
1935
|
};
|
|
829
1936
|
|
|
@@ -851,10 +1958,13 @@ export function removeServerFromTargets(name, options = {}) {
|
|
|
851
1958
|
const trimmedName = String(name || "").trim();
|
|
852
1959
|
if (!trimmedName) throw new Error("server name is required");
|
|
853
1960
|
|
|
854
|
-
const registry =
|
|
855
|
-
|
|
856
|
-
?
|
|
857
|
-
|
|
1961
|
+
const registry =
|
|
1962
|
+
options.registry ||
|
|
1963
|
+
(inspectRegistry().valid ? loadRegistry() : cloneDefaultRegistry());
|
|
1964
|
+
const targetsFilter =
|
|
1965
|
+
Array.isArray(options.targets) && options.targets.length > 0
|
|
1966
|
+
? new Set(options.targets)
|
|
1967
|
+
: null;
|
|
858
1968
|
const actions = [];
|
|
859
1969
|
|
|
860
1970
|
for (const target of listManagedConfigTargets(registry)) {
|
|
@@ -896,11 +2006,20 @@ export function removeServerFromTargets(name, options = {}) {
|
|
|
896
2006
|
|
|
897
2007
|
export function syncRegistryTargets(options = {}) {
|
|
898
2008
|
const registry = options.registry || loadRegistryOrDefault();
|
|
2009
|
+
const syncTargets = Array.isArray(options.targets)
|
|
2010
|
+
? options.targets
|
|
2011
|
+
: listManagedConfigTargets(registry);
|
|
2012
|
+
const primaryTargets = syncTargets.filter((target) =>
|
|
2013
|
+
isPrimaryConfigTarget(target.filePath),
|
|
2014
|
+
);
|
|
899
2015
|
const actions = [];
|
|
2016
|
+
const invalidConfigs = new Set();
|
|
2017
|
+
const denylist = syncDenylistEntries(registry.policies);
|
|
900
2018
|
|
|
901
|
-
for (const target of
|
|
2019
|
+
for (const target of syncTargets) {
|
|
902
2020
|
const snapshot = scanConfig(target.filePath);
|
|
903
2021
|
if (snapshot.parseError) {
|
|
2022
|
+
invalidConfigs.add(normalizeForMatch(target.filePath));
|
|
904
2023
|
actions.push({
|
|
905
2024
|
type: "sync",
|
|
906
2025
|
filePath: target.filePath,
|
|
@@ -911,8 +2030,14 @@ export function syncRegistryTargets(options = {}) {
|
|
|
911
2030
|
continue;
|
|
912
2031
|
}
|
|
913
2032
|
|
|
914
|
-
if (
|
|
915
|
-
|
|
2033
|
+
if (
|
|
2034
|
+
snapshot.stdioServers.length > 0 &&
|
|
2035
|
+
!isClaudeUserConfig(target.filePath)
|
|
2036
|
+
) {
|
|
2037
|
+
const remediation = remediate(target.filePath, snapshot.stdioServers, {
|
|
2038
|
+
...registry.policies,
|
|
2039
|
+
registry,
|
|
2040
|
+
});
|
|
916
2041
|
actions.push({
|
|
917
2042
|
type: "remediate",
|
|
918
2043
|
filePath: target.filePath,
|
|
@@ -925,10 +2050,50 @@ export function syncRegistryTargets(options = {}) {
|
|
|
925
2050
|
}
|
|
926
2051
|
}
|
|
927
2052
|
|
|
928
|
-
for (const target of
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
2053
|
+
for (const target of primaryTargets) {
|
|
2054
|
+
const snapshot = scanConfig(target.filePath);
|
|
2055
|
+
const targetKey = normalizeForMatch(target.filePath);
|
|
2056
|
+
if (snapshot.parseError) {
|
|
2057
|
+
if (!invalidConfigs.has(targetKey)) {
|
|
2058
|
+
actions.push({
|
|
2059
|
+
type: "sync",
|
|
2060
|
+
filePath: target.filePath,
|
|
2061
|
+
label: target.label,
|
|
2062
|
+
status: "invalid-config",
|
|
2063
|
+
message: snapshot.parseError.message,
|
|
2064
|
+
});
|
|
2065
|
+
}
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
const updates = [];
|
|
2070
|
+
for (const [name, serverConfig] of Object.entries(registry.servers || {})) {
|
|
2071
|
+
if (serverConfig?.safe !== true) continue;
|
|
2072
|
+
if (!serverAppliesToClient(serverConfig, target.client)) continue;
|
|
2073
|
+
|
|
2074
|
+
const denyKey = syncDenylistKey(target.client, name);
|
|
2075
|
+
if (denylist.has(denyKey)) {
|
|
2076
|
+
const existing = snapshot.servers.find(
|
|
2077
|
+
(server) => server.name === name,
|
|
2078
|
+
);
|
|
2079
|
+
if (!existing) {
|
|
2080
|
+
actions.push({
|
|
2081
|
+
type: "sync",
|
|
2082
|
+
filePath: target.filePath,
|
|
2083
|
+
label: target.label,
|
|
2084
|
+
status: "policy-skip",
|
|
2085
|
+
server: name,
|
|
2086
|
+
client: target.client,
|
|
2087
|
+
message: `[mcp-guard] policy skip: ${denyKey}`,
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
continue;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
updates.push(
|
|
2094
|
+
buildDesiredServerRecord(name, serverConfig, target.filePath),
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
932
2097
|
|
|
933
2098
|
if (updates.length === 0) continue;
|
|
934
2099
|
|
|
@@ -947,6 +2112,7 @@ export function syncRegistryTargets(options = {}) {
|
|
|
947
2112
|
label: target.label,
|
|
948
2113
|
status: result.modified ? "updated" : "ok",
|
|
949
2114
|
serverCount: updates.length,
|
|
2115
|
+
warnings: updates.flatMap((update) => update.warnings || []),
|
|
950
2116
|
});
|
|
951
2117
|
}
|
|
952
2118
|
|