@triflux/remote 10.48.1 → 10.50.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/hub/server.mjs +0 -122
- package/hub/team/claude-daemon-control.mjs +54 -6
- package/hub/team/claude-transcript.mjs +240 -0
- package/hub/team/cli/commands/start/index.mjs +17 -3
- package/hub/team/cli/commands/start/parse-args.mjs +1 -0
- package/hub/team/session-context.mjs +203 -0
- package/hub/team/uds-orchestrator.mjs +63 -1
- package/hub/team/worktree-lifecycle.mjs +0 -1
- package/package.json +2 -3
- package/scripts/lib/doctor-env-checks.mjs +0 -85
- package/scripts/lib/legacy-hook-cleanup.mjs +230 -0
- package/scripts/lib/legacy-mcp-cleanup.mjs +772 -0
- package/scripts/lib/mcp-guard-engine.mjs +44 -271
- package/scripts/lib/mcp-manifest.mjs +2 -2
- package/hub/mac-focus.mjs +0 -94
- package/hub/mac-tray.swift +0 -144
- package/hub/public/tray-icon.ico +0 -0
- package/hub/public/tray-icon.png +0 -0
- package/hub/public/tray.html +0 -756
- package/hub/tray-lifecycle.mjs +0 -138
- package/hub/tray-runtime.mjs +0 -265
- package/hub/tray-state.mjs +0 -524
- package/hub/tray.mjs +0 -527
- package/scripts/lib/cross-review-utils.mjs +0 -59
- package/scripts/lib/hook-utils.mjs +0 -16
- package/scripts/lib/keyword-rules.mjs +0 -219
- package/scripts/lib/mcp-gateway-health-check.mjs +0 -224
- package/scripts/lib/mcp-gateway-servers.mjs +0 -70
- package/scripts/lib/mcp-gateway-wrapper-check.mjs +0 -35
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
chmodSync,
|
|
3
|
-
copyFileSync,
|
|
4
3
|
existsSync,
|
|
5
4
|
mkdirSync,
|
|
6
5
|
readdirSync,
|
|
@@ -22,22 +21,7 @@ const PROJECT_ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
|
|
22
21
|
const DEFAULT_REGISTRY_PATH = join(PROJECT_ROOT, "config", "mcp-registry.json");
|
|
23
22
|
const LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1"]);
|
|
24
23
|
const DEFAULT_HUB_PATH = "/mcp";
|
|
25
|
-
const SERVER_POLICIES = new Set([
|
|
26
|
-
"hosted",
|
|
27
|
-
"gateway-sse",
|
|
28
|
-
"gateway-http",
|
|
29
|
-
"stdio",
|
|
30
|
-
]);
|
|
31
|
-
const GATEWAY_PORTS = Object.freeze({
|
|
32
|
-
context7: 8100,
|
|
33
|
-
"brave-search": 8101,
|
|
34
|
-
exa: 8102,
|
|
35
|
-
tavily: 8103,
|
|
36
|
-
jira: 8104,
|
|
37
|
-
serena: 8105,
|
|
38
|
-
notion: 8106,
|
|
39
|
-
"notion-guest": 8107,
|
|
40
|
-
});
|
|
24
|
+
const SERVER_POLICIES = new Set(["hosted", "stdio"]);
|
|
41
25
|
const DEFAULT_REGISTRY = Object.freeze({
|
|
42
26
|
$schema: "mcp-registry-schema",
|
|
43
27
|
version: 1,
|
|
@@ -65,7 +49,6 @@ const DEFAULT_REGISTRY = Object.freeze({
|
|
|
65
49
|
},
|
|
66
50
|
},
|
|
67
51
|
policies: {
|
|
68
|
-
stdio_action: "replace-with-hub",
|
|
69
52
|
unknown_server_action: "warn",
|
|
70
53
|
sync_denylist: [],
|
|
71
54
|
watched_paths: [
|
|
@@ -153,12 +136,6 @@ function registryPath() {
|
|
|
153
136
|
: DEFAULT_REGISTRY_PATH;
|
|
154
137
|
}
|
|
155
138
|
|
|
156
|
-
function ensureBackup(filePath) {
|
|
157
|
-
const backupPath = `${filePath}.bak`;
|
|
158
|
-
copyFileSync(filePath, backupPath);
|
|
159
|
-
return backupPath;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
139
|
function isJsonMcpConfig(filePath) {
|
|
163
140
|
const name = pathBasename(filePath);
|
|
164
141
|
return (
|
|
@@ -484,12 +461,9 @@ function resolveEnvDescriptors(name, serverConfig) {
|
|
|
484
461
|
const envName = descriptor.env;
|
|
485
462
|
const envValue = process.env[envName];
|
|
486
463
|
if (typeof envValue !== "string" || envValue.length === 0) {
|
|
487
|
-
warnings.push(
|
|
488
|
-
`[mcp-guard] ${name}.${envKey} env skipped: env ${envName} is not set`,
|
|
489
|
-
);
|
|
490
|
-
continue;
|
|
464
|
+
warnings.push(`[mcp-guard] ${name}.${envKey} env ${envName} is not set`);
|
|
491
465
|
}
|
|
492
|
-
env[envKey] =
|
|
466
|
+
env[envKey] = "${" + envName + "}";
|
|
493
467
|
}
|
|
494
468
|
|
|
495
469
|
return { descriptors, env, warnings };
|
|
@@ -567,22 +541,6 @@ function hasOwnEntries(value) {
|
|
|
567
541
|
return value && typeof value === "object" && Object.keys(value).length > 0;
|
|
568
542
|
}
|
|
569
543
|
|
|
570
|
-
function redactHeaders(headers = {}) {
|
|
571
|
-
const redacted = {};
|
|
572
|
-
for (const key of Object.keys(headers || {})) {
|
|
573
|
-
redacted[key] = "***REDACTED***";
|
|
574
|
-
}
|
|
575
|
-
return redacted;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function redactServerConfig(config = {}) {
|
|
579
|
-
if (!config || typeof config !== "object") return config;
|
|
580
|
-
return {
|
|
581
|
-
...config,
|
|
582
|
-
...(config.headers ? { headers: redactHeaders(config.headers) } : {}),
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
|
|
586
544
|
function descriptorsFromCodexConfig(config = {}) {
|
|
587
545
|
const descriptors = {};
|
|
588
546
|
if (typeof config.bearer_token_env_var === "string") {
|
|
@@ -691,6 +649,7 @@ function upsertTomlServer(raw, name, config, codex = {}) {
|
|
|
691
649
|
"http_headers",
|
|
692
650
|
"env_http_headers",
|
|
693
651
|
"env",
|
|
652
|
+
"env_vars",
|
|
694
653
|
]);
|
|
695
654
|
const nextManagedLines = [];
|
|
696
655
|
if (typeof config.url === "string") {
|
|
@@ -707,6 +666,9 @@ function upsertTomlServer(raw, name, config, codex = {}) {
|
|
|
707
666
|
}
|
|
708
667
|
const env = formatTomlInlineTable(config.env || {});
|
|
709
668
|
if (env) nextManagedLines.push(`env = ${env}`);
|
|
669
|
+
if (Array.isArray(codex.env_vars) && codex.env_vars.length > 0) {
|
|
670
|
+
nextManagedLines.push(`env_vars = ${formatTomlArray(codex.env_vars)}`);
|
|
671
|
+
}
|
|
710
672
|
if (codex.bearer_token_env_var) {
|
|
711
673
|
nextManagedLines.push(
|
|
712
674
|
`bearer_token_env_var = ${formatTomlString(codex.bearer_token_env_var)}`,
|
|
@@ -795,34 +757,6 @@ function serverPolicy(serverConfig = {}, registry = {}) {
|
|
|
795
757
|
return "hosted";
|
|
796
758
|
}
|
|
797
759
|
|
|
798
|
-
function gatewaySseUrl(name, serverConfig = {}) {
|
|
799
|
-
if (typeof serverConfig.url === "string" && serverConfig.url.trim()) {
|
|
800
|
-
return normalizeUrl(serverConfig.url);
|
|
801
|
-
}
|
|
802
|
-
const port =
|
|
803
|
-
Number(serverConfig.gateway_port || serverConfig.port || 0) ||
|
|
804
|
-
GATEWAY_PORTS[name];
|
|
805
|
-
if (!Number.isFinite(port) || port <= 0) return "";
|
|
806
|
-
return `http://127.0.0.1:${port}/sse`;
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
function gatewayHttpUrl(name, serverConfig = {}) {
|
|
810
|
-
if (typeof serverConfig.url === "string" && serverConfig.url.trim()) {
|
|
811
|
-
return normalizeUrl(serverConfig.url);
|
|
812
|
-
}
|
|
813
|
-
const port =
|
|
814
|
-
Number(serverConfig.gateway_port || serverConfig.port || 0) ||
|
|
815
|
-
GATEWAY_PORTS[name];
|
|
816
|
-
if (!Number.isFinite(port) || port <= 0) return "";
|
|
817
|
-
const path =
|
|
818
|
-
typeof serverConfig.gateway_path === "string" &&
|
|
819
|
-
serverConfig.gateway_path.trim()
|
|
820
|
-
? serverConfig.gateway_path.trim()
|
|
821
|
-
: "/mcp";
|
|
822
|
-
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
823
|
-
return `http://127.0.0.1:${port}${normalizedPath}`;
|
|
824
|
-
}
|
|
825
|
-
|
|
826
760
|
function syncDenylistEntries(policy = {}) {
|
|
827
761
|
if (!Array.isArray(policy?.sync_denylist)) return new Set();
|
|
828
762
|
return new Set(
|
|
@@ -848,9 +782,11 @@ export function buildDesiredServerRecord(name, serverConfig, filePath) {
|
|
|
848
782
|
const policy = serverPolicy(serverConfig);
|
|
849
783
|
if (policy === "stdio") {
|
|
850
784
|
const resolvedEnv = resolveEnvDescriptors(name, serverConfig);
|
|
851
|
-
const
|
|
852
|
-
|
|
853
|
-
|
|
785
|
+
const codexTarget = isCodexConfig(filePath);
|
|
786
|
+
const envConfig =
|
|
787
|
+
!codexTarget && hasOwnEntries(resolvedEnv.env)
|
|
788
|
+
? { env: resolvedEnv.env }
|
|
789
|
+
: {};
|
|
854
790
|
return {
|
|
855
791
|
name,
|
|
856
792
|
config: {
|
|
@@ -859,19 +795,21 @@ export function buildDesiredServerRecord(name, serverConfig, filePath) {
|
|
|
859
795
|
...envConfig,
|
|
860
796
|
},
|
|
861
797
|
envDescriptors: resolvedEnv.descriptors,
|
|
862
|
-
codex:
|
|
798
|
+
codex: codexTarget
|
|
799
|
+
? {
|
|
800
|
+
env_vars: Object.values(resolvedEnv.descriptors).map(
|
|
801
|
+
(item) => item.env,
|
|
802
|
+
),
|
|
803
|
+
}
|
|
804
|
+
: {},
|
|
863
805
|
warnings: resolvedEnv.warnings,
|
|
864
806
|
};
|
|
865
807
|
}
|
|
866
808
|
|
|
867
809
|
const url =
|
|
868
|
-
|
|
869
|
-
?
|
|
870
|
-
:
|
|
871
|
-
? gatewayHttpUrl(name, serverConfig)
|
|
872
|
-
: serverConfig?.transport === "hub-url"
|
|
873
|
-
? resolveHubUrl()
|
|
874
|
-
: normalizeUrl(serverConfig?.url || "");
|
|
810
|
+
serverConfig?.transport === "hub-url"
|
|
811
|
+
? resolveHubUrl()
|
|
812
|
+
: normalizeUrl(serverConfig?.url || "");
|
|
875
813
|
const basenameValue = pathBasename(filePath);
|
|
876
814
|
const resolvedHeaders = resolveHeaderDescriptors(
|
|
877
815
|
name,
|
|
@@ -882,46 +820,6 @@ export function buildDesiredServerRecord(name, serverConfig, filePath) {
|
|
|
882
820
|
? { headers: resolvedHeaders.headers }
|
|
883
821
|
: {};
|
|
884
822
|
|
|
885
|
-
if (policy === "gateway-http") {
|
|
886
|
-
if (isCodexConfig(filePath)) {
|
|
887
|
-
return {
|
|
888
|
-
name,
|
|
889
|
-
config: { url },
|
|
890
|
-
headerDescriptors: {},
|
|
891
|
-
codex: {},
|
|
892
|
-
warnings: [],
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
return {
|
|
897
|
-
name,
|
|
898
|
-
config: { type: "http", url },
|
|
899
|
-
headerDescriptors: {},
|
|
900
|
-
codex: {},
|
|
901
|
-
warnings: [],
|
|
902
|
-
};
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
if (policy === "gateway-sse") {
|
|
906
|
-
if (isCodexConfig(filePath)) {
|
|
907
|
-
return {
|
|
908
|
-
name,
|
|
909
|
-
config: { url, transport: "sse" },
|
|
910
|
-
headerDescriptors: {},
|
|
911
|
-
codex: {},
|
|
912
|
-
warnings: [],
|
|
913
|
-
};
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
return {
|
|
917
|
-
name,
|
|
918
|
-
config: { type: "sse", url },
|
|
919
|
-
headerDescriptors: {},
|
|
920
|
-
codex: {},
|
|
921
|
-
warnings: [],
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
|
|
925
823
|
if (
|
|
926
824
|
basenameValue === ".mcp.json" ||
|
|
927
825
|
isClaudeProjectMcpConfig(filePath) ||
|
|
@@ -1122,7 +1020,6 @@ function scanCodexConfig(filePath) {
|
|
|
1122
1020
|
exists: true,
|
|
1123
1021
|
parseError: null,
|
|
1124
1022
|
servers,
|
|
1125
|
-
// Wave 1/2-B: Codex stdio servers are observed but not auto-remediated.
|
|
1126
1023
|
stdioServers: [],
|
|
1127
1024
|
};
|
|
1128
1025
|
} catch (error) {
|
|
@@ -1305,9 +1202,7 @@ export function validateRegistry(registry) {
|
|
|
1305
1202
|
typeof server.policy !== "string" ||
|
|
1306
1203
|
!SERVER_POLICIES.has(server.policy)
|
|
1307
1204
|
) {
|
|
1308
|
-
errors.push(
|
|
1309
|
-
`registry.servers.${name}.policy must be hosted, gateway-sse, gateway-http, or stdio`,
|
|
1310
|
-
);
|
|
1205
|
+
errors.push(`registry.servers.${name}.policy must be hosted or stdio`);
|
|
1311
1206
|
}
|
|
1312
1207
|
const transport = server.transport || registry.defaults?.transport;
|
|
1313
1208
|
if (!["hub-url", "http", "stdio"].includes(transport)) {
|
|
@@ -1321,19 +1216,6 @@ export function validateRegistry(registry) {
|
|
|
1321
1216
|
) {
|
|
1322
1217
|
errors.push(`registry.servers.${name}.url must be a non-empty string`);
|
|
1323
1218
|
}
|
|
1324
|
-
if (policy === "gateway-sse" || policy === "gateway-http") {
|
|
1325
|
-
const port = Number(server.gateway_port || server.port || 0);
|
|
1326
|
-
const hasGatewayUrl =
|
|
1327
|
-
typeof server.url === "string" && server.url.trim().length > 0;
|
|
1328
|
-
if (
|
|
1329
|
-
!hasGatewayUrl &&
|
|
1330
|
-
(!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
1331
|
-
) {
|
|
1332
|
-
errors.push(
|
|
1333
|
-
`registry.servers.${name}.gateway_port must be a valid TCP port for ${policy}`,
|
|
1334
|
-
);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
1219
|
if (policy === "stdio" && server.url !== undefined) {
|
|
1338
1220
|
errors.push(`registry.servers.${name}.url is not used for stdio`);
|
|
1339
1221
|
}
|
|
@@ -1520,14 +1402,6 @@ export function validateRegistry(registry) {
|
|
|
1520
1402
|
}
|
|
1521
1403
|
}
|
|
1522
1404
|
}
|
|
1523
|
-
if (
|
|
1524
|
-
registry.policies.stdio_action &&
|
|
1525
|
-
!["replace-with-hub", "warn"].includes(registry.policies.stdio_action)
|
|
1526
|
-
) {
|
|
1527
|
-
errors.push(
|
|
1528
|
-
"registry.policies.stdio_action must be replace-with-hub or warn",
|
|
1529
|
-
);
|
|
1530
|
-
}
|
|
1531
1405
|
}
|
|
1532
1406
|
|
|
1533
1407
|
return errors;
|
|
@@ -1795,101 +1669,6 @@ export function scanForStdioServers(filePath) {
|
|
|
1795
1669
|
return scanConfig(filePath).stdioServers;
|
|
1796
1670
|
}
|
|
1797
1671
|
|
|
1798
|
-
export function remediate(filePath, stdioServers, policy = {}) {
|
|
1799
|
-
const resolvedPath = resolveFilePath(filePath);
|
|
1800
|
-
const offenders = Array.isArray(stdioServers)
|
|
1801
|
-
? stdioServers.filter((server) => server?.name)
|
|
1802
|
-
: [];
|
|
1803
|
-
const action = policy?.stdio_action || "warn";
|
|
1804
|
-
|
|
1805
|
-
if (offenders.length === 0) {
|
|
1806
|
-
return {
|
|
1807
|
-
action: "noop",
|
|
1808
|
-
modified: false,
|
|
1809
|
-
backupPath: null,
|
|
1810
|
-
removedServers: [],
|
|
1811
|
-
warnings: [],
|
|
1812
|
-
};
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
if (action === "warn") {
|
|
1816
|
-
return {
|
|
1817
|
-
action,
|
|
1818
|
-
modified: false,
|
|
1819
|
-
backupPath: null,
|
|
1820
|
-
removedServers: [],
|
|
1821
|
-
warnings: [
|
|
1822
|
-
`[mcp-guard] stdio MCP 감지: ${offenders.map((server) => server.name).join(", ")}`,
|
|
1823
|
-
],
|
|
1824
|
-
};
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
if (isCodexConfig(resolvedPath)) {
|
|
1828
|
-
return {
|
|
1829
|
-
action,
|
|
1830
|
-
modified: false,
|
|
1831
|
-
backupPath: null,
|
|
1832
|
-
removedServers: [],
|
|
1833
|
-
warnings: ["[mcp-guard] Codex TOML 자동 수정은 Wave 2-B 범위 밖입니다."],
|
|
1834
|
-
};
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
const snapshot = scanConfig(resolvedPath);
|
|
1838
|
-
if (snapshot.parseError) {
|
|
1839
|
-
return {
|
|
1840
|
-
action,
|
|
1841
|
-
modified: false,
|
|
1842
|
-
backupPath: null,
|
|
1843
|
-
removedServers: [],
|
|
1844
|
-
warnings: [`[mcp-guard] 설정 파싱 실패: ${snapshot.parseError.message}`],
|
|
1845
|
-
};
|
|
1846
|
-
}
|
|
1847
|
-
|
|
1848
|
-
let backupPath = null;
|
|
1849
|
-
try {
|
|
1850
|
-
if (existsSync(resolvedPath)) backupPath = ensureBackup(resolvedPath);
|
|
1851
|
-
} catch (error) {
|
|
1852
|
-
return {
|
|
1853
|
-
action,
|
|
1854
|
-
modified: false,
|
|
1855
|
-
backupPath: null,
|
|
1856
|
-
removedServers: [],
|
|
1857
|
-
warnings: [`[mcp-guard] 백업 생성 실패: ${error.message}`],
|
|
1858
|
-
};
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
const removals = offenders.map((server) => server.name);
|
|
1862
|
-
const updates = [];
|
|
1863
|
-
let replacement = null;
|
|
1864
|
-
|
|
1865
|
-
if (action === "replace-with-hub") {
|
|
1866
|
-
const registry = policy.registry || loadRegistryOrDefault();
|
|
1867
|
-
const matchingOffender = offenders.find((server) =>
|
|
1868
|
-
Object.hasOwn(registry.servers || {}, server.name),
|
|
1869
|
-
);
|
|
1870
|
-
const [hubServerName, hubServerConfig] = matchingOffender
|
|
1871
|
-
? [matchingOffender.name, registry.servers[matchingOffender.name]]
|
|
1872
|
-
: getHubServerEntry(registry);
|
|
1873
|
-
const desired = buildDesiredServerRecord(
|
|
1874
|
-
hubServerName,
|
|
1875
|
-
hubServerConfig,
|
|
1876
|
-
resolvedPath,
|
|
1877
|
-
);
|
|
1878
|
-
replacement = { name: desired.name, ...redactServerConfig(desired.config) };
|
|
1879
|
-
updates.push(desired);
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
const result = updateJsonConfig(resolvedPath, updates, removals);
|
|
1883
|
-
return {
|
|
1884
|
-
action,
|
|
1885
|
-
modified: result.modified,
|
|
1886
|
-
backupPath,
|
|
1887
|
-
removedServers: removals,
|
|
1888
|
-
replacement,
|
|
1889
|
-
warnings: updates.flatMap((update) => update.warnings || []),
|
|
1890
|
-
};
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
1672
|
export function resolveHubUrl() {
|
|
1894
1673
|
const registryState = inspectRegistry();
|
|
1895
1674
|
const registry = registryState.valid
|
|
@@ -1981,13 +1760,7 @@ export function addRegistryServer(name, url, options = {}) {
|
|
|
1981
1760
|
: cloneDefaultRegistry();
|
|
1982
1761
|
const transport =
|
|
1983
1762
|
options.transport || (trimmedName === "tfx-hub" ? "hub-url" : "http");
|
|
1984
|
-
const policy =
|
|
1985
|
-
options.policy ||
|
|
1986
|
-
(transport === "stdio"
|
|
1987
|
-
? "stdio"
|
|
1988
|
-
: options.gateway_port || options.port
|
|
1989
|
-
? "gateway-http"
|
|
1990
|
-
: "hosted");
|
|
1763
|
+
const policy = options.policy || (transport === "stdio" ? "stdio" : "hosted");
|
|
1991
1764
|
|
|
1992
1765
|
registry.servers[trimmedName] = {
|
|
1993
1766
|
policy,
|
|
@@ -2127,26 +1900,6 @@ export function syncRegistryTargets(options = {}) {
|
|
|
2127
1900
|
status: "invalid-config",
|
|
2128
1901
|
message: snapshot.parseError.message,
|
|
2129
1902
|
});
|
|
2130
|
-
continue;
|
|
2131
|
-
}
|
|
2132
|
-
|
|
2133
|
-
if (
|
|
2134
|
-
snapshot.stdioServers.length > 0 &&
|
|
2135
|
-
!isClaudeUserConfig(target.filePath)
|
|
2136
|
-
) {
|
|
2137
|
-
const remediation = remediate(target.filePath, snapshot.stdioServers, {
|
|
2138
|
-
...registry.policies,
|
|
2139
|
-
registry,
|
|
2140
|
-
});
|
|
2141
|
-
actions.push({
|
|
2142
|
-
type: "remediate",
|
|
2143
|
-
filePath: target.filePath,
|
|
2144
|
-
label: target.label,
|
|
2145
|
-
status: remediation.modified ? "updated" : "warning",
|
|
2146
|
-
removedServers: remediation.removedServers,
|
|
2147
|
-
replacement: remediation.replacement || null,
|
|
2148
|
-
warnings: remediation.warnings || [],
|
|
2149
|
-
});
|
|
2150
1903
|
}
|
|
2151
1904
|
}
|
|
2152
1905
|
|
|
@@ -2175,6 +1928,26 @@ export function syncRegistryTargets(options = {}) {
|
|
|
2175
1928
|
if (serverConfig?.safe !== true) continue;
|
|
2176
1929
|
if (!serverAppliesToClient(serverConfig, target.client)) continue;
|
|
2177
1930
|
|
|
1931
|
+
if (
|
|
1932
|
+
isCodexConfig(target.filePath) &&
|
|
1933
|
+
serverPolicy(serverConfig) === "stdio"
|
|
1934
|
+
) {
|
|
1935
|
+
const remappedEnv = Object.entries(
|
|
1936
|
+
normalizeEnvDescriptors(serverConfig.env),
|
|
1937
|
+
).find(([key, descriptor]) => key !== descriptor.env);
|
|
1938
|
+
if (remappedEnv) {
|
|
1939
|
+
actions.push({
|
|
1940
|
+
type: "sync",
|
|
1941
|
+
filePath: target.filePath,
|
|
1942
|
+
label: target.label,
|
|
1943
|
+
status: "warning",
|
|
1944
|
+
server: name,
|
|
1945
|
+
message: `${name}: Codex env_vars는 ${remappedEnv[1].env}를 ${remappedEnv[0]}로 이름 변경할 수 없습니다`,
|
|
1946
|
+
});
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
|
|
2178
1951
|
const denyKey = syncDenylistKey(target.client, name);
|
|
2179
1952
|
if (denylist.has(denyKey)) {
|
|
2180
1953
|
const existing = snapshot.servers.find(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// scripts/lib/mcp-manifest.mjs
|
|
2
2
|
// MCP 서버 활성화 매니페스트 — 단일 진실 소스.
|
|
3
|
-
// tfx-setup 위저드가 저장하고,
|
|
3
|
+
// tfx-setup 위저드가 저장하고, MCP 필터가 참조한다.
|
|
4
4
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
@@ -13,7 +13,7 @@ export const MANIFEST_PATH = join(
|
|
|
13
13
|
"mcp-enabled.json",
|
|
14
14
|
);
|
|
15
15
|
|
|
16
|
-
/** API 키 불필요
|
|
16
|
+
/** API 키 불필요 서버 (현재 없음) */
|
|
17
17
|
export const CORE_SERVERS = Object.freeze([]);
|
|
18
18
|
|
|
19
19
|
/** 검색 MCP — API 키 필요 */
|
package/hub/mac-focus.mjs
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
|
|
3
|
-
export const FOCUS_SCRIPT_TIMEOUT_MS = 5_000;
|
|
4
|
-
|
|
5
|
-
const DEFAULT_FOCUS_CANDIDATES = [
|
|
6
|
-
"iTerm2",
|
|
7
|
-
"Terminal",
|
|
8
|
-
"Codex",
|
|
9
|
-
"Claude",
|
|
10
|
-
"Antigravity",
|
|
11
|
-
"Code",
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
function normalizeAgentId(value) {
|
|
15
|
-
const id = String(value || "")
|
|
16
|
-
.trim()
|
|
17
|
-
.toLowerCase();
|
|
18
|
-
if (id === "agy" || id === "anti") return "antigravity";
|
|
19
|
-
return id;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function getFocusCandidates({ agentId } = {}) {
|
|
23
|
-
switch (normalizeAgentId(agentId)) {
|
|
24
|
-
case "codex":
|
|
25
|
-
return ["Codex", "iTerm2", "Terminal", "Code"];
|
|
26
|
-
case "claude":
|
|
27
|
-
return ["Claude", "iTerm2", "Terminal"];
|
|
28
|
-
case "antigravity":
|
|
29
|
-
return ["Antigravity", "iTerm2", "Terminal"];
|
|
30
|
-
default:
|
|
31
|
-
return [...DEFAULT_FOCUS_CANDIDATES];
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function appleScriptList(values) {
|
|
36
|
-
return `{${values.map((value) => JSON.stringify(String(value))).join(", ")}}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function buildFocusScript({ agentId, appCandidates } = {}) {
|
|
40
|
-
const candidates = Array.isArray(appCandidates)
|
|
41
|
-
? appCandidates.map(String).filter(Boolean)
|
|
42
|
-
: getFocusCandidates({ agentId });
|
|
43
|
-
return `
|
|
44
|
-
set appCandidates to ${appleScriptList(candidates)}
|
|
45
|
-
tell application "System Events"
|
|
46
|
-
set runningNames to name of every process
|
|
47
|
-
end tell
|
|
48
|
-
repeat with appName in appCandidates
|
|
49
|
-
set appText to appName as text
|
|
50
|
-
if runningNames contains appText then
|
|
51
|
-
tell application appText to activate
|
|
52
|
-
return "activated " & appText
|
|
53
|
-
end if
|
|
54
|
-
end repeat
|
|
55
|
-
return "no focus candidate running"
|
|
56
|
-
`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function focusSessionOnMac(target = {}, maybeUdsId = null) {
|
|
60
|
-
const normalizedTarget =
|
|
61
|
-
target && typeof target === "object"
|
|
62
|
-
? target
|
|
63
|
-
: { sessionId: target, udsId: maybeUdsId };
|
|
64
|
-
const script = buildFocusScript({ agentId: normalizedTarget.agentId });
|
|
65
|
-
return new Promise((resolve) => {
|
|
66
|
-
execFile(
|
|
67
|
-
"osascript",
|
|
68
|
-
["-e", script],
|
|
69
|
-
{ timeout: FOCUS_SCRIPT_TIMEOUT_MS },
|
|
70
|
-
(err, stdout = "", stderr = "") => {
|
|
71
|
-
const output = String(stdout || stderr || "").trim();
|
|
72
|
-
if (err) {
|
|
73
|
-
console.error("mac-focus error:", err?.message || err);
|
|
74
|
-
resolve({
|
|
75
|
-
ok: false,
|
|
76
|
-
sessionId: normalizedTarget.sessionId || null,
|
|
77
|
-
pid: Number(normalizedTarget.pid) || null,
|
|
78
|
-
agentId: normalizedTarget.agentId || null,
|
|
79
|
-
error: err?.message || String(err),
|
|
80
|
-
output,
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
resolve({
|
|
85
|
-
ok: true,
|
|
86
|
-
sessionId: normalizedTarget.sessionId || null,
|
|
87
|
-
pid: Number(normalizedTarget.pid) || null,
|
|
88
|
-
agentId: normalizedTarget.agentId || null,
|
|
89
|
-
output,
|
|
90
|
-
});
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
}
|
package/hub/mac-tray.swift
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import Cocoa
|
|
2
|
-
import WebKit
|
|
3
|
-
|
|
4
|
-
class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
|
|
5
|
-
var statusItem: NSStatusItem!
|
|
6
|
-
var popover: NSPopover!
|
|
7
|
-
var webVC: WebViewController!
|
|
8
|
-
var port: String = "27888"
|
|
9
|
-
|
|
10
|
-
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
11
|
-
if CommandLine.arguments.count > 1 {
|
|
12
|
-
port = CommandLine.arguments[1]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
16
|
-
if let button = statusItem.button {
|
|
17
|
-
button.title = "T"
|
|
18
|
-
button.target = self
|
|
19
|
-
button.action = #selector(togglePopover(_:))
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
webVC = WebViewController()
|
|
23
|
-
webVC.port = port
|
|
24
|
-
webVC.onFocusComplete = { [weak self] in
|
|
25
|
-
self?.popover.performClose(nil)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
popover = NSPopover()
|
|
29
|
-
popover.contentViewController = webVC
|
|
30
|
-
popover.behavior = .transient
|
|
31
|
-
popover.animates = false
|
|
32
|
-
popover.delegate = self
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@objc func togglePopover(_ sender: Any?) {
|
|
36
|
-
if popover.isShown {
|
|
37
|
-
popover.performClose(sender)
|
|
38
|
-
} else {
|
|
39
|
-
if let button = statusItem.button {
|
|
40
|
-
webVC.reloadForPopover()
|
|
41
|
-
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
class WebViewController: NSViewController, WKNavigationDelegate, WKScriptMessageHandler {
|
|
48
|
-
var webView: WKWebView!
|
|
49
|
-
var port: String = "27888"
|
|
50
|
-
let canonicalPort = "27888"
|
|
51
|
-
var consecutiveFailures = 0
|
|
52
|
-
var retryWorkItem: DispatchWorkItem?
|
|
53
|
-
var onFocusComplete: (() -> Void)?
|
|
54
|
-
|
|
55
|
-
func logError(_ msg: String) {
|
|
56
|
-
let path = "/tmp/mac-tray.log"
|
|
57
|
-
if let outputStream = OutputStream(toFileAtPath: path, append: true) {
|
|
58
|
-
outputStream.open()
|
|
59
|
-
let text = "\(Date()): \(msg)\n"
|
|
60
|
-
let bytesWritten = outputStream.write(text, maxLength: text.utf8.count)
|
|
61
|
-
outputStream.close()
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
override func loadView() {
|
|
66
|
-
let webConfiguration = WKWebViewConfiguration()
|
|
67
|
-
webConfiguration.userContentController.add(self, name: "tray")
|
|
68
|
-
let initialFrame = NSRect(x: 0, y: 0, width: 460, height: 720)
|
|
69
|
-
webView = WKWebView(frame: initialFrame, configuration: webConfiguration)
|
|
70
|
-
|
|
71
|
-
// Transparent background for Glassmorphism
|
|
72
|
-
webView.setValue(false, forKey: "drawsBackground")
|
|
73
|
-
|
|
74
|
-
self.view = NSView(frame: initialFrame)
|
|
75
|
-
webView.frame = self.view.bounds
|
|
76
|
-
webView.autoresizingMask = [.width, .height]
|
|
77
|
-
self.view.addSubview(webView)
|
|
78
|
-
|
|
79
|
-
self.preferredContentSize = initialFrame.size
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
override func viewDidLoad() {
|
|
83
|
-
super.viewDidLoad()
|
|
84
|
-
webView.navigationDelegate = self
|
|
85
|
-
loadTray()
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
func loadTray() {
|
|
89
|
-
retryWorkItem?.cancel()
|
|
90
|
-
if let url = URL(string: "http://127.0.0.1:\(port)/tray.html?t=\(Date().timeIntervalSince1970)") {
|
|
91
|
-
let request = URLRequest(url: url)
|
|
92
|
-
webView.load(request)
|
|
93
|
-
} else {
|
|
94
|
-
logError("Invalid URL")
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
func reloadForPopover() {
|
|
99
|
-
if isViewLoaded {
|
|
100
|
-
loadTray()
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
|
105
|
-
logError("Failed to load: \(error.localizedDescription)")
|
|
106
|
-
consecutiveFailures += 1
|
|
107
|
-
if consecutiveFailures >= 3 && port != canonicalPort {
|
|
108
|
-
port = canonicalPort
|
|
109
|
-
consecutiveFailures = 0
|
|
110
|
-
logError("Switching to canonical port \(canonicalPort) after repeated failures")
|
|
111
|
-
loadTray()
|
|
112
|
-
return
|
|
113
|
-
}
|
|
114
|
-
let retry = DispatchWorkItem { [weak self] in
|
|
115
|
-
self?.loadTray()
|
|
116
|
-
}
|
|
117
|
-
retryWorkItem = retry
|
|
118
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35, execute: retry)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
122
|
-
retryWorkItem?.cancel()
|
|
123
|
-
retryWorkItem = nil
|
|
124
|
-
consecutiveFailures = 0
|
|
125
|
-
logError("Successfully loaded URL")
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
129
|
-
guard message.name == "tray" else { return }
|
|
130
|
-
if let body = message.body as? [String: Any],
|
|
131
|
-
let type = body["type"] as? String,
|
|
132
|
-
type == "focus-complete" {
|
|
133
|
-
DispatchQueue.main.async { [weak self] in
|
|
134
|
-
self?.onFocusComplete?()
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let app = NSApplication.shared
|
|
141
|
-
let delegate = AppDelegate()
|
|
142
|
-
app.delegate = delegate
|
|
143
|
-
app.setActivationPolicy(.accessory)
|
|
144
|
-
app.run()
|
package/hub/public/tray-icon.ico
DELETED
|
Binary file
|