squeezr-ai 1.17.5 → 1.17.6

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
@@ -174,7 +174,7 @@ Squeezr uses cheap/free models for AI compression (the deterministic layer is pu
174
174
  ## CLI commands
175
175
 
176
176
  ```bash
177
- squeezr setup # configure env vars, auto-start, CA trust
177
+ squeezr setup # configure env vars, auto-start, CA trust, install MCP server
178
178
  squeezr start # start the proxy (auto-restarts if version mismatch after update)
179
179
  squeezr update # kill old processes, install latest from npm, restart
180
180
  squeezr stop # stop the proxy
@@ -184,10 +184,28 @@ squeezr config # print current config
184
184
  squeezr ports # change HTTP and MITM proxy ports
185
185
  squeezr gain # estimate token savings for a directory
186
186
  squeezr discover # detect which AI CLIs are installed
187
+ squeezr mcp install # register MCP server in Claude Code, Cursor, Windsurf, Cline
188
+ squeezr mcp uninstall # remove MCP server registration
187
189
  squeezr uninstall # remove Squeezr completely (env vars, CA, auto-start, logs)
188
190
  squeezr version # print version
189
191
  ```
190
192
 
193
+ ## MCP server
194
+
195
+ Squeezr ships with a built-in MCP server (`squeezr-mcp`) that gives any MCP-capable AI CLI real-time awareness of Squeezr's state and control over it.
196
+
197
+ **Installed automatically** by `squeezr setup` into Claude Code, Cursor, Windsurf, and Cline.
198
+
199
+ Available MCP tools:
200
+
201
+ | Tool | Description |
202
+ |---|---|
203
+ | `squeezr_status` | Is proxy running? Version, port, uptime, mode |
204
+ | `squeezr_stats` | Token savings, compression %, cost saved, per-tool breakdown |
205
+ | `squeezr_set_mode` | Change compression mode instantly (soft / normal / aggressive / critical) |
206
+ | `squeezr_config` | Current thresholds, keepRecent, cache sizes |
207
+ | `squeezr_habits` | Detect wasteful patterns this session (duplicate reads, high Bash count, cache efficiency) |
208
+
191
209
  ## Requirements
192
210
 
193
211
  - Node.js 18+ (compatible with Node.js 24)
package/bin/squeezr.js CHANGED
@@ -199,10 +199,10 @@ Usage:
199
199
  squeezr discover Show pattern coverage report (proxy must be running)
200
200
  squeezr status Check if proxy is running
201
201
  squeezr config Print config file path and current settings
202
+ squeezr mcp install Register Squeezr MCP server in Claude Code, Cursor, Windsurf & Cline
203
+ squeezr mcp uninstall Remove Squeezr MCP registration
202
204
  squeezr ports Change HTTP and MITM proxy ports
203
205
  squeezr tunnel Expose proxy via Cloudflare Tunnel for Cursor IDE
204
- squeezr cursor Start Cursor subscription MITM proxy (no BYOK needed)
205
- squeezr cursor stop Stop Cursor proxy and clean up system proxy settings
206
206
  squeezr update Kill old processes, install latest from npm, restart
207
207
  squeezr uninstall Remove Squeezr completely (env vars, CA, auto-start, logs)
208
208
  squeezr version Print version
@@ -393,6 +393,100 @@ function showConfig() {
393
393
  }
394
394
  }
395
395
 
