mcp-compression-proxy 1.0.2 → 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/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 +5 -4
package/dist/cli/commands.d.ts
CHANGED
|
@@ -14,10 +14,28 @@ export declare function handleInfo(socketPath: string, serverTool: string): Prom
|
|
|
14
14
|
* mcp-cli call <server>/<tool> '<json>' — execute a tool
|
|
15
15
|
*/
|
|
16
16
|
export declare function handleCall(socketPath: string, serverTool: string, jsonPayload: string): Promise<void>;
|
|
17
|
+
export declare function handlePayloadRead(socketPath: string, id: string, options?: {
|
|
18
|
+
offset?: number;
|
|
19
|
+
length?: number;
|
|
20
|
+
all?: boolean;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
export declare function handlePayloadFind(socketPath: string, id: string, query: string): Promise<void>;
|
|
23
|
+
export declare function handleScript(socketPath: string, jsonPayload: string): Promise<void>;
|
|
17
24
|
/**
|
|
18
25
|
* mcp-cli stats — get compression/server statistics
|
|
19
26
|
*/
|
|
20
27
|
export declare function handleStats(socketPath: string): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Last `count` lines of a log file's contents.
|
|
30
|
+
*
|
|
31
|
+
* Kept as a pure function so it is testable without a real log on disk - the
|
|
32
|
+
* CLI entry point is excluded from coverage collection, this file is not.
|
|
33
|
+
*/
|
|
34
|
+
export declare function tailLines(content: string, count: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* mcp-cli doctor — validate config and report live backend health
|
|
37
|
+
*/
|
|
38
|
+
export declare function handleDoctor(socketPath: string): Promise<void>;
|
|
21
39
|
/**
|
|
22
40
|
* mcp-cli daemon status — show daemon status
|
|
23
41
|
*/
|
package/dist/cli/commands.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sendRequest, isDaemonRunning } from './ipc-client.js';
|
|
2
|
+
import { loadJSONServers } from '../config/loader.js';
|
|
2
3
|
/**
|
|
3
4
|
* Format tool entries as aligned plain text:
|
|
4
5
|
* server/tool_name Short description
|
|
@@ -7,9 +8,9 @@ function formatToolList(tools) {
|
|
|
7
8
|
if (tools.length === 0)
|
|
8
9
|
return 'No tools found.';
|
|
9
10
|
// Calculate column width for alignment
|
|
10
|
-
const names = tools.map(t => `${t.server}/${t.tool}`);
|
|
11
|
-
const maxLen = Math.min(Math.max(...names.map(n => n.length)), 40);
|
|
12
|
-
const lines = tools.map(t => {
|
|
11
|
+
const names = tools.map((t) => `${t.server}/${t.tool}`);
|
|
12
|
+
const maxLen = Math.min(Math.max(...names.map((n) => n.length)), 40);
|
|
13
|
+
const lines = tools.map((t) => {
|
|
13
14
|
const name = `${t.server}/${t.tool}`;
|
|
14
15
|
const padded = name.padEnd(maxLen + 4);
|
|
15
16
|
return `${padded}${t.description}`;
|
|
@@ -28,7 +29,7 @@ export async function handleTools(socketPath) {
|
|
|
28
29
|
const result = response.result;
|
|
29
30
|
console.log(formatToolList(result.tools));
|
|
30
31
|
// Summary line
|
|
31
|
-
const serverCount = new Set(result.tools.map(t => t.server)).size;
|
|
32
|
+
const serverCount = new Set(result.tools.map((t) => t.server)).size;
|
|
32
33
|
console.log(`\n(${result.count} tools across ${serverCount} servers)`);
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
@@ -76,7 +77,7 @@ export async function handleInfo(socketPath, serverTool) {
|
|
|
76
77
|
export async function handleCall(socketPath, serverTool, jsonPayload) {
|
|
77
78
|
const slashIndex = serverTool.indexOf('/');
|
|
78
79
|
if (slashIndex === -1) {
|
|
79
|
-
console.error(
|
|
80
|
+
console.error("Usage: mcp-cli call <server>/<tool> '<json_payload>'");
|
|
80
81
|
process.exit(1);
|
|
81
82
|
}
|
|
82
83
|
const server = serverTool.slice(0, slashIndex);
|
|
@@ -107,6 +108,49 @@ export async function handleCall(socketPath, serverTool, jsonPayload) {
|
|
|
107
108
|
}
|
|
108
109
|
console.log(result.output);
|
|
109
110
|
}
|
|
111
|
+
export async function handlePayloadRead(socketPath, id, options = {}) {
|
|
112
|
+
const response = await sendRequest(socketPath, 'payload-read', {
|
|
113
|
+
id,
|
|
114
|
+
...options,
|
|
115
|
+
});
|
|
116
|
+
if (response.error) {
|
|
117
|
+
console.error(`Error: ${response.error.message}`);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
}
|
|
120
|
+
console.log(JSON.stringify(response.result, null, 2));
|
|
121
|
+
}
|
|
122
|
+
export async function handlePayloadFind(socketPath, id, query) {
|
|
123
|
+
const response = await sendRequest(socketPath, 'payload-find', {
|
|
124
|
+
id,
|
|
125
|
+
query,
|
|
126
|
+
});
|
|
127
|
+
if (response.error) {
|
|
128
|
+
console.error(`Error: ${response.error.message}`);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
console.log(JSON.stringify(response.result, null, 2));
|
|
132
|
+
}
|
|
133
|
+
export async function handleScript(socketPath, jsonPayload) {
|
|
134
|
+
let parsed;
|
|
135
|
+
try {
|
|
136
|
+
parsed = JSON.parse(jsonPayload);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
console.error('Error: Invalid JSON script');
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const steps = Array.isArray(parsed) ? parsed : parsed?.steps;
|
|
143
|
+
if (!Array.isArray(steps)) {
|
|
144
|
+
console.error('Error: Script must be an array of steps or an object with a steps array');
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
const response = await sendRequest(socketPath, 'script', { steps });
|
|
148
|
+
if (response.error) {
|
|
149
|
+
console.error(`Error: ${response.error.message}`);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
console.log(JSON.stringify(response.result, null, 2));
|
|
153
|
+
}
|
|
110
154
|
/**
|
|
111
155
|
* mcp-cli stats — get compression/server statistics
|
|
112
156
|
*/
|
|
@@ -118,6 +162,87 @@ export async function handleStats(socketPath) {
|
|
|
118
162
|
}
|
|
119
163
|
console.log(JSON.stringify(response.result, null, 2));
|
|
120
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* Last `count` lines of a log file's contents.
|
|
167
|
+
*
|
|
168
|
+
* Kept as a pure function so it is testable without a real log on disk - the
|
|
169
|
+
* CLI entry point is excluded from coverage collection, this file is not.
|
|
170
|
+
*/
|
|
171
|
+
export function tailLines(content, count) {
|
|
172
|
+
if (!content)
|
|
173
|
+
return '';
|
|
174
|
+
// A trailing newline is a terminator, not an empty final line; without this
|
|
175
|
+
// `-n 1` would return a blank.
|
|
176
|
+
const lines = content.replace(/\n$/, '').split('\n');
|
|
177
|
+
return lines.slice(Math.max(0, lines.length - count)).join('\n');
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* mcp-cli doctor — validate config and report live backend health
|
|
181
|
+
*/
|
|
182
|
+
export async function handleDoctor(socketPath) {
|
|
183
|
+
let healthy = true;
|
|
184
|
+
console.log('Configuration');
|
|
185
|
+
// Uncached on purpose: a doctor reports what is on disk right now.
|
|
186
|
+
// A schema error throws, and an unformatted stack trace here would bury the
|
|
187
|
+
// one thing the user came for.
|
|
188
|
+
let configuredServers = [];
|
|
189
|
+
try {
|
|
190
|
+
const config = loadJSONServers();
|
|
191
|
+
if (!config) {
|
|
192
|
+
console.log(' ! No servers.json found (user or project level)');
|
|
193
|
+
console.log(' The proxy will start with management tools only.');
|
|
194
|
+
healthy = false;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
configuredServers = config.servers.map((server) => server.name);
|
|
198
|
+
const disabled = config.servers.filter((server) => server.enabled === false).length;
|
|
199
|
+
console.log(` ✓ Loaded ${config.servers.length} server(s)${disabled ? `, ${disabled} disabled` : ''}`);
|
|
200
|
+
if (config.excludePatterns.length > 0) {
|
|
201
|
+
console.log(` excludeTools: ${config.excludePatterns.join(', ')}`);
|
|
202
|
+
}
|
|
203
|
+
if (config.noCompressPatterns.length > 0) {
|
|
204
|
+
console.log(` noCompressTools: ${config.noCompressPatterns.join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.log(' ✗ Invalid configuration');
|
|
210
|
+
for (const line of String(error instanceof Error ? error.message : error).split('\n')) {
|
|
211
|
+
console.log(` ${line}`);
|
|
212
|
+
}
|
|
213
|
+
console.log('\nFix the configuration before checking backend health.');
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
console.log('\nBackends');
|
|
217
|
+
const response = await sendRequest(socketPath, 'daemon-status');
|
|
218
|
+
if (response.error) {
|
|
219
|
+
console.log(` ✗ Daemon did not respond: ${response.error.message}`);
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
const status = response.result;
|
|
223
|
+
for (const server of status.servers) {
|
|
224
|
+
if (server.connected) {
|
|
225
|
+
console.log(` ✓ ${server.name}`);
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
console.log(` ✗ ${server.name}: ${server.lastError || 'not connected'}`);
|
|
229
|
+
healthy = false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// A server in the config that the daemon has no record of predates the
|
|
233
|
+
// daemon's own startup, so its warm connections are stale.
|
|
234
|
+
const known = new Set(status.servers.map((server) => server.name));
|
|
235
|
+
const missing = configuredServers.filter((name) => !known.has(name));
|
|
236
|
+
if (missing.length > 0) {
|
|
237
|
+
console.log(` ! Not known to the running daemon: ${missing.join(', ')}`);
|
|
238
|
+
console.log(' Restart it to pick them up: mcp-cli daemon restart');
|
|
239
|
+
healthy = false;
|
|
240
|
+
}
|
|
241
|
+
console.log(`\n${healthy ? 'All checks passed.' : 'Some checks failed (see above).'}`);
|
|
242
|
+
if (!healthy) {
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
121
246
|
/**
|
|
122
247
|
* mcp-cli daemon status — show daemon status
|
|
123
248
|
*/
|
|
@@ -136,16 +261,30 @@ export async function handleDaemonStatus(socketPath) {
|
|
|
136
261
|
const hours = Math.floor(status.uptime / 3600);
|
|
137
262
|
const minutes = Math.floor((status.uptime % 3600) / 60);
|
|
138
263
|
const uptimeStr = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
139
|
-
console.log(`Daemon running (PID ${status.pid}, uptime ${uptimeStr})`);
|
|
140
|
-
|
|
264
|
+
console.log(`Daemon running (PID ${status.pid}, release ${status.releaseId ?? 'legacy'}, uptime ${uptimeStr})`);
|
|
265
|
+
const failed = status.servers.filter((server) => server.state === 'failed');
|
|
266
|
+
const inactive = status.servers.filter((server) => !server.connected && server.state !== 'failed');
|
|
267
|
+
console.log(`Servers: ${status.connectedServers} connected, ${inactive.length} inactive, ${failed.length} failed`);
|
|
141
268
|
console.log(`Tools: ${status.cachedToolCount} cached`);
|
|
142
269
|
console.log(`Socket: ${status.socketPath}`);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
270
|
+
if (status.servers.length > 0) {
|
|
271
|
+
console.log('\nConnection lifecycle:');
|
|
272
|
+
for (const server of status.servers) {
|
|
273
|
+
const details = [
|
|
274
|
+
`state=${server.state ?? (server.connected ? 'ready' : 'failed')}`,
|
|
275
|
+
server.generation ? `generation=${server.generation}` : undefined,
|
|
276
|
+
server.connectionAgeSeconds !== undefined
|
|
277
|
+
? `age=${server.connectionAgeSeconds}s`
|
|
278
|
+
: undefined,
|
|
279
|
+
`active=${server.activeCalls ?? 0}`,
|
|
280
|
+
`recycles=${server.recycleCount ?? 0}`,
|
|
281
|
+
`auth-resets=${server.authInvalidations ?? 0}`,
|
|
282
|
+
`failures=${server.consecutiveFailures ?? 0}`,
|
|
283
|
+
].filter((value) => value !== undefined);
|
|
284
|
+
console.log(` - ${server.name}: ${details.join(', ')}`);
|
|
285
|
+
if (server.lastError) {
|
|
286
|
+
console.log(` last error: ${server.lastError}`);
|
|
287
|
+
}
|
|
149
288
|
}
|
|
150
289
|
}
|
|
151
290
|
}
|
package/dist/cli/daemon.js
CHANGED
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
import net from 'net';
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
|
-
import { homedir } from 'os';
|
|
6
5
|
import pino from 'pino';
|
|
7
6
|
import { MCPClientManager } from '../mcp/client-manager.js';
|
|
7
|
+
import { callToolWithAuthRecovery } from '../mcp/tool-call-executor.js';
|
|
8
8
|
import { CompressionCache } from '../services/compression-cache.js';
|
|
9
9
|
import { SessionManager } from '../services/session-manager.js';
|
|
10
10
|
import { StatsService } from '../services/stats-service.js';
|
|
11
|
-
import { loadJSONServers, matchesIgnorePattern } from '../config/loader.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
11
|
+
import { loadJSONServers, loadJSONServersCached, matchesIgnorePattern } from '../config/loader.js';
|
|
12
|
+
import { DEFAULT_PAYLOAD_THRESHOLD, PayloadStore } from './payload-interceptor.js';
|
|
13
|
+
import { runCallScript } from '../mcp/call-script.js';
|
|
14
|
+
import { getDaemonRuntimePaths } from './runtime-paths.js';
|
|
15
|
+
const RUNTIME_PATHS = getDaemonRuntimePaths();
|
|
16
|
+
const { baseDir: BASE_DIR, socketPath: SOCKET_PATH, pidFile: PID_FILE, readyFile: READY_FILE, logFile: LOG_FILE, payloadDir: PAYLOAD_DIR, releaseId: RELEASE_ID, } = RUNTIME_PATHS;
|
|
17
17
|
export function getSocketPath() {
|
|
18
18
|
return SOCKET_PATH;
|
|
19
19
|
}
|
|
@@ -33,6 +33,12 @@ async function startDaemon() {
|
|
|
33
33
|
// mkdirSync ignores `mode` when the directory already exists, so an
|
|
34
34
|
// upgrade from a previous version still gets tightened.
|
|
35
35
|
fs.chmodSync(BASE_DIR, 0o700);
|
|
36
|
+
for (const runtimePath of [SOCKET_PATH, PID_FILE, READY_FILE, LOG_FILE]) {
|
|
37
|
+
fs.mkdirSync(path.dirname(runtimePath), {
|
|
38
|
+
recursive: true,
|
|
39
|
+
mode: 0o700,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
36
42
|
const logger = pino({
|
|
37
43
|
name: 'mcp-cli-daemon',
|
|
38
44
|
level: process.env.LOG_LEVEL || 'info',
|
|
@@ -48,11 +54,15 @@ async function startDaemon() {
|
|
|
48
54
|
},
|
|
49
55
|
});
|
|
50
56
|
const startTime = Date.now();
|
|
51
|
-
logger.info({ pid: process.pid }, 'Daemon starting');
|
|
57
|
+
logger.info({ pid: process.pid, releaseId: RELEASE_ID }, 'Daemon starting');
|
|
52
58
|
// Write PID file
|
|
53
59
|
fs.writeFileSync(PID_FILE, String(process.pid), 'utf-8');
|
|
54
60
|
// Initialize services (reusing existing components)
|
|
55
61
|
const clientManager = new MCPClientManager(logger);
|
|
62
|
+
const payloadStore = new PayloadStore({
|
|
63
|
+
directory: PAYLOAD_DIR,
|
|
64
|
+
removeDirectoryOnDestroy: false,
|
|
65
|
+
});
|
|
56
66
|
const compressionCache = new CompressionCache(logger);
|
|
57
67
|
const sessionManager = new SessionManager(logger);
|
|
58
68
|
const statsService = new StatsService(logger, clientManager, compressionCache, sessionManager);
|
|
@@ -73,10 +83,19 @@ async function startDaemon() {
|
|
|
73
83
|
if (rawConfig.cli && typeof rawConfig.cli === 'object') {
|
|
74
84
|
cliConfig = rawConfig.cli;
|
|
75
85
|
}
|
|
76
|
-
const enabledServers = config.servers.filter(s => s.enabled !== false);
|
|
86
|
+
const enabledServers = config.servers.filter((s) => s.enabled !== false);
|
|
77
87
|
logger.info({ total: config.servers.length, enabled: enabledServers.length }, 'Initializing backend MCP servers');
|
|
78
88
|
try {
|
|
79
|
-
|
|
89
|
+
// inheritEnv must be passed here too: the config watch below reconciles
|
|
90
|
+
// with it, so omitting it makes every connection's stored config differ
|
|
91
|
+
// from the reconciled one on the first tick - bouncing every healthy
|
|
92
|
+
// backend ~5s after startup.
|
|
93
|
+
await clientManager.initializeServers(enabledServers, config.defaultTimeout, config.inheritEnv, {
|
|
94
|
+
softMaxConnectionAgeSeconds: config.softMaxConnectionAgeSeconds,
|
|
95
|
+
hardMaxConnectionAgeSeconds: config.hardMaxConnectionAgeSeconds,
|
|
96
|
+
authErrorPatterns: config.authErrorPatterns,
|
|
97
|
+
authRetryTools: config.authRetryTools,
|
|
98
|
+
});
|
|
80
99
|
logger.info('Backend MCP servers initialization complete');
|
|
81
100
|
}
|
|
82
101
|
catch (error) {
|
|
@@ -86,6 +105,13 @@ async function startDaemon() {
|
|
|
86
105
|
else {
|
|
87
106
|
logger.warn('No configuration found. Daemon started with no backend servers.');
|
|
88
107
|
}
|
|
108
|
+
// The daemon outlives any single CLI invocation, so it is the entry point
|
|
109
|
+
// that most needs this: editing servers.json would otherwise mean stopping a
|
|
110
|
+
// daemon that is holding warm connections. Cached loader rather than the
|
|
111
|
+
// uncached one used above - the poll is what its mtime fingerprint is for.
|
|
112
|
+
clientManager.startConfigWatch(loadJSONServersCached, undefined, (reloaded) => {
|
|
113
|
+
compressionCache.setNoCompressPatterns(reloaded.noCompressPatterns);
|
|
114
|
+
});
|
|
89
115
|
// Clean up stale socket file if it exists
|
|
90
116
|
if (fs.existsSync(SOCKET_PATH)) {
|
|
91
117
|
fs.unlinkSync(SOCKET_PATH);
|
|
@@ -97,18 +123,18 @@ async function startDaemon() {
|
|
|
97
123
|
try {
|
|
98
124
|
switch (method) {
|
|
99
125
|
case 'tools': {
|
|
100
|
-
const
|
|
126
|
+
const serverNames = clientManager.getConfiguredServerNames();
|
|
101
127
|
const toolEntries = [];
|
|
102
|
-
for (const
|
|
128
|
+
for (const name of serverNames) {
|
|
103
129
|
try {
|
|
104
|
-
const result = await client.listTools();
|
|
130
|
+
const result = await clientManager.withClient(name, async ({ client }) => client.listTools());
|
|
105
131
|
for (const tool of result.tools) {
|
|
106
132
|
const fullName = `${name}__${tool.name}`;
|
|
107
133
|
if (matchesIgnorePattern(fullName, excludePatterns))
|
|
108
134
|
continue;
|
|
109
|
-
const desc = compressionCache.getCompressedDescription(name, tool.name)
|
|
110
|
-
|
|
111
|
-
|
|
135
|
+
const desc = compressionCache.getCompressedDescription(name, tool.name) ||
|
|
136
|
+
tool.description ||
|
|
137
|
+
'';
|
|
112
138
|
// Truncate to ~60 chars for compact listing
|
|
113
139
|
const shortDesc = desc.length > 60 ? desc.slice(0, 57) + '...' : desc;
|
|
114
140
|
toolEntries.push({ server: name, tool: tool.name, description: shortDesc });
|
|
@@ -122,18 +148,18 @@ async function startDaemon() {
|
|
|
122
148
|
}
|
|
123
149
|
case 'search': {
|
|
124
150
|
const query = String(params?.query || '').toLowerCase();
|
|
125
|
-
const
|
|
151
|
+
const serverNames = clientManager.getConfiguredServerNames();
|
|
126
152
|
const matches = [];
|
|
127
|
-
for (const
|
|
153
|
+
for (const name of serverNames) {
|
|
128
154
|
try {
|
|
129
|
-
const result = await client.listTools();
|
|
155
|
+
const result = await clientManager.withClient(name, async ({ client }) => client.listTools());
|
|
130
156
|
for (const tool of result.tools) {
|
|
131
157
|
const fullName = `${name}__${tool.name}`;
|
|
132
158
|
if (matchesIgnorePattern(fullName, excludePatterns))
|
|
133
159
|
continue;
|
|
134
|
-
const desc = compressionCache.getCompressedDescription(name, tool.name)
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
const desc = compressionCache.getCompressedDescription(name, tool.name) ||
|
|
161
|
+
tool.description ||
|
|
162
|
+
'';
|
|
137
163
|
const searchText = `${name}/${tool.name} ${desc}`.toLowerCase();
|
|
138
164
|
if (searchText.includes(query)) {
|
|
139
165
|
const shortDesc = desc.length > 60 ? desc.slice(0, 57) + '...' : desc;
|
|
@@ -150,15 +176,17 @@ async function startDaemon() {
|
|
|
150
176
|
case 'info': {
|
|
151
177
|
const serverName = String(params?.server || '');
|
|
152
178
|
const toolName = String(params?.tool || '');
|
|
153
|
-
const client = clientManager.getClient(serverName);
|
|
154
|
-
if (!client) {
|
|
155
|
-
return { id, error: { code: -1, message: `Server '${serverName}' not found or not connected` } };
|
|
156
|
-
}
|
|
157
179
|
try {
|
|
158
|
-
const result = await client.listTools();
|
|
159
|
-
const tool = result.tools.find(t => t.name === toolName);
|
|
180
|
+
const result = await clientManager.withClient(serverName, async ({ client }) => client.listTools());
|
|
181
|
+
const tool = result.tools.find((t) => t.name === toolName);
|
|
160
182
|
if (!tool) {
|
|
161
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
id,
|
|
185
|
+
error: {
|
|
186
|
+
code: -1,
|
|
187
|
+
message: `Tool '${toolName}' not found on server '${serverName}'`,
|
|
188
|
+
},
|
|
189
|
+
};
|
|
162
190
|
}
|
|
163
191
|
return {
|
|
164
192
|
id,
|
|
@@ -179,28 +207,69 @@ async function startDaemon() {
|
|
|
179
207
|
const serverName = String(params?.server || '');
|
|
180
208
|
const toolName = String(params?.tool || '');
|
|
181
209
|
const args = (params?.arguments || {});
|
|
182
|
-
const threshold = cliConfig.payloadThreshold ??
|
|
183
|
-
const client = clientManager.getClient(serverName);
|
|
184
|
-
if (!client) {
|
|
185
|
-
return { id, error: { code: -1, message: `Server '${serverName}' not found or not connected` } };
|
|
186
|
-
}
|
|
210
|
+
const threshold = cliConfig.payloadThreshold ?? DEFAULT_PAYLOAD_THRESHOLD;
|
|
187
211
|
try {
|
|
188
|
-
const result = await
|
|
212
|
+
const result = await callToolWithAuthRecovery(clientManager, logger, serverName, toolName, args);
|
|
189
213
|
const content = result.content;
|
|
190
214
|
// Extract text content
|
|
191
215
|
// flatMap rather than filter+map: filter does not narrow the
|
|
192
216
|
// element type, which is why this needed a non-null assertion.
|
|
193
|
-
const textParts = content.flatMap((c) => c.type === 'text' && c.text ? [c.text] : []);
|
|
217
|
+
const textParts = content.flatMap((c) => (c.type === 'text' && c.text ? [c.text] : []));
|
|
194
218
|
const fullOutput = textParts.join('\n');
|
|
195
219
|
// Apply payload interception
|
|
196
|
-
const
|
|
197
|
-
return {
|
|
220
|
+
const captured = payloadStore.capture(fullOutput, threshold);
|
|
221
|
+
return {
|
|
222
|
+
id,
|
|
223
|
+
result: {
|
|
224
|
+
output: captured.output,
|
|
225
|
+
isError: result.isError,
|
|
226
|
+
payload: captured.reference,
|
|
227
|
+
},
|
|
228
|
+
};
|
|
198
229
|
}
|
|
199
230
|
catch (error) {
|
|
200
231
|
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
201
232
|
return { id, error: { code: -1, message: msg } };
|
|
202
233
|
}
|
|
203
234
|
}
|
|
235
|
+
case 'payload-read': {
|
|
236
|
+
const payloadId = String(params?.id || '');
|
|
237
|
+
const result = payloadStore.read(payloadId, {
|
|
238
|
+
offset: params?.offset,
|
|
239
|
+
length: params?.length,
|
|
240
|
+
all: params?.all,
|
|
241
|
+
});
|
|
242
|
+
return { id, result };
|
|
243
|
+
}
|
|
244
|
+
case 'payload-find': {
|
|
245
|
+
const payloadId = String(params?.id || '');
|
|
246
|
+
const query = String(params?.query || '');
|
|
247
|
+
const result = payloadStore.find(payloadId, query, {
|
|
248
|
+
caseSensitive: params?.caseSensitive,
|
|
249
|
+
maxMatches: params?.maxMatches,
|
|
250
|
+
contextChars: params?.contextChars,
|
|
251
|
+
});
|
|
252
|
+
return { id, result };
|
|
253
|
+
}
|
|
254
|
+
case 'script': {
|
|
255
|
+
const steps = params?.steps;
|
|
256
|
+
if (!Array.isArray(steps)) {
|
|
257
|
+
return {
|
|
258
|
+
id,
|
|
259
|
+
error: { code: -1, message: 'Script steps must be an array' },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const threshold = cliConfig.payloadThreshold ?? DEFAULT_PAYLOAD_THRESHOLD;
|
|
263
|
+
const result = await runCallScript(steps, async (serverName, toolName, args) => {
|
|
264
|
+
const callResult = await callToolWithAuthRecovery(clientManager, logger, serverName, toolName, args);
|
|
265
|
+
const content = callResult.content;
|
|
266
|
+
const output = content
|
|
267
|
+
.flatMap((item) => (item.type === 'text' && item.text ? [item.text] : []))
|
|
268
|
+
.join('\n');
|
|
269
|
+
return { output, isError: callResult.isError };
|
|
270
|
+
}, payloadStore, threshold);
|
|
271
|
+
return { id, result };
|
|
272
|
+
}
|
|
204
273
|
case 'stats': {
|
|
205
274
|
const stats = await statsService.getStats({
|
|
206
275
|
serverName: params?.serverName,
|
|
@@ -210,13 +279,14 @@ async function startDaemon() {
|
|
|
210
279
|
}
|
|
211
280
|
case 'daemon-status': {
|
|
212
281
|
const statuses = clientManager.getServerStatuses();
|
|
213
|
-
const connectedCount = statuses.filter(s => s.connected).length;
|
|
282
|
+
const connectedCount = statuses.filter((s) => s.connected).length;
|
|
214
283
|
const cacheMetrics = compressionCache.getCacheMetrics();
|
|
215
284
|
return {
|
|
216
285
|
id,
|
|
217
286
|
result: {
|
|
218
287
|
running: true,
|
|
219
288
|
pid: process.pid,
|
|
289
|
+
releaseId: RELEASE_ID,
|
|
220
290
|
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
221
291
|
servers: statuses,
|
|
222
292
|
cachedToolCount: cacheMetrics.totalCached,
|
|
@@ -257,7 +327,10 @@ async function startDaemon() {
|
|
|
257
327
|
.catch((error) => {
|
|
258
328
|
const errResponse = {
|
|
259
329
|
id: request.id,
|
|
260
|
-
error: {
|
|
330
|
+
error: {
|
|
331
|
+
code: -1,
|
|
332
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
333
|
+
},
|
|
261
334
|
};
|
|
262
335
|
socket.write(JSON.stringify(errResponse) + '\n');
|
|
263
336
|
});
|
|
@@ -291,37 +364,48 @@ async function startDaemon() {
|
|
|
291
364
|
try {
|
|
292
365
|
fs.unlinkSync(PID_FILE);
|
|
293
366
|
}
|
|
294
|
-
catch {
|
|
367
|
+
catch {
|
|
368
|
+
/* ignore */
|
|
369
|
+
}
|
|
295
370
|
process.exit(1);
|
|
296
371
|
});
|
|
297
372
|
server.listen(SOCKET_PATH, () => {
|
|
298
373
|
logger.info({ socketPath: SOCKET_PATH, pid: process.pid }, 'Daemon listening');
|
|
299
374
|
// Signal readiness by writing a ready marker
|
|
300
|
-
|
|
301
|
-
fs.writeFileSync(readyFile, String(Date.now()), 'utf-8');
|
|
375
|
+
fs.writeFileSync(READY_FILE, String(Date.now()), 'utf-8');
|
|
302
376
|
});
|
|
303
377
|
// Graceful shutdown
|
|
304
378
|
function shutdown() {
|
|
305
379
|
logger.info('Daemon shutting down');
|
|
306
380
|
server.close();
|
|
307
|
-
clientManager
|
|
381
|
+
clientManager
|
|
382
|
+
.disconnectAll()
|
|
383
|
+
.then(() => {
|
|
308
384
|
sessionManager.destroy();
|
|
385
|
+
payloadStore.destroy();
|
|
309
386
|
// Clean up files
|
|
310
387
|
try {
|
|
311
388
|
fs.unlinkSync(SOCKET_PATH);
|
|
312
389
|
}
|
|
313
|
-
catch {
|
|
390
|
+
catch {
|
|
391
|
+
/* ignore */
|
|
392
|
+
}
|
|
314
393
|
try {
|
|
315
394
|
fs.unlinkSync(PID_FILE);
|
|
316
395
|
}
|
|
317
|
-
catch {
|
|
396
|
+
catch {
|
|
397
|
+
/* ignore */
|
|
398
|
+
}
|
|
318
399
|
try {
|
|
319
|
-
fs.unlinkSync(
|
|
400
|
+
fs.unlinkSync(READY_FILE);
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
/* ignore */
|
|
320
404
|
}
|
|
321
|
-
catch { /* ignore */ }
|
|
322
405
|
logger.info('Daemon stopped');
|
|
323
406
|
process.exit(0);
|
|
324
|
-
})
|
|
407
|
+
})
|
|
408
|
+
.catch(() => {
|
|
325
409
|
process.exit(1);
|
|
326
410
|
});
|
|
327
411
|
}
|