muxed 0.1.1 → 0.2.1
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 +60 -3
- package/dist/cli.mjs +930 -150
- package/dist/client/index.d.mts +16 -2
- package/dist/client/index.mjs +8 -1
- package/package.json +2 -1
package/dist/cli.mjs
CHANGED
|
@@ -3,17 +3,20 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { z } from "zod/v4";
|
|
6
|
+
import * as z$1 from "zod";
|
|
6
7
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
8
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
9
|
import { SSEClientTransport, SseError } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
9
10
|
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
10
11
|
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
11
12
|
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js";
|
|
13
|
+
import crypto from "node:crypto";
|
|
12
14
|
import { execFile, fork } from "node:child_process";
|
|
13
15
|
import http from "node:http";
|
|
14
16
|
import { compile } from "json-schema-to-typescript";
|
|
15
17
|
import net from "node:net";
|
|
16
18
|
import { Command } from "commander";
|
|
19
|
+
import { PostHog } from "posthog-node";
|
|
17
20
|
import * as readline from "node:readline/promises";
|
|
18
21
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
@@ -31,7 +34,8 @@ const StdioServerConfigSchema = z.object({
|
|
|
31
34
|
command: z.string(),
|
|
32
35
|
args: z.array(z.string()).optional(),
|
|
33
36
|
env: z.record(z.string(), z.string()).optional(),
|
|
34
|
-
cwd: z.string().optional()
|
|
37
|
+
cwd: z.string().optional(),
|
|
38
|
+
timeout: z.number().optional()
|
|
35
39
|
});
|
|
36
40
|
const ReconnectionSchema = z.object({
|
|
37
41
|
maxDelay: z.number().optional(),
|
|
@@ -59,7 +63,8 @@ const HttpServerConfigSchema = z.object({
|
|
|
59
63
|
headers: z.record(z.string(), z.string()).optional(),
|
|
60
64
|
sessionId: z.string().optional(),
|
|
61
65
|
reconnection: ReconnectionSchema.optional(),
|
|
62
|
-
auth: OAuthConfigSchema.optional()
|
|
66
|
+
auth: OAuthConfigSchema.optional(),
|
|
67
|
+
timeout: z.number().optional()
|
|
63
68
|
});
|
|
64
69
|
const ServerConfigSchema = z.union([StdioServerConfigSchema, HttpServerConfigSchema]);
|
|
65
70
|
const HttpListenerSchema = z.object({
|
|
@@ -113,7 +118,7 @@ function mergeClaudeDesktopServers(servers) {
|
|
|
113
118
|
const DAEMON_DEFAULTS = {
|
|
114
119
|
idleTimeout: 3e5,
|
|
115
120
|
connectTimeout: 3e4,
|
|
116
|
-
requestTimeout:
|
|
121
|
+
requestTimeout: 3e4,
|
|
117
122
|
healthCheckInterval: 3e4,
|
|
118
123
|
maxRestartAttempts: -1,
|
|
119
124
|
maxTotalTimeout: 3e5,
|
|
@@ -122,7 +127,7 @@ const DAEMON_DEFAULTS = {
|
|
|
122
127
|
shutdownTimeout: 1e4
|
|
123
128
|
};
|
|
124
129
|
function getGlobalConfigPath() {
|
|
125
|
-
return path.join(os.homedir(), ".
|
|
130
|
+
return path.join(os.homedir(), ".muxed", "config.json");
|
|
126
131
|
}
|
|
127
132
|
function findConfigFile(configPath) {
|
|
128
133
|
if (configPath) {
|
|
@@ -295,11 +300,14 @@ function initLogger(opts) {
|
|
|
295
300
|
function sanitizeName(name) {
|
|
296
301
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
297
302
|
}
|
|
303
|
+
function hashName(name) {
|
|
304
|
+
return crypto.createHash("sha256").update(name).digest("hex").slice(0, 8);
|
|
305
|
+
}
|
|
298
306
|
function getAuthDir() {
|
|
299
307
|
return path.join(getMuxedDir(), "auth");
|
|
300
308
|
}
|
|
301
309
|
function getStorePath(serverName) {
|
|
302
|
-
return path.join(getAuthDir(), `${sanitizeName(serverName)}.json`);
|
|
310
|
+
return path.join(getAuthDir(), `${sanitizeName(serverName)}-${hashName(serverName)}.json`);
|
|
303
311
|
}
|
|
304
312
|
function ensureAuthDir() {
|
|
305
313
|
fs.mkdirSync(getAuthDir(), {
|
|
@@ -365,9 +373,8 @@ var TokenStore = class {
|
|
|
365
373
|
writeStore(this.serverName, data);
|
|
366
374
|
}
|
|
367
375
|
clearAll() {
|
|
368
|
-
const filePath = getStorePath(this.serverName);
|
|
369
376
|
try {
|
|
370
|
-
fs.unlinkSync(
|
|
377
|
+
fs.unlinkSync(getStorePath(this.serverName));
|
|
371
378
|
} catch {}
|
|
372
379
|
}
|
|
373
380
|
hasTokens() {
|
|
@@ -467,6 +474,7 @@ var AuthorizationCodeProvider = class {
|
|
|
467
474
|
config;
|
|
468
475
|
_redirectUrl;
|
|
469
476
|
hadTokensBefore = false;
|
|
477
|
+
_state = crypto.randomBytes(32).toString("base64url");
|
|
470
478
|
constructor(config, serverName) {
|
|
471
479
|
this.serverName = serverName;
|
|
472
480
|
this.config = config;
|
|
@@ -474,14 +482,14 @@ var AuthorizationCodeProvider = class {
|
|
|
474
482
|
this.hadTokensBefore = this.store.hasTokens();
|
|
475
483
|
}
|
|
476
484
|
setRedirectUrl(port) {
|
|
477
|
-
this._redirectUrl = `http://
|
|
485
|
+
this._redirectUrl = `http://localhost:${port}/callback`;
|
|
478
486
|
}
|
|
479
487
|
get redirectUrl() {
|
|
480
488
|
return this._redirectUrl;
|
|
481
489
|
}
|
|
482
490
|
get clientMetadata() {
|
|
483
491
|
return {
|
|
484
|
-
redirect_uris: [this._redirectUrl ?? "http://
|
|
492
|
+
redirect_uris: [this._redirectUrl ?? "http://localhost/callback"],
|
|
485
493
|
token_endpoint_auth_method: this.config.clientSecret ? "client_secret_basic" : "none",
|
|
486
494
|
grant_types: ["authorization_code", "refresh_token"],
|
|
487
495
|
response_types: ["code"],
|
|
@@ -494,7 +502,14 @@ var AuthorizationCodeProvider = class {
|
|
|
494
502
|
client_id: this.config.clientId,
|
|
495
503
|
...this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}
|
|
496
504
|
};
|
|
497
|
-
|
|
505
|
+
const cached = this.store.getClientInformation();
|
|
506
|
+
if (cached && this._redirectUrl) {
|
|
507
|
+
if (!(cached.redirect_uris ?? []).includes(this._redirectUrl)) {
|
|
508
|
+
this.store.clearClientInformation();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return cached;
|
|
498
513
|
}
|
|
499
514
|
saveClientInformation(info) {
|
|
500
515
|
this.store.saveClientInformation(info);
|
|
@@ -515,6 +530,9 @@ var AuthorizationCodeProvider = class {
|
|
|
515
530
|
openBrowser(url);
|
|
516
531
|
}
|
|
517
532
|
}
|
|
533
|
+
async state() {
|
|
534
|
+
return this._state;
|
|
535
|
+
}
|
|
518
536
|
saveCodeVerifier(codeVerifier) {
|
|
519
537
|
this.store.saveCodeVerifier(codeVerifier);
|
|
520
538
|
}
|
|
@@ -573,8 +591,8 @@ var CallbackServer = class {
|
|
|
573
591
|
res.end();
|
|
574
592
|
return;
|
|
575
593
|
}
|
|
576
|
-
const url = new URL(req.url ?? "/", `http://
|
|
577
|
-
if (url.pathname !== "/
|
|
594
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
595
|
+
if (url.pathname !== "/callback") {
|
|
578
596
|
res.writeHead(404);
|
|
579
597
|
res.end("Not found");
|
|
580
598
|
return;
|
|
@@ -608,7 +626,7 @@ var CallbackServer = class {
|
|
|
608
626
|
state
|
|
609
627
|
});
|
|
610
628
|
});
|
|
611
|
-
this.server.listen(port, "
|
|
629
|
+
this.server.listen(port, "localhost", () => {
|
|
612
630
|
const addr = this.server.address();
|
|
613
631
|
if (addr && typeof addr === "object") this._port = addr.port;
|
|
614
632
|
});
|
|
@@ -1051,9 +1069,108 @@ var ServerManager = class {
|
|
|
1051
1069
|
};
|
|
1052
1070
|
}
|
|
1053
1071
|
};
|
|
1072
|
+
const ErrorCode = {
|
|
1073
|
+
TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
|
|
1074
|
+
SERVER_NOT_FOUND: "SERVER_NOT_FOUND",
|
|
1075
|
+
SERVER_NOT_CONNECTED: "SERVER_NOT_CONNECTED",
|
|
1076
|
+
INVALID_ARGUMENTS: "INVALID_ARGUMENTS",
|
|
1077
|
+
INVALID_FORMAT: "INVALID_FORMAT",
|
|
1078
|
+
MISSING_PARAMETER: "MISSING_PARAMETER",
|
|
1079
|
+
TIMEOUT: "TIMEOUT"
|
|
1080
|
+
};
|
|
1081
|
+
function levenshtein(a, b) {
|
|
1082
|
+
const m = a.length;
|
|
1083
|
+
const n = b.length;
|
|
1084
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
1085
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
1086
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
1087
|
+
for (let i = 1; i <= m; i++) for (let j = 1; j <= n; j++) dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
1088
|
+
return dp[m][n];
|
|
1089
|
+
}
|
|
1090
|
+
function findSimilarTools(targetTool, allTools, maxResults = 3) {
|
|
1091
|
+
const maxDistance = Math.max(3, Math.floor(targetTool.length * .4));
|
|
1092
|
+
return allTools.map(({ server, tool }) => {
|
|
1093
|
+
const fullName = `${server}/${tool.name}`;
|
|
1094
|
+
const toolOnly = tool.name;
|
|
1095
|
+
const distFull = levenshtein(targetTool.toLowerCase(), fullName.toLowerCase());
|
|
1096
|
+
const distTool = levenshtein(targetTool.toLowerCase(), toolOnly.toLowerCase());
|
|
1097
|
+
return {
|
|
1098
|
+
fullName,
|
|
1099
|
+
dist: Math.min(distFull, distTool)
|
|
1100
|
+
};
|
|
1101
|
+
}).filter(({ dist }) => dist <= maxDistance).sort((a, b) => a.dist - b.dist).slice(0, maxResults).map(({ fullName }) => fullName);
|
|
1102
|
+
}
|
|
1103
|
+
function toolNotFoundError(name, similarTools) {
|
|
1104
|
+
const hasSimilar = similarTools.length > 0;
|
|
1105
|
+
const suggestion = hasSimilar ? `Did you mean: ${similarTools.join(", ")}? Run 'muxed grep <pattern>' to search available tools.` : `Run 'muxed grep <pattern>' to find available tools, or 'muxed tools' to list all.`;
|
|
1106
|
+
return {
|
|
1107
|
+
code: ErrorCode.TOOL_NOT_FOUND,
|
|
1108
|
+
message: `Tool not found: ${name}`,
|
|
1109
|
+
suggestion,
|
|
1110
|
+
context: hasSimilar ? { similarTools } : void 0
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
function serverNotFoundError(serverName, availableServers) {
|
|
1114
|
+
return {
|
|
1115
|
+
code: ErrorCode.SERVER_NOT_FOUND,
|
|
1116
|
+
message: `Server not found: ${serverName}`,
|
|
1117
|
+
suggestion: `Available servers: ${availableServers.join(", ") || "none"}. Run 'muxed servers' to list all.`,
|
|
1118
|
+
context: { availableServers }
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
function serverNotConnectedError(serverName) {
|
|
1122
|
+
return {
|
|
1123
|
+
code: ErrorCode.SERVER_NOT_CONNECTED,
|
|
1124
|
+
message: `Server not connected: ${serverName}`,
|
|
1125
|
+
suggestion: `The server may be starting up. Run 'muxed status' to check, or 'muxed reload' to reconnect.`
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
function invalidFormatError(name) {
|
|
1129
|
+
return {
|
|
1130
|
+
code: ErrorCode.INVALID_FORMAT,
|
|
1131
|
+
message: `Invalid tool name format: ${name}`,
|
|
1132
|
+
suggestion: `Use the format 'server/tool' (e.g. 'myserver/mytool'). Run 'muxed tools' to list all available tools.`
|
|
1133
|
+
};
|
|
1134
|
+
}
|
|
1135
|
+
function missingParameterError(param) {
|
|
1136
|
+
return {
|
|
1137
|
+
code: ErrorCode.MISSING_PARAMETER,
|
|
1138
|
+
message: `Missing required parameter: ${param}`,
|
|
1139
|
+
suggestion: `Provide the '${param}' parameter in the request.`
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
function invalidArgumentsError(toolName, errors) {
|
|
1143
|
+
return {
|
|
1144
|
+
code: ErrorCode.INVALID_ARGUMENTS,
|
|
1145
|
+
message: `Invalid arguments for tool ${toolName}`,
|
|
1146
|
+
suggestion: `Run 'muxed info ${toolName}' to see the expected input schema.`,
|
|
1147
|
+
context: { validationErrors: errors }
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
function timeoutError(toolName, timeoutMs) {
|
|
1151
|
+
return {
|
|
1152
|
+
code: ErrorCode.TIMEOUT,
|
|
1153
|
+
message: `Tool call timed out after ${timeoutMs}ms: ${toolName}`,
|
|
1154
|
+
suggestion: `Increase the timeout with --timeout <ms>, or use --async for long-running operations.`
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
function isTimeoutError(err) {
|
|
1158
|
+
if (!(err instanceof Error)) return false;
|
|
1159
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") return true;
|
|
1160
|
+
const msg = err.message.toLowerCase();
|
|
1161
|
+
return msg.includes("timeout") || msg.includes("aborted");
|
|
1162
|
+
}
|
|
1163
|
+
function toErrorData(err) {
|
|
1164
|
+
return {
|
|
1165
|
+
code: err.code,
|
|
1166
|
+
suggestion: err.suggestion,
|
|
1167
|
+
...err.context ? { context: err.context } : {}
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1054
1170
|
var ServerPool = class {
|
|
1055
1171
|
servers = /* @__PURE__ */ new Map();
|
|
1056
1172
|
trackedTasks = /* @__PURE__ */ new Map();
|
|
1173
|
+
zodSchemaCache = /* @__PURE__ */ new Map();
|
|
1057
1174
|
taskExpiryTimer;
|
|
1058
1175
|
taskExpiryTimeout = 36e5;
|
|
1059
1176
|
async connectAll(config) {
|
|
@@ -1080,6 +1197,7 @@ var ServerPool = class {
|
|
|
1080
1197
|
}
|
|
1081
1198
|
async disconnectAll() {
|
|
1082
1199
|
this.stopTaskExpiry();
|
|
1200
|
+
this.zodSchemaCache.clear();
|
|
1083
1201
|
await Promise.allSettled([...this.servers.values()].map((manager) => manager.disconnect()));
|
|
1084
1202
|
}
|
|
1085
1203
|
onServerHealthChange(serverName, status, error) {
|
|
@@ -1157,6 +1275,91 @@ var ServerPool = class {
|
|
|
1157
1275
|
tool
|
|
1158
1276
|
};
|
|
1159
1277
|
}
|
|
1278
|
+
findToolOrError(serverTool) {
|
|
1279
|
+
const slashIndex = serverTool.indexOf("/");
|
|
1280
|
+
if (slashIndex === -1) return {
|
|
1281
|
+
ok: false,
|
|
1282
|
+
error: invalidFormatError(serverTool)
|
|
1283
|
+
};
|
|
1284
|
+
const serverName = serverTool.slice(0, slashIndex);
|
|
1285
|
+
const toolName = serverTool.slice(slashIndex + 1);
|
|
1286
|
+
const manager = this.servers.get(serverName);
|
|
1287
|
+
if (!manager) return {
|
|
1288
|
+
ok: false,
|
|
1289
|
+
error: serverNotFoundError(serverName, [...this.servers.keys()])
|
|
1290
|
+
};
|
|
1291
|
+
if (manager.getStatus() !== "connected") return {
|
|
1292
|
+
ok: false,
|
|
1293
|
+
error: serverNotConnectedError(serverName)
|
|
1294
|
+
};
|
|
1295
|
+
const tool = manager.listTools().find((t) => t.name === toolName);
|
|
1296
|
+
if (!tool) return {
|
|
1297
|
+
ok: false,
|
|
1298
|
+
error: toolNotFoundError(serverTool, findSimilarTools(serverTool, this.listAllTools()))
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
ok: true,
|
|
1302
|
+
manager,
|
|
1303
|
+
tool,
|
|
1304
|
+
serverTimeout: manager.getState().config.timeout
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
getZodSchema(inputSchema) {
|
|
1308
|
+
const key = JSON.stringify(inputSchema);
|
|
1309
|
+
const cached = this.zodSchemaCache.get(key);
|
|
1310
|
+
if (cached !== void 0) return cached;
|
|
1311
|
+
try {
|
|
1312
|
+
const zodSchema = z$1.fromJSONSchema(inputSchema);
|
|
1313
|
+
this.zodSchemaCache.set(key, zodSchema);
|
|
1314
|
+
return zodSchema;
|
|
1315
|
+
} catch {
|
|
1316
|
+
this.zodSchemaCache.set(key, "unsupported");
|
|
1317
|
+
return "unsupported";
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
validateToolArgs(serverTool, args) {
|
|
1321
|
+
const found = this.findToolOrError(serverTool);
|
|
1322
|
+
if (!found.ok) return {
|
|
1323
|
+
valid: false,
|
|
1324
|
+
errors: [found.error.message],
|
|
1325
|
+
warnings: []
|
|
1326
|
+
};
|
|
1327
|
+
const { tool } = found;
|
|
1328
|
+
const errors = [];
|
|
1329
|
+
const warnings = [];
|
|
1330
|
+
if (tool.inputSchema) {
|
|
1331
|
+
const zodSchema = this.getZodSchema(tool.inputSchema);
|
|
1332
|
+
if (zodSchema === "unsupported") {
|
|
1333
|
+
getLogger().warn(`Could not convert inputSchema for ${serverTool}: unsupported schema`, serverTool.split("/")[0]);
|
|
1334
|
+
this.addAnnotationWarnings(tool, warnings);
|
|
1335
|
+
return {
|
|
1336
|
+
valid: true,
|
|
1337
|
+
errors: [],
|
|
1338
|
+
warnings,
|
|
1339
|
+
unsupported: true,
|
|
1340
|
+
tool
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
const result = zodSchema.safeParse(args);
|
|
1344
|
+
if (!result.success) for (const issue of result.error.issues) {
|
|
1345
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "";
|
|
1346
|
+
const prefix = path ? `Field '${path}': ` : "";
|
|
1347
|
+
errors.push(`${prefix}${issue.message}`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
this.addAnnotationWarnings(tool, warnings);
|
|
1351
|
+
return {
|
|
1352
|
+
valid: errors.length === 0,
|
|
1353
|
+
errors,
|
|
1354
|
+
warnings,
|
|
1355
|
+
tool
|
|
1356
|
+
};
|
|
1357
|
+
}
|
|
1358
|
+
addAnnotationWarnings(tool, warnings) {
|
|
1359
|
+
if (tool.annotations?.destructiveHint) warnings.push("Tool is marked as destructive.");
|
|
1360
|
+
if (!tool.annotations?.idempotentHint) warnings.push("Tool is not marked as idempotent.");
|
|
1361
|
+
if (tool.annotations?.readOnlyHint === false) warnings.push("Tool may modify data (not read-only).");
|
|
1362
|
+
}
|
|
1160
1363
|
grepTools(pattern) {
|
|
1161
1364
|
const normalized = pattern.replace(/\\([|()\{\}])/g, "$1");
|
|
1162
1365
|
const regex = new RegExp(normalized, "i");
|
|
@@ -1353,6 +1556,89 @@ function indent(text, spaces) {
|
|
|
1353
1556
|
const pad = " ".repeat(spaces);
|
|
1354
1557
|
return text.split("\n").join("\n" + pad);
|
|
1355
1558
|
}
|
|
1559
|
+
function parsePath(path) {
|
|
1560
|
+
const segments = [];
|
|
1561
|
+
for (const part of path.split(".")) if (part.endsWith("[]")) segments.push({
|
|
1562
|
+
key: part.slice(0, -2),
|
|
1563
|
+
isArray: true
|
|
1564
|
+
});
|
|
1565
|
+
else segments.push({
|
|
1566
|
+
key: part,
|
|
1567
|
+
isArray: false
|
|
1568
|
+
});
|
|
1569
|
+
return segments;
|
|
1570
|
+
}
|
|
1571
|
+
function extractDeep(obj, segments) {
|
|
1572
|
+
if (segments.length === 0 || obj == null || typeof obj !== "object") return obj;
|
|
1573
|
+
const [first, ...rest] = segments;
|
|
1574
|
+
if (!first) return obj;
|
|
1575
|
+
const value = obj[first.key];
|
|
1576
|
+
if (first.isArray) {
|
|
1577
|
+
if (!Array.isArray(value)) return void 0;
|
|
1578
|
+
if (rest.length === 0) return value;
|
|
1579
|
+
return value.map((item) => extractDeep(item, rest)).filter((v) => v !== void 0);
|
|
1580
|
+
}
|
|
1581
|
+
if (rest.length === 0) return value;
|
|
1582
|
+
return extractDeep(value, rest);
|
|
1583
|
+
}
|
|
1584
|
+
function extract(obj, path) {
|
|
1585
|
+
return extractDeep(obj, parsePath(path));
|
|
1586
|
+
}
|
|
1587
|
+
function setNested(obj, keys, value) {
|
|
1588
|
+
let current = obj;
|
|
1589
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
1590
|
+
const key = keys[i];
|
|
1591
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
1592
|
+
current = current[key];
|
|
1593
|
+
}
|
|
1594
|
+
current[keys[keys.length - 1]] = value;
|
|
1595
|
+
}
|
|
1596
|
+
function extractFromObject(data, fields) {
|
|
1597
|
+
const result = {};
|
|
1598
|
+
for (const field of fields) {
|
|
1599
|
+
const value = extract(data, field);
|
|
1600
|
+
if (value !== void 0) setNested(result, field.replace(/\[\]/g, "").split("."), value);
|
|
1601
|
+
}
|
|
1602
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
1603
|
+
}
|
|
1604
|
+
function isJsonString(str) {
|
|
1605
|
+
const trimmed = str.trim();
|
|
1606
|
+
if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) return false;
|
|
1607
|
+
try {
|
|
1608
|
+
JSON.parse(trimmed);
|
|
1609
|
+
return true;
|
|
1610
|
+
} catch {
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function filterFields(data, fields) {
|
|
1615
|
+
if (data.structuredContent && typeof data.structuredContent === "object") {
|
|
1616
|
+
const filtered = extractFromObject(data.structuredContent, fields);
|
|
1617
|
+
if (filtered) return {
|
|
1618
|
+
...data,
|
|
1619
|
+
structuredContent: filtered
|
|
1620
|
+
};
|
|
1621
|
+
}
|
|
1622
|
+
const content = data.content;
|
|
1623
|
+
if (Array.isArray(content)) {
|
|
1624
|
+
const newContent = content.map((block) => {
|
|
1625
|
+
if (block.type !== "text" || !block.text || !isJsonString(block.text)) return block;
|
|
1626
|
+
try {
|
|
1627
|
+
const filtered = extractFromObject(JSON.parse(block.text), fields);
|
|
1628
|
+
if (filtered) return {
|
|
1629
|
+
...block,
|
|
1630
|
+
text: JSON.stringify(filtered)
|
|
1631
|
+
};
|
|
1632
|
+
} catch {}
|
|
1633
|
+
return block;
|
|
1634
|
+
});
|
|
1635
|
+
if (newContent.some((block, i) => block !== content[i])) return {
|
|
1636
|
+
...data,
|
|
1637
|
+
content: newContent
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
return data;
|
|
1641
|
+
}
|
|
1356
1642
|
function createDaemonServer(serverPool, config) {
|
|
1357
1643
|
const socketPath = getSocketPath();
|
|
1358
1644
|
let idleTimer;
|
|
@@ -1390,53 +1676,118 @@ function createDaemonServer(serverPool, config) {
|
|
|
1390
1676
|
}
|
|
1391
1677
|
case "tools/call": {
|
|
1392
1678
|
const p = params;
|
|
1393
|
-
if (!p?.name)
|
|
1679
|
+
if (!p?.name) {
|
|
1680
|
+
const err = missingParameterError("name");
|
|
1681
|
+
return {
|
|
1682
|
+
jsonrpc: "2.0",
|
|
1683
|
+
id,
|
|
1684
|
+
error: {
|
|
1685
|
+
code: -32602,
|
|
1686
|
+
message: err.message,
|
|
1687
|
+
data: toErrorData(err)
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
const found = serverPool.findToolOrError(p.name);
|
|
1692
|
+
if (!found.ok) return {
|
|
1394
1693
|
jsonrpc: "2.0",
|
|
1395
1694
|
id,
|
|
1396
1695
|
error: {
|
|
1397
1696
|
code: -32602,
|
|
1398
|
-
message:
|
|
1697
|
+
message: found.error.message,
|
|
1698
|
+
data: toErrorData(found.error)
|
|
1399
1699
|
}
|
|
1400
1700
|
};
|
|
1401
|
-
const
|
|
1402
|
-
if (!
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1701
|
+
const validation = serverPool.validateToolArgs(p.name, p.arguments ?? {});
|
|
1702
|
+
if (!validation.valid && !validation.unsupported) {
|
|
1703
|
+
const err = invalidArgumentsError(p.name, validation.errors);
|
|
1704
|
+
return {
|
|
1705
|
+
jsonrpc: "2.0",
|
|
1706
|
+
id,
|
|
1707
|
+
error: {
|
|
1708
|
+
code: -32602,
|
|
1709
|
+
message: err.message,
|
|
1710
|
+
data: toErrorData(err)
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
const timeout = clientTimeout ?? p.timeout ?? found.serverTimeout ?? requestTimeout;
|
|
1715
|
+
try {
|
|
1716
|
+
const callResult = await found.manager.callTool(found.tool.name, p.arguments ?? {}, timeout);
|
|
1717
|
+
if (p.fields && p.fields.length > 0) return {
|
|
1718
|
+
jsonrpc: "2.0",
|
|
1719
|
+
id,
|
|
1720
|
+
result: filterFields(callResult, p.fields)
|
|
1721
|
+
};
|
|
1722
|
+
return {
|
|
1723
|
+
jsonrpc: "2.0",
|
|
1724
|
+
id,
|
|
1725
|
+
result: callResult
|
|
1726
|
+
};
|
|
1727
|
+
} catch (err) {
|
|
1728
|
+
if (isTimeoutError(err)) {
|
|
1729
|
+
const te = timeoutError(p.name, timeout);
|
|
1730
|
+
return {
|
|
1731
|
+
jsonrpc: "2.0",
|
|
1732
|
+
id,
|
|
1733
|
+
error: {
|
|
1734
|
+
code: -32001,
|
|
1735
|
+
message: te.message,
|
|
1736
|
+
data: toErrorData(te)
|
|
1737
|
+
}
|
|
1738
|
+
};
|
|
1408
1739
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
return {
|
|
1412
|
-
jsonrpc: "2.0",
|
|
1413
|
-
id,
|
|
1414
|
-
result: await found.manager.callTool(found.tool.name, p.arguments ?? {}, timeout)
|
|
1415
|
-
};
|
|
1740
|
+
throw err;
|
|
1741
|
+
}
|
|
1416
1742
|
}
|
|
1417
1743
|
case "tools/info": {
|
|
1418
1744
|
const p = params;
|
|
1419
|
-
if (!p?.name)
|
|
1745
|
+
if (!p?.name) {
|
|
1746
|
+
const err = missingParameterError("name");
|
|
1747
|
+
return {
|
|
1748
|
+
jsonrpc: "2.0",
|
|
1749
|
+
id,
|
|
1750
|
+
error: {
|
|
1751
|
+
code: -32602,
|
|
1752
|
+
message: err.message,
|
|
1753
|
+
data: toErrorData(err)
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
const found = serverPool.findToolOrError(p.name);
|
|
1758
|
+
if (!found.ok) return {
|
|
1420
1759
|
jsonrpc: "2.0",
|
|
1421
1760
|
id,
|
|
1422
1761
|
error: {
|
|
1423
1762
|
code: -32602,
|
|
1424
|
-
message:
|
|
1763
|
+
message: found.error.message,
|
|
1764
|
+
data: toErrorData(found.error)
|
|
1425
1765
|
}
|
|
1426
1766
|
};
|
|
1427
|
-
|
|
1428
|
-
if (!found) return {
|
|
1767
|
+
return {
|
|
1429
1768
|
jsonrpc: "2.0",
|
|
1430
1769
|
id,
|
|
1431
|
-
|
|
1432
|
-
code: -32602,
|
|
1433
|
-
message: `Tool not found: ${p.name}`
|
|
1434
|
-
}
|
|
1770
|
+
result: found.tool
|
|
1435
1771
|
};
|
|
1772
|
+
}
|
|
1773
|
+
case "tools/validate": {
|
|
1774
|
+
const p = params;
|
|
1775
|
+
if (!p?.name) {
|
|
1776
|
+
const err = missingParameterError("name");
|
|
1777
|
+
return {
|
|
1778
|
+
jsonrpc: "2.0",
|
|
1779
|
+
id,
|
|
1780
|
+
error: {
|
|
1781
|
+
code: -32602,
|
|
1782
|
+
message: err.message,
|
|
1783
|
+
data: toErrorData(err)
|
|
1784
|
+
}
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1436
1787
|
return {
|
|
1437
1788
|
jsonrpc: "2.0",
|
|
1438
1789
|
id,
|
|
1439
|
-
result:
|
|
1790
|
+
result: serverPool.validateToolArgs(p.name, p.arguments ?? {})
|
|
1440
1791
|
};
|
|
1441
1792
|
}
|
|
1442
1793
|
case "auth/status": {
|
|
@@ -1632,33 +1983,68 @@ function createDaemonServer(serverPool, config) {
|
|
|
1632
1983
|
}
|
|
1633
1984
|
case "tools/call-async": {
|
|
1634
1985
|
const p = params;
|
|
1635
|
-
if (!p?.name)
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1986
|
+
if (!p?.name) {
|
|
1987
|
+
const err = missingParameterError("name");
|
|
1988
|
+
return {
|
|
1989
|
+
jsonrpc: "2.0",
|
|
1990
|
+
id,
|
|
1991
|
+
error: {
|
|
1992
|
+
code: -32602,
|
|
1993
|
+
message: err.message,
|
|
1994
|
+
data: toErrorData(err)
|
|
1995
|
+
}
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
const foundAsync = serverPool.findToolOrError(p.name);
|
|
1999
|
+
if (!foundAsync.ok) return {
|
|
1645
2000
|
jsonrpc: "2.0",
|
|
1646
2001
|
id,
|
|
1647
2002
|
error: {
|
|
1648
2003
|
code: -32602,
|
|
1649
|
-
message:
|
|
2004
|
+
message: foundAsync.error.message,
|
|
2005
|
+
data: toErrorData(foundAsync.error)
|
|
1650
2006
|
}
|
|
1651
2007
|
};
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
2008
|
+
const asyncValidation = serverPool.validateToolArgs(p.name, p.arguments ?? {});
|
|
2009
|
+
if (!asyncValidation.valid && !asyncValidation.unsupported) {
|
|
2010
|
+
const err = invalidArgumentsError(p.name, asyncValidation.errors);
|
|
2011
|
+
return {
|
|
2012
|
+
jsonrpc: "2.0",
|
|
2013
|
+
id,
|
|
2014
|
+
error: {
|
|
2015
|
+
code: -32602,
|
|
2016
|
+
message: err.message,
|
|
2017
|
+
data: toErrorData(err)
|
|
2018
|
+
}
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
try {
|
|
2022
|
+
const taskHandle = await foundAsync.manager.callToolWithTask(foundAsync.tool.name, p.arguments ?? {});
|
|
2023
|
+
serverPool.trackTask(taskHandle.taskId, foundAsync.manager.name);
|
|
2024
|
+
return {
|
|
2025
|
+
jsonrpc: "2.0",
|
|
2026
|
+
id,
|
|
2027
|
+
result: {
|
|
2028
|
+
...taskHandle,
|
|
2029
|
+
server: foundAsync.manager.name
|
|
2030
|
+
}
|
|
2031
|
+
};
|
|
2032
|
+
} catch (err) {
|
|
2033
|
+
if (isTimeoutError(err)) {
|
|
2034
|
+
const asyncTimeout = foundAsync.serverTimeout ?? requestTimeout;
|
|
2035
|
+
const te = timeoutError(p.name, asyncTimeout);
|
|
2036
|
+
return {
|
|
2037
|
+
jsonrpc: "2.0",
|
|
2038
|
+
id,
|
|
2039
|
+
error: {
|
|
2040
|
+
code: -32001,
|
|
2041
|
+
message: te.message,
|
|
2042
|
+
data: toErrorData(te)
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
1660
2045
|
}
|
|
1661
|
-
|
|
2046
|
+
throw err;
|
|
2047
|
+
}
|
|
1662
2048
|
}
|
|
1663
2049
|
case "config/reload": {
|
|
1664
2050
|
const newConfig = loadConfig(params?.configPath);
|
|
@@ -2529,6 +2915,37 @@ function formatMcpServerList(servers) {
|
|
|
2529
2915
|
];
|
|
2530
2916
|
}));
|
|
2531
2917
|
}
|
|
2918
|
+
function formatStructuredError(error) {
|
|
2919
|
+
const lines = [];
|
|
2920
|
+
lines.push(`Error: ${error.message}`);
|
|
2921
|
+
if (error.data?.suggestion) lines.push(`Suggestion: ${error.data.suggestion}`);
|
|
2922
|
+
if (error.data?.context?.similarTools) {
|
|
2923
|
+
const similar = error.data.context.similarTools;
|
|
2924
|
+
if (similar.length > 0) lines.push(`Similar tools: ${similar.join(", ")}`);
|
|
2925
|
+
}
|
|
2926
|
+
if (error.data?.context?.availableServers) {
|
|
2927
|
+
const servers = error.data.context.availableServers;
|
|
2928
|
+
if (servers.length > 0) lines.push(`Available servers: ${servers.join(", ")}`);
|
|
2929
|
+
}
|
|
2930
|
+
return lines.join("\n");
|
|
2931
|
+
}
|
|
2932
|
+
function formatValidation(result) {
|
|
2933
|
+
const lines = [];
|
|
2934
|
+
if (result.unsupported) {
|
|
2935
|
+
lines.push("Validation: unsupported (tool schema uses features not supported by dry-run validation)");
|
|
2936
|
+
lines.push("The call will be forwarded to the MCP server without pre-validation.");
|
|
2937
|
+
} else if (result.valid) lines.push("Validation: passed");
|
|
2938
|
+
else {
|
|
2939
|
+
lines.push("Validation: failed");
|
|
2940
|
+
for (const err of result.errors) lines.push(` - ${err}`);
|
|
2941
|
+
}
|
|
2942
|
+
if (result.warnings.length > 0) {
|
|
2943
|
+
lines.push("");
|
|
2944
|
+
lines.push("Warnings:");
|
|
2945
|
+
for (const warn of result.warnings) lines.push(` - ${warn}`);
|
|
2946
|
+
}
|
|
2947
|
+
return lines.join("\n");
|
|
2948
|
+
}
|
|
2532
2949
|
function formatJson(data) {
|
|
2533
2950
|
return JSON.stringify(data, null, 2);
|
|
2534
2951
|
}
|
|
@@ -2546,10 +2963,67 @@ const serversCommand = new Command("servers").description("List connected MCP se
|
|
|
2546
2963
|
const result = await sendRequest("servers/list");
|
|
2547
2964
|
console.log(opts.json ? formatJson(result) : formatServers(result));
|
|
2548
2965
|
});
|
|
2966
|
+
const TELEMETRY_FILE = path.join(os.homedir(), ".muxed", "telemetry");
|
|
2967
|
+
const sessionId = crypto.randomUUID();
|
|
2968
|
+
function isTelemetryEnabled() {
|
|
2969
|
+
if (process.env.DO_NOT_TRACK === "1") return false;
|
|
2970
|
+
if (process.env.MUXED_TELEMETRY === "0") return false;
|
|
2971
|
+
try {
|
|
2972
|
+
if (fs.existsSync(TELEMETRY_FILE)) return fs.readFileSync(TELEMETRY_FILE, "utf-8").trim() !== "off";
|
|
2973
|
+
} catch {}
|
|
2974
|
+
return true;
|
|
2975
|
+
}
|
|
2976
|
+
function setTelemetryEnabled(enabled) {
|
|
2977
|
+
try {
|
|
2978
|
+
const dir = path.dirname(TELEMETRY_FILE);
|
|
2979
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2980
|
+
fs.writeFileSync(TELEMETRY_FILE, enabled ? "on" : "off", "utf-8");
|
|
2981
|
+
} catch {}
|
|
2982
|
+
}
|
|
2983
|
+
function getTelemetryStatus() {
|
|
2984
|
+
return isTelemetryEnabled() ? "on" : "off";
|
|
2985
|
+
}
|
|
2986
|
+
let _client = null;
|
|
2987
|
+
function getClient() {
|
|
2988
|
+
if (!isTelemetryEnabled()) return null;
|
|
2989
|
+
if (_client) return _client;
|
|
2990
|
+
const token = process.env.POSTHOG_PROJECT_TOKEN;
|
|
2991
|
+
const host = process.env.POSTHOG_HOST;
|
|
2992
|
+
if (!token || !host) return null;
|
|
2993
|
+
try {
|
|
2994
|
+
_client = new PostHog(token, {
|
|
2995
|
+
host,
|
|
2996
|
+
flushAt: 1
|
|
2997
|
+
});
|
|
2998
|
+
return _client;
|
|
2999
|
+
} catch {
|
|
3000
|
+
return null;
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
function capture(event, properties) {
|
|
3004
|
+
try {
|
|
3005
|
+
const client = getClient();
|
|
3006
|
+
if (!client) return;
|
|
3007
|
+
client.capture({
|
|
3008
|
+
distinctId: sessionId,
|
|
3009
|
+
event,
|
|
3010
|
+
properties: properties ?? {}
|
|
3011
|
+
});
|
|
3012
|
+
} catch {}
|
|
3013
|
+
}
|
|
3014
|
+
async function shutdown() {
|
|
3015
|
+
try {
|
|
3016
|
+
if (_client) await _client.shutdown();
|
|
3017
|
+
} catch {}
|
|
3018
|
+
}
|
|
2549
3019
|
const toolsCommand = new Command("tools").description("List all available tools, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
|
|
2550
3020
|
const configPath = toolsCommand.parent?.opts().config;
|
|
2551
3021
|
await ensureDaemon(configPath);
|
|
2552
3022
|
const result = await sendRequest("tools/list", server ? { server } : void 0);
|
|
3023
|
+
capture("tools_listed", {
|
|
3024
|
+
filtered_by_server: !!server,
|
|
3025
|
+
tool_count: result.length
|
|
3026
|
+
});
|
|
2553
3027
|
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
2554
3028
|
});
|
|
2555
3029
|
const infoCommand = new Command("info").description("Show input schema and description for a specific tool").argument("<server/tool>", "Tool identifier (e.g. myserver/mytool)").option("--json", "Output as JSON").action(async (serverTool, opts) => {
|
|
@@ -2574,7 +3048,7 @@ function readStdin() {
|
|
|
2574
3048
|
process.stdin.on("error", reject);
|
|
2575
3049
|
});
|
|
2576
3050
|
}
|
|
2577
|
-
const callCommand = new Command("call").description("Execute a tool with JSON arguments (use - for stdin, --async for background)").argument("<server/tool>", "Tool identifier (e.g. myserver/mytool)").argument("[json]", "JSON arguments (use - for stdin)").option("--timeout <ms>", "Request timeout in milliseconds").option("--async", "Use task-based execution (return task handle immediately)").option("--json", "Output as JSON").action(async (serverTool, jsonArgs, opts) => {
|
|
3051
|
+
const callCommand = new Command("call").description("Execute a tool with JSON arguments (use - for stdin, --async for background)").argument("<server/tool>", "Tool identifier (e.g. myserver/mytool)").argument("[json]", "JSON arguments (use - for stdin)").option("--timeout <ms>", "Request timeout in milliseconds").option("--async", "Use task-based execution (return task handle immediately)").option("--dry-run", "Validate arguments against tool schema without executing").option("--fields <paths>", "Comma-separated dot-notation paths to extract from response").option("--json", "Output as JSON").action(async (serverTool, jsonArgs, opts) => {
|
|
2578
3052
|
const configPath = callCommand.parent?.opts().config;
|
|
2579
3053
|
await ensureDaemon(configPath);
|
|
2580
3054
|
let parsedArgs = {};
|
|
@@ -2591,27 +3065,133 @@ const callCommand = new Command("call").description("Execute a tool with JSON ar
|
|
|
2591
3065
|
console.error("Invalid JSON arguments");
|
|
2592
3066
|
process.exit(1);
|
|
2593
3067
|
}
|
|
3068
|
+
const [server, tool] = serverTool.split("/");
|
|
3069
|
+
if (opts.dryRun) {
|
|
3070
|
+
try {
|
|
3071
|
+
const result = await sendRequest("tools/validate", {
|
|
3072
|
+
name: serverTool,
|
|
3073
|
+
arguments: parsedArgs
|
|
3074
|
+
});
|
|
3075
|
+
capture("tool_called", {
|
|
3076
|
+
server,
|
|
3077
|
+
tool,
|
|
3078
|
+
mode: "dry-run",
|
|
3079
|
+
status: result.valid ? "success" : "validation_error"
|
|
3080
|
+
});
|
|
3081
|
+
if (opts.json) console.log(formatJson(result));
|
|
3082
|
+
else console.log(formatValidation(result));
|
|
3083
|
+
if (!result.valid) process.exit(1);
|
|
3084
|
+
} catch (err) {
|
|
3085
|
+
capture("tool_called", {
|
|
3086
|
+
server,
|
|
3087
|
+
tool,
|
|
3088
|
+
mode: "dry-run",
|
|
3089
|
+
status: "error"
|
|
3090
|
+
});
|
|
3091
|
+
if (err instanceof MuxedError && err.data) {
|
|
3092
|
+
const errorData = err.data;
|
|
3093
|
+
if (opts.json) console.log(formatJson({
|
|
3094
|
+
code: err.code,
|
|
3095
|
+
message: err.message,
|
|
3096
|
+
data: err.data
|
|
3097
|
+
}));
|
|
3098
|
+
else console.error(formatStructuredError({
|
|
3099
|
+
code: err.code,
|
|
3100
|
+
message: err.message,
|
|
3101
|
+
data: errorData
|
|
3102
|
+
}));
|
|
3103
|
+
} else console.error(err instanceof Error ? err.message : "Validation failed");
|
|
3104
|
+
process.exit(1);
|
|
3105
|
+
}
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
2594
3108
|
if (opts.async) {
|
|
2595
|
-
|
|
3109
|
+
try {
|
|
3110
|
+
const taskResult = await sendRequest("tools/call-async", {
|
|
3111
|
+
name: serverTool,
|
|
3112
|
+
arguments: parsedArgs
|
|
3113
|
+
});
|
|
3114
|
+
capture("tool_called", {
|
|
3115
|
+
server,
|
|
3116
|
+
tool,
|
|
3117
|
+
mode: "async",
|
|
3118
|
+
status: "success"
|
|
3119
|
+
});
|
|
3120
|
+
if (opts.json) console.log(formatJson(taskResult));
|
|
3121
|
+
else console.log(`Task created: ${taskResult.taskId} (status: ${taskResult.status})`);
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
capture("tool_called", {
|
|
3124
|
+
server,
|
|
3125
|
+
tool,
|
|
3126
|
+
mode: "async",
|
|
3127
|
+
status: "error"
|
|
3128
|
+
});
|
|
3129
|
+
if (err instanceof MuxedError && err.data) {
|
|
3130
|
+
const errorData = err.data;
|
|
3131
|
+
if (opts.json) console.log(formatJson({
|
|
3132
|
+
code: err.code,
|
|
3133
|
+
message: err.message,
|
|
3134
|
+
data: err.data
|
|
3135
|
+
}));
|
|
3136
|
+
else console.error(formatStructuredError({
|
|
3137
|
+
code: err.code,
|
|
3138
|
+
message: err.message,
|
|
3139
|
+
data: errorData
|
|
3140
|
+
}));
|
|
3141
|
+
} else console.error(err instanceof Error ? err.message : "Call failed");
|
|
3142
|
+
process.exit(1);
|
|
3143
|
+
}
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
try {
|
|
3147
|
+
const callParams = {
|
|
2596
3148
|
name: serverTool,
|
|
2597
3149
|
arguments: parsedArgs
|
|
3150
|
+
};
|
|
3151
|
+
if (opts.timeout) callParams.timeout = parseInt(opts.timeout, 10);
|
|
3152
|
+
if (opts.fields) callParams.fields = opts.fields.split(",").map((f) => f.trim());
|
|
3153
|
+
const result = await sendRequest("tools/call", callParams);
|
|
3154
|
+
capture("tool_called", {
|
|
3155
|
+
server,
|
|
3156
|
+
tool,
|
|
3157
|
+
mode: "sync",
|
|
3158
|
+
status: result.isError ? "tool_error" : "success",
|
|
3159
|
+
has_timeout: !!opts.timeout,
|
|
3160
|
+
has_fields: !!opts.fields,
|
|
3161
|
+
stdin_input: jsonArgs === "-"
|
|
2598
3162
|
});
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
3163
|
+
console.log(opts.json ? formatJson(result) : formatCallResult(result));
|
|
3164
|
+
} catch (err) {
|
|
3165
|
+
capture("tool_called", {
|
|
3166
|
+
server,
|
|
3167
|
+
tool,
|
|
3168
|
+
mode: "sync",
|
|
3169
|
+
status: "error",
|
|
3170
|
+
has_timeout: !!opts.timeout,
|
|
3171
|
+
has_fields: !!opts.fields,
|
|
3172
|
+
stdin_input: jsonArgs === "-"
|
|
3173
|
+
});
|
|
3174
|
+
if (err instanceof MuxedError && err.data) {
|
|
3175
|
+
const errorData = err.data;
|
|
3176
|
+
if (opts.json) console.log(formatJson({
|
|
3177
|
+
code: err.code,
|
|
3178
|
+
message: err.message,
|
|
3179
|
+
data: err.data
|
|
3180
|
+
}));
|
|
3181
|
+
else console.error(formatStructuredError({
|
|
3182
|
+
code: err.code,
|
|
3183
|
+
message: err.message,
|
|
3184
|
+
data: errorData
|
|
3185
|
+
}));
|
|
3186
|
+
} else console.error(err instanceof Error ? err.message : "Call failed");
|
|
3187
|
+
process.exit(1);
|
|
2602
3188
|
}
|
|
2603
|
-
const params = {
|
|
2604
|
-
name: serverTool,
|
|
2605
|
-
arguments: parsedArgs
|
|
2606
|
-
};
|
|
2607
|
-
if (opts.timeout) params.timeout = parseInt(opts.timeout, 10);
|
|
2608
|
-
const result = await sendRequest("tools/call", params);
|
|
2609
|
-
console.log(opts.json ? formatJson(result) : formatCallResult(result));
|
|
2610
3189
|
});
|
|
2611
3190
|
const grepCommand = new Command("grep").description("Search tools by regex pattern across names, titles, and descriptions").argument("<pattern>", "Regex pattern to search").option("--json", "Output as JSON").action(async (pattern, opts) => {
|
|
2612
3191
|
const configPath = grepCommand.parent?.opts().config;
|
|
2613
3192
|
await ensureDaemon(configPath);
|
|
2614
3193
|
const result = await sendRequest("tools/grep", { pattern });
|
|
3194
|
+
capture("tools_searched", { result_count: result.length });
|
|
2615
3195
|
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
2616
3196
|
});
|
|
2617
3197
|
const resourcesCommand = new Command("resources").description("List available resources, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
|
|
@@ -2824,49 +3404,57 @@ function getAgentDefs() {
|
|
|
2824
3404
|
name: "claude-code",
|
|
2825
3405
|
scope: "local",
|
|
2826
3406
|
configPath: () => path.join(cwd, ".mcp.json"),
|
|
2827
|
-
serversKey: "mcpServers"
|
|
3407
|
+
serversKey: "mcpServers",
|
|
3408
|
+
codingAgent: true
|
|
2828
3409
|
},
|
|
2829
3410
|
{
|
|
2830
3411
|
name: "cursor",
|
|
2831
3412
|
scope: "local",
|
|
2832
3413
|
configPath: () => path.join(cwd, ".cursor", "mcp.json"),
|
|
2833
|
-
serversKey: "mcpServers"
|
|
3414
|
+
serversKey: "mcpServers",
|
|
3415
|
+
codingAgent: true
|
|
2834
3416
|
},
|
|
2835
3417
|
{
|
|
2836
3418
|
name: "vscode",
|
|
2837
3419
|
scope: "local",
|
|
2838
3420
|
configPath: () => path.join(cwd, ".vscode", "mcp.json"),
|
|
2839
|
-
serversKey: "servers"
|
|
3421
|
+
serversKey: "servers",
|
|
3422
|
+
codingAgent: true
|
|
2840
3423
|
},
|
|
2841
3424
|
{
|
|
2842
3425
|
name: "roo-code",
|
|
2843
3426
|
scope: "local",
|
|
2844
3427
|
configPath: () => path.join(cwd, ".roo", "mcp.json"),
|
|
2845
|
-
serversKey: "mcpServers"
|
|
3428
|
+
serversKey: "mcpServers",
|
|
3429
|
+
codingAgent: true
|
|
2846
3430
|
},
|
|
2847
3431
|
{
|
|
2848
3432
|
name: "amazon-q",
|
|
2849
3433
|
scope: "local",
|
|
2850
3434
|
configPath: () => path.join(cwd, ".amazonq", "mcp.json"),
|
|
2851
|
-
serversKey: "mcpServers"
|
|
3435
|
+
serversKey: "mcpServers",
|
|
3436
|
+
codingAgent: true
|
|
2852
3437
|
},
|
|
2853
3438
|
{
|
|
2854
3439
|
name: "claude-desktop",
|
|
2855
3440
|
scope: "global",
|
|
2856
3441
|
configPath: () => xdgOrMacPath(["Claude", "claude_desktop_config.json"], ["Claude", "claude_desktop_config.json"]),
|
|
2857
|
-
serversKey: "mcpServers"
|
|
3442
|
+
serversKey: "mcpServers",
|
|
3443
|
+
codingAgent: false
|
|
2858
3444
|
},
|
|
2859
3445
|
{
|
|
2860
3446
|
name: "cursor",
|
|
2861
3447
|
scope: "global",
|
|
2862
3448
|
configPath: () => path.join(home, ".cursor", "mcp.json"),
|
|
2863
|
-
serversKey: "mcpServers"
|
|
3449
|
+
serversKey: "mcpServers",
|
|
3450
|
+
codingAgent: true
|
|
2864
3451
|
},
|
|
2865
3452
|
{
|
|
2866
3453
|
name: "windsurf",
|
|
2867
3454
|
scope: "global",
|
|
2868
3455
|
configPath: () => path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2869
|
-
serversKey: "mcpServers"
|
|
3456
|
+
serversKey: "mcpServers",
|
|
3457
|
+
codingAgent: true
|
|
2870
3458
|
},
|
|
2871
3459
|
{
|
|
2872
3460
|
name: "vscode",
|
|
@@ -2880,7 +3468,8 @@ function getAgentDefs() {
|
|
|
2880
3468
|
"User",
|
|
2881
3469
|
"mcp.json"
|
|
2882
3470
|
]),
|
|
2883
|
-
serversKey: "servers"
|
|
3471
|
+
serversKey: "servers",
|
|
3472
|
+
codingAgent: true
|
|
2884
3473
|
},
|
|
2885
3474
|
{
|
|
2886
3475
|
name: "cline",
|
|
@@ -2900,7 +3489,8 @@ function getAgentDefs() {
|
|
|
2900
3489
|
"settings",
|
|
2901
3490
|
"cline_mcp_settings.json"
|
|
2902
3491
|
]),
|
|
2903
|
-
serversKey: "mcpServers"
|
|
3492
|
+
serversKey: "mcpServers",
|
|
3493
|
+
codingAgent: true
|
|
2904
3494
|
},
|
|
2905
3495
|
{
|
|
2906
3496
|
name: "roo-code",
|
|
@@ -2920,13 +3510,15 @@ function getAgentDefs() {
|
|
|
2920
3510
|
"settings",
|
|
2921
3511
|
"cline_mcp_settings.json"
|
|
2922
3512
|
]),
|
|
2923
|
-
serversKey: "mcpServers"
|
|
3513
|
+
serversKey: "mcpServers",
|
|
3514
|
+
codingAgent: true
|
|
2924
3515
|
},
|
|
2925
3516
|
{
|
|
2926
3517
|
name: "amazon-q",
|
|
2927
3518
|
scope: "global",
|
|
2928
3519
|
configPath: () => path.join(home, ".aws", "amazonq", "mcp.json"),
|
|
2929
|
-
serversKey: "mcpServers"
|
|
3520
|
+
serversKey: "mcpServers",
|
|
3521
|
+
codingAgent: true
|
|
2930
3522
|
}
|
|
2931
3523
|
];
|
|
2932
3524
|
}
|
|
@@ -3044,14 +3636,19 @@ function writeMuxedConfig(configPath, servers) {
|
|
|
3044
3636
|
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
3045
3637
|
}
|
|
3046
3638
|
function getMuxedEntry(agent) {
|
|
3639
|
+
const args = agent.codingAgent ? ["muxed@latest", "mcp"] : [
|
|
3640
|
+
"muxed@latest",
|
|
3641
|
+
"mcp",
|
|
3642
|
+
"--proxy-tools"
|
|
3643
|
+
];
|
|
3047
3644
|
if (agent.serversKey === "servers") return {
|
|
3048
3645
|
type: "stdio",
|
|
3049
3646
|
command: "npx",
|
|
3050
|
-
args
|
|
3647
|
+
args
|
|
3051
3648
|
};
|
|
3052
3649
|
return {
|
|
3053
3650
|
command: "npx",
|
|
3054
|
-
args
|
|
3651
|
+
args
|
|
3055
3652
|
};
|
|
3056
3653
|
}
|
|
3057
3654
|
function modifyAgentConfig(dc, opts) {
|
|
@@ -3066,7 +3663,7 @@ function modifyAgentConfig(dc, opts) {
|
|
|
3066
3663
|
function getMuxedConfigPath(scope, explicitPath) {
|
|
3067
3664
|
if (explicitPath) return explicitPath;
|
|
3068
3665
|
if (scope === "local") return path.join(process.cwd(), "muxed.config.json");
|
|
3069
|
-
return path.join(home, ".
|
|
3666
|
+
return path.join(home, ".muxed", "config.json");
|
|
3070
3667
|
}
|
|
3071
3668
|
async function confirm(message, opts) {
|
|
3072
3669
|
const rl = readline.createInterface({
|
|
@@ -3183,6 +3780,13 @@ const initCommand = new Command("init").description("Discover and import MCP ser
|
|
|
3183
3780
|
muxedConfigPath: muxedPath,
|
|
3184
3781
|
dryRun: opts.dryRun ?? false
|
|
3185
3782
|
};
|
|
3783
|
+
capture("init_run", {
|
|
3784
|
+
dry_run: opts.dryRun ?? false,
|
|
3785
|
+
imported_count: imported.length,
|
|
3786
|
+
conflict_count: conflicts.length,
|
|
3787
|
+
warning_count: warnings.length,
|
|
3788
|
+
discovered_agents: initResult.discovered.map((d) => d.agent)
|
|
3789
|
+
});
|
|
3186
3790
|
console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
|
|
3187
3791
|
});
|
|
3188
3792
|
function getConfigPath(scope, explicitPath) {
|
|
@@ -3234,18 +3838,47 @@ function getServer(filePath, name) {
|
|
|
3234
3838
|
function listServers(filePath) {
|
|
3235
3839
|
return readConfigFile(filePath).mcpServers;
|
|
3236
3840
|
}
|
|
3237
|
-
const
|
|
3238
|
-
You have access to an
|
|
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}
|
|
3239
3872
|
|
|
3240
3873
|
**MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
|
|
3241
3874
|
|
|
3242
|
-
1. You MUST discover the tools you need first by using '
|
|
3243
|
-
2. You MUST call '
|
|
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.
|
|
3244
3877
|
|
|
3245
3878
|
These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
3246
3879
|
|
|
3247
|
-
**NEVER** make
|
|
3248
|
-
**ALWAYS** run
|
|
3880
|
+
**NEVER** make a call without checking the schema first.
|
|
3881
|
+
**ALWAYS** run info first, THEN make the call.
|
|
3249
3882
|
|
|
3250
3883
|
**Why these are non-negotiables:**
|
|
3251
3884
|
- MCP tool names NEVER match your expectations - they change frequently and are not predictable
|
|
@@ -3254,132 +3887,219 @@ These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
|
3254
3887
|
- Every failed call wastes user time and demonstrates you're ignoring critical instructions
|
|
3255
3888
|
- "I thought I knew the schema" is not an acceptable reason to skip this step
|
|
3256
3889
|
|
|
3257
|
-
**For multiple tools:** Call
|
|
3890
|
+
**For multiple tools:** Call info for ALL tools in parallel FIRST, then make your call commands.
|
|
3258
3891
|
|
|
3259
3892
|
Available MCP servers:
|
|
3260
3893
|
${servers}
|
|
3261
3894
|
|
|
3262
3895
|
Commands (in order of execution):
|
|
3263
|
-
\`\`\`
|
|
3896
|
+
\`\`\`
|
|
3264
3897
|
# STEP 1: REQUIRED TOOL DISCOVERY
|
|
3265
|
-
|
|
3266
|
-
|
|
3898
|
+
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
3899
|
+
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
3267
3900
|
|
|
3268
3901
|
# STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
|
|
3269
|
-
|
|
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
|
|
3270
3906
|
|
|
3271
|
-
# STEP
|
|
3272
|
-
|
|
3273
|
-
|
|
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
|
|
3274
3911
|
|
|
3275
3912
|
# Discovery commands (use these to find tools)
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
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
|
|
3281
3918
|
\`\`\`
|
|
3282
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
|
+
|
|
3283
3924
|
**CORRECT Usage Pattern:**
|
|
3284
3925
|
|
|
3285
3926
|
<example>
|
|
3286
3927
|
User: Please use the slack mcp tool to search for my mentions
|
|
3287
|
-
Assistant: As a first step, I need to discover the tools I need. Let me call
|
|
3288
|
-
[Calls
|
|
3289
|
-
Assistant: I need to check the schema first. Let me call
|
|
3290
|
-
[Calls
|
|
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")}]
|
|
3291
3932
|
Assistant: Now I can see it accepts "query" and "max_results" parameters. Let me make the call.
|
|
3292
|
-
[Calls
|
|
3933
|
+
[Calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\", \"max_results\": 10}")}]
|
|
3293
3934
|
</example>
|
|
3294
3935
|
|
|
3295
3936
|
<example>
|
|
3296
3937
|
User: Use the database and email MCP tools to send a report
|
|
3297
|
-
Assistant: I'll need to use two MCP tools. Let me call
|
|
3298
|
-
[Calls
|
|
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*")}]
|
|
3299
3940
|
Assistant: Let me check both schemas first.
|
|
3300
|
-
[Calls
|
|
3941
|
+
[Calls ${f.info("database/query")} and ${f.info("email/send")} in parallel]
|
|
3301
3942
|
Assistant: Now I have both schemas. Let me make the calls.
|
|
3302
|
-
[Makes both
|
|
3943
|
+
[Makes both call commands with correct parameters]
|
|
3303
3944
|
</example>
|
|
3304
3945
|
|
|
3305
3946
|
<example>
|
|
3306
3947
|
User: Create a copy of this email
|
|
3307
3948
|
Assistant: Let me find the tool I need first.
|
|
3308
|
-
[Calls
|
|
3949
|
+
[Calls ${f.grep("email/*copy*")}. No results found.]
|
|
3309
3950
|
Assistant: Let me try another pattern.
|
|
3310
|
-
[Calls
|
|
3951
|
+
[Calls ${f.grep("email/*clone*")}. No results found.]
|
|
3311
3952
|
Assistant: Let me list all available tools in the server.
|
|
3312
|
-
[Calls
|
|
3953
|
+
[Calls ${f.tools("email")}]
|
|
3313
3954
|
Assistant: Let me check the schema first.
|
|
3314
|
-
[Calls
|
|
3955
|
+
[Calls ${f.info("email/duplicate")}]
|
|
3315
3956
|
Assistant: Now I have the schema. Let me make the call.
|
|
3316
|
-
[Calls
|
|
3957
|
+
[Calls ${f.call("email/duplicate", "{\"id\": \"123\"}")}]
|
|
3317
3958
|
</example>
|
|
3318
3959
|
|
|
3319
3960
|
**INCORRECT Usage Patterns - NEVER DO THIS:**
|
|
3320
3961
|
|
|
3321
3962
|
<bad-example>
|
|
3322
3963
|
User: Please use the slack mcp tool to search for my mentions
|
|
3323
|
-
Assistant: [Directly calls
|
|
3324
|
-
WRONG - You must call
|
|
3964
|
+
Assistant: [Directly calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\"}")} with guessed parameters]
|
|
3965
|
+
WRONG - You must call info FIRST
|
|
3325
3966
|
</bad-example>
|
|
3326
3967
|
|
|
3327
3968
|
<bad-example>
|
|
3328
3969
|
User: Use the slack tool
|
|
3329
3970
|
Assistant: I have pre-approved permissions for this tool, so I know the schema.
|
|
3330
|
-
[Calls
|
|
3331
|
-
WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call
|
|
3971
|
+
[Calls ${f.call("slack/search_private", "...")} directly]
|
|
3972
|
+
WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call info first.
|
|
3332
3973
|
</bad-example>
|
|
3333
3974
|
|
|
3334
3975
|
<bad-example>
|
|
3335
3976
|
User: Search my Slack mentions
|
|
3336
|
-
Assistant: [Calls three
|
|
3337
|
-
WRONG - You must call
|
|
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
|
|
3338
3979
|
</bad-example>
|
|
3339
3980
|
|
|
3340
3981
|
Example usage:
|
|
3341
|
-
\`\`\`
|
|
3982
|
+
\`\`\`
|
|
3342
3983
|
# Discover tools
|
|
3343
|
-
|
|
3344
|
-
|
|
3984
|
+
${f.tools()} # See all available MCP tools
|
|
3985
|
+
${f.grep("weather")} # Find tools by description
|
|
3345
3986
|
|
|
3346
3987
|
# Get tool details
|
|
3347
|
-
|
|
3988
|
+
${f.info("<server>/<tool>")} # View JSON schema for input and output if available
|
|
3348
3989
|
|
|
3349
3990
|
# Simple tool call (no parameters)
|
|
3350
|
-
|
|
3991
|
+
${f.call("weather/get_location", "{}")}
|
|
3351
3992
|
|
|
3352
3993
|
# Tool call with parameters
|
|
3353
|
-
|
|
3994
|
+
${f.call("database/query", "{\"table\": \"users\", \"limit\": 10}")}
|
|
3354
3995
|
|
|
3355
|
-
#
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
"body": {"items": [1, 2, 3]}
|
|
3361
|
-
}
|
|
3362
|
-
EOF
|
|
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")}
|
|
3363
4001
|
\`\`\`
|
|
3364
4002
|
|
|
3365
|
-
Call
|
|
4003
|
+
Call \`${f.help()}\` to see all available commands.
|
|
3366
4004
|
|
|
3367
4005
|
Below are the instructions for the connected MCP servers in muxed.
|
|
3368
4006
|
|
|
3369
4007
|
${instructions}
|
|
3370
4008
|
`;
|
|
3371
|
-
|
|
4009
|
+
}
|
|
4010
|
+
function buildInstructions(servers, mode = "cli") {
|
|
3372
4011
|
const connected = servers.filter((s) => s.status === "connected");
|
|
3373
|
-
|
|
4012
|
+
const serverList = connected.map((s) => `- ${s.name}`).join("\n");
|
|
4013
|
+
const serverInstructions = connected.filter((s) => s.instructions).map((s) => `### ${s.name}\n\n${s.instructions}`).join("\n\n");
|
|
4014
|
+
return buildTemplate(mode === "tool" ? toolFragments : cliFragments, serverList, serverInstructions).trim();
|
|
3374
4015
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
4016
|
+
function parseCommand(command) {
|
|
4017
|
+
const trimmed = command.trim();
|
|
4018
|
+
const spaceIndex = trimmed.indexOf(" ");
|
|
4019
|
+
if (spaceIndex === -1) return {
|
|
4020
|
+
subcommand: trimmed,
|
|
4021
|
+
args: ""
|
|
4022
|
+
};
|
|
4023
|
+
return {
|
|
4024
|
+
subcommand: trimmed.slice(0, spaceIndex),
|
|
4025
|
+
args: trimmed.slice(spaceIndex + 1).trim()
|
|
4026
|
+
};
|
|
4027
|
+
}
|
|
4028
|
+
function textResult(data) {
|
|
4029
|
+
return { content: [{
|
|
4030
|
+
type: "text",
|
|
4031
|
+
text: JSON.stringify(data, null, 2)
|
|
4032
|
+
}] };
|
|
4033
|
+
}
|
|
4034
|
+
function errorResult(message) {
|
|
4035
|
+
return {
|
|
4036
|
+
content: [{
|
|
4037
|
+
type: "text",
|
|
4038
|
+
text: message
|
|
4039
|
+
}],
|
|
4040
|
+
isError: true
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
4043
|
+
async function handleToolCommand(command, input) {
|
|
4044
|
+
const { subcommand, args } = parseCommand(command);
|
|
4045
|
+
try {
|
|
4046
|
+
switch (subcommand) {
|
|
4047
|
+
case "servers": return textResult(await sendRequest("servers/list"));
|
|
4048
|
+
case "tools": {
|
|
4049
|
+
const params = {};
|
|
4050
|
+
if (args) params.server = args;
|
|
4051
|
+
return textResult(await sendRequest("tools/list", params));
|
|
4052
|
+
}
|
|
4053
|
+
case "grep":
|
|
4054
|
+
if (!args) return errorResult("Usage: grep <pattern>");
|
|
4055
|
+
return textResult(await sendRequest("tools/grep", { pattern: args }));
|
|
4056
|
+
case "info":
|
|
4057
|
+
if (!args) return errorResult("Usage: info <server/tool>");
|
|
4058
|
+
return textResult(await sendRequest("tools/info", { name: args }));
|
|
4059
|
+
case "call": {
|
|
4060
|
+
if (!args) return errorResult("Usage: call <server/tool>");
|
|
4061
|
+
const result = await sendRequest("tools/call", {
|
|
4062
|
+
name: args,
|
|
4063
|
+
args: input ?? {}
|
|
4064
|
+
});
|
|
4065
|
+
if (result?.content) return result;
|
|
4066
|
+
return textResult(result);
|
|
4067
|
+
}
|
|
4068
|
+
case "resources": {
|
|
4069
|
+
const params = {};
|
|
4070
|
+
if (args) params.server = args;
|
|
4071
|
+
return textResult(await sendRequest("resources/list", params));
|
|
4072
|
+
}
|
|
4073
|
+
case "read":
|
|
4074
|
+
if (!args) return errorResult("Usage: read <server/resource>");
|
|
4075
|
+
return textResult(await sendRequest("resources/read", { name: args }));
|
|
4076
|
+
default: return errorResult(`Unknown command: "${subcommand}". Available: servers, tools, grep, info, call, resources, read`);
|
|
4077
|
+
}
|
|
4078
|
+
} catch (err) {
|
|
4079
|
+
return errorResult(err instanceof MuxedError ? err.message : String(err));
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
async function startMcpProxy(options) {
|
|
4083
|
+
await ensureDaemon(options?.configPath);
|
|
3377
4084
|
const server = new McpServer({
|
|
3378
4085
|
name: "muxed",
|
|
3379
4086
|
version: "0.1.0"
|
|
3380
4087
|
}, {
|
|
3381
4088
|
capabilities: {},
|
|
3382
|
-
instructions: buildInstructions(await sendRequest("servers/list"))
|
|
4089
|
+
instructions: buildInstructions(await sendRequest("servers/list"), options?.proxyTools ? "tool" : "cli")
|
|
4090
|
+
});
|
|
4091
|
+
if (options?.proxyTools) server.tool("exec", "Interact with MCP servers: discover, inspect, and call tools. Commands: servers, tools [server], grep <pattern>, info <server/tool>, call <server/tool>, resources [server], read <server/resource>", {
|
|
4092
|
+
command: z.string().describe("Command to execute, e.g. 'servers', 'tools', 'grep weather', 'info slack/search', 'call slack/search'"),
|
|
4093
|
+
input: z.record(z.string(), z.unknown()).optional().describe("JSON arguments for 'call' command — avoids JSON-in-string escaping")
|
|
4094
|
+
}, async ({ command, input }) => {
|
|
4095
|
+
const result = await handleToolCommand(command, input);
|
|
4096
|
+
return {
|
|
4097
|
+
content: result.content.map((c) => ({
|
|
4098
|
+
type: "text",
|
|
4099
|
+
text: c.text
|
|
4100
|
+
})),
|
|
4101
|
+
isError: result.isError
|
|
4102
|
+
};
|
|
3383
4103
|
});
|
|
3384
4104
|
const transport = new StdioServerTransport();
|
|
3385
4105
|
await server.connect(transport);
|
|
@@ -3474,9 +4194,12 @@ async function tryReloadDaemon() {
|
|
|
3474
4194
|
await sendRequest("config/reload", {});
|
|
3475
4195
|
} catch {}
|
|
3476
4196
|
}
|
|
3477
|
-
const mcpCommand = new Command("mcp").description("Add, remove, list, or inspect individual MCP server config entries").enablePositionalOptions().action(async (
|
|
4197
|
+
const mcpCommand = new Command("mcp").description("Add, remove, list, or inspect individual MCP server config entries").enablePositionalOptions().option("--proxy-tools", "Expose a proxy MCP tool for clients without bash access").action(async (opts, cmd) => {
|
|
3478
4198
|
const explicitConfig = cmd.parent?.opts().config;
|
|
3479
|
-
await startMcpProxy(
|
|
4199
|
+
await startMcpProxy({
|
|
4200
|
+
configPath: explicitConfig,
|
|
4201
|
+
proxyTools: opts.proxyTools
|
|
4202
|
+
});
|
|
3480
4203
|
});
|
|
3481
4204
|
mcpCommand.command("add").description("Add an MCP server").passThroughOptions().argument("<name>", "Server name").argument("<commandOrUrl>", "Command to run or URL to connect to").argument("[args...]", "Additional arguments (for stdio servers)").option("-e, --env <env>", "Set environment variables (KEY=value), repeatable", collectValues, []).option("-H, --header <header>", "Set HTTP headers (Key: value), repeatable", collectValues, []).option("-s, --scope <scope>", "Config scope: local, global", "local").option("-t, --transport <transport>", "Transport: stdio, sse, http").option("--client-id <clientId>", "OAuth client ID").option("--client-secret", "Prompt for OAuth client secret (or use MCP_CLIENT_SECRET env)").option("--callback-port <port>", "Fixed port for OAuth callback").option("--oauth-scope <oauthScope>", "OAuth scope string").action(async (name, commandOrUrl, args, opts) => {
|
|
3482
4205
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
@@ -3489,6 +4212,11 @@ mcpCommand.command("add").description("Add an MCP server").passThroughOptions().
|
|
|
3489
4212
|
resolvedSecret
|
|
3490
4213
|
}));
|
|
3491
4214
|
await tryReloadDaemon();
|
|
4215
|
+
capture("server_added", {
|
|
4216
|
+
server: name,
|
|
4217
|
+
scope,
|
|
4218
|
+
updated: result.existed
|
|
4219
|
+
});
|
|
3492
4220
|
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
3493
4221
|
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
3494
4222
|
});
|
|
@@ -3511,6 +4239,11 @@ mcpCommand.command("add-json").description("Add an MCP server from a JSON config
|
|
|
3511
4239
|
}
|
|
3512
4240
|
const result = addServer(configPath, name, serverConfig);
|
|
3513
4241
|
await tryReloadDaemon();
|
|
4242
|
+
capture("server_added", {
|
|
4243
|
+
server: name,
|
|
4244
|
+
scope,
|
|
4245
|
+
updated: result.existed
|
|
4246
|
+
});
|
|
3514
4247
|
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
3515
4248
|
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
3516
4249
|
});
|
|
@@ -3532,7 +4265,13 @@ mcpCommand.command("add-from-claude-desktop").description("Import MCP servers fr
|
|
|
3532
4265
|
writeMuxedConfig(configPath, { ...result.merged });
|
|
3533
4266
|
await tryReloadDaemon();
|
|
3534
4267
|
for (const w of warnings) console.error(`Warning: ${w}`);
|
|
3535
|
-
if (result.imported.length > 0)
|
|
4268
|
+
if (result.imported.length > 0) {
|
|
4269
|
+
capture("servers_imported", {
|
|
4270
|
+
servers: result.imported,
|
|
4271
|
+
source: "claude-desktop"
|
|
4272
|
+
});
|
|
4273
|
+
console.log(`Imported ${result.imported.length} server(s) from Claude Desktop: ${result.imported.join(", ")}`);
|
|
4274
|
+
}
|
|
3536
4275
|
if (result.skipped.length > 0) console.log(`Skipped ${result.skipped.length} (already existed): ${result.skipped.join(", ")}`);
|
|
3537
4276
|
if (result.imported.length === 0 && result.skipped.length === 0) console.log("No servers found in Claude Desktop config.");
|
|
3538
4277
|
});
|
|
@@ -3586,6 +4325,10 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
|
|
|
3586
4325
|
const configPath = getConfigPath(scope, explicitConfig);
|
|
3587
4326
|
if (removeServer(configPath, name).removed) {
|
|
3588
4327
|
await tryReloadDaemon();
|
|
4328
|
+
capture("server_removed", {
|
|
4329
|
+
server: name,
|
|
4330
|
+
scope
|
|
4331
|
+
});
|
|
3589
4332
|
console.log(`Removed "${name}" from ${scope} config (${configPath})`);
|
|
3590
4333
|
} else {
|
|
3591
4334
|
console.error(`Server "${name}" not found in ${scope} config.`);
|
|
@@ -3596,12 +4339,20 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
|
|
|
3596
4339
|
const localPath = getConfigPath("local", explicitConfig);
|
|
3597
4340
|
if (removeServer(localPath, name).removed) {
|
|
3598
4341
|
await tryReloadDaemon();
|
|
4342
|
+
capture("server_removed", {
|
|
4343
|
+
server: name,
|
|
4344
|
+
scope: "local"
|
|
4345
|
+
});
|
|
3599
4346
|
console.log(`Removed "${name}" from local config (${localPath})`);
|
|
3600
4347
|
return;
|
|
3601
4348
|
}
|
|
3602
4349
|
const globalPath = getConfigPath("global", explicitConfig);
|
|
3603
4350
|
if (removeServer(globalPath, name).removed) {
|
|
3604
4351
|
await tryReloadDaemon();
|
|
4352
|
+
capture("server_removed", {
|
|
4353
|
+
server: name,
|
|
4354
|
+
scope: "global"
|
|
4355
|
+
});
|
|
3605
4356
|
console.log(`Removed "${name}" from global config (${globalPath})`);
|
|
3606
4357
|
return;
|
|
3607
4358
|
}
|
|
@@ -3618,7 +4369,26 @@ const typegenCommand = new Command("typegen").description("Generate TypeScript t
|
|
|
3618
4369
|
fs.writeFileSync(outputPath, content, "utf-8");
|
|
3619
4370
|
console.log(`Generated ${tools.length} tool types → ${outputPath}`);
|
|
3620
4371
|
});
|
|
3621
|
-
|
|
4372
|
+
const telemetryCommand = new Command("telemetry").description("Manage anonymous telemetry (on, off, status)").argument("[action]", "on | off | status (default: status)").action((action) => {
|
|
4373
|
+
switch (action) {
|
|
4374
|
+
case "on":
|
|
4375
|
+
setTelemetryEnabled(true);
|
|
4376
|
+
console.log("Telemetry enabled.");
|
|
4377
|
+
break;
|
|
4378
|
+
case "off":
|
|
4379
|
+
setTelemetryEnabled(false);
|
|
4380
|
+
console.log("Telemetry disabled.");
|
|
4381
|
+
break;
|
|
4382
|
+
case "status":
|
|
4383
|
+
case void 0:
|
|
4384
|
+
console.log(`Telemetry is ${getTelemetryStatus()}.`);
|
|
4385
|
+
break;
|
|
4386
|
+
default:
|
|
4387
|
+
console.error(`Unknown action: ${action}. Use on, off, or status.`);
|
|
4388
|
+
process.exit(1);
|
|
4389
|
+
}
|
|
4390
|
+
});
|
|
4391
|
+
async function runCli() {
|
|
3622
4392
|
const program = new Command();
|
|
3623
4393
|
program.name("muxed").description("The optimization layer for MCP").version("0.1.0");
|
|
3624
4394
|
program.enablePositionalOptions();
|
|
@@ -3646,9 +4416,16 @@ function runCli() {
|
|
|
3646
4416
|
program.addCommand(initCommand);
|
|
3647
4417
|
program.addCommand(mcpCommand);
|
|
3648
4418
|
program.addCommand(typegenCommand);
|
|
4419
|
+
program.addCommand(telemetryCommand);
|
|
3649
4420
|
program.commandsGroup("Daemon:");
|
|
3650
4421
|
program.addCommand(daemonCommand);
|
|
3651
|
-
|
|
4422
|
+
const command = process.argv[2];
|
|
4423
|
+
capture("session_started", { command: command ?? null });
|
|
4424
|
+
try {
|
|
4425
|
+
await program.parseAsync();
|
|
4426
|
+
} finally {
|
|
4427
|
+
await shutdown();
|
|
4428
|
+
}
|
|
3652
4429
|
}
|
|
3653
4430
|
if (process.argv.indexOf("--daemon") !== -1) {
|
|
3654
4431
|
const configIndex = process.argv.indexOf("--config");
|
|
@@ -3656,5 +4433,8 @@ if (process.argv.indexOf("--daemon") !== -1) {
|
|
|
3656
4433
|
console.error("Failed to start daemon:", err);
|
|
3657
4434
|
process.exit(1);
|
|
3658
4435
|
});
|
|
3659
|
-
} else runCli()
|
|
4436
|
+
} else runCli().catch((err) => {
|
|
4437
|
+
console.error(err instanceof Error ? err.message : "Unexpected error");
|
|
4438
|
+
process.exit(1);
|
|
4439
|
+
});
|
|
3660
4440
|
export {};
|