mcp-compression-proxy 1.0.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 +97 -0
- package/LICENSE +21 -0
- package/README.md +842 -0
- package/dist/cli/commands.d.ts +25 -0
- package/dist/cli/commands.js +152 -0
- package/dist/cli/daemon.d.ts +4 -0
- package/dist/cli/daemon.js +336 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +269 -0
- package/dist/cli/ipc-client.d.ts +11 -0
- package/dist/cli/ipc-client.js +81 -0
- package/dist/cli/payload-interceptor.d.ts +6 -0
- package/dist/cli/payload-interceptor.js +49 -0
- package/dist/config/loader.d.ts +53 -0
- package/dist/config/loader.js +332 -0
- package/dist/config/schema.d.ts +164 -0
- package/dist/config/schema.js +127 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +821 -0
- package/dist/mcp/client-manager.d.ts +65 -0
- package/dist/mcp/client-manager.js +197 -0
- package/dist/services/compression-cache.d.ts +112 -0
- package/dist/services/compression-cache.js +238 -0
- package/dist/services/compression-persistence.d.ts +36 -0
- package/dist/services/compression-persistence.js +111 -0
- package/dist/services/compression-sampler.d.ts +89 -0
- package/dist/services/compression-sampler.js +171 -0
- package/dist/services/session-manager.d.ts +64 -0
- package/dist/services/session-manager.js +160 -0
- package/dist/services/stats-service.d.ts +101 -0
- package/dist/services/stats-service.js +246 -0
- package/dist/types/compression.d.ts +38 -0
- package/dist/types/compression.js +5 -0
- package/dist/types/index.d.ts +108 -0
- package/dist/types/index.js +2 -0
- package/dist/version.d.ts +11 -0
- package/dist/version.js +11 -0
- package/package.json +110 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import type { MCPServerConfig, ServerStatus } from '../types/index.js';
|
|
3
|
+
import type { Logger } from 'pino';
|
|
4
|
+
/**
|
|
5
|
+
* Manages connections to multiple MCP servers
|
|
6
|
+
*/
|
|
7
|
+
export declare class MCPClientManager {
|
|
8
|
+
private connections;
|
|
9
|
+
private logger;
|
|
10
|
+
private readonly DEFAULT_TIMEOUT_MS;
|
|
11
|
+
constructor(logger: Logger);
|
|
12
|
+
/**
|
|
13
|
+
* Wraps a promise with a timeout.
|
|
14
|
+
*
|
|
15
|
+
* The timer is always cleared - leaving it pending keeps the Node event loop
|
|
16
|
+
* alive for the full duration even after a fast connection succeeds.
|
|
17
|
+
*/
|
|
18
|
+
private withTimeout;
|
|
19
|
+
/**
|
|
20
|
+
* Build the environment handed to a spawned server.
|
|
21
|
+
*
|
|
22
|
+
* The stdio transport only inherits a small allowlist of "safe" variables
|
|
23
|
+
* (PATH, HOME, ...), so anything else the user exported - API tokens, base
|
|
24
|
+
* URLs - never reaches the child unless it is passed explicitly. By default
|
|
25
|
+
* we forward the proxy's full environment, matching what users expect from a
|
|
26
|
+
* process they launched themselves. `inheritEnv` narrows that when a server
|
|
27
|
+
* should not see unrelated secrets.
|
|
28
|
+
*/
|
|
29
|
+
private buildEnv;
|
|
30
|
+
/**
|
|
31
|
+
* Initialize and connect to all configured MCP servers
|
|
32
|
+
* @param servers - Server configurations to initialize
|
|
33
|
+
* @param defaultTimeout - Optional default timeout in seconds (overrides class default)
|
|
34
|
+
* @param defaultInheritEnv - Optional default env inheritance policy (overridden per-server)
|
|
35
|
+
*/
|
|
36
|
+
initializeServers(servers: MCPServerConfig[], defaultTimeout?: number, defaultInheritEnv?: boolean | string[]): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Connect to a single MCP server with timeout
|
|
39
|
+
*/
|
|
40
|
+
private connectToServer;
|
|
41
|
+
/**
|
|
42
|
+
* Get a connected client by server name
|
|
43
|
+
*/
|
|
44
|
+
getClient(serverName: string): Client | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Get all connected clients
|
|
47
|
+
*/
|
|
48
|
+
getConnectedClients(): Array<{
|
|
49
|
+
name: string;
|
|
50
|
+
client: Client;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Get status of all servers
|
|
54
|
+
*/
|
|
55
|
+
getServerStatuses(): ServerStatus[];
|
|
56
|
+
/**
|
|
57
|
+
* Check if at least one server is connected
|
|
58
|
+
*/
|
|
59
|
+
hasConnectedServers(): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Disconnect from all servers
|
|
62
|
+
*/
|
|
63
|
+
disconnectAll(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=client-manager.d.ts.map
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
2
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
3
|
+
import { SERVER_NAME, VERSION } from '../version.js';
|
|
4
|
+
/**
|
|
5
|
+
* Manages connections to multiple MCP servers
|
|
6
|
+
*/
|
|
7
|
+
export class MCPClientManager {
|
|
8
|
+
connections = new Map();
|
|
9
|
+
logger;
|
|
10
|
+
DEFAULT_TIMEOUT_MS = 30000; // 30 seconds default timeout
|
|
11
|
+
constructor(logger) {
|
|
12
|
+
this.logger = logger;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Wraps a promise with a timeout.
|
|
16
|
+
*
|
|
17
|
+
* The timer is always cleared - leaving it pending keeps the Node event loop
|
|
18
|
+
* alive for the full duration even after a fast connection succeeds.
|
|
19
|
+
*/
|
|
20
|
+
async withTimeout(promise, timeoutMs) {
|
|
21
|
+
let timer;
|
|
22
|
+
try {
|
|
23
|
+
return await Promise.race([
|
|
24
|
+
promise,
|
|
25
|
+
new Promise((_, reject) => {
|
|
26
|
+
timer = setTimeout(() => {
|
|
27
|
+
reject(new Error(`Connection timeout after ${timeoutMs}ms`));
|
|
28
|
+
}, timeoutMs);
|
|
29
|
+
}),
|
|
30
|
+
]);
|
|
31
|
+
}
|
|
32
|
+
finally {
|
|
33
|
+
if (timer !== undefined) {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Build the environment handed to a spawned server.
|
|
40
|
+
*
|
|
41
|
+
* The stdio transport only inherits a small allowlist of "safe" variables
|
|
42
|
+
* (PATH, HOME, ...), so anything else the user exported - API tokens, base
|
|
43
|
+
* URLs - never reaches the child unless it is passed explicitly. By default
|
|
44
|
+
* we forward the proxy's full environment, matching what users expect from a
|
|
45
|
+
* process they launched themselves. `inheritEnv` narrows that when a server
|
|
46
|
+
* should not see unrelated secrets.
|
|
47
|
+
*/
|
|
48
|
+
buildEnv(config) {
|
|
49
|
+
const inherit = config.inheritEnv ?? true;
|
|
50
|
+
// `false` defers entirely to the transport's safe defaults.
|
|
51
|
+
if (inherit === false) {
|
|
52
|
+
return config.env;
|
|
53
|
+
}
|
|
54
|
+
const names = Array.isArray(inherit) ? inherit : Object.keys(process.env);
|
|
55
|
+
const inherited = {};
|
|
56
|
+
for (const name of names) {
|
|
57
|
+
const value = process.env[name];
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
// Skip exported shell functions, which are a known injection vector.
|
|
61
|
+
if (value.startsWith('()'))
|
|
62
|
+
continue;
|
|
63
|
+
inherited[name] = value;
|
|
64
|
+
}
|
|
65
|
+
// Explicit `env` entries always win over inherited ones.
|
|
66
|
+
return { ...inherited, ...config.env };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Initialize and connect to all configured MCP servers
|
|
70
|
+
* @param servers - Server configurations to initialize
|
|
71
|
+
* @param defaultTimeout - Optional default timeout in seconds (overrides class default)
|
|
72
|
+
* @param defaultInheritEnv - Optional default env inheritance policy (overridden per-server)
|
|
73
|
+
*/
|
|
74
|
+
async initializeServers(servers, defaultTimeout, defaultInheritEnv) {
|
|
75
|
+
this.logger.info({ count: servers.length }, 'Initializing MCP servers');
|
|
76
|
+
// Apply defaults to servers that don't specify their own
|
|
77
|
+
const serversWithTimeout = servers.map(server => ({
|
|
78
|
+
...server,
|
|
79
|
+
timeout: server.timeout ?? defaultTimeout,
|
|
80
|
+
inheritEnv: server.inheritEnv ?? defaultInheritEnv,
|
|
81
|
+
}));
|
|
82
|
+
const connectionPromises = serversWithTimeout.map(async (config) => {
|
|
83
|
+
try {
|
|
84
|
+
await this.connectToServer(config);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.logger.error({ server: config.name, error }, 'Failed to connect to MCP server');
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
await Promise.allSettled(connectionPromises);
|
|
91
|
+
const connectedCount = Array.from(this.connections.values()).filter((c) => c.connected).length;
|
|
92
|
+
this.logger.info({ connected: connectedCount, total: servers.length }, 'MCP servers initialization complete');
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Connect to a single MCP server with timeout
|
|
96
|
+
*/
|
|
97
|
+
async connectToServer(config) {
|
|
98
|
+
// Use server-specific timeout or default (convert seconds to milliseconds)
|
|
99
|
+
const timeoutMs = config.timeout
|
|
100
|
+
? config.timeout * 1000
|
|
101
|
+
: this.DEFAULT_TIMEOUT_MS;
|
|
102
|
+
this.logger.info({ server: config.name, timeoutMs }, 'Connecting to MCP server');
|
|
103
|
+
const transport = new StdioClientTransport({
|
|
104
|
+
command: config.command,
|
|
105
|
+
args: config.args,
|
|
106
|
+
env: this.buildEnv(config),
|
|
107
|
+
});
|
|
108
|
+
const client = new Client({
|
|
109
|
+
name: SERVER_NAME,
|
|
110
|
+
version: VERSION,
|
|
111
|
+
}, {
|
|
112
|
+
capabilities: {},
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
const connectPromise = client.connect(transport);
|
|
116
|
+
// If the timeout wins the race below, this promise may still reject on its
|
|
117
|
+
// own later; swallow it so it doesn't surface as an unhandled rejection.
|
|
118
|
+
connectPromise.catch(() => { });
|
|
119
|
+
await this.withTimeout(connectPromise, timeoutMs);
|
|
120
|
+
this.connections.set(config.name, {
|
|
121
|
+
name: config.name,
|
|
122
|
+
client,
|
|
123
|
+
transport,
|
|
124
|
+
connected: true,
|
|
125
|
+
});
|
|
126
|
+
this.logger.info({ server: config.name }, 'Successfully connected to MCP server');
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
130
|
+
// A timed-out connect leaves the spawned process running. Tear the
|
|
131
|
+
// transport down so we don't orphan a child for the proxy's lifetime.
|
|
132
|
+
try {
|
|
133
|
+
await transport.close();
|
|
134
|
+
}
|
|
135
|
+
catch (closeError) {
|
|
136
|
+
this.logger.debug({ server: config.name, error: closeError }, 'Failed to close transport for unsuccessful connection');
|
|
137
|
+
}
|
|
138
|
+
this.connections.set(config.name, {
|
|
139
|
+
name: config.name,
|
|
140
|
+
client,
|
|
141
|
+
transport,
|
|
142
|
+
connected: false,
|
|
143
|
+
lastError: errorMessage,
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get a connected client by server name
|
|
150
|
+
*/
|
|
151
|
+
getClient(serverName) {
|
|
152
|
+
const connection = this.connections.get(serverName);
|
|
153
|
+
return connection?.connected ? connection.client : undefined;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get all connected clients
|
|
157
|
+
*/
|
|
158
|
+
getConnectedClients() {
|
|
159
|
+
return Array.from(this.connections.values())
|
|
160
|
+
.filter((conn) => conn.connected)
|
|
161
|
+
.map((conn) => ({ name: conn.name, client: conn.client }));
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get status of all servers
|
|
165
|
+
*/
|
|
166
|
+
getServerStatuses() {
|
|
167
|
+
return Array.from(this.connections.values()).map((conn) => ({
|
|
168
|
+
name: conn.name,
|
|
169
|
+
connected: conn.connected,
|
|
170
|
+
lastError: conn.lastError,
|
|
171
|
+
}));
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if at least one server is connected
|
|
175
|
+
*/
|
|
176
|
+
hasConnectedServers() {
|
|
177
|
+
return Array.from(this.connections.values()).some((conn) => conn.connected);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Disconnect from all servers
|
|
181
|
+
*/
|
|
182
|
+
async disconnectAll() {
|
|
183
|
+
this.logger.info('Disconnecting from all MCP servers');
|
|
184
|
+
const disconnectPromises = Array.from(this.connections.values()).map(async (conn) => {
|
|
185
|
+
try {
|
|
186
|
+
await conn.client.close();
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
this.logger.error({ server: conn.name, error }, 'Error disconnecting from server');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
await Promise.allSettled(disconnectPromises);
|
|
193
|
+
this.connections.clear();
|
|
194
|
+
this.logger.info('All MCP servers disconnected');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=client-manager.js.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { CompressionStats } from '../types/compression.js';
|
|
2
|
+
import type { CacheMetrics } from '../types/compression.js';
|
|
3
|
+
import type { Logger } from 'pino';
|
|
4
|
+
import { CompressionPersistence } from './compression-persistence.js';
|
|
5
|
+
import type { CompressionFallbackBehavior } from '../config/schema.js';
|
|
6
|
+
/**
|
|
7
|
+
* In-memory cache for compressed tool descriptions
|
|
8
|
+
* Key format: "serverName:toolName"
|
|
9
|
+
*/
|
|
10
|
+
export declare class CompressionCache {
|
|
11
|
+
private cache;
|
|
12
|
+
private logger;
|
|
13
|
+
private persistence;
|
|
14
|
+
private noCompressPatterns;
|
|
15
|
+
private fallbackBehavior;
|
|
16
|
+
constructor(logger: Logger, persistence?: CompressionPersistence);
|
|
17
|
+
/**
|
|
18
|
+
* Set what to show for tools that have no compressed description yet.
|
|
19
|
+
* 'original' (default) keeps the server's description; 'blank' hides it so
|
|
20
|
+
* uncompressed tools cost no context until they have been compressed.
|
|
21
|
+
*/
|
|
22
|
+
setFallbackBehavior(behavior: CompressionFallbackBehavior): void;
|
|
23
|
+
/**
|
|
24
|
+
* Current fallback behavior for uncompressed tools.
|
|
25
|
+
*/
|
|
26
|
+
getFallbackBehavior(): CompressionFallbackBehavior;
|
|
27
|
+
/**
|
|
28
|
+
* Set patterns for tools that should display original descriptions
|
|
29
|
+
* (tools are still compressed and cached, but show original when listing)
|
|
30
|
+
*/
|
|
31
|
+
setNoCompressPatterns(patterns: string[]): void;
|
|
32
|
+
/**
|
|
33
|
+
* Get all cached entries (for reporting/stats)
|
|
34
|
+
*/
|
|
35
|
+
getCacheEntries(): Array<{
|
|
36
|
+
serverName: string;
|
|
37
|
+
toolName: string;
|
|
38
|
+
original?: string;
|
|
39
|
+
compressed: string;
|
|
40
|
+
compressedAt: string;
|
|
41
|
+
}>;
|
|
42
|
+
/**
|
|
43
|
+
* Check if a tool should bypass compression display
|
|
44
|
+
* (for showing original descriptions while still caching compressed versions)
|
|
45
|
+
*/
|
|
46
|
+
private shouldBypassCompression;
|
|
47
|
+
/**
|
|
48
|
+
* Generate cache key from server and tool name
|
|
49
|
+
*/
|
|
50
|
+
private getKey;
|
|
51
|
+
/**
|
|
52
|
+
* Save compressed description for a tool
|
|
53
|
+
* Always saves compression to cache regardless of noCompress patterns
|
|
54
|
+
*/
|
|
55
|
+
saveCompressed(serverName: string, toolName: string, compressedDescription: string, originalDescription?: string): void;
|
|
56
|
+
/**
|
|
57
|
+
* Get description for a tool (session-aware)
|
|
58
|
+
* - If tool matches noCompress pattern: always use original (display-only bypass)
|
|
59
|
+
* - If tool is expanded in session: use original
|
|
60
|
+
* - If compressed exists: use compressed
|
|
61
|
+
* - Otherwise: apply the configured fallback behavior
|
|
62
|
+
*/
|
|
63
|
+
getDescription(serverName: string, toolName: string, originalDescription?: string, isExpandedInSession?: boolean): string | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Check if a tool has compressed description
|
|
66
|
+
*/
|
|
67
|
+
hasCompressed(serverName: string, toolName: string): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Get original description if cached
|
|
70
|
+
*/
|
|
71
|
+
getOriginalDescription(serverName: string, toolName: string): string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Get compressed description if cached
|
|
74
|
+
*/
|
|
75
|
+
getCompressedDescription(serverName: string, toolName: string): string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Get compression statistics
|
|
78
|
+
*/
|
|
79
|
+
getStats(): CompressionStats;
|
|
80
|
+
/**
|
|
81
|
+
* Detailed cache metrics for reporting
|
|
82
|
+
*/
|
|
83
|
+
getCacheMetrics(): CacheMetrics;
|
|
84
|
+
/**
|
|
85
|
+
* Clear all compressed descriptions
|
|
86
|
+
*/
|
|
87
|
+
clear(): void;
|
|
88
|
+
/**
|
|
89
|
+
* Get all cached tools
|
|
90
|
+
*/
|
|
91
|
+
getAllCached(): Array<{
|
|
92
|
+
serverName: string;
|
|
93
|
+
toolName: string;
|
|
94
|
+
}>;
|
|
95
|
+
/**
|
|
96
|
+
* Load cache from disk
|
|
97
|
+
*/
|
|
98
|
+
loadFromDisk(): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Save cache to disk
|
|
101
|
+
*/
|
|
102
|
+
saveToDisk(): Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Clear cache from both memory and disk
|
|
105
|
+
*/
|
|
106
|
+
clearAll(): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Get on-disk cache file path if available
|
|
109
|
+
*/
|
|
110
|
+
getCacheFilePath(): string;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=compression-cache.d.ts.map
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { CompressionPersistence } from './compression-persistence.js';
|
|
2
|
+
import { matchesIgnorePattern } from '../config/loader.js';
|
|
3
|
+
/**
|
|
4
|
+
* In-memory cache for compressed tool descriptions
|
|
5
|
+
* Key format: "serverName:toolName"
|
|
6
|
+
*/
|
|
7
|
+
export class CompressionCache {
|
|
8
|
+
cache = {};
|
|
9
|
+
logger;
|
|
10
|
+
persistence;
|
|
11
|
+
noCompressPatterns = [];
|
|
12
|
+
fallbackBehavior = 'original';
|
|
13
|
+
constructor(logger, persistence) {
|
|
14
|
+
this.logger = logger;
|
|
15
|
+
this.persistence = persistence || new CompressionPersistence(logger);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Set what to show for tools that have no compressed description yet.
|
|
19
|
+
* 'original' (default) keeps the server's description; 'blank' hides it so
|
|
20
|
+
* uncompressed tools cost no context until they have been compressed.
|
|
21
|
+
*/
|
|
22
|
+
setFallbackBehavior(behavior) {
|
|
23
|
+
this.fallbackBehavior = behavior;
|
|
24
|
+
this.logger.debug({ behavior }, 'Set compression fallback behavior');
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Current fallback behavior for uncompressed tools.
|
|
28
|
+
*/
|
|
29
|
+
getFallbackBehavior() {
|
|
30
|
+
return this.fallbackBehavior;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Set patterns for tools that should display original descriptions
|
|
34
|
+
* (tools are still compressed and cached, but show original when listing)
|
|
35
|
+
*/
|
|
36
|
+
setNoCompressPatterns(patterns) {
|
|
37
|
+
this.noCompressPatterns = patterns;
|
|
38
|
+
this.logger.debug({ patterns }, 'Set noCompress patterns (display-only bypass)');
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get all cached entries (for reporting/stats)
|
|
42
|
+
*/
|
|
43
|
+
getCacheEntries() {
|
|
44
|
+
return Object.entries(this.cache).map(([key, value]) => {
|
|
45
|
+
const [serverName, toolName] = key.split(':');
|
|
46
|
+
return {
|
|
47
|
+
serverName,
|
|
48
|
+
toolName,
|
|
49
|
+
original: value.original,
|
|
50
|
+
compressed: value.compressed,
|
|
51
|
+
compressedAt: value.compressedAt,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Check if a tool should bypass compression display
|
|
57
|
+
* (for showing original descriptions while still caching compressed versions)
|
|
58
|
+
*/
|
|
59
|
+
shouldBypassCompression(toolName) {
|
|
60
|
+
return matchesIgnorePattern(toolName, this.noCompressPatterns);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Generate cache key from server and tool name
|
|
64
|
+
*/
|
|
65
|
+
getKey(serverName, toolName) {
|
|
66
|
+
return `${serverName}:${toolName}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Save compressed description for a tool
|
|
70
|
+
* Always saves compression to cache regardless of noCompress patterns
|
|
71
|
+
*/
|
|
72
|
+
saveCompressed(serverName, toolName, compressedDescription, originalDescription) {
|
|
73
|
+
const key = this.getKey(serverName, toolName);
|
|
74
|
+
this.cache[key] = {
|
|
75
|
+
original: originalDescription,
|
|
76
|
+
compressed: compressedDescription,
|
|
77
|
+
compressedAt: new Date().toISOString(),
|
|
78
|
+
};
|
|
79
|
+
this.logger.debug({ serverName, toolName }, 'Saved compressed description');
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get description for a tool (session-aware)
|
|
83
|
+
* - If tool matches noCompress pattern: always use original (display-only bypass)
|
|
84
|
+
* - If tool is expanded in session: use original
|
|
85
|
+
* - If compressed exists: use compressed
|
|
86
|
+
* - Otherwise: apply the configured fallback behavior
|
|
87
|
+
*/
|
|
88
|
+
getDescription(serverName, toolName, originalDescription, isExpandedInSession) {
|
|
89
|
+
const fullToolName = `${serverName}__${toolName}`;
|
|
90
|
+
const key = this.getKey(serverName, toolName);
|
|
91
|
+
// Always bypass compression for noCompress patterns
|
|
92
|
+
if (this.shouldBypassCompression(fullToolName)) {
|
|
93
|
+
return originalDescription;
|
|
94
|
+
}
|
|
95
|
+
// If tool is expanded in session, use original description
|
|
96
|
+
if (isExpandedInSession) {
|
|
97
|
+
return this.cache[key]?.original || originalDescription;
|
|
98
|
+
}
|
|
99
|
+
const compressed = this.cache[key]?.compressed;
|
|
100
|
+
if (compressed) {
|
|
101
|
+
return compressed;
|
|
102
|
+
}
|
|
103
|
+
// Nothing compressed yet - 'blank' keeps the tool callable while costing
|
|
104
|
+
// no context; 'original' (default) shows the server's own description.
|
|
105
|
+
return this.fallbackBehavior === 'blank' ? '' : originalDescription;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Check if a tool has compressed description
|
|
109
|
+
*/
|
|
110
|
+
hasCompressed(serverName, toolName) {
|
|
111
|
+
const key = this.getKey(serverName, toolName);
|
|
112
|
+
return !!this.cache[key]?.compressed;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get original description if cached
|
|
116
|
+
*/
|
|
117
|
+
getOriginalDescription(serverName, toolName) {
|
|
118
|
+
const key = this.getKey(serverName, toolName);
|
|
119
|
+
return this.cache[key]?.original;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get compressed description if cached
|
|
123
|
+
*/
|
|
124
|
+
getCompressedDescription(serverName, toolName) {
|
|
125
|
+
const key = this.getKey(serverName, toolName);
|
|
126
|
+
return this.cache[key]?.compressed;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get compression statistics
|
|
130
|
+
*/
|
|
131
|
+
getStats() {
|
|
132
|
+
return {
|
|
133
|
+
totalTools: Object.keys(this.cache).length,
|
|
134
|
+
compressedTools: Object.keys(this.cache).length,
|
|
135
|
+
expandedTools: [],
|
|
136
|
+
cacheSize: JSON.stringify(this.cache).length,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Detailed cache metrics for reporting
|
|
141
|
+
*/
|
|
142
|
+
getCacheMetrics() {
|
|
143
|
+
const entries = this.getCacheEntries();
|
|
144
|
+
const perServer = {};
|
|
145
|
+
let totalOriginalChars = 0;
|
|
146
|
+
let totalCompressedChars = 0;
|
|
147
|
+
let missingOriginals = 0;
|
|
148
|
+
let latestCompressedAt;
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const originalLength = entry.original?.length ?? 0;
|
|
151
|
+
const compressedLength = entry.compressed.length;
|
|
152
|
+
if (!perServer[entry.serverName]) {
|
|
153
|
+
perServer[entry.serverName] = {
|
|
154
|
+
cached: 0,
|
|
155
|
+
totalOriginalChars: 0,
|
|
156
|
+
totalCompressedChars: 0,
|
|
157
|
+
missingOriginals: 0,
|
|
158
|
+
latestCompressedAt: undefined,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
perServer[entry.serverName].cached += 1;
|
|
162
|
+
perServer[entry.serverName].totalOriginalChars += originalLength;
|
|
163
|
+
perServer[entry.serverName].totalCompressedChars += compressedLength;
|
|
164
|
+
if (!entry.original) {
|
|
165
|
+
perServer[entry.serverName].missingOriginals += 1;
|
|
166
|
+
}
|
|
167
|
+
const serverLatest = perServer[entry.serverName].latestCompressedAt;
|
|
168
|
+
if (entry.compressedAt && (!serverLatest || serverLatest < entry.compressedAt)) {
|
|
169
|
+
perServer[entry.serverName].latestCompressedAt = entry.compressedAt;
|
|
170
|
+
}
|
|
171
|
+
totalOriginalChars += originalLength;
|
|
172
|
+
totalCompressedChars += compressedLength;
|
|
173
|
+
if (!entry.original)
|
|
174
|
+
missingOriginals += 1;
|
|
175
|
+
if (!latestCompressedAt || latestCompressedAt < entry.compressedAt) {
|
|
176
|
+
latestCompressedAt = entry.compressedAt;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
totalCached: entries.length,
|
|
181
|
+
totalOriginalChars,
|
|
182
|
+
totalCompressedChars,
|
|
183
|
+
missingOriginals,
|
|
184
|
+
latestCompressedAt,
|
|
185
|
+
cacheSizeBytes: Buffer.byteLength(JSON.stringify(this.cache)),
|
|
186
|
+
perServer,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Clear all compressed descriptions
|
|
191
|
+
*/
|
|
192
|
+
clear() {
|
|
193
|
+
this.cache = {};
|
|
194
|
+
this.logger.info('Cleared compression cache');
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Get all cached tools
|
|
198
|
+
*/
|
|
199
|
+
getAllCached() {
|
|
200
|
+
return Object.keys(this.cache).map((key) => {
|
|
201
|
+
const [serverName, toolName] = key.split(':');
|
|
202
|
+
return { serverName, toolName };
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Load cache from disk
|
|
207
|
+
*/
|
|
208
|
+
async loadFromDisk() {
|
|
209
|
+
const loadedCache = await this.persistence.load();
|
|
210
|
+
// Convert Map to cache object
|
|
211
|
+
for (const [key, value] of loadedCache.entries()) {
|
|
212
|
+
this.cache[key] = value;
|
|
213
|
+
}
|
|
214
|
+
this.logger.info({ count: Object.keys(this.cache).length }, 'Loaded compression cache into memory');
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Save cache to disk
|
|
218
|
+
*/
|
|
219
|
+
async saveToDisk() {
|
|
220
|
+
// Convert cache object to Map
|
|
221
|
+
const cacheMap = new Map(Object.entries(this.cache));
|
|
222
|
+
await this.persistence.save(cacheMap);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Clear cache from both memory and disk
|
|
226
|
+
*/
|
|
227
|
+
async clearAll() {
|
|
228
|
+
this.clear();
|
|
229
|
+
await this.persistence.clear();
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get on-disk cache file path if available
|
|
233
|
+
*/
|
|
234
|
+
getCacheFilePath() {
|
|
235
|
+
return this.persistence.getCacheFilePath();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
//# sourceMappingURL=compression-cache.js.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Logger } from 'pino';
|
|
2
|
+
/**
|
|
3
|
+
* Service for persisting compressed tool descriptions to disk
|
|
4
|
+
*/
|
|
5
|
+
export declare class CompressionPersistence {
|
|
6
|
+
private logger;
|
|
7
|
+
private cacheDir;
|
|
8
|
+
private cacheFile;
|
|
9
|
+
private readonly VERSION;
|
|
10
|
+
constructor(logger: Logger, cacheDir?: string);
|
|
11
|
+
/**
|
|
12
|
+
* Load cached compressions from disk
|
|
13
|
+
*/
|
|
14
|
+
load(): Promise<Map<string, {
|
|
15
|
+
original?: string;
|
|
16
|
+
compressed: string;
|
|
17
|
+
compressedAt: string;
|
|
18
|
+
}>>;
|
|
19
|
+
/**
|
|
20
|
+
* Save compressions to disk
|
|
21
|
+
*/
|
|
22
|
+
save(cache: Map<string, {
|
|
23
|
+
original?: string;
|
|
24
|
+
compressed: string;
|
|
25
|
+
compressedAt: string;
|
|
26
|
+
}>): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Clear the cache file from disk
|
|
29
|
+
*/
|
|
30
|
+
clear(): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Get the cache file path (useful for debugging)
|
|
33
|
+
*/
|
|
34
|
+
getCacheFilePath(): string;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=compression-persistence.d.ts.map
|