@yawlabs/mcph 0.47.1 → 0.47.3
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/CHANGELOG.md +4 -0
- package/README.md +1 -5
- package/dist/index.js +561 -481
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -125,7 +125,8 @@ function stripJsoncComments(src) {
|
|
|
125
125
|
return out;
|
|
126
126
|
}
|
|
127
127
|
function parseJsonc(src) {
|
|
128
|
-
const
|
|
128
|
+
const debommed = src.charCodeAt(0) === 65279 ? src.slice(1) : src;
|
|
129
|
+
const stripped = stripJsoncComments(debommed);
|
|
129
130
|
return JSON.parse(stripped);
|
|
130
131
|
}
|
|
131
132
|
|
|
@@ -191,9 +192,9 @@ var LEGACY_PROJECT_FILENAME = ".mcph.json";
|
|
|
191
192
|
var LEGACY_LOCAL_FILENAME = ".mcph.local.json";
|
|
192
193
|
var NEW_CONFIG_FILENAME = "config.json";
|
|
193
194
|
var NEW_LOCAL_FILENAME = "config.local.json";
|
|
194
|
-
async function exists(
|
|
195
|
+
async function exists(path5) {
|
|
195
196
|
try {
|
|
196
|
-
await stat(
|
|
197
|
+
await stat(path5);
|
|
197
198
|
return true;
|
|
198
199
|
} catch {
|
|
199
200
|
return false;
|
|
@@ -244,7 +245,7 @@ async function migrateLegacyConfigPaths(opts) {
|
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
async function findLegacyProjectRoot(cwd, home) {
|
|
247
|
-
const { resolve: resolve4, dirname:
|
|
248
|
+
const { resolve: resolve4, dirname: dirname2 } = await import("path");
|
|
248
249
|
const homeResolved = resolve4(home);
|
|
249
250
|
let dir = resolve4(cwd);
|
|
250
251
|
let prev = "";
|
|
@@ -254,7 +255,7 @@ async function findLegacyProjectRoot(cwd, home) {
|
|
|
254
255
|
const legacyLocal = join(dir, LEGACY_LOCAL_FILENAME);
|
|
255
256
|
if (await exists(legacyProject) || await exists(legacyLocal)) return dir;
|
|
256
257
|
prev = dir;
|
|
257
|
-
dir =
|
|
258
|
+
dir = dirname2(dir);
|
|
258
259
|
}
|
|
259
260
|
return null;
|
|
260
261
|
}
|
|
@@ -264,10 +265,10 @@ var CONFIG_FILENAME = "config.json";
|
|
|
264
265
|
var LOCAL_CONFIG_FILENAME = "config.local.json";
|
|
265
266
|
var CURRENT_SCHEMA_VERSION = 1;
|
|
266
267
|
var DEFAULT_API_BASE = "https://mcp.hosting";
|
|
267
|
-
async function readConfigAt(
|
|
268
|
+
async function readConfigAt(path5, scope, warnings) {
|
|
268
269
|
let raw;
|
|
269
270
|
try {
|
|
270
|
-
raw = await readFile(
|
|
271
|
+
raw = await readFile(path5, "utf8");
|
|
271
272
|
} catch {
|
|
272
273
|
return null;
|
|
273
274
|
}
|
|
@@ -276,43 +277,43 @@ async function readConfigAt(path4, scope, warnings) {
|
|
|
276
277
|
parsed = parseJsonc(raw);
|
|
277
278
|
} catch (err) {
|
|
278
279
|
const msg = err instanceof Error ? err.message : String(err);
|
|
279
|
-
warnings.push(`${
|
|
280
|
-
log("warn", "Config file is not valid JSON; ignoring", { path:
|
|
280
|
+
warnings.push(`${path5}: invalid JSON (${msg}) \u2014 file ignored`);
|
|
281
|
+
log("warn", "Config file is not valid JSON; ignoring", { path: path5, error: msg });
|
|
281
282
|
return null;
|
|
282
283
|
}
|
|
283
284
|
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
284
|
-
warnings.push(`${
|
|
285
|
+
warnings.push(`${path5}: root must be a JSON object \u2014 file ignored`);
|
|
285
286
|
return null;
|
|
286
287
|
}
|
|
287
288
|
const obj = parsed;
|
|
288
289
|
const version = typeof obj.version === "number" ? obj.version : void 0;
|
|
289
290
|
if (version !== void 0 && version > CURRENT_SCHEMA_VERSION) {
|
|
290
291
|
warnings.push(
|
|
291
|
-
`${
|
|
292
|
+
`${path5}: schema version ${version} is newer than this mcph (${CURRENT_SCHEMA_VERSION}); upgrade with \`npm i -g @yawlabs/mcph@latest\`. Loading best-effort.`
|
|
292
293
|
);
|
|
293
294
|
}
|
|
294
|
-
const
|
|
295
|
+
const token5 = typeof obj.token === "string" && obj.token.length > 0 ? obj.token : void 0;
|
|
295
296
|
const apiBase = typeof obj.apiBase === "string" && obj.apiBase.length > 0 ? obj.apiBase : void 0;
|
|
296
297
|
const servers = Array.isArray(obj.servers) ? obj.servers.filter((v) => typeof v === "string") : void 0;
|
|
297
298
|
const blocked = Array.isArray(obj.blocked) ? obj.blocked.filter((v) => typeof v === "string") : void 0;
|
|
298
|
-
if (
|
|
299
|
+
if (token5) {
|
|
299
300
|
if (scope === "project") {
|
|
300
301
|
warnings.push(
|
|
301
|
-
`${
|
|
302
|
+
`${path5}: 'token' should not appear in a project-shared file. Move it to ${CONFIG_DIRNAME}/${LOCAL_CONFIG_FILENAME} (gitignored) or ~/${CONFIG_DIRNAME}/${CONFIG_FILENAME}.`
|
|
302
303
|
);
|
|
303
304
|
}
|
|
304
|
-
await checkPermissions(
|
|
305
|
+
await checkPermissions(path5, warnings);
|
|
305
306
|
}
|
|
306
|
-
return { path:
|
|
307
|
+
return { path: path5, scope, version, token: token5, apiBase, servers, blocked };
|
|
307
308
|
}
|
|
308
|
-
async function checkPermissions(
|
|
309
|
+
async function checkPermissions(path5, warnings) {
|
|
309
310
|
if (process.platform === "win32") return;
|
|
310
311
|
try {
|
|
311
|
-
const st = await stat2(
|
|
312
|
+
const st = await stat2(path5);
|
|
312
313
|
const mode = st.mode & 511;
|
|
313
314
|
if ((mode & 63) !== 0) {
|
|
314
315
|
warnings.push(
|
|
315
|
-
`${
|
|
316
|
+
`${path5}: contains a token but is readable by group/other (mode ${mode.toString(8)}). Run \`chmod 600 ${path5}\` to restrict.`
|
|
316
317
|
);
|
|
317
318
|
}
|
|
318
319
|
} catch {
|
|
@@ -360,16 +361,16 @@ async function loadMcphConfig(opts = {}) {
|
|
|
360
361
|
if (project) loadedFiles.push(project);
|
|
361
362
|
const global = await readConfigAt(globalPath, "global", warnings);
|
|
362
363
|
if (global) loadedFiles.push(global);
|
|
363
|
-
let
|
|
364
|
+
let token5 = null;
|
|
364
365
|
let tokenSource = "missing";
|
|
365
366
|
if (typeof env.MCPH_TOKEN === "string" && env.MCPH_TOKEN.length > 0) {
|
|
366
|
-
|
|
367
|
+
token5 = env.MCPH_TOKEN;
|
|
367
368
|
tokenSource = "env";
|
|
368
369
|
} else if (local?.token) {
|
|
369
|
-
|
|
370
|
+
token5 = local.token;
|
|
370
371
|
tokenSource = "local";
|
|
371
372
|
} else if (global?.token) {
|
|
372
|
-
|
|
373
|
+
token5 = global.token;
|
|
373
374
|
tokenSource = "global";
|
|
374
375
|
}
|
|
375
376
|
let apiBase = DEFAULT_API_BASE;
|
|
@@ -388,7 +389,7 @@ async function loadMcphConfig(opts = {}) {
|
|
|
388
389
|
apiBaseSource = "global";
|
|
389
390
|
}
|
|
390
391
|
return {
|
|
391
|
-
token:
|
|
392
|
+
token: token5,
|
|
392
393
|
tokenSource,
|
|
393
394
|
apiBase,
|
|
394
395
|
apiBaseSource,
|
|
@@ -399,10 +400,10 @@ async function loadMcphConfig(opts = {}) {
|
|
|
399
400
|
warnings
|
|
400
401
|
};
|
|
401
402
|
}
|
|
402
|
-
function tokenFingerprint(
|
|
403
|
-
if (!
|
|
404
|
-
if (
|
|
405
|
-
return `${
|
|
403
|
+
function tokenFingerprint(token5) {
|
|
404
|
+
if (!token5) return "(none)";
|
|
405
|
+
if (token5.length <= 8) return `***${token5.slice(-2)}`;
|
|
406
|
+
return `${token5.slice(0, 8)}\u2026${token5.slice(-4)}`;
|
|
406
407
|
}
|
|
407
408
|
function toProfile(config) {
|
|
408
409
|
if (config.servers === void 0 && config.blocked === void 0) return null;
|
|
@@ -441,10 +442,10 @@ function profileAllows(profile, namespace) {
|
|
|
441
442
|
|
|
442
443
|
// src/config.ts
|
|
443
444
|
import { request } from "undici";
|
|
444
|
-
async function fetchConfig(
|
|
445
|
-
const url = `${
|
|
445
|
+
async function fetchConfig(apiUrl5, token5, currentVersion) {
|
|
446
|
+
const url = `${apiUrl5.replace(/\/$/, "")}/api/connect/config`;
|
|
446
447
|
const headers = {
|
|
447
|
-
Authorization: `Bearer ${
|
|
448
|
+
Authorization: `Bearer ${token5}`,
|
|
448
449
|
Accept: "application/json"
|
|
449
450
|
};
|
|
450
451
|
if (currentVersion) {
|
|
@@ -465,7 +466,7 @@ async function fetchConfig(apiUrl6, token6, currentVersion) {
|
|
|
465
466
|
await res.body.text().catch(() => {
|
|
466
467
|
});
|
|
467
468
|
throw new ConfigError(
|
|
468
|
-
`Token rejected (HTTP 401) \u2014 the token ${tokenFingerprint(
|
|
469
|
+
`Token rejected (HTTP 401) \u2014 the token ${tokenFingerprint(token5)} is invalid or revoked.
|
|
469
470
|
Generate a new token at https://mcp.hosting/dashboard/settings/tokens,
|
|
470
471
|
then re-run \`mcph install <client> --token mcp_pat_...\` or set MCPH_TOKEN.`,
|
|
471
472
|
true
|
|
@@ -475,7 +476,7 @@ async function fetchConfig(apiUrl6, token6, currentVersion) {
|
|
|
475
476
|
await res.body.text().catch(() => {
|
|
476
477
|
});
|
|
477
478
|
throw new ConfigError(
|
|
478
|
-
`Access denied (HTTP 403) \u2014 the token ${tokenFingerprint(
|
|
479
|
+
`Access denied (HTTP 403) \u2014 the token ${tokenFingerprint(token5)} was accepted but lacks permission to read this account's servers.
|
|
479
480
|
The account may be suspended or the token scope reduced \u2014 check
|
|
480
481
|
https://mcp.hosting/dashboard/settings/tokens, or reach support@mcp.hosting.`,
|
|
481
482
|
true
|
|
@@ -899,12 +900,12 @@ async function runComplianceCommand(argv) {
|
|
|
899
900
|
);
|
|
900
901
|
return 1;
|
|
901
902
|
}
|
|
902
|
-
const
|
|
903
|
+
const apiUrl5 = process.env.MCPH_URL ?? "https://mcp.hosting";
|
|
903
904
|
const report = await runTest(args);
|
|
904
905
|
if (!report) return 1;
|
|
905
906
|
printSummary(report);
|
|
906
907
|
if (publish) {
|
|
907
|
-
const result = await publishReport(
|
|
908
|
+
const result = await publishReport(apiUrl5, report);
|
|
908
909
|
if (!result) return 1;
|
|
909
910
|
process.stdout.write(`
|
|
910
911
|
Published: ${result.reportUrl}
|
|
@@ -964,9 +965,9 @@ Target: ${url}
|
|
|
964
965
|
`
|
|
965
966
|
);
|
|
966
967
|
}
|
|
967
|
-
async function publishReport(
|
|
968
|
+
async function publishReport(apiUrl5, report) {
|
|
968
969
|
try {
|
|
969
|
-
const res = await request2(`${
|
|
970
|
+
const res = await request2(`${apiUrl5.replace(/\/$/, "")}/api/compliance/ext`, {
|
|
970
971
|
method: "POST",
|
|
971
972
|
headers: { "Content-Type": "application/json" },
|
|
972
973
|
body: JSON.stringify(report)
|
|
@@ -994,6 +995,124 @@ import { readFile as readFile3 } from "fs/promises";
|
|
|
994
995
|
import { homedir as homedir4 } from "os";
|
|
995
996
|
import { join as join4 } from "path";
|
|
996
997
|
|
|
998
|
+
// src/analytics.ts
|
|
999
|
+
import { request as request3 } from "undici";
|
|
1000
|
+
var FLUSH_INTERVAL = 3e4;
|
|
1001
|
+
var FLUSH_SIZE = 50;
|
|
1002
|
+
var MAX_BUFFER = 5e3;
|
|
1003
|
+
var buffer = [];
|
|
1004
|
+
var dispatchBuffer = [];
|
|
1005
|
+
var flushTimer = null;
|
|
1006
|
+
var apiUrl = "";
|
|
1007
|
+
var token = "";
|
|
1008
|
+
var lastFailure = null;
|
|
1009
|
+
function getLastAnalyticsFailure() {
|
|
1010
|
+
return lastFailure;
|
|
1011
|
+
}
|
|
1012
|
+
function recordConnectEvent(event) {
|
|
1013
|
+
if (buffer.length >= MAX_BUFFER) return;
|
|
1014
|
+
buffer.push({ ...event, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1015
|
+
if (buffer.length >= FLUSH_SIZE) {
|
|
1016
|
+
flush().catch(() => {
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
function recordDispatchEvent(event) {
|
|
1021
|
+
if (dispatchBuffer.length >= MAX_BUFFER) return;
|
|
1022
|
+
dispatchBuffer.push(event);
|
|
1023
|
+
if (dispatchBuffer.length >= FLUSH_SIZE) {
|
|
1024
|
+
flushDispatch().catch(() => {
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
async function flush() {
|
|
1029
|
+
if (buffer.length === 0 || !apiUrl || !token) return;
|
|
1030
|
+
const events = buffer.splice(0, FLUSH_SIZE);
|
|
1031
|
+
const url = `${apiUrl.replace(/\/$/, "")}/api/connect/analytics`;
|
|
1032
|
+
try {
|
|
1033
|
+
const res = await request3(url, {
|
|
1034
|
+
method: "POST",
|
|
1035
|
+
headers: {
|
|
1036
|
+
Authorization: `Bearer ${token}`,
|
|
1037
|
+
"Content-Type": "application/json"
|
|
1038
|
+
},
|
|
1039
|
+
body: JSON.stringify({ events }),
|
|
1040
|
+
headersTimeout: 1e4,
|
|
1041
|
+
bodyTimeout: 1e4
|
|
1042
|
+
});
|
|
1043
|
+
if (res.statusCode >= 400) {
|
|
1044
|
+
const room = MAX_BUFFER - buffer.length;
|
|
1045
|
+
if (room > 0) buffer.push(...events.slice(0, room));
|
|
1046
|
+
log("warn", "Analytics flush failed", { status: res.statusCode });
|
|
1047
|
+
lastFailure = { statusCode: res.statusCode, url, at: Date.now() };
|
|
1048
|
+
} else {
|
|
1049
|
+
lastFailure = null;
|
|
1050
|
+
}
|
|
1051
|
+
await res.body.text().catch(() => {
|
|
1052
|
+
});
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
const room = MAX_BUFFER - buffer.length;
|
|
1055
|
+
if (room > 0) buffer.push(...events.slice(0, room));
|
|
1056
|
+
log("warn", "Analytics flush error", { error: err.message });
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
async function flushDispatch() {
|
|
1060
|
+
if (dispatchBuffer.length === 0 || !apiUrl || !token) return;
|
|
1061
|
+
const events = dispatchBuffer.splice(0, FLUSH_SIZE);
|
|
1062
|
+
const url = `${apiUrl.replace(/\/$/, "")}/api/connect/dispatch-events`;
|
|
1063
|
+
try {
|
|
1064
|
+
const res = await request3(url, {
|
|
1065
|
+
method: "POST",
|
|
1066
|
+
headers: {
|
|
1067
|
+
Authorization: `Bearer ${token}`,
|
|
1068
|
+
"Content-Type": "application/json"
|
|
1069
|
+
},
|
|
1070
|
+
body: JSON.stringify({ events }),
|
|
1071
|
+
headersTimeout: 1e4,
|
|
1072
|
+
bodyTimeout: 1e4
|
|
1073
|
+
});
|
|
1074
|
+
if (res.statusCode >= 400 && res.statusCode !== 204) {
|
|
1075
|
+
const room = MAX_BUFFER - dispatchBuffer.length;
|
|
1076
|
+
if (room > 0) dispatchBuffer.push(...events.slice(0, room));
|
|
1077
|
+
log("warn", "Dispatch-events flush failed", { status: res.statusCode });
|
|
1078
|
+
lastFailure = { statusCode: res.statusCode, url, at: Date.now() };
|
|
1079
|
+
} else if (res.statusCode < 400) {
|
|
1080
|
+
lastFailure = null;
|
|
1081
|
+
}
|
|
1082
|
+
await res.body.text().catch(() => {
|
|
1083
|
+
});
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
const room = MAX_BUFFER - dispatchBuffer.length;
|
|
1086
|
+
if (room > 0) dispatchBuffer.push(...events.slice(0, room));
|
|
1087
|
+
log("warn", "Dispatch-events flush error", { error: err.message });
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
function initAnalytics(url, tok) {
|
|
1091
|
+
apiUrl = url;
|
|
1092
|
+
token = tok;
|
|
1093
|
+
flushTimer = setInterval(() => {
|
|
1094
|
+
flush().catch(() => {
|
|
1095
|
+
});
|
|
1096
|
+
flushDispatch().catch(() => {
|
|
1097
|
+
});
|
|
1098
|
+
}, FLUSH_INTERVAL);
|
|
1099
|
+
if (flushTimer.unref) flushTimer.unref();
|
|
1100
|
+
}
|
|
1101
|
+
async function shutdownAnalytics() {
|
|
1102
|
+
if (flushTimer) {
|
|
1103
|
+
clearInterval(flushTimer);
|
|
1104
|
+
flushTimer = null;
|
|
1105
|
+
}
|
|
1106
|
+
for (let i = 0; i < 3 && buffer.length > 0; i++) {
|
|
1107
|
+
await flush();
|
|
1108
|
+
}
|
|
1109
|
+
for (let i = 0; i < 3 && dispatchBuffer.length > 0; i++) {
|
|
1110
|
+
await flushDispatch();
|
|
1111
|
+
}
|
|
1112
|
+
buffer.length = 0;
|
|
1113
|
+
dispatchBuffer.length = 0;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
997
1116
|
// src/cli-shadows.ts
|
|
998
1117
|
var EMPTY = [];
|
|
999
1118
|
var NAMESPACE_REGISTRY = {
|
|
@@ -1229,7 +1348,7 @@ var INSTALL_TARGETS = [
|
|
|
1229
1348
|
function resolveInstallPath(opts) {
|
|
1230
1349
|
const home = opts.home ?? homedir3();
|
|
1231
1350
|
const appData = opts.appData ?? process.env.APPDATA ?? join3(home, "AppData", "Roaming");
|
|
1232
|
-
const { clientId, scope, os, projectDir } = opts;
|
|
1351
|
+
const { clientId, scope, os, projectDir, claudeConfigDir } = opts;
|
|
1233
1352
|
const target = INSTALL_TARGETS.find((t) => t.clientId === clientId);
|
|
1234
1353
|
if (!target) throw new Error(`Unknown client: ${clientId}`);
|
|
1235
1354
|
const scopeSpec = target.scopes.find((s) => s.scope === scope);
|
|
@@ -1240,15 +1359,24 @@ function resolveInstallPath(opts) {
|
|
|
1240
1359
|
if (scopeSpec.requiresProjectDir && !projectDir) {
|
|
1241
1360
|
throw new Error(`Scope ${scope} for ${clientId} requires a project directory`);
|
|
1242
1361
|
}
|
|
1243
|
-
const p = pathFor(clientId, scope, os, {
|
|
1362
|
+
const p = pathFor(clientId, scope, os, {
|
|
1363
|
+
home,
|
|
1364
|
+
appData,
|
|
1365
|
+
projectDir: projectDir ?? "",
|
|
1366
|
+
claudeConfigDir: claudeConfigDir && claudeConfigDir.length > 0 ? claudeConfigDir : void 0
|
|
1367
|
+
});
|
|
1244
1368
|
return p;
|
|
1245
1369
|
}
|
|
1246
1370
|
function pathFor(client, scope, os, base) {
|
|
1247
|
-
const { home, appData, projectDir } = base;
|
|
1371
|
+
const { home, appData, projectDir, claudeConfigDir } = base;
|
|
1248
1372
|
const sep = os === "windows" ? "\\" : "/";
|
|
1249
1373
|
const joinPath = (...parts) => parts.join(sep);
|
|
1250
1374
|
if (client === "claude-code") {
|
|
1251
1375
|
if (scope === "user") {
|
|
1376
|
+
if (claudeConfigDir) {
|
|
1377
|
+
const absolute = join3(claudeConfigDir, ".claude.json");
|
|
1378
|
+
return { absolute, display: absolute, containerPath: ["mcpServers"] };
|
|
1379
|
+
}
|
|
1252
1380
|
const display = os === "windows" ? "%USERPROFILE%\\.claude.json" : "~/.claude.json";
|
|
1253
1381
|
return { absolute: join3(home, ".claude.json"), display, containerPath: ["mcpServers"] };
|
|
1254
1382
|
}
|
|
@@ -1259,6 +1387,10 @@ function pathFor(client, scope, os, base) {
|
|
|
1259
1387
|
containerPath: ["mcpServers"]
|
|
1260
1388
|
};
|
|
1261
1389
|
}
|
|
1390
|
+
if (claudeConfigDir) {
|
|
1391
|
+
const absolute = join3(claudeConfigDir, ".claude.json");
|
|
1392
|
+
return { absolute, display: absolute, containerPath: ["projects", projectDir, "mcpServers"] };
|
|
1393
|
+
}
|
|
1262
1394
|
return {
|
|
1263
1395
|
absolute: join3(home, ".claude.json"),
|
|
1264
1396
|
display: os === "windows" ? "%USERPROFILE%\\.claude.json" : "~/.claude.json",
|
|
@@ -1311,20 +1443,39 @@ function buildLaunchEntry(opts) {
|
|
|
1311
1443
|
var ENTRY_NAME = "mcp.hosting";
|
|
1312
1444
|
var CLAUDE_CODE_ALLOW_PATTERN = "mcp__mcp_hosting__*";
|
|
1313
1445
|
function resolveClaudeCodeSettingsPath(scope, opts) {
|
|
1314
|
-
const { home, projectDir } = opts;
|
|
1315
|
-
|
|
1446
|
+
const { home, projectDir, claudeConfigDir } = opts;
|
|
1447
|
+
const cfgDir = claudeConfigDir && claudeConfigDir.length > 0 ? claudeConfigDir : null;
|
|
1448
|
+
if (scope === "user") return cfgDir ? join3(cfgDir, "settings.json") : join3(home, ".claude", "settings.json");
|
|
1316
1449
|
if (scope === "project" && projectDir) return join3(projectDir, ".claude", "settings.json");
|
|
1317
1450
|
if (scope === "local" && projectDir) return join3(projectDir, ".claude", "settings.local.json");
|
|
1318
1451
|
return null;
|
|
1319
1452
|
}
|
|
1320
1453
|
|
|
1321
1454
|
// src/persistence.ts
|
|
1322
|
-
import {
|
|
1455
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1456
|
+
import path3 from "path";
|
|
1457
|
+
|
|
1458
|
+
// src/atomic-write.ts
|
|
1459
|
+
import { mkdir as mkdir2, rename as rename2, unlink, writeFile } from "fs/promises";
|
|
1323
1460
|
import path2 from "path";
|
|
1461
|
+
async function atomicWriteFile(filePath, contents, encoding = "utf8") {
|
|
1462
|
+
const dir = path2.dirname(filePath);
|
|
1463
|
+
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
1464
|
+
await mkdir2(dir, { recursive: true });
|
|
1465
|
+
try {
|
|
1466
|
+
await writeFile(tmp, contents, encoding);
|
|
1467
|
+
await rename2(tmp, filePath);
|
|
1468
|
+
} catch (err) {
|
|
1469
|
+
await unlink(tmp).catch(() => void 0);
|
|
1470
|
+
throw err;
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// src/persistence.ts
|
|
1324
1475
|
var STATE_SCHEMA_VERSION = 1;
|
|
1325
1476
|
var STATE_FILENAME = "state.json";
|
|
1326
1477
|
function statePath(configDir = userConfigDir()) {
|
|
1327
|
-
return
|
|
1478
|
+
return path3.join(configDir, STATE_FILENAME);
|
|
1328
1479
|
}
|
|
1329
1480
|
function emptyState() {
|
|
1330
1481
|
return { version: STATE_SCHEMA_VERSION, savedAt: 0, learning: {}, packHistory: [] };
|
|
@@ -1348,19 +1499,21 @@ async function loadState(filePath = statePath()) {
|
|
|
1348
1499
|
return emptyState();
|
|
1349
1500
|
}
|
|
1350
1501
|
}
|
|
1351
|
-
|
|
1502
|
+
var saveChain = Promise.resolve();
|
|
1503
|
+
function saveState(state, filePath = statePath()) {
|
|
1504
|
+
const next = saveChain.then(() => doSaveState(state, filePath));
|
|
1505
|
+
saveChain = next.catch(() => void 0);
|
|
1506
|
+
return next;
|
|
1507
|
+
}
|
|
1508
|
+
async function doSaveState(state, filePath) {
|
|
1352
1509
|
const payload = {
|
|
1353
1510
|
version: STATE_SCHEMA_VERSION,
|
|
1354
1511
|
savedAt: Date.now(),
|
|
1355
1512
|
learning: state.learning,
|
|
1356
1513
|
packHistory: state.packHistory
|
|
1357
1514
|
};
|
|
1358
|
-
const dir = path2.dirname(filePath);
|
|
1359
|
-
const tmp = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
1360
1515
|
try {
|
|
1361
|
-
await
|
|
1362
|
-
await writeFile(tmp, JSON.stringify(payload, null, 2), "utf8");
|
|
1363
|
-
await rename2(tmp, filePath);
|
|
1516
|
+
await atomicWriteFile(filePath, JSON.stringify(payload, null, 2));
|
|
1364
1517
|
} catch (err) {
|
|
1365
1518
|
log("warn", "Failed to save mcph state", { error: errorMessage(err) });
|
|
1366
1519
|
}
|
|
@@ -1399,6 +1552,45 @@ function errorMessage(err) {
|
|
|
1399
1552
|
return err instanceof Error ? err.message : String(err);
|
|
1400
1553
|
}
|
|
1401
1554
|
|
|
1555
|
+
// src/tool-report.ts
|
|
1556
|
+
import { request as request4 } from "undici";
|
|
1557
|
+
var apiUrl2 = "";
|
|
1558
|
+
var token2 = "";
|
|
1559
|
+
var lastFailure2 = null;
|
|
1560
|
+
function getLastReportFailure() {
|
|
1561
|
+
return lastFailure2;
|
|
1562
|
+
}
|
|
1563
|
+
function initToolReport(url, tok) {
|
|
1564
|
+
apiUrl2 = url;
|
|
1565
|
+
token2 = tok;
|
|
1566
|
+
}
|
|
1567
|
+
async function reportTools(serverId, tools) {
|
|
1568
|
+
if (!apiUrl2 || !token2 || !serverId) return;
|
|
1569
|
+
const url = `${apiUrl2.replace(/\/$/, "")}/api/connect/servers/${serverId}/tools`;
|
|
1570
|
+
try {
|
|
1571
|
+
const res = await request4(url, {
|
|
1572
|
+
method: "POST",
|
|
1573
|
+
headers: {
|
|
1574
|
+
Authorization: `Bearer ${token2}`,
|
|
1575
|
+
"Content-Type": "application/json"
|
|
1576
|
+
},
|
|
1577
|
+
body: JSON.stringify({ tools }),
|
|
1578
|
+
headersTimeout: 1e4,
|
|
1579
|
+
bodyTimeout: 1e4
|
|
1580
|
+
});
|
|
1581
|
+
await res.body.text().catch(() => {
|
|
1582
|
+
});
|
|
1583
|
+
if (res.statusCode >= 400 && res.statusCode !== 404) {
|
|
1584
|
+
log("warn", "Tool report failed", { serverId, status: res.statusCode });
|
|
1585
|
+
lastFailure2 = { statusCode: res.statusCode, url, at: Date.now() };
|
|
1586
|
+
} else if (res.statusCode < 400) {
|
|
1587
|
+
lastFailure2 = null;
|
|
1588
|
+
}
|
|
1589
|
+
} catch (err) {
|
|
1590
|
+
log("warn", "Tool report error", { serverId, error: err?.message });
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1402
1594
|
// src/usage-hints.ts
|
|
1403
1595
|
var MAX_PEERS = 3;
|
|
1404
1596
|
var MIN_SUCCESS_TO_SHOW = 1;
|
|
@@ -1458,7 +1650,7 @@ function selectFlakyNamespaces(entries, limit) {
|
|
|
1458
1650
|
}
|
|
1459
1651
|
|
|
1460
1652
|
// src/doctor-cmd.ts
|
|
1461
|
-
var VERSION = true ? "0.47.
|
|
1653
|
+
var VERSION = true ? "0.47.3" : "dev";
|
|
1462
1654
|
async function runDoctor(opts = {}) {
|
|
1463
1655
|
if (opts.json) return runDoctorJson(opts);
|
|
1464
1656
|
const lines = [];
|
|
@@ -1497,7 +1689,9 @@ async function runDoctor(opts = {}) {
|
|
|
1497
1689
|
renderEnvSection({ env, print });
|
|
1498
1690
|
await renderStateSection({ home, env, print });
|
|
1499
1691
|
await renderReliabilitySection({ home, env, print });
|
|
1500
|
-
|
|
1692
|
+
renderBackgroundPostersSection({ print });
|
|
1693
|
+
const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
|
|
1694
|
+
const clients = probeClients({ home, os, cwd, claudeConfigDir });
|
|
1501
1695
|
print("INSTALLED CLIENTS (probed config files)");
|
|
1502
1696
|
for (const c of clients) {
|
|
1503
1697
|
const status = c.unavailable ? "unavailable on this OS" : c.malformed ? "exists but JSON is malformed \u2014 fix or rerun `mcph install`" : c.hasMcphEntry ? `OK \u2014 has "${ENTRY_NAME}" entry` : c.exists ? `present, no "${ENTRY_NAME}" entry \u2014 run \`mcph install ${c.clientId}${c.scope === "user" ? "" : ` --scope ${c.scope}`}\`` : `not configured \u2014 run \`mcph install ${c.clientId}${c.scope === "user" ? "" : ` --scope ${c.scope}`}\``;
|
|
@@ -1557,7 +1751,8 @@ async function runDoctorJson(opts) {
|
|
|
1557
1751
|
const env = opts.env ?? process.env;
|
|
1558
1752
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
1559
1753
|
const config = await loadMcphConfig({ cwd, home, env });
|
|
1560
|
-
const
|
|
1754
|
+
const claudeConfigDir = env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.length > 0 ? env.CLAUDE_CONFIG_DIR : void 0;
|
|
1755
|
+
const clients = probeClients({ home, os, cwd, claudeConfigDir });
|
|
1561
1756
|
const envVarNames = [
|
|
1562
1757
|
"MCPH_POLL_INTERVAL",
|
|
1563
1758
|
"MCPH_SERVER_CAP",
|
|
@@ -1675,6 +1870,25 @@ async function renderStateSection(opts) {
|
|
|
1675
1870
|
}
|
|
1676
1871
|
const filePath = join4(userConfigDir(home), STATE_FILENAME);
|
|
1677
1872
|
print(` path: ${filePath}`);
|
|
1873
|
+
const peek = await peekStateFile(filePath);
|
|
1874
|
+
if (peek.kind === "malformed") {
|
|
1875
|
+
print(" status: corrupt -- file exists but JSON is unparseable");
|
|
1876
|
+
print(` fix: \`mcph reset-learning\` to clear, or open ${filePath} and fix by hand`);
|
|
1877
|
+
print(` detail: ${peek.message}`);
|
|
1878
|
+
print("");
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (peek.kind === "stale-version") {
|
|
1882
|
+
print(` status: schema mismatch (file is v${peek.version ?? "?"}, this mcph reads v${peek.expected})`);
|
|
1883
|
+
print(" fix: `mcph reset-learning` to drop the old file -- learning will rebuild on use");
|
|
1884
|
+
print("");
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (peek.kind === "unreadable") {
|
|
1888
|
+
print(` status: unreadable (${peek.message})`);
|
|
1889
|
+
print("");
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1678
1892
|
const persisted = await loadState(filePath);
|
|
1679
1893
|
if (persisted.savedAt === 0) {
|
|
1680
1894
|
print(" (no persisted state yet \u2014 will be created on the first tool call)");
|
|
@@ -1685,6 +1899,29 @@ async function renderStateSection(opts) {
|
|
|
1685
1899
|
}
|
|
1686
1900
|
print("");
|
|
1687
1901
|
}
|
|
1902
|
+
async function peekStateFile(filePath) {
|
|
1903
|
+
let raw;
|
|
1904
|
+
try {
|
|
1905
|
+
raw = await readFile3(filePath, "utf8");
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
1908
|
+
return { kind: "missing" };
|
|
1909
|
+
}
|
|
1910
|
+
return { kind: "unreadable", message: err instanceof Error ? err.message : String(err) };
|
|
1911
|
+
}
|
|
1912
|
+
let parsed;
|
|
1913
|
+
try {
|
|
1914
|
+
parsed = JSON.parse(raw);
|
|
1915
|
+
} catch (err) {
|
|
1916
|
+
return { kind: "malformed", message: err instanceof Error ? err.message : String(err) };
|
|
1917
|
+
}
|
|
1918
|
+
if (!parsed || typeof parsed !== "object") return { kind: "malformed", message: "top-level value is not an object" };
|
|
1919
|
+
const version = parsed.version;
|
|
1920
|
+
if (version !== STATE_SCHEMA_VERSION) {
|
|
1921
|
+
return { kind: "stale-version", version, expected: STATE_SCHEMA_VERSION };
|
|
1922
|
+
}
|
|
1923
|
+
return { kind: "ok" };
|
|
1924
|
+
}
|
|
1688
1925
|
async function renderReliabilitySection(opts) {
|
|
1689
1926
|
const { home, env, print } = opts;
|
|
1690
1927
|
const raw = env.MCPH_DISABLE_PERSISTENCE;
|
|
@@ -1705,6 +1942,18 @@ async function renderReliabilitySection(opts) {
|
|
|
1705
1942
|
}
|
|
1706
1943
|
print("");
|
|
1707
1944
|
}
|
|
1945
|
+
function renderBackgroundPostersSection(opts) {
|
|
1946
|
+
const { print } = opts;
|
|
1947
|
+
const analyticsFailure = getLastAnalyticsFailure();
|
|
1948
|
+
const reportFailure = getLastReportFailure();
|
|
1949
|
+
if (!analyticsFailure && !reportFailure) return;
|
|
1950
|
+
const now = Date.now();
|
|
1951
|
+
const fmt = (f) => `HTTP ${f.statusCode} from ${f.url}, ${formatRelativeAge(now - f.at)} ago`;
|
|
1952
|
+
print("BACKGROUND POSTERS (recent failures)");
|
|
1953
|
+
print(` analytics: ${analyticsFailure ? fmt(analyticsFailure) : "(no recent failure)"}`);
|
|
1954
|
+
print(` tool-report: ${reportFailure ? fmt(reportFailure) : "(no recent failure)"}`);
|
|
1955
|
+
print("");
|
|
1956
|
+
}
|
|
1708
1957
|
function formatRelativeAge(ms) {
|
|
1709
1958
|
const clamped = Math.max(0, ms);
|
|
1710
1959
|
const s = Math.floor(clamped / 1e3);
|
|
@@ -1746,7 +1995,8 @@ function probeClients(opts) {
|
|
|
1746
1995
|
scope: scope.scope,
|
|
1747
1996
|
os: opts.os,
|
|
1748
1997
|
home: opts.home,
|
|
1749
|
-
projectDir: scope.requiresProjectDir ? opts.cwd : void 0
|
|
1998
|
+
projectDir: scope.requiresProjectDir ? opts.cwd : void 0,
|
|
1999
|
+
claudeConfigDir: opts.claudeConfigDir
|
|
1750
2000
|
});
|
|
1751
2001
|
} catch {
|
|
1752
2002
|
continue;
|
|
@@ -1784,9 +2034,9 @@ function probeClients(opts) {
|
|
|
1784
2034
|
}
|
|
1785
2035
|
return out;
|
|
1786
2036
|
}
|
|
1787
|
-
function walkContainer(root,
|
|
2037
|
+
function walkContainer(root, path5) {
|
|
1788
2038
|
let cur = root;
|
|
1789
|
-
for (const key of
|
|
2039
|
+
for (const key of path5) {
|
|
1790
2040
|
if (typeof cur !== "object" || cur === null || Array.isArray(cur)) return null;
|
|
1791
2041
|
cur = cur[key];
|
|
1792
2042
|
}
|
|
@@ -1815,7 +2065,8 @@ async function probeClientsAsync(opts) {
|
|
|
1815
2065
|
scope: scope.scope,
|
|
1816
2066
|
os: opts.os,
|
|
1817
2067
|
home: opts.home,
|
|
1818
|
-
projectDir: scope.requiresProjectDir ? opts.cwd : void 0
|
|
2068
|
+
projectDir: scope.requiresProjectDir ? opts.cwd : void 0,
|
|
2069
|
+
claudeConfigDir: opts.claudeConfigDir
|
|
1819
2070
|
});
|
|
1820
2071
|
const exists3 = existsSync(resolved.absolute);
|
|
1821
2072
|
let hasMcphEntry = false;
|
|
@@ -1922,9 +2173,9 @@ function shellHistorySources(opts) {
|
|
|
1922
2173
|
}
|
|
1923
2174
|
return sources;
|
|
1924
2175
|
}
|
|
1925
|
-
function readTailLines(
|
|
2176
|
+
function readTailLines(path5, n) {
|
|
1926
2177
|
try {
|
|
1927
|
-
const raw = readFileSync(
|
|
2178
|
+
const raw = readFileSync(path5, "utf8");
|
|
1928
2179
|
const all = raw.split(/\r?\n/);
|
|
1929
2180
|
return all.length <= n ? all : all.slice(all.length - n);
|
|
1930
2181
|
} catch {
|
|
@@ -2025,9 +2276,8 @@ function closestNames(query, candidates, limit) {
|
|
|
2025
2276
|
|
|
2026
2277
|
// src/install-cmd.ts
|
|
2027
2278
|
import { existsSync as existsSync2 } from "fs";
|
|
2028
|
-
import { chmod,
|
|
2279
|
+
import { chmod, readFile as readFile4 } from "fs/promises";
|
|
2029
2280
|
import { homedir as homedir5 } from "os";
|
|
2030
|
-
import { dirname as dirname2 } from "path";
|
|
2031
2281
|
import { join as join5, resolve as resolve2 } from "path";
|
|
2032
2282
|
import { createInterface } from "readline/promises";
|
|
2033
2283
|
var USAGE = "Usage: mcph install <claude-code|claude-desktop|cursor|vscode> [--scope user|project|local]\n [--token <mcp_pat_\u2026>] [--project-dir <path>] [--os macos|linux|windows]\n [--force | --skip] [--dry-run] [--no-mcph-config]\n mcph install --list (detect clients; no writes)\n mcph install --all [--token <mcp_pat_\u2026>] (install into every detected client)";
|
|
@@ -2089,7 +2339,8 @@ ${USAGE}`);
|
|
|
2089
2339
|
scope,
|
|
2090
2340
|
os,
|
|
2091
2341
|
home: opts.home,
|
|
2092
|
-
projectDir
|
|
2342
|
+
projectDir,
|
|
2343
|
+
claudeConfigDir: opts.claudeConfigDir
|
|
2093
2344
|
});
|
|
2094
2345
|
} catch (e) {
|
|
2095
2346
|
err(`mcph install: ${e.message}`);
|
|
@@ -2097,12 +2348,12 @@ ${USAGE}`);
|
|
|
2097
2348
|
}
|
|
2098
2349
|
log2(`Target: ${target.label} (${scope})`);
|
|
2099
2350
|
log2(`File: ${resolved.absolute}`);
|
|
2100
|
-
let
|
|
2101
|
-
if (!
|
|
2351
|
+
let token5 = opts.token ?? null;
|
|
2352
|
+
if (!token5) {
|
|
2102
2353
|
const cfg = await loadMcphConfig({ home: opts.home, cwd: process.cwd(), env: {} });
|
|
2103
|
-
|
|
2354
|
+
token5 = cfg.token;
|
|
2104
2355
|
}
|
|
2105
|
-
if (!
|
|
2356
|
+
if (!token5) {
|
|
2106
2357
|
err(
|
|
2107
2358
|
"\nmcph install: no token available.\n Pass one with --token mcp_pat_\u2026, or run `mcph install` with --token once to seed ~/.mcph/config.json,\n or create the token at https://mcp.hosting \u2192 Settings \u2192 API Tokens."
|
|
2108
2359
|
);
|
|
@@ -2172,40 +2423,39 @@ ${USAGE}`);
|
|
|
2172
2423
|
const writeMcphConfig = !opts.skipMcphConfig;
|
|
2173
2424
|
const home = opts.home ?? homedir5();
|
|
2174
2425
|
const mcphConfigPath = join5(home, CONFIG_DIRNAME, CONFIG_FILENAME);
|
|
2175
|
-
const
|
|
2426
|
+
const mcphConfigComposed = await composeMcphConfig(mcphConfigPath, token5);
|
|
2427
|
+
if (mcphConfigComposed.backupPath) {
|
|
2428
|
+
log2(
|
|
2429
|
+
`mcph install: existing ${mcphConfigPath} was malformed; original bytes backed up to ${mcphConfigComposed.backupPath} before overwriting.`
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
2432
|
+
const mcphConfigJson = mcphConfigComposed.json;
|
|
2176
2433
|
const settingsPatch = opts.clientId === "claude-code" ? await prepareClaudeCodeSettingsPatch({
|
|
2177
2434
|
scope,
|
|
2178
2435
|
home,
|
|
2179
2436
|
projectDir,
|
|
2180
|
-
os
|
|
2437
|
+
os,
|
|
2438
|
+
claudeConfigDir: opts.claudeConfigDir
|
|
2181
2439
|
}) : null;
|
|
2182
2440
|
if (opts.dryRun) {
|
|
2183
2441
|
log2("\n--- dry run: would write the following ---");
|
|
2442
|
+
if (writeMcphConfig) log2(`# ${mcphConfigPath}
|
|
2443
|
+
${mcphConfigJson}`);
|
|
2184
2444
|
log2(`
|
|
2185
2445
|
# ${resolved.absolute}
|
|
2186
2446
|
${clientJson}`);
|
|
2187
|
-
if (writeMcphConfig) log2(`# ${mcphConfigPath}
|
|
2188
|
-
${mcphConfigJson}`);
|
|
2189
2447
|
if (settingsPatch?.changed) log2(`# ${settingsPatch.path}
|
|
2190
2448
|
${settingsPatch.nextJson}`);
|
|
2191
|
-
const wouldWrite = [
|
|
2449
|
+
const wouldWrite = [];
|
|
2192
2450
|
if (writeMcphConfig) wouldWrite.push(mcphConfigPath);
|
|
2451
|
+
wouldWrite.push(resolved.absolute);
|
|
2193
2452
|
if (settingsPatch?.changed) wouldWrite.push(settingsPatch.path);
|
|
2194
2453
|
return { written: [], wouldWrite, messages, exitCode: 0 };
|
|
2195
2454
|
}
|
|
2196
|
-
|
|
2197
|
-
await mkdir3(dirname2(resolved.absolute), { recursive: true });
|
|
2198
|
-
await writeFile2(resolved.absolute, clientJson, "utf8");
|
|
2199
|
-
} catch (e) {
|
|
2200
|
-
err(`mcph install: failed to write ${resolved.absolute}: ${e.message}`);
|
|
2201
|
-
return { written: [], wouldWrite: [], messages, exitCode: 1 };
|
|
2202
|
-
}
|
|
2203
|
-
log2(`Wrote ${resolved.absolute}`);
|
|
2204
|
-
const written = [resolved.absolute];
|
|
2455
|
+
const written = [];
|
|
2205
2456
|
if (writeMcphConfig) {
|
|
2206
2457
|
try {
|
|
2207
|
-
await
|
|
2208
|
-
await writeFile2(mcphConfigPath, mcphConfigJson, "utf8");
|
|
2458
|
+
await atomicWriteFile(mcphConfigPath, mcphConfigJson);
|
|
2209
2459
|
if (process.platform !== "win32") {
|
|
2210
2460
|
try {
|
|
2211
2461
|
await chmod(mcphConfigPath, 384);
|
|
@@ -2214,15 +2464,22 @@ ${settingsPatch.nextJson}`);
|
|
|
2214
2464
|
}
|
|
2215
2465
|
} catch (e) {
|
|
2216
2466
|
err(`mcph install: failed to write ${mcphConfigPath}: ${e.message}`);
|
|
2217
|
-
return { written, wouldWrite: [], messages, exitCode: 1 };
|
|
2467
|
+
return { written: [], wouldWrite: [], messages, exitCode: 1 };
|
|
2218
2468
|
}
|
|
2219
2469
|
log2(`Wrote ${mcphConfigPath}`);
|
|
2220
2470
|
written.push(mcphConfigPath);
|
|
2221
2471
|
}
|
|
2472
|
+
try {
|
|
2473
|
+
await atomicWriteFile(resolved.absolute, clientJson);
|
|
2474
|
+
} catch (e) {
|
|
2475
|
+
err(`mcph install: failed to write ${resolved.absolute}: ${e.message}`);
|
|
2476
|
+
return { written, wouldWrite: [], messages, exitCode: 1 };
|
|
2477
|
+
}
|
|
2478
|
+
log2(`Wrote ${resolved.absolute}`);
|
|
2479
|
+
written.push(resolved.absolute);
|
|
2222
2480
|
if (settingsPatch?.changed) {
|
|
2223
2481
|
try {
|
|
2224
|
-
await
|
|
2225
|
-
await writeFile2(settingsPatch.path, settingsPatch.nextJson, "utf8");
|
|
2482
|
+
await atomicWriteFile(settingsPatch.path, settingsPatch.nextJson);
|
|
2226
2483
|
log2(`Wrote ${settingsPatch.path} (added ${CLAUDE_CODE_ALLOW_PATTERN} to permissions.allow)`);
|
|
2227
2484
|
written.push(settingsPatch.path);
|
|
2228
2485
|
} catch (e) {
|
|
@@ -2237,33 +2494,34 @@ ${settingsPatch.nextJson}`);
|
|
|
2237
2494
|
return { written, wouldWrite: [], messages, exitCode: 0 };
|
|
2238
2495
|
}
|
|
2239
2496
|
async function prepareClaudeCodeSettingsPatch(opts) {
|
|
2240
|
-
const
|
|
2497
|
+
const path5 = resolveClaudeCodeSettingsPath(opts.scope, {
|
|
2241
2498
|
home: opts.home,
|
|
2242
2499
|
projectDir: opts.projectDir,
|
|
2243
|
-
os: opts.os
|
|
2500
|
+
os: opts.os,
|
|
2501
|
+
claudeConfigDir: opts.claudeConfigDir
|
|
2244
2502
|
});
|
|
2245
|
-
if (!
|
|
2503
|
+
if (!path5) return null;
|
|
2246
2504
|
let existing = {};
|
|
2247
|
-
if (existsSync2(
|
|
2505
|
+
if (existsSync2(path5)) {
|
|
2248
2506
|
try {
|
|
2249
|
-
const raw = await readFile4(
|
|
2507
|
+
const raw = await readFile4(path5, "utf8");
|
|
2250
2508
|
if (raw.trim().length > 0) {
|
|
2251
2509
|
const parsed = parseJsonc(raw);
|
|
2252
2510
|
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2253
2511
|
existing = parsed;
|
|
2254
2512
|
} else {
|
|
2255
|
-
return { path:
|
|
2513
|
+
return { path: path5, nextJson: "", changed: false };
|
|
2256
2514
|
}
|
|
2257
2515
|
}
|
|
2258
2516
|
} catch {
|
|
2259
|
-
return { path:
|
|
2517
|
+
return { path: path5, nextJson: "", changed: false };
|
|
2260
2518
|
}
|
|
2261
2519
|
}
|
|
2262
2520
|
const merged = mergePermissionsAllow(existing, [CLAUDE_CODE_ALLOW_PATTERN]);
|
|
2263
2521
|
const before = JSON.stringify(existing);
|
|
2264
2522
|
const after = JSON.stringify(merged);
|
|
2265
|
-
if (before === after) return { path:
|
|
2266
|
-
return { path:
|
|
2523
|
+
if (before === after) return { path: path5, nextJson: "", changed: false };
|
|
2524
|
+
return { path: path5, nextJson: `${JSON.stringify(merged, null, 2)}
|
|
2267
2525
|
`, changed: true };
|
|
2268
2526
|
}
|
|
2269
2527
|
function mergePermissionsAllow(existing, patterns) {
|
|
@@ -2279,13 +2537,13 @@ function mergePermissionsAllow(existing, patterns) {
|
|
|
2279
2537
|
out.permissions = perms;
|
|
2280
2538
|
return out;
|
|
2281
2539
|
}
|
|
2282
|
-
async function promptCollision(
|
|
2540
|
+
async function promptCollision(path5, io) {
|
|
2283
2541
|
const stdin = io?.stdin ?? process.stdin;
|
|
2284
2542
|
const stdout = io?.stdout ?? process.stdout;
|
|
2285
2543
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
2286
2544
|
try {
|
|
2287
2545
|
const answer = (await rl.question(
|
|
2288
|
-
`${
|
|
2546
|
+
`${path5} already has an "${ENTRY_NAME}" entry.
|
|
2289
2547
|
[o]verwrite, [s]kip, or [a]bort? (default: skip) `
|
|
2290
2548
|
)).trim().toLowerCase();
|
|
2291
2549
|
if (answer.startsWith("o")) return "overwrite";
|
|
@@ -2321,23 +2579,37 @@ function mergeClientConfig(existing, containerPath, entry) {
|
|
|
2321
2579
|
parent[leafKey] = container;
|
|
2322
2580
|
return out;
|
|
2323
2581
|
}
|
|
2324
|
-
async function composeMcphConfig(
|
|
2582
|
+
async function composeMcphConfig(path5, token5) {
|
|
2325
2583
|
let existing = {};
|
|
2326
|
-
|
|
2584
|
+
let backupPath;
|
|
2585
|
+
if (existsSync2(path5)) {
|
|
2586
|
+
let raw = "";
|
|
2327
2587
|
try {
|
|
2328
|
-
|
|
2329
|
-
const parsed = parseJsonc(raw);
|
|
2330
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2331
|
-
existing = parsed;
|
|
2332
|
-
}
|
|
2588
|
+
raw = await readFile4(path5, "utf8");
|
|
2333
2589
|
} catch {
|
|
2590
|
+
raw = "";
|
|
2591
|
+
}
|
|
2592
|
+
if (raw) {
|
|
2593
|
+
try {
|
|
2594
|
+
const parsed = parseJsonc(raw);
|
|
2595
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
2596
|
+
existing = parsed;
|
|
2597
|
+
}
|
|
2598
|
+
} catch {
|
|
2599
|
+
const candidate = `${path5}.bak-${Date.now()}`;
|
|
2600
|
+
try {
|
|
2601
|
+
await atomicWriteFile(candidate, raw);
|
|
2602
|
+
backupPath = candidate;
|
|
2603
|
+
} catch {
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2334
2606
|
}
|
|
2335
2607
|
}
|
|
2336
2608
|
const next = { version: CURRENT_SCHEMA_VERSION, ...existing };
|
|
2337
|
-
next.token =
|
|
2609
|
+
next.token = token5;
|
|
2338
2610
|
if (typeof next.version !== "number") next.version = CURRENT_SCHEMA_VERSION;
|
|
2339
|
-
return `${JSON.stringify(next, null, 2)}
|
|
2340
|
-
|
|
2611
|
+
return { json: `${JSON.stringify(next, null, 2)}
|
|
2612
|
+
`, backupPath };
|
|
2341
2613
|
}
|
|
2342
2614
|
function parseInstallArgs(argv) {
|
|
2343
2615
|
if (argv.length === 0) return { ok: false, error: USAGE };
|
|
@@ -2427,7 +2699,7 @@ async function runInstallList(opts, log2) {
|
|
|
2427
2699
|
const home = opts.home ?? homedir5();
|
|
2428
2700
|
const cwd = opts.cwd ?? process.cwd();
|
|
2429
2701
|
const os = opts.os ?? CURRENT_OS;
|
|
2430
|
-
const probes = await probeClientsAsync({ home, os, cwd });
|
|
2702
|
+
const probes = await probeClientsAsync({ home, os, cwd, claudeConfigDir: opts.claudeConfigDir });
|
|
2431
2703
|
const rows = probes.map((p) => ({
|
|
2432
2704
|
client: INSTALL_TARGETS.find((t) => t.clientId === p.clientId)?.label ?? p.clientId,
|
|
2433
2705
|
scope: p.scope,
|
|
@@ -2546,9 +2818,29 @@ async function runInstallAll(opts, log2, err) {
|
|
|
2546
2818
|
}
|
|
2547
2819
|
|
|
2548
2820
|
// src/reset-learning-cmd.ts
|
|
2549
|
-
import { unlink } from "fs/promises";
|
|
2821
|
+
import { unlink as unlink2 } from "fs/promises";
|
|
2550
2822
|
import { homedir as homedir6 } from "os";
|
|
2551
2823
|
import { join as join6 } from "path";
|
|
2824
|
+
var RESET_LEARNING_USAGE = `Usage: mcph reset-learning
|
|
2825
|
+
|
|
2826
|
+
Delete ~/.mcph/state.json so cross-session learning starts fresh.
|
|
2827
|
+
Use this after fixing the root cause of a flaky upstream (token
|
|
2828
|
+
rotated, account swapped, server replaced) so the routing penalty
|
|
2829
|
+
doesn't keep suppressing it.
|
|
2830
|
+
|
|
2831
|
+
-h, --help Show this help.`;
|
|
2832
|
+
function parseResetLearningArgs(argv) {
|
|
2833
|
+
for (const arg of argv) {
|
|
2834
|
+
if (arg === "-h" || arg === "--help") return { kind: "help" };
|
|
2835
|
+
return {
|
|
2836
|
+
kind: "error",
|
|
2837
|
+
error: `mcph reset-learning: unknown argument "${arg}"
|
|
2838
|
+
|
|
2839
|
+
${RESET_LEARNING_USAGE}`
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
return { kind: "ok", options: {} };
|
|
2843
|
+
}
|
|
2552
2844
|
async function runResetLearning(opts = {}) {
|
|
2553
2845
|
const home = opts.home ?? homedir6();
|
|
2554
2846
|
const env = opts.env ?? process.env;
|
|
@@ -2576,7 +2868,7 @@ async function runResetLearning(opts = {}) {
|
|
|
2576
2868
|
const learningCount = Object.keys(persisted.learning).length;
|
|
2577
2869
|
const packCount = persisted.packHistory.length;
|
|
2578
2870
|
try {
|
|
2579
|
-
await
|
|
2871
|
+
await unlink2(filePath);
|
|
2580
2872
|
} catch (err) {
|
|
2581
2873
|
if (isFileNotFound2(err)) {
|
|
2582
2874
|
print("mcph reset-learning: no persisted state to reset.");
|
|
@@ -2611,113 +2903,7 @@ import {
|
|
|
2611
2903
|
ListToolsRequestSchema,
|
|
2612
2904
|
ReadResourceRequestSchema
|
|
2613
2905
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
2614
|
-
import { request as
|
|
2615
|
-
|
|
2616
|
-
// src/analytics.ts
|
|
2617
|
-
import { request as request3 } from "undici";
|
|
2618
|
-
var FLUSH_INTERVAL = 3e4;
|
|
2619
|
-
var FLUSH_SIZE = 50;
|
|
2620
|
-
var MAX_BUFFER = 5e3;
|
|
2621
|
-
var buffer = [];
|
|
2622
|
-
var dispatchBuffer = [];
|
|
2623
|
-
var flushTimer = null;
|
|
2624
|
-
var apiUrl = "";
|
|
2625
|
-
var token = "";
|
|
2626
|
-
function recordConnectEvent(event) {
|
|
2627
|
-
if (buffer.length >= MAX_BUFFER) return;
|
|
2628
|
-
buffer.push({ ...event, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2629
|
-
if (buffer.length >= FLUSH_SIZE) {
|
|
2630
|
-
flush().catch(() => {
|
|
2631
|
-
});
|
|
2632
|
-
}
|
|
2633
|
-
}
|
|
2634
|
-
function recordDispatchEvent(event) {
|
|
2635
|
-
if (dispatchBuffer.length >= MAX_BUFFER) return;
|
|
2636
|
-
dispatchBuffer.push(event);
|
|
2637
|
-
if (dispatchBuffer.length >= FLUSH_SIZE) {
|
|
2638
|
-
flushDispatch().catch(() => {
|
|
2639
|
-
});
|
|
2640
|
-
}
|
|
2641
|
-
}
|
|
2642
|
-
async function flush() {
|
|
2643
|
-
if (buffer.length === 0 || !apiUrl || !token) return;
|
|
2644
|
-
const events = buffer.splice(0, FLUSH_SIZE);
|
|
2645
|
-
try {
|
|
2646
|
-
const res = await request3(`${apiUrl.replace(/\/$/, "")}/api/connect/analytics`, {
|
|
2647
|
-
method: "POST",
|
|
2648
|
-
headers: {
|
|
2649
|
-
Authorization: `Bearer ${token}`,
|
|
2650
|
-
"Content-Type": "application/json"
|
|
2651
|
-
},
|
|
2652
|
-
body: JSON.stringify({ events }),
|
|
2653
|
-
headersTimeout: 1e4,
|
|
2654
|
-
bodyTimeout: 1e4
|
|
2655
|
-
});
|
|
2656
|
-
if (res.statusCode >= 400) {
|
|
2657
|
-
const room = MAX_BUFFER - buffer.length;
|
|
2658
|
-
if (room > 0) buffer.push(...events.slice(0, room));
|
|
2659
|
-
log("warn", "Analytics flush failed", { status: res.statusCode });
|
|
2660
|
-
}
|
|
2661
|
-
await res.body.text().catch(() => {
|
|
2662
|
-
});
|
|
2663
|
-
} catch (err) {
|
|
2664
|
-
const room = MAX_BUFFER - buffer.length;
|
|
2665
|
-
if (room > 0) buffer.push(...events.slice(0, room));
|
|
2666
|
-
log("warn", "Analytics flush error", { error: err.message });
|
|
2667
|
-
}
|
|
2668
|
-
}
|
|
2669
|
-
async function flushDispatch() {
|
|
2670
|
-
if (dispatchBuffer.length === 0 || !apiUrl || !token) return;
|
|
2671
|
-
const events = dispatchBuffer.splice(0, FLUSH_SIZE);
|
|
2672
|
-
try {
|
|
2673
|
-
const res = await request3(`${apiUrl.replace(/\/$/, "")}/api/connect/dispatch-events`, {
|
|
2674
|
-
method: "POST",
|
|
2675
|
-
headers: {
|
|
2676
|
-
Authorization: `Bearer ${token}`,
|
|
2677
|
-
"Content-Type": "application/json"
|
|
2678
|
-
},
|
|
2679
|
-
body: JSON.stringify({ events }),
|
|
2680
|
-
headersTimeout: 1e4,
|
|
2681
|
-
bodyTimeout: 1e4
|
|
2682
|
-
});
|
|
2683
|
-
if (res.statusCode >= 400 && res.statusCode !== 204) {
|
|
2684
|
-
const room = MAX_BUFFER - dispatchBuffer.length;
|
|
2685
|
-
if (room > 0) dispatchBuffer.push(...events.slice(0, room));
|
|
2686
|
-
log("warn", "Dispatch-events flush failed", { status: res.statusCode });
|
|
2687
|
-
}
|
|
2688
|
-
await res.body.text().catch(() => {
|
|
2689
|
-
});
|
|
2690
|
-
} catch (err) {
|
|
2691
|
-
const room = MAX_BUFFER - dispatchBuffer.length;
|
|
2692
|
-
if (room > 0) dispatchBuffer.push(...events.slice(0, room));
|
|
2693
|
-
log("warn", "Dispatch-events flush error", { error: err.message });
|
|
2694
|
-
}
|
|
2695
|
-
}
|
|
2696
|
-
function initAnalytics(url, tok) {
|
|
2697
|
-
apiUrl = url;
|
|
2698
|
-
token = tok;
|
|
2699
|
-
flushTimer = setInterval(() => {
|
|
2700
|
-
flush().catch(() => {
|
|
2701
|
-
});
|
|
2702
|
-
flushDispatch().catch(() => {
|
|
2703
|
-
});
|
|
2704
|
-
}, FLUSH_INTERVAL);
|
|
2705
|
-
if (flushTimer.unref) flushTimer.unref();
|
|
2706
|
-
}
|
|
2707
|
-
async function shutdownAnalytics() {
|
|
2708
|
-
if (flushTimer) {
|
|
2709
|
-
clearInterval(flushTimer);
|
|
2710
|
-
flushTimer = null;
|
|
2711
|
-
}
|
|
2712
|
-
for (let i = 0; i < 3 && buffer.length > 0; i++) {
|
|
2713
|
-
await flush();
|
|
2714
|
-
}
|
|
2715
|
-
for (let i = 0; i < 3 && dispatchBuffer.length > 0; i++) {
|
|
2716
|
-
await flushDispatch();
|
|
2717
|
-
}
|
|
2718
|
-
buffer.length = 0;
|
|
2719
|
-
dispatchBuffer.length = 0;
|
|
2720
|
-
}
|
|
2906
|
+
import { request as request8 } from "undici";
|
|
2721
2907
|
|
|
2722
2908
|
// src/compliance.ts
|
|
2723
2909
|
var GRADE_ORDER = {
|
|
@@ -2727,13 +2913,20 @@ var GRADE_ORDER = {
|
|
|
2727
2913
|
D: 1,
|
|
2728
2914
|
F: 0
|
|
2729
2915
|
};
|
|
2916
|
+
function classifyGrade(grade) {
|
|
2917
|
+
if (grade === void 0 || grade === null) return { kind: "ungraded" };
|
|
2918
|
+
const trimmed = grade.trim();
|
|
2919
|
+
if (trimmed === "") return { kind: "ungraded" };
|
|
2920
|
+
const up = trimmed.toUpperCase();
|
|
2921
|
+
if (up in GRADE_ORDER) return { kind: "graded", rank: GRADE_ORDER[up] };
|
|
2922
|
+
return { kind: "unrecognized", raw: grade };
|
|
2923
|
+
}
|
|
2730
2924
|
function gradeRank(grade) {
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
if (up in GRADE_ORDER) return GRADE_ORDER[up];
|
|
2734
|
-
return -1;
|
|
2925
|
+
const c = classifyGrade(grade);
|
|
2926
|
+
return c.kind === "graded" ? c.rank : -1;
|
|
2735
2927
|
}
|
|
2736
2928
|
var invalidWarned = false;
|
|
2929
|
+
var unrecognizedServerWarned = /* @__PURE__ */ new Set();
|
|
2737
2930
|
function parseMinCompliance(raw) {
|
|
2738
2931
|
if (raw === void 0) return null;
|
|
2739
2932
|
const trimmed = raw.trim();
|
|
@@ -2750,9 +2943,19 @@ function parseMinCompliance(raw) {
|
|
|
2750
2943
|
}
|
|
2751
2944
|
function passesMinCompliance(serverGrade, min) {
|
|
2752
2945
|
if (min === null) return true;
|
|
2753
|
-
const
|
|
2754
|
-
if (
|
|
2755
|
-
|
|
2946
|
+
const c = classifyGrade(serverGrade);
|
|
2947
|
+
if (c.kind === "ungraded") return true;
|
|
2948
|
+
if (c.kind === "unrecognized") {
|
|
2949
|
+
if (!unrecognizedServerWarned.has(c.raw)) {
|
|
2950
|
+
unrecognizedServerWarned.add(c.raw);
|
|
2951
|
+
log("warn", "Unrecognized server compliance grade; failing closed under MCPH_MIN_COMPLIANCE", {
|
|
2952
|
+
grade: c.raw,
|
|
2953
|
+
min
|
|
2954
|
+
});
|
|
2955
|
+
}
|
|
2956
|
+
return false;
|
|
2957
|
+
}
|
|
2958
|
+
return c.rank >= gradeRank(min);
|
|
2756
2959
|
}
|
|
2757
2960
|
|
|
2758
2961
|
// src/cost-estimate.ts
|
|
@@ -3023,18 +3226,18 @@ function stepBindingKey(step, index) {
|
|
|
3023
3226
|
// src/guide.ts
|
|
3024
3227
|
import { readFile as readFile5 } from "fs/promises";
|
|
3025
3228
|
var GUIDE_READ_TIMEOUT_MS = 1e3;
|
|
3026
|
-
async function readGuide(
|
|
3229
|
+
async function readGuide(path5, scope) {
|
|
3027
3230
|
let raw;
|
|
3028
3231
|
try {
|
|
3029
3232
|
raw = await Promise.race([
|
|
3030
|
-
readFile5(
|
|
3233
|
+
readFile5(path5, "utf8"),
|
|
3031
3234
|
new Promise(
|
|
3032
3235
|
(_, reject) => setTimeout(() => reject(new Error("guide read timeout")), GUIDE_READ_TIMEOUT_MS)
|
|
3033
3236
|
)
|
|
3034
3237
|
]);
|
|
3035
3238
|
} catch (err) {
|
|
3036
3239
|
if (err instanceof Error && err.message === "guide read timeout") {
|
|
3037
|
-
log("warn", "Guide read timed out", { path:
|
|
3240
|
+
log("warn", "Guide read timed out", { path: path5 });
|
|
3038
3241
|
}
|
|
3039
3242
|
return null;
|
|
3040
3243
|
}
|
|
@@ -3042,7 +3245,7 @@ async function readGuide(path4, scope) {
|
|
|
3042
3245
|
if (content.length === 0) {
|
|
3043
3246
|
return null;
|
|
3044
3247
|
}
|
|
3045
|
-
return { scope, path:
|
|
3248
|
+
return { scope, path: path5, content };
|
|
3046
3249
|
}
|
|
3047
3250
|
async function loadUserGuide(home) {
|
|
3048
3251
|
const p = guidePath(userConfigDir(home));
|
|
@@ -3741,9 +3944,9 @@ var PackDetector = class {
|
|
|
3741
3944
|
|
|
3742
3945
|
// src/progress.ts
|
|
3743
3946
|
function createProgressReporter(extra) {
|
|
3744
|
-
const
|
|
3947
|
+
const token5 = extra?._meta?.progressToken;
|
|
3745
3948
|
const send = extra?.sendNotification;
|
|
3746
|
-
if (
|
|
3949
|
+
if (token5 === void 0 || token5 === null || !send) {
|
|
3747
3950
|
return () => {
|
|
3748
3951
|
};
|
|
3749
3952
|
}
|
|
@@ -3751,7 +3954,7 @@ function createProgressReporter(extra) {
|
|
|
3751
3954
|
return (message, progress, total) => {
|
|
3752
3955
|
step += 1;
|
|
3753
3956
|
const params = {
|
|
3754
|
-
progressToken:
|
|
3957
|
+
progressToken: token5,
|
|
3755
3958
|
progress: progress ?? step,
|
|
3756
3959
|
message
|
|
3757
3960
|
};
|
|
@@ -3822,6 +4025,14 @@ function buildToolRoutes(activeConnections, inactiveWithCache = []) {
|
|
|
3822
4025
|
const routes = /* @__PURE__ */ new Map();
|
|
3823
4026
|
for (const conn of activeConnections.values()) {
|
|
3824
4027
|
for (const tool of conn.tools) {
|
|
4028
|
+
const existing = routes.get(tool.namespacedName);
|
|
4029
|
+
if (existing && existing.namespace !== conn.config.namespace) {
|
|
4030
|
+
log("warn", "Tool route collision; later upstream shadows earlier", {
|
|
4031
|
+
tool: tool.namespacedName,
|
|
4032
|
+
shadowedNamespace: existing.namespace,
|
|
4033
|
+
winningNamespace: conn.config.namespace
|
|
4034
|
+
});
|
|
4035
|
+
}
|
|
3825
4036
|
routes.set(tool.namespacedName, {
|
|
3826
4037
|
namespace: conn.config.namespace,
|
|
3827
4038
|
originalName: tool.name
|
|
@@ -3969,9 +4180,12 @@ async function routeToolCall(toolName, args, toolRoutes, activeConnections) {
|
|
|
3969
4180
|
});
|
|
3970
4181
|
return result;
|
|
3971
4182
|
} catch (err) {
|
|
3972
|
-
|
|
4183
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4184
|
+
const code = err && typeof err === "object" && "code" in err && typeof err.code === "number" ? err.code : void 0;
|
|
4185
|
+
log("error", "Tool call failed", { tool: toolName, namespace: route.namespace, error: message, code });
|
|
4186
|
+
const codeTag = code !== void 0 ? ` [code=${code}]` : "";
|
|
3973
4187
|
return {
|
|
3974
|
-
content: [{ type: "text", text: `Error calling ${toolName}: ${
|
|
4188
|
+
content: [{ type: "text", text: `Error calling ${toolName}${codeTag}: ${message}` }],
|
|
3975
4189
|
isError: true
|
|
3976
4190
|
};
|
|
3977
4191
|
}
|
|
@@ -4194,26 +4408,26 @@ function rankServers(context, servers) {
|
|
|
4194
4408
|
}
|
|
4195
4409
|
|
|
4196
4410
|
// src/rerank.ts
|
|
4197
|
-
import { request as
|
|
4198
|
-
var
|
|
4199
|
-
var
|
|
4411
|
+
import { request as request5 } from "undici";
|
|
4412
|
+
var apiUrl3 = "";
|
|
4413
|
+
var token3 = "";
|
|
4200
4414
|
var RERANK_TIMEOUT_MS = 2e3;
|
|
4201
4415
|
function initRerank(url, tok) {
|
|
4202
|
-
|
|
4203
|
-
|
|
4416
|
+
apiUrl3 = url;
|
|
4417
|
+
token3 = tok;
|
|
4204
4418
|
}
|
|
4205
4419
|
async function rerank(intent, candidateIds, limit) {
|
|
4206
|
-
if (!
|
|
4420
|
+
if (!apiUrl3 || !token3) return null;
|
|
4207
4421
|
if (!intent?.trim()) return null;
|
|
4208
4422
|
if (candidateIds !== void 0 && candidateIds.length === 0) return null;
|
|
4209
4423
|
const payload = { intent: intent.trim() };
|
|
4210
4424
|
if (candidateIds && candidateIds.length > 0) payload.candidateIds = candidateIds;
|
|
4211
4425
|
if (typeof limit === "number" && limit > 0) payload.limit = limit;
|
|
4212
4426
|
try {
|
|
4213
|
-
const res = await
|
|
4427
|
+
const res = await request5(`${apiUrl3.replace(/\/$/, "")}/api/connect/rerank`, {
|
|
4214
4428
|
method: "POST",
|
|
4215
4429
|
headers: {
|
|
4216
|
-
Authorization: `Bearer ${
|
|
4430
|
+
Authorization: `Bearer ${token3}`,
|
|
4217
4431
|
"Content-Type": "application/json"
|
|
4218
4432
|
},
|
|
4219
4433
|
body: JSON.stringify(payload),
|
|
@@ -4243,14 +4457,14 @@ async function rerank(intent, candidateIds, limit) {
|
|
|
4243
4457
|
|
|
4244
4458
|
// src/runtime-detect.ts
|
|
4245
4459
|
import { spawn as spawn2 } from "child_process";
|
|
4246
|
-
import { request as
|
|
4460
|
+
import { request as request6 } from "undici";
|
|
4247
4461
|
var PROBE_TIMEOUT_MS = 3e3;
|
|
4248
4462
|
var RUNTIME_REPORT_PATH = "/api/connect/runtimes";
|
|
4249
|
-
var
|
|
4250
|
-
var
|
|
4463
|
+
var apiUrl4 = "";
|
|
4464
|
+
var token4 = "";
|
|
4251
4465
|
function initRuntimeDetect(url, tok) {
|
|
4252
|
-
|
|
4253
|
-
|
|
4466
|
+
apiUrl4 = url;
|
|
4467
|
+
token4 = tok;
|
|
4254
4468
|
}
|
|
4255
4469
|
var PROBES = {
|
|
4256
4470
|
node: {
|
|
@@ -4362,7 +4576,7 @@ async function detectRuntimes() {
|
|
|
4362
4576
|
return out;
|
|
4363
4577
|
}
|
|
4364
4578
|
async function reportRuntimes() {
|
|
4365
|
-
if (!
|
|
4579
|
+
if (!apiUrl4 || !token4) return;
|
|
4366
4580
|
let runtimes;
|
|
4367
4581
|
try {
|
|
4368
4582
|
runtimes = await detectRuntimes();
|
|
@@ -4371,10 +4585,10 @@ async function reportRuntimes() {
|
|
|
4371
4585
|
return;
|
|
4372
4586
|
}
|
|
4373
4587
|
try {
|
|
4374
|
-
const res = await
|
|
4588
|
+
const res = await request6(`${apiUrl4.replace(/\/$/, "")}${RUNTIME_REPORT_PATH}`, {
|
|
4375
4589
|
method: "POST",
|
|
4376
4590
|
headers: {
|
|
4377
|
-
Authorization: `Bearer ${
|
|
4591
|
+
Authorization: `Bearer ${token4}`,
|
|
4378
4592
|
"Content-Type": "application/json"
|
|
4379
4593
|
},
|
|
4380
4594
|
body: JSON.stringify({ runtimes }),
|
|
@@ -4419,15 +4633,23 @@ function buildTiebreakPrompt(intent, candidates) {
|
|
|
4419
4633
|
].join("\n");
|
|
4420
4634
|
}
|
|
4421
4635
|
function parseTiebreakResponse(response, candidates) {
|
|
4422
|
-
const namespaces =
|
|
4636
|
+
const namespaces = candidates.map((c) => c.namespace);
|
|
4637
|
+
const namespaceSet = new Set(namespaces);
|
|
4423
4638
|
for (const rawLine of response.split(/\r?\n/)) {
|
|
4424
4639
|
const line = rawLine.trim().replace(/^[`"'*>\-\s]+|[`"'*\s]+$/g, "");
|
|
4425
4640
|
if (!line) continue;
|
|
4426
|
-
if (
|
|
4641
|
+
if (namespaceSet.has(line)) return line;
|
|
4642
|
+
let bestNs = null;
|
|
4643
|
+
let bestPos = Number.POSITIVE_INFINITY;
|
|
4427
4644
|
for (const ns of namespaces) {
|
|
4428
4645
|
const re = new RegExp(`\\b${escapeRegex(ns)}\\b`);
|
|
4429
|
-
|
|
4646
|
+
const match = re.exec(line);
|
|
4647
|
+
if (match && match.index < bestPos) {
|
|
4648
|
+
bestPos = match.index;
|
|
4649
|
+
bestNs = ns;
|
|
4650
|
+
}
|
|
4430
4651
|
}
|
|
4652
|
+
if (bestNs) return bestNs;
|
|
4431
4653
|
}
|
|
4432
4654
|
return null;
|
|
4433
4655
|
}
|
|
@@ -4505,9 +4727,6 @@ function evaluateServerCap(namespace, loaded, cap) {
|
|
|
4505
4727
|
};
|
|
4506
4728
|
}
|
|
4507
4729
|
|
|
4508
|
-
// src/test-runner.ts
|
|
4509
|
-
import { request as request7 } from "undici";
|
|
4510
|
-
|
|
4511
4730
|
// src/upstream.ts
|
|
4512
4731
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
4513
4732
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
@@ -4524,9 +4743,9 @@ import { spawn as spawn3 } from "child_process";
|
|
|
4524
4743
|
import { createHash } from "crypto";
|
|
4525
4744
|
import { createWriteStream } from "fs";
|
|
4526
4745
|
import fs from "fs/promises";
|
|
4527
|
-
import
|
|
4746
|
+
import path4 from "path";
|
|
4528
4747
|
import { pipeline } from "stream/promises";
|
|
4529
|
-
import { request as
|
|
4748
|
+
import { request as request7 } from "undici";
|
|
4530
4749
|
var UV_VERSION = "0.11.7";
|
|
4531
4750
|
var RELEASE_BASE = `https://github.com/astral-sh/uv/releases/download/${UV_VERSION}`;
|
|
4532
4751
|
function uvTarget() {
|
|
@@ -4603,7 +4822,7 @@ async function onPath(cmd) {
|
|
|
4603
4822
|
async function fetchWithRedirects(url, maxHops = 5) {
|
|
4604
4823
|
let current = url;
|
|
4605
4824
|
for (let i = 0; i < maxHops; i++) {
|
|
4606
|
-
const res = await
|
|
4825
|
+
const res = await request7(current, { method: "GET" });
|
|
4607
4826
|
if (res.statusCode >= 300 && res.statusCode < 400) {
|
|
4608
4827
|
const loc = res.headers.location;
|
|
4609
4828
|
if (!loc) throw new Error(`Redirect without Location header from ${current}`);
|
|
@@ -4653,7 +4872,7 @@ function runCommand(cmd, args) {
|
|
|
4653
4872
|
async function findBinary(root, name) {
|
|
4654
4873
|
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
4655
4874
|
for (const e of entries) {
|
|
4656
|
-
const full =
|
|
4875
|
+
const full = path4.join(root, e.name);
|
|
4657
4876
|
if (e.isFile() && e.name === name) return full;
|
|
4658
4877
|
if (e.isDirectory()) {
|
|
4659
4878
|
const found = await findBinary(full, name);
|
|
@@ -4675,8 +4894,8 @@ async function resolveUv() {
|
|
|
4675
4894
|
`No prebuilt uv binary for ${process.platform}/${process.arch}. Install uv manually: https://docs.astral.sh/uv/`
|
|
4676
4895
|
);
|
|
4677
4896
|
}
|
|
4678
|
-
const installDir =
|
|
4679
|
-
const finalBin =
|
|
4897
|
+
const installDir = path4.join(cacheDir(), "uv", UV_VERSION);
|
|
4898
|
+
const finalBin = path4.join(installDir, binName());
|
|
4680
4899
|
if (await exists2(finalBin)) return finalBin;
|
|
4681
4900
|
await fs.mkdir(installDir, { recursive: true });
|
|
4682
4901
|
log("info", "Bootstrapping uv", { version: UV_VERSION, target, cache: installDir });
|
|
@@ -4689,11 +4908,11 @@ async function resolveUv() {
|
|
|
4689
4908
|
if (!expected || expected.toLowerCase() !== actual.toLowerCase()) {
|
|
4690
4909
|
throw new Error(`uv archive checksum mismatch (expected ${expected}, got ${actual})`);
|
|
4691
4910
|
}
|
|
4692
|
-
const archivePath =
|
|
4911
|
+
const archivePath = path4.join(installDir, archiveName);
|
|
4693
4912
|
await pipeline(async function* () {
|
|
4694
4913
|
yield archiveBuf;
|
|
4695
4914
|
}, createWriteStream(archivePath));
|
|
4696
|
-
const extractDir =
|
|
4915
|
+
const extractDir = path4.join(installDir, "extract");
|
|
4697
4916
|
await fs.rm(extractDir, { recursive: true, force: true });
|
|
4698
4917
|
await extractArchive(archivePath, extractDir);
|
|
4699
4918
|
const extracted = await findBinary(extractDir, binName());
|
|
@@ -4722,6 +4941,12 @@ var CONNECT_TIMEOUT = (() => {
|
|
|
4722
4941
|
const n = Number.parseInt(env, 10);
|
|
4723
4942
|
return Number.isFinite(n) && n > 0 ? n : 15e3;
|
|
4724
4943
|
})();
|
|
4944
|
+
var LIST_TIMEOUT = (() => {
|
|
4945
|
+
const env = process.env.MCP_LIST_TIMEOUT;
|
|
4946
|
+
if (!env) return 15e3;
|
|
4947
|
+
const n = Number.parseInt(env, 10);
|
|
4948
|
+
return Number.isFinite(n) && n > 0 ? n : 15e3;
|
|
4949
|
+
})();
|
|
4725
4950
|
var STDERR_RING_CAP = 8 * 1024;
|
|
4726
4951
|
var MAX_TOOLS_PER_SERVER = 1e3;
|
|
4727
4952
|
var MAX_RESOURCES_PER_SERVER = 1e3;
|
|
@@ -4746,7 +4971,7 @@ function categorizeSpawnError(err) {
|
|
|
4746
4971
|
}
|
|
4747
4972
|
async function connectToUpstream(config, onDisconnect, onListChanged) {
|
|
4748
4973
|
const client = new Client(
|
|
4749
|
-
{ name: "mcph", version: true ? "0.47.
|
|
4974
|
+
{ name: "mcph", version: true ? "0.47.3" : "dev" },
|
|
4750
4975
|
{ capabilities: {} }
|
|
4751
4976
|
);
|
|
4752
4977
|
let transport;
|
|
@@ -4911,7 +5136,7 @@ async function disconnectFromUpstream(connection) {
|
|
|
4911
5136
|
}
|
|
4912
5137
|
async function fetchResourcesFromUpstream(client, namespace) {
|
|
4913
5138
|
try {
|
|
4914
|
-
const result = await client.listResources();
|
|
5139
|
+
const result = await client.listResources({}, { timeout: LIST_TIMEOUT });
|
|
4915
5140
|
const raw = result.resources ?? [];
|
|
4916
5141
|
if (raw.length > MAX_RESOURCES_PER_SERVER) {
|
|
4917
5142
|
log("warn", "Upstream returned more resources than cap; truncating", {
|
|
@@ -4933,7 +5158,7 @@ async function fetchResourcesFromUpstream(client, namespace) {
|
|
|
4933
5158
|
}
|
|
4934
5159
|
async function fetchPromptsFromUpstream(client, namespace) {
|
|
4935
5160
|
try {
|
|
4936
|
-
const result = await client.listPrompts();
|
|
5161
|
+
const result = await client.listPrompts({}, { timeout: LIST_TIMEOUT });
|
|
4937
5162
|
const raw = result.prompts ?? [];
|
|
4938
5163
|
if (raw.length > MAX_PROMPTS_PER_SERVER) {
|
|
4939
5164
|
log("warn", "Upstream returned more prompts than cap; truncating", {
|
|
@@ -4953,7 +5178,7 @@ async function fetchPromptsFromUpstream(client, namespace) {
|
|
|
4953
5178
|
}
|
|
4954
5179
|
}
|
|
4955
5180
|
async function fetchToolsFromUpstream(client, namespace) {
|
|
4956
|
-
const result = await client.listTools();
|
|
5181
|
+
const result = await client.listTools({}, { timeout: LIST_TIMEOUT });
|
|
4957
5182
|
const raw = result.tools ?? [];
|
|
4958
5183
|
if (raw.length > MAX_TOOLS_PER_SERVER) {
|
|
4959
5184
|
log("warn", "Upstream returned more tools than cap; truncating", {
|
|
@@ -4971,179 +5196,6 @@ async function fetchToolsFromUpstream(client, namespace) {
|
|
|
4971
5196
|
}));
|
|
4972
5197
|
}
|
|
4973
5198
|
|
|
4974
|
-
// src/test-runner.ts
|
|
4975
|
-
var POLL_INTERVAL_MS = 3e4;
|
|
4976
|
-
var REQUEST_TIMEOUT_MS = 1e4;
|
|
4977
|
-
var apiUrl4 = "";
|
|
4978
|
-
var token4 = "";
|
|
4979
|
-
var pollTimer = null;
|
|
4980
|
-
var running = false;
|
|
4981
|
-
var configRef = () => null;
|
|
4982
|
-
function initTestRunner(url, tok, getConfig) {
|
|
4983
|
-
apiUrl4 = url;
|
|
4984
|
-
token4 = tok;
|
|
4985
|
-
configRef = getConfig;
|
|
4986
|
-
}
|
|
4987
|
-
function startTestRunner() {
|
|
4988
|
-
if (running) return;
|
|
4989
|
-
running = true;
|
|
4990
|
-
schedule();
|
|
4991
|
-
}
|
|
4992
|
-
function stopTestRunner() {
|
|
4993
|
-
running = false;
|
|
4994
|
-
if (pollTimer) {
|
|
4995
|
-
clearTimeout(pollTimer);
|
|
4996
|
-
pollTimer = null;
|
|
4997
|
-
}
|
|
4998
|
-
}
|
|
4999
|
-
function schedule() {
|
|
5000
|
-
pollTimer = setTimeout(async () => {
|
|
5001
|
-
try {
|
|
5002
|
-
await pollOnce();
|
|
5003
|
-
} catch (err) {
|
|
5004
|
-
log("warn", "Test runner poll failed", { error: err?.message });
|
|
5005
|
-
}
|
|
5006
|
-
if (running) schedule();
|
|
5007
|
-
}, POLL_INTERVAL_MS);
|
|
5008
|
-
pollTimer.unref?.();
|
|
5009
|
-
}
|
|
5010
|
-
async function pollOnce() {
|
|
5011
|
-
if (!apiUrl4 || !token4) return;
|
|
5012
|
-
const list = await fetchPending();
|
|
5013
|
-
if (list.length === 0) return;
|
|
5014
|
-
for (const pending2 of list) {
|
|
5015
|
-
if (!running) return;
|
|
5016
|
-
await runOne(pending2).catch((err) => {
|
|
5017
|
-
log("warn", "Test execution failed", { requestId: pending2.requestId, error: err?.message });
|
|
5018
|
-
});
|
|
5019
|
-
}
|
|
5020
|
-
}
|
|
5021
|
-
async function fetchPending() {
|
|
5022
|
-
try {
|
|
5023
|
-
const res = await request7(`${apiUrl4.replace(/\/$/, "")}/api/connect/test-requests`, {
|
|
5024
|
-
method: "GET",
|
|
5025
|
-
headers: { Authorization: `Bearer ${token4}` },
|
|
5026
|
-
headersTimeout: REQUEST_TIMEOUT_MS,
|
|
5027
|
-
bodyTimeout: REQUEST_TIMEOUT_MS
|
|
5028
|
-
});
|
|
5029
|
-
if (res.statusCode === 404) {
|
|
5030
|
-
await res.body.text().catch(() => {
|
|
5031
|
-
});
|
|
5032
|
-
stopTestRunner();
|
|
5033
|
-
return [];
|
|
5034
|
-
}
|
|
5035
|
-
if (res.statusCode !== 200) {
|
|
5036
|
-
await res.body.text().catch(() => {
|
|
5037
|
-
});
|
|
5038
|
-
return [];
|
|
5039
|
-
}
|
|
5040
|
-
const body = await res.body.json();
|
|
5041
|
-
return Array.isArray(body?.requests) ? body.requests : [];
|
|
5042
|
-
} catch {
|
|
5043
|
-
return [];
|
|
5044
|
-
}
|
|
5045
|
-
}
|
|
5046
|
-
async function runOne(pending2) {
|
|
5047
|
-
const config = configRef();
|
|
5048
|
-
const serverConfig = config?.servers.find((s) => s.id === pending2.serverId);
|
|
5049
|
-
if (!serverConfig) {
|
|
5050
|
-
await postResult(pending2.requestId, {
|
|
5051
|
-
status: "failed",
|
|
5052
|
-
message: `Server "${pending2.serverId}" is not in this mcph's current config \u2014 restart mcph or refresh the dashboard.`,
|
|
5053
|
-
errorCategory: "not_in_config"
|
|
5054
|
-
});
|
|
5055
|
-
return;
|
|
5056
|
-
}
|
|
5057
|
-
if (!serverConfig.isActive) {
|
|
5058
|
-
await postResult(pending2.requestId, {
|
|
5059
|
-
status: "failed",
|
|
5060
|
-
message: `Server "${serverConfig.namespace}" is disabled in the dashboard \u2014 re-enable it before testing.`,
|
|
5061
|
-
errorCategory: "disabled"
|
|
5062
|
-
});
|
|
5063
|
-
return;
|
|
5064
|
-
}
|
|
5065
|
-
let connection = null;
|
|
5066
|
-
try {
|
|
5067
|
-
connection = await connectToUpstream(serverConfig);
|
|
5068
|
-
await postResult(pending2.requestId, {
|
|
5069
|
-
status: "passed",
|
|
5070
|
-
toolCount: connection.tools.length,
|
|
5071
|
-
message: `Connected \u2014 ${connection.tools.length} tool${connection.tools.length === 1 ? "" : "s"} available.`
|
|
5072
|
-
});
|
|
5073
|
-
} catch (err) {
|
|
5074
|
-
if (err instanceof ActivationError) {
|
|
5075
|
-
await postResult(pending2.requestId, {
|
|
5076
|
-
status: "failed",
|
|
5077
|
-
message: err.message,
|
|
5078
|
-
errorCategory: err.category
|
|
5079
|
-
});
|
|
5080
|
-
} else {
|
|
5081
|
-
await postResult(pending2.requestId, {
|
|
5082
|
-
status: "failed",
|
|
5083
|
-
message: err instanceof Error ? err.message : String(err),
|
|
5084
|
-
errorCategory: "unknown"
|
|
5085
|
-
});
|
|
5086
|
-
}
|
|
5087
|
-
} finally {
|
|
5088
|
-
if (connection) {
|
|
5089
|
-
await disconnectFromUpstream(connection).catch(() => {
|
|
5090
|
-
});
|
|
5091
|
-
}
|
|
5092
|
-
}
|
|
5093
|
-
}
|
|
5094
|
-
async function postResult(requestId, result) {
|
|
5095
|
-
try {
|
|
5096
|
-
const res = await request7(
|
|
5097
|
-
`${apiUrl4.replace(/\/$/, "")}/api/connect/test-requests/${encodeURIComponent(requestId)}/result`,
|
|
5098
|
-
{
|
|
5099
|
-
method: "POST",
|
|
5100
|
-
headers: {
|
|
5101
|
-
Authorization: `Bearer ${token4}`,
|
|
5102
|
-
"Content-Type": "application/json"
|
|
5103
|
-
},
|
|
5104
|
-
body: JSON.stringify(result),
|
|
5105
|
-
headersTimeout: REQUEST_TIMEOUT_MS,
|
|
5106
|
-
bodyTimeout: REQUEST_TIMEOUT_MS
|
|
5107
|
-
}
|
|
5108
|
-
);
|
|
5109
|
-
await res.body.text().catch(() => {
|
|
5110
|
-
});
|
|
5111
|
-
} catch (err) {
|
|
5112
|
-
log("warn", "Posting test result failed", { requestId, error: err?.message });
|
|
5113
|
-
}
|
|
5114
|
-
}
|
|
5115
|
-
|
|
5116
|
-
// src/tool-report.ts
|
|
5117
|
-
import { request as request8 } from "undici";
|
|
5118
|
-
var apiUrl5 = "";
|
|
5119
|
-
var token5 = "";
|
|
5120
|
-
function initToolReport(url, tok) {
|
|
5121
|
-
apiUrl5 = url;
|
|
5122
|
-
token5 = tok;
|
|
5123
|
-
}
|
|
5124
|
-
async function reportTools(serverId, tools) {
|
|
5125
|
-
if (!apiUrl5 || !token5 || !serverId) return;
|
|
5126
|
-
try {
|
|
5127
|
-
const res = await request8(`${apiUrl5.replace(/\/$/, "")}/api/connect/servers/${serverId}/tools`, {
|
|
5128
|
-
method: "POST",
|
|
5129
|
-
headers: {
|
|
5130
|
-
Authorization: `Bearer ${token5}`,
|
|
5131
|
-
"Content-Type": "application/json"
|
|
5132
|
-
},
|
|
5133
|
-
body: JSON.stringify({ tools }),
|
|
5134
|
-
headersTimeout: 1e4,
|
|
5135
|
-
bodyTimeout: 1e4
|
|
5136
|
-
});
|
|
5137
|
-
await res.body.text().catch(() => {
|
|
5138
|
-
});
|
|
5139
|
-
if (res.statusCode >= 400 && res.statusCode !== 404) {
|
|
5140
|
-
log("warn", "Tool report failed", { serverId, status: res.statusCode });
|
|
5141
|
-
}
|
|
5142
|
-
} catch (err) {
|
|
5143
|
-
log("warn", "Tool report error", { serverId, error: err?.message });
|
|
5144
|
-
}
|
|
5145
|
-
}
|
|
5146
|
-
|
|
5147
5199
|
// src/server.ts
|
|
5148
5200
|
var DEFAULT_POLL_INTERVAL_MS = 6e4;
|
|
5149
5201
|
function resolvePollIntervalMs() {
|
|
@@ -5223,11 +5275,11 @@ function computeToolOverlaps(connections) {
|
|
|
5223
5275
|
return overlaps;
|
|
5224
5276
|
}
|
|
5225
5277
|
var ConnectServer = class _ConnectServer {
|
|
5226
|
-
constructor(
|
|
5227
|
-
this.apiUrl =
|
|
5228
|
-
this.token =
|
|
5278
|
+
constructor(apiUrl5, token5) {
|
|
5279
|
+
this.apiUrl = apiUrl5;
|
|
5280
|
+
this.token = token5;
|
|
5229
5281
|
this.server = new Server(
|
|
5230
|
-
{ name: "mcph", version: true ? "0.47.
|
|
5282
|
+
{ name: "mcph", version: true ? "0.47.3" : "dev" },
|
|
5231
5283
|
{
|
|
5232
5284
|
capabilities: {
|
|
5233
5285
|
tools: { listChanged: true },
|
|
@@ -5368,23 +5420,23 @@ var ConnectServer = class _ConnectServer {
|
|
|
5368
5420
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
5369
5421
|
tools: buildToolList(this.connections, this.getDeferredServers(), this.toolFilters)
|
|
5370
5422
|
}));
|
|
5371
|
-
this.server.setRequestHandler(CallToolRequestSchema, async (
|
|
5372
|
-
const { name, arguments: args } =
|
|
5423
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request9, extra) => {
|
|
5424
|
+
const { name, arguments: args } = request9.params;
|
|
5373
5425
|
return this.handleToolCall(name, args ?? {}, extra);
|
|
5374
5426
|
});
|
|
5375
5427
|
this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
5376
5428
|
resources: buildResourceList(this.connections, this.getBuiltinResources())
|
|
5377
5429
|
}));
|
|
5378
|
-
this.server.setRequestHandler(ReadResourceRequestSchema, async (
|
|
5379
|
-
return routeResourceRead(
|
|
5430
|
+
this.server.setRequestHandler(ReadResourceRequestSchema, async (request9) => {
|
|
5431
|
+
return routeResourceRead(request9.params.uri, this.resourceRoutes, this.connections, this.getBuiltinResourceMap());
|
|
5380
5432
|
});
|
|
5381
5433
|
this.server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
5382
5434
|
prompts: buildPromptList(this.connections)
|
|
5383
5435
|
}));
|
|
5384
|
-
this.server.setRequestHandler(GetPromptRequestSchema, async (
|
|
5436
|
+
this.server.setRequestHandler(GetPromptRequestSchema, async (request9) => {
|
|
5385
5437
|
return routePromptGet(
|
|
5386
|
-
|
|
5387
|
-
|
|
5438
|
+
request9.params.name,
|
|
5439
|
+
request9.params.arguments,
|
|
5388
5440
|
this.promptRoutes,
|
|
5389
5441
|
this.connections
|
|
5390
5442
|
);
|
|
@@ -5489,12 +5541,10 @@ var ConnectServer = class _ConnectServer {
|
|
|
5489
5541
|
initToolReport(this.apiUrl, this.token);
|
|
5490
5542
|
initRerank(this.apiUrl, this.token);
|
|
5491
5543
|
initRuntimeDetect(this.apiUrl, this.token);
|
|
5492
|
-
initTestRunner(this.apiUrl, this.token, () => this.config);
|
|
5493
5544
|
reportRuntimes().catch((err) => log("warn", "reportRuntimes failed", { error: err?.message }));
|
|
5494
5545
|
if (this.config?.servers.some((s) => s.command === "uv" || s.command === "uvx")) {
|
|
5495
5546
|
ensureUv().catch((err) => log("warn", "uv prewarm failed", { error: err?.message }));
|
|
5496
5547
|
}
|
|
5497
|
-
startTestRunner();
|
|
5498
5548
|
const transport = new StdioServerTransport();
|
|
5499
5549
|
await this.server.connect(transport);
|
|
5500
5550
|
this.startPolling();
|
|
@@ -5740,7 +5790,10 @@ var ConnectServer = class _ConnectServer {
|
|
|
5740
5790
|
if (serverConfig) {
|
|
5741
5791
|
let reconnected = false;
|
|
5742
5792
|
let lastErr;
|
|
5743
|
-
|
|
5793
|
+
const RECONNECT_ATTEMPTS = 2;
|
|
5794
|
+
const RECONNECT_DELAY_MS = 1e3;
|
|
5795
|
+
for (let attempt = 0; attempt < RECONNECT_ATTEMPTS; attempt++) {
|
|
5796
|
+
if (attempt > 0) await new Promise((r) => setTimeout(r, RECONNECT_DELAY_MS));
|
|
5744
5797
|
try {
|
|
5745
5798
|
await disconnectFromUpstream(conn);
|
|
5746
5799
|
const newConn = await connectToUpstream(
|
|
@@ -5756,23 +5809,23 @@ var ConnectServer = class _ConnectServer {
|
|
|
5756
5809
|
break;
|
|
5757
5810
|
} catch (err) {
|
|
5758
5811
|
lastErr = err;
|
|
5759
|
-
if (attempt
|
|
5812
|
+
if (attempt < RECONNECT_ATTEMPTS - 1) {
|
|
5760
5813
|
log("warn", "Auto-reconnect attempt failed, retrying", {
|
|
5761
5814
|
namespace: route.namespace,
|
|
5762
|
-
error: err.message
|
|
5815
|
+
error: err instanceof Error ? err.message : String(err)
|
|
5763
5816
|
});
|
|
5764
|
-
await new Promise((r) => setTimeout(r, 1e3 * 2 ** attempt));
|
|
5765
5817
|
}
|
|
5766
5818
|
}
|
|
5767
5819
|
}
|
|
5768
5820
|
if (!reconnected) {
|
|
5769
5821
|
conn.status = "error";
|
|
5770
|
-
|
|
5822
|
+
const lastErrMsg = lastErr instanceof Error ? lastErr.message : String(lastErr);
|
|
5823
|
+
log("error", "Auto-reconnect failed", { namespace: route.namespace, error: lastErrMsg });
|
|
5771
5824
|
return {
|
|
5772
5825
|
content: [
|
|
5773
5826
|
{
|
|
5774
5827
|
type: "text",
|
|
5775
|
-
text: `Server "${route.namespace}" disconnected and auto-reconnect failed: ${
|
|
5828
|
+
text: `Server "${route.namespace}" disconnected and auto-reconnect failed: ${lastErrMsg}. Use mcp_connect_activate with server "${route.namespace}" to reload it manually.`
|
|
5776
5829
|
}
|
|
5777
5830
|
],
|
|
5778
5831
|
isError: true
|
|
@@ -5800,8 +5853,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
5800
5853
|
toolName: route.originalName,
|
|
5801
5854
|
action: "tool_call",
|
|
5802
5855
|
latencyMs,
|
|
5803
|
-
success: !result.isError
|
|
5804
|
-
error: result.isError ? result.content[0]?.text : void 0
|
|
5856
|
+
success: !result.isError
|
|
5805
5857
|
});
|
|
5806
5858
|
if (!result.isError && Array.isArray(result.content)) {
|
|
5807
5859
|
try {
|
|
@@ -5848,9 +5900,11 @@ var ConnectServer = class _ConnectServer {
|
|
|
5848
5900
|
} catch {
|
|
5849
5901
|
}
|
|
5850
5902
|
}
|
|
5903
|
+
this.learning.recordDispatch(route.namespace);
|
|
5904
|
+
if (!result.isError) this.learning.recordSuccess(route.namespace);
|
|
5905
|
+
this.scheduleStateSave();
|
|
5851
5906
|
if (!result.isError) {
|
|
5852
5907
|
this.packDetector.recordCall(route.namespace, route.originalName, Date.now());
|
|
5853
|
-
this.scheduleStateSave();
|
|
5854
5908
|
}
|
|
5855
5909
|
await this.trackUsageAndAutoDeactivate(route.namespace);
|
|
5856
5910
|
}
|
|
@@ -5909,9 +5963,9 @@ var ConnectServer = class _ConnectServer {
|
|
|
5909
5963
|
const shortlist = bm25.slice(0, _ConnectServer.BM25_TOP_K);
|
|
5910
5964
|
const idByNamespace = new Map(servers.map((s) => [s.namespace, s.id]));
|
|
5911
5965
|
const candidateIds = shortlist.map((r) => idByNamespace.get(r.namespace)).filter((id) => typeof id === "string" && id.length > 0);
|
|
5912
|
-
if (candidateIds.length === 0) return shortlist;
|
|
5966
|
+
if (candidateIds.length === 0) return shortlist.map((r) => ({ ...r, hasRerank: false }));
|
|
5913
5967
|
const rerankResults = await rerank(context, candidateIds);
|
|
5914
|
-
if (!rerankResults) return shortlist;
|
|
5968
|
+
if (!rerankResults) return shortlist.map((r) => ({ ...r, hasRerank: false }));
|
|
5915
5969
|
const namespaceById = new Map(servers.map((s) => [s.id, s.namespace]));
|
|
5916
5970
|
const rerankScoreByNamespace = /* @__PURE__ */ new Map();
|
|
5917
5971
|
for (const r of rerankResults) {
|
|
@@ -5931,7 +5985,7 @@ var ConnectServer = class _ConnectServer {
|
|
|
5931
5985
|
if (a.hasRerank !== b.hasRerank) return a.hasRerank ? -1 : 1;
|
|
5932
5986
|
return b.score - a.score;
|
|
5933
5987
|
});
|
|
5934
|
-
return reordered
|
|
5988
|
+
return reordered;
|
|
5935
5989
|
}
|
|
5936
5990
|
// Auto-warm confidence gate — applied to discover(context) so a single
|
|
5937
5991
|
// clearly-winning server gets activated without the LLM needing to
|
|
@@ -5942,10 +5996,15 @@ var ConnectServer = class _ConnectServer {
|
|
|
5942
5996
|
return raw === void 0 || raw === "" || raw === "1" || raw.toLowerCase() === "true";
|
|
5943
5997
|
})();
|
|
5944
5998
|
// Top score must clear this floor AND the gap over the runner-up must
|
|
5945
|
-
// be convincing before we auto-activate. Values
|
|
5946
|
-
//
|
|
5947
|
-
|
|
5948
|
-
|
|
5999
|
+
// be convincing before we auto-activate. Values are scale-dependent --
|
|
6000
|
+
// BM25 scores are unbounded positive numbers, rerank cosines are in
|
|
6001
|
+
// [0, 1] -- so we keep separate thresholds and pick based on whether
|
|
6002
|
+
// the top entry was reranked. Tuned by intuition; revisit when we
|
|
6003
|
+
// have real usage data.
|
|
6004
|
+
static AUTO_ACTIVATE_MIN_SCORE_BM25 = 1;
|
|
6005
|
+
static AUTO_ACTIVATE_MARGIN_BM25 = 1.3;
|
|
6006
|
+
static AUTO_ACTIVATE_MIN_SCORE_COSINE = 0.5;
|
|
6007
|
+
static AUTO_ACTIVATE_MARGIN_COSINE = 1.25;
|
|
5949
6008
|
// Below this installed-server count, discover() appends a one-line
|
|
5950
6009
|
// marketplace pointer so sparse-config users see where to add more.
|
|
5951
6010
|
// At or above the threshold we stay silent — power users already know
|
|
@@ -5966,7 +6025,9 @@ var ConnectServer = class _ConnectServer {
|
|
|
5966
6025
|
if (ranked.length === 0) return this.handleDiscover(context);
|
|
5967
6026
|
const top = ranked[0];
|
|
5968
6027
|
const second = ranked[1];
|
|
5969
|
-
const
|
|
6028
|
+
const minScore = top?.hasRerank ? _ConnectServer.AUTO_ACTIVATE_MIN_SCORE_COSINE : _ConnectServer.AUTO_ACTIVATE_MIN_SCORE_BM25;
|
|
6029
|
+
const margin = top?.hasRerank ? _ConnectServer.AUTO_ACTIVATE_MARGIN_COSINE : _ConnectServer.AUTO_ACTIVATE_MARGIN_BM25;
|
|
6030
|
+
const topWinsDecisively = top !== void 0 && top.score >= minScore && (second === void 0 || top.score / (second.score || 1e-6) >= margin);
|
|
5970
6031
|
if (!topWinsDecisively || !top) return this.handleDiscover(context);
|
|
5971
6032
|
const existing = this.connections.get(top.namespace);
|
|
5972
6033
|
if (existing && existing.status === "connected") return this.handleDiscover(context);
|
|
@@ -6487,9 +6548,6 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
6487
6548
|
results.push(`${winner.namespace} (score ${winner.score.toFixed(2)}): ${r.message}`);
|
|
6488
6549
|
if (r.isChanged) anyChanged = true;
|
|
6489
6550
|
if (!r.ok) anyError = true;
|
|
6490
|
-
this.learning.recordDispatch(winner.namespace);
|
|
6491
|
-
if (r.ok) this.learning.recordSuccess(winner.namespace);
|
|
6492
|
-
this.scheduleStateSave();
|
|
6493
6551
|
}
|
|
6494
6552
|
if (anyChanged) {
|
|
6495
6553
|
this.rebuildRoutes();
|
|
@@ -6723,7 +6781,7 @@ ${activeCount} loaded in this session, ${totalTools} tools in context${tokenSumm
|
|
|
6723
6781
|
nsToKeys.set(s.namespace, existing);
|
|
6724
6782
|
}
|
|
6725
6783
|
const collisions = [...nsToKeys.entries()].filter(([, keys]) => keys.length > 1);
|
|
6726
|
-
const res = await
|
|
6784
|
+
const res = await request8(`${this.apiUrl.replace(/\/$/, "")}/api/connect/import`, {
|
|
6727
6785
|
method: "POST",
|
|
6728
6786
|
headers: {
|
|
6729
6787
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -6784,7 +6842,7 @@ Use mcp_connect_discover to see imported servers.`
|
|
|
6784
6842
|
}
|
|
6785
6843
|
const payload = built.payload;
|
|
6786
6844
|
try {
|
|
6787
|
-
const res = await
|
|
6845
|
+
const res = await request8(`${this.apiUrl.replace(/\/$/, "")}/api/connect/servers`, {
|
|
6788
6846
|
method: "POST",
|
|
6789
6847
|
headers: {
|
|
6790
6848
|
Authorization: `Bearer ${this.token}`,
|
|
@@ -7248,7 +7306,6 @@ To load the top pack in one step, call \`mcp_connect_activate\` with namespaces=
|
|
|
7248
7306
|
if (this.persistenceReady) {
|
|
7249
7307
|
await this.flushStateSave();
|
|
7250
7308
|
}
|
|
7251
|
-
stopTestRunner();
|
|
7252
7309
|
await shutdownAnalytics();
|
|
7253
7310
|
const disconnects = Array.from(this.connections.values()).map((conn) => disconnectFromUpstream(conn));
|
|
7254
7311
|
await Promise.allSettled(disconnects);
|
|
@@ -7551,13 +7608,24 @@ async function runUpgrade(opts = {}) {
|
|
|
7551
7608
|
print("No upgrade command available for this install method.");
|
|
7552
7609
|
return { exitCode: 0, lines };
|
|
7553
7610
|
}
|
|
7611
|
+
const autoRunnable = method === "global-npm";
|
|
7554
7612
|
if (!opts.run) {
|
|
7555
|
-
|
|
7613
|
+
if (autoRunnable) {
|
|
7614
|
+
print(`Run:
|
|
7615
|
+
${plan.command}
|
|
7616
|
+
|
|
7617
|
+
Or re-run with --run to upgrade in place.`);
|
|
7618
|
+
} else {
|
|
7619
|
+
print(`Suggested command (run it yourself; --run only works for global-npm installs):
|
|
7556
7620
|
${plan.command}`);
|
|
7621
|
+
}
|
|
7557
7622
|
return { exitCode: 1, lines };
|
|
7558
7623
|
}
|
|
7559
|
-
if (
|
|
7560
|
-
printErr(
|
|
7624
|
+
if (!autoRunnable) {
|
|
7625
|
+
printErr(
|
|
7626
|
+
`mcph upgrade --run: install method "${method}" can't be upgraded automatically. Run manually:
|
|
7627
|
+
${plan.command}`
|
|
7628
|
+
);
|
|
7561
7629
|
return { exitCode: 2, lines };
|
|
7562
7630
|
}
|
|
7563
7631
|
const runner = opts.spawnImpl ?? defaultSpawn;
|
|
@@ -7572,7 +7640,7 @@ async function runUpgrade(opts = {}) {
|
|
|
7572
7640
|
return { exitCode: 3, lines };
|
|
7573
7641
|
}
|
|
7574
7642
|
function readCurrentVersion() {
|
|
7575
|
-
return true ? "0.47.
|
|
7643
|
+
return true ? "0.47.3" : "dev";
|
|
7576
7644
|
}
|
|
7577
7645
|
|
|
7578
7646
|
// src/index.ts
|
|
@@ -7601,7 +7669,8 @@ if (subcommand === "compliance") {
|
|
|
7601
7669
|
`);
|
|
7602
7670
|
process.exit(2);
|
|
7603
7671
|
}
|
|
7604
|
-
|
|
7672
|
+
const claudeConfigDir = process.env.CLAUDE_CONFIG_DIR && process.env.CLAUDE_CONFIG_DIR.length > 0 ? process.env.CLAUDE_CONFIG_DIR : void 0;
|
|
7673
|
+
runInstall({ ...parsed.options, claudeConfigDir }).then((r) => process.exit(r.exitCode));
|
|
7605
7674
|
} else if (subcommand === "doctor") {
|
|
7606
7675
|
const doctorArgs = process.argv.slice(3);
|
|
7607
7676
|
const doctorJson = doctorArgs.includes("--json");
|
|
@@ -7619,6 +7688,17 @@ if (subcommand === "compliance") {
|
|
|
7619
7688
|
}
|
|
7620
7689
|
runDoctor({ json: doctorJson }).then((r) => process.exit(r.exitCode));
|
|
7621
7690
|
} else if (subcommand === "reset-learning") {
|
|
7691
|
+
const parsed = parseResetLearningArgs(process.argv.slice(3));
|
|
7692
|
+
if (parsed.kind === "help") {
|
|
7693
|
+
process.stdout.write(`${RESET_LEARNING_USAGE}
|
|
7694
|
+
`);
|
|
7695
|
+
process.exit(0);
|
|
7696
|
+
}
|
|
7697
|
+
if (parsed.kind === "error") {
|
|
7698
|
+
process.stderr.write(`${parsed.error}
|
|
7699
|
+
`);
|
|
7700
|
+
process.exit(2);
|
|
7701
|
+
}
|
|
7622
7702
|
runResetLearning().then((r) => process.exit(r.exitCode));
|
|
7623
7703
|
} else if (subcommand === "servers") {
|
|
7624
7704
|
const parsed = parseServersArgs(process.argv.slice(3));
|
|
@@ -7728,7 +7808,7 @@ if (subcommand === "compliance") {
|
|
|
7728
7808
|
`);
|
|
7729
7809
|
process.exit(0);
|
|
7730
7810
|
} else if (subcommand === "--version" || subcommand === "-V") {
|
|
7731
|
-
process.stdout.write(`mcph ${true ? "0.47.
|
|
7811
|
+
process.stdout.write(`mcph ${true ? "0.47.3" : "dev"}
|
|
7732
7812
|
`);
|
|
7733
7813
|
process.exit(0);
|
|
7734
7814
|
} else if (subcommand && !subcommand.startsWith("-")) {
|