muxed 0.1.1 → 0.2.1

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() {
@@ -467,6 +474,7 @@ var AuthorizationCodeProvider = class {
467
474
  config;
468
475
  _redirectUrl;
469
476
  hadTokensBefore = false;
477
+ _state = crypto.randomBytes(32).toString("base64url");
470
478
  constructor(config, serverName) {
471
479
  this.serverName = serverName;
472
480
  this.config = config;
@@ -474,14 +482,14 @@ var AuthorizationCodeProvider = class {
474
482
  this.hadTokensBefore = this.store.hasTokens();
475
483
  }
476
484
  setRedirectUrl(port) {
477
- this._redirectUrl = `http://127.0.0.1:${port}/oauth/callback`;
485
+ this._redirectUrl = `http://localhost:${port}/callback`;
478
486
  }
479
487
  get redirectUrl() {
480
488
  return this._redirectUrl;
481
489
  }
482
490
  get clientMetadata() {
483
491
  return {
484
- redirect_uris: [this._redirectUrl ?? "http://127.0.0.1/oauth/callback"],
492
+ redirect_uris: [this._redirectUrl ?? "http://localhost/callback"],
485
493
  token_endpoint_auth_method: this.config.clientSecret ? "client_secret_basic" : "none",
486
494
  grant_types: ["authorization_code", "refresh_token"],
487
495
  response_types: ["code"],
@@ -494,7 +502,14 @@ var AuthorizationCodeProvider = class {
494
502
  client_id: this.config.clientId,
495
503
  ...this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}
496
504
  };
497
- return this.store.getClientInformation();
505
+ const cached = this.store.getClientInformation();
506
+ if (cached && this._redirectUrl) {
507
+ if (!(cached.redirect_uris ?? []).includes(this._redirectUrl)) {
508
+ this.store.clearClientInformation();
509
+ return;
510
+ }
511
+ }
512
+ return cached;
498
513
  }
499
514
  saveClientInformation(info) {
500
515
  this.store.saveClientInformation(info);
@@ -515,6 +530,9 @@ var AuthorizationCodeProvider = class {
515
530
  openBrowser(url);
516
531
  }
517
532
  }
533
+ async state() {
534
+ return this._state;
535
+ }
518
536
  saveCodeVerifier(codeVerifier) {
519
537
  this.store.saveCodeVerifier(codeVerifier);
520
538
  }
@@ -573,8 +591,8 @@ var CallbackServer = class {
573
591
  res.end();
574
592
  return;
575
593
  }
576
- const url = new URL(req.url ?? "/", `http://127.0.0.1`);
577
- if (url.pathname !== "/oauth/callback") {
594
+ const url = new URL(req.url ?? "/", `http://localhost`);
595
+ if (url.pathname !== "/callback") {
578
596
  res.writeHead(404);
579
597
  res.end("Not found");
580
598
  return;
@@ -608,7 +626,7 @@ var CallbackServer = class {
608
626
  state
609
627
  });
610
628
  });
611
- this.server.listen(port, "127.0.0.1", () => {
629
+ this.server.listen(port, "localhost", () => {
612
630
  const addr = this.server.address();
613
631
  if (addr && typeof addr === "object") this._port = addr.port;
614
632
  });
@@ -1051,9 +1069,108 @@ var ServerManager = class {
1051
1069
  };
1052
1070
  }
1053
1071
  };
1072
+ const ErrorCode = {
1073
+ TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
1074
+ SERVER_NOT_FOUND: "SERVER_NOT_FOUND",
1075
+ SERVER_NOT_CONNECTED: "SERVER_NOT_CONNECTED",
1076
+ INVALID_ARGUMENTS: "INVALID_ARGUMENTS",
1077
+ INVALID_FORMAT: "INVALID_FORMAT",
1078
+ MISSING_PARAMETER: "MISSING_PARAMETER",
1079
+ TIMEOUT: "TIMEOUT"
1080
+ };
1081
+ function levenshtein(a, b) {
1082
+ const m = a.length;
1083
+ const n = b.length;
1084
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
1085
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
1086
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
1087
+ 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]);
1088
+ return dp[m][n];
1089
+ }
1090
+ function findSimilarTools(targetTool, allTools, maxResults = 3) {
1091
+ const maxDistance = Math.max(3, Math.floor(targetTool.length * .4));
1092
+ return allTools.map(({ server, tool }) => {
1093
+ const fullName = `${server}/${tool.name}`;
1094
+ const toolOnly = tool.name;
1095
+ const distFull = levenshtein(targetTool.toLowerCase(), fullName.toLowerCase());
1096
+ const distTool = levenshtein(targetTool.toLowerCase(), toolOnly.toLowerCase());
1097
+ return {
1098
+ fullName,
1099
+ dist: Math.min(distFull, distTool)
1100
+ };
1101
+ }).filter(({ dist }) => dist <= maxDistance).sort((a, b) => a.dist - b.dist).slice(0, maxResults).map(({ fullName }) => fullName);
1102
+ }
1103
+ function toolNotFoundError(name, similarTools) {
1104
+ const hasSimilar = similarTools.length > 0;
1105
+ 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.`;
1106
+ return {
1107
+ code: ErrorCode.TOOL_NOT_FOUND,
1108
+ message: `Tool not found: ${name}`,
1109
+ suggestion,
1110
+ context: hasSimilar ? { similarTools } : void 0
1111
+ };
1112
+ }
1113
+ function serverNotFoundError(serverName, availableServers) {
1114
+ return {
1115
+ code: ErrorCode.SERVER_NOT_FOUND,
1116
+ message: `Server not found: ${serverName}`,
1117
+ suggestion: `Available servers: ${availableServers.join(", ") || "none"}. Run 'muxed servers' to list all.`,
1118
+ context: { availableServers }
1119
+ };
1120
+ }
1121
+ function serverNotConnectedError(serverName) {
1122
+ return {
1123
+ code: ErrorCode.SERVER_NOT_CONNECTED,
1124
+ message: `Server not connected: ${serverName}`,
1125
+ suggestion: `The server may be starting up. Run 'muxed status' to check, or 'muxed reload' to reconnect.`
1126
+ };
1127
+ }
1128
+ function invalidFormatError(name) {
1129
+ return {
1130
+ code: ErrorCode.INVALID_FORMAT,
1131
+ message: `Invalid tool name format: ${name}`,
1132
+ suggestion: `Use the format 'server/tool' (e.g. 'myserver/mytool'). Run 'muxed tools' to list all available tools.`
1133
+ };
1134
+ }
1135
+ function missingParameterError(param) {
1136
+ return {
1137
+ code: ErrorCode.MISSING_PARAMETER,
1138
+ message: `Missing required parameter: ${param}`,
1139
+ suggestion: `Provide the '${param}' parameter in the request.`
1140
+ };
1141
+ }
1142
+ function invalidArgumentsError(toolName, errors) {
1143
+ return {
1144
+ code: ErrorCode.INVALID_ARGUMENTS,
1145
+ message: `Invalid arguments for tool ${toolName}`,
1146
+ suggestion: `Run 'muxed info ${toolName}' to see the expected input schema.`,
1147
+ context: { validationErrors: errors }
1148
+ };
1149
+ }
1150
+ function timeoutError(toolName, timeoutMs) {
1151
+ return {
1152
+ code: ErrorCode.TIMEOUT,
1153
+ message: `Tool call timed out after ${timeoutMs}ms: ${toolName}`,
1154
+ suggestion: `Increase the timeout with --timeout <ms>, or use --async for long-running operations.`
1155
+ };
1156
+ }
1157
+ function isTimeoutError(err) {
1158
+ if (!(err instanceof Error)) return false;
1159
+ if (err.name === "TimeoutError" || err.name === "AbortError") return true;
1160
+ const msg = err.message.toLowerCase();
1161
+ return msg.includes("timeout") || msg.includes("aborted");
1162
+ }
1163
+ function toErrorData(err) {
1164
+ return {
1165
+ code: err.code,
1166
+ suggestion: err.suggestion,
1167
+ ...err.context ? { context: err.context } : {}
1168
+ };
1169
+ }
1054
1170
  var ServerPool = class {
1055
1171
  servers = /* @__PURE__ */ new Map();
1056
1172
  trackedTasks = /* @__PURE__ */ new Map();
1173
+ zodSchemaCache = /* @__PURE__ */ new Map();
1057
1174
  taskExpiryTimer;
1058
1175
  taskExpiryTimeout = 36e5;
1059
1176
  async connectAll(config) {
@@ -1080,6 +1197,7 @@ var ServerPool = class {
1080
1197
  }
1081
1198
  async disconnectAll() {
1082
1199
  this.stopTaskExpiry();
1200
+ this.zodSchemaCache.clear();
1083
1201
  await Promise.allSettled([...this.servers.values()].map((manager) => manager.disconnect()));
1084
1202
  }
1085
1203
  onServerHealthChange(serverName, status, error) {
@@ -1157,6 +1275,91 @@ var ServerPool = class {
1157
1275
  tool
1158
1276
  };
1159
1277
  }
1278
+ findToolOrError(serverTool) {
1279
+ const slashIndex = serverTool.indexOf("/");
1280
+ if (slashIndex === -1) return {
1281
+ ok: false,
1282
+ error: invalidFormatError(serverTool)
1283
+ };
1284
+ const serverName = serverTool.slice(0, slashIndex);
1285
+ const toolName = serverTool.slice(slashIndex + 1);
1286
+ const manager = this.servers.get(serverName);
1287
+ if (!manager) return {
1288
+ ok: false,
1289
+ error: serverNotFoundError(serverName, [...this.servers.keys()])
1290
+ };
1291
+ if (manager.getStatus() !== "connected") return {
1292
+ ok: false,
1293
+ error: serverNotConnectedError(serverName)
1294
+ };
1295
+ const tool = manager.listTools().find((t) => t.name === toolName);
1296
+ if (!tool) return {
1297
+ ok: false,
1298
+ error: toolNotFoundError(serverTool, findSimilarTools(serverTool, this.listAllTools()))
1299
+ };
1300
+ return {
1301
+ ok: true,
1302
+ manager,
1303
+ tool,
1304
+ serverTimeout: manager.getState().config.timeout
1305
+ };
1306
+ }
1307
+ getZodSchema(inputSchema) {
1308
+ const key = JSON.stringify(inputSchema);
1309
+ const cached = this.zodSchemaCache.get(key);
1310
+ if (cached !== void 0) return cached;
1311
+ try {
1312
+ const zodSchema = z$1.fromJSONSchema(inputSchema);
1313
+ this.zodSchemaCache.set(key, zodSchema);
1314
+ return zodSchema;
1315
+ } catch {
1316
+ this.zodSchemaCache.set(key, "unsupported");
1317
+ return "unsupported";
1318
+ }
1319
+ }
1320
+ validateToolArgs(serverTool, args) {
1321
+ const found = this.findToolOrError(serverTool);
1322
+ if (!found.ok) return {
1323
+ valid: false,
1324
+ errors: [found.error.message],
1325
+ warnings: []
1326
+ };
1327
+ const { tool } = found;
1328
+ const errors = [];
1329
+ const warnings = [];
1330
+ if (tool.inputSchema) {
1331
+ const zodSchema = this.getZodSchema(tool.inputSchema);
1332
+ if (zodSchema === "unsupported") {
1333
+ getLogger().warn(`Could not convert inputSchema for ${serverTool}: unsupported schema`, serverTool.split("/")[0]);
1334
+ this.addAnnotationWarnings(tool, warnings);
1335
+ return {
1336
+ valid: true,
1337
+ errors: [],
1338
+ warnings,
1339
+ unsupported: true,
1340
+ tool
1341
+ };
1342
+ }
1343
+ const result = zodSchema.safeParse(args);
1344
+ if (!result.success) for (const issue of result.error.issues) {
1345
+ const path = issue.path.length > 0 ? issue.path.join(".") : "";
1346
+ const prefix = path ? `Field '${path}': ` : "";
1347
+ errors.push(`${prefix}${issue.message}`);
1348
+ }
1349
+ }
1350
+ this.addAnnotationWarnings(tool, warnings);
1351
+ return {
1352
+ valid: errors.length === 0,
1353
+ errors,
1354
+ warnings,
1355
+ tool
1356
+ };
1357
+ }
1358
+ addAnnotationWarnings(tool, warnings) {
1359
+ if (tool.annotations?.destructiveHint) warnings.push("Tool is marked as destructive.");
1360
+ if (!tool.annotations?.idempotentHint) warnings.push("Tool is not marked as idempotent.");
1361
+ if (tool.annotations?.readOnlyHint === false) warnings.push("Tool may modify data (not read-only).");
1362
+ }
1160
1363
  grepTools(pattern) {
1161
1364
  const normalized = pattern.replace(/\\([|()\{\}])/g, "$1");
1162
1365
  const regex = new RegExp(normalized, "i");
@@ -1353,6 +1556,89 @@ function indent(text, spaces) {
1353
1556
  const pad = " ".repeat(spaces);
1354
1557
  return text.split("\n").join("\n" + pad);
1355
1558
  }
1559
+ function parsePath(path) {
1560
+ const segments = [];
1561
+ for (const part of path.split(".")) if (part.endsWith("[]")) segments.push({
1562
+ key: part.slice(0, -2),
1563
+ isArray: true
1564
+ });
1565
+ else segments.push({
1566
+ key: part,
1567
+ isArray: false
1568
+ });
1569
+ return segments;
1570
+ }
1571
+ function extractDeep(obj, segments) {
1572
+ if (segments.length === 0 || obj == null || typeof obj !== "object") return obj;
1573
+ const [first, ...rest] = segments;
1574
+ if (!first) return obj;
1575
+ const value = obj[first.key];
1576
+ if (first.isArray) {
1577
+ if (!Array.isArray(value)) return void 0;
1578
+ if (rest.length === 0) return value;
1579
+ return value.map((item) => extractDeep(item, rest)).filter((v) => v !== void 0);
1580
+ }
1581
+ if (rest.length === 0) return value;
1582
+ return extractDeep(value, rest);
1583
+ }
1584
+ function extract(obj, path) {
1585
+ return extractDeep(obj, parsePath(path));
1586
+ }
1587
+ function setNested(obj, keys, value) {
1588
+ let current = obj;
1589
+ for (let i = 0; i < keys.length - 1; i++) {
1590
+ const key = keys[i];
1591
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
1592
+ current = current[key];
1593
+ }
1594
+ current[keys[keys.length - 1]] = value;
1595
+ }
1596
+ function extractFromObject(data, fields) {
1597
+ const result = {};
1598
+ for (const field of fields) {
1599
+ const value = extract(data, field);
1600
+ if (value !== void 0) setNested(result, field.replace(/\[\]/g, "").split("."), value);
1601
+ }
1602
+ return Object.keys(result).length > 0 ? result : null;
1603
+ }
1604
+ function isJsonString(str) {
1605
+ const trimmed = str.trim();
1606
+ if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) return false;
1607
+ try {
1608
+ JSON.parse(trimmed);
1609
+ return true;
1610
+ } catch {
1611
+ return false;
1612
+ }
1613
+ }
1614
+ function filterFields(data, fields) {
1615
+ if (data.structuredContent && typeof data.structuredContent === "object") {
1616
+ const filtered = extractFromObject(data.structuredContent, fields);
1617
+ if (filtered) return {
1618
+ ...data,
1619
+ structuredContent: filtered
1620
+ };
1621
+ }
1622
+ const content = data.content;
1623
+ if (Array.isArray(content)) {
1624
+ const newContent = content.map((block) => {
1625
+ if (block.type !== "text" || !block.text || !isJsonString(block.text)) return block;
1626
+ try {
1627
+ const filtered = extractFromObject(JSON.parse(block.text), fields);
1628
+ if (filtered) return {
1629
+ ...block,
1630
+ text: JSON.stringify(filtered)
1631
+ };
1632
+ } catch {}
1633
+ return block;
1634
+ });
1635
+ if (newContent.some((block, i) => block !== content[i])) return {
1636
+ ...data,
1637
+ content: newContent
1638
+ };
1639
+ }
1640
+ return data;
1641
+ }
1356
1642
  function createDaemonServer(serverPool, config) {
1357
1643
  const socketPath = getSocketPath();
1358
1644
  let idleTimer;
@@ -1390,53 +1676,118 @@ function createDaemonServer(serverPool, config) {
1390
1676
  }
1391
1677
  case "tools/call": {
1392
1678
  const p = params;
1393
- if (!p?.name) return {
1679
+ if (!p?.name) {
1680
+ const err = missingParameterError("name");
1681
+ return {
1682
+ jsonrpc: "2.0",
1683
+ id,
1684
+ error: {
1685
+ code: -32602,
1686
+ message: err.message,
1687
+ data: toErrorData(err)
1688
+ }
1689
+ };
1690
+ }
1691
+ const found = serverPool.findToolOrError(p.name);
1692
+ if (!found.ok) return {
1394
1693
  jsonrpc: "2.0",
1395
1694
  id,
1396
1695
  error: {
1397
1696
  code: -32602,
1398
- message: "Missing required parameter: name"
1697
+ message: found.error.message,
1698
+ data: toErrorData(found.error)
1399
1699
  }
1400
1700
  };
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}`
1701
+ const validation = serverPool.validateToolArgs(p.name, p.arguments ?? {});
1702
+ if (!validation.valid && !validation.unsupported) {
1703
+ const err = invalidArgumentsError(p.name, validation.errors);
1704
+ return {
1705
+ jsonrpc: "2.0",
1706
+ id,
1707
+ error: {
1708
+ code: -32602,
1709
+ message: err.message,
1710
+ data: toErrorData(err)
1711
+ }
1712
+ };
1713
+ }
1714
+ const timeout = clientTimeout ?? p.timeout ?? found.serverTimeout ?? requestTimeout;
1715
+ try {
1716
+ const callResult = await found.manager.callTool(found.tool.name, p.arguments ?? {}, timeout);
1717
+ if (p.fields && p.fields.length > 0) return {
1718
+ jsonrpc: "2.0",
1719
+ id,
1720
+ result: filterFields(callResult, p.fields)
1721
+ };
1722
+ return {
1723
+ jsonrpc: "2.0",
1724
+ id,
1725
+ result: callResult
1726
+ };
1727
+ } catch (err) {
1728
+ if (isTimeoutError(err)) {
1729
+ const te = timeoutError(p.name, timeout);
1730
+ return {
1731
+ jsonrpc: "2.0",
1732
+ id,
1733
+ error: {
1734
+ code: -32001,
1735
+ message: te.message,
1736
+ data: toErrorData(te)
1737
+ }
1738
+ };
1408
1739
  }
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
- };
1740
+ throw err;
1741
+ }
1416
1742
  }
