mcp-multitool 0.1.12 → 0.1.14
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 +13 -9
- package/dist/tools/astGrepSearch.js +33 -1
- package/dist/tools/readLogFile.js +59 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -75,6 +75,8 @@ Search code using AST patterns. Matches code structure, not text. Use `$VAR` for
|
|
|
75
75
|
|
|
76
76
|
**Response:** JSON object with `results` array (each with `file`, `range`, `text`) and `errors` array.
|
|
77
77
|
|
|
78
|
+
**Pattern Validation:** Patterns are validated before search. Invalid patterns (e.g., `class X` without a body, `function foo` without parentheses) return an error explaining the issue. Patterns must be syntactically complete code fragments.
|
|
79
|
+
|
|
78
80
|
**Examples:**
|
|
79
81
|
|
|
80
82
|
```
|
|
@@ -176,16 +178,17 @@ Compress a log file using semantic pattern extraction. Groups similar lines into
|
|
|
176
178
|
|
|
177
179
|
**Content-hashed template IDs:** Template IDs are 12-character base64URL hashes derived from the pattern itself. The same pattern **always** gets the same ID, regardless of file order or when you call the tool. This means drill-down always works if the pattern still exists.
|
|
178
180
|
|
|
179
|
-
| Parameter | Type | Required | Description
|
|
180
|
-
| -------------- | --------- | -------- |
|
|
181
|
-
| `path` | `string` | ✅ | Path to the log file.
|
|
182
|
-
| `simThreshold` | `number` | ✅ | Similarity threshold (0-1). Lower values group more aggressively.
|
|
183
|
-
| `tail` | `integer` | — | Last N lines.
|
|
184
|
-
| `head` | `integer` | — | First N lines.
|
|
185
|
-
| `grep` | `string` | — | Regex filter for lines
|
|
186
|
-
| `
|
|
181
|
+
| Parameter | Type | Required | Description |
|
|
182
|
+
| -------------- | --------- | -------- | ----------------------------------------------------------------------------- |
|
|
183
|
+
| `path` | `string` | ✅ | Path to the log file. |
|
|
184
|
+
| `simThreshold` | `number` | ✅ | Similarity threshold (0-1). Lower values group more aggressively. |
|
|
185
|
+
| `tail` | `integer` | — | Last N lines. |
|
|
186
|
+
| `head` | `integer` | — | First N lines. |
|
|
187
|
+
| `grep` | `string` | — | Regex filter for lines. Smart case: all-lowercase = case-insensitive. |
|
|
188
|
+
| `lineStart` | `string` | — | Regex for entry start lines. Non-matching lines join previous entry with `⏎`. |
|
|
189
|
+
| `templateId` | `string` | — | Drill into a specific template by its hash ID for sample captures. |
|
|
187
190
|
|
|
188
|
-
**Response:** Compressed log summary showing template IDs, occurrence counts, and patterns with `<*>` wildcards.
|
|
191
|
+
**Response:** Compressed log summary with header showing line reduction, character reduction, template IDs, occurrence counts, and patterns with `<*>` wildcards.
|
|
189
192
|
|
|
190
193
|
**Examples:**
|
|
191
194
|
|
|
@@ -194,6 +197,7 @@ readLogFile path="/var/log/app.log" simThreshold=0.4
|
|
|
194
197
|
readLogFile path="./logs/server.log" simThreshold=0.4 tail=1000
|
|
195
198
|
readLogFile path="app.log" simThreshold=0.3 grep="ERROR|WARN"
|
|
196
199
|
readLogFile path="app.log" simThreshold=0.4 templateId="aB3x_Yz7Q2Kf"
|
|
200
|
+
readLogFile path="tsserver.log" simThreshold=0.5 lineStart="^(Info|Err|Perf)\\s+\\d+"
|
|
197
201
|
```
|
|
198
202
|
|
|
199
203
|
<details>
|
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { findInFiles } from "@ast-grep/napi";
|
|
2
|
+
import { findInFiles, parse } from "@ast-grep/napi";
|
|
3
|
+
function validatePattern(lang, pattern) {
|
|
4
|
+
const tree = parse(lang, pattern);
|
|
5
|
+
const root = tree.root();
|
|
6
|
+
function findErrors(node) {
|
|
7
|
+
const errors = [];
|
|
8
|
+
if (node.kind() === "ERROR") {
|
|
9
|
+
errors.push(`"${node.text().slice(0, 50)}"`);
|
|
10
|
+
}
|
|
11
|
+
for (const child of node.children()) {
|
|
12
|
+
errors.push(...findErrors(child));
|
|
13
|
+
}
|
|
14
|
+
return errors;
|
|
15
|
+
}
|
|
16
|
+
const errors = findErrors(root);
|
|
17
|
+
if (errors.length > 0) {
|
|
18
|
+
return {
|
|
19
|
+
valid: false,
|
|
20
|
+
error: `Pattern "${pattern}" is not valid ${lang} syntax.\n` +
|
|
21
|
+
`Invalid syntax near: ${errors.join(", ")}\n\n` +
|
|
22
|
+
`Tip: Patterns must be syntactically complete code fragments. ` +
|
|
23
|
+
`For example, "class X" is invalid (needs body), use "class X { $$$BODY }" instead.`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { valid: true };
|
|
27
|
+
}
|
|
3
28
|
const builtinLangs = [
|
|
4
29
|
"javascript",
|
|
5
30
|
"typescript",
|
|
@@ -38,6 +63,13 @@ export function register(server) {
|
|
|
38
63
|
try {
|
|
39
64
|
const lang = langMap[input.lang];
|
|
40
65
|
const paths = Array.isArray(input.paths) ? input.paths : [input.paths];
|
|
66
|
+
const validation = validatePattern(lang, input.pattern);
|
|
67
|
+
if (!validation.valid) {
|
|
68
|
+
return {
|
|
69
|
+
isError: true,
|
|
70
|
+
content: [{ type: "text", text: validation.error }],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
41
73
|
const results = [];
|
|
42
74
|
const errors = [];
|
|
43
75
|
await findInFiles(lang, {
|
|
@@ -174,6 +174,24 @@ function compress(lines, simThreshold) {
|
|
|
174
174
|
function hashTemplateId(pattern) {
|
|
175
175
|
return createHash("sha256").update(pattern).digest("base64url").slice(0, 12);
|
|
176
176
|
}
|
|
177
|
+
const CONTINUATION_MARKER = " \u23CE ";
|
|
178
|
+
function joinMultilineEntries(lines, lineStartPattern) {
|
|
179
|
+
const result = [];
|
|
180
|
+
let current = "";
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
if (lineStartPattern.test(line)) {
|
|
183
|
+
if (current)
|
|
184
|
+
result.push(current);
|
|
185
|
+
current = line;
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
current += CONTINUATION_MARKER + line;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (current)
|
|
192
|
+
result.push(current);
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
177
195
|
const schema = z.object({
|
|
178
196
|
path: z.string().min(1).describe("Path to the log file."),
|
|
179
197
|
simThreshold: z
|
|
@@ -183,7 +201,14 @@ const schema = z.object({
|
|
|
183
201
|
.describe("Similarity threshold (0-1). Lower = more aggressive grouping."),
|
|
184
202
|
tail: z.number().int().min(1).optional().describe("Last N lines."),
|
|
185
203
|
head: z.number().int().min(1).optional().describe("First N lines."),
|
|
186
|
-
grep: z
|
|
204
|
+
grep: z
|
|
205
|
+
.string()
|
|
206
|
+
.optional()
|
|
207
|
+
.describe("Regex filter for lines. Smart case: all-lowercase pattern = case-insensitive, any uppercase = exact case."),
|
|
208
|
+
lineStart: z
|
|
209
|
+
.string()
|
|
210
|
+
.optional()
|
|
211
|
+
.describe("Regex identifying log entry start lines. Lines not matching are joined to previous entry with \u23CE. For tsserver: ^(Info|Err|Perf)\\s+\\d+"),
|
|
187
212
|
templateId: z
|
|
188
213
|
.string()
|
|
189
214
|
.optional()
|
|
@@ -219,10 +244,31 @@ async function processLog(input) {
|
|
|
219
244
|
lines = lines.slice(0, input.head);
|
|
220
245
|
else if (input.tail)
|
|
221
246
|
lines = lines.slice(-input.tail);
|
|
222
|
-
if (input.
|
|
223
|
-
|
|
247
|
+
if (input.lineStart) {
|
|
248
|
+
let lineStartRe;
|
|
249
|
+
try {
|
|
250
|
+
lineStartRe = new RegExp(input.lineStart);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return `Invalid lineStart regex: "${input.lineStart}"`;
|
|
254
|
+
}
|
|
255
|
+
lines = joinMultilineEntries(lines, lineStartRe);
|
|
256
|
+
}
|
|
257
|
+
if (input.grep) {
|
|
258
|
+
let re;
|
|
259
|
+
try {
|
|
260
|
+
// Smart case: all-lowercase pattern → case-insensitive (like ripgrep --smart-case)
|
|
261
|
+
const flags = /[A-Z]/.test(input.grep) ? "" : "i";
|
|
262
|
+
re = new RegExp(input.grep, flags);
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
return `Invalid grep regex: "${input.grep}"`;
|
|
266
|
+
}
|
|
267
|
+
lines = lines.filter((l) => re.test(l));
|
|
268
|
+
}
|
|
224
269
|
if (!lines.length)
|
|
225
270
|
return "No log lines to process.";
|
|
271
|
+
const inputLength = lines.reduce((sum, l) => sum + l.length, 0);
|
|
226
272
|
const templates = compress(lines, input.simThreshold);
|
|
227
273
|
const templateMap = buildTemplateMap(templates);
|
|
228
274
|
if (input.templateId) {
|
|
@@ -233,7 +279,7 @@ async function processLog(input) {
|
|
|
233
279
|
}
|
|
234
280
|
return formatDrillDown(template);
|
|
235
281
|
}
|
|
236
|
-
return formatOverview(templateMap, lines.length);
|
|
282
|
+
return formatOverview(templateMap, lines.length, inputLength);
|
|
237
283
|
}
|
|
238
284
|
function buildTemplateMap(templates) {
|
|
239
285
|
const map = new Map();
|
|
@@ -248,17 +294,22 @@ function buildTemplateMap(templates) {
|
|
|
248
294
|
}
|
|
249
295
|
return map;
|
|
250
296
|
}
|
|
251
|
-
function formatOverview(templateMap, lineCount) {
|
|
297
|
+
function formatOverview(templateMap, lineCount, inputLength) {
|
|
252
298
|
const sorted = [...templateMap.values()].sort((a, b) => b.count - a.count);
|
|
253
|
-
const
|
|
254
|
-
const header = `=== Log Compression ===\n${lineCount} lines → ${templateMap.size} templates (${reduction}% reduction)\n`;
|
|
299
|
+
const lineReduction = Math.round((1 - templateMap.size / lineCount) * 100);
|
|
255
300
|
const top20 = sorted.slice(0, 20);
|
|
256
301
|
const topLines = top20
|
|
257
302
|
.map((t) => `${t.id} [${t.count}x] ${t.pattern}`)
|
|
258
303
|
.join("\n");
|
|
259
304
|
const remaining = sorted.length - 20;
|
|
260
305
|
const footer = remaining > 0 ? `\n... and ${remaining} more templates` : "";
|
|
261
|
-
|
|
306
|
+
const body = `${topLines}${footer}`;
|
|
307
|
+
const outputLength = body.length;
|
|
308
|
+
const charReduction = Math.round((1 - outputLength / inputLength) * 100);
|
|
309
|
+
const header = `=== Log Compression ===\n` +
|
|
310
|
+
`${lineCount} lines → ${templateMap.size} templates (${lineReduction}% reduction)\n` +
|
|
311
|
+
`${inputLength.toLocaleString()} chars → ${outputLength.toLocaleString()} chars (${charReduction}% reduction)\n`;
|
|
312
|
+
return `${header}\n${body}`;
|
|
262
313
|
}
|
|
263
314
|
function formatDrillDown(template) {
|
|
264
315
|
const header = `Template: ${template.id}\nPattern: ${template.pattern}\nMatches: ${template.count}\n`;
|