muxed 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -262
- package/dist/cli.mjs +976 -239
- package/dist/client/index.d.mts +1 -0
- package/dist/client/index.mjs +20 -1
- package/package.json +17 -7
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
|
-
|
|
979
|
+
const client = this.ensureConnected();
|
|
971
980
|
const options = timeout ? { signal: AbortSignal.timeout(timeout) } : void 0;
|
|
972
|
-
return await
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1010
|
+
const client = this.ensureConnected();
|
|
1004
1011
|
if (!this.capabilities?.completions) throw new Error(`Server "${this.name}" does not support completions`);
|
|
1005
|
-
return await
|
|
1012
|
+
return await client.complete({
|
|
1006
1013
|
ref,
|
|
1007
1014
|
argument
|
|
1008
1015
|
});
|
|
1009
1016
|
}
|
|
1010
1017
|
async listTasks(cursor) {
|
|
1011
|
-
|
|
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
|
|
1020
|
+
return await client.experimental.tasks.listTasks(cursor);
|
|
1014
1021
|
}
|
|
1015
1022
|
async getTask(taskId) {
|
|
1016
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
2160
|
+
result: matches.map(({ server, tool }) => ({
|
|
2161
|
+
server,
|
|
2162
|
+
tool: {
|
|
2163
|
+
name: tool.name,
|
|
2164
|
+
title: tool.title,
|
|
2165
|
+
annotations: tool.annotations
|
|
2166
|
+
}
|
|
2167
|
+
}))
|
|
1852
2168
|
};
|
|
1853
2169
|
} catch (err) {
|
|
1854
2170
|
return {
|
|
@@ -2536,16 +2852,32 @@ async function ensureDaemon(configPath) {
|
|
|
2536
2852
|
400
|
|
2537
2853
|
]);
|
|
2538
2854
|
}
|
|
2539
|
-
async function sendRequest(method, params) {
|
|
2855
|
+
async function sendRequest(method, params, timeout = 9e4) {
|
|
2540
2856
|
const socketPath = getSocketPath();
|
|
2541
2857
|
return new Promise((resolve, reject) => {
|
|
2542
2858
|
const socket = net.createConnection(socketPath);
|
|
2543
2859
|
let buffer = "";
|
|
2860
|
+
let settled = false;
|
|
2861
|
+
const timer = setTimeout(() => {
|
|
2862
|
+
if (settled) return;
|
|
2863
|
+
settled = true;
|
|
2864
|
+
socket.destroy();
|
|
2865
|
+
reject(/* @__PURE__ */ new Error(`Request timed out after ${timeout}ms`));
|
|
2866
|
+
}, timeout);
|
|
2544
2867
|
socket.on("error", (err) => {
|
|
2868
|
+
if (settled) return;
|
|
2869
|
+
settled = true;
|
|
2870
|
+
clearTimeout(timer);
|
|
2545
2871
|
if (err.code === "ENOENT") reject(/* @__PURE__ */ new Error("Daemon is not running. Run `muxed status` to check."));
|
|
2546
2872
|
else if (err.code === "ECONNREFUSED") reject(/* @__PURE__ */ new Error("Daemon may have crashed. Try running a command to auto-restart it."));
|
|
2547
2873
|
else reject(err);
|
|
2548
2874
|
});
|
|
2875
|
+
socket.on("close", () => {
|
|
2876
|
+
if (settled) return;
|
|
2877
|
+
settled = true;
|
|
2878
|
+
clearTimeout(timer);
|
|
2879
|
+
reject(/* @__PURE__ */ new Error("Daemon closed the connection before sending a response"));
|
|
2880
|
+
});
|
|
2549
2881
|
socket.on("connect", () => {
|
|
2550
2882
|
const request = {
|
|
2551
2883
|
jsonrpc: "2.0",
|
|
@@ -2559,6 +2891,9 @@ async function sendRequest(method, params) {
|
|
|
2559
2891
|
buffer += data.toString();
|
|
2560
2892
|
const newlineIndex = buffer.indexOf("\n");
|
|
2561
2893
|
if (newlineIndex === -1) return;
|
|
2894
|
+
if (settled) return;
|
|
2895
|
+
settled = true;
|
|
2896
|
+
clearTimeout(timer);
|
|
2562
2897
|
const line = buffer.slice(0, newlineIndex).trim();
|
|
2563
2898
|
socket.destroy();
|
|
2564
2899
|
try {
|
|
@@ -2604,7 +2939,6 @@ function formatTools(tools) {
|
|
|
2604
2939
|
return formatTable([
|
|
2605
2940
|
"Tool",
|
|
2606
2941
|
"Title",
|
|
2607
|
-
"Description",
|
|
2608
2942
|
"Hints"
|
|
2609
2943
|
], tools.map(({ server, tool }) => {
|
|
2610
2944
|
const hints = [];
|
|
@@ -2614,7 +2948,6 @@ function formatTools(tools) {
|
|
|
2614
2948
|
return [
|
|
2615
2949
|
`${server}/${tool.name}`,
|
|
2616
2950
|
tool.title ?? "—",
|
|
2617
|
-
truncate(tool.description ?? "", 60),
|
|
2618
2951
|
hints.join(" ")
|
|
2619
2952
|
];
|
|
2620
2953
|
}));
|
|
@@ -2848,6 +3181,24 @@ function formatInit(result) {
|
|
|
2848
3181
|
lines.push("");
|
|
2849
3182
|
lines.push("All discovered servers already exist in muxed config. Nothing to do.");
|
|
2850
3183
|
}
|
|
3184
|
+
if (result.instructionResults && result.instructionResults.length > 0) {
|
|
3185
|
+
lines.push("");
|
|
3186
|
+
lines.push("Instructions:");
|
|
3187
|
+
for (const r of result.instructionResults) switch (r.action) {
|
|
3188
|
+
case "created":
|
|
3189
|
+
lines.push(` ${r.target} \u2014 created (v${r.newVersion})`);
|
|
3190
|
+
break;
|
|
3191
|
+
case "updated":
|
|
3192
|
+
lines.push(` ${r.target} \u2014 updated (v${r.previousVersion} \u2192 v${r.newVersion})`);
|
|
3193
|
+
break;
|
|
3194
|
+
case "up-to-date":
|
|
3195
|
+
lines.push(` ${r.target} \u2014 up-to-date (v${r.previousVersion})`);
|
|
3196
|
+
break;
|
|
3197
|
+
case "skipped":
|
|
3198
|
+
lines.push(` ${r.target} \u2014 skipped`);
|
|
3199
|
+
break;
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
2851
3202
|
return lines.join("\n");
|
|
2852
3203
|
}
|
|
2853
3204
|
function formatMcpServer(name, config, scope) {
|
|
@@ -2957,7 +3308,10 @@ function formatTable(headers, rows) {
|
|
|
2957
3308
|
...rows.map((row) => row.map((cell, i) => padRight(cell, widths[i])).join(" "))
|
|
2958
3309
|
].join("\n");
|
|
2959
3310
|
}
|
|
2960
|
-
const serversCommand = new Command("servers").description("List connected MCP servers and their
|
|
3311
|
+
const serversCommand = new Command("servers").description("List connected MCP servers and their status").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
|
|
3312
|
+
Examples:
|
|
3313
|
+
muxed servers List all servers with connection state
|
|
3314
|
+
muxed servers --json JSON output for scripting`).action(async (opts) => {
|
|
2961
3315
|
const configPath = serversCommand.parent?.opts().config;
|
|
2962
3316
|
await ensureDaemon(configPath);
|
|
2963
3317
|
const result = await sendRequest("servers/list");
|
|
@@ -3016,20 +3370,55 @@ async function shutdown() {
|
|
|
3016
3370
|
if (_client) await _client.shutdown();
|
|
3017
3371
|
} catch {}
|
|
3018
3372
|
}
|
|
3019
|
-
const toolsCommand = new Command("tools").description("List
|
|
3373
|
+
const toolsCommand = new Command("tools").description("List available tools across all servers").argument("[server]", "Show tools from this server only").option("--json", "Output as JSON (machine-readable)").option("--include <fields>", "Include extra fields: \"schema\" adds input schemas").option("--depth <n>", "Collapse schemas deeper than N levels (use with --include schema)", parseInt).addHelpText("after", `
|
|
3374
|
+
Schema options:
|
|
3375
|
+
--include schema Add input schemas to each tool in the output.
|
|
3376
|
+
--include schema --depth N Collapse schemas beyond N levels. Nodes deeper than N
|
|
3377
|
+
are replaced with { _collapsed: true, _hint: "..." }.
|
|
3378
|
+
Depth is auto-selected to fit a token budget if omitted.
|
|
3379
|
+
|
|
3380
|
+
Examples:
|
|
3381
|
+
muxed tools List all tools (names + descriptions)
|
|
3382
|
+
muxed tools postgres List tools from the "postgres" server only
|
|
3383
|
+
muxed tools --include schema List with full input schemas
|
|
3384
|
+
muxed tools --include schema --depth 1 List with schemas collapsed at depth 1`).action(async (server, opts) => {
|
|
3020
3385
|
const configPath = toolsCommand.parent?.opts().config;
|
|
3021
3386
|
await ensureDaemon(configPath);
|
|
3022
|
-
const
|
|
3387
|
+
const params = {};
|
|
3388
|
+
if (server) params.server = server;
|
|
3389
|
+
if (opts.include === "schema") params.includeSchema = true;
|
|
3390
|
+
if (opts.depth !== void 0) params.schemaDepth = opts.depth;
|
|
3391
|
+
const result = await sendRequest("tools/list", params);
|
|
3023
3392
|
capture("tools_listed", {
|
|
3024
3393
|
filtered_by_server: !!server,
|
|
3025
3394
|
tool_count: result.length
|
|
3026
3395
|
});
|
|
3027
3396
|
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
3028
3397
|
});
|
|
3029
|
-
const infoCommand = new Command("info").description("Show input schema
|
|
3398
|
+
const infoCommand = new Command("info").description("Show a tool's input schema — REQUIRED before calling any tool").argument("<server/tool>", "server_name/tool_name (e.g. postgres/query)").option("--json", "Output as JSON (machine-readable)").option("--path <path>", "Show only a subtree of the schema (e.g. \"filters.tags\")").option("--depth <n>", "Collapse schema deeper than N levels", parseInt).addHelpText("after", `
|
|
3399
|
+
Schema exploration:
|
|
3400
|
+
--depth N Show schema to N levels deep. Nodes beyond that depth are
|
|
3401
|
+
replaced with a summary: { _collapsed: true, _hint: "5 properties, 2 required" }.
|
|
3402
|
+
Scalar fields (string, number, boolean) are always shown regardless of depth.
|
|
3403
|
+
Start with --depth 1 for an overview, increase to explore deeper.
|
|
3404
|
+
|
|
3405
|
+
--path P Extract a subtree using dot-separated path. Navigates through:
|
|
3406
|
+
properties (by name), items, additionalProperties, anyOf/oneOf (by index).
|
|
3407
|
+
Combine with --depth to control how much of the subtree is shown.
|
|
3408
|
+
|
|
3409
|
+
Examples:
|
|
3410
|
+
muxed info postgres/query Full schema
|
|
3411
|
+
muxed info github/create_issue --depth 1 Top-level fields only, nested objects collapsed
|
|
3412
|
+
muxed info github/create_issue --depth 2 Two levels deep
|
|
3413
|
+
muxed info slack/search --path "filters" Only the "filters" property subtree
|
|
3414
|
+
muxed info slack/search --path "filters.tags" Drill into filters.tags
|
|
3415
|
+
muxed info api/create --path "body.items" --depth 1 Subtree with depth limit`).action(async (serverTool, opts) => {
|
|
3030
3416
|
const configPath = infoCommand.parent?.opts().config;
|
|
3031
3417
|
await ensureDaemon(configPath);
|
|
3032
|
-
const
|
|
3418
|
+
const params = { name: serverTool };
|
|
3419
|
+
if (opts.path) params.path = opts.path;
|
|
3420
|
+
if (opts.depth !== void 0) params.schemaDepth = opts.depth;
|
|
3421
|
+
const result = await sendRequest("tools/info", params);
|
|
3033
3422
|
if (opts.json) console.log(formatJson(result));
|
|
3034
3423
|
else {
|
|
3035
3424
|
const slashIndex = serverTool.indexOf("/");
|
|
@@ -3048,7 +3437,13 @@ function readStdin() {
|
|
|
3048
3437
|
process.stdin.on("error", reject);
|
|
3049
3438
|
});
|
|
3050
3439
|
}
|
|
3051
|
-
const callCommand = new Command("call").description("Execute a tool with JSON arguments
|
|
3440
|
+
const callCommand = new Command("call").description("Execute a tool with JSON arguments").argument("<server/tool>", "server_name/tool_name (e.g. postgres/query)").argument("[json]", "JSON object with arguments, or - to read from stdin").option("--dry-run", "Validate arguments without executing (catches errors early)").option("--fields <paths>", "Extract specific fields from response (comma-separated dot-notation)").option("--timeout <ms>", "Timeout in milliseconds").option("--async", "Run in background, return a task ID instead of waiting").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
|
|
3441
|
+
Examples:
|
|
3442
|
+
muxed call postgres/query '{"sql": "SELECT * FROM users LIMIT 5"}'
|
|
3443
|
+
muxed call fs/read_file '{"path": "/tmp/data.json"}' --fields "content"
|
|
3444
|
+
muxed call server/tool '{"a": 1}' --dry-run Validate without executing
|
|
3445
|
+
echo '{"sql": "..."}' | muxed call db/query - Read args from stdin
|
|
3446
|
+
muxed call analytics/export '{}' --async Returns task ID immediately`).action(async (serverTool, jsonArgs, opts) => {
|
|
3052
3447
|
const configPath = callCommand.parent?.opts().config;
|
|
3053
3448
|
await ensureDaemon(configPath);
|
|
3054
3449
|
let parsedArgs = {};
|
|
@@ -3187,20 +3582,38 @@ const callCommand = new Command("call").description("Execute a tool with JSON ar
|
|
|
3187
3582
|
process.exit(1);
|
|
3188
3583
|
}
|
|
3189
3584
|
});
|
|
3190
|
-
const grepCommand = new Command("grep").description("Search tools by
|
|
3585
|
+
const grepCommand = new Command("grep").description("Search tools by name or description (regex)").argument("<pattern>", "Regex pattern to match against tool names and descriptions").option("--json", "Output as JSON (machine-readable)").option("--include <fields>", "Include extra fields: \"schema\" adds input schemas").option("--depth <n>", "Collapse schemas deeper than N levels (use with --include schema)", parseInt).addHelpText("after", `
|
|
3586
|
+
Schema options:
|
|
3587
|
+
--include schema Add input schemas to matching tools.
|
|
3588
|
+
--include schema --depth N Collapse schemas beyond N levels.
|
|
3589
|
+
|
|
3590
|
+
Examples:
|
|
3591
|
+
muxed grep "search" Find tools related to searching
|
|
3592
|
+
muxed grep "file|read" Regex: tools matching "file" or "read"
|
|
3593
|
+
muxed grep "query" --include schema --depth 1 Matches with top-level schema
|
|
3594
|
+
muxed grep "query" --json Machine-readable output for scripting`).action(async (pattern, opts) => {
|
|
3191
3595
|
const configPath = grepCommand.parent?.opts().config;
|
|
3192
3596
|
await ensureDaemon(configPath);
|
|
3193
|
-
const
|
|
3597
|
+
const params = { pattern };
|
|
3598
|
+
if (opts.include === "schema") params.includeSchema = true;
|
|
3599
|
+
if (opts.depth !== void 0) params.schemaDepth = opts.depth;
|
|
3600
|
+
const result = await sendRequest("tools/grep", params);
|
|
3194
3601
|
capture("tools_searched", { result_count: result.length });
|
|
3195
3602
|
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
3196
3603
|
});
|
|
3197
|
-
const resourcesCommand = new Command("resources").description("List available resources
|
|
3604
|
+
const resourcesCommand = new Command("resources").description("List available MCP resources across all servers").argument("[server]", "Show resources from this server only").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
|
|
3605
|
+
Examples:
|
|
3606
|
+
muxed resources List all resources
|
|
3607
|
+
muxed resources github List resources from the "github" server only`).action(async (server, opts) => {
|
|
3198
3608
|
const configPath = resourcesCommand.parent?.opts().config;
|
|
3199
3609
|
await ensureDaemon(configPath);
|
|
3200
3610
|
const result = await sendRequest("resources/list", server ? { server } : void 0);
|
|
3201
3611
|
console.log(opts.json ? formatJson(result) : formatResources(result));
|
|
3202
3612
|
});
|
|
3203
|
-
const readCommand = new Command("read").description("Fetch and display the contents of
|
|
3613
|
+
const readCommand = new Command("read").description("Fetch and display the contents of an MCP resource").argument("<server/resource>", "server_name/resource_name (e.g. github/repos)").argument("[uri]", "Custom URI (defaults to the resource name)").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
|
|
3614
|
+
Examples:
|
|
3615
|
+
muxed read github/repos Read using resource name as URI
|
|
3616
|
+
muxed read github/repos "github://repos" Read with explicit URI`).action(async (serverResource, uri, opts) => {
|
|
3204
3617
|
const configPath = readCommand.parent?.opts().config;
|
|
3205
3618
|
await ensureDaemon(configPath);
|
|
3206
3619
|
const slashIndex = serverResource.indexOf("/");
|
|
@@ -3217,8 +3630,10 @@ const readCommand = new Command("read").description("Fetch and display the conte
|
|
|
3217
3630
|
function getExplicitConfig$1(cmd) {
|
|
3218
3631
|
return cmd.parent?.parent?.opts().config;
|
|
3219
3632
|
}
|
|
3220
|
-
const daemonCommand = new Command("daemon").description("
|
|
3221
|
-
|
|
3633
|
+
const daemonCommand = new Command("daemon").description("Manage the muxed background daemon").enablePositionalOptions().addHelpText("after", `
|
|
3634
|
+
The daemon auto-starts on first CLI command and shuts down after 5 min idle.
|
|
3635
|
+
These subcommands are for manual lifecycle control.`);
|
|
3636
|
+
daemonCommand.command("start").description("Start the daemon (usually not needed — auto-starts on first command)").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
3222
3637
|
const configPath = getExplicitConfig$1(daemonCommand);
|
|
3223
3638
|
if (await isDaemonRunning()) {
|
|
3224
3639
|
if (opts.json) console.log(formatJson({ status: "already_running" }));
|
|
@@ -3269,13 +3684,13 @@ daemonCommand.command("stop").description("Stop the running daemon process").act
|
|
|
3269
3684
|
console.log("Daemon is not running");
|
|
3270
3685
|
}
|
|
3271
3686
|
});
|
|
3272
|
-
daemonCommand.command("reload").description("Reload config and reconnect changed servers
|
|
3687
|
+
daemonCommand.command("reload").description("Reload config and reconnect changed servers (no restart)").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
3273
3688
|
const configPath = getExplicitConfig$1(daemonCommand);
|
|
3274
3689
|
await ensureDaemon(configPath);
|
|
3275
3690
|
const result = await sendRequest("config/reload", { configPath });
|
|
3276
3691
|
console.log(opts.json ? formatJson(result) : formatReload(result));
|
|
3277
3692
|
});
|
|
3278
|
-
daemonCommand.command("status").description("Show
|
|
3693
|
+
daemonCommand.command("status").description("Show PID, uptime, and connected servers").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
3279
3694
|
if (!await isDaemonRunning()) {
|
|
3280
3695
|
console.log("Daemon is not running");
|
|
3281
3696
|
return;
|
|
@@ -3283,13 +3698,15 @@ daemonCommand.command("status").description("Show daemon status including uptime
|
|
|
3283
3698
|
const result = await sendRequest("daemon/status");
|
|
3284
3699
|
console.log(opts.json ? formatJson(result) : formatStatus(result));
|
|
3285
3700
|
});
|
|
3286
|
-
const promptsCommand = new Command("prompts").description("List available prompt templates
|
|
3701
|
+
const promptsCommand = new Command("prompts").description("List available MCP prompt templates across all servers").argument("[server]", "Show prompts from this server only").option("--json", "Output as JSON (machine-readable)").action(async (server, opts) => {
|
|
3287
3702
|
const configPath = promptsCommand.parent?.opts().config;
|
|
3288
3703
|
await ensureDaemon(configPath);
|
|
3289
3704
|
const result = await sendRequest("prompts/list", server ? { server } : void 0);
|
|
3290
3705
|
console.log(opts.json ? formatJson(result) : formatPrompts(result));
|
|
3291
3706
|
});
|
|
3292
|
-
const promptCommand = new Command("prompt").description("Render a prompt template with
|
|
3707
|
+
const promptCommand = new Command("prompt").description("Render a prompt template with arguments").argument("<server/prompt>", "server_name/prompt_name (e.g. myserver/summarize)").argument("[args-json]", "JSON object with template arguments").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
|
|
3708
|
+
Examples:
|
|
3709
|
+
muxed prompt myserver/summarize '{"text": "..."}'`).action(async (serverPrompt, argsJson, opts) => {
|
|
3293
3710
|
const configPath = promptCommand.parent?.opts().config;
|
|
3294
3711
|
await ensureDaemon(configPath);
|
|
3295
3712
|
const slashIndex = serverPrompt.indexOf("/");
|
|
@@ -3313,7 +3730,10 @@ const promptCommand = new Command("prompt").description("Render a prompt templat
|
|
|
3313
3730
|
});
|
|
3314
3731
|
console.log(opts.json ? formatJson(result) : formatPromptMessages(result));
|
|
3315
3732
|
});
|
|
3316
|
-
const completionsCommand = new Command("completions").description("Get
|
|
3733
|
+
const completionsCommand = new Command("completions").description("Get argument completions for a prompt or resource").argument("<type>", "\"prompt\" or \"resource\"").argument("<name>", "server_name/template_name").argument("<arg>", "Argument name to complete").argument("<value>", "Partial value to get suggestions for").option("--json", "Output as JSON (machine-readable)").addHelpText("after", `
|
|
3734
|
+
Examples:
|
|
3735
|
+
muxed completions prompt myserver/summarize language "py"
|
|
3736
|
+
muxed completions resource myserver/files path "/home/"`).action(async (type, name, arg, value, opts) => {
|
|
3317
3737
|
const configPath = completionsCommand.parent?.opts().config;
|
|
3318
3738
|
await ensureDaemon(configPath);
|
|
3319
3739
|
const slashIndex = name.indexOf("/");
|
|
@@ -3338,13 +3758,13 @@ const completionsCommand = new Command("completions").description("Get auto-comp
|
|
|
3338
3758
|
});
|
|
3339
3759
|
console.log(opts.json ? formatJson(result) : formatCompletions(result));
|
|
3340
3760
|
});
|
|
3341
|
-
const tasksCommand = new Command("tasks").description("List active async tasks
|
|
3761
|
+
const tasksCommand = new Command("tasks").description("List active async tasks (from --async calls)").argument("[server]", "Show tasks from this server only").option("--json", "Output as JSON (machine-readable)").action(async (server, opts) => {
|
|
3342
3762
|
const configPath = tasksCommand.parent?.opts().config;
|
|
3343
3763
|
await ensureDaemon(configPath);
|
|
3344
3764
|
const result = await sendRequest("tasks/list", server ? { server } : void 0);
|
|
3345
3765
|
console.log(opts.json ? formatJson(result) : formatTasks(result));
|
|
3346
3766
|
});
|
|
3347
|
-
const taskCommand = new Command("task").description("Check the
|
|
3767
|
+
const taskCommand = new Command("task").description("Check the status of an async task").argument("<server/taskId>", "server_name/task_id (e.g. analytics/task-abc123)").option("--json", "Output as JSON (machine-readable)").action(async (serverTaskId, opts) => {
|
|
3348
3768
|
const configPath = taskCommand.parent?.opts().config;
|
|
3349
3769
|
await ensureDaemon(configPath);
|
|
3350
3770
|
const slashIndex = serverTaskId.indexOf("/");
|
|
@@ -3358,7 +3778,7 @@ const taskCommand = new Command("task").description("Check the current status of
|
|
|
3358
3778
|
});
|
|
3359
3779
|
console.log(opts.json ? formatJson(result) : formatTask(result));
|
|
3360
3780
|
});
|
|
3361
|
-
const taskResultCommand = new Command("task-result").description("
|
|
3781
|
+
const taskResultCommand = new Command("task-result").description("Get the result of a completed async task").argument("<server/taskId>", "server_name/task_id (e.g. analytics/task-abc123)").option("--json", "Output as JSON (machine-readable)").action(async (serverTaskId, opts) => {
|
|
3362
3782
|
const configPath = taskResultCommand.parent?.opts().config;
|
|
3363
3783
|
await ensureDaemon(configPath);
|
|
3364
3784
|
const slashIndex = serverTaskId.indexOf("/");
|
|
@@ -3372,7 +3792,7 @@ const taskResultCommand = new Command("task-result").description("Retrieve the o
|
|
|
3372
3792
|
});
|
|
3373
3793
|
console.log(opts.json ? formatJson(result) : formatCallResult(result));
|
|
3374
3794
|
});
|
|
3375
|
-
const taskCancelCommand = new Command("task-cancel").description("Cancel a running async task").argument("<server/taskId>", "
|
|
3795
|
+
const taskCancelCommand = new Command("task-cancel").description("Cancel a running async task").argument("<server/taskId>", "server_name/task_id (e.g. analytics/task-abc123)").option("--json", "Output as JSON (machine-readable)").action(async (serverTaskId, opts) => {
|
|
3376
3796
|
const configPath = taskCancelCommand.parent?.opts().config;
|
|
3377
3797
|
await ensureDaemon(configPath);
|
|
3378
3798
|
const slashIndex = serverTaskId.indexOf("/");
|
|
@@ -3665,6 +4085,418 @@ function getMuxedConfigPath(scope, explicitPath) {
|
|
|
3665
4085
|
if (scope === "local") return path.join(process.cwd(), "muxed.config.json");
|
|
3666
4086
|
return path.join(home, ".muxed", "config.json");
|
|
3667
4087
|
}
|
|
4088
|
+
let cached = null;
|
|
4089
|
+
function getVersion() {
|
|
4090
|
+
if (cached) return cached;
|
|
4091
|
+
const dir = path.dirname(fileURLToPath(import.meta.url));
|
|
4092
|
+
for (const rel of ["../package.json", "../../package.json"]) {
|
|
4093
|
+
const p = path.resolve(dir, rel);
|
|
4094
|
+
try {
|
|
4095
|
+
const pkg = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
4096
|
+
if (pkg.name === "muxed" && pkg.version) {
|
|
4097
|
+
cached = pkg.version;
|
|
4098
|
+
return cached;
|
|
4099
|
+
}
|
|
4100
|
+
} catch {}
|
|
4101
|
+
}
|
|
4102
|
+
return "0.0.0";
|
|
4103
|
+
}
|
|
4104
|
+
function makeCliFragments(run) {
|
|
4105
|
+
return {
|
|
4106
|
+
intro: `You have access to an \`${run} muxed\` CLI command for interacting with MCP (Model Context Protocol) servers. This command allows you to discover and call MCP tools on demand. Prioritize the use of skills over MCP tools.`,
|
|
4107
|
+
grep: (p) => `${run} muxed grep "${p}"`,
|
|
4108
|
+
tools: (s) => s ? `${run} muxed tools ${s}` : `${run} muxed tools`,
|
|
4109
|
+
toolsSchema: (s) => s ? `${run} muxed tools ${s} --include schema` : `${run} muxed tools --include schema`,
|
|
4110
|
+
info: (n) => `${run} muxed info ${n}`,
|
|
4111
|
+
infoDepth: (n, d) => `${run} muxed info ${n} --depth ${d}`,
|
|
4112
|
+
infoPath: (n, p) => `${run} muxed info ${n} --path ${p}`,
|
|
4113
|
+
call: (n, j) => `${run} muxed call ${n} '${j}'`,
|
|
4114
|
+
callStdin: (n) => `${run} muxed call ${n} -`,
|
|
4115
|
+
callDryRun: (n, j) => `${run} muxed call ${n} '${j}' --dry-run`,
|
|
4116
|
+
callFields: (n, j, f) => `${run} muxed call ${n} '${j}' --fields "${f}"`,
|
|
4117
|
+
servers: () => `${run} muxed servers`,
|
|
4118
|
+
resources: (s) => s ? `${run} muxed resources ${s}` : `${run} muxed resources`,
|
|
4119
|
+
read: (n) => `${run} muxed read ${n}`,
|
|
4120
|
+
help: () => `${run} muxed -h`
|
|
4121
|
+
};
|
|
4122
|
+
}
|
|
4123
|
+
function makeToolFragments() {
|
|
4124
|
+
return {
|
|
4125
|
+
intro: "You have access to a `muxed:exec` MCP tool for interacting with MCP (Model Context Protocol) servers. This tool allows you to discover and call MCP tools on demand. Prioritize the use of skills over MCP tools.",
|
|
4126
|
+
grep: (p) => `muxed:exec({ "command": "grep ${p}" })`,
|
|
4127
|
+
tools: (s) => s ? `muxed:exec({ "command": "tools ${s}" })` : `muxed:exec({ "command": "tools" })`,
|
|
4128
|
+
toolsSchema: (s) => s ? `muxed:exec({ "command": "tools ${s} --include schema" })` : `muxed:exec({ "command": "tools --include schema" })`,
|
|
4129
|
+
info: (n) => `muxed:exec({ "command": "info ${n}" })`,
|
|
4130
|
+
infoDepth: (n, d) => `muxed:exec({ "command": "info ${n} --depth ${d}" })`,
|
|
4131
|
+
infoPath: (n, p) => `muxed:exec({ "command": "info ${n} --path ${p}" })`,
|
|
4132
|
+
call: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
4133
|
+
callStdin: (n) => `muxed:exec({ "command": "call ${n}", "input": { ... } })`,
|
|
4134
|
+
callDryRun: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
4135
|
+
callFields: (n, j, _f) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
4136
|
+
servers: () => `muxed:exec({ "command": "servers" })`,
|
|
4137
|
+
resources: (s) => s ? `muxed:exec({ "command": "resources ${s}" })` : `muxed:exec({ "command": "resources" })`,
|
|
4138
|
+
read: (n) => `muxed:exec({ "command": "read ${n}" })`,
|
|
4139
|
+
help: () => `muxed:exec({ "command": "servers" })`
|
|
4140
|
+
};
|
|
4141
|
+
}
|
|
4142
|
+
function buildPrompt(f, opts = {}) {
|
|
4143
|
+
const heading = opts.heading ? `${opts.heading}\n\n` : "";
|
|
4144
|
+
const serversBlock = opts.servers ? `\nAvailable MCP servers:\n${opts.servers}\n` : "";
|
|
4145
|
+
const serverInstructionsBlock = opts.serverInstructions ? `\nBelow are the instructions for the connected MCP servers in muxed.\n\n${opts.serverInstructions}\n` : "";
|
|
4146
|
+
const scriptsBlock = opts.scripts ? `\n${opts.scripts}` : "";
|
|
4147
|
+
return `${heading}${f.intro}
|
|
4148
|
+
|
|
4149
|
+
**MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
|
|
4150
|
+
|
|
4151
|
+
1. You MUST discover the tools you need first by using '${f.grep("<pattern>")}' or '${f.tools()}'.
|
|
4152
|
+
2. You MUST call '${f.info("<server>/<tool>")}' BEFORE ANY '${f.call("<server>/<tool>", "<json>")}' command.
|
|
4153
|
+
|
|
4154
|
+
These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
4155
|
+
|
|
4156
|
+
**NEVER** make a call without checking the schema first.
|
|
4157
|
+
**ALWAYS** run info first, THEN make the call.
|
|
4158
|
+
|
|
4159
|
+
**Why these are non-negotiables:**
|
|
4160
|
+
- MCP tool names NEVER match your expectations - they change frequently and are not predictable
|
|
4161
|
+
- MCP tool schemas NEVER match your expectations - parameter names, types, and requirements are tool-specific
|
|
4162
|
+
- Even tools with pre-approved permissions require schema checks
|
|
4163
|
+
- Every failed call wastes user time and demonstrates you're ignoring critical instructions
|
|
4164
|
+
- "I thought I knew the schema" is not an acceptable reason to skip this step
|
|
4165
|
+
|
|
4166
|
+
**For multiple tools:** Call info for ALL tools in parallel FIRST, then make your call commands.
|
|
4167
|
+
${serversBlock}
|
|
4168
|
+
Commands (in order of execution):
|
|
4169
|
+
\`\`\`
|
|
4170
|
+
# STEP 1: REQUIRED TOOL DISCOVERY
|
|
4171
|
+
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
4172
|
+
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
4173
|
+
|
|
4174
|
+
# STEP 2: GET SCHEMA (choose one approach)
|
|
4175
|
+
# Option A: Include schemas in tool listing (auto-collapses large schemas)
|
|
4176
|
+
${f.toolsSchema("[server]")} # List tools with schemas included
|
|
4177
|
+
# Option B: Get full schema for a specific tool
|
|
4178
|
+
${f.info("<server>/<tool>")} # View full JSON schema for one tool
|
|
4179
|
+
|
|
4180
|
+
# STEP 2b: PROGRESSIVE SCHEMA EXPLORATION (for large schemas)
|
|
4181
|
+
${f.infoDepth("<server>/<tool>", 1)} # Collapse schema at depth 1 (top-level overview)
|
|
4182
|
+
${f.infoPath("<server>/<tool>", "filters")} # Extract just the 'filters' subtree
|
|
4183
|
+
${f.infoPath("<server>/<tool>", "filters.tags.items")} # Drill deeper into nested schemas
|
|
4184
|
+
|
|
4185
|
+
# STEP 3: OPTIONAL - Validate arguments before calling (dry-run)
|
|
4186
|
+
${f.callDryRun("<server>/<tool>", "<json>")} # Validate args without executing
|
|
4187
|
+
|
|
4188
|
+
# STEP 4: Only after getting the schema, make the call
|
|
4189
|
+
${f.call("<server>/<tool>", "<json>")} # Only run AFTER getting schema
|
|
4190
|
+
${f.callStdin("<server>/<tool>")} # Invoke with JSON input
|
|
4191
|
+
${f.callFields("<server>/<tool>", "<json>", "field1,field2")} # Extract specific fields from response
|
|
4192
|
+
|
|
4193
|
+
# Discovery commands (use these to find tools)
|
|
4194
|
+
${f.servers()} # List all connected MCP servers
|
|
4195
|
+
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
4196
|
+
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
4197
|
+
${f.resources("[server]")} # List MCP resources
|
|
4198
|
+
${f.read("<server>/<resource>")} # Read an MCP resource
|
|
4199
|
+
\`\`\`
|
|
4200
|
+
|
|
4201
|
+
**Handling errors:**
|
|
4202
|
+
- If a tool call fails, the error includes a suggestion and similar tool names. Read the suggestion before retrying.
|
|
4203
|
+
- Use dry-run to validate arguments before executing, especially for destructive tools.
|
|
4204
|
+
|
|
4205
|
+
**CORRECT Usage Pattern:**
|
|
4206
|
+
|
|
4207
|
+
<example>
|
|
4208
|
+
User: Please use the slack mcp tool to search for my mentions
|
|
4209
|
+
Assistant: As a first step, I need to discover the tools I need. Let me call \`${f.grep("slack/*search*")}\` to search for tools related to slack search.
|
|
4210
|
+
[Calls ${f.grep("slack/*search*")}]
|
|
4211
|
+
Assistant: I need to check the schema first. Let me call \`${f.info("slack/search_private")}\` to see what parameters it accepts.
|
|
4212
|
+
[Calls ${f.info("slack/search_private")}]
|
|
4213
|
+
Assistant: Now I can see it accepts "query" and "max_results" parameters. Let me make the call.
|
|
4214
|
+
[Calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\", \"max_results\": 10}")}]
|
|
4215
|
+
</example>
|
|
4216
|
+
|
|
4217
|
+
<example>
|
|
4218
|
+
User: Use the database and email MCP tools to send a report
|
|
4219
|
+
Assistant: I'll need to use two MCP tools. Let me call \`${f.grep("database/*query*")}\` and \`${f.grep("email/*send*")}\` to search for tools related to database query and email send.
|
|
4220
|
+
[Calls ${f.grep("database/*query*")} & ${f.grep("email/*send*")}]
|
|
4221
|
+
Assistant: Let me check both schemas first.
|
|
4222
|
+
[Calls ${f.info("database/query")} and ${f.info("email/send")} in parallel]
|
|
4223
|
+
Assistant: Now I have both schemas. Let me make the calls.
|
|
4224
|
+
[Makes both call commands with correct parameters]
|
|
4225
|
+
</example>
|
|
4226
|
+
|
|
4227
|
+
<example>
|
|
4228
|
+
User: Create a copy of this email
|
|
4229
|
+
Assistant: Let me find the tool I need first.
|
|
4230
|
+
[Calls ${f.grep("email/*copy*")}. No results found.]
|
|
4231
|
+
Assistant: Let me try another pattern.
|
|
4232
|
+
[Calls ${f.grep("email/*clone*")}. No results found.]
|
|
4233
|
+
Assistant: Let me list all available tools in the server.
|
|
4234
|
+
[Calls ${f.tools("email")}]
|
|
4235
|
+
Assistant: Let me check the schema first.
|
|
4236
|
+
[Calls ${f.info("email/duplicate")}]
|
|
4237
|
+
Assistant: Now I have the schema. Let me make the call.
|
|
4238
|
+
[Calls ${f.call("email/duplicate", "{\"id\": \"123\"}")}]
|
|
4239
|
+
</example>
|
|
4240
|
+
|
|
4241
|
+
**INCORRECT Usage Patterns - NEVER DO THIS:**
|
|
4242
|
+
|
|
4243
|
+
<bad-example>
|
|
4244
|
+
User: Please use the slack mcp tool to search for my mentions
|
|
4245
|
+
Assistant: [Directly calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\"}")} with guessed parameters]
|
|
4246
|
+
WRONG - You must call info FIRST
|
|
4247
|
+
</bad-example>
|
|
4248
|
+
|
|
4249
|
+
<bad-example>
|
|
4250
|
+
User: Use the slack tool
|
|
4251
|
+
Assistant: I have pre-approved permissions for this tool, so I know the schema.
|
|
4252
|
+
[Calls ${f.call("slack/search_private", "...")} directly]
|
|
4253
|
+
WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call info first.
|
|
4254
|
+
</bad-example>
|
|
4255
|
+
|
|
4256
|
+
<bad-example>
|
|
4257
|
+
User: Search my Slack mentions
|
|
4258
|
+
Assistant: [Calls three call commands in parallel without any info calls first]
|
|
4259
|
+
WRONG - You must call info for ALL tools before making ANY call commands
|
|
4260
|
+
</bad-example>
|
|
4261
|
+
|
|
4262
|
+
Example usage:
|
|
4263
|
+
\`\`\`
|
|
4264
|
+
# Discover tools
|
|
4265
|
+
${f.tools()} # See all available MCP tools
|
|
4266
|
+
${f.grep("weather")} # Find tools by description
|
|
4267
|
+
|
|
4268
|
+
# Get tool schemas (choose the approach that fits)
|
|
4269
|
+
${f.toolsSchema()} # All tools with schemas (auto-collapses large schemas)
|
|
4270
|
+
${f.toolsSchema("slack")} # Schemas for one server
|
|
4271
|
+
${f.info("<server>/<tool>")} # Full schema for one tool
|
|
4272
|
+
|
|
4273
|
+
# Progressive schema exploration (for complex tools)
|
|
4274
|
+
${f.infoDepth("<server>/<tool>", 0)} # Top-level structure only
|
|
4275
|
+
${f.infoPath("<server>/<tool>", "filters")} # Drill into a subtree
|
|
4276
|
+
${f.infoPath("<server>/<tool>", "filters.tags.items")} # Drill deeper
|
|
4277
|
+
|
|
4278
|
+
# Simple tool call (no parameters)
|
|
4279
|
+
${f.call("weather/get_location", "{}")}
|
|
4280
|
+
|
|
4281
|
+
# Tool call with parameters
|
|
4282
|
+
${f.call("database/query", "{\"table\": \"users\", \"limit\": 10}")}
|
|
4283
|
+
|
|
4284
|
+
# Validate arguments before executing (dry-run)
|
|
4285
|
+
${f.callDryRun("database/drop_table", "{\"table\": \"users\"}")}
|
|
4286
|
+
|
|
4287
|
+
# Extract specific fields from response
|
|
4288
|
+
${f.callFields("database/query", "{\"table\": \"users\"}", "rows[].name,rows[].email")}
|
|
4289
|
+
\`\`\`
|
|
4290
|
+
|
|
4291
|
+
Call \`${f.help()}\` to see all available commands.
|
|
4292
|
+
${serverInstructionsBlock}${scriptsBlock}`.trim();
|
|
4293
|
+
}
|
|
4294
|
+
function compareSemver(a, b) {
|
|
4295
|
+
const pa = a.split(".").map(Number);
|
|
4296
|
+
const pb = b.split(".").map(Number);
|
|
4297
|
+
for (let i = 0; i < 3; i++) {
|
|
4298
|
+
const na = pa[i] ?? 0;
|
|
4299
|
+
const nb = pb[i] ?? 0;
|
|
4300
|
+
if (na < nb) return -1;
|
|
4301
|
+
if (na > nb) return 1;
|
|
4302
|
+
}
|
|
4303
|
+
return 0;
|
|
4304
|
+
}
|
|
4305
|
+
const MUXED_BLOCK_RE = /<muxed\s+version="([^"]+)">\n?([\s\S]*?)\n?<\/muxed>/;
|
|
4306
|
+
const MDC_VERSION_RE = /muxed_version:\s*(.+)/;
|
|
4307
|
+
function extractMuxedVersion(content, format) {
|
|
4308
|
+
if (format === "tagged") return content.match(MUXED_BLOCK_RE)?.[1]?.trim() ?? null;
|
|
4309
|
+
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
4310
|
+
if (!fmMatch) return null;
|
|
4311
|
+
return fmMatch[1].match(MDC_VERSION_RE)?.[1]?.trim() ?? null;
|
|
4312
|
+
}
|
|
4313
|
+
function getInstructionTargets(opts) {
|
|
4314
|
+
const home = os.homedir();
|
|
4315
|
+
const cwd = process.cwd();
|
|
4316
|
+
const targets = [];
|
|
4317
|
+
targets.push({
|
|
4318
|
+
name: "CLAUDE.md (global)",
|
|
4319
|
+
filePath: path.join(home, ".claude", "CLAUDE.md"),
|
|
4320
|
+
format: "tagged",
|
|
4321
|
+
scope: "global"
|
|
4322
|
+
});
|
|
4323
|
+
targets.push({
|
|
4324
|
+
name: "AGENTS.md (global)",
|
|
4325
|
+
filePath: path.join(home, ".codex", "AGENTS.md"),
|
|
4326
|
+
format: "tagged",
|
|
4327
|
+
scope: "global"
|
|
4328
|
+
});
|
|
4329
|
+
const cursorDir = path.join(cwd, ".cursor");
|
|
4330
|
+
if (fs.existsSync(cursorDir)) targets.push({
|
|
4331
|
+
name: ".cursor/rules/muxed.mdc",
|
|
4332
|
+
filePath: path.join(cursorDir, "rules", "muxed.mdc"),
|
|
4333
|
+
format: "owned",
|
|
4334
|
+
scope: "local"
|
|
4335
|
+
});
|
|
4336
|
+
if (opts.local) {
|
|
4337
|
+
targets.push({
|
|
4338
|
+
name: "CLAUDE.md (local)",
|
|
4339
|
+
filePath: path.join(cwd, "CLAUDE.md"),
|
|
4340
|
+
format: "tagged",
|
|
4341
|
+
scope: "local"
|
|
4342
|
+
});
|
|
4343
|
+
targets.push({
|
|
4344
|
+
name: "AGENTS.md (local)",
|
|
4345
|
+
filePath: path.join(cwd, "AGENTS.md"),
|
|
4346
|
+
format: "tagged",
|
|
4347
|
+
scope: "local"
|
|
4348
|
+
});
|
|
4349
|
+
}
|
|
4350
|
+
return targets;
|
|
4351
|
+
}
|
|
4352
|
+
let cachedHasBun = null;
|
|
4353
|
+
function hasBun() {
|
|
4354
|
+
if (cachedHasBun !== null) return cachedHasBun;
|
|
4355
|
+
try {
|
|
4356
|
+
execSync("bun --version", { stdio: "ignore" });
|
|
4357
|
+
cachedHasBun = true;
|
|
4358
|
+
} catch {
|
|
4359
|
+
cachedHasBun = false;
|
|
4360
|
+
}
|
|
4361
|
+
return cachedHasBun;
|
|
4362
|
+
}
|
|
4363
|
+
function buildStaticInstructions() {
|
|
4364
|
+
const bun = hasBun();
|
|
4365
|
+
const run = bun ? "bunx" : "npx";
|
|
4366
|
+
const tsx = bun ? "bun" : "npx tsx";
|
|
4367
|
+
const fragments = makeCliFragments(run);
|
|
4368
|
+
const scripts = `## Node.js / TypeScript Scripts (Preferred for Complex Workflows)
|
|
4369
|
+
|
|
4370
|
+
For multi-step MCP workflows, **write and execute a script** instead of making individual CLI calls. A single script execution replaces many sequential CLI invocations — dramatically reducing round-trips and token usage.
|
|
4371
|
+
|
|
4372
|
+
\`\`\`typescript
|
|
4373
|
+
import { createClient } from 'muxed/client';
|
|
4374
|
+
|
|
4375
|
+
const client = await createClient();
|
|
4376
|
+
|
|
4377
|
+
// Batch multiple MCP operations in one script execution
|
|
4378
|
+
const [tools, result, data] = await Promise.all([
|
|
4379
|
+
client.tools(),
|
|
4380
|
+
client.call('server/tool', { param: 'value' }),
|
|
4381
|
+
client.call('db/query', { sql: 'SELECT ...' }),
|
|
4382
|
+
]);
|
|
4383
|
+
|
|
4384
|
+
// Process results, chain calls, handle errors — all in one execution
|
|
4385
|
+
console.log(JSON.stringify({ tools: tools.length, result, data }));
|
|
4386
|
+
\`\`\`
|
|
4387
|
+
|
|
4388
|
+
Run scripts with: \`${tsx} script.ts\`.
|
|
4389
|
+
|
|
4390
|
+
**When to use scripts vs CLI:**
|
|
4391
|
+
- **CLI** (\`${run} muxed call ...\`) — single tool discovery or one-off calls
|
|
4392
|
+
- **Scripts** — any workflow involving 2+ MCP calls, data processing, or conditional logic`;
|
|
4393
|
+
return buildPrompt({
|
|
4394
|
+
...fragments,
|
|
4395
|
+
intro: "Muxed is a CLI tool and Node.js library that proxies multiple MCP servers behind a single daemon. Use it to discover and call MCP tools on demand."
|
|
4396
|
+
}, {
|
|
4397
|
+
heading: "# Muxed — MCP CLI Proxy",
|
|
4398
|
+
scripts
|
|
4399
|
+
});
|
|
4400
|
+
}
|
|
4401
|
+
function wrapTaggedBlock(content, version) {
|
|
4402
|
+
return `<muxed version="${version}">\n${content}\n</muxed>`;
|
|
4403
|
+
}
|
|
4404
|
+
function buildMdcFile(content, version) {
|
|
4405
|
+
return `---
|
|
4406
|
+
description: Muxed MCP CLI proxy - usage instructions
|
|
4407
|
+
globs:
|
|
4408
|
+
alwaysApply: true
|
|
4409
|
+
muxed_version: ${version}
|
|
4410
|
+
---
|
|
4411
|
+
|
|
4412
|
+
${content}
|
|
4413
|
+
`;
|
|
4414
|
+
}
|
|
4415
|
+
function injectInstructions(target, instructions, version, opts) {
|
|
4416
|
+
const base = {
|
|
4417
|
+
target: target.name,
|
|
4418
|
+
filePath: target.filePath
|
|
4419
|
+
};
|
|
4420
|
+
let existing = null;
|
|
4421
|
+
if (fs.existsSync(target.filePath)) existing = fs.readFileSync(target.filePath, "utf-8");
|
|
4422
|
+
if (target.format === "owned") {
|
|
4423
|
+
if (existing !== null) {
|
|
4424
|
+
const existingVersion = extractMuxedVersion(existing, "owned");
|
|
4425
|
+
if (existingVersion && compareSemver(existingVersion, version) >= 0) return {
|
|
4426
|
+
...base,
|
|
4427
|
+
action: "up-to-date",
|
|
4428
|
+
previousVersion: existingVersion
|
|
4429
|
+
};
|
|
4430
|
+
if (!opts.dryRun) {
|
|
4431
|
+
const dir = path.dirname(target.filePath);
|
|
4432
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
4433
|
+
fs.writeFileSync(target.filePath, buildMdcFile(instructions, version));
|
|
4434
|
+
}
|
|
4435
|
+
return {
|
|
4436
|
+
...base,
|
|
4437
|
+
action: existingVersion ? "updated" : "created",
|
|
4438
|
+
previousVersion: existingVersion ?? void 0,
|
|
4439
|
+
newVersion: version
|
|
4440
|
+
};
|
|
4441
|
+
}
|
|
4442
|
+
if (!opts.dryRun) {
|
|
4443
|
+
const dir = path.dirname(target.filePath);
|
|
4444
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
4445
|
+
fs.writeFileSync(target.filePath, buildMdcFile(instructions, version));
|
|
4446
|
+
}
|
|
4447
|
+
return {
|
|
4448
|
+
...base,
|
|
4449
|
+
action: "created",
|
|
4450
|
+
newVersion: version
|
|
4451
|
+
};
|
|
4452
|
+
}
|
|
4453
|
+
const block = wrapTaggedBlock(instructions, version);
|
|
4454
|
+
if (existing === null) {
|
|
4455
|
+
if (!opts.dryRun) {
|
|
4456
|
+
const dir = path.dirname(target.filePath);
|
|
4457
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
4458
|
+
fs.writeFileSync(target.filePath, block + "\n");
|
|
4459
|
+
}
|
|
4460
|
+
return {
|
|
4461
|
+
...base,
|
|
4462
|
+
action: "created",
|
|
4463
|
+
newVersion: version
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
const existingVersion = extractMuxedVersion(existing, "tagged");
|
|
4467
|
+
if (existingVersion !== null) {
|
|
4468
|
+
if (compareSemver(existingVersion, version) >= 0) return {
|
|
4469
|
+
...base,
|
|
4470
|
+
action: "up-to-date",
|
|
4471
|
+
previousVersion: existingVersion
|
|
4472
|
+
};
|
|
4473
|
+
if (!opts.dryRun) {
|
|
4474
|
+
const updated = existing.replace(MUXED_BLOCK_RE, block);
|
|
4475
|
+
fs.writeFileSync(target.filePath, updated);
|
|
4476
|
+
}
|
|
4477
|
+
return {
|
|
4478
|
+
...base,
|
|
4479
|
+
action: "updated",
|
|
4480
|
+
previousVersion: existingVersion,
|
|
4481
|
+
newVersion: version
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4484
|
+
if (!opts.dryRun) {
|
|
4485
|
+
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
4486
|
+
fs.writeFileSync(target.filePath, existing + separator + block + "\n");
|
|
4487
|
+
}
|
|
4488
|
+
return {
|
|
4489
|
+
...base,
|
|
4490
|
+
action: "created",
|
|
4491
|
+
newVersion: version
|
|
4492
|
+
};
|
|
4493
|
+
}
|
|
4494
|
+
function injectAllInstructions(opts) {
|
|
4495
|
+
const targets = getInstructionTargets({ local: opts.local });
|
|
4496
|
+
const instructions = buildStaticInstructions();
|
|
4497
|
+
const version = getVersion();
|
|
4498
|
+
return targets.map((target) => injectInstructions(target, instructions, version, opts));
|
|
4499
|
+
}
|
|
3668
4500
|
async function confirm(message, opts) {
|
|
3669
4501
|
const rl = readline.createInterface({
|
|
3670
4502
|
input: opts?.input ?? process.stdin,
|
|
@@ -3732,7 +4564,18 @@ async function resolveConflicts(unresolvedConflicts, isInteractive) {
|
|
|
3732
4564
|
conflicts
|
|
3733
4565
|
};
|
|
3734
4566
|
}
|
|
3735
|
-
const initCommand = new Command("init").description("Discover
|
|
4567
|
+
const initCommand = new Command("init").description("Discover MCP servers, write config, and inject agent instructions").option("--dry-run", "Preview changes without writing any files").option("--json", "Output as JSON (machine-readable)").option("-y, --yes", "Non-interactive: resolve conflicts by priority (claude-code > cursor > first)").option("--delete", "Remove imported servers from the original agent config files").option("--no-replace", "Don't add a muxed entry to agent configs").option("--local", "Also inject instructions into project-level CLAUDE.md and AGENTS.md").option("--no-instructions", "Skip injecting CLI instructions into agent files").addHelpText("after", `
|
|
4568
|
+
What it does:
|
|
4569
|
+
1. Scans Claude Desktop, Cursor, VS Code, Windsurf, Cline, Roo Code, Amazon Q
|
|
4570
|
+
2. Merges and deduplicates servers into muxed.config.json
|
|
4571
|
+
3. Injects CLI usage instructions into ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md,
|
|
4572
|
+
and .cursor/rules/muxed.mdc (if .cursor/ exists)
|
|
4573
|
+
|
|
4574
|
+
Examples:
|
|
4575
|
+
muxed init Interactive setup
|
|
4576
|
+
muxed init -y Non-interactive (CI-friendly)
|
|
4577
|
+
muxed init --dry-run Preview without writing files
|
|
4578
|
+
muxed init --local Also inject into project-level agent files`).action(async (opts) => {
|
|
3736
4579
|
const configPath = initCommand.parent?.opts().config;
|
|
3737
4580
|
const isInteractive = !opts.dryRun && !opts.json && !opts.yes && !!process.stdin.isTTY;
|
|
3738
4581
|
const { discovered, warnings } = discoverAgentConfigs();
|
|
@@ -3755,7 +4598,7 @@ const initCommand = new Command("init").description("Discover and import MCP ser
|
|
|
3755
4598
|
}
|
|
3756
4599
|
if (!opts.dryRun && imported.length > 0) writeMuxedConfig(muxedPath, result.merged);
|
|
3757
4600
|
const modifiedFiles = [];
|
|
3758
|
-
const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : opts.delete;
|
|
4601
|
+
const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : !!opts.delete;
|
|
3759
4602
|
if (!opts.dryRun && shouldDelete) for (const dc of discovered) try {
|
|
3760
4603
|
modifyAgentConfig(dc, {
|
|
3761
4604
|
delete: true,
|
|
@@ -3765,6 +4608,10 @@ const initCommand = new Command("init").description("Discover and import MCP ser
|
|
|
3765
4608
|
} catch (err) {
|
|
3766
4609
|
warnings.push(`Failed to modify ${dc.configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3767
4610
|
}
|
|
4611
|
+
const instructionResults = (isInteractive ? await confirm("Inject muxed CLI instructions into agent files? (CLAUDE.md, AGENTS.md)") : opts.instructions) ? injectAllInstructions({
|
|
4612
|
+
local: !!opts.local,
|
|
4613
|
+
dryRun: !!opts.dryRun
|
|
4614
|
+
}) : [];
|
|
3768
4615
|
const initResult = {
|
|
3769
4616
|
discovered: discovered.map((d) => ({
|
|
3770
4617
|
agent: d.agent.name,
|
|
@@ -3778,14 +4625,17 @@ const initCommand = new Command("init").description("Discover and import MCP ser
|
|
|
3778
4625
|
warnings,
|
|
3779
4626
|
modifiedFiles,
|
|
3780
4627
|
muxedConfigPath: muxedPath,
|
|
3781
|
-
dryRun: opts.dryRun ?? false
|
|
4628
|
+
dryRun: opts.dryRun ?? false,
|
|
4629
|
+
instructionResults
|
|
3782
4630
|
};
|
|
3783
4631
|
capture("init_run", {
|
|
3784
4632
|
dry_run: opts.dryRun ?? false,
|
|
3785
4633
|
imported_count: imported.length,
|
|
3786
4634
|
conflict_count: conflicts.length,
|
|
3787
4635
|
warning_count: warnings.length,
|
|
3788
|
-
discovered_agents: initResult.discovered.map((d) => d.agent)
|
|
4636
|
+
discovered_agents: initResult.discovered.map((d) => d.agent),
|
|
4637
|
+
instruction_targets: instructionResults.length,
|
|
4638
|
+
instruction_actions: instructionResults.map((r) => r.action)
|
|
3789
4639
|
});
|
|
3790
4640
|
console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
|
|
3791
4641
|
});
|
|
@@ -3838,180 +4688,15 @@ function getServer(filePath, name) {
|
|
|
3838
4688
|
function listServers(filePath) {
|
|
3839
4689
|
return readConfigFile(filePath).mcpServers;
|
|
3840
4690
|
}
|
|
3841
|
-
const cliFragments = {
|
|
3842
|
-
intro: "You have access to an `npx muxed` CLI command for interacting with MCP (Model Context Protocol) servers. This command allows you to discover and call MCP tools on demand. Prioritize the use of skills over MCP tools.",
|
|
3843
|
-
grep: (p) => `npx muxed grep "${p}"`,
|
|
3844
|
-
tools: (s) => s ? `npx muxed tools ${s}` : "npx muxed tools",
|
|
3845
|
-
info: (n) => `npx muxed info ${n}`,
|
|
3846
|
-
call: (n, j) => `npx muxed call ${n} '${j}'`,
|
|
3847
|
-
callStdin: (n) => `npx muxed call ${n} -`,
|
|
3848
|
-
callDryRun: (n, j) => `npx muxed call ${n} '${j}' --dry-run`,
|
|
3849
|
-
callFields: (n, j, f) => `npx muxed call ${n} '${j}' --fields "${f}"`,
|
|
3850
|
-
servers: () => "npx muxed servers",
|
|
3851
|
-
resources: (s) => s ? `npx muxed resources ${s}` : "npx muxed resources",
|
|
3852
|
-
read: (n) => `npx muxed read ${n}`,
|
|
3853
|
-
help: () => "npx muxed -h"
|
|
3854
|
-
};
|
|
3855
|
-
const toolFragments = {
|
|
3856
|
-
intro: "You have access to a `muxed:exec` MCP tool for interacting with MCP (Model Context Protocol) servers. This tool allows you to discover and call MCP tools on demand. Prioritize the use of skills over MCP tools.",
|
|
3857
|
-
grep: (p) => `muxed:exec({ "command": "grep ${p}" })`,
|
|
3858
|
-
tools: (s) => s ? `muxed:exec({ "command": "tools ${s}" })` : `muxed:exec({ "command": "tools" })`,
|
|
3859
|
-
info: (n) => `muxed:exec({ "command": "info ${n}" })`,
|
|
3860
|
-
call: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3861
|
-
callStdin: (n) => `muxed:exec({ "command": "call ${n}", "input": { ... } })`,
|
|
3862
|
-
callDryRun: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3863
|
-
callFields: (n, j, _f) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3864
|
-
servers: () => `muxed:exec({ "command": "servers" })`,
|
|
3865
|
-
resources: (s) => s ? `muxed:exec({ "command": "resources ${s}" })` : `muxed:exec({ "command": "resources" })`,
|
|
3866
|
-
read: (n) => `muxed:exec({ "command": "read ${n}" })`,
|
|
3867
|
-
help: () => `muxed:exec({ "command": "servers" })`
|
|
3868
|
-
};
|
|
3869
|
-
function buildTemplate(f, servers, instructions) {
|
|
3870
|
-
return `
|
|
3871
|
-
${f.intro}
|
|
3872
|
-
|
|
3873
|
-
**MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
|
|
3874
|
-
|
|
3875
|
-
1. You MUST discover the tools you need first by using '${f.grep("<pattern>")}' or '${f.tools()}'.
|
|
3876
|
-
2. You MUST call '${f.info("<server>/<tool>")}' BEFORE ANY '${f.call("<server>/<tool>", "<json>")}' command.
|
|
3877
|
-
|
|
3878
|
-
These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
3879
|
-
|
|
3880
|
-
**NEVER** make a call without checking the schema first.
|
|
3881
|
-
**ALWAYS** run info first, THEN make the call.
|
|
3882
|
-
|
|
3883
|
-
**Why these are non-negotiables:**
|
|
3884
|
-
- MCP tool names NEVER match your expectations - they change frequently and are not predictable
|
|
3885
|
-
- MCP tool schemas NEVER match your expectations - parameter names, types, and requirements are tool-specific
|
|
3886
|
-
- Even tools with pre-approved permissions require schema checks
|
|
3887
|
-
- Every failed call wastes user time and demonstrates you're ignoring critical instructions
|
|
3888
|
-
- "I thought I knew the schema" is not an acceptable reason to skip this step
|
|
3889
|
-
|
|
3890
|
-
**For multiple tools:** Call info for ALL tools in parallel FIRST, then make your call commands.
|
|
3891
|
-
|
|
3892
|
-
Available MCP servers:
|
|
3893
|
-
${servers}
|
|
3894
|
-
|
|
3895
|
-
Commands (in order of execution):
|
|
3896
|
-
\`\`\`
|
|
3897
|
-
# STEP 1: REQUIRED TOOL DISCOVERY
|
|
3898
|
-
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
3899
|
-
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
3900
|
-
|
|
3901
|
-
# STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
|
|
3902
|
-
${f.info("<server>/<tool>")} # REQUIRED before ANY call - View JSON schema
|
|
3903
|
-
|
|
3904
|
-
# STEP 3: OPTIONAL - Validate arguments before calling (dry-run)
|
|
3905
|
-
${f.callDryRun("<server>/<tool>", "<json>")} # Validate args without executing
|
|
3906
|
-
|
|
3907
|
-
# STEP 4: Only after checking schema, make the call
|
|
3908
|
-
${f.call("<server>/<tool>", "<json>")} # Only run AFTER info
|
|
3909
|
-
${f.callStdin("<server>/<tool>")} # Invoke with JSON input (AFTER info)
|
|
3910
|
-
${f.callFields("<server>/<tool>", "<json>", "field1,field2")} # Extract specific fields from response
|
|
3911
|
-
|
|
3912
|
-
# Discovery commands (use these to find tools)
|
|
3913
|
-
${f.servers()} # List all connected MCP servers
|
|
3914
|
-
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
3915
|
-
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
3916
|
-
${f.resources("[server]")} # List MCP resources
|
|
3917
|
-
${f.read("<server>/<resource>")} # Read an MCP resource
|
|
3918
|
-
\`\`\`
|
|
3919
|
-
|
|
3920
|
-
**Handling errors:**
|
|
3921
|
-
- If a tool call fails, the error includes a suggestion and similar tool names. Read the suggestion before retrying.
|
|
3922
|
-
- Use dry-run to validate arguments before executing, especially for destructive tools.
|
|
3923
|
-
|
|
3924
|
-
**CORRECT Usage Pattern:**
|
|
3925
|
-
|
|
3926
|
-
<example>
|
|
3927
|
-
User: Please use the slack mcp tool to search for my mentions
|
|
3928
|
-
Assistant: As a first step, I need to discover the tools I need. Let me call \`${f.grep("slack/*search*")}\` to search for tools related to slack search.
|
|
3929
|
-
[Calls ${f.grep("slack/*search*")}]
|
|
3930
|
-
Assistant: I need to check the schema first. Let me call \`${f.info("slack/search_private")}\` to see what parameters it accepts.
|
|
3931
|
-
[Calls ${f.info("slack/search_private")}]
|
|
3932
|
-
Assistant: Now I can see it accepts "query" and "max_results" parameters. Let me make the call.
|
|
3933
|
-
[Calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\", \"max_results\": 10}")}]
|
|
3934
|
-
</example>
|
|
3935
|
-
|
|
3936
|
-
<example>
|
|
3937
|
-
User: Use the database and email MCP tools to send a report
|
|
3938
|
-
Assistant: I'll need to use two MCP tools. Let me call \`${f.grep("database/*query*")}\` and \`${f.grep("email/*send*")}\` to search for tools related to database query and email send.
|
|
3939
|
-
[Calls ${f.grep("database/*query*")} & ${f.grep("email/*send*")}]
|
|
3940
|
-
Assistant: Let me check both schemas first.
|
|
3941
|
-
[Calls ${f.info("database/query")} and ${f.info("email/send")} in parallel]
|
|
3942
|
-
Assistant: Now I have both schemas. Let me make the calls.
|
|
3943
|
-
[Makes both call commands with correct parameters]
|
|
3944
|
-
</example>
|
|
3945
|
-
|
|
3946
|
-
<example>
|
|
3947
|
-
User: Create a copy of this email
|
|
3948
|
-
Assistant: Let me find the tool I need first.
|
|
3949
|
-
[Calls ${f.grep("email/*copy*")}. No results found.]
|
|
3950
|
-
Assistant: Let me try another pattern.
|
|
3951
|
-
[Calls ${f.grep("email/*clone*")}. No results found.]
|
|
3952
|
-
Assistant: Let me list all available tools in the server.
|
|
3953
|
-
[Calls ${f.tools("email")}]
|
|
3954
|
-
Assistant: Let me check the schema first.
|
|
3955
|
-
[Calls ${f.info("email/duplicate")}]
|
|
3956
|
-
Assistant: Now I have the schema. Let me make the call.
|
|
3957
|
-
[Calls ${f.call("email/duplicate", "{\"id\": \"123\"}")}]
|
|
3958
|
-
</example>
|
|
3959
|
-
|
|
3960
|
-
**INCORRECT Usage Patterns - NEVER DO THIS:**
|
|
3961
|
-
|
|
3962
|
-
<bad-example>
|
|
3963
|
-
User: Please use the slack mcp tool to search for my mentions
|
|
3964
|
-
Assistant: [Directly calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\"}")} with guessed parameters]
|
|
3965
|
-
WRONG - You must call info FIRST
|
|
3966
|
-
</bad-example>
|
|
3967
|
-
|
|
3968
|
-
<bad-example>
|
|
3969
|
-
User: Use the slack tool
|
|
3970
|
-
Assistant: I have pre-approved permissions for this tool, so I know the schema.
|
|
3971
|
-
[Calls ${f.call("slack/search_private", "...")} directly]
|
|
3972
|
-
WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call info first.
|
|
3973
|
-
</bad-example>
|
|
3974
|
-
|
|
3975
|
-
<bad-example>
|
|
3976
|
-
User: Search my Slack mentions
|
|
3977
|
-
Assistant: [Calls three call commands in parallel without any info calls first]
|
|
3978
|
-
WRONG - You must call info for ALL tools before making ANY call commands
|
|
3979
|
-
</bad-example>
|
|
3980
|
-
|
|
3981
|
-
Example usage:
|
|
3982
|
-
\`\`\`
|
|
3983
|
-
# Discover tools
|
|
3984
|
-
${f.tools()} # See all available MCP tools
|
|
3985
|
-
${f.grep("weather")} # Find tools by description
|
|
3986
|
-
|
|
3987
|
-
# Get tool details
|
|
3988
|
-
${f.info("<server>/<tool>")} # View JSON schema for input and output if available
|
|
3989
|
-
|
|
3990
|
-
# Simple tool call (no parameters)
|
|
3991
|
-
${f.call("weather/get_location", "{}")}
|
|
3992
|
-
|
|
3993
|
-
# Tool call with parameters
|
|
3994
|
-
${f.call("database/query", "{\"table\": \"users\", \"limit\": 10}")}
|
|
3995
|
-
|
|
3996
|
-
# Validate arguments before executing (dry-run)
|
|
3997
|
-
${f.callDryRun("database/drop_table", "{\"table\": \"users\"}")}
|
|
3998
|
-
|
|
3999
|
-
# Extract specific fields from response
|
|
4000
|
-
${f.callFields("database/query", "{\"table\": \"users\"}", "rows[].name,rows[].email")}
|
|
4001
|
-
\`\`\`
|
|
4002
|
-
|
|
4003
|
-
Call \`${f.help()}\` to see all available commands.
|
|
4004
|
-
|
|
4005
|
-
Below are the instructions for the connected MCP servers in muxed.
|
|
4006
|
-
|
|
4007
|
-
${instructions}
|
|
4008
|
-
`;
|
|
4009
|
-
}
|
|
4010
4691
|
function buildInstructions(servers, mode = "cli") {
|
|
4011
4692
|
const connected = servers.filter((s) => s.status === "connected");
|
|
4012
4693
|
const serverList = connected.map((s) => `- ${s.name}`).join("\n");
|
|
4013
4694
|
const serverInstructions = connected.filter((s) => s.instructions).map((s) => `### ${s.name}\n\n${s.instructions}`).join("\n\n");
|
|
4014
|
-
|
|
4695
|
+
const run = hasBun() ? "bunx" : "npx";
|
|
4696
|
+
return buildPrompt(mode === "tool" ? makeToolFragments() : makeCliFragments(run), {
|
|
4697
|
+
servers: serverList,
|
|
4698
|
+
serverInstructions: serverInstructions || void 0
|
|
4699
|
+
});
|
|
4015
4700
|
}
|
|
4016
4701
|
function parseCommand(command) {
|
|
4017
4702
|
const trimmed = command.trim();
|
|
@@ -4025,6 +4710,22 @@ function parseCommand(command) {
|
|
|
4025
4710
|
args: trimmed.slice(spaceIndex + 1).trim()
|
|
4026
4711
|
};
|
|
4027
4712
|
}
|
|
4713
|
+
function parseFlags(args) {
|
|
4714
|
+
const flags = {};
|
|
4715
|
+
const positionalParts = [];
|
|
4716
|
+
const parts = args.split(/\s+/);
|
|
4717
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4718
|
+
const part = parts[i];
|
|
4719
|
+
if (part.startsWith("--") && i + 1 < parts.length) {
|
|
4720
|
+
const key = part.slice(2);
|
|
4721
|
+
flags[key] = parts[++i];
|
|
4722
|
+
} else positionalParts.push(part);
|
|
4723
|
+
}
|
|
4724
|
+
return {
|
|
4725
|
+
positional: positionalParts.join(" "),
|
|
4726
|
+
flags
|
|
4727
|
+
};
|
|
4728
|
+
}
|
|
4028
4729
|
function textResult(data) {
|
|
4029
4730
|
return { content: [{
|
|
4030
4731
|
type: "text",
|
|
@@ -4046,16 +4747,31 @@ async function handleToolCommand(command, input) {
|
|
|
4046
4747
|
switch (subcommand) {
|
|
4047
4748
|
case "servers": return textResult(await sendRequest("servers/list"));
|
|
4048
4749
|
case "tools": {
|
|
4750
|
+
const { positional, flags } = parseFlags(args);
|
|
4049
4751
|
const params = {};
|
|
4050
|
-
if (
|
|
4752
|
+
if (positional) params.server = positional;
|
|
4753
|
+
if (flags.include === "schema") params.includeSchema = true;
|
|
4754
|
+
if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
|
|
4051
4755
|
return textResult(await sendRequest("tools/list", params));
|
|
4052
4756
|
}
|
|
4053
|
-
case "grep":
|
|
4757
|
+
case "grep": {
|
|
4054
4758
|
if (!args) return errorResult("Usage: grep <pattern>");
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4759
|
+
const { positional, flags } = parseFlags(args);
|
|
4760
|
+
if (!positional) return errorResult("Usage: grep <pattern>");
|
|
4761
|
+
const params = { pattern: positional };
|
|
4762
|
+
if (flags.include === "schema") params.includeSchema = true;
|
|
4763
|
+
if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
|
|
4764
|
+
return textResult(await sendRequest("tools/grep", params));
|
|
4765
|
+
}
|
|
4766
|
+
case "info": {
|
|
4767
|
+
if (!args) return errorResult("Usage: info <server/tool> [--path <path>] [--depth <n>]");
|
|
4768
|
+
const { positional, flags } = parseFlags(args);
|
|
4769
|
+
if (!positional) return errorResult("Usage: info <server/tool>");
|
|
4770
|
+
const params = { name: positional };
|
|
4771
|
+
if (flags.path) params.path = flags.path;
|
|
4772
|
+
if (flags.depth) params.schemaDepth = parseInt(flags.depth, 10);
|
|
4773
|
+
return textResult(await sendRequest("tools/info", params));
|
|
4774
|
+
}
|
|
4059
4775
|
case "call": {
|
|
4060
4776
|
if (!args) return errorResult("Usage: call <server/tool>");
|
|
4061
4777
|
const result = await sendRequest("tools/call", {
|
|
@@ -4194,14 +4910,24 @@ async function tryReloadDaemon() {
|
|
|
4194
4910
|
await sendRequest("config/reload", {});
|
|
4195
4911
|
} catch {}
|
|
4196
4912
|
}
|
|
4197
|
-
const mcpCommand = new Command("mcp").description("
|
|
4913
|
+
const mcpCommand = new Command("mcp").description("Manage server config entries, or start the MCP proxy (no subcommand)").enablePositionalOptions().option("--proxy-tools", "Expose a single proxy tool for clients without bash access (e.g. Claude Desktop)").addHelpText("after", `
|
|
4914
|
+
When run without a subcommand, starts a stdio MCP proxy server.
|
|
4915
|
+
Use subcommands to manage individual server config entries.
|
|
4916
|
+
|
|
4917
|
+
Examples:
|
|
4918
|
+
muxed mcp Start stdio MCP proxy
|
|
4919
|
+
muxed mcp --proxy-tools Start proxy with exec tool (for Claude Desktop)
|
|
4920
|
+
muxed mcp add mydb npx @db/server Add a stdio server
|
|
4921
|
+
muxed mcp add api https://api.com Add an HTTP server
|
|
4922
|
+
muxed mcp list Show all configured servers
|
|
4923
|
+
muxed mcp remove mydb Remove a server`).action(async (opts, cmd) => {
|
|
4198
4924
|
const explicitConfig = cmd.parent?.opts().config;
|
|
4199
4925
|
await startMcpProxy({
|
|
4200
4926
|
configPath: explicitConfig,
|
|
4201
4927
|
proxyTools: opts.proxyTools
|
|
4202
4928
|
});
|
|
4203
4929
|
});
|
|
4204
|
-
mcpCommand.command("add").description("Add an MCP server").passThroughOptions().argument("<name>", "Server name").argument("<commandOrUrl>", "Command to run or URL to connect to").argument("[args...]", "Additional arguments (
|
|
4930
|
+
mcpCommand.command("add").description("Add or update an MCP server in config").passThroughOptions().argument("<name>", "Server name (used to reference it in other commands)").argument("<commandOrUrl>", "Command to run (stdio) or URL to connect to (HTTP)").argument("[args...]", "Additional command arguments (stdio only)").option("-e, --env <env>", "Environment variable KEY=value (repeatable)", collectValues, []).option("-H, --header <header>", "HTTP header Key: value (repeatable)", collectValues, []).option("-s, --scope <scope>", "Config scope: local or global (default: local)", "local").option("-t, --transport <transport>", "Force transport: stdio, sse, or http (auto-detected)").option("--client-id <clientId>", "OAuth client ID").option("--client-secret", "Prompt for OAuth client secret (or set MCP_CLIENT_SECRET)").option("--callback-port <port>", "Fixed port for OAuth callback").option("--oauth-scope <oauthScope>", "OAuth scope string").action(async (name, commandOrUrl, args, opts) => {
|
|
4205
4931
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
4206
4932
|
const scope = opts.scope;
|
|
4207
4933
|
const configPath = getConfigPath(scope, explicitConfig);
|
|
@@ -4220,7 +4946,7 @@ mcpCommand.command("add").description("Add an MCP server").passThroughOptions().
|
|
|
4220
4946
|
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
4221
4947
|
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
4222
4948
|
});
|
|
4223
|
-
mcpCommand.command("add-json").description("Add
|
|
4949
|
+
mcpCommand.command("add-json").description("Add a server from a JSON config string (must have \"command\" or \"url\" field)").argument("<name>", "Server name").argument("<json>", "JSON object: {\"command\":\"...\",\"args\":[...]} or {\"url\":\"...\"}").option("-s, --scope <scope>", "Config scope: local or global (default: local)", "local").action(async (name, jsonStr, opts) => {
|
|
4224
4950
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
4225
4951
|
const scope = opts.scope;
|
|
4226
4952
|
const configPath = getConfigPath(scope, explicitConfig);
|
|
@@ -4247,7 +4973,7 @@ mcpCommand.command("add-json").description("Add an MCP server from a JSON config
|
|
|
4247
4973
|
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
4248
4974
|
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
4249
4975
|
});
|
|
4250
|
-
mcpCommand.command("add-from-claude-desktop").description("Import
|
|
4976
|
+
mcpCommand.command("add-from-claude-desktop").description("Import servers from Claude Desktop config into muxed").option("-s, --scope <scope>", "Config scope: local or global (default: local)", "local").action(async (opts) => {
|
|
4251
4977
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
4252
4978
|
const scope = opts.scope;
|
|
4253
4979
|
const configPath = getConfigPath(scope, explicitConfig);
|
|
@@ -4275,7 +5001,7 @@ mcpCommand.command("add-from-claude-desktop").description("Import MCP servers fr
|
|
|
4275
5001
|
if (result.skipped.length > 0) console.log(`Skipped ${result.skipped.length} (already existed): ${result.skipped.join(", ")}`);
|
|
4276
5002
|
if (result.imported.length === 0 && result.skipped.length === 0) console.log("No servers found in Claude Desktop config.");
|
|
4277
5003
|
});
|
|
4278
|
-
mcpCommand.command("get").description("
|
|
5004
|
+
mcpCommand.command("get").description("Show config details for a server (checks local, then global)").argument("<name>", "Server name").option("--json", "Output as JSON (machine-readable)").action(async (name, opts) => {
|
|
4279
5005
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
4280
5006
|
const localPath = getConfigPath("local", explicitConfig);
|
|
4281
5007
|
const globalPath = getConfigPath("global", explicitConfig);
|
|
@@ -4295,7 +5021,7 @@ mcpCommand.command("get").description("Get details of a configured MCP server").
|
|
|
4295
5021
|
}));
|
|
4296
5022
|
else console.log(formatMcpServer(name, server, scope));
|
|
4297
5023
|
});
|
|
4298
|
-
mcpCommand.command("list").description("List all configured
|
|
5024
|
+
mcpCommand.command("list").description("List all configured servers (local + global)").option("--json", "Output as JSON (machine-readable)").action(async (opts) => {
|
|
4299
5025
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
4300
5026
|
const localPath = getConfigPath("local", explicitConfig);
|
|
4301
5027
|
const globalPath = getConfigPath("global", explicitConfig);
|
|
@@ -4318,7 +5044,7 @@ mcpCommand.command("list").description("List all configured MCP servers").option
|
|
|
4318
5044
|
if (opts.json) console.log(formatJson(entries));
|
|
4319
5045
|
else console.log(formatMcpServerList(entries));
|
|
4320
5046
|
});
|
|
4321
|
-
mcpCommand.command("remove").description("Remove
|
|
5047
|
+
mcpCommand.command("remove").description("Remove a server from config").argument("<name>", "Server name to remove").option("-s, --scope <scope>", "Scope: local or global (searches both if omitted)").action(async (name, opts) => {
|
|
4322
5048
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
4323
5049
|
if (opts.scope) {
|
|
4324
5050
|
const scope = opts.scope;
|
|
@@ -4359,7 +5085,10 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
|
|
|
4359
5085
|
console.error(`Server "${name}" not found in local or global config.`);
|
|
4360
5086
|
process.exitCode = 1;
|
|
4361
5087
|
});
|
|
4362
|
-
const typegenCommand = new Command("typegen").description("Generate TypeScript types from tool schemas
|
|
5088
|
+
const typegenCommand = new Command("typegen").description("Generate TypeScript types from live tool schemas").option("-c, --config <path>", "Path to muxed.config.json").addHelpText("after", `
|
|
5089
|
+
Writes to node_modules/muxed/muxed.generated.d.ts.
|
|
5090
|
+
After running, client.call() gets autocomplete on tool names and typed arguments.
|
|
5091
|
+
Re-run when tool schemas change (same workflow as prisma generate).`).action(async (opts) => {
|
|
4363
5092
|
await ensureDaemon(typegenCommand.parent?.opts().config ?? opts.config);
|
|
4364
5093
|
const tools = await sendRequest("tools/list");
|
|
4365
5094
|
const content = await generateTypes(tools);
|
|
@@ -4369,7 +5098,7 @@ const typegenCommand = new Command("typegen").description("Generate TypeScript t
|
|
|
4369
5098
|
fs.writeFileSync(outputPath, content, "utf-8");
|
|
4370
5099
|
console.log(`Generated ${tools.length} tool types → ${outputPath}`);
|
|
4371
5100
|
});
|
|
4372
|
-
const telemetryCommand = new Command("telemetry").description("
|
|
5101
|
+
const telemetryCommand = new Command("telemetry").description("Enable, disable, or check anonymous telemetry").argument("[action]", "on | off | status (default: status)").action((action) => {
|
|
4373
5102
|
switch (action) {
|
|
4374
5103
|
case "on":
|
|
4375
5104
|
setTelemetryEnabled(true);
|
|
@@ -4390,9 +5119,17 @@ const telemetryCommand = new Command("telemetry").description("Manage anonymous
|
|
|
4390
5119
|
});
|
|
4391
5120
|
async function runCli() {
|
|
4392
5121
|
const program = new Command();
|
|
4393
|
-
program.name("muxed").description("
|
|
5122
|
+
program.name("muxed").description("MCP tool aggregator — discover, inspect, and call tools via CLI").version(getVersion());
|
|
4394
5123
|
program.enablePositionalOptions();
|
|
4395
|
-
program.option("--config <path>", "Path to config
|
|
5124
|
+
program.option("--config <path>", "Path to muxed.config.json");
|
|
5125
|
+
program.addHelpText("after", `
|
|
5126
|
+
Workflow:
|
|
5127
|
+
muxed grep "<pattern>" Find tools by name or description
|
|
5128
|
+
muxed info <server>/<tool> Inspect schema (required before calling)
|
|
5129
|
+
muxed call <server>/<tool> '<json>' Execute a tool
|
|
5130
|
+
|
|
5131
|
+
Setup:
|
|
5132
|
+
npx muxed init Discover servers and inject agent instructions`);
|
|
4396
5133
|
program.commandsGroup("Servers:");
|
|
4397
5134
|
program.addCommand(serversCommand);
|
|
4398
5135
|
program.commandsGroup("Tools:");
|