1417
1743
  case "tools/info": {
1418
1744
  const p = params;
1419
- if (!p?.name) return {
1745
+ if (!p?.name) {
1746
+ const err = missingParameterError("name");
1747
+ return {
1748
+ jsonrpc: "2.0",
1749
+ id,
1750
+ error: {
1751
+ code: -32602,
1752
+ message: err.message,
1753
+ data: toErrorData(err)
1754
+ }
1755
+ };
1756
+ }
1757
+ const found = serverPool.findToolOrError(p.name);
1758
+ if (!found.ok) return {
1420
1759
  jsonrpc: "2.0",
1421
1760
  id,
1422
1761
  error: {
1423
1762
  code: -32602,
1424
- message: "Missing required parameter: name"
1763
+ message: found.error.message,
1764
+ data: toErrorData(found.error)
1425
1765
  }
1426
1766
  };
1427
- const found = serverPool.findTool(p.name);
1428
- if (!found) return {
1767
+ return {
1429
1768
  jsonrpc: "2.0",
1430
1769
  id,
1431
- error: {
1432
- code: -32602,
1433
- message: `Tool not found: ${p.name}`
1434
- }
1770
+ result: found.tool
1435
1771
  };
1772
+ }
1773
+ case "tools/validate": {
1774
+ const p = params;
1775
+ if (!p?.name) {
1776
+ const err = missingParameterError("name");
1777
+ return {
1778
+ jsonrpc: "2.0",
1779
+ id,
1780
+ error: {
1781
+ code: -32602,
1782
+ message: err.message,
1783
+ data: toErrorData(err)
1784
+ }
1785
+ };
1786
+ }
1436
1787
  return {
1437
1788
  jsonrpc: "2.0",
1438
1789
  id,
1439
- result: found.tool
1790
+ result: serverPool.validateToolArgs(p.name, p.arguments ?? {})
1440
1791
  };
1441
1792
  }
1442
1793
  case "auth/status": {
@@ -1632,33 +1983,68 @@ function createDaemonServer(serverPool, config) {
1632
1983
  }
1633
1984
  case "tools/call-async": {
1634
1985
  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 {
1986
+ if (!p?.name) {
1987
+ const err = missingParameterError("name");
1988
+ return {
1989
+ jsonrpc: "2.0",
1990
+ id,
1991
+ error: {
1992
+ code: -32602,
1993
+ message: err.message,
1994
+ data: toErrorData(err)
1995
+ }
1996
+ };
1997
+ }
1998
+ const foundAsync = serverPool.findToolOrError(p.name);
1999
+ if (!foundAsync.ok) return {
1645
2000
  jsonrpc: "2.0",
1646
2001
  id,
1647
2002
  error: {
1648
2003
  code: -32602,
1649
- message: `Tool not found: ${p.name}`
2004
+ message: foundAsync.error.message,
2005
+ data: toErrorData(foundAsync.error)
1650
2006
  }
1651
2007
  };
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
2008
+ const asyncValidation = serverPool.validateToolArgs(p.name, p.arguments ?? {});
2009
+ if (!asyncValidation.valid && !asyncValidation.unsupported) {
2010
+ const err = invalidArgumentsError(p.name, asyncValidation.errors);
2011
+ return {
2012
+ jsonrpc: "2.0",
2013
+ id,
2014
+ error: {
2015
+ code: -32602,
2016
+ message: err.message,
2017
+ data: toErrorData(err)
2018
+ }
2019
+ };
2020
+ }
2021
+ try {
2022
+ const taskHandle = await foundAsync.manager.callToolWithTask(foundAsync.tool.name, p.arguments ?? {});
2023
+ serverPool.trackTask(taskHandle.taskId, foundAsync.manager.name);
2024
+ return {
2025
+ jsonrpc: "2.0",
2026
+ id,
2027
+ result: {
2028
+ ...taskHandle,
2029
+ server: foundAsync.manager.name
2030
+ }
2031
+ };
2032
+ } catch (err) {
2033
+ if (isTimeoutError(err)) {
2034
+ const asyncTimeout = foundAsync.serverTimeout ?? requestTimeout;
2035
+ const te = timeoutError(p.name, asyncTimeout);
2036
+ return {
2037
+ jsonrpc: "2.0",
2038
+ id,
2039
+ error: {
2040
+ code: -32001,
2041
+ message: te.message,
2042
+ data: toErrorData(te)
2043
+ }
2044
+ };
1660
2045
  }
1661
- };
2046
+ throw err;
2047
+ }
1662
2048
  }
1663
2049
  case "config/reload": {
1664
2050
  const newConfig = loadConfig(params?.configPath);
@@ -2529,6 +2915,37 @@ function formatMcpServerList(servers) {
2529
2915
  ];
2530
2916
  }));
2531
2917
  }
