@perstack/base 0.0.54 → 0.0.56
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 +0 -10
- package/dist/bin/server.js +1 -1
- package/dist/chunk-HXMW3IUI.js +575 -0
- package/dist/chunk-HXMW3IUI.js.map +1 -0
- package/dist/src/index.d.ts +1 -80
- package/dist/src/index.js +1 -1
- package/package.json +5 -6
- package/dist/chunk-HUJMWPRE.js +0 -1225
- package/dist/chunk-HUJMWPRE.js.map +0 -1
package/README.md
CHANGED
|
@@ -39,22 +39,12 @@ registerWriteTextFile(server)
|
|
|
39
39
|
### File Operations
|
|
40
40
|
- `readTextFile` - Read text files with optional line range
|
|
41
41
|
- `writeTextFile` - Create or overwrite text files
|
|
42
|
-
- `appendTextFile` - Append content to existing files
|
|
43
42
|
- `editTextFile` - Replace text in existing files
|
|
44
|
-
- `deleteFile` - Remove files
|
|
45
|
-
- `moveFile` - Move or rename files
|
|
46
|
-
- `getFileInfo` - Get file metadata
|
|
47
43
|
- `readImageFile` - Read image files (PNG, JPEG, GIF, WebP)
|
|
48
44
|
- `readPdfFile` - Read PDF files
|
|
49
45
|
|
|
50
|
-
### Directory Operations
|
|
51
|
-
- `listDirectory` - List directory contents
|
|
52
|
-
- `createDirectory` - Create directories
|
|
53
|
-
- `deleteDirectory` - Remove directories
|
|
54
|
-
|
|
55
46
|
### Utilities
|
|
56
47
|
- `exec` - Execute system commands
|
|
57
|
-
- `healthCheck` - Check Perstack runtime health status
|
|
58
48
|
- `todo` - Task list management
|
|
59
49
|
- `clearTodo` - Clear task list
|
|
60
50
|
- `attemptCompletion` - Signal task completion (validates todos first)
|
package/dist/bin/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { package_default, createBaseServer } from '../chunk-
|
|
2
|
+
import { package_default, createBaseServer } from '../chunk-HXMW3IUI.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import { z } from 'zod/v4';
|
|
2
|
+
import { realpathSync, existsSync } from 'fs';
|
|
3
|
+
import fs, { constants, stat, mkdir, open, lstat } from 'fs/promises';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
import path, { dirname } from 'path';
|
|
6
|
+
import { execFile } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { getFilteredEnv } from '@perstack/core';
|
|
9
|
+
import mime from 'mime-types';
|
|
10
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
11
|
+
|
|
12
|
+
// package.json
|
|
13
|
+
var package_default = {
|
|
14
|
+
name: "@perstack/base",
|
|
15
|
+
version: "0.0.56",
|
|
16
|
+
description: "Perstack base skills for agents.",
|
|
17
|
+
author: "Wintermute Technologies, Inc.",
|
|
18
|
+
license: "Apache-2.0",
|
|
19
|
+
type: "module",
|
|
20
|
+
exports: {
|
|
21
|
+
".": "./src/index.ts"
|
|
22
|
+
},
|
|
23
|
+
publishConfig: {
|
|
24
|
+
access: "public",
|
|
25
|
+
bin: {
|
|
26
|
+
"@perstack/base": "dist/bin/server.js"
|
|
27
|
+
},
|
|
28
|
+
exports: {
|
|
29
|
+
".": "./dist/src/index.js"
|
|
30
|
+
},
|
|
31
|
+
types: {
|
|
32
|
+
".": "./dist/src/index.d.ts"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
files: [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
scripts: {
|
|
39
|
+
clean: "rm -rf dist",
|
|
40
|
+
build: "pnpm run clean && tsup",
|
|
41
|
+
typecheck: "tsc --noEmit"
|
|
42
|
+
},
|
|
43
|
+
dependencies: {
|
|
44
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
45
|
+
"@perstack/core": "workspace:*",
|
|
46
|
+
commander: "^14.0.3",
|
|
47
|
+
"mime-types": "^3.0.2",
|
|
48
|
+
zod: "^4.3.6"
|
|
49
|
+
},
|
|
50
|
+
devDependencies: {
|
|
51
|
+
"@tsconfig/node22": "^22.0.5",
|
|
52
|
+
"@types/mime-types": "^3.0.1",
|
|
53
|
+
"@types/node": "^25.2.3",
|
|
54
|
+
tsup: "^8.5.1",
|
|
55
|
+
typescript: "^5.9.3",
|
|
56
|
+
vitest: "^4.0.18"
|
|
57
|
+
},
|
|
58
|
+
engines: {
|
|
59
|
+
node: ">=22.0.0"
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/lib/tool-result.ts
|
|
64
|
+
function successToolResult(result) {
|
|
65
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
66
|
+
}
|
|
67
|
+
function errorToolResult(e) {
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text: JSON.stringify({ error: e.name, message: e.message }) }]
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
var Todo = class {
|
|
73
|
+
currentTodoId = 0;
|
|
74
|
+
todos = [];
|
|
75
|
+
processTodo(input) {
|
|
76
|
+
const { newTodos, completedTodos } = input;
|
|
77
|
+
if (newTodos) {
|
|
78
|
+
this.todos.push(
|
|
79
|
+
...newTodos.map((title) => ({ id: this.currentTodoId++, title, completed: false }))
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
if (completedTodos) {
|
|
83
|
+
this.todos = this.todos.map((todo2) => ({
|
|
84
|
+
...todo2,
|
|
85
|
+
completed: todo2.completed || completedTodos.includes(todo2.id)
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
todos: this.todos
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
clearTodo() {
|
|
93
|
+
this.todos = [];
|
|
94
|
+
this.currentTodoId = 0;
|
|
95
|
+
return {
|
|
96
|
+
todos: this.todos
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
var todoSingleton = new Todo();
|
|
101
|
+
async function todo(input) {
|
|
102
|
+
return todoSingleton.processTodo(input);
|
|
103
|
+
}
|
|
104
|
+
async function clearTodo() {
|
|
105
|
+
return todoSingleton.clearTodo();
|
|
106
|
+
}
|
|
107
|
+
function getRemainingTodos() {
|
|
108
|
+
return todoSingleton.todos.filter((t) => !t.completed);
|
|
109
|
+
}
|
|
110
|
+
function registerTodo(server) {
|
|
111
|
+
server.registerTool(
|
|
112
|
+
"todo",
|
|
113
|
+
{
|
|
114
|
+
title: "todo",
|
|
115
|
+
description: "Manage a todo list: add tasks and mark them completed.",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
newTodos: z.array(z.string()).describe("New todos to add").optional(),
|
|
118
|
+
completedTodos: z.array(z.number()).describe("Todo ids that are completed").optional()
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
async (input) => {
|
|
122
|
+
try {
|
|
123
|
+
return successToolResult(await todo(input));
|
|
124
|
+
} catch (e) {
|
|
125
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
function registerClearTodo(server) {
|
|
132
|
+
server.registerTool(
|
|
133
|
+
"clearTodo",
|
|
134
|
+
{
|
|
135
|
+
title: "clearTodo",
|
|
136
|
+
description: "Clear all todos.",
|
|
137
|
+
inputSchema: {}
|
|
138
|
+
},
|
|
139
|
+
async () => {
|
|
140
|
+
try {
|
|
141
|
+
return successToolResult(await clearTodo());
|
|
142
|
+
} catch (e) {
|
|
143
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
144
|
+
throw e;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/tools/attempt-completion.ts
|
|
151
|
+
async function attemptCompletion() {
|
|
152
|
+
const remainingTodos = getRemainingTodos();
|
|
153
|
+
if (remainingTodos.length > 0) {
|
|
154
|
+
return { remainingTodos };
|
|
155
|
+
}
|
|
156
|
+
return {};
|
|
157
|
+
}
|
|
158
|
+
function registerAttemptCompletion(server) {
|
|
159
|
+
server.registerTool(
|
|
160
|
+
"attemptCompletion",
|
|
161
|
+
{
|
|
162
|
+
title: "Attempt completion",
|
|
163
|
+
description: "Signal task completion. Validates all todos are complete before ending.",
|
|
164
|
+
inputSchema: {}
|
|
165
|
+
},
|
|
166
|
+
async () => {
|
|
167
|
+
try {
|
|
168
|
+
return successToolResult(await attemptCompletion());
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
171
|
+
throw e;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
var workspacePath = realpathSync(expandHome(process.cwd()));
|
|
177
|
+
function expandHome(filepath) {
|
|
178
|
+
if (filepath.startsWith("~/") || filepath === "~") {
|
|
179
|
+
return path.join(os.homedir(), filepath.slice(1));
|
|
180
|
+
}
|
|
181
|
+
return filepath;
|
|
182
|
+
}
|
|
183
|
+
async function validatePath(requestedPath) {
|
|
184
|
+
const expandedPath = expandHome(requestedPath);
|
|
185
|
+
const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath);
|
|
186
|
+
const perstackDir = `${workspacePath}/perstack`.toLowerCase();
|
|
187
|
+
if (absolute.toLowerCase() === perstackDir || absolute.toLowerCase().startsWith(`${perstackDir}/`)) {
|
|
188
|
+
throw new Error("Access denied - perstack directory is not allowed");
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
const realAbsolute = await fs.realpath(absolute);
|
|
192
|
+
if (!isWithinWorkspace(realAbsolute)) {
|
|
193
|
+
throw new Error("Access denied - symlink target outside allowed directories");
|
|
194
|
+
}
|
|
195
|
+
return realAbsolute;
|
|
196
|
+
} catch (_error) {
|
|
197
|
+
const parentDir = path.dirname(absolute);
|
|
198
|
+
try {
|
|
199
|
+
const realParentPath = await fs.realpath(parentDir);
|
|
200
|
+
if (!isWithinWorkspace(realParentPath)) {
|
|
201
|
+
throw new Error("Access denied - parent directory outside allowed directories");
|
|
202
|
+
}
|
|
203
|
+
return absolute;
|
|
204
|
+
} catch {
|
|
205
|
+
if (!isWithinWorkspace(absolute)) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Access denied - path outside allowed directories: ${absolute} not in ${workspacePath}`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function isWithinWorkspace(absolutePath) {
|
|
215
|
+
return absolutePath === workspacePath || absolutePath.startsWith(`${workspacePath}/`);
|
|
216
|
+
}
|
|
217
|
+
var O_NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
218
|
+
typeof constants.O_NOFOLLOW === "number";
|
|
219
|
+
async function checkNotSymlink(path2) {
|
|
220
|
+
const stats = await lstat(path2).catch(() => null);
|
|
221
|
+
if (stats?.isSymbolicLink()) {
|
|
222
|
+
throw new Error("Operation denied: target is a symbolic link");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function safeWriteFile(path2, data) {
|
|
226
|
+
let handle;
|
|
227
|
+
try {
|
|
228
|
+
await checkNotSymlink(path2);
|
|
229
|
+
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | O_NOFOLLOW;
|
|
230
|
+
handle = await open(path2, flags, 420);
|
|
231
|
+
await handle.writeFile(data, "utf-8");
|
|
232
|
+
} finally {
|
|
233
|
+
await handle?.close();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function safeReadFile(path2) {
|
|
237
|
+
let handle;
|
|
238
|
+
try {
|
|
239
|
+
await checkNotSymlink(path2);
|
|
240
|
+
const flags = constants.O_RDONLY | O_NOFOLLOW;
|
|
241
|
+
handle = await open(path2, flags);
|
|
242
|
+
const buffer = await handle.readFile("utf-8");
|
|
243
|
+
return buffer;
|
|
244
|
+
} finally {
|
|
245
|
+
await handle?.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/tools/edit-text-file.ts
|
|
250
|
+
async function editTextFile(input) {
|
|
251
|
+
const { path: path2, newText, oldText } = input;
|
|
252
|
+
const validatedPath = await validatePath(path2);
|
|
253
|
+
const stats = await stat(validatedPath).catch(() => null);
|
|
254
|
+
if (!stats) {
|
|
255
|
+
throw new Error(`File ${path2} does not exist.`);
|
|
256
|
+
}
|
|
257
|
+
if (!(stats.mode & 128)) {
|
|
258
|
+
throw new Error(`File ${path2} is not writable`);
|
|
259
|
+
}
|
|
260
|
+
await applyFileEdit(validatedPath, newText, oldText);
|
|
261
|
+
return {
|
|
262
|
+
path: validatedPath,
|
|
263
|
+
newText,
|
|
264
|
+
oldText
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function normalizeLineEndings(text) {
|
|
268
|
+
return text.replace(/\r\n/g, "\n");
|
|
269
|
+
}
|
|
270
|
+
async function applyFileEdit(filePath, newText, oldText) {
|
|
271
|
+
const content = normalizeLineEndings(await safeReadFile(filePath));
|
|
272
|
+
const normalizedOld = normalizeLineEndings(oldText);
|
|
273
|
+
const normalizedNew = normalizeLineEndings(newText);
|
|
274
|
+
if (!content.includes(normalizedOld)) {
|
|
275
|
+
throw new Error(`Could not find exact match for oldText in file ${filePath}`);
|
|
276
|
+
}
|
|
277
|
+
const modifiedContent = content.replace(normalizedOld, normalizedNew);
|
|
278
|
+
await safeWriteFile(filePath, modifiedContent);
|
|
279
|
+
}
|
|
280
|
+
function registerEditTextFile(server) {
|
|
281
|
+
server.registerTool(
|
|
282
|
+
"editTextFile",
|
|
283
|
+
{
|
|
284
|
+
title: "Edit text file",
|
|
285
|
+
description: "Replace exact text in an existing file. Normalizes line endings (CRLF \u2192 LF).",
|
|
286
|
+
inputSchema: {
|
|
287
|
+
path: z.string().describe("Target file path to edit."),
|
|
288
|
+
newText: z.string().describe("Text to replace with."),
|
|
289
|
+
oldText: z.string().describe("Exact text to find and replace.")
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
async (input) => {
|
|
293
|
+
try {
|
|
294
|
+
return successToolResult(await editTextFile(input));
|
|
295
|
+
} catch (e) {
|
|
296
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
297
|
+
throw e;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
var execFileAsync = promisify(execFile);
|
|
303
|
+
function isExecError(error) {
|
|
304
|
+
return error instanceof Error && "code" in error;
|
|
305
|
+
}
|
|
306
|
+
async function exec(input) {
|
|
307
|
+
const validatedCwd = await validatePath(input.cwd);
|
|
308
|
+
const { stdout, stderr } = await execFileAsync(input.command, input.args, {
|
|
309
|
+
cwd: validatedCwd,
|
|
310
|
+
env: getFilteredEnv(input.env),
|
|
311
|
+
timeout: input.timeout,
|
|
312
|
+
maxBuffer: 10 * 1024 * 1024
|
|
313
|
+
});
|
|
314
|
+
let output = "";
|
|
315
|
+
if (input.stdout) {
|
|
316
|
+
output += stdout;
|
|
317
|
+
}
|
|
318
|
+
if (input.stderr) {
|
|
319
|
+
output += stderr;
|
|
320
|
+
}
|
|
321
|
+
if (!output.trim()) {
|
|
322
|
+
output = "Command executed successfully, but produced no output.";
|
|
323
|
+
}
|
|
324
|
+
return { output };
|
|
325
|
+
}
|
|
326
|
+
function registerExec(server) {
|
|
327
|
+
server.registerTool(
|
|
328
|
+
"exec",
|
|
329
|
+
{
|
|
330
|
+
title: "Execute Command",
|
|
331
|
+
description: "Execute a system command. Returns stdout/stderr.",
|
|
332
|
+
inputSchema: {
|
|
333
|
+
command: z.string().describe("The command to execute"),
|
|
334
|
+
args: z.array(z.string()).describe("The arguments to pass to the command"),
|
|
335
|
+
env: z.record(z.string(), z.string()).describe("The environment variables to set"),
|
|
336
|
+
cwd: z.string().describe("The working directory to execute the command in"),
|
|
337
|
+
stdout: z.boolean().describe("Whether to capture the standard output"),
|
|
338
|
+
stderr: z.boolean().describe("Whether to capture the standard error"),
|
|
339
|
+
timeout: z.number().optional().default(6e4).describe("Timeout in milliseconds (default: 60000)")
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
async (input) => {
|
|
343
|
+
try {
|
|
344
|
+
return successToolResult(await exec(input));
|
|
345
|
+
} catch (error) {
|
|
346
|
+
let message;
|
|
347
|
+
let stdout;
|
|
348
|
+
let stderr;
|
|
349
|
+
if (isExecError(error)) {
|
|
350
|
+
if ((error.killed || error.signal === "SIGTERM") && typeof input.timeout === "number") {
|
|
351
|
+
message = `Command timed out after ${input.timeout}ms.`;
|
|
352
|
+
} else if (error.message.includes("timeout")) {
|
|
353
|
+
message = `Command timed out after ${input.timeout}ms.`;
|
|
354
|
+
} else {
|
|
355
|
+
message = error.message;
|
|
356
|
+
}
|
|
357
|
+
stdout = error.stdout;
|
|
358
|
+
stderr = error.stderr;
|
|
359
|
+
} else if (error instanceof Error) {
|
|
360
|
+
message = error.message;
|
|
361
|
+
} else {
|
|
362
|
+
message = "An unknown error occurred.";
|
|
363
|
+
}
|
|
364
|
+
const result = { error: message };
|
|
365
|
+
if (stdout && input.stdout) {
|
|
366
|
+
result.stdout = stdout;
|
|
367
|
+
}
|
|
368
|
+
if (stderr && input.stderr) {
|
|
369
|
+
result.stderr = stderr;
|
|
370
|
+
}
|
|
371
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
var MAX_IMAGE_SIZE = 15 * 1024 * 1024;
|
|
377
|
+
async function readImageFile(input) {
|
|
378
|
+
const { path: path2 } = input;
|
|
379
|
+
const validatedPath = await validatePath(path2);
|
|
380
|
+
const isFile = existsSync(validatedPath);
|
|
381
|
+
if (!isFile) {
|
|
382
|
+
throw new Error(`File ${path2} does not exist.`);
|
|
383
|
+
}
|
|
384
|
+
const mimeType = mime.lookup(validatedPath);
|
|
385
|
+
if (!mimeType || !["image/png", "image/jpeg", "image/gif", "image/webp"].includes(mimeType)) {
|
|
386
|
+
throw new Error(`File ${path2} is not supported.`);
|
|
387
|
+
}
|
|
388
|
+
const fileStats = await stat(validatedPath);
|
|
389
|
+
const fileSizeMB = fileStats.size / (1024 * 1024);
|
|
390
|
+
if (fileStats.size > MAX_IMAGE_SIZE) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`Image file too large (${fileSizeMB.toFixed(1)}MB). Maximum supported size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB. Please use a smaller image file.`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
path: validatedPath,
|
|
397
|
+
mimeType,
|
|
398
|
+
size: fileStats.size
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
function registerReadImageFile(server) {
|
|
402
|
+
server.registerTool(
|
|
403
|
+
"readImageFile",
|
|
404
|
+
{
|
|
405
|
+
title: "Read image file",
|
|
406
|
+
description: "Read an image file as base64. Supports PNG, JPEG, GIF, WebP. Max 15MB.",
|
|
407
|
+
inputSchema: {
|
|
408
|
+
path: z.string()
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
async (input) => {
|
|
412
|
+
try {
|
|
413
|
+
return successToolResult(await readImageFile(input));
|
|
414
|
+
} catch (e) {
|
|
415
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
416
|
+
throw e;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
var MAX_PDF_SIZE = 30 * 1024 * 1024;
|
|
422
|
+
async function readPdfFile(input) {
|
|
423
|
+
const { path: path2 } = input;
|
|
424
|
+
const validatedPath = await validatePath(path2);
|
|
425
|
+
const isFile = existsSync(validatedPath);
|
|
426
|
+
if (!isFile) {
|
|
427
|
+
throw new Error(`File ${path2} does not exist.`);
|
|
428
|
+
}
|
|
429
|
+
const mimeType = mime.lookup(validatedPath);
|
|
430
|
+
if (mimeType !== "application/pdf") {
|
|
431
|
+
throw new Error(`File ${path2} is not a PDF file.`);
|
|
432
|
+
}
|
|
433
|
+
const fileStats = await stat(validatedPath);
|
|
434
|
+
const fileSizeMB = fileStats.size / (1024 * 1024);
|
|
435
|
+
if (fileStats.size > MAX_PDF_SIZE) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
`PDF file too large (${fileSizeMB.toFixed(1)}MB). Maximum supported size is ${MAX_PDF_SIZE / (1024 * 1024)}MB. Please use a smaller PDF file.`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
path: validatedPath,
|
|
442
|
+
mimeType,
|
|
443
|
+
size: fileStats.size
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function registerReadPdfFile(server) {
|
|
447
|
+
server.registerTool(
|
|
448
|
+
"readPdfFile",
|
|
449
|
+
{
|
|
450
|
+
title: "Read PDF file",
|
|
451
|
+
description: "Read a PDF file as base64. Max 30MB.",
|
|
452
|
+
inputSchema: {
|
|
453
|
+
path: z.string()
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
async (input) => {
|
|
457
|
+
try {
|
|
458
|
+
return successToolResult(await readPdfFile(input));
|
|
459
|
+
} catch (e) {
|
|
460
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
461
|
+
throw e;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
async function readTextFile(input) {
|
|
467
|
+
const { path: path2, from, to } = input;
|
|
468
|
+
const validatedPath = await validatePath(path2);
|
|
469
|
+
const stats = await stat(validatedPath).catch(() => null);
|
|
470
|
+
if (!stats) {
|
|
471
|
+
throw new Error(`File ${path2} does not exist.`);
|
|
472
|
+
}
|
|
473
|
+
const fileContent = await safeReadFile(validatedPath);
|
|
474
|
+
const lines = fileContent.split("\n");
|
|
475
|
+
const fromLine = from ?? 0;
|
|
476
|
+
const toLine = to ?? lines.length;
|
|
477
|
+
const selectedLines = lines.slice(fromLine, toLine);
|
|
478
|
+
const content = selectedLines.join("\n");
|
|
479
|
+
return {
|
|
480
|
+
path: path2,
|
|
481
|
+
content,
|
|
482
|
+
from: fromLine,
|
|
483
|
+
to: toLine
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function registerReadTextFile(server) {
|
|
487
|
+
server.registerTool(
|
|
488
|
+
"readTextFile",
|
|
489
|
+
{
|
|
490
|
+
title: "Read text file",
|
|
491
|
+
description: "Read a UTF-8 text file. Supports partial reading via line ranges.",
|
|
492
|
+
inputSchema: {
|
|
493
|
+
path: z.string(),
|
|
494
|
+
from: z.number().optional().describe("The line number to start reading from."),
|
|
495
|
+
to: z.number().optional().describe("The line number to stop reading at.")
|
|
496
|
+
}
|
|
497
|
+
},
|
|
498
|
+
async (input) => {
|
|
499
|
+
try {
|
|
500
|
+
return successToolResult(await readTextFile(input));
|
|
501
|
+
} catch (e) {
|
|
502
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
503
|
+
throw e;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
async function writeTextFile(input) {
|
|
509
|
+
const { path: path2, text } = input;
|
|
510
|
+
const validatedPath = await validatePath(path2);
|
|
511
|
+
const stats = await stat(validatedPath).catch(() => null);
|
|
512
|
+
if (stats && !(stats.mode & 128)) {
|
|
513
|
+
throw new Error(`File ${path2} is not writable`);
|
|
514
|
+
}
|
|
515
|
+
const dir = dirname(validatedPath);
|
|
516
|
+
await mkdir(dir, { recursive: true });
|
|
517
|
+
await safeWriteFile(validatedPath, text);
|
|
518
|
+
return {
|
|
519
|
+
path: validatedPath,
|
|
520
|
+
text
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function registerWriteTextFile(server) {
|
|
524
|
+
server.registerTool(
|
|
525
|
+
"writeTextFile",
|
|
526
|
+
{
|
|
527
|
+
title: "writeTextFile",
|
|
528
|
+
description: "Create or overwrite a UTF-8 text file. Creates parent directories as needed.",
|
|
529
|
+
inputSchema: {
|
|
530
|
+
path: z.string().describe("Target file path (relative or absolute)."),
|
|
531
|
+
text: z.string().describe("Text to write to the file.")
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
async (input) => {
|
|
535
|
+
try {
|
|
536
|
+
return successToolResult(await writeTextFile(input));
|
|
537
|
+
} catch (e) {
|
|
538
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
539
|
+
throw e;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
var BASE_SKILL_NAME = package_default.name;
|
|
545
|
+
var BASE_SKILL_VERSION = package_default.version;
|
|
546
|
+
function registerAllTools(server) {
|
|
547
|
+
registerAttemptCompletion(server);
|
|
548
|
+
registerTodo(server);
|
|
549
|
+
registerClearTodo(server);
|
|
550
|
+
registerExec(server);
|
|
551
|
+
registerReadTextFile(server);
|
|
552
|
+
registerReadImageFile(server);
|
|
553
|
+
registerReadPdfFile(server);
|
|
554
|
+
registerWriteTextFile(server);
|
|
555
|
+
registerEditTextFile(server);
|
|
556
|
+
}
|
|
557
|
+
function createBaseServer() {
|
|
558
|
+
const server = new McpServer(
|
|
559
|
+
{
|
|
560
|
+
name: BASE_SKILL_NAME,
|
|
561
|
+
version: BASE_SKILL_VERSION
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
capabilities: {
|
|
565
|
+
tools: {}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
registerAllTools(server);
|
|
570
|
+
return server;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export { BASE_SKILL_NAME, BASE_SKILL_VERSION, attemptCompletion, clearTodo, createBaseServer, editTextFile, errorToolResult, exec, getRemainingTodos, package_default, readImageFile, readPdfFile, readTextFile, registerAllTools, registerAttemptCompletion, registerClearTodo, registerEditTextFile, registerExec, registerReadImageFile, registerReadPdfFile, registerReadTextFile, registerTodo, registerWriteTextFile, successToolResult, todo, validatePath, writeTextFile };
|
|
574
|
+
//# sourceMappingURL=chunk-HXMW3IUI.js.map
|
|
575
|
+
//# sourceMappingURL=chunk-HXMW3IUI.js.map
|