@atlassian/mcp-compressor 0.32.0 → 0.33.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/dist/cli.js CHANGED
@@ -61,7 +61,7 @@ function buildProgram(options = {}) {
61
61
  "invoke_tool, list_tools). The name will be sanitized to conform to MCP\n" +
62
62
  "tool name specifications (only A-Z, a-z, 0-9, _, -, .).")
63
63
  .option("-l, --log-level <level>", "The logging level.", "error")
64
- .option("--toonify", "Convert JSON tool responses to TOON format automatically.")
64
+ .option("--toonify", "Convert JSON, CSV and YAML tool responses to TOON format automatically.")
65
65
  .option("--cli-mode", "Start in CLI mode: expose a single help MCP tool, start a local HTTP\n" +
66
66
  "bridge, and generate a shell script for interacting with the wrapped\n" +
67
67
  "server via CLI. --toonify is automatically enabled in this mode.")
@@ -54,6 +54,19 @@ export interface GeneratedCodeClient {
54
54
  environment: Record<string, string>;
55
55
  }
56
56
  export declare function normalizeServers(servers: NativeServersInput): Promise<NormalizedBackendConfig[] | string>;
57
+ /**
58
+ * Hand a freshly started native session to `build`, closing the session if
59
+ * `build` throws.
60
+ *
61
+ * A started session owns backend processes and sockets that are only reachable
62
+ * through the object `build` returns. Without this the session would leak for
63
+ * the lifetime of the host process whenever adoption failed.
64
+ *
65
+ * Internal; not part of the public package surface.
66
+ */
67
+ export declare function adoptNativeSession<T>(session: {
68
+ close(): void;
69
+ }, build: () => T): T;
57
70
  export declare class CompressorProxy {
58
71
  private readonly session;
59
72
  private readonly defaultServer;
@@ -87,10 +100,15 @@ export declare class CompressorProxy {
87
100
  export declare class CompressorClient {
88
101
  private readonly options;
89
102
  private session;
103
+ private proxy;
104
+ private connectPromise;
105
+ private closingPromise;
90
106
  private readonly mode;
91
107
  private authProviders;
92
108
  constructor(options: CompressorClientOptions);
93
109
  connect(): Promise<CompressorProxy>;
110
+ private startConnection;
94
111
  close(): Promise<void>;
112
+ private closeCurrentConnection;
95
113
  private defaultServer;
96
114
  }
@@ -152,6 +152,31 @@ function transformMode(mode) {
152
152
  return "cli";
153
153
  return mode;
154
154
  }
155
+ const proxyCloseCallbacks = new WeakMap();
156
+ /**
157
+ * Hand a freshly started native session to `build`, closing the session if
158
+ * `build` throws.
159
+ *
160
+ * A started session owns backend processes and sockets that are only reachable
161
+ * through the object `build` returns. Without this the session would leak for
162
+ * the lifetime of the host process whenever adoption failed.
163
+ *
164
+ * Internal; not part of the public package surface.
165
+ */
166
+ export function adoptNativeSession(session, build) {
167
+ try {
168
+ return build();
169
+ }
170
+ catch (error) {
171
+ try {
172
+ session.close();
173
+ }
174
+ catch {
175
+ // Closing a session that never became reachable is best effort.
176
+ }
177
+ throw error;
178
+ }
179
+ }
155
180
  export class CompressorProxy {
156
181
  session;
157
182
  defaultServer;
@@ -242,8 +267,18 @@ export class CompressorProxy {
242
267
  return (await this.invokeWrapper(wrapper, { tool_name: tool, tool_input: toolInput })).text;
243
268
  }
244
269
  close() {
270
+ if (this.closed) {
271
+ return;
272
+ }
245
273
  this.closed = true;
246
- this.session.close();
274
+ try {
275
+ this.session.close();
276
+ }
277
+ finally {
278
+ const onClose = proxyCloseCallbacks.get(this);
279
+ proxyCloseCallbacks.delete(this);
280
+ onClose?.();
281
+ }
247
282
  }
248
283
  toExecutableTools() {
249
284
  const result = {};
@@ -285,6 +320,9 @@ export class CompressorProxy {
285
320
  export class CompressorClient {
286
321
  options;
287
322
  session = null;
323
+ proxy = null;
324
+ connectPromise = null;
325
+ closingPromise = null;
288
326
  mode;
289
327
  authProviders = [];
290
328
  constructor(options) {
@@ -292,9 +330,32 @@ export class CompressorClient {
292
330
  this.mode = options.mode ?? "compressed";
293
331
  }
294
332
  async connect() {
295
- if (this.session) {
296
- return new CompressorProxy(this.session, this.defaultServer(), this.authProviders);
333
+ if (this.closingPromise) {
334
+ try {
335
+ await this.closingPromise;
336
+ }
337
+ catch {
338
+ // The close caller receives the error; cleared state can reconnect independently.
339
+ }
340
+ }
341
+ if (this.proxy) {
342
+ return this.proxy;
297
343
  }
344
+ if (this.connectPromise) {
345
+ return this.connectPromise;
346
+ }
347
+ const connection = this.startConnection();
348
+ this.connectPromise = connection;
349
+ try {
350
+ return await connection;
351
+ }
352
+ finally {
353
+ if (this.connectPromise === connection) {
354
+ this.connectPromise = null;
355
+ }
356
+ }
357
+ }
358
+ async startConnection() {
298
359
  const normalized = await normalizeServersWithProviders(this.options.servers);
299
360
  const config = {
300
361
  compressionLevel: this.options.compressionLevel ?? "medium",
@@ -305,17 +366,55 @@ export class CompressorClient {
305
366
  transformMode: transformMode(this.mode),
306
367
  };
307
368
  this.authProviders = typeof normalized === "string" ? [] : normalized.providers;
308
- this.session =
309
- typeof normalized === "string"
310
- ? await startCompressedSessionFromMcpConfig(config, normalized)
311
- : normalized.providers.length > 0
312
- ? await startCompressedSessionWithAuthProviders(config, normalized.backends, normalized.providers)
313
- : await startCompressedSession(config, normalized.backends);
314
- return new CompressorProxy(this.session, this.defaultServer(), this.authProviders);
315
- }
316
- async close() {
317
- this.session?.close();
318
- this.session = null;
369
+ const session = typeof normalized === "string"
370
+ ? await startCompressedSessionFromMcpConfig(config, normalized)
371
+ : normalized.providers.length > 0
372
+ ? await startCompressedSessionWithAuthProviders(config, normalized.backends, normalized.providers)
373
+ : await startCompressedSession(config, normalized.backends);
374
+ const proxy = adoptNativeSession(session, () => {
375
+ const adopted = new CompressorProxy(session, this.defaultServer(), this.authProviders);
376
+ proxyCloseCallbacks.set(adopted, () => {
377
+ if (this.proxy === adopted) {
378
+ this.proxy = null;
379
+ this.session = null;
380
+ }
381
+ });
382
+ return adopted;
383
+ });
384
+ this.session = session;
385
+ this.proxy = proxy;
386
+ return proxy;
387
+ }
388
+ close() {
389
+ if (!this.closingPromise) {
390
+ const closing = this.closeCurrentConnection();
391
+ let trackedClosing;
392
+ trackedClosing = closing.finally(() => {
393
+ if (this.closingPromise === trackedClosing) {
394
+ this.closingPromise = null;
395
+ }
396
+ });
397
+ this.closingPromise = trackedClosing;
398
+ }
399
+ return this.closingPromise;
400
+ }
401
+ async closeCurrentConnection() {
402
+ try {
403
+ await this.connectPromise;
404
+ }
405
+ catch {
406
+ // A failed connection has no native session to release.
407
+ }
408
+ const proxy = this.proxy;
409
+ try {
410
+ proxy?.close();
411
+ }
412
+ finally {
413
+ if (this.proxy === proxy) {
414
+ this.proxy = null;
415
+ this.session = null;
416
+ }
417
+ }
319
418
  }
320
419
  defaultServer() {
321
420
  const servers = this.options.servers;
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlassian/mcp-compressor",
3
- "version": "0.32.0",
3
+ "version": "0.33.0",
4
4
  "description": "TypeScript MCP server wrapper for reducing tokens consumed by MCP tools.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://github.com/atlassian-labs/mcp-compressor",