337 lines
10 KiB
JavaScript
337 lines
10 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
|
|
*/
|
|
|
|
// Honk-managed: fleet verification v1
|
|
|
|
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 as keyof typeof PRESETS] || 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 as string;
|
|
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 as number) || preset.read.maxLines,
|
|
removeComments: !(args?.keepComments as boolean),
|
|
};
|
|
|
|
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 as string;
|
|
if (!pattern) {
|
|
return { content: [{ type: "text", text: "Error: pattern is required" }] };
|
|
}
|
|
|
|
const searchPath = (args?.path as string) || ".";
|
|
const glob = args?.glob as string;
|
|
|
|
// Build ripgrep command
|
|
let cmd = `rg -n "${pattern.replace(/"/g, '\\"')}"`;
|
|
if (glob) {
|
|
cmd += ` --glob "${glob}"`;
|
|
}
|
|
cmd += ` "${searchPath}" 2>/dev/null || true`;
|
|
|
|
let output: string;
|
|
try {
|
|
output = execSync(cmd, { encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 });
|
|
} catch {
|
|
output = "";
|
|
}
|
|
|
|
const options = {
|
|
...preset.grep,
|
|
maxMatchesPerFile: (args?.maxMatchesPerFile as number) || 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 as string;
|
|
if (!pattern) {
|
|
return { content: [{ type: "text", text: "Error: pattern is required" }] };
|
|
}
|
|
|
|
const basePath = (args?.path as string) || ".";
|
|
|
|
// Use fd with glob mode for proper ** support
|
|
let cmd: string;
|
|
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: string;
|
|
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 as number) || 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);
|