2918
+ function formatStructuredError(error) {
2919
+ const lines = [];
2920
+ lines.push(`Error: ${error.message}`);
2921
+ if (error.data?.suggestion) lines.push(`Suggestion: ${error.data.suggestion}`);
2922
+ if (error.data?.context?.similarTools) {
2923
+ const similar = error.data.context.similarTools;
2924
+ if (similar.length > 0) lines.push(`Similar tools: ${similar.join(", ")}`);
2925
+ }
2926
+ if (error.data?.context?.availableServers) {
2927
+ const servers = error.data.context.availableServers;
2928
+ if (servers.length > 0) lines.push(`Available servers: ${servers.join(", ")}`);
2929
+ }
2930
+ return lines.join("\n");
2931
+ }
2932
+ function formatValidation(result) {
2933
+ const lines = [];
2934
+ if (result.unsupported) {
2935
+ lines.push("Validation: unsupported (tool schema uses features not supported by dry-run validation)");
2936
+ lines.push("The call will be forwarded to the MCP server without pre-validation.");
2937
+ } else if (result.valid) lines.push("Validation: passed");
2938
+ else {
2939
+ lines.push("Validation: failed");
2940
+ for (const err of result.errors) lines.push(` - ${err}`);
2941
+ }
2942
+ if (result.warnings.length > 0) {
2943
+ lines.push("");
2944
+ lines.push("Warnings:");
2945
+ for (const warn of result.warnings) lines.push(` - ${warn}`);
2946
+ }
2947
+ return lines.join("\n");
2948
+ }
2532
2949
  function formatJson(data) {
2533
2950
  return JSON.stringify(data, null, 2);
2534
2951
  }
