muxed 0.1.0 → 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({
@@ -84,7 +89,7 @@ const DaemonConfigSchema = z.object({
84
89
  shutdownTimeout: z.number().optional(),
85
90
  http: HttpListenerSchema.optional()
86
91
  });
87
- const TooldConfigSchema = z.object({
92
+ const MuxedConfigSchema = z.object({
88
93
  mcpServers: z.record(z.string(), ServerConfigSchema),
89
94
  daemon: DaemonConfigSchema.optional(),
90
95
  mergeClaudeConfig: z.boolean().optional()
@@ -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,14 +127,14 @@ const DAEMON_DEFAULTS = {
122
127
  shutdownTimeout: 1e4
123
128
  };
124
129
  function getGlobalConfigPath() {
125
- return path.join(os.homedir(), ".config", "toold", "config.json");
130
+ return path.join(os.homedir(), ".muxed", "config.json");
126
131
  }
127
132
  function findConfigFile(configPath) {
128
133
  if (configPath) {
129
134
  if (!fs.existsSync(configPath)) throw new Error(`Config file not found: ${configPath}`);
130
135
  return configPath;
131
136
  }
132
- const cwdConfig = path.join(process.cwd(), "toold.config.json");
137
+ const cwdConfig = path.join(process.cwd(), "muxed.config.json");
133
138
  if (fs.existsSync(cwdConfig)) return cwdConfig;
134
139
  const homeConfig = getGlobalConfigPath();
135
140
  if (fs.existsSync(homeConfig)) return homeConfig;
@@ -148,7 +153,7 @@ function loadConfig(configPath) {
148
153
  const globalConfigPath = getGlobalConfigPath();
149
154
  if ((!filePath || path.resolve(filePath) !== path.resolve(globalConfigPath)) && fs.existsSync(globalConfigPath)) try {
150
155
  const globalRaw = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
151
- const globalResult = TooldConfigSchema.safeParse(globalRaw);
156
+ const globalResult = MuxedConfigSchema.safeParse(globalRaw);
152
157
  if (globalResult.success) config.mcpServers = {
153
158
  ...globalResult.data.mcpServers,
154
159
  ...config.mcpServers
@@ -176,7 +181,7 @@ function parseConfigFile(filePath) {
176
181
  } catch {
177
182
  throw new Error(`Invalid JSON in config file: ${filePath}`);
178
183
  }
179
- const result = TooldConfigSchema.safeParse(parsed);
184
+ const result = MuxedConfigSchema.safeParse(parsed);
180
185
  if (!result.success) throw new Error(`Invalid config: ${z.prettifyError(result.error)}`);
181
186
  return result.data;
182
187
  }
@@ -187,26 +192,26 @@ function isHttpConfig(config) {
187
192
  return "url" in config;
188
193
  }
189
194
  var paths_exports = /* @__PURE__ */ __exportAll({
190
- ensureTooldDir: () => ensureTooldDir,
195
+ ensureMuxedDir: () => ensureMuxedDir,
191
196
  getLogPath: () => getLogPath,
197
+ getMuxedDir: () => getMuxedDir,
192
198
  getPidPath: () => getPidPath,
193
- getSocketPath: () => getSocketPath,
194
- getTooldDir: () => getTooldDir
199
+ getSocketPath: () => getSocketPath
195
200
  });
196
- function getTooldDir() {
197
- return path.join(os.homedir(), ".toold");
201
+ function getMuxedDir() {
202
+ return path.join(os.homedir(), ".muxed");
198
203
  }
199
204
  function getSocketPath() {
200
- return path.join(getTooldDir(), "toold.sock");
205
+ return path.join(getMuxedDir(), "muxed.sock");
201
206
  }
202
207
  function getPidPath() {
203
- return path.join(getTooldDir(), "toold.pid");
208
+ return path.join(getMuxedDir(), "muxed.pid");
204
209
  }
205
210
  function getLogPath() {
206
- return path.join(getTooldDir(), "toold.log");
211
+ return path.join(getMuxedDir(), "muxed.log");
207
212
  }
208
- function ensureTooldDir() {
209
- fs.mkdirSync(getTooldDir(), { recursive: true });
213
+ function ensureMuxedDir() {
214
+ fs.mkdirSync(getMuxedDir(), { recursive: true });
210
215
  }
211
216
  const LOG_LEVELS = {
212
217
  debug: 0,
@@ -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
- return path.join(getTooldDir(), "auth");
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() {
@@ -388,7 +395,7 @@ var ClientCredentialsProvider = class {
388
395
  token_endpoint_auth_method: "client_secret_basic",
389
396
  grant_types: ["client_credentials"],
390
397
  response_types: [],
391
- client_name: "toold"
398
+ client_name: "muxed"
392
399
  };
393
400
  }
394
401
  clientInformation() {
@@ -443,7 +450,7 @@ function openBrowser(url) {
443
450
  openUrl(url);
444
451
  }
445
452
  async function notifyReauth(serverName, authUrl) {
446
- const title = "toold";
453
+ const title = "muxed";
447
454
  const message = `Server "${serverName}" needs re-authorization`;
448
455
  const platform = process.platform;
449
456
  if (platform === "darwin") {
@@ -485,7 +492,7 @@ var AuthorizationCodeProvider = class {
485
492
  token_endpoint_auth_method: this.config.clientSecret ? "client_secret_basic" : "none",
486
493
  grant_types: ["authorization_code", "refresh_token"],
487
494
  response_types: ["code"],
488
- client_name: "toold",
495
+ client_name: "muxed",
489
496
  scope: this.config.scope
490
497
  };
491
498
  }
@@ -542,13 +549,13 @@ var AuthorizationCodeProvider = class {
542
549
  }
543
550
  };
544
551
  const SUCCESS_HTML = `<!DOCTYPE html>
545
- <html><head><title>toold - Authorization Successful</title></head>
552
+ <html><head><title>muxed - Authorization Successful</title></head>
546
553
  <body style="font-family:system-ui;text-align:center;padding:60px">
547
554
  <h1>Authorization Successful</h1>
548
- <p>You can close this tab and return to toold.</p>
555
+ <p>You can close this tab and return to muxed.</p>
549
556
  </body></html>`;
550
557
  const ERROR_HTML = (msg) => `<!DOCTYPE html>
551
- <html><head><title>toold - Authorization Error</title></head>
558
+ <html><head><title>muxed - Authorization Error</title></head>
552
559
  <body style="font-family:system-ui;text-align:center;padding:60px">
553
560
  <h1>Authorization Error</h1>
554
561
  <p>${msg}</p>
@@ -761,7 +768,7 @@ var ServerManager = class {
761
768
  }
762
769
  createClient() {
763
770
  const client = new Client({
764
- name: "toold",
771
+ name: "muxed",
765
772
  version: "0.1.0"
766
773
  }, {
767
774
  capabilities: { tasks: {
@@ -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");
@@ -1318,11 +1510,11 @@ async function generateTypes(tools) {
1318
1510
  const jsdoc = tool.description ? ` /** ${escapeJsdoc(tool.description)} */\n` : "";
1319
1511
  toolTypes.push(`${jsdoc} '${qualifiedName}': {\n input: ${indent(inputType, 6)};\n output: ${indent(outputType, 6)};\n };`);
1320
1512
  }
1321
- return `// Auto-generated by \`toold typegen\` – do not edit
1322
- // Run \`toold typegen\` to regenerate
1513
+ return `// Auto-generated by \`muxed typegen\` – do not edit
1514
+ // Run \`muxed typegen\` to regenerate
1323
1515
 
1324
- declare module 'toold' {
1325
- interface TooldToolMap {
1516
+ declare module 'muxed' {
1517
+ interface MuxedToolMap {
1326
1518
  ${toolTypes.join("\n")}
1327
1519
  }
1328
1520
  }
@@ -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);
@@ -1874,8 +2249,8 @@ function createHttpListener(handleRequest, config) {
1874
2249
  async function runAutoTypegen(serverPool, logger) {
1875
2250
  try {
1876
2251
  const require = createRequire(path.resolve("package.json"));
1877
- const tooldPkgDir = path.dirname(require.resolve("toold/package.json"));
1878
- const outputPath = path.join(tooldPkgDir, "toold.generated.d.ts");
2252
+ const muxedPkgDir = path.dirname(require.resolve("muxed/package.json"));
2253
+ const outputPath = path.join(muxedPkgDir, "muxed.generated.d.ts");
1879
2254
  const tools = serverPool.listAllTools();
1880
2255
  if (tools.length === 0) {
1881
2256
  logger.debug("No tools available, skipping typegen");
@@ -1885,12 +2260,12 @@ async function runAutoTypegen(serverPool, logger) {
1885
2260
  fs.writeFileSync(outputPath, content, "utf-8");
1886
2261
  logger.info(`Auto-generated ${tools.length} tool types → ${outputPath}`);
1887
2262
  } catch {
1888
- logger.debug("Skipping auto-typegen (toold not resolvable as a dependency)");
2263
+ logger.debug("Skipping auto-typegen (muxed not resolvable as a dependency)");
1889
2264
  }
1890
2265
  }
1891
2266
  async function startDaemon(configPath) {
1892
2267
  const config = loadConfig(configPath);
1893
- ensureTooldDir();
2268
+ ensureMuxedDir();
1894
2269
  const isForeground = !!process.send;
1895
2270
  const logger = initLogger({
1896
2271
  level: config.daemon?.logLevel ?? "info",
@@ -1946,7 +2321,7 @@ async function startDaemon(configPath) {
1946
2321
  });
1947
2322
  }
1948
2323
  function getLockPath() {
1949
- return path.join(getTooldDir(), "toold.lock");
2324
+ return path.join(getMuxedDir(), "muxed.lock");
1950
2325
  }
1951
2326
  function getDaemonPid() {
1952
2327
  try {
@@ -1965,10 +2340,10 @@ function isProcessAlive(pid) {
1965
2340
  return false;
1966
2341
  }
1967
2342
  }
1968
- function isTooldProcess(pid) {
2343
+ function isMuxedProcess(pid) {
1969
2344
  try {
1970
2345
  const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
1971
- return cmdline.includes("toold") || cmdline.includes("node");
2346
+ return cmdline.includes("muxed") || cmdline.includes("node");
1972
2347
  } catch {
1973
2348
  return isProcessAlive(pid);
1974
2349
  }
@@ -1994,13 +2369,13 @@ function tryConnectSocket(socketPath) {
1994
2369
  function acquireLock() {
1995
2370
  const lockPath = getLockPath();
1996
2371
  try {
1997
- ensureTooldDir();
2372
+ ensureMuxedDir();
1998
2373
  fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
1999
2374
  return true;
2000
2375
  } catch (err) {
2001
2376
  if (err.code === "EEXIST") try {
2002
2377
  const lockPid = parseInt(fs.readFileSync(lockPath, "utf-8").trim(), 10);
2003
- if (!Number.isFinite(lockPid) || !isProcessAlive(lockPid) || !isTooldProcess(lockPid)) {
2378
+ if (!Number.isFinite(lockPid) || !isProcessAlive(lockPid) || !isMuxedProcess(lockPid)) {
2004
2379
  fs.unlinkSync(lockPath);
2005
2380
  try {
2006
2381
  fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
@@ -2026,7 +2401,7 @@ async function isDaemonRunning() {
2026
2401
  const pid = getDaemonPid();
2027
2402
  if (pid === null) return false;
2028
2403
  if (!isProcessAlive(pid)) return false;
2029
- if (!isTooldProcess(pid)) return false;
2404
+ if (!isMuxedProcess(pid)) return false;
2030
2405
  return tryConnectSocket(getSocketPath());
2031
2406
  }
2032
2407
  async function cleanupStaleFiles() {
@@ -2043,7 +2418,7 @@ async function cleanupStaleFiles() {
2043
2418
  releaseLock();
2044
2419
  return;
2045
2420
  }
2046
- if (pid !== null && isProcessAlive(pid) && !isTooldProcess(pid)) {
2421
+ if (pid !== null && isProcessAlive(pid) && !isMuxedProcess(pid)) {
2047
2422
  try {
2048
2423
  fs.unlinkSync(pidPath);
2049
2424
  } catch {}
@@ -2108,12 +2483,12 @@ async function daemonize(configPath) {
2108
2483
  releaseLock();
2109
2484
  }
2110
2485
  }
2111
- var TooldError = class extends Error {
2486
+ var MuxedError = class extends Error {
2112
2487
  code;
2113
2488
  data;
2114
2489
  constructor(code, message, data) {
2115
2490
  super(message);
2116
- this.name = "TooldError";
2491
+ this.name = "MuxedError";
2117
2492
  this.code = code;
2118
2493
  this.data = data;
2119
2494
  }
@@ -2156,7 +2531,7 @@ async function sendRequest(method, params) {
2156
2531
  const socket = net.createConnection(socketPath);
2157
2532
  let buffer = "";
2158
2533
  socket.on("error", (err) => {
2159
- if (err.code === "ENOENT") reject(/* @__PURE__ */ new Error("Daemon is not running. Run `toold status` to check."));
2534
+ if (err.code === "ENOENT") reject(/* @__PURE__ */ new Error("Daemon is not running. Run `muxed status` to check."));
2160
2535
  else if (err.code === "ECONNREFUSED") reject(/* @__PURE__ */ new Error("Daemon may have crashed. Try running a command to auto-restart it."));
2161
2536
  else reject(err);
2162
2537
  });
@@ -2177,7 +2552,7 @@ async function sendRequest(method, params) {
2177
2552
  socket.destroy();
2178
2553
  try {
2179
2554
  const response = JSON.parse(line);
2180
- if (response.error) reject(new TooldError(response.error.code, response.error.message, response.error.data));
2555
+ if (response.error) reject(new MuxedError(response.error.code, response.error.message, response.error.data));
2181
2556
  else resolve(response.result);
2182
2557
  } catch {
2183
2558
  reject(/* @__PURE__ */ new Error("Invalid response from daemon"));
@@ -2431,7 +2806,7 @@ function formatInit(result) {
2431
2806
  lines.push(formatTable(discHeaders, discRows));
2432
2807
  if (result.imported.length > 0) {
2433
2808
  lines.push("");
2434
- lines.push(`Imported ${result.imported.length} server(s) into ${result.tooldConfigPath}:`);
2809
+ lines.push(`Imported ${result.imported.length} server(s) into ${result.muxedConfigPath}:`);
2435
2810
  lines.push(` ${result.imported.join(", ")}`);
2436
2811
  }
2437
2812
  if (result.skipped.length > 0) {
@@ -2460,7 +2835,7 @@ function formatInit(result) {
2460
2835
  }
2461
2836
  if (result.imported.length === 0 && result.skipped.length > 0) {
2462
2837
  lines.push("");
2463
- lines.push("All discovered servers already exist in toold config. Nothing to do.");
2838
+ lines.push("All discovered servers already exist in muxed config. Nothing to do.");
2464
2839
  }
2465
2840
  return lines.join("\n");
2466
2841
  }
@@ -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) => {
@@ -2637,7 +3206,7 @@ const readCommand = new Command("read").description("Fetch and display the conte
2637
3206
  function getExplicitConfig$1(cmd) {
2638
3207
  return cmd.parent?.parent?.opts().config;
2639
3208
  }
2640
- const daemonCommand = new Command("daemon").description("Start, stop, reload, or check status of the toold background daemon").enablePositionalOptions();
3209
+ const daemonCommand = new Command("daemon").description("Start, stop, reload, or check status of the muxed background daemon").enablePositionalOptions();
2641
3210
  daemonCommand.command("start").description("Start the daemon process in the background").option("--json", "Output as JSON").action(async (opts) => {
2642
3211
  const configPath = getExplicitConfig$1(daemonCommand);
2643
3212
  if (await isDaemonRunning()) {
@@ -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,20 +3499,22 @@ 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
  }
2933
3514
  function normalizeServer(agent, name, raw, warnings) {
2934
3515
  const env = raw.env;
2935
3516
  if (env) {
2936
- for (const [key, value] of Object.entries(env)) if (typeof value === "string" && value.includes("${input:")) warnings.push(`${agent.name} server "${name}": env.${key} references ${value} \u2014 set manually in toold config`);
3517
+ for (const [key, value] of Object.entries(env)) if (typeof value === "string" && value.includes("${input:")) warnings.push(`${agent.name} server "${name}": env.${key} references ${value} \u2014 set manually in muxed config`);
2937
3518
  }
2938
3519
  const url = raw.url ?? raw.serverUrl;
2939
3520
  const command = raw.command;
@@ -2978,7 +3559,7 @@ function discoverAgentConfigs() {
2978
3559
  const servers = {};
2979
3560
  for (const [name, raw] of Object.entries(rawServers)) {
2980
3561
  if (typeof raw !== "object" || raw === null) continue;
2981
- if (name === "toold") continue;
3562
+ if (name === "muxed") continue;
2982
3563
  const normalized = normalizeServer(agent, name, raw, warnings);
2983
3564
  if (normalized) servers[name] = normalized;
2984
3565
  }
@@ -3033,7 +3614,7 @@ function mergeServers(discovered, existingServers) {
3033
3614
  function detectIndent(text) {
3034
3615
  return text.match(/^(\s+)"/m)?.[1] ?? " ";
3035
3616
  }
3036
- function writeTooldConfig(configPath, servers) {
3617
+ function writeMuxedConfig(configPath, servers) {
3037
3618
  let existing = {};
3038
3619
  if (fs.existsSync(configPath)) try {
3039
3620
  existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
@@ -3043,15 +3624,20 @@ function writeTooldConfig(configPath, servers) {
3043
3624
  existing.mcpServers = servers;
3044
3625
  fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
3045
3626
  }
3046
- function getTooldEntry(agent) {
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: ["toold@latest", "proxy"]
3636
+ args
3051
3637
  };
3052
3638
  return {
3053
3639
  command: "npx",
3054
- args: ["toold@latest", "proxy"]
3640
+ args
3055
3641
  };
3056
3642
  }
3057
3643
  function modifyAgentConfig(dc, opts) {
@@ -3059,14 +3645,14 @@ function modifyAgentConfig(dc, opts) {
3059
3645
  fs.writeFileSync(dc.configPath + ".bak", text);
3060
3646
  const indent = detectIndent(text);
3061
3647
  const content = { ...dc.rawContent };
3062
- if (opts.delete) if (opts.replace) content[dc.agent.serversKey] = { toold: getTooldEntry(dc.agent) };
3648
+ if (opts.delete) if (opts.replace) content[dc.agent.serversKey] = { muxed: getMuxedEntry(dc.agent) };
3063
3649
  else delete content[dc.agent.serversKey];
3064
3650
  fs.writeFileSync(dc.configPath, JSON.stringify(content, null, indent) + "\n");
3065
3651
  }
3066
- function getTooldConfigPath(scope, explicitPath) {
3652
+ function getMuxedConfigPath(scope, explicitPath) {
3067
3653
  if (explicitPath) return explicitPath;
3068
- if (scope === "local") return path.join(process.cwd(), "toold.config.json");
3069
- return path.join(home, ".config", "toold", "config.json");
3654
+ if (scope === "local") return path.join(process.cwd(), "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({
@@ -3135,7 +3721,7 @@ async function resolveConflicts(unresolvedConflicts, isInteractive) {
3135
3721
  conflicts
3136
3722
  };
3137
3723
  }
3138
- const initCommand = new Command("init").description("Discover and import MCP servers from agent configs (Claude Code, Cursor)").option("--dry-run", "Show what would be done without writing files").option("--json", "Output as JSON").option("-y, --yes", "Skip prompts; resolve conflicts by priority (claude-code > cursor > first)").option("--no-delete", "Keep original server entries in agent configs").option("--no-replace", "Don't add toold entry to agent configs").action(async (opts) => {
3724
+ const initCommand = new Command("init").description("Discover and import MCP servers from agent configs (Claude Code, Cursor)").option("--dry-run", "Show what would be done without writing files").option("--json", "Output as JSON").option("-y, --yes", "Skip prompts; resolve conflicts by priority (claude-code > cursor > first)").option("--no-delete", "Keep original server entries in agent configs").option("--no-replace", "Don't add muxed entry to agent configs").action(async (opts) => {
3139
3725
  const configPath = initCommand.parent?.opts().config;
3140
3726
  const isInteractive = !opts.dryRun && !opts.json && !opts.yes && !!process.stdin.isTTY;
3141
3727
  const { discovered, warnings } = discoverAgentConfigs();
@@ -3144,10 +3730,10 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3144
3730
  console.log(opts.json ? formatJson({ message: msg }) : msg);
3145
3731
  return;
3146
3732
  }
3147
- const tooldPath = getTooldConfigPath(discovered.some((d) => d.agent.scope === "local") ? "local" : "global", configPath);
3733
+ const muxedPath = getMuxedConfigPath(discovered.some((d) => d.agent.scope === "local") ? "local" : "global", configPath);
3148
3734
  let existingServers = {};
3149
- if (fs.existsSync(tooldPath)) try {
3150
- existingServers = JSON.parse(fs.readFileSync(tooldPath, "utf-8")).mcpServers ?? {};
3735
+ if (fs.existsSync(muxedPath)) try {
3736
+ existingServers = JSON.parse(fs.readFileSync(muxedPath, "utf-8")).mcpServers ?? {};
3151
3737
  } catch {}
3152
3738
  const result = mergeServers(discovered, existingServers);
3153
3739
  const { resolved, conflicts } = await resolveConflicts(result.unresolvedConflicts, isInteractive);
@@ -3156,7 +3742,7 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3156
3742
  result.merged[name] = config;
3157
3743
  imported.push(name);
3158
3744
  }
3159
- if (!opts.dryRun && imported.length > 0) writeTooldConfig(tooldPath, result.merged);
3745
+ if (!opts.dryRun && imported.length > 0) writeMuxedConfig(muxedPath, result.merged);
3160
3746
  const modifiedFiles = [];
3161
3747
  const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : opts.delete;
3162
3748
  if (!opts.dryRun && shouldDelete) for (const dc of discovered) try {
@@ -3180,13 +3766,20 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3180
3766
  conflicts,
3181
3767
  warnings,
3182
3768
  modifiedFiles,
3183
- tooldConfigPath: tooldPath,
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) {
3189
- return getTooldConfigPath(scope, explicitPath);
3782
+ return getMuxedConfigPath(scope, explicitPath);
3190
3783
  }
3191
3784
  function readConfigFile(filePath) {
3192
3785
  if (!fs.existsSync(filePath)) return { mcpServers: {} };
@@ -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 toold\` 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 toold grep <pattern>' or 'npx toold tools'.
3243
- 2. You MUST call 'npx toold info <server>/<tool>' BEFORE ANY 'npx toold 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 toold call without checking the schema first.
3248
- **ALWAYS** run npx toold 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 toold info' for ALL tools in parallel FIRST, then make your 'npx toold 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 toold grep <pattern> # Search tool names and descriptions
3266
- npx toold 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 toold 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 toold call <server>/<tool> '<json>' # Only run AFTER npx toold info
3273
- npx toold call <server>/<tool> - # Invoke with JSON from stdin (AFTER npx toold 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 toold servers # List all connected MCP servers
3277
- npx toold tools [server] # List available tools (optionally filter by server)
3278
- npx toold grep <pattern> # Search tool names and descriptions
3279
- npx toold resources [server] # List MCP resources
3280
- npx toold 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 toold grep "slack/*search*"\` to search for tools related to slack search.
3288
- [Calls npx toold grep "slack/*search*"]
3289
- Assistant: I need to check the schema first. Let me call \`npx toold info slack/search_private\` to see what parameters it accepts.
3290
- [Calls npx toold 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 toold 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 toold grep "database/*query*"\` and \`npx toold grep "email/*send*"\` to search for tools related to database query and email send.
3298
- [Calls npx toold grep "database/*query*" & npx toold 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 toold info database/query and npx toold 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 toold 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 toold 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 toold 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 toold tools email]
3942
+ [Calls ${f.tools("email")}]
3313
3943
  Assistant: Let me check the schema first.
3314
- [Calls npx toold 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 toold 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 toold call slack/search_private with guessed parameters]
3324
- WRONG - You must call npx toold 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 toold call slack/search_private directly]
3331
- WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call npx toold 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 toold call commands in parallel without any npx toold info calls first]
3337
- WRONG - You must call npx toold info for ALL tools before making ANY npx toold 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 toold tools # See all available MCP tools
3344
- npx toold 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 toold 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 toold call weather/get_location '{}'
3980
+ ${f.call("weather/get_location", "{}")}
3351
3981
 
3352
3982
  # Tool call with parameters
3353
- npx toold 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 toold 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 toold -h\` to see all available commands.
3992
+ Call \`${f.help()}\` to see all available commands.
3366
3993
 
3367
- Below are the instructions for the connected MCP servers in toold.
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
- name: "toold",
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
  });
@@ -3529,10 +4251,16 @@ mcpCommand.command("add-from-claude-desktop").description("Import MCP servers fr
3529
4251
  existingServers = JSON.parse(fs.readFileSync(configPath, "utf-8")).mcpServers ?? {};
3530
4252
  } catch {}
3531
4253
  const result = mergeServers(claudeDesktop, existingServers);
3532
- writeTooldConfig(configPath, { ...result.merged });
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,31 +4328,58 @@ 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
  }
3608
4348
  console.error(`Server "${name}" not found in local or global config.`);
3609
4349
  process.exitCode = 1;
3610
4350
  });
3611
- const typegenCommand = new Command("typegen").description("Generate TypeScript types from tool schemas for type-safe tool calls").option("-c, --config <path>", "Path to toold.config.json").action(async (opts) => {
4351
+ const typegenCommand = new Command("typegen").description("Generate TypeScript types from tool schemas for type-safe tool calls").option("-c, --config <path>", "Path to muxed.config.json").action(async (opts) => {
3612
4352
  await ensureDaemon(typegenCommand.parent?.opts().config ?? opts.config);
3613
4353
  const tools = await sendRequest("tools/list");
3614
4354
  const content = await generateTypes(tools);
3615
4355
  const require = createRequire(path.resolve("package.json"));
3616
- const tooldPkgDir = path.dirname(require.resolve("toold/package.json"));
3617
- const outputPath = path.join(tooldPkgDir, "toold.generated.d.ts");
4356
+ const muxedPkgDir = path.dirname(require.resolve("muxed/package.json"));
4357
+ const outputPath = path.join(muxedPkgDir, "muxed.generated.d.ts");
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
- program.name("toold").description("The optimization layer for MCP").version("0.1.0");
4382
+ program.name("muxed").description("The optimization layer for MCP").version("0.1.0");
3624
4383
  program.enablePositionalOptions();
3625
4384
  program.option("--config <path>", "Path to config file");
3626
4385
  program.commandsGroup("Servers:");
@@ -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 {};