muxed 0.1.0 → 0.2.0
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 +323 -0
- package/dist/cli.mjs +978 -209
- package/dist/client/index.d.mts +24 -10
- package/dist/client/index.mjs +28 -21
- package/package.json +14 -11
- package/LICENSE +0 -21
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({
|
|
@@ -84,7 +89,7 @@ const DaemonConfigSchema = z.object({
|
|
|
84
89
|
shutdownTimeout: z.number().optional(),
|
|
85
90
|
http: HttpListenerSchema.optional()
|
|
86
91
|
});
|
|
87
|
-
const
|
|
92
|
+
const MuxedConfigSchema = z.object({
|
|
88
93
|
mcpServers: z.record(z.string(), ServerConfigSchema),
|
|
89
94
|
daemon: DaemonConfigSchema.optional(),
|
|
90
95
|
mergeClaudeConfig: z.boolean().optional()
|
|
@@ -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,14 +127,14 @@ 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) {
|
|
129
134
|
if (!fs.existsSync(configPath)) throw new Error(`Config file not found: ${configPath}`);
|
|
130
135
|
return configPath;
|
|
131
136
|
}
|
|
132
|
-
const cwdConfig = path.join(process.cwd(), "
|
|
137
|
+
const cwdConfig = path.join(process.cwd(), "muxed.config.json");
|
|
133
138
|
if (fs.existsSync(cwdConfig)) return cwdConfig;
|
|
134
139
|
const homeConfig = getGlobalConfigPath();
|
|
135
140
|
if (fs.existsSync(homeConfig)) return homeConfig;
|
|
@@ -148,7 +153,7 @@ function loadConfig(configPath) {
|
|
|
148
153
|
const globalConfigPath = getGlobalConfigPath();
|
|
149
154
|
if ((!filePath || path.resolve(filePath) !== path.resolve(globalConfigPath)) && fs.existsSync(globalConfigPath)) try {
|
|
150
155
|
const globalRaw = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
|
|
151
|
-
const globalResult =
|
|
156
|
+
const globalResult = MuxedConfigSchema.safeParse(globalRaw);
|
|
152
157
|
if (globalResult.success) config.mcpServers = {
|
|
153
158
|
...globalResult.data.mcpServers,
|
|
154
159
|
...config.mcpServers
|
|
@@ -176,7 +181,7 @@ function parseConfigFile(filePath) {
|
|
|
176
181
|
} catch {
|
|
177
182
|
throw new Error(`Invalid JSON in config file: ${filePath}`);
|
|
178
183
|
}
|
|
179
|
-
const result =
|
|
184
|
+
const result = MuxedConfigSchema.safeParse(parsed);
|
|
180
185
|
if (!result.success) throw new Error(`Invalid config: ${z.prettifyError(result.error)}`);
|
|
181
186
|
return result.data;
|
|
182
187
|
}
|
|
@@ -187,26 +192,26 @@ function isHttpConfig(config) {
|
|
|
187
192
|
return "url" in config;
|
|
188
193
|
}
|
|
189
194
|
var paths_exports = /* @__PURE__ */ __exportAll({
|
|
190
|
-
|
|
195
|
+
ensureMuxedDir: () => ensureMuxedDir,
|
|
191
196
|
getLogPath: () => getLogPath,
|
|
197
|
+
getMuxedDir: () => getMuxedDir,
|
|
192
198
|
getPidPath: () => getPidPath,
|
|
193
|
-
getSocketPath: () => getSocketPath
|
|
194
|
-
getTooldDir: () => getTooldDir
|
|
199
|
+
getSocketPath: () => getSocketPath
|
|
195
200
|
});
|
|
196
|
-
function
|
|
197
|
-
return path.join(os.homedir(), ".
|
|
201
|
+
function getMuxedDir() {
|
|
202
|
+
return path.join(os.homedir(), ".muxed");
|
|
198
203
|
}
|
|
199
204
|
function getSocketPath() {
|
|
200
|
-
return path.join(
|
|
205
|
+
return path.join(getMuxedDir(), "muxed.sock");
|
|
201
206
|
}
|
|
202
207
|
function getPidPath() {
|
|
203
|
-
return path.join(
|
|
208
|
+
return path.join(getMuxedDir(), "muxed.pid");
|
|
204
209
|
}
|
|
205
210
|
function getLogPath() {
|
|
206
|
-
return path.join(
|
|
211
|
+
return path.join(getMuxedDir(), "muxed.log");
|
|
207
212
|
}
|
|
208
|
-
function
|
|
209
|
-
fs.mkdirSync(
|
|
213
|
+
function ensureMuxedDir() {
|
|
214
|
+
fs.mkdirSync(getMuxedDir(), { recursive: true });
|
|
210
215
|
}
|
|
211
216
|
const LOG_LEVELS = {
|
|
212
217
|
debug: 0,
|
|
@@ -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
|
-
return path.join(
|
|
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() {
|
|
@@ -388,7 +395,7 @@ var ClientCredentialsProvider = class {
|
|
|
388
395
|
token_endpoint_auth_method: "client_secret_basic",
|
|
389
396
|
grant_types: ["client_credentials"],
|
|
390
397
|
response_types: [],
|
|
391
|
-
client_name: "
|
|
398
|
+
client_name: "muxed"
|
|
392
399
|
};
|
|
393
400
|
}
|
|
394
401
|
clientInformation() {
|
|
@@ -443,7 +450,7 @@ function openBrowser(url) {
|
|
|
443
450
|
openUrl(url);
|
|
444
451
|
}
|
|
445
452
|
async function notifyReauth(serverName, authUrl) {
|
|
446
|
-
const title = "
|
|
453
|
+
const title = "muxed";
|
|
447
454
|
const message = `Server "${serverName}" needs re-authorization`;
|
|
448
455
|
const platform = process.platform;
|
|
449
456
|
if (platform === "darwin") {
|
|
@@ -485,7 +492,7 @@ var AuthorizationCodeProvider = class {
|
|
|
485
492
|
token_endpoint_auth_method: this.config.clientSecret ? "client_secret_basic" : "none",
|
|
486
493
|
grant_types: ["authorization_code", "refresh_token"],
|
|
487
494
|
response_types: ["code"],
|
|
488
|
-
client_name: "
|
|
495
|
+
client_name: "muxed",
|
|
489
496
|
scope: this.config.scope
|
|
490
497
|
};
|
|
491
498
|
}
|
|
@@ -542,13 +549,13 @@ var AuthorizationCodeProvider = class {
|
|
|
542
549
|
}
|
|
543
550
|
};
|
|
544
551
|
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
545
|
-
<html><head><title>
|
|
552
|
+
<html><head><title>muxed - Authorization Successful</title></head>
|
|
546
553
|
<body style="font-family:system-ui;text-align:center;padding:60px">
|
|
547
554
|
<h1>Authorization Successful</h1>
|
|
548
|
-
<p>You can close this tab and return to
|
|
555
|
+
<p>You can close this tab and return to muxed.</p>
|
|
549
556
|
</body></html>`;
|
|
550
557
|
const ERROR_HTML = (msg) => `<!DOCTYPE html>
|
|
551
|
-
<html><head><title>
|
|
558
|
+
<html><head><title>muxed - Authorization Error</title></head>
|
|
552
559
|
<body style="font-family:system-ui;text-align:center;padding:60px">
|
|
553
560
|
<h1>Authorization Error</h1>
|
|
554
561
|
<p>${msg}</p>
|
|
@@ -761,7 +768,7 @@ var ServerManager = class {
|
|
|
761
768
|
}
|
|
762
769
|
createClient() {
|
|
763
770
|
const client = new Client({
|
|
764
|
-
name: "
|
|
771
|
+
name: "muxed",
|
|
765
772
|
version: "0.1.0"
|
|
766
773
|
}, {
|
|
767
774
|
capabilities: { tasks: {
|
|
@@ -1051,9 +1058,108 @@ var ServerManager = class {
|
|
|
1051
1058
|
};
|
|
1052
1059
|
}
|
|
1053
1060
|
};
|
|
1061
|
+
const ErrorCode = {
|
|
1062
|
+
TOOL_NOT_FOUND: "TOOL_NOT_FOUND",
|
|
1063
|
+
SERVER_NOT_FOUND: "SERVER_NOT_FOUND",
|
|
1064
|
+
SERVER_NOT_CONNECTED: "SERVER_NOT_CONNECTED",
|
|
1065
|
+
INVALID_ARGUMENTS: "INVALID_ARGUMENTS",
|
|
1066
|
+
INVALID_FORMAT: "INVALID_FORMAT",
|
|
1067
|
+
MISSING_PARAMETER: "MISSING_PARAMETER",
|
|
1068
|
+
TIMEOUT: "TIMEOUT"
|
|
1069
|
+
};
|
|
1070
|
+
function levenshtein(a, b) {
|
|
1071
|
+
const m = a.length;
|
|
1072
|
+
const n = b.length;
|
|
1073
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
1074
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
1075
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
1076
|
+
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]);
|
|
1077
|
+
return dp[m][n];
|
|
1078
|
+
}
|
|
1079
|
+
function findSimilarTools(targetTool, allTools, maxResults = 3) {
|
|
1080
|
+
const maxDistance = Math.max(3, Math.floor(targetTool.length * .4));
|
|
1081
|
+
return allTools.map(({ server, tool }) => {
|
|
1082
|
+
const fullName = `${server}/${tool.name}`;
|
|
1083
|
+
const toolOnly = tool.name;
|
|
1084
|
+
const distFull = levenshtein(targetTool.toLowerCase(), fullName.toLowerCase());
|
|
1085
|
+
const distTool = levenshtein(targetTool.toLowerCase(), toolOnly.toLowerCase());
|
|
1086
|
+
return {
|
|
1087
|
+
fullName,
|
|
1088
|
+
dist: Math.min(distFull, distTool)
|
|
1089
|
+
};
|
|
1090
|
+
}).filter(({ dist }) => dist <= maxDistance).sort((a, b) => a.dist - b.dist).slice(0, maxResults).map(({ fullName }) => fullName);
|
|
1091
|
+
}
|
|
1092
|
+
function toolNotFoundError(name, similarTools) {
|
|
1093
|
+
const hasSimilar = similarTools.length > 0;
|
|
1094
|
+
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.`;
|
|
1095
|
+
return {
|
|
1096
|
+
code: ErrorCode.TOOL_NOT_FOUND,
|
|
1097
|
+
message: `Tool not found: ${name}`,
|
|
1098
|
+
suggestion,
|
|
1099
|
+
context: hasSimilar ? { similarTools } : void 0
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
function serverNotFoundError(serverName, availableServers) {
|
|
1103
|
+
return {
|
|
1104
|
+
code: ErrorCode.SERVER_NOT_FOUND,
|
|
1105
|
+
message: `Server not found: ${serverName}`,
|
|
1106
|
+
suggestion: `Available servers: ${availableServers.join(", ") || "none"}. Run 'muxed servers' to list all.`,
|
|
1107
|
+
context: { availableServers }
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function serverNotConnectedError(serverName) {
|
|
1111
|
+
return {
|
|
1112
|
+
code: ErrorCode.SERVER_NOT_CONNECTED,
|
|
1113
|
+
message: `Server not connected: ${serverName}`,
|
|
1114
|
+
suggestion: `The server may be starting up. Run 'muxed status' to check, or 'muxed reload' to reconnect.`
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
function invalidFormatError(name) {
|
|
1118
|
+
return {
|
|
1119
|
+
code: ErrorCode.INVALID_FORMAT,
|
|
1120
|
+
message: `Invalid tool name format: ${name}`,
|
|
1121
|
+
suggestion: `Use the format 'server/tool' (e.g. 'myserver/mytool'). Run 'muxed tools' to list all available tools.`
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function missingParameterError(param) {
|
|
1125
|
+
return {
|
|
1126
|
+
code: ErrorCode.MISSING_PARAMETER,
|
|
1127
|
+
message: `Missing required parameter: ${param}`,
|
|
1128
|
+
suggestion: `Provide the '${param}' parameter in the request.`
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
function invalidArgumentsError(toolName, errors) {
|
|
1132
|
+
return {
|
|
1133
|
+
code: ErrorCode.INVALID_ARGUMENTS,
|
|
1134
|
+
message: `Invalid arguments for tool ${toolName}`,
|
|
1135
|
+
suggestion: `Run 'muxed info ${toolName}' to see the expected input schema.`,
|
|
1136
|
+
context: { validationErrors: errors }
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
function timeoutError(toolName, timeoutMs) {
|
|
1140
|
+
return {
|
|
1141
|
+
code: ErrorCode.TIMEOUT,
|
|
1142
|
+
message: `Tool call timed out after ${timeoutMs}ms: ${toolName}`,
|
|
1143
|
+
suggestion: `Increase the timeout with --timeout <ms>, or use --async for long-running operations.`
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
function isTimeoutError(err) {
|
|
1147
|
+
if (!(err instanceof Error)) return false;
|
|
1148
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") return true;
|
|
1149
|
+
const msg = err.message.toLowerCase();
|
|
1150
|
+
return msg.includes("timeout") || msg.includes("aborted");
|
|
1151
|
+
}
|
|
1152
|
+
function toErrorData(err) {
|
|
1153
|
+
return {
|
|
1154
|
+
code: err.code,
|
|
1155
|
+
suggestion: err.suggestion,
|
|
1156
|
+
...err.context ? { context: err.context } : {}
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1054
1159
|
var ServerPool = class {
|
|
1055
1160
|
servers = /* @__PURE__ */ new Map();
|
|
1056
1161
|
trackedTasks = /* @__PURE__ */ new Map();
|
|
1162
|
+
zodSchemaCache = /* @__PURE__ */ new Map();
|
|
1057
1163
|
taskExpiryTimer;
|
|
1058
1164
|
taskExpiryTimeout = 36e5;
|
|
1059
1165
|
async connectAll(config) {
|
|
@@ -1080,6 +1186,7 @@ var ServerPool = class {
|
|
|
1080
1186
|
}
|
|
1081
1187
|
async disconnectAll() {
|
|
1082
1188
|
this.stopTaskExpiry();
|
|
1189
|
+
this.zodSchemaCache.clear();
|
|
1083
1190
|
await Promise.allSettled([...this.servers.values()].map((manager) => manager.disconnect()));
|
|
1084
1191
|
}
|
|
1085
1192
|
onServerHealthChange(serverName, status, error) {
|
|
@@ -1157,6 +1264,91 @@ var ServerPool = class {
|
|
|
1157
1264
|
tool
|
|
1158
1265
|
};
|
|
1159
1266
|
}
|
|
1267
|
+
findToolOrError(serverTool) {
|
|
1268
|
+
const slashIndex = serverTool.indexOf("/");
|
|
1269
|
+
if (slashIndex === -1) return {
|
|
1270
|
+
ok: false,
|
|
1271
|
+
error: invalidFormatError(serverTool)
|
|
1272
|
+
};
|
|
1273
|
+
const serverName = serverTool.slice(0, slashIndex);
|
|
1274
|
+
const toolName = serverTool.slice(slashIndex + 1);
|
|
1275
|
+
const manager = this.servers.get(serverName);
|
|
1276
|
+
if (!manager) return {
|
|
1277
|
+
ok: false,
|
|
1278
|
+
error: serverNotFoundError(serverName, [...this.servers.keys()])
|
|
1279
|
+
};
|
|
1280
|
+
if (manager.getStatus() !== "connected") return {
|
|
1281
|
+
ok: false,
|
|
1282
|
+
error: serverNotConnectedError(serverName)
|
|
1283
|
+
};
|
|
1284
|
+
const tool = manager.listTools().find((t) => t.name === toolName);
|
|
1285
|
+
if (!tool) return {
|
|
1286
|
+
ok: false,
|
|
1287
|
+
error: toolNotFoundError(serverTool, findSimilarTools(serverTool, this.listAllTools()))
|
|
1288
|
+
};
|
|
1289
|
+
return {
|
|
1290
|
+
ok: true,
|
|
1291
|
+
manager,
|
|
1292
|
+
tool,
|
|
1293
|
+
serverTimeout: manager.getState().config.timeout
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
getZodSchema(inputSchema) {
|
|
1297
|
+
const key = JSON.stringify(inputSchema);
|
|
1298
|
+
const cached = this.zodSchemaCache.get(key);
|
|
1299
|
+
if (cached !== void 0) return cached;
|
|
1300
|
+
try {
|
|
1301
|
+
const zodSchema = z$1.fromJSONSchema(inputSchema);
|
|
1302
|
+
this.zodSchemaCache.set(key, zodSchema);
|
|
1303
|
+
return zodSchema;
|
|
1304
|
+
} catch {
|
|
1305
|
+
this.zodSchemaCache.set(key, "unsupported");
|
|
1306
|
+
return "unsupported";
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
validateToolArgs(serverTool, args) {
|
|
1310
|
+
const found = this.findToolOrError(serverTool);
|
|
1311
|
+
if (!found.ok) return {
|
|
1312
|
+
valid: false,
|
|
1313
|
+
errors: [found.error.message],
|
|
1314
|
+
warnings: []
|
|
1315
|
+
};
|
|
1316
|
+
const { tool } = found;
|
|
1317
|
+
const errors = [];
|
|
1318
|
+
const warnings = [];
|
|
1319
|
+
if (tool.inputSchema) {
|
|
1320
|
+
const zodSchema = this.getZodSchema(tool.inputSchema);
|
|
1321
|
+
if (zodSchema === "unsupported") {
|
|
1322
|
+
getLogger().warn(`Could not convert inputSchema for ${serverTool}: unsupported schema`, serverTool.split("/")[0]);
|
|
1323
|
+
this.addAnnotationWarnings(tool, warnings);
|
|
1324
|
+
return {
|
|
1325
|
+
valid: true,
|
|
1326
|
+
errors: [],
|
|
1327
|
+
warnings,
|
|
1328
|
+
unsupported: true,
|
|
1329
|
+
tool
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const result = zodSchema.safeParse(args);
|
|
1333
|
+
if (!result.success) for (const issue of result.error.issues) {
|
|
1334
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "";
|
|
1335
|
+
const prefix = path ? `Field '${path}': ` : "";
|
|
1336
|
+
errors.push(`${prefix}${issue.message}`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
this.addAnnotationWarnings(tool, warnings);
|
|
1340
|
+
return {
|
|
1341
|
+
valid: errors.length === 0,
|
|
1342
|
+
errors,
|
|
1343
|
+
warnings,
|
|
1344
|
+
tool
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
addAnnotationWarnings(tool, warnings) {
|
|
1348
|
+
if (tool.annotations?.destructiveHint) warnings.push("Tool is marked as destructive.");
|
|
1349
|
+
if (!tool.annotations?.idempotentHint) warnings.push("Tool is not marked as idempotent.");
|
|
1350
|
+
if (tool.annotations?.readOnlyHint === false) warnings.push("Tool may modify data (not read-only).");
|
|
1351
|
+
}
|
|
1160
1352
|
grepTools(pattern) {
|
|
1161
1353
|
const normalized = pattern.replace(/\\([|()\{\}])/g, "$1");
|
|
1162
1354
|
const regex = new RegExp(normalized, "i");
|
|
@@ -1318,11 +1510,11 @@ async function generateTypes(tools) {
|
|
|
1318
1510
|
const jsdoc = tool.description ? ` /** ${escapeJsdoc(tool.description)} */\n` : "";
|
|
1319
1511
|
toolTypes.push(`${jsdoc} '${qualifiedName}': {\n input: ${indent(inputType, 6)};\n output: ${indent(outputType, 6)};\n };`);
|
|
1320
1512
|
}
|
|
1321
|
-
return `// Auto-generated by \`
|
|
1322
|
-
// Run \`
|
|
1513
|
+
return `// Auto-generated by \`muxed typegen\` – do not edit
|
|
1514
|
+
// Run \`muxed typegen\` to regenerate
|
|
1323
1515
|
|
|
1324
|
-
declare module '
|
|
1325
|
-
interface
|
|
1516
|
+
declare module 'muxed' {
|
|
1517
|
+
interface MuxedToolMap {
|
|
1326
1518
|
${toolTypes.join("\n")}
|
|
1327
1519
|
}
|
|
1328
1520
|
}
|
|
@@ -1353,6 +1545,89 @@ function indent(text, spaces) {
|
|
|
1353
1545
|
const pad = " ".repeat(spaces);
|
|
1354
1546
|
return text.split("\n").join("\n" + pad);
|
|
1355
1547
|
}
|
|
1548
|
+
function parsePath(path) {
|
|
1549
|
+
const segments = [];
|
|
1550
|
+
for (const part of path.split(".")) if (part.endsWith("[]")) segments.push({
|
|
1551
|
+
key: part.slice(0, -2),
|
|
1552
|
+
isArray: true
|
|
1553
|
+
});
|
|
1554
|
+
else segments.push({
|
|
1555
|
+
key: part,
|
|
1556
|
+
isArray: false
|
|
1557
|
+
});
|
|
1558
|
+
return segments;
|
|
1559
|
+
}
|
|
1560
|
+
function extractDeep(obj, segments) {
|
|
1561
|
+
if (segments.length === 0 || obj == null || typeof obj !== "object") return obj;
|
|
1562
|
+
const [first, ...rest] = segments;
|
|
1563
|
+
if (!first) return obj;
|
|
1564
|
+
const value = obj[first.key];
|
|
1565
|
+
if (first.isArray) {
|
|
1566
|
+
if (!Array.isArray(value)) return void 0;
|
|
1567
|
+
if (rest.length === 0) return value;
|
|
1568
|
+
return value.map((item) => extractDeep(item, rest)).filter((v) => v !== void 0);
|
|
1569
|
+
}
|
|
1570
|
+
if (rest.length === 0) return value;
|
|
1571
|
+
return extractDeep(value, rest);
|
|
1572
|
+
}
|
|
1573
|
+
function extract(obj, path) {
|
|
1574
|
+
return extractDeep(obj, parsePath(path));
|
|
1575
|
+
}
|
|
1576
|
+
function setNested(obj, keys, value) {
|
|
1577
|
+
let current = obj;
|
|
1578
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
1579
|
+
const key = keys[i];
|
|
1580
|
+
if (!(key in current) || typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
1581
|
+
current = current[key];
|
|
1582
|
+
}
|
|
1583
|
+
current[keys[keys.length - 1]] = value;
|
|
1584
|
+
}
|
|
1585
|
+
function extractFromObject(data, fields) {
|
|
1586
|
+
const result = {};
|
|
1587
|
+
for (const field of fields) {
|
|
1588
|
+
const value = extract(data, field);
|
|
1589
|
+
if (value !== void 0) setNested(result, field.replace(/\[\]/g, "").split("."), value);
|
|
1590
|
+
}
|
|
1591
|
+
return Object.keys(result).length > 0 ? result : null;
|
|
1592
|
+
}
|
|
1593
|
+
function isJsonString(str) {
|
|
1594
|
+
const trimmed = str.trim();
|
|
1595
|
+
if (!(trimmed.startsWith("{") || trimmed.startsWith("["))) return false;
|
|
1596
|
+
try {
|
|
1597
|
+
JSON.parse(trimmed);
|
|
1598
|
+
return true;
|
|
1599
|
+
} catch {
|
|
1600
|
+
return false;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function filterFields(data, fields) {
|
|
1604
|
+
if (data.structuredContent && typeof data.structuredContent === "object") {
|
|
1605
|
+
const filtered = extractFromObject(data.structuredContent, fields);
|
|
1606
|
+
if (filtered) return {
|
|
1607
|
+
...data,
|
|
1608
|
+
structuredContent: filtered
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const content = data.content;
|
|
1612
|
+
if (Array.isArray(content)) {
|
|
1613
|
+
const newContent = content.map((block) => {
|
|
1614
|
+
if (block.type !== "text" || !block.text || !isJsonString(block.text)) return block;
|
|
1615
|
+
try {
|
|
1616
|
+
const filtered = extractFromObject(JSON.parse(block.text), fields);
|
|
1617
|
+
if (filtered) return {
|
|
1618
|
+
...block,
|
|
1619
|
+
text: JSON.stringify(filtered)
|
|
1620
|
+
};
|
|
1621
|
+
} catch {}
|
|
1622
|
+
return block;
|
|
1623
|
+
});
|
|
1624
|
+
if (newContent.some((block, i) => block !== content[i])) return {
|
|
1625
|
+
...data,
|
|
1626
|
+
content: newContent
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
return data;
|
|
1630
|
+
}
|
|
1356
1631
|
function createDaemonServer(serverPool, config) {
|
|
1357
1632
|
const socketPath = getSocketPath();
|
|
1358
1633
|
let idleTimer;
|
|
@@ -1390,53 +1665,118 @@ function createDaemonServer(serverPool, config) {
|
|
|
1390
1665
|
}
|
|
1391
1666
|
case "tools/call": {
|
|
1392
1667
|
const p = params;
|
|
1393
|
-
if (!p?.name)
|
|
1668
|
+
if (!p?.name) {
|
|
1669
|
+
const err = missingParameterError("name");
|
|
1670
|
+
return {
|
|
1671
|
+
jsonrpc: "2.0",
|
|
1672
|
+
id,
|
|
1673
|
+
error: {
|
|
1674
|
+
code: -32602,
|
|
1675
|
+
message: err.message,
|
|
1676
|
+
data: toErrorData(err)
|
|
1677
|
+
}
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
const found = serverPool.findToolOrError(p.name);
|
|
1681
|
+
if (!found.ok) return {
|
|
1394
1682
|
jsonrpc: "2.0",
|
|
1395
1683
|
id,
|
|
1396
1684
|
error: {
|
|
1397
1685
|
code: -32602,
|
|
1398
|
-
message:
|
|
1686
|
+
message: found.error.message,
|
|
1687
|
+
data: toErrorData(found.error)
|
|
1399
1688
|
}
|
|
1400
1689
|
};
|
|
1401
|
-
const
|
|
1402
|
-
if (!
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1690
|
+
const validation = serverPool.validateToolArgs(p.name, p.arguments ?? {});
|
|
1691
|
+
if (!validation.valid && !validation.unsupported) {
|
|
1692
|
+
const err = invalidArgumentsError(p.name, validation.errors);
|
|
1693
|
+
return {
|
|
1694
|
+
jsonrpc: "2.0",
|
|
1695
|
+
id,
|
|
1696
|
+
error: {
|
|
1697
|
+
code: -32602,
|
|
1698
|
+
message: err.message,
|
|
1699
|
+
data: toErrorData(err)
|
|
1700
|
+
}
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
const timeout = clientTimeout ?? p.timeout ?? found.serverTimeout ?? requestTimeout;
|
|
1704
|
+
try {
|
|
1705
|
+
const callResult = await found.manager.callTool(found.tool.name, p.arguments ?? {}, timeout);
|
|
1706
|
+
if (p.fields && p.fields.length > 0) return {
|
|
1707
|
+
jsonrpc: "2.0",
|
|
1708
|
+
id,
|
|
1709
|
+
result: filterFields(callResult, p.fields)
|
|
1710
|
+
};
|
|
1711
|
+
return {
|
|
1712
|
+
jsonrpc: "2.0",
|
|
1713
|
+
id,
|
|
1714
|
+
result: callResult
|
|
1715
|
+
};
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
if (isTimeoutError(err)) {
|
|
1718
|
+
const te = timeoutError(p.name, timeout);
|
|
1719
|
+
return {
|
|
1720
|
+
jsonrpc: "2.0",
|
|
1721
|
+
id,
|
|
1722
|
+
error: {
|
|
1723
|
+
code: -32001,
|
|
1724
|
+
message: te.message,
|
|
1725
|
+
data: toErrorData(te)
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1408
1728
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
return {
|
|
1412
|
-
jsonrpc: "2.0",
|
|
1413
|
-
id,
|
|
1414
|
-
result: await found.manager.callTool(found.tool.name, p.arguments ?? {}, timeout)
|
|
1415
|
-
};
|
|
1729
|
+
throw err;
|
|
1730
|
+
}
|
|
1416
1731
|
}
|
|
1417
1732
|
case "tools/info": {
|
|
1418
1733
|
const p = params;
|
|
1419
|
-
if (!p?.name)
|
|
1734
|
+
if (!p?.name) {
|
|
1735
|
+
const err = missingParameterError("name");
|
|
1736
|
+
return {
|
|
1737
|
+
jsonrpc: "2.0",
|
|
1738
|
+
id,
|
|
1739
|
+
error: {
|
|
1740
|
+
code: -32602,
|
|
1741
|
+
message: err.message,
|
|
1742
|
+
data: toErrorData(err)
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
const found = serverPool.findToolOrError(p.name);
|
|
1747
|
+
if (!found.ok) return {
|
|
1420
1748
|
jsonrpc: "2.0",
|
|
1421
1749
|
id,
|
|
1422
1750
|
error: {
|
|
1423
1751
|
code: -32602,
|
|
1424
|
-
message:
|
|
1752
|
+
message: found.error.message,
|
|
1753
|
+
data: toErrorData(found.error)
|
|
1425
1754
|
}
|
|
1426
1755
|
};
|
|
1427
|
-
|
|
1428
|
-
if (!found) return {
|
|
1756
|
+
return {
|
|
1429
1757
|
jsonrpc: "2.0",
|
|
1430
1758
|
id,
|
|
1431
|
-
|
|
1432
|
-
code: -32602,
|
|
1433
|
-
message: `Tool not found: ${p.name}`
|
|
1434
|
-
}
|
|
1759
|
+
result: found.tool
|
|
1435
1760
|
};
|
|
1761
|
+
}
|
|
1762
|
+
case "tools/validate": {
|
|
1763
|
+
const p = params;
|
|
1764
|
+
if (!p?.name) {
|
|
1765
|
+
const err = missingParameterError("name");
|
|
1766
|
+
return {
|
|
1767
|
+
jsonrpc: "2.0",
|
|
1768
|
+
id,
|
|
1769
|
+
error: {
|
|
1770
|
+
code: -32602,
|
|
1771
|
+
message: err.message,
|
|
1772
|
+
data: toErrorData(err)
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1436
1776
|
return {
|
|
1437
1777
|
jsonrpc: "2.0",
|
|
1438
1778
|
id,
|
|
1439
|
-
result:
|
|
1779
|
+
result: serverPool.validateToolArgs(p.name, p.arguments ?? {})
|
|
1440
1780
|
};
|
|
1441
1781
|
}
|
|
1442
1782
|
case "auth/status": {
|
|
@@ -1632,33 +1972,68 @@ function createDaemonServer(serverPool, config) {
|
|
|
1632
1972
|
}
|
|
1633
1973
|
case "tools/call-async": {
|
|
1634
1974
|
const p = params;
|
|
1635
|
-
if (!p?.name)
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1975
|
+
if (!p?.name) {
|
|
1976
|
+
const err = missingParameterError("name");
|
|
1977
|
+
return {
|
|
1978
|
+
jsonrpc: "2.0",
|
|
1979
|
+
id,
|
|
1980
|
+
error: {
|
|
1981
|
+
code: -32602,
|
|
1982
|
+
message: err.message,
|
|
1983
|
+
data: toErrorData(err)
|
|
1984
|
+
}
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
const foundAsync = serverPool.findToolOrError(p.name);
|
|
1988
|
+
if (!foundAsync.ok) return {
|
|
1645
1989
|
jsonrpc: "2.0",
|
|
1646
1990
|
id,
|
|
1647
1991
|
error: {
|
|
1648
1992
|
code: -32602,
|
|
1649
|
-
message:
|
|
1993
|
+
message: foundAsync.error.message,
|
|
1994
|
+
data: toErrorData(foundAsync.error)
|
|
1650
1995
|
}
|
|
1651
1996
|
};
|
|
1652
|
-
const
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1997
|
+
const asyncValidation = serverPool.validateToolArgs(p.name, p.arguments ?? {});
|
|
1998
|
+
if (!asyncValidation.valid && !asyncValidation.unsupported) {
|
|
1999
|
+
const err = invalidArgumentsError(p.name, asyncValidation.errors);
|
|
2000
|
+
return {
|
|
2001
|
+
jsonrpc: "2.0",
|
|
2002
|
+
id,
|
|
2003
|
+
error: {
|
|
2004
|
+
code: -32602,
|
|
2005
|
+
message: err.message,
|
|
2006
|
+
data: toErrorData(err)
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
try {
|
|
2011
|
+
const taskHandle = await foundAsync.manager.callToolWithTask(foundAsync.tool.name, p.arguments ?? {});
|
|
2012
|
+
serverPool.trackTask(taskHandle.taskId, foundAsync.manager.name);
|
|
2013
|
+
return {
|
|
2014
|
+
jsonrpc: "2.0",
|
|
2015
|
+
id,
|
|
2016
|
+
result: {
|
|
2017
|
+
...taskHandle,
|
|
2018
|
+
server: foundAsync.manager.name
|
|
2019
|
+
}
|
|
2020
|
+
};
|
|
2021
|
+
} catch (err) {
|
|
2022
|
+
if (isTimeoutError(err)) {
|
|
2023
|
+
const asyncTimeout = foundAsync.serverTimeout ?? requestTimeout;
|
|
2024
|
+
const te = timeoutError(p.name, asyncTimeout);
|
|
2025
|
+
return {
|
|
2026
|
+
jsonrpc: "2.0",
|
|
2027
|
+
id,
|
|
2028
|
+
error: {
|
|
2029
|
+
code: -32001,
|
|
2030
|
+
message: te.message,
|
|
2031
|
+
data: toErrorData(te)
|
|
2032
|
+
}
|
|
2033
|
+
};
|
|
1660
2034
|
}
|
|
1661
|
-
|
|
2035
|
+
throw err;
|
|
2036
|
+
}
|
|
1662
2037
|
}
|
|
1663
2038
|
case "config/reload": {
|
|
1664
2039
|
const newConfig = loadConfig(params?.configPath);
|
|
@@ -1874,8 +2249,8 @@ function createHttpListener(handleRequest, config) {
|
|
|
1874
2249
|
async function runAutoTypegen(serverPool, logger) {
|
|
1875
2250
|
try {
|
|
1876
2251
|
const require = createRequire(path.resolve("package.json"));
|
|
1877
|
-
const
|
|
1878
|
-
const outputPath = path.join(
|
|
2252
|
+
const muxedPkgDir = path.dirname(require.resolve("muxed/package.json"));
|
|
2253
|
+
const outputPath = path.join(muxedPkgDir, "muxed.generated.d.ts");
|
|
1879
2254
|
const tools = serverPool.listAllTools();
|
|
1880
2255
|
if (tools.length === 0) {
|
|
1881
2256
|
logger.debug("No tools available, skipping typegen");
|
|
@@ -1885,12 +2260,12 @@ async function runAutoTypegen(serverPool, logger) {
|
|
|
1885
2260
|
fs.writeFileSync(outputPath, content, "utf-8");
|
|
1886
2261
|
logger.info(`Auto-generated ${tools.length} tool types → ${outputPath}`);
|
|
1887
2262
|
} catch {
|
|
1888
|
-
logger.debug("Skipping auto-typegen (
|
|
2263
|
+
logger.debug("Skipping auto-typegen (muxed not resolvable as a dependency)");
|
|
1889
2264
|
}
|
|
1890
2265
|
}
|
|
1891
2266
|
async function startDaemon(configPath) {
|
|
1892
2267
|
const config = loadConfig(configPath);
|
|
1893
|
-
|
|
2268
|
+
ensureMuxedDir();
|
|
1894
2269
|
const isForeground = !!process.send;
|
|
1895
2270
|
const logger = initLogger({
|
|
1896
2271
|
level: config.daemon?.logLevel ?? "info",
|
|
@@ -1946,7 +2321,7 @@ async function startDaemon(configPath) {
|
|
|
1946
2321
|
});
|
|
1947
2322
|
}
|
|
1948
2323
|
function getLockPath() {
|
|
1949
|
-
return path.join(
|
|
2324
|
+
return path.join(getMuxedDir(), "muxed.lock");
|
|
1950
2325
|
}
|
|
1951
2326
|
function getDaemonPid() {
|
|
1952
2327
|
try {
|
|
@@ -1965,10 +2340,10 @@ function isProcessAlive(pid) {
|
|
|
1965
2340
|
return false;
|
|
1966
2341
|
}
|
|
1967
2342
|
}
|
|
1968
|
-
function
|
|
2343
|
+
function isMuxedProcess(pid) {
|
|
1969
2344
|
try {
|
|
1970
2345
|
const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
1971
|
-
return cmdline.includes("
|
|
2346
|
+
return cmdline.includes("muxed") || cmdline.includes("node");
|
|
1972
2347
|
} catch {
|
|
1973
2348
|
return isProcessAlive(pid);
|
|
1974
2349
|
}
|
|
@@ -1994,13 +2369,13 @@ function tryConnectSocket(socketPath) {
|
|
|
1994
2369
|
function acquireLock() {
|
|
1995
2370
|
const lockPath = getLockPath();
|
|
1996
2371
|
try {
|
|
1997
|
-
|
|
2372
|
+
ensureMuxedDir();
|
|
1998
2373
|
fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
1999
2374
|
return true;
|
|
2000
2375
|
} catch (err) {
|
|
2001
2376
|
if (err.code === "EEXIST") try {
|
|
2002
2377
|
const lockPid = parseInt(fs.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
2003
|
-
if (!Number.isFinite(lockPid) || !isProcessAlive(lockPid) || !
|
|
2378
|
+
if (!Number.isFinite(lockPid) || !isProcessAlive(lockPid) || !isMuxedProcess(lockPid)) {
|
|
2004
2379
|
fs.unlinkSync(lockPath);
|
|
2005
2380
|
try {
|
|
2006
2381
|
fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
@@ -2026,7 +2401,7 @@ async function isDaemonRunning() {
|
|
|
2026
2401
|
const pid = getDaemonPid();
|
|
2027
2402
|
if (pid === null) return false;
|
|
2028
2403
|
if (!isProcessAlive(pid)) return false;
|
|
2029
|
-
if (!
|
|
2404
|
+
if (!isMuxedProcess(pid)) return false;
|
|
2030
2405
|
return tryConnectSocket(getSocketPath());
|
|
2031
2406
|
}
|
|
2032
2407
|
async function cleanupStaleFiles() {
|
|
@@ -2043,7 +2418,7 @@ async function cleanupStaleFiles() {
|
|
|
2043
2418
|
releaseLock();
|
|
2044
2419
|
return;
|
|
2045
2420
|
}
|
|
2046
|
-
if (pid !== null && isProcessAlive(pid) && !
|
|
2421
|
+
if (pid !== null && isProcessAlive(pid) && !isMuxedProcess(pid)) {
|
|
2047
2422
|
try {
|
|
2048
2423
|
fs.unlinkSync(pidPath);
|
|
2049
2424
|
} catch {}
|
|
@@ -2108,12 +2483,12 @@ async function daemonize(configPath) {
|
|
|
2108
2483
|
releaseLock();
|
|
2109
2484
|
}
|
|
2110
2485
|
}
|
|
2111
|
-
var
|
|
2486
|
+
var MuxedError = class extends Error {
|
|
2112
2487
|
code;
|
|
2113
2488
|
data;
|
|
2114
2489
|
constructor(code, message, data) {
|
|
2115
2490
|
super(message);
|
|
2116
|
-
this.name = "
|
|
2491
|
+
this.name = "MuxedError";
|
|
2117
2492
|
this.code = code;
|
|
2118
2493
|
this.data = data;
|
|
2119
2494
|
}
|
|
@@ -2156,7 +2531,7 @@ async function sendRequest(method, params) {
|
|
|
2156
2531
|
const socket = net.createConnection(socketPath);
|
|
2157
2532
|
let buffer = "";
|
|
2158
2533
|
socket.on("error", (err) => {
|
|
2159
|
-
if (err.code === "ENOENT") reject(/* @__PURE__ */ new Error("Daemon is not running. Run `
|
|
2534
|
+
if (err.code === "ENOENT") reject(/* @__PURE__ */ new Error("Daemon is not running. Run `muxed status` to check."));
|
|
2160
2535
|
else if (err.code === "ECONNREFUSED") reject(/* @__PURE__ */ new Error("Daemon may have crashed. Try running a command to auto-restart it."));
|
|
2161
2536
|
else reject(err);
|
|
2162
2537
|
});
|
|
@@ -2177,7 +2552,7 @@ async function sendRequest(method, params) {
|
|
|
2177
2552
|
socket.destroy();
|
|
2178
2553
|
try {
|
|
2179
2554
|
const response = JSON.parse(line);
|
|
2180
|
-
if (response.error) reject(new
|
|
2555
|
+
if (response.error) reject(new MuxedError(response.error.code, response.error.message, response.error.data));
|
|
2181
2556
|
else resolve(response.result);
|
|
2182
2557
|
} catch {
|
|
2183
2558
|
reject(/* @__PURE__ */ new Error("Invalid response from daemon"));
|
|
@@ -2431,7 +2806,7 @@ function formatInit(result) {
|
|
|
2431
2806
|
lines.push(formatTable(discHeaders, discRows));
|
|
2432
2807
|
if (result.imported.length > 0) {
|
|
2433
2808
|
lines.push("");
|
|
2434
|
-
lines.push(`Imported ${result.imported.length} server(s) into ${result.
|
|
2809
|
+
lines.push(`Imported ${result.imported.length} server(s) into ${result.muxedConfigPath}:`);
|
|
2435
2810
|
lines.push(` ${result.imported.join(", ")}`);
|
|
2436
2811
|
}
|
|
2437
2812
|
if (result.skipped.length > 0) {
|
|
@@ -2460,7 +2835,7 @@ function formatInit(result) {
|
|
|
2460
2835
|
}
|
|
2461
2836
|
if (result.imported.length === 0 && result.skipped.length > 0) {
|
|
2462
2837
|
lines.push("");
|
|
2463
|
-
lines.push("All discovered servers already exist in
|
|
2838
|
+
lines.push("All discovered servers already exist in muxed config. Nothing to do.");
|
|
2464
2839
|
}
|
|
2465
2840
|
return lines.join("\n");
|
|
2466
2841
|
}
|
|
@@ -2529,6 +2904,37 @@ function formatMcpServerList(servers) {
|
|
|
2529
2904
|
];
|
|
2530
2905
|
}));
|
|
2531
2906
|
}
|
|
2907
|
+
function formatStructuredError(error) {
|
|
2908
|
+
const lines = [];
|
|
2909
|
+
lines.push(`Error: ${error.message}`);
|
|
2910
|
+
if (error.data?.suggestion) lines.push(`Suggestion: ${error.data.suggestion}`);
|
|
2911
|
+
if (error.data?.context?.similarTools) {
|
|
2912
|
+
const similar = error.data.context.similarTools;
|
|
2913
|
+
if (similar.length > 0) lines.push(`Similar tools: ${similar.join(", ")}`);
|
|
2914
|
+
}
|
|
2915
|
+
if (error.data?.context?.availableServers) {
|
|
2916
|
+
const servers = error.data.context.availableServers;
|
|
2917
|
+
if (servers.length > 0) lines.push(`Available servers: ${servers.join(", ")}`);
|
|
2918
|
+
}
|
|
2919
|
+
return lines.join("\n");
|
|
2920
|
+
}
|
|
2921
|
+
function formatValidation(result) {
|
|
2922
|
+
const lines = [];
|
|
2923
|
+
if (result.unsupported) {
|
|
2924
|
+
lines.push("Validation: unsupported (tool schema uses features not supported by dry-run validation)");
|
|
2925
|
+
lines.push("The call will be forwarded to the MCP server without pre-validation.");
|
|
2926
|
+
} else if (result.valid) lines.push("Validation: passed");
|
|
2927
|
+
else {
|
|
2928
|
+
lines.push("Validation: failed");
|
|
2929
|
+
for (const err of result.errors) lines.push(` - ${err}`);
|
|
2930
|
+
}
|
|
2931
|
+
if (result.warnings.length > 0) {
|
|
2932
|
+
lines.push("");
|
|
2933
|
+
lines.push("Warnings:");
|
|
2934
|
+
for (const warn of result.warnings) lines.push(` - ${warn}`);
|
|
2935
|
+
}
|
|
2936
|
+
return lines.join("\n");
|
|
2937
|
+
}
|
|
2532
2938
|
function formatJson(data) {
|
|
2533
2939
|
return JSON.stringify(data, null, 2);
|
|
2534
2940
|
}
|
|
@@ -2546,10 +2952,67 @@ const serversCommand = new Command("servers").description("List connected MCP se
|
|
|
2546
2952
|
const result = await sendRequest("servers/list");
|
|
2547
2953
|
console.log(opts.json ? formatJson(result) : formatServers(result));
|
|
2548
2954
|
});
|
|
2955
|
+
const TELEMETRY_FILE = path.join(os.homedir(), ".muxed", "telemetry");
|
|
2956
|
+
const sessionId = crypto.randomUUID();
|
|
2957
|
+
function isTelemetryEnabled() {
|
|
2958
|
+
if (process.env.DO_NOT_TRACK === "1") return false;
|
|
2959
|
+
if (process.env.MUXED_TELEMETRY === "0") return false;
|
|
2960
|
+
try {
|
|
2961
|
+
if (fs.existsSync(TELEMETRY_FILE)) return fs.readFileSync(TELEMETRY_FILE, "utf-8").trim() !== "off";
|
|
2962
|
+
} catch {}
|
|
2963
|
+
return true;
|
|
2964
|
+
}
|
|
2965
|
+
function setTelemetryEnabled(enabled) {
|
|
2966
|
+
try {
|
|
2967
|
+
const dir = path.dirname(TELEMETRY_FILE);
|
|
2968
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
2969
|
+
fs.writeFileSync(TELEMETRY_FILE, enabled ? "on" : "off", "utf-8");
|
|
2970
|
+
} catch {}
|
|
2971
|
+
}
|
|
2972
|
+
function getTelemetryStatus() {
|
|
2973
|
+
return isTelemetryEnabled() ? "on" : "off";
|
|
2974
|
+
}
|
|
2975
|
+
let _client = null;
|
|
2976
|
+
function getClient() {
|
|
2977
|
+
if (!isTelemetryEnabled()) return null;
|
|
2978
|
+
if (_client) return _client;
|
|
2979
|
+
const token = process.env.POSTHOG_PROJECT_TOKEN;
|
|
2980
|
+
const host = process.env.POSTHOG_HOST;
|
|
2981
|
+
if (!token || !host) return null;
|
|
2982
|
+
try {
|
|
2983
|
+
_client = new PostHog(token, {
|
|
2984
|
+
host,
|
|
2985
|
+
flushAt: 1
|
|
2986
|
+
});
|
|
2987
|
+
return _client;
|
|
2988
|
+
} catch {
|
|
2989
|
+
return null;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
function capture(event, properties) {
|
|
2993
|
+
try {
|
|
2994
|
+
const client = getClient();
|
|
2995
|
+
if (!client) return;
|
|
2996
|
+
client.capture({
|
|
2997
|
+
distinctId: sessionId,
|
|
2998
|
+
event,
|
|
2999
|
+
properties: properties ?? {}
|
|
3000
|
+
});
|
|
3001
|
+
} catch {}
|
|
3002
|
+
}
|
|
3003
|
+
async function shutdown() {
|
|
3004
|
+
try {
|
|
3005
|
+
if (_client) await _client.shutdown();
|
|
3006
|
+
} catch {}
|
|
3007
|
+
}
|
|
2549
3008
|
const toolsCommand = new Command("tools").description("List all available tools, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
|
|
2550
3009
|
const configPath = toolsCommand.parent?.opts().config;
|
|
2551
3010
|
await ensureDaemon(configPath);
|
|
2552
3011
|
const result = await sendRequest("tools/list", server ? { server } : void 0);
|
|
3012
|
+
capture("tools_listed", {
|
|
3013
|
+
filtered_by_server: !!server,
|
|
3014
|
+
tool_count: result.length
|
|
3015
|
+
});
|
|
2553
3016
|
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
2554
3017
|
});
|
|
2555
3018
|
const infoCommand = new Command("info").description("Show input schema and description for a specific tool").argument("<server/tool>", "Tool identifier (e.g. myserver/mytool)").option("--json", "Output as JSON").action(async (serverTool, opts) => {
|
|
@@ -2574,7 +3037,7 @@ function readStdin() {
|
|
|
2574
3037
|
process.stdin.on("error", reject);
|
|
2575
3038
|
});
|
|
2576
3039
|
}
|
|
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) => {
|
|
3040
|
+
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
3041
|
const configPath = callCommand.parent?.opts().config;
|
|
2579
3042
|
await ensureDaemon(configPath);
|
|
2580
3043
|
let parsedArgs = {};
|
|
@@ -2591,27 +3054,133 @@ const callCommand = new Command("call").description("Execute a tool with JSON ar
|
|
|
2591
3054
|
console.error("Invalid JSON arguments");
|
|
2592
3055
|
process.exit(1);
|
|
2593
3056
|
}
|
|
3057
|
+
const [server, tool] = serverTool.split("/");
|
|
3058
|
+
if (opts.dryRun) {
|
|
3059
|
+
try {
|
|
3060
|
+
const result = await sendRequest("tools/validate", {
|
|
3061
|
+
name: serverTool,
|
|
3062
|
+
arguments: parsedArgs
|
|
3063
|
+
});
|
|
3064
|
+
capture("tool_called", {
|
|
3065
|
+
server,
|
|
3066
|
+
tool,
|
|
3067
|
+
mode: "dry-run",
|
|
3068
|
+
status: result.valid ? "success" : "validation_error"
|
|
3069
|
+
});
|
|
3070
|
+
if (opts.json) console.log(formatJson(result));
|
|
3071
|
+
else console.log(formatValidation(result));
|
|
3072
|
+
if (!result.valid) process.exit(1);
|
|
3073
|
+
} catch (err) {
|
|
3074
|
+
capture("tool_called", {
|
|
3075
|
+
server,
|
|
3076
|
+
tool,
|
|
3077
|
+
mode: "dry-run",
|
|
3078
|
+
status: "error"
|
|
3079
|
+
});
|
|
3080
|
+
if (err instanceof MuxedError && err.data) {
|
|
3081
|
+
const errorData = err.data;
|
|
3082
|
+
if (opts.json) console.log(formatJson({
|
|
3083
|
+
code: err.code,
|
|
3084
|
+
message: err.message,
|
|
3085
|
+
data: err.data
|
|
3086
|
+
}));
|
|
3087
|
+
else console.error(formatStructuredError({
|
|
3088
|
+
code: err.code,
|
|
3089
|
+
message: err.message,
|
|
3090
|
+
data: errorData
|
|
3091
|
+
}));
|
|
3092
|
+
} else console.error(err instanceof Error ? err.message : "Validation failed");
|
|
3093
|
+
process.exit(1);
|
|
3094
|
+
}
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
2594
3097
|
if (opts.async) {
|
|
2595
|
-
|
|
3098
|
+
try {
|
|
3099
|
+
const taskResult = await sendRequest("tools/call-async", {
|
|
3100
|
+
name: serverTool,
|
|
3101
|
+
arguments: parsedArgs
|
|
3102
|
+
});
|
|
3103
|
+
capture("tool_called", {
|
|
3104
|
+
server,
|
|
3105
|
+
tool,
|
|
3106
|
+
mode: "async",
|
|
3107
|
+
status: "success"
|
|
3108
|
+
});
|
|
3109
|
+
if (opts.json) console.log(formatJson(taskResult));
|
|
3110
|
+
else console.log(`Task created: ${taskResult.taskId} (status: ${taskResult.status})`);
|
|
3111
|
+
} catch (err) {
|
|
3112
|
+
capture("tool_called", {
|
|
3113
|
+
server,
|
|
3114
|
+
tool,
|
|
3115
|
+
mode: "async",
|
|
3116
|
+
status: "error"
|
|
3117
|
+
});
|
|
3118
|
+
if (err instanceof MuxedError && err.data) {
|
|
3119
|
+
const errorData = err.data;
|
|
3120
|
+
if (opts.json) console.log(formatJson({
|
|
3121
|
+
code: err.code,
|
|
3122
|
+
message: err.message,
|
|
3123
|
+
data: err.data
|
|
3124
|
+
}));
|
|
3125
|
+
else console.error(formatStructuredError({
|
|
3126
|
+
code: err.code,
|
|
3127
|
+
message: err.message,
|
|
3128
|
+
data: errorData
|
|
3129
|
+
}));
|
|
3130
|
+
} else console.error(err instanceof Error ? err.message : "Call failed");
|
|
3131
|
+
process.exit(1);
|
|
3132
|
+
}
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
3135
|
+
try {
|
|
3136
|
+
const callParams = {
|
|
2596
3137
|
name: serverTool,
|
|
2597
3138
|
arguments: parsedArgs
|
|
3139
|
+
};
|
|
3140
|
+
if (opts.timeout) callParams.timeout = parseInt(opts.timeout, 10);
|
|
3141
|
+
if (opts.fields) callParams.fields = opts.fields.split(",").map((f) => f.trim());
|
|
3142
|
+
const result = await sendRequest("tools/call", callParams);
|
|
3143
|
+
capture("tool_called", {
|
|
3144
|
+
server,
|
|
3145
|
+
tool,
|
|
3146
|
+
mode: "sync",
|
|
3147
|
+
status: result.isError ? "tool_error" : "success",
|
|
3148
|
+
has_timeout: !!opts.timeout,
|
|
3149
|
+
has_fields: !!opts.fields,
|
|
3150
|
+
stdin_input: jsonArgs === "-"
|
|
2598
3151
|
});
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
3152
|
+
console.log(opts.json ? formatJson(result) : formatCallResult(result));
|
|
3153
|
+
} catch (err) {
|
|
3154
|
+
capture("tool_called", {
|
|
3155
|
+
server,
|
|
3156
|
+
tool,
|
|
3157
|
+
mode: "sync",
|
|
3158
|
+
status: "error",
|
|
3159
|
+
has_timeout: !!opts.timeout,
|
|
3160
|
+
has_fields: !!opts.fields,
|
|
3161
|
+
stdin_input: jsonArgs === "-"
|
|
3162
|
+
});
|
|
3163
|
+
if (err instanceof MuxedError && err.data) {
|
|
3164
|
+
const errorData = err.data;
|
|
3165
|
+
if (opts.json) console.log(formatJson({
|
|
3166
|
+
code: err.code,
|
|
3167
|
+
message: err.message,
|
|
3168
|
+
data: err.data
|
|
3169
|
+
}));
|
|
3170
|
+
else console.error(formatStructuredError({
|
|
3171
|
+
code: err.code,
|
|
3172
|
+
message: err.message,
|
|
3173
|
+
data: errorData
|
|
3174
|
+
}));
|
|
3175
|
+
} else console.error(err instanceof Error ? err.message : "Call failed");
|
|
3176
|
+
process.exit(1);
|
|
2602
3177
|
}
|
|
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
3178
|
});
|
|
2611
3179
|
const grepCommand = new Command("grep").description("Search tools by regex pattern across names, titles, and descriptions").argument("<pattern>", "Regex pattern to search").option("--json", "Output as JSON").action(async (pattern, opts) => {
|
|
2612
3180
|
const configPath = grepCommand.parent?.opts().config;
|
|
2613
3181
|
await ensureDaemon(configPath);
|
|
2614
3182
|
const result = await sendRequest("tools/grep", { pattern });
|
|
3183
|
+
capture("tools_searched", { result_count: result.length });
|
|
2615
3184
|
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
2616
3185
|
});
|
|
2617
3186
|
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) => {
|
|
@@ -2637,7 +3206,7 @@ const readCommand = new Command("read").description("Fetch and display the conte
|
|
|
2637
3206
|
function getExplicitConfig$1(cmd) {
|
|
2638
3207
|
return cmd.parent?.parent?.opts().config;
|
|
2639
3208
|
}
|
|
2640
|
-
const daemonCommand = new Command("daemon").description("Start, stop, reload, or check status of the
|
|
3209
|
+
const daemonCommand = new Command("daemon").description("Start, stop, reload, or check status of the muxed background daemon").enablePositionalOptions();
|
|
2641
3210
|
daemonCommand.command("start").description("Start the daemon process in the background").option("--json", "Output as JSON").action(async (opts) => {
|
|
2642
3211
|
const configPath = getExplicitConfig$1(daemonCommand);
|
|
2643
3212
|
if (await isDaemonRunning()) {
|
|
@@ -2824,49 +3393,57 @@ function getAgentDefs() {
|
|
|
2824
3393
|
name: "claude-code",
|
|
2825
3394
|
scope: "local",
|
|
2826
3395
|
configPath: () => path.join(cwd, ".mcp.json"),
|
|
2827
|
-
serversKey: "mcpServers"
|
|
3396
|
+
serversKey: "mcpServers",
|
|
3397
|
+
codingAgent: true
|
|
2828
3398
|
},
|
|
2829
3399
|
{
|
|
2830
3400
|
name: "cursor",
|
|
2831
3401
|
scope: "local",
|
|
2832
3402
|
configPath: () => path.join(cwd, ".cursor", "mcp.json"),
|
|
2833
|
-
serversKey: "mcpServers"
|
|
3403
|
+
serversKey: "mcpServers",
|
|
3404
|
+
codingAgent: true
|
|
2834
3405
|
},
|
|
2835
3406
|
{
|
|
2836
3407
|
name: "vscode",
|
|
2837
3408
|
scope: "local",
|
|
2838
3409
|
configPath: () => path.join(cwd, ".vscode", "mcp.json"),
|
|
2839
|
-
serversKey: "servers"
|
|
3410
|
+
serversKey: "servers",
|
|
3411
|
+
codingAgent: true
|
|
2840
3412
|
},
|
|
2841
3413
|
{
|
|
2842
3414
|
name: "roo-code",
|
|
2843
3415
|
scope: "local",
|
|
2844
3416
|
configPath: () => path.join(cwd, ".roo", "mcp.json"),
|
|
2845
|
-
serversKey: "mcpServers"
|
|
3417
|
+
serversKey: "mcpServers",
|
|
3418
|
+
codingAgent: true
|
|
2846
3419
|
},
|
|
2847
3420
|
{
|
|
2848
3421
|
name: "amazon-q",
|
|
2849
3422
|
scope: "local",
|
|
2850
3423
|
configPath: () => path.join(cwd, ".amazonq", "mcp.json"),
|
|
2851
|
-
serversKey: "mcpServers"
|
|
3424
|
+
serversKey: "mcpServers",
|
|
3425
|
+
codingAgent: true
|
|
2852
3426
|
},
|
|
2853
3427
|
{
|
|
2854
3428
|
name: "claude-desktop",
|
|
2855
3429
|
scope: "global",
|
|
2856
3430
|
configPath: () => xdgOrMacPath(["Claude", "claude_desktop_config.json"], ["Claude", "claude_desktop_config.json"]),
|
|
2857
|
-
serversKey: "mcpServers"
|
|
3431
|
+
serversKey: "mcpServers",
|
|
3432
|
+
codingAgent: false
|
|
2858
3433
|
},
|
|
2859
3434
|
{
|
|
2860
3435
|
name: "cursor",
|
|
2861
3436
|
scope: "global",
|
|
2862
3437
|
configPath: () => path.join(home, ".cursor", "mcp.json"),
|
|
2863
|
-
serversKey: "mcpServers"
|
|
3438
|
+
serversKey: "mcpServers",
|
|
3439
|
+
codingAgent: true
|
|
2864
3440
|
},
|
|
2865
3441
|
{
|
|
2866
3442
|
name: "windsurf",
|
|
2867
3443
|
scope: "global",
|
|
2868
3444
|
configPath: () => path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2869
|
-
serversKey: "mcpServers"
|
|
3445
|
+
serversKey: "mcpServers",
|
|
3446
|
+
codingAgent: true
|
|
2870
3447
|
},
|
|
2871
3448
|
{
|
|
2872
3449
|
name: "vscode",
|
|
@@ -2880,7 +3457,8 @@ function getAgentDefs() {
|
|
|
2880
3457
|
"User",
|
|
2881
3458
|
"mcp.json"
|
|
2882
3459
|
]),
|
|
2883
|
-
serversKey: "servers"
|
|
3460
|
+
serversKey: "servers",
|
|
3461
|
+
codingAgent: true
|
|
2884
3462
|
},
|
|
2885
3463
|
{
|
|
2886
3464
|
name: "cline",
|
|
@@ -2900,7 +3478,8 @@ function getAgentDefs() {
|
|
|
2900
3478
|
"settings",
|
|
2901
3479
|
"cline_mcp_settings.json"
|
|
2902
3480
|
]),
|
|
2903
|
-
serversKey: "mcpServers"
|
|
3481
|
+
serversKey: "mcpServers",
|
|
3482
|
+
codingAgent: true
|
|
2904
3483
|
},
|
|
2905
3484
|
{
|
|
2906
3485
|
name: "roo-code",
|
|
@@ -2920,20 +3499,22 @@ function getAgentDefs() {
|
|
|
2920
3499
|
"settings",
|
|
2921
3500
|
"cline_mcp_settings.json"
|
|
2922
3501
|
]),
|
|
2923
|
-
serversKey: "mcpServers"
|
|
3502
|
+
serversKey: "mcpServers",
|
|
3503
|
+
codingAgent: true
|
|
2924
3504
|
},
|
|
2925
3505
|
{
|
|
2926
3506
|
name: "amazon-q",
|
|
2927
3507
|
scope: "global",
|
|
2928
3508
|
configPath: () => path.join(home, ".aws", "amazonq", "mcp.json"),
|
|
2929
|
-
serversKey: "mcpServers"
|
|
3509
|
+
serversKey: "mcpServers",
|
|
3510
|
+
codingAgent: true
|
|
2930
3511
|
}
|
|
2931
3512
|
];
|
|
2932
3513
|
}
|
|
2933
3514
|
function normalizeServer(agent, name, raw, warnings) {
|
|
2934
3515
|
const env = raw.env;
|
|
2935
3516
|
if (env) {
|
|
2936
|
-
for (const [key, value] of Object.entries(env)) if (typeof value === "string" && value.includes("${input:")) warnings.push(`${agent.name} server "${name}": env.${key} references ${value} \u2014 set manually in
|
|
3517
|
+
for (const [key, value] of Object.entries(env)) if (typeof value === "string" && value.includes("${input:")) warnings.push(`${agent.name} server "${name}": env.${key} references ${value} \u2014 set manually in muxed config`);
|
|
2937
3518
|
}
|
|
2938
3519
|
const url = raw.url ?? raw.serverUrl;
|
|
2939
3520
|
const command = raw.command;
|
|
@@ -2978,7 +3559,7 @@ function discoverAgentConfigs() {
|
|
|
2978
3559
|
const servers = {};
|
|
2979
3560
|
for (const [name, raw] of Object.entries(rawServers)) {
|
|
2980
3561
|
if (typeof raw !== "object" || raw === null) continue;
|
|
2981
|
-
if (name === "
|
|
3562
|
+
if (name === "muxed") continue;
|
|
2982
3563
|
const normalized = normalizeServer(agent, name, raw, warnings);
|
|
2983
3564
|
if (normalized) servers[name] = normalized;
|
|
2984
3565
|
}
|
|
@@ -3033,7 +3614,7 @@ function mergeServers(discovered, existingServers) {
|
|
|
3033
3614
|
function detectIndent(text) {
|
|
3034
3615
|
return text.match(/^(\s+)"/m)?.[1] ?? " ";
|
|
3035
3616
|
}
|
|
3036
|
-
function
|
|
3617
|
+
function writeMuxedConfig(configPath, servers) {
|
|
3037
3618
|
let existing = {};
|
|
3038
3619
|
if (fs.existsSync(configPath)) try {
|
|
3039
3620
|
existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
@@ -3043,15 +3624,20 @@ function writeTooldConfig(configPath, servers) {
|
|
|
3043
3624
|
existing.mcpServers = servers;
|
|
3044
3625
|
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
3045
3626
|
}
|
|
3046
|
-
function
|
|
3627
|
+
function getMuxedEntry(agent) {
|
|
3628
|
+
const args = agent.codingAgent ? ["muxed@latest", "mcp"] : [
|
|
3629
|
+
"muxed@latest",
|
|
3630
|
+
"mcp",
|
|
3631
|
+
"--proxy-tools"
|
|
3632
|
+
];
|
|
3047
3633
|
if (agent.serversKey === "servers") return {
|
|
3048
3634
|
type: "stdio",
|
|
3049
3635
|
command: "npx",
|
|
3050
|
-
args
|
|
3636
|
+
args
|
|
3051
3637
|
};
|
|
3052
3638
|
return {
|
|
3053
3639
|
command: "npx",
|
|
3054
|
-
args
|
|
3640
|
+
args
|
|
3055
3641
|
};
|
|
3056
3642
|
}
|
|
3057
3643
|
function modifyAgentConfig(dc, opts) {
|
|
@@ -3059,14 +3645,14 @@ function modifyAgentConfig(dc, opts) {
|
|
|
3059
3645
|
fs.writeFileSync(dc.configPath + ".bak", text);
|
|
3060
3646
|
const indent = detectIndent(text);
|
|
3061
3647
|
const content = { ...dc.rawContent };
|
|
3062
|
-
if (opts.delete) if (opts.replace) content[dc.agent.serversKey] = {
|
|
3648
|
+
if (opts.delete) if (opts.replace) content[dc.agent.serversKey] = { muxed: getMuxedEntry(dc.agent) };
|
|
3063
3649
|
else delete content[dc.agent.serversKey];
|
|
3064
3650
|
fs.writeFileSync(dc.configPath, JSON.stringify(content, null, indent) + "\n");
|
|
3065
3651
|
}
|
|
3066
|
-
function
|
|
3652
|
+
function getMuxedConfigPath(scope, explicitPath) {
|
|
3067
3653
|
if (explicitPath) return explicitPath;
|
|
3068
|
-
if (scope === "local") return path.join(process.cwd(), "
|
|
3069
|
-
return path.join(home, ".
|
|
3654
|
+
if (scope === "local") return path.join(process.cwd(), "muxed.config.json");
|
|
3655
|
+
return path.join(home, ".muxed", "config.json");
|
|
3070
3656
|
}
|
|
3071
3657
|
async function confirm(message, opts) {
|
|
3072
3658
|
const rl = readline.createInterface({
|
|
@@ -3135,7 +3721,7 @@ async function resolveConflicts(unresolvedConflicts, isInteractive) {
|
|
|
3135
3721
|
conflicts
|
|
3136
3722
|
};
|
|
3137
3723
|
}
|
|
3138
|
-
const initCommand = new Command("init").description("Discover and import MCP servers from agent configs (Claude Code, Cursor)").option("--dry-run", "Show what would be done without writing files").option("--json", "Output as JSON").option("-y, --yes", "Skip prompts; resolve conflicts by priority (claude-code > cursor > first)").option("--no-delete", "Keep original server entries in agent configs").option("--no-replace", "Don't add
|
|
3724
|
+
const initCommand = new Command("init").description("Discover and import MCP servers from agent configs (Claude Code, Cursor)").option("--dry-run", "Show what would be done without writing files").option("--json", "Output as JSON").option("-y, --yes", "Skip prompts; resolve conflicts by priority (claude-code > cursor > first)").option("--no-delete", "Keep original server entries in agent configs").option("--no-replace", "Don't add muxed entry to agent configs").action(async (opts) => {
|
|
3139
3725
|
const configPath = initCommand.parent?.opts().config;
|
|
3140
3726
|
const isInteractive = !opts.dryRun && !opts.json && !opts.yes && !!process.stdin.isTTY;
|
|
3141
3727
|
const { discovered, warnings } = discoverAgentConfigs();
|
|
@@ -3144,10 +3730,10 @@ const initCommand = new Command("init").description("Discover and import MCP ser
|
|
|
3144
3730
|
console.log(opts.json ? formatJson({ message: msg }) : msg);
|
|
3145
3731
|
return;
|
|
3146
3732
|
}
|
|
3147
|
-
const
|
|
3733
|
+
const muxedPath = getMuxedConfigPath(discovered.some((d) => d.agent.scope === "local") ? "local" : "global", configPath);
|
|
3148
3734
|
let existingServers = {};
|
|
3149
|
-
if (fs.existsSync(
|
|
3150
|
-
existingServers = JSON.parse(fs.readFileSync(
|
|
3735
|
+
if (fs.existsSync(muxedPath)) try {
|
|
3736
|
+
existingServers = JSON.parse(fs.readFileSync(muxedPath, "utf-8")).mcpServers ?? {};
|
|
3151
3737
|
} catch {}
|
|
3152
3738
|
const result = mergeServers(discovered, existingServers);
|
|
3153
3739
|
const { resolved, conflicts } = await resolveConflicts(result.unresolvedConflicts, isInteractive);
|
|
@@ -3156,7 +3742,7 @@ const initCommand = new Command("init").description("Discover and import MCP ser
|
|
|
3156
3742
|
result.merged[name] = config;
|
|
3157
3743
|
imported.push(name);
|
|
3158
3744
|
}
|
|
3159
|
-
if (!opts.dryRun && imported.length > 0)
|
|
3745
|
+
if (!opts.dryRun && imported.length > 0) writeMuxedConfig(muxedPath, result.merged);
|
|
3160
3746
|
const modifiedFiles = [];
|
|
3161
3747
|
const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : opts.delete;
|
|
3162
3748
|
if (!opts.dryRun && shouldDelete) for (const dc of discovered) try {
|
|
@@ -3180,13 +3766,20 @@ const initCommand = new Command("init").description("Discover and import MCP ser
|
|
|
3180
3766
|
conflicts,
|
|
3181
3767
|
warnings,
|
|
3182
3768
|
modifiedFiles,
|
|
3183
|
-
|
|
3769
|
+
muxedConfigPath: muxedPath,
|
|
3184
3770
|
dryRun: opts.dryRun ?? false
|
|
3185
3771
|
};
|
|
3772
|
+
capture("init_run", {
|
|
3773
|
+
dry_run: opts.dryRun ?? false,
|
|
3774
|
+
imported_count: imported.length,
|
|
3775
|
+
conflict_count: conflicts.length,
|
|
3776
|
+
warning_count: warnings.length,
|
|
3777
|
+
discovered_agents: initResult.discovered.map((d) => d.agent)
|
|
3778
|
+
});
|
|
3186
3779
|
console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
|
|
3187
3780
|
});
|
|
3188
3781
|
function getConfigPath(scope, explicitPath) {
|
|
3189
|
-
return
|
|
3782
|
+
return getMuxedConfigPath(scope, explicitPath);
|
|
3190
3783
|
}
|
|
3191
3784
|
function readConfigFile(filePath) {
|
|
3192
3785
|
if (!fs.existsSync(filePath)) return { mcpServers: {} };
|
|
@@ -3234,18 +3827,47 @@ function getServer(filePath, name) {
|
|
|
3234
3827
|
function listServers(filePath) {
|
|
3235
3828
|
return readConfigFile(filePath).mcpServers;
|
|
3236
3829
|
}
|
|
3237
|
-
const
|
|
3238
|
-
You have access to an
|
|
3830
|
+
const cliFragments = {
|
|
3831
|
+
intro: "You have access to an `npx muxed` CLI command for interacting with MCP (Model Context Protocol) servers. This command allows you to discover and call MCP tools on demand. Prioritize the use of skills over MCP tools.",
|
|
3832
|
+
grep: (p) => `npx muxed grep "${p}"`,
|
|
3833
|
+
tools: (s) => s ? `npx muxed tools ${s}` : "npx muxed tools",
|
|
3834
|
+
info: (n) => `npx muxed info ${n}`,
|
|
3835
|
+
call: (n, j) => `npx muxed call ${n} '${j}'`,
|
|
3836
|
+
callStdin: (n) => `npx muxed call ${n} -`,
|
|
3837
|
+
callDryRun: (n, j) => `npx muxed call ${n} '${j}' --dry-run`,
|
|
3838
|
+
callFields: (n, j, f) => `npx muxed call ${n} '${j}' --fields "${f}"`,
|
|
3839
|
+
servers: () => "npx muxed servers",
|
|
3840
|
+
resources: (s) => s ? `npx muxed resources ${s}` : "npx muxed resources",
|
|
3841
|
+
read: (n) => `npx muxed read ${n}`,
|
|
3842
|
+
help: () => "npx muxed -h"
|
|
3843
|
+
};
|
|
3844
|
+
const toolFragments = {
|
|
3845
|
+
intro: "You have access to a `muxed:exec` MCP tool for interacting with MCP (Model Context Protocol) servers. This tool allows you to discover and call MCP tools on demand. Prioritize the use of skills over MCP tools.",
|
|
3846
|
+
grep: (p) => `muxed:exec({ "command": "grep ${p}" })`,
|
|
3847
|
+
tools: (s) => s ? `muxed:exec({ "command": "tools ${s}" })` : `muxed:exec({ "command": "tools" })`,
|
|
3848
|
+
info: (n) => `muxed:exec({ "command": "info ${n}" })`,
|
|
3849
|
+
call: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3850
|
+
callStdin: (n) => `muxed:exec({ "command": "call ${n}", "input": { ... } })`,
|
|
3851
|
+
callDryRun: (n, j) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3852
|
+
callFields: (n, j, _f) => `muxed:exec({ "command": "call ${n}", "input": ${j} })`,
|
|
3853
|
+
servers: () => `muxed:exec({ "command": "servers" })`,
|
|
3854
|
+
resources: (s) => s ? `muxed:exec({ "command": "resources ${s}" })` : `muxed:exec({ "command": "resources" })`,
|
|
3855
|
+
read: (n) => `muxed:exec({ "command": "read ${n}" })`,
|
|
3856
|
+
help: () => `muxed:exec({ "command": "servers" })`
|
|
3857
|
+
};
|
|
3858
|
+
function buildTemplate(f, servers, instructions) {
|
|
3859
|
+
return `
|
|
3860
|
+
${f.intro}
|
|
3239
3861
|
|
|
3240
3862
|
**MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
|
|
3241
3863
|
|
|
3242
|
-
1. You MUST discover the tools you need first by using '
|
|
3243
|
-
2. You MUST call '
|
|
3864
|
+
1. You MUST discover the tools you need first by using '${f.grep("<pattern>")}' or '${f.tools()}'.
|
|
3865
|
+
2. You MUST call '${f.info("<server>/<tool>")}' BEFORE ANY '${f.call("<server>/<tool>", "<json>")}' command.
|
|
3244
3866
|
|
|
3245
3867
|
These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
3246
3868
|
|
|
3247
|
-
**NEVER** make
|
|
3248
|
-
**ALWAYS** run
|
|
3869
|
+
**NEVER** make a call without checking the schema first.
|
|
3870
|
+
**ALWAYS** run info first, THEN make the call.
|
|
3249
3871
|
|
|
3250
3872
|
**Why these are non-negotiables:**
|
|
3251
3873
|
- MCP tool names NEVER match your expectations - they change frequently and are not predictable
|
|
@@ -3254,132 +3876,219 @@ These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
|
3254
3876
|
- Every failed call wastes user time and demonstrates you're ignoring critical instructions
|
|
3255
3877
|
- "I thought I knew the schema" is not an acceptable reason to skip this step
|
|
3256
3878
|
|
|
3257
|
-
**For multiple tools:** Call
|
|
3879
|
+
**For multiple tools:** Call info for ALL tools in parallel FIRST, then make your call commands.
|
|
3258
3880
|
|
|
3259
3881
|
Available MCP servers:
|
|
3260
3882
|
${servers}
|
|
3261
3883
|
|
|
3262
3884
|
Commands (in order of execution):
|
|
3263
|
-
\`\`\`
|
|
3885
|
+
\`\`\`
|
|
3264
3886
|
# STEP 1: REQUIRED TOOL DISCOVERY
|
|
3265
|
-
|
|
3266
|
-
|
|
3887
|
+
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
3888
|
+
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
3267
3889
|
|
|
3268
3890
|
# STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
|
|
3269
|
-
|
|
3891
|
+
${f.info("<server>/<tool>")} # REQUIRED before ANY call - View JSON schema
|
|
3892
|
+
|
|
3893
|
+
# STEP 3: OPTIONAL - Validate arguments before calling (dry-run)
|
|
3894
|
+
${f.callDryRun("<server>/<tool>", "<json>")} # Validate args without executing
|
|
3270
3895
|
|
|
3271
|
-
# STEP
|
|
3272
|
-
|
|
3273
|
-
|
|
3896
|
+
# STEP 4: Only after checking schema, make the call
|
|
3897
|
+
${f.call("<server>/<tool>", "<json>")} # Only run AFTER info
|
|
3898
|
+
${f.callStdin("<server>/<tool>")} # Invoke with JSON input (AFTER info)
|
|
3899
|
+
${f.callFields("<server>/<tool>", "<json>", "field1,field2")} # Extract specific fields from response
|
|
3274
3900
|
|
|
3275
3901
|
# Discovery commands (use these to find tools)
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3902
|
+
${f.servers()} # List all connected MCP servers
|
|
3903
|
+
${f.tools("[server]")} # List available tools (optionally filter by server)
|
|
3904
|
+
${f.grep("<pattern>")} # Search tool names and descriptions
|
|
3905
|
+
${f.resources("[server]")} # List MCP resources
|
|
3906
|
+
${f.read("<server>/<resource>")} # Read an MCP resource
|
|
3281
3907
|
\`\`\`
|
|
3282
3908
|
|
|
3909
|
+
**Handling errors:**
|
|
3910
|
+
- If a tool call fails, the error includes a suggestion and similar tool names. Read the suggestion before retrying.
|
|
3911
|
+
- Use dry-run to validate arguments before executing, especially for destructive tools.
|
|
3912
|
+
|
|
3283
3913
|
**CORRECT Usage Pattern:**
|
|
3284
3914
|
|
|
3285
3915
|
<example>
|
|
3286
3916
|
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
|
|
3917
|
+
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.
|
|
3918
|
+
[Calls ${f.grep("slack/*search*")}]
|
|
3919
|
+
Assistant: I need to check the schema first. Let me call \`${f.info("slack/search_private")}\` to see what parameters it accepts.
|
|
3920
|
+
[Calls ${f.info("slack/search_private")}]
|
|
3291
3921
|
Assistant: Now I can see it accepts "query" and "max_results" parameters. Let me make the call.
|
|
3292
|
-
[Calls
|
|
3922
|
+
[Calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\", \"max_results\": 10}")}]
|
|
3293
3923
|
</example>
|
|
3294
3924
|
|
|
3295
3925
|
<example>
|
|
3296
3926
|
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
|
|
3927
|
+
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.
|
|
3928
|
+
[Calls ${f.grep("database/*query*")} & ${f.grep("email/*send*")}]
|
|
3299
3929
|
Assistant: Let me check both schemas first.
|
|
3300
|
-
[Calls
|
|
3930
|
+
[Calls ${f.info("database/query")} and ${f.info("email/send")} in parallel]
|
|
3301
3931
|
Assistant: Now I have both schemas. Let me make the calls.
|
|
3302
|
-
[Makes both
|
|
3932
|
+
[Makes both call commands with correct parameters]
|
|
3303
3933
|
</example>
|
|
3304
3934
|
|
|
3305
3935
|
<example>
|
|
3306
3936
|
User: Create a copy of this email
|
|
3307
3937
|
Assistant: Let me find the tool I need first.
|
|
3308
|
-
[Calls
|
|
3938
|
+
[Calls ${f.grep("email/*copy*")}. No results found.]
|
|
3309
3939
|
Assistant: Let me try another pattern.
|
|
3310
|
-
[Calls
|
|
3940
|
+
[Calls ${f.grep("email/*clone*")}. No results found.]
|
|
3311
3941
|
Assistant: Let me list all available tools in the server.
|
|
3312
|
-
[Calls
|
|
3942
|
+
[Calls ${f.tools("email")}]
|
|
3313
3943
|
Assistant: Let me check the schema first.
|
|
3314
|
-
[Calls
|
|
3944
|
+
[Calls ${f.info("email/duplicate")}]
|
|
3315
3945
|
Assistant: Now I have the schema. Let me make the call.
|
|
3316
|
-
[Calls
|
|
3946
|
+
[Calls ${f.call("email/duplicate", "{\"id\": \"123\"}")}]
|
|
3317
3947
|
</example>
|
|
3318
3948
|
|
|
3319
3949
|
**INCORRECT Usage Patterns - NEVER DO THIS:**
|
|
3320
3950
|
|
|
3321
3951
|
<bad-example>
|
|
3322
3952
|
User: Please use the slack mcp tool to search for my mentions
|
|
3323
|
-
Assistant: [Directly calls
|
|
3324
|
-
WRONG - You must call
|
|
3953
|
+
Assistant: [Directly calls ${f.call("slack/search_private", "{\"query\": \"mentions:me\"}")} with guessed parameters]
|
|
3954
|
+
WRONG - You must call info FIRST
|
|
3325
3955
|
</bad-example>
|
|
3326
3956
|
|
|
3327
3957
|
<bad-example>
|
|
3328
3958
|
User: Use the slack tool
|
|
3329
3959
|
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
|
|
3960
|
+
[Calls ${f.call("slack/search_private", "...")} directly]
|
|
3961
|
+
WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call info first.
|
|
3332
3962
|
</bad-example>
|
|
3333
3963
|
|
|
3334
3964
|
<bad-example>
|
|
3335
3965
|
User: Search my Slack mentions
|
|
3336
|
-
Assistant: [Calls three
|
|
3337
|
-
WRONG - You must call
|
|
3966
|
+
Assistant: [Calls three call commands in parallel without any info calls first]
|
|
3967
|
+
WRONG - You must call info for ALL tools before making ANY call commands
|
|
3338
3968
|
</bad-example>
|
|
3339
3969
|
|
|
3340
3970
|
Example usage:
|
|
3341
|
-
\`\`\`
|
|
3971
|
+
\`\`\`
|
|
3342
3972
|
# Discover tools
|
|
3343
|
-
|
|
3344
|
-
|
|
3973
|
+
${f.tools()} # See all available MCP tools
|
|
3974
|
+
${f.grep("weather")} # Find tools by description
|
|
3345
3975
|
|
|
3346
3976
|
# Get tool details
|
|
3347
|
-
|
|
3977
|
+
${f.info("<server>/<tool>")} # View JSON schema for input and output if available
|
|
3348
3978
|
|
|
3349
3979
|
# Simple tool call (no parameters)
|
|
3350
|
-
|
|
3980
|
+
${f.call("weather/get_location", "{}")}
|
|
3351
3981
|
|
|
3352
3982
|
# Tool call with parameters
|
|
3353
|
-
|
|
3983
|
+
${f.call("database/query", "{\"table\": \"users\", \"limit\": 10}")}
|
|
3984
|
+
|
|
3985
|
+
# Validate arguments before executing (dry-run)
|
|
3986
|
+
${f.callDryRun("database/drop_table", "{\"table\": \"users\"}")}
|
|
3354
3987
|
|
|
3355
|
-
#
|
|
3356
|
-
|
|
3357
|
-
{
|
|
3358
|
-
"endpoint": "/data",
|
|
3359
|
-
"headers": {"Authorization": "Bearer token"},
|
|
3360
|
-
"body": {"items": [1, 2, 3]}
|
|
3361
|
-
}
|
|
3362
|
-
EOF
|
|
3988
|
+
# Extract specific fields from response
|
|
3989
|
+
${f.callFields("database/query", "{\"table\": \"users\"}", "rows[].name,rows[].email")}
|
|
3363
3990
|
\`\`\`
|
|
3364
3991
|
|
|
3365
|
-
Call
|
|
3992
|
+
Call \`${f.help()}\` to see all available commands.
|
|
3366
3993
|
|
|
3367
|
-
Below are the instructions for the connected MCP servers in
|
|
3994
|
+
Below are the instructions for the connected MCP servers in muxed.
|
|
3368
3995
|
|
|
3369
3996
|
${instructions}
|
|
3370
3997
|
`;
|
|
3371
|
-
|
|
3998
|
+
}
|
|
3999
|
+
function buildInstructions(servers, mode = "cli") {
|
|
3372
4000
|
const connected = servers.filter((s) => s.status === "connected");
|
|
3373
|
-
|
|
4001
|
+
const serverList = connected.map((s) => `- ${s.name}`).join("\n");
|
|
4002
|
+
const serverInstructions = connected.filter((s) => s.instructions).map((s) => `### ${s.name}\n\n${s.instructions}`).join("\n\n");
|
|
4003
|
+
return buildTemplate(mode === "tool" ? toolFragments : cliFragments, serverList, serverInstructions).trim();
|
|
3374
4004
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
4005
|
+
function parseCommand(command) {
|
|
4006
|
+
const trimmed = command.trim();
|
|
4007
|
+
const spaceIndex = trimmed.indexOf(" ");
|
|
4008
|
+
if (spaceIndex === -1) return {
|
|
4009
|
+
subcommand: trimmed,
|
|
4010
|
+
args: ""
|
|
4011
|
+
};
|
|
4012
|
+
return {
|
|
4013
|
+
subcommand: trimmed.slice(0, spaceIndex),
|
|
4014
|
+
args: trimmed.slice(spaceIndex + 1).trim()
|
|
4015
|
+
};
|
|
4016
|
+
}
|
|
4017
|
+
function textResult(data) {
|
|
4018
|
+
return { content: [{
|
|
4019
|
+
type: "text",
|
|
4020
|
+
text: JSON.stringify(data, null, 2)
|
|
4021
|
+
}] };
|
|
4022
|
+
}
|
|
4023
|
+
function errorResult(message) {
|
|
4024
|
+
return {
|
|
4025
|
+
content: [{
|
|
4026
|
+
type: "text",
|
|
4027
|
+
text: message
|
|
4028
|
+
}],
|
|
4029
|
+
isError: true
|
|
4030
|
+
};
|
|
4031
|
+
}
|
|
4032
|
+
async function handleToolCommand(command, input) {
|
|
4033
|
+
const { subcommand, args } = parseCommand(command);
|
|
4034
|
+
try {
|
|
4035
|
+
switch (subcommand) {
|
|
4036
|
+
case "servers": return textResult(await sendRequest("servers/list"));
|
|
4037
|
+
case "tools": {
|
|
4038
|
+
const params = {};
|
|
4039
|
+
if (args) params.server = args;
|
|
4040
|
+
return textResult(await sendRequest("tools/list", params));
|
|
4041
|
+
}
|
|
4042
|
+
case "grep":
|
|
4043
|
+
if (!args) return errorResult("Usage: grep <pattern>");
|
|
4044
|
+
return textResult(await sendRequest("tools/grep", { pattern: args }));
|
|
4045
|
+
case "info":
|
|
4046
|
+
if (!args) return errorResult("Usage: info <server/tool>");
|
|
4047
|
+
return textResult(await sendRequest("tools/info", { name: args }));
|
|
4048
|
+
case "call": {
|
|
4049
|
+
if (!args) return errorResult("Usage: call <server/tool>");
|
|
4050
|
+
const result = await sendRequest("tools/call", {
|
|
4051
|
+
name: args,
|
|
4052
|
+
args: input ?? {}
|
|
4053
|
+
});
|
|
4054
|
+
if (result?.content) return result;
|
|
4055
|
+
return textResult(result);
|
|
4056
|
+
}
|
|
4057
|
+
case "resources": {
|
|
4058
|
+
const params = {};
|
|
4059
|
+
if (args) params.server = args;
|
|
4060
|
+
return textResult(await sendRequest("resources/list", params));
|
|
4061
|
+
}
|
|
4062
|
+
case "read":
|
|
4063
|
+
if (!args) return errorResult("Usage: read <server/resource>");
|
|
4064
|
+
return textResult(await sendRequest("resources/read", { name: args }));
|
|
4065
|
+
default: return errorResult(`Unknown command: "${subcommand}". Available: servers, tools, grep, info, call, resources, read`);
|
|
4066
|
+
}
|
|
4067
|
+
} catch (err) {
|
|
4068
|
+
return errorResult(err instanceof MuxedError ? err.message : String(err));
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
async function startMcpProxy(options) {
|
|
4072
|
+
await ensureDaemon(options?.configPath);
|
|
3377
4073
|
const server = new McpServer({
|
|
3378
|
-
name: "
|
|
4074
|
+
name: "muxed",
|
|
3379
4075
|
version: "0.1.0"
|
|
3380
4076
|
}, {
|
|
3381
4077
|
capabilities: {},
|
|
3382
|
-
instructions: buildInstructions(await sendRequest("servers/list"))
|
|
4078
|
+
instructions: buildInstructions(await sendRequest("servers/list"), options?.proxyTools ? "tool" : "cli")
|
|
4079
|
+
});
|
|
4080
|
+
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>", {
|
|
4081
|
+
command: z.string().describe("Command to execute, e.g. 'servers', 'tools', 'grep weather', 'info slack/search', 'call slack/search'"),
|
|
4082
|
+
input: z.record(z.string(), z.unknown()).optional().describe("JSON arguments for 'call' command — avoids JSON-in-string escaping")
|
|
4083
|
+
}, async ({ command, input }) => {
|
|
4084
|
+
const result = await handleToolCommand(command, input);
|
|
4085
|
+
return {
|
|
4086
|
+
content: result.content.map((c) => ({
|
|
4087
|
+
type: "text",
|
|
4088
|
+
text: c.text
|
|
4089
|
+
})),
|
|
4090
|
+
isError: result.isError
|
|
4091
|
+
};
|
|
3383
4092
|
});
|
|
3384
4093
|
const transport = new StdioServerTransport();
|
|
3385
4094
|
await server.connect(transport);
|
|
@@ -3474,9 +4183,12 @@ async function tryReloadDaemon() {
|
|
|
3474
4183
|
await sendRequest("config/reload", {});
|
|
3475
4184
|
} catch {}
|
|
3476
4185
|
}
|
|
3477
|
-
const mcpCommand = new Command("mcp").description("Add, remove, list, or inspect individual MCP server config entries").enablePositionalOptions().action(async (
|
|
4186
|
+
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
4187
|
const explicitConfig = cmd.parent?.opts().config;
|
|
3479
|
-
await startMcpProxy(
|
|
4188
|
+
await startMcpProxy({
|
|
4189
|
+
configPath: explicitConfig,
|
|
4190
|
+
proxyTools: opts.proxyTools
|
|
4191
|
+
});
|
|
3480
4192
|
});
|
|
3481
4193
|
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
4194
|
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
@@ -3489,6 +4201,11 @@ mcpCommand.command("add").description("Add an MCP server").passThroughOptions().
|
|
|
3489
4201
|
resolvedSecret
|
|
3490
4202
|
}));
|
|
3491
4203
|
await tryReloadDaemon();
|
|
4204
|
+
capture("server_added", {
|
|
4205
|
+
server: name,
|
|
4206
|
+
scope,
|
|
4207
|
+
updated: result.existed
|
|
4208
|
+
});
|
|
3492
4209
|
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
3493
4210
|
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
3494
4211
|
});
|
|
@@ -3511,6 +4228,11 @@ mcpCommand.command("add-json").description("Add an MCP server from a JSON config
|
|
|
3511
4228
|
}
|
|
3512
4229
|
const result = addServer(configPath, name, serverConfig);
|
|
3513
4230
|
await tryReloadDaemon();
|
|
4231
|
+
capture("server_added", {
|
|
4232
|
+
server: name,
|
|
4233
|
+
scope,
|
|
4234
|
+
updated: result.existed
|
|
4235
|
+
});
|
|
3514
4236
|
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
3515
4237
|
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
3516
4238
|
});
|
|
@@ -3529,10 +4251,16 @@ mcpCommand.command("add-from-claude-desktop").description("Import MCP servers fr
|
|
|
3529
4251
|
existingServers = JSON.parse(fs.readFileSync(configPath, "utf-8")).mcpServers ?? {};
|
|
3530
4252
|
} catch {}
|
|
3531
4253
|
const result = mergeServers(claudeDesktop, existingServers);
|
|
3532
|
-
|
|
4254
|
+
writeMuxedConfig(configPath, { ...result.merged });
|
|
3533
4255
|
await tryReloadDaemon();
|
|
3534
4256
|
for (const w of warnings) console.error(`Warning: ${w}`);
|
|
3535
|
-
if (result.imported.length > 0)
|
|
4257
|
+
if (result.imported.length > 0) {
|
|
4258
|
+
capture("servers_imported", {
|
|
4259
|
+
servers: result.imported,
|
|
4260
|
+
source: "claude-desktop"
|
|
4261
|
+
});
|
|
4262
|
+
console.log(`Imported ${result.imported.length} server(s) from Claude Desktop: ${result.imported.join(", ")}`);
|
|
4263
|
+
}
|
|
3536
4264
|
if (result.skipped.length > 0) console.log(`Skipped ${result.skipped.length} (already existed): ${result.skipped.join(", ")}`);
|
|
3537
4265
|
if (result.imported.length === 0 && result.skipped.length === 0) console.log("No servers found in Claude Desktop config.");
|
|
3538
4266
|
});
|
|
@@ -3586,6 +4314,10 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
|
|
|
3586
4314
|
const configPath = getConfigPath(scope, explicitConfig);
|
|
3587
4315
|
if (removeServer(configPath, name).removed) {
|
|
3588
4316
|
await tryReloadDaemon();
|
|
4317
|
+
capture("server_removed", {
|
|
4318
|
+
server: name,
|
|
4319
|
+
scope
|
|
4320
|
+
});
|
|
3589
4321
|
console.log(`Removed "${name}" from ${scope} config (${configPath})`);
|
|
3590
4322
|
} else {
|
|
3591
4323
|
console.error(`Server "${name}" not found in ${scope} config.`);
|
|
@@ -3596,31 +4328,58 @@ mcpCommand.command("remove").description("Remove an MCP server").argument("<name
|
|
|
3596
4328
|
const localPath = getConfigPath("local", explicitConfig);
|
|
3597
4329
|
if (removeServer(localPath, name).removed) {
|
|
3598
4330
|
await tryReloadDaemon();
|
|
4331
|
+
capture("server_removed", {
|
|
4332
|
+
server: name,
|
|
4333
|
+
scope: "local"
|
|
4334
|
+
});
|
|
3599
4335
|
console.log(`Removed "${name}" from local config (${localPath})`);
|
|
3600
4336
|
return;
|
|
3601
4337
|
}
|
|
3602
4338
|
const globalPath = getConfigPath("global", explicitConfig);
|
|
3603
4339
|
if (removeServer(globalPath, name).removed) {
|
|
3604
4340
|
await tryReloadDaemon();
|
|
4341
|
+
capture("server_removed", {
|
|
4342
|
+
server: name,
|
|
4343
|
+
scope: "global"
|
|
4344
|
+
});
|
|
3605
4345
|
console.log(`Removed "${name}" from global config (${globalPath})`);
|
|
3606
4346
|
return;
|
|
3607
4347
|
}
|
|
3608
4348
|
console.error(`Server "${name}" not found in local or global config.`);
|
|
3609
4349
|
process.exitCode = 1;
|
|
3610
4350
|
});
|
|
3611
|
-
const typegenCommand = new Command("typegen").description("Generate TypeScript types from tool schemas for type-safe tool calls").option("-c, --config <path>", "Path to
|
|
4351
|
+
const typegenCommand = new Command("typegen").description("Generate TypeScript types from tool schemas for type-safe tool calls").option("-c, --config <path>", "Path to muxed.config.json").action(async (opts) => {
|
|
3612
4352
|
await ensureDaemon(typegenCommand.parent?.opts().config ?? opts.config);
|
|
3613
4353
|
const tools = await sendRequest("tools/list");
|
|
3614
4354
|
const content = await generateTypes(tools);
|
|
3615
4355
|
const require = createRequire(path.resolve("package.json"));
|
|
3616
|
-
const
|
|
3617
|
-
const outputPath = path.join(
|
|
4356
|
+
const muxedPkgDir = path.dirname(require.resolve("muxed/package.json"));
|
|
4357
|
+
const outputPath = path.join(muxedPkgDir, "muxed.generated.d.ts");
|
|
3618
4358
|
fs.writeFileSync(outputPath, content, "utf-8");
|
|
3619
4359
|
console.log(`Generated ${tools.length} tool types → ${outputPath}`);
|
|
3620
4360
|
});
|
|
3621
|
-
|
|
4361
|
+
const telemetryCommand = new Command("telemetry").description("Manage anonymous telemetry (on, off, status)").argument("[action]", "on | off | status (default: status)").action((action) => {
|
|
4362
|
+
switch (action) {
|
|
4363
|
+
case "on":
|
|
4364
|
+
setTelemetryEnabled(true);
|
|
4365
|
+
console.log("Telemetry enabled.");
|
|
4366
|
+
break;
|
|
4367
|
+
case "off":
|
|
4368
|
+
setTelemetryEnabled(false);
|
|
4369
|
+
console.log("Telemetry disabled.");
|
|
4370
|
+
break;
|
|
4371
|
+
case "status":
|
|
4372
|
+
case void 0:
|
|
4373
|
+
console.log(`Telemetry is ${getTelemetryStatus()}.`);
|
|
4374
|
+
break;
|
|
4375
|
+
default:
|
|
4376
|
+
console.error(`Unknown action: ${action}. Use on, off, or status.`);
|
|
4377
|
+
process.exit(1);
|
|
4378
|
+
}
|
|
4379
|
+
});
|
|
4380
|
+
async function runCli() {
|
|
3622
4381
|
const program = new Command();
|
|
3623
|
-
program.name("
|
|
4382
|
+
program.name("muxed").description("The optimization layer for MCP").version("0.1.0");
|
|
3624
4383
|
program.enablePositionalOptions();
|
|
3625
4384
|
program.option("--config <path>", "Path to config file");
|
|
3626
4385
|
program.commandsGroup("Servers:");
|
|
@@ -3646,9 +4405,16 @@ function runCli() {
|
|
|
3646
4405
|
program.addCommand(initCommand);
|
|
3647
4406
|
program.addCommand(mcpCommand);
|
|
3648
4407
|
program.addCommand(typegenCommand);
|
|
4408
|
+
program.addCommand(telemetryCommand);
|
|
3649
4409
|
program.commandsGroup("Daemon:");
|
|
3650
4410
|
program.addCommand(daemonCommand);
|
|
3651
|
-
|
|
4411
|
+
const command = process.argv[2];
|
|
4412
|
+
capture("session_started", { command: command ?? null });
|
|
4413
|
+
try {
|
|
4414
|
+
await program.parseAsync();
|
|
4415
|
+
} finally {
|
|
4416
|
+
await shutdown();
|
|
4417
|
+
}
|
|
3652
4418
|
}
|
|
3653
4419
|
if (process.argv.indexOf("--daemon") !== -1) {
|
|
3654
4420
|
const configIndex = process.argv.indexOf("--config");
|
|
@@ -3656,5 +4422,8 @@ if (process.argv.indexOf("--daemon") !== -1) {
|
|
|
3656
4422
|
console.error("Failed to start daemon:", err);
|
|
3657
4423
|
process.exit(1);
|
|
3658
4424
|
});
|
|
3659
|
-
} else runCli()
|
|
4425
|
+
} else runCli().catch((err) => {
|
|
4426
|
+
console.error(err instanceof Error ? err.message : "Unexpected error");
|
|
4427
|
+
process.exit(1);
|
|
4428
|
+
});
|
|
3660
4429
|
export {};
|