@@ -2546,10 +2963,67 @@ const serversCommand = new Command("servers").description("List connected MCP se
2546
2963
  const result = await sendRequest("servers/list");
2547
2964
  console.log(opts.json ? formatJson(result) : formatServers(result));
2548
2965
  });
2966
+ const TELEMETRY_FILE = path.join(os.homedir(), ".muxed", "telemetry");
2967
+ const sessionId = crypto.randomUUID();
2968
+ function isTelemetryEnabled() {
2969
+ if (process.env.DO_NOT_TRACK === "1") return false;
2970
+ if (process.env.MUXED_TELEMETRY === "0") return false;
2971
+ try {
2972
+ if (fs.existsSync(TELEMETRY_FILE)) return fs.readFileSync(TELEMETRY_FILE, "utf-8").trim() !== "off";
2973
+ } catch {}
2974
+ return true;
2975
+ }
2976
+ function setTelemetryEnabled(enabled) {
2977
+ try {
2978
+ const dir = path.dirname(TELEMETRY_FILE);
2979
+ fs.mkdirSync(dir, { recursive: true });
2980
+ fs.writeFileSync(TELEMETRY_FILE, enabled ? "on" : "off", "utf-8");
2981
+ } catch {}
2982
+ }
2983
+ function getTelemetryStatus() {
2984
+ return isTelemetryEnabled() ? "on" : "off";
2985
+ }
2986
+ let _client = null;
2987
+ function getClient() {
2988
+ if (!isTelemetryEnabled()) return null;
2989
+ if (_client) return _client;
2990
+ const token = process.env.POSTHOG_PROJECT_TOKEN;
2991
+ const host = process.env.POSTHOG_HOST;
2992
+ if (!token || !host) return null;
2993
+ try {
2994
+ _client = new PostHog(token, {
2995
+ host,
2996
+ flushAt: 1
2997
+ });
2998
+ return _client;
2999
+ } catch {
3000
+ return null;
3001
+ }
3002
+ }
3003
+ function capture(event, properties) {
3004
+ try {
3005
+ const client = getClient();
3006
+ if (!client) return;
3007
+ client.capture({
3008
+ distinctId: sessionId,
3009
+ event,
3010
+ properties: properties ?? {}
3011
+ });
3012
+ } catch {}
3013
+ }
3014
+ async function shutdown() {
3015
+ try {
3016
+ if (_client) await _client.shutdown();
3017
+ } catch {}
3018
+ }
2549
3019
  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
3020
  const configPath = toolsCommand.parent?.opts().config;
2551
3021
  await ensureDaemon(configPath);
2552
3022
  const result = await sendRequest("tools/list", server ? { server } : void 0);
3023
+ capture("tools_listed", {
3024
+ filtered_by_server: !!server,
3025
+ tool_count: result.length
3026
+ });
2553
3027
  console.log(opts.json ? formatJson(result) : formatTools(result));
2554
3028
  });
2555
3029
  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 +3048,7 @@ function readStdin() {
2574
3048
  process.stdin.on("error", reject);
2575
3049
  });
