metheus-governance-mcp-cli 0.2.0 → 0.2.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/README.md +16 -1
- package/cli.mjs +256 -32
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Metheus Governance MCP helper CLI.
|
|
4
4
|
|
|
5
|
+
- `metheus-governance-mcp` (no args): bootstrap mode
|
|
6
|
+
- checks auth token
|
|
7
|
+
- if token is missing/expired, starts `auth login`
|
|
8
|
+
- checks Codex/Claude MCP registration
|
|
9
|
+
- registers only missing clients
|
|
5
10
|
- `setup`: register `metheus-governance-mcp` into Codex/Claude (if installed)
|
|
6
11
|
- `proxy`: stdio MCP bridge to Metheus HTTPS gateway
|
|
7
12
|
- `auth`: save/check/clear local Metheus token used by proxy
|
|
@@ -12,6 +17,16 @@ Metheus Governance MCP helper CLI.
|
|
|
12
17
|
npm install -g metheus-governance-mcp-cli
|
|
13
18
|
```
|
|
14
19
|
|
|
20
|
+
## One command bootstrap (recommended)
|
|
21
|
+
|
|
22
|
+
Run in your project folder:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
metheus-governance-mcp --project-id <project_uuid> --ctxpack-key "<ctxpack_key>" --base-url https://metheus.gesiaplatform.com
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If auth is not ready, login starts automatically, then MCP registration is completed.
|
|
29
|
+
|
|
15
30
|
## Setup
|
|
16
31
|
|
|
17
32
|
```bash
|
|
@@ -63,7 +78,7 @@ metheus-governance-mcp auth status
|
|
|
63
78
|
3. If token is rotated, set directly:
|
|
64
79
|
|
|
65
80
|
```bash
|
|
66
|
-
metheus-governance-mcp auth set --token "<access_token>"
|
|
81
|
+
metheus-governance-mcp auth set --token "<access_token>" --refresh-token "<refresh_token>"
|
|
67
82
|
```
|
|
68
83
|
|
|
69
84
|
4. Remove local token:
|
package/cli.mjs
CHANGED
|
@@ -14,6 +14,9 @@ const DEFAULT_SITE_URL = "https://metheus.gesiaplatform.com";
|
|
|
14
14
|
const DEFAULT_BASE_URL = `${DEFAULT_SITE_URL}/governance/mcp`;
|
|
15
15
|
const DEFAULT_SERVER_NAME = "metheus-governance-mcp";
|
|
16
16
|
const AUTH_STORE_RELATIVE_PATH = path.join(".metheus", "governance-mcp-auth.json");
|
|
17
|
+
const CLI_META = loadCLIMeta();
|
|
18
|
+
const CLI_NAME = CLI_META.name || "metheus-governance-mcp-cli";
|
|
19
|
+
const CLI_VERSION = CLI_META.version || "0.0.0";
|
|
17
20
|
|
|
18
21
|
function printUsage() {
|
|
19
22
|
process.stderr.write(
|
|
@@ -21,12 +24,15 @@ function printUsage() {
|
|
|
21
24
|
"Metheus Governance MCP CLI",
|
|
22
25
|
"",
|
|
23
26
|
"Usage:",
|
|
27
|
+
" metheus-governance-mcp [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
|
|
28
|
+
" metheus-governance-mcp init [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--flow <auto|device|callback|manual>]",
|
|
24
29
|
" metheus-governance-mcp setup [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--name <server_name>]",
|
|
25
30
|
" metheus-governance-mcp proxy [--project-id <uuid>] [--ctxpack-key <key>] [--base-url <url>] [--include-drafts <true|false>] [--timeout-seconds <n>]",
|
|
26
31
|
" metheus-governance-mcp auth status",
|
|
27
32
|
" metheus-governance-mcp auth login [--base-url <url>] [--flow <auto|device|callback|manual>] [--keycloak-url <url>] [--realm <name>] [--client-id <id>] [--open-browser <true|false>] [--callback-port <n>] [--timeout-seconds <n>] [--manual <true|false>]",
|
|
28
|
-
" metheus-governance-mcp auth set --token <jwt> [--base-url <url>]",
|
|
33
|
+
" metheus-governance-mcp auth set --token <jwt> [--refresh-token <token>] [--base-url <url>]",
|
|
29
34
|
" metheus-governance-mcp auth clear",
|
|
35
|
+
" metheus-governance-mcp -v | --version",
|
|
30
36
|
"",
|
|
31
37
|
"Environment:",
|
|
32
38
|
" METHEUS_TOKEN or MCP_AUTH_TOKEN is used first for proxy requests.",
|
|
@@ -37,6 +43,24 @@ function printUsage() {
|
|
|
37
43
|
);
|
|
38
44
|
}
|
|
39
45
|
|
|
46
|
+
function loadCLIMeta() {
|
|
47
|
+
try {
|
|
48
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
49
|
+
const raw = fs.readFileSync(path.join(dir, "package.json"), "utf8");
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
} catch {
|
|
56
|
+
return {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printVersion() {
|
|
61
|
+
process.stdout.write(`${CLI_NAME} ${CLI_VERSION}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
function parseArgs(argv) {
|
|
41
65
|
const out = {};
|
|
42
66
|
for (let i = 0; i < argv.length; i += 1) {
|
|
@@ -82,29 +106,31 @@ function loadStoredAuth() {
|
|
|
82
106
|
const filePath = authStoreFilePath();
|
|
83
107
|
try {
|
|
84
108
|
if (!fs.existsSync(filePath)) {
|
|
85
|
-
return { filePath, token: "", baseURL: "", updatedAt: "" };
|
|
109
|
+
return { filePath, token: "", refreshToken: "", baseURL: "", updatedAt: "" };
|
|
86
110
|
}
|
|
87
111
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
88
112
|
const parsed = JSON.parse(raw);
|
|
89
113
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
90
|
-
return { filePath, token: "", baseURL: "", updatedAt: "" };
|
|
114
|
+
return { filePath, token: "", refreshToken: "", baseURL: "", updatedAt: "" };
|
|
91
115
|
}
|
|
92
116
|
return {
|
|
93
117
|
filePath,
|
|
94
118
|
token: normalizeToken(parsed.token),
|
|
119
|
+
refreshToken: normalizeToken(parsed.refresh_token),
|
|
95
120
|
baseURL: String(parsed.base_url || "").trim(),
|
|
96
121
|
updatedAt: String(parsed.updated_at || "").trim(),
|
|
97
122
|
};
|
|
98
123
|
} catch {
|
|
99
|
-
return { filePath, token: "", baseURL: "", updatedAt: "" };
|
|
124
|
+
return { filePath, token: "", refreshToken: "", baseURL: "", updatedAt: "" };
|
|
100
125
|
}
|
|
101
126
|
}
|
|
102
127
|
|
|
103
|
-
function saveStoredAuth({ token, baseURL }) {
|
|
128
|
+
function saveStoredAuth({ token, refreshToken, baseURL }) {
|
|
104
129
|
const filePath = authStoreFilePath();
|
|
105
130
|
ensureParentDir(filePath);
|
|
106
131
|
const payload = {
|
|
107
132
|
token: normalizeToken(token),
|
|
133
|
+
refresh_token: normalizeToken(refreshToken),
|
|
108
134
|
base_url: String(baseURL || "").trim(),
|
|
109
135
|
updated_at: new Date().toISOString(),
|
|
110
136
|
};
|
|
@@ -512,6 +538,45 @@ async function exchangeAuthorizationCodeForToken({
|
|
|
512
538
|
return parsed;
|
|
513
539
|
}
|
|
514
540
|
|
|
541
|
+
async function exchangeRefreshTokenForToken({
|
|
542
|
+
tokenEndpoint,
|
|
543
|
+
clientID,
|
|
544
|
+
refreshToken,
|
|
545
|
+
timeoutSeconds,
|
|
546
|
+
}) {
|
|
547
|
+
const params = new URLSearchParams({
|
|
548
|
+
grant_type: "refresh_token",
|
|
549
|
+
client_id: String(clientID || "").trim(),
|
|
550
|
+
refresh_token: String(refreshToken || "").trim(),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
const response = await fetch(tokenEndpoint, {
|
|
554
|
+
method: "POST",
|
|
555
|
+
headers: {
|
|
556
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
557
|
+
accept: "application/json",
|
|
558
|
+
},
|
|
559
|
+
body: params.toString(),
|
|
560
|
+
redirect: "follow",
|
|
561
|
+
signal: AbortSignal.timeout(Math.max(5, intFromRaw(timeoutSeconds, 20)) * 1000),
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const body = await response.text();
|
|
565
|
+
const parsed = tryJsonParse(body) || {};
|
|
566
|
+
if (!response.ok) {
|
|
567
|
+
const reason =
|
|
568
|
+
String(parsed.error_description || parsed.error || "").trim() ||
|
|
569
|
+
body.slice(0, 250) ||
|
|
570
|
+
`${response.status} ${response.statusText}`;
|
|
571
|
+
throw new Error(`refresh token exchange failed: ${reason}`);
|
|
572
|
+
}
|
|
573
|
+
const accessToken = normalizeToken(parsed.access_token);
|
|
574
|
+
if (!accessToken) {
|
|
575
|
+
throw new Error("refresh token exchange succeeded but access_token is missing");
|
|
576
|
+
}
|
|
577
|
+
return parsed;
|
|
578
|
+
}
|
|
579
|
+
|
|
515
580
|
async function requestDeviceAuthorization({
|
|
516
581
|
deviceAuthorizationEndpoint,
|
|
517
582
|
clientID,
|
|
@@ -658,6 +723,48 @@ function resolveCurrentAccessToken() {
|
|
|
658
723
|
};
|
|
659
724
|
}
|
|
660
725
|
|
|
726
|
+
async function tryRefreshStoredAccessToken({ baseURL, timeoutSeconds }) {
|
|
727
|
+
const stored = loadStoredAuth();
|
|
728
|
+
const refreshCandidates = [
|
|
729
|
+
{ source: "METHEUS_REFRESH_TOKEN", token: normalizeToken(process.env.METHEUS_REFRESH_TOKEN) },
|
|
730
|
+
{ source: "MCP_AUTH_REFRESH_TOKEN", token: normalizeToken(process.env.MCP_AUTH_REFRESH_TOKEN) },
|
|
731
|
+
{ source: `${stored.filePath}:refresh_token`, token: normalizeToken(stored.refreshToken) },
|
|
732
|
+
];
|
|
733
|
+
const refreshRow = refreshCandidates.find((row) => Boolean(row.token));
|
|
734
|
+
if (!refreshRow) {
|
|
735
|
+
return { ok: false, error: "no refresh token configured" };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const siteBaseURL = normalizeSiteBaseURL(baseURL || stored.baseURL || DEFAULT_SITE_URL);
|
|
739
|
+
try {
|
|
740
|
+
const oidc = await discoverOIDCConfig({ siteBaseURL });
|
|
741
|
+
const payload = await exchangeRefreshTokenForToken({
|
|
742
|
+
tokenEndpoint: oidc.tokenEndpoint,
|
|
743
|
+
clientID: oidc.clientID,
|
|
744
|
+
refreshToken: refreshRow.token,
|
|
745
|
+
timeoutSeconds: intFromRaw(timeoutSeconds, 20),
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
const accessToken = normalizeToken(payload.access_token);
|
|
749
|
+
const nextRefreshToken = normalizeToken(payload.refresh_token) || refreshRow.token;
|
|
750
|
+
const filePath = saveStoredAuth({
|
|
751
|
+
token: accessToken,
|
|
752
|
+
refreshToken: nextRefreshToken,
|
|
753
|
+
baseURL: siteBaseURL,
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
ok: true,
|
|
758
|
+
token: accessToken,
|
|
759
|
+
source: filePath,
|
|
760
|
+
refreshSource: refreshRow.source,
|
|
761
|
+
expires: tokenExpiryIso(accessToken),
|
|
762
|
+
};
|
|
763
|
+
} catch (err) {
|
|
764
|
+
return { ok: false, error: String(err?.message || err), refreshSource: refreshRow.source };
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
661
768
|
function currentAccessToken() {
|
|
662
769
|
return resolveCurrentAccessToken().token;
|
|
663
770
|
}
|
|
@@ -755,6 +862,10 @@ async function runAuthStatus() {
|
|
|
755
862
|
const token = resolved.token;
|
|
756
863
|
const source = resolved.source;
|
|
757
864
|
const stored = resolved.stored;
|
|
865
|
+
const refreshToken =
|
|
866
|
+
normalizeToken(process.env.METHEUS_REFRESH_TOKEN) ||
|
|
867
|
+
normalizeToken(process.env.MCP_AUTH_REFRESH_TOKEN) ||
|
|
868
|
+
normalizeToken(stored.refreshToken);
|
|
758
869
|
if (!token) {
|
|
759
870
|
process.stdout.write("Auth token: missing\n");
|
|
760
871
|
if (resolved.expired?.source) {
|
|
@@ -767,6 +878,7 @@ async function runAuthStatus() {
|
|
|
767
878
|
process.stdout.write(
|
|
768
879
|
"Run: metheus-governance-mcp auth login --base-url https://metheus.gesiaplatform.com\n",
|
|
769
880
|
);
|
|
881
|
+
process.stdout.write(`Refresh token: ${refreshToken ? "configured" : "missing"}\n`);
|
|
770
882
|
process.exitCode = 1;
|
|
771
883
|
return;
|
|
772
884
|
}
|
|
@@ -782,10 +894,18 @@ async function runAuthStatus() {
|
|
|
782
894
|
process.stdout.write(`Stored at: ${stored.updatedAt}\n`);
|
|
783
895
|
}
|
|
784
896
|
}
|
|
897
|
+
process.stdout.write(`Refresh token: ${refreshToken ? "configured" : "missing"}\n`);
|
|
785
898
|
}
|
|
786
899
|
|
|
787
900
|
async function runAuthSet(flags) {
|
|
901
|
+
const stored = loadStoredAuth();
|
|
788
902
|
const rawToken = normalizeToken(flags.token || flags["access-token"] || "");
|
|
903
|
+
const rawRefreshToken = normalizeToken(
|
|
904
|
+
flags["refresh-token"] ||
|
|
905
|
+
process.env.METHEUS_REFRESH_TOKEN ||
|
|
906
|
+
process.env.MCP_AUTH_REFRESH_TOKEN ||
|
|
907
|
+
"",
|
|
908
|
+
);
|
|
789
909
|
let token = rawToken;
|
|
790
910
|
if (!token && process.stdin.isTTY) {
|
|
791
911
|
token = normalizeToken(await promptLine("Paste Metheus access token (JWT): "));
|
|
@@ -796,15 +916,20 @@ async function runAuthSet(flags) {
|
|
|
796
916
|
return;
|
|
797
917
|
}
|
|
798
918
|
const baseURL = normalizeSiteBaseURL(flags["base-url"] || DEFAULT_SITE_URL);
|
|
799
|
-
const
|
|
919
|
+
const refreshToken = rawRefreshToken || normalizeToken(stored.refreshToken);
|
|
920
|
+
const filePath = saveStoredAuth({ token, refreshToken, baseURL });
|
|
800
921
|
const exp = tokenExpiryIso(token);
|
|
801
922
|
process.stdout.write(`Token saved: ${filePath}\n`);
|
|
802
923
|
if (exp) {
|
|
803
924
|
process.stdout.write(`Token expires: ${exp}\n`);
|
|
804
925
|
}
|
|
926
|
+
if (refreshToken) {
|
|
927
|
+
process.stdout.write("Refresh token saved.\n");
|
|
928
|
+
}
|
|
805
929
|
}
|
|
806
930
|
|
|
807
931
|
async function runAuthLoginManual(flags) {
|
|
932
|
+
const stored = loadStoredAuth();
|
|
808
933
|
const baseURL = normalizeSiteBaseURL(flags["base-url"] || DEFAULT_SITE_URL);
|
|
809
934
|
const loginURL = authLoginURL(baseURL);
|
|
810
935
|
const shouldOpen = boolFromRaw(flags["open-browser"], true);
|
|
@@ -822,7 +947,11 @@ async function runAuthLoginManual(flags) {
|
|
|
822
947
|
process.exitCode = 1;
|
|
823
948
|
return;
|
|
824
949
|
}
|
|
825
|
-
const filePath = saveStoredAuth({
|
|
950
|
+
const filePath = saveStoredAuth({
|
|
951
|
+
token,
|
|
952
|
+
refreshToken: normalizeToken(stored.refreshToken),
|
|
953
|
+
baseURL,
|
|
954
|
+
});
|
|
826
955
|
const exp = tokenExpiryIso(token);
|
|
827
956
|
process.stdout.write(`Token saved: ${filePath}\n`);
|
|
828
957
|
if (exp) {
|
|
@@ -899,12 +1028,16 @@ async function runAuthLoginCallback(flags, baseURL, oidc) {
|
|
|
899
1028
|
}
|
|
900
1029
|
|
|
901
1030
|
const token = normalizeToken(tokenPayload.access_token);
|
|
902
|
-
const
|
|
1031
|
+
const refreshToken = normalizeToken(tokenPayload.refresh_token);
|
|
1032
|
+
const filePath = saveStoredAuth({ token, refreshToken, baseURL });
|
|
903
1033
|
const exp = tokenExpiryIso(token);
|
|
904
1034
|
process.stdout.write(`Token saved: ${filePath}\n`);
|
|
905
1035
|
if (exp) {
|
|
906
1036
|
process.stdout.write(`Token expires: ${exp}\n`);
|
|
907
1037
|
}
|
|
1038
|
+
if (refreshToken) {
|
|
1039
|
+
process.stdout.write("Refresh token saved.\n");
|
|
1040
|
+
}
|
|
908
1041
|
process.stdout.write("Auth login complete (callback flow).\n");
|
|
909
1042
|
process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
|
|
910
1043
|
}
|
|
@@ -962,12 +1095,16 @@ async function runAuthLoginDevice(flags, baseURL, oidc) {
|
|
|
962
1095
|
});
|
|
963
1096
|
|
|
964
1097
|
const token = normalizeToken(tokenPayload.access_token);
|
|
965
|
-
const
|
|
1098
|
+
const refreshToken = normalizeToken(tokenPayload.refresh_token);
|
|
1099
|
+
const filePath = saveStoredAuth({ token, refreshToken, baseURL });
|
|
966
1100
|
const exp = tokenExpiryIso(token);
|
|
967
1101
|
process.stdout.write(`Token saved: ${filePath}\n`);
|
|
968
1102
|
if (exp) {
|
|
969
1103
|
process.stdout.write(`Token expires: ${exp}\n`);
|
|
970
1104
|
}
|
|
1105
|
+
if (refreshToken) {
|
|
1106
|
+
process.stdout.write("Refresh token saved.\n");
|
|
1107
|
+
}
|
|
971
1108
|
process.stdout.write("Auth login complete (device flow).\n");
|
|
972
1109
|
process.stdout.write("Restart Codex/Claude session to apply new token to MCP proxy.\n");
|
|
973
1110
|
}
|
|
@@ -1132,6 +1269,8 @@ async function runProxy(flags) {
|
|
|
1132
1269
|
timeoutSeconds: intFromRaw(flags["timeout-seconds"], 30),
|
|
1133
1270
|
};
|
|
1134
1271
|
const gatewayURL = buildGatewayURL(args);
|
|
1272
|
+
let lastRefreshAttemptAtMs = 0;
|
|
1273
|
+
let lastRefreshError = "";
|
|
1135
1274
|
|
|
1136
1275
|
const rl = readline.createInterface({
|
|
1137
1276
|
input: process.stdin,
|
|
@@ -1148,7 +1287,24 @@ async function runProxy(flags) {
|
|
|
1148
1287
|
return;
|
|
1149
1288
|
}
|
|
1150
1289
|
|
|
1151
|
-
|
|
1290
|
+
let resolved = resolveCurrentAccessToken();
|
|
1291
|
+
if (!resolved.token) {
|
|
1292
|
+
const nowMs = Date.now();
|
|
1293
|
+
// Throttle refresh attempts to avoid hammering token endpoint on repeated requests.
|
|
1294
|
+
if (nowMs - lastRefreshAttemptAtMs > 10000) {
|
|
1295
|
+
lastRefreshAttemptAtMs = nowMs;
|
|
1296
|
+
const refreshed = await tryRefreshStoredAccessToken({
|
|
1297
|
+
baseURL: args.baseURL,
|
|
1298
|
+
timeoutSeconds: args.timeoutSeconds,
|
|
1299
|
+
});
|
|
1300
|
+
if (refreshed.ok) {
|
|
1301
|
+
resolved = resolveCurrentAccessToken();
|
|
1302
|
+
lastRefreshError = "";
|
|
1303
|
+
} else {
|
|
1304
|
+
lastRefreshError = String(refreshed.error || "").trim();
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1152
1308
|
const token = resolved.token;
|
|
1153
1309
|
if (!token) {
|
|
1154
1310
|
const helpURL = authLoginURL(args.baseURL);
|
|
@@ -1156,6 +1312,7 @@ async function runProxy(flags) {
|
|
|
1156
1312
|
resolved.expired?.source
|
|
1157
1313
|
? `; expired token ignored: ${resolved.expired.source}${resolved.expired.expires ? ` (${resolved.expired.expires})` : ""}`
|
|
1158
1314
|
: "";
|
|
1315
|
+
const refreshHint = lastRefreshError ? `; refresh failed: ${lastRefreshError}` : "";
|
|
1159
1316
|
process.stdout.write(
|
|
1160
1317
|
`${JSON.stringify(
|
|
1161
1318
|
jsonRpcError(
|
|
@@ -1163,7 +1320,7 @@ async function runProxy(flags) {
|
|
|
1163
1320
|
-32001,
|
|
1164
1321
|
`missing auth token (set METHEUS_TOKEN or run 'metheus-governance-mcp auth login --base-url ${normalizeSiteBaseURL(
|
|
1165
1322
|
args.baseURL,
|
|
1166
|
-
)}'; login: ${helpURL}${expiredHint})`,
|
|
1323
|
+
)}'; login: ${helpURL}${expiredHint}${refreshHint})`,
|
|
1167
1324
|
),
|
|
1168
1325
|
)}\n`,
|
|
1169
1326
|
);
|
|
@@ -1233,10 +1390,10 @@ function tryRegister(cliBin, serverName, proxyArgs) {
|
|
|
1233
1390
|
|
|
1234
1391
|
function runRemove(cliBin, serverName) {
|
|
1235
1392
|
if (cliBin === "claude") {
|
|
1236
|
-
runCLICommand(cliBin, ["mcp", "remove", serverName, "
|
|
1393
|
+
runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "user"], {
|
|
1237
1394
|
stdio: "ignore",
|
|
1238
1395
|
});
|
|
1239
|
-
runCLICommand(cliBin, ["mcp", "remove", serverName, "
|
|
1396
|
+
runCLICommand(cliBin, ["mcp", "remove", serverName, "-s", "local"], {
|
|
1240
1397
|
stdio: "ignore",
|
|
1241
1398
|
});
|
|
1242
1399
|
return;
|
|
@@ -1244,51 +1401,118 @@ function runRemove(cliBin, serverName) {
|
|
|
1244
1401
|
runCLICommand(cliBin, ["mcp", "remove", serverName], { stdio: "ignore" });
|
|
1245
1402
|
}
|
|
1246
1403
|
|
|
1247
|
-
function
|
|
1404
|
+
function isRegistered(cliBin, serverName) {
|
|
1405
|
+
const args =
|
|
1406
|
+
cliBin === "claude"
|
|
1407
|
+
? ["mcp", "get", serverName]
|
|
1408
|
+
: ["mcp", "get", serverName, "--json"];
|
|
1409
|
+
const run = runCLICommand(cliBin, args, { stdio: "ignore" });
|
|
1410
|
+
return run.status === 0;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function resolveSetupContext(flags) {
|
|
1248
1414
|
const workspaceMeta = loadWorkspaceMeta(process.cwd());
|
|
1249
1415
|
const projectID = String(flags["project-id"] || workspaceMeta.project_id || "").trim();
|
|
1250
1416
|
const ctxpackKey = String(flags["ctxpack-key"] || buildCtxpackKeyFromMeta(workspaceMeta) || "").trim();
|
|
1251
|
-
const baseURL = String(flags["base-url"] ||
|
|
1417
|
+
const baseURL = String(flags["base-url"] || DEFAULT_SITE_URL).trim().replace(/\/+$/, "");
|
|
1252
1418
|
const serverName = String(flags.name || DEFAULT_SERVER_NAME).trim() || DEFAULT_SERVER_NAME;
|
|
1253
|
-
|
|
1254
1419
|
const proxyArgs = ["--base-url", `${baseURL}/governance/mcp`];
|
|
1255
1420
|
if (projectID) proxyArgs.push("--project-id", projectID);
|
|
1256
1421
|
if (ctxpackKey) proxyArgs.push("--ctxpack-key", ctxpackKey);
|
|
1422
|
+
return { projectID, ctxpackKey, baseURL, serverName, proxyArgs };
|
|
1423
|
+
}
|
|
1257
1424
|
|
|
1425
|
+
function runSetupInternal(flags, options = {}) {
|
|
1426
|
+
const ensureOnly = Boolean(options.ensureOnly);
|
|
1427
|
+
const context = resolveSetupContext(flags);
|
|
1258
1428
|
const clients = ["codex", "claude"];
|
|
1259
1429
|
const results = [];
|
|
1430
|
+
|
|
1260
1431
|
for (const cliBin of clients) {
|
|
1261
1432
|
if (!commandExists(cliBin)) continue;
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1433
|
+
const alreadyRegistered = isRegistered(cliBin, context.serverName);
|
|
1434
|
+
if (ensureOnly && alreadyRegistered) {
|
|
1435
|
+
results.push({ cliBin, ok: true, action: "already-registered" });
|
|
1436
|
+
continue;
|
|
1437
|
+
}
|
|
1438
|
+
if (!ensureOnly) {
|
|
1439
|
+
runRemove(cliBin, context.serverName);
|
|
1440
|
+
}
|
|
1441
|
+
const ok = tryRegister(cliBin, context.serverName, context.proxyArgs);
|
|
1442
|
+
results.push({ cliBin, ok, action: alreadyRegistered ? "re-registered" : "registered" });
|
|
1265
1443
|
}
|
|
1266
1444
|
|
|
1267
|
-
process.stdout.write("\nInstall complete.\n");
|
|
1268
|
-
process.stdout.write(`Server: ${serverName}\n`);
|
|
1269
|
-
process.stdout.write(`Gateway: ${baseURL}/governance/mcp\n`);
|
|
1270
|
-
process.stdout.write(`Project: ${projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
|
|
1271
|
-
if (ctxpackKey) {
|
|
1272
|
-
process.stdout.write(`Ctxpack: ${ctxpackKey}\n`);
|
|
1445
|
+
process.stdout.write(ensureOnly ? "\nEnsure complete.\n" : "\nInstall complete.\n");
|
|
1446
|
+
process.stdout.write(`Server: ${context.serverName}\n`);
|
|
1447
|
+
process.stdout.write(`Gateway: ${context.baseURL}/governance/mcp\n`);
|
|
1448
|
+
process.stdout.write(`Project: ${context.projectID || "auto-detect from .metheus_ctxpack_sync.json"}\n`);
|
|
1449
|
+
if (context.ctxpackKey) {
|
|
1450
|
+
process.stdout.write(`Ctxpack: ${context.ctxpackKey}\n`);
|
|
1273
1451
|
}
|
|
1274
1452
|
if (results.length === 0) {
|
|
1275
1453
|
process.stdout.write("No codex/claude CLI found. Registration skipped.\n");
|
|
1276
|
-
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
for (const row of results) {
|
|
1457
|
+
process.stdout.write(`${row.cliBin}: ${row.ok ? row.action : "registration failed"}\n`);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
function runSetup(flags) {
|
|
1462
|
+
runSetupInternal(flags, { ensureOnly: false });
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
async function runBootstrap(flags) {
|
|
1466
|
+
process.stdout.write("Bootstrap start.\n");
|
|
1467
|
+
let resolved = resolveCurrentAccessToken();
|
|
1468
|
+
if (!resolved.token) {
|
|
1469
|
+
process.stdout.write("Auth token missing/expired. Starting login flow...\n");
|
|
1470
|
+
await runAuthLogin(flags);
|
|
1471
|
+
resolved = resolveCurrentAccessToken();
|
|
1472
|
+
if (!resolved.token) {
|
|
1473
|
+
process.stderr.write("Auth login finished but token is still unavailable.\n");
|
|
1474
|
+
process.exitCode = 1;
|
|
1475
|
+
return;
|
|
1280
1476
|
}
|
|
1477
|
+
} else {
|
|
1478
|
+
process.stdout.write(`Auth token OK (${resolved.source}).\n`);
|
|
1281
1479
|
}
|
|
1480
|
+
|
|
1481
|
+
runSetupInternal(flags, { ensureOnly: true });
|
|
1482
|
+
process.stdout.write("Bootstrap done.\n");
|
|
1282
1483
|
}
|
|
1283
1484
|
|
|
1284
1485
|
async function main() {
|
|
1285
|
-
const [, ,
|
|
1286
|
-
|
|
1486
|
+
const [, , rawCommand, ...rest] = process.argv;
|
|
1487
|
+
const command = String(rawCommand || "");
|
|
1488
|
+
|
|
1489
|
+
if (command === "-v" || command === "--version" || command === "version") {
|
|
1490
|
+
printVersion();
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
if (!command) {
|
|
1495
|
+
await runBootstrap({});
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
if (command === "-h" || command === "--help") {
|
|
1287
1500
|
printUsage();
|
|
1288
|
-
process.exit(
|
|
1501
|
+
process.exit(0);
|
|
1289
1502
|
return;
|
|
1290
1503
|
}
|
|
1504
|
+
|
|
1505
|
+
if (command.startsWith("--")) {
|
|
1506
|
+
const flags = parseArgs([command, ...rest]);
|
|
1507
|
+
await runBootstrap(flags);
|
|
1508
|
+
return;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1291
1511
|
const flags = parseArgs(rest);
|
|
1512
|
+
if (command === "init") {
|
|
1513
|
+
await runBootstrap(flags);
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1292
1516
|
if (command === "setup") {
|
|
1293
1517
|
runSetup(flags);
|
|
1294
1518
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metheus-governance-mcp-cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Metheus Governance MCP CLI (setup + stdio proxy)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"publish:public": "node release.mjs"
|
|
17
17
|
},
|
|
18
18
|
"bin": {
|
|
19
|
-
"metheus-governance-mcp": "bin/metheus-governance-mcp.js"
|
|
19
|
+
"metheus-governance-mcp": "bin/metheus-governance-mcp.js",
|
|
20
|
+
"metheus-governance-mcp-cli": "bin/metheus-governance-mcp.js"
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=18.0.0"
|