@mcp-s/cli 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/daemon.js ADDED
@@ -0,0 +1,1023 @@
1
+ // src/daemon.ts
2
+ import {
3
+ existsSync as existsSync3,
4
+ mkdirSync,
5
+ readFileSync as readFileSync2,
6
+ unlinkSync,
7
+ writeFileSync
8
+ } from "fs";
9
+ import { createServer } from "net";
10
+ import { dirname } from "path";
11
+
12
+ // src/client.ts
13
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
14
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
15
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
16
+
17
+ // src/auth.ts
18
+ var tokenStorage = null;
19
+ var clientStorage = null;
20
+ async function getAuthFilePath() {
21
+ const os = await import("os");
22
+ const path = await import("path");
23
+ const fs = await import("fs");
24
+ const dir = path.join(os.homedir(), ".config", "mcp-s-cli");
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ return path.join(dir, "auth.json");
27
+ }
28
+ async function loadAuthData() {
29
+ if (tokenStorage !== null && clientStorage !== null) {
30
+ return;
31
+ }
32
+ tokenStorage = /* @__PURE__ */ new Map();
33
+ clientStorage = /* @__PURE__ */ new Map();
34
+ try {
35
+ const fs = await import("fs/promises");
36
+ const authPath = await getAuthFilePath();
37
+ const data = await fs.readFile(authPath, "utf-8");
38
+ const parsed = JSON.parse(data);
39
+ if (parsed.tokens) {
40
+ for (const [key, value] of Object.entries(parsed.tokens)) {
41
+ tokenStorage.set(key, value);
42
+ }
43
+ }
44
+ if (parsed.clients) {
45
+ for (const [key, value] of Object.entries(parsed.clients)) {
46
+ clientStorage.set(key, value);
47
+ }
48
+ }
49
+ } catch {
50
+ }
51
+ }
52
+ async function saveAuthData() {
53
+ if (!tokenStorage || !clientStorage) {
54
+ return;
55
+ }
56
+ try {
57
+ const fs = await import("fs/promises");
58
+ const authPath = await getAuthFilePath();
59
+ const data = {
60
+ tokens: Object.fromEntries(tokenStorage),
61
+ clients: Object.fromEntries(clientStorage)
62
+ };
63
+ await fs.writeFile(authPath, JSON.stringify(data, null, 2), {
64
+ mode: 384
65
+ });
66
+ } catch (err) {
67
+ if (process.env.MCP_DEBUG) {
68
+ console.error(
69
+ `[mcp-s-cli] Failed to save auth data: ${err.message}`
70
+ );
71
+ }
72
+ }
73
+ }
74
+ async function getTokenStorage() {
75
+ await loadAuthData();
76
+ return tokenStorage ?? /* @__PURE__ */ new Map();
77
+ }
78
+ async function getClientStorage() {
79
+ await loadAuthData();
80
+ return clientStorage ?? /* @__PURE__ */ new Map();
81
+ }
82
+ function generateCodeVerifier() {
83
+ const array = new Uint8Array(32);
84
+ crypto.getRandomValues(array);
85
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
86
+ ""
87
+ );
88
+ }
89
+ async function generateCodeChallenge(verifier) {
90
+ const encoder = new TextEncoder();
91
+ const data = encoder.encode(verifier);
92
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
93
+ const hashArray = new Uint8Array(hashBuffer);
94
+ const base64 = btoa(String.fromCharCode(...hashArray)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
95
+ return base64;
96
+ }
97
+ function parseWWWAuthenticateHeader(header) {
98
+ const challenge = {};
99
+ const bearerMatch = header.match(/Bearer\s+(.*)/i);
100
+ if (!bearerMatch) {
101
+ return challenge;
102
+ }
103
+ const params = bearerMatch[1] ?? "";
104
+ const paramRegex = /(\w+)="([^"]*)"/g;
105
+ for (const match of params.matchAll(paramRegex)) {
106
+ const [, key, value] = match;
107
+ if (key === "error") {
108
+ challenge.error = value;
109
+ } else if (key === "error_description") {
110
+ challenge.error_description = value;
111
+ } else if (key === "resource_metadata") {
112
+ challenge.resource_metadata = value;
113
+ }
114
+ }
115
+ return challenge;
116
+ }
117
+ async function fetchProtectedResourceMetadata(metadataUrl) {
118
+ try {
119
+ const response = await fetch(metadataUrl, {
120
+ signal: AbortSignal.timeout(1e4)
121
+ });
122
+ if (!response.ok) {
123
+ return null;
124
+ }
125
+ return await response.json();
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ async function fetchAuthServerMetadata(authServerUrl) {
131
+ try {
132
+ const parsed = new URL(authServerUrl);
133
+ const metadataUrl = `${parsed.protocol}//${parsed.host}/.well-known/oauth-authorization-server`;
134
+ const response = await fetch(metadataUrl, {
135
+ signal: AbortSignal.timeout(1e4)
136
+ });
137
+ if (!response.ok) {
138
+ return null;
139
+ }
140
+ return await response.json();
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+ async function registerOAuthClient(registrationEndpoint, redirectUri) {
146
+ try {
147
+ const clientMetadata = {
148
+ client_name: "mcp-s-cli",
149
+ redirect_uris: [redirectUri],
150
+ grant_types: ["authorization_code"],
151
+ response_types: ["code"],
152
+ token_endpoint_auth_method: "none"
153
+ };
154
+ const response = await fetch(registrationEndpoint, {
155
+ method: "POST",
156
+ headers: {
157
+ "Content-Type": "application/json"
158
+ },
159
+ body: JSON.stringify(clientMetadata),
160
+ signal: AbortSignal.timeout(1e4)
161
+ });
162
+ if (!response.ok) {
163
+ return null;
164
+ }
165
+ return await response.json();
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+ async function exchangeAuthorizationCode(tokenEndpoint, clientId, code, codeVerifier, redirectUri) {
171
+ try {
172
+ const params = new URLSearchParams({
173
+ grant_type: "authorization_code",
174
+ code,
175
+ client_id: clientId,
176
+ code_verifier: codeVerifier,
177
+ redirect_uri: redirectUri
178
+ });
179
+ const response = await fetch(tokenEndpoint, {
180
+ method: "POST",
181
+ headers: {
182
+ "Content-Type": "application/x-www-form-urlencoded"
183
+ },
184
+ body: params.toString(),
185
+ signal: AbortSignal.timeout(1e4)
186
+ });
187
+ if (!response.ok) {
188
+ return null;
189
+ }
190
+ return await response.json();
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ async function startCallbackServer(port) {
196
+ const http = await import("http");
197
+ return new Promise((resolve2, reject) => {
198
+ const server = http.createServer((req, res) => {
199
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
200
+ const code = url.searchParams.get("code");
201
+ const error = url.searchParams.get("error");
202
+ if (error) {
203
+ res.writeHead(400, { "Content-Type": "text/html" });
204
+ res.end(`
205
+ <html>
206
+ <body>
207
+ <h1>Authorization Failed</h1>
208
+ <p>Error: ${error}</p>
209
+ <p>You can close this window.</p>
210
+ </body>
211
+ </html>
212
+ `);
213
+ server.close();
214
+ reject(new Error(`OAuth error: ${error}`));
215
+ return;
216
+ }
217
+ if (code) {
218
+ res.writeHead(200, { "Content-Type": "text/html" });
219
+ res.end(`
220
+ <html>
221
+ <body>
222
+ <h1>Authorization Successful</h1>
223
+ <p>You can close this window and return to the CLI.</p>
224
+ </body>
225
+ </html>
226
+ `);
227
+ server.close();
228
+ resolve2(code);
229
+ return;
230
+ }
231
+ res.writeHead(400, { "Content-Type": "text/html" });
232
+ res.end("<html><body><h1>Invalid Request</h1></body></html>");
233
+ });
234
+ server.listen(port, "127.0.0.1", () => {
235
+ });
236
+ setTimeout(
237
+ () => {
238
+ server.close();
239
+ reject(new Error("OAuth callback timeout"));
240
+ },
241
+ 5 * 60 * 1e3
242
+ );
243
+ });
244
+ }
245
+ async function getOpenCommand() {
246
+ const platform = process.platform;
247
+ if (platform === "darwin") {
248
+ return "open";
249
+ }
250
+ if (platform === "win32") {
251
+ return "start";
252
+ }
253
+ if (platform === "linux") {
254
+ return "xdg-open";
255
+ }
256
+ return null;
257
+ }
258
+ async function performOAuthFlow(authServerUrl, resourceUrl) {
259
+ const authMetadata = await fetchAuthServerMetadata(authServerUrl);
260
+ if (!authMetadata) {
261
+ console.error("Failed to fetch authorization server metadata");
262
+ return null;
263
+ }
264
+ if (!authMetadata.registration_endpoint) {
265
+ console.error(
266
+ "Authorization server does not support dynamic client registration"
267
+ );
268
+ return null;
269
+ }
270
+ const callbackPort = 8085;
271
+ const redirectUri = `http://127.0.0.1:${callbackPort}/callback`;
272
+ const serverKey = new URL(authServerUrl).origin;
273
+ const clients = await getClientStorage();
274
+ let clientInfo = clients.get(serverKey);
275
+ if (!clientInfo) {
276
+ const newClientInfo = await registerOAuthClient(
277
+ authMetadata.registration_endpoint,
278
+ redirectUri
279
+ );
280
+ if (!newClientInfo) {
281
+ console.error("Failed to register OAuth client");
282
+ return null;
283
+ }
284
+ clientInfo = newClientInfo;
285
+ clients.set(serverKey, clientInfo);
286
+ await saveAuthData();
287
+ }
288
+ const codeVerifier = generateCodeVerifier();
289
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
290
+ const authUrl = new URL(authMetadata.authorization_endpoint);
291
+ authUrl.searchParams.set("response_type", "code");
292
+ authUrl.searchParams.set("client_id", clientInfo.client_id);
293
+ authUrl.searchParams.set("redirect_uri", redirectUri);
294
+ authUrl.searchParams.set("code_challenge", codeChallenge);
295
+ authUrl.searchParams.set("code_challenge_method", "S256");
296
+ authUrl.searchParams.set("resource", resourceUrl);
297
+ console.error(
298
+ "\nAuthentication required. Opening browser for authorization..."
299
+ );
300
+ console.error(`If the browser doesn't open, visit: ${authUrl.toString()}
301
+ `);
302
+ const open = await getOpenCommand();
303
+ if (open) {
304
+ try {
305
+ const { spawn: spawn2 } = await import("child_process");
306
+ spawn2(open, [authUrl.toString()], {
307
+ detached: true,
308
+ stdio: "ignore"
309
+ }).unref();
310
+ } catch {
311
+ console.error("Could not open browser automatically.");
312
+ }
313
+ }
314
+ try {
315
+ const code = await startCallbackServer(callbackPort);
316
+ const tokens = await exchangeAuthorizationCode(
317
+ authMetadata.token_endpoint,
318
+ clientInfo.client_id,
319
+ code,
320
+ codeVerifier,
321
+ redirectUri
322
+ );
323
+ if (tokens) {
324
+ const resourceOrigin = new URL(resourceUrl).origin;
325
+ const tokenMap = await getTokenStorage();
326
+ tokenMap.set(resourceOrigin, tokens);
327
+ await saveAuthData();
328
+ }
329
+ return tokens;
330
+ } catch (error) {
331
+ console.error("OAuth flow failed:", error);
332
+ return null;
333
+ }
334
+ }
335
+ async function getStoredTokens(url) {
336
+ const origin = new URL(url).origin;
337
+ const tokens = await getTokenStorage();
338
+ return tokens.get(origin);
339
+ }
340
+ async function storeTokens(url, tokens) {
341
+ const origin = new URL(url).origin;
342
+ const tokenMap = await getTokenStorage();
343
+ tokenMap.set(origin, tokens);
344
+ await saveAuthData();
345
+ }
346
+
347
+ // src/config.ts
348
+ import { createHash } from "crypto";
349
+ import { existsSync, readFileSync } from "fs";
350
+ import { homedir } from "os";
351
+ import { join, resolve } from "path";
352
+ function isHttpServer(config) {
353
+ return "url" in config;
354
+ }
355
+ var DEFAULT_TIMEOUT_SECONDS = 1800;
356
+ var DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_SECONDS * 1e3;
357
+ var DEFAULT_MAX_RETRIES = 3;
358
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
359
+ var DEFAULT_DAEMON_TIMEOUT_SECONDS = 300;
360
+ function debug(message) {
361
+ if (process.env.MCP_DEBUG) {
362
+ console.error(`[mcp-s-cli] ${message}`);
363
+ }
364
+ }
365
+ function getTimeoutMs() {
366
+ const envTimeout = process.env.MCP_TIMEOUT;
367
+ if (envTimeout) {
368
+ const seconds = Number.parseInt(envTimeout, 10);
369
+ if (!Number.isNaN(seconds) && seconds > 0) {
370
+ return seconds * 1e3;
371
+ }
372
+ }
373
+ return DEFAULT_TIMEOUT_MS;
374
+ }
375
+ function getMaxRetries() {
376
+ const envRetries = process.env.MCP_MAX_RETRIES;
377
+ if (envRetries) {
378
+ const retries = Number.parseInt(envRetries, 10);
379
+ if (!Number.isNaN(retries) && retries >= 0) {
380
+ return retries;
381
+ }
382
+ }
383
+ return DEFAULT_MAX_RETRIES;
384
+ }
385
+ function getRetryDelayMs() {
386
+ const envDelay = process.env.MCP_RETRY_DELAY;
387
+ if (envDelay) {
388
+ const delay = Number.parseInt(envDelay, 10);
389
+ if (!Number.isNaN(delay) && delay > 0) {
390
+ return delay;
391
+ }
392
+ }
393
+ return DEFAULT_RETRY_DELAY_MS;
394
+ }
395
+ var DAEMON_SERVER_NAME = "mcp-s-cli";
396
+ function getDaemonTimeoutMs() {
397
+ const envTimeout = process.env.MCP_DAEMON_TIMEOUT;
398
+ if (envTimeout) {
399
+ const seconds = Number.parseInt(envTimeout, 10);
400
+ if (!Number.isNaN(seconds) && seconds > 0) {
401
+ return seconds * 1e3;
402
+ }
403
+ }
404
+ return DEFAULT_DAEMON_TIMEOUT_SECONDS * 1e3;
405
+ }
406
+ function getSocketDir() {
407
+ const uid = process.getuid?.() ?? "unknown";
408
+ const base = process.platform === "darwin" ? "/tmp" : "/tmp";
409
+ return join(base, `mcp-s-cli-${uid}`);
410
+ }
411
+ function getSocketPath() {
412
+ return join(getSocketDir(), `${DAEMON_SERVER_NAME}.sock`);
413
+ }
414
+ function getPidPath() {
415
+ return join(getSocketDir(), `${DAEMON_SERVER_NAME}.pid`);
416
+ }
417
+ function getReadyPath() {
418
+ return join(getSocketDir(), `${DAEMON_SERVER_NAME}.ready`);
419
+ }
420
+ function getConfigHash(config) {
421
+ const str = JSON.stringify(config, Object.keys(config).sort());
422
+ return createHash("sha256").update(str).digest("hex").slice(0, 16);
423
+ }
424
+
425
+ // src/daemon-client.ts
426
+ import { spawn } from "child_process";
427
+ import { existsSync as existsSync2, readdirSync } from "fs";
428
+ import { createConnection } from "net";
429
+ import { join as join2 } from "path";
430
+ import { fileURLToPath } from "url";
431
+
432
+ // src/version.ts
433
+ var VERSION = "0.0.9";
434
+
435
+ // src/client.ts
436
+ function getRetryConfig() {
437
+ const totalBudgetMs = getTimeoutMs();
438
+ const maxRetries = getMaxRetries();
439
+ const baseDelayMs = getRetryDelayMs();
440
+ const retryBudgetMs = Math.max(0, totalBudgetMs - 5e3);
441
+ return {
442
+ maxRetries,
443
+ baseDelayMs,
444
+ maxDelayMs: Math.min(1e4, retryBudgetMs / 2),
445
+ totalBudgetMs
446
+ };
447
+ }
448
+ function isTransientError(error) {
449
+ const nodeError = error;
450
+ if (nodeError.code) {
451
+ const transientCodes = [
452
+ "ECONNREFUSED",
453
+ "ECONNRESET",
454
+ "ETIMEDOUT",
455
+ "ENOTFOUND",
456
+ "EPIPE",
457
+ "ENETUNREACH",
458
+ "EHOSTUNREACH",
459
+ "EAI_AGAIN"
460
+ ];
461
+ if (transientCodes.includes(nodeError.code)) {
462
+ return true;
463
+ }
464
+ }
465
+ const message = error.message;
466
+ if (/^(502|503|504|429)\b/.test(message)) return true;
467
+ if (/\b(http|status(\s+code)?)\s*(502|503|504|429)\b/i.test(message))
468
+ return true;
469
+ if (/\b(502|503|504|429)\s+(bad gateway|service unavailable|gateway timeout|too many requests)/i.test(
470
+ message
471
+ ))
472
+ return true;
473
+ if (/network\s*(error|fail|unavailable|timeout)/i.test(message)) return true;
474
+ if (/connection\s*(reset|refused|timeout)/i.test(message)) return true;
475
+ if (/\btimeout\b/i.test(message)) return true;
476
+ return false;
477
+ }
478
+ function calculateDelay(attempt, config) {
479
+ const exponentialDelay = config.baseDelayMs * 2 ** attempt;
480
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelayMs);
481
+ const jitter = cappedDelay * 0.25 * (Math.random() * 2 - 1);
482
+ return Math.round(cappedDelay + jitter);
483
+ }
484
+ function sleep(ms) {
485
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
486
+ }
487
+ async function withRetry(fn, operationName, config = getRetryConfig()) {
488
+ let lastError;
489
+ const startTime = Date.now();
490
+ for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
491
+ const elapsed = Date.now() - startTime;
492
+ if (elapsed >= config.totalBudgetMs) {
493
+ debug(`${operationName}: timeout budget exhausted after ${elapsed}ms`);
494
+ break;
495
+ }
496
+ try {
497
+ return await fn();
498
+ } catch (error) {
499
+ lastError = error;
500
+ const remainingBudget = config.totalBudgetMs - (Date.now() - startTime);
501
+ const shouldRetry = attempt < config.maxRetries && isTransientError(lastError) && remainingBudget > 1e3;
502
+ if (shouldRetry) {
503
+ const delay = Math.min(
504
+ calculateDelay(attempt, config),
505
+ remainingBudget - 1e3
506
+ );
507
+ debug(
508
+ `${operationName} failed (attempt ${attempt + 1}/${config.maxRetries + 1}): ${lastError.message}. Retrying in ${delay}ms...`
509
+ );
510
+ await sleep(delay);
511
+ } else {
512
+ throw lastError;
513
+ }
514
+ }
515
+ }
516
+ throw lastError;
517
+ }
518
+ async function connectToServer(serverName, config) {
519
+ if (isHttpServer(config)) {
520
+ return connectToHttpServer(serverName, config);
521
+ }
522
+ const stderrChunks = [];
523
+ return withRetry(async () => {
524
+ const client = new Client(
525
+ {
526
+ name: "mcp-s-cli",
527
+ version: VERSION
528
+ },
529
+ {
530
+ capabilities: {}
531
+ }
532
+ );
533
+ const transport = createStdioTransport(config);
534
+ const stderrStream = transport.stderr;
535
+ if (stderrStream) {
536
+ stderrStream.on("data", (chunk) => {
537
+ const text = chunk.toString();
538
+ stderrChunks.push(text);
539
+ process.stderr.write(`[${serverName}] ${text}`);
540
+ });
541
+ }
542
+ try {
543
+ await client.connect(transport);
544
+ } catch (error) {
545
+ const stderrOutput = stderrChunks.join("").trim();
546
+ if (stderrOutput) {
547
+ const err = error;
548
+ err.message = `${err.message}
549
+
550
+ Server stderr:
551
+ ${stderrOutput}`;
552
+ }
553
+ throw error;
554
+ }
555
+ const stderrStream2 = transport.stderr;
556
+ if (stderrStream2) {
557
+ stderrStream2.on("data", (chunk) => {
558
+ process.stderr.write(chunk);
559
+ });
560
+ }
561
+ return {
562
+ client,
563
+ close: async () => {
564
+ await client.close();
565
+ }
566
+ };
567
+ }, `connect to ${serverName}`);
568
+ }
569
+ async function connectToHttpServer(serverName, config) {
570
+ const oauthEnabled = process.env.MCP_NO_OAUTH !== "1";
571
+ return withRetry(async () => {
572
+ const configuredAuth = config.headers?.Authorization || config.headers?.authorization;
573
+ let headers = { ...config.headers };
574
+ if (!configuredAuth) {
575
+ const stored = await getStoredTokens(config.url);
576
+ if (stored) {
577
+ headers = {
578
+ ...headers,
579
+ Authorization: `Bearer ${stored.access_token}`
580
+ };
581
+ }
582
+ }
583
+ return attemptHttpConnect(serverName, config, headers, oauthEnabled);
584
+ }, `connect to ${serverName}`);
585
+ }
586
+ async function attemptHttpConnect(serverName, config, headers, oauthEnabled = true) {
587
+ const hasAuth = Boolean(headers.Authorization || headers.authorization);
588
+ let effectiveHeaders = headers;
589
+ let oauthAlreadyRan = false;
590
+ if (oauthEnabled && !hasAuth) {
591
+ const probeTokens = await probeAndOAuth(serverName, config, headers);
592
+ if (probeTokens) {
593
+ effectiveHeaders = {
594
+ ...headers,
595
+ Authorization: `Bearer ${probeTokens.access_token}`
596
+ };
597
+ oauthAlreadyRan = true;
598
+ }
599
+ }
600
+ const client = new Client(
601
+ { name: "mcp-s-cli", version: VERSION },
602
+ { capabilities: {} }
603
+ );
604
+ const transport = new StreamableHTTPClientTransport(new URL(config.url), {
605
+ requestInit: { headers: effectiveHeaders }
606
+ });
607
+ try {
608
+ await client.connect(transport);
609
+ return {
610
+ client,
611
+ close: async () => {
612
+ await client.close();
613
+ }
614
+ };
615
+ } catch (connectError) {
616
+ if (oauthAlreadyRan) {
617
+ throw connectError;
618
+ }
619
+ const errMsg = connectError.message ?? "";
620
+ const isAuthError = errMsg.includes("401") || errMsg.toLowerCase().includes("unauthorized") || errMsg.includes("invalid_token") || errMsg.toLowerCase().includes("missing authorization") || errMsg.toLowerCase().includes("authentication") || errMsg.toLowerCase().includes("access denied");
621
+ if (!isAuthError || !oauthEnabled) {
622
+ throw connectError;
623
+ }
624
+ debug(`Auth error from ${serverName}, probing for OAuth metadata`);
625
+ const retryTokens = await probeAndOAuth(
626
+ serverName,
627
+ config,
628
+ effectiveHeaders,
629
+ true
630
+ );
631
+ if (!retryTokens) {
632
+ throw connectError;
633
+ }
634
+ const newHeaders = {
635
+ ...effectiveHeaders,
636
+ Authorization: `Bearer ${retryTokens.access_token}`
637
+ };
638
+ const newClient = new Client(
639
+ { name: "mcp-s-cli", version: VERSION },
640
+ { capabilities: {} }
641
+ );
642
+ const newTransport = new StreamableHTTPClientTransport(
643
+ new URL(config.url),
644
+ { requestInit: { headers: newHeaders } }
645
+ );
646
+ await newClient.connect(newTransport);
647
+ return {
648
+ client: newClient,
649
+ close: async () => {
650
+ await newClient.close();
651
+ }
652
+ };
653
+ }
654
+ }
655
+ async function probeAndOAuth(serverName, config, _headers, force = false) {
656
+ debug(`Probing ${serverName} for OAuth metadata`);
657
+ let probeResponse;
658
+ try {
659
+ probeResponse = await fetch(config.url, {
660
+ signal: AbortSignal.timeout(1e4)
661
+ });
662
+ } catch {
663
+ return null;
664
+ }
665
+ if (!force && probeResponse.status !== 401) {
666
+ return null;
667
+ }
668
+ const wwwAuth = probeResponse.headers.get("WWW-Authenticate");
669
+ if (!wwwAuth) {
670
+ return null;
671
+ }
672
+ const challenge = parseWWWAuthenticateHeader(wwwAuth);
673
+ if (!challenge.resource_metadata) {
674
+ return null;
675
+ }
676
+ const resourceMetadata = await fetchProtectedResourceMetadata(
677
+ challenge.resource_metadata
678
+ );
679
+ if (!resourceMetadata?.authorization_servers?.[0]) {
680
+ return null;
681
+ }
682
+ const newTokens = await performOAuthFlow(
683
+ resourceMetadata.authorization_servers[0],
684
+ resourceMetadata.resource
685
+ );
686
+ if (newTokens) {
687
+ await storeTokens(config.url, newTokens);
688
+ }
689
+ return newTokens;
690
+ }
691
+ function createStdioTransport(config) {
692
+ const mergedEnv = {};
693
+ for (const [key, value] of Object.entries(process.env)) {
694
+ if (value !== void 0) {
695
+ mergedEnv[key] = value;
696
+ }
697
+ }
698
+ if (config.env) {
699
+ Object.assign(mergedEnv, config.env);
700
+ }
701
+ return new StdioClientTransport({
702
+ command: config.command,
703
+ args: config.args,
704
+ env: mergedEnv,
705
+ cwd: config.cwd,
706
+ stderr: "pipe"
707
+ // Capture stderr for better error messages
708
+ });
709
+ }
710
+ async function listTools(client) {
711
+ return withRetry(async () => {
712
+ const result = await client.listTools();
713
+ return result.tools.map((tool) => ({
714
+ name: tool.name,
715
+ description: tool.description,
716
+ inputSchema: tool.inputSchema
717
+ }));
718
+ }, "list tools");
719
+ }
720
+ async function callTool(client, toolName, args) {
721
+ return withRetry(async () => {
722
+ const result = await client.callTool(
723
+ {
724
+ name: toolName,
725
+ arguments: args
726
+ },
727
+ void 0,
728
+ { timeout: getTimeoutMs() }
729
+ );
730
+ return result;
731
+ }, `call tool ${toolName}`);
732
+ }
733
+
734
+ // src/daemon.ts
735
+ function writePidFile(serverName, configHash) {
736
+ const pidPath = getPidPath();
737
+ const dir = dirname(pidPath);
738
+ if (!existsSync3(dir)) {
739
+ mkdirSync(dir, { recursive: true, mode: 448 });
740
+ }
741
+ const content = {
742
+ pid: process.pid,
743
+ configHash,
744
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
745
+ };
746
+ writeFileSync(pidPath, JSON.stringify(content), { mode: 384 });
747
+ }
748
+ function readPidFile(serverName) {
749
+ const pidPath = getPidPath();
750
+ if (!existsSync3(pidPath)) {
751
+ return null;
752
+ }
753
+ try {
754
+ const content = readFileSync2(pidPath, "utf-8");
755
+ return JSON.parse(content);
756
+ } catch {
757
+ return null;
758
+ }
759
+ }
760
+ function removePidFile(serverName) {
761
+ const pidPath = getPidPath();
762
+ try {
763
+ if (existsSync3(pidPath)) {
764
+ unlinkSync(pidPath);
765
+ }
766
+ } catch {
767
+ }
768
+ }
769
+ function removeSocketFile(serverName) {
770
+ const socketPath = getSocketPath();
771
+ try {
772
+ if (existsSync3(socketPath)) {
773
+ unlinkSync(socketPath);
774
+ }
775
+ } catch {
776
+ }
777
+ }
778
+ function removeReadyFile(serverName) {
779
+ const readyPath = getReadyPath();
780
+ try {
781
+ if (existsSync3(readyPath)) {
782
+ unlinkSync(readyPath);
783
+ }
784
+ } catch {
785
+ }
786
+ }
787
+ function isProcessRunning(pid) {
788
+ try {
789
+ process.kill(pid, 0);
790
+ return true;
791
+ } catch {
792
+ return false;
793
+ }
794
+ }
795
+ function killProcess(pid) {
796
+ try {
797
+ process.kill(pid, "SIGTERM");
798
+ return true;
799
+ } catch {
800
+ return false;
801
+ }
802
+ }
803
+ async function runDaemon(serverName, config) {
804
+ const socketPath = getSocketPath();
805
+ const configHash = getConfigHash(config);
806
+ const timeoutMs = getDaemonTimeoutMs();
807
+ let idleTimer = null;
808
+ let mcpClient = null;
809
+ let server = null;
810
+ const activeConnections = /* @__PURE__ */ new Set();
811
+ const cleanup = async () => {
812
+ debug(`[daemon:${serverName}] Shutting down...`);
813
+ if (idleTimer) {
814
+ clearTimeout(idleTimer);
815
+ idleTimer = null;
816
+ }
817
+ for (const conn of activeConnections) {
818
+ try {
819
+ conn.end();
820
+ } catch {
821
+ }
822
+ }
823
+ activeConnections.clear();
824
+ if (mcpClient) {
825
+ try {
826
+ await mcpClient.close();
827
+ } catch {
828
+ }
829
+ mcpClient = null;
830
+ }
831
+ if (server) {
832
+ try {
833
+ server.close();
834
+ } catch {
835
+ }
836
+ server = null;
837
+ }
838
+ removeSocketFile(serverName);
839
+ removePidFile(serverName);
840
+ removeReadyFile(serverName);
841
+ debug(`[daemon:${serverName}] Cleanup complete`);
842
+ };
843
+ const resetIdleTimer = () => {
844
+ if (idleTimer) {
845
+ clearTimeout(idleTimer);
846
+ }
847
+ idleTimer = setTimeout(async () => {
848
+ debug(`[daemon:${serverName}] Idle timeout reached, shutting down`);
849
+ await cleanup();
850
+ process.exit(0);
851
+ }, timeoutMs);
852
+ };
853
+ process.on("SIGTERM", async () => {
854
+ await cleanup();
855
+ process.exit(0);
856
+ });
857
+ process.on("SIGINT", async () => {
858
+ await cleanup();
859
+ process.exit(0);
860
+ });
861
+ const socketDir = getSocketDir();
862
+ if (!existsSync3(socketDir)) {
863
+ mkdirSync(socketDir, { recursive: true, mode: 448 });
864
+ }
865
+ removeSocketFile(serverName);
866
+ removeReadyFile(serverName);
867
+ writePidFile(serverName, configHash);
868
+ try {
869
+ debug(`[daemon:${serverName}] Connecting to MCP server...`);
870
+ mcpClient = await connectToServer(serverName, config);
871
+ debug(`[daemon:${serverName}] Connected to MCP server`);
872
+ } catch (error) {
873
+ console.error(
874
+ `[daemon:${serverName}] Failed to connect:`,
875
+ error.message
876
+ );
877
+ await cleanup();
878
+ process.exit(1);
879
+ }
880
+ const handleRequest = async (data) => {
881
+ resetIdleTimer();
882
+ let request;
883
+ try {
884
+ request = JSON.parse(data.toString());
885
+ } catch {
886
+ return {
887
+ id: "unknown",
888
+ success: false,
889
+ error: { code: "INVALID_REQUEST", message: "Invalid JSON" }
890
+ };
891
+ }
892
+ debug(`[daemon:${serverName}] Request: ${request.type} (${request.id})`);
893
+ if (!mcpClient) {
894
+ return {
895
+ id: request.id,
896
+ success: false,
897
+ error: { code: "NOT_CONNECTED", message: "MCP client not connected" }
898
+ };
899
+ }
900
+ try {
901
+ switch (request.type) {
902
+ case "ping":
903
+ return { id: request.id, success: true, data: "pong" };
904
+ case "listTools": {
905
+ const tools = await listTools(mcpClient.client);
906
+ return { id: request.id, success: true, data: tools };
907
+ }
908
+ case "callTool": {
909
+ if (!request.toolName) {
910
+ return {
911
+ id: request.id,
912
+ success: false,
913
+ error: { code: "MISSING_TOOL", message: "toolName required" }
914
+ };
915
+ }
916
+ const result = await callTool(
917
+ mcpClient.client,
918
+ request.toolName,
919
+ request.args ?? {}
920
+ );
921
+ return { id: request.id, success: true, data: result };
922
+ }
923
+ case "getInstructions": {
924
+ const instructions = mcpClient.client.getInstructions();
925
+ return { id: request.id, success: true, data: instructions };
926
+ }
927
+ case "close":
928
+ setTimeout(async () => {
929
+ await cleanup();
930
+ process.exit(0);
931
+ }, 100);
932
+ return { id: request.id, success: true, data: "closing" };
933
+ default:
934
+ return {
935
+ id: request.id,
936
+ success: false,
937
+ error: {
938
+ code: "UNKNOWN_TYPE",
939
+ message: `Unknown request type: ${request.type}`
940
+ }
941
+ };
942
+ }
943
+ } catch (error) {
944
+ const err = error;
945
+ return {
946
+ id: request.id,
947
+ success: false,
948
+ error: { code: "EXECUTION_ERROR", message: err.message }
949
+ };
950
+ }
951
+ };
952
+ await new Promise((resolve2, reject) => {
953
+ server = createServer((socket) => {
954
+ activeConnections.add(socket);
955
+ debug(`[daemon:${serverName}] Client connected`);
956
+ let buffer = "";
957
+ socket.on("data", async (data) => {
958
+ buffer += data.toString();
959
+ const lines = buffer.split("\n");
960
+ buffer = lines.pop() ?? "";
961
+ for (const line of lines) {
962
+ if (!line.trim()) continue;
963
+ const response = await handleRequest(Buffer.from(line));
964
+ socket.write(`${JSON.stringify(response)}
965
+ `);
966
+ }
967
+ });
968
+ socket.on("close", () => {
969
+ activeConnections.delete(socket);
970
+ debug(`[daemon:${serverName}] Client disconnected`);
971
+ });
972
+ socket.on("error", (error) => {
973
+ debug(`[daemon:${serverName}] Socket error: ${error.message}`);
974
+ activeConnections.delete(socket);
975
+ });
976
+ });
977
+ server.on("error", reject);
978
+ server.listen(socketPath, () => {
979
+ debug(`[daemon:${serverName}] Listening on ${socketPath}`);
980
+ resetIdleTimer();
981
+ writeFileSync(getReadyPath(), String(process.pid), {
982
+ mode: 384
983
+ });
984
+ resolve2();
985
+ });
986
+ }).catch(async (error) => {
987
+ console.error(
988
+ `[daemon:${serverName}] Failed to start socket server:`,
989
+ error.message
990
+ );
991
+ await cleanup();
992
+ process.exit(1);
993
+ });
994
+ }
995
+ if (process.argv[2] === "--daemon") {
996
+ const serverName = process.argv[3];
997
+ const configJson = process.argv[4];
998
+ if (!serverName || !configJson) {
999
+ console.error("Usage: daemon.ts --daemon <serverName> <configJson>");
1000
+ process.exit(1);
1001
+ }
1002
+ let config;
1003
+ try {
1004
+ config = JSON.parse(configJson);
1005
+ } catch {
1006
+ console.error("Invalid config JSON");
1007
+ process.exit(1);
1008
+ }
1009
+ runDaemon(serverName, config).catch((error) => {
1010
+ console.error("Daemon failed:", error);
1011
+ process.exit(1);
1012
+ });
1013
+ }
1014
+ export {
1015
+ isProcessRunning,
1016
+ killProcess,
1017
+ readPidFile,
1018
+ removePidFile,
1019
+ removeReadyFile,
1020
+ removeSocketFile,
1021
+ runDaemon,
1022
+ writePidFile
1023
+ };