muxed 0.1.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/LICENSE +21 -0
- package/bin/cli.mjs +2 -0
- package/dist/cli.mjs +3660 -0
- package/dist/client/index.d.mts +171 -0
- package/dist/client/index.mjs +351 -0
- package/muxed.generated.d.ts +2 -0
- package/package.json +78 -0
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,3660 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { z } from "zod/v4";
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
|
+
import { SSEClientTransport, SseError } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
9
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
10
|
+
import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
|
|
11
|
+
import { LATEST_PROTOCOL_VERSION } from "@modelcontextprotocol/sdk/types.js";
|
|
12
|
+
import { execFile, fork } from "node:child_process";
|
|
13
|
+
import http from "node:http";
|
|
14
|
+
import { compile } from "json-schema-to-typescript";
|
|
15
|
+
import net from "node:net";
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
import * as readline from "node:readline/promises";
|
|
18
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
+
var __defProp = Object.defineProperty;
|
|
21
|
+
var __exportAll = (all, no_symbols) => {
|
|
22
|
+
let target = {};
|
|
23
|
+
for (var name in all) __defProp(target, name, {
|
|
24
|
+
get: all[name],
|
|
25
|
+
enumerable: true
|
|
26
|
+
});
|
|
27
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
28
|
+
return target;
|
|
29
|
+
};
|
|
30
|
+
const StdioServerConfigSchema = z.object({
|
|
31
|
+
command: z.string(),
|
|
32
|
+
args: z.array(z.string()).optional(),
|
|
33
|
+
env: z.record(z.string(), z.string()).optional(),
|
|
34
|
+
cwd: z.string().optional()
|
|
35
|
+
});
|
|
36
|
+
const ReconnectionSchema = z.object({
|
|
37
|
+
maxDelay: z.number().optional(),
|
|
38
|
+
initialDelay: z.number().optional(),
|
|
39
|
+
growFactor: z.number().optional(),
|
|
40
|
+
maxRetries: z.number().optional()
|
|
41
|
+
});
|
|
42
|
+
const ClientCredentialsAuthSchema = z.object({
|
|
43
|
+
type: z.literal("client_credentials"),
|
|
44
|
+
clientId: z.string(),
|
|
45
|
+
clientSecret: z.string(),
|
|
46
|
+
scope: z.string().optional()
|
|
47
|
+
});
|
|
48
|
+
const AuthorizationCodeAuthSchema = z.object({
|
|
49
|
+
type: z.literal("authorization_code"),
|
|
50
|
+
clientId: z.string().optional(),
|
|
51
|
+
clientSecret: z.string().optional(),
|
|
52
|
+
scope: z.string().optional(),
|
|
53
|
+
callbackPort: z.number().int().min(0).max(65535).optional()
|
|
54
|
+
});
|
|
55
|
+
const OAuthConfigSchema = z.union([ClientCredentialsAuthSchema, AuthorizationCodeAuthSchema]);
|
|
56
|
+
const HttpServerConfigSchema = z.object({
|
|
57
|
+
url: z.string(),
|
|
58
|
+
transport: z.enum(["streamable-http", "sse"]).optional(),
|
|
59
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
60
|
+
sessionId: z.string().optional(),
|
|
61
|
+
reconnection: ReconnectionSchema.optional(),
|
|
62
|
+
auth: OAuthConfigSchema.optional()
|
|
63
|
+
});
|
|
64
|
+
const ServerConfigSchema = z.union([StdioServerConfigSchema, HttpServerConfigSchema]);
|
|
65
|
+
const HttpListenerSchema = z.object({
|
|
66
|
+
enabled: z.boolean().optional(),
|
|
67
|
+
port: z.number().optional(),
|
|
68
|
+
host: z.string().optional()
|
|
69
|
+
});
|
|
70
|
+
const DaemonConfigSchema = z.object({
|
|
71
|
+
idleTimeout: z.number().optional(),
|
|
72
|
+
connectTimeout: z.number().optional(),
|
|
73
|
+
requestTimeout: z.number().optional(),
|
|
74
|
+
healthCheckInterval: z.number().optional(),
|
|
75
|
+
maxRestartAttempts: z.number().optional(),
|
|
76
|
+
maxTotalTimeout: z.number().optional(),
|
|
77
|
+
taskExpiryTimeout: z.number().optional(),
|
|
78
|
+
logLevel: z.enum([
|
|
79
|
+
"debug",
|
|
80
|
+
"info",
|
|
81
|
+
"warn",
|
|
82
|
+
"error"
|
|
83
|
+
]).optional(),
|
|
84
|
+
shutdownTimeout: z.number().optional(),
|
|
85
|
+
http: HttpListenerSchema.optional()
|
|
86
|
+
});
|
|
87
|
+
const TooldConfigSchema = z.object({
|
|
88
|
+
mcpServers: z.record(z.string(), ServerConfigSchema),
|
|
89
|
+
daemon: DaemonConfigSchema.optional(),
|
|
90
|
+
mergeClaudeConfig: z.boolean().optional()
|
|
91
|
+
});
|
|
92
|
+
function getClaudeDesktopConfigPath() {
|
|
93
|
+
const platform = os.platform();
|
|
94
|
+
const home = os.homedir();
|
|
95
|
+
if (platform === "darwin") return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
96
|
+
if (platform === "linux") return path.join(home, ".config", "Claude", "claude_desktop_config.json");
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function mergeClaudeDesktopServers(servers) {
|
|
100
|
+
const configPath = getClaudeDesktopConfigPath();
|
|
101
|
+
if (!configPath || !fs.existsSync(configPath)) return servers;
|
|
102
|
+
try {
|
|
103
|
+
const claudeServers = JSON.parse(fs.readFileSync(configPath, "utf-8")).mcpServers;
|
|
104
|
+
if (!claudeServers || typeof claudeServers !== "object") return servers;
|
|
105
|
+
return {
|
|
106
|
+
...claudeServers,
|
|
107
|
+
...servers
|
|
108
|
+
};
|
|
109
|
+
} catch {
|
|
110
|
+
return servers;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const DAEMON_DEFAULTS = {
|
|
114
|
+
idleTimeout: 3e5,
|
|
115
|
+
connectTimeout: 3e4,
|
|
116
|
+
requestTimeout: 6e4,
|
|
117
|
+
healthCheckInterval: 3e4,
|
|
118
|
+
maxRestartAttempts: -1,
|
|
119
|
+
maxTotalTimeout: 3e5,
|
|
120
|
+
taskExpiryTimeout: 36e5,
|
|
121
|
+
logLevel: "info",
|
|
122
|
+
shutdownTimeout: 1e4
|
|
123
|
+
};
|
|
124
|
+
function getGlobalConfigPath() {
|
|
125
|
+
return path.join(os.homedir(), ".config", "toold", "config.json");
|
|
126
|
+
}
|
|
127
|
+
function findConfigFile(configPath) {
|
|
128
|
+
if (configPath) {
|
|
129
|
+
if (!fs.existsSync(configPath)) throw new Error(`Config file not found: ${configPath}`);
|
|
130
|
+
return configPath;
|
|
131
|
+
}
|
|
132
|
+
const cwdConfig = path.join(process.cwd(), "toold.config.json");
|
|
133
|
+
if (fs.existsSync(cwdConfig)) return cwdConfig;
|
|
134
|
+
const homeConfig = getGlobalConfigPath();
|
|
135
|
+
if (fs.existsSync(homeConfig)) return homeConfig;
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
function validateServerConfigs(config) {
|
|
139
|
+
for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
|
|
140
|
+
const hasCommand = "command" in serverConfig;
|
|
141
|
+
const hasUrl = "url" in serverConfig;
|
|
142
|
+
if (!hasCommand && !hasUrl) throw new Error(`Server "${name}": must have either "command" (stdio) or "url" (HTTP) property`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function loadConfig(configPath) {
|
|
146
|
+
const filePath = findConfigFile(configPath);
|
|
147
|
+
const config = filePath ? parseConfigFile(filePath) : { mcpServers: {} };
|
|
148
|
+
const globalConfigPath = getGlobalConfigPath();
|
|
149
|
+
if ((!filePath || path.resolve(filePath) !== path.resolve(globalConfigPath)) && fs.existsSync(globalConfigPath)) try {
|
|
150
|
+
const globalRaw = JSON.parse(fs.readFileSync(globalConfigPath, "utf-8"));
|
|
151
|
+
const globalResult = TooldConfigSchema.safeParse(globalRaw);
|
|
152
|
+
if (globalResult.success) config.mcpServers = {
|
|
153
|
+
...globalResult.data.mcpServers,
|
|
154
|
+
...config.mcpServers
|
|
155
|
+
};
|
|
156
|
+
} catch {}
|
|
157
|
+
if (config.mergeClaudeConfig) config.mcpServers = mergeClaudeDesktopServers(config.mcpServers);
|
|
158
|
+
validateServerConfigs(config);
|
|
159
|
+
config.daemon = {
|
|
160
|
+
...DAEMON_DEFAULTS,
|
|
161
|
+
...config.daemon,
|
|
162
|
+
http: {
|
|
163
|
+
enabled: false,
|
|
164
|
+
port: 3100,
|
|
165
|
+
host: "127.0.0.1",
|
|
166
|
+
...config.daemon?.http
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
return config;
|
|
170
|
+
}
|
|
171
|
+
function parseConfigFile(filePath) {
|
|
172
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
173
|
+
let parsed;
|
|
174
|
+
try {
|
|
175
|
+
parsed = JSON.parse(raw);
|
|
176
|
+
} catch {
|
|
177
|
+
throw new Error(`Invalid JSON in config file: ${filePath}`);
|
|
178
|
+
}
|
|
179
|
+
const result = TooldConfigSchema.safeParse(parsed);
|
|
180
|
+
if (!result.success) throw new Error(`Invalid config: ${z.prettifyError(result.error)}`);
|
|
181
|
+
return result.data;
|
|
182
|
+
}
|
|
183
|
+
function isStdioConfig(config) {
|
|
184
|
+
return "command" in config;
|
|
185
|
+
}
|
|
186
|
+
function isHttpConfig(config) {
|
|
187
|
+
return "url" in config;
|
|
188
|
+
}
|
|
189
|
+
var paths_exports = /* @__PURE__ */ __exportAll({
|
|
190
|
+
ensureTooldDir: () => ensureTooldDir,
|
|
191
|
+
getLogPath: () => getLogPath,
|
|
192
|
+
getPidPath: () => getPidPath,
|
|
193
|
+
getSocketPath: () => getSocketPath,
|
|
194
|
+
getTooldDir: () => getTooldDir
|
|
195
|
+
});
|
|
196
|
+
function getTooldDir() {
|
|
197
|
+
return path.join(os.homedir(), ".toold");
|
|
198
|
+
}
|
|
199
|
+
function getSocketPath() {
|
|
200
|
+
return path.join(getTooldDir(), "toold.sock");
|
|
201
|
+
}
|
|
202
|
+
function getPidPath() {
|
|
203
|
+
return path.join(getTooldDir(), "toold.pid");
|
|
204
|
+
}
|
|
205
|
+
function getLogPath() {
|
|
206
|
+
return path.join(getTooldDir(), "toold.log");
|
|
207
|
+
}
|
|
208
|
+
function ensureTooldDir() {
|
|
209
|
+
fs.mkdirSync(getTooldDir(), { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
const LOG_LEVELS = {
|
|
212
|
+
debug: 0,
|
|
213
|
+
info: 1,
|
|
214
|
+
warn: 2,
|
|
215
|
+
error: 3
|
|
216
|
+
};
|
|
217
|
+
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
218
|
+
var Logger = class {
|
|
219
|
+
level;
|
|
220
|
+
logPath;
|
|
221
|
+
fd = null;
|
|
222
|
+
writeToStderr;
|
|
223
|
+
constructor(opts) {
|
|
224
|
+
this.level = opts?.level ?? "info";
|
|
225
|
+
this.logPath = opts?.logPath ?? getLogPath();
|
|
226
|
+
this.writeToStderr = opts?.stderr ?? false;
|
|
227
|
+
}
|
|
228
|
+
openFile() {
|
|
229
|
+
if (this.fd !== null) return;
|
|
230
|
+
try {
|
|
231
|
+
this.fd = fs.openSync(this.logPath, "a");
|
|
232
|
+
} catch {}
|
|
233
|
+
}
|
|
234
|
+
rotateIfNeeded() {
|
|
235
|
+
if (this.fd === null) return;
|
|
236
|
+
try {
|
|
237
|
+
if (fs.fstatSync(this.fd).size > MAX_LOG_SIZE) {
|
|
238
|
+
fs.closeSync(this.fd);
|
|
239
|
+
fs.truncateSync(this.logPath, 0);
|
|
240
|
+
this.fd = fs.openSync(this.logPath, "a");
|
|
241
|
+
}
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
formatMessage(level, message, server) {
|
|
245
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
246
|
+
const serverTag = server ? ` [${server}]` : "";
|
|
247
|
+
return `${ts} ${level.toUpperCase()}${serverTag} ${message}`;
|
|
248
|
+
}
|
|
249
|
+
log(level, message, server) {
|
|
250
|
+
if (LOG_LEVELS[level] < LOG_LEVELS[this.level]) return;
|
|
251
|
+
const formatted = this.formatMessage(level, message, server);
|
|
252
|
+
if (this.writeToStderr) process.stderr.write(formatted + "\n");
|
|
253
|
+
this.openFile();
|
|
254
|
+
if (this.fd !== null) {
|
|
255
|
+
this.rotateIfNeeded();
|
|
256
|
+
try {
|
|
257
|
+
fs.writeSync(this.fd, formatted + "\n");
|
|
258
|
+
} catch {}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
debug(message, server) {
|
|
262
|
+
this.log("debug", message, server);
|
|
263
|
+
}
|
|
264
|
+
info(message, server) {
|
|
265
|
+
this.log("info", message, server);
|
|
266
|
+
}
|
|
267
|
+
warn(message, server) {
|
|
268
|
+
this.log("warn", message, server);
|
|
269
|
+
}
|
|
270
|
+
error(message, server) {
|
|
271
|
+
this.log("error", message, server);
|
|
272
|
+
}
|
|
273
|
+
setLevel(level) {
|
|
274
|
+
this.level = level;
|
|
275
|
+
}
|
|
276
|
+
close() {
|
|
277
|
+
if (this.fd !== null) {
|
|
278
|
+
try {
|
|
279
|
+
fs.closeSync(this.fd);
|
|
280
|
+
} catch {}
|
|
281
|
+
this.fd = null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
let defaultLogger;
|
|
286
|
+
function getLogger() {
|
|
287
|
+
if (!defaultLogger) defaultLogger = new Logger();
|
|
288
|
+
return defaultLogger;
|
|
289
|
+
}
|
|
290
|
+
function initLogger(opts) {
|
|
291
|
+
if (defaultLogger) defaultLogger.close();
|
|
292
|
+
defaultLogger = new Logger(opts);
|
|
293
|
+
return defaultLogger;
|
|
294
|
+
}
|
|
295
|
+
function sanitizeName(name) {
|
|
296
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
297
|
+
}
|
|
298
|
+
function getAuthDir() {
|
|
299
|
+
return path.join(getTooldDir(), "auth");
|
|
300
|
+
}
|
|
301
|
+
function getStorePath(serverName) {
|
|
302
|
+
return path.join(getAuthDir(), `${sanitizeName(serverName)}.json`);
|
|
303
|
+
}
|
|
304
|
+
function ensureAuthDir() {
|
|
305
|
+
fs.mkdirSync(getAuthDir(), {
|
|
306
|
+
recursive: true,
|
|
307
|
+
mode: 448
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
function readStore(serverName) {
|
|
311
|
+
const filePath = getStorePath(serverName);
|
|
312
|
+
try {
|
|
313
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
314
|
+
return JSON.parse(raw);
|
|
315
|
+
} catch {
|
|
316
|
+
return {};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
function writeStore(serverName, data) {
|
|
320
|
+
ensureAuthDir();
|
|
321
|
+
const filePath = getStorePath(serverName);
|
|
322
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 384 });
|
|
323
|
+
}
|
|
324
|
+
var TokenStore = class {
|
|
325
|
+
constructor(serverName) {
|
|
326
|
+
this.serverName = serverName;
|
|
327
|
+
}
|
|
328
|
+
getTokens() {
|
|
329
|
+
return readStore(this.serverName).tokens;
|
|
330
|
+
}
|
|
331
|
+
saveTokens(tokens) {
|
|
332
|
+
const data = readStore(this.serverName);
|
|
333
|
+
data.tokens = tokens;
|
|
334
|
+
writeStore(this.serverName, data);
|
|
335
|
+
}
|
|
336
|
+
getClientInformation() {
|
|
337
|
+
return readStore(this.serverName).clientInformation;
|
|
338
|
+
}
|
|
339
|
+
saveClientInformation(info) {
|
|
340
|
+
const data = readStore(this.serverName);
|
|
341
|
+
data.clientInformation = info;
|
|
342
|
+
writeStore(this.serverName, data);
|
|
343
|
+
}
|
|
344
|
+
getCodeVerifier() {
|
|
345
|
+
return readStore(this.serverName).codeVerifier;
|
|
346
|
+
}
|
|
347
|
+
saveCodeVerifier(verifier) {
|
|
348
|
+
const data = readStore(this.serverName);
|
|
349
|
+
data.codeVerifier = verifier;
|
|
350
|
+
writeStore(this.serverName, data);
|
|
351
|
+
}
|
|
352
|
+
clearTokens() {
|
|
353
|
+
const data = readStore(this.serverName);
|
|
354
|
+
delete data.tokens;
|
|
355
|
+
writeStore(this.serverName, data);
|
|
356
|
+
}
|
|
357
|
+
clearClientInformation() {
|
|
358
|
+
const data = readStore(this.serverName);
|
|
359
|
+
delete data.clientInformation;
|
|
360
|
+
writeStore(this.serverName, data);
|
|
361
|
+
}
|
|
362
|
+
clearCodeVerifier() {
|
|
363
|
+
const data = readStore(this.serverName);
|
|
364
|
+
delete data.codeVerifier;
|
|
365
|
+
writeStore(this.serverName, data);
|
|
366
|
+
}
|
|
367
|
+
clearAll() {
|
|
368
|
+
const filePath = getStorePath(this.serverName);
|
|
369
|
+
try {
|
|
370
|
+
fs.unlinkSync(filePath);
|
|
371
|
+
} catch {}
|
|
372
|
+
}
|
|
373
|
+
hasTokens() {
|
|
374
|
+
return readStore(this.serverName).tokens !== void 0;
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
var ClientCredentialsProvider = class {
|
|
378
|
+
store;
|
|
379
|
+
config;
|
|
380
|
+
constructor(config, serverName) {
|
|
381
|
+
this.config = config;
|
|
382
|
+
this.store = new TokenStore(serverName);
|
|
383
|
+
}
|
|
384
|
+
get redirectUrl() {}
|
|
385
|
+
get clientMetadata() {
|
|
386
|
+
return {
|
|
387
|
+
redirect_uris: [],
|
|
388
|
+
token_endpoint_auth_method: "client_secret_basic",
|
|
389
|
+
grant_types: ["client_credentials"],
|
|
390
|
+
response_types: [],
|
|
391
|
+
client_name: "toold"
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
clientInformation() {
|
|
395
|
+
return {
|
|
396
|
+
client_id: this.config.clientId,
|
|
397
|
+
client_secret: this.config.clientSecret
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
async tokens() {
|
|
401
|
+
return this.store.getTokens();
|
|
402
|
+
}
|
|
403
|
+
async saveTokens(tokens) {
|
|
404
|
+
this.store.saveTokens(tokens);
|
|
405
|
+
}
|
|
406
|
+
redirectToAuthorization() {
|
|
407
|
+
throw new Error("Client credentials flow does not use authorization redirects");
|
|
408
|
+
}
|
|
409
|
+
saveCodeVerifier() {}
|
|
410
|
+
codeVerifier() {
|
|
411
|
+
return "";
|
|
412
|
+
}
|
|
413
|
+
prepareTokenRequest(scope) {
|
|
414
|
+
const params = new URLSearchParams();
|
|
415
|
+
params.set("grant_type", "client_credentials");
|
|
416
|
+
const effectiveScope = scope ?? this.config.scope;
|
|
417
|
+
if (effectiveScope) params.set("scope", effectiveScope);
|
|
418
|
+
return params;
|
|
419
|
+
}
|
|
420
|
+
invalidateCredentials(scope) {
|
|
421
|
+
if (scope === "all" || scope === "tokens") this.store.clearTokens();
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
function exec(cmd, args) {
|
|
425
|
+
return new Promise((resolve) => {
|
|
426
|
+
execFile(cmd, args, { timeout: 5e3 }, (err) => {
|
|
427
|
+
resolve(!err);
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
function openUrl(url) {
|
|
432
|
+
const platform = process.platform;
|
|
433
|
+
if (platform === "darwin") execFile("open", [url]);
|
|
434
|
+
else if (platform === "win32") execFile("cmd", [
|
|
435
|
+
"/c",
|
|
436
|
+
"start",
|
|
437
|
+
"",
|
|
438
|
+
url
|
|
439
|
+
]);
|
|
440
|
+
else execFile("xdg-open", [url]);
|
|
441
|
+
}
|
|
442
|
+
function openBrowser(url) {
|
|
443
|
+
openUrl(url);
|
|
444
|
+
}
|
|
445
|
+
async function notifyReauth(serverName, authUrl) {
|
|
446
|
+
const title = "toold";
|
|
447
|
+
const message = `Server "${serverName}" needs re-authorization`;
|
|
448
|
+
const platform = process.platform;
|
|
449
|
+
if (platform === "darwin") {
|
|
450
|
+
if (await exec("terminal-notifier", [
|
|
451
|
+
"-title",
|
|
452
|
+
title,
|
|
453
|
+
"-message",
|
|
454
|
+
message,
|
|
455
|
+
"-open",
|
|
456
|
+
authUrl
|
|
457
|
+
])) return;
|
|
458
|
+
await exec("osascript", ["-e", `display notification "${message}" with title "${title}"`]);
|
|
459
|
+
openUrl(authUrl);
|
|
460
|
+
} else if (platform === "linux") {
|
|
461
|
+
await exec("notify-send", [title, message]);
|
|
462
|
+
openUrl(authUrl);
|
|
463
|
+
} else openUrl(authUrl);
|
|
464
|
+
}
|
|
465
|
+
var AuthorizationCodeProvider = class {
|
|
466
|
+
store;
|
|
467
|
+
config;
|
|
468
|
+
_redirectUrl;
|
|
469
|
+
hadTokensBefore = false;
|
|
470
|
+
constructor(config, serverName) {
|
|
471
|
+
this.serverName = serverName;
|
|
472
|
+
this.config = config;
|
|
473
|
+
this.store = new TokenStore(serverName);
|
|
474
|
+
this.hadTokensBefore = this.store.hasTokens();
|
|
475
|
+
}
|
|
476
|
+
setRedirectUrl(port) {
|
|
477
|
+
this._redirectUrl = `http://127.0.0.1:${port}/oauth/callback`;
|
|
478
|
+
}
|
|
479
|
+
get redirectUrl() {
|
|
480
|
+
return this._redirectUrl;
|
|
481
|
+
}
|
|
482
|
+
get clientMetadata() {
|
|
483
|
+
return {
|
|
484
|
+
redirect_uris: [this._redirectUrl ?? "http://127.0.0.1/oauth/callback"],
|
|
485
|
+
token_endpoint_auth_method: this.config.clientSecret ? "client_secret_basic" : "none",
|
|
486
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
487
|
+
response_types: ["code"],
|
|
488
|
+
client_name: "toold",
|
|
489
|
+
scope: this.config.scope
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
clientInformation() {
|
|
493
|
+
if (this.config.clientId) return {
|
|
494
|
+
client_id: this.config.clientId,
|
|
495
|
+
...this.config.clientSecret ? { client_secret: this.config.clientSecret } : {}
|
|
496
|
+
};
|
|
497
|
+
return this.store.getClientInformation();
|
|
498
|
+
}
|
|
499
|
+
saveClientInformation(info) {
|
|
500
|
+
this.store.saveClientInformation(info);
|
|
501
|
+
}
|
|
502
|
+
async tokens() {
|
|
503
|
+
return this.store.getTokens();
|
|
504
|
+
}
|
|
505
|
+
async saveTokens(tokens) {
|
|
506
|
+
this.store.saveTokens(tokens);
|
|
507
|
+
}
|
|
508
|
+
async redirectToAuthorization(authorizationUrl) {
|
|
509
|
+
const url = authorizationUrl.toString();
|
|
510
|
+
if (this.hadTokensBefore) {
|
|
511
|
+
getLogger().info(`Re-authorization needed for "${this.serverName}"`, this.serverName);
|
|
512
|
+
await notifyReauth(this.serverName, url);
|
|
513
|
+
} else {
|
|
514
|
+
getLogger().info(`Opening browser for authorization of "${this.serverName}"`, this.serverName);
|
|
515
|
+
openBrowser(url);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
saveCodeVerifier(codeVerifier) {
|
|
519
|
+
this.store.saveCodeVerifier(codeVerifier);
|
|
520
|
+
}
|
|
521
|
+
codeVerifier() {
|
|
522
|
+
return this.store.getCodeVerifier() ?? "";
|
|
523
|
+
}
|
|
524
|
+
invalidateCredentials(scope) {
|
|
525
|
+
if (scope === "all" || scope === "tokens") {
|
|
526
|
+
if (this.store.hasTokens()) this.hadTokensBefore = true;
|
|
527
|
+
}
|
|
528
|
+
switch (scope) {
|
|
529
|
+
case "all":
|
|
530
|
+
this.store.clearAll();
|
|
531
|
+
break;
|
|
532
|
+
case "client":
|
|
533
|
+
this.store.clearClientInformation();
|
|
534
|
+
break;
|
|
535
|
+
case "tokens":
|
|
536
|
+
this.store.clearTokens();
|
|
537
|
+
break;
|
|
538
|
+
case "verifier":
|
|
539
|
+
this.store.clearCodeVerifier();
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
const SUCCESS_HTML = `<!DOCTYPE html>
|
|
545
|
+
<html><head><title>toold - Authorization Successful</title></head>
|
|
546
|
+
<body style="font-family:system-ui;text-align:center;padding:60px">
|
|
547
|
+
<h1>Authorization Successful</h1>
|
|
548
|
+
<p>You can close this tab and return to toold.</p>
|
|
549
|
+
</body></html>`;
|
|
550
|
+
const ERROR_HTML = (msg) => `<!DOCTYPE html>
|
|
551
|
+
<html><head><title>toold - Authorization Error</title></head>
|
|
552
|
+
<body style="font-family:system-ui;text-align:center;padding:60px">
|
|
553
|
+
<h1>Authorization Error</h1>
|
|
554
|
+
<p>${msg}</p>
|
|
555
|
+
</body></html>`;
|
|
556
|
+
var CallbackServer = class {
|
|
557
|
+
server;
|
|
558
|
+
_port;
|
|
559
|
+
start(port = 0, timeoutMs = 3e5) {
|
|
560
|
+
return new Promise((resolve, reject) => {
|
|
561
|
+
let settled = false;
|
|
562
|
+
let timer;
|
|
563
|
+
const cleanup = () => {
|
|
564
|
+
if (timer) {
|
|
565
|
+
clearTimeout(timer);
|
|
566
|
+
timer = void 0;
|
|
567
|
+
}
|
|
568
|
+
this.close();
|
|
569
|
+
};
|
|
570
|
+
this.server = http.createServer((req, res) => {
|
|
571
|
+
if (settled) {
|
|
572
|
+
res.writeHead(400);
|
|
573
|
+
res.end();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const url = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
577
|
+
if (url.pathname !== "/oauth/callback") {
|
|
578
|
+
res.writeHead(404);
|
|
579
|
+
res.end("Not found");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const error = url.searchParams.get("error");
|
|
583
|
+
if (error) {
|
|
584
|
+
const desc = url.searchParams.get("error_description") ?? error;
|
|
585
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
586
|
+
res.end(ERROR_HTML(desc));
|
|
587
|
+
settled = true;
|
|
588
|
+
cleanup();
|
|
589
|
+
reject(/* @__PURE__ */ new Error(`OAuth error: ${desc}`));
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
const code = url.searchParams.get("code");
|
|
593
|
+
if (!code) {
|
|
594
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
595
|
+
res.end(ERROR_HTML("Missing authorization code"));
|
|
596
|
+
settled = true;
|
|
597
|
+
cleanup();
|
|
598
|
+
reject(/* @__PURE__ */ new Error("Missing authorization code in callback"));
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
const state = url.searchParams.get("state") ?? void 0;
|
|
602
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
603
|
+
res.end(SUCCESS_HTML);
|
|
604
|
+
settled = true;
|
|
605
|
+
cleanup();
|
|
606
|
+
resolve({
|
|
607
|
+
code,
|
|
608
|
+
state
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
this.server.listen(port, "127.0.0.1", () => {
|
|
612
|
+
const addr = this.server.address();
|
|
613
|
+
if (addr && typeof addr === "object") this._port = addr.port;
|
|
614
|
+
});
|
|
615
|
+
this.server.on("error", (err) => {
|
|
616
|
+
if (!settled) {
|
|
617
|
+
settled = true;
|
|
618
|
+
cleanup();
|
|
619
|
+
reject(err);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
timer = setTimeout(() => {
|
|
623
|
+
if (!settled) {
|
|
624
|
+
settled = true;
|
|
625
|
+
cleanup();
|
|
626
|
+
reject(/* @__PURE__ */ new Error("OAuth callback timed out"));
|
|
627
|
+
}
|
|
628
|
+
}, timeoutMs);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
get port() {
|
|
632
|
+
return this._port;
|
|
633
|
+
}
|
|
634
|
+
async waitForPort() {
|
|
635
|
+
if (this._port !== void 0) return this._port;
|
|
636
|
+
return new Promise((resolve, reject) => {
|
|
637
|
+
if (!this.server) {
|
|
638
|
+
reject(/* @__PURE__ */ new Error("Callback server not started"));
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
this.server.once("listening", () => {
|
|
642
|
+
const addr = this.server.address();
|
|
643
|
+
if (addr && typeof addr === "object") {
|
|
644
|
+
this._port = addr.port;
|
|
645
|
+
resolve(this._port);
|
|
646
|
+
} else reject(/* @__PURE__ */ new Error("Could not determine callback server port"));
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
close() {
|
|
651
|
+
if (this.server) {
|
|
652
|
+
this.server.close();
|
|
653
|
+
this.server = void 0;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
};
|
|
657
|
+
function createAuthProvider(config, serverName) {
|
|
658
|
+
switch (config.type) {
|
|
659
|
+
case "client_credentials": return new ClientCredentialsProvider(config, serverName);
|
|
660
|
+
case "authorization_code": return new AuthorizationCodeProvider(config, serverName);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
var ServerManager = class {
|
|
664
|
+
client;
|
|
665
|
+
transport;
|
|
666
|
+
status = "closed";
|
|
667
|
+
error;
|
|
668
|
+
serverInfo;
|
|
669
|
+
capabilities;
|
|
670
|
+
protocolVersion;
|
|
671
|
+
instructions;
|
|
672
|
+
tools = [];
|
|
673
|
+
resources = [];
|
|
674
|
+
prompts = [];
|
|
675
|
+
healthTimer;
|
|
676
|
+
consecutiveFailures = 0;
|
|
677
|
+
lastHealthCheck;
|
|
678
|
+
restartCount = 0;
|
|
679
|
+
restartTimer;
|
|
680
|
+
stopped = false;
|
|
681
|
+
connectTimeout;
|
|
682
|
+
healthCheckInterval;
|
|
683
|
+
maxRestartAttempts;
|
|
684
|
+
onHealthChange;
|
|
685
|
+
constructor(name, config, options) {
|
|
686
|
+
this.name = name;
|
|
687
|
+
this.config = config;
|
|
688
|
+
this.connectTimeout = options?.connectTimeout;
|
|
689
|
+
this.healthCheckInterval = options?.healthCheckInterval ?? 3e4;
|
|
690
|
+
this.maxRestartAttempts = options?.maxRestartAttempts ?? -1;
|
|
691
|
+
}
|
|
692
|
+
setHealthCallback(cb) {
|
|
693
|
+
this.onHealthChange = cb;
|
|
694
|
+
}
|
|
695
|
+
emitHealthChange(newStatus, error) {
|
|
696
|
+
if (this.onHealthChange) this.onHealthChange(this.name, newStatus, error);
|
|
697
|
+
}
|
|
698
|
+
async connect(connectTimeout) {
|
|
699
|
+
this.status = "connecting";
|
|
700
|
+
this.error = void 0;
|
|
701
|
+
const timeout = connectTimeout ?? this.connectTimeout;
|
|
702
|
+
try {
|
|
703
|
+
if (isStdioConfig(this.config)) this.transport = new StdioClientTransport({
|
|
704
|
+
command: this.config.command,
|
|
705
|
+
args: this.config.args,
|
|
706
|
+
env: this.config.env ? {
|
|
707
|
+
...process.env,
|
|
708
|
+
...this.config.env
|
|
709
|
+
} : void 0,
|
|
710
|
+
cwd: this.config.cwd
|
|
711
|
+
});
|
|
712
|
+
else if (isHttpConfig(this.config) && this.config.transport === "sse") {
|
|
713
|
+
if (this.config.auth?.type === "authorization_code") {
|
|
714
|
+
await this.connectWithOAuth(this.config, this.config.auth, timeout);
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
const opts = {};
|
|
718
|
+
if (this.config.headers) opts.requestInit = { headers: this.config.headers };
|
|
719
|
+
if (this.config.auth) opts.authProvider = createAuthProvider(this.config.auth, this.name);
|
|
720
|
+
this.transport = new SSEClientTransport(new URL(this.config.url), opts);
|
|
721
|
+
} else {
|
|
722
|
+
const httpConfig = this.config;
|
|
723
|
+
const opts = {};
|
|
724
|
+
if (httpConfig.headers) opts.requestInit = { headers: httpConfig.headers };
|
|
725
|
+
if (httpConfig.sessionId) opts.sessionId = httpConfig.sessionId;
|
|
726
|
+
if (httpConfig.reconnection) {
|
|
727
|
+
const r = httpConfig.reconnection;
|
|
728
|
+
opts.reconnectionOptions = {
|
|
729
|
+
maxReconnectionDelay: r.maxDelay ?? 3e4,
|
|
730
|
+
initialReconnectionDelay: r.initialDelay ?? 1e3,
|
|
731
|
+
reconnectionDelayGrowFactor: r.growFactor ?? 1.5,
|
|
732
|
+
maxRetries: r.maxRetries ?? 2
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (httpConfig.auth?.type === "authorization_code") {
|
|
736
|
+
await this.connectWithOAuth(httpConfig, httpConfig.auth, timeout);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (httpConfig.auth) opts.authProvider = createAuthProvider(httpConfig.auth, this.name);
|
|
740
|
+
this.transport = new StreamableHTTPClientTransport(new URL(httpConfig.url), opts);
|
|
741
|
+
}
|
|
742
|
+
this.client = this.createClient();
|
|
743
|
+
const requestOptions = timeout ? { signal: AbortSignal.timeout(timeout) } : void 0;
|
|
744
|
+
try {
|
|
745
|
+
await this.client.connect(this.transport, requestOptions);
|
|
746
|
+
} catch (err) {
|
|
747
|
+
if ((err instanceof UnauthorizedError || err instanceof StreamableHTTPError && err.code === 401 || err instanceof SseError && err.code === 401) && isHttpConfig(this.config) && !this.config.auth) {
|
|
748
|
+
getLogger().info("Server requires auth, initiating OAuth flow...", this.name);
|
|
749
|
+
await this.connectWithOAuth(this.config, { type: "authorization_code" }, timeout);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
throw err;
|
|
753
|
+
}
|
|
754
|
+
await this.finishConnect();
|
|
755
|
+
} catch (err) {
|
|
756
|
+
this.status = "error";
|
|
757
|
+
this.error = err instanceof Error ? err.message : String(err);
|
|
758
|
+
this.emitHealthChange("error", this.error);
|
|
759
|
+
getLogger().error(`Connection failed: ${this.error}`, this.name);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
createClient() {
|
|
763
|
+
const client = new Client({
|
|
764
|
+
name: "toold",
|
|
765
|
+
version: "0.1.0"
|
|
766
|
+
}, {
|
|
767
|
+
capabilities: { tasks: {
|
|
768
|
+
list: {},
|
|
769
|
+
cancel: {}
|
|
770
|
+
} },
|
|
771
|
+
listChanged: {
|
|
772
|
+
tools: {
|
|
773
|
+
autoRefresh: false,
|
|
774
|
+
onChanged: () => {
|
|
775
|
+
this.refreshTools().catch(() => {});
|
|
776
|
+
}
|
|
777
|
+
},
|
|
778
|
+
resources: {
|
|
779
|
+
autoRefresh: false,
|
|
780
|
+
onChanged: () => {
|
|
781
|
+
this.refreshResources().catch(() => {});
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
prompts: {
|
|
785
|
+
autoRefresh: false,
|
|
786
|
+
onChanged: () => {
|
|
787
|
+
this.refreshPrompts().catch(() => {});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
client.onclose = () => {
|
|
793
|
+
const previousStatus = this.status;
|
|
794
|
+
if (this.status !== "error") this.status = "closed";
|
|
795
|
+
if (previousStatus === "connected" && !this.stopped) {
|
|
796
|
+
getLogger().warn("Connection closed unexpectedly, will attempt restart", this.name);
|
|
797
|
+
this.emitHealthChange("closed");
|
|
798
|
+
this.scheduleRestart();
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
return client;
|
|
802
|
+
}
|
|
803
|
+
async finishConnect() {
|
|
804
|
+
if (!this.client) return;
|
|
805
|
+
this.capabilities = this.client.getServerCapabilities();
|
|
806
|
+
this.serverInfo = this.client.getServerVersion();
|
|
807
|
+
this.instructions = this.client.getInstructions();
|
|
808
|
+
this.protocolVersion = (this.transport instanceof StreamableHTTPClientTransport ? this.transport.protocolVersion : void 0) ?? LATEST_PROTOCOL_VERSION;
|
|
809
|
+
this.status = "connected";
|
|
810
|
+
this.consecutiveFailures = 0;
|
|
811
|
+
await this.refreshTools();
|
|
812
|
+
await this.refreshResources();
|
|
813
|
+
await this.refreshPrompts();
|
|
814
|
+
this.startHealthChecks();
|
|
815
|
+
this.emitHealthChange("connected");
|
|
816
|
+
getLogger().info("Connected successfully", this.name);
|
|
817
|
+
}
|
|
818
|
+
async connectWithOAuth(httpConfig, authConfig, timeout) {
|
|
819
|
+
const callbackServer = new CallbackServer();
|
|
820
|
+
try {
|
|
821
|
+
const callbackPromise = callbackServer.start(authConfig.callbackPort ?? 0);
|
|
822
|
+
const port = await callbackServer.waitForPort();
|
|
823
|
+
const authProvider = new AuthorizationCodeProvider(authConfig, this.name);
|
|
824
|
+
authProvider.setRedirectUrl(port);
|
|
825
|
+
const createTransport = () => {
|
|
826
|
+
if (httpConfig.transport === "sse") {
|
|
827
|
+
const opts = {};
|
|
828
|
+
if (httpConfig.headers) opts.requestInit = { headers: httpConfig.headers };
|
|
829
|
+
opts.authProvider = authProvider;
|
|
830
|
+
return new SSEClientTransport(new URL(httpConfig.url), opts);
|
|
831
|
+
}
|
|
832
|
+
const opts = {};
|
|
833
|
+
if (httpConfig.headers) opts.requestInit = { headers: httpConfig.headers };
|
|
834
|
+
if (httpConfig.sessionId) opts.sessionId = httpConfig.sessionId;
|
|
835
|
+
if (httpConfig.reconnection) {
|
|
836
|
+
const r = httpConfig.reconnection;
|
|
837
|
+
opts.reconnectionOptions = {
|
|
838
|
+
maxReconnectionDelay: r.maxDelay ?? 3e4,
|
|
839
|
+
initialReconnectionDelay: r.initialDelay ?? 1e3,
|
|
840
|
+
reconnectionDelayGrowFactor: r.growFactor ?? 1.5,
|
|
841
|
+
maxRetries: r.maxRetries ?? 2
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
opts.authProvider = authProvider;
|
|
845
|
+
return new StreamableHTTPClientTransport(new URL(httpConfig.url), opts);
|
|
846
|
+
};
|
|
847
|
+
let transport = createTransport();
|
|
848
|
+
this.transport = transport;
|
|
849
|
+
this.client = this.createClient();
|
|
850
|
+
const requestOptions = timeout ? { signal: AbortSignal.timeout(timeout) } : void 0;
|
|
851
|
+
try {
|
|
852
|
+
await this.client.connect(transport, requestOptions);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
if (err instanceof UnauthorizedError) {
|
|
855
|
+
getLogger().info("Authorization required, waiting for browser callback...", this.name);
|
|
856
|
+
const { code } = await callbackPromise;
|
|
857
|
+
await transport.finishAuth(code);
|
|
858
|
+
transport = createTransport();
|
|
859
|
+
this.transport = transport;
|
|
860
|
+
this.client = this.createClient();
|
|
861
|
+
await this.client.connect(transport, requestOptions);
|
|
862
|
+
} else throw err;
|
|
863
|
+
}
|
|
864
|
+
await this.finishConnect();
|
|
865
|
+
} finally {
|
|
866
|
+
callbackServer.close();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
async disconnect() {
|
|
870
|
+
this.stopped = true;
|
|
871
|
+
this.stopHealthChecks();
|
|
872
|
+
if (this.restartTimer) {
|
|
873
|
+
clearTimeout(this.restartTimer);
|
|
874
|
+
this.restartTimer = void 0;
|
|
875
|
+
}
|
|
876
|
+
if (this.client) await this.client.close();
|
|
877
|
+
this.status = "closed";
|
|
878
|
+
}
|
|
879
|
+
startHealthChecks() {
|
|
880
|
+
this.stopHealthChecks();
|
|
881
|
+
if (this.healthCheckInterval <= 0) return;
|
|
882
|
+
this.healthTimer = setInterval(() => {
|
|
883
|
+
this.performHealthCheck().catch(() => {});
|
|
884
|
+
}, this.healthCheckInterval);
|
|
885
|
+
}
|
|
886
|
+
stopHealthChecks() {
|
|
887
|
+
if (this.healthTimer) {
|
|
888
|
+
clearInterval(this.healthTimer);
|
|
889
|
+
this.healthTimer = void 0;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
async performHealthCheck() {
|
|
893
|
+
if (!this.client || this.status !== "connected") return;
|
|
894
|
+
this.lastHealthCheck = /* @__PURE__ */ new Date();
|
|
895
|
+
try {
|
|
896
|
+
await this.client.ping();
|
|
897
|
+
if (this.consecutiveFailures > 0) getLogger().info(`Health check recovered after ${this.consecutiveFailures} failures`, this.name);
|
|
898
|
+
this.consecutiveFailures = 0;
|
|
899
|
+
} catch (err) {
|
|
900
|
+
this.consecutiveFailures++;
|
|
901
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
902
|
+
getLogger().warn(`Health check failed (${this.consecutiveFailures} consecutive): ${msg}`, this.name);
|
|
903
|
+
if (this.consecutiveFailures >= 3) {
|
|
904
|
+
this.status = "error";
|
|
905
|
+
this.error = `Health check failed ${this.consecutiveFailures} times`;
|
|
906
|
+
this.emitHealthChange("error", this.error);
|
|
907
|
+
getLogger().error(`Marked as error after ${this.consecutiveFailures} failed pings`, this.name);
|
|
908
|
+
this.stopHealthChecks();
|
|
909
|
+
this.scheduleRestart();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
scheduleRestart() {
|
|
914
|
+
if (this.stopped) return;
|
|
915
|
+
if (this.maxRestartAttempts >= 0 && this.restartCount >= this.maxRestartAttempts) {
|
|
916
|
+
getLogger().error(`Max restart attempts (${this.maxRestartAttempts}) reached, giving up`, this.name);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const delay = Math.min(1e3 * Math.pow(2, this.restartCount), 6e4);
|
|
920
|
+
this.restartCount++;
|
|
921
|
+
getLogger().info(`Scheduling restart attempt ${this.restartCount} in ${delay}ms`, this.name);
|
|
922
|
+
this.restartTimer = setTimeout(() => {
|
|
923
|
+
this.restartTimer = void 0;
|
|
924
|
+
this.attemptRestart().catch(() => {});
|
|
925
|
+
}, delay);
|
|
926
|
+
}
|
|
927
|
+
async attemptRestart() {
|
|
928
|
+
if (this.stopped) return;
|
|
929
|
+
getLogger().info(`Attempting restart (attempt ${this.restartCount})`, this.name);
|
|
930
|
+
if (this.client) {
|
|
931
|
+
try {
|
|
932
|
+
await this.client.close();
|
|
933
|
+
} catch {}
|
|
934
|
+
this.client = void 0;
|
|
935
|
+
this.transport = void 0;
|
|
936
|
+
}
|
|
937
|
+
await this.connect(this.connectTimeout);
|
|
938
|
+
if (this.status === "connected") {
|
|
939
|
+
getLogger().info(`Restart successful after ${this.restartCount} attempts`, this.name);
|
|
940
|
+
this.restartCount = 0;
|
|
941
|
+
}
|
|
942
|
+
if (this.status === "error" && !this.stopped) this.scheduleRestart();
|
|
943
|
+
}
|
|
944
|
+
listTools() {
|
|
945
|
+
return this.tools;
|
|
946
|
+
}
|
|
947
|
+
async refreshTools() {
|
|
948
|
+
if (!this.client) return;
|
|
949
|
+
this.tools = (await this.client.listTools()).tools;
|
|
950
|
+
}
|
|
951
|
+
async callTool(name, args, timeout) {
|
|
952
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
953
|
+
const options = timeout ? { signal: AbortSignal.timeout(timeout) } : void 0;
|
|
954
|
+
return await this.client.callTool({
|
|
955
|
+
name,
|
|
956
|
+
arguments: args
|
|
957
|
+
}, void 0, options);
|
|
958
|
+
}
|
|
959
|
+
listResources() {
|
|
960
|
+
return this.resources;
|
|
961
|
+
}
|
|
962
|
+
async refreshResources() {
|
|
963
|
+
if (!this.client || !this.capabilities?.resources) return;
|
|
964
|
+
this.resources = (await this.client.listResources()).resources;
|
|
965
|
+
}
|
|
966
|
+
async readResource(uri) {
|
|
967
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
968
|
+
return await this.client.readResource({ uri });
|
|
969
|
+
}
|
|
970
|
+
listPrompts() {
|
|
971
|
+
return this.prompts;
|
|
972
|
+
}
|
|
973
|
+
async refreshPrompts() {
|
|
974
|
+
if (!this.client || !this.capabilities?.prompts) return;
|
|
975
|
+
this.prompts = (await this.client.listPrompts()).prompts;
|
|
976
|
+
}
|
|
977
|
+
async getPrompt(name, args) {
|
|
978
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
979
|
+
return await this.client.getPrompt({
|
|
980
|
+
name,
|
|
981
|
+
arguments: args
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
async complete(ref, argument) {
|
|
985
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
986
|
+
if (!this.capabilities?.completions) throw new Error(`Server "${this.name}" does not support completions`);
|
|
987
|
+
return await this.client.complete({
|
|
988
|
+
ref,
|
|
989
|
+
argument
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
async listTasks(cursor) {
|
|
993
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
994
|
+
if (!this.capabilities?.experimental?.tasks) throw new Error(`Server "${this.name}" does not support tasks`);
|
|
995
|
+
return await this.client.experimental.tasks.listTasks(cursor);
|
|
996
|
+
}
|
|
997
|
+
async getTask(taskId) {
|
|
998
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
999
|
+
return await this.client.experimental.tasks.getTask(taskId);
|
|
1000
|
+
}
|
|
1001
|
+
async getTaskResult(taskId) {
|
|
1002
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
1003
|
+
return await this.client.experimental.tasks.getTaskResult(taskId);
|
|
1004
|
+
}
|
|
1005
|
+
async cancelTask(taskId) {
|
|
1006
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
1007
|
+
return await this.client.experimental.tasks.cancelTask(taskId);
|
|
1008
|
+
}
|
|
1009
|
+
async callToolWithTask(name, args) {
|
|
1010
|
+
if (!this.client) throw new Error(`Server "${this.name}" is not connected`);
|
|
1011
|
+
const stream = this.client.experimental.tasks.callToolStream({
|
|
1012
|
+
name,
|
|
1013
|
+
arguments: args
|
|
1014
|
+
});
|
|
1015
|
+
for await (const message of stream) if (message.type === "taskCreated") return {
|
|
1016
|
+
taskId: message.task.taskId,
|
|
1017
|
+
status: message.task.status
|
|
1018
|
+
};
|
|
1019
|
+
throw new Error("No task created");
|
|
1020
|
+
}
|
|
1021
|
+
getStatus() {
|
|
1022
|
+
return this.status;
|
|
1023
|
+
}
|
|
1024
|
+
getAuthStatus() {
|
|
1025
|
+
if (!isHttpConfig(this.config)) return void 0;
|
|
1026
|
+
if (this.config.auth) {
|
|
1027
|
+
const store = new TokenStore(this.name);
|
|
1028
|
+
return {
|
|
1029
|
+
type: this.config.auth.type,
|
|
1030
|
+
hasTokens: store.hasTokens()
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
if (new TokenStore(this.name).hasTokens()) return {
|
|
1034
|
+
type: "authorization_code",
|
|
1035
|
+
hasTokens: true
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
getState() {
|
|
1039
|
+
return {
|
|
1040
|
+
name: this.name,
|
|
1041
|
+
config: this.config,
|
|
1042
|
+
status: this.status,
|
|
1043
|
+
error: this.error,
|
|
1044
|
+
serverInfo: this.serverInfo,
|
|
1045
|
+
capabilities: this.capabilities,
|
|
1046
|
+
protocolVersion: this.protocolVersion,
|
|
1047
|
+
instructions: this.instructions,
|
|
1048
|
+
restartCount: this.restartCount,
|
|
1049
|
+
lastHealthCheck: this.lastHealthCheck?.toISOString(),
|
|
1050
|
+
consecutiveFailures: this.consecutiveFailures
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
};
|
|
1054
|
+
var ServerPool = class {
|
|
1055
|
+
servers = /* @__PURE__ */ new Map();
|
|
1056
|
+
trackedTasks = /* @__PURE__ */ new Map();
|
|
1057
|
+
taskExpiryTimer;
|
|
1058
|
+
taskExpiryTimeout = 36e5;
|
|
1059
|
+
async connectAll(config) {
|
|
1060
|
+
const entries = Object.entries(config.mcpServers);
|
|
1061
|
+
this.taskExpiryTimeout = config.daemon?.taskExpiryTimeout ?? 36e5;
|
|
1062
|
+
for (const [name, serverConfig] of entries) {
|
|
1063
|
+
const manager = new ServerManager(name, serverConfig, {
|
|
1064
|
+
connectTimeout: config.daemon?.connectTimeout,
|
|
1065
|
+
healthCheckInterval: config.daemon?.healthCheckInterval,
|
|
1066
|
+
maxRestartAttempts: config.daemon?.maxRestartAttempts
|
|
1067
|
+
});
|
|
1068
|
+
manager.setHealthCallback((serverName, status, error) => {
|
|
1069
|
+
this.onServerHealthChange(serverName, status, error);
|
|
1070
|
+
});
|
|
1071
|
+
this.servers.set(name, manager);
|
|
1072
|
+
}
|
|
1073
|
+
const results = await Promise.allSettled([...this.servers.values()].map((manager) => manager.connect(config.daemon?.connectTimeout)));
|
|
1074
|
+
for (let i = 0; i < entries.length; i++) {
|
|
1075
|
+
const entry = entries[i];
|
|
1076
|
+
const result = results[i];
|
|
1077
|
+
if (entry && result && result.status === "rejected") getLogger().error(`Server "${entry[0]}" failed to connect: ${result.reason}`, entry[0]);
|
|
1078
|
+
}
|
|
1079
|
+
this.startTaskExpiry();
|
|
1080
|
+
}
|
|
1081
|
+
async disconnectAll() {
|
|
1082
|
+
this.stopTaskExpiry();
|
|
1083
|
+
await Promise.allSettled([...this.servers.values()].map((manager) => manager.disconnect()));
|
|
1084
|
+
}
|
|
1085
|
+
onServerHealthChange(serverName, status, error) {
|
|
1086
|
+
if (status === "closed" || status === "error") {
|
|
1087
|
+
for (const [taskId, task] of this.trackedTasks) if (task.server === serverName && task.status === "active") {
|
|
1088
|
+
task.status = "unreachable";
|
|
1089
|
+
getLogger().warn(`Task ${taskId} marked as unreachable (server ${serverName} disconnected)`, serverName);
|
|
1090
|
+
}
|
|
1091
|
+
} else if (status === "connected") getLogger().info(`Server reconnected`, serverName);
|
|
1092
|
+
if (error) getLogger().error(`Health change: ${status} - ${error}`, serverName);
|
|
1093
|
+
}
|
|
1094
|
+
trackTask(taskId, server) {
|
|
1095
|
+
this.trackedTasks.set(taskId, {
|
|
1096
|
+
taskId,
|
|
1097
|
+
server,
|
|
1098
|
+
status: "active",
|
|
1099
|
+
createdAt: Date.now()
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
getTrackedTask(taskId) {
|
|
1103
|
+
return this.trackedTasks.get(taskId);
|
|
1104
|
+
}
|
|
1105
|
+
startTaskExpiry() {
|
|
1106
|
+
this.stopTaskExpiry();
|
|
1107
|
+
this.taskExpiryTimer = setInterval(() => {
|
|
1108
|
+
const now = Date.now();
|
|
1109
|
+
for (const [taskId, task] of this.trackedTasks) if (now - task.createdAt > this.taskExpiryTimeout) {
|
|
1110
|
+
this.trackedTasks.delete(taskId);
|
|
1111
|
+
getLogger().info(`Expired stale task ${taskId}`, task.server);
|
|
1112
|
+
}
|
|
1113
|
+
}, 3e5);
|
|
1114
|
+
}
|
|
1115
|
+
stopTaskExpiry() {
|
|
1116
|
+
if (this.taskExpiryTimer) {
|
|
1117
|
+
clearInterval(this.taskExpiryTimer);
|
|
1118
|
+
this.taskExpiryTimer = void 0;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
getServer(name) {
|
|
1122
|
+
return this.servers.get(name);
|
|
1123
|
+
}
|
|
1124
|
+
listServers() {
|
|
1125
|
+
return [...this.servers.values()].map((manager) => manager.getState());
|
|
1126
|
+
}
|
|
1127
|
+
listAllTools(server) {
|
|
1128
|
+
if (server) {
|
|
1129
|
+
const manager = this.servers.get(server);
|
|
1130
|
+
if (!manager) return [];
|
|
1131
|
+
return manager.listTools().map((tool) => ({
|
|
1132
|
+
server,
|
|
1133
|
+
tool
|
|
1134
|
+
}));
|
|
1135
|
+
}
|
|
1136
|
+
const result = [];
|
|
1137
|
+
for (const [name, manager] of this.servers) {
|
|
1138
|
+
if (manager.getStatus() !== "connected") continue;
|
|
1139
|
+
for (const tool of manager.listTools()) result.push({
|
|
1140
|
+
server: name,
|
|
1141
|
+
tool
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
findTool(serverTool) {
|
|
1147
|
+
const slashIndex = serverTool.indexOf("/");
|
|
1148
|
+
if (slashIndex === -1) return void 0;
|
|
1149
|
+
const serverName = serverTool.slice(0, slashIndex);
|
|
1150
|
+
const toolName = serverTool.slice(slashIndex + 1);
|
|
1151
|
+
const manager = this.servers.get(serverName);
|
|
1152
|
+
if (!manager) return void 0;
|
|
1153
|
+
const tool = manager.listTools().find((t) => t.name === toolName);
|
|
1154
|
+
if (!tool) return void 0;
|
|
1155
|
+
return {
|
|
1156
|
+
manager,
|
|
1157
|
+
tool
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
grepTools(pattern) {
|
|
1161
|
+
const normalized = pattern.replace(/\\([|()\{\}])/g, "$1");
|
|
1162
|
+
const regex = new RegExp(normalized, "i");
|
|
1163
|
+
const result = [];
|
|
1164
|
+
for (const [name, manager] of this.servers) {
|
|
1165
|
+
if (manager.getStatus() !== "connected") continue;
|
|
1166
|
+
for (const tool of manager.listTools()) if (regex.test(`${name}/${tool.name}`) || tool.title && regex.test(tool.title) || tool.description && regex.test(tool.description)) result.push({
|
|
1167
|
+
server: name,
|
|
1168
|
+
tool
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
return result;
|
|
1172
|
+
}
|
|
1173
|
+
listAllResources(server) {
|
|
1174
|
+
if (server) {
|
|
1175
|
+
const manager = this.servers.get(server);
|
|
1176
|
+
if (!manager) return [];
|
|
1177
|
+
return manager.listResources().map((resource) => ({
|
|
1178
|
+
server,
|
|
1179
|
+
resource
|
|
1180
|
+
}));
|
|
1181
|
+
}
|
|
1182
|
+
const result = [];
|
|
1183
|
+
for (const [name, manager] of this.servers) {
|
|
1184
|
+
if (manager.getStatus() !== "connected") continue;
|
|
1185
|
+
for (const resource of manager.listResources()) result.push({
|
|
1186
|
+
server: name,
|
|
1187
|
+
resource
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1190
|
+
return result;
|
|
1191
|
+
}
|
|
1192
|
+
async readResource(serverName, uri) {
|
|
1193
|
+
const manager = this.servers.get(serverName);
|
|
1194
|
+
if (!manager) throw new Error(`Server not found: ${serverName}`);
|
|
1195
|
+
return await manager.readResource(uri);
|
|
1196
|
+
}
|
|
1197
|
+
listAllPrompts(server) {
|
|
1198
|
+
if (server) {
|
|
1199
|
+
const manager = this.servers.get(server);
|
|
1200
|
+
if (!manager) return [];
|
|
1201
|
+
return manager.listPrompts().map((prompt) => ({
|
|
1202
|
+
server,
|
|
1203
|
+
prompt
|
|
1204
|
+
}));
|
|
1205
|
+
}
|
|
1206
|
+
const result = [];
|
|
1207
|
+
for (const [name, manager] of this.servers) {
|
|
1208
|
+
if (manager.getStatus() !== "connected") continue;
|
|
1209
|
+
for (const prompt of manager.listPrompts()) result.push({
|
|
1210
|
+
server: name,
|
|
1211
|
+
prompt
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
return result;
|
|
1215
|
+
}
|
|
1216
|
+
async getPrompt(serverName, name, args) {
|
|
1217
|
+
const manager = this.servers.get(serverName);
|
|
1218
|
+
if (!manager) throw new Error(`Server not found: ${serverName}`);
|
|
1219
|
+
return await manager.getPrompt(name, args);
|
|
1220
|
+
}
|
|
1221
|
+
async complete(serverName, ref, argument) {
|
|
1222
|
+
const manager = this.servers.get(serverName);
|
|
1223
|
+
if (!manager) throw new Error(`Server not found: ${serverName}`);
|
|
1224
|
+
return await manager.complete(ref, argument);
|
|
1225
|
+
}
|
|
1226
|
+
async listAllTasks(server) {
|
|
1227
|
+
const result = [];
|
|
1228
|
+
const managers = server ? [[server, this.servers.get(server)]].filter(([, m]) => m) : [...this.servers.entries()];
|
|
1229
|
+
for (const [name, manager] of managers) {
|
|
1230
|
+
if (!manager || manager.getStatus() !== "connected") continue;
|
|
1231
|
+
try {
|
|
1232
|
+
const taskResult = await manager.listTasks();
|
|
1233
|
+
result.push({
|
|
1234
|
+
server: name,
|
|
1235
|
+
tasks: taskResult.tasks
|
|
1236
|
+
});
|
|
1237
|
+
} catch {}
|
|
1238
|
+
}
|
|
1239
|
+
return result;
|
|
1240
|
+
}
|
|
1241
|
+
async getTask(serverName, taskId) {
|
|
1242
|
+
const tracked = this.trackedTasks.get(taskId);
|
|
1243
|
+
if (tracked && tracked.status === "unreachable") throw new Error(`Task ${taskId} is unreachable: server "${tracked.server}" disconnected`);
|
|
1244
|
+
const manager = this.servers.get(serverName);
|
|
1245
|
+
if (!manager) throw new Error(`Server not found: ${serverName}`);
|
|
1246
|
+
return await manager.getTask(taskId);
|
|
1247
|
+
}
|
|
1248
|
+
async getTaskResult(serverName, taskId) {
|
|
1249
|
+
const tracked = this.trackedTasks.get(taskId);
|
|
1250
|
+
if (tracked && tracked.status === "unreachable") throw new Error(`Task ${taskId} is unreachable: server "${tracked.server}" disconnected`);
|
|
1251
|
+
const manager = this.servers.get(serverName);
|
|
1252
|
+
if (!manager) throw new Error(`Server not found: ${serverName}`);
|
|
1253
|
+
return await manager.getTaskResult(taskId);
|
|
1254
|
+
}
|
|
1255
|
+
async cancelTask(serverName, taskId) {
|
|
1256
|
+
const tracked = this.trackedTasks.get(taskId);
|
|
1257
|
+
if (tracked && tracked.status === "unreachable") throw new Error(`Task ${taskId} is unreachable: server "${tracked.server}" disconnected`);
|
|
1258
|
+
const manager = this.servers.get(serverName);
|
|
1259
|
+
if (!manager) throw new Error(`Server not found: ${serverName}`);
|
|
1260
|
+
const result = await manager.cancelTask(taskId);
|
|
1261
|
+
this.trackedTasks.delete(taskId);
|
|
1262
|
+
return result;
|
|
1263
|
+
}
|
|
1264
|
+
async reload(newConfig) {
|
|
1265
|
+
const oldNames = new Set(this.servers.keys());
|
|
1266
|
+
const newNames = new Set(Object.keys(newConfig.mcpServers));
|
|
1267
|
+
const added = [];
|
|
1268
|
+
const removed = [];
|
|
1269
|
+
const changed = [];
|
|
1270
|
+
for (const name of oldNames) if (!newNames.has(name)) {
|
|
1271
|
+
removed.push(name);
|
|
1272
|
+
const manager = this.servers.get(name);
|
|
1273
|
+
if (manager) await manager.disconnect().catch(() => {});
|
|
1274
|
+
this.servers.delete(name);
|
|
1275
|
+
}
|
|
1276
|
+
for (const [name, serverConfig] of Object.entries(newConfig.mcpServers)) if (!oldNames.has(name)) {
|
|
1277
|
+
added.push(name);
|
|
1278
|
+
const manager = new ServerManager(name, serverConfig, {
|
|
1279
|
+
connectTimeout: newConfig.daemon?.connectTimeout,
|
|
1280
|
+
healthCheckInterval: newConfig.daemon?.healthCheckInterval,
|
|
1281
|
+
maxRestartAttempts: newConfig.daemon?.maxRestartAttempts
|
|
1282
|
+
});
|
|
1283
|
+
manager.setHealthCallback((serverName, status, error) => {
|
|
1284
|
+
this.onServerHealthChange(serverName, status, error);
|
|
1285
|
+
});
|
|
1286
|
+
this.servers.set(name, manager);
|
|
1287
|
+
await manager.connect(newConfig.daemon?.connectTimeout).catch(() => {});
|
|
1288
|
+
} else {
|
|
1289
|
+
const oldState = this.servers.get(name).getState();
|
|
1290
|
+
if (JSON.stringify(oldState.config) !== JSON.stringify(serverConfig)) {
|
|
1291
|
+
changed.push(name);
|
|
1292
|
+
await this.servers.get(name).disconnect().catch(() => {});
|
|
1293
|
+
const newManager = new ServerManager(name, serverConfig, {
|
|
1294
|
+
connectTimeout: newConfig.daemon?.connectTimeout,
|
|
1295
|
+
healthCheckInterval: newConfig.daemon?.healthCheckInterval,
|
|
1296
|
+
maxRestartAttempts: newConfig.daemon?.maxRestartAttempts
|
|
1297
|
+
});
|
|
1298
|
+
newManager.setHealthCallback((serverName, status, error) => {
|
|
1299
|
+
this.onServerHealthChange(serverName, status, error);
|
|
1300
|
+
});
|
|
1301
|
+
this.servers.set(name, newManager);
|
|
1302
|
+
await newManager.connect(newConfig.daemon?.connectTimeout).catch(() => {});
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
added,
|
|
1307
|
+
removed,
|
|
1308
|
+
changed
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
async function generateTypes(tools) {
|
|
1313
|
+
const toolTypes = [];
|
|
1314
|
+
for (const { server, tool } of tools) {
|
|
1315
|
+
const qualifiedName = escapeKey(`${server}/${tool.name}`);
|
|
1316
|
+
const inputType = await schemaToType(tool.inputSchema, `${server}_${tool.name}_input`);
|
|
1317
|
+
const outputType = tool.outputSchema ? await schemaToType(tool.outputSchema, `${server}_${tool.name}_output`) : "unknown";
|
|
1318
|
+
const jsdoc = tool.description ? ` /** ${escapeJsdoc(tool.description)} */\n` : "";
|
|
1319
|
+
toolTypes.push(`${jsdoc} '${qualifiedName}': {\n input: ${indent(inputType, 6)};\n output: ${indent(outputType, 6)};\n };`);
|
|
1320
|
+
}
|
|
1321
|
+
return `// Auto-generated by \`toold typegen\` – do not edit
|
|
1322
|
+
// Run \`toold typegen\` to regenerate
|
|
1323
|
+
|
|
1324
|
+
declare module 'toold' {
|
|
1325
|
+
interface TooldToolMap {
|
|
1326
|
+
${toolTypes.join("\n")}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
export {};
|
|
1331
|
+
`;
|
|
1332
|
+
}
|
|
1333
|
+
async function schemaToType(schema, name) {
|
|
1334
|
+
return extractTypeBody(await compile(schema, name, {
|
|
1335
|
+
bannerComment: "",
|
|
1336
|
+
additionalProperties: false,
|
|
1337
|
+
unknownAny: true,
|
|
1338
|
+
format: false
|
|
1339
|
+
}));
|
|
1340
|
+
}
|
|
1341
|
+
function extractTypeBody(compiled) {
|
|
1342
|
+
const match = compiled.match(/\{[\s\S]*\}/);
|
|
1343
|
+
if (!match) return "Record<string, unknown>";
|
|
1344
|
+
return match[0];
|
|
1345
|
+
}
|
|
1346
|
+
function escapeKey(key) {
|
|
1347
|
+
return key.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
1348
|
+
}
|
|
1349
|
+
function escapeJsdoc(text) {
|
|
1350
|
+
return text.replace(/\*\//g, "*\\/");
|
|
1351
|
+
}
|
|
1352
|
+
function indent(text, spaces) {
|
|
1353
|
+
const pad = " ".repeat(spaces);
|
|
1354
|
+
return text.split("\n").join("\n" + pad);
|
|
1355
|
+
}
|
|
1356
|
+
function createDaemonServer(serverPool, config) {
|
|
1357
|
+
const socketPath = getSocketPath();
|
|
1358
|
+
let idleTimer;
|
|
1359
|
+
let shutdownInProgress = false;
|
|
1360
|
+
const activeSockets = /* @__PURE__ */ new Set();
|
|
1361
|
+
let inFlightRequests = 0;
|
|
1362
|
+
const idleTimeout = config.daemon?.idleTimeout ?? 3e5;
|
|
1363
|
+
const requestTimeout = config.daemon?.requestTimeout ?? 6e4;
|
|
1364
|
+
config.daemon?.maxTotalTimeout;
|
|
1365
|
+
const shutdownTimeout = config.daemon?.shutdownTimeout ?? 1e4;
|
|
1366
|
+
const startTime = Date.now();
|
|
1367
|
+
const logger = getLogger();
|
|
1368
|
+
function resetIdleTimer() {
|
|
1369
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1370
|
+
if (idleTimeout > 0) idleTimer = setTimeout(() => {
|
|
1371
|
+
logger.info("Idle timeout reached, shutting down");
|
|
1372
|
+
shutdown().catch(() => {});
|
|
1373
|
+
}, idleTimeout);
|
|
1374
|
+
}
|
|
1375
|
+
async function handleRequest(request, clientTimeout) {
|
|
1376
|
+
const { method, params, id } = request;
|
|
1377
|
+
switch (method) {
|
|
1378
|
+
case "servers/list": return {
|
|
1379
|
+
jsonrpc: "2.0",
|
|
1380
|
+
id,
|
|
1381
|
+
result: serverPool.listServers()
|
|
1382
|
+
};
|
|
1383
|
+
case "tools/list": {
|
|
1384
|
+
const server = params?.server;
|
|
1385
|
+
return {
|
|
1386
|
+
jsonrpc: "2.0",
|
|
1387
|
+
id,
|
|
1388
|
+
result: serverPool.listAllTools(server)
|
|
1389
|
+
};
|
|
1390
|
+
}
|
|
1391
|
+
case "tools/call": {
|
|
1392
|
+
const p = params;
|
|
1393
|
+
if (!p?.name) return {
|
|
1394
|
+
jsonrpc: "2.0",
|
|
1395
|
+
id,
|
|
1396
|
+
error: {
|
|
1397
|
+
code: -32602,
|
|
1398
|
+
message: "Missing required parameter: name"
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
const found = serverPool.findTool(p.name);
|
|
1402
|
+
if (!found) return {
|
|
1403
|
+
jsonrpc: "2.0",
|
|
1404
|
+
id,
|
|
1405
|
+
error: {
|
|
1406
|
+
code: -32602,
|
|
1407
|
+
message: `Tool not found: ${p.name}`
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
const timeout = clientTimeout ?? p.timeout ?? requestTimeout;
|
|
1411
|
+
return {
|
|
1412
|
+
jsonrpc: "2.0",
|
|
1413
|
+
id,
|
|
1414
|
+
result: await found.manager.callTool(found.tool.name, p.arguments ?? {}, timeout)
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
case "tools/info": {
|
|
1418
|
+
const p = params;
|
|
1419
|
+
if (!p?.name) return {
|
|
1420
|
+
jsonrpc: "2.0",
|
|
1421
|
+
id,
|
|
1422
|
+
error: {
|
|
1423
|
+
code: -32602,
|
|
1424
|
+
message: "Missing required parameter: name"
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
const found = serverPool.findTool(p.name);
|
|
1428
|
+
if (!found) return {
|
|
1429
|
+
jsonrpc: "2.0",
|
|
1430
|
+
id,
|
|
1431
|
+
error: {
|
|
1432
|
+
code: -32602,
|
|
1433
|
+
message: `Tool not found: ${p.name}`
|
|
1434
|
+
}
|
|
1435
|
+
};
|
|
1436
|
+
return {
|
|
1437
|
+
jsonrpc: "2.0",
|
|
1438
|
+
id,
|
|
1439
|
+
result: found.tool
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
case "auth/status": {
|
|
1443
|
+
const server = params?.server;
|
|
1444
|
+
const results = [];
|
|
1445
|
+
if (server) {
|
|
1446
|
+
const manager = serverPool.getServer(server);
|
|
1447
|
+
if (!manager) return {
|
|
1448
|
+
jsonrpc: "2.0",
|
|
1449
|
+
id,
|
|
1450
|
+
error: {
|
|
1451
|
+
code: -32602,
|
|
1452
|
+
message: `Server not found: ${server}`
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
const authStatus = manager.getAuthStatus();
|
|
1456
|
+
if (authStatus) results.push({
|
|
1457
|
+
server,
|
|
1458
|
+
auth: authStatus
|
|
1459
|
+
});
|
|
1460
|
+
} else for (const state of serverPool.listServers()) {
|
|
1461
|
+
const manager = serverPool.getServer(state.name);
|
|
1462
|
+
if (manager) {
|
|
1463
|
+
const authStatus = manager.getAuthStatus();
|
|
1464
|
+
if (authStatus) results.push({
|
|
1465
|
+
server: state.name,
|
|
1466
|
+
auth: authStatus
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
jsonrpc: "2.0",
|
|
1472
|
+
id,
|
|
1473
|
+
result: results
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
case "daemon/status": return {
|
|
1477
|
+
jsonrpc: "2.0",
|
|
1478
|
+
id,
|
|
1479
|
+
result: {
|
|
1480
|
+
pid: process.pid,
|
|
1481
|
+
uptime: Date.now() - startTime,
|
|
1482
|
+
serverCount: serverPool.listServers().length,
|
|
1483
|
+
servers: serverPool.listServers()
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
case "tools/grep": {
|
|
1487
|
+
const p = params;
|
|
1488
|
+
if (!p?.pattern) return {
|
|
1489
|
+
jsonrpc: "2.0",
|
|
1490
|
+
id,
|
|
1491
|
+
error: {
|
|
1492
|
+
code: -32602,
|
|
1493
|
+
message: "Missing required parameter: pattern"
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
try {
|
|
1497
|
+
return {
|
|
1498
|
+
jsonrpc: "2.0",
|
|
1499
|
+
id,
|
|
1500
|
+
result: serverPool.grepTools(p.pattern)
|
|
1501
|
+
};
|
|
1502
|
+
} catch (err) {
|
|
1503
|
+
return {
|
|
1504
|
+
jsonrpc: "2.0",
|
|
1505
|
+
id,
|
|
1506
|
+
error: {
|
|
1507
|
+
code: -32602,
|
|
1508
|
+
message: err instanceof Error ? err.message : "Invalid pattern"
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
case "resources/list": {
|
|
1514
|
+
const server = params?.server;
|
|
1515
|
+
return {
|
|
1516
|
+
jsonrpc: "2.0",
|
|
1517
|
+
id,
|
|
1518
|
+
result: serverPool.listAllResources(server)
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
case "resources/read": {
|
|
1522
|
+
const p = params;
|
|
1523
|
+
if (!p?.server || !p?.uri) return {
|
|
1524
|
+
jsonrpc: "2.0",
|
|
1525
|
+
id,
|
|
1526
|
+
error: {
|
|
1527
|
+
code: -32602,
|
|
1528
|
+
message: "Missing required parameters: server, uri"
|
|
1529
|
+
}
|
|
1530
|
+
};
|
|
1531
|
+
return {
|
|
1532
|
+
jsonrpc: "2.0",
|
|
1533
|
+
id,
|
|
1534
|
+
result: await serverPool.readResource(p.server, p.uri)
|
|
1535
|
+
};
|
|
1536
|
+
}
|
|
1537
|
+
case "prompts/list": {
|
|
1538
|
+
const server = params?.server;
|
|
1539
|
+
return {
|
|
1540
|
+
jsonrpc: "2.0",
|
|
1541
|
+
id,
|
|
1542
|
+
result: serverPool.listAllPrompts(server)
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
case "prompts/get": {
|
|
1546
|
+
const p = params;
|
|
1547
|
+
if (!p?.server || !p?.name) return {
|
|
1548
|
+
jsonrpc: "2.0",
|
|
1549
|
+
id,
|
|
1550
|
+
error: {
|
|
1551
|
+
code: -32602,
|
|
1552
|
+
message: "Missing required parameters: server, name"
|
|
1553
|
+
}
|
|
1554
|
+
};
|
|
1555
|
+
return {
|
|
1556
|
+
jsonrpc: "2.0",
|
|
1557
|
+
id,
|
|
1558
|
+
result: await serverPool.getPrompt(p.server, p.name, p.arguments)
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
case "completions/complete": {
|
|
1562
|
+
const p = params;
|
|
1563
|
+
if (!p?.server || !p?.ref || !p?.argument) return {
|
|
1564
|
+
jsonrpc: "2.0",
|
|
1565
|
+
id,
|
|
1566
|
+
error: {
|
|
1567
|
+
code: -32602,
|
|
1568
|
+
message: "Missing required parameters: server, ref, argument"
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
return {
|
|
1572
|
+
jsonrpc: "2.0",
|
|
1573
|
+
id,
|
|
1574
|
+
result: await serverPool.complete(p.server, p.ref, p.argument)
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
case "tasks/list": {
|
|
1578
|
+
const server = params?.server;
|
|
1579
|
+
return {
|
|
1580
|
+
jsonrpc: "2.0",
|
|
1581
|
+
id,
|
|
1582
|
+
result: await serverPool.listAllTasks(server)
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
case "tasks/get": {
|
|
1586
|
+
const p = params;
|
|
1587
|
+
if (!p?.server || !p?.taskId) return {
|
|
1588
|
+
jsonrpc: "2.0",
|
|
1589
|
+
id,
|
|
1590
|
+
error: {
|
|
1591
|
+
code: -32602,
|
|
1592
|
+
message: "Missing required parameters: server, taskId"
|
|
1593
|
+
}
|
|
1594
|
+
};
|
|
1595
|
+
return {
|
|
1596
|
+
jsonrpc: "2.0",
|
|
1597
|
+
id,
|
|
1598
|
+
result: await serverPool.getTask(p.server, p.taskId)
|
|
1599
|
+
};
|
|
1600
|
+
}
|
|
1601
|
+
case "tasks/result": {
|
|
1602
|
+
const p = params;
|
|
1603
|
+
if (!p?.server || !p?.taskId) return {
|
|
1604
|
+
jsonrpc: "2.0",
|
|
1605
|
+
id,
|
|
1606
|
+
error: {
|
|
1607
|
+
code: -32602,
|
|
1608
|
+
message: "Missing required parameters: server, taskId"
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
return {
|
|
1612
|
+
jsonrpc: "2.0",
|
|
1613
|
+
id,
|
|
1614
|
+
result: await serverPool.getTaskResult(p.server, p.taskId)
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
case "tasks/cancel": {
|
|
1618
|
+
const p = params;
|
|
1619
|
+
if (!p?.server || !p?.taskId) return {
|
|
1620
|
+
jsonrpc: "2.0",
|
|
1621
|
+
id,
|
|
1622
|
+
error: {
|
|
1623
|
+
code: -32602,
|
|
1624
|
+
message: "Missing required parameters: server, taskId"
|
|
1625
|
+
}
|
|
1626
|
+
};
|
|
1627
|
+
return {
|
|
1628
|
+
jsonrpc: "2.0",
|
|
1629
|
+
id,
|
|
1630
|
+
result: await serverPool.cancelTask(p.server, p.taskId)
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
case "tools/call-async": {
|
|
1634
|
+
const p = params;
|
|
1635
|
+
if (!p?.name) return {
|
|
1636
|
+
jsonrpc: "2.0",
|
|
1637
|
+
id,
|
|
1638
|
+
error: {
|
|
1639
|
+
code: -32602,
|
|
1640
|
+
message: "Missing required parameter: name"
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
const found = serverPool.findTool(p.name);
|
|
1644
|
+
if (!found) return {
|
|
1645
|
+
jsonrpc: "2.0",
|
|
1646
|
+
id,
|
|
1647
|
+
error: {
|
|
1648
|
+
code: -32602,
|
|
1649
|
+
message: `Tool not found: ${p.name}`
|
|
1650
|
+
}
|
|
1651
|
+
};
|
|
1652
|
+
const taskHandle = await found.manager.callToolWithTask(found.tool.name, p.arguments ?? {});
|
|
1653
|
+
serverPool.trackTask(taskHandle.taskId, found.manager.name);
|
|
1654
|
+
return {
|
|
1655
|
+
jsonrpc: "2.0",
|
|
1656
|
+
id,
|
|
1657
|
+
result: {
|
|
1658
|
+
...taskHandle,
|
|
1659
|
+
server: found.manager.name
|
|
1660
|
+
}
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
case "config/reload": {
|
|
1664
|
+
const newConfig = loadConfig(params?.configPath);
|
|
1665
|
+
const changes = await serverPool.reload(newConfig);
|
|
1666
|
+
logger.info(`Config reloaded: added=${changes.added.length}, removed=${changes.removed.length}, changed=${changes.changed.length}`);
|
|
1667
|
+
return {
|
|
1668
|
+
jsonrpc: "2.0",
|
|
1669
|
+
id,
|
|
1670
|
+
result: changes
|
|
1671
|
+
};
|
|
1672
|
+
}
|
|
1673
|
+
case "daemon/stop":
|
|
1674
|
+
setImmediate(() => {
|
|
1675
|
+
shutdown().catch(() => {});
|
|
1676
|
+
});
|
|
1677
|
+
return {
|
|
1678
|
+
jsonrpc: "2.0",
|
|
1679
|
+
id,
|
|
1680
|
+
result: { ok: true }
|
|
1681
|
+
};
|
|
1682
|
+
default: return {
|
|
1683
|
+
jsonrpc: "2.0",
|
|
1684
|
+
id,
|
|
1685
|
+
error: {
|
|
1686
|
+
code: -32601,
|
|
1687
|
+
message: `Method not found: ${method}`
|
|
1688
|
+
}
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
const server = net.createServer((socket) => {
|
|
1693
|
+
if (shutdownInProgress) {
|
|
1694
|
+
socket.destroy();
|
|
1695
|
+
return;
|
|
1696
|
+
}
|
|
1697
|
+
activeSockets.add(socket);
|
|
1698
|
+
let buffer = "";
|
|
1699
|
+
socket.on("close", () => {
|
|
1700
|
+
activeSockets.delete(socket);
|
|
1701
|
+
});
|
|
1702
|
+
socket.on("data", (data) => {
|
|
1703
|
+
buffer += data.toString();
|
|
1704
|
+
const lines = buffer.split("\n");
|
|
1705
|
+
buffer = lines.pop() ?? "";
|
|
1706
|
+
for (const line of lines) {
|
|
1707
|
+
const trimmed = line.trim();
|
|
1708
|
+
if (!trimmed) continue;
|
|
1709
|
+
resetIdleTimer();
|
|
1710
|
+
let request;
|
|
1711
|
+
try {
|
|
1712
|
+
request = JSON.parse(trimmed);
|
|
1713
|
+
} catch {
|
|
1714
|
+
socket.write(JSON.stringify({
|
|
1715
|
+
jsonrpc: "2.0",
|
|
1716
|
+
id: null,
|
|
1717
|
+
error: {
|
|
1718
|
+
code: -32700,
|
|
1719
|
+
message: "Parse error"
|
|
1720
|
+
}
|
|
1721
|
+
}) + "\n");
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
inFlightRequests++;
|
|
1725
|
+
const clientTimeout = request.params?.timeout;
|
|
1726
|
+
handleRequest(request, clientTimeout).then((response) => {
|
|
1727
|
+
if (!socket.destroyed) socket.write(JSON.stringify(response) + "\n");
|
|
1728
|
+
}).catch((err) => {
|
|
1729
|
+
const errorResponse = {
|
|
1730
|
+
jsonrpc: "2.0",
|
|
1731
|
+
id: request.id,
|
|
1732
|
+
error: {
|
|
1733
|
+
code: -32603,
|
|
1734
|
+
message: err instanceof Error ? err.message : "Internal error"
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
if (!socket.destroyed) socket.write(JSON.stringify(errorResponse) + "\n");
|
|
1738
|
+
}).finally(() => {
|
|
1739
|
+
inFlightRequests--;
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
});
|
|
1743
|
+
socket.on("error", () => {
|
|
1744
|
+
activeSockets.delete(socket);
|
|
1745
|
+
});
|
|
1746
|
+
});
|
|
1747
|
+
async function shutdown() {
|
|
1748
|
+
if (shutdownInProgress) return;
|
|
1749
|
+
shutdownInProgress = true;
|
|
1750
|
+
logger.info("Shutting down daemon...");
|
|
1751
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1752
|
+
server.close();
|
|
1753
|
+
if (inFlightRequests > 0) {
|
|
1754
|
+
logger.info(`Waiting for ${inFlightRequests} in-flight requests to complete...`);
|
|
1755
|
+
await Promise.race([new Promise((resolve) => {
|
|
1756
|
+
const check = setInterval(() => {
|
|
1757
|
+
if (inFlightRequests <= 0) {
|
|
1758
|
+
clearInterval(check);
|
|
1759
|
+
resolve();
|
|
1760
|
+
}
|
|
1761
|
+
}, 100);
|
|
1762
|
+
}), new Promise((resolve) => setTimeout(resolve, shutdownTimeout))]);
|
|
1763
|
+
if (inFlightRequests > 0) logger.warn(`Forcing shutdown with ${inFlightRequests} requests still in flight`);
|
|
1764
|
+
}
|
|
1765
|
+
for (const socket of activeSockets) socket.destroy();
|
|
1766
|
+
activeSockets.clear();
|
|
1767
|
+
await serverPool.disconnectAll();
|
|
1768
|
+
logger.info("All servers disconnected");
|
|
1769
|
+
try {
|
|
1770
|
+
fs.unlinkSync(socketPath);
|
|
1771
|
+
} catch {}
|
|
1772
|
+
try {
|
|
1773
|
+
const { getPidPath } = await Promise.resolve().then(() => paths_exports);
|
|
1774
|
+
fs.unlinkSync(getPidPath());
|
|
1775
|
+
} catch {}
|
|
1776
|
+
logger.info("Daemon shutdown complete");
|
|
1777
|
+
process.exit(0);
|
|
1778
|
+
}
|
|
1779
|
+
try {
|
|
1780
|
+
fs.unlinkSync(socketPath);
|
|
1781
|
+
} catch {}
|
|
1782
|
+
server.listen(socketPath);
|
|
1783
|
+
resetIdleTimer();
|
|
1784
|
+
return {
|
|
1785
|
+
server,
|
|
1786
|
+
resetIdleTimer,
|
|
1787
|
+
shutdown,
|
|
1788
|
+
handleRequest
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
const ALLOWED_ORIGIN_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
|
|
1792
|
+
function isOriginAllowed(origin) {
|
|
1793
|
+
return ALLOWED_ORIGIN_RE.test(origin);
|
|
1794
|
+
}
|
|
1795
|
+
function createHttpListener(handleRequest, config) {
|
|
1796
|
+
const logger = getLogger();
|
|
1797
|
+
const httpServer = http.createServer((req, res) => {
|
|
1798
|
+
const origin = req.headers.origin;
|
|
1799
|
+
if (origin && !isOriginAllowed(origin)) {
|
|
1800
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1801
|
+
res.end(JSON.stringify({
|
|
1802
|
+
jsonrpc: "2.0",
|
|
1803
|
+
id: null,
|
|
1804
|
+
error: {
|
|
1805
|
+
code: -32600,
|
|
1806
|
+
message: "Forbidden origin"
|
|
1807
|
+
}
|
|
1808
|
+
}));
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
if (req.method !== "POST") {
|
|
1812
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
1813
|
+
res.end(JSON.stringify({
|
|
1814
|
+
jsonrpc: "2.0",
|
|
1815
|
+
id: null,
|
|
1816
|
+
error: {
|
|
1817
|
+
code: -32600,
|
|
1818
|
+
message: "Method not allowed"
|
|
1819
|
+
}
|
|
1820
|
+
}));
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
let body = "";
|
|
1824
|
+
req.on("data", (chunk) => {
|
|
1825
|
+
body += chunk.toString();
|
|
1826
|
+
});
|
|
1827
|
+
req.on("end", () => {
|
|
1828
|
+
let request;
|
|
1829
|
+
try {
|
|
1830
|
+
request = JSON.parse(body);
|
|
1831
|
+
} catch {
|
|
1832
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1833
|
+
res.end(JSON.stringify({
|
|
1834
|
+
jsonrpc: "2.0",
|
|
1835
|
+
id: null,
|
|
1836
|
+
error: {
|
|
1837
|
+
code: -32700,
|
|
1838
|
+
message: "Parse error"
|
|
1839
|
+
}
|
|
1840
|
+
}));
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
const clientTimeout = request.params?.timeout;
|
|
1844
|
+
handleRequest(request, clientTimeout).then((response) => {
|
|
1845
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1846
|
+
res.end(JSON.stringify(response));
|
|
1847
|
+
}).catch((err) => {
|
|
1848
|
+
const errorResponse = {
|
|
1849
|
+
jsonrpc: "2.0",
|
|
1850
|
+
id: request.id,
|
|
1851
|
+
error: {
|
|
1852
|
+
code: -32603,
|
|
1853
|
+
message: err instanceof Error ? err.message : "Internal error"
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1857
|
+
res.end(JSON.stringify(errorResponse));
|
|
1858
|
+
});
|
|
1859
|
+
});
|
|
1860
|
+
});
|
|
1861
|
+
httpServer.listen(config.port, config.host, () => {
|
|
1862
|
+
logger.info(`HTTP listener on ${config.host}:${config.port}`);
|
|
1863
|
+
});
|
|
1864
|
+
function shutdown() {
|
|
1865
|
+
return new Promise((resolve) => {
|
|
1866
|
+
httpServer.close(() => resolve());
|
|
1867
|
+
});
|
|
1868
|
+
}
|
|
1869
|
+
return {
|
|
1870
|
+
httpServer,
|
|
1871
|
+
shutdown
|
|
1872
|
+
};
|
|
1873
|
+
}
|
|
1874
|
+
async function runAutoTypegen(serverPool, logger) {
|
|
1875
|
+
try {
|
|
1876
|
+
const require = createRequire(path.resolve("package.json"));
|
|
1877
|
+
const tooldPkgDir = path.dirname(require.resolve("toold/package.json"));
|
|
1878
|
+
const outputPath = path.join(tooldPkgDir, "toold.generated.d.ts");
|
|
1879
|
+
const tools = serverPool.listAllTools();
|
|
1880
|
+
if (tools.length === 0) {
|
|
1881
|
+
logger.debug("No tools available, skipping typegen");
|
|
1882
|
+
return;
|
|
1883
|
+
}
|
|
1884
|
+
const content = await generateTypes(tools);
|
|
1885
|
+
fs.writeFileSync(outputPath, content, "utf-8");
|
|
1886
|
+
logger.info(`Auto-generated ${tools.length} tool types → ${outputPath}`);
|
|
1887
|
+
} catch {
|
|
1888
|
+
logger.debug("Skipping auto-typegen (toold not resolvable as a dependency)");
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
async function startDaemon(configPath) {
|
|
1892
|
+
const config = loadConfig(configPath);
|
|
1893
|
+
ensureTooldDir();
|
|
1894
|
+
const isForeground = !!process.send;
|
|
1895
|
+
const logger = initLogger({
|
|
1896
|
+
level: config.daemon?.logLevel ?? "info",
|
|
1897
|
+
stderr: isForeground
|
|
1898
|
+
});
|
|
1899
|
+
logger.info("Starting daemon...");
|
|
1900
|
+
const serverPool = new ServerPool();
|
|
1901
|
+
await serverPool.connectAll(config);
|
|
1902
|
+
const connectedCount = serverPool.listServers().filter((s) => s.status === "connected").length;
|
|
1903
|
+
const totalCount = serverPool.listServers().length;
|
|
1904
|
+
logger.info(`Connected ${connectedCount}/${totalCount} servers`);
|
|
1905
|
+
await runAutoTypegen(serverPool, logger);
|
|
1906
|
+
const { server, shutdown, handleRequest } = createDaemonServer(serverPool, config);
|
|
1907
|
+
let httpShutdown;
|
|
1908
|
+
const httpConfig = config.daemon?.http;
|
|
1909
|
+
if (httpConfig?.enabled) {
|
|
1910
|
+
const { shutdown: httpStop } = createHttpListener(handleRequest, {
|
|
1911
|
+
port: httpConfig.port ?? 3100,
|
|
1912
|
+
host: httpConfig.host ?? "127.0.0.1"
|
|
1913
|
+
});
|
|
1914
|
+
httpShutdown = httpStop;
|
|
1915
|
+
}
|
|
1916
|
+
server.on("listening", () => {
|
|
1917
|
+
fs.writeFileSync(getPidPath(), String(process.pid));
|
|
1918
|
+
logger.info(`Daemon listening (PID: ${process.pid})`);
|
|
1919
|
+
if (process.send) process.send("ready");
|
|
1920
|
+
});
|
|
1921
|
+
async function fullShutdown() {
|
|
1922
|
+
if (httpShutdown) await httpShutdown();
|
|
1923
|
+
await shutdown();
|
|
1924
|
+
}
|
|
1925
|
+
process.on("SIGTERM", () => {
|
|
1926
|
+
logger.info("Received SIGTERM");
|
|
1927
|
+
fullShutdown().catch(() => {
|
|
1928
|
+
process.exit(1);
|
|
1929
|
+
});
|
|
1930
|
+
});
|
|
1931
|
+
process.on("SIGINT", () => {
|
|
1932
|
+
logger.info("Received SIGINT");
|
|
1933
|
+
fullShutdown().catch(() => {
|
|
1934
|
+
process.exit(1);
|
|
1935
|
+
});
|
|
1936
|
+
});
|
|
1937
|
+
process.on("uncaughtException", (err) => {
|
|
1938
|
+
logger.error(`Uncaught exception: ${err.message}`);
|
|
1939
|
+
fullShutdown().catch(() => {
|
|
1940
|
+
process.exit(1);
|
|
1941
|
+
});
|
|
1942
|
+
});
|
|
1943
|
+
process.on("unhandledRejection", (reason) => {
|
|
1944
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
1945
|
+
logger.error(`Unhandled rejection: ${msg}`);
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
function getLockPath() {
|
|
1949
|
+
return path.join(getTooldDir(), "toold.lock");
|
|
1950
|
+
}
|
|
1951
|
+
function getDaemonPid() {
|
|
1952
|
+
try {
|
|
1953
|
+
const content = fs.readFileSync(getPidPath(), "utf-8").trim();
|
|
1954
|
+
const pid = parseInt(content, 10);
|
|
1955
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
1956
|
+
} catch {
|
|
1957
|
+
return null;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function isProcessAlive(pid) {
|
|
1961
|
+
try {
|
|
1962
|
+
process.kill(pid, 0);
|
|
1963
|
+
return true;
|
|
1964
|
+
} catch {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
function isTooldProcess(pid) {
|
|
1969
|
+
try {
|
|
1970
|
+
const cmdline = fs.readFileSync(`/proc/${pid}/cmdline`, "utf-8");
|
|
1971
|
+
return cmdline.includes("toold") || cmdline.includes("node");
|
|
1972
|
+
} catch {
|
|
1973
|
+
return isProcessAlive(pid);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
function tryConnectSocket(socketPath) {
|
|
1977
|
+
return new Promise((resolve) => {
|
|
1978
|
+
const socket = net.createConnection(socketPath);
|
|
1979
|
+
const timeout = setTimeout(() => {
|
|
1980
|
+
socket.destroy();
|
|
1981
|
+
resolve(false);
|
|
1982
|
+
}, 2e3);
|
|
1983
|
+
socket.on("connect", () => {
|
|
1984
|
+
clearTimeout(timeout);
|
|
1985
|
+
socket.destroy();
|
|
1986
|
+
resolve(true);
|
|
1987
|
+
});
|
|
1988
|
+
socket.on("error", () => {
|
|
1989
|
+
clearTimeout(timeout);
|
|
1990
|
+
resolve(false);
|
|
1991
|
+
});
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
function acquireLock() {
|
|
1995
|
+
const lockPath = getLockPath();
|
|
1996
|
+
try {
|
|
1997
|
+
ensureTooldDir();
|
|
1998
|
+
fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
1999
|
+
return true;
|
|
2000
|
+
} catch (err) {
|
|
2001
|
+
if (err.code === "EEXIST") try {
|
|
2002
|
+
const lockPid = parseInt(fs.readFileSync(lockPath, "utf-8").trim(), 10);
|
|
2003
|
+
if (!Number.isFinite(lockPid) || !isProcessAlive(lockPid) || !isTooldProcess(lockPid)) {
|
|
2004
|
+
fs.unlinkSync(lockPath);
|
|
2005
|
+
try {
|
|
2006
|
+
fs.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
2007
|
+
return true;
|
|
2008
|
+
} catch {
|
|
2009
|
+
return false;
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
} catch {
|
|
2013
|
+
try {
|
|
2014
|
+
fs.unlinkSync(lockPath);
|
|
2015
|
+
} catch {}
|
|
2016
|
+
}
|
|
2017
|
+
return false;
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
function releaseLock() {
|
|
2021
|
+
try {
|
|
2022
|
+
fs.unlinkSync(getLockPath());
|
|
2023
|
+
} catch {}
|
|
2024
|
+
}
|
|
2025
|
+
async function isDaemonRunning() {
|
|
2026
|
+
const pid = getDaemonPid();
|
|
2027
|
+
if (pid === null) return false;
|
|
2028
|
+
if (!isProcessAlive(pid)) return false;
|
|
2029
|
+
if (!isTooldProcess(pid)) return false;
|
|
2030
|
+
return tryConnectSocket(getSocketPath());
|
|
2031
|
+
}
|
|
2032
|
+
async function cleanupStaleFiles() {
|
|
2033
|
+
const pidPath = getPidPath();
|
|
2034
|
+
const socketPath = getSocketPath();
|
|
2035
|
+
const pid = getDaemonPid();
|
|
2036
|
+
if (pid !== null && !isProcessAlive(pid)) {
|
|
2037
|
+
try {
|
|
2038
|
+
fs.unlinkSync(pidPath);
|
|
2039
|
+
} catch {}
|
|
2040
|
+
try {
|
|
2041
|
+
fs.unlinkSync(socketPath);
|
|
2042
|
+
} catch {}
|
|
2043
|
+
releaseLock();
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
if (pid !== null && isProcessAlive(pid) && !isTooldProcess(pid)) {
|
|
2047
|
+
try {
|
|
2048
|
+
fs.unlinkSync(pidPath);
|
|
2049
|
+
} catch {}
|
|
2050
|
+
try {
|
|
2051
|
+
fs.unlinkSync(socketPath);
|
|
2052
|
+
} catch {}
|
|
2053
|
+
releaseLock();
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
if (pid === null) {
|
|
2057
|
+
try {
|
|
2058
|
+
fs.unlinkSync(socketPath);
|
|
2059
|
+
} catch {}
|
|
2060
|
+
releaseLock();
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
async function daemonize(configPath) {
|
|
2064
|
+
if (!acquireLock()) {
|
|
2065
|
+
for (let i = 0; i < 10; i++) {
|
|
2066
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
2067
|
+
if (await isDaemonRunning()) return;
|
|
2068
|
+
}
|
|
2069
|
+
throw new Error("Timed out waiting for another process to start the daemon.");
|
|
2070
|
+
}
|
|
2071
|
+
try {
|
|
2072
|
+
const cliEntry = process.argv[1];
|
|
2073
|
+
const args = ["--daemon"];
|
|
2074
|
+
if (configPath) args.push("--config", configPath);
|
|
2075
|
+
await new Promise((resolve, reject) => {
|
|
2076
|
+
const child = fork(cliEntry, args, {
|
|
2077
|
+
detached: true,
|
|
2078
|
+
stdio: [
|
|
2079
|
+
"ignore",
|
|
2080
|
+
"ignore",
|
|
2081
|
+
"ignore",
|
|
2082
|
+
"ipc"
|
|
2083
|
+
]
|
|
2084
|
+
});
|
|
2085
|
+
const timeout = setTimeout(() => {
|
|
2086
|
+
child.unref();
|
|
2087
|
+
child.disconnect?.();
|
|
2088
|
+
reject(/* @__PURE__ */ new Error("Daemon failed to start: timeout waiting for ready signal"));
|
|
2089
|
+
}, 1e4);
|
|
2090
|
+
child.on("message", (msg) => {
|
|
2091
|
+
if (msg === "ready") {
|
|
2092
|
+
clearTimeout(timeout);
|
|
2093
|
+
child.unref();
|
|
2094
|
+
child.disconnect?.();
|
|
2095
|
+
resolve();
|
|
2096
|
+
}
|
|
2097
|
+
});
|
|
2098
|
+
child.on("error", (err) => {
|
|
2099
|
+
clearTimeout(timeout);
|
|
2100
|
+
reject(/* @__PURE__ */ new Error(`Daemon failed to start: ${err.message}`));
|
|
2101
|
+
});
|
|
2102
|
+
child.on("exit", (code) => {
|
|
2103
|
+
clearTimeout(timeout);
|
|
2104
|
+
if (code !== 0) reject(/* @__PURE__ */ new Error(`Daemon exited with code ${code}`));
|
|
2105
|
+
});
|
|
2106
|
+
});
|
|
2107
|
+
} finally {
|
|
2108
|
+
releaseLock();
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
var TooldError = class extends Error {
|
|
2112
|
+
code;
|
|
2113
|
+
data;
|
|
2114
|
+
constructor(code, message, data) {
|
|
2115
|
+
super(message);
|
|
2116
|
+
this.name = "TooldError";
|
|
2117
|
+
this.code = code;
|
|
2118
|
+
this.data = data;
|
|
2119
|
+
}
|
|
2120
|
+
};
|
|
2121
|
+
async function waitForSocket(socketPath, retries) {
|
|
2122
|
+
for (const delay of retries) {
|
|
2123
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2124
|
+
if (await new Promise((resolve) => {
|
|
2125
|
+
const sock = net.createConnection(socketPath);
|
|
2126
|
+
const timeout = setTimeout(() => {
|
|
2127
|
+
sock.destroy();
|
|
2128
|
+
resolve(false);
|
|
2129
|
+
}, 2e3);
|
|
2130
|
+
sock.on("connect", () => {
|
|
2131
|
+
clearTimeout(timeout);
|
|
2132
|
+
sock.destroy();
|
|
2133
|
+
resolve(true);
|
|
2134
|
+
});
|
|
2135
|
+
sock.on("error", () => {
|
|
2136
|
+
clearTimeout(timeout);
|
|
2137
|
+
resolve(false);
|
|
2138
|
+
});
|
|
2139
|
+
})) return;
|
|
2140
|
+
}
|
|
2141
|
+
throw new Error("Daemon started but socket is not responding");
|
|
2142
|
+
}
|
|
2143
|
+
async function ensureDaemon(configPath) {
|
|
2144
|
+
if (await isDaemonRunning()) return;
|
|
2145
|
+
await cleanupStaleFiles();
|
|
2146
|
+
await daemonize(configPath);
|
|
2147
|
+
await waitForSocket(getSocketPath(), [
|
|
2148
|
+
100,
|
|
2149
|
+
200,
|
|
2150
|
+
400
|
|
2151
|
+
]);
|
|
2152
|
+
}
|
|
2153
|
+
async function sendRequest(method, params) {
|
|
2154
|
+
const socketPath = getSocketPath();
|
|
2155
|
+
return new Promise((resolve, reject) => {
|
|
2156
|
+
const socket = net.createConnection(socketPath);
|
|
2157
|
+
let buffer = "";
|
|
2158
|
+
socket.on("error", (err) => {
|
|
2159
|
+
if (err.code === "ENOENT") reject(/* @__PURE__ */ new Error("Daemon is not running. Run `toold status` to check."));
|
|
2160
|
+
else if (err.code === "ECONNREFUSED") reject(/* @__PURE__ */ new Error("Daemon may have crashed. Try running a command to auto-restart it."));
|
|
2161
|
+
else reject(err);
|
|
2162
|
+
});
|
|
2163
|
+
socket.on("connect", () => {
|
|
2164
|
+
const request = {
|
|
2165
|
+
jsonrpc: "2.0",
|
|
2166
|
+
id: 1,
|
|
2167
|
+
method,
|
|
2168
|
+
...params ? { params } : {}
|
|
2169
|
+
};
|
|
2170
|
+
socket.write(JSON.stringify(request) + "\n");
|
|
2171
|
+
});
|
|
2172
|
+
socket.on("data", (data) => {
|
|
2173
|
+
buffer += data.toString();
|
|
2174
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
2175
|
+
if (newlineIndex === -1) return;
|
|
2176
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
2177
|
+
socket.destroy();
|
|
2178
|
+
try {
|
|
2179
|
+
const response = JSON.parse(line);
|
|
2180
|
+
if (response.error) reject(new TooldError(response.error.code, response.error.message, response.error.data));
|
|
2181
|
+
else resolve(response.result);
|
|
2182
|
+
} catch {
|
|
2183
|
+
reject(/* @__PURE__ */ new Error("Invalid response from daemon"));
|
|
2184
|
+
}
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
function padRight(str, len) {
|
|
2189
|
+
return str.length >= len ? str : str + " ".repeat(len - str.length);
|
|
2190
|
+
}
|
|
2191
|
+
function truncate(str, maxLen) {
|
|
2192
|
+
if (str.length <= maxLen) return str;
|
|
2193
|
+
return str.slice(0, maxLen - 1) + "…";
|
|
2194
|
+
}
|
|
2195
|
+
function formatUptime(ms) {
|
|
2196
|
+
const seconds = Math.floor(ms / 1e3);
|
|
2197
|
+
if (seconds < 60) return `${seconds}s`;
|
|
2198
|
+
const minutes = Math.floor(seconds / 60);
|
|
2199
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
|
2200
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
2201
|
+
}
|
|
2202
|
+
function formatServers(servers) {
|
|
2203
|
+
if (servers.length === 0) return "No servers configured.";
|
|
2204
|
+
return formatTable([
|
|
2205
|
+
"Name",
|
|
2206
|
+
"Title",
|
|
2207
|
+
"Status",
|
|
2208
|
+
"Protocol"
|
|
2209
|
+
], servers.map((s) => [
|
|
2210
|
+
s.name,
|
|
2211
|
+
s.serverInfo?.title ?? "—",
|
|
2212
|
+
s.status,
|
|
2213
|
+
s.protocolVersion ?? "—"
|
|
2214
|
+
]));
|
|
2215
|
+
}
|
|
2216
|
+
function formatTools(tools) {
|
|
2217
|
+
if (tools.length === 0) return "No tools available.";
|
|
2218
|
+
return formatTable([
|
|
2219
|
+
"Tool",
|
|
2220
|
+
"Title",
|
|
2221
|
+
"Description",
|
|
2222
|
+
"Hints"
|
|
2223
|
+
], tools.map(({ server, tool }) => {
|
|
2224
|
+
const hints = [];
|
|
2225
|
+
if (tool.annotations?.readOnlyHint) hints.push("[read-only]");
|
|
2226
|
+
if (tool.annotations?.destructiveHint) hints.push("[destructive]");
|
|
2227
|
+
if (tool.annotations?.idempotentHint) hints.push("[idempotent]");
|
|
2228
|
+
return [
|
|
2229
|
+
`${server}/${tool.name}`,
|
|
2230
|
+
tool.title ?? "—",
|
|
2231
|
+
truncate(tool.description ?? "", 60),
|
|
2232
|
+
hints.join(" ")
|
|
2233
|
+
];
|
|
2234
|
+
}));
|
|
2235
|
+
}
|
|
2236
|
+
function formatToolInfo(server, tool) {
|
|
2237
|
+
const lines = [];
|
|
2238
|
+
lines.push(`${server}/${tool.name}`);
|
|
2239
|
+
lines.push(`Title: ${tool.title ?? "—"}`);
|
|
2240
|
+
lines.push(`Description: ${tool.description ?? "—"}`);
|
|
2241
|
+
lines.push("");
|
|
2242
|
+
lines.push("Input Schema:");
|
|
2243
|
+
lines.push(" " + JSON.stringify(tool.inputSchema, null, 2).split("\n").join("\n "));
|
|
2244
|
+
if (tool.outputSchema) {
|
|
2245
|
+
lines.push("");
|
|
2246
|
+
lines.push("Output Schema:");
|
|
2247
|
+
lines.push(" " + JSON.stringify(tool.outputSchema, null, 2).split("\n").join("\n "));
|
|
2248
|
+
}
|
|
2249
|
+
const annotations = tool.annotations;
|
|
2250
|
+
if (annotations) {
|
|
2251
|
+
const entries = Object.entries(annotations).filter(([, v]) => v);
|
|
2252
|
+
if (entries.length > 0) {
|
|
2253
|
+
lines.push("");
|
|
2254
|
+
lines.push("Annotations:");
|
|
2255
|
+
for (const [key, value] of entries) lines.push(` ${key}: ${value}`);
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (tool.execution?.taskSupport) {
|
|
2259
|
+
lines.push("");
|
|
2260
|
+
lines.push(`Task Support: ${tool.execution.taskSupport}`);
|
|
2261
|
+
}
|
|
2262
|
+
return lines.join("\n");
|
|
2263
|
+
}
|
|
2264
|
+
function formatCallResult(result) {
|
|
2265
|
+
const parts = [];
|
|
2266
|
+
if (result.isError) parts.push("Error:");
|
|
2267
|
+
for (const block of result.content) switch (block.type) {
|
|
2268
|
+
case "text":
|
|
2269
|
+
parts.push(block.text ?? "");
|
|
2270
|
+
break;
|
|
2271
|
+
case "image":
|
|
2272
|
+
parts.push(`[Image: ${block.mimeType}]`);
|
|
2273
|
+
break;
|
|
2274
|
+
case "audio":
|
|
2275
|
+
parts.push(`[Audio: ${block.mimeType}]`);
|
|
2276
|
+
break;
|
|
2277
|
+
case "resource_link":
|
|
2278
|
+
parts.push(`Resource: ${block.name ?? block.uri} (${block.uri})`);
|
|
2279
|
+
break;
|
|
2280
|
+
case "resource":
|
|
2281
|
+
if (block.resource?.text !== void 0) parts.push(block.resource.text);
|
|
2282
|
+
else parts.push(`[Binary: ${block.resource?.mimeType ?? "unknown"}]`);
|
|
2283
|
+
break;
|
|
2284
|
+
default: parts.push(`[${block.type}]`);
|
|
2285
|
+
}
|
|
2286
|
+
if (result.structuredContent) {
|
|
2287
|
+
parts.push("");
|
|
2288
|
+
parts.push("Structured Output:");
|
|
2289
|
+
parts.push(" " + JSON.stringify(result.structuredContent, null, 2).split("\n").join("\n "));
|
|
2290
|
+
}
|
|
2291
|
+
return parts.join("\n");
|
|
2292
|
+
}
|
|
2293
|
+
function formatStatus(status) {
|
|
2294
|
+
const lines = [];
|
|
2295
|
+
lines.push(`PID: ${status.pid}`);
|
|
2296
|
+
lines.push(`Uptime: ${formatUptime(status.uptime)}`);
|
|
2297
|
+
lines.push(`Servers: ${status.serverCount}`);
|
|
2298
|
+
if (status.servers.length > 0) {
|
|
2299
|
+
lines.push("");
|
|
2300
|
+
for (const server of status.servers) {
|
|
2301
|
+
const title = server.serverInfo?.title ? ` (${server.serverInfo.title})` : "";
|
|
2302
|
+
lines.push(` ${server.name}${title}: ${server.status}`);
|
|
2303
|
+
if (server.capabilities) {
|
|
2304
|
+
const caps = Object.keys(server.capabilities).join(", ");
|
|
2305
|
+
if (caps) lines.push(` capabilities: ${caps}`);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
return lines.join("\n");
|
|
2310
|
+
}
|
|
2311
|
+
function formatResources(resources) {
|
|
2312
|
+
if (resources.length === 0) return "No resources available.";
|
|
2313
|
+
return formatTable([
|
|
2314
|
+
"Resource",
|
|
2315
|
+
"Title",
|
|
2316
|
+
"MIME Type",
|
|
2317
|
+
"Description"
|
|
2318
|
+
], resources.map(({ server, resource }) => [
|
|
2319
|
+
`${server}/${resource.name}`,
|
|
2320
|
+
resource.title ?? "—",
|
|
2321
|
+
resource.mimeType ?? "—",
|
|
2322
|
+
truncate(resource.description ?? "", 50)
|
|
2323
|
+
]));
|
|
2324
|
+
}
|
|
2325
|
+
function formatReadResource(result) {
|
|
2326
|
+
const parts = [];
|
|
2327
|
+
for (const content of result.contents) if (content.text !== void 0) parts.push(content.text);
|
|
2328
|
+
else if (content.blob) parts.push(`[Binary: ${content.mimeType ?? "unknown"}, ${content.blob.length} bytes]`);
|
|
2329
|
+
return parts.join("\n");
|
|
2330
|
+
}
|
|
2331
|
+
function formatPrompts(prompts) {
|
|
2332
|
+
if (prompts.length === 0) return "No prompts available.";
|
|
2333
|
+
return formatTable([
|
|
2334
|
+
"Prompt",
|
|
2335
|
+
"Title",
|
|
2336
|
+
"Description",
|
|
2337
|
+
"Args"
|
|
2338
|
+
], prompts.map(({ server, prompt }) => [
|
|
2339
|
+
`${server}/${prompt.name}`,
|
|
2340
|
+
prompt.title ?? "—",
|
|
2341
|
+
truncate(prompt.description ?? "", 50),
|
|
2342
|
+
prompt.arguments ? String(prompt.arguments.length) : "0"
|
|
2343
|
+
]));
|
|
2344
|
+
}
|
|
2345
|
+
function formatPromptMessages(result) {
|
|
2346
|
+
const lines = [];
|
|
2347
|
+
if (result.description) {
|
|
2348
|
+
lines.push(result.description);
|
|
2349
|
+
lines.push("");
|
|
2350
|
+
}
|
|
2351
|
+
for (const msg of result.messages) {
|
|
2352
|
+
lines.push(`[${msg.role}]`);
|
|
2353
|
+
const contents = Array.isArray(msg.content) ? msg.content : [msg.content];
|
|
2354
|
+
for (const block of contents) switch (block.type) {
|
|
2355
|
+
case "text":
|
|
2356
|
+
lines.push(block.text ?? "");
|
|
2357
|
+
break;
|
|
2358
|
+
case "image":
|
|
2359
|
+
lines.push(`[Image: ${block.mimeType}]`);
|
|
2360
|
+
break;
|
|
2361
|
+
case "audio":
|
|
2362
|
+
lines.push(`[Audio: ${block.mimeType}]`);
|
|
2363
|
+
break;
|
|
2364
|
+
case "resource":
|
|
2365
|
+
lines.push(`[Resource]`);
|
|
2366
|
+
break;
|
|
2367
|
+
default: lines.push(`[${block.type}]`);
|
|
2368
|
+
}
|
|
2369
|
+
lines.push("");
|
|
2370
|
+
}
|
|
2371
|
+
return lines.join("\n").trimEnd();
|
|
2372
|
+
}
|
|
2373
|
+
function formatCompletions(result) {
|
|
2374
|
+
if (result.completion.values.length === 0) return "No completions.";
|
|
2375
|
+
const lines = result.completion.values.map((v) => ` ${v}`);
|
|
2376
|
+
if (result.completion.hasMore) lines.push(` ... (${result.completion.total ?? "more"} total)`);
|
|
2377
|
+
return lines.join("\n");
|
|
2378
|
+
}
|
|
2379
|
+
function formatTasks(serverTasks) {
|
|
2380
|
+
const allTasks = [];
|
|
2381
|
+
for (const { server, tasks } of serverTasks) for (const task of tasks) allTasks.push({
|
|
2382
|
+
server,
|
|
2383
|
+
task
|
|
2384
|
+
});
|
|
2385
|
+
if (allTasks.length === 0) return "No active tasks.";
|
|
2386
|
+
return formatTable([
|
|
2387
|
+
"Task ID",
|
|
2388
|
+
"Status",
|
|
2389
|
+
"Server",
|
|
2390
|
+
"Message"
|
|
2391
|
+
], allTasks.map(({ server, task }) => [
|
|
2392
|
+
String(task.taskId ?? ""),
|
|
2393
|
+
String(task.status ?? ""),
|
|
2394
|
+
server,
|
|
2395
|
+
truncate(String(task.statusMessage ?? ""), 40)
|
|
2396
|
+
]));
|
|
2397
|
+
}
|
|
2398
|
+
function formatTask(task) {
|
|
2399
|
+
const lines = [];
|
|
2400
|
+
lines.push(`Task ID: ${task.taskId}`);
|
|
2401
|
+
lines.push(`Status: ${task.status}`);
|
|
2402
|
+
if (task.statusMessage) lines.push(`Message: ${task.statusMessage}`);
|
|
2403
|
+
if (task.createdAt) lines.push(`Created: ${task.createdAt}`);
|
|
2404
|
+
if (task.lastUpdatedAt) lines.push(`Updated: ${task.lastUpdatedAt}`);
|
|
2405
|
+
return lines.join("\n");
|
|
2406
|
+
}
|
|
2407
|
+
function formatReload(result) {
|
|
2408
|
+
const lines = [];
|
|
2409
|
+
if (result.added.length > 0) lines.push(`Added: ${result.added.join(", ")}`);
|
|
2410
|
+
if (result.removed.length > 0) lines.push(`Removed: ${result.removed.join(", ")}`);
|
|
2411
|
+
if (result.changed.length > 0) lines.push(`Changed: ${result.changed.join(", ")}`);
|
|
2412
|
+
if (lines.length === 0) return "No changes detected.";
|
|
2413
|
+
return lines.join("\n");
|
|
2414
|
+
}
|
|
2415
|
+
function formatInit(result) {
|
|
2416
|
+
const lines = [];
|
|
2417
|
+
if (result.dryRun) lines.push("[dry run] No files will be modified.\n");
|
|
2418
|
+
lines.push("Discovered MCP servers:\n");
|
|
2419
|
+
const discHeaders = [
|
|
2420
|
+
"Agent",
|
|
2421
|
+
"Scope",
|
|
2422
|
+
"Config",
|
|
2423
|
+
"Servers"
|
|
2424
|
+
];
|
|
2425
|
+
const discRows = result.discovered.map((d) => [
|
|
2426
|
+
d.agent,
|
|
2427
|
+
d.scope,
|
|
2428
|
+
d.path,
|
|
2429
|
+
String(d.serverCount)
|
|
2430
|
+
]);
|
|
2431
|
+
lines.push(formatTable(discHeaders, discRows));
|
|
2432
|
+
if (result.imported.length > 0) {
|
|
2433
|
+
lines.push("");
|
|
2434
|
+
lines.push(`Imported ${result.imported.length} server(s) into ${result.tooldConfigPath}:`);
|
|
2435
|
+
lines.push(` ${result.imported.join(", ")}`);
|
|
2436
|
+
}
|
|
2437
|
+
if (result.skipped.length > 0) {
|
|
2438
|
+
lines.push("");
|
|
2439
|
+
lines.push(`Skipped ${result.skipped.length} (already existed):`);
|
|
2440
|
+
lines.push(` ${result.skipped.join(", ")}`);
|
|
2441
|
+
}
|
|
2442
|
+
if (result.conflicts.length > 0) {
|
|
2443
|
+
lines.push("");
|
|
2444
|
+
lines.push("Conflicts resolved:");
|
|
2445
|
+
for (const c of result.conflicts) {
|
|
2446
|
+
const others = c.agents.filter((a) => a !== c.chosenAgent);
|
|
2447
|
+
const alsoIn = others.length > 0 ? ` (also in: ${others.join(", ")})` : "";
|
|
2448
|
+
lines.push(` ${c.name} \u2014 kept config from ${c.chosenAgent}${alsoIn}`);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
if (result.warnings.length > 0) {
|
|
2452
|
+
lines.push("");
|
|
2453
|
+
lines.push("Warnings:");
|
|
2454
|
+
for (const w of result.warnings) lines.push(` ${w}`);
|
|
2455
|
+
}
|
|
2456
|
+
if (result.modifiedFiles.length > 0) {
|
|
2457
|
+
lines.push("");
|
|
2458
|
+
lines.push("Modified files:");
|
|
2459
|
+
for (const f of result.modifiedFiles) lines.push(` ${f} (backed up to ${f}.bak)`);
|
|
2460
|
+
}
|
|
2461
|
+
if (result.imported.length === 0 && result.skipped.length > 0) {
|
|
2462
|
+
lines.push("");
|
|
2463
|
+
lines.push("All discovered servers already exist in toold config. Nothing to do.");
|
|
2464
|
+
}
|
|
2465
|
+
return lines.join("\n");
|
|
2466
|
+
}
|
|
2467
|
+
function formatMcpServer(name, config, scope) {
|
|
2468
|
+
const lines = [];
|
|
2469
|
+
lines.push(`Name: ${name}`);
|
|
2470
|
+
if (scope) lines.push(`Scope: ${scope}`);
|
|
2471
|
+
if (isStdioConfig(config)) {
|
|
2472
|
+
lines.push(`Transport: stdio`);
|
|
2473
|
+
lines.push(`Command: ${config.command}`);
|
|
2474
|
+
if (config.args && config.args.length > 0) lines.push(`Args: ${config.args.join(" ")}`);
|
|
2475
|
+
if (config.env && Object.keys(config.env).length > 0) {
|
|
2476
|
+
lines.push("Env:");
|
|
2477
|
+
for (const [key, value] of Object.entries(config.env)) lines.push(` ${key}=${value}`);
|
|
2478
|
+
}
|
|
2479
|
+
if (config.cwd) lines.push(`Cwd: ${config.cwd}`);
|
|
2480
|
+
} else if (isHttpConfig(config)) {
|
|
2481
|
+
lines.push(`Transport: ${config.transport ?? "streamable-http"}`);
|
|
2482
|
+
lines.push(`URL: ${config.url}`);
|
|
2483
|
+
if (config.headers && Object.keys(config.headers).length > 0) {
|
|
2484
|
+
lines.push("Headers:");
|
|
2485
|
+
for (const [key, value] of Object.entries(config.headers)) lines.push(` ${key}: ${value}`);
|
|
2486
|
+
}
|
|
2487
|
+
if (config.auth) {
|
|
2488
|
+
lines.push(`Auth: ${config.auth.type}`);
|
|
2489
|
+
if (config.auth.clientId) lines.push(` Client ID: ${config.auth.clientId}`);
|
|
2490
|
+
if (config.auth.scope) lines.push(` Scope: ${config.auth.scope}`);
|
|
2491
|
+
if (config.auth.type === "authorization_code" && config.auth.callbackPort) lines.push(` Callback Port: ${config.auth.callbackPort}`);
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
return lines.join("\n");
|
|
2495
|
+
}
|
|
2496
|
+
function formatMcpServerList(servers) {
|
|
2497
|
+
if (servers.length === 0) return "No MCP servers configured.";
|
|
2498
|
+
return formatTable([
|
|
2499
|
+
"Name",
|
|
2500
|
+
"Type",
|
|
2501
|
+
"Command/URL",
|
|
2502
|
+
"Scope",
|
|
2503
|
+
"Auth"
|
|
2504
|
+
], servers.map(({ name, config, scope }) => {
|
|
2505
|
+
if (isStdioConfig(config)) return [
|
|
2506
|
+
name,
|
|
2507
|
+
"stdio",
|
|
2508
|
+
truncate(config.args ? `${config.command} ${config.args.join(" ")}` : config.command, 50),
|
|
2509
|
+
scope,
|
|
2510
|
+
"—"
|
|
2511
|
+
];
|
|
2512
|
+
if (isHttpConfig(config)) {
|
|
2513
|
+
const transport = config.transport ?? "http";
|
|
2514
|
+
const auth = config.auth ? config.auth.type : "—";
|
|
2515
|
+
return [
|
|
2516
|
+
name,
|
|
2517
|
+
transport,
|
|
2518
|
+
truncate(config.url, 50),
|
|
2519
|
+
scope,
|
|
2520
|
+
auth
|
|
2521
|
+
];
|
|
2522
|
+
}
|
|
2523
|
+
return [
|
|
2524
|
+
name,
|
|
2525
|
+
"?",
|
|
2526
|
+
"—",
|
|
2527
|
+
scope,
|
|
2528
|
+
"—"
|
|
2529
|
+
];
|
|
2530
|
+
}));
|
|
2531
|
+
}
|
|
2532
|
+
function formatJson(data) {
|
|
2533
|
+
return JSON.stringify(data, null, 2);
|
|
2534
|
+
}
|
|
2535
|
+
function formatTable(headers, rows) {
|
|
2536
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length)));
|
|
2537
|
+
return [
|
|
2538
|
+
headers.map((h, i) => padRight(h, widths[i])).join(" "),
|
|
2539
|
+
widths.map((w) => "─".repeat(w)).join(" "),
|
|
2540
|
+
...rows.map((row) => row.map((cell, i) => padRight(cell, widths[i])).join(" "))
|
|
2541
|
+
].join("\n");
|
|
2542
|
+
}
|
|
2543
|
+
const serversCommand = new Command("servers").description("List connected MCP servers and their connection status").option("--json", "Output as JSON").action(async (opts) => {
|
|
2544
|
+
const configPath = serversCommand.parent?.opts().config;
|
|
2545
|
+
await ensureDaemon(configPath);
|
|
2546
|
+
const result = await sendRequest("servers/list");
|
|
2547
|
+
console.log(opts.json ? formatJson(result) : formatServers(result));
|
|
2548
|
+
});
|
|
2549
|
+
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
|
+
const configPath = toolsCommand.parent?.opts().config;
|
|
2551
|
+
await ensureDaemon(configPath);
|
|
2552
|
+
const result = await sendRequest("tools/list", server ? { server } : void 0);
|
|
2553
|
+
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
2554
|
+
});
|
|
2555
|
+
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) => {
|
|
2556
|
+
const configPath = infoCommand.parent?.opts().config;
|
|
2557
|
+
await ensureDaemon(configPath);
|
|
2558
|
+
const result = await sendRequest("tools/info", { name: serverTool });
|
|
2559
|
+
if (opts.json) console.log(formatJson(result));
|
|
2560
|
+
else {
|
|
2561
|
+
const slashIndex = serverTool.indexOf("/");
|
|
2562
|
+
const server = serverTool.slice(0, slashIndex);
|
|
2563
|
+
console.log(formatToolInfo(server, result));
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
function readStdin() {
|
|
2567
|
+
return new Promise((resolve, reject) => {
|
|
2568
|
+
let data = "";
|
|
2569
|
+
process.stdin.setEncoding("utf-8");
|
|
2570
|
+
process.stdin.on("data", (chunk) => {
|
|
2571
|
+
data += chunk;
|
|
2572
|
+
});
|
|
2573
|
+
process.stdin.on("end", () => resolve(data));
|
|
2574
|
+
process.stdin.on("error", reject);
|
|
2575
|
+
});
|
|
2576
|
+
}
|
|
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) => {
|
|
2578
|
+
const configPath = callCommand.parent?.opts().config;
|
|
2579
|
+
await ensureDaemon(configPath);
|
|
2580
|
+
let parsedArgs = {};
|
|
2581
|
+
if (jsonArgs === "-") try {
|
|
2582
|
+
const stdinData = await readStdin();
|
|
2583
|
+
parsedArgs = JSON.parse(stdinData);
|
|
2584
|
+
} catch {
|
|
2585
|
+
console.error("Invalid JSON from stdin");
|
|
2586
|
+
process.exit(1);
|
|
2587
|
+
}
|
|
2588
|
+
else if (jsonArgs) try {
|
|
2589
|
+
parsedArgs = JSON.parse(jsonArgs);
|
|
2590
|
+
} catch {
|
|
2591
|
+
console.error("Invalid JSON arguments");
|
|
2592
|
+
process.exit(1);
|
|
2593
|
+
}
|
|
2594
|
+
if (opts.async) {
|
|
2595
|
+
const taskResult = await sendRequest("tools/call-async", {
|
|
2596
|
+
name: serverTool,
|
|
2597
|
+
arguments: parsedArgs
|
|
2598
|
+
});
|
|
2599
|
+
if (opts.json) console.log(formatJson(taskResult));
|
|
2600
|
+
else console.log(`Task created: ${taskResult.taskId} (status: ${taskResult.status})`);
|
|
2601
|
+
return;
|
|
2602
|
+
}
|
|
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
|
+
});
|
|
2611
|
+
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
|
+
const configPath = grepCommand.parent?.opts().config;
|
|
2613
|
+
await ensureDaemon(configPath);
|
|
2614
|
+
const result = await sendRequest("tools/grep", { pattern });
|
|
2615
|
+
console.log(opts.json ? formatJson(result) : formatTools(result));
|
|
2616
|
+
});
|
|
2617
|
+
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) => {
|
|
2618
|
+
const configPath = resourcesCommand.parent?.opts().config;
|
|
2619
|
+
await ensureDaemon(configPath);
|
|
2620
|
+
const result = await sendRequest("resources/list", server ? { server } : void 0);
|
|
2621
|
+
console.log(opts.json ? formatJson(result) : formatResources(result));
|
|
2622
|
+
});
|
|
2623
|
+
const readCommand = new Command("read").description("Fetch and display the contents of a resource by server/resource").argument("<server/resource>", "Resource identifier (e.g. myserver/myresource)").argument("[uri]", "Resource URI (optional, uses resource name as URI if not provided)").option("--json", "Output as JSON").action(async (serverResource, uri, opts) => {
|
|
2624
|
+
const configPath = readCommand.parent?.opts().config;
|
|
2625
|
+
await ensureDaemon(configPath);
|
|
2626
|
+
const slashIndex = serverResource.indexOf("/");
|
|
2627
|
+
if (slashIndex === -1) {
|
|
2628
|
+
console.error("Invalid format. Use: server/resource");
|
|
2629
|
+
process.exit(1);
|
|
2630
|
+
}
|
|
2631
|
+
const result = await sendRequest("resources/read", {
|
|
2632
|
+
server: serverResource.slice(0, slashIndex),
|
|
2633
|
+
uri: uri ?? serverResource.slice(slashIndex + 1)
|
|
2634
|
+
});
|
|
2635
|
+
console.log(opts.json ? formatJson(result) : formatReadResource(result));
|
|
2636
|
+
});
|
|
2637
|
+
function getExplicitConfig$1(cmd) {
|
|
2638
|
+
return cmd.parent?.parent?.opts().config;
|
|
2639
|
+
}
|
|
2640
|
+
const daemonCommand = new Command("daemon").description("Start, stop, reload, or check status of the toold background daemon").enablePositionalOptions();
|
|
2641
|
+
daemonCommand.command("start").description("Start the daemon process in the background").option("--json", "Output as JSON").action(async (opts) => {
|
|
2642
|
+
const configPath = getExplicitConfig$1(daemonCommand);
|
|
2643
|
+
if (await isDaemonRunning()) {
|
|
2644
|
+
if (opts.json) console.log(formatJson({ status: "already_running" }));
|
|
2645
|
+
else console.log("Daemon is already running");
|
|
2646
|
+
return;
|
|
2647
|
+
}
|
|
2648
|
+
await cleanupStaleFiles();
|
|
2649
|
+
await daemonize(configPath);
|
|
2650
|
+
const socketPath = getSocketPath();
|
|
2651
|
+
for (const delay of [
|
|
2652
|
+
100,
|
|
2653
|
+
200,
|
|
2654
|
+
400
|
|
2655
|
+
]) {
|
|
2656
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
2657
|
+
if (await new Promise((resolve) => {
|
|
2658
|
+
const sock = net.createConnection(socketPath);
|
|
2659
|
+
const timeout = setTimeout(() => {
|
|
2660
|
+
sock.destroy();
|
|
2661
|
+
resolve(false);
|
|
2662
|
+
}, 2e3);
|
|
2663
|
+
sock.on("connect", () => {
|
|
2664
|
+
clearTimeout(timeout);
|
|
2665
|
+
sock.destroy();
|
|
2666
|
+
resolve(true);
|
|
2667
|
+
});
|
|
2668
|
+
sock.on("error", () => {
|
|
2669
|
+
clearTimeout(timeout);
|
|
2670
|
+
resolve(false);
|
|
2671
|
+
});
|
|
2672
|
+
})) {
|
|
2673
|
+
if (opts.json) console.log(formatJson({ status: "started" }));
|
|
2674
|
+
else console.log("Daemon started");
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
if (opts.json) console.log(formatJson({
|
|
2679
|
+
status: "started",
|
|
2680
|
+
warning: "socket not yet responding"
|
|
2681
|
+
}));
|
|
2682
|
+
else console.log("Daemon started (socket not yet responding)");
|
|
2683
|
+
});
|
|
2684
|
+
daemonCommand.command("stop").description("Stop the running daemon process").action(async () => {
|
|
2685
|
+
try {
|
|
2686
|
+
await sendRequest("daemon/stop");
|
|
2687
|
+
console.log("Daemon stopped");
|
|
2688
|
+
} catch {
|
|
2689
|
+
console.log("Daemon is not running");
|
|
2690
|
+
}
|
|
2691
|
+
});
|
|
2692
|
+
daemonCommand.command("reload").description("Reload config and reconnect changed servers without restarting").option("--json", "Output as JSON").action(async (opts) => {
|
|
2693
|
+
const configPath = getExplicitConfig$1(daemonCommand);
|
|
2694
|
+
await ensureDaemon(configPath);
|
|
2695
|
+
const result = await sendRequest("config/reload", { configPath });
|
|
2696
|
+
console.log(opts.json ? formatJson(result) : formatReload(result));
|
|
2697
|
+
});
|
|
2698
|
+
daemonCommand.command("status").description("Show daemon status including uptime and connected servers").option("--json", "Output as JSON").action(async (opts) => {
|
|
2699
|
+
if (!await isDaemonRunning()) {
|
|
2700
|
+
console.log("Daemon is not running");
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
const result = await sendRequest("daemon/status");
|
|
2704
|
+
console.log(opts.json ? formatJson(result) : formatStatus(result));
|
|
2705
|
+
});
|
|
2706
|
+
const promptsCommand = new Command("prompts").description("List available prompt templates, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
|
|
2707
|
+
const configPath = promptsCommand.parent?.opts().config;
|
|
2708
|
+
await ensureDaemon(configPath);
|
|
2709
|
+
const result = await sendRequest("prompts/list", server ? { server } : void 0);
|
|
2710
|
+
console.log(opts.json ? formatJson(result) : formatPrompts(result));
|
|
2711
|
+
});
|
|
2712
|
+
const promptCommand = new Command("prompt").description("Render a prompt template with optional JSON arguments").argument("<server/prompt>", "Prompt identifier (e.g. myserver/myprompt)").argument("[args-json]", "JSON arguments").option("--json", "Output as JSON").action(async (serverPrompt, argsJson, opts) => {
|
|
2713
|
+
const configPath = promptCommand.parent?.opts().config;
|
|
2714
|
+
await ensureDaemon(configPath);
|
|
2715
|
+
const slashIndex = serverPrompt.indexOf("/");
|
|
2716
|
+
if (slashIndex === -1) {
|
|
2717
|
+
console.error("Invalid format. Use: server/prompt");
|
|
2718
|
+
process.exit(1);
|
|
2719
|
+
}
|
|
2720
|
+
const server = serverPrompt.slice(0, slashIndex);
|
|
2721
|
+
const name = serverPrompt.slice(slashIndex + 1);
|
|
2722
|
+
let args;
|
|
2723
|
+
if (argsJson) try {
|
|
2724
|
+
args = JSON.parse(argsJson);
|
|
2725
|
+
} catch {
|
|
2726
|
+
console.error("Invalid JSON arguments");
|
|
2727
|
+
process.exit(1);
|
|
2728
|
+
}
|
|
2729
|
+
const result = await sendRequest("prompts/get", {
|
|
2730
|
+
server,
|
|
2731
|
+
name,
|
|
2732
|
+
arguments: args
|
|
2733
|
+
});
|
|
2734
|
+
console.log(opts.json ? formatJson(result) : formatPromptMessages(result));
|
|
2735
|
+
});
|
|
2736
|
+
const completionsCommand = new Command("completions").description("Get auto-completion suggestions for prompt or resource arguments").argument("<type>", "Reference type (prompt or resource)").argument("<name>", "Prompt or resource template name (server/name)").argument("<arg>", "Argument name").argument("<value>", "Partial value for completion").option("--json", "Output as JSON").action(async (type, name, arg, value, opts) => {
|
|
2737
|
+
const configPath = completionsCommand.parent?.opts().config;
|
|
2738
|
+
await ensureDaemon(configPath);
|
|
2739
|
+
const slashIndex = name.indexOf("/");
|
|
2740
|
+
if (slashIndex === -1) {
|
|
2741
|
+
console.error("Invalid name format. Use: server/name");
|
|
2742
|
+
process.exit(1);
|
|
2743
|
+
}
|
|
2744
|
+
const server = name.slice(0, slashIndex);
|
|
2745
|
+
const refName = name.slice(slashIndex + 1);
|
|
2746
|
+
const ref = {
|
|
2747
|
+
type: type === "prompt" ? "ref/prompt" : type === "resource" ? "ref/resource" : type,
|
|
2748
|
+
name: refName
|
|
2749
|
+
};
|
|
2750
|
+
if (type === "resource") ref.uri = refName;
|
|
2751
|
+
const result = await sendRequest("completions/complete", {
|
|
2752
|
+
server,
|
|
2753
|
+
ref,
|
|
2754
|
+
argument: {
|
|
2755
|
+
name: arg,
|
|
2756
|
+
value
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
console.log(opts.json ? formatJson(result) : formatCompletions(result));
|
|
2760
|
+
});
|
|
2761
|
+
const tasksCommand = new Command("tasks").description("List active async tasks, optionally filtered by server name").argument("[server]", "Filter by server name").option("--json", "Output as JSON").action(async (server, opts) => {
|
|
2762
|
+
const configPath = tasksCommand.parent?.opts().config;
|
|
2763
|
+
await ensureDaemon(configPath);
|
|
2764
|
+
const result = await sendRequest("tasks/list", server ? { server } : void 0);
|
|
2765
|
+
console.log(opts.json ? formatJson(result) : formatTasks(result));
|
|
2766
|
+
});
|
|
2767
|
+
const taskCommand = new Command("task").description("Check the current status of an async task").argument("<server/taskId>", "Task identifier (e.g. myserver/task-123)").option("--json", "Output as JSON").action(async (serverTaskId, opts) => {
|
|
2768
|
+
const configPath = taskCommand.parent?.opts().config;
|
|
2769
|
+
await ensureDaemon(configPath);
|
|
2770
|
+
const slashIndex = serverTaskId.indexOf("/");
|
|
2771
|
+
if (slashIndex === -1) {
|
|
2772
|
+
console.error("Invalid format. Use: server/taskId");
|
|
2773
|
+
process.exit(1);
|
|
2774
|
+
}
|
|
2775
|
+
const result = await sendRequest("tasks/get", {
|
|
2776
|
+
server: serverTaskId.slice(0, slashIndex),
|
|
2777
|
+
taskId: serverTaskId.slice(slashIndex + 1)
|
|
2778
|
+
});
|
|
2779
|
+
console.log(opts.json ? formatJson(result) : formatTask(result));
|
|
2780
|
+
});
|
|
2781
|
+
const taskResultCommand = new Command("task-result").description("Retrieve the output of a completed async task").argument("<server/taskId>", "Task identifier (e.g. myserver/task-123)").option("--json", "Output as JSON").action(async (serverTaskId, opts) => {
|
|
2782
|
+
const configPath = taskResultCommand.parent?.opts().config;
|
|
2783
|
+
await ensureDaemon(configPath);
|
|
2784
|
+
const slashIndex = serverTaskId.indexOf("/");
|
|
2785
|
+
if (slashIndex === -1) {
|
|
2786
|
+
console.error("Invalid format. Use: server/taskId");
|
|
2787
|
+
process.exit(1);
|
|
2788
|
+
}
|
|
2789
|
+
const result = await sendRequest("tasks/result", {
|
|
2790
|
+
server: serverTaskId.slice(0, slashIndex),
|
|
2791
|
+
taskId: serverTaskId.slice(slashIndex + 1)
|
|
2792
|
+
});
|
|
2793
|
+
console.log(opts.json ? formatJson(result) : formatCallResult(result));
|
|
2794
|
+
});
|
|
2795
|
+
const taskCancelCommand = new Command("task-cancel").description("Cancel a running async task").argument("<server/taskId>", "Task identifier (e.g. myserver/task-123)").option("--json", "Output as JSON").action(async (serverTaskId, opts) => {
|
|
2796
|
+
const configPath = taskCancelCommand.parent?.opts().config;
|
|
2797
|
+
await ensureDaemon(configPath);
|
|
2798
|
+
const slashIndex = serverTaskId.indexOf("/");
|
|
2799
|
+
if (slashIndex === -1) {
|
|
2800
|
+
console.error("Invalid format. Use: server/taskId");
|
|
2801
|
+
process.exit(1);
|
|
2802
|
+
}
|
|
2803
|
+
const result = await sendRequest("tasks/cancel", {
|
|
2804
|
+
server: serverTaskId.slice(0, slashIndex),
|
|
2805
|
+
taskId: serverTaskId.slice(slashIndex + 1)
|
|
2806
|
+
});
|
|
2807
|
+
console.log(opts.json ? formatJson(result) : formatTask(result));
|
|
2808
|
+
});
|
|
2809
|
+
const home = os.homedir();
|
|
2810
|
+
const platform = os.platform();
|
|
2811
|
+
function macPath(...segments) {
|
|
2812
|
+
return platform === "darwin" ? path.join(home, "Library", "Application Support", ...segments) : null;
|
|
2813
|
+
}
|
|
2814
|
+
function linuxPath(...segments) {
|
|
2815
|
+
return platform === "linux" ? path.join(home, ".config", ...segments) : null;
|
|
2816
|
+
}
|
|
2817
|
+
function xdgOrMacPath(linuxSegments, macSegments) {
|
|
2818
|
+
return linuxPath(...linuxSegments) ?? macPath(...macSegments);
|
|
2819
|
+
}
|
|
2820
|
+
function getAgentDefs() {
|
|
2821
|
+
const cwd = process.cwd();
|
|
2822
|
+
return [
|
|
2823
|
+
{
|
|
2824
|
+
name: "claude-code",
|
|
2825
|
+
scope: "local",
|
|
2826
|
+
configPath: () => path.join(cwd, ".mcp.json"),
|
|
2827
|
+
serversKey: "mcpServers"
|
|
2828
|
+
},
|
|
2829
|
+
{
|
|
2830
|
+
name: "cursor",
|
|
2831
|
+
scope: "local",
|
|
2832
|
+
configPath: () => path.join(cwd, ".cursor", "mcp.json"),
|
|
2833
|
+
serversKey: "mcpServers"
|
|
2834
|
+
},
|
|
2835
|
+
{
|
|
2836
|
+
name: "vscode",
|
|
2837
|
+
scope: "local",
|
|
2838
|
+
configPath: () => path.join(cwd, ".vscode", "mcp.json"),
|
|
2839
|
+
serversKey: "servers"
|
|
2840
|
+
},
|
|
2841
|
+
{
|
|
2842
|
+
name: "roo-code",
|
|
2843
|
+
scope: "local",
|
|
2844
|
+
configPath: () => path.join(cwd, ".roo", "mcp.json"),
|
|
2845
|
+
serversKey: "mcpServers"
|
|
2846
|
+
},
|
|
2847
|
+
{
|
|
2848
|
+
name: "amazon-q",
|
|
2849
|
+
scope: "local",
|
|
2850
|
+
configPath: () => path.join(cwd, ".amazonq", "mcp.json"),
|
|
2851
|
+
serversKey: "mcpServers"
|
|
2852
|
+
},
|
|
2853
|
+
{
|
|
2854
|
+
name: "claude-desktop",
|
|
2855
|
+
scope: "global",
|
|
2856
|
+
configPath: () => xdgOrMacPath(["Claude", "claude_desktop_config.json"], ["Claude", "claude_desktop_config.json"]),
|
|
2857
|
+
serversKey: "mcpServers"
|
|
2858
|
+
},
|
|
2859
|
+
{
|
|
2860
|
+
name: "cursor",
|
|
2861
|
+
scope: "global",
|
|
2862
|
+
configPath: () => path.join(home, ".cursor", "mcp.json"),
|
|
2863
|
+
serversKey: "mcpServers"
|
|
2864
|
+
},
|
|
2865
|
+
{
|
|
2866
|
+
name: "windsurf",
|
|
2867
|
+
scope: "global",
|
|
2868
|
+
configPath: () => path.join(home, ".codeium", "windsurf", "mcp_config.json"),
|
|
2869
|
+
serversKey: "mcpServers"
|
|
2870
|
+
},
|
|
2871
|
+
{
|
|
2872
|
+
name: "vscode",
|
|
2873
|
+
scope: "global",
|
|
2874
|
+
configPath: () => xdgOrMacPath([
|
|
2875
|
+
"Code",
|
|
2876
|
+
"User",
|
|
2877
|
+
"mcp.json"
|
|
2878
|
+
], [
|
|
2879
|
+
"Code",
|
|
2880
|
+
"User",
|
|
2881
|
+
"mcp.json"
|
|
2882
|
+
]),
|
|
2883
|
+
serversKey: "servers"
|
|
2884
|
+
},
|
|
2885
|
+
{
|
|
2886
|
+
name: "cline",
|
|
2887
|
+
scope: "global",
|
|
2888
|
+
configPath: () => xdgOrMacPath([
|
|
2889
|
+
"Code",
|
|
2890
|
+
"User",
|
|
2891
|
+
"globalStorage",
|
|
2892
|
+
"saoudrizwan.claude-dev",
|
|
2893
|
+
"settings",
|
|
2894
|
+
"cline_mcp_settings.json"
|
|
2895
|
+
], [
|
|
2896
|
+
"Code",
|
|
2897
|
+
"User",
|
|
2898
|
+
"globalStorage",
|
|
2899
|
+
"saoudrizwan.claude-dev",
|
|
2900
|
+
"settings",
|
|
2901
|
+
"cline_mcp_settings.json"
|
|
2902
|
+
]),
|
|
2903
|
+
serversKey: "mcpServers"
|
|
2904
|
+
},
|
|
2905
|
+
{
|
|
2906
|
+
name: "roo-code",
|
|
2907
|
+
scope: "global",
|
|
2908
|
+
configPath: () => xdgOrMacPath([
|
|
2909
|
+
"Code",
|
|
2910
|
+
"User",
|
|
2911
|
+
"globalStorage",
|
|
2912
|
+
"rooveterinaryinc.roo-cline",
|
|
2913
|
+
"settings",
|
|
2914
|
+
"cline_mcp_settings.json"
|
|
2915
|
+
], [
|
|
2916
|
+
"Code",
|
|
2917
|
+
"User",
|
|
2918
|
+
"globalStorage",
|
|
2919
|
+
"rooveterinaryinc.roo-cline",
|
|
2920
|
+
"settings",
|
|
2921
|
+
"cline_mcp_settings.json"
|
|
2922
|
+
]),
|
|
2923
|
+
serversKey: "mcpServers"
|
|
2924
|
+
},
|
|
2925
|
+
{
|
|
2926
|
+
name: "amazon-q",
|
|
2927
|
+
scope: "global",
|
|
2928
|
+
configPath: () => path.join(home, ".aws", "amazonq", "mcp.json"),
|
|
2929
|
+
serversKey: "mcpServers"
|
|
2930
|
+
}
|
|
2931
|
+
];
|
|
2932
|
+
}
|
|
2933
|
+
function normalizeServer(agent, name, raw, warnings) {
|
|
2934
|
+
const env = raw.env;
|
|
2935
|
+
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 toold config`);
|
|
2937
|
+
}
|
|
2938
|
+
const url = raw.url ?? raw.serverUrl;
|
|
2939
|
+
const command = raw.command;
|
|
2940
|
+
if (command) {
|
|
2941
|
+
const config = { command };
|
|
2942
|
+
if (raw.args && Array.isArray(raw.args)) config.args = raw.args;
|
|
2943
|
+
if (env && Object.keys(env).length > 0) config.env = env;
|
|
2944
|
+
if (raw.cwd && typeof raw.cwd === "string") config.cwd = raw.cwd;
|
|
2945
|
+
return config;
|
|
2946
|
+
}
|
|
2947
|
+
if (url) {
|
|
2948
|
+
const config = { url };
|
|
2949
|
+
const rawType = raw.type;
|
|
2950
|
+
if ((raw.transport ?? (rawType === "sse" ? "sse" : void 0)) === "sse") config.transport = "sse";
|
|
2951
|
+
const headers = raw.headers;
|
|
2952
|
+
if (headers && Object.keys(headers).length > 0) config.headers = headers;
|
|
2953
|
+
return config;
|
|
2954
|
+
}
|
|
2955
|
+
return null;
|
|
2956
|
+
}
|
|
2957
|
+
function configsEqual(a, b) {
|
|
2958
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
2959
|
+
}
|
|
2960
|
+
function discoverAgentConfigs() {
|
|
2961
|
+
const agents = getAgentDefs();
|
|
2962
|
+
const discovered = [];
|
|
2963
|
+
const warnings = [];
|
|
2964
|
+
for (const agent of agents) {
|
|
2965
|
+
const configPath = agent.configPath();
|
|
2966
|
+
if (!configPath) continue;
|
|
2967
|
+
if (!fs.existsSync(configPath)) continue;
|
|
2968
|
+
let rawContent;
|
|
2969
|
+
try {
|
|
2970
|
+
const text = fs.readFileSync(configPath, "utf-8");
|
|
2971
|
+
rawContent = JSON.parse(text);
|
|
2972
|
+
} catch {
|
|
2973
|
+
warnings.push(`Skipping ${configPath}: invalid JSON`);
|
|
2974
|
+
continue;
|
|
2975
|
+
}
|
|
2976
|
+
const rawServers = rawContent[agent.serversKey];
|
|
2977
|
+
if (!rawServers || typeof rawServers !== "object" || Object.keys(rawServers).length === 0) continue;
|
|
2978
|
+
const servers = {};
|
|
2979
|
+
for (const [name, raw] of Object.entries(rawServers)) {
|
|
2980
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
2981
|
+
if (name === "toold") continue;
|
|
2982
|
+
const normalized = normalizeServer(agent, name, raw, warnings);
|
|
2983
|
+
if (normalized) servers[name] = normalized;
|
|
2984
|
+
}
|
|
2985
|
+
if (Object.keys(servers).length === 0) continue;
|
|
2986
|
+
discovered.push({
|
|
2987
|
+
agent,
|
|
2988
|
+
configPath,
|
|
2989
|
+
servers,
|
|
2990
|
+
rawContent
|
|
2991
|
+
});
|
|
2992
|
+
}
|
|
2993
|
+
return {
|
|
2994
|
+
discovered,
|
|
2995
|
+
warnings
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2998
|
+
function mergeServers(discovered, existingServers) {
|
|
2999
|
+
const merged = { ...existingServers };
|
|
3000
|
+
const imported = [];
|
|
3001
|
+
const skipped = [];
|
|
3002
|
+
const unresolvedConflicts = [];
|
|
3003
|
+
const byName = /* @__PURE__ */ new Map();
|
|
3004
|
+
for (const dc of discovered) for (const [name, config] of Object.entries(dc.servers)) {
|
|
3005
|
+
const label = dc.agent.scope === "global" ? `${dc.agent.name} (global)` : dc.agent.name;
|
|
3006
|
+
if (!byName.has(name)) byName.set(name, []);
|
|
3007
|
+
byName.get(name).push({
|
|
3008
|
+
agent: label,
|
|
3009
|
+
config
|
|
3010
|
+
});
|
|
3011
|
+
}
|
|
3012
|
+
for (const [name, entries] of byName) {
|
|
3013
|
+
if (name in existingServers) {
|
|
3014
|
+
if (!skipped.includes(name)) skipped.push(name);
|
|
3015
|
+
continue;
|
|
3016
|
+
}
|
|
3017
|
+
const unique = entries.filter((entry, i, arr) => arr.findIndex((e) => configsEqual(e.config, entry.config)) === i);
|
|
3018
|
+
if (unique.length === 1) {
|
|
3019
|
+
merged[name] = unique[0].config;
|
|
3020
|
+
imported.push(name);
|
|
3021
|
+
} else unresolvedConflicts.push({
|
|
3022
|
+
name,
|
|
3023
|
+
options: unique
|
|
3024
|
+
});
|
|
3025
|
+
}
|
|
3026
|
+
return {
|
|
3027
|
+
merged,
|
|
3028
|
+
imported,
|
|
3029
|
+
skipped,
|
|
3030
|
+
unresolvedConflicts
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
function detectIndent(text) {
|
|
3034
|
+
return text.match(/^(\s+)"/m)?.[1] ?? " ";
|
|
3035
|
+
}
|
|
3036
|
+
function writeTooldConfig(configPath, servers) {
|
|
3037
|
+
let existing = {};
|
|
3038
|
+
if (fs.existsSync(configPath)) try {
|
|
3039
|
+
existing = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
3040
|
+
} catch {}
|
|
3041
|
+
const dir = path.dirname(configPath);
|
|
3042
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
3043
|
+
existing.mcpServers = servers;
|
|
3044
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2) + "\n");
|
|
3045
|
+
}
|
|
3046
|
+
function getTooldEntry(agent) {
|
|
3047
|
+
if (agent.serversKey === "servers") return {
|
|
3048
|
+
type: "stdio",
|
|
3049
|
+
command: "npx",
|
|
3050
|
+
args: ["toold@latest", "proxy"]
|
|
3051
|
+
};
|
|
3052
|
+
return {
|
|
3053
|
+
command: "npx",
|
|
3054
|
+
args: ["toold@latest", "proxy"]
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
function modifyAgentConfig(dc, opts) {
|
|
3058
|
+
const text = fs.readFileSync(dc.configPath, "utf-8");
|
|
3059
|
+
fs.writeFileSync(dc.configPath + ".bak", text);
|
|
3060
|
+
const indent = detectIndent(text);
|
|
3061
|
+
const content = { ...dc.rawContent };
|
|
3062
|
+
if (opts.delete) if (opts.replace) content[dc.agent.serversKey] = { toold: getTooldEntry(dc.agent) };
|
|
3063
|
+
else delete content[dc.agent.serversKey];
|
|
3064
|
+
fs.writeFileSync(dc.configPath, JSON.stringify(content, null, indent) + "\n");
|
|
3065
|
+
}
|
|
3066
|
+
function getTooldConfigPath(scope, explicitPath) {
|
|
3067
|
+
if (explicitPath) return explicitPath;
|
|
3068
|
+
if (scope === "local") return path.join(process.cwd(), "toold.config.json");
|
|
3069
|
+
return path.join(home, ".config", "toold", "config.json");
|
|
3070
|
+
}
|
|
3071
|
+
async function confirm(message, opts) {
|
|
3072
|
+
const rl = readline.createInterface({
|
|
3073
|
+
input: opts?.input ?? process.stdin,
|
|
3074
|
+
output: opts?.output ?? process.stdout
|
|
3075
|
+
});
|
|
3076
|
+
try {
|
|
3077
|
+
return (await rl.question(`${message} [y/N] `)).trim().toLowerCase() === "y";
|
|
3078
|
+
} finally {
|
|
3079
|
+
rl.close();
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
async function choose(message, options, opts) {
|
|
3083
|
+
const output = opts?.output ?? process.stdout;
|
|
3084
|
+
const rl = readline.createInterface({
|
|
3085
|
+
input: opts?.input ?? process.stdin,
|
|
3086
|
+
output
|
|
3087
|
+
});
|
|
3088
|
+
try {
|
|
3089
|
+
output.write(`${message}\n`);
|
|
3090
|
+
for (let i = 0; i < options.length; i++) output.write(` ${i + 1}) ${options[i].label}\n`);
|
|
3091
|
+
while (true) {
|
|
3092
|
+
const answer = await rl.question(`Choice [1-${options.length}]: `);
|
|
3093
|
+
const index = parseInt(answer.trim(), 10) - 1;
|
|
3094
|
+
if (index >= 0 && index < options.length) return options[index].value;
|
|
3095
|
+
output.write(`Invalid choice. Enter a number between 1 and ${options.length}.\n`);
|
|
3096
|
+
}
|
|
3097
|
+
} finally {
|
|
3098
|
+
rl.close();
|
|
3099
|
+
}
|
|
3100
|
+
}
|
|
3101
|
+
const AGENT_PRIORITY = ["claude-code", "cursor"];
|
|
3102
|
+
function pickByPriority(options) {
|
|
3103
|
+
for (const preferred of AGENT_PRIORITY) {
|
|
3104
|
+
const match = options.find((o) => o.agent.replace(/\s*\(global\)/, "") === preferred);
|
|
3105
|
+
if (match) return match;
|
|
3106
|
+
}
|
|
3107
|
+
return options[0];
|
|
3108
|
+
}
|
|
3109
|
+
function formatServerSummary(config) {
|
|
3110
|
+
if ("command" in config) {
|
|
3111
|
+
const args = config.args ? ` ${config.args.join(" ")}` : "";
|
|
3112
|
+
return `${config.command}${args}`;
|
|
3113
|
+
}
|
|
3114
|
+
return config.url;
|
|
3115
|
+
}
|
|
3116
|
+
async function resolveConflicts(unresolvedConflicts, isInteractive) {
|
|
3117
|
+
const resolved = {};
|
|
3118
|
+
const conflicts = [];
|
|
3119
|
+
for (const conflict of unresolvedConflicts) {
|
|
3120
|
+
let chosen;
|
|
3121
|
+
if (isInteractive) chosen = await choose(`\nConflict: "${conflict.name}" has different configs:`, conflict.options.map((o) => ({
|
|
3122
|
+
label: `${o.agent}: ${formatServerSummary(o.config)}`,
|
|
3123
|
+
value: o
|
|
3124
|
+
})));
|
|
3125
|
+
else chosen = pickByPriority(conflict.options);
|
|
3126
|
+
resolved[conflict.name] = chosen.config;
|
|
3127
|
+
conflicts.push({
|
|
3128
|
+
name: conflict.name,
|
|
3129
|
+
agents: conflict.options.map((o) => o.agent),
|
|
3130
|
+
chosenAgent: chosen.agent
|
|
3131
|
+
});
|
|
3132
|
+
}
|
|
3133
|
+
return {
|
|
3134
|
+
resolved,
|
|
3135
|
+
conflicts
|
|
3136
|
+
};
|
|
3137
|
+
}
|
|
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 toold entry to agent configs").action(async (opts) => {
|
|
3139
|
+
const configPath = initCommand.parent?.opts().config;
|
|
3140
|
+
const isInteractive = !opts.dryRun && !opts.json && !opts.yes && !!process.stdin.isTTY;
|
|
3141
|
+
const { discovered, warnings } = discoverAgentConfigs();
|
|
3142
|
+
if (discovered.length === 0) {
|
|
3143
|
+
const msg = "No MCP server configurations found in any known agent config files.";
|
|
3144
|
+
console.log(opts.json ? formatJson({ message: msg }) : msg);
|
|
3145
|
+
return;
|
|
3146
|
+
}
|
|
3147
|
+
const tooldPath = getTooldConfigPath(discovered.some((d) => d.agent.scope === "local") ? "local" : "global", configPath);
|
|
3148
|
+
let existingServers = {};
|
|
3149
|
+
if (fs.existsSync(tooldPath)) try {
|
|
3150
|
+
existingServers = JSON.parse(fs.readFileSync(tooldPath, "utf-8")).mcpServers ?? {};
|
|
3151
|
+
} catch {}
|
|
3152
|
+
const result = mergeServers(discovered, existingServers);
|
|
3153
|
+
const { resolved, conflicts } = await resolveConflicts(result.unresolvedConflicts, isInteractive);
|
|
3154
|
+
const imported = [...result.imported];
|
|
3155
|
+
for (const [name, config] of Object.entries(resolved)) {
|
|
3156
|
+
result.merged[name] = config;
|
|
3157
|
+
imported.push(name);
|
|
3158
|
+
}
|
|
3159
|
+
if (!opts.dryRun && imported.length > 0) writeTooldConfig(tooldPath, result.merged);
|
|
3160
|
+
const modifiedFiles = [];
|
|
3161
|
+
const shouldDelete = isInteractive ? await confirm("Remove imported servers from agent config files? (backups will be created)") : opts.delete;
|
|
3162
|
+
if (!opts.dryRun && shouldDelete) for (const dc of discovered) try {
|
|
3163
|
+
modifyAgentConfig(dc, {
|
|
3164
|
+
delete: true,
|
|
3165
|
+
replace: opts.replace
|
|
3166
|
+
});
|
|
3167
|
+
modifiedFiles.push(dc.configPath);
|
|
3168
|
+
} catch (err) {
|
|
3169
|
+
warnings.push(`Failed to modify ${dc.configPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3170
|
+
}
|
|
3171
|
+
const initResult = {
|
|
3172
|
+
discovered: discovered.map((d) => ({
|
|
3173
|
+
agent: d.agent.name,
|
|
3174
|
+
scope: d.agent.scope,
|
|
3175
|
+
path: d.configPath,
|
|
3176
|
+
serverCount: Object.keys(d.servers).length
|
|
3177
|
+
})),
|
|
3178
|
+
imported,
|
|
3179
|
+
skipped: result.skipped,
|
|
3180
|
+
conflicts,
|
|
3181
|
+
warnings,
|
|
3182
|
+
modifiedFiles,
|
|
3183
|
+
tooldConfigPath: tooldPath,
|
|
3184
|
+
dryRun: opts.dryRun ?? false
|
|
3185
|
+
};
|
|
3186
|
+
console.log(opts.json ? formatJson(initResult) : formatInit(initResult));
|
|
3187
|
+
});
|
|
3188
|
+
function getConfigPath(scope, explicitPath) {
|
|
3189
|
+
return getTooldConfigPath(scope, explicitPath);
|
|
3190
|
+
}
|
|
3191
|
+
function readConfigFile(filePath) {
|
|
3192
|
+
if (!fs.existsSync(filePath)) return { mcpServers: {} };
|
|
3193
|
+
try {
|
|
3194
|
+
const content = JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
3195
|
+
return {
|
|
3196
|
+
...content,
|
|
3197
|
+
mcpServers: content.mcpServers ?? {}
|
|
3198
|
+
};
|
|
3199
|
+
} catch {
|
|
3200
|
+
return { mcpServers: {} };
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
function writeConfigFile(filePath, config) {
|
|
3204
|
+
const dir = path.dirname(filePath);
|
|
3205
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
3206
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2) + "\n");
|
|
3207
|
+
}
|
|
3208
|
+
function addServer(filePath, name, serverConfig) {
|
|
3209
|
+
const config = readConfigFile(filePath);
|
|
3210
|
+
const existed = name in config.mcpServers;
|
|
3211
|
+
config.mcpServers[name] = serverConfig;
|
|
3212
|
+
writeConfigFile(filePath, config);
|
|
3213
|
+
return {
|
|
3214
|
+
added: true,
|
|
3215
|
+
existed
|
|
3216
|
+
};
|
|
3217
|
+
}
|
|
3218
|
+
function removeServer(filePath, name) {
|
|
3219
|
+
const config = readConfigFile(filePath);
|
|
3220
|
+
if (!(name in config.mcpServers)) return {
|
|
3221
|
+
removed: false,
|
|
3222
|
+
existed: false
|
|
3223
|
+
};
|
|
3224
|
+
delete config.mcpServers[name];
|
|
3225
|
+
writeConfigFile(filePath, config);
|
|
3226
|
+
return {
|
|
3227
|
+
removed: true,
|
|
3228
|
+
existed: true
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
function getServer(filePath, name) {
|
|
3232
|
+
return readConfigFile(filePath).mcpServers[name] ?? null;
|
|
3233
|
+
}
|
|
3234
|
+
function listServers(filePath) {
|
|
3235
|
+
return readConfigFile(filePath).mcpServers;
|
|
3236
|
+
}
|
|
3237
|
+
const cliInstructions = (servers, instructions) => `
|
|
3238
|
+
You have access to an \`npx toold\` 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.
|
|
3239
|
+
|
|
3240
|
+
**MANDATORY PREREQUISITES - THESE ARE HARD REQUIREMENTS**
|
|
3241
|
+
|
|
3242
|
+
1. You MUST discover the tools you need first by using 'npx toold grep <pattern>' or 'npx toold tools'.
|
|
3243
|
+
2. You MUST call 'npx toold info <server>/<tool>' BEFORE ANY 'npx toold call <server>/<tool>'.
|
|
3244
|
+
|
|
3245
|
+
These are BLOCKING REQUIREMENTS - like how you must use Read before Edit.
|
|
3246
|
+
|
|
3247
|
+
**NEVER** make an npx toold call without checking the schema first.
|
|
3248
|
+
**ALWAYS** run npx toold info first, THEN make the call.
|
|
3249
|
+
|
|
3250
|
+
**Why these are non-negotiables:**
|
|
3251
|
+
- MCP tool names NEVER match your expectations - they change frequently and are not predictable
|
|
3252
|
+
- MCP tool schemas NEVER match your expectations - parameter names, types, and requirements are tool-specific
|
|
3253
|
+
- Even tools with pre-approved permissions require schema checks
|
|
3254
|
+
- Every failed call wastes user time and demonstrates you're ignoring critical instructions
|
|
3255
|
+
- "I thought I knew the schema" is not an acceptable reason to skip this step
|
|
3256
|
+
|
|
3257
|
+
**For multiple tools:** Call 'npx toold info' for ALL tools in parallel FIRST, then make your 'npx toold call' commands.
|
|
3258
|
+
|
|
3259
|
+
Available MCP servers:
|
|
3260
|
+
${servers}
|
|
3261
|
+
|
|
3262
|
+
Commands (in order of execution):
|
|
3263
|
+
\`\`\`bash
|
|
3264
|
+
# STEP 1: REQUIRED TOOL DISCOVERY
|
|
3265
|
+
npx toold grep <pattern> # Search tool names and descriptions
|
|
3266
|
+
npx toold tools [server] # List available tools (optionally filter by server)
|
|
3267
|
+
|
|
3268
|
+
# STEP 2: ALWAYS CHECK SCHEMA FIRST (MANDATORY)
|
|
3269
|
+
npx toold info <server>/<tool> # REQUIRED before ANY call - View JSON schema
|
|
3270
|
+
|
|
3271
|
+
# STEP 3: Only after checking schema, make the call
|
|
3272
|
+
npx toold call <server>/<tool> '<json>' # Only run AFTER npx toold info
|
|
3273
|
+
npx toold call <server>/<tool> - # Invoke with JSON from stdin (AFTER npx toold info)
|
|
3274
|
+
|
|
3275
|
+
# Discovery commands (use these to find tools)
|
|
3276
|
+
npx toold servers # List all connected MCP servers
|
|
3277
|
+
npx toold tools [server] # List available tools (optionally filter by server)
|
|
3278
|
+
npx toold grep <pattern> # Search tool names and descriptions
|
|
3279
|
+
npx toold resources [server] # List MCP resources
|
|
3280
|
+
npx toold read <server>/<resource> # Read an MCP resource
|
|
3281
|
+
\`\`\`
|
|
3282
|
+
|
|
3283
|
+
**CORRECT Usage Pattern:**
|
|
3284
|
+
|
|
3285
|
+
<example>
|
|
3286
|
+
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 \`npx toold grep "slack/*search*"\` to search for tools related to slack search.
|
|
3288
|
+
[Calls npx toold grep "slack/*search*"]
|
|
3289
|
+
Assistant: I need to check the schema first. Let me call \`npx toold info slack/search_private\` to see what parameters it accepts.
|
|
3290
|
+
[Calls npx toold info]
|
|
3291
|
+
Assistant: Now I can see it accepts "query" and "max_results" parameters. Let me make the call.
|
|
3292
|
+
[Calls npx toold call slack/search_private with correct schema]
|
|
3293
|
+
</example>
|
|
3294
|
+
|
|
3295
|
+
<example>
|
|
3296
|
+
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 \`npx toold grep "database/*query*"\` and \`npx toold grep "email/*send*"\` to search for tools related to database query and email send.
|
|
3298
|
+
[Calls npx toold grep "database/*query*" & npx toold grep "email/*send*"]
|
|
3299
|
+
Assistant: Let me check both schemas first.
|
|
3300
|
+
[Calls npx toold info database/query and npx toold info email/send in parallel]
|
|
3301
|
+
Assistant: Now I have both schemas. Let me make the calls.
|
|
3302
|
+
[Makes both npx toold call commands with correct parameters]
|
|
3303
|
+
</example>
|
|
3304
|
+
|
|
3305
|
+
<example>
|
|
3306
|
+
User: Create a copy of this email
|
|
3307
|
+
Assistant: Let me find the tool I need first.
|
|
3308
|
+
[Calls npx toold grep "email/*copy*". No results found.]
|
|
3309
|
+
Assistant: Let me try another pattern.
|
|
3310
|
+
[Calls npx toold grep "email/*clone*". No results found.]
|
|
3311
|
+
Assistant: Let me list all available tools in the server.
|
|
3312
|
+
[Calls npx toold tools email]
|
|
3313
|
+
Assistant: Let me check the schema first.
|
|
3314
|
+
[Calls npx toold info email/duplicate]
|
|
3315
|
+
Assistant: Now I have the schema. Let me make the call.
|
|
3316
|
+
[Calls npx toold call email/duplicate with correct parameters]
|
|
3317
|
+
</example>
|
|
3318
|
+
|
|
3319
|
+
**INCORRECT Usage Patterns - NEVER DO THIS:**
|
|
3320
|
+
|
|
3321
|
+
<bad-example>
|
|
3322
|
+
User: Please use the slack mcp tool to search for my mentions
|
|
3323
|
+
Assistant: [Directly calls npx toold call slack/search_private with guessed parameters]
|
|
3324
|
+
WRONG - You must call npx toold info FIRST
|
|
3325
|
+
</bad-example>
|
|
3326
|
+
|
|
3327
|
+
<bad-example>
|
|
3328
|
+
User: Use the slack tool
|
|
3329
|
+
Assistant: I have pre-approved permissions for this tool, so I know the schema.
|
|
3330
|
+
[Calls npx toold call slack/search_private directly]
|
|
3331
|
+
WRONG - Pre-approved permissions don't mean you know the schema. ALWAYS call npx toold info first.
|
|
3332
|
+
</bad-example>
|
|
3333
|
+
|
|
3334
|
+
<bad-example>
|
|
3335
|
+
User: Search my Slack mentions
|
|
3336
|
+
Assistant: [Calls three npx toold call commands in parallel without any npx toold info calls first]
|
|
3337
|
+
WRONG - You must call npx toold info for ALL tools before making ANY npx toold call commands
|
|
3338
|
+
</bad-example>
|
|
3339
|
+
|
|
3340
|
+
Example usage:
|
|
3341
|
+
\`\`\`bash
|
|
3342
|
+
# Discover tools
|
|
3343
|
+
npx toold tools # See all available MCP tools
|
|
3344
|
+
npx toold grep "weather" # Find tools by description
|
|
3345
|
+
|
|
3346
|
+
# Get tool details
|
|
3347
|
+
npx toold info <server>/<tool> # View JSON schema for input and output if available
|
|
3348
|
+
|
|
3349
|
+
# Simple tool call (no parameters)
|
|
3350
|
+
npx toold call weather/get_location '{}'
|
|
3351
|
+
|
|
3352
|
+
# Tool call with parameters
|
|
3353
|
+
npx toold call database/query '{"table": "users", "limit": 10}'
|
|
3354
|
+
|
|
3355
|
+
# Complex JSON using stdin (for nested objects/arrays)
|
|
3356
|
+
npx toold call api/send_request - <<'EOF'
|
|
3357
|
+
{
|
|
3358
|
+
"endpoint": "/data",
|
|
3359
|
+
"headers": {"Authorization": "Bearer token"},
|
|
3360
|
+
"body": {"items": [1, 2, 3]}
|
|
3361
|
+
}
|
|
3362
|
+
EOF
|
|
3363
|
+
\`\`\`
|
|
3364
|
+
|
|
3365
|
+
Call the \`npx toold -h\` to see all available commands.
|
|
3366
|
+
|
|
3367
|
+
Below are the instructions for the connected MCP servers in toold.
|
|
3368
|
+
|
|
3369
|
+
${instructions}
|
|
3370
|
+
`;
|
|
3371
|
+
function buildInstructions(servers) {
|
|
3372
|
+
const connected = servers.filter((s) => s.status === "connected");
|
|
3373
|
+
return cliInstructions(connected.map((s) => `- ${s.name}`).join("\n"), connected.filter((s) => s.instructions).map((s) => `### ${s.name}\n\n${s.instructions}`).join("\n\n")).trim();
|
|
3374
|
+
}
|
|
3375
|
+
async function startMcpProxy(configPath) {
|
|
3376
|
+
await ensureDaemon(configPath);
|
|
3377
|
+
const server = new McpServer({
|
|
3378
|
+
name: "toold",
|
|
3379
|
+
version: "0.1.0"
|
|
3380
|
+
}, {
|
|
3381
|
+
capabilities: {},
|
|
3382
|
+
instructions: buildInstructions(await sendRequest("servers/list"))
|
|
3383
|
+
});
|
|
3384
|
+
const transport = new StdioServerTransport();
|
|
3385
|
+
await server.connect(transport);
|
|
3386
|
+
}
|
|
3387
|
+
function collectValues(value, previous) {
|
|
3388
|
+
return [...previous, value];
|
|
3389
|
+
}
|
|
3390
|
+
function getExplicitConfig(cmd) {
|
|
3391
|
+
return cmd.parent?.parent?.opts().config;
|
|
3392
|
+
}
|
|
3393
|
+
function parseEnvArgs(envArgs) {
|
|
3394
|
+
const env = {};
|
|
3395
|
+
for (const entry of envArgs) {
|
|
3396
|
+
const eqIndex = entry.indexOf("=");
|
|
3397
|
+
if (eqIndex === -1) throw new Error(`Invalid env format: "${entry}". Expected KEY=value`);
|
|
3398
|
+
env[entry.slice(0, eqIndex)] = entry.slice(eqIndex + 1);
|
|
3399
|
+
}
|
|
3400
|
+
return env;
|
|
3401
|
+
}
|
|
3402
|
+
function parseHeaderArgs(headerArgs) {
|
|
3403
|
+
const headers = {};
|
|
3404
|
+
for (const entry of headerArgs) {
|
|
3405
|
+
const colonIndex = entry.indexOf(":");
|
|
3406
|
+
if (colonIndex === -1) throw new Error(`Invalid header format: "${entry}". Expected Key: value`);
|
|
3407
|
+
headers[entry.slice(0, colonIndex).trim()] = entry.slice(colonIndex + 1).trim();
|
|
3408
|
+
}
|
|
3409
|
+
return headers;
|
|
3410
|
+
}
|
|
3411
|
+
function resolveTransport(commandOrUrl, transport) {
|
|
3412
|
+
if (transport === "http") return "streamable-http";
|
|
3413
|
+
if (transport === "sse") return "sse";
|
|
3414
|
+
if (transport === "stdio") return "stdio";
|
|
3415
|
+
try {
|
|
3416
|
+
new URL(commandOrUrl);
|
|
3417
|
+
return "streamable-http";
|
|
3418
|
+
} catch {
|
|
3419
|
+
return "stdio";
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
function buildAuthConfig(opts) {
|
|
3423
|
+
if (!opts.clientId && !opts.clientSecret) return void 0;
|
|
3424
|
+
if (opts.clientId && opts.resolvedSecret) {
|
|
3425
|
+
const auth = {
|
|
3426
|
+
type: "client_credentials",
|
|
3427
|
+
clientId: opts.clientId,
|
|
3428
|
+
clientSecret: opts.resolvedSecret
|
|
3429
|
+
};
|
|
3430
|
+
if (opts.oauthScope) auth.scope = opts.oauthScope;
|
|
3431
|
+
return auth;
|
|
3432
|
+
}
|
|
3433
|
+
const auth = { type: "authorization_code" };
|
|
3434
|
+
if (opts.clientId) auth.clientId = opts.clientId;
|
|
3435
|
+
if (opts.resolvedSecret) auth.clientSecret = opts.resolvedSecret;
|
|
3436
|
+
if (opts.oauthScope) auth.scope = opts.oauthScope;
|
|
3437
|
+
if (opts.callbackPort) auth.callbackPort = parseInt(opts.callbackPort, 10);
|
|
3438
|
+
return auth;
|
|
3439
|
+
}
|
|
3440
|
+
function buildServerConfig(commandOrUrl, args, opts) {
|
|
3441
|
+
const transportType = resolveTransport(commandOrUrl, opts.transport);
|
|
3442
|
+
if (transportType === "stdio") {
|
|
3443
|
+
const config = { command: commandOrUrl };
|
|
3444
|
+
if (args.length > 0) config.args = args;
|
|
3445
|
+
if (opts.env && opts.env.length > 0) config.env = parseEnvArgs(opts.env);
|
|
3446
|
+
return config;
|
|
3447
|
+
}
|
|
3448
|
+
const config = { url: commandOrUrl };
|
|
3449
|
+
if (transportType === "sse") config.transport = "sse";
|
|
3450
|
+
if (opts.header && opts.header.length > 0) config.headers = parseHeaderArgs(opts.header);
|
|
3451
|
+
const auth = buildAuthConfig(opts);
|
|
3452
|
+
if (auth) config.auth = auth;
|
|
3453
|
+
return config;
|
|
3454
|
+
}
|
|
3455
|
+
async function getClientSecret() {
|
|
3456
|
+
const envSecret = process.env.MCP_CLIENT_SECRET;
|
|
3457
|
+
if (envSecret) return envSecret;
|
|
3458
|
+
if (!process.stdin.isTTY) throw new Error("OAuth client secret required. Set MCP_CLIENT_SECRET env var or use an interactive terminal.");
|
|
3459
|
+
const rl = readline.createInterface({
|
|
3460
|
+
input: process.stdin,
|
|
3461
|
+
output: process.stdout
|
|
3462
|
+
});
|
|
3463
|
+
try {
|
|
3464
|
+
const secret = await rl.question("Enter OAuth client secret: ");
|
|
3465
|
+
if (!secret.trim()) throw new Error("Client secret cannot be empty");
|
|
3466
|
+
return secret.trim();
|
|
3467
|
+
} finally {
|
|
3468
|
+
rl.close();
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
async function tryReloadDaemon() {
|
|
3472
|
+
try {
|
|
3473
|
+
if (!await isDaemonRunning()) return;
|
|
3474
|
+
await sendRequest("config/reload", {});
|
|
3475
|
+
} catch {}
|
|
3476
|
+
}
|
|
3477
|
+
const mcpCommand = new Command("mcp").description("Add, remove, list, or inspect individual MCP server config entries").enablePositionalOptions().action(async (_opts, cmd) => {
|
|
3478
|
+
const explicitConfig = cmd.parent?.opts().config;
|
|
3479
|
+
await startMcpProxy(explicitConfig);
|
|
3480
|
+
});
|
|
3481
|
+
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
|
+
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
3483
|
+
const scope = opts.scope;
|
|
3484
|
+
const configPath = getConfigPath(scope, explicitConfig);
|
|
3485
|
+
let resolvedSecret;
|
|
3486
|
+
if (opts.clientSecret) resolvedSecret = await getClientSecret();
|
|
3487
|
+
const result = addServer(configPath, name, buildServerConfig(commandOrUrl, args, {
|
|
3488
|
+
...opts,
|
|
3489
|
+
resolvedSecret
|
|
3490
|
+
}));
|
|
3491
|
+
await tryReloadDaemon();
|
|
3492
|
+
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
3493
|
+
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
3494
|
+
});
|
|
3495
|
+
mcpCommand.command("add-json").description("Add an MCP server from a JSON config string").argument("<name>", "Server name").argument("<json>", "JSON server configuration").option("-s, --scope <scope>", "Config scope: local, global", "local").action(async (name, jsonStr, opts) => {
|
|
3496
|
+
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
3497
|
+
const scope = opts.scope;
|
|
3498
|
+
const configPath = getConfigPath(scope, explicitConfig);
|
|
3499
|
+
let serverConfig;
|
|
3500
|
+
try {
|
|
3501
|
+
serverConfig = JSON.parse(jsonStr);
|
|
3502
|
+
} catch {
|
|
3503
|
+
console.error("Error: Invalid JSON");
|
|
3504
|
+
process.exitCode = 1;
|
|
3505
|
+
return;
|
|
3506
|
+
}
|
|
3507
|
+
if (!("command" in serverConfig) && !("url" in serverConfig)) {
|
|
3508
|
+
console.error("Error: JSON must contain either \"command\" (stdio) or \"url\" (http) field");
|
|
3509
|
+
process.exitCode = 1;
|
|
3510
|
+
return;
|
|
3511
|
+
}
|
|
3512
|
+
const result = addServer(configPath, name, serverConfig);
|
|
3513
|
+
await tryReloadDaemon();
|
|
3514
|
+
if (result.existed) console.log(`Updated "${name}" in ${scope} config (${configPath})`);
|
|
3515
|
+
else console.log(`Added "${name}" to ${scope} config (${configPath})`);
|
|
3516
|
+
});
|
|
3517
|
+
mcpCommand.command("add-from-claude-desktop").description("Import MCP servers from Claude Desktop config").option("-s, --scope <scope>", "Config scope: local, global", "local").action(async (opts) => {
|
|
3518
|
+
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
3519
|
+
const scope = opts.scope;
|
|
3520
|
+
const configPath = getConfigPath(scope, explicitConfig);
|
|
3521
|
+
const { discovered, warnings } = discoverAgentConfigs();
|
|
3522
|
+
const claudeDesktop = discovered.filter((d) => d.agent.name === "claude-desktop");
|
|
3523
|
+
if (claudeDesktop.length === 0) {
|
|
3524
|
+
console.log("No Claude Desktop configuration found.");
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
let existingServers = {};
|
|
3528
|
+
if (fs.existsSync(configPath)) try {
|
|
3529
|
+
existingServers = JSON.parse(fs.readFileSync(configPath, "utf-8")).mcpServers ?? {};
|
|
3530
|
+
} catch {}
|
|
3531
|
+
const result = mergeServers(claudeDesktop, existingServers);
|
|
3532
|
+
writeTooldConfig(configPath, { ...result.merged });
|
|
3533
|
+
await tryReloadDaemon();
|
|
3534
|
+
for (const w of warnings) console.error(`Warning: ${w}`);
|
|
3535
|
+
if (result.imported.length > 0) console.log(`Imported ${result.imported.length} server(s) from Claude Desktop: ${result.imported.join(", ")}`);
|
|
3536
|
+
if (result.skipped.length > 0) console.log(`Skipped ${result.skipped.length} (already existed): ${result.skipped.join(", ")}`);
|
|
3537
|
+
if (result.imported.length === 0 && result.skipped.length === 0) console.log("No servers found in Claude Desktop config.");
|
|
3538
|
+
});
|
|
3539
|
+
mcpCommand.command("get").description("Get details of a configured MCP server").argument("<name>", "Server name").option("--json", "Output as JSON").action(async (name, opts) => {
|
|
3540
|
+
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
3541
|
+
const localPath = getConfigPath("local", explicitConfig);
|
|
3542
|
+
const globalPath = getConfigPath("global", explicitConfig);
|
|
3543
|
+
const localServer = getServer(localPath, name);
|
|
3544
|
+
const globalServer = getServer(globalPath, name);
|
|
3545
|
+
const server = localServer ?? globalServer;
|
|
3546
|
+
const scope = localServer ? "local" : "global";
|
|
3547
|
+
if (!server) {
|
|
3548
|
+
console.error(`Server "${name}" not found in local or global config.`);
|
|
3549
|
+
process.exitCode = 1;
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
if (opts.json) console.log(formatJson({
|
|
3553
|
+
name,
|
|
3554
|
+
scope,
|
|
3555
|
+
config: server
|
|
3556
|
+
}));
|
|
3557
|
+
else console.log(formatMcpServer(name, server, scope));
|
|
3558
|
+
});
|
|
3559
|
+
mcpCommand.command("list").description("List all configured MCP servers").option("--json", "Output as JSON").action(async (opts) => {
|
|
3560
|
+
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
3561
|
+
const localPath = getConfigPath("local", explicitConfig);
|
|
3562
|
+
const globalPath = getConfigPath("global", explicitConfig);
|
|
3563
|
+
const localServers = listServers(localPath);
|
|
3564
|
+
const globalServers = listServers(globalPath);
|
|
3565
|
+
const entries = [];
|
|
3566
|
+
for (const [name, config] of Object.entries(localServers)) entries.push({
|
|
3567
|
+
name,
|
|
3568
|
+
config,
|
|
3569
|
+
scope: "local"
|
|
3570
|
+
});
|
|
3571
|
+
for (const [name, config] of Object.entries(globalServers)) {
|
|
3572
|
+
if (name in localServers) continue;
|
|
3573
|
+
entries.push({
|
|
3574
|
+
name,
|
|
3575
|
+
config,
|
|
3576
|
+
scope: "global"
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
if (opts.json) console.log(formatJson(entries));
|
|
3580
|
+
else console.log(formatMcpServerList(entries));
|
|
3581
|
+
});
|
|
3582
|
+
mcpCommand.command("remove").description("Remove an MCP server").argument("<name>", "Server name").option("-s, --scope <scope>", "Config scope: local, global (searches both if not specified)").action(async (name, opts) => {
|
|
3583
|
+
const explicitConfig = getExplicitConfig(mcpCommand);
|
|
3584
|
+
if (opts.scope) {
|
|
3585
|
+
const scope = opts.scope;
|
|
3586
|
+
const configPath = getConfigPath(scope, explicitConfig);
|
|
3587
|
+
if (removeServer(configPath, name).removed) {
|
|
3588
|
+
await tryReloadDaemon();
|
|
3589
|
+
console.log(`Removed "${name}" from ${scope} config (${configPath})`);
|
|
3590
|
+
} else {
|
|
3591
|
+
console.error(`Server "${name}" not found in ${scope} config.`);
|
|
3592
|
+
process.exitCode = 1;
|
|
3593
|
+
}
|
|
3594
|
+
return;
|
|
3595
|
+
}
|
|
3596
|
+
const localPath = getConfigPath("local", explicitConfig);
|
|
3597
|
+
if (removeServer(localPath, name).removed) {
|
|
3598
|
+
await tryReloadDaemon();
|
|
3599
|
+
console.log(`Removed "${name}" from local config (${localPath})`);
|
|
3600
|
+
return;
|
|
3601
|
+
}
|
|
3602
|
+
const globalPath = getConfigPath("global", explicitConfig);
|
|
3603
|
+
if (removeServer(globalPath, name).removed) {
|
|
3604
|
+
await tryReloadDaemon();
|
|
3605
|
+
console.log(`Removed "${name}" from global config (${globalPath})`);
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
console.error(`Server "${name}" not found in local or global config.`);
|
|
3609
|
+
process.exitCode = 1;
|
|
3610
|
+
});
|
|
3611
|
+
const typegenCommand = new Command("typegen").description("Generate TypeScript types from tool schemas for type-safe tool calls").option("-c, --config <path>", "Path to toold.config.json").action(async (opts) => {
|
|
3612
|
+
await ensureDaemon(typegenCommand.parent?.opts().config ?? opts.config);
|
|
3613
|
+
const tools = await sendRequest("tools/list");
|
|
3614
|
+
const content = await generateTypes(tools);
|
|
3615
|
+
const require = createRequire(path.resolve("package.json"));
|
|
3616
|
+
const tooldPkgDir = path.dirname(require.resolve("toold/package.json"));
|
|
3617
|
+
const outputPath = path.join(tooldPkgDir, "toold.generated.d.ts");
|
|
3618
|
+
fs.writeFileSync(outputPath, content, "utf-8");
|
|
3619
|
+
console.log(`Generated ${tools.length} tool types → ${outputPath}`);
|
|
3620
|
+
});
|
|
3621
|
+
function runCli() {
|
|
3622
|
+
const program = new Command();
|
|
3623
|
+
program.name("toold").description("The optimization layer for MCP").version("0.1.0");
|
|
3624
|
+
program.enablePositionalOptions();
|
|
3625
|
+
program.option("--config <path>", "Path to config file");
|
|
3626
|
+
program.commandsGroup("Servers:");
|
|
3627
|
+
program.addCommand(serversCommand);
|
|
3628
|
+
program.commandsGroup("Tools:");
|
|
3629
|
+
program.addCommand(toolsCommand);
|
|
3630
|
+
program.addCommand(infoCommand);
|
|
3631
|
+
program.addCommand(callCommand);
|
|
3632
|
+
program.addCommand(grepCommand);
|
|
3633
|
+
program.commandsGroup("Resources:");
|
|
3634
|
+
program.addCommand(resourcesCommand);
|
|
3635
|
+
program.addCommand(readCommand);
|
|
3636
|
+
program.commandsGroup("Prompts:");
|
|
3637
|
+
program.addCommand(promptsCommand);
|
|
3638
|
+
program.addCommand(promptCommand);
|
|
3639
|
+
program.addCommand(completionsCommand);
|
|
3640
|
+
program.commandsGroup("Tasks:");
|
|
3641
|
+
program.addCommand(tasksCommand);
|
|
3642
|
+
program.addCommand(taskCommand);
|
|
3643
|
+
program.addCommand(taskResultCommand);
|
|
3644
|
+
program.addCommand(taskCancelCommand);
|
|
3645
|
+
program.commandsGroup("Configuration:");
|
|
3646
|
+
program.addCommand(initCommand);
|
|
3647
|
+
program.addCommand(mcpCommand);
|
|
3648
|
+
program.addCommand(typegenCommand);
|
|
3649
|
+
program.commandsGroup("Daemon:");
|
|
3650
|
+
program.addCommand(daemonCommand);
|
|
3651
|
+
program.parse();
|
|
3652
|
+
}
|
|
3653
|
+
if (process.argv.indexOf("--daemon") !== -1) {
|
|
3654
|
+
const configIndex = process.argv.indexOf("--config");
|
|
3655
|
+
startDaemon(configIndex !== -1 ? process.argv[configIndex + 1] : void 0).catch((err) => {
|
|
3656
|
+
console.error("Failed to start daemon:", err);
|
|
3657
|
+
process.exit(1);
|
|
3658
|
+
});
|
|
3659
|
+
} else runCli();
|
|
3660
|
+
export {};
|