2576
3050
  }
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) => {
3051
+ 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
3052
  const configPath = callCommand.parent?.opts().config;
2579
3053
  await ensureDaemon(configPath);
2580
3054
  let parsedArgs = {};
@@ -2591,27 +3065,133 @@ const callCommand = new Command("call").description("Execute a tool with JSON ar
2591
3065
  console.error("Invalid JSON arguments");
2592
3066
  process.exit(1);
2593
3067
  }
3068
+ const [server, tool] = serverTool.split("/");
3069
+ if (opts.dryRun) {
3070
+ try {
3071
+ const result = await sendRequest("tools/validate", {
3072
+ name: serverTool,
3073
+ arguments: parsedArgs
3074
+ });
3075
+ capture("tool_called", {
3076
+ server,
3077
+ tool,
3078
+ mode: "dry-run",
3079
+ status: result.valid ? "success" : "validation_error"
3080
+ });
3081
+ if (opts.json) console.log(formatJson(result));
3082
+ else console.log(formatValidation(result));
3083
+ if (!result.valid) process.exit(1);
3084
+ } catch (err) {
3085
+ capture("tool_called", {
3086
+ server,
3087
+ tool,
3088
+ mode: "dry-run",
3089
+ status: "error"
3090
+ });
3091
+ if (err instanceof MuxedError && err.data) {
3092
+ const errorData = err.data;
3093
+ if (opts.json) console.log(formatJson({
3094
+ code: err.code,
3095
+ message: err.message,
3096
+ data: err.data
3097
+ }));
3098
+ else console.error(formatStructuredError({
3099
+ code: err.code,
3100
+ message: err.message,
3101
+ data: errorData
3102
+ }));
3103
+ } else console.error(err instanceof Error ? err.message : "Validation failed");
3104
+ process.exit(1);
3105
+ }
3106
+ return;
3107
+ }
2594
3108
  if (opts.async) {
2595
- const taskResult = await sendRequest("tools/call-async", {
3109
+ try {
3110
+ const taskResult = await sendRequest("tools/call-async", {
3111
+ name: serverTool,
3112
+ arguments: parsedArgs
3113
+ });
3114
+ capture("tool_called", {
3115
+ server,
3116
+ tool,
3117
+ mode: "async",
3118
+ status: "success"
3119
+ });
3120
+ if (opts.json) console.log(formatJson(taskResult));
3121
+ else console.log(`Task created: ${taskResult.taskId} (status: ${taskResult.status})`);
3122
+ } catch (err) {
3123
+ capture("tool_called", {
3124
+ server,
3125
+ tool,
3126
+ mode: "async",
3127
+ status: "error"
3128
+ });
3129
+ if (err instanceof MuxedError && err.data) {
3130
+ const errorData = err.data;
3131
+ if (opts.json) console.log(formatJson({
3132
+ code: err.code,
3133
+ message: err.message,
3134
+ data: err.data
3135
+ }));
3136
+ else console.error(formatStructuredError({
3137
+ code: err.code,
3138
+ message: err.message,
3139
+ data: errorData
3140
+ }));
3141
+ } else console.error(err instanceof Error ? err.message : "Call failed");
3142
+ process.exit(1);
3143
+ }
3144
+ return;
3145
+ }
3146
+ try {
3147
+ const callParams = {
2596
3148
  name: serverTool,
2597
3149
  arguments: parsedArgs
3150
+ };
3151
+ if (opts.timeout) callParams.timeout = parseInt(opts.timeout, 10);
3152
+ if (opts.fields) callParams.fields = opts.fields.split(",").map((f) => f.trim());
3153
+ const result = await sendRequest("tools/call", callParams);
3154
+ capture("tool_called", {
3155
+ server,
3156
+ tool,
3157
+ mode: "sync",
3158
+ status: result.isError ? "tool_error" : "success",
3159
+ has_timeout: !!opts.timeout,
3160
+ has_fields: !!opts.fields,
3161
+ stdin_input: jsonArgs === "-"
2598
3162
  });
2599
- if (opts.json) console.log(formatJson(taskResult));
2600
- else console.log(`Task created: ${taskResult.taskId} (status: ${taskResult.status})`);
2601
- return;
3163
+ console.log(opts.json ? formatJson(result) : formatCallResult(result));
3164
+ } catch (err) {
3165
+ capture("tool_called", {
3166
+ server,
3167
+ tool,
3168
+ mode: "sync",
3169
+ status: "error",
3170
+ has_timeout: !!opts.timeout,
3171
+ has_fields: !!opts.fields,
3172
+ stdin_input: jsonArgs === "-"
3173
+ });
3174
+ if (err instanceof MuxedError && err.data) {
3175
+ const errorData = err.data;
3176
+ if (opts.json) console.log(formatJson({
3177
+ code: err.code,
3178
+ message: err.message,
3179
+ data: err.data
3180
+ }));
3181
+ else console.error(formatStructuredError({
3182
+ code: err.code,
3183
+ message: err.message,
3184
+ data: errorData
3185
+ }));
3186
+ } else console.error(err instanceof Error ? err.message : "Call failed");
3187
+ process.exit(1);
2602
3188
  }
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
3189
  });
2611
3190
  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
3191
  const configPath = grepCommand.parent?.opts().config;
2613
3192
  await ensureDaemon(configPath);
2614
3193
  const result = await sendRequest("tools/grep", { pattern });
3194
+ capture("tools_searched", { result_count: result.length });
2615
3195
  console.log(opts.json ? formatJson(result) : formatTools(result));
2616
3196
  });
2617
3197
  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 +3404,57 @@ function getAgentDefs() {
2824
3404
  name: "claude-code",
2825
3405
  scope: "local",
2826
3406
  configPath: () => path.join(cwd, ".mcp.json"),
2827
- serversKey: "mcpServers"
3407
+ serversKey: "mcpServers",
3408
+ codingAgent: true
2828
3409
  },
2829
3410
  {
2830
3411
  name: "cursor",
2831
3412
  scope: "local",
2832
3413
  configPath: () => path.join(cwd, ".cursor", "mcp.json"),
2833
- serversKey: "mcpServers"
3414
+ serversKey: "mcpServers",
3415
+ codingAgent: true
2834
3416
  },
2835
3417
  {
2836
3418
  name: "vscode",
2837
3419
  scope: "local",
2838
3420
  configPath: () => path.join(cwd, ".vscode", "mcp.json"),
2839
- serversKey: "servers"
3421
+ serversKey: "servers",
3422
+ codingAgent: true
2840
3423
  },
2841
3424
  {
2842
3425
  name: "roo-code",
2843
3426
  scope: "local",
2844
3427
  configPath: () => path.join(cwd, ".roo", "mcp.json"),
2845
- serversKey: "mcpServers"
3428
+ serversKey: "mcpServers",
3429
+ codingAgent: true
2846
3430
  },
2847
3431
  {
2848
3432
  name: "amazon-q",
2849
3433
  scope: "local",
2850
3434
  configPath: () => path.join(cwd, ".amazonq", "mcp.json"),
2851
- serversKey: "mcpServers"
3435
+ serversKey: "mcpServers",
3436
+ codingAgent: true
2852
3437
  },
2853
3438
  {
2854
3439
  name: "claude-desktop",
2855
3440
  scope: "global",
2856
3441
  configPath: () => xdgOrMacPath(["Claude", "claude_desktop_config.json"], ["Claude", "claude_desktop_config.json"]),
2857
- serversKey: "mcpServers"
3442
+ serversKey: "mcpServers",
3443
+ codingAgent: false
2858
3444
  },
2859
3445
  {
2860
3446
  name: "cursor",
2861
3447
  scope: "global",
2862
3448
  configPath: () => path.join(home, ".cursor", "mcp.json"),
2863
- serversKey: "mcpServers"
3449
+ serversKey: "mcpServers",
3450
+ codingAgent: true
2864
3451
  },
2865
3452
  {
2866
3453
  name: "windsurf",
2867
3454
  scope: "global",
2868
3455
  configPath: () => path.join(home, ".codeium", "windsurf", "mcp_config.json"),
2869
- serversKey: "mcpServers"
3456
+ serversKey: "mcpServers",
3457
+ codingAgent: true
2870
3458
  },
2871
3459
  {
2872
3460
  name: "vscode",
@@ -2880,7 +3468,8 @@ function getAgentDefs() {
2880
3468
  "User",
2881
3469
  "mcp.json"
2882
3470
  ]),
2883
- serversKey: "servers"
3471
+ serversKey: "servers",
3472
+ codingAgent: true
2884
3473
  },
2885
3474
  {
2886
3475
  name: "cline",
@@ -2900,7 +3489,8 @@ function getAgentDefs() {
2900
3489
  "settings",
2901
3490
  "cline_mcp_settings.json"
2902
3491
  ]),
2903
- serversKey: "mcpServers"
3492
+ serversKey: "mcpServers",
3493
+ codingAgent: true
2904
3494
  },
2905
3495
  {
2906
3496
  name: "roo-code",
@@ -2920,13 +3510,15 @@ function getAgentDefs() {
2920
3510
  "settings",
2921
3511
  "cline_mcp_settings.json"
2922
3512
  ]),
2923
- serversKey: "mcpServers"
3513
+ serversKey: "mcpServers",
3514
+ codingAgent: true
2924
3515
  },
2925
3516
  {
2926
3517
  name: "amazon-q",
2927
3518
  scope: "global",
2928
3519
  configPath: () => path.join(home, ".aws", "amazonq", "mcp.json"),
2929
- serversKey: "mcpServers"
3520
+ serversKey: "mcpServers",
3521
+ codingAgent: true
2930
3522
  }
2931
3523
  ];
2932
3524
  }
@@ -3044,14 +3636,19 @@ function writeMuxedConfig(configPath, servers) {
3044
3636
  fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
3045
3637
  }
