@triflux/core 10.50.2 → 10.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/agy-session-hook.mjs +11 -56
- package/hooks/codex-session-hook.mjs +11 -101
- package/hub/bridge.mjs +65 -1250
- package/hub/cli-adapter-base.mjs +12 -90
- package/hub/codex-adapter.mjs +0 -1
- package/hub/codex-preflight.mjs +5 -28
- package/hub/index.mjs +3 -19
- package/hub/lib/process-utils.mjs +1 -156
- package/hub/lib/spawn-trace.mjs +3 -2
- package/hub/lib/timeout-defaults.mjs +0 -1
- package/hub/team/claude-daemon-control.mjs +1 -2
- package/hub/team/claude-session-projection.mjs +5 -73
- package/hub/team/nativeProxy.mjs +759 -0
- package/hub/team/session-context.mjs +1 -1
- package/hud/cli-policy.mjs +0 -22
- package/hud/constants.mjs +13 -26
- package/hud/hud-qos-status.mjs +42 -54
- package/hud/providers/gemini.mjs +125 -20
- package/hud/renderers.mjs +101 -122
- package/hud/utils.mjs +10 -6
- package/package.json +2 -6
- package/scripts/lib/agent-route-policy.mjs +143 -62
- package/scripts/lib/cli-claude.mjs +0 -14
- package/scripts/lib/env-probe.mjs +2 -131
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-guard-engine.mjs +40 -98
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +11 -59
- package/scripts/lib/session-presence.mjs +2 -100
- package/scripts/lib/skill-template.mjs +0 -245
- package/hub/adaptive-diagnostic.mjs +0 -350
- package/hub/adaptive-inject.mjs +0 -224
- package/hub/adaptive-memory.mjs +0 -277
- package/hub/adaptive.mjs +0 -172
- package/hub/assign-callbacks.mjs +0 -170
- package/hub/delegator/contracts.mjs +0 -38
- package/hub/delegator/index.mjs +0 -14
- package/hub/delegator/schema/delegator-tools.schema.json +0 -250
- package/hub/delegator/service.mjs +0 -336
- package/hub/delegator/tool-definitions.mjs +0 -35
- package/hub/dynamic-routing-engine.mjs +0 -511
- package/hub/hitl.mjs +0 -202
- package/hub/lib/known-errors.json +0 -72
- package/hub/lib/memory-store.mjs +0 -1001
- package/hub/lib/phase-manager.mjs +0 -493
- package/hub/lib/risk-tier.mjs +0 -53
- package/hub/lib/ssh-command.mjs +0 -231
- package/hub/lib/staged-file-detect.mjs +0 -43
- package/hub/lib/state-snapshot.mjs +0 -293
- package/hub/lib/tfx-route-args.mjs +0 -353
- package/hub/lib/trace-recorder.mjs +0 -153
- package/hub/lib/uuidv7.mjs +0 -45
- package/hub/memory-doctor.mjs +0 -640
- package/hub/middleware/quota-middleware.mjs +0 -70
- package/hub/middleware/request-logger.mjs +0 -191
- package/hub/paths.mjs +0 -24
- package/hub/pipeline/index.mjs +0 -163
- package/hub/pipeline/state.mjs +0 -191
- package/hub/pipeline/transitions.mjs +0 -131
- package/hub/reflexion.mjs +0 -290
- package/hub/role-contract.mjs +0 -314
- package/hub/router.mjs +0 -2026
- package/hub/routing-snapshot.mjs +0 -261
- package/hub/session-fingerprint.mjs +0 -418
- package/hub/state.mjs +0 -300
- package/hub/team/synapse-http.mjs +0 -311
- package/hub/team-bridge.mjs +0 -27
- package/mesh/index.mjs +0 -69
- package/mesh/mesh-budget.mjs +0 -137
- package/mesh/mesh-heartbeat.mjs +0 -100
- package/mesh/mesh-protocol.mjs +0 -96
- package/mesh/mesh-queue.mjs +0 -163
- package/mesh/mesh-registry.mjs +0 -78
- package/mesh/mesh-router.mjs +0 -77
- package/scripts/lib/claudemd-scanner.mjs +0 -223
- package/scripts/lib/context.mjs +0 -67
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/handoff.mjs +0 -187
- package/scripts/lib/logger.mjs +0 -122
- package/scripts/lib/stealth-fetch.mjs +0 -176
|
@@ -15,10 +15,6 @@ import {
|
|
|
15
15
|
uniqueStrings,
|
|
16
16
|
} from "./mcp-server-catalog.mjs";
|
|
17
17
|
|
|
18
|
-
export const KNOWN_MCP_SERVERS = Object.freeze(
|
|
19
|
-
Object.keys(MCP_SERVER_TOOL_CATALOG),
|
|
20
|
-
);
|
|
21
|
-
|
|
22
18
|
const SEARCH_INTENT_PATTERNS = Object.freeze([
|
|
23
19
|
/\b(search|web|browse|look ?up|find|latest|recent|news|current|today|release(?: note)?s?|changelog|announcement|pricing|status|verify|fact[- ]?check)\b/i,
|
|
24
20
|
/(검색|웹|브라우즈|찾아|조회|최신|최근|뉴스|현재|오늘|릴리즈|배포|변경사항|공지|가격|상태|검증)/u,
|
|
@@ -145,7 +141,7 @@ const PROFILE_DEFINITIONS = Object.freeze({
|
|
|
145
141
|
* 파이프라인 단계별 MCP 서버/도구 제한 (post-filter).
|
|
146
142
|
* role-based 프로필 위에 추가 적용. 빈 배열 = 전체 차단, 미정의 = 제한 없음.
|
|
147
143
|
*/
|
|
148
|
-
|
|
144
|
+
const PHASE_OVERRIDES = Object.freeze({
|
|
149
145
|
plan: Object.freeze({
|
|
150
146
|
description: "계획 단계: 읽기 전용 탐색만 허용",
|
|
151
147
|
allowedServers: Object.freeze(["context7"]),
|
|
@@ -166,7 +162,7 @@ export const PHASE_OVERRIDES = Object.freeze({
|
|
|
166
162
|
}),
|
|
167
163
|
});
|
|
168
164
|
|
|
169
|
-
|
|
165
|
+
const LEGACY_PROFILE_ALIASES = Object.freeze({
|
|
170
166
|
implement: "executor",
|
|
171
167
|
analyze: "analyze",
|
|
172
168
|
review: "reviewer",
|
|
@@ -257,30 +253,51 @@ function loadInventory(inventoryFile = "") {
|
|
|
257
253
|
}
|
|
258
254
|
}
|
|
259
255
|
|
|
256
|
+
function availableServersFromInventory(inventory, cliType) {
|
|
257
|
+
const servers = inventory?.[cliType]?.servers || [];
|
|
258
|
+
return uniqueStrings(
|
|
259
|
+
servers
|
|
260
|
+
.filter((server) => ["enabled", "configured"].includes(server.status))
|
|
261
|
+
.map((server) => server.name),
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function codexServersFromConfig(configFile) {
|
|
266
|
+
if (!configFile) return [];
|
|
267
|
+
try {
|
|
268
|
+
return uniqueStrings(
|
|
269
|
+
readFileSync(configFile, "utf8")
|
|
270
|
+
.split(/\r?\n/)
|
|
271
|
+
.map((line) => /^\[mcp_servers\.([^\].]+)\]$/.exec(line)?.[1])
|
|
272
|
+
.filter(Boolean),
|
|
273
|
+
).sort();
|
|
274
|
+
} catch {
|
|
275
|
+
return [];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
260
279
|
function buildInventoryIndex(inventory = null) {
|
|
261
280
|
const index = new Map();
|
|
262
281
|
if (!inventory || typeof inventory !== "object") return index;
|
|
263
282
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
tool_count
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
domain_tags:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
});
|
|
283
|
-
}
|
|
283
|
+
const servers = Array.isArray(inventory.codex?.servers)
|
|
284
|
+
? inventory.codex.servers
|
|
285
|
+
: [];
|
|
286
|
+
for (const server of servers) {
|
|
287
|
+
if (!server || typeof server.name !== "string" || !server.name.trim())
|
|
288
|
+
continue;
|
|
289
|
+
const name = server.name.trim();
|
|
290
|
+
const previous = index.get(name) || {};
|
|
291
|
+
index.set(name, {
|
|
292
|
+
...previous,
|
|
293
|
+
tool_count: Number.isFinite(server.tool_count)
|
|
294
|
+
? Math.max(previous.tool_count ?? 0, Math.trunc(server.tool_count))
|
|
295
|
+
: previous.tool_count,
|
|
296
|
+
domain_tags: uniqueStrings([
|
|
297
|
+
...(Array.isArray(previous.domain_tags) ? previous.domain_tags : []),
|
|
298
|
+
...(Array.isArray(server.domain_tags) ? server.domain_tags : []),
|
|
299
|
+
]),
|
|
300
|
+
});
|
|
284
301
|
}
|
|
285
302
|
|
|
286
303
|
return index;
|
|
@@ -498,7 +515,7 @@ export function resolveMcpProfile(agentType = "", requestedProfile = "auto") {
|
|
|
498
515
|
return normalized === "auto" ? resolveAutoProfile(agentType) : normalized;
|
|
499
516
|
}
|
|
500
517
|
|
|
501
|
-
|
|
518
|
+
function parseAvailableServers(rawAvailableServers = "") {
|
|
502
519
|
if (Array.isArray(rawAvailableServers))
|
|
503
520
|
return uniqueStrings(rawAvailableServers);
|
|
504
521
|
if (typeof rawAvailableServers !== "string" || !rawAvailableServers.trim())
|
|
@@ -554,15 +571,21 @@ function applyManifestFilter(servers) {
|
|
|
554
571
|
return servers.filter((s) => enabled.has(s));
|
|
555
572
|
}
|
|
556
573
|
|
|
557
|
-
|
|
574
|
+
function resolveAllowedServers(options = {}) {
|
|
558
575
|
const resolvedProfile = resolveMcpProfile(
|
|
559
576
|
options.agentType,
|
|
560
577
|
options.requestedProfile,
|
|
561
578
|
);
|
|
562
579
|
const profile = getProfileDefinition(resolvedProfile);
|
|
563
580
|
const availableServers = parseAvailableServers(options.availableServers);
|
|
564
|
-
const inventory =
|
|
565
|
-
|
|
581
|
+
const inventory =
|
|
582
|
+
options.inventory !== undefined
|
|
583
|
+
? options.inventory
|
|
584
|
+
: loadInventory(options.inventoryFile);
|
|
585
|
+
const inventoryIndex =
|
|
586
|
+
options.inventoryIndex instanceof Map
|
|
587
|
+
? options.inventoryIndex
|
|
588
|
+
: buildInventoryIndex(inventory);
|
|
566
589
|
const baseServers = availableServers.length
|
|
567
590
|
? profile.allowedServers.filter((server) =>
|
|
568
591
|
availableServers.includes(server),
|
|
@@ -585,8 +608,14 @@ export function buildPromptHint(options = {}) {
|
|
|
585
608
|
);
|
|
586
609
|
if (resolvedProfile === "none") return "";
|
|
587
610
|
|
|
588
|
-
const inventory =
|
|
589
|
-
|
|
611
|
+
const inventory =
|
|
612
|
+
options.inventory !== undefined
|
|
613
|
+
? options.inventory
|
|
614
|
+
: loadInventory(options.inventoryFile);
|
|
615
|
+
const inventoryIndex =
|
|
616
|
+
options.inventoryIndex instanceof Map
|
|
617
|
+
? options.inventoryIndex
|
|
618
|
+
: buildInventoryIndex(inventory);
|
|
590
619
|
const allowedServers = resolveAllowedServers({
|
|
591
620
|
...options,
|
|
592
621
|
inventory,
|
|
@@ -636,16 +665,7 @@ export function buildPromptHint(options = {}) {
|
|
|
636
665
|
.join(" ");
|
|
637
666
|
}
|
|
638
667
|
|
|
639
|
-
export function getGeminiAllowedServers(options = {}) {
|
|
640
|
-
return resolveAllowedServers(options);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
668
|
export function getCodexMcpConfig(options = {}) {
|
|
644
|
-
const allowedServers = new Set(resolveAllowedServers(options));
|
|
645
|
-
const resolvedProfile = resolveMcpProfile(
|
|
646
|
-
options.agentType,
|
|
647
|
-
options.requestedProfile,
|
|
648
|
-
);
|
|
649
669
|
// Codex에 실제 등록된 서버만 대상으로 config override 생성.
|
|
650
670
|
// 미등록 서버에 enabled=false를 보내면 "invalid transport" 에러 발생.
|
|
651
671
|
const registeredServers = parseAvailableServers(options.availableServers);
|
|
@@ -654,6 +674,11 @@ export function getCodexMcpConfig(options = {}) {
|
|
|
654
674
|
if (registeredServers.length === 0) {
|
|
655
675
|
return { mcp_servers: {} };
|
|
656
676
|
}
|
|
677
|
+
const allowedServers = new Set(resolveAllowedServers(options));
|
|
678
|
+
const resolvedProfile = resolveMcpProfile(
|
|
679
|
+
options.agentType,
|
|
680
|
+
options.requestedProfile,
|
|
681
|
+
);
|
|
657
682
|
const targetServers = registeredServers;
|
|
658
683
|
|
|
659
684
|
if (resolvedProfile === "none") {
|
|
@@ -712,9 +737,27 @@ export function getCodexConfigOverrides(options = {}) {
|
|
|
712
737
|
}
|
|
713
738
|
|
|
714
739
|
export function buildMcpPolicy(options = {}) {
|
|
715
|
-
const inventory =
|
|
740
|
+
const inventory =
|
|
741
|
+
options.inventory !== undefined
|
|
742
|
+
? options.inventory
|
|
743
|
+
: loadInventory(options.inventoryFile);
|
|
716
744
|
const inventoryIndex = buildInventoryIndex(inventory);
|
|
717
|
-
|
|
745
|
+
let availableServers = options.availableServers;
|
|
746
|
+
if (availableServers === undefined) {
|
|
747
|
+
availableServers = availableServersFromInventory(
|
|
748
|
+
inventory,
|
|
749
|
+
options.cliType,
|
|
750
|
+
);
|
|
751
|
+
if (availableServers.length === 0 && options.cliType === "codex") {
|
|
752
|
+
availableServers = codexServersFromConfig(options.codexConfig);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
const resolvedOptions = {
|
|
756
|
+
...options,
|
|
757
|
+
availableServers,
|
|
758
|
+
inventory,
|
|
759
|
+
inventoryIndex,
|
|
760
|
+
};
|
|
718
761
|
const resolvedProfile = resolveMcpProfile(
|
|
719
762
|
options.agentType,
|
|
720
763
|
options.requestedProfile,
|
|
@@ -730,6 +773,12 @@ export function buildMcpPolicy(options = {}) {
|
|
|
730
773
|
allowedServers = allowedServers.filter((s) => !blocked.has(s));
|
|
731
774
|
}
|
|
732
775
|
|
|
776
|
+
// 플러그인 서버는 transport 설정이 없어 CLI override를 받으면 부팅이 실패한다.
|
|
777
|
+
const codexOptions = {
|
|
778
|
+
...resolvedOptions,
|
|
779
|
+
availableServers: codexServersFromConfig(options.codexConfig),
|
|
780
|
+
};
|
|
781
|
+
|
|
733
782
|
return {
|
|
734
783
|
requestedProfile:
|
|
735
784
|
typeof options.requestedProfile === "string" && options.requestedProfile
|
|
@@ -739,9 +788,8 @@ export function buildMcpPolicy(options = {}) {
|
|
|
739
788
|
resolvedPhase: phase || null,
|
|
740
789
|
allowedServers,
|
|
741
790
|
hint,
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
codexConfigOverrides: getCodexConfigOverrides(resolvedOptions),
|
|
791
|
+
codexConfig: getCodexMcpConfig(codexOptions),
|
|
792
|
+
codexConfigOverrides: getCodexConfigOverrides(codexOptions),
|
|
745
793
|
};
|
|
746
794
|
}
|
|
747
795
|
|
|
@@ -753,25 +801,25 @@ function shellArray(name, values) {
|
|
|
753
801
|
return `${name}=(${values.map((value) => shellEscape(value)).join(" ")})`;
|
|
754
802
|
}
|
|
755
803
|
|
|
756
|
-
|
|
804
|
+
function toDelimited(policy) {
|
|
757
805
|
const RS = "\x1e";
|
|
758
806
|
return [
|
|
759
807
|
policy.requestedProfile,
|
|
760
808
|
policy.resolvedProfile,
|
|
761
809
|
policy.hint,
|
|
762
|
-
policy.
|
|
763
|
-
policy.codexConfigOverrides.flatMap((o) => ["-c", o]).join("
|
|
810
|
+
policy.allowedServers.join(","),
|
|
811
|
+
policy.codexConfigOverrides.flatMap((o) => ["-c", o]).join("\x1f"),
|
|
764
812
|
JSON.stringify(policy.codexConfig),
|
|
765
813
|
policy.resolvedPhase || "",
|
|
766
814
|
].join(RS);
|
|
767
815
|
}
|
|
768
816
|
|
|
769
|
-
|
|
817
|
+
function toShellExports(policy) {
|
|
770
818
|
const lines = [
|
|
771
819
|
`MCP_PROFILE_REQUESTED=${shellEscape(policy.requestedProfile)}`,
|
|
772
820
|
`MCP_RESOLVED_PROFILE=${shellEscape(policy.resolvedProfile)}`,
|
|
773
821
|
`MCP_HINT=${shellEscape(policy.hint)}`,
|
|
774
|
-
shellArray("
|
|
822
|
+
shellArray("ALLOWED_MCP_SERVERS", policy.allowedServers),
|
|
775
823
|
shellArray(
|
|
776
824
|
"CODEX_CONFIG_FLAGS",
|
|
777
825
|
policy.codexConfigOverrides.flatMap((override) => ["-c", override]),
|
|
@@ -789,8 +837,10 @@ function parseCliArgs(argv) {
|
|
|
789
837
|
command: "json",
|
|
790
838
|
agentType: "",
|
|
791
839
|
requestedProfile: "auto",
|
|
792
|
-
availableServers:
|
|
840
|
+
availableServers: undefined,
|
|
793
841
|
inventoryFile: "",
|
|
842
|
+
cliType: "",
|
|
843
|
+
codexConfig: "",
|
|
794
844
|
searchTool: "",
|
|
795
845
|
taskText: "",
|
|
796
846
|
workerIndex: undefined,
|
|
@@ -821,6 +871,12 @@ function parseCliArgs(argv) {
|
|
|
821
871
|
case "--available":
|
|
822
872
|
args.availableServers = parseAvailableServers(next());
|
|
823
873
|
break;
|
|
874
|
+
case "--cli-type":
|
|
875
|
+
args.cliType = next();
|
|
876
|
+
break;
|
|
877
|
+
case "--codex-config":
|
|
878
|
+
args.codexConfig = next();
|
|
879
|
+
break;
|
|
824
880
|
case "--inventory-file":
|
|
825
881
|
args.inventoryFile = next();
|
|
826
882
|
break;
|
|
@@ -844,7 +900,7 @@ function parseCliArgs(argv) {
|
|
|
844
900
|
return args;
|
|
845
901
|
}
|
|
846
902
|
|
|
847
|
-
|
|
903
|
+
async function runCli(argv = process.argv.slice(2)) {
|
|
848
904
|
let args;
|
|
849
905
|
try {
|
|
850
906
|
args = parseCliArgs(argv);
|
|
@@ -19,35 +19,23 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
|
|
20
20
|
const PROJECT_ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
|
21
21
|
const DEFAULT_REGISTRY_PATH = join(PROJECT_ROOT, "config", "mcp-registry.json");
|
|
22
|
-
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
23
|
-
const DEFAULT_HUB_PATH = "/mcp";
|
|
24
22
|
const SERVER_POLICIES = new Set(["hosted", "stdio"]);
|
|
25
23
|
const DEFAULT_REGISTRY = Object.freeze({
|
|
26
24
|
$schema: "mcp-registry-schema",
|
|
27
25
|
version: 1,
|
|
28
26
|
description: "MCP 서버 중앙 레지스트리 — 진실의 원천",
|
|
29
27
|
defaults: {
|
|
30
|
-
transport: "
|
|
31
|
-
hub_base: "http://127.0.0.1:27888",
|
|
28
|
+
transport: "http",
|
|
32
29
|
},
|
|
33
30
|
policy_notes: {
|
|
34
31
|
transport:
|
|
35
|
-
'Server transport accepts "
|
|
32
|
+
'Server transport accepts "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.',
|
|
36
33
|
headers:
|
|
37
34
|
'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.',
|
|
38
35
|
secret_safety:
|
|
39
36
|
"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.",
|
|
40
37
|
},
|
|
41
|
-
servers: {
|
|
42
|
-
"tfx-hub": {
|
|
43
|
-
policy: "hosted",
|
|
44
|
-
transport: "hub-url",
|
|
45
|
-
url: "http://127.0.0.1:27888/mcp",
|
|
46
|
-
safe: true,
|
|
47
|
-
targets: ["claude", "gemini", "codex", "antigravity"],
|
|
48
|
-
description: "triflux Hub MCP 서버",
|
|
49
|
-
},
|
|
50
|
-
},
|
|
38
|
+
servers: {},
|
|
51
39
|
policies: {
|
|
52
40
|
unknown_server_action: "warn",
|
|
53
41
|
sync_denylist: [],
|
|
@@ -711,28 +699,6 @@ function upsertTomlServer(raw, name, config, codex = {}) {
|
|
|
711
699
|
return output.join("\n");
|
|
712
700
|
}
|
|
713
701
|
|
|
714
|
-
function getHubServerEntry(registry) {
|
|
715
|
-
const entries = Object.entries(registry?.servers || {});
|
|
716
|
-
if (entries.length === 0) {
|
|
717
|
-
return [
|
|
718
|
-
"tfx-hub",
|
|
719
|
-
{
|
|
720
|
-
url: `${registry?.defaults?.hub_base || "http://127.0.0.1:27888"}${DEFAULT_HUB_PATH}`,
|
|
721
|
-
},
|
|
722
|
-
];
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
return (
|
|
726
|
-
entries.find(([name]) => name === "tfx-hub") ||
|
|
727
|
-
entries.find(([, config]) => config?.transport === "hub-url") ||
|
|
728
|
-
entries[0]
|
|
729
|
-
);
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function _makeHubRuntimeConfig() {
|
|
733
|
-
return { url: resolveHubUrl() };
|
|
734
|
-
}
|
|
735
|
-
|
|
736
702
|
function serverTargets(serverConfig) {
|
|
737
703
|
if (Array.isArray(serverConfig?.targets) && serverConfig.targets.length > 0) {
|
|
738
704
|
return [
|
|
@@ -806,10 +772,7 @@ export function buildDesiredServerRecord(name, serverConfig, filePath) {
|
|
|
806
772
|
};
|
|
807
773
|
}
|
|
808
774
|
|
|
809
|
-
const url =
|
|
810
|
-
serverConfig?.transport === "hub-url"
|
|
811
|
-
? resolveHubUrl()
|
|
812
|
-
: normalizeUrl(serverConfig?.url || "");
|
|
775
|
+
const url = normalizeUrl(serverConfig?.url || "");
|
|
813
776
|
const basenameValue = pathBasename(filePath);
|
|
814
777
|
const resolvedHeaders = resolveHeaderDescriptors(
|
|
815
778
|
name,
|
|
@@ -1205,10 +1168,8 @@ export function validateRegistry(registry) {
|
|
|
1205
1168
|
errors.push(`registry.servers.${name}.policy must be hosted or stdio`);
|
|
1206
1169
|
}
|
|
1207
1170
|
const transport = server.transport || registry.defaults?.transport;
|
|
1208
|
-
if (!["
|
|
1209
|
-
errors.push(
|
|
1210
|
-
`registry.servers.${name}.transport must be hub-url, http, or stdio`,
|
|
1211
|
-
);
|
|
1171
|
+
if (!["http", "stdio"].includes(transport)) {
|
|
1172
|
+
errors.push(`registry.servers.${name}.transport must be http or stdio`);
|
|
1212
1173
|
}
|
|
1213
1174
|
if (
|
|
1214
1175
|
policy === "hosted" &&
|
|
@@ -1407,6 +1368,38 @@ export function validateRegistry(registry) {
|
|
|
1407
1368
|
return errors;
|
|
1408
1369
|
}
|
|
1409
1370
|
|
|
1371
|
+
const warnedLegacyHub = new Set();
|
|
1372
|
+
|
|
1373
|
+
// 허브가 있던 시기의 사용자 레지스트리는 hub-url 항목을 걸러내고 기본 transport 를 http 로 읽는다.
|
|
1374
|
+
function dropLegacyHub(registry) {
|
|
1375
|
+
if (!registry || typeof registry !== "object" || Array.isArray(registry))
|
|
1376
|
+
return registry;
|
|
1377
|
+
const next = { ...registry };
|
|
1378
|
+
if (next.defaults?.transport === "hub-url") {
|
|
1379
|
+
const { hub_base: _hubBase, ...defaults } = next.defaults;
|
|
1380
|
+
next.defaults = { ...defaults, transport: "http" };
|
|
1381
|
+
}
|
|
1382
|
+
if (
|
|
1383
|
+
next.servers &&
|
|
1384
|
+
typeof next.servers === "object" &&
|
|
1385
|
+
!Array.isArray(next.servers)
|
|
1386
|
+
) {
|
|
1387
|
+
next.servers = Object.fromEntries(
|
|
1388
|
+
Object.entries(next.servers).filter(([name, server]) => {
|
|
1389
|
+
if (server?.transport !== "hub-url") return true;
|
|
1390
|
+
if (!warnedLegacyHub.has(name)) {
|
|
1391
|
+
warnedLegacyHub.add(name);
|
|
1392
|
+
process.stderr.write(
|
|
1393
|
+
`[mcp-guard] 제거된 hub-url 서버 ${name} 항목을 무시합니다\n`,
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
return false;
|
|
1397
|
+
}),
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
return next;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1410
1403
|
export function inspectRegistry() {
|
|
1411
1404
|
if (!existsSync(registryPath())) {
|
|
1412
1405
|
return {
|
|
@@ -1419,7 +1412,7 @@ export function inspectRegistry() {
|
|
|
1419
1412
|
}
|
|
1420
1413
|
|
|
1421
1414
|
try {
|
|
1422
|
-
const registry = readJsonFile(registryPath());
|
|
1415
|
+
const registry = dropLegacyHub(readJsonFile(registryPath()));
|
|
1423
1416
|
const errors = validateRegistry(registry);
|
|
1424
1417
|
return {
|
|
1425
1418
|
path: registryPath(),
|
|
@@ -1669,56 +1662,6 @@ export function scanForStdioServers(filePath) {
|
|
|
1669
1662
|
return scanConfig(filePath).stdioServers;
|
|
1670
1663
|
}
|
|
1671
1664
|
|
|
1672
|
-
export function resolveHubUrl() {
|
|
1673
|
-
const registryState = inspectRegistry();
|
|
1674
|
-
const registry = registryState.valid
|
|
1675
|
-
? registryState.registry
|
|
1676
|
-
: cloneDefaultRegistry();
|
|
1677
|
-
const [, hubServer] = getHubServerEntry(registry);
|
|
1678
|
-
const fallbackRaw =
|
|
1679
|
-
hubServer?.url ||
|
|
1680
|
-
`${registry?.defaults?.hub_base || "http://127.0.0.1:27888"}${DEFAULT_HUB_PATH}`;
|
|
1681
|
-
|
|
1682
|
-
let fallback;
|
|
1683
|
-
try {
|
|
1684
|
-
fallback = new URL(fallbackRaw);
|
|
1685
|
-
} catch {
|
|
1686
|
-
fallback = new URL(`http://127.0.0.1:27888${DEFAULT_HUB_PATH}`);
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
const envPortRaw = Number(process.env.TFX_HUB_PORT || "");
|
|
1690
|
-
const envPort =
|
|
1691
|
-
Number.isFinite(envPortRaw) && envPortRaw > 0 ? envPortRaw : null;
|
|
1692
|
-
const target = {
|
|
1693
|
-
protocol: fallback.protocol || "http:",
|
|
1694
|
-
host: fallback.hostname || "127.0.0.1",
|
|
1695
|
-
port: envPort || Number(fallback.port || 27888),
|
|
1696
|
-
pathname:
|
|
1697
|
-
fallback.pathname && fallback.pathname !== "/"
|
|
1698
|
-
? fallback.pathname
|
|
1699
|
-
: DEFAULT_HUB_PATH,
|
|
1700
|
-
};
|
|
1701
|
-
|
|
1702
|
-
// PR #158 정책: port = TFX_HUB_PORT env (없으면 registry/default 27888) single source.
|
|
1703
|
-
// hub.pid 는 loopback host 힌트 전용. 과거 pid port cascade 는 오염된 port 영속화의
|
|
1704
|
-
// 원인이었고 hub-ensure.resolveHubTarget 에서 이미 제거됨. 여기도 일관 적용.
|
|
1705
|
-
const hubPidPath = join(homedir(), ".claude", "cache", "tfx-hub", "hub.pid");
|
|
1706
|
-
if (existsSync(hubPidPath)) {
|
|
1707
|
-
try {
|
|
1708
|
-
const info = readJsonFile(hubPidPath);
|
|
1709
|
-
if (typeof info?.host === "string") {
|
|
1710
|
-
const host = info.host.trim();
|
|
1711
|
-
if (LOOPBACK_HOSTS.has(host)) target.host = host;
|
|
1712
|
-
}
|
|
1713
|
-
} catch {
|
|
1714
|
-
// pid 파일 파싱 실패 시 registry 기본값 사용
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
const hostPart = target.host.includes(":") ? `[${target.host}]` : target.host;
|
|
1719
|
-
return `${target.protocol}//${hostPart}:${target.port}${target.pathname}`;
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
1665
|
export function isWatchedPath(filePath) {
|
|
1723
1666
|
const registryState = inspectRegistry();
|
|
1724
1667
|
const registry = registryState.valid
|
|
@@ -1758,8 +1701,7 @@ export function addRegistryServer(name, url, options = {}) {
|
|
|
1758
1701
|
const registry = registryState.valid
|
|
1759
1702
|
? loadRegistry()
|
|
1760
1703
|
: cloneDefaultRegistry();
|
|
1761
|
-
const transport =
|
|
1762
|
-
options.transport || (trimmedName === "tfx-hub" ? "hub-url" : "http");
|
|
1704
|
+
const transport = options.transport || "http";
|
|
1763
1705
|
const policy = options.policy || (transport === "stdio" ? "stdio" : "hosted");
|
|
1764
1706
|
|
|
1765
1707
|
registry.servers[trimmedName] = {
|
|
@@ -224,7 +224,7 @@ export function parseMcpServersFromToml(content = "") {
|
|
|
224
224
|
return servers;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
|
|
227
|
+
function readMcpServers(configPath = DEFAULT_CONFIG_PATH) {
|
|
228
228
|
if (!existsSync(configPath)) return {};
|
|
229
229
|
try {
|
|
230
230
|
const content = readFileSync(configPath, "utf8");
|
|
@@ -754,7 +754,7 @@ function renderOutput(results, source, format) {
|
|
|
754
754
|
return JSON.stringify({ source, healthy, dead, results }, null, 2);
|
|
755
755
|
}
|
|
756
756
|
|
|
757
|
-
|
|
757
|
+
async function runCli(argv = process.argv.slice(2)) {
|
|
758
758
|
let args;
|
|
759
759
|
try {
|
|
760
760
|
args = parseCliArgs(argv);
|
|
@@ -296,7 +296,7 @@ export function uniqueStrings(values = []) {
|
|
|
296
296
|
];
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
|
|
299
|
+
function inferDomainTagsFromText(text = "") {
|
|
300
300
|
if (typeof text !== "string" || !text.trim()) return [];
|
|
301
301
|
const normalized = text.toLocaleLowerCase();
|
|
302
302
|
const matched = [];
|
|
@@ -314,7 +314,7 @@ export function inferDomainTagsFromText(text = "") {
|
|
|
314
314
|
return uniqueStrings(matched);
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
function getDefaultServerMetadata(serverName = "") {
|
|
318
318
|
const toolCount = MCP_SERVER_TOOL_CATALOG[serverName]?.length || 0;
|
|
319
319
|
const domainTags = uniqueStrings([
|
|
320
320
|
...(MCP_SERVER_DOMAIN_TAGS[serverName] || []),
|
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const PSMUX_RECOMMENDED_VERSION = "3.3.1";
|
|
4
|
+
const PSMUX_REQUIRED_COMMANDS = [
|
|
5
5
|
"new-session",
|
|
6
6
|
"attach-session",
|
|
7
7
|
"kill-session",
|
|
8
8
|
"capture-pane",
|
|
9
9
|
];
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
const PSMUX_OPTIONAL_COMMANDS = ["detach-client"];
|
|
12
12
|
|
|
13
13
|
// Windows 전용 설치 명령
|
|
14
|
-
|
|
14
|
+
const PSMUX_INSTALL_COMMANDS = [
|
|
15
15
|
"winget install psmux",
|
|
16
16
|
"scoop install psmux",
|
|
17
17
|
"choco install psmux",
|
|
18
18
|
"cargo install psmux",
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
const PSMUX_UPDATE_COMMANDS = [
|
|
22
22
|
"winget upgrade psmux",
|
|
23
23
|
"scoop update psmux",
|
|
24
24
|
"choco upgrade psmux",
|
|
25
25
|
"cargo install psmux --force",
|
|
26
26
|
];
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
function getPsmuxInstallCommandsFor(platform = process.platform) {
|
|
29
29
|
if (platform === "win32") return PSMUX_INSTALL_COMMANDS;
|
|
30
30
|
return [platform === "darwin" ? "brew install tmux" : "apt install tmux"];
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
function getPsmuxUpdateCommandsFor(platform = process.platform) {
|
|
34
34
|
if (platform === "win32") return PSMUX_UPDATE_COMMANDS;
|
|
35
35
|
return [
|
|
36
36
|
platform === "darwin"
|
|
@@ -39,7 +39,7 @@ export function getPsmuxUpdateCommandsFor(platform = process.platform) {
|
|
|
39
39
|
];
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
function formatPsmuxCommandList(
|
|
43
43
|
commands = PSMUX_INSTALL_COMMANDS,
|
|
44
44
|
indent = "",
|
|
45
45
|
) {
|
|
@@ -60,12 +60,12 @@ export function formatPsmuxUpdateGuidance(
|
|
|
60
60
|
return formatPsmuxCommandList(getPsmuxUpdateCommandsFor(platform), indent);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
function parsePsmuxVersion(output = "") {
|
|
64
64
|
const match = String(output).match(/psmux\s+v?(\d+\.\d+\.\d+)/i);
|
|
65
65
|
return match?.[1] || null;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
|
|
68
|
+
function compareSemver(a, b) {
|
|
69
69
|
const left = String(a || "")
|
|
70
70
|
.split(".")
|
|
71
71
|
.map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -79,7 +79,7 @@ export function compareSemver(a, b) {
|
|
|
79
79
|
return 0;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
function isRecommendedPsmuxVersion(version) {
|
|
83
83
|
if (!version) return false;
|
|
84
84
|
return compareSemver(version, PSMUX_RECOMMENDED_VERSION) >= 0;
|
|
85
85
|
}
|
|
@@ -143,51 +143,3 @@ export function probePsmuxSupport(options = {}) {
|
|
|
143
143
|
};
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
|
-
|
|
147
|
-
export function probePrimaryMultiplexerSupport(options = {}) {
|
|
148
|
-
const platform = options.platform || process.platform;
|
|
149
|
-
const execFileSyncFn = options.execFileSyncFn || execFileSync;
|
|
150
|
-
|
|
151
|
-
if (platform === "win32") {
|
|
152
|
-
return {
|
|
153
|
-
kind: "psmux",
|
|
154
|
-
...probePsmuxSupport(options),
|
|
155
|
-
};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const bin = options.bin || "tmux";
|
|
159
|
-
try {
|
|
160
|
-
const versionOutput = execFileSyncFn(bin, ["-V"], {
|
|
161
|
-
encoding: "utf8",
|
|
162
|
-
timeout: 2000,
|
|
163
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
164
|
-
windowsHide: true,
|
|
165
|
-
});
|
|
166
|
-
return {
|
|
167
|
-
ok: true,
|
|
168
|
-
installed: true,
|
|
169
|
-
kind: "tmux",
|
|
170
|
-
version: String(versionOutput || "").trim() || null,
|
|
171
|
-
recommendedVersion: null,
|
|
172
|
-
recommended: true,
|
|
173
|
-
missingCommands: [],
|
|
174
|
-
missingOptionalCommands: [],
|
|
175
|
-
installHint: formatPsmuxInstallGuidance(" ", platform),
|
|
176
|
-
updateHint: formatPsmuxUpdateGuidance(" ", platform),
|
|
177
|
-
};
|
|
178
|
-
} catch (error) {
|
|
179
|
-
return {
|
|
180
|
-
ok: false,
|
|
181
|
-
installed: false,
|
|
182
|
-
kind: "tmux",
|
|
183
|
-
version: null,
|
|
184
|
-
recommendedVersion: null,
|
|
185
|
-
recommended: false,
|
|
186
|
-
missingCommands: ["tmux"],
|
|
187
|
-
missingOptionalCommands: [],
|
|
188
|
-
installHint: formatPsmuxInstallGuidance(" ", platform),
|
|
189
|
-
updateHint: formatPsmuxUpdateGuidance(" ", platform),
|
|
190
|
-
error: error?.message || String(error),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
}
|