feat: Tool Compression MCP server for Phase 8
MCP server providing compressed versions of Read/Grep/Glob: - compressed_read: removes comments, blanks, collapses imports - compressed_grep: groups by file, dedupes adjacent matches - compressed_glob: collapses directories, shows type distribution Test results: 66.7% compression on sample file 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
265
dist/index.js
vendored
Normal file
265
dist/index.js
vendored
Normal file
@@ -0,0 +1,265 @@
|
||||
#!/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";
|
||||
// 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);
|
||||
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);
|
||||
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 find or fd for globbing
|
||||
let cmd;
|
||||
try {
|
||||
// Try fd first (faster)
|
||||
execSync("which fd", { encoding: "utf-8" });
|
||||
cmd = `fd --type f "${pattern}" "${basePath}" 2>/dev/null || true`;
|
||||
}
|
||||
catch {
|
||||
// Fall back to find
|
||||
cmd = `find "${basePath}" -type f -name "${pattern}" 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);
|
||||
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);
|
||||
Reference in New Issue
Block a user