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.
@@ -67,6 +67,8 @@ function expandEnvVarsInObject(obj, unresolved) {
67
67
  }
68
68
  return obj;
69
69
  }
70
+ /** Fields that only configure a process we spawn ourselves. */
71
+ const STDIO_ONLY_FIELDS = ['args', 'env', 'inheritEnv'];
70
72
  /**
71
73
  * Validates JSON config against schema
72
74
  */
@@ -82,7 +84,48 @@ function validateConfig(config) {
82
84
  console.error(`[Config] Validation failed:\n${errors}`);
83
85
  throw new Error(`Invalid server configuration:\n${errors}`);
84
86
  }
85
- return config;
87
+ const validated = config;
88
+ // Derived from the schema rather than hand-listed, so a field added there
89
+ // cannot start warning about itself.
90
+ const knownServerKeys = new Set(Object.keys(serverConfigSchema.properties.mcpServers.items.properties));
91
+ for (const server of validated.mcpServers) {
92
+ const unknown = Object.keys(server).filter((key) => !knownServerKeys.has(key));
93
+ if (unknown.length > 0) {
94
+ // A warning, not a throw. These are usually keys carried over from
95
+ // another MCP client's config format, and failing here would take every
96
+ // server down over a field this proxy simply does not read.
97
+ console.error(`[Config] WARNING: server "${server.name}" has unrecognized field(s): ${unknown.join(', ')}. ` +
98
+ `They are ignored. Check the spelling if you expected them to take effect.`);
99
+ }
100
+ }
101
+ // The schema's `oneOf` only rules out `command` next to `url`; it has nothing
102
+ // to say about the stdio-only fields, so a remote entry carrying `args` would
103
+ // validate and then silently drop them. That combination is always a typo, so
104
+ // it throws like every other config mistake here rather than warning - a soft
105
+ // path would be the only one in this file.
106
+ for (const server of validated.mcpServers) {
107
+ if (server.url) {
108
+ const offending = STDIO_ONLY_FIELDS.filter((field) => server[field] !== undefined);
109
+ if (offending.length > 0) {
110
+ const message = `Invalid server configuration: "${server.name}" sets ${offending.join(', ')} ` +
111
+ `alongside "url". Those fields configure a locally spawned process and have no ` +
112
+ `effect on a remote server; use "headers" to send credentials instead.`;
113
+ console.error(`[Config] Validation failed:\n - ${message}`);
114
+ throw new Error(message);
115
+ }
116
+ continue;
117
+ }
118
+ // The mirror of the check above. Headers are an HTTP concept, so on a
119
+ // spawned server they would be dropped in silence - the same failure mode
120
+ // that rejecting unknown keys was meant to end.
121
+ if (server.headers !== undefined) {
122
+ const message = `Invalid server configuration: "${server.name}" sets headers alongside "command". ` +
123
+ `Headers are only sent to a remote server; use "env" to pass values to a spawned one.`;
124
+ console.error(`[Config] Validation failed:\n - ${message}`);
125
+ throw new Error(message);
126
+ }
127
+ }
128
+ return validated;
86
129
  }
