mcp-compression-proxy 1.0.3 → 1.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/dist/cli/index.js CHANGED
@@ -1,15 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'child_process';
3
- import { existsSync, readFileSync, unlinkSync } from 'fs';
3
+ import { existsSync, readFileSync, unlinkSync, statSync, openSync, readSync, closeSync, } from 'fs';
4
4
  import { join, dirname } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import { homedir } from 'os';
7
7
  import { isDaemonRunning } from './ipc-client.js';
8
- import { handleTools, handleSearch, handleInfo, handleCall, handleStats, handleDaemonStatus, } from './commands.js';
8
+ import { isManagedRouterConfigured, managedRouterUnavailableMessage } from './runtime-mode.js';
9
+ import { handleTools, handleSearch, handleInfo, handleCall, handlePayloadRead, handlePayloadFind, handleScript, handleStats, handleDaemonStatus, handleDoctor, tailLines, } from './commands.js';
9
10
  const BASE_DIR = join(homedir(), '.mcp-compression-proxy');
10
11
  const SOCKET_PATH = join(BASE_DIR, 'daemon.sock');
11
12
  const PID_FILE = join(BASE_DIR, 'daemon.pid');
12
13
  const READY_FILE = join(BASE_DIR, 'daemon.ready');
14
+ const LOG_FILE = join(BASE_DIR, 'daemon.log');
13
15
  const USAGE = `
14
16
  mcp-cli — Progressive MCP tool discovery for LLMs
15
17
 
@@ -18,10 +20,17 @@ Usage:
18
20
  mcp-cli search <query> Search tools by name/description
19
21
  mcp-cli info <server>/<tool> Get full schema for a tool
20
22
  mcp-cli call <server>/<tool> <json> Execute a tool
23
+ mcp-cli output read <id> [offset] [length|all]
24
+ Read cached large output
25
+ mcp-cli output find <id> <query> Find text in cached large output
26
+ mcp-cli script <json> Run a declarative MCP call chain
21
27
  mcp-cli stats Show compression statistics
28
+ mcp-cli doctor Check config and backend health
22
29
  mcp-cli daemon start Start the background daemon
23
30
  mcp-cli daemon stop Stop the daemon
31
+ mcp-cli daemon restart Restart the daemon
24
32
  mcp-cli daemon status Show daemon status
33
+ mcp-cli daemon logs [-n N] [-f] Show daemon logs (default: last 50)
25
34
  mcp-cli help Show this help
26
35
 
27
36
  Options:
@@ -32,6 +41,10 @@ Options:
32
41
  * Forks the daemon.ts module with detached: true.
33
42
  */
34
43
  async function startDaemon() {
44
+ if (isManagedRouterConfigured(BASE_DIR)) {
45
+ console.error('Error: This installation uses the managed MCP router. Refusing to start a legacy daemon on the stable socket.');
46
+ return false;
47
+ }
35
48
  const __filename = fileURLToPath(import.meta.url);
36
49
  const __dirname = dirname(__filename);
37
50
  const daemonScript = join(__dirname, 'daemon.js');
@@ -61,7 +74,7 @@ async function startDaemon() {
61
74
  if (existsSync(READY_FILE)) {
62
75
  return true;
63
76
  }
64
- await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
77
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
65
78
  }
66
79
  // Fallback: check if socket is reachable
67
80
  return isDaemonRunning(SOCKET_PATH);
@@ -132,6 +145,68 @@ async function stopDaemon() {
132
145
  console.error(`Daemon (PID ${pid}) did not exit within 10s; it may still be shutting down.`);
133
146
  return false;
134
147
  }
148
+ /**
149
+ * Print the tail of the daemon log, optionally following it.
150
+ *
151
+ * Polls rather than shelling out to `tail`: spawning it drags in platform
152
+ * differences (BSD vs GNU flags) for something this file can do itself, and a
153
+ * dependency for it would be worse still.
154
+ */
155
+ async function showLogs(args) {
156
+ const follow = args.includes('-f') || args.includes('--follow');
157
+ const countIndex = args.findIndex((arg) => arg === '-n' || arg === '--lines');
158
+ const requested = countIndex === -1 ? NaN : Number.parseInt(args[countIndex + 1] ?? '', 10);
159
+ const count = Number.isInteger(requested) && requested > 0 ? requested : 50;
160
+ if (!existsSync(LOG_FILE)) {
161
+ console.error(`No daemon log at ${LOG_FILE}.`);
162
+ console.error('The daemon writes it on first start: mcp-cli daemon start');
163
+ process.exit(1);
164
+ }
165
+ console.log(tailLines(readFileSync(LOG_FILE, 'utf-8'), count));
166
+ if (!follow)
167
+ return;
168
+ // Track the offset rather than re-reading the file: a long-running daemon's
169
+ // log is appended to constantly.
170
+ let offset = statSync(LOG_FILE).size;
171
+ await new Promise((resolve) => {
172
+ const timer = setInterval(() => {
173
+ let size;
174
+ try {
175
+ size = statSync(LOG_FILE).size;
176
+ }
177
+ catch {
178
+ return; // rotated out from under us; pick it up on the next tick
179
+ }
180
+ // Truncation or rotation resets the file, so start over rather than
181
+ // seeking past the end and printing nothing forever.
182
+ if (size < offset) {
183
+ offset = 0;
184
+ }
185
+ if (size === offset)
186
+ return;
187
+ const handle = openSync(LOG_FILE, 'r');
188
+ try {
189
+ const buffer = Buffer.alloc(size - offset);
190
+ readSync(handle, buffer, 0, buffer.length, offset);
191
+ process.stdout.write(buffer.toString('utf-8'));
192
+ offset = size;
193
+ }
194
+ finally {
195
+ closeSync(handle);
196
+ }
197
+ }, 500);
198
+ // Housekeeping must never be the reason this process cannot exit.
199
+ timer.unref?.();
200
+ const stop = () => {
201
+ clearInterval(timer);
202
+ process.off('SIGINT', stop);
203
+ process.off('SIGTERM', stop);
204
+ resolve();
205
+ };
206
+ process.on('SIGINT', stop);
207
+ process.on('SIGTERM', stop);
208
+ });
209
+ }
135
210
  /**
136
211
  * Ensure daemon is running, auto-starting if needed.
137
212
  */
@@ -139,6 +214,10 @@ async function ensureDaemon(noAutoStart) {
139
214
  const running = await isDaemonRunning(SOCKET_PATH);
140
215
  if (running)
141
216
  return;
217
+ if (isManagedRouterConfigured(BASE_DIR)) {
218
+ console.error(`Error: ${managedRouterUnavailableMessage()}`);
219
+ process.exit(1);
220
+ }
142
221
  if (noAutoStart) {
143
222
  console.error('Error: Daemon is not running. Start it with: mcp-cli daemon start');
144
223
  process.exit(1);
@@ -165,7 +244,9 @@ async function readStdin() {
165
244
  // stdin has already ended.
166
245
  const timer = setTimeout(() => resolve(data.trim() || null), 1000);
167
246
  timer.unref?.();
168
- process.stdin.on('data', (chunk) => { data += chunk; });
247
+ process.stdin.on('data', (chunk) => {
248
+ data += chunk;
249
+ });
169
250
  process.stdin.on('end', () => {
170
251
  clearTimeout(timer);
171
252
  resolve(data.trim() || null);
@@ -175,7 +256,7 @@ async function readStdin() {
175
256
  async function main() {
176
257
  const args = process.argv.slice(2);
177
258
  const noAutoStart = args.includes('--no-auto-start');
178
- const filteredArgs = args.filter(a => a !== '--no-auto-start');
259
+ const filteredArgs = args.filter((a) => a !== '--no-auto-start');
179
260
  const command = filteredArgs[0];
180
261
  if (!command || command === 'help' || command === '--help' || command === '-h') {
181
262
  console.log(USAGE);
@@ -212,11 +293,26 @@ async function main() {
212
293
  case 'stop':
213
294
  await stopDaemon();
214
295
  return;
296
+ case 'restart': {
297
+ // stopDaemon() returning false just means nothing was running, which
298
+ // is a fine state to start from.
299
+ await stopDaemon();
300
+ const started = await startDaemon();
301
+ if (!started) {
302
+ console.error('Failed to start daemon.');
303
+ process.exit(1);
304
+ }
305
+ console.log('Daemon restarted.');
306
+ return;
307
+ }
215
308
  case 'status':
216
309
  await handleDaemonStatus(SOCKET_PATH);
217
310
  return;
311
+ case 'logs':
312
+ await showLogs(filteredArgs.slice(2));
313
+ return;
218
314
  default:
219
- console.error('Usage: mcp-cli daemon <start|stop|status>');
315
+ console.error('Usage: mcp-cli daemon <start|stop|restart|status|logs>');
220
316
  process.exit(1);
221
317
  }
222
318
  }
@@ -238,7 +334,7 @@ async function main() {
238
334
  break;
239
335
  case 'call': {
240
336
  if (!filteredArgs[1]) {
241
- console.error('Usage: mcp-cli call <server>/<tool> \'<json_payload>\'');
337
+ console.error("Usage: mcp-cli call <server>/<tool> '<json_payload>'");
242
338
  process.exit(1);
243
339
  }
244
340
  // Payload from args or stdin
@@ -253,9 +349,53 @@ async function main() {
253
349
  await handleCall(SOCKET_PATH, filteredArgs[1], payload);
254
350
  break;
255
351
  }
352
+ case 'output': {
353
+ const action = filteredArgs[1];
354
+ const id = filteredArgs[2];
355
+ if (!id || (action !== 'read' && action !== 'find')) {
356
+ console.error('Usage: mcp-cli output <read|find> <payload-id> ...');
357
+ process.exit(1);
358
+ }
359
+ if (action === 'find') {
360
+ const query = filteredArgs.slice(3).join(' ');
361
+ if (!query) {
362
+ console.error('Usage: mcp-cli output find <payload-id> <query>');
363
+ process.exit(1);
364
+ }
365
+ await handlePayloadFind(SOCKET_PATH, id, query);
366
+ break;
367
+ }
368
+ const offset = filteredArgs[3] === undefined ? undefined : Number.parseInt(filteredArgs[3], 10);
369
+ const lengthArg = filteredArgs[4];
370
+ const all = lengthArg === 'all';
371
+ const length = lengthArg && !all ? Number.parseInt(lengthArg, 10) : undefined;
372
+ await handlePayloadRead(SOCKET_PATH, id, {
373
+ offset: Number.isFinite(offset) ? offset : undefined,
374
+ length: Number.isFinite(length) ? length : undefined,
375
+ all,
376
+ });
377
+ break;
378
+ }
379
+ case 'script': {
380
+ let payload = filteredArgs[1] || '';
381
+ if (!payload) {
382
+ const stdinData = await readStdin();
383
+ if (stdinData)
384
+ payload = stdinData;
385
+ }
386
+ if (!payload) {
387
+ console.error('Usage: mcp-cli script <json>');
388
+ process.exit(1);
389
+ }
390
+ await handleScript(SOCKET_PATH, payload);
391
+ break;
392
+ }
256
393
  case 'stats':
257
394
  await handleStats(SOCKET_PATH);
258
395
  break;
396
+ case 'doctor':
397
+ await handleDoctor(SOCKET_PATH);
398
+ break;
259
399
  default:
260
400
  console.error(`Unknown command: ${command}\n`);
261
401
  console.log(USAGE);
@@ -1,6 +1,75 @@
1
+ export declare const DEFAULT_PAYLOAD_THRESHOLD = 10000;
2
+ export declare const DEFAULT_PAYLOAD_READ_LENGTH = 10000;
3
+ export interface PayloadReference {
4
+ id: string;
5
+ path: string;
6
+ chars: number;
7
+ createdAt: number;
8
+ }
9
+ export interface CapturedPayload {
10
+ output: string;
11
+ reference?: PayloadReference;
12
+ }
13
+ export interface PayloadReadResult {
14
+ id: string;
15
+ content: string;
16
+ offset: number;
17
+ nextOffset: number;
18
+ totalChars: number;
19
+ eof: boolean;
20
+ }
21
+ export interface PayloadFindMatch {
22
+ offset: number;
23
+ line: number;
24
+ match: string;
25
+ context: string;
26
+ }
27
+ export interface PayloadFindResult {
28
+ id: string;
29
+ query: string;
30
+ totalChars: number;
31
+ matches: PayloadFindMatch[];
32
+ truncated: boolean;
33
+ }
34
+ export interface PayloadStoreOptions {
35
+ maxEntries?: number;
36
+ directory?: string;
37
+ removeDirectoryOnDestroy?: boolean;
38
+ }
1
39
  /**
2
- * Intercepts large tool call outputs, saves them to a temp file,
3
- * and returns a reference instead of the full payload.
40
+ * Process-local, file-backed storage for large MCP outputs.
41
+ *
42
+ * Handles, not paths, are accepted by read/find operations so callers cannot
43
+ * use the daemon as an arbitrary local-file reader. Files live in a private
44
+ * 0700 temp directory and are written 0600.
45
+ */
46
+ export declare class PayloadStore {
47
+ private readonly maxEntries;
48
+ private readonly configuredDirectory;
49
+ private readonly removeDirectoryOnDestroy;
50
+ private readonly entries;
51
+ private payloadDir;
52
+ constructor(options?: PayloadStoreOptions);
53
+ private getPayloadDir;
54
+ private getEntry;
55
+ private refreshEntries;
56
+ private evictOldest;
57
+ capture(output: string, threshold?: number): CapturedPayload;
58
+ private referenceMessage;
59
+ read(id: string, options?: {
60
+ offset?: number;
61
+ length?: number;
62
+ all?: boolean;
63
+ }): PayloadReadResult;
64
+ find(id: string, query: string, options?: {
65
+ caseSensitive?: boolean;
66
+ maxMatches?: number;
67
+ contextChars?: number;
68
+ }): PayloadFindResult;
69
+ destroy(): void;
70
+ }
71
+ /**
72
+ * Backward-compatible string-only wrapper used by existing callers.
4
73
  */
5
74
  export declare function interceptPayload(output: string, threshold?: number): string;
6
75
  //# sourceMappingURL=payload-interceptor.d.ts.map
@@ -1,49 +1,227 @@
1
1
  import { createHash } from 'crypto';
2
- import { mkdtempSync, writeFileSync } from 'fs';
2
+ import { existsSync, chmodSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, unlinkSync, writeFileSync, } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
- const DEFAULT_THRESHOLD = 500;
5
+ export const DEFAULT_PAYLOAD_THRESHOLD = 10_000;
6
+ export const DEFAULT_PAYLOAD_READ_LENGTH = 10_000;
6
7
  /**
7
- * Private directory for intercepted payloads, created on first use.
8
+ * Process-local, file-backed storage for large MCP outputs.
8
9
  *
9
- * Writing `mcp_output_<hash>.txt` straight into the shared temp directory
10
- * made the path predictable from the content alone: another local user could
11
- * pre-plant a symlink there and turn our write into an arbitrary-file
12
- * overwrite, or simply read whatever a backend MCP server had just returned.
13
- * mkdtemp gives us a 0700 directory with an unpredictable name, so neither is
14
- * reachable. The name is stable for the lifetime of the process, which keeps
15
- * identical content mapping to a single file.
10
+ * Handles, not paths, are accepted by read/find operations so callers cannot
11
+ * use the daemon as an arbitrary local-file reader. Files live in a private
12
+ * 0700 temp directory and are written 0600.
16
13
  */
17
- let payloadDir;
18
- function getPayloadDir() {
19
- if (payloadDir === undefined) {
20
- payloadDir = mkdtempSync(join(tmpdir(), 'mcp-output-'));
14
+ export class PayloadStore {
15
+ maxEntries;
16
+ configuredDirectory;
17
+ removeDirectoryOnDestroy;
18
+ entries = new Map();
19
+ payloadDir;
20
+ constructor(options = {}) {
21
+ this.maxEntries = Math.max(1, options.maxEntries ?? 100);
22
+ this.configuredDirectory = options.directory;
23
+ this.removeDirectoryOnDestroy =
24
+ options.removeDirectoryOnDestroy ??
25
+ options.directory === undefined;
21
26
  }
22
- return payloadDir;
23
- }
24
- /**
25
- * Intercepts large tool call outputs, saves them to a temp file,
26
- * and returns a reference instead of the full payload.
27
- */
28
- export function interceptPayload(output, threshold) {
29
- const limit = threshold ?? DEFAULT_THRESHOLD;
30
- if (output.length <= limit) {
31
- return output;
27
+ getPayloadDir() {
28
+ if (this.payloadDir === undefined) {
29
+ if (this.configuredDirectory) {
30
+ mkdirSync(this.configuredDirectory, {
31
+ recursive: true,
32
+ mode: 0o700,
33
+ });
34
+ // Tighten an existing directory created under a permissive umask.
35
+ const mode = statSync(this.configuredDirectory).mode & 0o777;
36
+ if (mode !== 0o700) {
37
+ chmodSync(this.configuredDirectory, 0o700);
38
+ }
39
+ this.payloadDir = this.configuredDirectory;
40
+ }
41
+ else {
42
+ this.payloadDir = mkdtempSync(join(tmpdir(), 'mcp-output-'));
43
+ }
44
+ }
45
+ return this.payloadDir;
32
46
  }
33
- const hash = createHash('sha256').update(output).digest('hex').slice(0, 12);
34
- const filename = `mcp_output_${hash}.txt`;
35
- const filepath = join(getPayloadDir(), filename);
36
- try {
37
- // 'wx' fails rather than following anything already at the path.
38
- writeFileSync(filepath, output, { encoding: 'utf-8', mode: 0o600, flag: 'wx' });
47
+ getEntry(id) {
48
+ if (!/^[a-f0-9]{16,64}$/.test(id)) {
49
+ throw new Error(`Payload '${id}' not found or expired`);
50
+ }
51
+ let entry = this.entries.get(id);
52
+ if (!entry) {
53
+ const path = join(this.getPayloadDir(), `mcp_output_${id}.txt`);
54
+ if (existsSync(path)) {
55
+ const content = readFileSync(path, 'utf-8');
56
+ const stat = statSync(path);
57
+ entry = {
58
+ id,
59
+ path,
60
+ chars: content.length,
61
+ createdAt: stat.mtimeMs,
62
+ hash: createHash('sha256').update(content).digest('hex'),
63
+ };
64
+ this.entries.set(id, entry);
65
+ }
66
+ }
67
+ if (!entry || !existsSync(entry.path)) {
68
+ this.entries.delete(id);
69
+ throw new Error(`Payload '${id}' not found or expired`);
70
+ }
71
+ return entry;
39
72
  }
40
- catch (error) {
41
- // Same content already written by this process - the existing file is the
42
- // file we were about to write, so reuse it. Anything else is a real error.
43
- if (error.code !== 'EEXIST') {
44
- throw error;
73
+ refreshEntries() {
74
+ const directory = this.getPayloadDir();
75
+ for (const filename of readdirSync(directory)) {
76
+ const match = filename.match(/^mcp_output_([a-f0-9]{16,64})\.txt$/);
77
+ if (!match || this.entries.has(match[1]))
78
+ continue;
79
+ const path = join(directory, filename);
80
+ try {
81
+ const stat = statSync(path);
82
+ this.entries.set(match[1], {
83
+ id: match[1],
84
+ path,
85
+ chars: stat.size,
86
+ createdAt: stat.mtimeMs,
87
+ hash: match[1],
88
+ });
89
+ }
90
+ catch {
91
+ /* another process removed it */
92
+ }
45
93
  }
46
94
  }
47
- return `Output saved to ${filepath} (${output.length} chars). Read file for full content.`;
95
+ evictOldest() {
96
+ this.refreshEntries();
97
+ const oldest = Array.from(this.entries.values())
98
+ .sort((left, right) => left.createdAt - right.createdAt)[0];
99
+ if (!oldest)
100
+ return;
101
+ this.entries.delete(oldest.id);
102
+ try {
103
+ unlinkSync(oldest.path);
104
+ }
105
+ catch {
106
+ /* already gone */
107
+ }
108
+ }
109
+ capture(output, threshold = DEFAULT_PAYLOAD_THRESHOLD) {
110
+ if (output.length <= threshold) {
111
+ return { output };
112
+ }
113
+ const hash = createHash('sha256').update(output).digest('hex');
114
+ const id = hash;
115
+ const existing = this.entries.get(id);
116
+ if (existing && existing.hash === hash && existsSync(existing.path)) {
117
+ return {
118
+ output: this.referenceMessage(existing),
119
+ reference: existing,
120
+ };
121
+ }
122
+ if (existing) {
123
+ this.entries.delete(id);
124
+ }
125
+ this.refreshEntries();
126
+ while (this.entries.size >= this.maxEntries) {
127
+ this.evictOldest();
128
+ }
129
+ const filename = `mcp_output_${id}.txt`;
130
+ const path = join(this.getPayloadDir(), filename);
131
+ try {
132
+ writeFileSync(path, output, {
133
+ encoding: 'utf-8',
134
+ mode: 0o600,
135
+ flag: 'wx',
136
+ });
137
+ }
138
+ catch (error) {
139
+ if (error.code !== 'EEXIST') {
140
+ throw error;
141
+ }
142
+ }
143
+ const entry = {
144
+ id,
145
+ path,
146
+ chars: output.length,
147
+ createdAt: Date.now(),
148
+ hash,
149
+ };
150
+ this.entries.set(id, entry);
151
+ return {
152
+ output: this.referenceMessage(entry),
153
+ reference: entry,
154
+ };
155
+ }
156
+ referenceMessage(reference) {
157
+ return (`Output saved to ${reference.path} (${reference.chars} chars). ` +
158
+ `Payload ID: ${reference.id}. Use mcp_find_output or mcp_read_output to inspect it.`);
159
+ }
160
+ read(id, options = {}) {
161
+ const entry = this.getEntry(id);
162
+ const content = readFileSync(entry.path, 'utf-8');
163
+ const offset = Math.min(Math.max(0, Math.trunc(options.offset ?? 0)), content.length);
164
+ const requestedLength = options.all
165
+ ? content.length - offset
166
+ : Math.max(1, Math.trunc(options.length ?? DEFAULT_PAYLOAD_READ_LENGTH));
167
+ const nextOffset = Math.min(offset + requestedLength, content.length);
168
+ return {
169
+ id,
170
+ content: content.slice(offset, nextOffset),
171
+ offset,
172
+ nextOffset,
173
+ totalChars: content.length,
174
+ eof: nextOffset >= content.length,
175
+ };
176
+ }
177
+ find(id, query, options = {}) {
178
+ if (query.length === 0) {
179
+ throw new Error('Payload search query must not be empty');
180
+ }
181
+ const entry = this.getEntry(id);
182
+ const content = readFileSync(entry.path, 'utf-8');
183
+ const caseSensitive = options.caseSensitive ?? false;
184
+ const maxMatches = Math.min(100, Math.max(1, Math.trunc(options.maxMatches ?? 20)));
185
+ const contextChars = Math.min(2000, Math.max(0, Math.trunc(options.contextChars ?? 200)));
186
+ const haystack = caseSensitive ? content : content.toLowerCase();
187
+ const needle = caseSensitive ? query : query.toLowerCase();
188
+ const matches = [];
189
+ let fromIndex = 0;
190
+ while (matches.length < maxMatches) {
191
+ const offset = haystack.indexOf(needle, fromIndex);
192
+ if (offset === -1)
193
+ break;
194
+ const contextStart = Math.max(0, offset - contextChars);
195
+ const contextEnd = Math.min(content.length, offset + query.length + contextChars);
196
+ matches.push({
197
+ offset,
198
+ line: content.slice(0, offset).split('\n').length,
199
+ match: content.slice(offset, offset + query.length),
200
+ context: content.slice(contextStart, contextEnd),
201
+ });
202
+ fromIndex = offset + Math.max(1, query.length);
203
+ }
204
+ return {
205
+ id,
206
+ query,
207
+ totalChars: content.length,
208
+ matches,
209
+ truncated: haystack.indexOf(needle, fromIndex) !== -1,
210
+ };
211
+ }
212
+ destroy() {
213
+ this.entries.clear();
214
+ if (this.payloadDir && this.removeDirectoryOnDestroy) {
215
+ rmSync(this.payloadDir, { recursive: true, force: true });
216
+ }
217
+ this.payloadDir = undefined;
218
+ }
219
+ }
220
+ const defaultPayloadStore = new PayloadStore();
221
+ /**
222
+ * Backward-compatible string-only wrapper used by existing callers.
223
+ */
224
+ export function interceptPayload(output, threshold) {
225
+ return defaultPayloadStore.capture(output, threshold).output;
48
226
  }
49
227
  //# sourceMappingURL=payload-interceptor.js.map
@@ -0,0 +1,3 @@
1
+ export declare function isManagedRouterConfigured(baseDir: string): boolean;
2
+ export declare function managedRouterUnavailableMessage(env?: NodeJS.ProcessEnv): string;
3
+ //# sourceMappingURL=runtime-mode.d.ts.map
@@ -0,0 +1,12 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ export function isManagedRouterConfigured(baseDir) {
4
+ return existsSync(join(baseDir, 'active-release.json'));
5
+ }
6
+ export function managedRouterUnavailableMessage(env = process.env) {
7
+ if (env.CODEX_SANDBOX_NETWORK_DISABLED === '1') {
8
+ return 'The managed MCP router may be healthy outside this restricted Codex shell. Retry the same mcp-cli command with host/elevated execution (sandbox_permissions=require_escalated); do not run mcp-cli daemon start.';
9
+ }
10
+ return 'The managed MCP router is not reachable. Run mcp-proxy-upgrade status, then restart the router owner only if host execution also fails.';
11
+ }
12
+ //# sourceMappingURL=runtime-mode.js.map
@@ -0,0 +1,15 @@
1
+ export interface DaemonRuntimePaths {
2
+ baseDir: string;
3
+ socketPath: string;
4
+ pidFile: string;
5
+ readyFile: string;
6
+ logFile: string;
7
+ payloadDir: string;
8
+ releaseId: string;
9
+ }
10
+ /**
11
+ * Resolve daemon paths from environment variables so multiple release
12
+ * generations can run beside each other while sharing durable payload files.
13
+ */
14
+ export declare function getDaemonRuntimePaths(env?: NodeJS.ProcessEnv, home?: string): DaemonRuntimePaths;
15
+ //# sourceMappingURL=runtime-paths.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { homedir as osHomedir } from 'os';
2
+ import { join } from 'path';
3
+ /**
4
+ * Resolve daemon paths from environment variables so multiple release
5
+ * generations can run beside each other while sharing durable payload files.
6
+ */
7
+ export function getDaemonRuntimePaths(env = process.env, home = osHomedir()) {
8
+ const baseDir = env.MCP_DAEMON_BASE_DIR ||
9
+ join(home, '.mcp-compression-proxy');
10
+ return {
11
+ baseDir,
12
+ socketPath: env.MCP_DAEMON_SOCKET_PATH ||
13
+ join(baseDir, 'daemon.sock'),
14
+ pidFile: env.MCP_DAEMON_PID_FILE ||
15
+ join(baseDir, 'daemon.pid'),
16
+ readyFile: env.MCP_DAEMON_READY_FILE ||
17
+ join(baseDir, 'daemon.ready'),
18
+ logFile: env.MCP_DAEMON_LOG_FILE ||
19
+ join(baseDir, 'daemon.log'),
20
+ payloadDir: env.MCP_PAYLOAD_DIR ||
21
+ join(baseDir, 'payloads'),
22
+ releaseId: env.MCP_DAEMON_RELEASE_ID?.trim() ||
23
+ 'legacy',
24
+ };
25
+ }
26
+ //# sourceMappingURL=runtime-paths.js.map
@@ -9,6 +9,10 @@ export type ConfigResult = {
9
9
  excludePatterns: string[];
10
10
  noCompressPatterns: string[];
11
11
  defaultTimeout?: number;
12
+ softMaxConnectionAgeSeconds?: number;
13
+ hardMaxConnectionAgeSeconds?: number;
14
+ authErrorPatterns?: string[];
15
+ authRetryTools?: string[];
12
16
  cli?: {
13
17
  payloadThreshold?: number;
14
18
  autoStartDaemon?: boolean;