mcp-multitool 0.1.7 → 0.1.9
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 +80 -36
- package/dist/index.js +15 -9
- package/dist/tools/checkFile.d.ts +2 -0
- package/dist/tools/checkFile.js +45 -0
- package/dist/tools/checkFileOrDir.d.ts +2 -0
- package/dist/tools/checkFileOrDir.js +45 -0
- package/dist/tools/deleteFile.js +0 -1
- package/dist/tools/deleteFileOrDir.d.ts +2 -0
- package/dist/tools/deleteFileOrDir.js +34 -0
- package/dist/tools/moveFile.js +0 -1
- package/dist/tools/moveFileOrDir.d.ts +2 -0
- package/dist/tools/moveFileOrDir.js +49 -0
- package/dist/tools/readLogFile.d.ts +2 -0
- package/dist/tools/readLogFile.js +151 -0
- package/dist/tools/renameFile.d.ts +2 -0
- package/dist/tools/renameFile.js +53 -0
- package/dist/tools/renameFileOrDir.d.ts +2 -0
- package/dist/tools/renameFileOrDir.js +60 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,12 +8,14 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server with **
|
|
|
8
8
|
|
|
9
9
|
## Tools
|
|
10
10
|
|
|
11
|
-
| Tool
|
|
12
|
-
|
|
|
13
|
-
| `
|
|
14
|
-
| `
|
|
15
|
-
| `
|
|
16
|
-
| `
|
|
11
|
+
| Tool | Description |
|
|
12
|
+
| ----------------- | ------------------------------------------------------- |
|
|
13
|
+
| `checkFileOrDir` | Check if a file or directory exists and return metadata |
|
|
14
|
+
| `deleteFileOrDir` | Delete one or more files or directories |
|
|
15
|
+
| `moveFileOrDir` | Move one or more files or directories to a new location |
|
|
16
|
+
| `readLogFile` | Read and compress logs with 60-90% token reduction |
|
|
17
|
+
| `renameFileOrDir` | Rename a single file or directory |
|
|
18
|
+
| `wait` | Pause execution for rate limits or timing |
|
|
17
19
|
|
|
18
20
|
## Why
|
|
19
21
|
|
|
@@ -59,54 +61,74 @@ npx mcp-multitool
|
|
|
59
61
|
|
|
60
62
|
## Tool Reference
|
|
61
63
|
|
|
62
|
-
### `
|
|
64
|
+
### `checkFileOrDir`
|
|
65
|
+
|
|
66
|
+
Check if a file or directory exists and return its metadata (type, size, timestamps, permissions). Returns an error if the path does not exist.
|
|
67
|
+
|
|
68
|
+
| Parameter | Type | Required | Description |
|
|
69
|
+
| --------- | -------- | -------- | --------------------------------------- |
|
|
70
|
+
| `path` | `string` | ✅ | Path to the file or directory to check. |
|
|
71
|
+
|
|
72
|
+
**Response:** JSON object with raw `fs.Stats` properties plus computed `name`, `path`, and `type` fields.
|
|
73
|
+
|
|
74
|
+
**Examples:**
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
checkFileOrDir path="config.json"
|
|
78
|
+
checkFileOrDir path="/var/log/"
|
|
79
|
+
checkFileOrDir path="./src/index.ts"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### `deleteFileOrDir`
|
|
63
85
|
|
|
64
86
|
Delete one or more files or directories.
|
|
65
87
|
|
|
66
|
-
| Parameter | Type | Required |
|
|
67
|
-
| ----------- | -------------------- | -------- |
|
|
68
|
-
| `paths` | `string \| string[]` | ✅ |
|
|
69
|
-
| `recursive` | `boolean` |
|
|
88
|
+
| Parameter | Type | Required | Description |
|
|
89
|
+
| ----------- | -------------------- | -------- | ----------------------------------------------------- |
|
|
90
|
+
| `paths` | `string \| string[]` | ✅ | File or directory path(s) to delete. |
|
|
91
|
+
| `recursive` | `boolean` | ✅ | If true, delete directories and contents recursively. |
|
|
70
92
|
|
|
71
93
|
**Response:** `"Deleted N path(s)."`
|
|
72
94
|
|
|
73
95
|
**Examples:**
|
|
74
96
|
|
|
75
97
|
```
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
deleteFileOrDir paths="temp.txt" recursive=false
|
|
99
|
+
deleteFileOrDir paths=["a.txt", "b.txt"] recursive=false
|
|
100
|
+
deleteFileOrDir paths="build/" recursive=true
|
|
79
101
|
```
|
|
80
102
|
|
|
81
103
|
---
|
|
82
104
|
|
|
83
|
-
### `
|
|
105
|
+
### `moveFileOrDir`
|
|
84
106
|
|
|
85
107
|
Move one or more files or directories to a destination directory.
|
|
86
108
|
|
|
87
|
-
| Parameter | Type | Required |
|
|
88
|
-
| ----------- | -------------------- | -------- |
|
|
89
|
-
| `from` | `string \| string[]` | ✅ |
|
|
90
|
-
| `to` | `string` | ✅ |
|
|
91
|
-
| `overwrite` | `boolean` |
|
|
109
|
+
| Parameter | Type | Required | Description |
|
|
110
|
+
| ----------- | -------------------- | -------- | ---------------------------------- |
|
|
111
|
+
| `from` | `string \| string[]` | ✅ | Source path(s) to move. |
|
|
112
|
+
| `to` | `string` | ✅ | Destination directory. |
|
|
113
|
+
| `overwrite` | `boolean` | ✅ | If true, overwrite existing files. |
|
|
92
114
|
|
|
93
115
|
**Response:** `"Moved N path(s)."`
|
|
94
116
|
|
|
95
117
|
**Examples:**
|
|
96
118
|
|
|
97
119
|
```
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
moveFileOrDir from="old.txt" to="archive/" overwrite=false
|
|
121
|
+
moveFileOrDir from=["a.txt", "b.txt"] to="backup/" overwrite=false
|
|
122
|
+
moveFileOrDir from="config.json" to="dest/" overwrite=true
|
|
101
123
|
```
|
|
102
124
|
|
|
103
125
|
---
|
|
104
126
|
|
|
105
|
-
### `
|
|
127
|
+
### `readLogFile`
|
|
106
128
|
|
|
107
|
-
Compress a log file using semantic pattern extraction (60-90% token reduction). Creates stateful drains for incremental reads. Use `
|
|
129
|
+
Compress a log file using semantic pattern extraction (60-90% token reduction). Creates stateful drains for incremental reads. Use `flushLogFile` to release.
|
|
108
130
|
|
|
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 `
|
|
131
|
+
**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 `flushLogFile` tool appears to release drains.
|
|
110
132
|
|
|
111
133
|
| Parameter | Type | Required | Description |
|
|
112
134
|
| -------------- | --------- | -------- | ------------------------------------------------ |
|
|
@@ -123,16 +145,16 @@ Compress a log file using semantic pattern extraction (60-90% token reduction).
|
|
|
123
145
|
**Examples:**
|
|
124
146
|
|
|
125
147
|
```
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
148
|
+
readLogFile path="/var/log/app.log" format="summary" depth=4 simThreshold=0.4
|
|
149
|
+
readLogFile path="./logs/server.log" format="detailed" depth=4 simThreshold=0.4 tail=1000
|
|
150
|
+
readLogFile path="app.log" format="json" depth=6 simThreshold=0.3 grep="ERROR|WARN"
|
|
129
151
|
```
|
|
130
152
|
|
|
131
153
|
---
|
|
132
154
|
|
|
133
|
-
### `
|
|
155
|
+
### `flushLogFile` (dynamic)
|
|
134
156
|
|
|
135
|
-
Release a log drain to free memory. Next `
|
|
157
|
+
Release a log drain to free memory. Next `readLogFile` 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
158
|
|
|
137
159
|
| Parameter | Type | Required | Description |
|
|
138
160
|
| --------- | -------- | -------- | ------------------------------ |
|
|
@@ -143,7 +165,27 @@ Release a log drain to free memory. Next `readLog` creates fresh drain. **This t
|
|
|
143
165
|
**Example:**
|
|
144
166
|
|
|
145
167
|
```
|
|
146
|
-
|
|
168
|
+
flushLogFile path="/var/log/app.log"
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### `renameFileOrDir`
|
|
174
|
+
|
|
175
|
+
Rename a single file or directory. Only the name can change — the parent directory must stay the same. Use `moveFileOrDir` to change directories.
|
|
176
|
+
|
|
177
|
+
| Parameter | Type | Required | Description |
|
|
178
|
+
| --------- | -------- | -------- | -------------------------------------------- |
|
|
179
|
+
| `oldPath` | `string` | ✅ | Current path to the file or directory. |
|
|
180
|
+
| `newPath` | `string` | ✅ | New path with the renamed file or directory. |
|
|
181
|
+
|
|
182
|
+
**Response:** `"Renamed "{oldName}" to "{newName}"."`
|
|
183
|
+
|
|
184
|
+
**Examples:**
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
renameFileOrDir oldPath="config.json" newPath="config.backup.json"
|
|
188
|
+
renameFileOrDir oldPath="/app/src" newPath="/app/source"
|
|
147
189
|
```
|
|
148
190
|
|
|
149
191
|
---
|
|
@@ -172,10 +214,12 @@ wait durationSeconds=1 reason="animation to complete"
|
|
|
172
214
|
| Variable | Default | Description |
|
|
173
215
|
| ------------------------ | ------- | -------------------------------------------------------------------------------------------------------------- |
|
|
174
216
|
| `waitMaxDurationSeconds` | `300` | Override the maximum allowed `durationSeconds`. Must be a positive number. Server refuses to start if invalid. |
|
|
175
|
-
| `
|
|
176
|
-
| `
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
217
|
+
| `readLogFileTimeoutMs` | `5000` | Override the timeout for `readLogFile` processing in milliseconds. Server refuses to start if invalid. |
|
|
218
|
+
| `checkFileOrDir` | _(on)_ | Set to `"false"` to disable the `checkFileOrDir` tool at startup. |
|
|
219
|
+
| `deleteFileOrDir` | _(on)_ | Set to `"false"` to disable the `deleteFileOrDir` tool at startup. |
|
|
220
|
+
| `moveFileOrDir` | _(on)_ | Set to `"false"` to disable the `moveFileOrDir` tool at startup. |
|
|
221
|
+
| `readLogFile` | _(on)_ | Set to `"false"` to disable the `readLogFile` tool at startup. |
|
|
222
|
+
| `renameFileOrDir` | _(on)_ | Set to `"false"` to disable the `renameFileOrDir` tool at startup. |
|
|
179
223
|
| `wait` | _(on)_ | Set to `"false"` to disable the `wait` tool at startup. |
|
|
180
224
|
|
|
181
225
|
### Disabling Individual Tools
|
package/dist/index.js
CHANGED
|
@@ -2,20 +2,26 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
-
import { register as
|
|
6
|
-
import { register as
|
|
7
|
-
import { register as
|
|
5
|
+
import { register as registerCheckFileOrDir } from "./tools/checkFileOrDir.js";
|
|
6
|
+
import { register as registerDeleteFileOrDir } from "./tools/deleteFileOrDir.js";
|
|
7
|
+
import { register as registerMoveFileOrDir } from "./tools/moveFileOrDir.js";
|
|
8
|
+
import { register as registerReadLogFile } from "./tools/readLogFile.js";
|
|
9
|
+
import { register as registerRenameFileOrDir } from "./tools/renameFileOrDir.js";
|
|
8
10
|
import { register as registerWait } from "./tools/wait.js";
|
|
9
11
|
const require = createRequire(import.meta.url);
|
|
10
12
|
const { version } = require("../package.json");
|
|
11
13
|
const isEnabled = (name) => process.env[name] !== "false";
|
|
12
14
|
const server = new McpServer({ name: "mcp-multitool", version });
|
|
13
|
-
if (isEnabled("
|
|
14
|
-
|
|
15
|
-
if (isEnabled("
|
|
16
|
-
|
|
17
|
-
if (isEnabled("
|
|
18
|
-
|
|
15
|
+
if (isEnabled("checkFileOrDir"))
|
|
16
|
+
registerCheckFileOrDir(server);
|
|
17
|
+
if (isEnabled("deleteFileOrDir"))
|
|
18
|
+
registerDeleteFileOrDir(server);
|
|
19
|
+
if (isEnabled("moveFileOrDir"))
|
|
20
|
+
registerMoveFileOrDir(server);
|
|
21
|
+
if (isEnabled("readLogFile"))
|
|
22
|
+
registerReadLogFile(server);
|
|
23
|
+
if (isEnabled("renameFileOrDir"))
|
|
24
|
+
registerRenameFileOrDir(server);
|
|
19
25
|
if (isEnabled("wait"))
|
|
20
26
|
registerWait(server);
|
|
21
27
|
await server.connect(new StdioServerTransport());
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
path: z.string().min(1).describe("Path to the file or folder to check."),
|
|
6
|
+
});
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.registerTool("checkFile", {
|
|
9
|
+
description: "Check if a file or folder exists and return its metadata (type, size, timestamps, permissions). Returns an error if the path does not exist.",
|
|
10
|
+
inputSchema: schema,
|
|
11
|
+
annotations: {
|
|
12
|
+
readOnlyHint: true,
|
|
13
|
+
openWorldHint: false,
|
|
14
|
+
},
|
|
15
|
+
}, async (input) => {
|
|
16
|
+
try {
|
|
17
|
+
const fullPath = resolve(process.cwd(), input.path);
|
|
18
|
+
const stats = await lstat(fullPath);
|
|
19
|
+
const type = stats.isFile() ? "file"
|
|
20
|
+
: stats.isDirectory() ? "directory"
|
|
21
|
+
: stats.isSymbolicLink() ? "symlink"
|
|
22
|
+
: stats.isBlockDevice() ? "block-device"
|
|
23
|
+
: stats.isCharacterDevice() ? "character-device"
|
|
24
|
+
: stats.isFIFO() ? "fifo"
|
|
25
|
+
: stats.isSocket() ? "socket"
|
|
26
|
+
: "unknown";
|
|
27
|
+
// Build output from raw stats, adding computed fields
|
|
28
|
+
const meta = {
|
|
29
|
+
name: basename(fullPath),
|
|
30
|
+
path: fullPath,
|
|
31
|
+
type,
|
|
32
|
+
...stats,
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(meta, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
isError: true,
|
|
41
|
+
content: [{ type: "text", text: String(err) }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { lstat } from "node:fs/promises";
|
|
3
|
+
import { basename, resolve } from "node:path";
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
path: z.string().min(1).describe("Path to the file or folder to check."),
|
|
6
|
+
});
|
|
7
|
+
export function register(server) {
|
|
8
|
+
server.registerTool("checkFileOrDir", {
|
|
9
|
+
description: "Check if a file or directory exists and return its metadata (type, size, timestamps, permissions). Returns an error if the path does not exist.",
|
|
10
|
+
inputSchema: schema,
|
|
11
|
+
annotations: {
|
|
12
|
+
readOnlyHint: true,
|
|
13
|
+
openWorldHint: false,
|
|
14
|
+
},
|
|
15
|
+
}, async (input) => {
|
|
16
|
+
try {
|
|
17
|
+
const fullPath = resolve(process.cwd(), input.path);
|
|
18
|
+
const stats = await lstat(fullPath);
|
|
19
|
+
const type = stats.isFile() ? "file"
|
|
20
|
+
: stats.isDirectory() ? "directory"
|
|
21
|
+
: stats.isSymbolicLink() ? "symlink"
|
|
22
|
+
: stats.isBlockDevice() ? "block-device"
|
|
23
|
+
: stats.isCharacterDevice() ? "character-device"
|
|
24
|
+
: stats.isFIFO() ? "fifo"
|
|
25
|
+
: stats.isSocket() ? "socket"
|
|
26
|
+
: "unknown";
|
|
27
|
+
// Build output from raw stats, adding computed fields
|
|
28
|
+
const meta = {
|
|
29
|
+
name: basename(fullPath),
|
|
30
|
+
path: fullPath,
|
|
31
|
+
type,
|
|
32
|
+
...stats,
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: JSON.stringify(meta, null, 2) }],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
return {
|
|
40
|
+
isError: true,
|
|
41
|
+
content: [{ type: "text", text: String(err) }],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
package/dist/tools/deleteFile.js
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
const schema = z.object({
|
|
4
|
+
paths: z
|
|
5
|
+
.union([z.string(), z.array(z.string())])
|
|
6
|
+
.describe("File or directory path(s) to delete."),
|
|
7
|
+
recursive: z
|
|
8
|
+
.boolean()
|
|
9
|
+
.describe("If true, delete directories and contents recursively."),
|
|
10
|
+
});
|
|
11
|
+
export function register(server) {
|
|
12
|
+
server.registerTool("deleteFileOrDir", {
|
|
13
|
+
description: "Delete one or more files or directories.",
|
|
14
|
+
inputSchema: schema,
|
|
15
|
+
annotations: {
|
|
16
|
+
destructiveHint: true,
|
|
17
|
+
openWorldHint: false,
|
|
18
|
+
},
|
|
19
|
+
}, async (input) => {
|
|
20
|
+
try {
|
|
21
|
+
const paths = Array.isArray(input.paths) ? input.paths : [input.paths];
|
|
22
|
+
await Promise.all(paths.map((p) => rm(p, { recursive: input.recursive })));
|
|
23
|
+
return {
|
|
24
|
+
content: [{ type: "text", text: `Deleted ${paths.length} path(s).` }],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
return {
|
|
29
|
+
isError: true,
|
|
30
|
+
content: [{ type: "text", text: String(err) }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
package/dist/tools/moveFile.js
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { access, rename } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
from: z
|
|
6
|
+
.union([z.string(), z.array(z.string())])
|
|
7
|
+
.describe("Source path(s) to move."),
|
|
8
|
+
to: z.string().describe("Destination directory."),
|
|
9
|
+
overwrite: z.boolean().describe("If true, overwrite existing files."),
|
|
10
|
+
});
|
|
11
|
+
async function exists(path) {
|
|
12
|
+
try {
|
|
13
|
+
await access(path);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function register(server) {
|
|
21
|
+
server.registerTool("moveFileOrDir", {
|
|
22
|
+
description: "Move one or more files or directories to a destination directory.",
|
|
23
|
+
inputSchema: schema,
|
|
24
|
+
annotations: {
|
|
25
|
+
destructiveHint: true,
|
|
26
|
+
openWorldHint: false,
|
|
27
|
+
},
|
|
28
|
+
}, async (input) => {
|
|
29
|
+
try {
|
|
30
|
+
const sources = Array.isArray(input.from) ? input.from : [input.from];
|
|
31
|
+
for (const src of sources) {
|
|
32
|
+
const dest = join(input.to, basename(src));
|
|
33
|
+
if (!input.overwrite && (await exists(dest))) {
|
|
34
|
+
throw new Error(`Destination exists: ${dest}. Set overwrite=true to replace.`);
|
|
35
|
+
}
|
|
36
|
+
await rename(src, dest);
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: `Moved ${sources.length} path(s).` }],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
return {
|
|
44
|
+
isError: true,
|
|
45
|
+
content: [{ type: "text", text: String(err) }],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
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.readLogFileTimeoutMs;
|
|
7
|
+
if (!env)
|
|
8
|
+
return 5000;
|
|
9
|
+
const n = Number(env);
|
|
10
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
11
|
+
process.stderr.write(`Invalid readLogFileTimeoutMs: "${env}".\n`);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
return n;
|
|
15
|
+
})();
|
|
16
|
+
const drains = new Map();
|
|
17
|
+
let flushTool = null;
|
|
18
|
+
const schema = z.object({
|
|
19
|
+
path: z.string().min(1).describe("Path to the log file."),
|
|
20
|
+
format: z.enum(["summary", "detailed", "json"]).describe("Output format."),
|
|
21
|
+
depth: z.number().int().min(2).max(8).describe("Parse tree depth (2-8)."),
|
|
22
|
+
simThreshold: z
|
|
23
|
+
.number()
|
|
24
|
+
.min(0)
|
|
25
|
+
.max(1)
|
|
26
|
+
.describe("Similarity threshold (0-1)."),
|
|
27
|
+
tail: z
|
|
28
|
+
.number()
|
|
29
|
+
.int()
|
|
30
|
+
.min(1)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("Last N lines (first read only)."),
|
|
33
|
+
head: z
|
|
34
|
+
.number()
|
|
35
|
+
.int()
|
|
36
|
+
.min(1)
|
|
37
|
+
.optional()
|
|
38
|
+
.describe("First N lines (first read only)."),
|
|
39
|
+
grep: z.string().optional().describe("Regex filter for lines."),
|
|
40
|
+
});
|
|
41
|
+
const flushSchema = z.object({
|
|
42
|
+
path: z.string().min(1).describe("Path to the log file to flush."),
|
|
43
|
+
});
|
|
44
|
+
const ok = (text) => ({ content: [{ type: "text", text }] });
|
|
45
|
+
const err = (text) => ({
|
|
46
|
+
isError: true,
|
|
47
|
+
content: [{ type: "text", text }],
|
|
48
|
+
});
|
|
49
|
+
export function register(server) {
|
|
50
|
+
server.registerTool("readLogFile", {
|
|
51
|
+
description: "Compress a log file using semantic pattern extraction (60-90% reduction). Creates stateful drains for incremental reads. Use flushLogFile to release.",
|
|
52
|
+
inputSchema: schema,
|
|
53
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
54
|
+
}, async (input) => {
|
|
55
|
+
try {
|
|
56
|
+
return ok(await Promise.race([processLog(server, input), timeout()]));
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
return err(String(e));
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function processLog(server, input) {
|
|
64
|
+
const path = resolve(process.cwd(), input.path);
|
|
65
|
+
let state = drains.get(path);
|
|
66
|
+
if (input.head && input.tail) {
|
|
67
|
+
return "Cannot use both head and tail.";
|
|
68
|
+
}
|
|
69
|
+
if (state &&
|
|
70
|
+
(state.depth !== input.depth || state.simThreshold !== input.simThreshold)) {
|
|
71
|
+
return `Error: Drain exists with depth=${state.depth}, simThreshold=${state.simThreshold}. Flush first.`;
|
|
72
|
+
}
|
|
73
|
+
const content = await readFile(path, "utf-8");
|
|
74
|
+
let lines = content.split(/\r?\n/).filter(Boolean);
|
|
75
|
+
if (!state) {
|
|
76
|
+
const wasEmpty = drains.size === 0;
|
|
77
|
+
if (input.head)
|
|
78
|
+
lines = lines.slice(0, input.head);
|
|
79
|
+
else if (input.tail)
|
|
80
|
+
lines = lines.slice(-input.tail);
|
|
81
|
+
if (input.grep)
|
|
82
|
+
lines = lines.filter((l) => new RegExp(input.grep).test(l));
|
|
83
|
+
if (!lines.length)
|
|
84
|
+
return "No log lines to process.";
|
|
85
|
+
const drain = createDrain({
|
|
86
|
+
depth: input.depth,
|
|
87
|
+
simThreshold: input.simThreshold,
|
|
88
|
+
});
|
|
89
|
+
drain.addLogLines(lines);
|
|
90
|
+
state = {
|
|
91
|
+
drain,
|
|
92
|
+
lastLine: lines.length,
|
|
93
|
+
depth: input.depth,
|
|
94
|
+
simThreshold: input.simThreshold,
|
|
95
|
+
};
|
|
96
|
+
drains.set(path, state);
|
|
97
|
+
if (wasEmpty)
|
|
98
|
+
registerFlush(server);
|
|
99
|
+
return `${state.drain.getResult(input.format).formatted}\n\n[New drain. ${state.lastLine} lines. Use flushLogFile when done.]`;
|
|
100
|
+
}
|
|
101
|
+
const newLines = lines.slice(state.lastLine);
|
|
102
|
+
if (!newLines.length) {
|
|
103
|
+
return `${state.drain.getResult(input.format).formatted}\n\n[No new lines. Total: ${state.lastLine}]`;
|
|
104
|
+
}
|
|
105
|
+
const filtered = input.grep ?
|
|
106
|
+
newLines.filter((l) => new RegExp(input.grep).test(l))
|
|
107
|
+
: newLines;
|
|
108
|
+
if (filtered.length)
|
|
109
|
+
state.drain.addLogLines(filtered);
|
|
110
|
+
state.lastLine = lines.length;
|
|
111
|
+
return `${state.drain.getResult(input.format).formatted}\n\n[+${newLines.length} lines. Total: ${state.lastLine}]`;
|
|
112
|
+
}
|
|
113
|
+
function registerFlush(server) {
|
|
114
|
+
if (flushTool)
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
flushTool = server.registerTool("flushLogFile", {
|
|
118
|
+
description: "Release a log drain to free memory. Next readLogFile creates fresh drain.",
|
|
119
|
+
inputSchema: flushSchema,
|
|
120
|
+
annotations: { destructiveHint: false, idempotentHint: true },
|
|
121
|
+
}, async (input) => {
|
|
122
|
+
try {
|
|
123
|
+
const path = resolve(process.cwd(), input.path);
|
|
124
|
+
const state = drains.get(path);
|
|
125
|
+
if (!state) {
|
|
126
|
+
if (!drains.size)
|
|
127
|
+
return ok("No active drains.");
|
|
128
|
+
return ok(`No drain for "${basename(path)}". Active: ${[...drains.keys()].map((p) => basename(p)).join(", ")}`);
|
|
129
|
+
}
|
|
130
|
+
const { totalClusters, lastLine } = {
|
|
131
|
+
totalClusters: state.drain.totalClusters,
|
|
132
|
+
lastLine: state.lastLine,
|
|
133
|
+
};
|
|
134
|
+
drains.delete(path);
|
|
135
|
+
if (!drains.size && flushTool) {
|
|
136
|
+
flushTool.remove();
|
|
137
|
+
flushTool = null;
|
|
138
|
+
}
|
|
139
|
+
return ok(`Flushed ${basename(path)}. Released ${totalClusters} templates from ${lastLine} lines.`);
|
|
140
|
+
}
|
|
141
|
+
catch (e) {
|
|
142
|
+
return err(String(e));
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
server.sendToolListChanged();
|
|
146
|
+
}
|
|
147
|
+
catch { }
|
|
148
|
+
}
|
|
149
|
+
function timeout() {
|
|
150
|
+
return new Promise((_, rej) => setTimeout(() => rej(new Error(`Timeout: ${timeoutMs}ms`)), timeoutMs));
|
|
151
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rename, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, basename } from "node:path";
|
|
4
|
+
const schema = z
|
|
5
|
+
.object({
|
|
6
|
+
oldPath: z.string().min(1).describe("Current path to the file or folder."),
|
|
7
|
+
newPath: z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.describe("New path with the renamed file or folder."),
|
|
11
|
+
})
|
|
12
|
+
.refine((d) => dirname(d.oldPath) === dirname(d.newPath), {
|
|
13
|
+
message: "Parent directory must match. Use moveFile to change directories.",
|
|
14
|
+
});
|
|
15
|
+
export function register(server) {
|
|
16
|
+
server.registerTool("renameFile", {
|
|
17
|
+
description: "Rename a single file or folder. Only the name can change — the parent directory must stay the same. Use moveFile to change directories.",
|
|
18
|
+
inputSchema: schema,
|
|
19
|
+
annotations: {
|
|
20
|
+
destructiveHint: true,
|
|
21
|
+
idempotentHint: false,
|
|
22
|
+
openWorldHint: false,
|
|
23
|
+
},
|
|
24
|
+
}, async (input) => {
|
|
25
|
+
try {
|
|
26
|
+
const oldName = basename(input.oldPath);
|
|
27
|
+
const newName = basename(input.newPath);
|
|
28
|
+
if (oldName === newName) {
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: `Nothing to rename — names are identical.`,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
await stat(input.oldPath); // Verify exists
|
|
39
|
+
await rename(input.oldPath, input.newPath);
|
|
40
|
+
return {
|
|
41
|
+
content: [
|
|
42
|
+
{ type: "text", text: `Renamed "${oldName}" to "${newName}".` },
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
return {
|
|
48
|
+
isError: true,
|
|
49
|
+
content: [{ type: "text", text: String(err) }],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { rename, stat } from "node:fs/promises";
|
|
3
|
+
import { dirname, basename } from "node:path";
|
|
4
|
+
const schema = z.object({
|
|
5
|
+
oldPath: z.string().min(1).describe("Current path to the file or directory."),
|
|
6
|
+
newPath: z
|
|
7
|
+
.string()
|
|
8
|
+
.min(1)
|
|
9
|
+
.describe("New path with the renamed file or directory."),
|
|
10
|
+
});
|
|
11
|
+
export function register(server) {
|
|
12
|
+
server.registerTool("renameFileOrDir", {
|
|
13
|
+
description: "Rename a single file or directory. Only the name can change — the parent directory must stay the same. Use moveFileOrDir to change directories.",
|
|
14
|
+
inputSchema: schema,
|
|
15
|
+
annotations: {
|
|
16
|
+
destructiveHint: true,
|
|
17
|
+
idempotentHint: false,
|
|
18
|
+
openWorldHint: false,
|
|
19
|
+
},
|
|
20
|
+
}, async (input) => {
|
|
21
|
+
try {
|
|
22
|
+
if (dirname(input.oldPath) !== dirname(input.newPath)) {
|
|
23
|
+
return {
|
|
24
|
+
isError: true,
|
|
25
|
+
content: [
|
|
26
|
+
{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: "Parent directory must match. Use moveFileOrDir to change directories.",
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const oldName = basename(input.oldPath);
|
|
34
|
+
const newName = basename(input.newPath);
|
|
35
|
+
if (oldName === newName) {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: `Nothing to rename — names are identical.`,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
await stat(input.oldPath); // Verify exists
|
|
46
|
+
await rename(input.oldPath, input.newPath);
|
|
47
|
+
return {
|
|
48
|
+
content: [
|
|
49
|
+
{ type: "text", text: `Renamed "${oldName}" to "${newName}".` },
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
return {
|
|
55
|
+
isError: true,
|
|
56
|
+
content: [{ type: "text", text: String(err) }],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|