ultra-lean-mcp-proxy 0.3.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/bin/cli.mjs +311 -0
- package/package.json +23 -0
- package/src/compress.mjs +170 -0
- package/src/config.mjs +496 -0
- package/src/delta.mjs +188 -0
- package/src/installer.mjs +1756 -0
- package/src/proxy.mjs +1122 -0
- package/src/result-compression.mjs +332 -0
- package/src/state.mjs +293 -0
- package/src/tools-hash-sync.mjs +52 -0
- package/src/watcher.mjs +530 -0
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ultra Lean MCP Proxy - CLI entry point.
|
|
5
|
+
*
|
|
6
|
+
* Commands:
|
|
7
|
+
* install [--dry-run] [--client NAME] [--skip SERVER] [--offline] [--runtime npm|pip] [--no-wrap-url] [-v]
|
|
8
|
+
* uninstall [--dry-run] [--client NAME] [--runtime npm|pip] [--all] [-v]
|
|
9
|
+
* status
|
|
10
|
+
* wrap-cloud [--dry-run] [--runtime npm|pip] [--suffix NAME_SUFFIX] [-v]
|
|
11
|
+
* proxy [v2 flags] [--stats] [--trace-rpc] [--runtime npm] -- <upstream-command>
|
|
12
|
+
* watch [--interval SEC] [--daemon] [--stop] [--offline] [--no-wrap-url] [-v]
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
16
|
+
import { dirname, join } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
19
|
+
const __dirname = dirname(__filename);
|
|
20
|
+
|
|
21
|
+
// Helper: dynamic import by file path (works cross-platform with ESM)
|
|
22
|
+
function importLocal(relPath) {
|
|
23
|
+
const absPath = join(__dirname, '..', relPath);
|
|
24
|
+
return import(pathToFileURL(absPath).href);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Minimal argument parser -- no external deps
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const args = argv.slice(0);
|
|
33
|
+
const result = {
|
|
34
|
+
command: null,
|
|
35
|
+
flags: {},
|
|
36
|
+
positional: [],
|
|
37
|
+
rest: [], // everything after "--"
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function setFlagValue(key, value) {
|
|
41
|
+
if (!(key in result.flags)) {
|
|
42
|
+
result.flags[key] = value;
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(result.flags[key])) {
|
|
46
|
+
result.flags[key].push(value);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
result.flags[key] = [result.flags[key], value];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Extract the subcommand (first non-flag token)
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < args.length) {
|
|
55
|
+
if (args[i] === '--') {
|
|
56
|
+
result.rest = args.slice(i + 1);
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (args[i].startsWith('-')) {
|
|
60
|
+
// flag
|
|
61
|
+
const raw = args[i].replace(/^-+/, '');
|
|
62
|
+
// Check for --flag=value
|
|
63
|
+
if (args[i].includes('=')) {
|
|
64
|
+
const [key, ...valParts] = raw.split('=');
|
|
65
|
+
setFlagValue(key, valParts.join('='));
|
|
66
|
+
} else if (
|
|
67
|
+
i + 1 < args.length &&
|
|
68
|
+
!args[i + 1].startsWith('-') &&
|
|
69
|
+
args[i + 1] !== '--'
|
|
70
|
+
) {
|
|
71
|
+
// Peek: boolean-like short flags that never take a value
|
|
72
|
+
const boolFlags = new Set([
|
|
73
|
+
'v', 'verbose', 'dry-run', 'stats', 'offline', 'all', 'help', 'h',
|
|
74
|
+
'daemon', 'stop', 'trace-rpc', 'no-wrap-url', 'include-url',
|
|
75
|
+
'strict-config', 'dump-effective-config',
|
|
76
|
+
'no-cloud',
|
|
77
|
+
'enable-result-compression', 'disable-result-compression',
|
|
78
|
+
'enable-delta-responses', 'disable-delta-responses',
|
|
79
|
+
'enable-lazy-loading', 'disable-lazy-loading',
|
|
80
|
+
'enable-tools-hash-sync', 'disable-tools-hash-sync',
|
|
81
|
+
'enable-caching', 'disable-caching',
|
|
82
|
+
]);
|
|
83
|
+
if (boolFlags.has(raw)) {
|
|
84
|
+
result.flags[raw] = true;
|
|
85
|
+
} else {
|
|
86
|
+
setFlagValue(raw, args[i + 1]);
|
|
87
|
+
i++;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
result.flags[raw] = true;
|
|
91
|
+
}
|
|
92
|
+
} else if (result.command === null) {
|
|
93
|
+
result.command = args[i];
|
|
94
|
+
} else {
|
|
95
|
+
result.positional.push(args[i]);
|
|
96
|
+
}
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Help text
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
const HELP = `
|
|
108
|
+
ultra-lean-mcp-proxy - lightweight optimization proxy for MCP
|
|
109
|
+
|
|
110
|
+
Usage:
|
|
111
|
+
ultra-lean-mcp-proxy install [--dry-run] [--client NAME] [--skip SERVER] [--offline] [--runtime npm|pip] [--no-wrap-url] [--no-cloud] [--suffix NAME] [-v]
|
|
112
|
+
ultra-lean-mcp-proxy uninstall [--dry-run] [--client NAME] [--runtime npm|pip] [--all] [-v]
|
|
113
|
+
ultra-lean-mcp-proxy status
|
|
114
|
+
ultra-lean-mcp-proxy wrap-cloud [--dry-run] [--runtime npm|pip] [--suffix NAME_SUFFIX] [-v]
|
|
115
|
+
ultra-lean-mcp-proxy proxy [v2 flags] [--stats] [--trace-rpc] [--runtime npm] -- <upstream-command>
|
|
116
|
+
ultra-lean-mcp-proxy watch [--interval SEC] [--daemon] [--stop] [--offline] [--no-wrap-url] [--suffix NAME] [--cloud-interval SEC] [-v]
|
|
117
|
+
|
|
118
|
+
Options:
|
|
119
|
+
-v, --verbose Verbose output
|
|
120
|
+
-h, --help Show this help message
|
|
121
|
+
|
|
122
|
+
Commands:
|
|
123
|
+
install Wrap MCP server entries in client configs to route through proxy
|
|
124
|
+
uninstall Remove proxy wrapping from client configs
|
|
125
|
+
status Show which clients/servers are currently wrapped
|
|
126
|
+
wrap-cloud Mirror cloud-scoped Claude MCP URL connectors into local config, already wrapped
|
|
127
|
+
proxy Run as stdio proxy in front of an upstream MCP server
|
|
128
|
+
--enable-result-compression / --disable-result-compression
|
|
129
|
+
--enable-delta-responses / --disable-delta-responses
|
|
130
|
+
--enable-lazy-loading / --disable-lazy-loading
|
|
131
|
+
--enable-tools-hash-sync / --disable-tools-hash-sync
|
|
132
|
+
--enable-caching / --disable-caching
|
|
133
|
+
--cache-ttl <seconds> --delta-min-savings <ratio> --lazy-mode off|minimal|search_only
|
|
134
|
+
watch Watch config files and auto-wrap new MCP servers
|
|
135
|
+
--daemon Run watcher as background daemon
|
|
136
|
+
--stop Stop running daemon
|
|
137
|
+
--interval Polling interval in seconds (default: 5)
|
|
138
|
+
--suffix Suffix for cloud connector names (default: -ulmp)
|
|
139
|
+
--cloud-interval Cloud discovery interval in seconds (default: 60)
|
|
140
|
+
`.trim();
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Main
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
|
|
146
|
+
async function main() {
|
|
147
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
148
|
+
const firstValue = (value, fallback = null) => (Array.isArray(value) ? value[value.length - 1] : (value ?? fallback));
|
|
149
|
+
|
|
150
|
+
if (parsed.flags.help || parsed.flags.h || !parsed.command) {
|
|
151
|
+
console.log(HELP);
|
|
152
|
+
process.exit(parsed.command ? 0 : 1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const verbose = !!(parsed.flags.v || parsed.flags.verbose);
|
|
156
|
+
|
|
157
|
+
switch (parsed.command) {
|
|
158
|
+
// ---- install ----
|
|
159
|
+
case 'install': {
|
|
160
|
+
const { doInstall } = await importLocal('src/installer.mjs');
|
|
161
|
+
const skipRaw = parsed.flags.skip;
|
|
162
|
+
const wrapUrl = parsed.flags['no-wrap-url'] ? false : true;
|
|
163
|
+
const installRuntime = firstValue(parsed.flags.runtime, 'npm');
|
|
164
|
+
if (!['npm', 'pip'].includes(String(installRuntime))) {
|
|
165
|
+
console.error(`Error: invalid --runtime value "${installRuntime}". Expected "npm" or "pip".`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
await doInstall({
|
|
169
|
+
dryRun: !!parsed.flags['dry-run'],
|
|
170
|
+
clientFilter: firstValue(parsed.flags.client, null),
|
|
171
|
+
skipNames: Array.isArray(skipRaw) ? skipRaw : skipRaw ? [skipRaw] : [],
|
|
172
|
+
offline: !!parsed.flags.offline,
|
|
173
|
+
wrapUrl,
|
|
174
|
+
runtime: installRuntime,
|
|
175
|
+
verbose,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Cloud connector discovery (enabled by default, opt-out with --no-cloud)
|
|
179
|
+
if (!parsed.flags['no-cloud']) {
|
|
180
|
+
const { commandExists, doWrapCloud } = await importLocal('src/installer.mjs');
|
|
181
|
+
if (commandExists('claude')) {
|
|
182
|
+
try {
|
|
183
|
+
const suffix = firstValue(parsed.flags.suffix, '-ulmp');
|
|
184
|
+
await doWrapCloud({
|
|
185
|
+
dryRun: !!parsed.flags['dry-run'],
|
|
186
|
+
runtime: installRuntime,
|
|
187
|
+
suffix,
|
|
188
|
+
verbose,
|
|
189
|
+
});
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.log(`[install] Cloud connector discovery failed: ${err.message}`);
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
console.log('[install] Cloud connector discovery skipped: claude CLI not found on PATH');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---- uninstall ----
|
|
201
|
+
case 'uninstall': {
|
|
202
|
+
const { doUninstall } = await importLocal('src/installer.mjs');
|
|
203
|
+
await doUninstall({
|
|
204
|
+
dryRun: !!parsed.flags['dry-run'],
|
|
205
|
+
clientFilter: firstValue(parsed.flags.client, null),
|
|
206
|
+
runtime: firstValue(parsed.flags.runtime, 'npm'),
|
|
207
|
+
all: !!parsed.flags.all,
|
|
208
|
+
verbose,
|
|
209
|
+
});
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---- status ----
|
|
214
|
+
case 'status': {
|
|
215
|
+
const { showStatus } = await importLocal('src/installer.mjs');
|
|
216
|
+
await showStatus();
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- wrap-cloud ----
|
|
221
|
+
case 'wrap-cloud': {
|
|
222
|
+
const { doWrapCloud } = await importLocal('src/installer.mjs');
|
|
223
|
+
const wrapCloudRuntime = firstValue(parsed.flags.runtime, 'npm');
|
|
224
|
+
if (!['npm', 'pip'].includes(String(wrapCloudRuntime))) {
|
|
225
|
+
console.error(`Error: invalid --runtime value "${wrapCloudRuntime}". Expected "npm" or "pip".`);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
await doWrapCloud({
|
|
229
|
+
dryRun: !!parsed.flags['dry-run'],
|
|
230
|
+
runtime: wrapCloudRuntime,
|
|
231
|
+
suffix: firstValue(parsed.flags.suffix, '-ulmp'),
|
|
232
|
+
verbose,
|
|
233
|
+
});
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---- proxy ----
|
|
238
|
+
case 'proxy': {
|
|
239
|
+
const upstreamCmd = parsed.rest;
|
|
240
|
+
if (upstreamCmd.length === 0) {
|
|
241
|
+
console.error('Error: No upstream server command provided.');
|
|
242
|
+
console.error('Usage: ultra-lean-mcp-proxy proxy -- <command> [args...]');
|
|
243
|
+
console.error('Example: ultra-lean-mcp-proxy proxy -- npx @modelcontextprotocol/server-filesystem /tmp');
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
// --runtime is accepted but only used by the wrapper / installer; ignored here.
|
|
247
|
+
const stats = !!parsed.flags.stats;
|
|
248
|
+
const traceRpc = !!parsed.flags['trace-rpc'];
|
|
249
|
+
const toggle = (enableFlag, disableFlag) => {
|
|
250
|
+
if (parsed.flags[enableFlag]) return true;
|
|
251
|
+
if (parsed.flags[disableFlag]) return false;
|
|
252
|
+
return null;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const { runProxy } = await importLocal('src/proxy.mjs');
|
|
256
|
+
await runProxy(upstreamCmd, {
|
|
257
|
+
stats,
|
|
258
|
+
traceRpc,
|
|
259
|
+
verbose,
|
|
260
|
+
configPath: firstValue(parsed.flags.config, null),
|
|
261
|
+
sessionId: firstValue(parsed.flags['session-id'], null),
|
|
262
|
+
strictConfig: parsed.flags['strict-config'] ? true : null,
|
|
263
|
+
resultCompression: toggle('enable-result-compression', 'disable-result-compression'),
|
|
264
|
+
deltaResponses: toggle('enable-delta-responses', 'disable-delta-responses'),
|
|
265
|
+
lazyLoading: toggle('enable-lazy-loading', 'disable-lazy-loading'),
|
|
266
|
+
toolsHashSync: toggle('enable-tools-hash-sync', 'disable-tools-hash-sync'),
|
|
267
|
+
caching: toggle('enable-caching', 'disable-caching'),
|
|
268
|
+
cacheTtl: firstValue(parsed.flags['cache-ttl'], null),
|
|
269
|
+
deltaMinSavings: firstValue(parsed.flags['delta-min-savings'], null),
|
|
270
|
+
lazyMode: firstValue(parsed.flags['lazy-mode'], null),
|
|
271
|
+
toolsHashRefreshInterval: firstValue(parsed.flags['tools-hash-refresh-interval'], null),
|
|
272
|
+
searchTopK: firstValue(parsed.flags['search-top-k'], null),
|
|
273
|
+
resultCompressionMode: firstValue(parsed.flags['result-compression-mode'], null),
|
|
274
|
+
dumpEffectiveConfig: !!parsed.flags['dump-effective-config'],
|
|
275
|
+
});
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---- watch ----
|
|
280
|
+
case 'watch': {
|
|
281
|
+
const interval = parseInt(firstValue(parsed.flags.interval, 5), 10) || 5;
|
|
282
|
+
const offline = !!parsed.flags.offline;
|
|
283
|
+
const runtime = firstValue(parsed.flags.runtime, 'npm');
|
|
284
|
+
const wrapUrl = parsed.flags['no-wrap-url'] ? false : true;
|
|
285
|
+
const suffix = firstValue(parsed.flags.suffix, '-ulmp');
|
|
286
|
+
const cloudInterval = parseInt(firstValue(parsed.flags['cloud-interval'], 60), 10) || 60;
|
|
287
|
+
|
|
288
|
+
if (parsed.flags.stop) {
|
|
289
|
+
const { stopDaemon } = await importLocal('src/watcher.mjs');
|
|
290
|
+
stopDaemon();
|
|
291
|
+
} else if (parsed.flags.daemon) {
|
|
292
|
+
const { startDaemon } = await importLocal('src/watcher.mjs');
|
|
293
|
+
startDaemon({ interval, runtime, offline, wrapUrl, verbose, suffix, cloudInterval });
|
|
294
|
+
} else {
|
|
295
|
+
const { runWatch } = await importLocal('src/watcher.mjs');
|
|
296
|
+
await runWatch({ interval, runtime, offline, wrapUrl, verbose, suffix, cloudInterval });
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
default:
|
|
302
|
+
console.error(`Unknown command: ${parsed.command}`);
|
|
303
|
+
console.log(HELP);
|
|
304
|
+
process.exit(1);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
main().catch((err) => {
|
|
309
|
+
console.error(err.message || err);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ultra-lean-mcp-proxy",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Ultra Lean MCP Proxy - lightweight optimization proxy for MCP (Node.js)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ultra-lean-mcp-proxy": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/lean-agent-protocol/ultra-lean-mcp-proxy.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["mcp", "proxy", "optimization", "compression"]
|
|
23
|
+
}
|
package/src/compress.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition compression for Ultra Lean MCP Proxy (Node.js port).
|
|
3
|
+
*
|
|
4
|
+
* Port of the Python compression rules from ultra-lean-mcp-core.
|
|
5
|
+
* Zero dependencies - uses built-in RegExp only.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Compression rules: [pattern, replacement]
|
|
10
|
+
// Patterns use the "gi" flags (global, case-insensitive) to match Python's
|
|
11
|
+
// re.IGNORECASE behaviour. Word-boundary \b works the same in JS.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
const COMPRESSION_RULES = [
|
|
15
|
+
// Remove filler phrases
|
|
16
|
+
[/\bThis tool (?:will |can |is used to |enables (?:you|users|LLMs|AI assistants) to |allows (?:you|users|LLMs|AI assistants) to )/gi, ''],
|
|
17
|
+
[/\bThis server (?:enables|allows|provides)\b/gi, ''],
|
|
18
|
+
[/\bThis operation (?:will|can)\b/gi, ''],
|
|
19
|
+
[/\bYou can use this (?:tool |to )\b/gi, ''],
|
|
20
|
+
[/\bProvides? (?:the )?ability to\b/gi, ''],
|
|
21
|
+
[/\bProvides? access to\b/gi, 'Access'],
|
|
22
|
+
[/\bGives? (?:you )?access to\b/gi, 'Access'],
|
|
23
|
+
[/\bmust be provided\b/gi, 'required'],
|
|
24
|
+
[/\bshould be provided\b/gi, 'recommended'],
|
|
25
|
+
[/\bcan be used (?:to |for )\b/gi, 'for '],
|
|
26
|
+
[/\bEnables you to\b/gi, ''],
|
|
27
|
+
[/\bAllows you to\b/gi, ''],
|
|
28
|
+
|
|
29
|
+
// Simplify phrases
|
|
30
|
+
[/\bin order to\b/gi, 'to'],
|
|
31
|
+
[/\bas well as\b/gi, 'and'],
|
|
32
|
+
[/\bprior to\b/gi, 'before'],
|
|
33
|
+
[/\bwith respect to\b/gi, 'for'],
|
|
34
|
+
|
|
35
|
+
// Remove qualifiers
|
|
36
|
+
[/\bvery\b/gi, ''],
|
|
37
|
+
[/\bsimply\b/gi, ''],
|
|
38
|
+
[/\bbasically\b/gi, ''],
|
|
39
|
+
[/\bessentially\b/gi, ''],
|
|
40
|
+
|
|
41
|
+
// Shorten terms
|
|
42
|
+
[/\brepository\b/gi, 'repo'],
|
|
43
|
+
[/\bconfiguration\b/gi, 'config'],
|
|
44
|
+
[/\binformation\b/gi, 'info'],
|
|
45
|
+
[/\bdocumentation\b/gi, 'docs'],
|
|
46
|
+
[/\bapplication\b/gi, 'app'],
|
|
47
|
+
[/\bdatabase\b/gi, 'DB'],
|
|
48
|
+
[/\benvironment\b/gi, 'env'],
|
|
49
|
+
[/\bparameters\b/gi, 'params'],
|
|
50
|
+
[/\bparameter\b/gi, 'param'],
|
|
51
|
+
|
|
52
|
+
// Shorten verbs
|
|
53
|
+
[/\bretrieve(?:s)?\b/gi, 'get'],
|
|
54
|
+
[/\bfetch(?:es)?\b/gi, 'get'],
|
|
55
|
+
[/\bexecute(?:s)?\b/gi, 'run'],
|
|
56
|
+
[/\bgenerate(?:s)?\b/gi, 'create'],
|
|
57
|
+
|
|
58
|
+
// Shorten notes
|
|
59
|
+
[/\bfor example\b/gi, 'e.g.'],
|
|
60
|
+
[/\bsuch as\b/gi, 'like'],
|
|
61
|
+
|
|
62
|
+
// Clean up whitespace and punctuation
|
|
63
|
+
[/ +/g, ' '],
|
|
64
|
+
[/ +([.,;:])/g, '$1'],
|
|
65
|
+
[/^\s+|\s+$/g, ''],
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Public API
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compress a tool or parameter description string.
|
|
74
|
+
*
|
|
75
|
+
* Returns the original string untouched when it is falsy or shorter than 20
|
|
76
|
+
* characters (too short to benefit from compression).
|
|
77
|
+
*
|
|
78
|
+
* @param {string|null|undefined} desc
|
|
79
|
+
* @returns {string|null|undefined}
|
|
80
|
+
*/
|
|
81
|
+
export function compressDescription(desc) {
|
|
82
|
+
if (!desc || desc.length < 20) {
|
|
83
|
+
return desc;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let result = desc;
|
|
87
|
+
|
|
88
|
+
for (const [pattern, replacement] of COMPRESSION_RULES) {
|
|
89
|
+
// Reset lastIndex for global regexes to ensure a clean match each time
|
|
90
|
+
pattern.lastIndex = 0;
|
|
91
|
+
result = result.replace(pattern, replacement);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Collapse repeated dots
|
|
95
|
+
result = result.replace(/\.+/g, '.');
|
|
96
|
+
|
|
97
|
+
// Capitalise first letter after ". "
|
|
98
|
+
result = result.replace(/(\. )([a-z])/g, (_match, dot, letter) => dot + letter.toUpperCase());
|
|
99
|
+
|
|
100
|
+
// Capitalise very first character
|
|
101
|
+
if (result && result[0] === result[0].toLowerCase()) {
|
|
102
|
+
result = result[0].toUpperCase() + result.slice(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return result.trim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Recursively compress description fields inside a JSON Schema object.
|
|
110
|
+
*
|
|
111
|
+
* Mutates the schema in-place and returns it for convenience.
|
|
112
|
+
*
|
|
113
|
+
* @param {object} schema
|
|
114
|
+
* @returns {object}
|
|
115
|
+
*/
|
|
116
|
+
export function compressSchema(schema) {
|
|
117
|
+
if (typeof schema !== 'object' || schema === null) {
|
|
118
|
+
return schema;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof schema.description === 'string') {
|
|
122
|
+
schema.description = compressDescription(schema.description);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (schema.properties && typeof schema.properties === 'object') {
|
|
126
|
+
for (const key of Object.keys(schema.properties)) {
|
|
127
|
+
const propSchema = schema.properties[key];
|
|
128
|
+
if (typeof propSchema === 'object' && propSchema !== null) {
|
|
129
|
+
compressSchema(propSchema);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (schema.items && typeof schema.items === 'object') {
|
|
135
|
+
compressSchema(schema.items);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return schema;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Compress an entire MCP tools list (the `tools` array from tools/list).
|
|
143
|
+
*
|
|
144
|
+
* Each tool entry is expected to have `description` and `inputSchema` fields.
|
|
145
|
+
* The function mutates entries in-place and returns the array.
|
|
146
|
+
*
|
|
147
|
+
* @param {Array<object>} tools
|
|
148
|
+
* @returns {Array<object>}
|
|
149
|
+
*/
|
|
150
|
+
export function compressManifest(tools) {
|
|
151
|
+
if (!Array.isArray(tools)) {
|
|
152
|
+
return tools;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const tool of tools) {
|
|
156
|
+
if (typeof tool !== 'object' || tool === null) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof tool.description === 'string') {
|
|
161
|
+
tool.description = compressDescription(tool.description);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (tool.inputSchema && typeof tool.inputSchema === 'object') {
|
|
165
|
+
compressSchema(tool.inputSchema);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return tools;
|
|
170
|
+
}
|