mcp-compression-proxy 1.0.3 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +227 -659
- package/dist/cli/commands.d.ts +18 -0
- package/dist/cli/commands.js +152 -13
- package/dist/cli/daemon.js +133 -49
- package/dist/cli/index.js +147 -7
- package/dist/cli/payload-interceptor.d.ts +71 -2
- package/dist/cli/payload-interceptor.js +214 -36
- package/dist/cli/runtime-mode.d.ts +3 -0
- package/dist/cli/runtime-mode.js +12 -0
- package/dist/cli/runtime-paths.d.ts +15 -0
- package/dist/cli/runtime-paths.js +26 -0
- package/dist/config/loader.d.ts +4 -0
- package/dist/config/loader.js +92 -1
- package/dist/config/schema.d.ts +90 -1
- package/dist/config/schema.js +95 -13
- package/dist/index.js +319 -28
- package/dist/mcp/call-script.d.ts +33 -0
- package/dist/mcp/call-script.js +153 -0
- package/dist/mcp/client-manager.d.ts +110 -27
- package/dist/mcp/client-manager.js +706 -83
- package/dist/mcp/tool-call-executor.d.ts +11 -0
- package/dist/mcp/tool-call-executor.js +94 -0
- package/dist/services/compression-cache.d.ts +18 -0
- package/dist/services/compression-cache.js +32 -0
- package/dist/services/session-manager.js +5 -0
- package/dist/services/stats-service.d.ts +4 -0
- package/dist/services/stats-service.js +18 -39
- package/dist/types/index.d.ts +59 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -1
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 {
|
|
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) => {
|
|
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(
|
|
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
|
-
*
|
|
3
|
-
*
|
|
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
|
|
5
|
+
export const DEFAULT_PAYLOAD_THRESHOLD = 10_000;
|
|
6
|
+
export const DEFAULT_PAYLOAD_READ_LENGTH = 10_000;
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Process-local, file-backed storage for large MCP outputs.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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,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
|
package/dist/config/loader.d.ts
CHANGED
|
@@ -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;
|