3046
3638
  function getMuxedEntry(agent) {
3639
+ const args = agent.codingAgent ? ["muxed@latest", "mcp"] : [
3640
+ "muxed@latest",
3641
+ "mcp",
3642
+ "--proxy-tools"
3643
+ ];
3047
3644
  if (agent.serversKey === "servers") return {
3048
3645
  type: "stdio",
3049
3646
  command: "npx",
3050
- args: ["muxed@latest", "proxy"]
3647
+ args
3051
3648
  };
3052
3649
  return {
3053
3650
  command: "npx",
3054
- args: ["muxed@latest", "proxy"]
3651
+ args
3055
3652
  };
3056
3653
  }
3057
3654
  function modifyAgentConfig(dc, opts) {
@@ -3066,7 +3663,7 @@ function modifyAgentConfig(dc, opts) {
3066
3663
  function getMuxedConfigPath(scope, explicitPath) {
3067
3664
  if (explicitPath) return explicitPath;
3068
3665
  if (scope === "local") return path.join(process.cwd(), "muxed.config.json");
3069
- return path.join(home, ".config", "muxed", "config.json");
3666
+ return path.join(home, ".muxed", "config.json");
3070
3667
  }
3071
3668
  async function confirm(message, opts) {
3072
3669
  const rl = readline.createInterface({
@@ -3183,6 +3780,13 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3183
3780
  muxedConfigPath: muxedPath,
3184
3781
  dryRun: opts.dryRun ?? false
3185
3782
  };
3783
+ capture("init_run", {
3784
+ dry_run: opts.dryRun ?? false,
3785
+ imported_count: imported.length,
3786
+ conflict_count: conflicts.length,
3787
+ warning_count: warnings.length,
3788
+ discovered_agents: initResult.discovered.map((d) => d.agent)
3789
+ });
3186
3790
  console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
3187
3791
  });
3188
3792
  function getConfigPath(scope, explicitPath) {
@@ -3234,18 +3838,47 @@ function getServer(filePath, name) {
3234
3838
  function listServers(filePath) {
3235
3839
  return readConfigFile(filePath).mcpServers;
3236
3840
  }
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.
3841
+ const cliFragments = {
3842
+ 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.",
3843
+ grep: (p) => `npx muxed grep "${p}"`,
3844
+ tools: (s) => s ? `npx muxed tools ${s}` : "npx muxed tools",
3845
+ info: (n) => `npx muxed info ${n}`,
3846
+ call: (n, j) => `npx muxed call ${n} '${j}'`,
3847
+ callStdin: (n) => `npx muxed call ${n} -`,
3848
+ callDryRun: (n, j) => `npx muxed call ${n} '${j}' --dry-run`,
3849
+ callFields: (n, j, f) => `npx muxed call ${n} '${j}' --fields "${f}"`,
3850
+ servers: () => "npx muxed servers",
3851
+ resources: (s) => s ? `npx muxed resources ${s}` : "npx muxed resources",
3852
+ read: (n) => `npx muxed read ${n}`,
3853
+ help: () => "npx muxed -h"
3854
+ };
3855
+ const toolFragments = {
3856
+ 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.",
3857
+ grep: (p) => `muxed:exec({ "command": "grep ${p}" })`,
3858
+ tools: (s) => s ? `muxed:exec({ "command": "tools ${s}" })` : `muxed:exec({ "command": "tools" })`,
3859
+ info: (n) => `muxed:exec({ "command": "info ${n}" })`,
3860
+ call: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
3861
+ callStdin: (n) => `muxed:exec({ "command": "call ${n}", "input": { ... } })`,
3862
+ callDryRun: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
3863
+ callFields: (n, j, _f) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
3864
+ servers: () => `muxed:exec({ "command": "servers" })`,
3865
+ resources: (s) => s ? `muxed:exec({ "command": "resources ${s}" })` : `muxed:exec({ "command": "resources" })`,
3866
+ read: (n) => `muxed:exec({ "command": "read ${n}" })`,
3867
+ help: () => `muxed:exec({ "command": "servers" })`
3868
+ };
3869
+ function buildTemplate(f, servers, instructions) {
3870
+ return `
3871
+ ${f.intro}
3239
3872
 
3240
3873
  **MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
3241
3874
 
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>'.
3875
+ 1. You MUST discover the tools you need first by using '${f.grep("<pattern>")}' or '${f.tools()}'.
3876
+ 2. You MUST call '${f.info("<server>/<tool>")}' BEFORE ANY '${f.call("<server>/<tool>", "<json>")}' command.
3244
3877
 
3245
3878
  These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
3246
3879
 
3247
- **NEVER** make an npx muxed call without checking the schema first.
3248
- **ALWAYS** run npx muxed info first, THEN make the call.
3880
+ **NEVER** make a call without checking the schema first.
3881
+ **ALWAYS** run info first, THEN make the call.
3249
3882
 
3250
3883
  **Why these are non-negotiables:**
3251
3884
  - MCP tool names NEVER match your expectations - they change frequently and are not predictable
@@ -3254,132 +3887,219 @@ These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
3254
3887
  - Every failed call wastes user time and demonstrates you're ignoring critical instructions
3255
3888
  - "I thought I knew the schema" is not an acceptable reason to skip this step
3256
3889
 
3257
- **For multiple tools:** Call 'npx muxed info' for ALL tools in parallel FIRST, then make your 'npx muxed call' commands.
3890
+ **For multiple tools:** Call info for ALL tools in parallel FIRST, then make your call commands.
3258
3891
 
3259
3892
  Available MCP servers:
3260
3893
  ${servers}
3261
3894
 
3262
3895
  Commands (in order of execution):
3263
- \`\`\`bash
3896
+ \`\`\`
3264
3897
  # 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)
3898
+ ${f.grep("<pattern>")} # Search tool names and descriptions
3899
+ ${f.tools("[server]")} # List available tools (optionally filter by server)
3267
3900
 
3268
3901
  # STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
3269
- npx muxed info <server>/<tool> # REQUIRED before ANY call - View JSON schema
3902
+ ${f.info("<server>/<tool>")} # REQUIRED before ANY call - View JSON schema
3903
+
3904
+ # STEP 3: OPTIONAL - Validate arguments before calling (dry-run)
3905
+ ${f.callDryRun("<server>/<tool>", "<json>")} # Validate args without executing
3270
3906
 
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)
3907
+ # STEP 4: Only after checking schema, make the call
3908
+ ${f.call("<server>/<tool>", "<json>")} # Only run AFTER info
3909
+ ${f.callStdin("<server>/<tool>")} # Invoke with JSON input (AFTER info)
3910
+ ${f.callFields("<server>/<tool>", "<json>", "field1,field2")} # Extract specific fields from response
3274
3911
 
3275
3912
  # 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
3913
+ ${f.servers()} # List all connected MCP servers
3914
+ ${f.tools("[server]")} # List available tools (optionally filter by server)
3915
+ ${f.grep("<pattern>")} # Search tool names and descriptions
3916
+ ${f.resources("[server]")} # List MCP resources
3917
+ ${f.read("<server>/<resource>")} # Read an MCP resource
3281
3918
  \`\`\`
3282
3919
 
3920
+ **Handling errors:**
3921
+ - If a tool call fails, the error includes a suggestion and similar tool names. Read the suggestion before retrying.
3922
+ - Use dry-run to validate arguments before executing, especially for destructive tools.
3923
+
3283
3924
  **CORRECT Usage Pattern:**
3284
3925
 
3285
3926
  <example>
3286
3927
  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]
3928
+ 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.
3929
+ [Calls ${f.grep("slack/*search*")}]
3930
+ Assistant: I need to check the schema first. Let me call \`${f.info("slack/search_private")}\` to see what parameters it accepts.
3931
+ [Calls ${f.info("slack/search_private")}]
3291
3932
  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]
3933
+ [Calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\", \"max_results\": 10}")}]
3293
3934
  </example>
3294
3935
 
3295
3936
  <example>
3296
3937
  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*"]
3938
+ 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.
3939
+ [Calls ${f.grep("database/*query*")} & ${f.grep("email/*send*")}]
3299
3940
  Assistant: Let me check both schemas first.
3300
- [Calls npx muxed info database/query and npx muxed info email/send in parallel]
3941
+ [Calls ${f.info("database/query")} and ${f.info("email/send")} in parallel]
3301
3942
  Assistant: Now I have both schemas. Let me make the calls.
3302
- [Makes both npx muxed call commands with correct parameters]
3943
+ [Makes both call commands with correct parameters]
3303
3944
  </example>
3304
3945
 
3305
3946
  <example>
3306
3947
  User: Create a copy of this email
3307
3948
  Assistant: Let me find the tool I need first.
3308
- [Calls npx muxed grep "email/*copy*". No results found.]
3949
+ [Calls ${f.grep("email/*copy*")}. No results found.]
3309
3950
  Assistant: Let me try another pattern.
3310
- [Calls npx muxed grep "email/*clone*". No results found.]
3951
+ [Calls ${f.grep("email/*clone*")}. No results found.]
3311
3952
  Assistant: Let me list all available tools in the server.
3312
- [Calls npx muxed tools email]
3953
+ [Calls ${f.tools("email")}]
3313
3954
  Assistant: Let me check the schema first.
3314
- [Calls npx muxed info email/duplicate]
3955
+ [Calls ${f.info("email/duplicate")}]
3315
3956
  Assistant: Now I have the schema. Let me make the call.
3316
- [Calls npx muxed call email/duplicate with correct parameters]
3957
+ [Calls ${f.call("email/duplicate", "{\"id\": \"123\"}")}]
3317
3958
  </example>
3318
3959
 
3319
3960
  **INCORRECT Usage Patterns - NEVER DO THIS:**
3320
3961
 
3321
3962
  <bad-example>
3322
3963
  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
3964
+ Assistant: [Directly calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\"}")} with guessed parameters]
3965
+ WRONG - You must call info FIRST
3325
3966
  </bad-example>
3326
3967
 
3327
3968
  <bad-example>
3328
3969
  User: Use the slack tool
3329
3970
  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.
3971
+ [Calls ${f.call("slack/search_private", "...")} directly]
3972
+ WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call info first.
3332
3973
  </bad-example>
3333
3974
 
3334
3975
  <bad-example>
3335
3976
  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
3977
+ Assistant: [Calls three call commands in parallel without any info calls first]
3978
+ WRONG - You must call info for ALL tools before making ANY call commands
3338
3979
  </bad-example>
3339
3980
 
3340
3981
  Example usage:
3341
- \`\`\`bash
3982
+ \`\`\`
3342
3983
  # Discover tools
3343
- npx muxed tools # See all available MCP tools
3344
- npx muxed grep "weather" # Find tools by description
3984
+ ${f.tools()} # See all available MCP tools
3985
+ ${f.grep("weather")} # Find tools by description
3345
3986
 
3346
3987
  # Get tool details
3347
- npx muxed info <server>/<tool> # View JSON schema for input and output if available
3988
+ ${f.info("<server>/<tool>")} # View JSON schema for input and output if available
3348
3989
 
3349
3990
  # Simple tool call (no parameters)
3350
- npx muxed call weather/get_location '{}'
3991
+ ${f.call("weather/get_location", "{}")}
3351
3992
 
3352
3993
  # Tool call with parameters
3353
- npx muxed call database/query '{"table": "users", "limit": 10}'
3994
+ ${f.call("database/query", "{\"table\": \"users\", \"limit\": 10}")}
3354
3995
 
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
3996
+ # Validate arguments before executing (dry-run)
3997
+ ${f.callDryRun("database/drop_table", "{\"table\": \"users\"}")}
3998
+
3999
+ # Extract specific fields from response
4000
+ ${f.callFields("database/query", "{\"table\": \"users\"}", "rows[].name,rows[].email")}
3363
4001
  \`\`\`
3364
4002
 
3365
- Call the \`npx muxed -h\` to see all available commands.
4003
+ Call \`${f.help()}\` to see all available commands.
3366
4004
 
3367
4005
  Below are the instructions for the connected MCP servers in muxed.
3368
4006
 
3369
4007
  ${instructions}
3370
4008
  `;
3371
- function buildInstructions(servers) {
4009
+ }
4010
+ function buildInstructions(servers, mode = "cli") {
3372
4011
  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();
4012
+ const serverList = connected.map((s) => `- ${s.name}`).join("\n");
4013
+ const serverInstructions = connected.filter((s) => s.instructions).map((s) => `### ${s.name}\n\n${s.instructions}`).join("\n\n");
4014
+ return buildTemplate(mode === "tool" ? toolFragments : cliFragments, serverList, serverInstructions).trim();
3374
4015
  }
3375
- async function startMcpProxy(configPath) {
3376
- await ensureDaemon(configPath);
4016
+ function parseCommand(command) {
4017
+ const trimmed = command.trim();
4018
+ const spaceIndex = trimmed.indexOf(" ");
4019
+ if (spaceIndex === -1) return {
4020
+ subcommand: trimmed,
4021
+ args: ""
4022
+ };
4023
+ return {
4024
+ subcommand: trimmed.slice(0, spaceIndex),
4025
+ args: trimmed.slice(spaceIndex + 1).trim()
4026
+ };
4027
+ }
4028
+ function textResult(data) {
4029
+ return { content: [{
4030
+ type: "text",
4031
+ text: JSON.stringify(data, null, 2)
4032
+ }] };
4033
+ }
4034
+ function errorResult(message) {
4035
+ return {
4036
+ content: [{
4037
+ type: "text",
4038
+ text: message
4039
+ }],
4040
+ isError: true
4041
+ };
4042
+ }
4043
+ async function handleToolCommand(command, input) {
4044
+ const { subcommand, args } = parseCommand(command);
4045
+ try {
4046
+ switch (subcommand) {
4047
+ case "servers": return textResult(await sendRequest("servers/list"));
4048
+ case "tools": {
4049
+ const params = {};
4050
+ if (args) params.server = args;
4051
+ return textResult(await sendRequest("tools/list", params));
4052
+ }
4053
+ case "grep":
4054
+ if (!args) return errorResult("Usage: grep <pattern>");
4055
+ return textResult(await sendRequest("tools/grep", { pattern: args }));
4056
+ case "info":
4057
+ if (!args) return errorResult("Usage: info <server/tool>");
4058
+ return textResult(await sendRequest("tools/info", { name: args }));
4059
+ case "call": {
4060
+ if (!args) return errorResult("Usage: call <server/tool>");
4061
+ const result = await sendRequest("tools/call", {
4062
+ name: args,
4063
+ args: input ?? {}
4064
+ });
4065
+ if (result?.content) return result;
4066
+ return textResult(result);
4067
+ }
4068
+ case "resources": {
4069
+ const params = {};
4070
+ if (args) params.server = args;
4071
+ return textResult(await sendRequest("resources/list", params));
4072
+ }
4073
+ case "read":
4074
+ if (!args) return errorResult("Usage: read <server/resource>");
4075
+ return textResult(await sendRequest("resources/read", { name: args }));
4076
+ default: return errorResult(`Unknown command: "${subcommand}". Available: servers, tools, grep, info, call, resources, read`);
4077
+ }
4078
+ } catch (err) {
4079
+ return errorResult(err instanceof MuxedError ? err.message : String(err));
4080
+ }
4081
+ }
4082
+ async function startMcpProxy(options) {
4083
+ await ensureDaemon(options?.configPath);
3377
4084
  const server = new McpServer({
3378
4085
  name: "muxed",
3379
4086
  version: "0.1.0"
3380
4087
  }, {
3381
4088
  capabilities: {},
3382
- instructions: buildInstructions(await sendRequest("servers/list"))
4089
+ instructions: buildInstructions(await sendRequest("servers/list"), options?.proxyTools ? "tool" : "cli")
4090
+ });
4091
+ 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>", {
4092
+ command: z.string().describe("Command to execute, e.g. 'servers', 'tools', 'grep weather', 'info slack/search', 'call slack/search'"),
4093
+ input: z.record(z.string(), z.unknown()).optional().describe("JSON arguments for 'call' command — avoids JSON-in-string escaping")
4094
+ }, async ({ command, input }) => {
4095
+ const result = await handleToolCommand(command, input);
4096
+ return {
4097
+ content: result.content.map((c) => ({
4098
+ type: "text",
4099
+ text: c.text
4100
+ })),
4101
+ isError: result.isError
4102
+ };
3383
4103
  });
3384
4104
  const transport = new StdioServerTransport();
3385
4105
  await server.connect(transport);
@@ -3474,9 +4194,12 @@ async function tryReloadDaemon() {
3474
4194
  await sendRequest("config/reload", {});
3475
4195
  } catch {}
3476
4196
  }
3477
- const mcpCommand = new Command("mcp").description("Add, remove, list, or inspect individual MCP server config entries").enablePositionalOptions().action(async (_opts, cmd) => {
4197
+ 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
4198
  const explicitConfig = cmd.parent?.opts().config;
3479
- await startMcpProxy(explicitConfig);
4199
+ await startMcpProxy({
4200
+ configPath: explicitConfig,
4201
+ proxyTools: opts.proxyTools
4202
+ });
3480
4203
  });
3481
4204
  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
4205
  const explicitConfig = getExplicitConfig(mcpCommand);
@@ -3489,6 +4212,11 @@ mcpCommand.command("add").description("Add an MCP server").passThroughOptions().
3489
4212
  resolvedSecret
3490
4213
  }));
3491
4214
  await tryReloadDaemon();
4215
+ capture("server_added", {
4216
+ server: name,
4217
+ scope,
4218
+ updated: result.existed
4219
+ });
3492
4220
  if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
3493
4221
  else console.log(`Added "${name}" to ${scope} config (${configPath})`);
3494
4222
  });
