mcp-multitool 0.1.6 → 0.1.7
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 +56 -7
- package/dist/index.js +3 -0
- package/dist/tools/deleteFile.js +4 -0
- package/dist/tools/moveFile.js +4 -0
- package/dist/tools/readLog.d.ts +2 -0
- package/dist/tools/readLog.js +152 -0
- package/dist/tools/wait.js +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,19 +4,20 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/mcp-multitool)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|
|
|
7
|
-
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server with
|
|
7
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server with **file operations**, **log compression**, and **timing utilities**.
|
|
8
8
|
|
|
9
9
|
## Tools
|
|
10
10
|
|
|
11
|
-
| Tool | Description
|
|
12
|
-
| ------------ |
|
|
13
|
-
| `deleteFile` | Delete files or directories (single or batch)
|
|
14
|
-
| `moveFile` | Move/rename files or directories
|
|
15
|
-
| `
|
|
11
|
+
| Tool | Description |
|
|
12
|
+
| ------------ | -------------------------------------------------- |
|
|
13
|
+
| `deleteFile` | Delete files or directories (single or batch) |
|
|
14
|
+
| `moveFile` | Move/rename files or directories |
|
|
15
|
+
| `readLog` | Read and compress logs with 60-90% token reduction |
|
|
16
|
+
| `wait` | Pause execution for rate limits or timing |
|
|
16
17
|
|
|
17
18
|
## Why
|
|
18
19
|
|
|
19
|
-
Some tasks need simple utilities that don't warrant a larger server — cleaning up temp files, pausing for rate limits. `mcp-multitool` gives any MCP-compatible client a small set of reliable tools.
|
|
20
|
+
Some tasks need simple utilities that don't warrant a larger server — cleaning up temp files, compressing verbose logs before analysis, pausing for rate limits. `mcp-multitool` gives any MCP-compatible client a small set of reliable tools.
|
|
20
21
|
|
|
21
22
|
## Quick Start
|
|
22
23
|
|
|
@@ -101,6 +102,52 @@ moveFile from="config.json" to="dest/" overwrite=true
|
|
|
101
102
|
|
|
102
103
|
---
|
|
103
104
|
|
|
105
|
+
### `readLog`
|
|
106
|
+
|
|
107
|
+
Compress a log file using semantic pattern extraction (60-90% token reduction). Creates stateful drains for incremental reads. Use `flushLog` to release.
|
|
108
|
+
|
|
109
|
+
**Stateful drains:** On first call for a file, creates a stateful drain. Subsequent calls append only new lines to the existing drain, preserving template IDs. This enables incremental log analysis as files grow. When any drain is active, a dynamic `flushLog` tool appears to release drains.
|
|
110
|
+
|
|
111
|
+
| Parameter | Type | Required | Description |
|
|
112
|
+
| -------------- | --------- | -------- | ------------------------------------------------ |
|
|
113
|
+
| `path` | `string` | ✅ | Path to the log file. |
|
|
114
|
+
| `format` | `string` | ✅ | Output format: `summary`, `detailed`, or `json`. |
|
|
115
|
+
| `depth` | `integer` | ✅ | Parse tree depth (2-8). |
|
|
116
|
+
| `simThreshold` | `number` | ✅ | Similarity threshold (0-1). |
|
|
117
|
+
| `tail` | `integer` | — | Last N lines (first read only). |
|
|
118
|
+
| `head` | `integer` | — | First N lines (first read only). |
|
|
119
|
+
| `grep` | `string` | — | Regex filter for lines. |
|
|
120
|
+
|
|
121
|
+
**Response:** Compressed log summary showing unique templates and occurrence counts.
|
|
122
|
+
|
|
123
|
+
**Examples:**
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
readLog path="/var/log/app.log" format="summary" depth=4 simThreshold=0.4
|
|
127
|
+
readLog path="./logs/server.log" format="detailed" depth=4 simThreshold=0.4 tail=1000
|
|
128
|
+
readLog path="app.log" format="json" depth=6 simThreshold=0.3 grep="ERROR|WARN"
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### `flushLog` (dynamic)
|
|
134
|
+
|
|
135
|
+
Release a log drain to free memory. Next `readLog` creates fresh drain. **This tool only appears when at least one drain is active.** When the last drain is flushed, the tool is automatically removed.
|
|
136
|
+
|
|
137
|
+
| Parameter | Type | Required | Description |
|
|
138
|
+
| --------- | -------- | -------- | ------------------------------ |
|
|
139
|
+
| `path` | `string` | ✅ | Path to the log file to flush. |
|
|
140
|
+
|
|
141
|
+
**Response:** `"Flushed {filename}. Released N templates from M lines."`
|
|
142
|
+
|
|
143
|
+
**Example:**
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
flushLog path="/var/log/app.log"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
104
151
|
### `wait`
|
|
105
152
|
|
|
106
153
|
Wait for a specified duration before continuing.
|
|
@@ -125,8 +172,10 @@ wait durationSeconds=1 reason="animation to complete"
|
|
|
125
172
|
| Variable | Default | Description |
|
|
126
173
|
| ------------------------ | ------- | -------------------------------------------------------------------------------------------------------------- |
|
|
127
174
|
| `waitMaxDurationSeconds` | `300` | Override the maximum allowed `durationSeconds`. Must be a positive number. Server refuses to start if invalid. |
|
|
175
|
+
| `readLogTimeoutMs` | `5000` | Override the timeout for `readLog` processing in milliseconds. Server refuses to start if invalid. |
|
|
128
176
|
| `deleteFile` | _(on)_ | Set to `"false"` to disable the `deleteFile` tool at startup. |
|
|
129
177
|
| `moveFile` | _(on)_ | Set to `"false"` to disable the `moveFile` tool at startup. |
|
|
178
|
+
| `readLog` | _(on)_ | Set to `"false"` to disable the `readLog` tool at startup. |
|
|
130
179
|
| `wait` | _(on)_ | Set to `"false"` to disable the `wait` tool at startup. |
|
|
131
180
|
|
|
132
181
|
### Disabling Individual Tools
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { register as registerDeleteFile } from "./tools/deleteFile.js";
|
|
6
6
|
import { register as registerMoveFile } from "./tools/moveFile.js";
|
|
7
|
+
import { register as registerReadLog } from "./tools/readLog.js";
|
|
7
8
|
import { register as registerWait } from "./tools/wait.js";
|
|
8
9
|
const require = createRequire(import.meta.url);
|
|
9
10
|
const { version } = require("../package.json");
|
|
@@ -13,6 +14,8 @@ if (isEnabled("deleteFile"))
|
|
|
13
14
|
registerDeleteFile(server);
|
|
14
15
|
if (isEnabled("moveFile"))
|
|
15
16
|
registerMoveFile(server);
|
|
17
|
+
if (isEnabled("readLog"))
|
|
18
|
+
registerReadLog(server);
|
|
16
19
|
if (isEnabled("wait"))
|
|
17
20
|
registerWait(server);
|
|
18
21
|
await server.connect(new StdioServerTransport());
|
package/dist/tools/deleteFile.js
CHANGED
|
@@ -13,6 +13,10 @@ export function register(server) {
|
|
|
13
13
|
server.registerTool("deleteFile", {
|
|
14
14
|
description: "Delete one or more files or directories.",
|
|
15
15
|
inputSchema: schema,
|
|
16
|
+
annotations: {
|
|
17
|
+
destructiveHint: true,
|
|
18
|
+
openWorldHint: false,
|
|
19
|
+
},
|
|
16
20
|
}, async (input) => {
|
|
17
21
|
try {
|
|
18
22
|
const paths = Array.isArray(input.paths) ? input.paths : [input.paths];
|
package/dist/tools/moveFile.js
CHANGED
|
@@ -24,6 +24,10 @@ export function register(server) {
|
|
|
24
24
|
server.registerTool("moveFile", {
|
|
25
25
|
description: "Move one or more files or directories to a destination directory.",
|
|
26
26
|
inputSchema: schema,
|
|
27
|
+
annotations: {
|
|
28
|
+
destructiveHint: true,
|
|
29
|
+
openWorldHint: false,
|
|
30
|
+
},
|
|
27
31
|
}, async (input) => {
|
|
28
32
|
try {
|
|
29
33
|
const sources = Array.isArray(input.from) ? input.from : [input.from];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve, basename } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createDrain } from "logpare";
|
|
5
|
+
const timeoutMs = (() => {
|
|
6
|
+
const env = process.env.readLogTimeoutMs;
|
|
7
|
+
if (!env)
|
|
8
|
+
return 5000;
|
|
9
|
+
const n = Number(env);
|
|
10
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
11
|
+
process.stderr.write(`Invalid readLogTimeoutMs: "${env}".\n`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
return n;
|
|
15
|
+
})();
|
|
16
|
+
const drains = new Map();
|
|
17
|
+
let flushTool = null;
|
|
18
|
+
const schema = z
|
|
19
|
+
.object({
|
|
20
|
+
path: z.string().min(1).describe("Path to the log file."),
|
|
21
|
+
format: z.enum(["summary", "detailed", "json"]).describe("Output format."),
|
|
22
|
+
depth: z.number().int().min(2).max(8).describe("Parse tree depth (2-8)."),
|
|
23
|
+
simThreshold: z
|
|
24
|
+
.number()
|
|
25
|
+
.min(0)
|
|
26
|
+
.max(1)
|
|
27
|
+
.describe("Similarity threshold (0-1)."),
|
|
28
|
+
tail: z
|
|
29
|
+
.number()
|
|
30
|
+
.int()
|
|
31
|
+
.min(1)
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("Last N lines (first read only)."),
|
|
34
|
+
head: z
|
|
35
|
+
.number()
|
|
36
|
+
.int()
|
|
37
|
+
.min(1)
|
|
38
|
+
.optional()
|
|
39
|
+
.describe("First N lines (first read only)."),
|
|
40
|
+
grep: z.string().optional().describe("Regex filter for lines."),
|
|
41
|
+
})
|
|
42
|
+
.refine((d) => !(d.head && d.tail), {
|
|
43
|
+
message: "Cannot use both head and tail.",
|
|
44
|
+
});
|
|
45
|
+
const flushSchema = z.object({
|
|
46
|
+
path: z.string().min(1).describe("Path to the log file to flush."),
|
|
47
|
+
});
|
|
48
|
+
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
49
|
+
const err = (text) => ({
|
|
50
|
+
isError: true,
|
|
51
|
+
content: [{ type: "text", text }],
|
|
52
|
+
});
|
|
53
|
+
export function register(server) {
|
|
54
|
+
server.registerTool("readLog", {
|
|
55
|
+
description: "Compress a log file using semantic pattern extraction (60-90% reduction). Creates stateful drains for incremental reads. Use flushLog to release.",
|
|
56
|
+
inputSchema: schema,
|
|
57
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
58
|
+
}, async (input) => {
|
|
59
|
+
try {
|
|
60
|
+
return ok(await Promise.race([processLog(server, input), timeout()]));
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
return err(String(e));
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
async function processLog(server, input) {
|
|
68
|
+
const path = resolve(process.cwd(), input.path);
|
|
69
|
+
let state = drains.get(path);
|
|
70
|
+
if (state &&
|
|
71
|
+
(state.depth !== input.depth || state.simThreshold !== input.simThreshold)) {
|
|
72
|
+
return `Error: Drain exists with depth=${state.depth}, simThreshold=${state.simThreshold}. Flush first.`;
|
|
73
|
+
}
|
|
74
|
+
const content = await readFile(path, "utf-8");
|
|
75
|
+
let lines = content.split(/\r?\n/).filter(Boolean);
|
|
76
|
+
if (!state) {
|
|
77
|
+
const wasEmpty = drains.size === 0;
|
|
78
|
+
if (input.head)
|
|
79
|
+
lines = lines.slice(0, input.head);
|
|
80
|
+
else if (input.tail)
|
|
81
|
+
lines = lines.slice(-input.tail);
|
|
82
|
+
if (input.grep)
|
|
83
|
+
lines = lines.filter((l) => new RegExp(input.grep).test(l));
|
|
84
|
+
if (!lines.length)
|
|
85
|
+
return "No log lines to process.";
|
|
86
|
+
const drain = createDrain({
|
|
87
|
+
depth: input.depth,
|
|
88
|
+
simThreshold: input.simThreshold,
|
|
89
|
+
});
|
|
90
|
+
drain.addLogLines(lines);
|
|
91
|
+
state = {
|
|
92
|
+
drain,
|
|
93
|
+
lastLine: lines.length,
|
|
94
|
+
depth: input.depth,
|
|
95
|
+
simThreshold: input.simThreshold,
|
|
96
|
+
};
|
|
97
|
+
drains.set(path, state);
|
|
98
|
+
if (wasEmpty)
|
|
99
|
+
registerFlush(server);
|
|
100
|
+
return `${state.drain.getResult(input.format).formatted}\n\n[New drain. ${state.lastLine} lines. Use flushLog when done.]`;
|
|
101
|
+
}
|
|
102
|
+
const newLines = lines.slice(state.lastLine);
|
|
103
|
+
if (!newLines.length) {
|
|
104
|
+
return `${state.drain.getResult(input.format).formatted}\n\n[No new lines. Total: ${state.lastLine}]`;
|
|
105
|
+
}
|
|
106
|
+
const filtered = input.grep ?
|
|
107
|
+
newLines.filter((l) => new RegExp(input.grep).test(l))
|
|
108
|
+
: newLines;
|
|
109
|
+
if (filtered.length)
|
|
110
|
+
state.drain.addLogLines(filtered);
|
|
111
|
+
state.lastLine = lines.length;
|
|
112
|
+
return `${state.drain.getResult(input.format).formatted}\n\n[+${newLines.length} lines. Total: ${state.lastLine}]`;
|
|
113
|
+
}
|
|
114
|
+
function registerFlush(server) {
|
|
115
|
+
if (flushTool)
|
|
116
|
+
return;
|
|
117
|
+
try {
|
|
118
|
+
flushTool = server.registerTool("flushLog", {
|
|
119
|
+
description: "Release a log drain to free memory. Next readLog creates fresh drain.",
|
|
120
|
+
inputSchema: flushSchema,
|
|
121
|
+
annotations: { destructiveHint: true, idempotentHint: true },
|
|
122
|
+
}, async (input) => {
|
|
123
|
+
try {
|
|
124
|
+
const path = resolve(process.cwd(), input.path);
|
|
125
|
+
const state = drains.get(path);
|
|
126
|
+
if (!state) {
|
|
127
|
+
if (!drains.size)
|
|
128
|
+
return ok("No active drains.");
|
|
129
|
+
return ok(`No drain for "${basename(path)}". Active: ${[...drains.keys()].map((p) => basename(p)).join(", ")}`);
|
|
130
|
+
}
|
|
131
|
+
const { totalClusters, lastLine } = {
|
|
132
|
+
totalClusters: state.drain.totalClusters,
|
|
133
|
+
lastLine: state.lastLine,
|
|
134
|
+
};
|
|
135
|
+
drains.delete(path);
|
|
136
|
+
if (!drains.size && flushTool) {
|
|
137
|
+
flushTool.remove();
|
|
138
|
+
flushTool = null;
|
|
139
|
+
}
|
|
140
|
+
return ok(`Flushed ${basename(path)}. Released ${totalClusters} templates from ${lastLine} lines.`);
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
return err(String(e));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
server.sendToolListChanged();
|
|
147
|
+
}
|
|
148
|
+
catch { }
|
|
149
|
+
}
|
|
150
|
+
function timeout() {
|
|
151
|
+
return new Promise((_, rej) => setTimeout(() => rej(new Error(`Timeout: ${timeoutMs}ms`)), timeoutMs));
|
|
152
|
+
}
|
package/dist/tools/wait.js
CHANGED
|
@@ -29,6 +29,10 @@ export function register(server) {
|
|
|
29
29
|
server.registerTool("wait", {
|
|
30
30
|
description: "Wait for a specified duration before continuing.",
|
|
31
31
|
inputSchema: schema,
|
|
32
|
+
annotations: {
|
|
33
|
+
readOnlyHint: true,
|
|
34
|
+
idempotentHint: true,
|
|
35
|
+
},
|
|
32
36
|
}, async (input) => {
|
|
33
37
|
try {
|
|
34
38
|
const ms = Math.round(input.durationSeconds * 1000);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-multitool",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "MCP server with file operations (delete, move) and timing utilities.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@modelcontextprotocol/sdk": "1.29.0",
|
|
33
|
+
"logpare": "^0.1.0",
|
|
33
34
|
"zod": "^3.24.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|