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/src/config.mjs
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime configuration for Ultra Lean MCP Proxy v2 features.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
|
|
8
|
+
function parseBool(value, fallback = null) {
|
|
9
|
+
if (value === undefined || value === null) return fallback;
|
|
10
|
+
if (typeof value === 'boolean') return value;
|
|
11
|
+
if (typeof value === 'number') return Boolean(value);
|
|
12
|
+
const text = String(value).trim().toLowerCase();
|
|
13
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(text)) return true;
|
|
14
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(text)) return false;
|
|
15
|
+
return fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function deepMerge(base, override) {
|
|
19
|
+
const out = { ...base };
|
|
20
|
+
for (const [key, value] of Object.entries(override || {})) {
|
|
21
|
+
if (
|
|
22
|
+
value
|
|
23
|
+
&& typeof value === 'object'
|
|
24
|
+
&& !Array.isArray(value)
|
|
25
|
+
&& out[key]
|
|
26
|
+
&& typeof out[key] === 'object'
|
|
27
|
+
&& !Array.isArray(out[key])
|
|
28
|
+
) {
|
|
29
|
+
out[key] = deepMerge(out[key], value);
|
|
30
|
+
} else {
|
|
31
|
+
out[key] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readConfigFile(configPath) {
|
|
38
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
39
|
+
const ext = path.extname(configPath).toLowerCase();
|
|
40
|
+
if (ext === '.json' || ext === '') {
|
|
41
|
+
const parsed = JSON.parse(raw);
|
|
42
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
43
|
+
throw new Error('Proxy config must be a mapping object');
|
|
44
|
+
}
|
|
45
|
+
return parsed;
|
|
46
|
+
}
|
|
47
|
+
if (ext === '.yaml' || ext === '.yml') {
|
|
48
|
+
throw new Error('YAML config is not supported in npm runtime. Use JSON config.');
|
|
49
|
+
}
|
|
50
|
+
const parsed = JSON.parse(raw);
|
|
51
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
52
|
+
throw new Error('Proxy config must be a mapping object');
|
|
53
|
+
}
|
|
54
|
+
return parsed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getCliValue(cli, ...keys) {
|
|
58
|
+
for (const key of keys) {
|
|
59
|
+
if (cli[key] !== undefined && cli[key] !== null) {
|
|
60
|
+
return cli[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createDefaultProxyConfig() {
|
|
67
|
+
return {
|
|
68
|
+
stats: false,
|
|
69
|
+
verbose: false,
|
|
70
|
+
sessionId: 'default',
|
|
71
|
+
strictConfig: false,
|
|
72
|
+
sourcePath: null,
|
|
73
|
+
|
|
74
|
+
definitionCompressionEnabled: true,
|
|
75
|
+
definitionMode: 'balanced',
|
|
76
|
+
|
|
77
|
+
resultCompressionEnabled: true,
|
|
78
|
+
resultCompressionMode: 'balanced',
|
|
79
|
+
resultMinPayloadBytes: 512,
|
|
80
|
+
resultStripNulls: false,
|
|
81
|
+
resultStripDefaults: false,
|
|
82
|
+
resultMinTokenSavingsAbs: 100,
|
|
83
|
+
resultMinTokenSavingsRatio: 0.05,
|
|
84
|
+
resultMinCompressibility: 0.2,
|
|
85
|
+
resultSharedKeyRegistry: true,
|
|
86
|
+
resultKeyBootstrapInterval: 8,
|
|
87
|
+
resultMinifyRedundantText: true,
|
|
88
|
+
|
|
89
|
+
deltaResponsesEnabled: true,
|
|
90
|
+
deltaMinSavingsRatio: 0.15,
|
|
91
|
+
deltaMaxPatchBytes: 65536,
|
|
92
|
+
deltaMaxPatchRatio: 0.8,
|
|
93
|
+
deltaSnapshotInterval: 5,
|
|
94
|
+
|
|
95
|
+
lazyLoadingEnabled: true,
|
|
96
|
+
lazyMode: 'minimal',
|
|
97
|
+
lazyTopK: 8,
|
|
98
|
+
lazySemantic: false,
|
|
99
|
+
lazyMinTools: 10,
|
|
100
|
+
lazyMinTokens: 2500,
|
|
101
|
+
lazyMinConfidenceScore: 2,
|
|
102
|
+
lazyFallbackFullOnLowConfidence: true,
|
|
103
|
+
|
|
104
|
+
toolsHashSyncEnabled: true,
|
|
105
|
+
toolsHashSyncAlgorithm: 'sha256',
|
|
106
|
+
toolsHashSyncRefreshInterval: 50,
|
|
107
|
+
toolsHashSyncIncludeServerFingerprint: true,
|
|
108
|
+
|
|
109
|
+
cachingEnabled: true,
|
|
110
|
+
cacheTtlSeconds: 300,
|
|
111
|
+
cacheMaxEntries: 5000,
|
|
112
|
+
cacheErrors: false,
|
|
113
|
+
cacheMutatingTools: false,
|
|
114
|
+
cacheAdaptiveTtl: true,
|
|
115
|
+
cacheTtlMinSeconds: 30,
|
|
116
|
+
cacheTtlMaxSeconds: 1800,
|
|
117
|
+
|
|
118
|
+
autoDisableEnabled: true,
|
|
119
|
+
autoDisableThreshold: 3,
|
|
120
|
+
autoDisableCooldownRequests: 20,
|
|
121
|
+
|
|
122
|
+
serverName: 'default',
|
|
123
|
+
toolOverrides: {},
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function extractServerProfile(configData, upstreamCommand) {
|
|
128
|
+
const servers = configData.servers;
|
|
129
|
+
if (!servers || typeof servers !== 'object' || Array.isArray(servers)) {
|
|
130
|
+
return ['default', {}];
|
|
131
|
+
}
|
|
132
|
+
const commandText = upstreamCommand.join(' ');
|
|
133
|
+
let selectedName = 'default';
|
|
134
|
+
let selectedProfile = {};
|
|
135
|
+
|
|
136
|
+
if (servers.default && typeof servers.default === 'object' && !Array.isArray(servers.default)) {
|
|
137
|
+
selectedProfile = { ...servers.default };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const [serverName, profile] of Object.entries(servers)) {
|
|
141
|
+
if (serverName === 'default') continue;
|
|
142
|
+
if (!profile || typeof profile !== 'object' || Array.isArray(profile)) continue;
|
|
143
|
+
const match = profile.match;
|
|
144
|
+
if (!match || typeof match !== 'object' || Array.isArray(match)) continue;
|
|
145
|
+
const commandContains = match.command_contains;
|
|
146
|
+
if (typeof commandContains === 'string' && commandText.includes(commandContains)) {
|
|
147
|
+
selectedName = serverName;
|
|
148
|
+
selectedProfile = deepMerge(selectedProfile, profile);
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return [selectedName, selectedProfile];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function applyGlobalConfig(cfg, configData, upstreamCommand, { applyServerProfiles = true } = {}) {
|
|
157
|
+
const out = { ...cfg };
|
|
158
|
+
const proxy = configData.proxy;
|
|
159
|
+
if (proxy && typeof proxy === 'object' && !Array.isArray(proxy)) {
|
|
160
|
+
const stats = parseBool(proxy.stats, null);
|
|
161
|
+
if (stats !== null) out.stats = stats;
|
|
162
|
+
const verbose = parseBool(proxy.verbose, null);
|
|
163
|
+
if (verbose !== null) out.verbose = verbose;
|
|
164
|
+
if (typeof proxy.session_id === 'string' && proxy.session_id) out.sessionId = proxy.session_id;
|
|
165
|
+
if (Number.isInteger(proxy.max_sessions) && proxy.max_sessions > 0) {
|
|
166
|
+
out.cacheMaxEntries = proxy.max_sessions * 10;
|
|
167
|
+
}
|
|
168
|
+
if (typeof proxy.strict_config === 'boolean') out.strictConfig = proxy.strict_config;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const optimizations = configData.optimizations;
|
|
172
|
+
if (optimizations && typeof optimizations === 'object' && !Array.isArray(optimizations)) {
|
|
173
|
+
const def = optimizations.definition_compression;
|
|
174
|
+
if (def && typeof def === 'object' && !Array.isArray(def)) {
|
|
175
|
+
const enabled = parseBool(def.enabled, null);
|
|
176
|
+
if (enabled !== null) out.definitionCompressionEnabled = enabled;
|
|
177
|
+
if (typeof def.mode === 'string') out.definitionMode = def.mode;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const rcfg = optimizations.result_compression;
|
|
181
|
+
if (rcfg && typeof rcfg === 'object' && !Array.isArray(rcfg)) {
|
|
182
|
+
const enabled = parseBool(rcfg.enabled, null);
|
|
183
|
+
if (enabled !== null) out.resultCompressionEnabled = enabled;
|
|
184
|
+
if (typeof rcfg.mode === 'string') out.resultCompressionMode = rcfg.mode;
|
|
185
|
+
if (Number.isInteger(rcfg.min_payload_bytes)) out.resultMinPayloadBytes = Math.max(0, rcfg.min_payload_bytes);
|
|
186
|
+
if (Number.isInteger(rcfg.min_token_savings_abs)) {
|
|
187
|
+
out.resultMinTokenSavingsAbs = Math.max(0, rcfg.min_token_savings_abs);
|
|
188
|
+
}
|
|
189
|
+
if (typeof rcfg.min_token_savings_ratio === 'number') {
|
|
190
|
+
out.resultMinTokenSavingsRatio = Math.min(Math.max(rcfg.min_token_savings_ratio, 0), 1);
|
|
191
|
+
}
|
|
192
|
+
if (typeof rcfg.min_compressibility === 'number') {
|
|
193
|
+
out.resultMinCompressibility = Math.min(Math.max(rcfg.min_compressibility, 0), 1);
|
|
194
|
+
}
|
|
195
|
+
const sharedRegistry = parseBool(rcfg.shared_key_registry, null);
|
|
196
|
+
if (sharedRegistry !== null) out.resultSharedKeyRegistry = sharedRegistry;
|
|
197
|
+
if (Number.isInteger(rcfg.key_bootstrap_interval)) {
|
|
198
|
+
out.resultKeyBootstrapInterval = Math.max(0, rcfg.key_bootstrap_interval);
|
|
199
|
+
}
|
|
200
|
+
const minify = parseBool(rcfg.minify_redundant_text, null);
|
|
201
|
+
if (minify !== null) out.resultMinifyRedundantText = minify;
|
|
202
|
+
const stripNulls = parseBool(rcfg.strip_nulls, null);
|
|
203
|
+
if (stripNulls !== null) out.resultStripNulls = stripNulls;
|
|
204
|
+
const stripDefaults = parseBool(rcfg.strip_defaults, null);
|
|
205
|
+
if (stripDefaults !== null) out.resultStripDefaults = stripDefaults;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const dcfg = optimizations.delta_responses;
|
|
209
|
+
if (dcfg && typeof dcfg === 'object' && !Array.isArray(dcfg)) {
|
|
210
|
+
const enabled = parseBool(dcfg.enabled, null);
|
|
211
|
+
if (enabled !== null) out.deltaResponsesEnabled = enabled;
|
|
212
|
+
if (typeof dcfg.min_savings_ratio === 'number') {
|
|
213
|
+
out.deltaMinSavingsRatio = Math.min(Math.max(dcfg.min_savings_ratio, 0), 1);
|
|
214
|
+
}
|
|
215
|
+
if (Number.isInteger(dcfg.max_patch_bytes)) out.deltaMaxPatchBytes = Math.max(0, dcfg.max_patch_bytes);
|
|
216
|
+
if (typeof dcfg.max_patch_ratio === 'number') {
|
|
217
|
+
out.deltaMaxPatchRatio = Math.min(Math.max(dcfg.max_patch_ratio, 0), 1);
|
|
218
|
+
}
|
|
219
|
+
if (Number.isInteger(dcfg.snapshot_interval)) {
|
|
220
|
+
out.deltaSnapshotInterval = Math.max(1, dcfg.snapshot_interval);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const lcfg = optimizations.lazy_loading;
|
|
225
|
+
if (lcfg && typeof lcfg === 'object' && !Array.isArray(lcfg)) {
|
|
226
|
+
const enabled = parseBool(lcfg.enabled, null);
|
|
227
|
+
if (enabled !== null) out.lazyLoadingEnabled = enabled;
|
|
228
|
+
if (typeof lcfg.mode === 'string') out.lazyMode = lcfg.mode;
|
|
229
|
+
if (Number.isInteger(lcfg.top_k)) out.lazyTopK = Math.max(1, lcfg.top_k);
|
|
230
|
+
if (Number.isInteger(lcfg.min_tools)) out.lazyMinTools = Math.max(0, lcfg.min_tools);
|
|
231
|
+
if (Number.isInteger(lcfg.min_tokens)) out.lazyMinTokens = Math.max(0, lcfg.min_tokens);
|
|
232
|
+
if (typeof lcfg.min_confidence_score === 'number') out.lazyMinConfidenceScore = lcfg.min_confidence_score;
|
|
233
|
+
const fallback = parseBool(lcfg.fallback_full_on_low_confidence, null);
|
|
234
|
+
if (fallback !== null) out.lazyFallbackFullOnLowConfidence = fallback;
|
|
235
|
+
const semantic = parseBool(lcfg.semantic, null);
|
|
236
|
+
if (semantic !== null) out.lazySemantic = semantic;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const hcfg = optimizations.tools_hash_sync;
|
|
240
|
+
if (hcfg && typeof hcfg === 'object' && !Array.isArray(hcfg)) {
|
|
241
|
+
const enabled = parseBool(hcfg.enabled, null);
|
|
242
|
+
if (enabled !== null) out.toolsHashSyncEnabled = enabled;
|
|
243
|
+
if (typeof hcfg.algorithm === 'string') out.toolsHashSyncAlgorithm = hcfg.algorithm.trim().toLowerCase();
|
|
244
|
+
if (Number.isInteger(hcfg.refresh_interval)) out.toolsHashSyncRefreshInterval = Math.max(1, hcfg.refresh_interval);
|
|
245
|
+
const includeFingerprint = parseBool(hcfg.include_server_fingerprint, null);
|
|
246
|
+
if (includeFingerprint !== null) out.toolsHashSyncIncludeServerFingerprint = includeFingerprint;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const ccfg = optimizations.caching;
|
|
250
|
+
if (ccfg && typeof ccfg === 'object' && !Array.isArray(ccfg)) {
|
|
251
|
+
const enabled = parseBool(ccfg.enabled, null);
|
|
252
|
+
if (enabled !== null) out.cachingEnabled = enabled;
|
|
253
|
+
if (Number.isInteger(ccfg.default_ttl_seconds)) out.cacheTtlSeconds = Math.max(0, ccfg.default_ttl_seconds);
|
|
254
|
+
if (Number.isInteger(ccfg.max_entries)) out.cacheMaxEntries = Math.max(1, ccfg.max_entries);
|
|
255
|
+
const cacheErrors = parseBool(ccfg.cache_errors, null);
|
|
256
|
+
if (cacheErrors !== null) out.cacheErrors = cacheErrors;
|
|
257
|
+
const cacheMutating = parseBool(ccfg.cache_mutating_tools, null);
|
|
258
|
+
if (cacheMutating !== null) out.cacheMutatingTools = cacheMutating;
|
|
259
|
+
const adaptive = parseBool(ccfg.adaptive_ttl, null);
|
|
260
|
+
if (adaptive !== null) out.cacheAdaptiveTtl = adaptive;
|
|
261
|
+
if (Number.isInteger(ccfg.ttl_min_seconds)) out.cacheTtlMinSeconds = Math.max(0, ccfg.ttl_min_seconds);
|
|
262
|
+
if (Number.isInteger(ccfg.ttl_max_seconds)) out.cacheTtlMaxSeconds = Math.max(0, ccfg.ttl_max_seconds);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const acfg = optimizations.auto_disable;
|
|
266
|
+
if (acfg && typeof acfg === 'object' && !Array.isArray(acfg)) {
|
|
267
|
+
const enabled = parseBool(acfg.enabled, null);
|
|
268
|
+
if (enabled !== null) out.autoDisableEnabled = enabled;
|
|
269
|
+
if (Number.isInteger(acfg.threshold)) out.autoDisableThreshold = Math.max(1, acfg.threshold);
|
|
270
|
+
if (Number.isInteger(acfg.cooldown_requests)) {
|
|
271
|
+
out.autoDisableCooldownRequests = Math.max(1, acfg.cooldown_requests);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (applyServerProfiles) {
|
|
277
|
+
const [serverName, profile] = extractServerProfile(configData, upstreamCommand);
|
|
278
|
+
out.serverName = serverName;
|
|
279
|
+
if (profile && typeof profile === 'object' && !Array.isArray(profile) && Object.keys(profile).length > 0) {
|
|
280
|
+
const profileOpts = {};
|
|
281
|
+
if (profile.proxy && typeof profile.proxy === 'object') profileOpts.proxy = profile.proxy;
|
|
282
|
+
if (profile.optimizations && typeof profile.optimizations === 'object') {
|
|
283
|
+
profileOpts.optimizations = profile.optimizations;
|
|
284
|
+
}
|
|
285
|
+
if (Object.keys(profileOpts).length > 0) {
|
|
286
|
+
const merged = applyGlobalConfig(out, profileOpts, upstreamCommand, { applyServerProfiles: false });
|
|
287
|
+
Object.assign(out, merged);
|
|
288
|
+
}
|
|
289
|
+
if (profile.tools && typeof profile.tools === 'object' && !Array.isArray(profile.tools)) {
|
|
290
|
+
out.toolOverrides = deepMerge(out.toolOverrides || {}, profile.tools);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function applyEnv(cfg, env) {
|
|
299
|
+
const out = { ...cfg };
|
|
300
|
+
|
|
301
|
+
const stats = parseBool(env.ULTRA_LEAN_MCP_PROXY_STATS, null);
|
|
302
|
+
if (stats !== null) out.stats = stats;
|
|
303
|
+
const verbose = parseBool(env.ULTRA_LEAN_MCP_PROXY_VERBOSE, null);
|
|
304
|
+
if (verbose !== null) out.verbose = verbose;
|
|
305
|
+
if (env.ULTRA_LEAN_MCP_PROXY_SESSION_ID) out.sessionId = String(env.ULTRA_LEAN_MCP_PROXY_SESSION_ID);
|
|
306
|
+
|
|
307
|
+
const resultCompression = parseBool(env.ULTRA_LEAN_MCP_PROXY_RESULT_COMPRESSION, null);
|
|
308
|
+
if (resultCompression !== null) out.resultCompressionEnabled = resultCompression;
|
|
309
|
+
if (env.ULTRA_LEAN_MCP_PROXY_RESULT_COMPRESSION_MODE) {
|
|
310
|
+
out.resultCompressionMode = String(env.ULTRA_LEAN_MCP_PROXY_RESULT_COMPRESSION_MODE);
|
|
311
|
+
}
|
|
312
|
+
if (env.ULTRA_LEAN_MCP_PROXY_RESULT_MIN_TOKEN_SAVINGS_ABS) {
|
|
313
|
+
const n = Number.parseInt(env.ULTRA_LEAN_MCP_PROXY_RESULT_MIN_TOKEN_SAVINGS_ABS, 10);
|
|
314
|
+
if (Number.isFinite(n)) out.resultMinTokenSavingsAbs = Math.max(0, n);
|
|
315
|
+
}
|
|
316
|
+
if (env.ULTRA_LEAN_MCP_PROXY_RESULT_MIN_TOKEN_SAVINGS_RATIO) {
|
|
317
|
+
const ratio = Number.parseFloat(env.ULTRA_LEAN_MCP_PROXY_RESULT_MIN_TOKEN_SAVINGS_RATIO);
|
|
318
|
+
if (Number.isFinite(ratio)) out.resultMinTokenSavingsRatio = Math.min(Math.max(ratio, 0), 1);
|
|
319
|
+
}
|
|
320
|
+
const sharedRegistry = parseBool(env.ULTRA_LEAN_MCP_PROXY_RESULT_SHARED_KEY_REGISTRY, null);
|
|
321
|
+
if (sharedRegistry !== null) out.resultSharedKeyRegistry = sharedRegistry;
|
|
322
|
+
if (env.ULTRA_LEAN_MCP_PROXY_RESULT_KEY_BOOTSTRAP_INTERVAL) {
|
|
323
|
+
const n = Number.parseInt(env.ULTRA_LEAN_MCP_PROXY_RESULT_KEY_BOOTSTRAP_INTERVAL, 10);
|
|
324
|
+
if (Number.isFinite(n)) out.resultKeyBootstrapInterval = Math.max(0, n);
|
|
325
|
+
}
|
|
326
|
+
const minify = parseBool(env.ULTRA_LEAN_MCP_PROXY_RESULT_MINIFY_REDUNDANT_TEXT, null);
|
|
327
|
+
if (minify !== null) out.resultMinifyRedundantText = minify;
|
|
328
|
+
|
|
329
|
+
const deltaResponses = parseBool(env.ULTRA_LEAN_MCP_PROXY_DELTA_RESPONSES, null);
|
|
330
|
+
if (deltaResponses !== null) out.deltaResponsesEnabled = deltaResponses;
|
|
331
|
+
if (env.ULTRA_LEAN_MCP_PROXY_DELTA_MIN_SAVINGS) {
|
|
332
|
+
const ratio = Number.parseFloat(env.ULTRA_LEAN_MCP_PROXY_DELTA_MIN_SAVINGS);
|
|
333
|
+
if (Number.isFinite(ratio)) out.deltaMinSavingsRatio = Math.min(Math.max(ratio, 0), 1);
|
|
334
|
+
}
|
|
335
|
+
if (env.ULTRA_LEAN_MCP_PROXY_DELTA_MAX_PATCH_RATIO) {
|
|
336
|
+
const ratio = Number.parseFloat(env.ULTRA_LEAN_MCP_PROXY_DELTA_MAX_PATCH_RATIO);
|
|
337
|
+
if (Number.isFinite(ratio)) out.deltaMaxPatchRatio = Math.min(Math.max(ratio, 0), 1);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const lazyLoading = parseBool(env.ULTRA_LEAN_MCP_PROXY_LAZY_LOADING, null);
|
|
341
|
+
if (lazyLoading !== null) out.lazyLoadingEnabled = lazyLoading;
|
|
342
|
+
if (env.ULTRA_LEAN_MCP_PROXY_LAZY_MODE) out.lazyMode = String(env.ULTRA_LEAN_MCP_PROXY_LAZY_MODE);
|
|
343
|
+
if (env.ULTRA_LEAN_MCP_PROXY_SEARCH_TOP_K) {
|
|
344
|
+
const n = Number.parseInt(env.ULTRA_LEAN_MCP_PROXY_SEARCH_TOP_K, 10);
|
|
345
|
+
if (Number.isFinite(n)) out.lazyTopK = Math.max(1, n);
|
|
346
|
+
}
|
|
347
|
+
if (env.ULTRA_LEAN_MCP_PROXY_LAZY_MIN_TOOLS) {
|
|
348
|
+
const n = Number.parseInt(env.ULTRA_LEAN_MCP_PROXY_LAZY_MIN_TOOLS, 10);
|
|
349
|
+
if (Number.isFinite(n)) out.lazyMinTools = Math.max(0, n);
|
|
350
|
+
}
|
|
351
|
+
if (env.ULTRA_LEAN_MCP_PROXY_LAZY_MIN_TOKENS) {
|
|
352
|
+
const n = Number.parseInt(env.ULTRA_LEAN_MCP_PROXY_LAZY_MIN_TOKENS, 10);
|
|
353
|
+
if (Number.isFinite(n)) out.lazyMinTokens = Math.max(0, n);
|
|
354
|
+
}
|
|
355
|
+
if (env.ULTRA_LEAN_MCP_PROXY_LAZY_MIN_CONFIDENCE) {
|
|
356
|
+
const n = Number.parseFloat(env.ULTRA_LEAN_MCP_PROXY_LAZY_MIN_CONFIDENCE);
|
|
357
|
+
if (Number.isFinite(n)) out.lazyMinConfidenceScore = n;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const toolsHashSync = parseBool(env.ULTRA_LEAN_MCP_PROXY_TOOLS_HASH_SYNC, null);
|
|
361
|
+
if (toolsHashSync !== null) out.toolsHashSyncEnabled = toolsHashSync;
|
|
362
|
+
if (env.ULTRA_LEAN_MCP_PROXY_TOOLS_HASH_REFRESH_INTERVAL) {
|
|
363
|
+
const n = Number.parseInt(env.ULTRA_LEAN_MCP_PROXY_TOOLS_HASH_REFRESH_INTERVAL, 10);
|
|
364
|
+
if (Number.isFinite(n)) out.toolsHashSyncRefreshInterval = Math.max(1, n);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const caching = parseBool(env.ULTRA_LEAN_MCP_PROXY_CACHING, null);
|
|
368
|
+
if (caching !== null) out.cachingEnabled = caching;
|
|
369
|
+
if (env.ULTRA_LEAN_MCP_PROXY_CACHE_TTL_SECONDS) {
|
|
370
|
+
const n = Number.parseInt(env.ULTRA_LEAN_MCP_PROXY_CACHE_TTL_SECONDS, 10);
|
|
371
|
+
if (Number.isFinite(n)) out.cacheTtlSeconds = Math.max(0, n);
|
|
372
|
+
}
|
|
373
|
+
const adaptive = parseBool(env.ULTRA_LEAN_MCP_PROXY_CACHE_ADAPTIVE_TTL, null);
|
|
374
|
+
if (adaptive !== null) out.cacheAdaptiveTtl = adaptive;
|
|
375
|
+
|
|
376
|
+
return out;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function applyCliOverrides(cfg, cli) {
|
|
380
|
+
const out = { ...cfg };
|
|
381
|
+
const setBool = (value, key) => {
|
|
382
|
+
if (value !== undefined && value !== null) {
|
|
383
|
+
out[key] = Boolean(value);
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
setBool(getCliValue(cli, 'stats'), 'stats');
|
|
388
|
+
setBool(getCliValue(cli, 'verbose'), 'verbose');
|
|
389
|
+
setBool(getCliValue(cli, 'resultCompression', 'result_compression'), 'resultCompressionEnabled');
|
|
390
|
+
setBool(getCliValue(cli, 'deltaResponses', 'delta_responses'), 'deltaResponsesEnabled');
|
|
391
|
+
setBool(getCliValue(cli, 'lazyLoading', 'lazy_loading'), 'lazyLoadingEnabled');
|
|
392
|
+
setBool(getCliValue(cli, 'toolsHashSync', 'tools_hash_sync'), 'toolsHashSyncEnabled');
|
|
393
|
+
setBool(getCliValue(cli, 'caching'), 'cachingEnabled');
|
|
394
|
+
|
|
395
|
+
const sessionId = getCliValue(cli, 'sessionId', 'session_id');
|
|
396
|
+
if (sessionId) out.sessionId = String(sessionId);
|
|
397
|
+
|
|
398
|
+
const strictConfig = getCliValue(cli, 'strictConfig', 'strict_config');
|
|
399
|
+
if (strictConfig !== undefined && strictConfig !== null) {
|
|
400
|
+
out.strictConfig = Boolean(strictConfig);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const cacheTtl = getCliValue(cli, 'cacheTtl', 'cache_ttl');
|
|
404
|
+
if (cacheTtl !== undefined && cacheTtl !== null) {
|
|
405
|
+
out.cacheTtlSeconds = Math.max(0, Number.parseInt(cacheTtl, 10) || 0);
|
|
406
|
+
}
|
|
407
|
+
const deltaMinSavings = getCliValue(cli, 'deltaMinSavings', 'delta_min_savings');
|
|
408
|
+
if (deltaMinSavings !== undefined && deltaMinSavings !== null) {
|
|
409
|
+
const ratio = Number.parseFloat(deltaMinSavings);
|
|
410
|
+
if (Number.isFinite(ratio)) out.deltaMinSavingsRatio = Math.min(Math.max(ratio, 0), 1);
|
|
411
|
+
}
|
|
412
|
+
const lazyMode = getCliValue(cli, 'lazyMode', 'lazy_mode');
|
|
413
|
+
if (lazyMode) out.lazyMode = String(lazyMode);
|
|
414
|
+
const searchTopK = getCliValue(cli, 'searchTopK', 'search_top_k');
|
|
415
|
+
if (searchTopK !== undefined && searchTopK !== null) {
|
|
416
|
+
out.lazyTopK = Math.max(1, Number.parseInt(searchTopK, 10) || 1);
|
|
417
|
+
}
|
|
418
|
+
const resultCompressionMode = getCliValue(cli, 'resultCompressionMode', 'result_compression_mode');
|
|
419
|
+
if (resultCompressionMode) out.resultCompressionMode = String(resultCompressionMode);
|
|
420
|
+
const toolsHashRefreshInterval = getCliValue(
|
|
421
|
+
cli,
|
|
422
|
+
'toolsHashRefreshInterval',
|
|
423
|
+
'tools_hash_refresh_interval'
|
|
424
|
+
);
|
|
425
|
+
if (toolsHashRefreshInterval !== undefined && toolsHashRefreshInterval !== null) {
|
|
426
|
+
out.toolsHashSyncRefreshInterval = Math.max(1, Number.parseInt(toolsHashRefreshInterval, 10) || 1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function featureEnabledForTool(cfg, toolName, featureName, defaultValue) {
|
|
433
|
+
if (!toolName) return defaultValue;
|
|
434
|
+
const toolCfg = cfg.toolOverrides?.[toolName] || {};
|
|
435
|
+
const featureCfg = toolCfg[featureName];
|
|
436
|
+
if (typeof featureCfg === 'boolean') return featureCfg;
|
|
437
|
+
if (featureCfg && typeof featureCfg === 'object' && !Array.isArray(featureCfg)) {
|
|
438
|
+
const enabled = parseBool(featureCfg.enabled, null);
|
|
439
|
+
if (enabled !== null) return enabled;
|
|
440
|
+
}
|
|
441
|
+
return defaultValue;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export function cacheTtlForTool(cfg, toolName) {
|
|
445
|
+
if (!toolName) return cfg.cacheTtlSeconds;
|
|
446
|
+
const toolCfg = cfg.toolOverrides?.[toolName] || {};
|
|
447
|
+
const cachingCfg = toolCfg.caching;
|
|
448
|
+
if (cachingCfg && typeof cachingCfg === 'object' && !Array.isArray(cachingCfg)) {
|
|
449
|
+
if (Number.isInteger(cachingCfg.ttl_seconds) && cachingCfg.ttl_seconds >= 0) {
|
|
450
|
+
return cachingCfg.ttl_seconds;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return cfg.cacheTtlSeconds;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function loadProxyConfig({
|
|
457
|
+
upstreamCommand,
|
|
458
|
+
configPath = null,
|
|
459
|
+
cliOverrides = {},
|
|
460
|
+
env = process.env,
|
|
461
|
+
} = {}) {
|
|
462
|
+
if (!Array.isArray(upstreamCommand)) {
|
|
463
|
+
throw new Error('upstreamCommand is required');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let cfg = createDefaultProxyConfig();
|
|
467
|
+
const resolvedPath = configPath || cliOverrides.configPath || env.ULTRA_LEAN_MCP_PROXY_CONFIG || null;
|
|
468
|
+
if (resolvedPath) {
|
|
469
|
+
const configData = readConfigFile(resolvedPath);
|
|
470
|
+
cfg = applyGlobalConfig(cfg, configData, upstreamCommand);
|
|
471
|
+
cfg.sourcePath = resolvedPath;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
cfg = applyEnv(cfg, env);
|
|
475
|
+
cfg = applyCliOverrides(cfg, cliOverrides);
|
|
476
|
+
|
|
477
|
+
if (!['off', 'minimal', 'catalog', 'search_only'].includes(cfg.lazyMode)) {
|
|
478
|
+
throw new Error(`Invalid lazy mode: ${cfg.lazyMode}`);
|
|
479
|
+
}
|
|
480
|
+
if (!['off', 'balanced', 'aggressive'].includes(cfg.resultCompressionMode)) {
|
|
481
|
+
throw new Error(`Invalid result compression mode: ${cfg.resultCompressionMode}`);
|
|
482
|
+
}
|
|
483
|
+
if (cfg.toolsHashSyncAlgorithm !== 'sha256') {
|
|
484
|
+
throw new Error(`Invalid tools hash sync algorithm: ${cfg.toolsHashSyncAlgorithm}`);
|
|
485
|
+
}
|
|
486
|
+
if (cfg.cacheTtlMaxSeconds < cfg.cacheTtlMinSeconds) {
|
|
487
|
+
cfg.cacheTtlMaxSeconds = cfg.cacheTtlMinSeconds;
|
|
488
|
+
}
|
|
489
|
+
if (cfg.lazyMode !== 'off') {
|
|
490
|
+
cfg.lazyLoadingEnabled = true;
|
|
491
|
+
}
|
|
492
|
+
if (cfg.resultCompressionMode === 'off') {
|
|
493
|
+
cfg.resultCompressionEnabled = false;
|
|
494
|
+
}
|
|
495
|
+
return cfg;
|
|
496
|
+
}
|
package/src/delta.mjs
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Delta response helpers for Ultra Lean MCP Proxy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import { cloneJson, canonicalize } from './state.mjs';
|
|
7
|
+
|
|
8
|
+
function jsonBytes(value) {
|
|
9
|
+
return Buffer.byteLength(JSON.stringify(value), 'utf-8');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function stableHash(value) {
|
|
13
|
+
const text = JSON.stringify(canonicalize(value));
|
|
14
|
+
return createHash('sha256').update(text, 'utf-8').digest('hex');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isObject(value) {
|
|
18
|
+
return value && typeof value === 'object' && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function deepEqual(a, b) {
|
|
22
|
+
return JSON.stringify(canonicalize(a)) === JSON.stringify(canonicalize(b));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function diffValues(previous, current, path, ops) {
|
|
26
|
+
if (deepEqual(previous, current)) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (Array.isArray(previous) && Array.isArray(current)) {
|
|
31
|
+
if (previous.length !== current.length) {
|
|
32
|
+
ops.push({ op: 'set', path, value: cloneJson(current) });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (let i = 0; i < current.length; i++) {
|
|
36
|
+
diffValues(previous[i], current[i], [...path, i], ops);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isObject(previous) && isObject(current)) {
|
|
42
|
+
const keys = new Set([...Object.keys(previous), ...Object.keys(current)]);
|
|
43
|
+
for (const key of Array.from(keys).sort()) {
|
|
44
|
+
if (!(key in current)) {
|
|
45
|
+
ops.push({ op: 'delete', path: [...path, key] });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (!(key in previous)) {
|
|
49
|
+
ops.push({ op: 'set', path: [...path, key], value: cloneJson(current[key]) });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
diffValues(previous[key], current[key], [...path, key], ops);
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ops.push({ op: 'set', path, value: cloneJson(current) });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function createDelta(
|
|
61
|
+
previous,
|
|
62
|
+
current,
|
|
63
|
+
minSavingsRatio = 0.15,
|
|
64
|
+
maxPatchBytes = 65536
|
|
65
|
+
) {
|
|
66
|
+
const canonicalPrevious = canonicalize(previous);
|
|
67
|
+
const canonicalCurrent = canonicalize(current);
|
|
68
|
+
if (deepEqual(canonicalPrevious, canonicalCurrent)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const ops = [];
|
|
73
|
+
diffValues(canonicalPrevious, canonicalCurrent, [], ops);
|
|
74
|
+
if (ops.length === 0) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const patchBytes = jsonBytes(ops);
|
|
79
|
+
const fullBytes = jsonBytes(canonicalCurrent);
|
|
80
|
+
if (patchBytes > maxPatchBytes) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const savingsRatio = fullBytes > 0 ? (fullBytes - patchBytes) / fullBytes : 0;
|
|
85
|
+
if (savingsRatio < minSavingsRatio) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
encoding: 'lapc-delta-v1',
|
|
91
|
+
baselineHash: stableHash(canonicalPrevious),
|
|
92
|
+
currentHash: stableHash(canonicalCurrent),
|
|
93
|
+
ops,
|
|
94
|
+
patchBytes,
|
|
95
|
+
fullBytes,
|
|
96
|
+
savedBytes: fullBytes - patchBytes,
|
|
97
|
+
savedRatio: savingsRatio,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getParentForPath(root, path) {
|
|
102
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
103
|
+
return { parent: null, key: null };
|
|
104
|
+
}
|
|
105
|
+
let cursor = root;
|
|
106
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
107
|
+
const segment = path[i];
|
|
108
|
+
const nextSegment = path[i + 1];
|
|
109
|
+
if (Array.isArray(cursor)) {
|
|
110
|
+
const idx = Number(segment);
|
|
111
|
+
if (!Number.isInteger(idx) || idx < 0) {
|
|
112
|
+
throw new Error('Invalid array index in delta path');
|
|
113
|
+
}
|
|
114
|
+
if (cursor[idx] === undefined) {
|
|
115
|
+
cursor[idx] = Number.isInteger(Number(nextSegment)) ? [] : {};
|
|
116
|
+
}
|
|
117
|
+
cursor = cursor[idx];
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (!isObject(cursor)) {
|
|
121
|
+
throw new Error('Invalid delta path parent');
|
|
122
|
+
}
|
|
123
|
+
if (!(segment in cursor) || cursor[segment] === null || cursor[segment] === undefined) {
|
|
124
|
+
cursor[segment] = Number.isInteger(Number(nextSegment)) ? [] : {};
|
|
125
|
+
}
|
|
126
|
+
cursor = cursor[segment];
|
|
127
|
+
}
|
|
128
|
+
return { parent: cursor, key: path[path.length - 1] };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function applyDelta(previous, delta) {
|
|
132
|
+
if (!delta || typeof delta !== 'object' || delta.encoding !== 'lapc-delta-v1') {
|
|
133
|
+
throw new Error('Unsupported delta envelope');
|
|
134
|
+
}
|
|
135
|
+
const ops = Array.isArray(delta.ops) ? delta.ops : null;
|
|
136
|
+
if (!ops) {
|
|
137
|
+
throw new Error('Delta envelope missing ops');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let output = cloneJson(previous);
|
|
141
|
+
for (const op of ops) {
|
|
142
|
+
if (!op || typeof op !== 'object' || !Array.isArray(op.path)) {
|
|
143
|
+
throw new Error('Invalid delta op');
|
|
144
|
+
}
|
|
145
|
+
const path = op.path;
|
|
146
|
+
if (op.op === 'set') {
|
|
147
|
+
if (path.length === 0) {
|
|
148
|
+
output = cloneJson(op.value);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const { parent, key } = getParentForPath(output, path);
|
|
152
|
+
if (Array.isArray(parent)) {
|
|
153
|
+
const idx = Number(key);
|
|
154
|
+
if (!Number.isInteger(idx) || idx < 0) {
|
|
155
|
+
throw new Error('Invalid array index in set op');
|
|
156
|
+
}
|
|
157
|
+
parent[idx] = cloneJson(op.value);
|
|
158
|
+
} else if (isObject(parent)) {
|
|
159
|
+
parent[key] = cloneJson(op.value);
|
|
160
|
+
} else {
|
|
161
|
+
throw new Error('Invalid set op parent');
|
|
162
|
+
}
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (op.op === 'delete') {
|
|
167
|
+
if (path.length === 0) {
|
|
168
|
+
output = null;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const { parent, key } = getParentForPath(output, path);
|
|
172
|
+
if (Array.isArray(parent)) {
|
|
173
|
+
const idx = Number(key);
|
|
174
|
+
if (!Number.isInteger(idx) || idx < 0 || idx >= parent.length) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
parent.splice(idx, 1);
|
|
178
|
+
} else if (isObject(parent)) {
|
|
179
|
+
delete parent[key];
|
|
180
|
+
}
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw new Error(`Unsupported delta op: ${op.op}`);
|
|
185
|
+
}
|
|
186
|
+
return output;
|
|
187
|
+
}
|
|
188
|
+
|