mcp-multitool 0.1.7 → 0.1.8

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 CHANGED
@@ -8,12 +8,14 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server with **
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
- | `readLog` | Read and compress logs with 60-90% token reduction |
16
- | `wait` | Pause execution for rate limits or timing |
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
- ### `deleteFile`
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 | Default | Description |
67
- | ----------- | -------------------- | -------- | ------- | ----------------------------------------------------- |
68
- | `paths` | `string \| string[]` | ✅ | — | File or directory path(s) to delete. |
69
- | `recursive` | `boolean` | | `false` | If true, delete directories and contents recursively. |
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
- deleteFile paths="temp.txt"
77
- deleteFile paths=["a.txt", "b.txt"]
78
- deleteFile paths="build/" recursive=true
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
- ### `moveFile`
105
+ ### `moveFileOrDir`
84
106
 
85
107
  Move one or more files or directories to a destination directory.
86
108
 
87
- | Parameter | Type | Required | Default | Description |
88
- | ----------- | -------------------- | -------- | ------- | ---------------------------------- |
89
- | `from` | `string \| string[]` | ✅ | — | Source path(s) to move. |
90
- | `to` | `string` | ✅ | — | Destination directory. |
91
- | `overwrite` | `boolean` | | `false` | If true, overwrite existing files. |
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
- moveFile from="old.txt" to="archive/"
99
- moveFile from=["a.txt", "b.txt"] to="backup/"
100
- moveFile from="config.json" to="dest/" overwrite=true
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
- ### `readLog`
127
+ ### `readLogFile`
106
128
 
107
- Compress a log file using semantic pattern extraction (60-90% token reduction). Creates stateful drains for incremental reads. Use `flushLog` to release.
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 `flushLog` tool appears to release drains.
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
- 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"
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
- ### `flushLog` (dynamic)
155
+ ### `flushLogFile` (dynamic)
134
156
 
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.
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
- flushLog path="/var/log/app.log"
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
- | `readLogTimeoutMs` | `5000` | Override the timeout for `readLog` processing in milliseconds. Server refuses to start if invalid. |
176
- | `deleteFile` | _(on)_ | Set to `"false"` to disable the `deleteFile` tool at startup. |
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. |
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 registerDeleteFile } from "./tools/deleteFile.js";
6
- import { register as registerMoveFile } from "./tools/moveFile.js";
7
- import { register as registerReadLog } from "./tools/readLog.js";
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("deleteFile"))
14
- registerDeleteFile(server);
15
- if (isEnabled("moveFile"))
16
- registerMoveFile(server);
17
- if (isEnabled("readLog"))
18
- registerReadLog(server);
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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -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
+ }
@@ -6,7 +6,6 @@ const schema = z.object({
6
6
  .describe("File or directory path(s) to delete."),
7
7
  recursive: z
8
8
  .boolean()
9
- .default(false)
10
9
  .describe("If true, delete directories and contents recursively."),
11
10
  });
12
11
  export function register(server) {
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -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
+ }
@@ -8,7 +8,6 @@ const schema = z.object({
8
8
  to: z.string().describe("Destination directory."),
9
9
  overwrite: z
10
10
  .boolean()
11
- .default(false)
12
11
  .describe("If true, overwrite existing files."),
13
12
  });
14
13
  async function exists(path) {
@@ -0,0 +1,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -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: true, 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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -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,2 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function register(server: McpServer): void;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-multitool",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "MCP server with file operations (delete, move) and timing utilities.",
5
5
  "license": "MIT",
6
6
  "type": "module",