@atlassian/mcp-compressor 0.0.2
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 +393 -0
- package/dist/adapters.d.ts +17 -0
- package/dist/adapters.js +23 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.js +175 -0
- package/dist/config.d.ts +36 -0
- package/dist/config.js +102 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +12 -0
- package/dist/formatting.d.ts +3 -0
- package/dist/formatting.js +4 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/just_bash_host.d.ts +11 -0
- package/dist/just_bash_host.js +63 -0
- package/dist/just_bash_transform.d.ts +75 -0
- package/dist/just_bash_transform.js +167 -0
- package/dist/native.d.ts +25 -0
- package/dist/native.js +11 -0
- package/dist/native_client.d.ts +96 -0
- package/dist/native_client.js +330 -0
- package/dist/rust_core.d.ts +100 -0
- package/dist/rust_core.js +103 -0
- package/dist/types.d.ts +34 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/native/index.d.ts +31 -0
- package/native/index.darwin-arm64.node +0 -0
- package/native/index.js +591 -0
- package/native/package.json +3 -0
- package/package.json +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
# @atlassian/mcp-compressor (TypeScript)
|
|
2
|
+
|
|
3
|
+
A TypeScript MCP proxy that wraps one or more MCP servers and reduces the token footprint exposed to LLMs.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
mcp-compressor connects to upstream MCP servers (via stdio, HTTP, or SSE) and replaces their full tool catalogs with a compressed interface. Instead of exposing every tool individually, it provides:
|
|
8
|
+
|
|
9
|
+
- **`get_tool_schema(tool_name)`** — returns the full schema for a specific tool on demand
|
|
10
|
+
- **`invoke_tool(tool_name, tool_input)`** — calls an upstream tool
|
|
11
|
+
- **`list_tools()`** — lists available tool names (only at `max` compression)
|
|
12
|
+
|
|
13
|
+
This dramatically reduces the token cost of tool descriptions in LLM context windows while preserving full tool functionality.
|
|
14
|
+
|
|
15
|
+
### Compression levels
|
|
16
|
+
|
|
17
|
+
| Level | What the LLM sees |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `low` | Tool name, parameters, and full description |
|
|
20
|
+
| `medium` (default) | Tool name, parameters, and first sentence of description |
|
|
21
|
+
| `high` | Tool name and parameters only |
|
|
22
|
+
| `max` | Tool names only (requires `list_tools` call to discover) |
|
|
23
|
+
|
|
24
|
+
### Three modes
|
|
25
|
+
|
|
26
|
+
| Mode | Tools exposed | How the LLM invokes tools |
|
|
27
|
+
|---|---|---|
|
|
28
|
+
| **Compressed** (default) | `get_tool_schema` + `invoke_tool` | Via MCP tool calls |
|
|
29
|
+
| **CLI** | Per-server `_help` tools | Via bash CLI commands (bridge + generated scripts) |
|
|
30
|
+
| **Bash** | Per-server `_help` tools + `bash` tool | Via a sandboxed [just-bash](https://www.npmjs.com/package/just-bash) shell |
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @atlassian/mcp-compressor
|
|
36
|
+
|
|
37
|
+
# Optional: for bash mode
|
|
38
|
+
npm install just-bash
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## STDIO MCP proxy
|
|
42
|
+
|
|
43
|
+
The simplest way to use mcp-compressor is as a CLI that wraps another MCP server and exposes a compressed proxy over stdio.
|
|
44
|
+
|
|
45
|
+
### Compressed mode (default)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Wrap a remote MCP server
|
|
49
|
+
bun cli -- https://mcp.atlassian.com/v1/mcp
|
|
50
|
+
|
|
51
|
+
# Wrap a local stdio server
|
|
52
|
+
bun cli --server-name filesystem -- npx -y @modelcontextprotocol/server-filesystem .
|
|
53
|
+
|
|
54
|
+
# With options
|
|
55
|
+
bun cli -c high --server-name atlassian --toonify -- https://mcp.atlassian.com/v1/mcp
|
|
56
|
+
|
|
57
|
+
# Multi-server via MCP config JSON
|
|
58
|
+
bun cli -- '{"mcpServers":{"jira":{"url":"https://jira-mcp.example.com"},"confluence":{"command":"node","args":["confluence-server.js"]}}}'
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### CLI mode
|
|
62
|
+
|
|
63
|
+
CLI mode generates shell scripts for each backend server so the LLM (or user) can invoke tools directly from bash. The MCP proxy exposes per-server help tools that describe the available commands.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Start CLI mode
|
|
67
|
+
bun cli --cli-mode --server-name atlassian -- https://mcp.atlassian.com/v1/mcp
|
|
68
|
+
|
|
69
|
+
# In another terminal, use the generated CLI:
|
|
70
|
+
atlassian --help
|
|
71
|
+
atlassian search-confluence --query oauth
|
|
72
|
+
atlassian get-jira-issue --issue-url https://jira.example.com/browse/PROJ-123
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
TOON output formatting is automatically enabled in CLI mode.
|
|
76
|
+
|
|
77
|
+
### Bash mode
|
|
78
|
+
|
|
79
|
+
Bash mode registers all backend tools as custom commands in a sandboxed just-bash shell, then exposes a single `bash` MCP tool plus per-server help tools. The LLM can run MCP tools alongside standard Unix utilities, including pipes and composition.
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
bun cli --just-bash --server-name atlassian -- https://mcp.atlassian.com/v1/mcp
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The LLM then sees:
|
|
86
|
+
- `bash` — execute commands in the sandboxed shell
|
|
87
|
+
- `atlassian_help` — lists available atlassian subcommands
|
|
88
|
+
|
|
89
|
+
Example commands the LLM can run via the bash tool:
|
|
90
|
+
```bash
|
|
91
|
+
atlassian search-issues --jql "project=PROJ" | jq '.issues[].key'
|
|
92
|
+
atlassian get-page --page-id 12345 | grep "summary"
|
|
93
|
+
echo "hello" | grep hello
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### OAuth
|
|
97
|
+
|
|
98
|
+
For remote OAuth backends, the CLI handles the authorization flow automatically — it opens the browser, completes the code exchange, and persists tokens for future sessions.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# Clear cached OAuth state
|
|
102
|
+
bun cli clear-oauth https://mcp.atlassian.com/v1/mcp
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## In-process usage (CompressorClient)
|
|
106
|
+
|
|
107
|
+
For TypeScript applications and agent frameworks, `CompressorClient` provides a single unified interface for all modes — no subprocess needed.
|
|
108
|
+
|
|
109
|
+
### Compressed mode
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { CompressorClient } from '@atlassian/mcp-compressor';
|
|
113
|
+
|
|
114
|
+
const client = new CompressorClient({
|
|
115
|
+
servers: {
|
|
116
|
+
jira: { url: 'https://jira-mcp.example.com' },
|
|
117
|
+
confluence: { command: 'node', args: ['confluence-server.js'] },
|
|
118
|
+
},
|
|
119
|
+
compressionLevel: 'medium',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
await client.connect();
|
|
123
|
+
const tools = await client.getTools();
|
|
124
|
+
// → { jira_get_tool_schema, jira_invoke_tool, confluence_get_tool_schema, confluence_invoke_tool }
|
|
125
|
+
|
|
126
|
+
// Use with any AI SDK-compatible framework
|
|
127
|
+
const schema = await tools.jira_get_tool_schema.execute({ tool_name: 'search_issues' });
|
|
128
|
+
const result = await tools.jira_invoke_tool.execute({
|
|
129
|
+
tool_name: 'search_issues',
|
|
130
|
+
tool_input: { query: 'oauth' },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await client.close();
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### CLI mode
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
const client = new CompressorClient({
|
|
140
|
+
servers: { atlassian: { url: 'https://mcp.atlassian.com/v1/mcp' } },
|
|
141
|
+
mode: 'cli',
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
await client.connect();
|
|
145
|
+
const tools = await client.getTools();
|
|
146
|
+
// → { atlassian_help }
|
|
147
|
+
// Side effect: HTTP bridge started, shell script generated
|
|
148
|
+
|
|
149
|
+
// client.scripts has info about generated scripts
|
|
150
|
+
for (const script of client.scripts) {
|
|
151
|
+
console.log(`Run '${script.cliName} --help' for usage`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await client.close();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Bash mode
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
const client = new CompressorClient({
|
|
161
|
+
servers: {
|
|
162
|
+
jira: { url: 'https://jira-mcp.example.com' },
|
|
163
|
+
confluence: { command: 'node', args: ['confluence-server.js'] },
|
|
164
|
+
},
|
|
165
|
+
mode: 'bash',
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
await client.connect();
|
|
169
|
+
const tools = await client.getTools();
|
|
170
|
+
// → { bash, jira_help, confluence_help }
|
|
171
|
+
|
|
172
|
+
// Execute commands via the bash tool
|
|
173
|
+
const result = await tools.bash.execute({ command: 'jira search-issues --query oauth' });
|
|
174
|
+
|
|
175
|
+
// Access the Bash instance directly
|
|
176
|
+
const execResult = await client.bash!.exec('jira search-issues --query test | jq .issues');
|
|
177
|
+
|
|
178
|
+
await client.close();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Bash mode with a pre-existing Bash instance
|
|
182
|
+
|
|
183
|
+
If your application already has a `Bash` instance (e.g. with its own custom commands), you can inject it:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
import { Bash } from 'just-bash';
|
|
187
|
+
|
|
188
|
+
const existingBash = new Bash({ customCommands: [myCustomCommand] });
|
|
189
|
+
|
|
190
|
+
const client = new CompressorClient({
|
|
191
|
+
servers: { atlassian: { url: 'https://mcp.atlassian.com/v1/mcp' } },
|
|
192
|
+
mode: 'bash',
|
|
193
|
+
bash: { bash: existingBash },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
await client.connect();
|
|
197
|
+
const tools = await client.getTools();
|
|
198
|
+
// → { bash, atlassian_help }
|
|
199
|
+
// MCP commands are registered into existingBash via registerCommand()
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Server configuration formats
|
|
203
|
+
|
|
204
|
+
`CompressorClient` accepts several formats for the `servers` option:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// Named servers map (recommended for multi-server)
|
|
208
|
+
new CompressorClient({
|
|
209
|
+
servers: {
|
|
210
|
+
jira: { url: 'https://jira-mcp.example.com' },
|
|
211
|
+
filesystem: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '.'] },
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Single BackendConfig
|
|
216
|
+
new CompressorClient({
|
|
217
|
+
servers: { type: 'http', url: 'https://mcp.example.com' },
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// URL string
|
|
221
|
+
new CompressorClient({
|
|
222
|
+
servers: 'https://mcp.example.com',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// MCP config JSON string
|
|
226
|
+
new CompressorClient({
|
|
227
|
+
servers: '{"mcpServers":{"jira":{"url":"https://jira-mcp.example.com"}}}',
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Escape hatches
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
// Access individual runtimes
|
|
235
|
+
const jiraRuntime = client.getRuntime('jira');
|
|
236
|
+
await jiraRuntime.invokeTool('search_issues', { query: 'bug' });
|
|
237
|
+
|
|
238
|
+
// List all server names
|
|
239
|
+
console.log(client.serverNames); // ['jira', 'confluence']
|
|
240
|
+
|
|
241
|
+
// Access all runtimes
|
|
242
|
+
for (const runtime of client.runtimes) {
|
|
243
|
+
console.log(runtime.serverName, await runtime.listToolNames());
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Rust-backed native client
|
|
248
|
+
|
|
249
|
+
The Rust-core migration exposes a high-level native client for applications that want to start compressed MCP proxy sessions in-process, without spawning the `mcp-compressor` CLI as a stdio subprocess.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { CompressorClient } from "@atlassian/mcp-compressor";
|
|
253
|
+
|
|
254
|
+
const client = new CompressorClient({
|
|
255
|
+
servers: {
|
|
256
|
+
atlassian: {
|
|
257
|
+
url: "https://mcp.atlassian.com/v1/mcp",
|
|
258
|
+
headers: {
|
|
259
|
+
Authorization: `Basic ${token}`,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
compressionLevel: "medium",
|
|
264
|
+
includeTools: ["getConfluencePage", "updateConfluencePage"],
|
|
265
|
+
toonify: true,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const proxy = await client.connect();
|
|
269
|
+
try {
|
|
270
|
+
console.log(proxy.tools.map((tool) => tool.name));
|
|
271
|
+
const output = await proxy.invoke("getAccessibleAtlassianResources", {}, { server: "atlassian" });
|
|
272
|
+
console.log(output);
|
|
273
|
+
} finally {
|
|
274
|
+
await client.close();
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
The package root now exposes the Rust-backed `CompressorClient` as the primary SDK surface on the migration trunk.
|
|
279
|
+
|
|
280
|
+
Just Bash mode exposes typed provider metadata so language hosts can register backend MCP tools as Just Bash commands while Rust owns compression/proxy routing:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const proxy = await new CompressorClient({ servers, mode: "bash" }).connect();
|
|
284
|
+
try {
|
|
285
|
+
for (const provider of proxy.justBashProviders) {
|
|
286
|
+
console.log(provider.providerName, provider.helpToolName);
|
|
287
|
+
for (const command of provider.tools) {
|
|
288
|
+
console.log(command.commandName, command.backendToolName, command.invokeToolName);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
proxy.close();
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Connected proxies can also write shell, Python, or TypeScript clients that call the live Rust proxy:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
const proxy = await client.connect();
|
|
300
|
+
try {
|
|
301
|
+
proxy.writeClient("cli", "./bin", { name: "atlassian" });
|
|
302
|
+
proxy.writeClient("python", "./generated-py", { name: "atlassian" });
|
|
303
|
+
proxy.writeClient("typescript", "./generated-ts", { name: "atlassian" });
|
|
304
|
+
} finally {
|
|
305
|
+
proxy.close();
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Development
|
|
310
|
+
|
|
311
|
+
### Setup
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
cd typescript
|
|
315
|
+
bun install
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Commands
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
bun run test # run tests with vitest
|
|
322
|
+
bun run check # lint + format + typecheck + test
|
|
323
|
+
bun run lint # lint with oxlint
|
|
324
|
+
bun run format # format with oxfmt
|
|
325
|
+
bun run format:check # check formatting
|
|
326
|
+
bun run build # compile to dist/
|
|
327
|
+
bun cli # run the CLI directly from source
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Packaging smoke test
|
|
331
|
+
|
|
332
|
+
Build and test the npm package from a clean temporary project:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
bun install
|
|
336
|
+
bun run build
|
|
337
|
+
bun run build:native
|
|
338
|
+
bun pm pack --filename /tmp/mcp-compressor-ts-package.tgz
|
|
339
|
+
mkdir -p /tmp/mcp-compressor-ts-package-test
|
|
340
|
+
cd /tmp/mcp-compressor-ts-package-test
|
|
341
|
+
bun init -y
|
|
342
|
+
bun add --registry https://registry.npmjs.org /tmp/mcp-compressor-ts-package.tgz
|
|
343
|
+
bun --eval 'import { CompressorClient, compressToolListing } from "@atlassian/mcp-compressor"; console.log(typeof CompressorClient, compressToolListing("high", [{ name: "echo", inputSchema: { type: "object", properties: {} } }]))'
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
CI runs the same package smoke test and uploads the packed tarball as an artifact.
|
|
347
|
+
|
|
348
|
+
### Toolchain
|
|
349
|
+
|
|
350
|
+
| Tool | Purpose |
|
|
351
|
+
|---|---|
|
|
352
|
+
| [Bun](https://bun.sh/) | Package manager and runtime |
|
|
353
|
+
| [TypeScript](https://www.typescriptlang.org/) | Type checking |
|
|
354
|
+
| [Vitest](https://vitest.dev/) | Test runner |
|
|
355
|
+
| [oxlint](https://oxc.rs/docs/guide/usage/linter) | Linter |
|
|
356
|
+
| [oxfmt](https://oxc.rs/docs/guide/usage/formatter) | Formatter |
|
|
357
|
+
| [Commander](https://github.com/tj/commander.js) | CLI argument parsing |
|
|
358
|
+
|
|
359
|
+
## Publishing
|
|
360
|
+
|
|
361
|
+
To publish from source, ensure you have active credentials for Artifactory, then run:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
bun run build
|
|
365
|
+
npm version <PUBLISH_VERSION> --no-git-tag-version
|
|
366
|
+
npm publish
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Published as `@atlassian/mcp-compressor` via Atlassian Artifactory → npmJS.
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npm install @atlassian/mcp-compressor
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Sub-path exports
|
|
376
|
+
|
|
377
|
+
`@atlassian/mcp-compressor` ships a small set of sub-path exports for consumers that only need the lightweight pieces. Importing from these sub-paths avoids loading `fastmcp`, the MCP SDK, and the rest of the runtime, which can add hundreds of ms of cold-import cost.
|
|
378
|
+
|
|
379
|
+
| Sub-path | Contents | When to use |
|
|
380
|
+
|---|---|---|
|
|
381
|
+
| `@atlassian/mcp-compressor` | Full runtime (`CompressorRuntime`, `CompressorServer`, `BackendClient`, `FastMCP`, ...) | Building / hosting a compressor proxy. |
|
|
382
|
+
| `@atlassian/mcp-compressor/bash` | The just-bash transform helpers | Bash-tool transformation. |
|
|
383
|
+
| `@atlassian/mcp-compressor/config` | `interpolateString`, `interpolateRecord`, `parseServerConfigJson` | Pure config parsing / `${ENV}` interpolation in tooling. |
|
|
384
|
+
| `@atlassian/mcp-compressor/errors` | `InvalidConfigurationError` and friends | Error type checks without the runtime. |
|
|
385
|
+
| `@atlassian/mcp-compressor/types` | `BackendConfig`, `CompressionLevel`, `CommonProxyOptions`, ... | Type-only consumers. |
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
// heavy: pulls fastmcp + MCP SDK
|
|
389
|
+
import { CompressorRuntime } from "@atlassian/mcp-compressor";
|
|
390
|
+
|
|
391
|
+
// light: ~zero runtime deps
|
|
392
|
+
import { interpolateString } from "@atlassian/mcp-compressor/config";
|
|
393
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ExecutableTool {
|
|
2
|
+
name: string;
|
|
3
|
+
description?: string;
|
|
4
|
+
inputSchema: Record<string, unknown>;
|
|
5
|
+
execute(input?: Record<string, unknown>): Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
export interface AISDKToolFactory<TTool = unknown> {
|
|
8
|
+
(options: {
|
|
9
|
+
description?: string;
|
|
10
|
+
inputSchema: Record<string, unknown>;
|
|
11
|
+
execute: (input: Record<string, unknown>) => Promise<string>;
|
|
12
|
+
}): TTool;
|
|
13
|
+
}
|
|
14
|
+
export declare function toAISDKTools<TTool = unknown>(tools: Record<string, ExecutableTool>, options?: {
|
|
15
|
+
tool?: AISDKToolFactory<TTool>;
|
|
16
|
+
}): Record<string, TTool | Omit<ExecutableTool, "name">>;
|
|
17
|
+
export declare function toMastraTools(tools: Record<string, ExecutableTool>): Record<string, Omit<ExecutableTool, "name">>;
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function toAISDKTools(tools, options = {}) {
|
|
2
|
+
const result = {};
|
|
3
|
+
for (const [name, executable] of Object.entries(tools)) {
|
|
4
|
+
const definition = {
|
|
5
|
+
description: executable.description,
|
|
6
|
+
inputSchema: executable.inputSchema,
|
|
7
|
+
execute: (input) => executable.execute(input),
|
|
8
|
+
};
|
|
9
|
+
result[name] = options.tool ? options.tool(definition) : definition;
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
export function toMastraTools(tools) {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [name, executable] of Object.entries(tools)) {
|
|
16
|
+
result[name] = {
|
|
17
|
+
description: executable.description,
|
|
18
|
+
inputSchema: executable.inputSchema,
|
|
19
|
+
execute: (input = {}) => executable.execute(input),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import type { BackendConfig } from "./types.js";
|
|
3
|
+
export interface ParsedCliArgs {
|
|
4
|
+
backend: BackendConfig | string;
|
|
5
|
+
justBash: boolean;
|
|
6
|
+
cliMode: boolean;
|
|
7
|
+
cliPort: string | undefined;
|
|
8
|
+
compressionLevel: "low" | "medium" | "high" | "max" | undefined;
|
|
9
|
+
cwd: string | undefined;
|
|
10
|
+
env: Record<string, string>;
|
|
11
|
+
excludeTools: string[];
|
|
12
|
+
headers: Record<string, string>;
|
|
13
|
+
includeTools: string[];
|
|
14
|
+
logLevel: string;
|
|
15
|
+
serverName: string | undefined;
|
|
16
|
+
timeout: number;
|
|
17
|
+
toonify: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function parseCliArgs(argv: string[]): ParsedCliArgs;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { VERSION } from "./version.js";
|
|
8
|
+
function parseBackendArg(backendArgs) {
|
|
9
|
+
if (backendArgs.length === 0) {
|
|
10
|
+
throw new Error("Expected a backend URL, MCP config JSON string, or stdio command.");
|
|
11
|
+
}
|
|
12
|
+
if (backendArgs.length === 1) {
|
|
13
|
+
return backendArgs[0];
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
type: "stdio",
|
|
17
|
+
command: backendArgs[0],
|
|
18
|
+
args: backendArgs.slice(1),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function collect(value, previous) {
|
|
22
|
+
previous.push(value);
|
|
23
|
+
return previous;
|
|
24
|
+
}
|
|
25
|
+
function collectKeyValue(value, previous) {
|
|
26
|
+
const eqIndex = value.indexOf("=");
|
|
27
|
+
if (eqIndex === -1) {
|
|
28
|
+
throw new Error(`Invalid key=value format: '${value}'. Expected KEY=VALUE.`);
|
|
29
|
+
}
|
|
30
|
+
const key = value.slice(0, eqIndex);
|
|
31
|
+
let val = value.slice(eqIndex + 1);
|
|
32
|
+
// Support ${VAR_NAME} environment variable expansion
|
|
33
|
+
val = val.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] ?? "");
|
|
34
|
+
previous[key] = val;
|
|
35
|
+
return previous;
|
|
36
|
+
}
|
|
37
|
+
function buildProgram(options = {}) {
|
|
38
|
+
const program = new Command()
|
|
39
|
+
.name("mcp-compressor")
|
|
40
|
+
.description("Run the MCP Compressor proxy server.\n\n" +
|
|
41
|
+
"Connects to an MCP server (via stdio, HTTP, or SSE) and wraps it\n" +
|
|
42
|
+
"with a compressed tool interface.")
|
|
43
|
+
.allowUnknownOption(false)
|
|
44
|
+
.allowExcessArguments(true)
|
|
45
|
+
.version(VERSION, "-V, --version")
|
|
46
|
+
.argument("[command_or_url...]", "The backend to wrap: either a remote MCP URL, a stdio command plus\n" +
|
|
47
|
+
"arguments, or an MCP config JSON string with one or more servers.\n" +
|
|
48
|
+
"Example stdio usage: bun run dist/cli.js -- uvx mcp-server-fetch")
|
|
49
|
+
.option("--cwd <dir>", "The working directory to use when running stdio MCP servers.")
|
|
50
|
+
.option("-e, --env <VAR=VALUE>", "Environment variables to set when running stdio MCP servers, in the\n" +
|
|
51
|
+
"form VAR_NAME=VALUE. Can be used multiple times. Supports environment\n" +
|
|
52
|
+
"variable expansion with ${VAR_NAME} syntax.", collectKeyValue, {})
|
|
53
|
+
.option("-H, --header <NAME=VALUE>", "Headers to use for remote (HTTP/SSE) MCP server connections, in the\n" +
|
|
54
|
+
"form Header-Name=Header-Value. Can be used multiple times. Supports\n" +
|
|
55
|
+
"environment variable expansion with ${VAR_NAME} syntax.", collectKeyValue, {})
|
|
56
|
+
.option("-t, --timeout <seconds>", "The timeout in seconds for connecting to the MCP server and making requests.", "10")
|
|
57
|
+
.addOption(new Option("-c, --compression-level <level>", "The level of compression to apply to the tool descriptions of the wrapped MCP server.")
|
|
58
|
+
.choices(["low", "medium", "high", "max"])
|
|
59
|
+
.default("medium"))
|
|
60
|
+
.option("-n, --server-name <name>", "Optional custom name to prefix the wrapper tool names (get_tool_schema,\n" +
|
|
61
|
+
"invoke_tool, list_tools). The name will be sanitized to conform to MCP\n" +
|
|
62
|
+
"tool name specifications (only A-Z, a-z, 0-9, _, -, .).")
|
|
63
|
+
.option("-l, --log-level <level>", "The logging level.", "error")
|
|
64
|
+
.option("--toonify", "Convert JSON tool responses to TOON format automatically.")
|
|
65
|
+
.option("--cli-mode", "Start in CLI mode: expose a single help MCP tool, start a local HTTP\n" +
|
|
66
|
+
"bridge, and generate a shell script for interacting with the wrapped\n" +
|
|
67
|
+
"server via CLI. --toonify is automatically enabled in this mode.")
|
|
68
|
+
.option("--cli-port <port>", "Port for the local CLI bridge HTTP server (default: random free port).")
|
|
69
|
+
.option("--just-bash", "Start in just-bash mode: expose a single 'bash' MCP tool powered by\n" +
|
|
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.")
|
|
73
|
+
.option("--include-tool <tool>", "Wrapped server tool name to expose. Can be used multiple times.\n" +
|
|
74
|
+
"If omitted, all tools are included.", collect, [])
|
|
75
|
+
.option("--exclude-tool <tool>", "Wrapped server tool name to hide. Can be used multiple times.", collect, []);
|
|
76
|
+
if (options.exitOverride) {
|
|
77
|
+
program.exitOverride();
|
|
78
|
+
}
|
|
79
|
+
return program;
|
|
80
|
+
}
|
|
81
|
+
function parseCliArgsWithOptions(argv, parseOptions = {}) {
|
|
82
|
+
const program = buildProgram(parseOptions);
|
|
83
|
+
program.parse(argv, { from: "user" });
|
|
84
|
+
const parsedOptions = program.opts();
|
|
85
|
+
const backend = parseBackendArg(program.args);
|
|
86
|
+
const justBash = parsedOptions.justBash ?? false;
|
|
87
|
+
const cliMode = parsedOptions.cliMode ?? false;
|
|
88
|
+
const toonify = (parsedOptions.toonify ?? false) || cliMode || justBash;
|
|
89
|
+
return {
|
|
90
|
+
backend,
|
|
91
|
+
justBash,
|
|
92
|
+
cliMode,
|
|
93
|
+
cliPort: parsedOptions.cliPort,
|
|
94
|
+
compressionLevel: parsedOptions.compressionLevel,
|
|
95
|
+
cwd: parsedOptions.cwd,
|
|
96
|
+
env: parsedOptions.env ?? {},
|
|
97
|
+
excludeTools: parsedOptions.excludeTool ?? [],
|
|
98
|
+
headers: parsedOptions.header ?? {},
|
|
99
|
+
includeTools: parsedOptions.includeTool ?? [],
|
|
100
|
+
logLevel: parsedOptions.logLevel,
|
|
101
|
+
serverName: parsedOptions.serverName,
|
|
102
|
+
timeout: Number.parseFloat(parsedOptions.timeout),
|
|
103
|
+
toonify,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export function parseCliArgs(argv) {
|
|
107
|
+
return parseCliArgsWithOptions(argv, { exitOverride: true });
|
|
108
|
+
}
|
|
109
|
+
function parseClearOAuthArgs(args) {
|
|
110
|
+
if (args[0] !== "clear-oauth") {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const clearProgram = new Command()
|
|
114
|
+
.name("mcp-compressor clear-oauth")
|
|
115
|
+
.exitOverride()
|
|
116
|
+
.allowExcessArguments(false)
|
|
117
|
+
.argument("[backend]", "backend URL or MCP config JSON string")
|
|
118
|
+
.option("--all", "also remove the encryption key");
|
|
119
|
+
clearProgram.parse(args.slice(1), { from: "user" });
|
|
120
|
+
const target = clearProgram.args[0];
|
|
121
|
+
const options = clearProgram.opts();
|
|
122
|
+
return { ...(target ? { target } : {}), all: options.all ?? false };
|
|
123
|
+
}
|
|
124
|
+
function candidateCoreBinaries() {
|
|
125
|
+
const candidates = [];
|
|
126
|
+
if (process.env.MCP_COMPRESSOR_BINARY) {
|
|
127
|
+
candidates.push(process.env.MCP_COMPRESSOR_BINARY);
|
|
128
|
+
}
|
|
129
|
+
candidates.push("mcp-compressor");
|
|
130
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
131
|
+
candidates.push(join(here, "..", "..", "target", "debug", process.platform === "win32" ? "mcp-compressor.exe" : "mcp-compressor"));
|
|
132
|
+
candidates.push(join(process.cwd(), "..", "target", "debug", process.platform === "win32" ? "mcp-compressor.exe" : "mcp-compressor"));
|
|
133
|
+
return candidates;
|
|
134
|
+
}
|
|
135
|
+
function translateArgsForRust(args) {
|
|
136
|
+
const clearOAuth = parseClearOAuthArgs(args);
|
|
137
|
+
if (clearOAuth) {
|
|
138
|
+
return clearOAuth.target ? ["clear-oauth", clearOAuth.target] : ["clear-oauth"];
|
|
139
|
+
}
|
|
140
|
+
return args;
|
|
141
|
+
}
|
|
142
|
+
async function runRustCoreCli(args) {
|
|
143
|
+
for (const binary of candidateCoreBinaries()) {
|
|
144
|
+
if (binary !== "mcp-compressor" && !existsSync(binary)) {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const child = spawn(binary, translateArgsForRust(args), { stdio: "inherit" });
|
|
148
|
+
return await new Promise((resolve, reject) => {
|
|
149
|
+
child.on("error", (error) => {
|
|
150
|
+
if (error.code === "ENOENT") {
|
|
151
|
+
resolve(127);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
reject(error);
|
|
155
|
+
});
|
|
156
|
+
child.on("exit", (code, signal) => {
|
|
157
|
+
if (signal) {
|
|
158
|
+
resolve(1);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
resolve(code ?? 0);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
console.error("mcp-compressor binary was not found. Build it with `cargo build -p mcp-compressor-core` or set MCP_COMPRESSOR_BINARY.");
|
|
166
|
+
return 127;
|
|
167
|
+
}
|
|
168
|
+
async function main() {
|
|
169
|
+
const exitCode = await runRustCoreCli(process.argv.slice(2));
|
|
170
|
+
process.exitCode = exitCode;
|
|
171
|
+
}
|
|
172
|
+
main().catch((error) => {
|
|
173
|
+
console.error(error instanceof Error ? (error.stack ?? error.message) : String(error));
|
|
174
|
+
process.exitCode = 1;
|
|
175
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BackendConfig, JsonConfigServerEntry, MCPConfigShape } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Interpolate environment variables in a single string using ${VAR_NAME} or $VAR_NAME syntax.
|
|
4
|
+
* If a referenced variable is not set, the placeholder is left as-is (matching Python behaviour).
|
|
5
|
+
*/
|
|
6
|
+
export declare function interpolateString(value: string): string;
|
|
7
|
+
export declare function interpolateRecord(record: Record<string, string> | undefined): Record<string, string> | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Interpolate environment variables in all string values of an MCP config object or JSON string.
|
|
10
|
+
*
|
|
11
|
+
* Accepts either a parsed `MCPConfigShape` object or a raw JSON string and returns an interpolated
|
|
12
|
+
* copy with `${VAR_NAME}` and `$VAR_NAME` placeholders replaced by their environment variable
|
|
13
|
+
* values. Unset variables are left as-is.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const config = interpolateMCPConfig({
|
|
18
|
+
* mcpServers: {
|
|
19
|
+
* myServer: { url: "https://example.com", headers: { Authorization: "Bearer $MY_TOKEN" } },
|
|
20
|
+
* },
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare function interpolateMCPConfig(config: MCPConfigShape | string): MCPConfigShape;
|
|
25
|
+
/**
|
|
26
|
+
* Parse an MCP config JSON string containing one or more servers.
|
|
27
|
+
*
|
|
28
|
+
* Returns an array of `{ backend, serverName }` entries — one per server in `mcpServers` — or
|
|
29
|
+
* `null` if the input is not a JSON object string. Throws {@link InvalidConfigurationError} for
|
|
30
|
+
* malformed JSON or an empty `mcpServers` map.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseServerConfigJson(input: string): Array<{
|
|
33
|
+
backend: BackendConfig;
|
|
34
|
+
serverName: string;
|
|
35
|
+
}> | null;
|
|
36
|
+
export declare function normalizeConfigServer(entry: JsonConfigServerEntry): BackendConfig;
|