@atlassian/mcp-compressor 0.37.0 → 0.38.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.
@@ -1,3 +1,4 @@
1
+ import { type HostShrinkfitOptions } from "./host_shrinkfit.js";
1
2
  import type { ExecutableTool } from "./adapters.js";
2
3
  import type { ToolSpec } from "./rust_core.js";
3
4
  export interface ExecutableToolBridgeServer {
@@ -15,7 +16,7 @@ export interface ExecutableToolBridge extends ExecutableToolBridgeRuntime {
15
16
  token: string;
16
17
  close(): void;
17
18
  }
18
- export interface CreateExecutableToolBridgeOptions {
19
+ export interface CreateExecutableToolBridgeOptions extends HostShrinkfitOptions {
19
20
  serverName: string;
20
21
  }
21
22
  export declare function createExecutableToolBridge(tools: Record<string, ExecutableTool<unknown>>, options: CreateExecutableToolBridgeOptions): Promise<ExecutableToolBridge>;
@@ -1,6 +1,9 @@
1
+ import { shrinkfitTools } from "./host_shrinkfit.js";
1
2
  import { startLocalToolBridge } from "./local_tool_bridge.js";
2
3
  import { executableToolsToSpecs } from "./tool_specs.js";
3
4
  export async function createExecutableToolBridge(tools, options) {
5
+ const shrinking = shrinkfitTools(tools, options);
6
+ tools = shrinking.tools;
4
7
  const bridge = await startLocalToolBridge(tools);
5
8
  const server = {
6
9
  name: options.serverName,
@@ -12,7 +15,10 @@ export async function createExecutableToolBridge(tools, options) {
12
15
  server,
13
16
  bridgeUrl: bridge.bridgeUrl,
14
17
  token: bridge.token,
15
- close: bridge.close,
18
+ close: () => {
19
+ shrinking.close();
20
+ bridge.close();
21
+ },
16
22
  invokeTool: async (toolName, input = {}) => invokeBridgeTool(bridge.bridgeUrl, bridge.token, toolName, input),
17
23
  };
18
24
  }
@@ -0,0 +1,10 @@
1
+ import type { ExecutableTool } from "./adapters.js";
2
+ export interface HostShrinkfitOptions {
3
+ /** Text-content character budget including recovery instructions. Structured content is never shrunk. Disabled by default. */
4
+ shrinkfit?: number;
5
+ }
6
+ /** Per-toolset recovery: no files, no backend retries, no shared cross-session cache. */
7
+ export declare function shrinkfitTools(tools: Record<string, ExecutableTool<unknown>>, options?: HostShrinkfitOptions): {
8
+ tools: Record<string, ExecutableTool<unknown>>;
9
+ close(): void;
10
+ };
@@ -0,0 +1,188 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { loadNativeCore } from "./native.js";
3
+ import { normalizeHostToolResult } from "./rust_core.js";
4
+ /** Per-toolset recovery: no files, no backend retries, no shared cross-session cache. */
5
+ export function shrinkfitTools(tools, options = {}) {
6
+ const limit = options.shrinkfit;
7
+ if (limit === undefined)
8
+ return { tools, close() { } };
9
+ if (!Number.isInteger(limit) || limit <= 0 || limit > 0xffffffff) {
10
+ throw new Error("shrinkfit must be a positive integer no greater than 4294967295");
11
+ }
12
+ // Avoid collisions with genuine upstream tools, including CLI kebab aliases.
13
+ let recoveryName = "compressor_recover_response";
14
+ const normalizeName = (name) => name.replace(/[^a-z0-9]/giu, "").toLowerCase();
15
+ const names = new Set(Object.keys(tools).map(normalizeName));
16
+ let suffix = 2;
17
+ while (names.has(normalizeName(recoveryName)))
18
+ recoveryName = `compressor_recover_response_${suffix++}`;
19
+ const cache = new Map();
20
+ const maxBytes = 64 * 1024 * 1024;
21
+ let bytes = 0;
22
+ let closed = false;
23
+ function remove(id) {
24
+ const entry = cache.get(id);
25
+ if (entry)
26
+ bytes -= entry.bytes;
27
+ cache.delete(id);
28
+ }
29
+ function prune() {
30
+ for (const [id, entry] of cache)
31
+ if (entry.expires <= Date.now())
32
+ remove(id);
33
+ }
34
+ const result = {};
35
+ for (const [name, tool] of Object.entries(tools)) {
36
+ result[name] = {
37
+ ...tool,
38
+ execute: async (input) => {
39
+ if (closed)
40
+ throw new Error("Shrinkfit toolset is closed");
41
+ const value = await tool.execute(input);
42
+ // Error and multimodal envelopes must retain their original semantics.
43
+ if (preserveEnvelope(value))
44
+ return value;
45
+ // Envelope content is presentation; structuredContent is schema-bearing.
46
+ // Preserve all envelope fields and shrink only text blocks, never the
47
+ // serialized envelope or its structured payload.
48
+ const envelope = contentEnvelope(value);
49
+ if (envelope && !Array.isArray(envelope.content))
50
+ return value;
51
+ const blocks = envelope?.content;
52
+ const output = blocks
53
+ ? blocks.map((block) => (typeof block.text === "string" ? block.text : "")).join("\n")
54
+ : normalizeHostToolResult(value, false);
55
+ if (output.length <= limit || withinLimit(output, limit))
56
+ return value;
57
+ const id = randomUUID();
58
+ const message = `This response was automatically shrunk to limit token consumption. Only if more detail is required, call ${recoveryName} with response_id=${id} (CLI: --response-id ${id}). Recovery returns the cached original without repeating the upstream call. Cache expires after 5 minutes and may be evicted earlier.`;
59
+ let preview;
60
+ try {
61
+ if (blocks) {
62
+ // Budget all text, including separators, without touching annotations.
63
+ let remaining = limit - Math.max(0, blocks.length - 1);
64
+ const pending = blocks.map((block, index) => ({
65
+ block,
66
+ index,
67
+ length: Array.from(String(block.text ?? "")).length,
68
+ }));
69
+ const content = blocks.map((block) => ({ ...block }));
70
+ // Small blocks retain their full text; divide remaining space among
71
+ // larger blocks. A budget too small for notices fails explicitly.
72
+ pending.sort((a, b) => a.length - b.length);
73
+ for (let i = 0; i < pending.length; i++) {
74
+ const { block, index, length } = pending[i];
75
+ const budget = Math.floor(remaining / (pending.length - i));
76
+ if (budget < 1)
77
+ throw new Error("Text content budget too small");
78
+ const text = String(block.text ?? "");
79
+ const condensed = length <= budget ? text : ((await shrinkOutput(text, budget, message)) ?? text);
80
+ content[index] = { ...block, text: condensed };
81
+ remaining -= Array.from(condensed).length;
82
+ }
83
+ preview = replaceContentEnvelope(value, { ...envelope, content });
84
+ }
85
+ else {
86
+ preview = await shrinkOutput(output, limit, message);
87
+ }
88
+ }
89
+ catch (error) {
90
+ throw new Error(`Could not shrink response after upstream execution; do not retry a write. ${error instanceof Error ? error.message : String(error)}`);
91
+ }
92
+ if (preview === null)
93
+ return value;
94
+ if (closed)
95
+ throw new Error("Shrinkfit toolset was closed during execution");
96
+ const original = envelope ? JSON.stringify(value) : output;
97
+ const size = Buffer.byteLength(original, "utf8");
98
+ if (size > maxBytes)
99
+ throw new Error("Response exceeds shrinkfit recovery cache capacity; upstream call already executed. Do not retry a write.");
100
+ prune();
101
+ while (cache.size >= 128 || bytes + size > maxBytes)
102
+ remove(cache.keys().next().value);
103
+ cache.set(id, { output: original, bytes: size, expires: Date.now() + 300_000 });
104
+ bytes += size;
105
+ return preview;
106
+ },
107
+ };
108
+ }
109
+ result[recoveryName] = {
110
+ name: recoveryName,
111
+ description: "Retrieve a complete cached response only when its shortened preview lacks required information. Never repeats an upstream call. IDs are scoped to this toolset; expired or evicted IDs fail safely.",
112
+ inputSchema: {
113
+ type: "object",
114
+ properties: {
115
+ response_id: { type: "string", description: "Response ID from the truncation notice." },
116
+ },
117
+ required: ["response_id"],
118
+ additionalProperties: false,
119
+ },
120
+ execute: async (input) => {
121
+ prune();
122
+ const id = input?.response_id;
123
+ const entry = typeof id === "string" && !closed ? cache.get(id) : undefined;
124
+ if (!entry)
125
+ throw new Error("Recovery response unavailable (unknown, expired, evicted, or closed). The upstream tool was NOT called.");
126
+ return entry.output;
127
+ },
128
+ };
129
+ return {
130
+ tools: result,
131
+ close() {
132
+ closed = true;
133
+ cache.clear();
134
+ bytes = 0;
135
+ },
136
+ };
137
+ }
138
+ function preserveEnvelope(value) {
139
+ if (!value || typeof value !== "object" || Array.isArray(value))
140
+ return false;
141
+ const object = value;
142
+ if (Object.keys(object).length === 1 && "result" in object)
143
+ return preserveEnvelope(object.result);
144
+ if (object.isError === true)
145
+ return true;
146
+ return (Array.isArray(object.content) &&
147
+ object.content.some((block) => !block || typeof block !== "object" || block.type !== "text"));
148
+ }
149
+ function withinLimit(output, limit) {
150
+ let count = 0;
151
+ for (const _char of output)
152
+ if (++count > limit)
153
+ return false;
154
+ return true;
155
+ }
156
+ // Bound active CPU jobs across toolsets (N-API itself executes these off-thread).
157
+ let activeJobs = 0;
158
+ const waiters = [];
159
+ async function shrinkOutput(output, limit, message) {
160
+ if (activeJobs >= 4)
161
+ await new Promise((resolve) => waiters.push(resolve));
162
+ else
163
+ activeJobs++;
164
+ try {
165
+ return await loadNativeCore().shrinkHostOutput(output, limit, message);
166
+ }
167
+ finally {
168
+ const next = waiters.shift();
169
+ if (next)
170
+ next();
171
+ else
172
+ activeJobs--;
173
+ }
174
+ }
175
+ function contentEnvelope(value) {
176
+ if (!value || typeof value !== "object" || Array.isArray(value))
177
+ return undefined;
178
+ const object = value;
179
+ if (Object.keys(object).length === 1 && "result" in object)
180
+ return contentEnvelope(object.result);
181
+ return "content" in object || "structuredContent" in object ? object : undefined;
182
+ }
183
+ function replaceContentEnvelope(value, envelope) {
184
+ const object = value;
185
+ return Object.keys(object).length === 1 && "result" in object
186
+ ? { ...object, result: replaceContentEnvelope(object.result, envelope) }
187
+ : envelope;
188
+ }
package/dist/index.d.ts CHANGED
@@ -13,3 +13,4 @@ export * from "./transforms.js";
13
13
  export { interpolateString, interpolateRecord, interpolateMCPConfig, parseServerConfigJson, normalizeConfigServer, } from "./config.js";
14
14
  export type { BackendConfig, HttpBackendConfig, JsonConfigServerEntry, MCPConfigShape, SseBackendConfig, StdioBackendConfig, } from "./types.js";
15
15
  export { type GeneratedClientKind, type GeneratedCodeClient, type JustBashCommand, type JustBashProvider, type CompressorClientOptions, type NativeCompressorMode as CompressorMode, type NativeServersInput as ServersInput, CompressorClient, CompressorProxy, type NormalizedBackendConfig, type ProxyResponse, type ProxyTool, normalizeServers, } from "./native_client.js";
16
+ export * from "./host_shrinkfit.js";
package/dist/index.js CHANGED
@@ -12,3 +12,4 @@ export * from "./tool_specs.js";
12
12
  export * from "./transforms.js";
13
13
  export { interpolateString, interpolateRecord, interpolateMCPConfig, parseServerConfigJson, normalizeConfigServer, } from "./config.js";
14
14
  export { CompressorClient, CompressorProxy, normalizeServers, } from "./native_client.js";
15
+ export * from "./host_shrinkfit.js";
@@ -1,3 +1,4 @@
1
+ import { type HostShrinkfitOptions } from "./host_shrinkfit.js";
1
2
  import type { CompressionLevel } from "./types.js";
2
3
  import type { ExecutableTool } from "./adapters.js";
3
4
  export interface LocalTool<TInput = Record<string, unknown>, TResult = unknown> {
@@ -5,7 +6,7 @@ export interface LocalTool<TInput = Record<string, unknown>, TResult = unknown>
5
6
  inputSchema: unknown;
6
7
  execute: (input: TInput) => TResult | Promise<TResult>;
7
8
  }
8
- export interface CompressToolsOptions {
9
+ export interface CompressToolsOptions extends HostShrinkfitOptions {
9
10
  compressionLevel?: CompressionLevel;
10
11
  namePrefix?: string;
11
12
  toonify?: boolean;
@@ -1,3 +1,4 @@
1
+ import { shrinkfitTools } from "./host_shrinkfit.js";
1
2
  import { compressToolListing, formatToolSchemaResponse, normalizeHostToolResult, } from "./rust_core.js";
2
3
  import { stringifyToolResult } from "./tool_specs.js";
3
4
  function wrapperName(prefix, name) {
@@ -48,6 +49,24 @@ export function compressTools(tools, options = {}) {
48
49
  description: tool.description,
49
50
  inputSchema: asJsonSchema(tool.inputSchema, options.schemaAdapter),
50
51
  }));
52
+ if (options.shrinkfit !== undefined) {
53
+ const executable = Object.fromEntries(specs.map((spec) => [
54
+ spec.name,
55
+ {
56
+ ...tools[spec.name],
57
+ name: spec.name,
58
+ execute: async (input) => tools[spec.name].execute(input),
59
+ inputSchema: spec.inputSchema,
60
+ },
61
+ ]));
62
+ const shrunk = shrinkfitTools(executable, options);
63
+ return compressTools(shrunk.tools, {
64
+ ...options,
65
+ shrinkfit: undefined,
66
+ toonify: false,
67
+ schemaAdapter: undefined,
68
+ });
69
+ }
51
70
  const specByName = new Map(specs.map((spec) => [spec.name, spec]));
52
71
  const toolByName = new Map(Object.entries(tools));
53
72
  const listing = compressToolListing(compressionLevel, specs);
package/dist/native.d.ts CHANGED
@@ -4,6 +4,7 @@ export interface NativeToolSpec {
4
4
  input_schema: Record<string, unknown>;
5
5
  }
6
6
  export interface NativeCore {
7
+ shrinkHostOutput(output: string, maxChars: number, recoveryMessage: string): Promise<string | null>;
7
8
  compressToolListingJson(level: string, toolsJson: string): string;
8
9
  formatToolSchemaResponseJson(toolJson: string): string;
9
10
  maybeToonifyOutputJson(output: string): string;
@@ -1,6 +1,7 @@
1
+ import { type HostShrinkfitOptions } from "./host_shrinkfit.js";
1
2
  import type { ExecutableTool } from "./adapters.js";
2
3
  import { type JustBashCommandRegistration } from "./just_bash_commands.js";
3
- export interface TransformToolOptions {
4
+ export interface TransformToolOptions extends HostShrinkfitOptions {
4
5
  serverName?: string;
5
6
  }
6
7
  export interface PlanToolsForJustBashOptions extends TransformToolOptions {
@@ -15,6 +16,7 @@ export interface TransformToolsForJustBashOptions extends PlanToolsForJustBashOp
15
16
  bash: unknown;
16
17
  }
17
18
  export interface JustBashTransformResult {
19
+ close(): void;
18
20
  tools: Record<string, ExecutableTool>;
19
21
  registrations: JustBashCommandRegistration[];
20
22
  }
@@ -1,14 +1,18 @@
1
+ import { shrinkfitTools } from "./host_shrinkfit.js";
1
2
  import { createJustBashCommandRegistrations, installJustBashRegistrations, } from "./just_bash_commands.js";
2
3
  import { startLocalToolBridge } from "./local_tool_bridge.js";
3
4
  import { buildHostTransformPlan, normalizeHostToolResult, } from "./rust_core.js";
4
5
  import { executableToolToSpec, executableToolsToSpecs, normalizeServerName } from "./tool_specs.js";
5
6
  export function planToolsForJustBash(tools, options = {}) {
6
7
  const serverName = normalizeServerName(options.serverName);
8
+ const shrinking = shrinkfitTools(tools, options);
9
+ tools = shrinking.tools;
7
10
  const toolSpecs = executableToolsToSpecs(tools);
8
11
  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, options.toonify ?? false)));
12
+ const registrations = createJustBashCommandRegistrations((plan.justBash?.commands ?? []).map((command) => justBashSource(serverName, command.commandName, command.backendToolName, tools, options.shrinkfit === undefined && (options.toonify ?? false))));
10
13
  return {
11
14
  registrations,
15
+ close: () => shrinking.close(),
12
16
  tools: helpTools({
13
17
  name: plan.helpToolName,
14
18
  description: plan.helpDescription,
@@ -24,6 +28,7 @@ export function transformToolsForJustBash(tools, options) {
24
28
  export async function transformToolsForCodeMode(tools, options) {
25
29
  const serverName = normalizeServerName(options.serverName);
26
30
  return generatedTransform(tools, {
31
+ shrinkfit: options.shrinkfit,
27
32
  kind: options.language,
28
33
  serverName,
29
34
  outputDir: options.outputDir ?? "./dist",
@@ -32,6 +37,7 @@ export async function transformToolsForCodeMode(tools, options) {
32
37
  export async function transformToolsForCliMode(tools, options = {}) {
33
38
  const serverName = normalizeServerName(options.serverName);
34
39
  return generatedTransform(tools, {
40
+ shrinkfit: options.shrinkfit,
35
41
  kind: "cli",
36
42
  serverName,
37
43
  outputDir: options.outputDir,
@@ -53,6 +59,8 @@ function justBashSource(serverName, commandName, backendToolName, tools, toonify
53
59
  };
54
60
  }
55
61
  async function generatedTransform(tools, options) {
62
+ const shrinking = shrinkfitTools(tools, options);
63
+ tools = shrinking.tools;
56
64
  const bridge = await startLocalToolBridge(tools);
57
65
  const plan = buildHostTransformPlan({
58
66
  kind: options.kind,
@@ -74,7 +82,10 @@ async function generatedTransform(tools, options) {
74
82
  description: plan.helpDescription,
75
83
  output: plan.helpDescription,
76
84
  }),
77
- close: () => bridge.close(),
85
+ close: () => {
86
+ shrinking.close();
87
+ bridge.close();
88
+ },
78
89
  };
79
90
  }
80
91
  function helpTools(options) {
package/native/index.d.ts CHANGED
@@ -44,6 +44,8 @@ export declare function renderCliTopLevelHelpJson(command: string, cliName: stri
44
44
  */
45
45
  export declare function runCliJson(argvJson: string, version?: string | undefined | null): number
46
46
 
47
+ export declare function shrinkHostOutput(output: string, maxChars: number, recoveryMessage: string): Promise<unknown>
48
+
47
49
  export declare function startCompressedSessionFromMcpConfigJson(configJson: string, mcpConfigJson: string): Promise<NativeCompressedSession>
48
50
 
49
51
  export declare function startCompressedSessionJson(configJson: string, backendsJson: string): Promise<NativeCompressedSession>
Binary file
Binary file
package/native/index.js CHANGED
@@ -593,6 +593,7 @@ module.exports.rememberOauthBackendJson = nativeBinding.rememberOauthBackendJson
593
593
  module.exports.renderCliSubcommandHelpJson = nativeBinding.renderCliSubcommandHelpJson
594
594
  module.exports.renderCliTopLevelHelpJson = nativeBinding.renderCliTopLevelHelpJson
595
595
  module.exports.runCliJson = nativeBinding.runCliJson
596
+ module.exports.shrinkHostOutput = nativeBinding.shrinkHostOutput
596
597
  module.exports.startCompressedSessionFromMcpConfigJson = nativeBinding.startCompressedSessionFromMcpConfigJson
597
598
  module.exports.startCompressedSessionJson = nativeBinding.startCompressedSessionJson
598
599
  module.exports.startCompressedSessionWithProviderBackendsJson = nativeBinding.startCompressedSessionWithProviderBackendsJson
Binary file
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/native/index.linux-x64-gnu.node
3
- size: 34486552 bytes
4
- sha256: fb95dc982dbf4787bf810b312d1c0a8b70e996260fc10cae09a06cbe6f5aad1a
3
+ size: 34581720 bytes
4
+ sha256: 8ea4450d7e1728dcf06a408aad5cb4af1c6edba709ea6bbd8cdf2420fc4a14d6
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlassian/mcp-compressor",
3
- "version": "0.37.0",
3
+ "version": "0.38.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",