muxed 0.1.1 → 0.2.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/README.md +60 -3
- package/dist/cli.mjs +913 -144
- package/dist/client/index.d.mts +16 -2
- package/dist/client/index.mjs +8 -1
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -3,17 +3,20 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { z } from "zod/v4";
|
|
6
|
+
import * as z$1 from "zod";
|
|
6
7
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
8
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
9
|
import { SSEClientTransport, SseError } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
9
10
|
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
10
11
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
11
12
|
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import crypto from "node:crypto";
|
|
12
14
|
import { execFile, fork } from "node:child_process";
|
|
13
15
|
import http from "node:http";
|
|
14
16
|
import { compile } from "json-schema-to-typescript";
|
|
15
17
|
import net from "node:net";
|
|
16
18
|
import { Command } from "commander";
|
|
19
|
+
import { PostHog } from "posthog-node";
|
|
17
20
|
import * as readline from "node:readline/promises";
|
|
18
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -31,7 +34,8 @@ const StdioServerConfigSchema = z.object({
|
|
|
31
34
|
command: z.string(),
|
|
32
35
|
args: z.array(z.string()).optional(),
|
|
33
36
|
env: z.record(z.string(), z.string()).optional(),
|
|
34
|
-
cwd: z.string().optional()
|
|
37
|
+
cwd: z.string().optional(),
|
|
38
|
+
timeout: z.number().optional()
|
|
35
39
|
});
|
|
36
40
|
const ReconnectionSchema = z.object({
|
|
37
41
|
maxDelay: z.number().optional(),
|
|
@@ -59,7 +63,8 @@ const HttpServerConfigSchema = z.object({
|
|
|
59
63
|
headers: z.record(z.string(), z.string()).optional(),
|
|
60
64
|
sessionId: z.string().optional(),
|
|
61
65
|
reconnection: ReconnectionSchema.optional(),
|
|
62
|
-
auth: OAuthConfigSchema.optional()
|
|
66
|
+
auth: OAuthConfigSchema.optional(),
|
|
67
|
+
timeout: z.number().optional()
|
|
63
68
|
});
|
|
64
69
|
const ServerConfigSchema = z.union([StdioServerConfigSchema, HttpServerConfigSchema]);
|
|
65
70
|
const HttpListenerSchema = z.object({
|
|
@@ -113,7 +118,7 @@ function mergeClaudeDesktopServers(servers) {
|
|
|
113
118
|
const DAEMON_DEFAULTS = {
|
|
114
119
|
idleTimeout: 3e5,
|
|
115
120
|
connectTimeout: 3e4,
|
|
116
|
-
requestTimeout:
|
|
121
|
+
requestTimeout: 3e4,
|
|
117
122
|
healthCheckInterval: 3e4,
|
|
118
123
|
maxRestartAttempts: -1,
|
|
119
124
|
maxTotalTimeout: 3e5,
|
|
@@ -122,7 +127,7 @@ const DAEMON_DEFAULTS = {
|
|
|
122
127
|
shutdownTimeout: 1e4
|
|
123
128
|
};
|
|
124
129
|
function getGlobalConfigPath() {
|
|
125
|
-
return path.join(os.homedir(), ".
|
|
130
|
+
return path.join(os.homedir(), ".muxed", "config.json");
|
|
126
131
|
}
|
|
127
132
|
function findConfigFile(configPath) {
|
|
128
133
|
if (configPath) {
|
|
@@ -295,11 +300,14 @@ function initLogger(opts) {
|
|
|
295
300
|
function sanitizeName(name) {
|
|
296
301
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
297
302
|
}
|
|
303
|
+
function hashName(name) {
|
|
304
|
+
return crypto.createHash("sha256").update(name).digest("hex").slice(0, 8);
|
|
305
|
+
}
|
|
298
306
|
function getAuthDir() {
|
|
299
307
|
return path.join(getMuxedDir(), "auth");
|
|
300
308
|
}
|
|
301
309
|
function getStorePath(serverName) {
|
|
302
|
-
return path.join(getAuthDir(), `${sanitizeName(serverName)}.json`);
|
|
310
|
+
return path.join(getAuthDir(), `${sanitizeName(serverName)}-${hashName(serverName)}.json`);
|
|
303
311
|
}
|
|
304
312
|
function ensureAuthDir() {
|
|
305
313
|
fs.mkdirSync(getAuthDir(), {
|
|
@@ -365,9 +373,8 @@ var TokenStore = class {
|
|
|
365
373
|
writeStore(this.serverName, data);
|
|
366
374
|
}
|
|
367
375
|
clearAll() {
|
|
368
|
-
const filePath = getStorePath(this.serverName);
|
|
369
376
|
try {
|
|
370
|
-
fs.unlinkSync(
|
|
377
|
+
fs.unlinkSync(getStorePath(this.serverName));
|
|
371
378
|
} catch {}
|
|
372
379
|
}
|
|
373
380
|
hasTokens() {
|
|
@@ -1051,9 +1058,108 @@ var ServerManager = class {
|
|
|
1051
1058
|
};
|
|
1052
1059
|
}
|
|
1053
1060
|
};
|
|
1061
|
+
const ErrorCode = {
|
|
1062
|
+
TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
|
|
1063
|
+
SERVER_NOT_FOUND: "SERVER_NOT_FOUND",
|
|
1064
|
+
SERVER_NOT_CONNECTED: "SERVER_NOT_CONNECTED",
|
|
1065
|
+
INVALID_ARGUMENTS: "INVALID_ARGUMENTS",
|
|
1066
|
+
INVALID_FORMAT: "INVALID_FORMAT",
|
|
1067
|
+
MISSING_PARAMETER: "MISSING_PARAMETER",
|
|
1068
|
+
TIMEOUT: "TIMEOUT"
|
|
1069
|
+
};
|
|
1070
|
+
function levenshtein(a, b) {
|
|
1071
|
+
const m = a.length;
|
|
1072
|
+
const n = b.length;
|
|
1073
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
1074
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
1075
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
1076
|
+
for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
1077
|
+
return dp[m][n];
|
|
1078
|
+
}
|
|
1079
|
+
function findSimilarTools(targetTool, allTools, maxResults = 3) {
|
|
1080
|
+
const maxDistance = Math.max(3, Math.floor(targetTool.length * .4));
|
|
1081
|
+
return allTools.map(({ server, tool }) => {
|
|
1082
|
+
const fullName = `${server}/${tool.name}`;
|
|
1083
|
+
const toolOnly = tool.name;
|
|
1084
|
+
const distFull = levenshtein(targetTool.toLowerCase(), fullName.toLowerCase());
|
|
1085
|
+
const distTool = levenshtein(targetTool.toLowerCase(), toolOnly.toLowerCase());
|
|
1086
|
+
return {
|
|
1087
|
+
fullName,
|
|
1088
|
+
dist: Math.min(distFull, distTool)
|
|
1089
|
+
};
|
|
1090
|
+
}).filter(({ dist }) => dist <= maxDistance).sort((a, b) => a.dist - b.dist).slice(0, maxResults).map(({ fullName }) => fullName);
|
|
1091
|
+
}
|
|
1092
|
+
function toolNotFoundError(name, similarTools) {
|
|
1093
|
+
const hasSimilar = similarTools.length > 0;
|
|
1094
|
+
const suggestion = hasSimilar ? `Did you mean: ${similarTools.join(", ")}? Run 'muxed grep <pattern>' to search available tools.` : `Run 'muxed grep <pattern>' to find available tools, or 'muxed tools' to list all.`;
|
|
1095
|
+
return {
|
|
1096
|
+
code: ErrorCode.TOOL_NOT_FOUND,
|
|
1097
|
+
message: `Tool not found: ${name}`,
|
|
1098
|
+
suggestion,
|
|
1099
|
+
context: hasSimilar ? { similarTools } : void 0
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function serverNotFoundError(serverName, availableServers) {
|
|
1103
|
+
return {
|
|
1104
|
+
code: ErrorCode.SERVER_NOT_FOUND,
|
|
1105
|
+
message: `Server not found: ${serverName}`,
|
|
1106
|
+
suggestion: `Available servers: ${availableServers.join(", ") || "none"}. Run 'muxed servers' to list all.`,
|
|
1107
|
+
context: { availableServers }
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function serverNotConnectedError(serverName) {
|
|
1111
|
+
return {
|
|
1112
|
+
code: ErrorCode.SERVER_NOT_CONNECTED,
|
|
1113
|
+
message: `Server not connected: ${serverName}`,
|
|
1114
|
+
suggestion: `The server may be starting up. Run 'muxed status' to check, or 'muxed reload' to reconnect.`
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
function invalidFormatError(name) {
|
|
1118
|
+
return {
|
|
1119
|
+
code: ErrorCode.INVALID_FORMAT,
|
|
1120
|
+
message: `Invalid tool name format: ${name}`,
|
|
1121
|
+
suggestion: `Use the format 'server/tool' (e.g. 'myserver/mytool'). Run 'muxed tools' to list all available tools.`
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function missingParameterError(param) {
|
|
1125
|
+
return {
|
|
1126
|
+
code: ErrorCode.MISSING_PARAMETER,
|
|
1127
|
+
message: `Missing required parameter: ${param}`,
|
|
1128
|
+
suggestion: `Provide the '${param}' parameter in the request.`
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function invalidArgumentsError(toolName, errors) {
|
|
1132
|
+
return {
|
|
1133
|
+
code: ErrorCode.INVALID_ARGUMENTS,
|
|
1134
|
+
message: `Invalid arguments for tool ${toolName}`,
|
|
1135
|
+
suggestion: `Run 'muxed info ${toolName}' to see the expected input schema.`,
|
|
1136
|
+
context: { validationErrors: errors }
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function timeoutError(toolName, timeoutMs) {
|
|
1140
|
+
return {
|
|
1141
|
+
code: ErrorCode.TIMEOUT,
|
|
1142
|
+
message: `Tool call timed out after ${timeoutMs}ms: ${toolName}`,
|
|
1143
|
+
suggestion: `Increase the timeout with --timeout <ms>, or use --async for long-running operations.`
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function isTimeoutError(err) {
|
|
1147
|
+
if (!(err instanceof Error)) return false;
|
|
1148
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") return true;
|
|
1149
|
+
const msg = err.message.toLowerCase();
|
|
1150
|
+
return msg.includes("timeout") || msg.includes("aborted");
|
|
1151
|
+
}
|
|
1152
|
+
function toErrorData(err) {
|
|
1153
|
+
return {
|
|
1154
|
+
code: err.code,
|
|
1155
|
+
suggestion: err.suggestion,
|
|
1156
|
+
...err.context ? { context: err.context } : {}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1054
1159
|
var ServerPool = class {
|
|
1055
1160
|
servers = /* @__PURE__ */ new Map();
|
|
1056
1161
|
trackedTasks = /* @__PURE__ */ new Map();
|
|
1162
|
+
zodSchemaCache = /* @__PURE__ */ new Map();
|
|
1057
1163
|
taskExpiryTimer;
|
|
1058
1164
|
taskExpiryTimeout = 36e5;
|
|
1059
1165
|
async connectAll(config) {
|
|
@@ -1080,6 +1186,7 @@ var ServerPool = class {
|
|
|
1080
1186
|
}
|
|
1081
1187
|
async disconnectAll() {
|
|
1082
1188
|
this.stopTaskExpiry();
|
|
1189
|
+
this.zodSchemaCache.clear();
|
|
1083
1190
|
await Promise.allSettled([...this.servers.values()].map((manager) => manager.disconnect()));
|
|
1084
1191
|
}
|
|
1085
1192
|
onServerHealthChange(serverName, status, error) {
|
|
@@ -1157,6 +1264,91 @@ var ServerPool = class {
|
|
|
1157
1264
|
tool
|
|
1158
1265
|
};
|
|
1159
1266
|
}
|
|
1267
|
+
findToolOrError(serverTool) {
|
|
1268
|
+
const slashIndex = serverTool.indexOf("/");
|
|
1269
|
+
if (slashIndex === -1) return {
|
|
1270
|
+
ok: false,
|
|
1271
|
+
error: invalidFormatError(serverTool)
|
|
1272
|
+
};
|
|
1273
|
+
const serverName = serverTool.slice(0, slashIndex);
|
|
1274
|
+
const toolName = serverTool.slice(slashIndex + 1);
|
|
1275
|
+
const manager = this.servers.get(serverName);
|
|
1276
|
+
if (!manager) return {
|
|
1277
|
+
ok: false,
|
|
1278
|
+
error: serverNotFoundError(serverName, [...this.servers.keys()])
|
|
1279
|
+
};
|
|
1280
|
+
if (manager.getStatus() !== "connected") return {
|
|
1281
|
+
ok: false,
|
|
1282
|
+
error: serverNotConnectedError(serverName)
|
|
1283
|
+
};
|
|
1284
|
+
const tool = manager.listTools().find((t) => t.name === toolName);
|
|
1285
|
+
if (!tool) return {
|
|
1286
|
+
ok: false,
|
|
1287
|
+
error: toolNotFoundError(serverTool, findSimilarTools(serverTool, this.listAllTools()))
|
|
1288
|
+
};
|
|
1289
|
+
return {
|
|
1290
|
+
ok: true,
|
|
1291
|
+
manager,
|
|
1292
|
+
tool,
|
|
1293
|
+
serverTimeout: manager.getState().config.timeout
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
getZodSchema(inputSchema) {
|
|
1297
|
+
const key = JSON.stringify(inputSchema);
|
|
1298
|
+
const cached = this.zodSchemaCache.get(key);
|
|
1299
|
+
if (cached !== void 0) return cached;
|
|
1300
|
+
try {
|
|
1301
|
+
const zodSchema = z$1.fromJSONSchema(inputSchema);
|
|
1302
|
+
this.zodSchemaCache.set(key, zodSchema);
|
|
1303
|
+
return zodSchema;
|
|
1304
|
+
} catch {
|
|
1305
|
+
this.zodSchemaCache.set(key, "unsupported");
|
|
1306
|
+
return "unsupported";
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
validateToolArgs(serverTool, args) {
|
|
1310
|
+
const found = this.findToolOrError(serverTool);
|
|
1311
|
+
if (!found.ok) return {
|
|
1312
|
+
valid: false,
|
|
1313
|
+
errors: [found.error.message],
|
|
1314
|
+
warnings: []
|
|
1315
|
+
};
|
|
1316
|
+
const { tool } = found;
|
|
1317
|
+
const errors = [];
|
|
1318
|
+
const warnings = [];
|
|
1319
|
+
if (tool.inputSchema) {
|
|
1320
|
+
const zodSchema = this.getZodSchema(tool.inputSchema);
|
|
1321
|
+
if (zodSchema === "unsupported") {
|
|
1322
|
+
getLogger().warn(`Could not convert inputSchema for ${serverTool}: unsupported schema`, serverTool.split("/")[0]);
|
|
1323
|
+
this.addAnnotationWarnings(tool, warnings);
|
|
1324
|
+
return {
|
|
1325
|
+
valid: true,
|
|
1326
|
+
errors: [],
|
|
1327
|
+
warnings,
|
|
1328
|
+
unsupported: true,
|
|
1329
|
+
tool
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const result = zodSchema.safeParse(args);
|
|
1333
|
+
if (!result.success) for (const issue of result.error.issues) {
|
|
1334
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "";
|
|
1335
|
+
const prefix = path ? `Field '${path}': ` : "";
|
|
1336
|
+
errors.push(`${prefix}${issue.message}`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
this.addAnnotationWarnings(tool, warnings);
|
|
1340
|
+
return {
|
|
1341
|
+
valid: errors.length === 0,
|
|
1342
|
+
errors,
|
|
1343
|
+
warnings,
|
|
1344
|
+
tool
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
addAnnotationWarnings(tool, warnings) {
|
|
1348
|
+
if (tool.annotations?.destructiveHint) warnings.push("Tool is marked as destructive.");
|
|
1349
|
+
if (!tool.annotations?.idempotentHint) warnings.push("Tool is not marked as idempotent.");
|
|
1350
|
+
if (tool.annotations?.readOnlyHint === false) warnings.push("Tool may modify data (not read-only).");
|
|
1351
|
+
}
|
|
1160
1352
|
grepTools(pattern) {
|
|
1161
1353
|
const normalized = pattern.replace(/\\([|()\{\}])/g, "$1");
|
|
1162
1354
|
const regex = new RegExp(normalized, "i");
|
|
@@ -1353,6 +1545,89 @@ function indent(text, spaces) {
|
|
|
1353
1545
|
const pad = " ".repeat(spaces);
|
|
1354
1546
|
return text.split("\n").join("\n" + pad);
|
|
1355
1547
|
}
|
|
1548
|
+
function parsePath(path) {
|
|
1549
|
+
const segments = [];
|
|
1550
|
+
for (const part of path.split(".")) if (part.endsWith("[]")) segments.push({
|
|
1551
|
+
key: part.slice(0, -2),
|
|
1552
|
+
isArray: true
|
|
1553
|
+
});
|
|
1554
|
+
else segments.push({
|
|
1555
|
+
key: part,
|
|
1556
|
+
isArray: false
|
|
1557
|
+
});
|
|
1558
|
+
return segments;
|
|
1559
|
+
}
|
|
1560
|
+
function extractDeep(obj, segments) {
|
|
1561
|
+
if (segments.length === 0 || obj == null || typeof obj !== "object") return obj;
|
|
1562
|
+
const [first, ...rest] = segments;
|
|
1563
|
+
if (!first) return obj;
|
|
1564
|
+
const value = obj[first.key];
|
|
1565
|
+
if (first.isArray) {
|
|
1566
|
+
if (!Array.isArray(value)) return void 0;
|
|
1567
|
+
if (rest.length === 0) return value;
|
|
1568
|
+
return value.map((item) => extractDeep(item, rest)).filter((v) => v !== void 0);
|
|
1569
|
+
}
|
|
1570
|
+
if (rest.length === 0) return value;
|
|
1571
|
+
return extractDeep(value, rest);
|
|
1572
|
+
}
|
|
1573
|
+
function extract(obj, path) {
|
|
1574
|
+
return extractDeep(obj, parsePath(path));
|
|
1575
|
+
}
|
|
1576
|
+
function setNested(obj, keys, value) {
|
|
1577
|
+
let current = obj;
|
|
1578
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
1579
|
+
const key = keys[i];
|
|
1580
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
1581
|
+
current = current[key];
|
|
1582
|
+
}
|
|
1583
|
+
current[keys[keys.length - 1]] = value;
|
|
1584
|
+
}
|
|
1585
|
+
function extractFromObject(data, fields) {
|
|
1586
|
+
const result = {};
|
|
1587
|
+
for (const field of fields) {
|
|
1588
|
+
const value = extract(data, field);
|
|
1589
|
+
if (value !== void 0) setNested(result, field.replace(/\[\]/g, "").split("."), value);
|
|
1590
|
+
}
|
|
1591
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
1592
|
+
}
|
|
1593
|
+
function isJsonString(str) {
|
|
1594
|
+
const trimmed = str.trim();
|
|
1595
|
+
if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) return false;
|
|
1596
|
+
try {
|
|
1597
|
+
JSON.parse(trimmed);
|
|
1598
|
+
return true;
|
|
1599
|
+
} catch {
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function filterFields(data, fields) {
|
|
1604
|
+
if (data.structuredContent && typeof data.structuredContent === "object") {
|
|
1605
|
+
const filtered = extractFromObject(data.structuredContent, fields);
|
|
1606
|
+
if (filtered) return {
|
|
1607
|
+
...data,
|
|
1608
|
+
structuredContent: filtered
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const content = data.content;
|
|
1612
|
+
if (Array.isArray(content)) {
|
|
1613
|
+
const newContent = content.map((block) => {
|
|
1614
|
+
if (block.type !== "text" || !block.text || !isJsonString(block.text)) return block;
|
|
1615
|
+
try {
|
|
1616
|
+
const filtered = extractFromObject(JSON.parse(block.text), fields);
|
|
1617
|
+
if (filtered) return {
|
|
1618
|
+
...block,
|
|
1619
|
+
text: JSON.stringify(filtered)
|
|
1620
|
+
};
|
|
1621
|
+
} catch {}
|
|
1622
|
+
return block;
|
|
1623
|
+
});
|
|
1624
|
+
if (newContent.some((block, i) => block !== content[i])) return {
|
|
1625
|
+
...data,
|
|
1626
|
+
content: newContent
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
return data;
|
|
1630
|
+
}
|
|
1356
1631
|
function createDaemonServer(serverPool, config) {
|
|
1357
1632
|
const socketPath = getSocketPath();
|
|
1358
1633
|
let idleTimer;
|
|
@@ -1390,53 +1665,118 @@ function createDaemonServer(serverPool, config) {
|
|
|
1390
1665
|
}
|
|
1391
1666
|
case "tools/call": {
|
|
1392
1667
|
const p = params;
|
|
1393
|
-
if (!p?.name)
|
|
1668
|
+
if (!p?.name) {
|
|
1669
|
+
const err = missingParameterError("name");
|
|
1670
|
+
return {
|
|
1671
|
+
jsonrpc: "2.0",
|
|
1672
|
+
id,
|
|
1673
|
+
error: {
|
|
1674
|
+
code: -32602,
|
|
1675
|
+
message: err.message,
|
|
1676
|
+
data: toErrorData(err)
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
const found = serverPool.findToolOrError(p.name);
|
|
1681
|
+
if (!found.ok) return {
|
|
1394
1682
|
jsonrpc: "2.0",
|
|
1395
1683
|
id,
|
|
1396
1684
|
error: {
|
|
1397
1685
|
code: -32602,
|
|
1398
|
-
message:
|
|
1686
|
+
message: found.error.message,
|
|
1687
|
+
data: toErrorData(found.error)
|
|
1399
1688
|
}
|
|
1400
1689
|
};
|
|
1401
|
-
const
|
|
1402
|
-
if (!
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1690
|
+
const validation = serverPool.validateToolArgs(p.name, p.arguments ?? {});
|
|
1691
|
+
if (!validation.valid && !validation.unsupported) {
|
|
1692
|
+
const err = invalidArgumentsError(p.name, validation.errors);
|
|
1693
|
+
return {
|
|
1694
|
+
jsonrpc: "2.0",
|
|
1695
|
+
id,
|
|
1696
|
+
error: {
|
|
1697
|
+
code: -32602,
|
|
1698
|
+
message: err.message,
|
|
1699
|
+
data: toErrorData(err)
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
const timeout = clientTimeout ?? p.timeout ?? found.serverTimeout ?? requestTimeout;
|
|
1704
|
+
try {
|
|
1705
|
+
const callResult = await found.manager.callTool(found.tool.name, p.arguments ?? {}, timeout);
|
|
1706
|
+
if (p.fields && p.fields.length > 0) return {
|
|
1707
|
+
jsonrpc: "2.0",
|
|
1708
|
+
id,
|
|
1709
|
+
result: filterFields(callResult, p.fields)
|
|
1710
|
+
};
|
|
1711
|
+
return {
|
|
1712
|
+
jsonrpc: "2.0",
|
|
1713
|
+
id,
|
|
1714
|
+
result: callResult
|
|
1715
|
+
};
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
if (isTimeoutError(err)) {
|
|
1718
|
+
const te = timeoutError(p.name, timeout);
|
|
1719
|
+
return {
|
|
1720
|
+
jsonrpc: "2.0",
|
|
1721
|
+
id,
|
|
1722
|
+
error: {
|
|
1723
|
+
code: -32001,
|
|
1724
|
+
message: te.message,
|
|
1725
|
+
data: toErrorData(te)
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1408
1728
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
return {
|
|
1412
|
-
jsonrpc: "2.0",
|
|
1413
|
-
id,
|
|
1414
|
-
result: await found.manager.callTool(found.tool.name, p.arguments ?? {}, timeout)
|
|
1415
|
-
};
|
|
1729
|
+
throw err;
|
|
1730
|
+
}
|
|
1416
1731
|
}
|
|
1417
1732
|
case "tools/info": {
|
|
1418
1733
|
const p = params;
|
|
1419
|
-
if (!p?.name)
|
|
1734
|
+
if (!p?.name) {
|
|
1735
|
+
const err = missingParameterError("name");
|
|
1736
|
+
return {
|
|
1737
|
+
jsonrpc: "2.0",
|
|
1738
|
+
id,
|
|
1739
|
+
error: {
|
|
1740
|
+
code: -32602,
|
|
1741
|
+
message: err.message,
|
|
1742
|
+
data: toErrorData(err)
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
const found = serverPool.findToolOrError(p.name);
|
|
1747
|
+
if (!found.ok) return {
|
|
1420
1748
|
jsonrpc: "2.0",
|
|
1421
1749
|
id,
|
|
1422
1750
|
error: {
|
|
1423
1751
|
code: -32602,
|
|
1424
|
-
message:
|
|
1752
|
+
message: found.error.message,
|
|
1753
|
+
data: toErrorData(found.error)
|
|
1425
1754
|
}
|
|
1426
1755
|
};
|
|
1427
|
-
|
|
1428
|
-
if (!found) return {
|
|
1756
|
+
return {
|
|
1429
1757
|
jsonrpc: "2.0",
|
|
1430
1758
|
id,
|
|
1431
|
-
|
|
1432
|
-
code: -32602,
|
|
1433
|
-
message: `Tool not found: ${p.name}`
|
|
1434
|
-
}
|
|
1759
|
+
result: found.tool
|
|
1435
1760
|
};
|
|
1761
|
+
}
|
|
1762
|
+
case "tools/validate": {
|
|
1763
|
+
const p = params;
|
|
1764
|
+
if (!p?.name) {
|
|
1765
|
+
const err = missingParameterError("name");
|
|
1766
|
+
return {
|
|
1767
|
+
jsonrpc: "2.0",
|
|
1768
|
+
id,
|
|
1769
|
+
error: {
|
|
1770
|
+
code: -32602,
|
|
1771
|
+
message: err.message,
|
|
1772
|
+
data: toErrorData(err)
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1436
1776
|
return {
|
|
1437
1777
|
jsonrpc: "2.0",
|
|
1438
1778
|
id,
|
|
1439
|
-
result:
|
|
1779
|
+
result: serverPool.validateToolArgs(p.name, p.arguments ?? {})
|
|
1440
1780
|
};
|
|
1441
1781
|
}
|
|
1442
1782
|
case "auth/status": {
|
|
@@ -1632,33 +1972,68 @@ function createDaemonServer(serverPool, config) {
|
|
|
1632
1972
|
}
|
|
1633
1973
|
case "tools/call-async": {
|
|
1634
1974
|
const p = params;
|
|
1635
|
-
if (!p?.name)
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1975
|
+
if (!p?.name) {
|
|
1976
|
+
const err = missingParameterError("name");
|
|
1977
|
+
return {
|
|
1978
|
+
jsonrpc: "2.0",
|
|
1979
|
+
id,
|
|
1980
|
+
error: {
|
|
1981
|
+
code: -32602,
|
|
1982
|
+
message: err.message,
|
|
1983
|
+
data: toErrorData(err)
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
const foundAsync = serverPool.findToolOrError(p.name);
|
|
1988
|
+
if (!foundAsync.ok) return {
|
|
1645
1989
|
jsonrpc: "2.0",
|
|
1646
1990
|
id,
|
|
1647
1991
|
error: {
|
|
1648
1992
|
code: -32602,
|
|
1649
|
-
message:
|
|
1993
|
+
message: foundAsync.error.message,
|
|
1994
|
+
data: toErrorData(foundAsync.error)
|
|
1650
1995
|
}
|
|
1651
1996
|
};
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1997
|
+
const asyncValidation = serverPool.validateToolArgs(p.name, p.arguments ?? {});
|
|
1998
|
+
if (!asyncValidation.valid && !asyncValidation.unsupported) {
|
|
1999
|
+
const err = invalidArgumentsError(p.name, asyncValidation.errors);
|
|
2000
|
+
return {
|
|
2001
|
+
jsonrpc: "2.0",
|
|
2002
|
+
id,
|
|
2003
|
+
error: {
|
|
2004
|
+
code: -32602,
|
|
2005
|
+
message: err.message,
|
|
2006
|
+
data: toErrorData(err)
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
const taskHandle = await foundAsync.manager.callToolWithTask(foundAsync.tool.name, p.arguments ?? {});
|
|
2012
|
+
serverPool.trackTask(taskHandle.taskId, foundAsync.manager.name);
|
|
2013
|
+
return {
|
|
2014
|
+
jsonrpc: "2.0",
|
|
2015
|
+
id,
|
|
2016
|
+
result: {
|
|
2017
|
+
...taskHandle,
|
|
2018
|
+
server: foundAsync.manager.name
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
if (isTimeoutError(err)) {
|
|
2023
|
+
const asyncTimeout = foundAsync.serverTimeout ?? requestTimeout;
|
|
2024
|
+
const te = timeoutError(p.name, asyncTimeout);
|
|
2025
|
+
return {
|
|
2026
|
+
jsonrpc: "2.0",
|
|
2027
|
+
id,
|
|
2028
|
+
error: {
|
|
2029
|
+
code: -32001,
|
|
2030
|
+
message: te.message,
|
|
2031
|
+
data: toErrorData(te)
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
1660
2034
|
}
|
|
1661
|
-
|
|
2035
|
+
throw err;
|
|
2036
|
+
}
|
|
1662
2037
|
}
|
|
1663
2038
|
case "config/reload": {
|
|
1664
2039
|
const newConfig = loadConfig(params?.configPath);
|
|
@@ -2529,6 +2904,37 @@ function formatMcpServerList(servers) {
|
|
|
2529
2904
|
];
|
|
2530
2905
|
}));
|
|
2531
2906
|
}
|
|
2907
|
+
function formatStructuredError(error) {
|
|
2908
|
+
const lines = [];
|
|
2909
|
+
lines.push(`Error: ${error.message}`);
|
|
2910
|
+
if (error.data?.suggestion) lines.push(`Suggestion: ${error.data.suggestion}`);
|
|
2911
|
+
if (error.data?.context?.similarTools) {
|
|
2912
|
+
const similar = error.data.context.similarTools;
|
|
2913
|
+
if (similar.length > 0) lines.push(`Similar tools: ${similar.join(", ")}`);
|
|
2914
|
+
}
|
|
2915
|
+
if (error.data?.context?.availableServers) {
|
|
2916
|
+
const servers = error.data.context.availableServers;
|
|
2917
|
+
if (servers.length > 0) lines.push(`Available servers: ${servers.join(", ")}`);
|
|
2918
|
+
}
|
|
2919
|
+
return lines.join("\n");
|
|
2920
|
+
}
|
|
2921
|
+
function formatValidation(result) {
|
|
2922
|
+
const lines = [];
|
|
2923
|
+
if (result.unsupported) {
|
|
2924
|
+
lines.push("Validation: unsupported (tool schema uses features not supported by dry-run validation)");
|
|
2925
|
+
lines.push("The call will be forwarded to the MCP server without pre-validation.");
|
|
2926
|
+
} else if (result.valid) lines.push("Validation: passed");
|
|
2927
|
+
else {
|
|
2928
|
+
lines.push("Validation: failed");
|
|
2929
|
+
for (const err of result.errors) lines.push(` - ${err}`);
|
|
2930
|
+
}
|
|
2931
|
+
if (result.warnings.length > 0) {
|
|
2932
|
+
lines.push("");
|
|
2933
|
+
lines.push("Warnings:");
|
|
2934
|
+
for (const warn of result.warnings) lines.push(` - ${warn}`);
|
|
2935
|
+
}
|
|
2936
|
+
return lines.join("\n");
|
|
2937
|
+
}
|
|
2532
2938
|
function formatJson(data) {
|
|
2533
2939
|
return JSON.stringify(data, null, 2);
|
|
2534
2940
|
}
|
|
@@ -2546,10 +2952,67 @@ const serversCommand = new Command("servers").description("List connected MCP se
|
|
|
2546
2952
|
const result = await sendRequest("servers/list");
|
|
2547
2953
|
console.log(opts.json ? formatJson(result) : formatServers(result));
|
|
2548
2954
|
});
|
|
2955
|
+
const TELEMETRY_FILE = path.join(os.homedir(), ".muxed", "telemetry");
|
|
2956
|
+
const sessionId = crypto.randomUUID();
|
|
2957
|
+
function isTelemetryEnabled() {
|
|
2958
|
+
if (process.env.DO_NOT_TRACK === "1") return false;
|
|
2959
|
+
if (process.env.MUXED_TELEMETRY === "0") return false;
|
|
2960
|
+
try {
|
|
2961
|
+
if (fs.existsSync(TELEMETRY_FILE)) return fs.readFileSync(TELEMETRY_FILE, "utf-8").trim() !== "off";
|
|
2962
|
+
} catch {}
|
|
2963
|
+
return true;
|
|
2964
|
+
}
|
|
2965
|
+
function setTelemetryEnabled(enabled) {
|
|
2966
|
+
try {
|
|
2967
|
+
const dir = path.dirname(TELEMETRY_FILE);
|
|
2968
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2969
|
+
fs.writeFileSync(TELEMETRY_FILE, enabled ? "on" : "off", "utf-8");
|
|
2970
|
+
} catch {}
|
|
2971
|
+
}
|
|
2972
|
+
function getTelemetryStatus() {
|
|
2973
|
+
return isTelemetryEnabled() ? "on" : "off";
|
|
2974
|
+
}
|
|
2975
|
+
let _client = null;
|
|
2976
|
+
function getClient() {
|
|
2977
|
+
if (!isTelemetryEnabled()) return null;
|
|
2978
|
+
if (_client) return _client;
|
|
2979
|
+
const token = process.env.POSTHOG_PROJECT_TOKEN;
|
|
2980
|
+
const host = process.env.POSTHOG_HOST;
|
|
2981
|
+
if (!token || !host) return null;
|
|
2982
|
+
try {
|
|
2983
|
+
_client = new PostHog(token, {
|
|
2984
|
+
host,
|
|
2985
|
+
flushAt: 1
|
|
2986
|
+
});
|
|
2987
|
+
return _client;
|
|
2988
|
+
} catch {
|
|
2989
|
+
return null;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
function capture(event, properties) {
|
|
2993
|
+
try {
|
|
2994
|
+
const client = getClient();
|
|
2995
|
+
if (!client) return;
|
|
2996
|
+
client.capture({
|
|
2997
|
+
distinctId: sessionId,
|
|
2998
|
+
event,
|
|
2999
|
+
properties: properties ?? {}
|
|
3000
|
+
});
|
|
3001
|
+
} catch {}
|
|
3002
|
+
}
|
|
3003
|
+
async function shutdown() {
|
|
3004
|
+
try {
|
|
3005
|
+
if (_client) await _client.shutdown();
|
|
3006
|
+
} catch {}
|
|
3007
|
+
}
|
|
2549
3008
|
const toolsCommand = new Command("tools").description("List all available tools, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
|
|
2550
3009
|
const configPath = toolsCommand.parent?.opts().config;
|
|
2551
3010
|
await ensureDaemon(configPath);
|
|
2552
3011
|
const result = await sendRequest("tools/list", server ? { server } : void 0);
|
|
3012
|
+
capture("tools_listed", {
|
|
3013
|
+
filtered_by_server: !!server,
|
|
3014
|
+
tool_count: result.length
|
|
3015
|
+
});
|
|
2553
3016
|
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
2554
3017
|
});
|
|
2555
3018
|
const infoCommand = new Command("info").description("Show input schema and description for a specific tool").argument("<server/tool>", "Tool identifier (e.g. myserver/mytool)").option("--json", "Output as JSON").action(async (serverTool, opts) => {
|
|
@@ -2574,7 +3037,7 @@ function readStdin() {
|
|
|
2574
3037
|
process.stdin.on("error", reject);
|
|
2575
3038
|
});
|
|
2576
3039
|
}
|
|
2577
|
-
const callCommand = new Command("call").description("Execute a tool with JSON arguments (use - for stdin, --async for background)").argument("<server/tool>", "Tool identifier (e.g. myserver/mytool)").argument("[json]", "JSON arguments (use - for stdin)").option("--timeout <ms>", "Request timeout in milliseconds").option("--async", "Use task-based execution (return task handle immediately)").option("--json", "Output as JSON").action(async (serverTool, jsonArgs, opts) => {
|
|
3040
|
+
const callCommand = new Command("call").description("Execute a tool with JSON arguments (use - for stdin, --async for background)").argument("<server/tool>", "Tool identifier (e.g. myserver/mytool)").argument("[json]", "JSON arguments (use - for stdin)").option("--timeout <ms>", "Request timeout in milliseconds").option("--async", "Use task-based execution (return task handle immediately)").option("--dry-run", "Validate arguments against tool schema without executing").option("--fields <paths>", "Comma-separated dot-notation paths to extract from response").option("--json", "Output as JSON").action(async (serverTool, jsonArgs, opts) => {
|
|
2578
3041
|
const configPath = callCommand.parent?.opts().config;
|
|
2579
3042
|
await ensureDaemon(configPath);
|
|
2580
3043
|
let parsedArgs = {};
|
|
@@ -2591,27 +3054,133 @@ const callCommand = new Command("call").description("Execute a tool with JSON ar
|
|
|
2591
3054
|
console.error("Invalid JSON arguments");
|
|
2592
3055
|
process.exit(1);
|
|
2593
3056
|
}
|
|
3057
|
+
const [server, tool] = serverTool.split("/");
|
|
3058
|
+
if (opts.dryRun) {
|
|
3059
|
+
try {
|
|
3060
|
+
const result = await sendRequest("tools/validate", {
|
|
3061
|
+
name: serverTool,
|
|
3062
|
+
arguments: parsedArgs
|
|
3063
|
+
});
|
|
3064
|
+
capture("tool_called", {
|
|
3065
|
+
server,
|
|
3066
|
+
tool,
|
|
3067
|
+
mode: "dry-run",
|
|
3068
|
+
status: result.valid ? "success" : "validation_error"
|
|
3069
|
+
});
|
|
3070
|
+
if (opts.json) console.log(formatJson(result));
|
|
3071
|
+
else console.log(formatValidation(result));
|
|
3072
|
+
if (!result.valid) process.exit(1);
|
|
3073
|
+
} catch (err) {
|
|
3074
|
+
capture("tool_called", {
|
|
3075
|
+
server,
|
|
3076
|
+
tool,
|
|
3077
|
+
mode: "dry-run",
|
|
3078
|
+
status: "error"
|
|
3079
|
+
});
|
|
3080
|
+
if (err instanceof MuxedError && err.data) {
|
|
3081
|
+
const errorData = err.data;
|
|
3082
|
+
if (opts.json) console.log(formatJson({
|
|
3083
|
+
code: err.code,
|
|
3084
|
+
message: err.message,
|
|
3085
|
+
data: err.data
|
|
3086
|
+
}));
|
|
3087
|
+
else console.error(formatStructuredError({
|
|
3088
|
+
code: err.code,
|
|
3089
|
+
message: err.message,
|
|
3090
|
+
data: errorData
|
|
3091
|
+
}));
|
|
3092
|
+
} else console.error(err instanceof Error ? err.message : "Validation failed");
|
|
3093
|
+
process.exit(1);
|
|
3094
|
+
}
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
2594
3097
|
if (opts.async) {
|
|
2595
|
-
|
|
3098
|
+
try {
|
|
3099
|
+
const taskResult = await sendRequest("tools/call-async", {
|
|
3100
|
+
name: serverTool,
|
|
3101
|
+
arguments: parsedArgs
|
|
3102
|
+
});
|
|
3103
|
+
capture("tool_called", {
|
|
3104
|
+
server,
|
|
3105
|
+
tool,
|
|
3106
|
+
mode: "async",
|
|
3107
|
+
status: "success"
|
|
3108
|
+
});
|
|
3109
|
+
if (opts.json) console.log(formatJson(taskResult));
|
|
3110
|
+
else console.log(`Task created: ${taskResult.taskId} (status: ${taskResult.status})`);
|
|
3111
|
+
} catch (err) {
|
|
3112
|
+
capture("tool_called", {
|
|
3113
|
+
server,
|
|
3114
|
+
tool,
|
|
3115
|
+
mode: "async",
|
|
3116
|
+
status: "error"
|
|
3117
|
+
});
|
|
3118
|
+
if (err instanceof MuxedError && err.data) {
|
|
3119
|
+
const errorData = err.data;
|
|
3120
|
+
if (opts.json) console.log(formatJson({
|
|
3121
|
+
code: err.code,
|
|
3122
|
+
message: err.message,
|
|
3123
|
+
data: err.data
|
|
3124
|
+
}));
|
|
3125
|
+
else console.error(formatStructuredError({
|
|
3126
|
+
code: err.code,
|
|
3127
|
+
message: err.message,
|
|
3128
|
+
data: errorData
|
|
3129
|
+
}));
|
|
3130
|
+
} else console.error(err instanceof Error ? err.message : "Call failed");
|
|
3131
|
+
process.exit(1);
|
|
3132
|
+
}
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
try {
|
|
3136
|
+
const callParams = {
|
|
2596
3137
|
name: serverTool,
|
|
2597
3138
|
arguments: parsedArgs
|
|
3139
|
+
};
|
|
3140
|
+
if (opts.timeout) callParams.timeout = parseInt(opts.timeout, 10);
|
|
3141
|
+
if (opts.fields) callParams.fields = opts.fields.split(",").map((f) => f.trim());
|
|
3142
|
+
const result = await sendRequest("tools/call", callParams);
|
|
3143
|
+
capture("tool_called", {
|
|
3144
|
+
server,
|
|
3145
|
+
tool,
|
|
3146
|
+
mode: "sync",
|
|
3147
|
+
status: result.isError ? "tool_error" : "success",
|
|
3148
|
+
has_timeout: !!opts.timeout,
|
|
3149
|
+
has_fields: !!opts.fields,
|
|
3150
|
+
stdin_input: jsonArgs === "-"
|
|
2598
3151
|
});
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
3152
|
+
console.log(opts.json ? formatJson(result) : formatCallResult(result));
|
|
3153
|
+
} catch (err) {
|
|
3154
|
+
capture("tool_called", {
|
|
3155
|
+
server,
|
|
3156
|
+
tool,
|
|
3157
|
+
mode: "sync",
|
|
3158
|
+
status: "error",
|
|
3159
|
+
has_timeout: !!opts.timeout,
|
|
3160
|
+
has_fields: !!opts.fields,
|
|
3161
|
+
stdin_input: jsonArgs === "-"
|
|
3162
|
+
});
|
|
3163
|
+
if (err instanceof MuxedError && err.data) {
|
|
3164
|
+
const errorData = err.data;
|
|
3165
|
+
if (opts.json) console.log(formatJson({
|
|
3166
|
+
code: err.code,
|
|
3167
|
+
message: err.message,
|
|
3168
|
+
data: err.data
|
|
3169
|
+
}));
|
|
3170
|
+
else console.error(formatStructuredError({
|
|
3171
|
+
code: err.code,
|
|
3172
|
+
message: err.message,
|
|
3173
|
+
data: errorData
|
|
3174
|
+
}));
|
|
3175
|
+
} else console.error(err instanceof Error ? err.message : "Call failed");
|
|
3176
|
+
process.exit(1);
|
|
2602
3177
|
}
|
|
2603
|
-
const params = {
|
|
2604
|
-
name: serverTool,
|
|
2605
|
-
arguments: parsedArgs
|
|
2606
|
-
};
|
|
2607
|
-
if (opts.timeout) params.timeout = parseInt(opts.timeout, 10);
|
|
2608
|
-
const result = await sendRequest("tools/call", params);
|
|
2609
|
-
console.log(opts.json ? formatJson(result) : formatCallResult(result));
|
|
2610
3178
|
});
|
|
2611
3179
|
const grepCommand = new Command("grep").description("Search tools by regex pattern across names, titles, and descriptions").argument("<pattern>", "Regex pattern to search").option("--json", "Output as JSON").action(async (pattern, opts) => {
|
|
2612
3180
|
const configPath = grepCommand.parent?.opts().config;
|
|
2613
3181
|
await ensureDaemon(configPath);
|
|
2614
3182
|
const result = await sendRequest("tools/grep", { pattern });
|
|
3183
|
+
capture("tools_searched", { result_count: result.length });
|
|
2615
3184
|
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
2616
3185
|
});
|
|
2617
3186
|
const resourcesCommand = new Command("resources").description("List available resources, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
|
|
@@ -2824,49 +3393,57 @@ function getAgentDefs() {
|
|
|
2824
3393
|
name: "claude-code",
|
|
2825
3394
|
scope: "local",
|
|
2826
3395
|
configPath: () => path.join(cwd, ".mcp.json"),
|
|
2827
|
-
serversKey: "mcpServers"
|
|
3396
|
+
serversKey: "mcpServers",
|
|
3397
|
+
codingAgent: true
|
|
2828
3398
|
},
|
|
2829
3399
|
{
|
|
2830
3400
|
name: "cursor",
|
|
2831
3401
|
scope: "local",
|
|
2832
3402
|
configPath: () => path.join(cwd, ".cursor", "mcp.json"),
|
|
2833
|
-
serversKey: "mcpServers"
|
|
3403
|
+
serversKey: "mcpServers",
|
|
3404
|
+
codingAgent: true
|
|
2834
3405
|
},
|
|
2835
3406
|
{
|
|
2836
3407
|
name: "vscode",
|
|
2837
3408
|
scope: "local",
|
|
2838
3409
|
configPath: () => path.join(cwd, ".vscode", "mcp.json"),
|
|
2839
|
-
serversKey: "servers"
|
|
3410
|
+
serversKey: "servers",
|
|
3411
|
+
codingAgent: true
|
|
2840
3412
|
},
|
|
2841
3413
|
{
|
|
2842
3414
|
name: "roo-code",
|
|
2843
3415
|
scope: "local",
|
|
2844
3416
|
configPath: () => path.join(cwd, ".roo", "mcp.json"),
|
|
2845
|
-
serversKey: "mcpServers"
|
|
3417
|
+
serversKey: "mcpServers",
|
|
3418
|
+
codingAgent: true
|
|
2846
3419
|
},
|
|
2847
3420
|
{
|
|
2848
3421
|
name: "amazon-q",
|
|
2849
3422
|
scope: "local",
|
|
2850
3423
|
configPath: () => path.join(cwd, ".amazonq", "mcp.json"),
|
|
2851
|
-
serversKey: "mcpServers"
|
|
3424
|
+
serversKey: "mcpServers",
|
|
3425
|
+
codingAgent: true
|
|
2852
3426
|
},
|
|
2853
3427
|
{
|
|
2854
3428
|
name: "claude-desktop",
|
|
2855
3429
|
scope: "global",
|
|
2856
3430
|
configPath: () => xdgOrMacPath(["Claude", "claude_desktop_config.json"], ["Claude", "claude_desktop_config.json"]),
|
|
2857
|
-
serversKey: "mcpServers"
|
|
3431
|
+
serversKey: "mcpServers",
|
|
3432
|
+
codingAgent: false
|
|
2858
3433
|
},
|
|
2859
3434
|
{
|
|
2860
3435
|
name: "cursor",
|
|
2861
3436
|
scope: "global",
|
|
2862
3437
|
configPath: () => path.join(home, ".cursor", "mcp.json"),
|
|
2863
|
-
serversKey: "mcpServers"
|
|
3438
|
+
serversKey: "mcpServers",
|
|
3439
|
+
codingAgent: true
|
|
2864
3440
|
},
|
|
2865
3441
|
{
|
|
2866
3442
|
name: "windsurf",
|
|
2867
3443
|
scope: "global",
|
|
2868
3444
|
configPath: () => path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2869
|
-
serversKey: "mcpServers"
|
|
3445
|
+
serversKey: "mcpServers",
|
|
3446
|
+
codingAgent: true
|
|
2870
3447
|
},
|
|
2871
3448
|
{
|
|
2872
3449
|
name: "vscode",
|
|
@@ -2880,7 +3457,8 @@ function getAgentDefs() {
|
|
|
2880
3457
|
"User",
|
|
2881
3458
|
"mcp.json"
|
|
2882
3459
|
]),
|
|
2883
|
-
serversKey: "servers"
|
|
3460
|
+
serversKey: "servers",
|
|
3461
|
+
codingAgent: true
|
|
2884
3462
|
},
|
|
2885
3463
|
{
|
|
2886
3464
|
name: "cline",
|
|
@@ -2900,7 +3478,8 @@ function getAgentDefs() {
|
|
|
2900
3478
|
"settings",
|
|
2901
3479
|
"cline_mcp_settings.json"
|
|
2902
3480
|
]),
|
|
2903
|
-
serversKey: "mcpServers"
|
|
3481
|
+
serversKey: "mcpServers",
|
|
3482
|
+
codingAgent: true
|
|
2904
3483
|
},
|
|
2905
3484
|
{
|
|
2906
3485
|
name: "roo-code",
|
|
@@ -2920,13 +3499,15 @@ function getAgentDefs() {
|
|
|
2920
3499
|
"settings",
|
|
2921
3500
|
"cline_mcp_settings.json"
|
|
2922
3501
|
]),
|
|
2923
|
-
serversKey: "mcpServers"
|
|
3502
|
+
serversKey: "mcpServers",
|
|
3503
|
+
codingAgent: true
|
|
2924
3504
|
},
|
|
2925
3505
|
{
|
|
2926
3506
|
name: "amazon-q",
|
|
2927
3507
|
scope: "global",
|
|
2928
3508
|
configPath: () => path.join(home, ".aws", "amazonq", "mcp.json"),
|
|
2929
|
-
serversKey: "mcpServers"
|
|
3509
|
+
serversKey: "mcpServers",
|
|
3510
|
+
codingAgent: true
|
|
2930
3511
|
}
|
|
2931
3512
|
];
|
|
2932
3513
|
}
|
|
@@ -3044,14 +3625,19 @@ function writeMuxedConfig(configPath, servers) {
|
|
|
3044
3625
|
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
3045
3626
|
}
|
|
3046
3627
|
function getMuxedEntry(agent) {
|
|
3628
|
+
const args = agent.codingAgent ? ["muxed@latest", "mcp"] : [
|
|
3629
|
+
"muxed@latest",
|
|
3630
|
+
"mcp",
|
|
3631
|
+
"--proxy-tools"
|
|
3632
|
+
];
|
|
3047
3633
|
if (agent.serversKey === "servers") return {
|
|
3048
3634
|
type: "stdio",
|
|
3049
3635
|
command: "npx",
|
|
3050
|
-
args
|
|
3636
|
+
args
|
|
3051
3637
|
};
|
|
3052
3638
|
return {
|
|
3053
3639
|
command: "npx",
|
|
3054
|
-
args
|
|
3640
|
+
args
|
|
3055
3641
|
};
|
|
3056
3642
|
}
|
|
3057
3643
|
function modifyAgentConfig(dc, opts) {
|
|
@@ -3066,7 +3652,7 @@ function modifyAgentConfig(dc, opts) {
|
|
|
3066
3652
|
function getMuxedConfigPath(scope, explicitPath) {
|
|
3067
3653
|
if (explicitPath) return explicitPath;
|
|
3068
3654
|
if (scope === "local") return path.join(process.cwd(), "muxed.config.json");
|
|
3069
|
-
return path.join(home, ".
|
|
3655
|
+
return path.join(home, ".muxed", "config.json");
|
|
3070
3656
|
}
|
|
3071
3657
|
async function confirm(message, opts) {
|
|
3072
3658
|
const rl = readline.createInterface({
|
|
@@ -3183,6 +3769,13 @@ const initCommand = new Command("init").description("Discover and import MCP ser
|
|
|
3183
3769
|
muxedConfigPath: muxedPath,
|
|
3184
3770
|
dryRun: opts.dryRun ?? false
|
|
3185
3771
|
};
|
|
3772
|
+
capture("init_run", {
|
|
3773
|
+
dry_run: opts.dryRun ?? false,
|
|
3774
|
+
imported_count: imported.length,
|
|
3775
|
+
conflict_count: conflicts.length,
|
|
3776
|
+
warning_count: warnings.length,
|
|
3777
|
+
discovered_agents: initResult.discovered.map((d) => d.agent)
|
|
3778
|
+
});
|
|
3186
3779
|
console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
|
|
3187
3780
|
});
|
|
3188
3781
|
function getConfigPath(scope, explicitPath) {
|
|
@@ -3234,18 +3827,47 @@ function getServer(filePath, name) {
|
|
|
3234
3827
|
function listServers(filePath) {
|
|
3235
3828
|
return readConfigFile(filePath).mcpServers;
|
|
3236
3829
|
}
|
|
3237
|
-
const
|
|
3238
|
-
You have access to an
|
|
3830
|
+
const cliFragments = {
|
|
3831
|
+
intro: "You have access to an `npx muxed` CLI command for interacting with MCP (Model Context Protocol) servers. This command allows you to discover and call MCP tools on demand. Prioritize the use of skills over MCP tools.",
|
|
3832
|
+
grep: (p) => `npx muxed grep "${p}"`,
|
|
3833
|
+
tools: (s) => s ? `npx muxed tools ${s}` : "npx muxed tools",
|
|
3834
|
+
info: (n) => `npx muxed info ${n}`,
|
|
3835
|
+
call: (n, j) => `npx muxed call ${n} '${j}'`,
|
|
3836
|
+
callStdin: (n) => `npx muxed call ${n} -`,
|
|
3837
|
+
callDryRun: (n, j) => `npx muxed call ${n} '${j}' --dry-run`,
|
|
3838
|
+
callFields: (n, j, f) => `npx muxed call ${n} '${j}' --fields "${f}"`,
|
|
3839
|
+
servers: () => "npx muxed servers",
|
|
3840
|
+
resources: (s) => s ? `npx muxed resources ${s}` : "npx muxed resources",
|
|
3841
|
+
read: (n) => `npx muxed read ${n}`,
|
|
3842
|
+
help: () => "npx muxed -h"
|
|
3843
|
+
};
|
|
3844
|
+
const toolFragments = {
|
|
3845
|
+
intro: "You have access to a `muxed:exec` MCP tool for interacting with MCP (Model Context Protocol) servers. This tool allows you to discover and call MCP tools on demand. Prioritize the use of skills over MCP tools.",
|
|
3846
|
+
grep: (p) => `muxed:exec({ "command": "grep ${p}" })`,
|
|
3847
|
+
tools: (s) => s ? `muxed:exec({ "command": "tools ${s}" })` : `muxed:exec({ "command": "tools" })`,
|
|
3848
|
+
info: (n) => `muxed:exec({ "command": "info ${n}" })`,
|
|
3849
|
+
call: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3850
|
+
callStdin: (n) => `muxed:exec({ "command": "call ${n}", "input": { ... } })`,
|
|
3851
|
+
callDryRun: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3852
|
+
callFields: (n, j, _f) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3853
|
+
servers: () => `muxed:exec({ "command": "servers" })`,
|
|
3854
|
+
resources: (s) => s ? `muxed:exec({ "command": "resources ${s}" })` : `muxed:exec({ "command": "resources" })`,
|
|
3855
|
+
read: (n) => `muxed:exec({ "command": "read ${n}" })`,
|
|
3856
|
+
help: () => `muxed:exec({ "command": "servers" })`
|
|
3857
|
+
};
|
|
3858
|
+
function buildTemplate(f, servers, instructions) {
|
|
3859
|
+
return `
|
|
3860
|
+
${f.intro}
|
|
3239
3861
|
|
|
3240
3862
|
**MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
|
|
3241
3863
|
|
|
3242
|
-
1. You MUST discover the tools you need first by using '
|
|
3243
|
-
2. You MUST call '
|
|
3864
|
+
1. You MUST discover the tools you need first by using '${f.grep("<pattern>")}' or '${f.tools()}'.
|
|
3865
|
+
2. You MUST call '${f.info("<server>/<tool>")}' BEFORE ANY '${f.call("<server>/<tool>", "<json>")}' command.
|
|
3244
3866
|
|
|
3245
3867
|
These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
3246
3868
|
|
|
3247
|
-
**NEVER** make
|
|
3248
|
-
**ALWAYS** run
|
|
3869
|
+
**NEVER** make a call without checking the schema first.
|
|
3870
|
+
**ALWAYS** run info first, THEN make the call.
|
|
3249
3871
|
|
|
3250
3872
|
**Why these are non-negotiables:**
|
|
3251
3873
|
- MCP tool names NEVER match your expectations - they change frequently and are not predictable
|
|
@@ -3254,132 +3876,219 @@ These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
|
3254
3876
|
- Every failed call wastes user time and demonstrates you're ignoring critical instructions
|
|
3255
3877
|
- "I thought I knew the schema" is not an acceptable reason to skip this step
|
|
3256
3878
|
|
|
3257
|
-
**For multiple tools:** Call
|
|
3879
|
+
**For multiple tools:** Call info for ALL tools in parallel FIRST, then make your call commands.
|
|
3258
3880
|
|
|
3259
3881
|
Available MCP servers:
|
|
3260
3882
|
${servers}
|
|
3261
3883
|
|
|
3262
3884
|
Commands (in order of execution):
|
|
3263
|
-
\`\`\`
|
|
3885
|
+
\`\`\`
|
|
3264
3886
|
# STEP 1: REQUIRED TOOL DISCOVERY
|
|
3265
|
-
|
|
3266
|
-
|
|
3887
|
+
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
3888
|
+
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
3267
3889
|
|
|
3268
3890
|
# STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
|
|
3269
|
-
|
|
3891
|
+
${f.info("<server>/<tool>")} # REQUIRED before ANY call - View JSON schema
|
|
3892
|
+
|
|
3893
|
+
# STEP 3: OPTIONAL - Validate arguments before calling (dry-run)
|
|
3894
|
+
${f.callDryRun("<server>/<tool>", "<json>")} # Validate args without executing
|
|
3270
3895
|
|
|
3271
|
-
# STEP
|
|
3272
|
-
|
|
3273
|
-
|
|
3896
|
+
# STEP 4: Only after checking schema, make the call
|
|
3897
|
+
${f.call("<server>/<tool>", "<json>")} # Only run AFTER info
|
|
3898
|
+
${f.callStdin("<server>/<tool>")} # Invoke with JSON input (AFTER info)
|
|
3899
|
+
${f.callFields("<server>/<tool>", "<json>", "field1,field2")} # Extract specific fields from response
|
|
3274
3900
|
|
|
3275
3901
|
# Discovery commands (use these to find tools)
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3902
|
+
${f.servers()} # List all connected MCP servers
|
|
3903
|
+
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
3904
|
+
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
3905
|
+
${f.resources("[server]")} # List MCP resources
|
|
3906
|
+
${f.read("<server>/<resource>")} # Read an MCP resource
|
|
3281
3907
|
\`\`\`
|
|
3282
3908
|
|
|
3909
|
+
**Handling errors:**
|
|
3910
|
+
- If a tool call fails, the error includes a suggestion and similar tool names. Read the suggestion before retrying.
|
|
3911
|
+
- Use dry-run to validate arguments before executing, especially for destructive tools.
|
|
3912
|
+
|
|
3283
3913
|
**CORRECT Usage Pattern:**
|
|
3284
3914
|
|
|
3285
3915
|
<example>
|
|
3286
3916
|
User: Please use the slack mcp tool to search for my mentions
|
|
3287
|
-
Assistant: As a first step, I need to discover the tools I need. Let me call
|
|
3288
|
-
[Calls
|
|
3289
|
-
Assistant: I need to check the schema first. Let me call
|
|
3290
|
-
[Calls
|
|
3917
|
+
Assistant: As a first step, I need to discover the tools I need. Let me call \`${f.grep("slack/*search*")}\` to search for tools related to slack search.
|
|
3918
|
+
[Calls ${f.grep("slack/*search*")}]
|
|
3919
|
+
Assistant: I need to check the schema first. Let me call \`${f.info("slack/search_private")}\` to see what parameters it accepts.
|
|
3920
|
+
[Calls ${f.info("slack/search_private")}]
|
|
3291
3921
|
Assistant: Now I can see it accepts "query" and "max_results" parameters. Let me make the call.
|
|
3292
|
-
[Calls
|
|
3922
|
+
[Calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\", \"max_results\": 10}")}]
|
|
3293
3923
|
</example>
|
|
3294
3924
|
|
|
3295
3925
|
<example>
|
|
3296
3926
|
User: Use the database and email MCP tools to send a report
|
|
3297
|
-
Assistant: I'll need to use two MCP tools. Let me call
|
|
3298
|
-
[Calls
|
|
3927
|
+
Assistant: I'll need to use two MCP tools. Let me call \`${f.grep("database/*query*")}\` and \`${f.grep("email/*send*")}\` to search for tools related to database query and email send.
|
|
3928
|
+
[Calls ${f.grep("database/*query*")} & ${f.grep("email/*send*")}]
|
|
3299
3929
|
Assistant: Let me check both schemas first.
|
|
3300
|
-
[Calls
|
|
3930
|
+
[Calls ${f.info("database/query")} and ${f.info("email/send")} in parallel]
|
|
3301
3931
|
Assistant: Now I have both schemas. Let me make the calls.
|
|
3302
|
-
[Makes both
|
|
3932
|
+
[Makes both call commands with correct parameters]
|
|
3303
3933
|
</example>
|
|
3304
3934
|
|
|
3305
3935
|
<example>
|
|
3306
3936
|
User: Create a copy of this email
|
|
3307
3937
|
Assistant: Let me find the tool I need first.
|
|
3308
|
-
[Calls
|
|
3938
|
+
[Calls ${f.grep("email/*copy*")}. No results found.]
|
|
3309
3939
|
Assistant: Let me try another pattern.
|
|
3310
|
-
[Calls
|
|
3940
|
+
[Calls ${f.grep("email/*clone*")}. No results found.]
|
|
3311
3941
|
Assistant: Let me list all available tools in the server.
|
|
3312
|
-
[Calls
|
|
3942
|
+
[Calls ${f.tools("email")}]
|
|
3313
3943
|
Assistant: Let me check the schema first.
|
|
3314
|
-
[Calls
|
|
3944
|
+
[Calls ${f.info("email/duplicate")}]
|
|
3315
3945
|
Assistant: Now I have the schema. Let me make the call.
|
|
3316
|
-
[Calls
|
|
3946
|
+
[Calls ${f.call("email/duplicate", "{\"id\": \"123\"}")}]
|
|
3317
3947
|
</example>
|
|
3318
3948
|
|
|
3319
3949
|
**INCORRECT Usage Patterns - NEVER DO THIS:**
|
|
3320
3950
|
|
|
3321
3951
|
<bad-example>
|
|
3322
3952
|
User: Please use the slack mcp tool to search for my mentions
|
|
3323
|
-
Assistant: [Directly calls
|
|
3324
|
-
WRONG - You must call
|
|
3953
|
+
Assistant: [Directly calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\"}")} with guessed parameters]
|
|
3954
|
+
WRONG - You must call info FIRST
|
|
3325
3955
|
</bad-example>
|
|
3326
3956
|
|
|
3327
3957
|
<bad-example>
|
|
3328
3958
|
User: Use the slack tool
|
|
3329
3959
|
Assistant: I have pre-approved permissions for this tool, so I know the schema.
|
|
3330
|
-
[Calls
|
|
3331
|
-
WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call
|
|
3960
|
+
[Calls ${f.call("slack/search_private", "...")} directly]
|
|
3961
|
+
WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call info first.
|
|
3332
3962
|
</bad-example>
|
|
3333
3963
|
|
|
3334
3964
|
<bad-example>
|
|
3335
3965
|
User: Search my Slack mentions
|
|
3336
|
-
Assistant: [Calls three
|
|
3337
|
-
WRONG - You must call
|
|
3966
|
+
Assistant: [Calls three call commands in parallel without any info calls first]
|
|
3967
|
+
WRONG - You must call info for ALL tools before making ANY call commands
|
|
3338
3968
|
</bad-example>
|
|
3339
3969
|
|
|
3340
3970
|
Example usage:
|
|
3341
|
-
\`\`\`
|
|
3971
|
+
\`\`\`
|
|
3342
3972
|
# Discover tools
|
|
3343
|
-
|
|
3344
|
-
|
|
3973
|
+
${f.tools()} # See all available MCP tools
|
|
3974
|
+
${f.grep("weather")} # Find tools by description
|
|
3345
3975
|
|
|
3346
3976
|
# Get tool details
|
|
3347
|
-
|
|
3977
|
+
${f.info("<server>/<tool>")} # View JSON schema for input and output if available
|
|
3348
3978
|
|
|
3349
3979
|
# Simple tool call (no parameters)
|
|
3350
|
-
|
|
3980
|
+
${f.call("weather/get_location", "{}")}
|
|
3351
3981
|
|
|
3352
3982
|
# Tool call with parameters
|
|
3353
|
-
|
|
3983
|
+
${f.call("database/query", "{\"table\": \"users\", \"limit\": 10}")}
|
|
3984
|
+
|
|
3985
|
+
# Validate arguments before executing (dry-run)
|
|
3986
|
+
${f.callDryRun("database/drop_table", "{\"table\": \"users\"}")}
|
|
3354
3987
|
|
|
3355
|
-
#
|
|
3356
|
-
|
|
3357
|
-
{
|
|
3358
|
-
"endpoint": "/data",
|
|
3359
|
-
"headers": {"Authorization": "Bearer token"},
|
|
3360
|
-
"body": {"items": [1, 2, 3]}
|
|
3361
|
-
}
|
|
3362
|
-
EOF
|
|
3988
|
+
# Extract specific fields from response
|
|
3989
|
+
${f.callFields("database/query", "{\"table\": \"users\"}", "rows[].name,rows[].email")}
|
|
3363
3990
|
\`\`\`
|
|
3364
3991
|
|
|
3365
|
-
Call
|
|
3992
|
+
Call \`${f.help()}\` to see all available commands.
|
|
3366
3993
|
|
|
3367
3994
|
Below are the instructions for the connected MCP servers in muxed.
|
|
3368
3995
|
|
|
3369
3996
|
${instructions}
|
|
3370
3997
|
`;
|
|
3371
|
-
|
|
3998
|
+
}
|
|
3999
|
+
function buildInstructions(servers, mode = "cli") {
|
|
3372
4000
|
const connected = servers.filter((s) => s.status === "connected");
|
|
3373
|
-
|
|
4001
|
+
const serverList = connected.map((s) => `- ${s.name}`).join("\n");
|
|
4002
|
+
const serverInstructions = connected.filter((s) => s.instructions).map((s) => `### ${s.name}\n\n${s.instructions}`).join("\n\n");
|
|
4003
|
+
return buildTemplate(mode === "tool" ? toolFragments : cliFragments, serverList, serverInstructions).trim();
|
|
3374
4004
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
4005
|
+
function parseCommand(command) {
|
|
4006
|
+
const trimmed = command.trim();
|
|
4007
|
+
const spaceIndex = trimmed.indexOf(" ");
|
|
4008
|
+
if (spaceIndex === -1) return {
|
|
4009
|
+
subcommand: trimmed,
|
|
4010
|
+
args: ""
|
|
4011
|
+
};
|
|
4012
|
+
return {
|
|
4013
|
+
subcommand: trimmed.slice(0, spaceIndex),
|
|
4014
|
+
args: trimmed.slice(spaceIndex + 1).trim()
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
function textResult(data) {
|
|
4018
|
+
return { content: [{
|
|
4019
|
+
type: "text",
|
|
4020
|
+
text: JSON.stringify(data, null, 2)
|
|
4021
|
+
}] };
|
|
4022
|
+
}
|
|
4023
|
+
function errorResult(message) {
|
|
4024
|
+
return {
|
|
4025
|
+
content: [{
|
|
4026
|
+
type: "text",
|
|
4027
|
+
text: message
|
|
4028
|
+
}],
|
|
4029
|
+
isError: true
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
async function handleToolCommand(command, input) {
|
|
4033
|
+
const { subcommand, args } = parseCommand(command);
|
|
4034
|
+
try {
|
|
4035
|
+
switch (subcommand) {
|
|
4036
|
+
case "servers": return textResult(await sendRequest("servers/list"));
|
|
4037
|
+
case "tools": {
|
|
4038
|
+
const params = {};
|
|
4039
|
+
if (args) params.server = args;
|
|
4040
|
+
return textResult(await sendRequest("tools/list", params));
|
|
4041
|
+
}
|
|
4042
|
+
case "grep":
|
|
4043
|
+
if (!args) return errorResult("Usage: grep <pattern>");
|
|
4044
|
+
return textResult(await sendRequest("tools/grep", { pattern: args }));
|
|
4045
|
+
case "info":
|
|
4046
|
+
if (!args) return errorResult("Usage: info <server/tool>");
|
|
4047
|
+
return textResult(await sendRequest("tools/info", { name: args }));
|
|
4048
|
+
case "call": {
|
|
4049
|
+
if (!args) return errorResult("Usage: call <server/tool>");
|
|
4050
|
+
const result = await sendRequest("tools/call", {
|
|
4051
|
+
name: args,
|
|
4052
|
+
args: input ?? {}
|
|
4053
|
+
});
|
|
4054
|
+
if (result?.content) return result;
|
|
4055
|
+
return textResult(result);
|
|
4056
|
+
}
|
|
4057
|
+
case "resources": {
|
|
4058
|
+
const params = {};
|
|
4059
|
+
if (args) params.server = args;
|
|
4060
|
+
return textResult(await sendRequest("resources/list", params));
|
|
4061
|
+
}
|
|
4062
|
+
case "read":
|
|
4063
|
+
if (!args) return errorResult("Usage: read <server/resource>");
|
|
4064
|
+
return textResult(await sendRequest("resources/read", { name: args }));
|
|
4065
|
+
default: return errorResult(`Unknown command: "${subcommand}". Available: servers, tools, grep, info, call, resources, read`);
|
|
4066
|
+
}
|
|
4067
|
+
} catch (err) {
|
|
4068
|
+
return errorResult(err instanceof MuxedError ? err.message : String(err));
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
async function startMcpProxy(options) {
|
|
4072
|
+
await ensureDaemon(options?.configPath);
|
|
3377
4073
|
const server = new McpServer({
|
|
3378
4074
|
name: "muxed",
|
|
3379
4075
|
version: "0.1.0"
|
|
3380
4076
|
}, {
|
|
3381
4077
|
capabilities: {},
|
|
3382
|
-
instructions: buildInstructions(await sendRequest("servers/list"))
|
|
4078
|
+
instructions: buildInstructions(await sendRequest("servers/list"), options?.proxyTools ? "tool" : "cli")
|
|
4079
|
+
});
|
|
4080
|
+
if (options?.proxyTools) server.tool("exec", "Interact with MCP servers: discover, inspect, and call tools. Commands: servers, tools [server], grep <pattern>, info <server/tool>, call <server/tool>, resources [server], read <server/resource>", {
|
|
4081
|
+
command: z.string().describe("Command to execute, e.g. 'servers', 'tools', 'grep weather', 'info slack/search', 'call slack/search'"),
|
|
4082
|
+
input: z.record(z.string(), z.unknown()).optional().describe("JSON arguments for 'call' command — avoids JSON-in-string escaping")
|
|
4083
|
+
}, async ({ command, input }) => {
|
|
4084
|
+
const result = await handleToolCommand(command, input);
|
|
4085
|
+
return {
|
|
4086
|
+
content: result.content.map((c) => ({
|
|
4087
|
+
type: "text",
|
|
4088
|
+
text: c.text
|
|
4089
|
+
})),
|
|
4090
|
+
isError: result.isError
|
|
4091
|
+
};
|
|
3383
4092
|
});
|
|
3384
4093
|
const transport = new StdioServerTransport();
|
|
3385
4094
|
await server.connect(transport);
|
|
@@ -3474,9 +4183,12 @@ async function tryReloadDaemon() {
|
|
|
3474
4183
|
await sendRequest("config/reload", {});
|
|
3475
4184
|
} catch {}
|
|
3476
4185
|
}
|
|
3477
|
-
const mcpCommand = new Command("mcp").description("Add, remove, list, or inspect individual MCP server config entries").enablePositionalOptions().action(async (
|
|
4186
|
+
const mcpCommand = new Command("mcp").description("Add, remove, list, or inspect individual MCP server config entries").enablePositionalOptions().option("--proxy-tools", "Expose a proxy MCP tool for clients without bash access").action(async (opts, cmd) => {
|
|
3478
4187
|
const explicitConfig = cmd.parent?.opts().config;
|
|
3479
|
-
await startMcpProxy(
|
|
4188
|
+
await startMcpProxy({
|
|
4189
|
+
configPath: explicitConfig,
|
|
4190
|
+
proxyTools: opts.proxyTools
|
|
4191
|
+
});
|
|
3480
4192
|
});
|
|
3481
4193
|
mcpCommand.command("add").description("Add an MCP server").passThroughOptions().argument("<name>", "Server name").argument("<commandOrUrl>", "Command to run or URL to connect to").argument("[args...]", "Additional arguments (for stdio servers)").option("-e, --env <env>", "Set environment variables (KEY=value), repeatable", collectValues, []).option("-H, --header <header>", "Set HTTP headers (Key: value), repeatable", collectValues, []).option("-s, --scope <scope>", "Config scope: local, global", "local").option("-t, --transport <transport>", "Transport: stdio, sse, http").option("--client-id <clientId>", "OAuth client ID").option("--client-secret", "Prompt for OAuth client secret (or use MCP_CLIENT_SECRET env)").option("--callback-port <port>", "Fixed port for OAuth callback").option("--oauth-scope <oauthScope>", "OAuth scope string").action(async (name, commandOrUrl, args, opts) => {
|
|
3482
4194
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
@@ -3489,6 +4201,11 @@ mcpCommand.command("add").description("Add an MCP server").passThroughOptions().
|
|
|
3489
4201
|
resolvedSecret
|
|
3490
4202
|
}));
|
|
3491
4203
|
await tryReloadDaemon();
|
|
4204
|
+
capture("server_added", {
|
|
4205
|
+
server: name,
|
|
4206
|
+
scope,
|
|
4207
|
+
updated: result.existed
|
|
4208
|
+
});
|
|
3492
4209
|
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
3493
4210
|
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
3494
4211
|
});
|
|
@@ -3511,6 +4228,11 @@ mcpCommand.command("add-json").description("Add an MCP server from a JSON config
|
|
|
3511
4228
|
}
|
|
3512
4229
|
const result = addServer(configPath, name, serverConfig);
|
|
3513
4230
|
await tryReloadDaemon();
|
|
4231
|
+
capture("server_added", {
|
|
4232
|
+
server: name,
|
|
4233
|
+
scope,
|
|
4234
|
+
updated: result.existed
|
|
4235
|
+
});
|
|
3514
4236
|
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
3515
4237
|
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
3516
4238
|
});
|
|
@@ -3532,7 +4254,13 @@ mcpCommand.command("add-from-claude-desktop").description("Import MCP servers fr
|
|
|
3532
4254
|
writeMuxedConfig(configPath, { ...result.merged });
|
|
3533
4255
|
await tryReloadDaemon();
|
|
3534
4256
|
for (const w of warnings) console.error(`Warning: ${w}`);
|
|
3535
|
-
if (result.imported.length > 0)
|
|
4257
|
+
if (result.imported.length > 0) {
|
|
4258
|
+
capture("servers_imported", {
|
|
4259
|
+
servers: result.imported,
|
|
4260
|
+
source: "claude-desktop"
|
|
4261
|
+
});
|
|
4262
|
+
console.log(`Imported ${result.imported.length} server(s) from Claude Desktop: ${result.imported.join(", ")}`);
|
|
4263
|
+
}
|
|
3536
4264
|
if (result.skipped.length > 0) console.log(`Skipped ${result.skipped.length} (already existed): ${result.skipped.join(", ")}`);
|
|
3537
4265
|
if (result.imported.length === 0 && result.skipped.length === 0) console.log("No servers found in Claude Desktop config.");
|
|
3538
4266
|
});
|
|
@@ -3586,6 +4314,10 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
|
|
|
3586
4314
|
const configPath = getConfigPath(scope, explicitConfig);
|
|
3587
4315
|
if (removeServer(configPath, name).removed) {
|
|
3588
4316
|
await tryReloadDaemon();
|
|
4317
|
+
capture("server_removed", {
|
|
4318
|
+
server: name,
|
|
4319
|
+
scope
|
|
4320
|
+
});
|
|
3589
4321
|
console.log(`Removed "${name}" from ${scope} config (${configPath})`);
|
|
3590
4322
|
} else {
|
|
3591
4323
|
console.error(`Server "${name}" not found in ${scope} config.`);
|
|
@@ -3596,12 +4328,20 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
|
|
|
3596
4328
|
const localPath = getConfigPath("local", explicitConfig);
|
|
3597
4329
|
if (removeServer(localPath, name).removed) {
|
|
3598
4330
|
await tryReloadDaemon();
|
|
4331
|
+
capture("server_removed", {
|
|
4332
|
+
server: name,
|
|
4333
|
+
scope: "local"
|
|
4334
|
+
});
|
|
3599
4335
|
console.log(`Removed "${name}" from local config (${localPath})`);
|
|
3600
4336
|
return;
|
|
3601
4337
|
}
|
|
3602
4338
|
const globalPath = getConfigPath("global", explicitConfig);
|
|
3603
4339
|
if (removeServer(globalPath, name).removed) {
|
|
3604
4340
|
await tryReloadDaemon();
|
|
4341
|
+
capture("server_removed", {
|
|
4342
|
+
server: name,
|
|
4343
|
+
scope: "global"
|
|
4344
|
+
});
|
|
3605
4345
|
console.log(`Removed "${name}" from global config (${globalPath})`);
|
|
3606
4346
|
return;
|
|
3607
4347
|
}
|
|
@@ -3618,7 +4358,26 @@ const typegenCommand = new Command("typegen").description("Generate TypeScript t
|
|
|
3618
4358
|
fs.writeFileSync(outputPath, content, "utf-8");
|
|
3619
4359
|
console.log(`Generated ${tools.length} tool types → ${outputPath}`);
|
|
3620
4360
|
});
|
|
3621
|
-
|
|
4361
|
+
const telemetryCommand = new Command("telemetry").description("Manage anonymous telemetry (on, off, status)").argument("[action]", "on | off | status (default: status)").action((action) => {
|
|
4362
|
+
switch (action) {
|
|
4363
|
+
case "on":
|
|
4364
|
+
setTelemetryEnabled(true);
|
|
4365
|
+
console.log("Telemetry enabled.");
|
|
4366
|
+
break;
|
|
4367
|
+
case "off":
|
|
4368
|
+
setTelemetryEnabled(false);
|
|
4369
|
+
console.log("Telemetry disabled.");
|
|
4370
|
+
break;
|
|
4371
|
+
case "status":
|
|
4372
|
+
case void 0:
|
|
4373
|
+
console.log(`Telemetry is ${getTelemetryStatus()}.`);
|
|
4374
|
+
break;
|
|
4375
|
+
default:
|
|
4376
|
+
console.error(`Unknown action: ${action}. Use on, off, or status.`);
|
|
4377
|
+
process.exit(1);
|
|
4378
|
+
}
|
|
4379
|
+
});
|
|
4380
|
+
async function runCli() {
|
|
3622
4381
|
const program = new Command();
|
|
3623
4382
|
program.name("muxed").description("The optimization layer for MCP").version("0.1.0");
|
|
3624
4383
|
program.enablePositionalOptions();
|
|
@@ -3646,9 +4405,16 @@ function runCli() {
|
|
|
3646
4405
|
program.addCommand(initCommand);
|
|
3647
4406
|
program.addCommand(mcpCommand);
|
|
3648
4407
|
program.addCommand(typegenCommand);
|
|
4408
|
+
program.addCommand(telemetryCommand);
|
|
3649
4409
|
program.commandsGroup("Daemon:");
|
|
3650
4410
|
program.addCommand(daemonCommand);
|
|
3651
|
-
|
|
4411
|
+
const command = process.argv[2];
|
|
4412
|
+
capture("session_started", { command: command ?? null });
|
|
4413
|
+
try {
|
|
4414
|
+
await program.parseAsync();
|
|
4415
|
+
} finally {
|
|
4416
|
+
await shutdown();
|
|
4417
|
+
}
|
|
3652
4418
|
}
|
|
3653
4419
|
if (process.argv.indexOf("--daemon") !== -1) {
|
|
3654
4420
|
const configIndex = process.argv.indexOf("--config");
|
|
@@ -3656,5 +4422,8 @@ if (process.argv.indexOf("--daemon") !== -1) {
|
|
|
3656
4422
|
console.error("Failed to start daemon:", err);
|
|
3657
4423
|
process.exit(1);
|
|
3658
4424
|
});
|
|
3659
|
-
} else runCli()
|
|
4425
|
+
} else runCli().catch((err) => {
|
|
4426
|
+
console.error(err instanceof Error ? err.message : "Unexpected error");
|
|
4427
|
+
process.exit(1);
|
|
4428
|
+
});
|
|
3660
4429
|
export {};
|