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/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: 6e4,
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(), ".config", "muxed", "config.json");
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(filePath);
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) return {
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: "Missing required parameter: name"
1686
+ message: found.error.message,
1687
+ data: toErrorData(found.error)
1399
1688
  }
1400
1689
  };
1401
- const found = serverPool.findTool(p.name);
1402
- if (!found) return {
1403
- jsonrpc: "2.0",
1404
- id,
1405
- error: {
1406
- code: -32602,
1407
- message: `Tool not found: ${p.name}`
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
- const timeout = clientTimeout ?? p.timeout ?? requestTimeout;
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) return {
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: "Missing required parameter: name"
1752
+ message: found.error.message,
1753
+ data: toErrorData(found.error)
1425
1754
  }
1426
1755
  };
1427
- const found = serverPool.findTool(p.name);
1428
- if (!found) return {
1756
+ return {
1429
1757
  jsonrpc: "2.0",
1430
1758
  id,
1431
- error: {
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: found.tool
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) return {
1636
- jsonrpc: "2.0",
1637
- id,
1638
- error: {
1639
- code: -32602,
1640
- message: "Missing required parameter: name"
1641
- }
1642
- };
1643
- const found = serverPool.findTool(p.name);
1644
- if (!found) return {
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: `Tool not found: ${p.name}`
1993
+ message: foundAsync.error.message,
1994
+ data: toErrorData(foundAsync.error)
1650
1995
  }
1651
1996
  };
1652
- const taskHandle = await found.manager.callToolWithTask(found.tool.name, p.arguments ?? {});
1653
- serverPool.trackTask(taskHandle.taskId, found.manager.name);
1654
- return {
1655
- jsonrpc: "2.0",
1656
- id,
1657
- result: {
1658
- ...taskHandle,
1659
- server: found.manager.name
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
- const taskResult = await sendRequest("tools/call-async", {
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
- if (opts.json) console.log(formatJson(taskResult));
2600
- else console.log(`Task created: ${taskResult.taskId} (status: ${taskResult.status})`);
2601
- return;
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: ["muxed@latest", "proxy"]
3636
+ args
3051
3637
  };
3052
3638
  return {
3053
3639
  command: "npx",
3054
- args: ["muxed@latest", "proxy"]
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, ".config", "muxed", "config.json");
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 cliInstructions = (servers, instructions) => `
3238
- 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.
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 'npx muxed grep <pattern>' or 'npx muxed tools'.
3243
- 2. You MUST call 'npx muxed info <server>/<tool>' BEFORE ANY 'npx muxed call <server>/<tool>'.
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 an npx muxed call without checking the schema first.
3248
- **ALWAYS** run npx muxed info first, THEN make the call.
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 'npx muxed info' for ALL tools in parallel FIRST, then make your 'npx muxed call' commands.
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
- \`\`\`bash
3885
+ \`\`\`
3264
3886
  # STEP 1: REQUIRED TOOL DISCOVERY
3265
- npx muxed grep <pattern> # Search tool names and descriptions
3266
- npx muxed tools [server] # List available tools (optionally filter by server)
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
- npx muxed info <server>/<tool> # REQUIRED before ANY call - View JSON schema
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 3: Only after checking schema, make the call
3272
- npx muxed call <server>/<tool> '<json>' # Only run AFTER npx muxed info
3273
- npx muxed call <server>/<tool> - # Invoke with JSON from stdin (AFTER npx muxed info)
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
- npx muxed servers # List all connected MCP servers
3277
- npx muxed tools [server] # List available tools (optionally filter by server)
3278
- npx muxed grep <pattern> # Search tool names and descriptions
3279
- npx muxed resources [server] # List MCP resources
3280
- npx muxed read <server>/<resource> # Read an MCP resource
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 \`npx muxed grep "slack/*search*"\` to search for tools related to slack search.
3288
- [Calls npx muxed grep "slack/*search*"]
3289
- Assistant: I need to check the schema first. Let me call \`npx muxed info slack/search_private\` to see what parameters it accepts.
3290
- [Calls npx muxed info]
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 npx muxed call slack/search_private with correct schema]
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 \`npx muxed grep "database/*query*"\` and \`npx muxed grep "email/*send*"\` to search for tools related to database query and email send.
3298
- [Calls npx muxed grep "database/*query*" & npx muxed grep "email/*send*"]
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 npx muxed info database/query and npx muxed info email/send in parallel]
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 npx muxed call commands with correct parameters]
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 npx muxed grep "email/*copy*". No results found.]
3938
+ [Calls ${f.grep("email/*copy*")}. No results found.]
3309
3939
  Assistant: Let me try another pattern.
3310
- [Calls npx muxed grep "email/*clone*". No results found.]
3940
+ [Calls ${f.grep("email/*clone*")}. No results found.]
3311
3941
  Assistant: Let me list all available tools in the server.
3312
- [Calls npx muxed tools email]
3942
+ [Calls ${f.tools("email")}]
3313
3943
  Assistant: Let me check the schema first.
3314
- [Calls npx muxed info email/duplicate]
3944
+ [Calls ${f.info("email/duplicate")}]
3315
3945
  Assistant: Now I have the schema. Let me make the call.
3316
- [Calls npx muxed call email/duplicate with correct parameters]
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 npx muxed call slack/search_private with guessed parameters]
3324
- WRONG - You must call npx muxed info FIRST
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 npx muxed call slack/search_private directly]
3331
- WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call npx muxed info first.
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 npx muxed call commands in parallel without any npx muxed info calls first]
3337
- WRONG - You must call npx muxed info for ALL tools before making ANY npx muxed call commands
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
- \`\`\`bash
3971
+ \`\`\`
3342
3972
  # Discover tools
3343
- npx muxed tools # See all available MCP tools
3344
- npx muxed grep "weather" # Find tools by description
3973
+ ${f.tools()} # See all available MCP tools
3974
+ ${f.grep("weather")} # Find tools by description
3345
3975
 
3346
3976
  # Get tool details
3347
- npx muxed info <server>/<tool> # View JSON schema for input and output if available
3977
+ ${f.info("<server>/<tool>")} # View JSON schema for input and output if available
3348
3978
 
3349
3979
  # Simple tool call (no parameters)
3350
- npx muxed call weather/get_location '{}'
3980
+ ${f.call("weather/get_location", "{}")}
3351
3981
 
3352
3982
  # Tool call with parameters
3353
- npx muxed call database/query '{"table": "users", "limit": 10}'
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
- # Complex JSON using stdin (for nested objects/arrays)
3356
- npx muxed call api/send_request - <<'EOF'
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 the \`npx muxed -h\` to see all available commands.
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
- function buildInstructions(servers) {
3998
+ }
3999
+ function buildInstructions(servers, mode = "cli") {
3372
4000
  const connected = servers.filter((s) => s.status === "connected");
3373
- return cliInstructions(connected.map((s) => `- ${s.name}`).join("\n"), connected.filter((s) => s.instructions).map((s) => `### ${s.name}\n\n${s.instructions}`).join("\n\n")).trim();
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
- async function startMcpProxy(configPath) {
3376
- await ensureDaemon(configPath);
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 (_opts, cmd) => {
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(explicitConfig);
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) console.log(`Imported ${result.imported.length} server(s) from Claude Desktop: ${result.imported.join(", ")}`);
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
- function runCli() {
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
- program.parse();
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 {};