muxed 0.2.1 → 0.2.3

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
@@ -11,12 +11,13 @@ import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontex
11
11
  import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
12
12
  import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js";
13
13
  import crypto from "node:crypto";
14
- import { execFile, fork } from "node:child_process";
14
+ import { execFile, execSync, fork } from "node:child_process";
15
15
  import http from "node:http";
16
16
  import { compile } from "json-schema-to-typescript";
17
17
  import net from "node:net";
18
18
  import { Command } from "commander";
19
19
  import { PostHog } from "posthog-node";
20
+ import { fileURLToPath } from "node:url";
20
21
  import * as readline from "node:readline/promises";
21
22
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
22
23
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -77,6 +78,7 @@ const DaemonConfigSchema = z.object({
77
78
  connectTimeout: z.number().optional(),
78
79
  requestTimeout: z.number().optional(),
79
80
  healthCheckInterval: z.number().optional(),
81
+ healthCheckTimeout: z.number().optional(),
80
82
  maxRestartAttempts: z.number().optional(),
81
83
  maxTotalTimeout: z.number().optional(),
82
84
  taskExpiryTimeout: z.number().optional(),
@@ -120,6 +122,7 @@ const DAEMON_DEFAULTS = {
120
122
  connectTimeout: 3e4,
121
123
  requestTimeout: 3e4,
122
124
  healthCheckInterval: 3e4,
125
+ healthCheckTimeout: 1e4,
123
126
  maxRestartAttempts: -1,
124
127
  maxTotalTimeout: 3e5,
125
128
  taskExpiryTimeout: 36e5,
@@ -698,6 +701,7 @@ var ServerManager = class {
698
701
  stopped = false;
699
702
  connectTimeout;
700
703
  healthCheckInterval;
704
+ healthCheckTimeout;
701
705
  maxRestartAttempts;
702
706
  onHealthChange;
703
707
  constructor(name, config, options) {
@@ -705,6 +709,7 @@ var ServerManager = class {
705
709
  this.config = config;
706
710
  this.connectTimeout = options?.connectTimeout;
707
711
  this.healthCheckInterval = options?.healthCheckInterval ?? 3e4;
712
+ this.healthCheckTimeout = options?.healthCheckTimeout ?? 1e4;
708
713
  this.maxRestartAttempts = options?.maxRestartAttempts ?? -1;
709
714
  }
710
715
  setHealthCallback(cb) {
@@ -911,7 +916,7 @@ var ServerManager = class {
911
916
  if (!this.client || this.status !== "connected") return;
912
917
  this.lastHealthCheck = /* @__PURE__ */ new Date();
913
918
  try {
914
- await this.client.ping();
919
+ await this.client.ping({ timeout: this.healthCheckTimeout });
915
920
  if (this.consecutiveFailures > 0) getLogger().info(`Health check recovered after ${this.consecutiveFailures} failures`, this.name);
916
921
  this.consecutiveFailures = 0;
917
922
  } catch (err) {
@@ -966,10 +971,14 @@ var ServerManager = class {
966
971
  if (!this.client) return;
967
972
  this.tools = (await this.client.listTools()).tools;
968
973
  }
974
+ ensureConnected() {
975
+ if (!this.client || this.status !== "connected") throw new Error(`Server "${this.name}" is not connected`);
976
+ return this.client;
977
+ }
969
978
  async callTool(name, args, timeout) {
970
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
979
+ const client = this.ensureConnected();
971
980
  const options = timeout ? { signal: AbortSignal.timeout(timeout) } : void 0;
972
- return await this.client.callTool({
981
+ return await client.callTool({
973
982
  name,
974
983
  arguments: args
975
984
  }, void 0, options);
@@ -982,8 +991,7 @@ var ServerManager = class {
982
991
  this.resources = (await this.client.listResources()).resources;
983
992
  }
984
993
  async readResource(uri) {
985
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
986
- return await this.client.readResource({ uri });
994
+ return await this.ensureConnected().readResource({ uri });
987
995
  }
988
996
  listPrompts() {
989
997
  return this.prompts;
@@ -993,40 +1001,35 @@ var ServerManager = class {
993
1001
  this.prompts = (await this.client.listPrompts()).prompts;
994
1002
  }
995
1003
  async getPrompt(name, args) {
996
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
997
- return await this.client.getPrompt({
1004
+ return await this.ensureConnected().getPrompt({
998
1005
  name,
999
1006
  arguments: args
1000
1007
  });
1001
1008
  }
1002
1009
  async complete(ref, argument) {
1003
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1010
+ const client = this.ensureConnected();
1004
1011
  if (!this.capabilities?.completions) throw new Error(`Server "${this.name}" does not support completions`);
1005
- return await this.client.complete({
1012
+ return await client.complete({
1006
1013
  ref,
1007
1014
  argument
1008
1015
  });
1009
1016
  }
1010
1017
  async listTasks(cursor) {
1011
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1018
+ const client = this.ensureConnected();
1012
1019
  if (!this.capabilities?.experimental?.tasks) throw new Error(`Server "${this.name}" does not support tasks`);
1013
- return await this.client.experimental.tasks.listTasks(cursor);
1020
+ return await client.experimental.tasks.listTasks(cursor);
1014
1021
  }
1015
1022
  async getTask(taskId) {
1016
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1017
- return await this.client.experimental.tasks.getTask(taskId);
1023
+ return await this.ensureConnected().experimental.tasks.getTask(taskId);
1018
1024
  }
1019
1025
  async getTaskResult(taskId) {
1020
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1021
- return await this.client.experimental.tasks.getTaskResult(taskId);
1026
+ return await this.ensureConnected().experimental.tasks.getTaskResult(taskId);
1022
1027
  }
1023
1028
  async cancelTask(taskId) {
1024
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1025
- return await this.client.experimental.tasks.cancelTask(taskId);
1029
+ return await this.ensureConnected().experimental.tasks.cancelTask(taskId);
1026
1030
  }
1027
1031
  async callToolWithTask(name, args) {
1028
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1029
- const stream = this.client.experimental.tasks.callToolStream({
1032
+ const stream = this.ensureConnected().experimental.tasks.callToolStream({
1030
1033
  name,
1031
1034
  arguments: args
1032
1035
  });
@@ -1180,6 +1183,7 @@ var ServerPool = class {
1180
1183
  const manager = new ServerManager(name, serverConfig, {
1181
1184
  connectTimeout: config.daemon?.connectTimeout,
1182
1185
  healthCheckInterval: config.daemon?.healthCheckInterval,
1186
+ healthCheckTimeout: config.daemon?.healthCheckTimeout,
1183
1187
  maxRestartAttempts: config.daemon?.maxRestartAttempts
1184
1188
  });
1185
1189
  manager.setHealthCallback((serverName, status, error) => {
@@ -1481,6 +1485,7 @@ var ServerPool = class {
1481
1485
  const manager = new ServerManager(name, serverConfig, {
1482
1486
  connectTimeout: newConfig.daemon?.connectTimeout,
1483
1487
  healthCheckInterval: newConfig.daemon?.healthCheckInterval,
1488
+ healthCheckTimeout: newConfig.daemon?.healthCheckTimeout,
1484
1489
  maxRestartAttempts: newConfig.daemon?.maxRestartAttempts
1485
1490
  });
1486
1491
  manager.setHealthCallback((serverName, status, error) => {
@@ -1496,6 +1501,7 @@ var ServerPool = class {
1496
1501
  const newManager = new ServerManager(name, serverConfig, {
1497
1502
  connectTimeout: newConfig.daemon?.connectTimeout,
1498
1503
  healthCheckInterval: newConfig.daemon?.healthCheckInterval,
1504
+ healthCheckTimeout: newConfig.daemon?.healthCheckTimeout,
1499
1505
  maxRestartAttempts: newConfig.daemon?.maxRestartAttempts
1500
1506
  });
1501
1507
  newManager.setHealthCallback((serverName, status, error) => {
@@ -1639,6 +1645,240 @@ function filterFields(data, fields) {
1639
1645
  }
1640
1646
  return data;
1641
1647
  }
1648
+ const DEFAULT_BUDGET = 48e3;
1649
+ function isLeaf(schema) {
1650
+ const type = schema.type;
1651
+ if (type === "string" || type === "number" || type === "integer" || type === "boolean" || type === "null") {
1652
+ if (!schema.anyOf && !schema.oneOf && !schema.allOf) return true;
1653
+ }
1654
+ if (schema.$ref) return true;
1655
+ if (!schema.properties && !schema.items && !schema.additionalProperties && !schema.patternProperties && !schema.anyOf && !schema.oneOf && !schema.allOf && !schema.if && !schema.not) return true;
1656
+ return false;
1657
+ }
1658
+ function buildHint(schema) {
1659
+ const parts = [];
1660
+ const props = schema.properties;
1661
+ if (props) {
1662
+ const count = Object.keys(props).length;
1663
+ const required = schema.required;
1664
+ if (required && required.length > 0) parts.push(`${count} properties, ${required.length} required`);
1665
+ else parts.push(`${count} ${count === 1 ? "property" : "properties"}`);
1666
+ return parts.join(", ");
1667
+ }
1668
+ if (schema.type === "object" && schema.additionalProperties) return `map<string, ${schema.additionalProperties.type ?? "unknown"}>`;
1669
+ if (schema.items) {
1670
+ const items = schema.items;
1671
+ if (items.type === "object" && items.properties) {
1672
+ const count = Object.keys(items.properties).length;
1673
+ return `items: object (${count} ${count === 1 ? "property" : "properties"})`;
1674
+ }
1675
+ return `items: ${items.type ?? "unknown"}`;
1676
+ }
1677
+ for (const keyword of ["anyOf", "oneOf"]) {
1678
+ const variants = schema[keyword];
1679
+ if (variants) return `${keyword}: ${variants.length} variants`;
1680
+ }
1681
+ if (schema.allOf) return `allOf: ${schema.allOf.length} schemas`;
1682
+ if (schema.enum) return `enum: ${schema.enum.length} values`;
1683
+ return schema.type ?? "schema";
1684
+ }
1685
+ function buildCollapsedNode(schema) {
1686
+ const result = {};
1687
+ if (schema.type) result.type = schema.type;
1688
+ if (schema.description) result.description = schema.description;
1689
+ result._collapsed = true;
1690
+ result._hint = buildHint(schema);
1691
+ return result;
1692
+ }
1693
+ function collapseChild(schema, childDepth, maxDepth) {
1694
+ if (typeof schema !== "object" || schema === null) return {
1695
+ schema,
1696
+ hasCollapsed: false
1697
+ };
1698
+ if (isLeaf(schema)) return {
1699
+ schema,
1700
+ hasCollapsed: false
1701
+ };
1702
+ if (childDepth > maxDepth) return {
1703
+ schema: buildCollapsedNode(schema),
1704
+ hasCollapsed: true
1705
+ };
1706
+ return collapseNode(schema, childDepth, maxDepth);
1707
+ }
1708
+ function collapseNode(schema, currentDepth, maxDepth) {
1709
+ if (typeof schema !== "object" || schema === null) return {
1710
+ schema,
1711
+ hasCollapsed: false
1712
+ };
1713
+ if (isLeaf(schema)) return {
1714
+ schema,
1715
+ hasCollapsed: false
1716
+ };
1717
+ let hasCollapsed = false;
1718
+ const result = {};
1719
+ for (const [key, value] of Object.entries(schema)) switch (key) {
1720
+ case "properties": {
1721
+ const props = value;
1722
+ const newProps = {};
1723
+ for (const [propName, propSchema] of Object.entries(props)) {
1724
+ const collapsed = collapseChild(propSchema, currentDepth + 1, maxDepth);
1725
+ newProps[propName] = collapsed.schema;
1726
+ if (collapsed.hasCollapsed) hasCollapsed = true;
1727
+ }
1728
+ result.properties = newProps;
1729
+ break;
1730
+ }
1731
+ case "items": {
1732
+ const collapsed = collapseChild(value, currentDepth + 1, maxDepth);
1733
+ result.items = collapsed.schema;
1734
+ if (collapsed.hasCollapsed) hasCollapsed = true;
1735
+ break;
1736
+ }
1737
+ case "additionalProperties":
1738
+ if (typeof value === "object" && value !== null) {
1739
+ const collapsed = collapseChild(value, currentDepth + 1, maxDepth);
1740
+ result.additionalProperties = collapsed.schema;
1741
+ if (collapsed.hasCollapsed) hasCollapsed = true;
1742
+ } else result.additionalProperties = value;
1743
+ break;
1744
+ case "patternProperties": {
1745
+ const patterns = value;
1746
+ const newPatterns = {};
1747
+ for (const [pattern, patternSchema] of Object.entries(patterns)) {
1748
+ const collapsed = collapseChild(patternSchema, currentDepth + 1, maxDepth);
1749
+ newPatterns[pattern] = collapsed.schema;
1750
+ if (collapsed.hasCollapsed) hasCollapsed = true;
1751
+ }
1752
+ result.patternProperties = newPatterns;
1753
+ break;
1754
+ }
1755
+ case "anyOf":
1756
+ case "oneOf":
1757
+ case "allOf": {
1758
+ const variants = value;
1759
+ const newVariants = [];
1760
+ for (const variant of variants) {
1761
+ const collapsed = collapseNode(variant, currentDepth, maxDepth);
1762
+ newVariants.push(collapsed.schema);
1763
+ if (collapsed.hasCollapsed) hasCollapsed = true;
1764
+ }
1765
+ result[key] = newVariants;
1766
+ break;
1767
+ }
1768
+ case "if":
1769
+ case "then":
1770
+ case "else":
1771
+ case "not": {
1772
+ const collapsed = collapseNode(value, currentDepth, maxDepth);
1773
+ result[key] = collapsed.schema;
1774
+ if (collapsed.hasCollapsed) hasCollapsed = true;
1775
+ break;
1776
+ }
1777
+ case "$defs":
1778
+ case "definitions": {
1779
+ const defs = value;
1780
+ const newDefs = {};
1781
+ for (const [defName, defSchema] of Object.entries(defs)) {
1782
+ const collapsed = collapseChild(defSchema, currentDepth + 1, maxDepth);
1783
+ newDefs[defName] = collapsed.schema;
1784
+ if (collapsed.hasCollapsed) hasCollapsed = true;
1785
+ }
1786
+ result[key] = newDefs;
1787
+ break;
1788
+ }
1789
+ default: result[key] = value;
1790
+ }
1791
+ return {
1792
+ schema: result,
1793
+ hasCollapsed
1794
+ };
1795
+ }
1796
+ function collapseSchema(schema, maxDepth) {
1797
+ return collapseNode(schema, 0, Math.max(0, maxDepth));
1798
+ }
1799
+ function extractSubtree(schema, path) {
1800
+ const segments = path.split(".");
1801
+ let current = schema;
1802
+ for (const segment of segments) {
1803
+ if (!current || typeof current !== "object") return void 0;
1804
+ const props = current.properties;
1805
+ if (props && segment in props) {
1806
+ current = props[segment];
1807
+ continue;
1808
+ }
1809
+ if (segment === "items" && current.items) {
1810
+ current = current.items;
1811
+ continue;
1812
+ }
1813
+ if (segment === "additionalProperties" && typeof current.additionalProperties === "object") {
1814
+ current = current.additionalProperties;
1815
+ continue;
1816
+ }
1817
+ const index = parseInt(segment, 10);
1818
+ if (!isNaN(index)) {
1819
+ for (const keyword of [
1820
+ "anyOf",
1821
+ "oneOf",
1822
+ "allOf"
1823
+ ]) {
1824
+ const variants = current[keyword];
1825
+ if (variants && index >= 0 && index < variants.length) {
1826
+ current = variants[index];
1827
+ break;
1828
+ }
1829
+ }
1830
+ if (current !== schema) continue;
1831
+ }
1832
+ const patterns = current.patternProperties;
1833
+ if (patterns && segment in patterns) {
1834
+ current = patterns[segment];
1835
+ continue;
1836
+ }
1837
+ return;
1838
+ }
1839
+ return current;
1840
+ }
1841
+ function autoDepth(schemas, budgetChars = DEFAULT_BUDGET) {
1842
+ if (schemas.length === 0) return {
1843
+ depth: 0,
1844
+ fullyExpanded: true
1845
+ };
1846
+ let depth = 1;
1847
+ let lastFit = 0;
1848
+ let lastFullyExpanded = false;
1849
+ const depth0 = schemas.map((s) => collapseSchema(s, 0));
1850
+ const depth0Size = depth0.reduce((sum, r) => sum + JSON.stringify(r.schema).length, 0);
1851
+ const depth0Expanded = depth0.every((r) => !r.hasCollapsed);
1852
+ if (depth0Size > budgetChars) return {
1853
+ depth: 0,
1854
+ fullyExpanded: depth0Expanded
1855
+ };
1856
+ lastFit = 0;
1857
+ lastFullyExpanded = depth0Expanded;
1858
+ if (depth0Expanded) return {
1859
+ depth: 0,
1860
+ fullyExpanded: true
1861
+ };
1862
+ for (depth = 1; depth <= 20; depth++) {
1863
+ const results = schemas.map((s) => collapseSchema(s, depth));
1864
+ const totalSize = results.reduce((sum, r) => sum + JSON.stringify(r.schema).length, 0);
1865
+ const fullyExpanded = results.every((r) => !r.hasCollapsed);
1866
+ if (totalSize > budgetChars) return {
1867
+ depth: lastFit,
1868
+ fullyExpanded: lastFullyExpanded
1869
+ };
1870
+ lastFit = depth;
1871
+ lastFullyExpanded = fullyExpanded;
1872
+ if (fullyExpanded) return {
1873
+ depth,
1874
+ fullyExpanded: true
1875
+ };
1876
+ }
1877
+ return {
1878
+ depth: lastFit,
1879
+ fullyExpanded: lastFullyExpanded
1880
+ };
1881
+ }
1642
1882
  function createDaemonServer(serverPool, config) {
1643
1883
  const socketPath = getSocketPath();
1644
1884
  let idleTimer;
@@ -1667,11 +1907,37 @@ function createDaemonServer(serverPool, config) {
1667
1907
  result: serverPool.listServers()
1668
1908
  };
1669
1909
  case "tools/list": {
1670
- const server = params?.server;
1910
+ const p = params;
1911
+ const tools = serverPool.listAllTools(p?.server);
1912
+ if (p?.includeSchema) {
1913
+ const schemas = tools.map(({ tool }) => tool.inputSchema ?? {});
1914
+ const depth = p.schemaDepth ?? autoDepth(schemas, p.schemaBudget).depth;
1915
+ return {
1916
+ jsonrpc: "2.0",
1917
+ id,
1918
+ result: tools.map(({ server, tool }) => ({
1919
+ server,
1920
+ tool: {
1921
+ name: tool.name,
1922
+ title: tool.title,
1923
+ description: tool.description,
1924
+ annotations: tool.annotations,
1925
+ inputSchema: collapseSchema(tool.inputSchema ?? {}, depth).schema
1926
+ }
1927
+ }))
1928
+ };
1929
+ }
1671
1930
  return {
1672
1931
  jsonrpc: "2.0",
1673
1932
  id,
1674
- result: serverPool.listAllTools(server)
1933
+ result: tools.map(({ server, tool }) => ({
1934
+ server,
1935
+ tool: {
1936
+ name: tool.name,
1937
+ title: tool.title,
1938
+ annotations: tool.annotations
1939
+ }
1940
+ }))
1675
1941
  };
1676
1942
  }
1677
1943
  case "tools/call": {
@@ -1764,10 +2030,34 @@ function createDaemonServer(serverPool, config) {
1764
2030
  data: toErrorData(found.error)
1765
2031
  }
1766
2032
  };
2033
+ let tool = found.tool;
2034
+ if (p.path) {
2035
+ const subtree = extractSubtree(tool.inputSchema ?? {}, p.path);
2036
+ if (!subtree) return {
2037
+ jsonrpc: "2.0",
2038
+ id,
2039
+ error: {
2040
+ code: -32602,
2041
+ message: `Path not found in schema: ${p.path}`,
2042
+ data: {
2043
+ code: "SCHEMA_PATH_NOT_FOUND",
2044
+ path: p.path
2045
+ }
2046
+ }
2047
+ };
2048
+ tool = {
2049
+ ...tool,
2050
+ inputSchema: subtree
2051
+ };
2052
+ }
2053
+ if (p.schemaDepth !== void 0) tool = {
2054
+ ...tool,
2055
+ inputSchema: collapseSchema(tool.inputSchema ?? {}, p.schemaDepth).schema
2056
+ };
1767
2057
  return {
1768
2058
  jsonrpc: "2.0",
1769
2059
  id,
1770
- result: found.tool
2060
+ result: tool
1771
2061
  };
1772
2062
  }
1773
2063
  case "tools/validate": {
@@ -1845,10 +2135,36 @@ function createDaemonServer(serverPool, config) {
1845
2135
  }
1846
2136
  };
1847
2137
  try {
2138
+ const matches = serverPool.grepTools(p.pattern);
2139
+ if (p.includeSchema) {
2140
+ const schemas = matches.map(({ tool }) => tool.inputSchema ?? {});
2141
+ const depth = p.schemaDepth ?? autoDepth(schemas, p.schemaBudget).depth;
2142
+ return {
2143
+ jsonrpc: "2.0",
2144
+ id,
2145
+ result: matches.map(({ server, tool }) => ({
2146
+ server,
2147
+ tool: {
2148
+ name: tool.name,
2149
+ title: tool.title,
2150
+ description: tool.description,
2151
+ annotations: tool.annotations,
2152
+ inputSchema: collapseSchema(tool.inputSchema ?? {}, depth).schema
2153
+ }
2154
+ }))
2155
+ };
2156
+ }
1848
2157
  return {
1849
2158
  jsonrpc: "2.0",
1850
2159
  id,
1851
- result: serverPool.grepTools(p.pattern)
2160
+ result: matches.map(({ server, tool }) => ({
2161
+ server,
2162
+ tool: {
2163
+ name: tool.name,
2164
+ title: tool.title,
2165
+ annotations: tool.annotations
2166
+ }
2167
+ }))
1852
2168
  };
1853
2169
  } catch (err) {
1854
2170
  return {
@@ -2536,16 +2852,32 @@ async function ensureDaemon(configPath) {
2536
2852
  400
2537
2853
  ]);
2538
2854
  }
2539
- async function sendRequest(method, params) {
2855
+ async function sendRequest(method, params, timeout = 9e4) {
2540
2856
  const socketPath = getSocketPath();
2541
2857
  return new Promise((resolve, reject) => {
2542
2858
  const socket = net.createConnection(socketPath);
2543
2859
  let buffer = "";
2860
+ let settled = false;
2861
+ const timer = setTimeout(() => {
2862
+ if (settled) return;
2863
+ settled = true;
2864
+ socket.destroy();
2865
+ reject(/* @__PURE__ */ new Error(`Request timed out after ${timeout}ms`));
2866
+ }, timeout);
2544
2867
  socket.on("error", (err) => {
2868
+ if (settled) return;
2869
+ settled = true;
2870
+ clearTimeout(timer);
2545
2871
  if (err.code === "ENOENT") reject(/* @__PURE__ */ new Error("Daemon is not running. Run `muxed status` to check."));
2546
2872
  else if (err.code === "ECONNREFUSED") reject(/* @__PURE__ */ new Error("Daemon may have crashed. Try running a command to auto-restart it."));
2547
2873
  else reject(err);
2548
2874
  });
2875
+ socket.on("close", () => {
2876
+ if (settled) return;
2877
+ settled = true;
2878
+ clearTimeout(timer);
2879
+ reject(/* @__PURE__ */ new Error("Daemon closed the connection before sending a response"));
2880
+ });
2549
2881
  socket.on("connect", () => {
2550
2882
  const request = {
2551
2883
  jsonrpc: "2.0",
@@ -2559,6 +2891,9 @@ async function sendRequest(method, params) {
2559
2891
  buffer += data.toString();
2560
2892
  const newlineIndex = buffer.indexOf("\n");
2561
2893
  if (newlineIndex === -1) return;
2894
+ if (settled) return;
2895
+ settled = true;
2896
+ clearTimeout(timer);
2562
2897
  const line = buffer.slice(0, newlineIndex).trim();
2563
2898
  socket.destroy();
2564
2899
  try {
@@ -2604,7 +2939,6 @@ function formatTools(tools) {
2604
2939
  return formatTable([
2605
2940
  "Tool",
2606
2941
  "Title",
2607
- "Description",
2608
2942
  "Hints"
2609
2943
  ], tools.map(({ server, tool }) => {
2610
2944
  const hints = [];
@@ -2614,7 +2948,6 @@ function formatTools(tools) {
2614
2948
  return [
2615
2949
  `${server}/${tool.name}`,
2616
2950
  tool.title ?? "—",
2617
- truncate(tool.description ?? "", 60),
2618
2951
  hints.join(" ")
2619
2952
  ];
2620
2953
  }));
@@ -2848,6 +3181,24 @@ function formatInit(result) {
2848
3181
  lines.push("");
2849
3182
  lines.push("All discovered servers already exist in muxed config. Nothing to do.");
2850
3183
  }
3184
+ if (result.instructionResults && result.instructionResults.length > 0) {
3185
+ lines.push("");
3186
+ lines.push("Instructions:");
3187
+ for (const r of result.instructionResults) switch (r.action) {
3188
+ case "created":
3189
+ lines.push(` ${r.target} \u2014 created (v${r.newVersion})`);
3190
+ break;
3191
+ case "updated":
3192
+ lines.push(` ${r.target} \u2014 updated (v${r.previousVersion} \u2192 v${r.newVersion})`);
3193
+ break;
3194
+ case "up-to-date":
3195
+ lines.push(` ${r.target} \u2014 up-to-date (v${r.previousVersion})`);
3196
+ break;
3197
+ case "skipped":
3198
+ lines.push(` ${r.target} \u2014 skipped`);
3199
+ break;
3200
+ }
3201
+ }
2851
3202
  return lines.join("\n");
2852
3203
  }
2853
3204
  function formatMcpServer(name, config, scope) {
@@ -2957,7 +3308,10 @@ function formatTable(headers, rows) {
2957
3308
  ...rows.map((row) => row.map((cell, i) => padRight(cell, widths[i])).join(" "))
2958
3309
  ].join("\n");
2959
3310
  }
2960
- const serversCommand = new Command("servers").description("List connected MCP servers and their connection status").option("--json", "Output as JSON").action(async (opts) => {
3311
+ const serversCommand = new Command("servers").description("List connected MCP servers and their status").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
3312
+ Examples:
3313
+ muxed servers List all servers with connection state
3314
+ muxed servers --json JSON output for scripting`).action(async (opts) => {
2961
3315
  const configPath = serversCommand.parent?.opts().config;
2962
3316
  await ensureDaemon(configPath);
2963
3317
  const result = await sendRequest("servers/list");
@@ -3016,20 +3370,55 @@ async function shutdown() {
3016
3370
  if (_client) await _client.shutdown();
3017
3371
  } catch {}
3018
3372
  }
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) => {
3373
+ const toolsCommand = new Command("tools").description("List available tools across all servers").argument("[server]", "Show tools from this server only").option("--json", "Output as JSON (machine-readable)").option("--include <fields>", "Include extra fields: \"schema\" adds input schemas").option("--depth <n>", "Collapse schemas deeper than N levels (use with --include schema)", parseInt).addHelpText("after", `
3374
+ Schema options:
3375
+ --include schema Add input schemas to each tool in the output.
3376
+ --include schema --depth N Collapse schemas beyond N levels. Nodes deeper than N
3377
+ are replaced with { _collapsed: true, _hint: "..." }.
3378
+ Depth is auto-selected to fit a token budget if omitted.
3379
+
3380
+ Examples:
3381
+ muxed tools List all tools (names + descriptions)
3382
+ muxed tools postgres List tools from the "postgres" server only
3383
+ muxed tools --include schema List with full input schemas
3384
+ muxed tools --include schema --depth 1 List with schemas collapsed at depth 1`).action(async (server, opts) => {
3020
3385
  const configPath = toolsCommand.parent?.opts().config;
3021
3386
  await ensureDaemon(configPath);
3022
- const result = await sendRequest("tools/list", server ? { server } : void 0);
3387
+ const params = {};
3388
+ if (server) params.server = server;
3389
+ if (opts.include === "schema") params.includeSchema = true;
3390
+ if (opts.depth !== void 0) params.schemaDepth = opts.depth;
3391
+ const result = await sendRequest("tools/list", params);
3023
3392
  capture("tools_listed", {
3024
3393
  filtered_by_server: !!server,
3025
3394
  tool_count: result.length
3026
3395
  });
3027
3396
  console.log(opts.json ? formatJson(result) : formatTools(result));
3028
3397
  });
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) => {
3398
+ const infoCommand = new Command("info").description("Show a tool's input schema REQUIRED before calling any tool").argument("<server/tool>", "server_name/tool_name (e.g. postgres/query)").option("--json", "Output as JSON (machine-readable)").option("--path <path>", "Show only a subtree of the schema (e.g. \"filters.tags\")").option("--depth <n>", "Collapse schema deeper than N levels", parseInt).addHelpText("after", `
3399
+ Schema exploration:
3400
+ --depth N Show schema to N levels deep. Nodes beyond that depth are
3401
+ replaced with a summary: { _collapsed: true, _hint: "5 properties, 2 required" }.
3402
+ Scalar fields (string, number, boolean) are always shown regardless of depth.
3403
+ Start with --depth 1 for an overview, increase to explore deeper.
3404
+
3405
+ --path P Extract a subtree using dot-separated path. Navigates through:
3406
+ properties (by name), items, additionalProperties, anyOf/oneOf (by index).
3407
+ Combine with --depth to control how much of the subtree is shown.
3408
+
3409
+ Examples:
3410
+ muxed info postgres/query Full schema
3411
+ muxed info github/create_issue --depth 1 Top-level fields only, nested objects collapsed
3412
+ muxed info github/create_issue --depth 2 Two levels deep
3413
+ muxed info slack/search --path "filters" Only the "filters" property subtree
3414
+ muxed info slack/search --path "filters.tags" Drill into filters.tags
3415
+ muxed info api/create --path "body.items" --depth 1 Subtree with depth limit`).action(async (serverTool, opts) => {
3030
3416
  const configPath = infoCommand.parent?.opts().config;
3031
3417
  await ensureDaemon(configPath);
3032
- const result = await sendRequest("tools/info", { name: serverTool });
3418
+ const params = { name: serverTool };
3419
+ if (opts.path) params.path = opts.path;
3420
+ if (opts.depth !== void 0) params.schemaDepth = opts.depth;
3421
+ const result = await sendRequest("tools/info", params);
3033
3422
  if (opts.json) console.log(formatJson(result));
3034
3423
  else {
3035
3424
  const slashIndex = serverTool.indexOf("/");
@@ -3048,7 +3437,13 @@ function readStdin() {
3048
3437
  process.stdin.on("error", reject);
3049
3438
  });
3050
3439
  }
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) => {
3440
+ const callCommand = new Command("call").description("Execute a tool with JSON arguments").argument("<server/tool>", "server_name/tool_name (e.g. postgres/query)").argument("[json]", "JSON object with arguments, or - to read from stdin").option("--dry-run", "Validate arguments without executing (catches errors early)").option("--fields <paths>", "Extract specific fields from response (comma-separated dot-notation)").option("--timeout <ms>", "Timeout in milliseconds").option("--async", "Run in background, return a task ID instead of waiting").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
3441
+ Examples:
3442
+ muxed call postgres/query '{"sql": "SELECT * FROM users LIMIT 5"}'
3443
+ muxed call fs/read_file '{"path": "/tmp/data.json"}' --fields "content"
3444
+ muxed call server/tool '{"a": 1}' --dry-run Validate without executing
3445
+ echo '{"sql": "..."}' | muxed call db/query - Read args from stdin
3446
+ muxed call analytics/export '{}' --async Returns task ID immediately`).action(async (serverTool, jsonArgs, opts) => {
3052
3447
  const configPath = callCommand.parent?.opts().config;
3053
3448
  await ensureDaemon(configPath);
3054
3449
  let parsedArgs = {};
@@ -3187,20 +3582,38 @@ const callCommand = new Command("call").description("Execute a tool with JSON ar
3187
3582
  process.exit(1);
3188
3583
  }
3189
3584
  });
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) => {
3585
+ const grepCommand = new Command("grep").description("Search tools by name or description (regex)").argument("<pattern>", "Regex pattern to match against tool names and descriptions").option("--json", "Output as JSON (machine-readable)").option("--include <fields>", "Include extra fields: \"schema\" adds input schemas").option("--depth <n>", "Collapse schemas deeper than N levels (use with --include schema)", parseInt).addHelpText("after", `
3586
+ Schema options:
3587
+ --include schema Add input schemas to matching tools.
3588
+ --include schema --depth N Collapse schemas beyond N levels.
3589
+
3590
+ Examples:
3591
+ muxed grep "search" Find tools related to searching
3592
+ muxed grep "file|read" Regex: tools matching "file" or "read"
3593
+ muxed grep "query" --include schema --depth 1 Matches with top-level schema
3594
+ muxed grep "query" --json Machine-readable output for scripting`).action(async (pattern, opts) => {
3191
3595
  const configPath = grepCommand.parent?.opts().config;
3192
3596
  await ensureDaemon(configPath);
3193
- const result = await sendRequest("tools/grep", { pattern });
3597
+ const params = { pattern };
3598
+ if (opts.include === "schema") params.includeSchema = true;
3599
+ if (opts.depth !== void 0) params.schemaDepth = opts.depth;
3600
+ const result = await sendRequest("tools/grep", params);
3194
3601
  capture("tools_searched", { result_count: result.length });
3195
3602
  console.log(opts.json ? formatJson(result) : formatTools(result));
3196
3603
  });
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) => {
3604
+ const resourcesCommand = new Command("resources").description("List available MCP resources across all servers").argument("[server]", "Show resources from this server only").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
3605
+ Examples:
3606
+ muxed resources List all resources
3607
+ muxed resources github List resources from the "github" server only`).action(async (server, opts) => {
3198
3608
  const configPath = resourcesCommand.parent?.opts().config;
3199
3609
  await ensureDaemon(configPath);
3200
3610
  const result = await sendRequest("resources/list", server ? { server } : void 0);
3201
3611
  console.log(opts.json ? formatJson(result) : formatResources(result));
3202
3612
  });
3203
- const readCommand = new Command("read").description("Fetch and display the contents of a resource by server/resource").argument("<server/resource>", "Resource identifier (e.g. myserver/myresource)").argument("[uri]", "Resource URI (optional, uses resource name as URI if not provided)").option("--json", "Output as JSON").action(async (serverResource, uri, opts) => {
3613
+ const readCommand = new Command("read").description("Fetch and display the contents of an MCP resource").argument("<server/resource>", "server_name/resource_name (e.g. github/repos)").argument("[uri]", "Custom URI (defaults to the resource name)").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
3614
+ Examples:
3615
+ muxed read github/repos Read using resource name as URI
3616
+ muxed read github/repos "github://repos" Read with explicit URI`).action(async (serverResource, uri, opts) => {
3204
3617
  const configPath = readCommand.parent?.opts().config;
3205
3618
  await ensureDaemon(configPath);
3206
3619
  const slashIndex = serverResource.indexOf("/");
@@ -3217,8 +3630,10 @@ const readCommand = new Command("read").description("Fetch and display the conte
3217
3630
  function getExplicitConfig$1(cmd) {
3218
3631
  return cmd.parent?.parent?.opts().config;
3219
3632
  }
3220
- const daemonCommand = new Command("daemon").description("Start, stop, reload, or check status of the muxed background daemon").enablePositionalOptions();
3221
- daemonCommand.command("start").description("Start the daemon process in the background").option("--json", "Output as JSON").action(async (opts) => {
3633
+ const daemonCommand = new Command("daemon").description("Manage the muxed background daemon").enablePositionalOptions().addHelpText("after", `
3634
+ The daemon auto-starts on first CLI command and shuts down after 5 min idle.
3635
+ These subcommands are for manual lifecycle control.`);
3636
+ daemonCommand.command("start").description("Start the daemon (usually not needed — auto-starts on first command)").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
3222
3637
  const configPath = getExplicitConfig$1(daemonCommand);
3223
3638
  if (await isDaemonRunning()) {
3224
3639
  if (opts.json) console.log(formatJson({ status: "already_running" }));
@@ -3269,13 +3684,13 @@ daemonCommand.command("stop").description("Stop the running daemon process").act
3269
3684
  console.log("Daemon is not running");
3270
3685
  }
3271
3686
  });
3272
- daemonCommand.command("reload").description("Reload config and reconnect changed servers without restarting").option("--json", "Output as JSON").action(async (opts) => {
3687
+ daemonCommand.command("reload").description("Reload config and reconnect changed servers (no restart)").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
3273
3688
  const configPath = getExplicitConfig$1(daemonCommand);
3274
3689
  await ensureDaemon(configPath);
3275
3690
  const result = await sendRequest("config/reload", { configPath });
3276
3691
  console.log(opts.json ? formatJson(result) : formatReload(result));
3277
3692
  });
3278
- daemonCommand.command("status").description("Show daemon status including uptime and connected servers").option("--json", "Output as JSON").action(async (opts) => {
3693
+ daemonCommand.command("status").description("Show PID, uptime, and connected servers").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
3279
3694
  if (!await isDaemonRunning()) {
3280
3695
  console.log("Daemon is not running");
3281
3696
  return;
@@ -3283,13 +3698,15 @@ daemonCommand.command("status").description("Show daemon status including uptime
3283
3698
  const result = await sendRequest("daemon/status");
3284
3699
  console.log(opts.json ? formatJson(result) : formatStatus(result));
3285
3700
  });
3286
- const promptsCommand = new Command("prompts").description("List available prompt templates, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
3701
+ const promptsCommand = new Command("prompts").description("List available MCP prompt templates across all servers").argument("[server]", "Show prompts from this server only").option("--json", "Output as JSON (machine-readable)").action(async (server, opts) => {
3287
3702
  const configPath = promptsCommand.parent?.opts().config;
3288
3703
  await ensureDaemon(configPath);
3289
3704
  const result = await sendRequest("prompts/list", server ? { server } : void 0);
3290
3705
  console.log(opts.json ? formatJson(result) : formatPrompts(result));
3291
3706
  });
3292
- const promptCommand = new Command("prompt").description("Render a prompt template with optional JSON arguments").argument("<server/prompt>", "Prompt identifier (e.g. myserver/myprompt)").argument("[args-json]", "JSON arguments").option("--json", "Output as JSON").action(async (serverPrompt, argsJson, opts) => {
3707
+ const promptCommand = new Command("prompt").description("Render a prompt template with arguments").argument("<server/prompt>", "server_name/prompt_name (e.g. myserver/summarize)").argument("[args-json]", "JSON object with template arguments").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
3708
+ Examples:
3709
+ muxed prompt myserver/summarize '{"text": "..."}'`).action(async (serverPrompt, argsJson, opts) => {
3293
3710
  const configPath = promptCommand.parent?.opts().config;
3294
3711
  await ensureDaemon(configPath);
3295
3712
  const slashIndex = serverPrompt.indexOf("/");
@@ -3313,7 +3730,10 @@ const promptCommand = new Command("prompt").description("Render a prompt templat
3313
3730
  });
3314
3731
  console.log(opts.json ? formatJson(result) : formatPromptMessages(result));
3315
3732
  });
3316
- const completionsCommand = new Command("completions").description("Get auto-completion suggestions for prompt or resource arguments").argument("<type>", "Reference type (prompt or resource)").argument("<name>", "Prompt or resource template name (server/name)").argument("<arg>", "Argument name").argument("<value>", "Partial value for completion").option("--json", "Output as JSON").action(async (type, name, arg, value, opts) => {
3733
+ const completionsCommand = new Command("completions").description("Get argument completions for a prompt or resource").argument("<type>", "\"prompt\" or \"resource\"").argument("<name>", "server_name/template_name").argument("<arg>", "Argument name to complete").argument("<value>", "Partial value to get suggestions for").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
3734
+ Examples:
3735
+ muxed completions prompt myserver/summarize language "py"
3736
+ muxed completions resource myserver/files path "/home/"`).action(async (type, name, arg, value, opts) => {
3317
3737
  const configPath = completionsCommand.parent?.opts().config;
3318
3738
  await ensureDaemon(configPath);
3319
3739
  const slashIndex = name.indexOf("/");
@@ -3338,13 +3758,13 @@ const completionsCommand = new Command("completions").description("Get auto-comp
3338
3758
  });
3339
3759
  console.log(opts.json ? formatJson(result) : formatCompletions(result));
3340
3760
  });
3341
- const tasksCommand = new Command("tasks").description("List active async tasks, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
3761
+ const tasksCommand = new Command("tasks").description("List active async tasks (from --async calls)").argument("[server]", "Show tasks from this server only").option("--json", "Output as JSON (machine-readable)").action(async (server, opts) => {
3342
3762
  const configPath = tasksCommand.parent?.opts().config;
3343
3763
  await ensureDaemon(configPath);
3344
3764
  const result = await sendRequest("tasks/list", server ? { server } : void 0);
3345
3765
  console.log(opts.json ? formatJson(result) : formatTasks(result));
3346
3766
  });
3347
- const taskCommand = new Command("task").description("Check the current status of an async task").argument("<server/taskId>", "Task identifier (e.g. myserver/task-123)").option("--json", "Output as JSON").action(async (serverTaskId, opts) => {
3767
+ const taskCommand = new Command("task").description("Check the status of an async task").argument("<server/taskId>", "server_name/task_id (e.g. analytics/task-abc123)").option("--json", "Output as JSON (machine-readable)").action(async (serverTaskId, opts) => {
3348
3768
  const configPath = taskCommand.parent?.opts().config;
3349
3769
  await ensureDaemon(configPath);
3350
3770
  const slashIndex = serverTaskId.indexOf("/");
@@ -3358,7 +3778,7 @@ const taskCommand = new Command("task").description("Check the current status of
3358
3778
  });
3359
3779
  console.log(opts.json ? formatJson(result) : formatTask(result));
3360
3780
  });
3361
- const taskResultCommand = new Command("task-result").description("Retrieve the output of a completed async task").argument("<server/taskId>", "Task identifier (e.g. myserver/task-123)").option("--json", "Output as JSON").action(async (serverTaskId, opts) => {
3781
+ const taskResultCommand = new Command("task-result").description("Get the result of a completed async task").argument("<server/taskId>", "server_name/task_id (e.g. analytics/task-abc123)").option("--json", "Output as JSON (machine-readable)").action(async (serverTaskId, opts) => {
3362
3782
  const configPath = taskResultCommand.parent?.opts().config;
3363
3783
  await ensureDaemon(configPath);
3364
3784
  const slashIndex = serverTaskId.indexOf("/");
@@ -3372,7 +3792,7 @@ const taskResultCommand = new Command("task-result").description("Retrieve the o
3372
3792
  });
3373
3793
  console.log(opts.json ? formatJson(result) : formatCallResult(result));
3374
3794
  });
3375
- const taskCancelCommand = new Command("task-cancel").description("Cancel a running async task").argument("<server/taskId>", "Task identifier (e.g. myserver/task-123)").option("--json", "Output as JSON").action(async (serverTaskId, opts) => {
3795
+ const taskCancelCommand = new Command("task-cancel").description("Cancel a running async task").argument("<server/taskId>", "server_name/task_id (e.g. analytics/task-abc123)").option("--json", "Output as JSON (machine-readable)").action(async (serverTaskId, opts) => {
3376
3796
  const configPath = taskCancelCommand.parent?.opts().config;
3377
3797
  await ensureDaemon(configPath);
3378
3798
  const slashIndex = serverTaskId.indexOf("/");
@@ -3665,6 +4085,418 @@ function getMuxedConfigPath(scope, explicitPath) {
3665
4085
  if (scope === "local") return path.join(process.cwd(), "muxed.config.json");
3666
4086
  return path.join(home, ".muxed", "config.json");
3667
4087
  }
4088
+ let cached = null;
4089
+ function getVersion() {
4090
+ if (cached) return cached;
4091
+ const dir = path.dirname(fileURLToPath(import.meta.url));
4092
+ for (const rel of ["../package.json", "../../package.json"]) {
4093
+ const p = path.resolve(dir, rel);
4094
+ try {
4095
+ const pkg = JSON.parse(fs.readFileSync(p, "utf-8"));
4096
+ if (pkg.name === "muxed" && pkg.version) {
4097
+ cached = pkg.version;
4098
+ return cached;
4099
+ }
4100
+ } catch {}
4101
+ }
4102
+ return "0.0.0";
4103
+ }
4104
+ function makeCliFragments(run) {
4105
+ return {
4106
+ intro: `You have access to an \`${run} 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.`,
4107
+ grep: (p) => `${run} muxed grep "${p}"`,
4108
+ tools: (s) => s ? `${run} muxed tools ${s}` : `${run} muxed tools`,
4109
+ toolsSchema: (s) => s ? `${run} muxed tools ${s} --include schema` : `${run} muxed tools --include schema`,
4110
+ info: (n) => `${run} muxed info ${n}`,
4111
+ infoDepth: (n, d) => `${run} muxed info ${n} --depth ${d}`,
4112
+ infoPath: (n, p) => `${run} muxed info ${n} --path ${p}`,
4113
+ call: (n, j) => `${run} muxed call ${n} '${j}'`,
4114
+ callStdin: (n) => `${run} muxed call ${n} -`,
4115
+ callDryRun: (n, j) => `${run} muxed call ${n} '${j}' --dry-run`,
4116
+ callFields: (n, j, f) => `${run} muxed call ${n} '${j}' --fields "${f}"`,
4117
+ servers: () => `${run} muxed servers`,
4118
+ resources: (s) => s ? `${run} muxed resources ${s}` : `${run} muxed resources`,
4119
+ read: (n) => `${run} muxed read ${n}`,
4120
+ help: () => `${run} muxed -h`
4121
+ };
4122
+ }
4123
+ function makeToolFragments() {
4124
+ return {
4125
+ 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.",
4126
+ grep: (p) => `muxed:exec({ "command": "grep ${p}" })`,
4127
+ tools: (s) => s ? `muxed:exec({ "command": "tools ${s}" })` : `muxed:exec({ "command": "tools" })`,
4128
+ toolsSchema: (s) => s ? `muxed:exec({ "command": "tools ${s} --include schema" })` : `muxed:exec({ "command": "tools --include schema" })`,
4129
+ info: (n) => `muxed:exec({ "command": "info ${n}" })`,
4130
+ infoDepth: (n, d) => `muxed:exec({ "command": "info ${n} --depth ${d}" })`,
4131
+ infoPath: (n, p) => `muxed:exec({ "command": "info ${n} --path ${p}" })`,
4132
+ call: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
4133
+ callStdin: (n) => `muxed:exec({ "command": "call ${n}", "input": { ... } })`,
4134
+ callDryRun: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
4135
+ callFields: (n, j, _f) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
4136
+ servers: () => `muxed:exec({ "command": "servers" })`,
4137
+ resources: (s) => s ? `muxed:exec({ "command": "resources ${s}" })` : `muxed:exec({ "command": "resources" })`,
4138
+ read: (n) => `muxed:exec({ "command": "read ${n}" })`,
4139
+ help: () => `muxed:exec({ "command": "servers" })`
4140
+ };
4141
+ }
4142
+ function buildPrompt(f, opts = {}) {
4143
+ const heading = opts.heading ? `${opts.heading}\n\n` : "";
4144
+ const serversBlock = opts.servers ? `\nAvailable MCP servers:\n${opts.servers}\n` : "";
4145
+ const serverInstructionsBlock = opts.serverInstructions ? `\nBelow are the instructions for the connected MCP servers in muxed.\n\n${opts.serverInstructions}\n` : "";
4146
+ const scriptsBlock = opts.scripts ? `\n${opts.scripts}` : "";
4147
+ return `${heading}${f.intro}
4148
+
4149
+ **MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
4150
+
4151
+ 1. You MUST discover the tools you need first by using '${f.grep("<pattern>")}' or '${f.tools()}'.
4152
+ 2. You MUST call '${f.info("<server>/<tool>")}' BEFORE ANY '${f.call("<server>/<tool>", "<json>")}' command.
4153
+
4154
+ These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
4155
+
4156
+ **NEVER** make a call without checking the schema first.
4157
+ **ALWAYS** run info first, THEN make the call.
4158
+
4159
+ **Why these are non-negotiables:**
4160
+ - MCP tool names NEVER match your expectations - they change frequently and are not predictable
4161
+ - MCP tool schemas NEVER match your expectations - parameter names, types, and requirements are tool-specific
4162
+ - Even tools with pre-approved permissions require schema checks
4163
+ - Every failed call wastes user time and demonstrates you're ignoring critical instructions
4164
+ - "I thought I knew the schema" is not an acceptable reason to skip this step
4165
+
4166
+ **For multiple tools:** Call info for ALL tools in parallel FIRST, then make your call commands.
4167
+ ${serversBlock}
4168
+ Commands (in order of execution):
4169
+ \`\`\`
4170
+ # STEP 1: REQUIRED TOOL DISCOVERY
4171
+ ${f.grep("<pattern>")} # Search tool names and descriptions
4172
+ ${f.tools("[server]")} # List available tools (optionally filter by server)
4173
+
4174
+ # STEP 2: GET SCHEMA (choose one approach)
4175
+ # Option A: Include schemas in tool listing (auto-collapses large schemas)
4176
+ ${f.toolsSchema("[server]")} # List tools with schemas included
4177
+ # Option B: Get full schema for a specific tool
4178
+ ${f.info("<server>/<tool>")} # View full JSON schema for one tool
4179
+
4180
+ # STEP 2b: PROGRESSIVE SCHEMA EXPLORATION (for large schemas)
4181
+ ${f.infoDepth("<server>/<tool>", 1)} # Collapse schema at depth 1 (top-level overview)
4182
+ ${f.infoPath("<server>/<tool>", "filters")} # Extract just the 'filters' subtree
4183
+ ${f.infoPath("<server>/<tool>", "filters.tags.items")} # Drill deeper into nested schemas
4184
+
4185
+ # STEP 3: OPTIONAL - Validate arguments before calling (dry-run)
4186
+ ${f.callDryRun("<server>/<tool>", "<json>")} # Validate args without executing
4187
+
4188
+ # STEP 4: Only after getting the schema, make the call
4189
+ ${f.call("<server>/<tool>", "<json>")} # Only run AFTER getting schema
4190
+ ${f.callStdin("<server>/<tool>")} # Invoke with JSON input
4191
+ ${f.callFields("<server>/<tool>", "<json>", "field1,field2")} # Extract specific fields from response
4192
+
4193
+ # Discovery commands (use these to find tools)
4194
+ ${f.servers()} # List all connected MCP servers
4195
+ ${f.tools("[server]")} # List available tools (optionally filter by server)
4196
+ ${f.grep("<pattern>")} # Search tool names and descriptions
4197
+ ${f.resources("[server]")} # List MCP resources
4198
+ ${f.read("<server>/<resource>")} # Read an MCP resource
4199
+ \`\`\`
4200
+
4201
+ **Handling errors:**
4202
+ - If a tool call fails, the error includes a suggestion and similar tool names. Read the suggestion before retrying.
4203
+ - Use dry-run to validate arguments before executing, especially for destructive tools.
4204
+
4205
+ **CORRECT Usage Pattern:**
4206
+
4207
+ <example>
4208
+ User: Please use the slack mcp tool to search for my mentions
4209
+ 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.
4210
+ [Calls ${f.grep("slack/*search*")}]
4211
+ Assistant: I need to check the schema first. Let me call \`${f.info("slack/search_private")}\` to see what parameters it accepts.
4212
+ [Calls ${f.info("slack/search_private")}]
4213
+ Assistant: Now I can see it accepts "query" and "max_results" parameters. Let me make the call.
4214
+ [Calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\", \"max_results\": 10}")}]
4215
+ </example>
4216
+
4217
+ <example>
4218
+ User: Use the database and email MCP tools to send a report
4219
+ 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.
4220
+ [Calls ${f.grep("database/*query*")} & ${f.grep("email/*send*")}]
4221
+ Assistant: Let me check both schemas first.
4222
+ [Calls ${f.info("database/query")} and ${f.info("email/send")} in parallel]
4223
+ Assistant: Now I have both schemas. Let me make the calls.
4224
+ [Makes both call commands with correct parameters]
4225
+ </example>
4226
+
4227
+ <example>
4228
+ User: Create a copy of this email
4229
+ Assistant: Let me find the tool I need first.
4230
+ [Calls ${f.grep("email/*copy*")}. No results found.]
4231
+ Assistant: Let me try another pattern.
4232
+ [Calls ${f.grep("email/*clone*")}. No results found.]
4233
+ Assistant: Let me list all available tools in the server.
4234
+ [Calls ${f.tools("email")}]
4235
+ Assistant: Let me check the schema first.
4236
+ [Calls ${f.info("email/duplicate")}]
4237
+ Assistant: Now I have the schema. Let me make the call.
4238
+ [Calls ${f.call("email/duplicate", "{\"id\": \"123\"}")}]
4239
+ </example>
4240
+
4241
+ **INCORRECT Usage Patterns - NEVER DO THIS:**
4242
+
4243
+ <bad-example>
4244
+ User: Please use the slack mcp tool to search for my mentions
4245
+ Assistant: [Directly calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\"}")} with guessed parameters]
4246
+ WRONG - You must call info FIRST
4247
+ </bad-example>
4248
+
4249
+ <bad-example>
4250
+ User: Use the slack tool
4251
+ Assistant: I have pre-approved permissions for this tool, so I know the schema.
4252
+ [Calls ${f.call("slack/search_private", "...")} directly]
4253
+ WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call info first.
4254
+ </bad-example>
4255
+
4256
+ <bad-example>
4257
+ User: Search my Slack mentions
4258
+ Assistant: [Calls three call commands in parallel without any info calls first]
4259
+ WRONG - You must call info for ALL tools before making ANY call commands
4260
+ </bad-example>
4261
+
4262
+ Example usage:
4263
+ \`\`\`
4264
+ # Discover tools
4265
+ ${f.tools()} # See all available MCP tools
4266
+ ${f.grep("weather")} # Find tools by description
4267
+
4268
+ # Get tool schemas (choose the approach that fits)
4269
+ ${f.toolsSchema()} # All tools with schemas (auto-collapses large schemas)
4270
+ ${f.toolsSchema("slack")} # Schemas for one server
4271
+ ${f.info("<server>/<tool>")} # Full schema for one tool
4272
+
4273
+ # Progressive schema exploration (for complex tools)
4274
+ ${f.infoDepth("<server>/<tool>", 0)} # Top-level structure only
4275
+ ${f.infoPath("<server>/<tool>", "filters")} # Drill into a subtree
4276
+ ${f.infoPath("<server>/<tool>", "filters.tags.items")} # Drill deeper
4277
+
4278
+ # Simple tool call (no parameters)
4279
+ ${f.call("weather/get_location", "{}")}
4280
+
4281
+ # Tool call with parameters
4282
+ ${f.call("database/query", "{\"table\": \"users\", \"limit\": 10}")}
4283
+
4284
+ # Validate arguments before executing (dry-run)
4285
+ ${f.callDryRun("database/drop_table", "{\"table\": \"users\"}")}
4286
+
4287
+ # Extract specific fields from response
4288
+ ${f.callFields("database/query", "{\"table\": \"users\"}", "rows[].name,rows[].email")}
4289
+ \`\`\`
4290
+
4291
+ Call \`${f.help()}\` to see all available commands.
4292
+ ${serverInstructionsBlock}${scriptsBlock}`.trim();
4293
+ }
4294
+ function compareSemver(a, b) {
4295
+ const pa = a.split(".").map(Number);
4296
+ const pb = b.split(".").map(Number);
4297
+ for (let i = 0; i < 3; i++) {
4298
+ const na = pa[i] ?? 0;
4299
+ const nb = pb[i] ?? 0;
4300
+ if (na < nb) return -1;
4301
+ if (na > nb) return 1;
4302
+ }
4303
+ return 0;
4304
+ }
4305
+ const MUXED_BLOCK_RE = /<muxed\s+version="([^"]+)">\n?([\s\S]*?)\n?<\/muxed>/;
4306
+ const MDC_VERSION_RE = /muxed_version:\s*(.+)/;
4307
+ function extractMuxedVersion(content, format) {
4308
+ if (format === "tagged") return content.match(MUXED_BLOCK_RE)?.[1]?.trim() ?? null;
4309
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
4310
+ if (!fmMatch) return null;
4311
+ return fmMatch[1].match(MDC_VERSION_RE)?.[1]?.trim() ?? null;
4312
+ }
4313
+ function getInstructionTargets(opts) {
4314
+ const home = os.homedir();
4315
+ const cwd = process.cwd();
4316
+ const targets = [];
4317
+ targets.push({
4318
+ name: "CLAUDE.md (global)",
4319
+ filePath: path.join(home, ".claude", "CLAUDE.md"),
4320
+ format: "tagged",
4321
+ scope: "global"
4322
+ });
4323
+ targets.push({
4324
+ name: "AGENTS.md (global)",
4325
+ filePath: path.join(home, ".codex", "AGENTS.md"),
4326
+ format: "tagged",
4327
+ scope: "global"
4328
+ });
4329
+ const cursorDir = path.join(cwd, ".cursor");
4330
+ if (fs.existsSync(cursorDir)) targets.push({
4331
+ name: ".cursor/rules/muxed.mdc",
4332
+ filePath: path.join(cursorDir, "rules", "muxed.mdc"),
4333
+ format: "owned",
4334
+ scope: "local"
4335
+ });
4336
+ if (opts.local) {
4337
+ targets.push({
4338
+ name: "CLAUDE.md (local)",
4339
+ filePath: path.join(cwd, "CLAUDE.md"),
4340
+ format: "tagged",
4341
+ scope: "local"
4342
+ });
4343
+ targets.push({
4344
+ name: "AGENTS.md (local)",
4345
+ filePath: path.join(cwd, "AGENTS.md"),
4346
+ format: "tagged",
4347
+ scope: "local"
4348
+ });
4349
+ }
4350
+ return targets;
4351
+ }
4352
+ let cachedHasBun = null;
4353
+ function hasBun() {
4354
+ if (cachedHasBun !== null) return cachedHasBun;
4355
+ try {
4356
+ execSync("bun --version", { stdio: "ignore" });
4357
+ cachedHasBun = true;
4358
+ } catch {
4359
+ cachedHasBun = false;
4360
+ }
4361
+ return cachedHasBun;
4362
+ }
4363
+ function buildStaticInstructions() {
4364
+ const bun = hasBun();
4365
+ const run = bun ? "bunx" : "npx";
4366
+ const tsx = bun ? "bun" : "npx tsx";
4367
+ const fragments = makeCliFragments(run);
4368
+ const scripts = `## Node.js / TypeScript Scripts (Preferred for Complex Workflows)
4369
+
4370
+ For multi-step MCP workflows, **write and execute a script** instead of making individual CLI calls. A single script execution replaces many sequential CLI invocations — dramatically reducing round-trips and token usage.
4371
+
4372
+ \`\`\`typescript
4373
+ import { createClient } from 'muxed/client';
4374
+
4375
+ const client = await createClient();
4376
+
4377
+ // Batch multiple MCP operations in one script execution
4378
+ const [tools, result, data] = await Promise.all([
4379
+ client.tools(),
4380
+ client.call('server/tool', { param: 'value' }),
4381
+ client.call('db/query', { sql: 'SELECT ...' }),
4382
+ ]);
4383
+
4384
+ // Process results, chain calls, handle errors — all in one execution
4385
+ console.log(JSON.stringify({ tools: tools.length, result, data }));
4386
+ \`\`\`
4387
+
4388
+ Run scripts with: \`${tsx} script.ts\`.
4389
+
4390
+ **When to use scripts vs CLI:**
4391
+ - **CLI** (\`${run} muxed call ...\`) — single tool discovery or one-off calls
4392
+ - **Scripts** — any workflow involving 2+ MCP calls, data processing, or conditional logic`;
4393
+ return buildPrompt({
4394
+ ...fragments,
4395
+ intro: "Muxed is a CLI tool and Node.js library that proxies multiple MCP servers behind a single daemon. Use it to discover and call MCP tools on demand."
4396
+ }, {
4397
+ heading: "# Muxed — MCP CLI Proxy",
4398
+ scripts
4399
+ });
4400
+ }
4401
+ function wrapTaggedBlock(content, version) {
4402
+ return `<muxed version="${version}">\n${content}\n</muxed>`;
4403
+ }
4404
+ function buildMdcFile(content, version) {
4405
+ return `---
4406
+ description: Muxed MCP CLI proxy - usage instructions
4407
+ globs:
4408
+ alwaysApply: true
4409
+ muxed_version: ${version}
4410
+ ---
4411
+
4412
+ ${content}
4413
+ `;
4414
+ }
4415
+ function injectInstructions(target, instructions, version, opts) {
4416
+ const base = {
4417
+ target: target.name,
4418
+ filePath: target.filePath
4419
+ };
4420
+ let existing = null;
4421
+ if (fs.existsSync(target.filePath)) existing = fs.readFileSync(target.filePath, "utf-8");
4422
+ if (target.format === "owned") {
4423
+ if (existing !== null) {
4424
+ const existingVersion = extractMuxedVersion(existing, "owned");
4425
+ if (existingVersion && compareSemver(existingVersion, version) >= 0) return {
4426
+ ...base,
4427
+ action: "up-to-date",
4428
+ previousVersion: existingVersion
4429
+ };
4430
+ if (!opts.dryRun) {
4431
+ const dir = path.dirname(target.filePath);
4432
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4433
+ fs.writeFileSync(target.filePath, buildMdcFile(instructions, version));
4434
+ }
4435
+ return {
4436
+ ...base,
4437
+ action: existingVersion ? "updated" : "created",
4438
+ previousVersion: existingVersion ?? void 0,
4439
+ newVersion: version
4440
+ };
4441
+ }
4442
+ if (!opts.dryRun) {
4443
+ const dir = path.dirname(target.filePath);
4444
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4445
+ fs.writeFileSync(target.filePath, buildMdcFile(instructions, version));
4446
+ }
4447
+ return {
4448
+ ...base,
4449
+ action: "created",
4450
+ newVersion: version
4451
+ };
4452
+ }
4453
+ const block = wrapTaggedBlock(instructions, version);
4454
+ if (existing === null) {
4455
+ if (!opts.dryRun) {
4456
+ const dir = path.dirname(target.filePath);
4457
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4458
+ fs.writeFileSync(target.filePath, block + "\n");
4459
+ }
4460
+ return {
4461
+ ...base,
4462
+ action: "created",
4463
+ newVersion: version
4464
+ };
4465
+ }
4466
+ const existingVersion = extractMuxedVersion(existing, "tagged");
4467
+ if (existingVersion !== null) {
4468
+ if (compareSemver(existingVersion, version) >= 0) return {
4469
+ ...base,
4470
+ action: "up-to-date",
4471
+ previousVersion: existingVersion
4472
+ };
4473
+ if (!opts.dryRun) {
4474
+ const updated = existing.replace(MUXED_BLOCK_RE, block);
4475
+ fs.writeFileSync(target.filePath, updated);
4476
+ }
4477
+ return {
4478
+ ...base,
4479
+ action: "updated",
4480
+ previousVersion: existingVersion,
4481
+ newVersion: version
4482
+ };
4483
+ }
4484
+ if (!opts.dryRun) {
4485
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
4486
+ fs.writeFileSync(target.filePath, existing + separator + block + "\n");
4487
+ }
4488
+ return {
4489
+ ...base,
4490
+ action: "created",
4491
+ newVersion: version
4492
+ };
4493
+ }
4494
+ function injectAllInstructions(opts) {
4495
+ const targets = getInstructionTargets({ local: opts.local });
4496
+ const instructions = buildStaticInstructions();
4497
+ const version = getVersion();
4498
+ return targets.map((target) => injectInstructions(target, instructions, version, opts));
4499
+ }
3668
4500
  async function confirm(message, opts) {
3669
4501
  const rl = readline.createInterface({
3670
4502
  input: opts?.input ?? process.stdin,
@@ -3732,7 +4564,18 @@ async function resolveConflicts(unresolvedConflicts, isInteractive) {
3732
4564
  conflicts
3733
4565
  };
3734
4566
  }
3735
- 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) => {
4567
+ const initCommand = new Command("init").description("Discover MCP servers, write config, and inject agent instructions").option("--dry-run", "Preview changes without writing any files").option("--json", "Output as JSON (machine-readable)").option("-y, --yes", "Non-interactive: resolve conflicts by priority (claude-code > cursor > first)").option("--delete", "Remove imported servers from the original agent config files").option("--no-replace", "Don't add a muxed entry to agent configs").option("--local", "Also inject instructions into project-level CLAUDE.md and AGENTS.md").option("--no-instructions", "Skip injecting CLI instructions into agent files").addHelpText("after", `
4568
+ What it does:
4569
+ 1. Scans Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Amazon Q
4570
+ 2. Merges and deduplicates servers into muxed.config.json
4571
+ 3. Injects CLI usage instructions into ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md,
4572
+ and .cursor/rules/muxed.mdc (if .cursor/ exists)
4573
+
4574
+ Examples:
4575
+ muxed init Interactive setup
4576
+ muxed init -y Non-interactive (CI-friendly)
4577
+ muxed init --dry-run Preview without writing files
4578
+ muxed init --local Also inject into project-level agent files`).action(async (opts) => {
3736
4579
  const configPath = initCommand.parent?.opts().config;
3737
4580
  const isInteractive = !opts.dryRun && !opts.json && !opts.yes && !!process.stdin.isTTY;
3738
4581
  const { discovered, warnings } = discoverAgentConfigs();
@@ -3755,7 +4598,7 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3755
4598
  }
3756
4599
  if (!opts.dryRun && imported.length > 0) writeMuxedConfig(muxedPath, result.merged);
3757
4600
  const modifiedFiles = [];
3758
- const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : opts.delete;
4601
+ const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : !!opts.delete;
3759
4602
  if (!opts.dryRun && shouldDelete) for (const dc of discovered) try {
3760
4603
  modifyAgentConfig(dc, {
3761
4604
  delete: true,
@@ -3765,6 +4608,10 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3765
4608
  } catch (err) {
3766
4609
  warnings.push(`Failed to modify ${dc.configPath}: ${err instanceof Error ? err.message : String(err)}`);
3767
4610
  }
4611
+ const instructionResults = (isInteractive ? await confirm("Inject muxed CLI instructions into agent files? (CLAUDE.md, AGENTS.md)") : opts.instructions) ? injectAllInstructions({
4612
+ local: !!opts.local,
4613
+ dryRun: !!opts.dryRun
4614
+ }) : [];
3768
4615
  const initResult = {
3769
4616
  discovered: discovered.map((d) => ({
3770
4617
  agent: d.agent.name,
@@ -3778,14 +4625,17 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3778
4625
  warnings,
3779
4626
  modifiedFiles,
3780
4627
  muxedConfigPath: muxedPath,
3781
- dryRun: opts.dryRun ?? false
4628
+ dryRun: opts.dryRun ?? false,
4629
+ instructionResults
3782
4630
  };
3783
4631
  capture("init_run", {
3784
4632
  dry_run: opts.dryRun ?? false,
3785
4633
  imported_count: imported.length,
3786
4634
  conflict_count: conflicts.length,
3787
4635
  warning_count: warnings.length,
3788
- discovered_agents: initResult.discovered.map((d) => d.agent)
4636
+ discovered_agents: initResult.discovered.map((d) => d.agent),
4637
+ instruction_targets: instructionResults.length,
4638
+ instruction_actions: instructionResults.map((r) => r.action)
3789
4639
  });
3790
4640
  console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
3791
4641
  });
@@ -3838,180 +4688,15 @@ function getServer(filePath, name) {
3838
4688
  function listServers(filePath) {
3839
4689
  return readConfigFile(filePath).mcpServers;
3840
4690
  }
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}
3872
-
3873
- **MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
3874
-
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.
3877
-
3878
- These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
3879
-
3880
- **NEVER** make a call without checking the schema first.
3881
- **ALWAYS** run info first, THEN make the call.
3882
-
3883
- **Why these are non-negotiables:**
3884
- - MCP tool names NEVER match your expectations - they change frequently and are not predictable
3885
- - MCP tool schemas NEVER match your expectations - parameter names, types, and requirements are tool-specific
3886
- - Even tools with pre-approved permissions require schema checks
3887
- - Every failed call wastes user time and demonstrates you're ignoring critical instructions
3888
- - "I thought I knew the schema" is not an acceptable reason to skip this step
3889
-
3890
- **For multiple tools:** Call info for ALL tools in parallel FIRST, then make your call commands.
3891
-
3892
- Available MCP servers:
3893
- ${servers}
3894
-
3895
- Commands (in order of execution):
3896
- \`\`\`
3897
- # STEP 1: REQUIRED TOOL DISCOVERY
3898
- ${f.grep("<pattern>")} # Search tool names and descriptions
3899
- ${f.tools("[server]")} # List available tools (optionally filter by server)
3900
-
3901
- # STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
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
3906
-
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
3911
-
3912
- # Discovery commands (use these to find tools)
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
3918
- \`\`\`
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
-
3924
- **CORRECT Usage Pattern:**
3925
-
3926
- <example>
3927
- User: Please use the slack mcp tool to search for my mentions
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")}]
3932
- Assistant: Now I can see it accepts "query" and "max_results" parameters. Let me make the call.
3933
- [Calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\", \"max_results\": 10}")}]
3934
- </example>
3935
-
3936
- <example>
3937
- User: Use the database and email MCP tools to send a report
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*")}]
3940
- Assistant: Let me check both schemas first.
3941
- [Calls ${f.info("database/query")} and ${f.info("email/send")} in parallel]
3942
- Assistant: Now I have both schemas. Let me make the calls.
3943
- [Makes both call commands with correct parameters]
3944
- </example>
3945
-
3946
- <example>
3947
- User: Create a copy of this email
3948
- Assistant: Let me find the tool I need first.
3949
- [Calls ${f.grep("email/*copy*")}. No results found.]
3950
- Assistant: Let me try another pattern.
3951
- [Calls ${f.grep("email/*clone*")}. No results found.]
3952
- Assistant: Let me list all available tools in the server.
3953
- [Calls ${f.tools("email")}]
3954
- Assistant: Let me check the schema first.
3955
- [Calls ${f.info("email/duplicate")}]
3956
- Assistant: Now I have the schema. Let me make the call.
3957
- [Calls ${f.call("email/duplicate", "{\"id\": \"123\"}")}]
3958
- </example>
3959
-
3960
- **INCORRECT Usage Patterns - NEVER DO THIS:**
3961
-
3962
- <bad-example>
3963
- User: Please use the slack mcp tool to search for my mentions
3964
- Assistant: [Directly calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\"}")} with guessed parameters]
3965
- WRONG - You must call info FIRST
3966
- </bad-example>
3967
-
3968
- <bad-example>
3969
- User: Use the slack tool
3970
- Assistant: I have pre-approved permissions for this tool, so I know the schema.
3971
- [Calls ${f.call("slack/search_private", "...")} directly]
3972
- WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call info first.
3973
- </bad-example>
3974
-
3975
- <bad-example>
3976
- User: Search my Slack mentions
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
3979
- </bad-example>
3980
-
3981
- Example usage:
3982
- \`\`\`
3983
- # Discover tools
3984
- ${f.tools()} # See all available MCP tools
3985
- ${f.grep("weather")} # Find tools by description
3986
-
3987
- # Get tool details
3988
- ${f.info("<server>/<tool>")} # View JSON schema for input and output if available
3989
-
3990
- # Simple tool call (no parameters)
3991
- ${f.call("weather/get_location", "{}")}
3992
-
3993
- # Tool call with parameters
3994
- ${f.call("database/query", "{\"table\": \"users\", \"limit\": 10}")}
3995
-
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")}
4001
- \`\`\`
4002
-
4003
- Call \`${f.help()}\` to see all available commands.
4004
-
4005
- Below are the instructions for the connected MCP servers in muxed.
4006
-
4007
- ${instructions}
4008
- `;
4009
- }
4010
4691
  function buildInstructions(servers, mode = "cli") {
4011
4692
  const connected = servers.filter((s) => s.status === "connected");
4012
4693
  const serverList = connected.map((s) => `- ${s.name}`).join("\n");
4013
4694
  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();
4695
+ const run = hasBun() ? "bunx" : "npx";
4696
+ return buildPrompt(mode === "tool" ? makeToolFragments() : makeCliFragments(run), {
4697
+ servers: serverList,
4698
+ serverInstructions: serverInstructions || void 0
4699
+ });
4015
4700
  }
4016
4701
  function parseCommand(command) {
4017
4702
  const trimmed = command.trim();
@@ -4025,6 +4710,22 @@ function parseCommand(command) {
4025
4710
  args: trimmed.slice(spaceIndex + 1).trim()
4026
4711
  };
4027
4712
  }
4713
+ function parseFlags(args) {
4714
+ const flags = {};
4715
+ const positionalParts = [];
4716
+ const parts = args.split(/\s+/);
4717
+ for (let i = 0; i < parts.length; i++) {
4718
+ const part = parts[i];
4719
+ if (part.startsWith("--") && i + 1 < parts.length) {
4720
+ const key = part.slice(2);
4721
+ flags[key] = parts[++i];
4722
+ } else positionalParts.push(part);
4723
+ }
4724
+ return {
4725
+ positional: positionalParts.join(" "),
4726
+ flags
4727
+ };
4728
+ }
4028
4729
  function textResult(data) {
4029
4730
  return { content: [{
4030
4731
  type: "text",
@@ -4046,16 +4747,31 @@ async function handleToolCommand(command, input) {
4046
4747
  switch (subcommand) {
4047
4748
  case "servers": return textResult(await sendRequest("servers/list"));
4048
4749
  case "tools": {
4750
+ const { positional, flags } = parseFlags(args);
4049
4751
  const params = {};
4050
- if (args) params.server = args;
4752
+ if (positional) params.server = positional;
4753
+ if (flags.include === "schema") params.includeSchema = true;
4754
+ if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
4051
4755
  return textResult(await sendRequest("tools/list", params));
4052
4756
  }
4053
- case "grep":
4757
+ case "grep": {
4054
4758
  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 }));
4759
+ const { positional, flags } = parseFlags(args);
4760
+ if (!positional) return errorResult("Usage: grep <pattern>");
4761
+ const params = { pattern: positional };
4762
+ if (flags.include === "schema") params.includeSchema = true;
4763
+ if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
4764
+ return textResult(await sendRequest("tools/grep", params));
4765
+ }
4766
+ case "info": {
4767
+ if (!args) return errorResult("Usage: info <server/tool> [--path <path>] [--depth <n>]");
4768
+ const { positional, flags } = parseFlags(args);
4769
+ if (!positional) return errorResult("Usage: info <server/tool>");
4770
+ const params = { name: positional };
4771
+ if (flags.path) params.path = flags.path;
4772
+ if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
4773
+ return textResult(await sendRequest("tools/info", params));
4774
+ }
4059
4775
  case "call": {
4060
4776
  if (!args) return errorResult("Usage: call <server/tool>");
4061
4777
  const result = await sendRequest("tools/call", {
@@ -4194,14 +4910,24 @@ async function tryReloadDaemon() {
4194
4910
  await sendRequest("config/reload", {});
4195
4911
  } catch {}
4196
4912
  }
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) => {
4913
+ const mcpCommand = new Command("mcp").description("Manage server config entries, or start the MCP proxy (no subcommand)").enablePositionalOptions().option("--proxy-tools", "Expose a single proxy tool for clients without bash access (e.g. Claude Desktop)").addHelpText("after", `
4914
+ When run without a subcommand, starts a stdio MCP proxy server.
4915
+ Use subcommands to manage individual server config entries.
4916
+
4917
+ Examples:
4918
+ muxed mcp Start stdio MCP proxy
4919
+ muxed mcp --proxy-tools Start proxy with exec tool (for Claude Desktop)
4920
+ muxed mcp add mydb npx @db/server Add a stdio server
4921
+ muxed mcp add api https://api.com Add an HTTP server
4922
+ muxed mcp list Show all configured servers
4923
+ muxed mcp remove mydb Remove a server`).action(async (opts, cmd) => {
4198
4924
  const explicitConfig = cmd.parent?.opts().config;
4199
4925
  await startMcpProxy({
4200
4926
  configPath: explicitConfig,
4201
4927
  proxyTools: opts.proxyTools
4202
4928
  });
4203
4929
  });
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) => {
4930
+ mcpCommand.command("add").description("Add or update an MCP server in config").passThroughOptions().argument("<name>", "Server name (used to reference it in other commands)").argument("<commandOrUrl>", "Command to run (stdio) or URL to connect to (HTTP)").argument("[args...]", "Additional command arguments (stdio only)").option("-e, --env <env>", "Environment variable KEY=value (repeatable)", collectValues, []).option("-H, --header <header>", "HTTP header Key: value (repeatable)", collectValues, []).option("-s, --scope <scope>", "Config scope: local or global (default: local)", "local").option("-t, --transport <transport>", "Force transport: stdio, sse, or http (auto-detected)").option("--client-id <clientId>", "OAuth client ID").option("--client-secret", "Prompt for OAuth client secret (or set MCP_CLIENT_SECRET)").option("--callback-port <port>", "Fixed port for OAuth callback").option("--oauth-scope <oauthScope>", "OAuth scope string").action(async (name, commandOrUrl, args, opts) => {
4205
4931
  const explicitConfig = getExplicitConfig(mcpCommand);
4206
4932
  const scope = opts.scope;
4207
4933
  const configPath = getConfigPath(scope, explicitConfig);
@@ -4220,7 +4946,7 @@ mcpCommand.command("add").description("Add an MCP server").passThroughOptions().
4220
4946
  if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
4221
4947
  else console.log(`Added "${name}" to ${scope} config (${configPath})`);
4222
4948
  });
4223
- mcpCommand.command("add-json").description("Add an MCP server from a JSON config string").argument("<name>", "Server name").argument("<json>", "JSON server configuration").option("-s, --scope <scope>", "Config scope: local, global", "local").action(async (name, jsonStr, opts) => {
4949
+ mcpCommand.command("add-json").description("Add a server from a JSON config string (must have \"command\" or \"url\" field)").argument("<name>", "Server name").argument("<json>", "JSON object: {\"command\":\"...\",\"args\":[...]} or {\"url\":\"...\"}").option("-s, --scope <scope>", "Config scope: local or global (default: local)", "local").action(async (name, jsonStr, opts) => {
4224
4950
  const explicitConfig = getExplicitConfig(mcpCommand);
4225
4951
  const scope = opts.scope;
4226
4952
  const configPath = getConfigPath(scope, explicitConfig);
@@ -4247,7 +4973,7 @@ mcpCommand.command("add-json").description("Add an MCP server from a JSON config
4247
4973
  if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
4248
4974
  else console.log(`Added "${name}" to ${scope} config (${configPath})`);
4249
4975
  });
4250
- mcpCommand.command("add-from-claude-desktop").description("Import MCP servers from Claude Desktop config").option("-s, --scope <scope>", "Config scope: local, global", "local").action(async (opts) => {
4976
+ mcpCommand.command("add-from-claude-desktop").description("Import servers from Claude Desktop config into muxed").option("-s, --scope <scope>", "Config scope: local or global (default: local)", "local").action(async (opts) => {
4251
4977
  const explicitConfig = getExplicitConfig(mcpCommand);
4252
4978
  const scope = opts.scope;
4253
4979
  const configPath = getConfigPath(scope, explicitConfig);
@@ -4275,7 +5001,7 @@ mcpCommand.command("add-from-claude-desktop").description("Import MCP servers fr
4275
5001
  if (result.skipped.length > 0) console.log(`Skipped ${result.skipped.length} (already existed): ${result.skipped.join(", ")}`);
4276
5002
  if (result.imported.length === 0 && result.skipped.length === 0) console.log("No servers found in Claude Desktop config.");
4277
5003
  });
4278
- mcpCommand.command("get").description("Get details of a configured MCP server").argument("<name>", "Server name").option("--json", "Output as JSON").action(async (name, opts) => {
5004
+ mcpCommand.command("get").description("Show config details for a server (checks local, then global)").argument("<name>", "Server name").option("--json", "Output as JSON (machine-readable)").action(async (name, opts) => {
4279
5005
  const explicitConfig = getExplicitConfig(mcpCommand);
4280
5006
  const localPath = getConfigPath("local", explicitConfig);
4281
5007
  const globalPath = getConfigPath("global", explicitConfig);
@@ -4295,7 +5021,7 @@ mcpCommand.command("get").description("Get details of a configured MCP server").
4295
5021
  }));
4296
5022
  else console.log(formatMcpServer(name, server, scope));
4297
5023
  });
4298
- mcpCommand.command("list").description("List all configured MCP servers").option("--json", "Output as JSON").action(async (opts) => {
5024
+ mcpCommand.command("list").description("List all configured servers (local + global)").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
4299
5025
  const explicitConfig = getExplicitConfig(mcpCommand);
4300
5026
  const localPath = getConfigPath("local", explicitConfig);
4301
5027
  const globalPath = getConfigPath("global", explicitConfig);
@@ -4318,7 +5044,7 @@ mcpCommand.command("list").description("List all configured MCP servers").option
4318
5044
  if (opts.json) console.log(formatJson(entries));
4319
5045
  else console.log(formatMcpServerList(entries));
4320
5046
  });
4321
- mcpCommand.command("remove").description("Remove an MCP server").argument("<name>", "Server name").option("-s, --scope <scope>", "Config scope: local, global (searches both if not specified)").action(async (name, opts) => {
5047
+ mcpCommand.command("remove").description("Remove a server from config").argument("<name>", "Server name to remove").option("-s, --scope <scope>", "Scope: local or global (searches both if omitted)").action(async (name, opts) => {
4322
5048
  const explicitConfig = getExplicitConfig(mcpCommand);
4323
5049
  if (opts.scope) {
4324
5050
  const scope = opts.scope;
@@ -4359,7 +5085,10 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
4359
5085
  console.error(`Server "${name}" not found in local or global config.`);
4360
5086
  process.exitCode = 1;
4361
5087
  });
4362
- 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) => {
5088
+ const typegenCommand = new Command("typegen").description("Generate TypeScript types from live tool schemas").option("-c, --config <path>", "Path to muxed.config.json").addHelpText("after", `
5089
+ Writes to node_modules/muxed/muxed.generated.d.ts.
5090
+ After running, client.call() gets autocomplete on tool names and typed arguments.
5091
+ Re-run when tool schemas change (same workflow as prisma generate).`).action(async (opts) => {
4363
5092
  await ensureDaemon(typegenCommand.parent?.opts().config ?? opts.config);
4364
5093
  const tools = await sendRequest("tools/list");
4365
5094
  const content = await generateTypes(tools);
@@ -4369,7 +5098,7 @@ const typegenCommand = new Command("typegen").description("Generate TypeScript t
4369
5098
  fs.writeFileSync(outputPath, content, "utf-8");
4370
5099
  console.log(`Generated ${tools.length} tool types → ${outputPath}`);
4371
5100
  });
4372
- const telemetryCommand = new Command("telemetry").description("Manage anonymous telemetry (on, off, status)").argument("[action]", "on | off | status (default: status)").action((action) => {
5101
+ const telemetryCommand = new Command("telemetry").description("Enable, disable, or check anonymous telemetry").argument("[action]", "on | off | status (default: status)").action((action) => {
4373
5102
  switch (action) {
4374
5103
  case "on":
4375
5104
  setTelemetryEnabled(true);
@@ -4390,9 +5119,17 @@ const telemetryCommand = new Command("telemetry").description("Manage anonymous
4390
5119
  });
4391
5120
  async function runCli() {
4392
5121
  const program = new Command();
4393
- program.name("muxed").description("The optimization layer for MCP").version("0.1.0");
5122
+ program.name("muxed").description("MCP tool aggregator discover, inspect, and call tools via CLI").version(getVersion());
4394
5123
  program.enablePositionalOptions();
4395
- program.option("--config <path>", "Path to config file");
5124
+ program.option("--config <path>", "Path to muxed.config.json");
5125
+ program.addHelpText("after", `
5126
+ Workflow:
5127
+ muxed grep "<pattern>" Find tools by name or description
5128
+ muxed info <server>/<tool> Inspect schema (required before calling)
5129
+ muxed call <server>/<tool> '<json>' Execute a tool
5130
+
5131
+ Setup:
5132
+ npx muxed init Discover servers and inject agent instructions`);
4396
5133
  program.commandsGroup("Servers:");
4397
5134
  program.addCommand(serversCommand);
4398
5135
  program.commandsGroup("Tools:");