@atlassian/mcp-compressor 0.32.1 → 0.33.1

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/README.md CHANGED
@@ -72,7 +72,7 @@ atlassian search-confluence --query oauth
72
72
  atlassian get-jira-issue --issue-url https://jira.example.com/browse/PROJ-123
73
73
  ```
74
74
 
75
- TOON output formatting is automatically enabled in CLI mode.
75
+ Tool output is returned verbatim in CLI mode. Pass `--toonify` to opt in to TOON conversion of JSON, CSV and YAML results.
76
76
 
77
77
  ### Bash mode
78
78
 
package/dist/cli.js CHANGED
@@ -61,15 +61,14 @@ 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
- "server via CLI. --toonify is automatically enabled in this mode.")
67
+ "server via CLI.")
68
68
  .option("--cli-port <port>", "Port for the local CLI bridge HTTP server (default: random free port).")
69
69
  .option("--just-bash", "Start in just-bash mode: expose a single 'bash' MCP tool powered by\n" +
70
70
  "just-bash, with all backend server tools available as custom commands.\n" +
71
- "Requires the 'just-bash' package to be installed. --toonify is\n" +
72
- "automatically enabled in this mode.")
71
+ "Requires the 'just-bash' package to be installed.")
73
72
  .option("--include-tool <tool>", "Wrapped server tool name to expose. Can be used multiple times.\n" +
74
73
  "If omitted, all tools are included.", collect, [])
75
74
  .option("--exclude-tool <tool>", "Wrapped server tool name to hide. Can be used multiple times.", collect, []);
@@ -85,7 +84,7 @@ function parseCliArgsWithOptions(argv, parseOptions = {}) {
85
84
  const backend = parseBackendArg(program.args);
86
85
  const justBash = parsedOptions.justBash ?? false;
87
86
  const cliMode = parsedOptions.cliMode ?? false;
88
- const toonify = (parsedOptions.toonify ?? false) || cliMode || justBash;
87
+ const toonify = parsedOptions.toonify ?? false;
89
88
  return {
90
89
  backend,
91
90
  justBash,
package/dist/native.d.ts CHANGED
@@ -20,7 +20,7 @@ export interface NativeCore {
20
20
  listOauthCredentialsJson(): string;
21
21
  clearOauthCredentialsJson(target?: string | null): string;
22
22
  startCompressedSessionJson(configJson: string, backendsJson: string): Promise<NativeCompressedSession>;
23
- startCompressedSessionWithProviderBackendsJson(configJson: string, backendsJson: string, providersJson: string): Promise<NativeCompressedSession>;
23
+ startCompressedSessionWithProviderBackendsJson(configJson: string, backendsJson: string, providersJson: string, refreshProviders: () => Promise<string>): Promise<NativeCompressedSession>;
24
24
  startCompressedSessionFromMcpConfigJson(configJson: string, mcpConfigJson: string): Promise<NativeCompressedSession>;
25
25
  }
26
26
  export interface NativeCompressedSession {
@@ -54,13 +54,24 @@ 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;
60
- private readonly authProviders;
61
73
  private closed;
62
- constructor(session: CompressedSession, defaultServer: string | null, authProviders?: AuthProvider[]);
63
- private refreshAuthProviders;
74
+ constructor(session: CompressedSession, defaultServer: string | null);
64
75
  info(): CompressedSessionInfo;
65
76
  get bridgeUrl(): string;
66
77
  get token(): string;
@@ -87,10 +98,15 @@ export declare class CompressorProxy {
87
98
  export declare class CompressorClient {
88
99
  private readonly options;
89
100
  private session;
101
+ private proxy;
102
+ private connectPromise;
103
+ private closingPromise;
90
104
  private readonly mode;
91
105
  private authProviders;
92
106
  constructor(options: CompressorClientOptions);
93
107
  connect(): Promise<CompressorProxy>;
108
+ private startConnection;
94
109
  close(): Promise<void>;
110
+ private closeCurrentConnection;
95
111
  private defaultServer;
96
112
  }
@@ -46,7 +46,7 @@ async function normalizeServersWithProviders(servers) {
46
46
  for (const [name, config] of Object.entries(servers)) {
47
47
  if (typeof config === "object" && config !== null && "url" in config) {
48
48
  const provider = providerFromConfig(config);
49
- const backend = await sdkObjectToNative(name, config, { includeProvider: true });
49
+ const backend = await sdkObjectToNative(name, config, { includeProvider: provider === undefined });
50
50
  if (provider !== undefined) {
51
51
  backend.providerIndex = providers.length;
52
52
  providers.push(provider);
@@ -152,25 +152,38 @@ 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;
158
- authProviders;
159
183
  closed = false;
160
- constructor(session, defaultServer, authProviders = []) {
184
+ constructor(session, defaultServer) {
161
185
  this.session = session;
162
186
  this.defaultServer = defaultServer;
163
- this.authProviders = authProviders;
164
- }
165
- async refreshAuthProviders() {
166
- await Promise.all(this.authProviders.map(async (provider, index) => {
167
- const headers = await provider();
168
- const materialized = {};
169
- for (const [key, value] of Object.entries(headers)) {
170
- materialized[key] = String(value);
171
- }
172
- this.session.updateAuthProviderHeaders(index, materialized);
173
- }));
174
187
  }
175
188
  info() {
176
189
  return this.session.info();
@@ -208,7 +221,6 @@ export class CompressorProxy {
208
221
  if (this.closed) {
209
222
  throw new Error("Compressor proxy is closed");
210
223
  }
211
- await this.refreshAuthProviders();
212
224
  const response = await fetch(`${this.bridgeUrl}/exec`, {
213
225
  method: "POST",
214
226
  headers: {
@@ -242,8 +254,18 @@ export class CompressorProxy {
242
254
  return (await this.invokeWrapper(wrapper, { tool_name: tool, tool_input: toolInput })).text;
243
255
  }
244
256
  close() {
257
+ if (this.closed) {
258
+ return;
259
+ }
245
260
  this.closed = true;
246
- this.session.close();
261
+ try {
262
+ this.session.close();
263
+ }
264
+ finally {
265
+ const onClose = proxyCloseCallbacks.get(this);
266
+ proxyCloseCallbacks.delete(this);
267
+ onClose?.();
268
+ }
247
269
  }
248
270
  toExecutableTools() {
249
271
  const result = {};
@@ -285,6 +307,9 @@ export class CompressorProxy {
285
307
  export class CompressorClient {
286
308
  options;
287
309
  session = null;
310
+ proxy = null;
311
+ connectPromise = null;
312
+ closingPromise = null;
288
313
  mode;
289
314
  authProviders = [];
290
315
  constructor(options) {
@@ -292,9 +317,32 @@ export class CompressorClient {
292
317
  this.mode = options.mode ?? "compressed";
293
318
  }
294
319
  async connect() {
295
- if (this.session) {
296
- return new CompressorProxy(this.session, this.defaultServer(), this.authProviders);
320
+ if (this.closingPromise) {
321
+ try {
322
+ await this.closingPromise;
323
+ }
324
+ catch {
325
+ // The close caller receives the error; cleared state can reconnect independently.
326
+ }
327
+ }
328
+ if (this.proxy) {
329
+ return this.proxy;
330
+ }
331
+ if (this.connectPromise) {
332
+ return this.connectPromise;
333
+ }
334
+ const connection = this.startConnection();
335
+ this.connectPromise = connection;
336
+ try {
337
+ return await connection;
297
338
  }
339
+ finally {
340
+ if (this.connectPromise === connection) {
341
+ this.connectPromise = null;
342
+ }
343
+ }
344
+ }
345
+ async startConnection() {
298
346
  const normalized = await normalizeServersWithProviders(this.options.servers);
299
347
  const config = {
300
348
  compressionLevel: this.options.compressionLevel ?? "medium",
@@ -305,17 +353,55 @@ export class CompressorClient {
305
353
  transformMode: transformMode(this.mode),
306
354
  };
307
355
  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;
356
+ const session = typeof normalized === "string"
357
+ ? await startCompressedSessionFromMcpConfig(config, normalized)
358
+ : normalized.providers.length > 0
359
+ ? await startCompressedSessionWithAuthProviders(config, normalized.backends, normalized.providers)
360
+ : await startCompressedSession(config, normalized.backends);
361
+ const proxy = adoptNativeSession(session, () => {
362
+ const adopted = new CompressorProxy(session, this.defaultServer());
363
+ proxyCloseCallbacks.set(adopted, () => {
364
+ if (this.proxy === adopted) {
365
+ this.proxy = null;
366
+ this.session = null;
367
+ }
368
+ });
369
+ return adopted;
370
+ });
371
+ this.session = session;
372
+ this.proxy = proxy;
373
+ return proxy;
374
+ }
375
+ close() {
376
+ if (!this.closingPromise) {
377
+ const closing = this.closeCurrentConnection();
378
+ let trackedClosing;
379
+ trackedClosing = closing.finally(() => {
380
+ if (this.closingPromise === trackedClosing) {
381
+ this.closingPromise = null;
382
+ }
383
+ });
384
+ this.closingPromise = trackedClosing;
385
+ }
386
+ return this.closingPromise;
387
+ }
388
+ async closeCurrentConnection() {
389
+ try {
390
+ await this.connectPromise;
391
+ }
392
+ catch {
393
+ // A failed connection has no native session to release.
394
+ }
395
+ const proxy = this.proxy;
396
+ try {
397
+ proxy?.close();
398
+ }
399
+ finally {
400
+ if (this.proxy === proxy) {
401
+ this.proxy = null;
402
+ this.session = null;
403
+ }
404
+ }
319
405
  }
320
406
  defaultServer() {
321
407
  const servers = this.options.servers;
package/dist/rust_core.js CHANGED
@@ -88,13 +88,19 @@ export async function startCompressedSession(config, backends) {
88
88
  const session = await loadNativeCore().startCompressedSessionJson(stringify(toNativeSessionConfig(config)), stringify(backends.map(toNativeBackendConfig)));
89
89
  return new CompressedSession(session);
90
90
  }
91
+ async function materializeAuthProviderHeaders(providers) {
92
+ return Promise.all(providers.map(async (provider) => {
93
+ const headers = await provider();
94
+ return Object.fromEntries(Object.entries(headers).map(([key, value]) => [key, String(value)]));
95
+ }));
96
+ }
91
97
  export async function startCompressedSessionWithAuthProviders(config, backends, providers) {
92
98
  const nativeBackends = backends.map((backend) => ({
93
99
  ...toNativeBackendConfig(backend),
94
100
  provider_index: backend.providerIndex ?? null,
95
101
  }));
96
- const initialHeaders = await Promise.all(providers.map((provider) => provider()));
97
- const session = await loadNativeCore().startCompressedSessionWithProviderBackendsJson(stringify(toNativeSessionConfig(config)), stringify(nativeBackends), stringify(initialHeaders));
102
+ const initialHeaders = await materializeAuthProviderHeaders(providers);
103
+ const session = await loadNativeCore().startCompressedSessionWithProviderBackendsJson(stringify(toNativeSessionConfig(config)), stringify(nativeBackends), stringify(initialHeaders), async () => stringify(await materializeAuthProviderHeaders(providers)));
98
104
  return new CompressedSession(session);
99
105
  }
100
106
  export async function startCompressedSessionFromMcpConfig(config, mcpConfigJson) {
@@ -3,10 +3,16 @@ import { type JustBashCommandRegistration } from "./just_bash_commands.js";
3
3
  export interface TransformToolOptions {
4
4
  serverName?: string;
5
5
  }
6
- export interface TransformToolsForJustBashOptions extends TransformToolOptions {
7
- bash: unknown;
8
- }
9
6
  export interface PlanToolsForJustBashOptions extends TransformToolOptions {
7
+ /**
8
+ * Convert JSON/CSV/YAML tool results to TOON before writing them to stdout.
9
+ * Defaults to ``false`` so Just Bash output matches the generated CLI byte for
10
+ * byte (and stays pipeable into tools such as ``jq``).
11
+ */
12
+ toonify?: boolean;
13
+ }
14
+ export interface TransformToolsForJustBashOptions extends PlanToolsForJustBashOptions {
15
+ bash: unknown;
10
16
  }
11
17
  export interface JustBashTransformResult {
12
18
  tools: Record<string, ExecutableTool>;
@@ -6,7 +6,7 @@ export function planToolsForJustBash(tools, options = {}) {
6
6
  const serverName = normalizeServerName(options.serverName);
7
7
  const toolSpecs = executableToolsToSpecs(tools);
8
8
  const plan = buildHostTransformPlan({ kind: "just-bash", serverName, tools: toolSpecs });
9
- const registrations = createJustBashCommandRegistrations((plan.justBash?.commands ?? []).map((command) => justBashSource(serverName, command.commandName, command.backendToolName, tools)));
9
+ const registrations = createJustBashCommandRegistrations((plan.justBash?.commands ?? []).map((command) => justBashSource(serverName, command.commandName, command.backendToolName, tools, options.toonify ?? false)));
10
10
  return {
11
11
  registrations,
12
12
  tools: helpTools({
@@ -38,7 +38,7 @@ export async function transformToolsForCliMode(tools, options = {}) {
38
38
  commandName: serverName,
39
39
  });
40
40
  }
41
- function justBashSource(serverName, commandName, backendToolName, tools) {
41
+ function justBashSource(serverName, commandName, backendToolName, tools, toonify) {
42
42
  const tool = tools[backendToolName];
43
43
  if (tool === undefined) {
44
44
  throw new Error(`missing Just Bash backend tool: ${backendToolName}`);
@@ -49,7 +49,7 @@ function justBashSource(serverName, commandName, backendToolName, tools) {
49
49
  backendToolName,
50
50
  helpToolName: `${serverName}_help`,
51
51
  tool: executableToolToSpec(backendToolName, tool),
52
- invoke: async (input) => normalizeHostToolResult(await tool.execute(input), true),
52
+ invoke: async (input) => normalizeHostToolResult(await tool.execute(input), toonify),
53
53
  };
54
54
  }
55
55
  async function generatedTransform(tools, options) {
package/native/index.d.ts CHANGED
@@ -40,4 +40,4 @@ export declare function startCompressedSessionFromMcpConfigJson(configJson: stri
40
40
 
41
41
  export declare function startCompressedSessionJson(configJson: string, backendsJson: string): Promise<NativeCompressedSession>
42
42
 
43
- export declare function startCompressedSessionWithProviderBackendsJson(configJson: string, backendsJson: string, providersJson: string): Promise<NativeCompressedSession>
43
+ export declare function startCompressedSessionWithProviderBackendsJson(configJson: string, backendsJson: string, providersJson: string, refreshProviders: RefreshProviders): Promise<NativeCompressedSession>
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.1",
3
+ "version": "0.33.1",
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",