87
130
  /**
88
131
  * Load and parse JSON config from a file
@@ -162,6 +205,10 @@ export function loadJSONServers() {
162
205
  let aggregatedExcludePatterns = [];
163
206
  let aggregatedNoCompressPatterns = [];
164
207
  let defaultTimeout;
208
+ let softMaxConnectionAgeSeconds;
209
+ let hardMaxConnectionAgeSeconds;
210
+ let authErrorPatterns;
211
+ let authRetryTools;
165
212
  let cliConfig;
166
213
  let inheritEnv;
167
214
  let compressionFallbackBehavior = 'original';
@@ -182,6 +229,20 @@ export function loadJSONServers() {
182
229
  if (userConfig.defaultTimeout) {
183
230
  defaultTimeout = userConfig.defaultTimeout;
184
231
  }
232
+ const userSoftMaxAge = userConfig.softMaxConnectionAgeSeconds ??
233
+ userConfig.maxConnectionAgeSeconds;
234
+ if (userSoftMaxAge !== undefined) {
235
+ softMaxConnectionAgeSeconds = userSoftMaxAge;
236
+ }
237
+ if (userConfig.hardMaxConnectionAgeSeconds !== undefined) {
238
+ hardMaxConnectionAgeSeconds = userConfig.hardMaxConnectionAgeSeconds;
239
+ }
240
+ if (userConfig.authErrorPatterns !== undefined) {
241
+ authErrorPatterns = [...userConfig.authErrorPatterns];
242
+ }
243
+ if (userConfig.authRetryTools !== undefined) {
244
+ authRetryTools = [...userConfig.authRetryTools];
245
+ }
185
246
  if (userConfig.cli) {
186
247
  cliConfig = { ...userConfig.cli };
187
248
  }
@@ -215,6 +276,20 @@ export function loadJSONServers() {
215
276
  if (projectConfig.defaultTimeout) {
216
277
  defaultTimeout = projectConfig.defaultTimeout;
217
278
  }
279
+ const projectSoftMaxAge = projectConfig.softMaxConnectionAgeSeconds ??
280
+ projectConfig.maxConnectionAgeSeconds;
281
+ if (projectSoftMaxAge !== undefined) {
282
+ softMaxConnectionAgeSeconds = projectSoftMaxAge;
283
+ }
284
+ if (projectConfig.hardMaxConnectionAgeSeconds !== undefined) {
285
+ hardMaxConnectionAgeSeconds = projectConfig.hardMaxConnectionAgeSeconds;
286
+ }
287
+ if (projectConfig.authErrorPatterns !== undefined) {
288
+ authErrorPatterns = [...projectConfig.authErrorPatterns];
289
+ }
290
+ if (projectConfig.authRetryTools !== undefined) {
291
+ authRetryTools = [...projectConfig.authRetryTools];
292
+ }
218
293
  // Project-level CLI config overrides user-level
219
294
  if (projectConfig.cli) {
220
295
  cliConfig = { ...cliConfig, ...projectConfig.cli };
@@ -249,6 +324,18 @@ export function loadJSONServers() {
249
324
  if (defaultTimeout) {
250
325
  console.error(`[Config] Default timeout: ${defaultTimeout} seconds`);
251
326
  }
327
+ if (softMaxConnectionAgeSeconds !== undefined) {
328
+ console.error(`[Config] Soft max connection age: ${softMaxConnectionAgeSeconds} seconds`);
329
+ }
330
+ if (hardMaxConnectionAgeSeconds !== undefined) {
331
+ console.error(`[Config] Hard max connection age: ${hardMaxConnectionAgeSeconds} seconds`);
332
+ }
333
+ if (authErrorPatterns && authErrorPatterns.length > 0) {
334
+ console.error(`[Config] Authentication error patterns configured: ${authErrorPatterns.length}`);
335
+ }
336
+ if (authRetryTools && authRetryTools.length > 0) {
337
+ console.error(`[Config] Authentication retry-safe tools configured: ${authRetryTools.length}`);
338
+ }
252
339
  // Log environment inheritance policy when it deviates from the default
253
340
  if (inheritEnv !== undefined && inheritEnv !== true) {
254
341
  console.error(`[Config] Environment inheritance: ${inheritEnv === false ? 'safe defaults only' : `allowlist (${inheritEnv.join(', ')})`}`);
@@ -263,6 +350,10 @@ export function loadJSONServers() {
263
350
  excludePatterns: aggregatedExcludePatterns,
264
351
  noCompressPatterns: aggregatedNoCompressPatterns,
265
352
  defaultTimeout,
353
+ softMaxConnectionAgeSeconds,
354
+ hardMaxConnectionAgeSeconds,
355
+ authErrorPatterns,
356
+ authRetryTools,
266
357
  cli: cliConfig,
267
358
  inheritEnv,
268
359
  compressionFallbackBehavior,
@@ -20,6 +20,18 @@ export declare const serverConfigSchema: {
20
20
  description: string;
21
21
  minLength: number;
22
22
  };
23
+ url: {
24
+ type: string;
25
+ description: string;
26
+ minLength: number;
27
+ };
28
+ headers: {
29
+ type: string;
30
+ description: string;
31
+ additionalProperties: {
32
+ type: string;
33
+ };
34
+ };
23
35
  args: {
24
36
  type: string;
25
37
  description: string;
@@ -54,6 +66,37 @@ export declare const serverConfigSchema: {
54
66
  type: string;
55
67
  description: string;
56
68
  };
69
+ softMaxConnectionAgeSeconds: {
70
+ type: string;
71
+ description: string;
72
+ minimum: number;
73
+ };
74
+ hardMaxConnectionAgeSeconds: {
75
+ type: string;
76
+ description: string;
77
+ minimum: number;
78
+ };
79
+ maxConnectionAgeSeconds: {
80
+ type: string;
81
+ description: string;
82
+ minimum: number;
83
+ };
84
+ authErrorPatterns: {
85
+ type: string;
86
+ description: string;
87
+ items: {
88
+ type: string;
89
+ minLength: number;
90
+ };
91
+ };
92
+ authRetryTools: {
93
+ type: string;
94
+ description: string;
95
+ items: {
96
+ type: string;
97
+ minLength: number;
98
+ };
99
+ };
57
100
  type: {
58
101
  type: string;
59
102
  description: string;
@@ -67,6 +110,9 @@ export declare const serverConfigSchema: {
67
110
  };
68
111
  };
69
112
  required: string[];
113
+ oneOf: {
114
+ required: string[];
115
+ }[];
70
116
  additionalProperties: boolean;
71
117
  };
72
118
  };
@@ -89,6 +135,37 @@ export declare const serverConfigSchema: {
89
135
  description: string;
90
136
  minimum: number;
91
137
  };
138
+ softMaxConnectionAgeSeconds: {
139
+ type: string;
140
+ description: string;
141
+ minimum: number;
142
+ };
143
+ hardMaxConnectionAgeSeconds: {
144
+ type: string;
145
+ description: string;
146
+ minimum: number;
147
+ };
148
+ maxConnectionAgeSeconds: {
149
+ type: string;
150
+ description: string;
151
+ minimum: number;
152
+ };
153
+ authErrorPatterns: {
154
+ type: string;
155
+ description: string;
156
+ items: {
157
+ type: string;
158
+ minLength: number;
159
+ };
160
+ };
161
+ authRetryTools: {
162
+ type: string;
163
+ description: string;
164
+ items: {
165
+ type: string;
166
+ minLength: number;
167
+ };
168
+ };
92
169
  cli: {
93
170
  type: string;
94
171
  description: string;
@@ -141,18 +218,30 @@ export type CompressionFallbackBehavior = 'original' | 'blank';
141
218
  export type ServerConfigJSON = {
142
219
  mcpServers: Array<{
143
220
  name: string;
144
- command: string;
221
+ command?: string;
145
222
  args?: string[];
146
223
  env?: Record<string, string>;
147
224
  inheritEnv?: InheritEnv;
225
+ url?: string;
226
+ headers?: Record<string, string>;
148
227
  enabled?: boolean;
149
228
  timeout?: number;
229
+ softMaxConnectionAgeSeconds?: number;
230
+ hardMaxConnectionAgeSeconds?: number;
231
+ maxConnectionAgeSeconds?: number;
232
+ authErrorPatterns?: string[];
233
+ authRetryTools?: string[];
150
234
  type?: string;
151
235
  autoApprove?: string[];
152
236
  }>;
153
237
  excludeTools?: string[];
154
238
  noCompressTools?: string[];
155
239
  defaultTimeout?: number;
240
+ softMaxConnectionAgeSeconds?: number;
241
+ hardMaxConnectionAgeSeconds?: number;
242
+ maxConnectionAgeSeconds?: number;
243
+ authErrorPatterns?: string[];
244
+ authRetryTools?: string[];
156
245
  cli?: {
157
246
  payloadThreshold?: number;
158
247
  autoStartDaemon?: boolean;
@@ -17,9 +17,21 @@ export const serverConfigSchema = {
17
17
  },
18
18
  command: {
19
19
  type: 'string',
20
- description: 'Command to execute',
20
+ description: 'Command to execute for a locally spawned (stdio) server',
21
21
  minLength: 1,
22
22
  },
23
+ url: {
24
+ type: 'string',
25
+ description: 'Endpoint of a hosted MCP server, spoken over Streamable HTTP. Mutually exclusive with "command"; the stdio-only fields (args, env, inheritEnv) do not apply.',
26
+ minLength: 1,
27
+ },
28
+ headers: {
29
+ type: 'object',
30
+ description: 'Static HTTP headers sent with every request to "url", e.g. { "Authorization": "Bearer ${MY_TOKEN}" }. Values go through the same ${VAR} expansion as "env".',
31
+ additionalProperties: {
32
+ type: 'string',
33
+ },
34
+ },
23
35
  args: {
24
36
  type: 'array',
25
37
  description: 'Command arguments',
@@ -36,10 +48,7 @@ export const serverConfigSchema = {
36
48
  },
37
49
  inheritEnv: {
38
50
  description: "Which of the proxy's own environment variables this server inherits. true = inherit all (default), false = inherit only the transport's safe defaults (PATH, HOME, ...), or an array of variable names to inherit. Values in `env` always take precedence. Overrides the top-level `inheritEnv`.",
39
- oneOf: [
40
- { type: 'boolean' },
41
- { type: 'array', items: { type: 'string' } },
42
- ],
51
+ oneOf: [{ type: 'boolean' }, { type: 'array', items: { type: 'string' } }],
43
52
  },
44
53
  enabled: {
45
54
  type: 'boolean',
@@ -49,9 +58,43 @@ export const serverConfigSchema = {
49
58
  type: 'number',
50
59
  description: 'Server timeout in seconds',
51
60
  },
61
+ // Accepted but never read. Presence of `url` is the transport
62
+ // discriminator; keeping `type` declared only stops configs that
63
+ // already carry it from failing the stricter check below.
64
+ softMaxConnectionAgeSeconds: {
65
+ type: 'number',
66
+ description: 'Lazy recycle threshold in seconds. On the first use at or after this age, the old connection drains and a fresh backend is opened. 0 disables.',
67
+ minimum: 0,
68
+ },
69
+ hardMaxConnectionAgeSeconds: {
70
+ type: 'number',
71
+ description: 'Absolute connection lifetime in seconds. At this age the connection drains, closes after active calls finish, and remains closed until reused. 0 disables.',
72
+ minimum: 0,
73
+ },
74
+ maxConnectionAgeSeconds: {
75
+ type: 'number',
76
+ description: 'Deprecated alias for softMaxConnectionAgeSeconds.',
77
+ minimum: 0,
78
+ },
79
+ authErrorPatterns: {
80
+ type: 'array',
81
+ description: 'Case-insensitive substrings that identify authentication failures in tool results or thrown errors.',
82
+ items: {
83
+ type: 'string',
84
+ minLength: 1,
85
+ },
86
+ },
87
+ authRetryTools: {
88
+ type: 'array',
89
+ description: 'Tool-name wildcard patterns that are safe to retry once after an authentication failure reopens the backend.',
90
+ items: {
91
+ type: 'string',
92
+ minLength: 1,
93
+ },
94
+ },
52
95
  type: {
53
96
  type: 'string',
54
- description: 'Server transport type (usually "stdio")',
97
+ description: 'Ignored. Kept for compatibility with existing configs.',
55
98
  },
56
99
  autoApprove: {
57
100
  type: 'array',
@@ -61,7 +104,18 @@ export const serverConfigSchema = {
61
104
  },
62
105
  },
63
106
  },
64
- required: ['name', 'command'],
107
+ required: ['name'],
108
+ // Exactly one transport. `oneOf` also rejects an entry that sets both,
109
+ // so no extra `not` is needed to catch command+url.
110
+ oneOf: [{ required: ['command'] }, { required: ['url'] }],
111
+ // Deliberately permissive. A misspelled *required* key like "comand"
112
+ // is already rejected by the oneOf above - neither command nor url
113
+ // survives the typo - so strictness here would only add misspelled
114
+ // optional keys, and it would pay for that by failing the entire
115
+ // config, and so every server, on something like Claude Desktop's
116
+ // `disabled` copied in from another client. The loader warns about
117
+ // unrecognized keys instead, which keeps the diagnostic without
118
+ // turning a cosmetic field into total loss of tools.
65
119
  additionalProperties: true,
66
120
  },
67
121
  },
@@ -84,15 +138,46 @@ export const serverConfigSchema = {
84
138
  description: 'Default timeout in seconds for all servers (can be overridden per-server). Default is 30 seconds if not specified.',
85
139
  minimum: 1,
86
140
  },
141
+ softMaxConnectionAgeSeconds: {
142
+ type: 'number',
143
+ description: 'Global lazy recycle threshold in seconds. Default is 3600 (1 hour). 0 disables.',
144
+ minimum: 0,
145
+ },
146
+ hardMaxConnectionAgeSeconds: {
147
+ type: 'number',
148
+ description: 'Global absolute connection lifetime in seconds. Default is 28800 (8 hours). 0 disables.',
149
+ minimum: 0,
150
+ },
151
+ maxConnectionAgeSeconds: {
152
+ type: 'number',
153
+ description: 'Deprecated alias for softMaxConnectionAgeSeconds.',
154
+ minimum: 0,
155
+ },
156
+ authErrorPatterns: {
157
+ type: 'array',
158
+ description: 'Global case-insensitive substrings that identify authentication failures.',
159
+ items: {
160
+ type: 'string',
161
+ minLength: 1,
162
+ },
163
+ },
164
+ authRetryTools: {
165
+ type: 'array',
166
+ description: 'Global tool-name wildcard patterns that are safe to retry once after authentication recovery.',
167
+ items: {
168
+ type: 'string',
169
+ minLength: 1,
170
+ },
171
+ },
87
172
  cli: {
88
173
  type: 'object',
89
174
  description: 'CLI (mcp-cli) configuration for lazy-loading mode',
90
175
  properties: {
91
176
  payloadThreshold: {
92
177
  type: 'number',
93
- description: 'Character threshold for redirecting large tool outputs to temp files. Default: 500.',
178
+ description: 'Character threshold for caching large tool outputs in private files. Default: 10000.',
94
179
  minimum: 0,
95
- default: 500,
180
+ default: 10000,
96
181
  },
97
182
  autoStartDaemon: {
98
183
  type: 'boolean',
@@ -110,10 +195,7 @@ export const serverConfigSchema = {
110
195
  },
111
196
  inheritEnv: {
112
197
  description: "Default environment inheritance for all servers (can be overridden per-server). true = pass the proxy's full environment to every backend server (default), false = pass only the transport's safe defaults (PATH, HOME, ...), or an array of variable names to pass through.",
113
- oneOf: [
114
- { type: 'boolean' },
115
- { type: 'array', items: { type: 'string' } },
116
- ],
198
+ oneOf: [{ type: 'boolean' }, { type: 'array', items: { type: 'string' } }],
117
199
  },
118
200
  compressionFallbackBehavior: {
119
201
  type: 'string',