396
+
397
+ // ── squeezr mcp ───────────────────────────────────────────────────────────────
398
+
399
+ async function mcpInstall() {
400
+ const mcpServerPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'mcp.js')
401
+ const entry = {
402
+ type: 'stdio',
403
+ command: 'node',
404
+ args: [mcpServerPath],
405
+ }
406
+
407
+ const targets = [
408
+ {
409
+ name: 'Claude Code',
410
+ file: path.join(os.homedir(), '.claude.json'),
411
+ key: 'mcpServers',
412
+ },
413
+ {
414
+ name: 'Cursor',
415
+ file: path.join(os.homedir(), '.cursor', 'mcp.json'),
416
+ key: 'mcpServers',
417
+ },
418
+ {
419
+ name: 'Windsurf',
420
+ file: path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
421
+ key: 'mcpServers',
422
+ },
423
+ {
424
+ name: 'Cline / Roo-Cline',
425
+ file: path.join(os.homedir(), '.vscode', 'extensions', 'mcp_settings.json'),
426
+ key: 'mcpServers',
427
+ },
428
+ ]
429
+
430
+ let installed = 0
431
+
432
+ for (const target of targets) {
433
+ try {
434
+ // Only install into configs that already exist (user has that tool)
435
+ if (!fs.existsSync(target.file) && target.name !== 'Claude Code') continue
436
+
437
+ let cfg = {}
438
+ if (fs.existsSync(target.file)) {
439
+ try { cfg = JSON.parse(fs.readFileSync(target.file, 'utf-8')) } catch { cfg = {} }
440
+ }
441
+ cfg[target.key] = cfg[target.key] || {}
442
+ cfg[target.key].squeezr = entry
443
+
444
+ const dir = path.dirname(target.file)
445
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
446
+ fs.writeFileSync(target.file, JSON.stringify(cfg, null, 2))
447
+ console.log()
448
+ console.log(' ok ' + target.name + ': ' + target.file)
449
+ } catch (e) {
450
+ console.warn()
451
+ console.warn(' warn ' + target.name + ': ' + (e.message || e))
452
+ }
453
+
454
+ console.log()
455
+ console.log('MCP server registered in ' + installed + ' client(s).')
456
+ console.log('Server binary: ' + mcpServerPath)
457
+ console.log('')
458
+ console.log('Available tools in Claude/Codex/Cursor:')
459
+ console.log(' squeezr_status — Check if Squeezr is running')
460
+ console.log(' squeezr_stats — Real-time token savings')
461
+ console.log(' squeezr_set_mode — Change compression aggressiveness')
462
+ console.log(' squeezr_config — Current configuration')
463
+ console.log(' squeezr_habits — Wasteful pattern report')
464
+ }
465
+
466
+ async function mcpUninstall() {
467
+ const files = [
468
+ path.join(os.homedir(), '.claude.json'),
469
+ path.join(os.homedir(), '.cursor', 'mcp.json'),
470
+ path.join(os.homedir(), '.codeium', 'windsurf', 'mcp_config.json'),
471
+ path.join(os.homedir(), '.vscode', 'extensions', 'mcp_settings.json'),
472
+ ]
473
+ let removed = 0
474
+ for (const file of files) {
475
+ if (!fs.existsSync(file)) continue
476
+ try {
477
+ const cfg = JSON.parse(fs.readFileSync(file, 'utf-8'))
478
+ if (cfg.mcpServers?.squeezr) {
479
+ delete cfg.mcpServers.squeezr
480
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2))
481
+ console.log()
482
+ removed++
483
+ }
484
+ } catch { /* ignore */ }
485
+ }
486
+ if (removed === 0) console.log('Squeezr MCP not found in any config.')
487
+ else console.log()
488
+ }
489
+
396
490
  // ── squeezr ports ─────────────────────────────────────────────────────────────
397
491
 
