@ai-ecoverse/mcpcat 0.2.0 → 0.4.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 +2783 -51
- package/dist/kernel/mcpcat.mjs +2754 -50
- package/package.json +8 -4
package/dist/host/mcpcat.mjs
CHANGED
|
@@ -1273,7 +1273,7 @@ var DEFAULT_RECONNECT_MAX_DELAY_MS = 3e4;
|
|
|
1273
1273
|
var DEFAULT_RECONNECT_MAX_RETRIES = 5;
|
|
1274
1274
|
async function consumeSseStream(stream, options) {
|
|
1275
1275
|
const reader = stream.getReader();
|
|
1276
|
-
const
|
|
1276
|
+
const decoder2 = new TextDecoder();
|
|
1277
1277
|
let buffered = "";
|
|
1278
1278
|
let eventName;
|
|
1279
1279
|
let eventId;
|
|
@@ -1324,7 +1324,7 @@ async function consumeSseStream(stream, options) {
|
|
|
1324
1324
|
const { done, value } = await reader.read();
|
|
1325
1325
|
if (done)
|
|
1326
1326
|
break;
|
|
1327
|
-
buffered +=
|
|
1327
|
+
buffered += decoder2.decode(value, { stream: true });
|
|
1328
1328
|
let newline = buffered.indexOf("\n");
|
|
1329
1329
|
while (newline >= 0) {
|
|
1330
1330
|
processLine(buffered.slice(0, newline));
|
|
@@ -1334,7 +1334,7 @@ async function consumeSseStream(stream, options) {
|
|
|
1334
1334
|
if (Buffer.byteLength(buffered) > maxEventBytes)
|
|
1335
1335
|
throw new Error(`MCP SSE event exceeds ${maxEventBytes} bytes`);
|
|
1336
1336
|
}
|
|
1337
|
-
buffered +=
|
|
1337
|
+
buffered += decoder2.decode();
|
|
1338
1338
|
if (buffered)
|
|
1339
1339
|
processLine(buffered);
|
|
1340
1340
|
dispatch();
|
|
@@ -1862,16 +1862,16 @@ async function createStatelessConnection(options) {
|
|
|
1862
1862
|
}
|
|
1863
1863
|
if (res.status >= 400) {
|
|
1864
1864
|
const text = await res.text();
|
|
1865
|
-
let
|
|
1865
|
+
let rpcError2;
|
|
1866
1866
|
try {
|
|
1867
1867
|
const parsed = JSON.parse(text);
|
|
1868
1868
|
if (parsed.error && typeof parsed.error.code === "number") {
|
|
1869
|
-
|
|
1869
|
+
rpcError2 = parsed.error;
|
|
1870
1870
|
}
|
|
1871
1871
|
} catch {
|
|
1872
1872
|
}
|
|
1873
|
-
if (
|
|
1874
|
-
throw new StatelessRpcError(
|
|
1873
|
+
if (rpcError2) {
|
|
1874
|
+
throw new StatelessRpcError(rpcError2.code, rpcError2.message, rpcError2.data);
|
|
1875
1875
|
}
|
|
1876
1876
|
throw new StatelessHttpError(res.status, res.statusText, method, text.slice(0, 512));
|
|
1877
1877
|
}
|
|
@@ -1888,10 +1888,10 @@ async function createStatelessConnection(options) {
|
|
|
1888
1888
|
}
|
|
1889
1889
|
return frame.result;
|
|
1890
1890
|
}
|
|
1891
|
-
const
|
|
1891
|
+
const discoverResult2 = await request("server/discover", {
|
|
1892
1892
|
protocolVersion: STATELESS_PROTOCOL_VERSION
|
|
1893
1893
|
});
|
|
1894
|
-
const supported = Array.isArray(
|
|
1894
|
+
const supported = Array.isArray(discoverResult2?.supportedVersions) ? discoverResult2.supportedVersions.filter((v) => typeof v === "string") : [];
|
|
1895
1895
|
if (!supported.includes(STATELESS_PROTOCOL_VERSION)) {
|
|
1896
1896
|
throw new Error(
|
|
1897
1897
|
`Server does not support ${STATELESS_PROTOCOL_VERSION} (advertised: ${supported.join(", ") || "none"})`
|
|
@@ -1902,7 +1902,7 @@ async function createStatelessConnection(options) {
|
|
|
1902
1902
|
return {
|
|
1903
1903
|
async info() {
|
|
1904
1904
|
if (serverInfoCache) return serverInfoCache;
|
|
1905
|
-
const result =
|
|
1905
|
+
const result = discoverResult2;
|
|
1906
1906
|
serverInfoCache = {
|
|
1907
1907
|
name: result?.serverInfo?.name,
|
|
1908
1908
|
version: result?.serverInfo?.version,
|
|
@@ -2073,32 +2073,2626 @@ 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
|
+
}
|
|
2336
|
+
return {
|
|
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/expand.ts
|
|
2397
|
+
var PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
2398
|
+
var PLACEHOLDER_TEST = /\$\{[A-Za-z_][A-Za-z0-9_]*\}/;
|
|
2399
|
+
function containsPlaceholders(value) {
|
|
2400
|
+
return PLACEHOLDER_TEST.test(value);
|
|
2401
|
+
}
|
|
2402
|
+
function expandPlaceholdersStrict(value, env) {
|
|
2403
|
+
let hasUndefined = false;
|
|
2404
|
+
const result = value.replace(PLACEHOLDER, (_, name) => {
|
|
2405
|
+
if (env[name] === void 0) {
|
|
2406
|
+
hasUndefined = true;
|
|
2407
|
+
return "";
|
|
2408
|
+
}
|
|
2409
|
+
return env[name];
|
|
2410
|
+
});
|
|
2411
|
+
if (hasUndefined) return void 0;
|
|
2412
|
+
return result;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
// src/serve/catalog.ts
|
|
2416
|
+
var NAME_RE = /^[A-Za-z][\w-]*$/;
|
|
2417
|
+
function sanitizeName(raw) {
|
|
2418
|
+
const cleaned = raw.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2419
|
+
return cleaned || "cli";
|
|
2420
|
+
}
|
|
2421
|
+
function cliPrefix(help, filePath, explicit) {
|
|
2422
|
+
if (explicit) return sanitizeName(explicit);
|
|
2423
|
+
const fromUsage = usageProgramName(help);
|
|
2424
|
+
if (fromUsage) return sanitizeName(fromUsage);
|
|
2425
|
+
const base = filePath.split("/").pop() ?? filePath;
|
|
2426
|
+
return sanitizeName(base.replace(/\.jsh$/i, ""));
|
|
2427
|
+
}
|
|
2428
|
+
function parseHelp(help) {
|
|
2429
|
+
const block = commandsFromBlock(help);
|
|
2430
|
+
const program = usageProgramName(help);
|
|
2431
|
+
const commands = mergeCommandLists(block.commands, usageCommands(help, program));
|
|
2432
|
+
return {
|
|
2433
|
+
...program ? { program } : {},
|
|
2434
|
+
commands: applyScopedFlags(commands, block.scoped),
|
|
2435
|
+
globalFlags: block.globalFlags
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
function isGroupCandidate(command) {
|
|
2439
|
+
return command.path.length === 1 && command.positionals.length === 0 && command.flags.length === 0;
|
|
2440
|
+
}
|
|
2441
|
+
function nestGroup(commands, group, nested, topNames) {
|
|
2442
|
+
const children = attachGlobalFlags(nested.commands, nested.globalFlags);
|
|
2443
|
+
if (children.length === 0) return null;
|
|
2444
|
+
const childNames = children.map((child) => child.path[0] ?? "");
|
|
2445
|
+
const same = childNames.length === topNames.size && childNames.every((name) => topNames.has(name));
|
|
2446
|
+
if (same) return null;
|
|
2447
|
+
const nestedCommands = children.map((child) => ({ ...child, path: childPath(group, child) }));
|
|
2448
|
+
return commands.filter((command) => command.path[0] !== group).concat(nestedCommands);
|
|
2449
|
+
}
|
|
2450
|
+
function attachGlobalFlags(commands, globalFlags) {
|
|
2451
|
+
if (globalFlags.length === 0) return commands;
|
|
2452
|
+
return commands.map((command) => ({ ...command, flags: mergeFlags(command.flags, globalFlags) }));
|
|
2453
|
+
}
|
|
2454
|
+
function parseMcpOverride(stdout) {
|
|
2455
|
+
let value;
|
|
2456
|
+
try {
|
|
2457
|
+
value = JSON.parse(stdout);
|
|
2458
|
+
} catch {
|
|
2459
|
+
return null;
|
|
2460
|
+
}
|
|
2461
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2462
|
+
const commandsValue = Object.getOwnPropertyDescriptor(value, "commands")?.value;
|
|
2463
|
+
if (!Array.isArray(commandsValue)) return null;
|
|
2464
|
+
const commands = [];
|
|
2465
|
+
for (const entry of commandsValue) {
|
|
2466
|
+
const command = readMcpCommand(entry);
|
|
2467
|
+
if (command) commands.push(command);
|
|
2468
|
+
}
|
|
2469
|
+
if (commands.length === 0) return null;
|
|
2470
|
+
return { commands, globalFlags: [] };
|
|
2471
|
+
}
|
|
2472
|
+
function buildTools(prefix, commands) {
|
|
2473
|
+
const tools = [
|
|
2474
|
+
{
|
|
2475
|
+
name: `${prefix}_help`,
|
|
2476
|
+
description: `Show ${prefix} help.`,
|
|
2477
|
+
inputSchema: objectSchema({}),
|
|
2478
|
+
cli: prefix,
|
|
2479
|
+
kind: "help",
|
|
2480
|
+
commandPath: []
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
name: `${prefix}_invoke`,
|
|
2484
|
+
description: `Run ${prefix} with an argv array (one element per argument) and optional stdin. A non-zero exit is a tool error.`,
|
|
2485
|
+
inputSchema: invokeSchema(),
|
|
2486
|
+
cli: prefix,
|
|
2487
|
+
kind: "invoke",
|
|
2488
|
+
commandPath: []
|
|
2489
|
+
}
|
|
2490
|
+
];
|
|
2491
|
+
for (const command of commands) {
|
|
2492
|
+
const leaf = command.path.join("_");
|
|
2493
|
+
tools.push({
|
|
2494
|
+
name: `${prefix}_${leaf}`,
|
|
2495
|
+
description: command.description || `Run ${prefix} ${command.path.join(" ")}`,
|
|
2496
|
+
inputSchema: commandSchema(command),
|
|
2497
|
+
cli: prefix,
|
|
2498
|
+
kind: "command",
|
|
2499
|
+
commandPath: command.path
|
|
2500
|
+
});
|
|
2501
|
+
}
|
|
2502
|
+
return tools;
|
|
2503
|
+
}
|
|
2504
|
+
function readToolArguments(input) {
|
|
2505
|
+
if (input === void 0 || input === null) return { fields: [] };
|
|
2506
|
+
if (typeof input !== "object" || Array.isArray(input)) {
|
|
2507
|
+
return { fields: [], error: "arguments must be an object" };
|
|
2508
|
+
}
|
|
2509
|
+
const fields = [];
|
|
2510
|
+
let stdin;
|
|
2511
|
+
let passthrough;
|
|
2512
|
+
let argv;
|
|
2513
|
+
for (const key of Object.keys(input)) {
|
|
2514
|
+
const value = Object.getOwnPropertyDescriptor(input, key)?.value;
|
|
2515
|
+
const read = readArgument(key, value);
|
|
2516
|
+
if (read.error) return { fields, error: read.error };
|
|
2517
|
+
if (read.stdin !== void 0) stdin = read.stdin;
|
|
2518
|
+
else if (read.passthrough) passthrough = read.passthrough;
|
|
2519
|
+
else if (read.argv) argv = read.argv;
|
|
2520
|
+
else if (read.field) fields.push(read.field);
|
|
2521
|
+
}
|
|
2522
|
+
return {
|
|
2523
|
+
fields,
|
|
2524
|
+
...stdin !== void 0 ? { stdin } : {},
|
|
2525
|
+
...passthrough ? { passthrough } : {},
|
|
2526
|
+
...argv ? { argv } : {}
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
function argvForCommand(command, args2) {
|
|
2530
|
+
if (args2.error) return { error: args2.error };
|
|
2531
|
+
const argv = [...command.path];
|
|
2532
|
+
for (const positional of command.positionals) {
|
|
2533
|
+
const found = args2.fields.find((field) => field.name === positional.name);
|
|
2534
|
+
if (!found) {
|
|
2535
|
+
if (positional.required) return { error: `missing ${positional.name}` };
|
|
2536
|
+
continue;
|
|
2537
|
+
}
|
|
2538
|
+
if (typeof found.value !== "string" && typeof found.value !== "number") {
|
|
2539
|
+
return { error: `${positional.name} must be a string` };
|
|
2540
|
+
}
|
|
2541
|
+
argv.push(String(found.value));
|
|
2542
|
+
}
|
|
2543
|
+
const flags = appendFlags(command.flags, args2.fields);
|
|
2544
|
+
if ("error" in flags) return flags;
|
|
2545
|
+
argv.push(...flags.argv);
|
|
2546
|
+
if (args2.passthrough) argv.push(...args2.passthrough);
|
|
2547
|
+
return { argv, ...args2.stdin !== void 0 ? { stdin: args2.stdin } : {} };
|
|
2548
|
+
}
|
|
2549
|
+
function appendFlags(flags, fields) {
|
|
2550
|
+
const argv = [];
|
|
2551
|
+
for (const flag of flags) {
|
|
2552
|
+
const found = fields.find((field) => field.name === flag.name);
|
|
2553
|
+
if (!found || found.value === false) continue;
|
|
2554
|
+
const encoded = encodeFlag(flag, found.value);
|
|
2555
|
+
if ("error" in encoded) return encoded;
|
|
2556
|
+
argv.push(...encoded.argv);
|
|
2557
|
+
}
|
|
2558
|
+
return { argv };
|
|
2559
|
+
}
|
|
2560
|
+
function encodeFlag(flag, value) {
|
|
2561
|
+
if (flag.type === "boolean") {
|
|
2562
|
+
return value === true ? { argv: [`--${flag.name}`] } : { argv: [] };
|
|
2563
|
+
}
|
|
2564
|
+
if (flag.type === "string[]") {
|
|
2565
|
+
if (!Array.isArray(value)) return { error: `${flag.name} must be an array of strings` };
|
|
2566
|
+
const argv = [];
|
|
2567
|
+
for (const item of value) argv.push(`--${flag.name}`, item);
|
|
2568
|
+
return { argv };
|
|
2569
|
+
}
|
|
2570
|
+
if (typeof value !== "string" && typeof value !== "number") {
|
|
2571
|
+
return { error: `${flag.name} must be a string` };
|
|
2572
|
+
}
|
|
2573
|
+
return { argv: [`--${flag.name}`, String(value)] };
|
|
2574
|
+
}
|
|
2575
|
+
function commandsFromBlock(help) {
|
|
2576
|
+
let section = "none";
|
|
2577
|
+
const commands = [];
|
|
2578
|
+
const globalFlags = [];
|
|
2579
|
+
const scoped = [];
|
|
2580
|
+
for (const rawLine of help.split("\n")) {
|
|
2581
|
+
const line = rawLine.replace(/\t/g, " ");
|
|
2582
|
+
const header = classifyHeader(line);
|
|
2583
|
+
if (header) {
|
|
2584
|
+
section = header.kind === "other" ? "none" : header.kind;
|
|
2585
|
+
if (header.kind === "scoped" && header.name) scoped.push({ name: header.name, flags: [] });
|
|
2586
|
+
continue;
|
|
2587
|
+
}
|
|
2588
|
+
pushSectionLine(section, line, commands, globalFlags, scoped);
|
|
2589
|
+
}
|
|
2590
|
+
return { commands, globalFlags, scoped };
|
|
2591
|
+
}
|
|
2592
|
+
function pushSectionLine(section, line, commands, globalFlags, scoped) {
|
|
2593
|
+
if (section === "commands") {
|
|
2594
|
+
const command = parseCommandLine(line);
|
|
2595
|
+
if (command) commands.push(command);
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
2598
|
+
const flag = parseFlagLine(line);
|
|
2599
|
+
if (!flag) return;
|
|
2600
|
+
if (section === "options") globalFlags.push(flag);
|
|
2601
|
+
if (section === "scoped") scoped[scoped.length - 1]?.flags.push(flag);
|
|
2602
|
+
}
|
|
2603
|
+
function classifyHeader(line) {
|
|
2604
|
+
const text = line.trim();
|
|
2605
|
+
if (/^commands:?$/i.test(text)) return { kind: "commands" };
|
|
2606
|
+
if (/^options:?$/i.test(text)) return { kind: "options" };
|
|
2607
|
+
const flags = /^([A-Za-z][\w-]*)\s+flags:?$/i.exec(text);
|
|
2608
|
+
if (flags?.[1]) return { kind: "scoped", name: flags[1].toLowerCase() };
|
|
2609
|
+
if (/^[A-Z][A-Z0-9 ]+:$/.test(text)) return { kind: "other" };
|
|
2610
|
+
return null;
|
|
2611
|
+
}
|
|
2612
|
+
function parseCommandLine(line) {
|
|
2613
|
+
if (!/^\s{2,}\S/.test(line)) return null;
|
|
2614
|
+
const parts = line.trim().split(/\s{2,}/);
|
|
2615
|
+
if (parts.length < 2) return null;
|
|
2616
|
+
const tokens = tokenize(parts[0] ?? "");
|
|
2617
|
+
const name = tokens[0] ?? "";
|
|
2618
|
+
if (!NAME_RE.test(name) || name.startsWith("-")) return null;
|
|
2619
|
+
const spec = emptySpec();
|
|
2620
|
+
for (const token of tokens.slice(1)) parseUsageToken(token, spec);
|
|
2621
|
+
return {
|
|
2622
|
+
path: [name],
|
|
2623
|
+
description: parts.slice(1).join(" ").trim(),
|
|
2624
|
+
positionals: spec.positionals,
|
|
2625
|
+
flags: spec.flags
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
function usageProgramName(help) {
|
|
2629
|
+
for (const raw of help.split("\n")) {
|
|
2630
|
+
if (!/^[\s>*]*usage:/i.test(raw)) continue;
|
|
2631
|
+
const tokens = tokenize(raw);
|
|
2632
|
+
const name = tokens[0];
|
|
2633
|
+
if (name && NAME_RE.test(name)) return name;
|
|
2634
|
+
}
|
|
2635
|
+
return void 0;
|
|
2636
|
+
}
|
|
2637
|
+
function usageCommands(help, program) {
|
|
2638
|
+
const commands = [];
|
|
2639
|
+
for (const raw of help.split("\n")) {
|
|
2640
|
+
const command = usageCommand(raw, program);
|
|
2641
|
+
if (command) commands.push(command);
|
|
2642
|
+
}
|
|
2643
|
+
return commands;
|
|
2644
|
+
}
|
|
2645
|
+
function isCommandSlot(token) {
|
|
2646
|
+
const inner = token.replace(/^\[|\]$/g, "").replace(/^<|>$/g, "").toLowerCase();
|
|
2647
|
+
return inner === "command" || inner === "cmd" || inner === "subcommand" || inner === "verb";
|
|
2648
|
+
}
|
|
2649
|
+
function usageCommand(raw, program) {
|
|
2650
|
+
const line = raw.trim();
|
|
2651
|
+
const tokens = tokenize(line);
|
|
2652
|
+
if (tokens.length < 2) return null;
|
|
2653
|
+
const isUsage = /^usage:/i.test(line);
|
|
2654
|
+
if (!isUsage && tokens[0] !== program) return null;
|
|
2655
|
+
const path = [];
|
|
2656
|
+
let index = 1;
|
|
2657
|
+
while (index < tokens.length && path.length < 2 && NAME_RE.test(tokens[index] ?? "")) {
|
|
2658
|
+
path.push(tokens[index] ?? "");
|
|
2659
|
+
index++;
|
|
2660
|
+
}
|
|
2661
|
+
if (path.length === 0) return null;
|
|
2662
|
+
if (isCommandSlot(tokens[index] ?? "")) return null;
|
|
2663
|
+
const spec = emptySpec();
|
|
2664
|
+
for (const token of tokens.slice(index)) parseUsageToken(token, spec);
|
|
2665
|
+
return { path, description: "", positionals: spec.positionals, flags: spec.flags };
|
|
2666
|
+
}
|
|
2667
|
+
function emptySpec() {
|
|
2668
|
+
return { positionals: [], flags: [] };
|
|
2669
|
+
}
|
|
2670
|
+
function parseUsageToken(token, into) {
|
|
2671
|
+
const optional = token.startsWith("[") && token.endsWith("]");
|
|
2672
|
+
const inner = optional ? token.slice(1, -1).trim() : token;
|
|
2673
|
+
if (inner.startsWith("--")) {
|
|
2674
|
+
const flag = parseFlagToken(inner);
|
|
2675
|
+
if (flag) into.flags.push(flag);
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
if (inner.startsWith("<") && inner.endsWith(">")) {
|
|
2679
|
+
into.positionals.push({
|
|
2680
|
+
name: cleanName(inner.slice(1, -1)),
|
|
2681
|
+
required: !optional,
|
|
2682
|
+
description: ""
|
|
2683
|
+
});
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
if (NAME_RE.test(inner)) {
|
|
2687
|
+
into.positionals.push({ name: cleanName(inner), required: !optional, description: "" });
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
function parseFlagToken(inner) {
|
|
2691
|
+
const match = /^--([A-Za-z][\w-]*)(?:(?:=|\s+)(\S+))?$/.exec(inner.trim());
|
|
2692
|
+
if (!match?.[1]) return null;
|
|
2693
|
+
const value = match[2] ?? "";
|
|
2694
|
+
if (!value) return { name: match[1], type: "boolean", description: "" };
|
|
2695
|
+
return { name: match[1], type: value.includes("...") ? "string[]" : "string", description: "" };
|
|
2696
|
+
}
|
|
2697
|
+
function parseFlagLine(line) {
|
|
2698
|
+
const trimmed = line.trim();
|
|
2699
|
+
if (!trimmed.includes("--")) return null;
|
|
2700
|
+
const match = /--([A-Za-z][\w-]*)(?:=(\S+)|(?:\s+(<[^>]+>|\[[^\]]+\]|[A-Z][A-Z0-9_-]+)))?/.exec(
|
|
2701
|
+
trimmed
|
|
2702
|
+
);
|
|
2703
|
+
if (!match?.[1]) return null;
|
|
2704
|
+
const value = match[2] || match[3] || "";
|
|
2705
|
+
const repeatable = /\brepeat|\.\.\.|one or more/i.test(trimmed);
|
|
2706
|
+
const description = flagDescription(trimmed);
|
|
2707
|
+
if (!value && !repeatable) return { name: match[1], type: "boolean", description };
|
|
2708
|
+
return { name: match[1], type: repeatable ? "string[]" : "string", description };
|
|
2709
|
+
}
|
|
2710
|
+
function flagDescription(line) {
|
|
2711
|
+
const parts = line.trim().split(/\s{2,}/);
|
|
2712
|
+
return parts.length > 1 ? parts.slice(1).join(" ").trim() : "";
|
|
2713
|
+
}
|
|
2714
|
+
function tokenize(line) {
|
|
2715
|
+
const source = line.trim().replace(/^usage:\s*/i, "");
|
|
2716
|
+
const tokens = [];
|
|
2717
|
+
let index = 0;
|
|
2718
|
+
while (index < source.length) {
|
|
2719
|
+
if (source[index] === " ") {
|
|
2720
|
+
index++;
|
|
2721
|
+
continue;
|
|
2722
|
+
}
|
|
2723
|
+
const closer = source[index] === "[" ? "]" : source[index] === "<" ? ">" : "";
|
|
2724
|
+
if (closer) {
|
|
2725
|
+
const end = source.indexOf(closer, index);
|
|
2726
|
+
if (end === -1) {
|
|
2727
|
+
tokens.push(source.slice(index));
|
|
2728
|
+
break;
|
|
2729
|
+
}
|
|
2730
|
+
tokens.push(source.slice(index, end + 1));
|
|
2731
|
+
index = end + 1;
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
const next = source.indexOf(" ", index);
|
|
2735
|
+
if (next === -1) {
|
|
2736
|
+
tokens.push(source.slice(index));
|
|
2737
|
+
break;
|
|
2738
|
+
}
|
|
2739
|
+
tokens.push(source.slice(index, next));
|
|
2740
|
+
index = next;
|
|
2741
|
+
}
|
|
2742
|
+
return tokens;
|
|
2743
|
+
}
|
|
2744
|
+
function mergeCommandLists(primary, extra) {
|
|
2745
|
+
const merged = primary.map((command) => ({ ...command }));
|
|
2746
|
+
for (const command of extra) {
|
|
2747
|
+
const existing = merged.find((item) => samePath(item.path, command.path));
|
|
2748
|
+
if (!existing) {
|
|
2749
|
+
merged.push(command);
|
|
2750
|
+
continue;
|
|
2751
|
+
}
|
|
2752
|
+
if (existing.positionals.length === 0) existing.positionals = command.positionals;
|
|
2753
|
+
if (!existing.description) existing.description = command.description;
|
|
2754
|
+
existing.flags = mergeFlags(existing.flags, command.flags);
|
|
2755
|
+
}
|
|
2756
|
+
return merged;
|
|
2757
|
+
}
|
|
2758
|
+
function applyScopedFlags(commands, scoped) {
|
|
2759
|
+
return commands.map((command) => {
|
|
2760
|
+
const extra = scoped.find((scope) => scope.name === (command.path[0] ?? "").toLowerCase());
|
|
2761
|
+
if (!extra) return command;
|
|
2762
|
+
return { ...command, flags: mergeFlags(command.flags, extra.flags) };
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
function mergeFlags(left, right) {
|
|
2766
|
+
const merged = [...left];
|
|
2767
|
+
for (const flag of right) {
|
|
2768
|
+
if (!merged.some((existing) => existing.name === flag.name)) merged.push(flag);
|
|
2769
|
+
}
|
|
2770
|
+
return merged;
|
|
2771
|
+
}
|
|
2772
|
+
function samePath(left, right) {
|
|
2773
|
+
return left.length === right.length && left.every((part, index) => part === right[index]);
|
|
2774
|
+
}
|
|
2775
|
+
function childPath(group, child) {
|
|
2776
|
+
if (child.path[0] === group) return child.path.slice(0, 2);
|
|
2777
|
+
return [group, child.path[0] ?? ""].filter((part) => part.length > 0).slice(0, 2);
|
|
2778
|
+
}
|
|
2779
|
+
function cleanName(raw) {
|
|
2780
|
+
const cleaned = raw.replace(/[^A-Za-z0-9_-]+/g, "").replace(/^-+/, "");
|
|
2781
|
+
return cleaned || "arg";
|
|
2782
|
+
}
|
|
2783
|
+
function readMcpCommand(value) {
|
|
2784
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2785
|
+
const name = ownString(value, "name");
|
|
2786
|
+
if (!name) return null;
|
|
2787
|
+
const path = name.split(/\s+/).filter((part) => NAME_RE.test(part)).slice(0, 2);
|
|
2788
|
+
if (path.length === 0) return null;
|
|
2789
|
+
return {
|
|
2790
|
+
path,
|
|
2791
|
+
description: ownString(value, "description") ?? "",
|
|
2792
|
+
positionals: readMcpPositionals(Object.getOwnPropertyDescriptor(value, "positionals")?.value),
|
|
2793
|
+
flags: readMcpFlags(Object.getOwnPropertyDescriptor(value, "flags")?.value)
|
|
2794
|
+
};
|
|
2795
|
+
}
|
|
2796
|
+
function readMcpPositionals(value) {
|
|
2797
|
+
if (!Array.isArray(value)) return [];
|
|
2798
|
+
const positionals = [];
|
|
2799
|
+
for (const entry of value) {
|
|
2800
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
2801
|
+
const name = ownString(entry, "name");
|
|
2802
|
+
if (!name) continue;
|
|
2803
|
+
const required = Object.getOwnPropertyDescriptor(entry, "required")?.value;
|
|
2804
|
+
positionals.push({ name, required: required !== false, description: "" });
|
|
2805
|
+
}
|
|
2806
|
+
return positionals;
|
|
2807
|
+
}
|
|
2808
|
+
function readMcpFlags(value) {
|
|
2809
|
+
if (!Array.isArray(value)) return [];
|
|
2810
|
+
const flags = [];
|
|
2811
|
+
for (const entry of value) {
|
|
2812
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
|
|
2813
|
+
const name = ownString(entry, "name");
|
|
2814
|
+
const type = ownString(entry, "type");
|
|
2815
|
+
if (!name || type !== "boolean" && type !== "string" && type !== "string[]") continue;
|
|
2816
|
+
flags.push({ name, type, description: "" });
|
|
2817
|
+
}
|
|
2818
|
+
return flags;
|
|
2819
|
+
}
|
|
2820
|
+
function ownString(value, key) {
|
|
2821
|
+
const found = Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
2822
|
+
return typeof found === "string" && found.length > 0 ? found : void 0;
|
|
2823
|
+
}
|
|
2824
|
+
function objectSchema(properties, required) {
|
|
2825
|
+
return {
|
|
2826
|
+
type: "object",
|
|
2827
|
+
properties,
|
|
2828
|
+
additionalProperties: false,
|
|
2829
|
+
...required && required.length > 0 ? { required } : {}
|
|
2830
|
+
};
|
|
2831
|
+
}
|
|
2832
|
+
function invokeSchema() {
|
|
2833
|
+
return objectSchema(
|
|
2834
|
+
{
|
|
2835
|
+
argv: {
|
|
2836
|
+
type: "array",
|
|
2837
|
+
items: { type: "string" },
|
|
2838
|
+
description: "Arguments, one element per argument. Never a shell string."
|
|
2839
|
+
},
|
|
2840
|
+
stdin: { type: "string", description: "Optional standard input." }
|
|
2841
|
+
},
|
|
2842
|
+
["argv"]
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
function commandSchema(command) {
|
|
2846
|
+
const properties = {};
|
|
2847
|
+
const required = [];
|
|
2848
|
+
for (const positional of command.positionals) {
|
|
2849
|
+
properties[positional.name] = {
|
|
2850
|
+
type: "string",
|
|
2851
|
+
description: positional.description || positional.name
|
|
2852
|
+
};
|
|
2853
|
+
if (positional.required) required.push(positional.name);
|
|
2854
|
+
}
|
|
2855
|
+
for (const flag of command.flags) {
|
|
2856
|
+
properties[flag.name] = flagProperty(flag);
|
|
2857
|
+
}
|
|
2858
|
+
properties.stdin = { type: "string", description: "Optional standard input." };
|
|
2859
|
+
properties.passthrough = {
|
|
2860
|
+
type: "array",
|
|
2861
|
+
items: { type: "string" },
|
|
2862
|
+
description: "Extra arguments appended as-is."
|
|
2863
|
+
};
|
|
2864
|
+
return objectSchema(properties, required);
|
|
2865
|
+
}
|
|
2866
|
+
function flagProperty(flag) {
|
|
2867
|
+
if (flag.type === "boolean")
|
|
2868
|
+
return { type: "boolean", description: flag.description || flag.name };
|
|
2869
|
+
if (flag.type === "string[]") {
|
|
2870
|
+
return { type: "array", items: { type: "string" }, description: flag.description || flag.name };
|
|
2871
|
+
}
|
|
2872
|
+
return { type: "string", description: flag.description || flag.name };
|
|
2873
|
+
}
|
|
2874
|
+
function readArgument(key, value) {
|
|
2875
|
+
if (key === "stdin") {
|
|
2876
|
+
return typeof value === "string" ? { stdin: value } : { error: "stdin must be a string" };
|
|
2877
|
+
}
|
|
2878
|
+
if (key === "passthrough") return readStringArray(value, "passthrough");
|
|
2879
|
+
if (key === "argv") {
|
|
2880
|
+
const read = readStringArray(value, "argv");
|
|
2881
|
+
return read.passthrough ? { argv: read.passthrough } : read;
|
|
2882
|
+
}
|
|
2883
|
+
if (!isArgValue(value)) return { error: `${key} has an unsupported type` };
|
|
2884
|
+
return { field: { name: key, value } };
|
|
2885
|
+
}
|
|
2886
|
+
function readStringArray(value, label) {
|
|
2887
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) {
|
|
2888
|
+
return { error: `${label} must be an array of strings` };
|
|
2889
|
+
}
|
|
2890
|
+
return { passthrough: value };
|
|
2891
|
+
}
|
|
2892
|
+
function isArgValue(value) {
|
|
2893
|
+
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number")
|
|
2894
|
+
return true;
|
|
2895
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
2896
|
+
}
|
|
2897
|
+
function parseToolTemplate(spec) {
|
|
2898
|
+
const colonIdx = spec.indexOf(":");
|
|
2899
|
+
if (colonIdx <= 0) return null;
|
|
2900
|
+
const name = spec.slice(0, colonIdx).trim();
|
|
2901
|
+
const template = spec.slice(colonIdx + 1).trim();
|
|
2902
|
+
if (!name || !template) return null;
|
|
2903
|
+
const placeholders = [];
|
|
2904
|
+
const re = /\{(\w+)\}/g;
|
|
2905
|
+
let m;
|
|
2906
|
+
while ((m = re.exec(template)) !== null) {
|
|
2907
|
+
if (!placeholders.includes(m[1])) placeholders.push(m[1]);
|
|
2908
|
+
}
|
|
2909
|
+
const properties = {};
|
|
2910
|
+
for (const ph of placeholders) {
|
|
2911
|
+
properties[ph] = { type: "string", description: ph };
|
|
2912
|
+
}
|
|
2913
|
+
return {
|
|
2914
|
+
name,
|
|
2915
|
+
description: template,
|
|
2916
|
+
inputSchema: {
|
|
2917
|
+
type: "object",
|
|
2918
|
+
properties,
|
|
2919
|
+
required: placeholders,
|
|
2920
|
+
additionalProperties: false
|
|
2921
|
+
},
|
|
2922
|
+
cli: "",
|
|
2923
|
+
kind: "command",
|
|
2924
|
+
commandPath: []
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
function templateToArgv(template, args2) {
|
|
2928
|
+
let expanded = template;
|
|
2929
|
+
for (const [key, value] of Object.entries(args2)) {
|
|
2930
|
+
expanded = expanded.replaceAll(`{${key}}`, value);
|
|
2931
|
+
}
|
|
2932
|
+
return expanded.split(/\s+/).filter(Boolean);
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// src/serve/http.ts
|
|
2936
|
+
var encoder = new TextEncoder();
|
|
2937
|
+
var decoder = new TextDecoder();
|
|
2938
|
+
var MAX_HEADER_SIZE = 16384;
|
|
2939
|
+
var MAX_BODY_SIZE = 10485760;
|
|
2940
|
+
async function readUntil(reader, buf, bufLen, sentinel, maxSize) {
|
|
2941
|
+
for (; ; ) {
|
|
2942
|
+
const text = decoder.decode(new Uint8Array(buf.buffer, buf.byteOffset, bufLen));
|
|
2943
|
+
const pos = text.indexOf(sentinel);
|
|
2944
|
+
if (pos >= 0) return { buf, bufLen, pos };
|
|
2945
|
+
if (bufLen >= maxSize) return null;
|
|
2946
|
+
const { done, value } = await reader.read();
|
|
2947
|
+
if (done) return null;
|
|
2948
|
+
if (bufLen + value.length > buf.length) {
|
|
2949
|
+
const newBuf = new Uint8Array(Math.max(buf.length * 2, bufLen + value.length));
|
|
2950
|
+
newBuf.set(new Uint8Array(buf.buffer, buf.byteOffset, bufLen));
|
|
2951
|
+
buf = newBuf;
|
|
2952
|
+
}
|
|
2953
|
+
buf.set(value, bufLen);
|
|
2954
|
+
bufLen += value.length;
|
|
2955
|
+
}
|
|
2956
|
+
}
|
|
2957
|
+
async function readExact(reader, buf, bufLen, needed) {
|
|
2958
|
+
while (bufLen < needed) {
|
|
2959
|
+
const { done, value } = await reader.read();
|
|
2960
|
+
if (done) return null;
|
|
2961
|
+
if (bufLen + value.length > buf.length) {
|
|
2962
|
+
const newBuf = new Uint8Array(Math.max(buf.length * 2, bufLen + value.length));
|
|
2963
|
+
newBuf.set(new Uint8Array(buf.buffer, buf.byteOffset, bufLen));
|
|
2964
|
+
buf = newBuf;
|
|
2965
|
+
}
|
|
2966
|
+
buf.set(value, bufLen);
|
|
2967
|
+
bufLen += value.length;
|
|
2968
|
+
}
|
|
2969
|
+
return { buf, bufLen };
|
|
2970
|
+
}
|
|
2971
|
+
function parseRequestLine(buf, endPos) {
|
|
2972
|
+
const headText = decoder.decode(new Uint8Array(buf.buffer, buf.byteOffset, endPos));
|
|
2973
|
+
const lines = headText.split("\r\n");
|
|
2974
|
+
const requestLine = lines[0];
|
|
2975
|
+
if (!requestLine) return null;
|
|
2976
|
+
const parts = requestLine.split(" ");
|
|
2977
|
+
if (parts.length < 2) return null;
|
|
2978
|
+
const method = parts[0];
|
|
2979
|
+
const target = parts[1];
|
|
2980
|
+
const headers = {};
|
|
2981
|
+
for (let i = 1; i < lines.length; i++) {
|
|
2982
|
+
const line = lines[i];
|
|
2983
|
+
if (!line) continue;
|
|
2984
|
+
const colonIdx = line.indexOf(":");
|
|
2985
|
+
if (colonIdx <= 0) continue;
|
|
2986
|
+
headers[line.slice(0, colonIdx).trim().toLowerCase()] = line.slice(colonIdx + 1).trim();
|
|
2987
|
+
}
|
|
2988
|
+
return { method, target, headers, headerEnd: endPos + 4 };
|
|
2989
|
+
}
|
|
2990
|
+
async function handleConnection(readable, writable, handler) {
|
|
2991
|
+
const reader = readable.getReader();
|
|
2992
|
+
const writer = writable.getWriter();
|
|
2993
|
+
let buf = new Uint8Array(4096);
|
|
2994
|
+
let bufLen = 0;
|
|
2995
|
+
try {
|
|
2996
|
+
for (; ; ) {
|
|
2997
|
+
const headerResult = await readUntil(reader, buf, bufLen, "\r\n\r\n", MAX_HEADER_SIZE);
|
|
2998
|
+
if (!headerResult) break;
|
|
2999
|
+
buf = headerResult.buf;
|
|
3000
|
+
bufLen = headerResult.bufLen;
|
|
3001
|
+
const parsed = parseRequestLine(buf, headerResult.pos);
|
|
3002
|
+
if (!parsed) {
|
|
3003
|
+
await writeResponse(writer, {
|
|
3004
|
+
status: 400,
|
|
3005
|
+
reason: "Bad Request",
|
|
3006
|
+
headers: { "content-type": "text/plain" },
|
|
3007
|
+
body: "Bad Request"
|
|
3008
|
+
});
|
|
3009
|
+
break;
|
|
3010
|
+
}
|
|
3011
|
+
const contentLength = Number(parsed.headers["content-length"] ?? 0);
|
|
3012
|
+
if (contentLength > MAX_BODY_SIZE) {
|
|
3013
|
+
await writeResponse(writer, {
|
|
3014
|
+
status: 413,
|
|
3015
|
+
reason: "Payload Too Large",
|
|
3016
|
+
headers: { "content-type": "text/plain" },
|
|
3017
|
+
body: "Payload Too Large"
|
|
3018
|
+
});
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
3021
|
+
const bodyStart = parsed.headerEnd;
|
|
3022
|
+
const totalNeeded = bodyStart + contentLength;
|
|
3023
|
+
const bodyResult = await readExact(reader, buf, bufLen, totalNeeded);
|
|
3024
|
+
if (!bodyResult && contentLength > 0) break;
|
|
3025
|
+
if (bodyResult) {
|
|
3026
|
+
buf = bodyResult.buf;
|
|
3027
|
+
bufLen = bodyResult.bufLen;
|
|
3028
|
+
}
|
|
3029
|
+
const bodyBytes = buf.slice(bodyStart, bodyStart + contentLength);
|
|
3030
|
+
const body = decoder.decode(bodyBytes);
|
|
3031
|
+
const request = {
|
|
3032
|
+
method: parsed.method,
|
|
3033
|
+
target: parsed.target,
|
|
3034
|
+
headers: parsed.headers,
|
|
3035
|
+
body
|
|
3036
|
+
};
|
|
3037
|
+
const response = await handler(request);
|
|
3038
|
+
await writeResponse(writer, response);
|
|
3039
|
+
const connectionHeader = (parsed.headers.connection ?? "").toLowerCase();
|
|
3040
|
+
if (connectionHeader === "close") break;
|
|
3041
|
+
const consumed = bodyStart + contentLength;
|
|
3042
|
+
const remaining = bufLen - consumed;
|
|
3043
|
+
if (remaining > 0) {
|
|
3044
|
+
const newBuf = new Uint8Array(Math.max(4096, remaining));
|
|
3045
|
+
newBuf.set(new Uint8Array(buf.buffer, buf.byteOffset + consumed, remaining));
|
|
3046
|
+
buf = newBuf;
|
|
3047
|
+
bufLen = remaining;
|
|
3048
|
+
} else {
|
|
3049
|
+
buf = new Uint8Array(4096);
|
|
3050
|
+
bufLen = 0;
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
} finally {
|
|
3054
|
+
reader.releaseLock();
|
|
3055
|
+
try {
|
|
3056
|
+
await writer.close();
|
|
3057
|
+
} catch {
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
async function writeResponse(writer, response) {
|
|
3062
|
+
const bodyLength = new TextEncoder().encode(response.body).length;
|
|
3063
|
+
const headers = { ...response.headers, "content-length": String(bodyLength) };
|
|
3064
|
+
const lines = [`HTTP/1.1 ${response.status} ${response.reason}`];
|
|
3065
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
3066
|
+
lines.push(`${name}: ${value}`);
|
|
3067
|
+
}
|
|
3068
|
+
lines.push("", "");
|
|
3069
|
+
const headBytes = encoder.encode(lines.join("\r\n"));
|
|
3070
|
+
await writer.write(headBytes);
|
|
3071
|
+
if (response.body.length > 0) {
|
|
3072
|
+
const bodyBytes = encoder.encode(response.body);
|
|
3073
|
+
await writer.write(bodyBytes);
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
// src/serve/jsonrpc.ts
|
|
3078
|
+
var PROTOCOL_VERSIONS = ["2026-07-28", "2025-11-25", "2025-06-18"];
|
|
3079
|
+
var OUTPUT_CAP = 1048576;
|
|
3080
|
+
var JSON_CT = "application/json; charset=utf-8";
|
|
3081
|
+
async function handleJsonRpc(raw, tools, call, serverName, serverVersion) {
|
|
3082
|
+
let parsed;
|
|
3083
|
+
try {
|
|
3084
|
+
parsed = JSON.parse(raw);
|
|
3085
|
+
} catch {
|
|
3086
|
+
return rpcError(null, -32700, "Parse error");
|
|
3087
|
+
}
|
|
3088
|
+
if (Array.isArray(parsed)) return rpcError(null, -32600, "Batch requests are not supported");
|
|
3089
|
+
if (!parsed || typeof parsed !== "object") return rpcError(null, -32600, "Invalid request");
|
|
3090
|
+
const method = readString(parsed, "method");
|
|
3091
|
+
if (!method) return rpcError(readId(parsed), -32600, "Invalid request");
|
|
3092
|
+
if (!hasOwn(parsed, "id")) return { status: 202, contentType: "text/plain", body: "" };
|
|
3093
|
+
const id = readId(parsed);
|
|
3094
|
+
const params = Object.getOwnPropertyDescriptor(parsed, "params")?.value;
|
|
3095
|
+
if (method === "initialize")
|
|
3096
|
+
return rpcOk(id, initializeResult(params, serverName, serverVersion));
|
|
3097
|
+
if (method === "server/discover") return rpcOk(id, discoverResult(serverName, serverVersion));
|
|
3098
|
+
if (method === "tools/list") return rpcOk(id, { tools });
|
|
3099
|
+
if (method === "ping") return rpcOk(id, {});
|
|
3100
|
+
if (method === "tools/call") return toolCall(id, params, tools, call);
|
|
3101
|
+
return rpcError(id, -32601, `Method not found: ${method}`);
|
|
3102
|
+
}
|
|
3103
|
+
async function toolCall(id, params, tools, call) {
|
|
3104
|
+
const name = params && typeof params === "object" ? readString(params, "name") : void 0;
|
|
3105
|
+
if (!name) return rpcError(id, -32602, "tools/call requires a name");
|
|
3106
|
+
if (!tools.some((tool) => tool.name === name))
|
|
3107
|
+
return rpcError(id, -32602, `Unknown tool: ${name}`);
|
|
3108
|
+
const args2 = params && typeof params === "object" ? Object.getOwnPropertyDescriptor(params, "arguments")?.value : void 0;
|
|
3109
|
+
try {
|
|
3110
|
+
const result = await call(name, args2);
|
|
3111
|
+
return rpcOk(id, toolResult(result));
|
|
3112
|
+
} catch (err) {
|
|
3113
|
+
const message = err instanceof Error ? err.message : "tool call failed";
|
|
3114
|
+
return rpcError(id, -32e3, message);
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
function toolResult(result) {
|
|
3118
|
+
const stdout = cap(result.stdout);
|
|
3119
|
+
const stderr = cap(result.stderr);
|
|
3120
|
+
const text = stderr ? stdout ? `${stdout}
|
|
3121
|
+
${stderr}` : stderr : stdout;
|
|
3122
|
+
return {
|
|
3123
|
+
content: [{ type: "text", text }],
|
|
3124
|
+
isError: result.exitCode !== 0,
|
|
3125
|
+
structuredContent: { stdout, stderr, exitCode: result.exitCode }
|
|
3126
|
+
};
|
|
3127
|
+
}
|
|
3128
|
+
function initializeResult(params, serverName, serverVersion) {
|
|
3129
|
+
const requested = params && typeof params === "object" ? readString(params, "protocolVersion") : void 0;
|
|
3130
|
+
const protocolVersion = PROTOCOL_VERSIONS.find((version) => version === requested) ?? PROTOCOL_VERSIONS[0];
|
|
3131
|
+
return {
|
|
3132
|
+
protocolVersion,
|
|
3133
|
+
capabilities: { tools: { listChanged: false } },
|
|
3134
|
+
serverInfo: { name: serverName, version: serverVersion }
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
function discoverResult(serverName, serverVersion) {
|
|
3138
|
+
return {
|
|
3139
|
+
protocolVersion: PROTOCOL_VERSIONS[0],
|
|
3140
|
+
supportedVersions: PROTOCOL_VERSIONS,
|
|
3141
|
+
capabilities: { tools: { listChanged: false } },
|
|
3142
|
+
serverInfo: { name: serverName, version: serverVersion }
|
|
3143
|
+
};
|
|
3144
|
+
}
|
|
3145
|
+
function cap(value) {
|
|
3146
|
+
if (value.length <= OUTPUT_CAP) return value;
|
|
3147
|
+
const half = Math.floor(OUTPUT_CAP / 2);
|
|
3148
|
+
const cut = value.length - OUTPUT_CAP;
|
|
3149
|
+
return `${value.slice(0, half)}
|
|
3150
|
+
\u2026 ${cut} characters cut \u2026
|
|
3151
|
+
${value.slice(value.length - half)}`;
|
|
3152
|
+
}
|
|
3153
|
+
function rpcOk(id, result) {
|
|
3154
|
+
return {
|
|
3155
|
+
status: 200,
|
|
3156
|
+
contentType: JSON_CT,
|
|
3157
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, result })
|
|
3158
|
+
};
|
|
3159
|
+
}
|
|
3160
|
+
function rpcError(id, code2, message) {
|
|
2081
3161
|
return {
|
|
2082
|
-
|
|
3162
|
+
status: 200,
|
|
3163
|
+
contentType: JSON_CT,
|
|
3164
|
+
body: JSON.stringify({ jsonrpc: "2.0", id, error: { code: code2, message } })
|
|
3165
|
+
};
|
|
3166
|
+
}
|
|
3167
|
+
function readId(value) {
|
|
3168
|
+
const id = Object.getOwnPropertyDescriptor(value, "id")?.value;
|
|
3169
|
+
if (typeof id === "string" || typeof id === "number" || id === null) return id;
|
|
3170
|
+
return null;
|
|
3171
|
+
}
|
|
3172
|
+
function readString(value, key) {
|
|
3173
|
+
const found = Object.getOwnPropertyDescriptor(value, key)?.value;
|
|
3174
|
+
return typeof found === "string" ? found : void 0;
|
|
3175
|
+
}
|
|
3176
|
+
function hasOwn(value, key) {
|
|
3177
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
// src/serve/origin.ts
|
|
3181
|
+
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
3182
|
+
function isLoopbackOrigin(origin) {
|
|
3183
|
+
try {
|
|
3184
|
+
const url = new URL(origin);
|
|
3185
|
+
const hostname = url.hostname;
|
|
3186
|
+
if (LOOPBACK_HOSTS.has(hostname)) return true;
|
|
3187
|
+
if (hostname === "::1") return true;
|
|
3188
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
3189
|
+
return false;
|
|
3190
|
+
} catch {
|
|
3191
|
+
return false;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
function isLoopbackHost(host) {
|
|
3195
|
+
const hostname = host.replace(/:\d+$/, "");
|
|
3196
|
+
if (LOOPBACK_HOSTS.has(hostname)) return true;
|
|
3197
|
+
if (hostname === "::1") return true;
|
|
3198
|
+
if (hostname.endsWith(".localhost")) return true;
|
|
3199
|
+
return false;
|
|
3200
|
+
}
|
|
3201
|
+
function checkOrigin(origin, allowedOrigins, boundLoopback) {
|
|
3202
|
+
if (origin === void 0) return { ok: true };
|
|
3203
|
+
if (allowedOrigins.has(origin)) return { ok: true };
|
|
3204
|
+
if (isLoopbackOrigin(origin)) return { ok: true };
|
|
3205
|
+
if (!boundLoopback) return { ok: true };
|
|
3206
|
+
return { ok: false, reason: `origin ${origin} is not allowed` };
|
|
3207
|
+
}
|
|
3208
|
+
function checkHost(host, boundLoopback) {
|
|
3209
|
+
if (!boundLoopback) return { ok: true };
|
|
3210
|
+
if (!host) return { ok: false, reason: "missing Host header" };
|
|
3211
|
+
if (isLoopbackHost(host)) return { ok: true };
|
|
3212
|
+
return { ok: false, reason: `host ${host} is not loopback` };
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
// src/serve/runtime.ts
|
|
3216
|
+
var CALL_TIMEOUT_MS = 12e4;
|
|
3217
|
+
var MAX_OUTPUT_DEFAULT = 1048576;
|
|
3218
|
+
var ALLOWED_ENV_KEYS = /* @__PURE__ */ new Set(["PATH", "HOME", "LANG"]);
|
|
3219
|
+
function parseHttpFlag(args2, i) {
|
|
3220
|
+
const next = args2[i + 1];
|
|
3221
|
+
if (!next || next.startsWith("-")) return { host: "127.0.0.1", port: 0, next: i };
|
|
3222
|
+
const colonIdx = next.lastIndexOf(":");
|
|
3223
|
+
if (colonIdx > 0 && colonIdx < next.length - 1) {
|
|
3224
|
+
return { host: next.slice(0, colonIdx), port: Number(next.slice(colonIdx + 1)), next: i + 1 };
|
|
3225
|
+
}
|
|
3226
|
+
return { host: "127.0.0.1", port: Number(next), next: i + 1 };
|
|
3227
|
+
}
|
|
3228
|
+
function parseCommandTarget(arg) {
|
|
3229
|
+
const eqIdx = arg.indexOf("=");
|
|
3230
|
+
if (eqIdx > 0 && !arg.slice(0, eqIdx).includes("/")) {
|
|
3231
|
+
return { name: arg.slice(0, eqIdx), path: arg.slice(eqIdx + 1) };
|
|
3232
|
+
}
|
|
3233
|
+
return { path: arg };
|
|
3234
|
+
}
|
|
3235
|
+
function parseSingleFlag(args2, i, opts) {
|
|
3236
|
+
const arg = args2[i];
|
|
3237
|
+
if (arg === "--stdio") {
|
|
3238
|
+
opts.transport = "stdio";
|
|
3239
|
+
} else if (arg === "--http") {
|
|
3240
|
+
opts.transport = "http";
|
|
3241
|
+
const parsed = parseHttpFlag(args2, i);
|
|
3242
|
+
opts.host = parsed.host;
|
|
3243
|
+
opts.port = parsed.port;
|
|
3244
|
+
return parsed.next;
|
|
3245
|
+
} else if (arg === "--bearer" && args2[i + 1]) {
|
|
3246
|
+
opts.bearer = args2[++i];
|
|
3247
|
+
} else if (arg === "--allow-origin" && args2[i + 1]) {
|
|
3248
|
+
opts.allowOrigins.push(args2[++i]);
|
|
3249
|
+
} else if (arg === "--env" && args2[i + 1]) {
|
|
3250
|
+
opts.envAllow.push(args2[++i]);
|
|
3251
|
+
} else if (arg === "--tool" && args2[i + 1]) {
|
|
3252
|
+
opts.toolTemplates.push(args2[++i]);
|
|
3253
|
+
} else if (arg === "--call-timeout" && args2[i + 1]) {
|
|
3254
|
+
opts.callTimeout = Number(args2[++i]) * 1e3;
|
|
3255
|
+
} else if (arg === "--concurrency" && args2[i + 1]) {
|
|
3256
|
+
opts.concurrency = Number(args2[++i]);
|
|
3257
|
+
} else if (arg === "--max-output" && args2[i + 1]) {
|
|
3258
|
+
opts.maxOutput = Number(args2[++i]);
|
|
3259
|
+
} else if (arg === "--cwd" && args2[i + 1]) {
|
|
3260
|
+
opts.cwd = args2[++i];
|
|
3261
|
+
} else if (arg === "--json") {
|
|
3262
|
+
opts.json = true;
|
|
3263
|
+
} else if (!arg.startsWith("-")) {
|
|
3264
|
+
opts.commands.push(parseCommandTarget(arg));
|
|
3265
|
+
}
|
|
3266
|
+
return i;
|
|
3267
|
+
}
|
|
3268
|
+
function parseServeFlags(args2) {
|
|
3269
|
+
const opts = {
|
|
3270
|
+
transport: "stdio",
|
|
3271
|
+
host: "127.0.0.1",
|
|
3272
|
+
port: 0,
|
|
3273
|
+
bearer: void 0,
|
|
3274
|
+
allowOrigins: [],
|
|
3275
|
+
envAllow: [],
|
|
3276
|
+
toolTemplates: [],
|
|
3277
|
+
callTimeout: CALL_TIMEOUT_MS,
|
|
3278
|
+
concurrency: 1,
|
|
3279
|
+
maxOutput: MAX_OUTPUT_DEFAULT,
|
|
3280
|
+
cwd: void 0,
|
|
3281
|
+
json: false,
|
|
3282
|
+
commands: []
|
|
3283
|
+
};
|
|
3284
|
+
for (let i = 0; i < args2.length; i++) {
|
|
3285
|
+
i = parseSingleFlag(args2, i, opts);
|
|
3286
|
+
}
|
|
3287
|
+
return opts;
|
|
3288
|
+
}
|
|
3289
|
+
function expandBearerToken(template, env) {
|
|
3290
|
+
if (!containsPlaceholders(template)) return void 0;
|
|
3291
|
+
return expandPlaceholdersStrict(template, env);
|
|
3292
|
+
}
|
|
3293
|
+
function constantTimeEqual(a, b) {
|
|
3294
|
+
if (a.length !== b.length) return false;
|
|
3295
|
+
let mismatch = 0;
|
|
3296
|
+
for (let i = 0; i < a.length; i++) {
|
|
3297
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
3298
|
+
}
|
|
3299
|
+
return mismatch === 0;
|
|
3300
|
+
}
|
|
3301
|
+
function buildChildEnv(platform2, envAllow) {
|
|
3302
|
+
const env = {};
|
|
3303
|
+
for (const key of ALLOWED_ENV_KEYS) {
|
|
3304
|
+
if (platform2.env[key]) env[key] = platform2.env[key];
|
|
3305
|
+
}
|
|
3306
|
+
for (const entry of envAllow) {
|
|
3307
|
+
const eqIdx = entry.indexOf("=");
|
|
3308
|
+
if (eqIdx > 0) {
|
|
3309
|
+
env[entry.slice(0, eqIdx)] = entry.slice(eqIdx + 1);
|
|
3310
|
+
} else if (platform2.env[entry]) {
|
|
3311
|
+
env[entry] = platform2.env[entry];
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
return env;
|
|
3315
|
+
}
|
|
3316
|
+
function capOutput(value, limit) {
|
|
3317
|
+
if (value.length <= limit) return value;
|
|
3318
|
+
const half = Math.floor(limit / 2);
|
|
3319
|
+
const cut = value.length - limit;
|
|
3320
|
+
return `${value.slice(0, half)}
|
|
3321
|
+
\u2026 ${cut} characters cut \u2026
|
|
3322
|
+
${value.slice(value.length - half)}`;
|
|
3323
|
+
}
|
|
3324
|
+
var inflightProcs = /* @__PURE__ */ new Set();
|
|
3325
|
+
async function runCommand(platform2, cmdPath, argv, childEnv, stdin, timeoutMs, maxOutput, cwd) {
|
|
3326
|
+
let proc;
|
|
3327
|
+
try {
|
|
3328
|
+
proc = await platform2.spawn({
|
|
3329
|
+
argv: [cmdPath, ...argv],
|
|
3330
|
+
env: childEnv,
|
|
3331
|
+
cwd
|
|
3332
|
+
});
|
|
3333
|
+
} catch (err) {
|
|
3334
|
+
return { stdout: "", stderr: `spawn failed: ${err.message}`, exitCode: 127 };
|
|
3335
|
+
}
|
|
3336
|
+
inflightProcs.add(proc);
|
|
3337
|
+
if (stdin !== void 0) {
|
|
3338
|
+
const writer = proc.stdin.getWriter();
|
|
3339
|
+
await writer.write(new TextEncoder().encode(stdin));
|
|
3340
|
+
await writer.close();
|
|
3341
|
+
} else {
|
|
3342
|
+
try {
|
|
3343
|
+
await proc.stdin.getWriter().close();
|
|
3344
|
+
} catch {
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
const stdoutChunks = [];
|
|
3348
|
+
const stderrChunks = [];
|
|
3349
|
+
const readStream = async (stream, into) => {
|
|
3350
|
+
const r = stream.getReader();
|
|
3351
|
+
try {
|
|
3352
|
+
for (; ; ) {
|
|
3353
|
+
const { done, value } = await r.read();
|
|
3354
|
+
if (done) break;
|
|
3355
|
+
into.push(value);
|
|
3356
|
+
}
|
|
3357
|
+
} finally {
|
|
3358
|
+
r.releaseLock();
|
|
3359
|
+
}
|
|
3360
|
+
};
|
|
3361
|
+
const timer = new Promise(
|
|
3362
|
+
(resolve) => setTimeout(() => resolve("timeout"), timeoutMs)
|
|
3363
|
+
);
|
|
3364
|
+
const work = Promise.all([
|
|
3365
|
+
readStream(proc.stdout, stdoutChunks),
|
|
3366
|
+
readStream(proc.stderr, stderrChunks),
|
|
3367
|
+
proc.wait()
|
|
3368
|
+
]);
|
|
3369
|
+
const result = await Promise.race([work, timer]);
|
|
3370
|
+
if (result === "timeout") {
|
|
3371
|
+
proc.kill("SIGTERM");
|
|
3372
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
3373
|
+
try {
|
|
3374
|
+
proc.kill("SIGKILL");
|
|
3375
|
+
} catch {
|
|
3376
|
+
}
|
|
3377
|
+
inflightProcs.delete(proc);
|
|
3378
|
+
return { stdout: "", stderr: "timed out", exitCode: 124 };
|
|
3379
|
+
}
|
|
3380
|
+
inflightProcs.delete(proc);
|
|
3381
|
+
const [, , exitResult] = result;
|
|
3382
|
+
const dec = new TextDecoder();
|
|
3383
|
+
return {
|
|
3384
|
+
stdout: capOutput(dec.decode(concat(stdoutChunks)), maxOutput),
|
|
3385
|
+
stderr: capOutput(dec.decode(concat(stderrChunks)), maxOutput),
|
|
3386
|
+
exitCode: exitResult.status
|
|
3387
|
+
};
|
|
3388
|
+
}
|
|
3389
|
+
function concat(chunks) {
|
|
3390
|
+
const total = chunks.reduce((s, c) => s + c.length, 0);
|
|
3391
|
+
const out = new Uint8Array(total);
|
|
3392
|
+
let offset = 0;
|
|
3393
|
+
for (const c of chunks) {
|
|
3394
|
+
out.set(c, offset);
|
|
3395
|
+
offset += c.length;
|
|
3396
|
+
}
|
|
3397
|
+
return out;
|
|
3398
|
+
}
|
|
3399
|
+
async function killInflight() {
|
|
3400
|
+
for (const proc of inflightProcs) {
|
|
3401
|
+
try {
|
|
3402
|
+
proc.kill("SIGTERM");
|
|
3403
|
+
} catch {
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
if (inflightProcs.size > 0) {
|
|
3407
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
3408
|
+
for (const proc of inflightProcs) {
|
|
2083
3409
|
try {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
3410
|
+
proc.kill("SIGKILL");
|
|
3411
|
+
} catch {
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
inflightProcs.clear();
|
|
3416
|
+
}
|
|
3417
|
+
async function runHelpForCli(platform2, filePath, childEnv, cwd) {
|
|
3418
|
+
const dashed = await runCommand(
|
|
3419
|
+
platform2,
|
|
3420
|
+
filePath,
|
|
3421
|
+
["--help"],
|
|
3422
|
+
childEnv,
|
|
3423
|
+
void 0,
|
|
3424
|
+
1e4,
|
|
3425
|
+
MAX_OUTPUT_DEFAULT,
|
|
3426
|
+
cwd
|
|
3427
|
+
);
|
|
3428
|
+
if (dashed.exitCode === 0 && dashed.stdout.trim()) return dashed.stdout;
|
|
3429
|
+
const plain = await runCommand(
|
|
3430
|
+
platform2,
|
|
3431
|
+
filePath,
|
|
3432
|
+
["help"],
|
|
3433
|
+
childEnv,
|
|
3434
|
+
void 0,
|
|
3435
|
+
1e4,
|
|
3436
|
+
MAX_OUTPUT_DEFAULT,
|
|
3437
|
+
cwd
|
|
3438
|
+
);
|
|
3439
|
+
if (plain.exitCode === 0 && plain.stdout.trim()) return plain.stdout;
|
|
3440
|
+
return dashed.stdout.trim() ? dashed.stdout : "";
|
|
3441
|
+
}
|
|
3442
|
+
async function expandGroupCommands(platform2, filePath, commands, childEnv, cwd) {
|
|
3443
|
+
const topNames = new Set(commands.map((c) => c.path[0] ?? ""));
|
|
3444
|
+
let current = commands;
|
|
3445
|
+
for (const command of commands) {
|
|
3446
|
+
if (!isGroupCandidate(command)) continue;
|
|
3447
|
+
const group = command.path[0] ?? "";
|
|
3448
|
+
const nested = await runCommand(
|
|
3449
|
+
platform2,
|
|
3450
|
+
filePath,
|
|
3451
|
+
[group, "--help"],
|
|
3452
|
+
childEnv,
|
|
3453
|
+
void 0,
|
|
3454
|
+
1e4,
|
|
3455
|
+
MAX_OUTPUT_DEFAULT,
|
|
3456
|
+
cwd
|
|
3457
|
+
);
|
|
3458
|
+
if (nested.exitCode !== 0 || !nested.stdout.trim()) continue;
|
|
3459
|
+
const replaced = nestGroup(current, group, parseHelp(nested.stdout), topNames);
|
|
3460
|
+
if (replaced) current = replaced;
|
|
3461
|
+
}
|
|
3462
|
+
return current;
|
|
3463
|
+
}
|
|
3464
|
+
async function discoverCli(platform2, filePath, explicitName, childEnv, cwd) {
|
|
3465
|
+
const mcp = await runCommand(
|
|
3466
|
+
platform2,
|
|
3467
|
+
filePath,
|
|
3468
|
+
["--mcp"],
|
|
3469
|
+
childEnv,
|
|
3470
|
+
void 0,
|
|
3471
|
+
1e4,
|
|
3472
|
+
MAX_OUTPUT_DEFAULT,
|
|
3473
|
+
cwd
|
|
3474
|
+
);
|
|
3475
|
+
const override = mcp.exitCode === 0 ? parseMcpOverride(mcp.stdout) : null;
|
|
3476
|
+
const helpText2 = override ? "" : await runHelpForCli(platform2, filePath, childEnv, cwd);
|
|
3477
|
+
if (!override && !helpText2.trim()) {
|
|
3478
|
+
throw new Error(`${filePath} did not print help or --mcp descriptor`);
|
|
3479
|
+
}
|
|
3480
|
+
const parsed = override ?? parseHelp(helpText2);
|
|
3481
|
+
let commands = parsed.commands;
|
|
3482
|
+
if (!override) {
|
|
3483
|
+
commands = attachGlobalFlags(commands, parsed.globalFlags);
|
|
3484
|
+
commands = await expandGroupCommands(platform2, filePath, commands, childEnv, cwd);
|
|
3485
|
+
}
|
|
3486
|
+
return { name: cliPrefix(helpText2, filePath, explicitName), path: filePath, commands };
|
|
3487
|
+
}
|
|
3488
|
+
function createToolExecutor(platform2, clis, allTools, childEnv, options, serveCwd) {
|
|
3489
|
+
const rpcTools = allTools.map((t) => ({
|
|
3490
|
+
name: t.name,
|
|
3491
|
+
description: t.description,
|
|
3492
|
+
inputSchema: t.inputSchema
|
|
3493
|
+
}));
|
|
3494
|
+
const queues = /* @__PURE__ */ new Map();
|
|
3495
|
+
function enqueue(key, job) {
|
|
3496
|
+
const previous = queues.get(key) ?? Promise.resolve();
|
|
3497
|
+
const run2 = previous.then(job, job);
|
|
3498
|
+
queues.set(
|
|
3499
|
+
key,
|
|
3500
|
+
run2.then(
|
|
3501
|
+
() => void 0,
|
|
3502
|
+
() => void 0
|
|
3503
|
+
)
|
|
3504
|
+
);
|
|
3505
|
+
return run2;
|
|
3506
|
+
}
|
|
3507
|
+
function invokeTemplateTool(tool, args2) {
|
|
3508
|
+
const templateArgs = {};
|
|
3509
|
+
if (args2 && typeof args2 === "object") {
|
|
3510
|
+
for (const [k, v] of Object.entries(args2)) {
|
|
3511
|
+
templateArgs[k] = String(v);
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
const argv = templateToArgv(tool.description, templateArgs);
|
|
3515
|
+
return enqueue(
|
|
3516
|
+
"__template__",
|
|
3517
|
+
() => runCommand(
|
|
3518
|
+
platform2,
|
|
3519
|
+
argv[0],
|
|
3520
|
+
argv.slice(1),
|
|
3521
|
+
childEnv,
|
|
3522
|
+
void 0,
|
|
3523
|
+
options.callTimeout,
|
|
3524
|
+
options.maxOutput,
|
|
3525
|
+
serveCwd
|
|
3526
|
+
)
|
|
3527
|
+
);
|
|
3528
|
+
}
|
|
3529
|
+
async function executeTool(tool, cli, args2) {
|
|
3530
|
+
if (tool.kind === "help") {
|
|
3531
|
+
const result = await runCommand(
|
|
3532
|
+
platform2,
|
|
3533
|
+
cli.path,
|
|
3534
|
+
["--help"],
|
|
3535
|
+
childEnv,
|
|
3536
|
+
void 0,
|
|
3537
|
+
options.callTimeout,
|
|
3538
|
+
options.maxOutput,
|
|
3539
|
+
serveCwd
|
|
3540
|
+
);
|
|
3541
|
+
if (result.exitCode === 0) return result;
|
|
3542
|
+
return runCommand(
|
|
3543
|
+
platform2,
|
|
3544
|
+
cli.path,
|
|
3545
|
+
["help"],
|
|
3546
|
+
childEnv,
|
|
3547
|
+
void 0,
|
|
3548
|
+
options.callTimeout,
|
|
3549
|
+
options.maxOutput,
|
|
3550
|
+
serveCwd
|
|
3551
|
+
);
|
|
3552
|
+
}
|
|
3553
|
+
if (tool.kind === "invoke") {
|
|
3554
|
+
const parsed2 = readToolArguments(args2 ?? {});
|
|
3555
|
+
if (parsed2.error) return { stdout: "", stderr: parsed2.error, exitCode: 2 };
|
|
3556
|
+
if (!parsed2.argv) return { stdout: "", stderr: "argv is required", exitCode: 2 };
|
|
3557
|
+
return runCommand(
|
|
3558
|
+
platform2,
|
|
3559
|
+
cli.path,
|
|
3560
|
+
parsed2.argv,
|
|
3561
|
+
childEnv,
|
|
3562
|
+
parsed2.stdin,
|
|
3563
|
+
options.callTimeout,
|
|
3564
|
+
options.maxOutput,
|
|
3565
|
+
serveCwd
|
|
3566
|
+
);
|
|
3567
|
+
}
|
|
3568
|
+
const parsed = readToolArguments(args2 ?? {});
|
|
3569
|
+
const command = cli.commands.find((c) => c.path.join("_") === tool.commandPath.join("_"));
|
|
3570
|
+
if (!command) return { stdout: "", stderr: `unknown command ${tool.name}`, exitCode: 2 };
|
|
3571
|
+
const planned = argvForCommand(command, parsed);
|
|
3572
|
+
if ("error" in planned) return { stdout: "", stderr: planned.error, exitCode: 2 };
|
|
3573
|
+
return runCommand(
|
|
3574
|
+
platform2,
|
|
3575
|
+
cli.path,
|
|
3576
|
+
planned.argv,
|
|
3577
|
+
childEnv,
|
|
3578
|
+
planned.stdin,
|
|
3579
|
+
options.callTimeout,
|
|
3580
|
+
options.maxOutput,
|
|
3581
|
+
serveCwd
|
|
3582
|
+
);
|
|
3583
|
+
}
|
|
3584
|
+
async function invoke(name, args2) {
|
|
3585
|
+
const tool = allTools.find((t) => t.name === name);
|
|
3586
|
+
if (!tool) return { stdout: "", stderr: `unknown tool ${name}`, exitCode: 1 };
|
|
3587
|
+
if (tool.cli === "" && tool.commandPath.length === 0) {
|
|
3588
|
+
return invokeTemplateTool(tool, args2);
|
|
3589
|
+
}
|
|
3590
|
+
const cli = clis.find((c) => c.name === tool.cli);
|
|
3591
|
+
if (!cli) return { stdout: "", stderr: `unknown cli ${tool.cli}`, exitCode: 1 };
|
|
3592
|
+
return enqueue(cli.name, () => executeTool(tool, cli, args2));
|
|
3593
|
+
}
|
|
3594
|
+
return { invoke, rpcTools };
|
|
3595
|
+
}
|
|
3596
|
+
async function runServe(platform2, options, version) {
|
|
3597
|
+
let resolvedBearer;
|
|
3598
|
+
if (options.bearer) {
|
|
3599
|
+
const expanded = expandBearerToken(options.bearer, platform2.env);
|
|
3600
|
+
if (!expanded) {
|
|
3601
|
+
await platform2.write(
|
|
3602
|
+
2,
|
|
3603
|
+
"mcpcat serve: --bearer requires a ${NAME} variable reference, not a literal token. Use --bearer '${MY_TOKEN}' and set MY_TOKEN in the environment.\n"
|
|
3604
|
+
);
|
|
3605
|
+
return 2;
|
|
3606
|
+
}
|
|
3607
|
+
resolvedBearer = expanded;
|
|
3608
|
+
}
|
|
3609
|
+
const serveCwd = options.cwd ?? platform2.cwd;
|
|
3610
|
+
const childEnv = buildChildEnv(platform2, options.envAllow);
|
|
3611
|
+
const clis = [];
|
|
3612
|
+
for (const cmd of options.commands) {
|
|
3613
|
+
try {
|
|
3614
|
+
const cli = await discoverCli(platform2, cmd.path, cmd.name, childEnv, serveCwd);
|
|
3615
|
+
clis.push(cli);
|
|
3616
|
+
} catch (err) {
|
|
3617
|
+
await platform2.write(2, `mcpcat serve: ${err.message}
|
|
3618
|
+
`);
|
|
3619
|
+
return 2;
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
const allTools = clis.flatMap((cli) => buildTools(cli.name, cli.commands));
|
|
3623
|
+
for (const spec of options.toolTemplates) {
|
|
3624
|
+
const tool = parseToolTemplate(spec);
|
|
3625
|
+
if (!tool) {
|
|
3626
|
+
await platform2.write(2, `mcpcat serve: invalid --tool spec: ${spec}
|
|
3627
|
+
`);
|
|
3628
|
+
return 2;
|
|
3629
|
+
}
|
|
3630
|
+
allTools.push(tool);
|
|
3631
|
+
}
|
|
3632
|
+
const executor = createToolExecutor(platform2, clis, allTools, childEnv, options, serveCwd);
|
|
3633
|
+
const serverName = "mcpcat-serve";
|
|
3634
|
+
if (options.transport === "stdio") {
|
|
3635
|
+
return runStdioServer(platform2, executor.rpcTools, executor.invoke, serverName, version);
|
|
3636
|
+
}
|
|
3637
|
+
return runHttpServer(
|
|
3638
|
+
platform2,
|
|
3639
|
+
options,
|
|
3640
|
+
executor.rpcTools,
|
|
3641
|
+
executor.invoke,
|
|
3642
|
+
serverName,
|
|
3643
|
+
version,
|
|
3644
|
+
resolvedBearer
|
|
3645
|
+
);
|
|
3646
|
+
}
|
|
3647
|
+
async function runStdioServer(platform2, tools, call, serverName, version) {
|
|
3648
|
+
const reader = platform2.stdin.getReader();
|
|
3649
|
+
let buffer = "";
|
|
3650
|
+
const dec = new TextDecoder();
|
|
3651
|
+
const enc = new TextEncoder();
|
|
3652
|
+
try {
|
|
3653
|
+
for (; ; ) {
|
|
3654
|
+
const { done, value } = await reader.read();
|
|
3655
|
+
if (done) break;
|
|
3656
|
+
buffer += dec.decode(value, { stream: true });
|
|
3657
|
+
let nlIdx;
|
|
3658
|
+
while ((nlIdx = buffer.indexOf("\n")) >= 0) {
|
|
3659
|
+
const line = buffer.slice(0, nlIdx).trim();
|
|
3660
|
+
buffer = buffer.slice(nlIdx + 1);
|
|
3661
|
+
if (!line) continue;
|
|
3662
|
+
const outcome = await handleJsonRpc(line, tools, call, serverName, version);
|
|
3663
|
+
if (outcome.status === 202) continue;
|
|
3664
|
+
const writer = platform2.stdout.getWriter();
|
|
3665
|
+
await writer.write(enc.encode(`${outcome.body}
|
|
3666
|
+
`));
|
|
3667
|
+
writer.releaseLock();
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
} finally {
|
|
3671
|
+
reader.releaseLock();
|
|
3672
|
+
}
|
|
3673
|
+
return 0;
|
|
3674
|
+
}
|
|
3675
|
+
async function runHttpServer(platform2, options, tools, call, serverName, version, resolvedBearer) {
|
|
3676
|
+
if (!platform2.listen) {
|
|
3677
|
+
await platform2.write(2, "mcpcat serve: HTTP transport requires platform.listen\n");
|
|
3678
|
+
return 3;
|
|
3679
|
+
}
|
|
3680
|
+
const boundLoopback = options.host === "127.0.0.1" || options.host === "localhost" || options.host === "::1";
|
|
3681
|
+
if (!boundLoopback && !resolvedBearer) {
|
|
3682
|
+
await platform2.write(2, "mcpcat serve: --bearer is required when binding off loopback\n");
|
|
3683
|
+
return 2;
|
|
3684
|
+
}
|
|
3685
|
+
const allowedOrigins = new Set(options.allowOrigins);
|
|
3686
|
+
const listener = await platform2.listen({ host: options.host, port: options.port });
|
|
3687
|
+
const actualPort = listener.port;
|
|
3688
|
+
const listenUrl = `http://${options.host}:${actualPort}/mcp`;
|
|
3689
|
+
if (options.json) {
|
|
3690
|
+
await platform2.write(1, `${JSON.stringify({ url: listenUrl })}
|
|
3691
|
+
`);
|
|
3692
|
+
}
|
|
3693
|
+
await platform2.write(2, `mcpcat serve: listening on ${listenUrl}
|
|
3694
|
+
`);
|
|
3695
|
+
let shutdownRequested = false;
|
|
3696
|
+
const shutdown = () => {
|
|
3697
|
+
if (shutdownRequested) return;
|
|
3698
|
+
shutdownRequested = true;
|
|
3699
|
+
listener.close();
|
|
3700
|
+
void killInflight();
|
|
3701
|
+
};
|
|
3702
|
+
if (platform2.onSignal) {
|
|
3703
|
+
platform2.onSignal("SIGINT", shutdown);
|
|
3704
|
+
platform2.onSignal("SIGTERM", shutdown);
|
|
3705
|
+
}
|
|
3706
|
+
const handler = (request) => handleMcpRequest(
|
|
3707
|
+
request,
|
|
3708
|
+
tools,
|
|
3709
|
+
call,
|
|
3710
|
+
serverName,
|
|
3711
|
+
version,
|
|
3712
|
+
allowedOrigins,
|
|
3713
|
+
boundLoopback,
|
|
3714
|
+
resolvedBearer
|
|
3715
|
+
);
|
|
3716
|
+
try {
|
|
3717
|
+
for await (const connection of listener) {
|
|
3718
|
+
if (shutdownRequested) break;
|
|
3719
|
+
const conn = connection;
|
|
3720
|
+
handleConnection(conn.readable, conn.writable, handler).catch(() => {
|
|
3721
|
+
}).finally(() => {
|
|
3722
|
+
try {
|
|
3723
|
+
conn.close();
|
|
3724
|
+
} catch {
|
|
2095
3725
|
}
|
|
2096
|
-
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
} catch {
|
|
3729
|
+
if (!shutdownRequested) throw new Error("listener closed unexpectedly");
|
|
3730
|
+
}
|
|
3731
|
+
return 0;
|
|
3732
|
+
}
|
|
3733
|
+
async function handleMcpRequest(request, tools, call, serverName, version, allowedOrigins, boundLoopback, resolvedBearer) {
|
|
3734
|
+
if (resolvedBearer) {
|
|
3735
|
+
const authHeader = request.headers.authorization;
|
|
3736
|
+
const provided = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
3737
|
+
if (!provided || !constantTimeEqual(provided, resolvedBearer)) {
|
|
3738
|
+
return {
|
|
3739
|
+
status: 401,
|
|
3740
|
+
reason: "Unauthorized",
|
|
3741
|
+
headers: { "content-type": "text/plain" },
|
|
3742
|
+
body: "Unauthorized"
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
const originCheck = checkOrigin(request.headers.origin, allowedOrigins, boundLoopback);
|
|
3747
|
+
if (!originCheck.ok) {
|
|
3748
|
+
return {
|
|
3749
|
+
status: 403,
|
|
3750
|
+
reason: "Forbidden",
|
|
3751
|
+
headers: { "content-type": "text/plain" },
|
|
3752
|
+
body: originCheck.reason ?? "Forbidden"
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
const hostCheck = checkHost(request.headers.host, boundLoopback);
|
|
3756
|
+
if (!hostCheck.ok) {
|
|
3757
|
+
return {
|
|
3758
|
+
status: 403,
|
|
3759
|
+
reason: "Forbidden",
|
|
3760
|
+
headers: { "content-type": "text/plain" },
|
|
3761
|
+
body: hostCheck.reason ?? "Forbidden"
|
|
3762
|
+
};
|
|
3763
|
+
}
|
|
3764
|
+
if (request.target !== "/mcp") {
|
|
3765
|
+
return {
|
|
3766
|
+
status: 404,
|
|
3767
|
+
reason: "Not Found",
|
|
3768
|
+
headers: { "content-type": "text/plain" },
|
|
3769
|
+
body: "Not Found"
|
|
3770
|
+
};
|
|
3771
|
+
}
|
|
3772
|
+
if (request.method !== "POST") {
|
|
3773
|
+
return {
|
|
3774
|
+
status: 405,
|
|
3775
|
+
reason: "Method Not Allowed",
|
|
3776
|
+
headers: { "content-type": "text/plain", allow: "POST" },
|
|
3777
|
+
body: "Method Not Allowed"
|
|
3778
|
+
};
|
|
3779
|
+
}
|
|
3780
|
+
const outcome = await handleJsonRpc(request.body, tools, call, serverName, version);
|
|
3781
|
+
return {
|
|
3782
|
+
status: outcome.status,
|
|
3783
|
+
reason: outcome.status === 200 ? "OK" : "Accepted",
|
|
3784
|
+
headers: { "content-type": outcome.contentType },
|
|
3785
|
+
body: outcome.body
|
|
3786
|
+
};
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
// src/config/lock.ts
|
|
3790
|
+
var LOCK_STALE_MS = 1e4;
|
|
3791
|
+
var LOCK_POLL_MS = 50;
|
|
3792
|
+
var LOCK_MAX_WAIT_MS = 5e3;
|
|
3793
|
+
function isEexist(err) {
|
|
3794
|
+
if (err instanceof Error && "code" in err)
|
|
3795
|
+
return err.code === "EEXIST";
|
|
3796
|
+
if (err instanceof Error) return err.message.includes("EEXIST");
|
|
3797
|
+
return false;
|
|
3798
|
+
}
|
|
3799
|
+
async function removeStaleLock(fs, lockPath) {
|
|
3800
|
+
try {
|
|
3801
|
+
const s = await fs.stat(lockPath);
|
|
3802
|
+
if (Date.now() - s.mtimeMs > LOCK_STALE_MS) {
|
|
3803
|
+
try {
|
|
3804
|
+
await fs.rmdir(lockPath);
|
|
2097
3805
|
} catch {
|
|
2098
|
-
|
|
3806
|
+
}
|
|
3807
|
+
return true;
|
|
3808
|
+
}
|
|
3809
|
+
} catch {
|
|
3810
|
+
}
|
|
3811
|
+
return false;
|
|
3812
|
+
}
|
|
3813
|
+
async function acquireLock(fs, configPath) {
|
|
3814
|
+
const lockPath = `${configPath}.lock`;
|
|
3815
|
+
const start = Date.now();
|
|
3816
|
+
while (true) {
|
|
3817
|
+
try {
|
|
3818
|
+
await fs.mkdirExclusive(lockPath);
|
|
3819
|
+
break;
|
|
3820
|
+
} catch (err) {
|
|
3821
|
+
if (!isEexist(err)) throw err;
|
|
3822
|
+
await removeStaleLock(fs, lockPath);
|
|
3823
|
+
}
|
|
3824
|
+
if (Date.now() - start > LOCK_MAX_WAIT_MS) {
|
|
3825
|
+
throw new Error(`mcpcat: could not acquire lock ${lockPath} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
3826
|
+
}
|
|
3827
|
+
await new Promise((r) => setTimeout(r, LOCK_POLL_MS));
|
|
3828
|
+
}
|
|
3829
|
+
let released = false;
|
|
3830
|
+
return async () => {
|
|
3831
|
+
if (released) return;
|
|
3832
|
+
released = true;
|
|
3833
|
+
try {
|
|
3834
|
+
await fs.rmdir(lockPath);
|
|
3835
|
+
} catch {
|
|
3836
|
+
}
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// src/config/edit.ts
|
|
3841
|
+
function isRecord2(value) {
|
|
3842
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3843
|
+
}
|
|
3844
|
+
function detectIndent(text) {
|
|
3845
|
+
const match = /^([ \t]+)\S/m.exec(text);
|
|
3846
|
+
return match ? match[1] : " ";
|
|
3847
|
+
}
|
|
3848
|
+
async function readText(fs, path) {
|
|
3849
|
+
try {
|
|
3850
|
+
const data = await fs.readFile(path);
|
|
3851
|
+
return new TextDecoder().decode(data);
|
|
3852
|
+
} catch {
|
|
3853
|
+
return void 0;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
async function editMcpServers(fs, path, edit) {
|
|
3857
|
+
const release = await acquireLock(fs, path);
|
|
3858
|
+
try {
|
|
3859
|
+
const text = await readText(fs, path);
|
|
3860
|
+
const parsed = text === void 0 ? {} : JSON.parse(text);
|
|
3861
|
+
if (!isRecord2(parsed) || parsed.mcpServers !== void 0 && !isRecord2(parsed.mcpServers))
|
|
3862
|
+
throw new Error(`${path}: expected an object with an "mcpServers" object`);
|
|
3863
|
+
const servers = isRecord2(parsed.mcpServers) ? parsed.mcpServers : void 0;
|
|
3864
|
+
if (!edit(servers, parsed)) return { changed: false };
|
|
3865
|
+
const indent = text ? detectIndent(text) : " ";
|
|
3866
|
+
const output = `${JSON.stringify(parsed, null, indent)}
|
|
3867
|
+
`;
|
|
3868
|
+
const dir = path.slice(0, path.lastIndexOf("/"));
|
|
3869
|
+
if (dir) {
|
|
3870
|
+
try {
|
|
3871
|
+
await fs.mkdir(dir);
|
|
3872
|
+
} catch {
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
const tmpPath = `${path}.tmp.${Date.now()}`;
|
|
3876
|
+
await fs.writeFile(tmpPath, output);
|
|
3877
|
+
await fs.rename(tmpPath, path);
|
|
3878
|
+
return { changed: true };
|
|
3879
|
+
} finally {
|
|
3880
|
+
await release();
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
async function addServer(fs, path, name, config) {
|
|
3884
|
+
let replaced = false;
|
|
3885
|
+
await editMcpServers(fs, path, (servers, parsed) => {
|
|
3886
|
+
const target = servers ?? {};
|
|
3887
|
+
replaced = target[name] !== void 0;
|
|
3888
|
+
target[name] = config;
|
|
3889
|
+
parsed.mcpServers = target;
|
|
3890
|
+
return true;
|
|
3891
|
+
});
|
|
3892
|
+
return { replaced };
|
|
3893
|
+
}
|
|
3894
|
+
async function removeServer(fs, path, name) {
|
|
3895
|
+
let removed = false;
|
|
3896
|
+
await editMcpServers(fs, path, (servers) => {
|
|
3897
|
+
if (!servers || servers[name] === void 0) return false;
|
|
3898
|
+
delete servers[name];
|
|
3899
|
+
removed = true;
|
|
3900
|
+
return true;
|
|
3901
|
+
});
|
|
3902
|
+
return removed;
|
|
3903
|
+
}
|
|
3904
|
+
async function updateServer(fs, path, name, patch) {
|
|
3905
|
+
let updated = false;
|
|
3906
|
+
await editMcpServers(fs, path, (servers) => {
|
|
3907
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
3908
|
+
const entry = servers[name];
|
|
3909
|
+
if (patch.enabled !== void 0) entry.enabled = patch.enabled;
|
|
3910
|
+
if (patch.exposure !== void 0) entry.exposure = patch.exposure;
|
|
3911
|
+
if (patch.toolExposure !== void 0) entry.toolExposure = patch.toolExposure;
|
|
3912
|
+
updated = true;
|
|
3913
|
+
return true;
|
|
3914
|
+
});
|
|
3915
|
+
return updated;
|
|
3916
|
+
}
|
|
3917
|
+
async function setExposure(fs, path, name, exposure, toolGlob) {
|
|
3918
|
+
let updated = false;
|
|
3919
|
+
await editMcpServers(fs, path, (servers) => {
|
|
3920
|
+
if (!servers || !isRecord2(servers[name])) return false;
|
|
3921
|
+
const entry = servers[name];
|
|
3922
|
+
if (toolGlob) {
|
|
3923
|
+
const te = isRecord2(entry.toolExposure) ? { ...entry.toolExposure } : {};
|
|
3924
|
+
te[toolGlob] = exposure;
|
|
3925
|
+
entry.toolExposure = te;
|
|
3926
|
+
} else {
|
|
3927
|
+
entry.exposure = exposure;
|
|
3928
|
+
}
|
|
3929
|
+
updated = true;
|
|
3930
|
+
return true;
|
|
3931
|
+
});
|
|
3932
|
+
return updated;
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
// src/config/import.ts
|
|
3936
|
+
function isRecord3(value) {
|
|
3937
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3938
|
+
}
|
|
3939
|
+
function isStringRecord2(value) {
|
|
3940
|
+
return isRecord3(value) && Object.values(value).every((v) => typeof v === "string");
|
|
3941
|
+
}
|
|
3942
|
+
function tryParse(text) {
|
|
3943
|
+
try {
|
|
3944
|
+
return { parsed: JSON.parse(text) };
|
|
3945
|
+
} catch (e) {
|
|
3946
|
+
return { parsed: void 0, error: `not valid JSON: ${e.message}` };
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
function filterNames(entries, names) {
|
|
3950
|
+
const result = [];
|
|
3951
|
+
for (const [name, value] of entries) {
|
|
3952
|
+
if (names && names.length > 0 && !names.includes(name)) continue;
|
|
3953
|
+
result.push([name, value]);
|
|
3954
|
+
}
|
|
3955
|
+
return result;
|
|
3956
|
+
}
|
|
3957
|
+
function replaceInputVars(value) {
|
|
3958
|
+
return value.replace(
|
|
3959
|
+
/\$\{input:([^}]+)\}/g,
|
|
3960
|
+
(_, name) => `\${${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}}`
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
function importClaude(text, names) {
|
|
3964
|
+
const result = { servers: [], errors: [] };
|
|
3965
|
+
const { parsed, error } = tryParse(text);
|
|
3966
|
+
if (error) {
|
|
3967
|
+
result.errors.push(error);
|
|
3968
|
+
return result;
|
|
3969
|
+
}
|
|
3970
|
+
if (!isRecord3(parsed)) {
|
|
3971
|
+
result.errors.push("expected an object");
|
|
3972
|
+
return result;
|
|
3973
|
+
}
|
|
3974
|
+
const mcpServers = parsed.mcpServers;
|
|
3975
|
+
if (!isRecord3(mcpServers)) {
|
|
3976
|
+
result.errors.push('no "mcpServers" object');
|
|
3977
|
+
return result;
|
|
3978
|
+
}
|
|
3979
|
+
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
3980
|
+
if (!isRecord3(entry)) {
|
|
3981
|
+
result.errors.push(`${name}: not an object`);
|
|
3982
|
+
continue;
|
|
3983
|
+
}
|
|
3984
|
+
result.servers.push({ name, config: entry });
|
|
3985
|
+
}
|
|
3986
|
+
return result;
|
|
3987
|
+
}
|
|
3988
|
+
function importVSCode(text, names) {
|
|
3989
|
+
const result = { servers: [], errors: [] };
|
|
3990
|
+
const { parsed, error } = tryParse(text);
|
|
3991
|
+
if (error) {
|
|
3992
|
+
result.errors.push(error);
|
|
3993
|
+
return result;
|
|
3994
|
+
}
|
|
3995
|
+
if (!isRecord3(parsed)) {
|
|
3996
|
+
result.errors.push("expected an object");
|
|
3997
|
+
return result;
|
|
3998
|
+
}
|
|
3999
|
+
const servers = parsed.servers ?? parsed.mcpServers;
|
|
4000
|
+
if (!isRecord3(servers)) {
|
|
4001
|
+
result.errors.push('no "servers" or "mcpServers" object');
|
|
4002
|
+
return result;
|
|
4003
|
+
}
|
|
4004
|
+
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
4005
|
+
if (!isRecord3(entry)) {
|
|
4006
|
+
result.errors.push(`${name}: not an object`);
|
|
4007
|
+
continue;
|
|
4008
|
+
}
|
|
4009
|
+
result.servers.push({ name, config: convertVSCodeEntry(entry) });
|
|
4010
|
+
}
|
|
4011
|
+
return result;
|
|
4012
|
+
}
|
|
4013
|
+
function convertVSCodeEntry(entry) {
|
|
4014
|
+
const config = {};
|
|
4015
|
+
for (const [key, value] of Object.entries(entry)) {
|
|
4016
|
+
if (key === "type" || key === "command" || key === "args" || key === "cwd" || key === "url") {
|
|
4017
|
+
config[key] = value;
|
|
4018
|
+
} else if (key === "headers" && isStringRecord2(value)) {
|
|
4019
|
+
const headers = {};
|
|
4020
|
+
for (const [h, v] of Object.entries(value)) headers[h] = replaceInputVars(v);
|
|
4021
|
+
config.headers = headers;
|
|
4022
|
+
} else if (key === "env" && isStringRecord2(value)) {
|
|
4023
|
+
const env = {};
|
|
4024
|
+
for (const [k, v] of Object.entries(value)) env[k] = replaceInputVars(v);
|
|
4025
|
+
config.env = env;
|
|
4026
|
+
} else {
|
|
4027
|
+
config[key] = value;
|
|
4028
|
+
}
|
|
4029
|
+
}
|
|
4030
|
+
return config;
|
|
4031
|
+
}
|
|
4032
|
+
function importSixServers(text, names) {
|
|
4033
|
+
const result = { servers: [], errors: [] };
|
|
4034
|
+
const { parsed, error } = tryParse(text);
|
|
4035
|
+
if (error) {
|
|
4036
|
+
result.errors.push(error);
|
|
4037
|
+
return result;
|
|
4038
|
+
}
|
|
4039
|
+
if (!isRecord3(parsed)) {
|
|
4040
|
+
result.errors.push("expected an object");
|
|
4041
|
+
return result;
|
|
4042
|
+
}
|
|
4043
|
+
const servers = parsed.servers;
|
|
4044
|
+
if (!isRecord3(servers)) {
|
|
4045
|
+
result.errors.push('no "servers" object');
|
|
4046
|
+
return result;
|
|
4047
|
+
}
|
|
4048
|
+
for (const [name, entry] of filterNames(Object.entries(servers), names)) {
|
|
4049
|
+
if (!isRecord3(entry)) {
|
|
4050
|
+
result.errors.push(`${name}: not an object`);
|
|
4051
|
+
continue;
|
|
4052
|
+
}
|
|
4053
|
+
const config = {};
|
|
4054
|
+
if (typeof entry.url === "string") config.url = entry.url;
|
|
4055
|
+
if (isStringRecord2(entry.headers)) config.headers = entry.headers;
|
|
4056
|
+
if (typeof entry.exposure === "string") config.exposure = entry.exposure;
|
|
4057
|
+
if (isRecord3(entry.toolExposure)) config.toolExposure = entry.toolExposure;
|
|
4058
|
+
result.servers.push({ name, config });
|
|
4059
|
+
}
|
|
4060
|
+
return result;
|
|
4061
|
+
}
|
|
4062
|
+
function importCodexToml(text, names) {
|
|
4063
|
+
const result = { servers: [], errors: [] };
|
|
4064
|
+
const sections = parseTomlSections(text);
|
|
4065
|
+
for (const [sectionName, fields] of sections) {
|
|
4066
|
+
const prefix = "mcp_servers.";
|
|
4067
|
+
if (!sectionName.startsWith(prefix)) continue;
|
|
4068
|
+
const name = sectionName.slice(prefix.length);
|
|
4069
|
+
if (names && names.length > 0 && !names.includes(name)) continue;
|
|
4070
|
+
const config = {};
|
|
4071
|
+
if (fields.command) config.command = fields.command;
|
|
4072
|
+
if (fields.args) config.args = fields.args;
|
|
4073
|
+
if (fields.url) config.url = fields.url;
|
|
4074
|
+
if (fields.env && isRecord3(fields.env)) config.env = fields.env;
|
|
4075
|
+
result.servers.push({ name, config });
|
|
4076
|
+
}
|
|
4077
|
+
return result;
|
|
4078
|
+
}
|
|
4079
|
+
function parseTomlSections(text) {
|
|
4080
|
+
const sections = [];
|
|
4081
|
+
let current;
|
|
4082
|
+
let currentName = "";
|
|
4083
|
+
for (const line of text.split("\n")) {
|
|
4084
|
+
const trimmed = line.trim();
|
|
4085
|
+
if (trimmed.startsWith("#") || trimmed === "") continue;
|
|
4086
|
+
const sectionMatch = /^\[([^\]]+)\]/.exec(trimmed);
|
|
4087
|
+
if (sectionMatch) {
|
|
4088
|
+
if (current) sections.push([currentName, current]);
|
|
4089
|
+
currentName = sectionMatch[1].trim();
|
|
4090
|
+
current = {};
|
|
4091
|
+
continue;
|
|
4092
|
+
}
|
|
4093
|
+
if (!current) continue;
|
|
4094
|
+
const kvMatch = /^(\w+)\s*=\s*(.+)/.exec(trimmed);
|
|
4095
|
+
if (kvMatch) {
|
|
4096
|
+
current[kvMatch[1]] = parseTomlValue(kvMatch[2].trim());
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
if (current) sections.push([currentName, current]);
|
|
4100
|
+
return sections;
|
|
4101
|
+
}
|
|
4102
|
+
function parseTomlValue(raw) {
|
|
4103
|
+
if (raw.startsWith('"') && raw.endsWith('"')) return raw.slice(1, -1);
|
|
4104
|
+
if (raw.startsWith("'") && raw.endsWith("'")) return raw.slice(1, -1);
|
|
4105
|
+
if (raw.startsWith("[")) {
|
|
4106
|
+
try {
|
|
4107
|
+
return JSON.parse(raw.replace(/'/g, '"'));
|
|
4108
|
+
} catch {
|
|
4109
|
+
return raw;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
if (raw === "true") return true;
|
|
4113
|
+
if (raw === "false") return false;
|
|
4114
|
+
const num = Number(raw);
|
|
4115
|
+
if (!Number.isNaN(num)) return num;
|
|
4116
|
+
return raw;
|
|
4117
|
+
}
|
|
4118
|
+
function convertOpenCodeLocal(entry) {
|
|
4119
|
+
const config = {};
|
|
4120
|
+
if (Array.isArray(entry.command) && entry.command.length > 0) {
|
|
4121
|
+
config.command = entry.command[0];
|
|
4122
|
+
if (entry.command.length > 1) config.args = entry.command.slice(1);
|
|
4123
|
+
} else if (typeof entry.command === "string") {
|
|
4124
|
+
config.command = entry.command;
|
|
4125
|
+
}
|
|
4126
|
+
if (isRecord3(entry.environment)) {
|
|
4127
|
+
const env = {};
|
|
4128
|
+
for (const [k, v] of Object.entries(entry.environment)) {
|
|
4129
|
+
if (typeof v === "string")
|
|
4130
|
+
env[k] = v.replace(/\{env:([^}]+)\}/g, (_, n) => `\${${n}}`);
|
|
4131
|
+
}
|
|
4132
|
+
config.env = env;
|
|
4133
|
+
}
|
|
4134
|
+
return config;
|
|
4135
|
+
}
|
|
4136
|
+
function convertOpenCodeRemote(entry) {
|
|
4137
|
+
const config = {};
|
|
4138
|
+
config.url = entry.url;
|
|
4139
|
+
if (isStringRecord2(entry.headers)) config.headers = entry.headers;
|
|
4140
|
+
return config;
|
|
4141
|
+
}
|
|
4142
|
+
function importOpenCode(text, names) {
|
|
4143
|
+
const result = { servers: [], errors: [] };
|
|
4144
|
+
const { parsed, error } = tryParse(text);
|
|
4145
|
+
if (error) {
|
|
4146
|
+
result.errors.push(error);
|
|
4147
|
+
return result;
|
|
4148
|
+
}
|
|
4149
|
+
if (!isRecord3(parsed)) {
|
|
4150
|
+
result.errors.push("expected an object");
|
|
4151
|
+
return result;
|
|
4152
|
+
}
|
|
4153
|
+
const mcpServers = parsed.mcpServers ?? parsed.mcp;
|
|
4154
|
+
if (!isRecord3(mcpServers)) {
|
|
4155
|
+
result.errors.push("no mcp servers found");
|
|
4156
|
+
return result;
|
|
4157
|
+
}
|
|
4158
|
+
for (const [name, entry] of filterNames(Object.entries(mcpServers), names)) {
|
|
4159
|
+
if (!isRecord3(entry)) {
|
|
4160
|
+
result.errors.push(`${name}: not an object`);
|
|
4161
|
+
continue;
|
|
4162
|
+
}
|
|
4163
|
+
const isLocal = entry.type === "local" || entry.command;
|
|
4164
|
+
const config = isLocal ? convertOpenCodeLocal(entry) : convertOpenCodeRemote(entry);
|
|
4165
|
+
result.servers.push({ name, config });
|
|
4166
|
+
}
|
|
4167
|
+
return result;
|
|
4168
|
+
}
|
|
4169
|
+
function detectFormat(text, filename) {
|
|
4170
|
+
if (filename.endsWith(".toml")) return "codex";
|
|
4171
|
+
if (filename.includes("servers.json")) return "six";
|
|
4172
|
+
if (filename.includes(".vscode")) return "vscode";
|
|
4173
|
+
try {
|
|
4174
|
+
const parsed = JSON.parse(text);
|
|
4175
|
+
if (isRecord3(parsed)) {
|
|
4176
|
+
if (parsed.servers && isRecord3(parsed.servers) && parsed.version !== void 0) return "six";
|
|
4177
|
+
if (parsed.servers && isRecord3(parsed.servers)) return "vscode";
|
|
4178
|
+
if (parsed.mcpServers) return "claude";
|
|
4179
|
+
if (parsed.mcp) return "opencode";
|
|
4180
|
+
}
|
|
4181
|
+
} catch {
|
|
4182
|
+
}
|
|
4183
|
+
if (text.includes("[mcp_servers.")) return "codex";
|
|
4184
|
+
return "claude";
|
|
4185
|
+
}
|
|
4186
|
+
function importFile(text, filename, names) {
|
|
4187
|
+
const format = detectFormat(text, filename);
|
|
4188
|
+
switch (format) {
|
|
4189
|
+
case "claude":
|
|
4190
|
+
return importClaude(text, names);
|
|
4191
|
+
case "vscode":
|
|
4192
|
+
return importVSCode(text, names);
|
|
4193
|
+
case "six":
|
|
4194
|
+
return importSixServers(text, names);
|
|
4195
|
+
case "codex":
|
|
4196
|
+
return importCodexToml(text, names);
|
|
4197
|
+
case "opencode":
|
|
4198
|
+
return importOpenCode(text, names);
|
|
4199
|
+
default:
|
|
4200
|
+
return importClaude(text, names);
|
|
4201
|
+
}
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
// src/config/warnings.ts
|
|
4205
|
+
var SECRET_HEADER = /authorization|cookie|token|key|secret|password|session/i;
|
|
4206
|
+
var PLACEHOLDER2 = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
4207
|
+
var SCHEME = /^\s*(?:bearer|basic|token|apikey|api-key)\s+/i;
|
|
4208
|
+
var SECRET_PARAM = /token|key|secret|password|signature|sig|auth|code/i;
|
|
4209
|
+
function literalSecret(header, value) {
|
|
4210
|
+
if (!SECRET_HEADER.test(header)) return false;
|
|
4211
|
+
return value.replace(PLACEHOLDER2, "").replace(SCHEME, "").trim() !== "";
|
|
4212
|
+
}
|
|
4213
|
+
function urlHasSecret(url) {
|
|
4214
|
+
if (!URL.canParse(url)) return void 0;
|
|
4215
|
+
const parsed = new URL(url);
|
|
4216
|
+
if (parsed.username || parsed.password) return "url";
|
|
4217
|
+
for (const name of parsed.searchParams.keys()) if (SECRET_PARAM.test(name)) return `url ?${name}`;
|
|
4218
|
+
return void 0;
|
|
4219
|
+
}
|
|
4220
|
+
var SLICC_OAUTH_REFUSED = {
|
|
4221
|
+
clientSecret: "a client secret would have to live in mcp.json; SLICC signs in as a public client",
|
|
4222
|
+
callbackPort: "SLICC signs in through its own page, not a loopback port",
|
|
4223
|
+
callbackUrl: "SLICC signs in through its own page, not a loopback URL",
|
|
4224
|
+
clientRegistration: "SLICC registers dynamically or uses oauth.clientId"
|
|
4225
|
+
};
|
|
4226
|
+
function checkAgentWarnings(name, config) {
|
|
4227
|
+
const warnings = [];
|
|
4228
|
+
if ("command" in config && config.command) {
|
|
4229
|
+
warnings.push({
|
|
4230
|
+
server: name,
|
|
4231
|
+
message: "stdio servers need slicc-kernel#68, so the agent will skip this entry",
|
|
4232
|
+
advice: "use mcpcat serve --proxy to bridge it as HTTP"
|
|
4233
|
+
});
|
|
4234
|
+
}
|
|
4235
|
+
if ("headers" in config && config.headers) {
|
|
4236
|
+
for (const [h, v] of Object.entries(config.headers)) {
|
|
4237
|
+
if (v.trim().startsWith("!")) {
|
|
4238
|
+
warnings.push({
|
|
4239
|
+
server: name,
|
|
4240
|
+
message: `headers.${h} runs a command, which the agent can't do`,
|
|
4241
|
+
advice: "use ${NAME} and set the value in Settings > MCP servers"
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
if (literalSecret(h, v)) {
|
|
4245
|
+
warnings.push({
|
|
4246
|
+
server: name,
|
|
4247
|
+
message: `headers.${h} holds a literal secret`,
|
|
4248
|
+
advice: "write ${NAME} instead, and add the value in Settings > MCP servers"
|
|
4249
|
+
});
|
|
4250
|
+
}
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
if ("url" in config && config.url) {
|
|
4254
|
+
const secret = urlHasSecret(config.url);
|
|
4255
|
+
if (secret) {
|
|
4256
|
+
warnings.push({
|
|
4257
|
+
server: name,
|
|
4258
|
+
message: `${secret} holds a literal secret`,
|
|
4259
|
+
advice: "put it in a header as ${NAME}"
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
}
|
|
4263
|
+
if ("oauth" in config && config.oauth) {
|
|
4264
|
+
for (const key of Object.keys(config.oauth)) {
|
|
4265
|
+
const reason = SLICC_OAUTH_REFUSED[key];
|
|
4266
|
+
if (reason) {
|
|
4267
|
+
warnings.push({
|
|
4268
|
+
server: name,
|
|
4269
|
+
message: `oauth.${key} is not supported by the agent: ${reason}`,
|
|
4270
|
+
advice: `remove oauth.${key}`
|
|
4271
|
+
});
|
|
2099
4272
|
}
|
|
2100
4273
|
}
|
|
4274
|
+
}
|
|
4275
|
+
return warnings;
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
// src/cli/config-verbs.ts
|
|
4279
|
+
function parseHeader(hdr) {
|
|
4280
|
+
const colonIdx = hdr.indexOf(":");
|
|
4281
|
+
if (colonIdx < 1) return `invalid header '${hdr}'`;
|
|
4282
|
+
return [hdr.slice(0, colonIdx).trim(), hdr.slice(colonIdx + 1).trim()];
|
|
4283
|
+
}
|
|
4284
|
+
function parseEnvPair(pair) {
|
|
4285
|
+
const eqIdx = pair.indexOf("=");
|
|
4286
|
+
if (eqIdx < 1) return `invalid --env '${pair}'`;
|
|
4287
|
+
return [pair.slice(0, eqIdx), pair.slice(eqIdx + 1)];
|
|
4288
|
+
}
|
|
4289
|
+
function parseAddFlagAt(flagArgs, i, flags) {
|
|
4290
|
+
const arg = flagArgs[i];
|
|
4291
|
+
if (arg === "-l" || arg === "--local") {
|
|
4292
|
+
flags.local = true;
|
|
4293
|
+
return { next: i + 1 };
|
|
4294
|
+
}
|
|
4295
|
+
if (arg === "--no-probe") {
|
|
4296
|
+
flags.noProbe = true;
|
|
4297
|
+
return { next: i + 1 };
|
|
4298
|
+
}
|
|
4299
|
+
if (arg === "--config" && i + 1 < flagArgs.length) {
|
|
4300
|
+
flags.configFile = flagArgs[i + 1];
|
|
4301
|
+
return { next: i + 2 };
|
|
4302
|
+
}
|
|
4303
|
+
if (arg === "--description" && i + 1 < flagArgs.length) {
|
|
4304
|
+
flags.description = flagArgs[i + 1];
|
|
4305
|
+
return { next: i + 2 };
|
|
4306
|
+
}
|
|
4307
|
+
if (arg === "--exposure" && i + 1 < flagArgs.length) {
|
|
4308
|
+
flags.exposure = flagArgs[i + 1];
|
|
4309
|
+
return { next: i + 2 };
|
|
4310
|
+
}
|
|
4311
|
+
if ((arg === "-H" || arg === "--header") && i + 1 < flagArgs.length) {
|
|
4312
|
+
const result = parseHeader(flagArgs[i + 1]);
|
|
4313
|
+
if (typeof result === "string") return { next: i + 2, error: result };
|
|
4314
|
+
flags.headers.push(result);
|
|
4315
|
+
return { next: i + 2 };
|
|
4316
|
+
}
|
|
4317
|
+
if (arg === "--env" && i + 1 < flagArgs.length) {
|
|
4318
|
+
const result = parseEnvPair(flagArgs[i + 1]);
|
|
4319
|
+
if (typeof result === "string") return { next: i + 2, error: result };
|
|
4320
|
+
flags.envPairs.push(result);
|
|
4321
|
+
return { next: i + 2 };
|
|
4322
|
+
}
|
|
4323
|
+
if (arg === "--cwd" && i + 1 < flagArgs.length) {
|
|
4324
|
+
flags.cwd = flagArgs[i + 1];
|
|
4325
|
+
return { next: i + 2 };
|
|
4326
|
+
}
|
|
4327
|
+
if (arg.startsWith("-")) return { next: i + 1, error: `unknown option '${arg}'` };
|
|
4328
|
+
flags.positional.push(arg);
|
|
4329
|
+
return { next: i + 1 };
|
|
4330
|
+
}
|
|
4331
|
+
function parseAddFlags(args2) {
|
|
4332
|
+
const flags = {
|
|
4333
|
+
local: false,
|
|
4334
|
+
noProbe: false,
|
|
4335
|
+
headers: [],
|
|
4336
|
+
envPairs: [],
|
|
4337
|
+
positional: [],
|
|
4338
|
+
cmdArgs: []
|
|
2101
4339
|
};
|
|
4340
|
+
const dashDashIdx = args2.indexOf("--");
|
|
4341
|
+
const flagArgs = dashDashIdx >= 0 ? args2.slice(0, dashDashIdx) : args2;
|
|
4342
|
+
flags.cmdArgs = dashDashIdx >= 0 ? args2.slice(dashDashIdx + 1) : [];
|
|
4343
|
+
let i = 0;
|
|
4344
|
+
while (i < flagArgs.length) {
|
|
4345
|
+
const step = parseAddFlagAt(flagArgs, i, flags);
|
|
4346
|
+
if (step.error) return step.error;
|
|
4347
|
+
i = step.next;
|
|
4348
|
+
}
|
|
4349
|
+
return flags;
|
|
4350
|
+
}
|
|
4351
|
+
function buildStdioConfig(flags) {
|
|
4352
|
+
const config = {
|
|
4353
|
+
command: flags.cmdArgs[0],
|
|
4354
|
+
...flags.cmdArgs.length > 1 ? { args: flags.cmdArgs.slice(1) } : {}
|
|
4355
|
+
};
|
|
4356
|
+
if (flags.envPairs.length > 0) {
|
|
4357
|
+
const env = {};
|
|
4358
|
+
for (const [k, v] of flags.envPairs) env[k] = v;
|
|
4359
|
+
config.env = env;
|
|
4360
|
+
}
|
|
4361
|
+
if (flags.cwd) config.cwd = flags.cwd;
|
|
4362
|
+
if (flags.description) config.description = flags.description;
|
|
4363
|
+
if (flags.exposure) config.exposure = flags.exposure;
|
|
4364
|
+
return config;
|
|
4365
|
+
}
|
|
4366
|
+
function buildHttpConfig(urlStr, flags) {
|
|
4367
|
+
const config = { url: urlStr };
|
|
4368
|
+
if (flags.headers.length > 0) {
|
|
4369
|
+
const hdrs = {};
|
|
4370
|
+
for (const [k, v] of flags.headers) {
|
|
4371
|
+
if (literalSecret(k, v)) return `header '${k}' holds a literal secret; use \${NAME} instead`;
|
|
4372
|
+
hdrs[k] = v;
|
|
4373
|
+
}
|
|
4374
|
+
config.headers = hdrs;
|
|
4375
|
+
}
|
|
4376
|
+
if (flags.description) config.description = flags.description;
|
|
4377
|
+
if (flags.exposure) config.exposure = flags.exposure;
|
|
4378
|
+
return config;
|
|
4379
|
+
}
|
|
4380
|
+
async function runAdd(platform2, args2, probe) {
|
|
4381
|
+
const flags = parseAddFlags(args2);
|
|
4382
|
+
if (typeof flags === "string") {
|
|
4383
|
+
await platform2.write(2, `mcpcat add: ${flags}
|
|
4384
|
+
`);
|
|
4385
|
+
return 2;
|
|
4386
|
+
}
|
|
4387
|
+
if (flags.positional.length < 1) {
|
|
4388
|
+
await platform2.write(2, "mcpcat add: requires a server name\n");
|
|
4389
|
+
return 2;
|
|
4390
|
+
}
|
|
4391
|
+
let name;
|
|
4392
|
+
let config;
|
|
4393
|
+
if (flags.cmdArgs.length > 0) {
|
|
4394
|
+
name = flags.positional[0];
|
|
4395
|
+
config = buildStdioConfig(flags);
|
|
4396
|
+
} else if (flags.positional.length >= 2) {
|
|
4397
|
+
let urlStr;
|
|
4398
|
+
if (URL.canParse(flags.positional[0]) && !URL.canParse(flags.positional[1])) {
|
|
4399
|
+
urlStr = flags.positional[0];
|
|
4400
|
+
name = flags.positional[1];
|
|
4401
|
+
} else {
|
|
4402
|
+
name = flags.positional[0];
|
|
4403
|
+
urlStr = flags.positional[1];
|
|
4404
|
+
}
|
|
4405
|
+
if (!URL.canParse(urlStr)) {
|
|
4406
|
+
await platform2.write(2, `mcpcat add: '${urlStr}' is not a valid URL
|
|
4407
|
+
`);
|
|
4408
|
+
return 2;
|
|
4409
|
+
}
|
|
4410
|
+
const secret = urlHasSecret(urlStr);
|
|
4411
|
+
if (secret) {
|
|
4412
|
+
await platform2.write(
|
|
4413
|
+
2,
|
|
4414
|
+
`mcpcat add: ${secret} holds a literal secret; use \${NAME} in a header instead
|
|
4415
|
+
`
|
|
4416
|
+
);
|
|
4417
|
+
return 2;
|
|
4418
|
+
}
|
|
4419
|
+
const httpResult = buildHttpConfig(urlStr, flags);
|
|
4420
|
+
if (typeof httpResult === "string") {
|
|
4421
|
+
await platform2.write(2, `mcpcat add: ${httpResult}
|
|
4422
|
+
`);
|
|
4423
|
+
return 2;
|
|
4424
|
+
}
|
|
4425
|
+
config = httpResult;
|
|
4426
|
+
} else {
|
|
4427
|
+
await platform2.write(2, "mcpcat add: requires a URL or -- <command>\n");
|
|
4428
|
+
return 2;
|
|
4429
|
+
}
|
|
4430
|
+
const validation = validateServer(name, config);
|
|
4431
|
+
if ("message" in validation) {
|
|
4432
|
+
await platform2.write(2, `mcpcat add: ${validation.message}
|
|
4433
|
+
`);
|
|
4434
|
+
return 2;
|
|
4435
|
+
}
|
|
4436
|
+
const path = flags.configFile ?? (flags.local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
4437
|
+
const { replaced } = await addServer(platform2.fs, path, name, config);
|
|
4438
|
+
const warnings = checkAgentWarnings(name, config);
|
|
4439
|
+
for (const w of warnings) {
|
|
4440
|
+
await platform2.write(2, `mcpcat add: warning: ${w.message}; ${w.advice}
|
|
4441
|
+
`);
|
|
4442
|
+
}
|
|
4443
|
+
await platform2.write(1, `${replaced ? "replaced" : "added"} ${name} in ${path}
|
|
4444
|
+
`);
|
|
4445
|
+
if (!flags.noProbe && probe) {
|
|
4446
|
+
const reason = await probe(name, config, platform2);
|
|
4447
|
+
if (reason) {
|
|
4448
|
+
await platform2.write(2, `mcpcat add: probe failed: ${reason}
|
|
4449
|
+
`);
|
|
4450
|
+
return 3;
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
return 0;
|
|
4454
|
+
}
|
|
4455
|
+
async function runRm(platform2, args2) {
|
|
4456
|
+
let configFile;
|
|
4457
|
+
let local = false;
|
|
4458
|
+
const names = [];
|
|
4459
|
+
for (let i = 0; i < args2.length; i++) {
|
|
4460
|
+
const arg = args2[i];
|
|
4461
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
4462
|
+
configFile = args2[++i];
|
|
4463
|
+
continue;
|
|
4464
|
+
}
|
|
4465
|
+
if (arg === "-l" || arg === "--local") {
|
|
4466
|
+
local = true;
|
|
4467
|
+
continue;
|
|
4468
|
+
}
|
|
4469
|
+
if (arg === "--logout") continue;
|
|
4470
|
+
if (arg.startsWith("-")) {
|
|
4471
|
+
await platform2.write(2, `mcpcat rm: unknown option '${arg}'
|
|
4472
|
+
`);
|
|
4473
|
+
return 2;
|
|
4474
|
+
}
|
|
4475
|
+
names.push(arg);
|
|
4476
|
+
}
|
|
4477
|
+
if (names.length === 0) {
|
|
4478
|
+
await platform2.write(2, "mcpcat rm: requires a server name\n");
|
|
4479
|
+
return 2;
|
|
4480
|
+
}
|
|
4481
|
+
const path = configFile ?? (local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
4482
|
+
for (const name of names) {
|
|
4483
|
+
const removed = await removeServer(platform2.fs, path, name);
|
|
4484
|
+
if (!removed) {
|
|
4485
|
+
await platform2.write(2, `mcpcat rm: '${name}' not found in ${path}
|
|
4486
|
+
`);
|
|
4487
|
+
return 1;
|
|
4488
|
+
}
|
|
4489
|
+
await platform2.write(1, `removed ${name} from ${path}
|
|
4490
|
+
`);
|
|
4491
|
+
}
|
|
4492
|
+
return 0;
|
|
4493
|
+
}
|
|
4494
|
+
function formatListEntry(entry) {
|
|
4495
|
+
const c = entry.config;
|
|
4496
|
+
const transport = "url" in c ? "http" : "stdio";
|
|
4497
|
+
const target = "url" in c ? c.url : c.command;
|
|
4498
|
+
const enabled = "enabled" in c && c.enabled === false ? " (disabled)" : "";
|
|
4499
|
+
const exp = c.exposure ? ` [${c.exposure}]` : "";
|
|
4500
|
+
return `${entry.name} ${transport} ${target}${enabled}${exp} (${entry.source})`;
|
|
4501
|
+
}
|
|
4502
|
+
async function printListJson(platform2, config) {
|
|
4503
|
+
const entries = {};
|
|
4504
|
+
for (const entry of config.servers) {
|
|
4505
|
+
entries[entry.name] = {
|
|
4506
|
+
...entry.config,
|
|
4507
|
+
source: entry.source,
|
|
4508
|
+
...entry.scope ? { scope: entry.scope } : {},
|
|
4509
|
+
...entry.override ? { override: entry.override } : {}
|
|
4510
|
+
};
|
|
4511
|
+
}
|
|
4512
|
+
await platform2.write(1, `${JSON.stringify(entries, null, 2)}
|
|
4513
|
+
`);
|
|
4514
|
+
}
|
|
4515
|
+
async function printListText(platform2, config) {
|
|
4516
|
+
if (config.servers.length === 0 && config.problems.length === 0) {
|
|
4517
|
+
await platform2.write(1, "no servers configured\n");
|
|
4518
|
+
return;
|
|
4519
|
+
}
|
|
4520
|
+
for (const entry of config.servers) {
|
|
4521
|
+
await platform2.write(1, `${formatListEntry(entry)}
|
|
4522
|
+
`);
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4525
|
+
async function runList(platform2, args2) {
|
|
4526
|
+
let json = false;
|
|
4527
|
+
let configFile;
|
|
4528
|
+
let trustProject = false;
|
|
4529
|
+
for (let i = 0; i < args2.length; i++) {
|
|
4530
|
+
const arg = args2[i];
|
|
4531
|
+
if (arg === "--json") {
|
|
4532
|
+
json = true;
|
|
4533
|
+
continue;
|
|
4534
|
+
}
|
|
4535
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
4536
|
+
configFile = args2[++i];
|
|
4537
|
+
continue;
|
|
4538
|
+
}
|
|
4539
|
+
if (arg === "--trust-project") {
|
|
4540
|
+
trustProject = true;
|
|
4541
|
+
}
|
|
4542
|
+
}
|
|
4543
|
+
const source = await MergedConfigSource.load(platform2, { trustProject, configFile });
|
|
4544
|
+
const { config } = source;
|
|
4545
|
+
if (json) {
|
|
4546
|
+
await printListJson(platform2, config);
|
|
4547
|
+
} else {
|
|
4548
|
+
await printListText(platform2, config);
|
|
4549
|
+
}
|
|
4550
|
+
let hasProblems = false;
|
|
4551
|
+
for (const p of config.problems) {
|
|
4552
|
+
hasProblems = true;
|
|
4553
|
+
const prefix = p.server ? `${p.server}: ` : "";
|
|
4554
|
+
await platform2.write(2, `mcpcat list: ${prefix}${p.message}
|
|
4555
|
+
`);
|
|
4556
|
+
}
|
|
4557
|
+
return hasProblems ? 1 : 0;
|
|
4558
|
+
}
|
|
4559
|
+
async function runEnable(platform2, args2, enable) {
|
|
4560
|
+
let configFile;
|
|
4561
|
+
let local = false;
|
|
4562
|
+
const names = [];
|
|
4563
|
+
for (let i = 0; i < args2.length; i++) {
|
|
4564
|
+
const arg = args2[i];
|
|
4565
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
4566
|
+
configFile = args2[++i];
|
|
4567
|
+
continue;
|
|
4568
|
+
}
|
|
4569
|
+
if (arg === "-l" || arg === "--local") {
|
|
4570
|
+
local = true;
|
|
4571
|
+
continue;
|
|
4572
|
+
}
|
|
4573
|
+
if (arg.startsWith("-")) continue;
|
|
4574
|
+
names.push(arg);
|
|
4575
|
+
}
|
|
4576
|
+
if (names.length === 0) {
|
|
4577
|
+
await platform2.write(2, `mcpcat ${enable ? "enable" : "disable"}: requires a server name
|
|
4578
|
+
`);
|
|
4579
|
+
return 2;
|
|
4580
|
+
}
|
|
4581
|
+
const path = configFile ?? (local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
4582
|
+
for (const name of names) {
|
|
4583
|
+
const ok = await updateServer(platform2.fs, path, name, { enabled: enable });
|
|
4584
|
+
if (!ok) {
|
|
4585
|
+
await platform2.write(
|
|
4586
|
+
2,
|
|
4587
|
+
`mcpcat ${enable ? "enable" : "disable"}: '${name}' not found in ${path}
|
|
4588
|
+
`
|
|
4589
|
+
);
|
|
4590
|
+
return 1;
|
|
4591
|
+
}
|
|
4592
|
+
await platform2.write(1, `${enable ? "enabled" : "disabled"} ${name} in ${path}
|
|
4593
|
+
`);
|
|
4594
|
+
}
|
|
4595
|
+
return 0;
|
|
4596
|
+
}
|
|
4597
|
+
async function runExposure(platform2, args2) {
|
|
4598
|
+
let configFile;
|
|
4599
|
+
let local = false;
|
|
4600
|
+
let toolGlob;
|
|
4601
|
+
const positional = [];
|
|
4602
|
+
for (let i = 0; i < args2.length; i++) {
|
|
4603
|
+
const arg = args2[i];
|
|
4604
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
4605
|
+
configFile = args2[++i];
|
|
4606
|
+
continue;
|
|
4607
|
+
}
|
|
4608
|
+
if (arg === "-l" || arg === "--local") {
|
|
4609
|
+
local = true;
|
|
4610
|
+
continue;
|
|
4611
|
+
}
|
|
4612
|
+
if (arg === "--tool" && i + 1 < args2.length) {
|
|
4613
|
+
toolGlob = args2[++i];
|
|
4614
|
+
continue;
|
|
4615
|
+
}
|
|
4616
|
+
if (arg.startsWith("-")) continue;
|
|
4617
|
+
positional.push(arg);
|
|
4618
|
+
}
|
|
4619
|
+
if (positional.length < 2) {
|
|
4620
|
+
await platform2.write(2, "mcpcat exposure: requires <name> <mode>\n");
|
|
4621
|
+
return 2;
|
|
4622
|
+
}
|
|
4623
|
+
const [name, mode] = positional;
|
|
4624
|
+
const validModes = ["codemode", "deferred", "direct", "hidden"];
|
|
4625
|
+
if (!validModes.includes(mode)) {
|
|
4626
|
+
await platform2.write(2, `mcpcat exposure: mode must be one of ${validModes.join(", ")}
|
|
4627
|
+
`);
|
|
4628
|
+
return 2;
|
|
4629
|
+
}
|
|
4630
|
+
const path = configFile ?? (local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
4631
|
+
const ok = await setExposure(platform2.fs, path, name, mode, toolGlob);
|
|
4632
|
+
if (!ok) {
|
|
4633
|
+
await platform2.write(2, `mcpcat exposure: '${name}' not found in ${path}
|
|
4634
|
+
`);
|
|
4635
|
+
return 1;
|
|
4636
|
+
}
|
|
4637
|
+
const what = toolGlob ? `${name} tool ${toolGlob}` : name;
|
|
4638
|
+
await platform2.write(1, `set ${what} to ${mode} in ${path}
|
|
4639
|
+
`);
|
|
4640
|
+
return 0;
|
|
4641
|
+
}
|
|
4642
|
+
async function runImport(platform2, args2) {
|
|
4643
|
+
let configFile;
|
|
4644
|
+
let local = false;
|
|
4645
|
+
const positional = [];
|
|
4646
|
+
for (let i = 0; i < args2.length; i++) {
|
|
4647
|
+
const arg = args2[i];
|
|
4648
|
+
if (arg === "--config" && i + 1 < args2.length) {
|
|
4649
|
+
configFile = args2[++i];
|
|
4650
|
+
continue;
|
|
4651
|
+
}
|
|
4652
|
+
if (arg === "-l" || arg === "--local") {
|
|
4653
|
+
local = true;
|
|
4654
|
+
continue;
|
|
4655
|
+
}
|
|
4656
|
+
if (arg.startsWith("-")) continue;
|
|
4657
|
+
positional.push(arg);
|
|
4658
|
+
}
|
|
4659
|
+
if (positional.length < 1) {
|
|
4660
|
+
await platform2.write(2, "mcpcat import: requires a file path\n");
|
|
4661
|
+
return 2;
|
|
4662
|
+
}
|
|
4663
|
+
const filePath = positional[0];
|
|
4664
|
+
const names = positional.slice(1);
|
|
4665
|
+
let text;
|
|
4666
|
+
try {
|
|
4667
|
+
const data = await platform2.fs.readFile(filePath);
|
|
4668
|
+
text = new TextDecoder().decode(data);
|
|
4669
|
+
} catch {
|
|
4670
|
+
await platform2.write(2, `mcpcat import: cannot read '${filePath}'
|
|
4671
|
+
`);
|
|
4672
|
+
return 2;
|
|
4673
|
+
}
|
|
4674
|
+
const result = importFile(text, filePath, names.length > 0 ? names : void 0);
|
|
4675
|
+
for (const err of result.errors) {
|
|
4676
|
+
await platform2.write(2, `mcpcat import: ${err}
|
|
4677
|
+
`);
|
|
4678
|
+
}
|
|
4679
|
+
if (result.servers.length === 0) {
|
|
4680
|
+
await platform2.write(2, "mcpcat import: no servers found\n");
|
|
4681
|
+
return 1;
|
|
4682
|
+
}
|
|
4683
|
+
const destPath = configFile ?? (local ? projectConfigPath(platform2) : userConfigPath(platform2));
|
|
4684
|
+
for (const { name, config } of result.servers) {
|
|
4685
|
+
const validation = validateServer(name, config);
|
|
4686
|
+
if ("message" in validation) {
|
|
4687
|
+
await platform2.write(2, `mcpcat import: ${name}: ${validation.message}
|
|
4688
|
+
`);
|
|
4689
|
+
continue;
|
|
4690
|
+
}
|
|
4691
|
+
const { replaced } = await addServer(platform2.fs, destPath, name, config);
|
|
4692
|
+
await platform2.write(1, `${replaced ? "replaced" : "imported"} ${name}
|
|
4693
|
+
`);
|
|
4694
|
+
}
|
|
4695
|
+
return 0;
|
|
2102
4696
|
}
|
|
2103
4697
|
|
|
2104
4698
|
// src/cli/main.ts
|
|
@@ -2130,6 +4724,15 @@ var VERBS = [
|
|
|
2130
4724
|
"inbox",
|
|
2131
4725
|
"help"
|
|
2132
4726
|
];
|
|
4727
|
+
var CONFIG_VERBS = {
|
|
4728
|
+
add: true,
|
|
4729
|
+
rm: true,
|
|
4730
|
+
list: true,
|
|
4731
|
+
enable: true,
|
|
4732
|
+
disable: true,
|
|
4733
|
+
exposure: true,
|
|
4734
|
+
import: true
|
|
4735
|
+
};
|
|
2133
4736
|
var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
2134
4737
|
"resources",
|
|
2135
4738
|
"read",
|
|
@@ -2138,21 +4741,13 @@ var UNIMPLEMENTED_VERBS = /* @__PURE__ */ new Set([
|
|
|
2138
4741
|
"grep",
|
|
2139
4742
|
"complete",
|
|
2140
4743
|
"task",
|
|
2141
|
-
"add",
|
|
2142
|
-
"rm",
|
|
2143
|
-
"list",
|
|
2144
|
-
"enable",
|
|
2145
|
-
"disable",
|
|
2146
|
-
"exposure",
|
|
2147
|
-
"import",
|
|
2148
4744
|
"login",
|
|
2149
4745
|
"logout",
|
|
2150
|
-
"serve",
|
|
2151
4746
|
"code",
|
|
2152
4747
|
"discover",
|
|
2153
4748
|
"inbox"
|
|
2154
4749
|
]);
|
|
2155
|
-
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe"]);
|
|
4750
|
+
var CLIENT_VERBS = /* @__PURE__ */ new Set(["info", "tools", "call", "describe", "serve"]);
|
|
2156
4751
|
function helpText(version) {
|
|
2157
4752
|
return `mcpcat ${version} \u2013 a curl for the Model Context Protocol
|
|
2158
4753
|
|
|
@@ -2166,17 +4761,17 @@ Client verbs:
|
|
|
2166
4761
|
describe <target> [tool] Tool schema and help
|
|
2167
4762
|
|
|
2168
4763
|
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
|
|
4764
|
+
add <name> <url> Add a server to mcp.json
|
|
4765
|
+
rm <name> Remove a server
|
|
4766
|
+
list List configured servers
|
|
4767
|
+
enable|disable <name> Toggle a server
|
|
4768
|
+
exposure <name> <mode> Set exposure mode
|
|
4769
|
+
import <file> Import from other configs
|
|
2175
4770
|
login <target> OAuth sign-in (not implemented yet)
|
|
2176
4771
|
logout <target> Remove tokens (not implemented yet)
|
|
2177
4772
|
|
|
2178
4773
|
Server and code verbs:
|
|
2179
|
-
serve [--stdio|--http] \u2026 Expose commands as MCP server
|
|
4774
|
+
serve [--stdio|--http] \u2026 Expose commands as MCP server
|
|
2180
4775
|
code [-e src|file.js] Run a codemode script (not implemented yet)
|
|
2181
4776
|
discover <url> Find MCP servers (not implemented yet)
|
|
2182
4777
|
inbox Discovery inbox (not implemented yet)
|
|
@@ -2430,6 +5025,55 @@ async function runVerb(platform2, parsed, conn) {
|
|
|
2430
5025
|
return 2;
|
|
2431
5026
|
}
|
|
2432
5027
|
}
|
|
5028
|
+
async function probeServer(_name, config, platform2) {
|
|
5029
|
+
try {
|
|
5030
|
+
const opts = { version: "probe" };
|
|
5031
|
+
if ("url" in config && config.url) {
|
|
5032
|
+
opts.url = config.url;
|
|
5033
|
+
if ("headers" in config) opts.headers = config.headers;
|
|
5034
|
+
} else if ("command" in config && config.command) {
|
|
5035
|
+
opts.command = config.command;
|
|
5036
|
+
if ("args" in config) opts.args = config.args;
|
|
5037
|
+
if ("env" in config) opts.env = config.env;
|
|
5038
|
+
if ("cwd" in config) opts.cwd = config.cwd;
|
|
5039
|
+
} else {
|
|
5040
|
+
return void 0;
|
|
5041
|
+
}
|
|
5042
|
+
const conn = await connect(platform2, opts);
|
|
5043
|
+
try {
|
|
5044
|
+
await conn.info();
|
|
5045
|
+
} finally {
|
|
5046
|
+
try {
|
|
5047
|
+
await conn.close();
|
|
5048
|
+
} catch {
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
5051
|
+
return void 0;
|
|
5052
|
+
} catch (err) {
|
|
5053
|
+
return err.message;
|
|
5054
|
+
}
|
|
5055
|
+
}
|
|
5056
|
+
async function dispatchConfigVerb(platform2, verb, args2) {
|
|
5057
|
+
const rest = args2.slice(1);
|
|
5058
|
+
switch (verb) {
|
|
5059
|
+
case "add":
|
|
5060
|
+
return runAdd(platform2, rest, probeServer);
|
|
5061
|
+
case "rm":
|
|
5062
|
+
return runRm(platform2, rest);
|
|
5063
|
+
case "list":
|
|
5064
|
+
return runList(platform2, rest);
|
|
5065
|
+
case "enable":
|
|
5066
|
+
return runEnable(platform2, rest, true);
|
|
5067
|
+
case "disable":
|
|
5068
|
+
return runEnable(platform2, rest, false);
|
|
5069
|
+
case "exposure":
|
|
5070
|
+
return runExposure(platform2, rest);
|
|
5071
|
+
case "import":
|
|
5072
|
+
return runImport(platform2, rest);
|
|
5073
|
+
default:
|
|
5074
|
+
return 1;
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
2433
5077
|
async function run(platform2, version, args2) {
|
|
2434
5078
|
if (args2.includes("--version") || args2.includes("-V")) {
|
|
2435
5079
|
await platform2.write(1, `${version}
|
|
@@ -2449,11 +5093,23 @@ async function run(platform2, version, args2) {
|
|
|
2449
5093
|
await platform2.write(2, "mcpcat: no verb or target given\n");
|
|
2450
5094
|
return 2;
|
|
2451
5095
|
}
|
|
5096
|
+
if (parsed.verb in CONFIG_VERBS) {
|
|
5097
|
+
return dispatchConfigVerb(platform2, parsed.verb, args2);
|
|
5098
|
+
}
|
|
2452
5099
|
if (UNIMPLEMENTED_VERBS.has(parsed.verb)) {
|
|
2453
5100
|
await platform2.write(2, `mcpcat: '${parsed.verb}' is not implemented yet
|
|
2454
5101
|
`);
|
|
2455
5102
|
return 1;
|
|
2456
5103
|
}
|
|
5104
|
+
if (parsed.verb === "serve") {
|
|
5105
|
+
const serveArgs = args2.slice(args2.indexOf("serve") + 1);
|
|
5106
|
+
const serveOptions = parseServeFlags(serveArgs);
|
|
5107
|
+
if (serveOptions.commands.length === 0 && serveOptions.toolTemplates.length === 0) {
|
|
5108
|
+
await platform2.write(2, "mcpcat serve: no commands or --tool templates given\n");
|
|
5109
|
+
return 2;
|
|
5110
|
+
}
|
|
5111
|
+
return runServe(platform2, serveOptions, version);
|
|
5112
|
+
}
|
|
2457
5113
|
if (!CLIENT_VERBS.has(parsed.verb)) {
|
|
2458
5114
|
await platform2.write(2, `mcpcat: unknown command '${parsed.verb}'
|
|
2459
5115
|
`);
|
|
@@ -2468,7 +5124,7 @@ async function run(platform2, version, args2) {
|
|
|
2468
5124
|
`);
|
|
2469
5125
|
return 2;
|
|
2470
5126
|
}
|
|
2471
|
-
const config =
|
|
5127
|
+
const config = await MergedConfigSource.load(platform2);
|
|
2472
5128
|
let resolved;
|
|
2473
5129
|
try {
|
|
2474
5130
|
resolved = await resolveTarget(
|
|
@@ -2516,7 +5172,8 @@ async function run(platform2, version, args2) {
|
|
|
2516
5172
|
// src/platform/host.ts
|
|
2517
5173
|
import { spawn as cpSpawn } from "node:child_process";
|
|
2518
5174
|
import { constants } from "node:fs";
|
|
2519
|
-
import { access, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
5175
|
+
import { access, mkdir, readFile as readFile2, rename, rmdir, stat, writeFile } from "node:fs/promises";
|
|
5176
|
+
import { createServer } from "node:net";
|
|
2520
5177
|
import { Readable, Writable } from "node:stream";
|
|
2521
5178
|
import * as tty from "node:tty";
|
|
2522
5179
|
function nodeSpawn(options) {
|
|
@@ -2559,7 +5216,7 @@ function signalNumber(sig) {
|
|
|
2559
5216
|
}
|
|
2560
5217
|
var hostFs = {
|
|
2561
5218
|
async readFile(path) {
|
|
2562
|
-
return new Uint8Array(await
|
|
5219
|
+
return new Uint8Array(await readFile2(path));
|
|
2563
5220
|
},
|
|
2564
5221
|
async writeFile(path, data) {
|
|
2565
5222
|
await writeFile(path, data);
|
|
@@ -2581,6 +5238,12 @@ var hostFs = {
|
|
|
2581
5238
|
} catch {
|
|
2582
5239
|
return false;
|
|
2583
5240
|
}
|
|
5241
|
+
},
|
|
5242
|
+
async mkdirExclusive(path) {
|
|
5243
|
+
await mkdir(path, { recursive: false });
|
|
5244
|
+
},
|
|
5245
|
+
async rmdir(path) {
|
|
5246
|
+
await rmdir(path);
|
|
2584
5247
|
}
|
|
2585
5248
|
};
|
|
2586
5249
|
function createStdinStream() {
|
|
@@ -2592,11 +5255,77 @@ function createStdoutStream() {
|
|
|
2592
5255
|
function createStderrStream() {
|
|
2593
5256
|
return Writable.toWeb(process.stderr);
|
|
2594
5257
|
}
|
|
5258
|
+
function nodeListen(options) {
|
|
5259
|
+
return new Promise((resolve, reject) => {
|
|
5260
|
+
const server = createServer();
|
|
5261
|
+
const pending = [];
|
|
5262
|
+
const ready = [];
|
|
5263
|
+
let closed = false;
|
|
5264
|
+
server.on("connection", (socket) => {
|
|
5265
|
+
const conn = {
|
|
5266
|
+
readable: Readable.toWeb(socket),
|
|
5267
|
+
writable: Writable.toWeb(socket),
|
|
5268
|
+
remoteAddress: socket.remoteAddress ?? "",
|
|
5269
|
+
close() {
|
|
5270
|
+
socket.destroy();
|
|
5271
|
+
}
|
|
5272
|
+
};
|
|
5273
|
+
const waiter = pending.shift();
|
|
5274
|
+
if (waiter) {
|
|
5275
|
+
waiter.resolve(conn);
|
|
5276
|
+
} else {
|
|
5277
|
+
ready.push(conn);
|
|
5278
|
+
}
|
|
5279
|
+
});
|
|
5280
|
+
server.on("error", (err) => {
|
|
5281
|
+
if (!closed) reject(err);
|
|
5282
|
+
for (const waiter of pending) waiter.reject(err);
|
|
5283
|
+
pending.length = 0;
|
|
5284
|
+
});
|
|
5285
|
+
server.listen(options.port, options.host ?? "127.0.0.1", () => {
|
|
5286
|
+
const addr = server.address();
|
|
5287
|
+
const actualPort = typeof addr === "object" && addr ? addr.port : options.port;
|
|
5288
|
+
const listener = {
|
|
5289
|
+
get port() {
|
|
5290
|
+
return actualPort;
|
|
5291
|
+
},
|
|
5292
|
+
accept() {
|
|
5293
|
+
const queued = ready.shift();
|
|
5294
|
+
if (queued) return Promise.resolve(queued);
|
|
5295
|
+
return new Promise((res, rej) => {
|
|
5296
|
+
if (closed) {
|
|
5297
|
+
rej(new Error("listener closed"));
|
|
5298
|
+
return;
|
|
5299
|
+
}
|
|
5300
|
+
pending.push({ resolve: res, reject: rej });
|
|
5301
|
+
});
|
|
5302
|
+
},
|
|
5303
|
+
close() {
|
|
5304
|
+
closed = true;
|
|
5305
|
+
server.close();
|
|
5306
|
+
for (const waiter of pending) waiter.reject(new Error("listener closed"));
|
|
5307
|
+
pending.length = 0;
|
|
5308
|
+
},
|
|
5309
|
+
async *[Symbol.asyncIterator]() {
|
|
5310
|
+
while (!closed) {
|
|
5311
|
+
try {
|
|
5312
|
+
yield await this.accept();
|
|
5313
|
+
} catch {
|
|
5314
|
+
break;
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
};
|
|
5319
|
+
resolve(listener);
|
|
5320
|
+
});
|
|
5321
|
+
});
|
|
5322
|
+
}
|
|
2595
5323
|
function createHostPlatform() {
|
|
2596
|
-
const
|
|
5324
|
+
const encoder2 = new TextEncoder();
|
|
2597
5325
|
return {
|
|
2598
5326
|
fetch: globalThis.fetch,
|
|
2599
5327
|
spawn: nodeSpawn,
|
|
5328
|
+
listen: nodeListen,
|
|
2600
5329
|
fs: hostFs,
|
|
2601
5330
|
env: process.env,
|
|
2602
5331
|
cwd: process.cwd(),
|
|
@@ -2607,7 +5336,7 @@ function createHostPlatform() {
|
|
|
2607
5336
|
stdout: createStdoutStream(),
|
|
2608
5337
|
stderr: createStderrStream(),
|
|
2609
5338
|
async write(fd, data) {
|
|
2610
|
-
const bytes = typeof data === "string" ?
|
|
5339
|
+
const bytes = typeof data === "string" ? encoder2.encode(data) : data;
|
|
2611
5340
|
const stream = fd === 1 ? process.stdout : process.stderr;
|
|
2612
5341
|
return new Promise((resolve, reject) => {
|
|
2613
5342
|
stream.write(bytes, (err) => err ? reject(err) : resolve());
|
|
@@ -2615,6 +5344,9 @@ function createHostPlatform() {
|
|
|
2615
5344
|
},
|
|
2616
5345
|
exit(code2) {
|
|
2617
5346
|
process.exit(code2);
|
|
5347
|
+
},
|
|
5348
|
+
onSignal(signal, handler) {
|
|
5349
|
+
process.on(signal, handler);
|
|
2618
5350
|
}
|
|
2619
5351
|
};
|
|
2620
5352
|
}
|
|
@@ -2622,5 +5354,5 @@ function createHostPlatform() {
|
|
|
2622
5354
|
// src/host.ts
|
|
2623
5355
|
var platform = createHostPlatform();
|
|
2624
5356
|
var args = process.argv.slice(2);
|
|
2625
|
-
var code = await run(platform, "0.
|
|
5357
|
+
var code = await run(platform, "0.4.0", args);
|
|
2626
5358
|
process.exit(code);
|