muxed 0.2.1 → 0.2.2

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) {
@@ -3016,20 +3367,27 @@ async function shutdown() {
3016
3367
  if (_client) await _client.shutdown();
3017
3368
  } catch {}
3018
3369
  }
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) => {
3370
+ 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").option("--include <fields>", "Include additional fields (e.g. \"schema\")").option("--depth <n>", "Schema collapse depth (requires --include schema)", parseInt).action(async (server, opts) => {
3020
3371
  const configPath = toolsCommand.parent?.opts().config;
3021
3372
  await ensureDaemon(configPath);
3022
- const result = await sendRequest("tools/list", server ? { server } : void 0);
3373
+ const params = {};
3374
+ if (server) params.server = server;
3375
+ if (opts.include === "schema") params.includeSchema = true;
3376
+ if (opts.depth !== void 0) params.schemaDepth = opts.depth;
3377
+ const result = await sendRequest("tools/list", params);
3023
3378
  capture("tools_listed", {
3024
3379
  filtered_by_server: !!server,
3025
3380
  tool_count: result.length
3026
3381
  });
3027
3382
  console.log(opts.json ? formatJson(result) : formatTools(result));
3028
3383
  });
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) => {
3384
+ 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").option("--path <path>", "Extract a subtree of the input schema (e.g. \"filters.tags\")").option("--depth <n>", "Collapse schema at this depth", parseInt).action(async (serverTool, opts) => {
3030
3385
  const configPath = infoCommand.parent?.opts().config;
3031
3386
  await ensureDaemon(configPath);
3032
- const result = await sendRequest("tools/info", { name: serverTool });
3387
+ const params = { name: serverTool };
3388
+ if (opts.path) params.path = opts.path;
3389
+ if (opts.depth !== void 0) params.schemaDepth = opts.depth;
3390
+ const result = await sendRequest("tools/info", params);
3033
3391
  if (opts.json) console.log(formatJson(result));
3034
3392
  else {
3035
3393
  const slashIndex = serverTool.indexOf("/");
@@ -3187,10 +3545,13 @@ const callCommand = new Command("call").description("Execute a tool with JSON ar
3187
3545
  process.exit(1);
3188
3546
  }
3189
3547
  });
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) => {
3548
+ 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").option("--include <fields>", "Include additional fields (e.g. \"schema\")").option("--depth <n>", "Schema collapse depth (requires --include schema)", parseInt).action(async (pattern, opts) => {
3191
3549
  const configPath = grepCommand.parent?.opts().config;
3192
3550
  await ensureDaemon(configPath);
3193
- const result = await sendRequest("tools/grep", { pattern });
3551
+ const params = { pattern };
3552
+ if (opts.include === "schema") params.includeSchema = true;
3553
+ if (opts.depth !== void 0) params.schemaDepth = opts.depth;
3554
+ const result = await sendRequest("tools/grep", params);
3194
3555
  capture("tools_searched", { result_count: result.length });
3195
3556
  console.log(opts.json ? formatJson(result) : formatTools(result));
3196
3557
  });
@@ -3665,6 +4026,261 @@ function getMuxedConfigPath(scope, explicitPath) {
3665
4026
  if (scope === "local") return path.join(process.cwd(), "muxed.config.json");
3666
4027
  return path.join(home, ".muxed", "config.json");
3667
4028
  }
4029
+ let cached = null;
4030
+ function getVersion() {
4031
+ if (cached) return cached;
4032
+ const dir = path.dirname(fileURLToPath(import.meta.url));
4033
+ for (const rel of ["../package.json", "../../package.json"]) {
4034
+ const p = path.resolve(dir, rel);
4035
+ try {
4036
+ const pkg = JSON.parse(fs.readFileSync(p, "utf-8"));
4037
+ if (pkg.name === "muxed" && pkg.version) {
4038
+ cached = pkg.version;
4039
+ return cached;
4040
+ }
4041
+ } catch {}
4042
+ }
4043
+ return "0.0.0";
4044
+ }
4045
+ function compareSemver(a, b) {
4046
+ const pa = a.split(".").map(Number);
4047
+ const pb = b.split(".").map(Number);
4048
+ for (let i = 0; i < 3; i++) {
4049
+ const na = pa[i] ?? 0;
4050
+ const nb = pb[i] ?? 0;
4051
+ if (na < nb) return -1;
4052
+ if (na > nb) return 1;
4053
+ }
4054
+ return 0;
4055
+ }
4056
+ const MUXED_BLOCK_RE = /<muxed\s+version="([^"]+)">\n?([\s\S]*?)\n?<\/muxed>/;
4057
+ const MDC_VERSION_RE = /muxed_version:\s*(.+)/;
4058
+ function extractMuxedVersion(content, format) {
4059
+ if (format === "tagged") return content.match(MUXED_BLOCK_RE)?.[1]?.trim() ?? null;
4060
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
4061
+ if (!fmMatch) return null;
4062
+ return fmMatch[1].match(MDC_VERSION_RE)?.[1]?.trim() ?? null;
4063
+ }
4064
+ function getInstructionTargets(opts) {
4065
+ const home = os.homedir();
4066
+ const cwd = process.cwd();
4067
+ const targets = [];
4068
+ targets.push({
4069
+ name: "CLAUDE.md (global)",
4070
+ filePath: path.join(home, ".claude", "CLAUDE.md"),
4071
+ format: "tagged",
4072
+ scope: "global"
4073
+ });
4074
+ targets.push({
4075
+ name: "AGENTS.md (global)",
4076
+ filePath: path.join(home, ".codex", "AGENTS.md"),
4077
+ format: "tagged",
4078
+ scope: "global"
4079
+ });
4080
+ const cursorDir = path.join(cwd, ".cursor");
4081
+ if (fs.existsSync(cursorDir)) targets.push({
4082
+ name: ".cursor/rules/muxed.mdc",
4083
+ filePath: path.join(cursorDir, "rules", "muxed.mdc"),
4084
+ format: "owned",
4085
+ scope: "local"
4086
+ });
4087
+ if (opts.local) {
4088
+ targets.push({
4089
+ name: "CLAUDE.md (local)",
4090
+ filePath: path.join(cwd, "CLAUDE.md"),
4091
+ format: "tagged",
4092
+ scope: "local"
4093
+ });
4094
+ targets.push({
4095
+ name: "AGENTS.md (local)",
4096
+ filePath: path.join(cwd, "AGENTS.md"),
4097
+ format: "tagged",
4098
+ scope: "local"
4099
+ });
4100
+ }
4101
+ return targets;
4102
+ }
4103
+ let cachedHasBun = null;
4104
+ function hasBun() {
4105
+ if (cachedHasBun !== null) return cachedHasBun;
4106
+ try {
4107
+ execSync("bun --version", { stdio: "ignore" });
4108
+ cachedHasBun = true;
4109
+ } catch {
4110
+ cachedHasBun = false;
4111
+ }
4112
+ return cachedHasBun;
4113
+ }
4114
+ function buildStaticInstructions() {
4115
+ const bun = hasBun();
4116
+ const run = bun ? "bunx" : "npx";
4117
+ return `# Muxed — MCP Server Aggregator
4118
+
4119
+ Muxed is a CLI tool and Node.js library that aggregates multiple MCP servers behind a single daemon. Use it to discover and call MCP tools on demand.
4120
+
4121
+ ## CLI Usage
4122
+
4123
+ ### Mandatory Workflow
4124
+
4125
+ **ALWAYS follow this order — never skip the inspect step.**
4126
+
4127
+ 1. **Discover** tools:
4128
+ \`\`\`
4129
+ ${run} muxed grep "<pattern>" # Search tool names and descriptions
4130
+ ${run} muxed tools [server] # List available tools
4131
+ \`\`\`
4132
+
4133
+ 2. **Inspect** schema (REQUIRED before calling):
4134
+ \`\`\`
4135
+ ${run} muxed info <server>/<tool> # View tool JSON schema
4136
+ \`\`\`
4137
+
4138
+ 3. **Call** with correct parameters:
4139
+ \`\`\`
4140
+ ${run} muxed call <server>/<tool> '<json>' # Execute a tool
4141
+ \`\`\`
4142
+
4143
+ Tool names and parameter schemas change frequently and cannot be guessed. Every call without inspecting the schema first will fail.
4144
+
4145
+ ### Additional Commands
4146
+
4147
+ \`\`\`
4148
+ ${run} muxed servers # List connected MCP servers
4149
+ ${run} muxed resources [server] # List MCP resources
4150
+ ${run} muxed read <server>/<resource> # Read an MCP resource
4151
+ ${run} muxed call <s>/<t> '<j>' --dry-run # Validate args without executing
4152
+ ${run} muxed call <s>/<t> '<j>' --fields "a,b" # Extract specific fields
4153
+ ${run} muxed -h # Full command reference
4154
+ \`\`\`
4155
+
4156
+ ### Error Handling
4157
+
4158
+ - If a tool call fails, read the error — it includes suggestions and similar tool names.
4159
+ - Use \`--dry-run\` to validate arguments before executing destructive tools.
4160
+
4161
+ ## Node.js / TypeScript Scripts (Preferred for Complex Workflows)
4162
+
4163
+ 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.
4164
+
4165
+ \`\`\`typescript
4166
+ import { createClient } from 'muxed/client';
4167
+
4168
+ const client = await createClient();
4169
+
4170
+ // Batch multiple MCP operations in one script execution
4171
+ const tools = await client.tools();
4172
+ const result = await client.call('server/tool', { param: 'value' });
4173
+ const data = await client.call('db/query', { sql: 'SELECT ...' });
4174
+
4175
+ // Process results, chain calls, handle errors — all in one execution
4176
+ console.log(JSON.stringify({ tools: tools.length, result, data }));
4177
+ \`\`\`
4178
+
4179
+ Run scripts with: \`${bun ? "bun" : "npx tsx"} script.ts\`.
4180
+
4181
+ **When to use scripts vs CLI:**
4182
+ - **CLI** (\`${run} muxed call ...\`) — single tool discovery or one-off calls
4183
+ - **Scripts** — any workflow involving 2+ MCP calls, data processing, or conditional logic`;
4184
+ }
4185
+ function wrapTaggedBlock(content, version) {
4186
+ return `<muxed version="${version}">\n${content}\n</muxed>`;
4187
+ }
4188
+ function buildMdcFile(content, version) {
4189
+ return `---
4190
+ description: Muxed MCP server aggregator - CLI usage instructions
4191
+ globs:
4192
+ alwaysApply: true
4193
+ muxed_version: ${version}
4194
+ ---
4195
+
4196
+ ${content}
4197
+ `;
4198
+ }
4199
+ function injectInstructions(target, instructions, version, opts) {
4200
+ const base = {
4201
+ target: target.name,
4202
+ filePath: target.filePath
4203
+ };
4204
+ let existing = null;
4205
+ if (fs.existsSync(target.filePath)) existing = fs.readFileSync(target.filePath, "utf-8");
4206
+ if (target.format === "owned") {
4207
+ if (existing !== null) {
4208
+ const existingVersion = extractMuxedVersion(existing, "owned");
4209
+ if (existingVersion && compareSemver(existingVersion, version) >= 0) return {
4210
+ ...base,
4211
+ action: "up-to-date",
4212
+ previousVersion: existingVersion
4213
+ };
4214
+ if (!opts.dryRun) {
4215
+ const dir = path.dirname(target.filePath);
4216
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4217
+ fs.writeFileSync(target.filePath, buildMdcFile(instructions, version));
4218
+ }
4219
+ return {
4220
+ ...base,
4221
+ action: existingVersion ? "updated" : "created",
4222
+ previousVersion: existingVersion ?? void 0,
4223
+ newVersion: version
4224
+ };
4225
+ }
4226
+ if (!opts.dryRun) {
4227
+ const dir = path.dirname(target.filePath);
4228
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4229
+ fs.writeFileSync(target.filePath, buildMdcFile(instructions, version));
4230
+ }
4231
+ return {
4232
+ ...base,
4233
+ action: "created",
4234
+ newVersion: version
4235
+ };
4236
+ }
4237
+ const block = wrapTaggedBlock(instructions, version);
4238
+ if (existing === null) {
4239
+ if (!opts.dryRun) {
4240
+ const dir = path.dirname(target.filePath);
4241
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
4242
+ fs.writeFileSync(target.filePath, block + "\n");
4243
+ }
4244
+ return {
4245
+ ...base,
4246
+ action: "created",
4247
+ newVersion: version
4248
+ };
4249
+ }
4250
+ const existingVersion = extractMuxedVersion(existing, "tagged");
4251
+ if (existingVersion !== null) {
4252
+ if (compareSemver(existingVersion, version) >= 0) return {
4253
+ ...base,
4254
+ action: "up-to-date",
4255
+ previousVersion: existingVersion
4256
+ };
4257
+ if (!opts.dryRun) {
4258
+ const updated = existing.replace(MUXED_BLOCK_RE, block);
4259
+ fs.writeFileSync(target.filePath, updated);
4260
+ }
4261
+ return {
4262
+ ...base,
4263
+ action: "updated",
4264
+ previousVersion: existingVersion,
4265
+ newVersion: version
4266
+ };
4267
+ }
4268
+ if (!opts.dryRun) {
4269
+ const separator = existing.endsWith("\n") ? "\n" : "\n\n";
4270
+ fs.writeFileSync(target.filePath, existing + separator + block + "\n");
4271
+ }
4272
+ return {
4273
+ ...base,
4274
+ action: "created",
4275
+ newVersion: version
4276
+ };
4277
+ }
4278
+ function injectAllInstructions(opts) {
4279
+ const targets = getInstructionTargets({ local: opts.local });
4280
+ const instructions = buildStaticInstructions();
4281
+ const version = getVersion();
4282
+ return targets.map((target) => injectInstructions(target, instructions, version, opts));
4283
+ }
3668
4284
  async function confirm(message, opts) {
3669
4285
  const rl = readline.createInterface({
3670
4286
  input: opts?.input ?? process.stdin,
@@ -3732,7 +4348,7 @@ async function resolveConflicts(unresolvedConflicts, isInteractive) {
3732
4348
  conflicts
3733
4349
  };
3734
4350
  }
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) => {
4351
+ 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("--delete", "Remove imported servers from original agent configs").option("--no-replace", "Don't add muxed entry to agent configs").option("--local", "Also inject instructions into local agent files (CLAUDE.md, AGENTS.md)").option("--no-instructions", "Skip injecting CLI instructions into agent files").action(async (opts) => {
3736
4352
  const configPath = initCommand.parent?.opts().config;
3737
4353
  const isInteractive = !opts.dryRun && !opts.json && !opts.yes && !!process.stdin.isTTY;
3738
4354
  const { discovered, warnings } = discoverAgentConfigs();
@@ -3755,7 +4371,7 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3755
4371
  }
3756
4372
  if (!opts.dryRun && imported.length > 0) writeMuxedConfig(muxedPath, result.merged);
3757
4373
  const modifiedFiles = [];
3758
- const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : opts.delete;
4374
+ const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : !!opts.delete;
3759
4375
  if (!opts.dryRun && shouldDelete) for (const dc of discovered) try {
3760
4376
  modifyAgentConfig(dc, {
3761
4377
  delete: true,
@@ -3765,6 +4381,10 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3765
4381
  } catch (err) {
3766
4382
  warnings.push(`Failed to modify ${dc.configPath}: ${err instanceof Error ? err.message : String(err)}`);
3767
4383
  }
4384
+ const instructionResults = (isInteractive ? await confirm("Inject muxed CLI instructions into agent files? (CLAUDE.md, AGENTS.md)") : opts.instructions) ? injectAllInstructions({
4385
+ local: !!opts.local,
4386
+ dryRun: !!opts.dryRun
4387
+ }) : [];
3768
4388
  const initResult = {
3769
4389
  discovered: discovered.map((d) => ({
3770
4390
  agent: d.agent.name,
@@ -3778,14 +4398,17 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3778
4398
  warnings,
3779
4399
  modifiedFiles,
3780
4400
  muxedConfigPath: muxedPath,
3781
- dryRun: opts.dryRun ?? false
4401
+ dryRun: opts.dryRun ?? false,
4402
+ instructionResults
3782
4403
  };
3783
4404
  capture("init_run", {
3784
4405
  dry_run: opts.dryRun ?? false,
3785
4406
  imported_count: imported.length,
3786
4407
  conflict_count: conflicts.length,
3787
4408
  warning_count: warnings.length,
3788
- discovered_agents: initResult.discovered.map((d) => d.agent)
4409
+ discovered_agents: initResult.discovered.map((d) => d.agent),
4410
+ instruction_targets: instructionResults.length,
4411
+ instruction_actions: instructionResults.map((r) => r.action)
3789
4412
  });
3790
4413
  console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
3791
4414
  });
@@ -3842,7 +4465,10 @@ const cliFragments = {
3842
4465
  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
4466
  grep: (p) => `npx muxed grep "${p}"`,
3844
4467
  tools: (s) => s ? `npx muxed tools ${s}` : "npx muxed tools",
4468
+ toolsSchema: (s) => s ? `npx muxed tools ${s} --include schema` : "npx muxed tools --include schema",
3845
4469
  info: (n) => `npx muxed info ${n}`,
4470
+ infoDepth: (n, d) => `npx muxed info ${n} --depth ${d}`,
4471
+ infoPath: (n, p) => `npx muxed info ${n} --path ${p}`,
3846
4472
  call: (n, j) => `npx muxed call ${n} '${j}'`,
3847
4473
  callStdin: (n) => `npx muxed call ${n} -`,
3848
4474
  callDryRun: (n, j) => `npx muxed call ${n} '${j}' --dry-run`,
@@ -3856,7 +4482,10 @@ const toolFragments = {
3856
4482
  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
4483
  grep: (p) => `muxed:exec({ "command": "grep ${p}" })`,
3858
4484
  tools: (s) => s ? `muxed:exec({ "command": "tools ${s}" })` : `muxed:exec({ "command": "tools" })`,
4485
+ toolsSchema: (s) => s ? `muxed:exec({ "command": "tools ${s} --include schema" })` : `muxed:exec({ "command": "tools --include schema" })`,
3859
4486
  info: (n) => `muxed:exec({ "command": "info ${n}" })`,
4487
+ infoDepth: (n, d) => `muxed:exec({ "command": "info ${n} --depth ${d}" })`,
4488
+ infoPath: (n, p) => `muxed:exec({ "command": "info ${n} --path ${p}" })`,
3860
4489
  call: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
3861
4490
  callStdin: (n) => `muxed:exec({ "command": "call ${n}", "input": { ... } })`,
3862
4491
  callDryRun: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
@@ -3898,15 +4527,23 @@ Commands (in order of execution):
3898
4527
  ${f.grep("<pattern>")} # Search tool names and descriptions
3899
4528
  ${f.tools("[server]")} # List available tools (optionally filter by server)
3900
4529
 
3901
- # STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
3902
- ${f.info("<server>/<tool>")} # REQUIRED before ANY call - View JSON schema
4530
+ # STEP 2: GET SCHEMA (choose one approach)
4531
+ # Option A: Include schemas in tool listing (auto-collapses to fit 48k budget)
4532
+ ${f.toolsSchema("[server]")} # List tools with schemas included
4533
+ # Option B: Get full schema for a specific tool
4534
+ ${f.info("<server>/<tool>")} # View full JSON schema for one tool
4535
+
4536
+ # STEP 2b: PROGRESSIVE SCHEMA EXPLORATION (for large schemas)
4537
+ ${f.infoDepth("<server>/<tool>", 1)} # Collapse schema at depth 1 (top-level overview)
4538
+ ${f.infoPath("<server>/<tool>", "filters")} # Extract just the 'filters' subtree
4539
+ ${f.infoPath("<server>/<tool>", "filters.tags.items")} # Drill deeper into nested schemas
3903
4540
 
3904
4541
  # STEP 3: OPTIONAL - Validate arguments before calling (dry-run)
3905
4542
  ${f.callDryRun("<server>/<tool>", "<json>")} # Validate args without executing
3906
4543
 
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)
4544
+ # STEP 4: Only after getting the schema, make the call
4545
+ ${f.call("<server>/<tool>", "<json>")} # Only run AFTER getting schema
4546
+ ${f.callStdin("<server>/<tool>")} # Invoke with JSON input
3910
4547
  ${f.callFields("<server>/<tool>", "<json>", "field1,field2")} # Extract specific fields from response
3911
4548
 
3912
4549
  # Discovery commands (use these to find tools)
@@ -3984,8 +4621,15 @@ Example usage:
3984
4621
  ${f.tools()} # See all available MCP tools
3985
4622
  ${f.grep("weather")} # Find tools by description
3986
4623
 
3987
- # Get tool details
3988
- ${f.info("<server>/<tool>")} # View JSON schema for input and output if available
4624
+ # Get tool schemas (choose the approach that fits)
4625
+ ${f.toolsSchema()} # All tools with schemas (auto-collapses large schemas)
4626
+ ${f.toolsSchema("slack")} # Schemas for one server
4627
+ ${f.info("<server>/<tool>")} # Full schema for one tool
4628
+
4629
+ # Progressive schema exploration (for complex tools)
4630
+ ${f.infoDepth("<server>/<tool>", 0)} # Top-level structure only
4631
+ ${f.infoPath("<server>/<tool>", "filters")} # Drill into a subtree
4632
+ ${f.infoPath("<server>/<tool>", "filters.tags.items")} # Drill deeper
3989
4633
 
3990
4634
  # Simple tool call (no parameters)
3991
4635
  ${f.call("weather/get_location", "{}")}
@@ -4025,6 +4669,22 @@ function parseCommand(command) {
4025
4669
  args: trimmed.slice(spaceIndex + 1).trim()
4026
4670
  };
4027
4671
  }
4672
+ function parseFlags(args) {
4673
+ const flags = {};
4674
+ const positionalParts = [];
4675
+ const parts = args.split(/\s+/);
4676
+ for (let i = 0; i < parts.length; i++) {
4677
+ const part = parts[i];
4678
+ if (part.startsWith("--") && i + 1 < parts.length) {
4679
+ const key = part.slice(2);
4680
+ flags[key] = parts[++i];
4681
+ } else positionalParts.push(part);
4682
+ }
4683
+ return {
4684
+ positional: positionalParts.join(" "),
4685
+ flags
4686
+ };
4687
+ }
4028
4688
  function textResult(data) {
4029
4689
  return { content: [{
4030
4690
  type: "text",
@@ -4046,16 +4706,31 @@ async function handleToolCommand(command, input) {
4046
4706
  switch (subcommand) {
4047
4707
  case "servers": return textResult(await sendRequest("servers/list"));
4048
4708
  case "tools": {
4709
+ const { positional, flags } = parseFlags(args);
4049
4710
  const params = {};
4050
- if (args) params.server = args;
4711
+ if (positional) params.server = positional;
4712
+ if (flags.include === "schema") params.includeSchema = true;
4713
+ if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
4051
4714
  return textResult(await sendRequest("tools/list", params));
4052
4715
  }
4053
- case "grep":
4716
+ case "grep": {
4054
4717
  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 }));
4718
+ const { positional, flags } = parseFlags(args);
4719
+ if (!positional) return errorResult("Usage: grep <pattern>");
4720
+ const params = { pattern: positional };
4721
+ if (flags.include === "schema") params.includeSchema = true;
4722
+ if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
4723
+ return textResult(await sendRequest("tools/grep", params));
4724
+ }
4725
+ case "info": {
4726
+ if (!args) return errorResult("Usage: info <server/tool> [--path <path>] [--depth <n>]");
4727
+ const { positional, flags } = parseFlags(args);
4728
+ if (!positional) return errorResult("Usage: info <server/tool>");
4729
+ const params = { name: positional };
4730
+ if (flags.path) params.path = flags.path;
4731
+ if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
4732
+ return textResult(await sendRequest("tools/info", params));
4733
+ }
4059
4734
  case "call": {
4060
4735
  if (!args) return errorResult("Usage: call <server/tool>");
4061
4736
  const result = await sendRequest("tools/call", {
@@ -4390,7 +5065,7 @@ const telemetryCommand = new Command("telemetry").description("Manage anonymous
4390
5065
  });
4391
5066
  async function runCli() {
4392
5067
  const program = new Command();
4393
- program.name("muxed").description("The optimization layer for MCP").version("0.1.0");
5068
+ program.name("muxed").description("The optimization layer for MCP").version(getVersion());
4394
5069
  program.enablePositionalOptions();
4395
5070
  program.option("--config <path>", "Path to config file");
4396
5071
  program.commandsGroup("Servers:");