@@ -3511,6 +4239,11 @@ mcpCommand.command("add-json").description("Add an MCP server from a JSON config
3511
4239
  }
3512
4240
  const result = addServer(configPath, name, serverConfig);
3513
4241
  await tryReloadDaemon();
4242
+ capture("server_added", {
4243
+ server: name,
4244
+ scope,
4245
+ updated: result.existed
4246
+ });
3514
4247
  if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
3515
4248
  else console.log(`Added "${name}" to ${scope} config (${configPath})`);
3516
4249
  });
@@ -3532,7 +4265,13 @@ mcpCommand.command("add-from-claude-desktop").description("Import MCP servers fr
3532
4265
  writeMuxedConfig(configPath, { ...result.merged });
3533
4266
  await tryReloadDaemon();
3534
4267
  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(", ")}`);
4268
+ if (result.imported.length > 0) {
4269
+ capture("servers_imported", {
4270
+ servers: result.imported,
4271
+ source: "claude-desktop"
4272
+ });
4273
+ console.log(`Imported ${result.imported.length} server(s) from Claude Desktop: ${result.imported.join(", ")}`);
4274
+ }
3536
4275
  if (result.skipped.length > 0) console.log(`Skipped ${result.skipped.length} (already existed): ${result.skipped.join(", ")}`);
3537
4276
  if (result.imported.length === 0 && result.skipped.length === 0) console.log("No servers found in Claude Desktop config.");
3538
4277
  });
@@ -3586,6 +4325,10 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
3586
4325
  const configPath = getConfigPath(scope, explicitConfig);
3587
4326
  if (removeServer(configPath, name).removed) {
3588
4327
  await tryReloadDaemon();
4328
+ capture("server_removed", {
4329
+ server: name,
4330
+ scope
4331
+ });
3589
4332
  console.log(`Removed "${name}" from ${scope} config (${configPath})`);
3590
4333
  } else {
3591
4334
  console.error(`Server "${name}" not found in ${scope} config.`);
@@ -3596,12 +4339,20 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
3596
4339
  const localPath = getConfigPath("local", explicitConfig);
3597
4340
  if (removeServer(localPath, name).removed) {
3598
4341
  await tryReloadDaemon();
4342
+ capture("server_removed", {
4343
+ server: name,
4344
+ scope: "local"
4345
+ });
3599
4346
  console.log(`Removed "${name}" from local config (${localPath})`);
3600
4347
  return;
3601
4348
  }
3602
4349
  const globalPath = getConfigPath("global", explicitConfig);
3603
4350
  if (removeServer(globalPath, name).removed) {
3604
4351
  await tryReloadDaemon();
4352
+ capture("server_removed", {
4353
+ server: name,
4354
+ scope: "global"
4355
+ });
3605
4356
  console.log(`Removed "${name}" from global config (${globalPath})`);
3606
4357
  return;
3607
4358
  }
@@ -3618,7 +4369,26 @@ const typegenCommand = new Command("typegen").description("Generate TypeScript t
3618
4369
  fs.writeFileSync(outputPath, content, "utf-8");
3619
4370
  console.log(`Generated ${tools.length} tool types → ${outputPath}`);
3620
4371
  });
3621
- function runCli() {
4372
+ const telemetryCommand = new Command("telemetry").description("Manage anonymous telemetry (on, off, status)").argument("[action]", "on | off | status (default: status)").action((action) => {
4373
+ switch (action) {
4374
+ case "on":
4375
+ setTelemetryEnabled(true);
4376
+ console.log("Telemetry enabled.");
4377
+ break;
4378
+ case "off":
4379
+ setTelemetryEnabled(false);
4380
+ console.log("Telemetry disabled.");
4381
+ break;
4382
+ case "status":
4383
+ case void 0:
4384
+ console.log(`Telemetry is ${getTelemetryStatus()}.`);
4385
+ break;
4386
+ default:
4387
+ console.error(`Unknown action: ${action}. Use on, off, or status.`);
4388
+ process.exit(1);
4389
+ }
4390
+ });
4391
+ async function runCli() {
3622
4392
  const program = new Command();
3623
4393
  program.name("muxed").description("The optimization layer for MCP").version("0.1.0");
3624
4394
  program.enablePositionalOptions();
@@ -3646,9 +4416,16 @@ function runCli() {
3646
4416
  program.addCommand(initCommand);
3647
4417
  program.addCommand(mcpCommand);
3648
4418
  program.addCommand(typegenCommand);
4419
+ program.addCommand(telemetryCommand);
3649
4420
  program.commandsGroup("Daemon:");
3650
4421
  program.addCommand(daemonCommand);
3651
- program.parse();
4422
+ const command = process.argv[2];
4423
+ capture("session_started", { command: command ?? null });
4424
+ try {
4425
+ await program.parseAsync();
4426
+ } finally {
4427
+ await shutdown();
4428
+ }
3652
4429
  }
3653
4430
  if (process.argv.indexOf("--daemon") !== -1) {
3654
4431
  const configIndex = process.argv.indexOf("--config");
@@ -3656,5 +4433,8 @@ if (process.argv.indexOf("--daemon") !== -1) {
3656
4433
  console.error("Failed to start daemon:", err);
3657
4434
  process.exit(1);
3658
4435
  });
3659
- } else runCli();
4436
+ } else runCli().catch((err) => {
4437
+ console.error(err instanceof Error ? err.message : "Unexpected error");
4438
+ process.exit(1);
4439
+ });
3660
4440
  export {};