398
492
  async function configurePorts() {
@@ -1256,162 +1350,6 @@ async function startTunnel() {
1256
1350
  process.on('SIGTERM', () => { child.kill(); process.exit(0) })
1257
1351
  }
1258
1352
 
1259
- // ── squeezr cursor ───────────────────────────────────────────────────────────
1260
-
1261
- async function startCursorProxy() {
1262
- const port = getPort()
1263
- const mitmPort = getMitmPort(port)
1264
-
1265
- // Verify main proxy is running first
1266
- const running = await new Promise(resolve => {
1267
- const req = http.get(`http://localhost:${port}/squeezr/health`, res => {
1268
- resolve(res.statusCode === 200)
1269
- })
1270
- req.on('error', () => resolve(false))
1271
- req.setTimeout(2000, () => { req.destroy(); resolve(false) })
1272
- })
1273
-
1274
- if (!running) {
1275
- console.error(`Squeezr proxy is not running on port ${port}.`)
1276
- console.error('Start it first: squeezr start')
1277
- process.exit(1)
1278
- }
1279
-
1280
- // Verify CA exists
1281
- const caDir = path.join(os.homedir(), '.squeezr', 'mitm-ca')
1282
- const caCertPath = path.join(caDir, 'ca.crt')
1283
- if (!fs.existsSync(caCertPath)) {
1284
- console.error('MITM CA certificate not found. Run `squeezr setup` first.')
1285
- process.exit(1)
1286
- }
1287
-
1288
- console.log('Starting Cursor MITM proxy...')
1289
-
1290
- const distPath = path.join(ROOT, 'dist', 'cursorMitm.js')
1291
- if (!fs.existsSync(distPath)) {
1292
- console.error(`Error: ${distPath} not found. Run 'npm run build' first.`)
1293
- process.exit(1)
1294
- }
1295
-
1296
- const distUrl = process.platform === 'win32' ? 'file:///' + distPath.replace(/\\/g, '/') : distPath
1297
- const { startCursorMitm, getCursorMitmPort, getCursorStats } = await import(distUrl)
1298
-
1299
- try {
1300
- await startCursorMitm()
1301
- } catch (err) {
1302
- console.error('Failed to start Cursor proxy:', err.message)
1303
- process.exit(1)
1304
- }
1305
-
1306
- const actualPort = getCursorMitmPort()
1307
- const proxyConfigured = configureSystemProxy(actualPort)
1308
-
1309
- console.log('')
1310
- console.log('╔══════════════════════════════════════════════════════════════════╗')
1311
- console.log(`║ Cursor MITM proxy active on port ${actualPort} ║`)
1312
- console.log('╠══════════════════════════════════════════════════════════════════╣')
1313
- console.log('║ ║')
1314
- console.log('║ Intercepting: api2.cursor.sh (chat, agent, composer) ║')
1315
- console.log('║ Compressing: conversation context via cursor-small ║')
1316
- console.log('║ Everything else: transparent pass-through ║')
1317
- console.log('║ ║')
1318
- if (proxyConfigured) {
1319
- console.log('║ System proxy configured automatically. ║')
1320
- console.log('║ Cursor will route through Squeezr on next request. ║')
1321
- } else {
1322
- console.log('║ ⚠ Could not set system proxy automatically. ║')
1323
- console.log('║ Set it manually: ║')
1324
- if (process.platform === 'win32') {
1325
- console.log(`║ Settings > Network > Proxy > Manual: 127.0.0.1:${actualPort} ║`)
1326
- } else if (process.platform === 'darwin') {
1327
- console.log(`║ networksetup -setsecurewebproxy Wi-Fi 127.0.0.1 ${actualPort} ║`)
1328
- } else {
1329
- console.log(`║ export HTTPS_PROXY=http://127.0.0.1:${actualPort} ║`)
1330
- }
1331
- }
1332
- console.log('║ ║')
1333
- console.log('║ Press Ctrl+C to stop and clean up proxy settings ║')
1334
- console.log('╚══════════════════════════════════════════════════════════════════╝')
1335
- console.log('')
1336
-
1337
- const statsInterval = setInterval(() => {
1338
- const s = getCursorStats()
1339
- if (s.requests > 0) {
1340
- console.log(`[squeezr/cursor] Stats: ${s.requests} requests, ${s.compressed} compressed, -${s.charsSaved.toLocaleString()} chars saved`)
1341
- }
1342
- }, 30_000)
1343
-
1344
- const cleanup = () => {
1345
- clearInterval(statsInterval)
1346
- console.log('\nStopping Cursor proxy...')
1347
- cleanSystemProxy()
1348
- const s = getCursorStats()
1349
- console.log(`Session: ${s.requests} requests, ${s.compressed} compressed, -${s.charsSaved.toLocaleString()} chars saved`)
1350
- process.exit(0)
1351
- }
1352
-
1353
- process.on('SIGINT', cleanup)
1354
- process.on('SIGTERM', cleanup)
1355
- }
1356
-
1357
- function configureSystemProxy(port) {
1358
- try {
1359
- if (process.platform === 'win32') {
1360
- execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 1 /f`, { stdio: 'pipe' })
1361
- execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyServer /t REG_SZ /d "127.0.0.1:${port}" /f`, { stdio: 'pipe' })
1362
- execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyOverride /t REG_SZ /d "<local>;localhost;127.0.0.1" /f`, { stdio: 'pipe' })
1363
- return true
1364
- } else if (process.platform === 'darwin') {
1365
- try {
1366
- const services = execSync('networksetup -listallnetworkservices', { encoding: 'utf-8' })
1367
- .split('\n')
1368
- .filter(s => s.trim() && !s.startsWith('*') && !s.startsWith('An asterisk'))
1369
- for (const svc of services) {
1370
- try {
1371
- execSync(`networksetup -setsecurewebproxy "${svc.trim()}" 127.0.0.1 ${port}`, { stdio: 'pipe' })
1372
- } catch {}
1373
- }
1374
- return true
1375
- } catch { return false }
1376
- } else {
1377
- try {
1378
- execSync(`gsettings set org.gnome.system.proxy mode 'manual'`, { stdio: 'pipe' })
1379
- execSync(`gsettings set org.gnome.system.proxy.https host '127.0.0.1'`, { stdio: 'pipe' })
1380
- execSync(`gsettings set org.gnome.system.proxy.https port ${port}`, { stdio: 'pipe' })
1381
- return true
1382
- } catch { return false }
1383
- }
1384
- } catch {
1385
- return false
1386
- }
1387
- }
1388
-
1389
- function cleanSystemProxy() {
1390
- try {
1391
- if (process.platform === 'win32') {
1392
- execSync(`reg add "HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings" /v ProxyEnable /t REG_DWORD /d 0 /f`, { stdio: 'pipe' })
1393
- } else if (process.platform === 'darwin') {
1394
- try {
1395
- const services = execSync('networksetup -listallnetworkservices', { encoding: 'utf-8' })
1396
- .split('\n')
1397
- .filter(s => s.trim() && !s.startsWith('*') && !s.startsWith('An asterisk'))
1398
- for (const svc of services) {
1399
- try {
1400
- execSync(`networksetup -setsecurewebproxystate "${svc.trim()}" off`, { stdio: 'pipe' })
1401
- } catch {}
1402
- }
1403
- } catch {}
1404
- } else {
1405
- try {
1406
- execSync(`gsettings set org.gnome.system.proxy mode 'none'`, { stdio: 'pipe' })
1407
- } catch {}
1408
- }
1409
- console.log('System proxy settings cleaned up.')
1410
- } catch {
1411
- console.warn('Could not clean system proxy settings. You may need to disable manually.')
1412
- }
1413
- }
1414
-
1415
1353
  // ── CLI router ────────────────────────────────────────────────────────────────
1416
1354
 
1417
1355
  switch (command) {
@@ -1529,14 +1467,6 @@ switch (command) {
1529
1467
  await startTunnel()
1530
1468
  break
1531
1469
 
1532
- case 'cursor':
1533
- if (args[1] === 'stop') {
1534
- cleanSystemProxy()
1535
- } else {
1536
- await startCursorProxy()
1537
- }
1538
- break
1539
-
1540
1470
  case 'uninstall':
1541
1471
  await uninstall()
1542
1472
  break
@@ -1544,6 +1474,12 @@ switch (command) {
1544
1474
  showConfig()
1545
1475
  break
1546
1476
 
1477
+ case 'mcp': {
1478
+ const subCmd = args[0] ?? 'install'
1479
+ if (subCmd === 'uninstall') await mcpUninstall()
1480
+ else await mcpInstall()
1481
+ break
1482
+ }
1547
1483
  case 'version':
1548
1484
  case '--version':
1549
1485
  case '-v':
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Squeezr MCP Server
4
+ *
5
+ * Gives any MCP-compatible AI CLI (Claude Code, Cursor, Windsurf, Cline…)
6
+ * real-time awareness of Squeezr's state and control over it.
7
+ *
8
+ * Transport: stdio (universal — works with all MCP clients)
9
+ * Queries the Squeezr proxy via HTTP on localhost.
10
+ *
11
+ * Tools exposed:
12
+ * squeezr_status — Is proxy running? Port, version, uptime
13
+ * squeezr_stats — Token savings, compression %, by-tool breakdown
14
+ * squeezr_set_mode — Change compression aggressiveness instantly
15
+ * squeezr_config — Current thresholds and settings
16
+ * squeezr_habits — Detected wasteful patterns this session
17
+ */
18
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Squeezr MCP Server
4
+ *
5
+ * Gives any MCP-compatible AI CLI (Claude Code, Cursor, Windsurf, Cline…)
6
+ * real-time awareness of Squeezr's state and control over it.
7
+ *
8
+ * Transport: stdio (universal — works with all MCP clients)
9
+ * Queries the Squeezr proxy via HTTP on localhost.
10
+ *
11
+ * Tools exposed:
12
+ * squeezr_status — Is proxy running? Port, version, uptime
13
+ * squeezr_stats — Token savings, compression %, by-tool breakdown
14
+ * squeezr_set_mode — Change compression aggressiveness instantly
15
+ * squeezr_config — Current thresholds and settings
16
+ * squeezr_habits — Detected wasteful patterns this session
17
+ */
18
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
19
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
20
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
21
+ import { z } from 'zod';
22
+ import { readFileSync, existsSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { fileURLToPath } from 'node:url';
25
+ import { dirname } from 'node:path';
26
+ import { createRequire } from 'node:module';
27
+ const __filename = fileURLToPath(import.meta.url);
28
+ const __dirname = dirname(__filename);
29
+ const require = createRequire(import.meta.url);
30
+ // ── Resolve proxy port from squeezr.toml ─────────────────────────────────────
31
+ function getProxyPort() {
32
+ try {
33
+ const tomlPath = join(__dirname, '..', 'squeezr.toml');
34
+ if (existsSync(tomlPath)) {
35
+ const toml = readFileSync(tomlPath, 'utf-8');
36
+ const m = toml.match(/^\s*port\s*=\s*(\d+)/m);
37
+ if (m)
38
+ return parseInt(m[1]);
39
+ }
40
+ }
41
+ catch { /* ignore */ }
42
+ return parseInt(process.env.SQUEEZR_PORT ?? '8080');
43
+ }
44
+ const BASE_URL = process.env.SQUEEZR_URL ?? `http://localhost:${getProxyPort()}`;
45
+ // ── HTTP helpers ──────────────────────────────────────────────────────────────
46
+ async function proxyGet(path) {
47
+ try {
48
+ const res = await fetch(`${BASE_URL}${path}`, { signal: AbortSignal.timeout(3000) });
49
+ if (!res.ok)
50
+ return null;
51
+ return await res.json();
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ async function proxyPost(path, body) {
58
+ try {
59
+ const res = await fetch(`${BASE_URL}${path}`, {
60
+ method: 'POST',
61
+ headers: { 'content-type': 'application/json' },
62
+ body: JSON.stringify(body),
63
+ signal: AbortSignal.timeout(3000),
64
+ });
65
+ if (!res.ok)
66
+ return null;
67
+ return await res.json();
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ }
73
+ function fmtNum(n) {
74
+ if (n >= 1_000_000)
75
+ return (n / 1_000_000).toFixed(1) + 'M';
76
+ if (n >= 1_000)
77
+ return (n / 1_000).toFixed(1) + 'K';
78
+ return String(n);
79
+ }
80
+ function fmtUptime(s) {
81
+ if (s < 60)
82
+ return `${s}s`;
83
+ if (s < 3600)
84
+ return `${Math.floor(s / 60)}m ${s % 60}s`;
85
+ return `${Math.floor(s / 3600)}h ${Math.floor((s % 3600) / 60)}m`;
86
+ }
87
+ // ── MCP Server setup ──────────────────────────────────────────────────────────
88
+ const pkg = require('../package.json');
89
+ const server = new Server({ name: 'squeezr', version: pkg.version }, { capabilities: { tools: {} } });
90
+ // ── Tool definitions ──────────────────────────────────────────────────────────
91
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
92
+ tools: [
93
+ {
94
+ name: 'squeezr_status',
95
+ description: 'Check if the Squeezr proxy is running. Returns version, port, uptime, ' +
96
+ 'compression mode, and whether dry-run is active. ' +
97
+ 'Call this first to confirm Squeezr is active before querying other tools.',
98
+ inputSchema: { type: 'object', properties: {}, required: [] },
99
+ },
100
+ {
101
+ name: 'squeezr_stats',
102
+ description: 'Get real-time token compression statistics for the current Squeezr session. ' +
103
+ 'Returns: tokens saved, chars saved, compression %, total requests, ' +
104
+ 'session cache hits, cost saved estimate, and per-tool breakdown. ' +
105
+ 'Use this to understand how much context Squeezr is saving right now.',
106
+ inputSchema: { type: 'object', properties: {}, required: [] },
107
+ },
108
+ {
109
+ name: 'squeezr_set_mode',
110
+ description: 'Change Squeezr compression aggressiveness instantly without restarting. ' +
111
+ 'Takes effect on the next request. Use "aggressive" or "critical" when ' +
112
+ 'approaching context limits. Use "soft" when you need full fidelity on outputs.',
113
+ inputSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ mode: {
117
+ type: 'string',
118
+ enum: ['soft', 'normal', 'aggressive', 'critical'],
119
+ description: 'soft: minimal compression, last 10 results kept, no AI compression. ' +
120
+ 'normal: default (threshold 800 chars, last 3 kept). ' +
121
+ 'aggressive: threshold 200 chars, last 1 kept, AI compression on. ' +
122
+ 'critical: threshold 50 chars, everything compressed, max savings.',
123
+ },
124
+ },
125
+ required: ['mode'],
126
+ },
127
+ },
128
+ {
129
+ name: 'squeezr_config',
130
+ description: 'Get the current Squeezr configuration: active compression mode, ' +
131
+ 'threshold values, keepRecent setting, which tools are AI-skipped, ' +
132
+ 'and whether AI compression is currently enabled.',
133
+ inputSchema: { type: 'object', properties: {}, required: [] },
134
+ },
135
+ {
136
+ name: 'squeezr_habits',
137
+ description: 'Get a report of detected token-wasteful patterns in the current session. ' +
138
+ 'Shows which deterministic patterns fired (duplicate reads, lock files, ' +
139
+ 'repeated errors, large outputs) and how many tokens they saved or wasted. ' +
140
+ 'Useful for improving how you use Claude to spend fewer tokens.',
141
+ inputSchema: { type: 'object', properties: {}, required: [] },
142
+ },
143
+ ],
144
+ }));
145
+ // ── Tool handlers ─────────────────────────────────────────────────────────────
146
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
147
+ const { name, arguments: args } = req.params;
148
+ // ── squeezr_status ──────────────────────────────────────────────────────────
149
+ if (name === 'squeezr_status') {
150
+ const health = await proxyGet('/squeezr/health');
151
+ const stats = await proxyGet('/squeezr/stats');
152
+ if (!health) {
153
+ return {
154
+ content: [{
155
+ type: 'text',
156
+ text: [
157
+ '❌ Squeezr proxy is NOT running.',
158
+ '',
159
+ `Expected at: ${BASE_URL}`,
160
+ 'Start it with: squeezr start',
161
+ '',
162
+ 'Without Squeezr, your tool results are sent to the API uncompressed.',
163
+ ].join('\n'),
164
+ }],
165
+ isError: false,
166
+ };
167
+ }
168
+ const uptime = stats ? fmtUptime(stats.uptime_seconds ?? 0) : '?';
169
+ const mode = stats ? (stats.mode ?? 'normal') : 'normal';
170
+ const dryRun = stats ? stats.dry_run : false;
171
+ return {
172
+ content: [{
173
+ type: 'text',
174
+ text: [
175
+ `✅ Squeezr v${health.version ?? '?'} is running`,
176
+ ` Port : ${BASE_URL}`,
177
+ ` Uptime : ${uptime}`,
178
+ ` Mode : ${mode}${dryRun ? ' (dry-run)' : ''}`,
179
+ ` Dashboard: ${BASE_URL}/squeezr/dashboard`,
180
+ ].join('\n'),
181
+ }],
182
+ };
183
+ }
184
+ // ── squeezr_stats ───────────────────────────────────────────────────────────
185
+ if (name === 'squeezr_stats') {
186
+ const data = await proxyGet('/squeezr/stats');
187
+ if (!data) {
188
+ return {
189
+ content: [{ type: 'text', text: '❌ Squeezr proxy is not running. Start with: squeezr start' }],
190
+ isError: false,
191
+ };
192
+ }
193
+ const savedTokens = data.total_saved_tokens ?? 0;
194
+ const savedChars = data.total_saved_chars ?? 0;
195
+ const pct = data.savings_pct ?? 0;
196
+ const requests = data.requests ?? 0;
197
+ const compressions = data.compressions ?? 0;
198
+ const cacheHits = data.session_cache_hits ?? 0;
199
+ const uptime = fmtUptime(data.uptime_seconds ?? 0);
200
+ const costUsd = (savedTokens / 1_000_000) * 3;
201
+ const byTool = data.by_tool ?? {};
202
+ const sessionCache = data.session_cache_size ?? 0;
203
+ const patternHits = data.pattern_hits ?? {};
204
+ const totalPatterns = Object.values(patternHits).reduce((s, v) => s + v, 0);
205
+ const toolLines = Object.entries(byTool)
206
+ .sort((a, b) => b[1].saved_tokens - a[1].saved_tokens)
207
+ .slice(0, 8)
208
+ .map(([tool, t]) => ` ${tool.padEnd(16)} ${fmtNum(t.saved_tokens).padStart(7)} tokens ${t.avg_pct}% avg ×${t.count}`);
209
+ return {
210
+ content: [{
211
+ type: 'text',
212
+ text: [
213
+ '📊 Squeezr Session Stats',
214
+ '─'.repeat(40),
215
+ `Tokens saved : ${fmtNum(savedTokens)} (~${savedChars.toLocaleString()} chars)`,
216
+ `Compression : ${pct}% of tool results`,
217
+ `Cost saved : $${costUsd.toFixed(3)} (@ $3/MTok)`,
218
+ `Requests : ${requests} (${compressions} compressions)`,
219
+ `Session cache : ${cacheHits} hits (${sessionCache} entries)`,
220
+ `Pattern hits : ${totalPatterns.toLocaleString()}`,
221
+ `Uptime : ${uptime}`,
222
+ '',
223
+ toolLines.length > 0 ? 'By tool:\n' + toolLines.join('\n') : 'No tool results compressed yet.',
224
+ ].join('\n'),
225
+ }],
226
+ };
227
+ }
228
+ // ── squeezr_set_mode ────────────────────────────────────────────────────────
229
+ if (name === 'squeezr_set_mode') {
230
+ const parsed = z.object({
231
+ mode: z.enum(['soft', 'normal', 'aggressive', 'critical']),
232
+ }).parse(args);
233
+ const modeInfo = {
234
+ soft: 'threshold=3000 chars, last 10 results uncompressed, AI off',
235
+ normal: 'threshold=800 chars, last 3 results uncompressed, AI on new blocks',
236
+ aggressive: 'threshold=200 chars, last 1 result uncompressed, AI on',
237
+ critical: 'threshold=50 chars, all results compressed, maximum savings',
238
+ };
239
+ const result = await proxyPost('/squeezr/config', { mode: parsed.mode });
240
+ if (!result) {
241
+ return {
242
+ content: [{ type: 'text', text: '❌ Squeezr proxy is not running. Start with: squeezr start' }],
243
+ isError: false,
244
+ };
245
+ }
246
+ return {
247
+ content: [{
248
+ type: 'text',
249
+ text: [
250
+ `✅ Compression mode set to: ${parsed.mode}`,
251
+ ` ${modeInfo[parsed.mode]}`,
252
+ '',
253
+ parsed.mode === 'critical'
254
+ ? '⚠️ Critical mode compresses everything aggressively. Switch back to normal when context pressure drops.'
255
+ : parsed.mode === 'aggressive'
256
+ ? '🔥 Aggressive mode active. Use when context is above 70%.'
257
+ : '',
258
+ ].filter(Boolean).join('\n'),
259
+ }],
260
+ };
261
+ }
262
+ // ── squeezr_config ──────────────────────────────────────────────────────────
263
+ if (name === 'squeezr_config') {
264
+ const data = await proxyGet('/squeezr/stats');
265
+ if (!data) {
266
+ return {
267
+ content: [{ type: 'text', text: '❌ Squeezr proxy is not running. Start with: squeezr start' }],
268
+ isError: false,
269
+ };
270
+ }
271
+ const mode = data.mode ?? 'normal';
272
+ const dryRun = data.dry_run ?? false;
273
+ const cacheStats = data.cache;
274
+ return {
275
+ content: [{
276
+ type: 'text',
277
+ text: [
278
+ '⚙️ Squeezr Configuration',
279
+ '─'.repeat(40),
280
+ `Compression mode : ${mode}`,
281
+ `Dry-run : ${dryRun ? 'yes (no actual compression)' : 'no'}`,
282
+ `LRU cache : ${cacheStats?.size ?? '?'} entries | ${cacheStats?.hit_rate_pct ?? '?'}% hit rate`,
283
+ `Session cache : ${data.session_cache_size ?? '?'} entries`,
284
+ `Expand store : ${data.expand_store_size ?? '?'} entries`,
285
+ '',
286
+ 'Modes available:',
287
+ ' soft — minimal, no AI compression',
288
+ ' normal — default balanced',
289
+ ' aggressive — max useful compression',
290
+ ' critical — compress everything',
291
+ '',
292
+ `Change with: squeezr_set_mode({ mode: "aggressive" })`,
293
+ ].join('\n'),
294
+ }],
295
+ };
296
+ }
297
+ // ── squeezr_habits ──────────────────────────────────────────────────────────
298
+ if (name === 'squeezr_habits') {
299
+ const data = await proxyGet('/squeezr/stats');
300
+ if (!data) {
301
+ return {
302
+ content: [{ type: 'text', text: '❌ Squeezr proxy is not running. Start with: squeezr start' }],
303
+ isError: false,
304
+ };
305
+ }
306
+ const patternHits = data.pattern_hits ?? {};
307
+ const byTool = data.by_tool ?? {};
308
+ const habits = [];
309
+ // Detect lock file reads
310
+ const readTool = byTool['Read'] ?? byTool['read'];
311
+ if (readTool && readTool.count > 5) {
312
+ habits.push({
313
+ level: readTool.count > 20 ? '🔴' : '🟡',
314
+ msg: `Read tool called ${readTool.count}× this session. Check for lock file reads or repeated file reads.`,
315
+ });
316
+ }
317
+ // Detect read-dedup pattern hits (same file read multiple times)
318
+ if ((patternHits['readDedup'] ?? 0) > 0) {
319
+ habits.push({
320
+ level: '🟡',
321
+ msg: `${patternHits['readDedup']} duplicate file read(s) detected and collapsed. Consider using @file to pin frequently needed files.`,
322
+ });
323
+ }
324
+ // Detect high bash call count
325
+ const bashTool = byTool['Bash'] ?? byTool['bash'];
326
+ if (bashTool && bashTool.count > 30) {
327
+ habits.push({
328
+ level: '🟡',
329
+ msg: `Bash called ${bashTool.count}× — if repeatedly running the same command, consider using --watch mode.`,
330
+ });
331
+ }
332
+ // Detect low compression on Read (code quality concern)
333
+ if (readTool && readTool.avg_pct < 10 && readTool.count > 3) {
334
+ habits.push({
335
+ level: '🟢',
336
+ msg: `Read tool results have ${readTool.avg_pct}% compression (code files protected from AI summarization — this is correct).`,
337
+ });
338
+ }
339
+ // Session cache effectiveness
340
+ const cacheHits = data.session_cache_hits ?? 0;
341
+ const compressions = data.compressions ?? 0;
342
+ if (compressions > 0) {
343
+ const hitRate = Math.round((cacheHits / (cacheHits + compressions)) * 100);
344
+ if (hitRate > 50) {
345
+ habits.push({
346
+ level: '🟢',
347
+ msg: `Session cache ${hitRate}% hit rate — repeated tool results are being reused efficiently.`,
348
+ });
349
+ }
350
+ }
351
+ const totalSaved = data.total_saved_tokens ?? 0;
352
+ const requests = data.requests ?? 0;
353
+ if (habits.length === 0) {
354
+ habits.push({ level: '🟢', msg: 'No significant wasteful patterns detected this session.' });
355
+ }
356
+ return {
357
+ content: [{
358
+ type: 'text',
359
+ text: [
360
+ '🔍 Squeezr Habit Report (current session)',
361
+ '─'.repeat(40),
362
+ `Session: ${requests} requests | ${fmtNum(totalSaved)} tokens saved`,
363
+ '',
364
+ ...habits.map(h => `${h.level} ${h.msg}`),
365
+ '',
366
+ 'Tip: use squeezr_set_mode to control compression aggressiveness.',
367
+ ].join('\n'),
368
+ }],
369
+ };
370
+ }
371
+ return {
372
+ content: [{ type: 'text', text: `Unknown tool: ${name}` }],
373
+ isError: true,
374
+ };
375
+ });
376
+ // ── Start server ──────────────────────────────────────────────────────────────
377
+ const transport = new StdioServerTransport();
378
+ await server.connect(transport);
379
+ // stderr only — stdout is reserved for MCP protocol
380
+ process.stderr.write(`[squeezr-mcp] Squeezr MCP server v${pkg.version} ready (proxy: ${BASE_URL})\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squeezr-ai",
3
- "version": "1.17.5",
3
+ "version": "1.17.6",
4
4
  "description": "AI proxy that compresses Claude Code, Codex, Aider, Gemini CLI and Ollama context windows to save thousands of tokens per session",
5
5
  "keywords": [
6
6
  "claude",
@@ -23,7 +23,8 @@
23
23
  "homepage": "https://github.com/sergioramosv/Squeezr#readme",
24
24
  "type": "module",
25
25
  "bin": {
26
- "squeezr": "bin/squeezr.js"
26
+ "squeezr": "bin/squeezr.js",
27
+ "squeezr-mcp": "dist/mcp.js"
27
28
  },
28
29
  "scripts": {
29
30
  "build": "tsc",
@@ -43,10 +44,12 @@
43
44
  "@anthropic-ai/sdk": "^0.39.0",
44
45
  "@bufbuild/protobuf": "^2.11.0",
45
46
  "@hono/node-server": "^1.13.7",
47
+ "@modelcontextprotocol/sdk": "^1.29.0",
46
48
  "hono": "^4.7.5",
47
49
  "node-forge": "^1.4.0",
48
50
  "openai": "^4.93.0",
49
- "smol-toml": "^1.3.1"
51
+ "smol-toml": "^1.3.1",
52
+ "zod": "^3.24.0"
50
53
  },
51
54
  "devDependencies": {
52
55
  "@types/node": "^22.14.0",