Files
Christian Gick cb8dd13cac build: Add telemetry compiled output
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-08 14:30:38 +02:00

297 lines
12 KiB
JavaScript

#!/usr/bin/env node
/**
* Tool Compression MCP Server
*
* Provides compressed versions of Read/Grep/Glob operations
* to reduce context token usage by 40-50%
*
* Tools:
* - compressed_read: Read file with comment/blank removal
* - compressed_grep: Search with grouped/deduped results
* - compressed_glob: File listing with collapsed directories
*/
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
import { readFileSync, existsSync, statSync } from "fs";
import { execSync } from "child_process";
import { compressRead } from "./compressors/read.js";
import { compressGrep } from "./compressors/grep.js";
import { compressGlob } from "./compressors/glob.js";
import { logTelemetry, calculateCompressionRatio } from "./telemetry.js";
// Configuration from environment
const COMPRESSION_LEVEL = process.env.COMPRESSION_LEVEL || "medium";
// Compression presets
const PRESETS = {
light: {
read: { removeBlankLines: true, removeComments: false, collapseImports: false, maxLines: 1000 },
grep: { maxMatchesPerFile: 5, maxTotalMatches: 50, dedupeAdjacent: false },
glob: { maxFiles: 50, collapseDepth: 6 },
},
medium: {
read: { removeBlankLines: true, removeComments: true, collapseImports: true, maxLines: 500 },
grep: { maxMatchesPerFile: 3, maxTotalMatches: 20, dedupeAdjacent: true },
glob: { maxFiles: 30, collapseDepth: 4 },
},
aggressive: {
read: { removeBlankLines: true, removeComments: true, collapseImports: true, maxLines: 200 },
grep: { maxMatchesPerFile: 2, maxTotalMatches: 10, dedupeAdjacent: true },
glob: { maxFiles: 15, collapseDepth: 3 },
},
};
const preset = PRESETS[COMPRESSION_LEVEL] || PRESETS.medium;
// Create MCP server
const server = new Server({
name: "tool-compression-mcp",
version: "1.0.0",
}, {
capabilities: {
tools: {},
},
});
// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "compressed_read",
description: "Read a file with compression: removes blank lines, comments, and collapses imports. " +
"Use this instead of Read for large files to save context tokens. " +
"Returns content with line numbers preserved for reference.",
inputSchema: {
type: "object",
properties: {
path: {
type: "string",
description: "Absolute path to the file to read",
},
maxLines: {
type: "number",
description: "Maximum lines to return (default: 500)",
},
keepComments: {
type: "boolean",
description: "Keep comment lines (default: false)",
},
},
required: ["path"],
},
},
{
name: "compressed_grep",
description: "Search files with compressed results: groups by file, shows top matches, dedupes adjacent. " +
"Use this instead of Grep when expecting many matches. " +
"Returns summary with match counts per file.",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description: "Regex pattern to search for",
},
path: {
type: "string",
description: "Directory or file to search in",
},
glob: {
type: "string",
description: "File glob pattern (e.g., '*.ts')",
},
maxMatchesPerFile: {
type: "number",
description: "Max matches to show per file (default: 3)",
},
},
required: ["pattern"],
},
},
{
name: "compressed_glob",
description: "List files with compression: collapses deep paths, groups by directory, shows file type distribution. " +
"Use this instead of Glob when expecting many files. " +
"Returns structured summary with counts.",
inputSchema: {
type: "object",
properties: {
pattern: {
type: "string",
description: "Glob pattern (e.g., '**/*.ts')",
},
path: {
type: "string",
description: "Base directory to search from",
},
maxFiles: {
type: "number",
description: "Max files to list (default: 30)",
},
},
required: ["pattern"],
},
},
],
};
});
// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case "compressed_read": {
const path = args?.path;
if (!path) {
return { content: [{ type: "text", text: "Error: path is required" }] };
}
if (!existsSync(path)) {
return { content: [{ type: "text", text: `Error: File not found: ${path}` }] };
}
const stats = statSync(path);
if (stats.isDirectory()) {
return { content: [{ type: "text", text: `Error: Path is a directory: ${path}` }] };
}
const content = readFileSync(path, "utf-8");
const options = {
...preset.read,
maxLines: args?.maxLines || preset.read.maxLines,
removeComments: !args?.keepComments,
};
const result = compressRead(content, path, options);
// Log telemetry
logTelemetry({
tool: "compressed_read",
inputSize: result.originalLines,
outputSize: result.compressedLines,
compressionRatio: calculateCompressionRatio(result.originalLines, result.compressedLines),
path,
success: true,
});
return {
content: [
{
type: "text",
text: `📄 ${path}\n` +
`[Compressed: ${result.originalLines}${result.compressedLines} lines (${result.savings} saved)]\n\n` +
result.content,
},
],
};
}
case "compressed_grep": {
const pattern = args?.pattern;
if (!pattern) {
return { content: [{ type: "text", text: "Error: pattern is required" }] };
}
const searchPath = args?.path || ".";
const glob = args?.glob;
// Build ripgrep command
let cmd = `rg -n "${pattern.replace(/"/g, '\\"')}"`;
if (glob) {
cmd += ` --glob "${glob}"`;
}
cmd += ` "${searchPath}" 2>/dev/null || true`;
let output;
try {
output = execSync(cmd, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
}
catch {
output = "";
}
const options = {
...preset.grep,
maxMatchesPerFile: args?.maxMatchesPerFile || preset.grep.maxMatchesPerFile,
};
const result = compressGrep(output, options);
// Log telemetry
logTelemetry({
tool: "compressed_grep",
inputSize: result.originalMatches,
outputSize: result.compressedMatches,
compressionRatio: calculateCompressionRatio(result.originalMatches, result.compressedMatches),
path: searchPath,
pattern,
success: true,
});
return {
content: [
{
type: "text",
text: `🔍 Search: "${pattern}"${glob ? ` (${glob})` : ""}\n` +
`[Found ${result.originalMatches} matches in ${result.filesMatched} files, showing ${result.compressedMatches} (${result.savings} compressed)]\n\n` +
result.content,
},
],
};
}
case "compressed_glob": {
const pattern = args?.pattern;
if (!pattern) {
return { content: [{ type: "text", text: "Error: pattern is required" }] };
}
const basePath = args?.path || ".";
// Use fd with glob mode for proper ** support
let cmd;
try {
// Try fd first with --glob flag for proper glob pattern support
execSync("which fd", { encoding: "utf-8" });
cmd = `fd --type f --glob "${pattern}" "${basePath}" 2>/dev/null || true`;
}
catch {
// Fall back to find - extract just filename pattern from glob
const simplePattern = pattern.replace(/^\*\*\//, '');
cmd = `find "${basePath}" -type f -name "${simplePattern}" 2>/dev/null || true`;
}
let output;
try {
output = execSync(cmd, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
}
catch {
output = "";
}
const paths = output.split("\n").filter((p) => p.trim());
const options = {
...preset.glob,
maxFiles: args?.maxFiles || preset.glob.maxFiles,
};
const result = compressGlob(paths, options);
// Log telemetry
logTelemetry({
tool: "compressed_glob",
inputSize: paths.length,
outputSize: result.compressedCount,
compressionRatio: calculateCompressionRatio(paths.length, result.compressedCount),
path: basePath,
pattern,
success: true,
});
return {
content: [
{
type: "text",
text: `📁 Glob: "${pattern}" in ${basePath}\n` +
`[${result.savings} compression]\n\n` +
result.content,
},
],
};
}
default:
return {
content: [{ type: "text", text: `Unknown tool: ${name}` }],
};
}
}
catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
};
}
});
// Start server
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Tool Compression MCP server started");
}
main().catch(console.error);