@ai-ecoverse/mcpcat 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/host/mcpcat.mjs +1298 -37
- package/dist/kernel/mcpcat.mjs +1301 -36
- package/package.json +4 -3
package/dist/host/mcpcat.mjs
CHANGED
|
@@ -2073,32 +2073,1233 @@ function configToTarget(entry) {
|
|
|
2073
2073
|
throw new Error("config entry has neither url nor command");
|
|
2074
2074
|
}
|
|
2075
2075
|
|
|
2076
|
-
// src/config/
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2076
|
+
// src/config/parse.ts
|
|
2077
|
+
var SERVER_NAME = /^[A-Za-z0-9_-]+$/;
|
|
2078
|
+
var EXPOSURES = ["codemode", "deferred", "direct", "hidden"];
|
|
2079
|
+
var EXPOSURE_ALIASES = { "codemode-deferred": "codemode" };
|
|
2080
|
+
function isRecord(value) {
|
|
2081
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2082
|
+
}
|
|
2083
|
+
function isStringRecord(value) {
|
|
2084
|
+
return isRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
2085
|
+
}
|
|
2086
|
+
function isExposure(value) {
|
|
2087
|
+
return typeof value === "string" && EXPOSURES.includes(value);
|
|
2088
|
+
}
|
|
2089
|
+
function resolveAlias(value) {
|
|
2090
|
+
return typeof value === "string" ? EXPOSURE_ALIASES[value] ?? value : value;
|
|
2091
|
+
}
|
|
2092
|
+
function resolveAliases(value) {
|
|
2093
|
+
const out = { ...value };
|
|
2094
|
+
if (out.exposure !== void 0) out.exposure = resolveAlias(out.exposure);
|
|
2095
|
+
if (isRecord(out.toolExposure)) {
|
|
2096
|
+
out.toolExposure = Object.fromEntries(
|
|
2097
|
+
Object.entries(out.toolExposure).map(([k, v]) => [k, resolveAlias(v)])
|
|
2098
|
+
);
|
|
2099
|
+
}
|
|
2100
|
+
return out;
|
|
2101
|
+
}
|
|
2102
|
+
function fail(server, message, field) {
|
|
2103
|
+
return { server, ...field ? { field } : {}, message };
|
|
2104
|
+
}
|
|
2105
|
+
function validateCommon(name, value) {
|
|
2106
|
+
const exposures = EXPOSURES.map((e) => `"${e}"`).join(", ");
|
|
2107
|
+
if (value.exposure !== void 0 && !isExposure(value.exposure))
|
|
2108
|
+
return fail(name, `exposure must be one of ${exposures}`, "exposure");
|
|
2109
|
+
if (value.toolExposure !== void 0) {
|
|
2110
|
+
if (!isRecord(value.toolExposure))
|
|
2111
|
+
return fail(name, "toolExposure must map tool names to exposures", "toolExposure");
|
|
2112
|
+
for (const [tool, exp] of Object.entries(value.toolExposure))
|
|
2113
|
+
if (!isExposure(exp))
|
|
2114
|
+
return fail(
|
|
2115
|
+
name,
|
|
2116
|
+
`toolExposure "${tool}" must be one of ${exposures}`,
|
|
2117
|
+
`toolExposure.${tool}`
|
|
2118
|
+
);
|
|
2119
|
+
}
|
|
2120
|
+
if (value.enabled !== void 0 && typeof value.enabled !== "boolean")
|
|
2121
|
+
return fail(name, "enabled must be a boolean", "enabled");
|
|
2122
|
+
if (value.description !== void 0 && typeof value.description !== "string")
|
|
2123
|
+
return fail(name, "description must be a string", "description");
|
|
2124
|
+
if (value.timeout !== void 0 && (typeof value.timeout !== "number" || !(value.timeout > 0)))
|
|
2125
|
+
return fail(name, "timeout must be a positive number of seconds", "timeout");
|
|
2126
|
+
return void 0;
|
|
2127
|
+
}
|
|
2128
|
+
function validateOAuthFields(name, oauth) {
|
|
2129
|
+
if (oauth.clientId !== void 0 && typeof oauth.clientId !== "string")
|
|
2130
|
+
return fail(name, "oauth.clientId must be a string", "oauth.clientId");
|
|
2131
|
+
if (oauth.clientSecret !== void 0 && typeof oauth.clientSecret !== "string")
|
|
2132
|
+
return fail(name, "oauth.clientSecret must be a string", "oauth.clientSecret");
|
|
2133
|
+
const port = oauth.callbackPort;
|
|
2134
|
+
if (port !== void 0 && (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535))
|
|
2135
|
+
return fail(name, "oauth.callbackPort must be a port number", "oauth.callbackPort");
|
|
2136
|
+
if (oauth.callbackUrl !== void 0 && typeof oauth.callbackUrl !== "string")
|
|
2137
|
+
return fail(name, "oauth.callbackUrl must be a string", "oauth.callbackUrl");
|
|
2138
|
+
if (oauth.scope !== void 0 && typeof oauth.scope !== "string")
|
|
2139
|
+
return fail(name, "oauth.scope must be a string", "oauth.scope");
|
|
2140
|
+
if (oauth.clientName !== void 0 && (typeof oauth.clientName !== "string" || !oauth.clientName.trim()))
|
|
2141
|
+
return fail(name, "oauth.clientName must be a non-empty string", "oauth.clientName");
|
|
2142
|
+
return void 0;
|
|
2143
|
+
}
|
|
2144
|
+
function validateOAuthExtras(name, oauth) {
|
|
2145
|
+
if (oauth.clientRegistration !== void 0 && oauth.clientRegistration !== "dcr" && oauth.clientRegistration !== "cimd")
|
|
2146
|
+
return fail(
|
|
2147
|
+
name,
|
|
2148
|
+
'oauth.clientRegistration must be "dcr" or "cimd"',
|
|
2149
|
+
"oauth.clientRegistration"
|
|
2150
|
+
);
|
|
2151
|
+
if (oauth.authServerMetadataUrl !== void 0) {
|
|
2152
|
+
const url = typeof oauth.authServerMetadataUrl === "string" && URL.canParse(oauth.authServerMetadataUrl) ? new URL(oauth.authServerMetadataUrl) : void 0;
|
|
2153
|
+
if (!url || url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname)))
|
|
2154
|
+
return fail(
|
|
2155
|
+
name,
|
|
2156
|
+
"oauth.authServerMetadataUrl must be an https URL",
|
|
2157
|
+
"oauth.authServerMetadataUrl"
|
|
2158
|
+
);
|
|
2159
|
+
}
|
|
2160
|
+
return void 0;
|
|
2161
|
+
}
|
|
2162
|
+
function validateOAuth(name, value) {
|
|
2163
|
+
const { oauth } = value;
|
|
2164
|
+
if (oauth === void 0) return void 0;
|
|
2165
|
+
if (!isRecord(oauth)) return fail(name, "oauth must be an object", "oauth");
|
|
2166
|
+
return validateOAuthFields(name, oauth) ?? validateOAuthExtras(name, oauth);
|
|
2167
|
+
}
|
|
2168
|
+
function validateHttpServer(name, value) {
|
|
2169
|
+
if (typeof value.url !== "string" || value.type !== void 0 && value.type !== "http" && value.type !== "streamable-http")
|
|
2170
|
+
return void 0;
|
|
2171
|
+
if (!URL.canParse(value.url) || !/^https?:$/.test(new URL(value.url).protocol))
|
|
2172
|
+
return fail(name, "url must be an http or https URL", "url");
|
|
2173
|
+
if (value.headers !== void 0 && !isStringRecord(value.headers))
|
|
2174
|
+
return fail(name, "headers must map names to strings", "headers");
|
|
2175
|
+
const oauthErr = validateOAuth(name, value);
|
|
2176
|
+
if (oauthErr) return oauthErr;
|
|
2177
|
+
return value;
|
|
2178
|
+
}
|
|
2179
|
+
function validateStdioServer(name, value) {
|
|
2180
|
+
if (typeof value.command !== "string" || value.type !== void 0 && value.type !== "stdio")
|
|
2181
|
+
return void 0;
|
|
2182
|
+
if (value.args !== void 0 && !(Array.isArray(value.args) && value.args.every((a) => typeof a === "string")))
|
|
2183
|
+
return fail(name, "args must be an array of strings", "args");
|
|
2184
|
+
if (value.env !== void 0 && !isStringRecord(value.env))
|
|
2185
|
+
return fail(name, "env must map names to strings", "env");
|
|
2186
|
+
if (value.cwd !== void 0 && typeof value.cwd !== "string")
|
|
2187
|
+
return fail(name, "cwd must be a string", "cwd");
|
|
2188
|
+
return value;
|
|
2189
|
+
}
|
|
2190
|
+
function validateServer(name, raw) {
|
|
2191
|
+
if (!SERVER_NAME.test(name))
|
|
2192
|
+
return fail(name, `invalid server name "${name}" (use letters, digits, "_" and "-")`);
|
|
2193
|
+
if (!isRecord(raw)) return fail(name, "must be an object");
|
|
2194
|
+
const value = resolveAliases(raw);
|
|
2195
|
+
if (value.type === "sse")
|
|
2196
|
+
return fail(name, "legacy SSE transport is not supported; use the streamable HTTP URL", "type");
|
|
2197
|
+
const common = validateCommon(name, value);
|
|
2198
|
+
if (common) return common;
|
|
2199
|
+
const httpResult = validateHttpServer(name, value);
|
|
2200
|
+
if (httpResult) return httpResult;
|
|
2201
|
+
const stdioResult = validateStdioServer(name, value);
|
|
2202
|
+
if (stdioResult) return stdioResult;
|
|
2203
|
+
if (value.type !== void 0 && !["http", "streamable-http", "stdio"].includes(value.type))
|
|
2204
|
+
return fail(name, 'type must be "stdio", "http", or "streamable-http"', "type");
|
|
2205
|
+
return fail(name, 'needs a "url" or "command"');
|
|
2206
|
+
}
|
|
2207
|
+
function isOverride(value) {
|
|
2208
|
+
return value.command === void 0 && value.url === void 0 && value.type === void 0;
|
|
2209
|
+
}
|
|
2210
|
+
function canonical(name) {
|
|
2211
|
+
return name.replace(/-/g, "_");
|
|
2212
|
+
}
|
|
2213
|
+
function parseMcpJson(text) {
|
|
2214
|
+
const result = { servers: [], overrides: [], problems: [] };
|
|
2215
|
+
let parsed;
|
|
2216
|
+
try {
|
|
2217
|
+
parsed = JSON.parse(text);
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
result.problems.push({ message: `not valid JSON: ${error.message}` });
|
|
2220
|
+
return result;
|
|
2221
|
+
}
|
|
2222
|
+
if (!isRecord(parsed) || parsed.mcpServers !== void 0 && !isRecord(parsed.mcpServers)) {
|
|
2223
|
+
result.problems.push({ message: "mcpServers must be an object" });
|
|
2224
|
+
return result;
|
|
2225
|
+
}
|
|
2226
|
+
if (typeof parsed.autoEnableCodemode === "boolean")
|
|
2227
|
+
result.autoEnableCodemode = parsed.autoEnableCodemode;
|
|
2228
|
+
else if (parsed.autoEnableCodemode !== void 0)
|
|
2229
|
+
result.problems.push({ message: "autoEnableCodemode must be a boolean" });
|
|
2230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2231
|
+
for (const [name, raw] of Object.entries(parsed.mcpServers ?? {})) {
|
|
2232
|
+
if (!SERVER_NAME.test(name)) {
|
|
2233
|
+
result.problems.push(
|
|
2234
|
+
fail(name, `invalid server name "${name}" (use letters, digits, "_" and "-")`)
|
|
2235
|
+
);
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
if (seen.has(canonical(name))) {
|
|
2239
|
+
result.problems.push(
|
|
2240
|
+
fail(name, 'names that differ only in "-" and "_" count as the same server')
|
|
2241
|
+
);
|
|
2242
|
+
continue;
|
|
2243
|
+
}
|
|
2244
|
+
seen.add(canonical(name));
|
|
2245
|
+
if (isRecord(raw) && isOverride(raw)) {
|
|
2246
|
+
result.overrides.push({ name, raw });
|
|
2247
|
+
continue;
|
|
2248
|
+
}
|
|
2249
|
+
const checked = validateServer(name, raw);
|
|
2250
|
+
if ("message" in checked) {
|
|
2251
|
+
result.problems.push(checked);
|
|
2252
|
+
continue;
|
|
2253
|
+
}
|
|
2254
|
+
result.servers.push({ name, config: checked });
|
|
2255
|
+
}
|
|
2256
|
+
return result;
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
// src/config/merge.ts
|
|
2260
|
+
var OVERRIDE_KEYS = /* @__PURE__ */ new Set(["enabled", "exposure", "toolExposure"]);
|
|
2261
|
+
async function readFile(fs, path) {
|
|
2262
|
+
try {
|
|
2263
|
+
const data = await fs.readFile(path);
|
|
2264
|
+
return new TextDecoder().decode(data);
|
|
2265
|
+
} catch {
|
|
2266
|
+
return void 0;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
function applyOverride(servers, problems, name, override, projectPath) {
|
|
2270
|
+
const raw = override;
|
|
2271
|
+
const base = servers.get(name);
|
|
2272
|
+
const extra = Object.keys(raw).filter((k) => !OVERRIDE_KEYS.has(k));
|
|
2273
|
+
if (!base) {
|
|
2274
|
+
problems.push({
|
|
2275
|
+
server: name,
|
|
2276
|
+
message: 'needs "command" or "url", or a user server to override'
|
|
2277
|
+
});
|
|
2278
|
+
return;
|
|
2279
|
+
}
|
|
2280
|
+
if (extra.length > 0) {
|
|
2281
|
+
problems.push({
|
|
2282
|
+
server: name,
|
|
2283
|
+
message: "an override can only set enabled, exposure, toolExposure"
|
|
2284
|
+
});
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const merged = { ...base.config, ...raw };
|
|
2288
|
+
servers.set(name, { ...base, config: merged, override: projectPath });
|
|
2289
|
+
}
|
|
2290
|
+
function applyProjectEntry(servers, problems, name, config, projectPath) {
|
|
2291
|
+
const clash = [...servers.keys()].find(
|
|
2292
|
+
(other) => other !== name && canonical(other) === canonical(name)
|
|
2293
|
+
);
|
|
2294
|
+
if (clash) {
|
|
2295
|
+
problems.push({
|
|
2296
|
+
server: name,
|
|
2297
|
+
message: `conflicts with "${clash}" (names differ only in "-"/"_")`
|
|
2298
|
+
});
|
|
2299
|
+
return;
|
|
2300
|
+
}
|
|
2301
|
+
servers.set(name, { name, config, source: projectPath, scope: "project" });
|
|
2302
|
+
}
|
|
2303
|
+
function loadUserServers(parsed, userPath, servers, problems) {
|
|
2304
|
+
for (const p of parsed.problems) problems.push(p);
|
|
2305
|
+
for (const { name, config } of parsed.servers) {
|
|
2306
|
+
servers.set(name, { name, config, source: userPath, scope: "user" });
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
function loadProjectServers(parsed, projectPath, servers, problems) {
|
|
2310
|
+
for (const p of parsed.problems) problems.push(p);
|
|
2311
|
+
for (const { name, raw } of parsed.overrides) {
|
|
2312
|
+
applyOverride(servers, problems, name, raw, projectPath);
|
|
2313
|
+
}
|
|
2314
|
+
for (const { name, config } of parsed.servers) {
|
|
2315
|
+
applyProjectEntry(servers, problems, name, config, projectPath);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
async function loadConfig(input) {
|
|
2319
|
+
const servers = /* @__PURE__ */ new Map();
|
|
2320
|
+
const problems = [];
|
|
2321
|
+
let autoEnableCodemode;
|
|
2322
|
+
const userText = await readFile(input.fs, input.userPath);
|
|
2323
|
+
if (userText !== void 0) {
|
|
2324
|
+
const parsed = parseMcpJson(userText);
|
|
2325
|
+
if (parsed.autoEnableCodemode !== void 0) autoEnableCodemode = parsed.autoEnableCodemode;
|
|
2326
|
+
loadUserServers(parsed, input.userPath, servers, problems);
|
|
2327
|
+
}
|
|
2328
|
+
if (input.projectPath && input.trustProject) {
|
|
2329
|
+
const projectText = await readFile(input.fs, input.projectPath);
|
|
2330
|
+
if (projectText !== void 0) {
|
|
2331
|
+
const parsed = parseMcpJson(projectText);
|
|
2332
|
+
if (parsed.autoEnableCodemode !== void 0) autoEnableCodemode = parsed.autoEnableCodemode;
|
|
2333
|
+
loadProjectServers(parsed, input.projectPath, servers, problems);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2081
2336
|
return {
|
|
2082
|
-
|
|
2337
|
+
servers: [...servers.values()],
|
|
2338
|
+
problems,
|
|
2339
|
+
...autoEnableCodemode !== void 0 ? { autoEnableCodemode } : {}
|
|
2340
|
+
};
|
|
2341
|
+
}
|
|
2342
|
+
|
|
2343
|
+
// src/config/paths.ts
|
|
2344
|
+
function userConfigDir(platform2) {
|
|
2345
|
+
const piDir = platform2.env.PI_CODING_AGENT_DIR;
|
|
2346
|
+
if (piDir) return piDir;
|
|
2347
|
+
const home = platform2.env.HOME ?? platform2.env.USERPROFILE ?? "/";
|
|
2348
|
+
return `${home}/.pi/agent`;
|
|
2349
|
+
}
|
|
2350
|
+
function userConfigPath(platform2) {
|
|
2351
|
+
return `${userConfigDir(platform2)}/mcp.json`;
|
|
2352
|
+
}
|
|
2353
|
+
function projectConfigPath(platform2) {
|
|
2354
|
+
return `${platform2.cwd}/.pi/mcp.json`;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
// src/config/index.ts
|
|
2358
|
+
var MergedConfigSource = class _MergedConfigSource {
|
|
2359
|
+
entries;
|
|
2360
|
+
config;
|
|
2361
|
+
constructor(config) {
|
|
2362
|
+
this.config = config;
|
|
2363
|
+
this.entries = /* @__PURE__ */ new Map();
|
|
2364
|
+
for (const entry of config.servers) {
|
|
2365
|
+
this.entries.set(entry.name, entry);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
async get(name) {
|
|
2369
|
+
const normalizedName = name.replace(/[-_]/g, "").toLowerCase();
|
|
2370
|
+
for (const [key, entry] of this.entries) {
|
|
2371
|
+
if (key.replace(/[-_]/g, "").toLowerCase() === normalizedName) {
|
|
2372
|
+
return entry.config;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return void 0;
|
|
2376
|
+
}
|
|
2377
|
+
entry(name) {
|
|
2378
|
+
return this.entries.get(name);
|
|
2379
|
+
}
|
|
2380
|
+
list() {
|
|
2381
|
+
return [...this.entries.values()];
|
|
2382
|
+
}
|
|
2383
|
+
static async load(platform2, options) {
|
|
2384
|
+
const userPath = options?.configFile ?? userConfigPath(platform2);
|
|
2385
|
+
const projectPath = options?.configFile ? void 0 : projectConfigPath(platform2);
|
|
2386
|
+
const config = await loadConfig({
|
|
2387
|
+
userPath,
|
|
2388
|
+
projectPath,
|
|
2389
|
+
trustProject: options?.trustProject ?? false,
|
|
2390
|
+
fs: platform2.fs
|
|
2391
|
+
});
|
|
2392
|
+
return new _MergedConfigSource(config);
|
|
2393
|
+
}
|
|
2394
|
+
};
|
|
2395
|
+
|
|
2396
|
+
// src/config/lock.ts
|
|
2397
|
+
var LOCK_STALE_MS = 1e4;
|
|
2398
|
+
var LOCK_POLL_MS = 50;
|
|
2399
|
+
var LOCK_MAX_WAIT_MS = 5e3;
|
|
2400
|
+
function isEexist(err) {
|
|
2401
|
+
if (err instanceof Error && "code" in err)
|
|
2402
|
+
return err.code === "EEXIST";
|
|
2403
|
+
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
2404
|
+
return false;
|
|
2405
|
+
}
|
|
2406
|
+
async function removeStaleLock(fs, lockPath) {
|
|
2407
|
+
try {
|
|
2408
|
+
const s = await fs.stat(lockPath);
|
|
2409
|
+
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
2083
2410
|
try {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2411
|
+
await fs.rmdir(lockPath);
|
|
2412
|
+
} catch {
|
|
2413
|
+
}
|
|
2414
|
+
return true;
|
|
2415
|
+
}
|
|
2416
|
+
} catch {
|
|
2417
|
+
}
|
|
2418
|
+
return false;
|
|
2419
|
+
}
|
|
2420
|
+
async function acquireLock(fs, configPath) {
|
|
2421
|
+
const lockPath = `${configPath}.lock`;
|
|
2422
|
+
const start = Date.now();
|
|
2423
|
+
while (true) {
|
|
2424
|
+
try {
|
|
2425
|
+
await fs.mkdirExclusive(lockPath);
|
|
2426
|
+
break;
|
|
2427
|
+
} catch (err) {
|
|
2428
|
+
if (!isEexist(err)) throw err;
|
|
2429
|
+
await removeStaleLock(fs, lockPath);
|
|
2430
|
+
}
|
|
2431
|
+
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
2432
|
+
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
2433
|
+
}
|
|
2434
|
+
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
2435
|
+
}
|
|
2436
|
+
let released = false;
|
|
2437
|
+
return async () => {
|
|
2438
|
+
if (released) return;
|
|
2439
|
+
released = true;
|
|
2440
|
+
try {
|
|
2441
|
+
await fs.rmdir(lockPath);
|
|
2442
|
+
} catch {
|
|
2443
|
+
}
|
|
2444
|
+
};
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
// src/config/edit.ts
|
|
2448
|
+
function isRecord2(value) {
|
|
2449
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2450
|
+
}
|
|
2451
|
+
function detectIndent(text) {
|
|
2452
|
+
const match = /^([ \t]+)\S/m.exec(text);
|
|
2453
|
+
return match ? match[1] : " ";
|
|
2454
|
+
}
|
|
2455
|
+
async function readText(fs, path) {
|
|
2456
|
+
try {
|
|
2457
|
+
const data = await fs.readFile(path);
|
|
2458
|
+
return new TextDecoder().decode(data);
|
|
2459
|
+
} catch {
|
|
2460
|
+
return void 0;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
async function editMcpServers(fs, path, edit) {
|
|
2464
|
+
const release = await acquireLock(fs, path);
|
|
2465
|
+
try {
|
|
2466
|
+
const text = await readText(fs, path);
|
|
2467
|
+
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
2468
|
+
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
2469
|
+
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
2470
|
+
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
2471
|
+
if (!edit(servers, parsed)) return { changed: false };
|
|
2472
|
+
const indent = text ? detectIndent(text) : " ";
|
|
2473
|
+
const output = `${JSON.stringify(parsed, null, indent)}
|
|
2474
|
+
`;
|
|
2475
|
+
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
2476
|
+
if (dir) {
|
|
2477
|
+
try {
|
|
2478
|
+
await fs.mkdir(dir);
|
|
2097
2479
|
} catch {
|
|
2098
|
-
return void 0;
|
|
2099
2480
|
}
|
|
2100
2481
|
}
|
|
2482
|
+
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
2483
|
+
await fs.writeFile(tmpPath, output);
|
|
2484
|
+
await fs.rename(tmpPath, path);
|
|
2485
|
+
return { changed: true };
|
|
2486
|
+
} finally {
|
|
2487
|
+
await release();
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
async function addServer(fs, path, name, config) {
|
|
2491
|
+
let replaced = false;
|
|
2492
|
+
await editMcpServers(fs, path, (servers, parsed) => {
|
|
2493
|
+
const target = servers ?? {};
|
|
2494
|
+
replaced = target[name] !== void 0;
|
|
2495
|
+
target[name] = config;
|
|
2496
|
+
parsed.mcpServers = target;
|
|
2497
|
+
return true;
|
|
2498
|
+
});
|
|
2499
|
+
return { replaced };
|
|
2500
|
+
}
|
|
2501
|
+
async function removeServer(fs, path, name) {
|
|
2502
|
+
let removed = false;
|
|
2503
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2504
|
+
if (!servers || servers[name] === void 0) return false;
|
|
2505
|
+
delete servers[name];
|
|
2506
|
+
removed = true;
|
|
2507
|
+
return true;
|
|
2508
|
+
});
|
|
2509
|
+
return removed;
|
|
2510
|
+
}
|
|
2511
|
+
async function updateServer(fs, path, name, patch) {
|
|
2512
|
+
let updated = false;
|
|
2513
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2514
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
2515
|
+
const entry = servers[name];
|
|
2516
|
+
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
2517
|
+
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
2518
|
+
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
2519
|
+
updated = true;
|
|
2520
|
+
return true;
|
|
2521
|
+
});
|
|
2522
|
+
return updated;
|
|
2523
|
+
}
|
|
2524
|
+
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
2525
|
+
let updated = false;
|
|
2526
|
+
await editMcpServers(fs, path, (servers) => {
|
|
2527
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
2528
|
+
const entry = servers[name];
|
|
2529
|
+
if (toolGlob) {
|
|
2530
|
+
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
2531
|
+
te[toolGlob] = exposure;
|
|
2532
|
+
entry.toolExposure = te;
|
|
2533
|
+
} else {
|
|
2534
|
+
entry.exposure = exposure;
|
|
2535
|
+
}
|
|
2536
|
+
updated = true;
|
|
2537
|
+
return true;
|
|
2538
|
+
});
|
|
2539
|
+
return updated;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
// src/config/import.ts
|
|
2543
|
+
function isRecord3(value) {
|
|
2544
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2545
|
+
}
|
|
2546
|
+
function isStringRecord2(value) {
|
|
2547
|
+
return isRecord3(value) && Object.values(value).every((v) => typeof v === "string");
|
|
2548
|
+
}
|
|
2549
|
+
function tryParse(text) {
|
|
2550
|
+
try {
|
|
2551
|
+
return { parsed: JSON.parse(text) };
|
|
2552
|
+
} catch (e) {
|
|
2553
|
+
return { parsed: void 0, error: `not valid JSON: ${e.message}` };
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
function filterNames(entries, names) {
|
|
2557
|
+
const result = [];
|
|
2558
|
+
for (const [name, value] of entries) {
|
|
2559
|
+
if (names && names.length > 0 && !names.includes(name)) continue;
|
|
2560
|
+
result.push([name, value]);
|
|
2561
|
+
}
|
|
2562
|
+
return result;
|
|
2563
|
+
}
|
|
2564
|
+
function replaceInputVars(value) {
|
|
2565
|
+
return value.replace(
|
|
2566
|
+
/\$\{input:([^}]+)\}/g,
|
|
2567
|
+
(_, name) => `\${${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}}`
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
function importClaude(text, names) {
|
|
2571
|
+
const result = { servers: [], errors: [] };
|
|
2572
|
+
const { parsed, error } = tryParse(text);
|
|
2573
|
+
if (error) {
|
|
2574
|
+
result.errors.push(error);
|
|
2575
|
+
return result;
|
|
2576
|
+
}
|
|
2577
|
+
if (!isRecord3(parsed)) {
|
|
2578
|
+
result.errors.push("expected an object");
|
|
2579
|
+
return result;
|
|
2580
|
+
}
|
|
2581
|
+
const mcpServers = parsed.mcpServers;
|
|
2582
|
+
if (!isRecord3(mcpServers)) {
|
|
2583
|
+
result.errors.push('no "mcpServers" object');
|
|
2584
|
+
return result;
|
|
2585
|
+
}
|
|
2586
|
+
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
2587
|
+
if (!isRecord3(entry)) {
|
|
2588
|
+
result.errors.push(`${name}: not an object`);
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
2591
|
+
result.servers.push({ name, config: entry });
|
|
2592
|
+
}
|
|
2593
|
+
return result;
|
|
2594
|
+
}
|
|
2595
|
+
function importVSCode(text, names) {
|
|
2596
|
+
const result = { servers: [], errors: [] };
|
|
2597
|
+
const { parsed, error } = tryParse(text);
|
|
2598
|
+
if (error) {
|
|
2599
|
+
result.errors.push(error);
|
|
2600
|
+
return result;
|
|
2601
|
+
}
|
|
2602
|
+
if (!isRecord3(parsed)) {
|
|
2603
|
+
result.errors.push("expected an object");
|
|
2604
|
+
return result;
|
|
2605
|
+
}
|
|
2606
|
+
const servers = parsed.servers ?? parsed.mcpServers;
|
|
2607
|
+
if (!isRecord3(servers)) {
|
|
2608
|
+
result.errors.push('no "servers" or "mcpServers" object');
|
|
2609
|
+
return result;
|
|
2610
|
+
}
|
|
2611
|
+
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
2612
|
+
if (!isRecord3(entry)) {
|
|
2613
|
+
result.errors.push(`${name}: not an object`);
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
result.servers.push({ name, config: convertVSCodeEntry(entry) });
|
|
2617
|
+
}
|
|
2618
|
+
return result;
|
|
2619
|
+
}
|
|
2620
|
+
function convertVSCodeEntry(entry) {
|
|
2621
|
+
const config = {};
|
|
2622
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
2623
|
+
if (key === "type" || key === "command" || key === "args" || key === "cwd" || key === "url") {
|
|
2624
|
+
config[key] = value;
|
|
2625
|
+
} else if (key === "headers" && isStringRecord2(value)) {
|
|
2626
|
+
const headers = {};
|
|
2627
|
+
for (const [h, v] of Object.entries(value)) headers[h] = replaceInputVars(v);
|
|
2628
|
+
config.headers = headers;
|
|
2629
|
+
} else if (key === "env" && isStringRecord2(value)) {
|
|
2630
|
+
const env = {};
|
|
2631
|
+
for (const [k, v] of Object.entries(value)) env[k] = replaceInputVars(v);
|
|
2632
|
+
config.env = env;
|
|
2633
|
+
} else {
|
|
2634
|
+
config[key] = value;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
return config;
|
|
2638
|
+
}
|
|
2639
|
+
function importSixServers(text, names) {
|
|
2640
|
+
const result = { servers: [], errors: [] };
|
|
2641
|
+
const { parsed, error } = tryParse(text);
|
|
2642
|
+
if (error) {
|
|
2643
|
+
result.errors.push(error);
|
|
2644
|
+
return result;
|
|
2645
|
+
}
|
|
2646
|
+
if (!isRecord3(parsed)) {
|
|
2647
|
+
result.errors.push("expected an object");
|
|
2648
|
+
return result;
|
|
2649
|
+
}
|
|
2650
|
+
const servers = parsed.servers;
|
|
2651
|
+
if (!isRecord3(servers)) {
|
|
2652
|
+
result.errors.push('no "servers" object');
|
|
2653
|
+
return result;
|
|
2654
|
+
}
|
|
2655
|
+
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
2656
|
+
if (!isRecord3(entry)) {
|
|
2657
|
+
result.errors.push(`${name}: not an object`);
|
|
2658
|
+
continue;
|
|
2659
|
+
}
|
|
2660
|
+
const config = {};
|
|
2661
|
+
if (typeof entry.url === "string") config.url = entry.url;
|
|
2662
|
+
if (isStringRecord2(entry.headers)) config.headers = entry.headers;
|
|
2663
|
+
if (typeof entry.exposure === "string") config.exposure = entry.exposure;
|
|
2664
|
+
if (isRecord3(entry.toolExposure)) config.toolExposure = entry.toolExposure;
|
|
2665
|
+
result.servers.push({ name, config });
|
|
2666
|
+
}
|
|
2667
|
+
return result;
|
|
2668
|
+
}
|
|
2669
|
+
function importCodexToml(text, names) {
|
|
2670
|
+
const result = { servers: [], errors: [] };
|
|
2671
|
+
const sections = parseTomlSections(text);
|
|
2672
|
+
for (const [sectionName, fields] of sections) {
|
|
2673
|
+
const prefix = "mcp_servers.";
|
|
2674
|
+
if (!sectionName.startsWith(prefix)) continue;
|
|
2675
|
+
const name = sectionName.slice(prefix.length);
|
|
2676
|
+
if (names && names.length > 0 && !names.includes(name)) continue;
|
|
2677
|
+
const config = {};
|
|
2678
|
+
if (fields.command) config.command = fields.command;
|
|
2679
|
+
if (fields.args) config.args = fields.args;
|
|
2680
|
+
if (fields.url) config.url = fields.url;
|
|
2681
|
+
if (fields.env && isRecord3(fields.env)) config.env = fields.env;
|
|
2682
|
+
result.servers.push({ name, config });
|
|
2683
|
+
}
|
|
2684
|
+
return result;
|
|
2685
|
+
}
|
|
2686
|
+
function parseTomlSections(text) {
|
|
2687
|
+
const sections = [];
|
|
2688
|
+
let current;
|
|
2689
|
+
let currentName = "";
|
|
2690
|
+
for (const line of text.split("\n")) {
|
|
2691
|
+
const trimmed = line.trim();
|
|
2692
|
+
if (trimmed.startsWith("#") || trimmed === "") continue;
|
|
2693
|
+
const sectionMatch = /^\[([^\]]+)\]/.exec(trimmed);
|
|
2694
|
+
if (sectionMatch) {
|
|
2695
|
+
if (current) sections.push([currentName, current]);
|
|
2696
|
+
currentName = sectionMatch[1].trim();
|
|
2697
|
+
current = {};
|
|
2698
|
+
continue;
|
|
2699
|
+
}
|
|
2700
|
+
if (!current) continue;
|
|
2701
|
+
const kvMatch = /^(\w+)\s*=\s*(.+)/.exec(trimmed);
|
|
2702
|
+
if (kvMatch) {
|
|
2703
|
+
current[kvMatch[1]] = parseTomlValue(kvMatch[2].trim());
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
if (current) sections.push([currentName, current]);
|
|
2707
|
+
return sections;
|
|
2708
|
+
}
|
|
2709
|
+
function parseTomlValue(raw) {
|
|
2710
|
+
if (raw.startsWith('"') && raw.endsWith('"')) return raw.slice(1, -1);
|
|
2711
|
+
if (raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
|
|
2712
|
+
if (raw.startsWith("[")) {
|
|
2713
|
+
try {
|
|
2714
|
+
return JSON.parse(raw.replace(/'/g, '"'));
|
|
2715
|
+
} catch {
|
|
2716
|
+
return raw;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
if (raw === "true") return true;
|
|
2720
|
+
if (raw === "false") return false;
|
|
2721
|
+
const num = Number(raw);
|
|
2722
|
+
if (!Number.isNaN(num)) return num;
|
|
2723
|
+
return raw;
|
|
2724
|
+
}
|
|
2725
|
+
function convertOpenCodeLocal(entry) {
|
|
2726
|
+
const config = {};
|
|
2727
|
+
if (Array.isArray(entry.command) && entry.command.length > 0) {
|
|
2728
|
+
config.command = entry.command[0];
|
|
2729
|
+
if (entry.command.length > 1) config.args = entry.command.slice(1);
|
|
2730
|
+
} else if (typeof entry.command === "string") {
|
|
2731
|
+
config.command = entry.command;
|
|
2732
|
+
}
|
|
2733
|
+
if (isRecord3(entry.environment)) {
|
|
2734
|
+
const env = {};
|
|
2735
|
+
for (const [k, v] of Object.entries(entry.environment)) {
|
|
2736
|
+
if (typeof v === "string")
|
|
2737
|
+
env[k] = v.replace(/\{env:([^}]+)\}/g, (_, n) => `\${${n}}`);
|
|
2738
|
+
}
|
|
2739
|
+
config.env = env;
|
|
2740
|
+
}
|
|
2741
|
+
return config;
|
|
2742
|
+
}
|
|
2743
|
+
function convertOpenCodeRemote(entry) {
|
|
2744
|
+
const config = {};
|
|
2745
|
+
config.url = entry.url;
|
|
2746
|
+
if (isStringRecord2(entry.headers)) config.headers = entry.headers;
|
|
2747
|
+
return config;
|
|
2748
|
+
}
|
|
2749
|
+
function importOpenCode(text, names) {
|
|
2750
|
+
const result = { servers: [], errors: [] };
|
|
2751
|
+
const { parsed, error } = tryParse(text);
|
|
2752
|
+
if (error) {
|
|
2753
|
+
result.errors.push(error);
|
|
2754
|
+
return result;
|
|
2755
|
+
}
|
|
2756
|
+
if (!isRecord3(parsed)) {
|
|
2757
|
+
result.errors.push("expected an object");
|
|
2758
|
+
return result;
|
|
2759
|
+
}
|
|
2760
|
+
const mcpServers = parsed.mcpServers ?? parsed.mcp;
|
|
2761
|
+
if (!isRecord3(mcpServers)) {
|
|
2762
|
+
result.errors.push("no mcp servers found");
|
|
2763
|
+
return result;
|
|
2764
|
+
}
|
|
2765
|
+
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
2766
|
+
if (!isRecord3(entry)) {
|
|
2767
|
+
result.errors.push(`${name}: not an object`);
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
const isLocal = entry.type === "local" || entry.command;
|
|
2771
|
+
const config = isLocal ? convertOpenCodeLocal(entry) : convertOpenCodeRemote(entry);
|
|
2772
|
+
result.servers.push({ name, config });
|
|
2773
|
+
}
|
|
2774
|
+
return result;
|
|
2775
|
+
}
|
|
2776
|
+
function detectFormat(text, filename) {
|
|
2777
|
+
if (filename.endsWith(".toml")) return "codex";
|
|
2778
|
+
if (filename.includes("servers.json")) return "six";
|
|
2779
|
+
if (filename.includes(".vscode")) return "vscode";
|
|
2780
|
+
try {
|
|
2781
|
+
const parsed = JSON.parse(text);
|
|
2782
|
+
if (isRecord3(parsed)) {
|
|
2783
|
+
if (parsed.servers && isRecord3(parsed.servers) && parsed.version !== void 0) return "six";
|
|
2784
|
+
if (parsed.servers && isRecord3(parsed.servers)) return "vscode";
|
|
2785
|
+
if (parsed.mcpServers) return "claude";
|
|
2786
|
+
if (parsed.mcp) return "opencode";
|
|
2787
|
+
}
|
|
2788
|
+
} catch {
|
|
2789
|
+
}
|
|
2790
|
+
if (text.includes("[mcp_servers.")) return "codex";
|
|
2791
|
+
return "claude";
|
|
2792
|
+
}
|
|
2793
|
+
function importFile(text, filename, names) {
|
|
2794
|
+
const format = detectFormat(text, filename);
|
|
2795
|
+
switch (format) {
|
|
2796
|
+
case "claude":
|
|
2797
|
+
return importClaude(text, names);
|
|
2798
|
+
case "vscode":
|
|
2799
|
+
return importVSCode(text, names);
|
|
2800
|
+
case "six":
|
|
2801
|
+
return importSixServers(text, names);
|
|
2802
|
+
case "codex":
|
|
2803
|
+
return importCodexToml(text, names);
|
|
2804
|
+
case "opencode":
|
|
2805
|
+
return importOpenCode(text, names);
|
|
2806
|
+
default:
|
|
2807
|
+
return importClaude(text, names);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
// src/config/warnings.ts
|
|
2812
|
+
var SECRET_HEADER = /authorization|cookie|token|key|secret|password|session/i;
|
|
2813
|
+
var PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
2814
|
+
var SCHEME = /^\s*(?:bearer|basic|token|apikey|api-key)\s+/i;
|
|
2815
|
+
var SECRET_PARAM = /token|key|secret|password|signature|sig|auth|code/i;
|
|
2816
|
+
function literalSecret(header, value) {
|
|
2817
|
+
if (!SECRET_HEADER.test(header)) return false;
|
|
2818
|
+
return value.replace(PLACEHOLDER, "").replace(SCHEME, "").trim() !== "";
|
|
2819
|
+
}
|
|
2820
|
+
function urlHasSecret(url) {
|
|
2821
|
+
if (!URL.canParse(url)) return void 0;
|
|
2822
|
+
const parsed = new URL(url);
|
|
2823
|
+
if (parsed.username || parsed.password) return "url";
|
|
2824
|
+
for (const name of parsed.searchParams.keys()) if (SECRET_PARAM.test(name)) return `url ?${name}`;
|
|
2825
|
+
return void 0;
|
|
2826
|
+
}
|
|
2827
|
+
var SLICC_OAUTH_REFUSED = {
|
|
2828
|
+
clientSecret: "a client secret would have to live in mcp.json; SLICC signs in as a public client",
|
|
2829
|
+
callbackPort: "SLICC signs in through its own page, not a loopback port",
|
|
2830
|
+
callbackUrl: "SLICC signs in through its own page, not a loopback URL",
|
|
2831
|
+
clientRegistration: "SLICC registers dynamically or uses oauth.clientId"
|
|
2832
|
+
};
|
|
2833
|
+
function checkAgentWarnings(name, config) {
|
|
2834
|
+
const warnings = [];
|
|
2835
|
+
if ("command" in config && config.command) {
|
|
2836
|
+
warnings.push({
|
|
2837
|
+
server: name,
|
|
2838
|
+
message: "stdio servers need slicc-kernel#68, so the agent will skip this entry",
|
|
2839
|
+
advice: "use mcpcat serve --proxy to bridge it as HTTP"
|
|
2840
|
+
});
|
|
2841
|
+
}
|
|
2842
|
+
if ("headers" in config && config.headers) {
|
|
2843
|
+
for (const [h, v] of Object.entries(config.headers)) {
|
|
2844
|
+
if (v.trim().startsWith("!")) {
|
|
2845
|
+
warnings.push({
|
|
2846
|
+
server: name,
|
|
2847
|
+
message: `headers.${h} runs a command, which the agent can't do`,
|
|
2848
|
+
advice: "use ${NAME} and set the value in Settings > MCP servers"
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
if (literalSecret(h, v)) {
|
|
2852
|
+
warnings.push({
|
|
2853
|
+
server: name,
|
|
2854
|
+
message: `headers.${h} holds a literal secret`,
|
|
2855
|
+
advice: "write ${NAME} instead, and add the value in Settings > MCP servers"
|
|
2856
|
+
});
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2860
|
+
if ("url" in config && config.url) {
|
|
2861
|
+
const secret = urlHasSecret(config.url);
|
|
2862
|
+
if (secret) {
|
|
2863
|
+
warnings.push({
|
|
2864
|
+
server: name,
|
|
2865
|
+
message: `${secret} holds a literal secret`,
|
|
2866
|
+
advice: "put it in a header as ${NAME}"
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
}
|
|
2870
|
+
if ("oauth" in config && config.oauth) {
|
|
2871
|
+
for (const key of Object.keys(config.oauth)) {
|
|
2872
|
+
const reason = SLICC_OAUTH_REFUSED[key];
|
|
2873
|
+
if (reason) {
|
|
2874
|
+
warnings.push({
|
|
2875
|
+
server: name,
|
|
2876
|
+
message: `oauth.${key} is not supported by the agent: ${reason}`,
|
|
2877
|
+
advice: `remove oauth.${key}`
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
return warnings;
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
// src/cli/config-verbs.ts
|
|
2886
|
+
function parseHeader(hdr) {
|
|
2887
|
+
const colonIdx = hdr.indexOf(":");
|
|
2888
|
+
if (colonIdx < 1) return `invalid header '${hdr}'`;
|
|
2889
|
+
return [hdr.slice(0, colonIdx).trim(), hdr.slice(colonIdx + 1).trim()];
|
|
2890
|
+
}
|
|
2891
|
+
function parseEnvPair(pair) {
|
|
2892
|
+
const eqIdx = pair.indexOf("=");
|
|
2893
|
+
if (eqIdx < 1) return `invalid --env '${pair}'`;
|
|
2894
|
+
return [pair.slice(0, eqIdx), pair.slice(eqIdx + 1)];
|
|
2895
|
+
}
|
|
2896
|
+
function parseAddFlagAt(flagArgs, i, flags) {
|
|
2897
|
+
const arg = flagArgs[i];
|
|
2898
|
+
if (arg === "-l" || arg === "--local") {
|
|
2899
|
+
flags.local = true;
|
|
2900
|
+
return { next: i + 1 };
|
|
2901
|
+
}
|
|
2902
|
+
if (arg === "--no-probe") {
|
|
2903
|
+
flags.noProbe = true;
|
|
2904
|
+
return { next: i + 1 };
|
|
2905
|
+
}
|
|
2906
|
+
if (arg === "--config" && i + 1 < flagArgs.length) {
|
|
2907
|
+
flags.configFile = flagArgs[i + 1];
|
|
2908
|
+
return { next: i + 2 };
|
|
2909
|
+
}
|
|
2910
|
+
if (arg === "--description" && i + 1 < flagArgs.length) {
|
|
2911
|
+
flags.description = flagArgs[i + 1];
|
|
2912
|
+
return { next: i + 2 };
|
|
2913
|
+
}
|
|
2914
|
+
if (arg === "--exposure" && i + 1 < flagArgs.length) {
|
|
2915
|
+
flags.exposure = flagArgs[i + 1];
|
|
2916
|
+
return { next: i + 2 };
|
|
2917
|
+
}
|
|
2918
|
+
if ((arg === "-H" || arg === "--header") && i + 1 < flagArgs.length) {
|
|
2919
|
+
const result = parseHeader(flagArgs[i + 1]);
|
|
2920
|
+
if (typeof result === "string") return { next: i + 2, error: result };
|
|
2921
|
+
flags.headers.push(result);
|
|
2922
|
+
return { next: i + 2 };
|
|
2923
|
+
}
|
|
2924
|
+
if (arg === "--env" && i + 1 < flagArgs.length) {
|
|
2925
|
+
const result = parseEnvPair(flagArgs[i + 1]);
|
|
2926
|
+
if (typeof result === "string") return { next: i + 2, error: result };
|
|
2927
|
+
flags.envPairs.push(result);
|
|
2928
|
+
return { next: i + 2 };
|
|
2929
|
+
}
|
|
2930
|
+
if (arg === "--cwd" && i + 1 < flagArgs.length) {
|
|
2931
|
+
flags.cwd = flagArgs[i + 1];
|
|
2932
|
+
return { next: i + 2 };
|
|
2933
|
+
}
|
|
2934
|
+
if (arg.startsWith("-")) return { next: i + 1, error: `unknown option '${arg}'` };
|
|
2935
|
+
flags.positional.push(arg);
|
|
2936
|
+
return { next: i + 1 };
|
|
2937
|
+
}
|
|
2938
|
+
function parseAddFlags(args2) {
|
|
2939
|
+
const flags = {
|
|
2940
|
+
local: false,
|
|
2941
|
+
noProbe: false,
|
|
2942
|
+
headers: [],
|
|
2943
|
+
envPairs: [],
|
|
2944
|
+
positional: [],
|
|
2945
|
+
cmdArgs: []
|
|
2101
2946
|
};
|
|
2947
|
+
const dashDashIdx = args2.indexOf("--");
|
|
2948
|
+
const flagArgs = dashDashIdx >= 0 ? args2.slice(0, dashDashIdx) : args2;
|
|
2949
|
+
flags.cmdArgs = dashDashIdx >= 0 ? args2.slice(dashDashIdx + 1) : [];
|
|
2950
|
+
let i = 0;
|
|
2951
|
+
while (i < flagArgs.length) {
|
|
2952
|
+
const step = parseAddFlagAt(flagArgs, i, flags);
|
|
2953
|
+
if (step.error) return step.error;
|
|
2954
|
+
i = step.next;
|
|
2955
|
+
}
|
|
2956
|
+
return flags;
|
|
2957
|
+
}
|
|
2958
|
+
function buildStdioConfig(flags) {
|
|
2959
|
+
const config = {
|
|
2960
|
+
command: flags.cmdArgs[0],
|
|
2961
|
+
...flags.cmdArgs.length > 1 ? { args: flags.cmdArgs.slice(1) } : {}
|
|
2962
|
+
};
|
|
2963
|
+
if (flags.envPairs.length > 0) {
|
|
2964
|
+
const env = {};
|
|
2965
|
+
for (const [k, v] of flags.envPairs) env[k] = v;
|
|
2966
|
+
config.env = env;
|
|
2967
|
+
}
|
|
2968
|
+
if (flags.cwd) config.cwd = flags.cwd;
|
|
2969
|
+
if (flags.description) config.description = flags.description;
|
|
2970
|
+
if (flags.exposure) config.exposure = flags.exposure;
|
|
2971
|
+
return config;
|
|
2972
|
+
}
|
|
2973
|
+
function buildHttpConfig(urlStr, flags) {
|
|
2974
|
+
const config = { url: urlStr };
|
|
2975
|
+
if (flags.headers.length > 0) {
|
|
2976
|
+
const hdrs = {};
|
|
2977
|
+
for (const [k, v] of flags.headers) {
|
|
2978
|
+
if (literalSecret(k, v)) return `header '${k}' holds a literal secret; use \${NAME} instead`;
|
|
2979
|
+
hdrs[k] = v;
|
|
2980
|
+
}
|
|
2981
|
+
config.headers = hdrs;
|
|
2982
|
+
}
|
|
2983
|
+
if (flags.description) config.description = flags.description;
|
|
2984
|
+
if (flags.exposure) config.exposure = flags.exposure;
|
|
2985
|
+
return config;
|
|
2986
|
+
}
|
|
2987
|
+
async function runAdd(platform2, args2, probe) {
|
|
2988
|
+
const flags = parseAddFlags(args2);
|
|
2989
|
+
if (typeof flags === "string") {
|
|
2990
|
+
await platform2.write(2, `mcpcat add: ${flags}
|
|
2991
|
+
`);
|
|
2992
|
+
return 2;
|
|
2993
|
+
}
|
|
2994
|
+
if (flags.positional.length < 1) {
|
|
2995
|
+
await platform2.write(2, "mcpcat add: requires a server name\n");
|
|
2996
|
+
return 2;
|
|
2997
|
+
}
|
|
2998
|
+
let name;
|
|
2999
|
+
let config;
|
|
3000
|
+
if (flags.cmdArgs.length > 0) {
|
|
3001
|
+
name = flags.positional[0];
|
|
3002
|
+
config = buildStdioConfig(flags);
|
|
3003
|
+
} else if (flags.positional.length >= 2) {
|
|
3004
|
+
let urlStr;
|
|
3005
|
+
if (URL.canParse(flags.positional[0]) && !URL.canParse(flags.positional[1])) {
|
|
3006
|
+
urlStr = flags.positional[0];
|
|
3007
|
+
name = flags.positional[1];
|
|
3008
|
+
} else {
|
|
3009
|
+
name = flags.positional[0];
|
|
3010
|
+
urlStr = flags.positional[1];
|
|
3011
|
+
}
|
|
3012
|
+
if (!URL.canParse(urlStr)) {
|
|
3013
|
+
await platform2.write(2, `mcpcat add: '${urlStr}' is not a valid URL
|
|
3014
|
+
`);
|
|
3015
|
+
return 2;
|
|
3016
|
+
}
|
|
3017
|
+
const secret = urlHasSecret(urlStr);
|
|
3018
|
+
if (secret) {
|
|
3019
|
+
await platform2.write(
|
|
3020
|
+
2,
|
|
3021
|
+
`mcpcat add: ${secret} holds a literal secret; use \${NAME} in a header instead
|
|
3022
|
+
`
|
|
3023
|
+
);
|
|
3024
|
+
return 2;
|
|
3025
|
+
}
|
|
3026
|
+
const httpResult = buildHttpConfig(urlStr, flags);
|
|
3027
|
+
if (typeof httpResult === "string") {
|
|
3028
|
+
await platform2.write(2, `mcpcat add: ${httpResult}
|
|
3029
|
+
`);
|
|
3030
|
+
return 2;
|
|
3031
|
+
}
|
|
3032
|
+
config = httpResult;
|
|
3033
|
+
} else {
|
|
3034
|
+
await platform2.write(2, "mcpcat add: requires a URL or -- <command>\n");
|
|
3035
|
+
return 2;
|
|
3036
|
+
}
|
|
3037
|
+
const validation = validateServer(name, config);
|
|
3038
|
+
if ("message" in validation) {
|
|
3039
|
+
await platform2.write(2, `mcpcat add: ${validation.message}
|
|
3040
|
+
`);
|
|
3041
|
+
return 2;
|
|
3042
|
+
}
|
|
3043
|
+
const path = flags.configFile ?? (flags.local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
3044
|
+
const { replaced } = await addServer(platform2.fs, path, name, config);
|
|
3045
|
+
const warnings = checkAgentWarnings(name, config);
|
|
3046
|
+
for (const w of warnings) {
|
|
3047
|
+
await platform2.write(2, `mcpcat add: warning: ${w.message}; ${w.advice}
|
|
3048
|
+
`);
|
|
3049
|
+
}
|
|
3050
|
+
await platform2.write(1, `${replaced ? "replaced" : "added"} ${name} in ${path}
|
|
3051
|
+
`);
|
|
3052
|
+
if (!flags.noProbe && probe) {
|
|
3053
|
+
const reason = await probe(name, config, platform2);
|
|
3054
|
+
if (reason) {
|
|
3055
|
+
await platform2.write(2, `mcpcat add: probe failed: ${reason}
|
|
3056
|
+
`);
|
|
3057
|
+
return 3;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
return 0;
|
|
3061
|
+
}
|
|
3062
|
+
async function runRm(platform2, args2) {
|
|
3063
|
+
let configFile;
|
|
3064
|
+
let local = false;
|
|
3065
|
+
const names = [];
|
|
3066
|
+
for (let i = 0; i < args2.length; i++) {
|
|
3067
|
+
const arg = args2[i];
|
|
3068
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
3069
|
+
configFile = args2[++i];
|
|
3070
|
+
continue;
|
|
3071
|
+
}
|
|
3072
|
+
if (arg === "-l" || arg === "--local") {
|
|
3073
|
+
local = true;
|
|
3074
|
+
continue;
|
|
3075
|
+
}
|
|
3076
|
+
if (arg === "--logout") continue;
|
|
3077
|
+
if (arg.startsWith("-")) {
|
|
3078
|
+
await platform2.write(2, `mcpcat rm: unknown option '${arg}'
|
|
3079
|
+
`);
|
|
3080
|
+
return 2;
|
|
3081
|
+
}
|
|
3082
|
+
names.push(arg);
|
|
3083
|
+
}
|
|
3084
|
+
if (names.length === 0) {
|
|
3085
|
+
await platform2.write(2, "mcpcat rm: requires a server name\n");
|
|
3086
|
+
return 2;
|
|
3087
|
+
}
|
|
3088
|
+
const path = configFile ?? (local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
3089
|
+
for (const name of names) {
|
|
3090
|
+
const removed = await removeServer(platform2.fs, path, name);
|
|
3091
|
+
if (!removed) {
|
|
3092
|
+
await platform2.write(2, `mcpcat rm: '${name}' not found in ${path}
|
|
3093
|
+
`);
|
|
3094
|
+
return 1;
|
|
3095
|
+
}
|
|
3096
|
+
await platform2.write(1, `removed ${name} from ${path}
|
|
3097
|
+
`);
|
|
3098
|
+
}
|
|
3099
|
+
return 0;
|
|
3100
|
+
}
|
|
3101
|
+
function formatListEntry(entry) {
|
|
3102
|
+
const c = entry.config;
|
|
3103
|
+
const transport = "url" in c ? "http" : "stdio";
|
|
3104
|
+
const target = "url" in c ? c.url : c.command;
|
|
3105
|
+
const enabled = "enabled" in c && c.enabled === false ? " (disabled)" : "";
|
|
3106
|
+
const exp = c.exposure ? ` [${c.exposure}]` : "";
|
|
3107
|
+
return `${entry.name} ${transport} ${target}${enabled}${exp} (${entry.source})`;
|
|
3108
|
+
}
|
|
3109
|
+
async function printListJson(platform2, config) {
|
|
3110
|
+
const entries = {};
|
|
3111
|
+
for (const entry of config.servers) {
|
|
3112
|
+
entries[entry.name] = {
|
|
3113
|
+
...entry.config,
|
|
3114
|
+
source: entry.source,
|
|
3115
|
+
...entry.scope ? { scope: entry.scope } : {},
|
|
3116
|
+
...entry.override ? { override: entry.override } : {}
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
await platform2.write(1, `${JSON.stringify(entries, null, 2)}
|
|
3120
|
+
`);
|
|
3121
|
+
}
|
|
3122
|
+
async function printListText(platform2, config) {
|
|
3123
|
+
if (config.servers.length === 0 && config.problems.length === 0) {
|
|
3124
|
+
await platform2.write(1, "no servers configured\n");
|
|
3125
|
+
return;
|
|
3126
|
+
}
|
|
3127
|
+
for (const entry of config.servers) {
|
|
3128
|
+
await platform2.write(1, `${formatListEntry(entry)}
|
|
3129
|
+
`);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
async function runList(platform2, args2) {
|
|
3133
|
+
let json = false;
|
|
3134
|
+
let configFile;
|
|
3135
|
+
let trustProject = false;
|
|
3136
|
+
for (let i = 0; i < args2.length; i++) {
|
|
3137
|
+
const arg = args2[i];
|
|
3138
|
+
if (arg === "--json") {
|
|
3139
|
+
json = true;
|
|
3140
|
+
continue;
|
|
3141
|
+
}
|
|
3142
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
3143
|
+
configFile = args2[++i];
|
|
3144
|
+
continue;
|
|
3145
|
+
}
|
|
3146
|
+
if (arg === "--trust-project") {
|
|
3147
|
+
trustProject = true;
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
const source = await MergedConfigSource.load(platform2, { trustProject, configFile });
|
|
3151
|
+
const { config } = source;
|
|
3152
|
+
if (json) {
|
|
3153
|
+
await printListJson(platform2, config);
|
|
3154
|
+
} else {
|
|
3155
|
+
await printListText(platform2, config);
|
|
3156
|
+
}
|
|
3157
|
+
let hasProblems = false;
|
|
3158
|
+
for (const p of config.problems) {
|
|
3159
|
+
hasProblems = true;
|
|
3160
|
+
const prefix = p.server ? `${p.server}: ` : "";
|
|
3161
|
+
await platform2.write(2, `mcpcat list: ${prefix}${p.message}
|
|
3162
|
+
`);
|
|
3163
|
+
}
|
|
3164
|
+
return hasProblems ? 1 : 0;
|
|
3165
|
+
}
|
|
3166
|
+
async function runEnable(platform2, args2, enable) {
|
|
3167
|
+
let configFile;
|
|
3168
|
+
let local = false;
|
|
3169
|
+
const names = [];
|
|
3170
|
+
for (let i = 0; i < args2.length; i++) {
|
|
3171
|
+
const arg = args2[i];
|
|
3172
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
3173
|
+
configFile = args2[++i];
|
|
3174
|
+
continue;
|
|
3175
|
+
}
|
|
3176
|
+
if (arg === "-l" || arg === "--local") {
|
|
3177
|
+
local = true;
|
|
3178
|
+
continue;
|
|
3179
|
+
}
|
|
3180
|
+
if (arg.startsWith("-")) continue;
|
|
3181
|
+
names.push(arg);
|
|
3182
|
+
}
|
|
3183
|
+
if (names.length === 0) {
|
|
3184
|
+
await platform2.write(2, `mcpcat ${enable ? "enable" : "disable"}: requires a server name
|
|
3185
|
+
`);
|
|
3186
|
+
return 2;
|
|
3187
|
+
}
|
|
3188
|
+
const path = configFile ?? (local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
3189
|
+
for (const name of names) {
|
|
3190
|
+
const ok = await updateServer(platform2.fs, path, name, { enabled: enable });
|
|
3191
|
+
if (!ok) {
|
|
3192
|
+
await platform2.write(
|
|
3193
|
+
2,
|
|
3194
|
+
`mcpcat ${enable ? "enable" : "disable"}: '${name}' not found in ${path}
|
|
3195
|
+
`
|
|
3196
|
+
);
|
|
3197
|
+
return 1;
|
|
3198
|
+
}
|
|
3199
|
+
await platform2.write(1, `${enable ? "enabled" : "disabled"} ${name} in ${path}
|
|
3200
|
+
`);
|
|
3201
|
+
}
|
|
3202
|
+
return 0;
|
|
3203
|
+
}
|
|
3204
|
+
async function runExposure(platform2, args2) {
|
|
3205
|
+
let configFile;
|
|
3206
|
+
let local = false;
|
|
3207
|
+
let toolGlob;
|
|
3208
|
+
const positional = [];
|
|
3209
|
+
for (let i = 0; i < args2.length; i++) {
|
|
3210
|
+
const arg = args2[i];
|
|
3211
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
3212
|
+
configFile = args2[++i];
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
if (arg === "-l" || arg === "--local") {
|
|
3216
|
+
local = true;
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
if (arg === "--tool" && i + 1 < args2.length) {
|
|
3220
|
+
toolGlob = args2[++i];
|
|
3221
|
+
continue;
|
|
3222
|
+
}
|
|
3223
|
+
if (arg.startsWith("-")) continue;
|
|
3224
|
+
positional.push(arg);
|
|
3225
|
+
}
|
|
3226
|
+
if (positional.length < 2) {
|
|
3227
|
+
await platform2.write(2, "mcpcat exposure: requires <name> <mode>\n");
|
|
3228
|
+
return 2;
|
|
3229
|
+
}
|
|
3230
|
+
const [name, mode] = positional;
|
|
3231
|
+
const validModes = ["codemode", "deferred", "direct", "hidden"];
|
|
3232
|
+
if (!validModes.includes(mode)) {
|
|
3233
|
+
await platform2.write(2, `mcpcat exposure: mode must be one of ${validModes.join(", ")}
|
|
3234
|
+
`);
|
|
3235
|
+
return 2;
|
|
3236
|
+
}
|
|
3237
|
+
const path = configFile ?? (local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
3238
|
+
const ok = await setExposure(platform2.fs, path, name, mode, toolGlob);
|
|
3239
|
+
if (!ok) {
|
|
3240
|
+
await platform2.write(2, `mcpcat exposure: '${name}' not found in ${path}
|
|
3241
|
+
`);
|
|
3242
|
+
return 1;
|
|
3243
|
+
}
|
|
3244
|
+
const what = toolGlob ? `${name} tool ${toolGlob}` : name;
|
|
3245
|
+
await platform2.write(1, `set ${what} to ${mode} in ${path}
|
|
3246
|
+
`);
|
|
3247
|
+
return 0;
|
|
3248
|
+
}
|
|
3249
|
+
async function runImport(platform2, args2) {
|
|
3250
|
+
let configFile;
|
|
3251
|
+
let local = false;
|
|
3252
|
+
const positional = [];
|
|
3253
|
+
for (let i = 0; i < args2.length; i++) {
|
|
3254
|
+
const arg = args2[i];
|
|
3255
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
3256
|
+
configFile = args2[++i];
|
|
3257
|
+
continue;
|
|
3258
|
+
}
|
|
3259
|
+
if (arg === "-l" || arg === "--local") {
|
|
3260
|
+
local = true;
|
|
3261
|
+
continue;
|
|
3262
|
+
}
|
|
3263
|
+
if (arg.startsWith("-")) continue;
|
|
3264
|
+
positional.push(arg);
|
|
3265
|
+
}
|
|
3266
|
+
if (positional.length < 1) {
|
|
3267
|
+
await platform2.write(2, "mcpcat import: requires a file path\n");
|
|
3268
|
+
return 2;
|
|
3269
|
+
}
|
|
3270
|
+
const filePath = positional[0];
|
|
3271
|
+
const names = positional.slice(1);
|
|
3272
|
+
let text;
|
|
3273
|
+
try {
|
|
3274
|
+
const data = await platform2.fs.readFile(filePath);
|
|
3275
|
+
text = new TextDecoder().decode(data);
|
|
3276
|
+
} catch {
|
|
3277
|
+
await platform2.write(2, `mcpcat import: cannot read '${filePath}'
|
|
3278
|
+
`);
|
|
3279
|
+
return 2;
|
|
3280
|
+
}
|
|
3281
|
+
const result = importFile(text, filePath, names.length > 0 ? names : void 0);
|
|
3282
|
+
for (const err of result.errors) {
|
|
3283
|
+
await platform2.write(2, `mcpcat import: ${err}
|
|
3284
|
+
`);
|
|
3285
|
+
}
|
|
3286
|
+
if (result.servers.length === 0) {
|
|
3287
|
+
await platform2.write(2, "mcpcat import: no servers found\n");
|
|
3288
|
+
return 1;
|
|
3289
|
+
}
|
|
3290
|
+
const destPath = configFile ?? (local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
3291
|
+
for (const { name, config } of result.servers) {
|
|
3292
|
+
const validation = validateServer(name, config);
|
|
3293
|
+
if ("message" in validation) {
|
|
3294
|
+
await platform2.write(2, `mcpcat import: ${name}: ${validation.message}
|
|
3295
|
+
`);
|
|
3296
|
+
continue;
|
|
3297
|
+
}
|
|
3298
|
+
const { replaced } = await addServer(platform2.fs, destPath, name, config);
|
|
3299
|
+
await platform2.write(1, `${replaced ? "replaced" : "imported"} ${name}
|
|
3300
|
+
`);
|
|
3301
|
+
}
|
|
3302
|
+
return 0;
|
|
2102
3303
|
}
|
|
2103
3304
|
|
|
2104
3305
|
// src/cli/main.ts
|
|
@@ -2130,6 +3331,15 @@ var VERBS = [
|
|
|
2130
3331
|
"inbox",
|
|
2131
3332
|
"help"
|
|
2132
3333
|
];
|
|
3334
|
+
var CONFIG_VERBS = {
|
|
3335
|
+
add: true,
|
|
3336
|
+
rm: true,
|
|
3337
|
+
list: true,
|
|
3338
|
+
enable: true,
|
|
3339
|
+
disable: true,
|
|
3340
|
+
exposure: true,
|
|
3341
|
+
import: true
|
|
3342
|
+
};
|
|
2133
3343
|
var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
2134
3344
|
"resources",
|
|
2135
3345
|
"read",
|
|
@@ -2138,13 +3348,6 @@ var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
|
2138
3348
|
"grep",
|
|
2139
3349
|
"complete",
|
|
2140
3350
|
"task",
|
|
2141
|
-
"add",
|
|
2142
|
-
"rm",
|
|
2143
|
-
"list",
|
|
2144
|
-
"enable",
|
|
2145
|
-
"disable",
|
|
2146
|
-
"exposure",
|
|
2147
|
-
"import",
|
|
2148
3351
|
"login",
|
|
2149
3352
|
"logout",
|
|
2150
3353
|
"serve",
|
|
@@ -2166,12 +3369,12 @@ Client verbs:
|
|
|
2166
3369
|
describe <target> [tool] Tool schema and help
|
|
2167
3370
|
|
|
2168
3371
|
Configuration verbs:
|
|
2169
|
-
add <name> <url> Add a server to mcp.json
|
|
2170
|
-
rm <name> Remove a server
|
|
2171
|
-
list List configured servers
|
|
2172
|
-
enable|disable <name> Toggle a server
|
|
2173
|
-
exposure <name> <mode> Set exposure mode
|
|
2174
|
-
import <file> Import from other configs
|
|
3372
|
+
add <name> <url> Add a server to mcp.json
|
|
3373
|
+
rm <name> Remove a server
|
|
3374
|
+
list List configured servers
|
|
3375
|
+
enable|disable <name> Toggle a server
|
|
3376
|
+
exposure <name> <mode> Set exposure mode
|
|
3377
|
+
import <file> Import from other configs
|
|
2175
3378
|
login <target> OAuth sign-in (not implemented yet)
|
|
2176
3379
|
logout <target> Remove tokens (not implemented yet)
|
|
2177
3380
|
|
|
@@ -2430,6 +3633,55 @@ async function runVerb(platform2, parsed, conn) {
|
|
|
2430
3633
|
return 2;
|
|
2431
3634
|
}
|
|
2432
3635
|
}
|
|
3636
|
+
async function probeServer(_name, config, platform2) {
|
|
3637
|
+
try {
|
|
3638
|
+
const opts = { version: "probe" };
|
|
3639
|
+
if ("url" in config && config.url) {
|
|
3640
|
+
opts.url = config.url;
|
|
3641
|
+
if ("headers" in config) opts.headers = config.headers;
|
|
3642
|
+
} else if ("command" in config && config.command) {
|
|
3643
|
+
opts.command = config.command;
|
|
3644
|
+
if ("args" in config) opts.args = config.args;
|
|
3645
|
+
if ("env" in config) opts.env = config.env;
|
|
3646
|
+
if ("cwd" in config) opts.cwd = config.cwd;
|
|
3647
|
+
} else {
|
|
3648
|
+
return void 0;
|
|
3649
|
+
}
|
|
3650
|
+
const conn = await connect(platform2, opts);
|
|
3651
|
+
try {
|
|
3652
|
+
await conn.info();
|
|
3653
|
+
} finally {
|
|
3654
|
+
try {
|
|
3655
|
+
await conn.close();
|
|
3656
|
+
} catch {
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
return void 0;
|
|
3660
|
+
} catch (err) {
|
|
3661
|
+
return err.message;
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
async function dispatchConfigVerb(platform2, verb, args2) {
|
|
3665
|
+
const rest = args2.slice(1);
|
|
3666
|
+
switch (verb) {
|
|
3667
|
+
case "add":
|
|
3668
|
+
return runAdd(platform2, rest, probeServer);
|
|
3669
|
+
case "rm":
|
|
3670
|
+
return runRm(platform2, rest);
|
|
3671
|
+
case "list":
|
|
3672
|
+
return runList(platform2, rest);
|
|
3673
|
+
case "enable":
|
|
3674
|
+
return runEnable(platform2, rest, true);
|
|
3675
|
+
case "disable":
|
|
3676
|
+
return runEnable(platform2, rest, false);
|
|
3677
|
+
case "exposure":
|
|
3678
|
+
return runExposure(platform2, rest);
|
|
3679
|
+
case "import":
|
|
3680
|
+
return runImport(platform2, rest);
|
|
3681
|
+
default:
|
|
3682
|
+
return 1;
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
2433
3685
|
async function run(platform2, version, args2) {
|
|
2434
3686
|
if (args2.includes("--version") || args2.includes("-V")) {
|
|
2435
3687
|
await platform2.write(1, `${version}
|
|
@@ -2449,6 +3701,9 @@ async function run(platform2, version, args2) {
|
|
|
2449
3701
|
await platform2.write(2, "mcpcat: no verb or target given\n");
|
|
2450
3702
|
return 2;
|
|
2451
3703
|
}
|
|
3704
|
+
if (parsed.verb in CONFIG_VERBS) {
|
|
3705
|
+
return dispatchConfigVerb(platform2, parsed.verb, args2);
|
|
3706
|
+
}
|
|
2452
3707
|
if (UNIMPLEMENTED_VERBS.has(parsed.verb)) {
|
|
2453
3708
|
await platform2.write(2, `mcpcat: '${parsed.verb}' is not implemented yet
|
|
2454
3709
|
`);
|
|
@@ -2468,7 +3723,7 @@ async function run(platform2, version, args2) {
|
|
|
2468
3723
|
`);
|
|
2469
3724
|
return 2;
|
|
2470
3725
|
}
|
|
2471
|
-
const config =
|
|
3726
|
+
const config = await MergedConfigSource.load(platform2);
|
|
2472
3727
|
let resolved;
|
|
2473
3728
|
try {
|
|
2474
3729
|
resolved = await resolveTarget(
|
|
@@ -2516,7 +3771,7 @@ async function run(platform2, version, args2) {
|
|
|
2516
3771
|
// src/platform/host.ts
|
|
2517
3772
|
import { spawn as cpSpawn } from "node:child_process";
|
|
2518
3773
|
import { constants } from "node:fs";
|
|
2519
|
-
import { access, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3774
|
+
import { access, mkdir, readFile as readFile2, rename, rmdir, stat, writeFile } from "node:fs/promises";
|
|
2520
3775
|
import { Readable, Writable } from "node:stream";
|
|
2521
3776
|
import * as tty from "node:tty";
|
|
2522
3777
|
function nodeSpawn(options) {
|
|
@@ -2559,7 +3814,7 @@ function signalNumber(sig) {
|
|
|
2559
3814
|
}
|
|
2560
3815
|
var hostFs = {
|
|
2561
3816
|
async readFile(path) {
|
|
2562
|
-
return new Uint8Array(await
|
|
3817
|
+
return new Uint8Array(await readFile2(path));
|
|
2563
3818
|
},
|
|
2564
3819
|
async writeFile(path, data) {
|
|
2565
3820
|
await writeFile(path, data);
|
|
@@ -2581,6 +3836,12 @@ var hostFs = {
|
|
|
2581
3836
|
} catch {
|
|
2582
3837
|
return false;
|
|
2583
3838
|
}
|
|
3839
|
+
},
|
|
3840
|
+
async mkdirExclusive(path) {
|
|
3841
|
+
await mkdir(path, { recursive: false });
|
|
3842
|
+
},
|
|
3843
|
+
async rmdir(path) {
|
|
3844
|
+
await rmdir(path);
|
|
2584
3845
|
}
|
|
2585
3846
|
};
|
|
2586
3847
|
function createStdinStream() {
|
|
@@ -2622,5 +3883,5 @@ function createHostPlatform() {
|
|
|
2622
3883
|
// src/host.ts
|
|
2623
3884
|
var platform = createHostPlatform();
|
|
2624
3885
|
var args = process.argv.slice(2);
|
|
2625
|
-
var code = await run(platform, "0.
|
|
3886
|
+
var code = await run(platform, "0.3.0", args);
|
|
2626
3887
|
process.exit(code);
|