muxed 0.2.0 → 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,
@@ -474,6 +477,7 @@ var AuthorizationCodeProvider = class {
474
477
  config;
475
478
  _redirectUrl;
476
479
  hadTokensBefore = false;
480
+ _state = crypto.randomBytes(32).toString("base64url");
477
481
  constructor(config, serverName) {
478
482
  this.serverName = serverName;
479
483
  this.config = config;
@@ -481,14 +485,14 @@ var AuthorizationCodeProvider = class {
481
485
  this.hadTokensBefore = this.store.hasTokens();
482
486
  }
483
487
  setRedirectUrl(port) {
484
- this._redirectUrl = `http://127.0.0.1:${port}/oauth/callback`;
488
+ this._redirectUrl = `http://localhost:${port}/callback`;
485
489
  }
486
490
  get redirectUrl() {
487
491
  return this._redirectUrl;
488
492
  }
489
493
  get clientMetadata() {
490
494
  return {
491
- redirect_uris: [this._redirectUrl ?? "http://127.0.0.1/oauth/callback"],
495
+ redirect_uris: [this._redirectUrl ?? "http://localhost/callback"],
492
496
  token_endpoint_auth_method: this.config.clientSecret ? "client_secret_basic" : "none",
493
497
  grant_types: ["authorization_code", "refresh_token"],
494
498
  response_types: ["code"],
@@ -501,7 +505,14 @@ var AuthorizationCodeProvider = class {
501
505
  client_id: this.config.clientId,
502
506
  ...this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}
503
507
  };
504
- return this.store.getClientInformation();
508
+ const cached = this.store.getClientInformation();
509
+ if (cached && this._redirectUrl) {
510
+ if (!(cached.redirect_uris ?? []).includes(this._redirectUrl)) {
511
+ this.store.clearClientInformation();
512
+ return;
513
+ }
514
+ }
515
+ return cached;
505
516
  }
506
517
  saveClientInformation(info) {
507
518
  this.store.saveClientInformation(info);
@@ -522,6 +533,9 @@ var AuthorizationCodeProvider = class {
522
533
  openBrowser(url);
523
534
  }
524
535
  }
536
+ async state() {
537
+ return this._state;
538
+ }
525
539
  saveCodeVerifier(codeVerifier) {
526
540
  this.store.saveCodeVerifier(codeVerifier);
527
541
  }
@@ -580,8 +594,8 @@ var CallbackServer = class {
580
594
  res.end();
581
595
  return;
582
596
  }
583
- const url = new URL(req.url ?? "/", `http://127.0.0.1`);
584
- if (url.pathname !== "/oauth/callback") {
597
+ const url = new URL(req.url ?? "/", `http://localhost`);
598
+ if (url.pathname !== "/callback") {
585
599
  res.writeHead(404);
586
600
  res.end("Not found");
587
601
  return;
@@ -615,7 +629,7 @@ var CallbackServer = class {
615
629
  state
616
630
  });
617
631
  });
618
- this.server.listen(port, "127.0.0.1", () => {
632
+ this.server.listen(port, "localhost", () => {
619
633
  const addr = this.server.address();
620
634
  if (addr && typeof addr === "object") this._port = addr.port;
621
635
  });
@@ -687,6 +701,7 @@ var ServerManager = class {
687
701
  stopped = false;
688
702
  connectTimeout;
689
703
  healthCheckInterval;
704
+ healthCheckTimeout;
690
705
  maxRestartAttempts;
691
706
  onHealthChange;
692
707
  constructor(name, config, options) {
@@ -694,6 +709,7 @@ var ServerManager = class {
694
709
  this.config = config;
695
710
  this.connectTimeout = options?.connectTimeout;
696
711
  this.healthCheckInterval = options?.healthCheckInterval ?? 3e4;
712
+ this.healthCheckTimeout = options?.healthCheckTimeout ?? 1e4;
697
713
  this.maxRestartAttempts = options?.maxRestartAttempts ?? -1;
698
714
  }
699
715
  setHealthCallback(cb) {
@@ -900,7 +916,7 @@ var ServerManager = class {
900
916
  if (!this.client || this.status !== "connected") return;
901
917
  this.lastHealthCheck = /* @__PURE__ */ new Date();
902
918
  try {
903
- await this.client.ping();
919
+ await this.client.ping({ timeout: this.healthCheckTimeout });
904
920
  if (this.consecutiveFailures > 0) getLogger().info(`Health check recovered after ${this.consecutiveFailures} failures`, this.name);
905
921
  this.consecutiveFailures = 0;
906
922
  } catch (err) {
@@ -955,10 +971,14 @@ var ServerManager = class {
955
971
  if (!this.client) return;
956
972
  this.tools = (await this.client.listTools()).tools;
957
973
  }
974
+ ensureConnected() {
975
+ if (!this.client || this.status !== "connected") throw new Error(`Server "${this.name}" is not connected`);
976
+ return this.client;
977
+ }
958
978
  async callTool(name, args, timeout) {
959
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
979
+ const client = this.ensureConnected();
960
980
  const options = timeout ? { signal: AbortSignal.timeout(timeout) } : void 0;
961
- return await this.client.callTool({
981
+ return await client.callTool({
962
982
  name,
963
983
  arguments: args
964
984
  }, void 0, options);
@@ -971,8 +991,7 @@ var ServerManager = class {
971
991
  this.resources = (await this.client.listResources()).resources;
972
992
  }
973
993
  async readResource(uri) {
974
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
975
- return await this.client.readResource({ uri });
994
+ return await this.ensureConnected().readResource({ uri });
976
995
  }
977
996
  listPrompts() {
978
997
  return this.prompts;
@@ -982,40 +1001,35 @@ var ServerManager = class {
982
1001
  this.prompts = (await this.client.listPrompts()).prompts;
983
1002
  }
984
1003
  async getPrompt(name, args) {
985
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
986
- return await this.client.getPrompt({
1004
+ return await this.ensureConnected().getPrompt({
987
1005
  name,
988
1006
  arguments: args
989
1007
  });
990
1008
  }
991
1009
  async complete(ref, argument) {
992
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1010
+ const client = this.ensureConnected();
993
1011
  if (!this.capabilities?.completions) throw new Error(`Server "${this.name}" does not support completions`);
994
- return await this.client.complete({
1012
+ return await client.complete({
995
1013
  ref,
996
1014
  argument
997
1015
  });
998
1016
  }
999
1017
  async listTasks(cursor) {
1000
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1018
+ const client = this.ensureConnected();
1001
1019
  if (!this.capabilities?.experimental?.tasks) throw new Error(`Server "${this.name}" does not support tasks`);
1002
- return await this.client.experimental.tasks.listTasks(cursor);
1020
+ return await client.experimental.tasks.listTasks(cursor);
1003
1021
  }
1004
1022
  async getTask(taskId) {
1005
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1006
- return await this.client.experimental.tasks.getTask(taskId);
1023
+ return await this.ensureConnected().experimental.tasks.getTask(taskId);
1007
1024
  }
1008
1025
  async getTaskResult(taskId) {
1009
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1010
- return await this.client.experimental.tasks.getTaskResult(taskId);
1026
+ return await this.ensureConnected().experimental.tasks.getTaskResult(taskId);
1011
1027
  }
1012
1028
  async cancelTask(taskId) {
1013
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1014
- return await this.client.experimental.tasks.cancelTask(taskId);
1029
+ return await this.ensureConnected().experimental.tasks.cancelTask(taskId);
1015
1030
  }
1016
1031
  async callToolWithTask(name, args) {
1017
- if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
1018
- const stream = this.client.experimental.tasks.callToolStream({
1032
+ const stream = this.ensureConnected().experimental.tasks.callToolStream({
1019
1033
  name,
1020
1034
  arguments: args
1021
1035
  });
@@ -1169,6 +1183,7 @@ var ServerPool = class {
1169
1183
  const manager = new ServerManager(name, serverConfig, {
1170
1184
  connectTimeout: config.daemon?.connectTimeout,
1171
1185
  healthCheckInterval: config.daemon?.healthCheckInterval,
1186
+ healthCheckTimeout: config.daemon?.healthCheckTimeout,
1172
1187
  maxRestartAttempts: config.daemon?.maxRestartAttempts
1173
1188
  });
1174
1189
  manager.setHealthCallback((serverName, status, error) => {
@@ -1470,6 +1485,7 @@ var ServerPool = class {
1470
1485
  const manager = new ServerManager(name, serverConfig, {
1471
1486
  connectTimeout: newConfig.daemon?.connectTimeout,
1472
1487
  healthCheckInterval: newConfig.daemon?.healthCheckInterval,
1488
+ healthCheckTimeout: newConfig.daemon?.healthCheckTimeout,
1473
1489
  maxRestartAttempts: newConfig.daemon?.maxRestartAttempts
1474
1490
  });
1475
1491
  manager.setHealthCallback((serverName, status, error) => {
@@ -1485,6 +1501,7 @@ var ServerPool = class {
1485
1501
  const newManager = new ServerManager(name, serverConfig, {
1486
1502
  connectTimeout: newConfig.daemon?.connectTimeout,
1487
1503
  healthCheckInterval: newConfig.daemon?.healthCheckInterval,
1504
+ healthCheckTimeout: newConfig.daemon?.healthCheckTimeout,
1488
1505
  maxRestartAttempts: newConfig.daemon?.maxRestartAttempts
1489
1506
  });
1490
1507
  newManager.setHealthCallback((serverName, status, error) => {
@@ -1628,6 +1645,240 @@ function filterFields(data, fields) {
1628
1645
  }
1629
1646
  return data;
1630
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
+ }
1631
1882
  function createDaemonServer(serverPool, config) {
1632
1883
  const socketPath = getSocketPath();
1633
1884
  let idleTimer;
@@ -1656,11 +1907,37 @@ function createDaemonServer(serverPool, config) {
1656
1907
  result: serverPool.listServers()
1657
1908
  };
1658
1909
  case "tools/list": {
1659
- 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
+ }
1660
1930
  return {
1661
1931
  jsonrpc: "2.0",
1662
1932
  id,
1663
- 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
+ }))
1664
1941
  };
1665
1942
  }
1666
1943
  case "tools/call": {
@@ -1753,10 +2030,34 @@ function createDaemonServer(serverPool, config) {
1753
2030
  data: toErrorData(found.error)
1754
2031
  }
1755
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
+ };
1756
2057
  return {
1757
2058
  jsonrpc: "2.0",
1758
2059
  id,
1759
- result: found.tool
2060
+ result: tool
1760
2061
  };
1761
2062
  }
1762
2063
  case "tools/validate": {
@@ -1834,10 +2135,36 @@ function createDaemonServer(serverPool, config) {
1834
2135
  }
1835
2136
  };
1836
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
+ }
1837
2157
  return {
1838
2158
  jsonrpc: "2.0",
1839
2159
  id,
1840
- 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
+ }))
1841
2168
  };
1842
2169
  } catch (err) {
1843
2170
  return {
@@ -2525,16 +2852,32 @@ async function ensureDaemon(configPath) {
2525
2852
  400
2526
2853
  ]);
2527
2854
  }
2528
- async function sendRequest(method, params) {
2855
+ async function sendRequest(method, params, timeout = 9e4) {
2529
2856
  const socketPath = getSocketPath();
2530
2857
  return new Promise((resolve, reject) => {
2531
2858
  const socket = net.createConnection(socketPath);
2532
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);
2533
2867
  socket.on("error", (err) => {
2868
+ if (settled) return;
2869
+ settled = true;
2870
+ clearTimeout(timer);
2534
2871
  if (err.code === "ENOENT") reject(/* @__PURE__ */ new Error("Daemon is not running. Run `muxed status` to check."));
2535
2872
  else if (err.code === "ECONNREFUSED") reject(/* @__PURE__ */ new Error("Daemon may have crashed. Try running a command to auto-restart it."));
2536
2873
  else reject(err);
2537
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
+ });
2538
2881
  socket.on("connect", () => {
2539
2882
  const request = {
2540
2883
  jsonrpc: "2.0",
@@ -2548,6 +2891,9 @@ async function sendRequest(method, params) {
2548
2891
  buffer += data.toString();
2549
2892
  const newlineIndex = buffer.indexOf("\n");
2550
2893
  if (newlineIndex === -1) return;
2894
+ if (settled) return;
2895
+ settled = true;
2896
+ clearTimeout(timer);
2551
2897
  const line = buffer.slice(0, newlineIndex).trim();
2552
2898
  socket.destroy();
2553
2899
  try {
@@ -2593,7 +2939,6 @@ function formatTools(tools) {
2593
2939
  return formatTable([
2594
2940
  "Tool",
2595
2941
  "Title",
2596
- "Description",
2597
2942
  "Hints"
2598
2943
  ], tools.map(({ server, tool }) => {
2599
2944
  const hints = [];
@@ -2603,7 +2948,6 @@ function formatTools(tools) {
2603
2948
  return [
2604
2949
  `${server}/${tool.name}`,
2605
2950
  tool.title ?? "—",
2606
- truncate(tool.description ?? "", 60),
2607
2951
  hints.join(" ")
2608
2952
  ];
2609
2953
  }));
@@ -2837,6 +3181,24 @@ function formatInit(result) {
2837
3181
  lines.push("");
2838
3182
  lines.push("All discovered servers already exist in muxed config. Nothing to do.");
2839
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
+ }
2840
3202
  return lines.join("\n");
2841
3203
  }
2842
3204
  function formatMcpServer(name, config, scope) {
@@ -3005,20 +3367,27 @@ async function shutdown() {
3005
3367
  if (_client) await _client.shutdown();
3006
3368
  } catch {}
3007
3369
  }
3008
- const toolsCommand = new Command("tools").description("List all available tools, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
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) => {
3009
3371
  const configPath = toolsCommand.parent?.opts().config;
3010
3372
  await ensureDaemon(configPath);
3011
- 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);
3012
3378
  capture("tools_listed", {
3013
3379
  filtered_by_server: !!server,
3014
3380
  tool_count: result.length
3015
3381
  });
3016
3382
  console.log(opts.json ? formatJson(result) : formatTools(result));
3017
3383
  });
3018
- const infoCommand = new Command("info").description("Show input schema and description for a specific tool").argument("<server/tool>", "Tool identifier (e.g. myserver/mytool)").option("--json", "Output as JSON").action(async (serverTool, opts) => {
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) => {
3019
3385
  const configPath = infoCommand.parent?.opts().config;
3020
3386
  await ensureDaemon(configPath);
3021
- 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);
3022
3391
  if (opts.json) console.log(formatJson(result));
3023
3392
  else {
3024
3393
  const slashIndex = serverTool.indexOf("/");
@@ -3176,10 +3545,13 @@ const callCommand = new Command("call").description("Execute a tool with JSON ar
3176
3545
  process.exit(1);
3177
3546
  }
3178
3547
  });
3179
- const grepCommand = new Command("grep").description("Search tools by regex pattern across names, titles, and descriptions").argument("<pattern>", "Regex pattern to search").option("--json", "Output as JSON").action(async (pattern, opts) => {
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) => {
3180
3549
  const configPath = grepCommand.parent?.opts().config;
3181
3550
  await ensureDaemon(configPath);
3182
- 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);
3183
3555
  capture("tools_searched", { result_count: result.length });
3184
3556
  console.log(opts.json ? formatJson(result) : formatTools(result));
3185
3557
  });
@@ -3654,6 +4026,261 @@ function getMuxedConfigPath(scope, explicitPath) {
3654
4026
  if (scope === "local") return path.join(process.cwd(), "muxed.config.json");
3655
4027
  return path.join(home, ".muxed", "config.json");
3656
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
+ }
3657
4284
  async function confirm(message, opts) {
3658
4285
  const rl = readline.createInterface({
3659
4286
  input: opts?.input ?? process.stdin,
@@ -3721,7 +4348,7 @@ async function resolveConflicts(unresolvedConflicts, isInteractive) {
3721
4348
  conflicts
3722
4349
  };
3723
4350
  }
3724
- const initCommand = new Command("init").description("Discover and import MCP servers from agent configs (Claude Code, Cursor)").option("--dry-run", "Show what would be done without writing files").option("--json", "Output as JSON").option("-y, --yes", "Skip prompts; resolve conflicts by priority (claude-code > cursor > first)").option("--no-delete", "Keep original server entries in agent configs").option("--no-replace", "Don't add muxed entry to agent configs").action(async (opts) => {
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) => {
3725
4352
  const configPath = initCommand.parent?.opts().config;
3726
4353
  const isInteractive = !opts.dryRun && !opts.json && !opts.yes && !!process.stdin.isTTY;
3727
4354
  const { discovered, warnings } = discoverAgentConfigs();
@@ -3744,7 +4371,7 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3744
4371
  }
3745
4372
  if (!opts.dryRun && imported.length > 0) writeMuxedConfig(muxedPath, result.merged);
3746
4373
  const modifiedFiles = [];
3747
- 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;
3748
4375
  if (!opts.dryRun && shouldDelete) for (const dc of discovered) try {
3749
4376
  modifyAgentConfig(dc, {
3750
4377
  delete: true,
@@ -3754,6 +4381,10 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3754
4381
  } catch (err) {
3755
4382
  warnings.push(`Failed to modify ${dc.configPath}: ${err instanceof Error ? err.message : String(err)}`);
3756
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
+ }) : [];
3757
4388
  const initResult = {
3758
4389
  discovered: discovered.map((d) => ({
3759
4390
  agent: d.agent.name,
@@ -3767,14 +4398,17 @@ const initCommand = new Command("init").description("Discover and import MCP ser
3767
4398
  warnings,
3768
4399
  modifiedFiles,
3769
4400
  muxedConfigPath: muxedPath,
3770
- dryRun: opts.dryRun ?? false
4401
+ dryRun: opts.dryRun ?? false,
4402
+ instructionResults
3771
4403
  };
3772
4404
  capture("init_run", {
3773
4405
  dry_run: opts.dryRun ?? false,
3774
4406
  imported_count: imported.length,
3775
4407
  conflict_count: conflicts.length,
3776
4408
  warning_count: warnings.length,
3777
- 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)
3778
4412
  });
3779
4413
  console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
3780
4414
  });
@@ -3831,7 +4465,10 @@ const cliFragments = {
3831
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.",
3832
4466
  grep: (p) => `npx muxed grep "${p}"`,
3833
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",
3834
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}`,
3835
4472
  call: (n, j) => `npx muxed call ${n} '${j}'`,
3836
4473
  callStdin: (n) => `npx muxed call ${n} -`,
3837
4474
  callDryRun: (n, j) => `npx muxed call ${n} '${j}' --dry-run`,
@@ -3845,7 +4482,10 @@ const toolFragments = {
3845
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.",
3846
4483
  grep: (p) => `muxed:exec({ "command": "grep ${p}" })`,
3847
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" })`,
3848
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}" })`,
3849
4489
  call: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
3850
4490
  callStdin: (n) => `muxed:exec({ "command": "call ${n}", "input": { ... } })`,
3851
4491
  callDryRun: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
@@ -3887,15 +4527,23 @@ Commands (in order of execution):
3887
4527
  ${f.grep("<pattern>")} # Search tool names and descriptions
3888
4528
  ${f.tools("[server]")} # List available tools (optionally filter by server)
3889
4529
 
3890
- # STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
3891
- ${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
3892
4540
 
3893
4541
  # STEP 3: OPTIONAL - Validate arguments before calling (dry-run)
3894
4542
  ${f.callDryRun("<server>/<tool>", "<json>")} # Validate args without executing
3895
4543
 
3896
- # STEP 4: Only after checking schema, make the call
3897
- ${f.call("<server>/<tool>", "<json>")} # Only run AFTER info
3898
- ${f.callStdin("<server>/<tool>")} # Invoke with JSON input (AFTER info)
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
3899
4547
  ${f.callFields("<server>/<tool>", "<json>", "field1,field2")} # Extract specific fields from response
3900
4548
 
3901
4549
  # Discovery commands (use these to find tools)
@@ -3973,8 +4621,15 @@ Example usage:
3973
4621
  ${f.tools()} # See all available MCP tools
3974
4622
  ${f.grep("weather")} # Find tools by description
3975
4623
 
3976
- # Get tool details
3977
- ${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
3978
4633
 
3979
4634
  # Simple tool call (no parameters)
3980
4635
  ${f.call("weather/get_location", "{}")}
@@ -4014,6 +4669,22 @@ function parseCommand(command) {
4014
4669
  args: trimmed.slice(spaceIndex + 1).trim()
4015
4670
  };
4016
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
+ }
4017
4688
  function textResult(data) {
4018
4689
  return { content: [{
4019
4690
  type: "text",
@@ -4035,16 +4706,31 @@ async function handleToolCommand(command, input) {
4035
4706
  switch (subcommand) {
4036
4707
  case "servers": return textResult(await sendRequest("servers/list"));
4037
4708
  case "tools": {
4709
+ const { positional, flags } = parseFlags(args);
4038
4710
  const params = {};
4039
- 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);
4040
4714
  return textResult(await sendRequest("tools/list", params));
4041
4715
  }
4042
- case "grep":
4716
+ case "grep": {
4043
4717
  if (!args) return errorResult("Usage: grep <pattern>");
4044
- return textResult(await sendRequest("tools/grep", { pattern: args }));
4045
- case "info":
4046
- if (!args) return errorResult("Usage: info <server/tool>");
4047
- return textResult(await sendRequest("tools/info", { name: args }));
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
+ }
4048
4734
  case "call": {
4049
4735
  if (!args) return errorResult("Usage: call <server/tool>");
4050
4736
  const result = await sendRequest("tools/call", {
@@ -4379,7 +5065,7 @@ const telemetryCommand = new Command("telemetry").description("Manage anonymous
4379
5065
  });
4380
5066
  async function runCli() {
4381
5067
  const program = new Command();
4382
- 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());
4383
5069
  program.enablePositionalOptions();
4384
5070
  program.option("--config <path>", "Path to config file");
4385
5071
  program.commandsGroup("Servers:");