@perstack/base 0.0.59 → 0.0.61
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/dist/bin/server.d.ts +1 -1
- package/dist/bin/server.js +15 -12
- package/dist/bin/server.js.map +1 -1
- package/dist/server-BM7K7dr8.js +588 -0
- package/dist/server-BM7K7dr8.js.map +1 -0
- package/dist/src/index.d.ts +125 -87
- package/dist/src/index.js +3 -3
- package/package.json +4 -4
- package/dist/chunk-SPQIG63O.js +0 -686
- package/dist/chunk-SPQIG63O.js.map +0 -1
- package/dist/src/index.js.map +0 -1
package/dist/bin/server.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
export { };
|
package/dist/bin/server.js
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { StdioServerTransport } from
|
|
4
|
-
import { Command } from
|
|
2
|
+
import { F as version, N as description, P as name, r as createBaseServer } from "../server-BM7K7dr8.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { Command } from "commander";
|
|
5
5
|
|
|
6
|
+
//#region bin/server.ts
|
|
6
7
|
async function main() {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const program = new Command();
|
|
9
|
+
program.name(name).description(description).version(version, "-v, --version", "display the version number").action(async () => {
|
|
10
|
+
const server = createBaseServer();
|
|
11
|
+
const transport = new StdioServerTransport();
|
|
12
|
+
console.error("Running @perstack/base version", version);
|
|
13
|
+
await server.connect(transport);
|
|
14
|
+
});
|
|
15
|
+
program.parse();
|
|
15
16
|
}
|
|
16
17
|
main();
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { };
|
|
18
21
|
//# sourceMappingURL=server.js.map
|
package/dist/bin/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"
|
|
1
|
+
{"version":3,"file":"server.js","names":["packageJson.name","packageJson.description","packageJson.version"],"sources":["../../bin/server.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\"\nimport { Command } from \"commander\"\nimport packageJson from \"../package.json\" with { type: \"json\" }\nimport { createBaseServer } from \"../src/server.js\"\n\nasync function main() {\n const program = new Command()\n program\n .name(packageJson.name)\n .description(packageJson.description)\n .version(packageJson.version, \"-v, --version\", \"display the version number\")\n .action(async () => {\n const server = createBaseServer()\n const transport = new StdioServerTransport()\n console.error(\"Running @perstack/base version\", packageJson.version)\n await server.connect(transport)\n })\n program.parse()\n}\nmain()\n"],"mappings":";;;;;;AAOA,eAAe,OAAO;CACpB,MAAM,UAAU,IAAI,SAAS;AAC7B,SACG,KAAKA,KAAiB,CACtB,YAAYC,YAAwB,CACpC,QAAQC,SAAqB,iBAAiB,6BAA6B,CAC3E,OAAO,YAAY;EAClB,MAAM,SAAS,kBAAkB;EACjC,MAAM,YAAY,IAAI,sBAAsB;AAC5C,UAAQ,MAAM,kCAAkCA,QAAoB;AACpE,QAAM,OAAO,QAAQ,UAAU;GAC/B;AACJ,SAAQ,OAAO;;AAEjB,MAAM"}
|
|
@@ -0,0 +1,588 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
import fs, { constants, lstat, mkdir, open, stat } from "node:fs/promises";
|
|
4
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path, { dirname, extname } from "node:path";
|
|
7
|
+
import { execFile } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { getFilteredEnv } from "@perstack/core";
|
|
10
|
+
|
|
11
|
+
//#region package.json
|
|
12
|
+
var name = "@perstack/base";
|
|
13
|
+
var version = "0.0.61";
|
|
14
|
+
var description = "Perstack base skills for agents.";
|
|
15
|
+
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/lib/tool-result.ts
|
|
18
|
+
function successToolResult(result) {
|
|
19
|
+
return { content: [{
|
|
20
|
+
type: "text",
|
|
21
|
+
text: JSON.stringify(result)
|
|
22
|
+
}] };
|
|
23
|
+
}
|
|
24
|
+
function errorToolResult(e) {
|
|
25
|
+
return { content: [{
|
|
26
|
+
type: "text",
|
|
27
|
+
text: JSON.stringify({
|
|
28
|
+
error: e.name,
|
|
29
|
+
message: e.message
|
|
30
|
+
})
|
|
31
|
+
}] };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/tools/todo.ts
|
|
36
|
+
var Todo = class {
|
|
37
|
+
currentTodoId = 0;
|
|
38
|
+
todos = [];
|
|
39
|
+
processTodo(input) {
|
|
40
|
+
const { newTodos, completedTodos } = input;
|
|
41
|
+
if (newTodos) this.todos.push(...newTodos.map((title) => ({
|
|
42
|
+
id: this.currentTodoId++,
|
|
43
|
+
title,
|
|
44
|
+
completed: false
|
|
45
|
+
})));
|
|
46
|
+
if (completedTodos) this.todos = this.todos.map((todo) => ({
|
|
47
|
+
...todo,
|
|
48
|
+
completed: todo.completed || completedTodos.includes(todo.id)
|
|
49
|
+
}));
|
|
50
|
+
return { todos: this.todos };
|
|
51
|
+
}
|
|
52
|
+
clearTodo() {
|
|
53
|
+
this.todos = [];
|
|
54
|
+
this.currentTodoId = 0;
|
|
55
|
+
return { todos: this.todos };
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const todoSingleton = new Todo();
|
|
59
|
+
async function todo(input) {
|
|
60
|
+
return todoSingleton.processTodo(input);
|
|
61
|
+
}
|
|
62
|
+
async function clearTodo() {
|
|
63
|
+
return todoSingleton.clearTodo();
|
|
64
|
+
}
|
|
65
|
+
function getRemainingTodos() {
|
|
66
|
+
return todoSingleton.todos.filter((t) => !t.completed);
|
|
67
|
+
}
|
|
68
|
+
function registerTodo(server) {
|
|
69
|
+
server.registerTool("todo", {
|
|
70
|
+
title: "todo",
|
|
71
|
+
description: "Manage a todo list: add tasks and mark them completed.",
|
|
72
|
+
inputSchema: {
|
|
73
|
+
newTodos: z.array(z.string()).describe("New todos to add").optional(),
|
|
74
|
+
completedTodos: z.array(z.number()).describe("Todo ids that are completed").optional()
|
|
75
|
+
}
|
|
76
|
+
}, async (input) => {
|
|
77
|
+
try {
|
|
78
|
+
return successToolResult(await todo(input));
|
|
79
|
+
} catch (e) {
|
|
80
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function registerClearTodo(server) {
|
|
86
|
+
server.registerTool("clearTodo", {
|
|
87
|
+
title: "clearTodo",
|
|
88
|
+
description: "Clear all todos.",
|
|
89
|
+
inputSchema: {}
|
|
90
|
+
}, async () => {
|
|
91
|
+
try {
|
|
92
|
+
return successToolResult(await clearTodo());
|
|
93
|
+
} catch (e) {
|
|
94
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
95
|
+
throw e;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/tools/attempt-completion.ts
|
|
102
|
+
async function attemptCompletion() {
|
|
103
|
+
const remainingTodos = getRemainingTodos();
|
|
104
|
+
if (remainingTodos.length > 0) return { remainingTodos };
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
function registerAttemptCompletion(server) {
|
|
108
|
+
server.registerTool("attemptCompletion", {
|
|
109
|
+
title: "Attempt completion",
|
|
110
|
+
description: "Signal task completion. Validates all todos are complete before ending.",
|
|
111
|
+
inputSchema: {}
|
|
112
|
+
}, async () => {
|
|
113
|
+
try {
|
|
114
|
+
return successToolResult(await attemptCompletion());
|
|
115
|
+
} catch (e) {
|
|
116
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
117
|
+
throw e;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/lib/path.ts
|
|
124
|
+
const workspacePath = realpathSync(expandHome(process.cwd()));
|
|
125
|
+
function expandHome(filepath) {
|
|
126
|
+
if (filepath.startsWith("~/") || filepath === "~") return path.join(os.homedir(), filepath.slice(1));
|
|
127
|
+
return filepath;
|
|
128
|
+
}
|
|
129
|
+
async function validatePath(requestedPath) {
|
|
130
|
+
const expandedPath = expandHome(requestedPath);
|
|
131
|
+
const absolute = path.isAbsolute(expandedPath) ? path.resolve(expandedPath) : path.resolve(process.cwd(), expandedPath);
|
|
132
|
+
const perstackDir = `${workspacePath}/perstack`.toLowerCase();
|
|
133
|
+
if (absolute.toLowerCase() === perstackDir || absolute.toLowerCase().startsWith(`${perstackDir}/`)) throw new Error("Access denied - perstack directory is not allowed");
|
|
134
|
+
try {
|
|
135
|
+
const realAbsolute = await fs.realpath(absolute);
|
|
136
|
+
if (!isWithinWorkspace(realAbsolute)) throw new Error("Access denied - symlink target outside allowed directories");
|
|
137
|
+
return realAbsolute;
|
|
138
|
+
} catch (_error) {
|
|
139
|
+
const parentDir = path.dirname(absolute);
|
|
140
|
+
try {
|
|
141
|
+
if (!isWithinWorkspace(await fs.realpath(parentDir))) throw new Error("Access denied - parent directory outside allowed directories");
|
|
142
|
+
return absolute;
|
|
143
|
+
} catch {
|
|
144
|
+
if (!isWithinWorkspace(absolute)) throw new Error(`Access denied - path outside allowed directories: ${absolute} not in ${workspacePath}`);
|
|
145
|
+
throw new Error(`Parent directory does not exist: ${parentDir}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function isWithinWorkspace(absolutePath) {
|
|
150
|
+
return absolutePath === workspacePath || absolutePath.startsWith(`${workspacePath}/`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
//#region src/lib/safe-file.ts
|
|
155
|
+
const O_NOFOLLOW = constants.O_NOFOLLOW ?? 0;
|
|
156
|
+
const O_NOFOLLOW_SUPPORTED = typeof constants.O_NOFOLLOW === "number";
|
|
157
|
+
async function checkNotSymlink(path) {
|
|
158
|
+
if ((await lstat(path).catch(() => null))?.isSymbolicLink()) throw new Error("Operation denied: target is a symbolic link");
|
|
159
|
+
}
|
|
160
|
+
async function safeWriteFile(path, data) {
|
|
161
|
+
let handle;
|
|
162
|
+
try {
|
|
163
|
+
await checkNotSymlink(path);
|
|
164
|
+
handle = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | O_NOFOLLOW, 420);
|
|
165
|
+
await handle.writeFile(data, "utf-8");
|
|
166
|
+
} finally {
|
|
167
|
+
await handle?.close();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
async function safeReadFile(path) {
|
|
171
|
+
let handle;
|
|
172
|
+
try {
|
|
173
|
+
await checkNotSymlink(path);
|
|
174
|
+
handle = await open(path, constants.O_RDONLY | O_NOFOLLOW);
|
|
175
|
+
return await handle.readFile("utf-8");
|
|
176
|
+
} finally {
|
|
177
|
+
await handle?.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region src/tools/edit-text-file.ts
|
|
183
|
+
async function editTextFile(input) {
|
|
184
|
+
const { path, newText, oldText } = input;
|
|
185
|
+
const validatedPath = await validatePath(path);
|
|
186
|
+
const stats = await stat(validatedPath).catch(() => null);
|
|
187
|
+
if (!stats) throw new Error(`File ${path} does not exist.`);
|
|
188
|
+
if (!(stats.mode & 128)) throw new Error(`File ${path} is not writable`);
|
|
189
|
+
await applyFileEdit(validatedPath, newText, oldText);
|
|
190
|
+
return {
|
|
191
|
+
path: validatedPath,
|
|
192
|
+
newText,
|
|
193
|
+
oldText
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function normalizeLineEndings(text) {
|
|
197
|
+
return text.replace(/\r\n/g, "\n");
|
|
198
|
+
}
|
|
199
|
+
async function applyFileEdit(filePath, newText, oldText) {
|
|
200
|
+
const content = normalizeLineEndings(await safeReadFile(filePath));
|
|
201
|
+
const normalizedOld = normalizeLineEndings(oldText);
|
|
202
|
+
const normalizedNew = normalizeLineEndings(newText);
|
|
203
|
+
if (!content.includes(normalizedOld)) throw new Error(`Could not find exact match for oldText in file ${filePath}`);
|
|
204
|
+
await safeWriteFile(filePath, content.replace(normalizedOld, normalizedNew));
|
|
205
|
+
}
|
|
206
|
+
function registerEditTextFile(server) {
|
|
207
|
+
server.registerTool("editTextFile", {
|
|
208
|
+
title: "Edit text file",
|
|
209
|
+
description: "Replace exact text in an existing file. Normalizes line endings (CRLF → LF).",
|
|
210
|
+
inputSchema: {
|
|
211
|
+
path: z.string().describe("Target file path to edit."),
|
|
212
|
+
newText: z.string().describe("Text to replace with."),
|
|
213
|
+
oldText: z.string().describe("Exact text to find and replace.")
|
|
214
|
+
}
|
|
215
|
+
}, async (input) => {
|
|
216
|
+
try {
|
|
217
|
+
return successToolResult(await editTextFile(input));
|
|
218
|
+
} catch (e) {
|
|
219
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
220
|
+
throw e;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/tools/exec.ts
|
|
227
|
+
const execFileAsync = promisify(execFile);
|
|
228
|
+
function isExecError(error) {
|
|
229
|
+
return error instanceof Error && "code" in error;
|
|
230
|
+
}
|
|
231
|
+
async function exec(input) {
|
|
232
|
+
const validatedCwd = await validatePath(input.cwd);
|
|
233
|
+
const { stdout, stderr } = await execFileAsync(input.command, input.args, {
|
|
234
|
+
cwd: validatedCwd,
|
|
235
|
+
env: getFilteredEnv(input.env),
|
|
236
|
+
timeout: input.timeout,
|
|
237
|
+
maxBuffer: 10 * 1024 * 1024
|
|
238
|
+
});
|
|
239
|
+
let output = "";
|
|
240
|
+
if (input.stdout) output += stdout;
|
|
241
|
+
if (input.stderr) output += stderr;
|
|
242
|
+
if (!output.trim()) output = "Command executed successfully, but produced no output.";
|
|
243
|
+
return { output };
|
|
244
|
+
}
|
|
245
|
+
function registerExec(server) {
|
|
246
|
+
server.registerTool("exec", {
|
|
247
|
+
title: "Execute Command",
|
|
248
|
+
description: "Execute a system command. Returns stdout/stderr.",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
command: z.string().describe("The command to execute"),
|
|
251
|
+
args: z.array(z.string()).describe("The arguments to pass to the command"),
|
|
252
|
+
env: z.record(z.string(), z.string()).describe("The environment variables to set"),
|
|
253
|
+
cwd: z.string().describe("The working directory to execute the command in"),
|
|
254
|
+
stdout: z.boolean().describe("Whether to capture the standard output"),
|
|
255
|
+
stderr: z.boolean().describe("Whether to capture the standard error"),
|
|
256
|
+
timeout: z.number().optional().default(6e4).describe("Timeout in milliseconds (default: 60000)")
|
|
257
|
+
}
|
|
258
|
+
}, async (input) => {
|
|
259
|
+
try {
|
|
260
|
+
return successToolResult(await exec(input));
|
|
261
|
+
} catch (error) {
|
|
262
|
+
let message;
|
|
263
|
+
let stdout;
|
|
264
|
+
let stderr;
|
|
265
|
+
if (isExecError(error)) {
|
|
266
|
+
if ((error.killed || error.signal === "SIGTERM") && typeof input.timeout === "number") message = `Command timed out after ${input.timeout}ms.`;
|
|
267
|
+
else if (error.message.includes("timeout")) message = `Command timed out after ${input.timeout}ms.`;
|
|
268
|
+
else message = error.message;
|
|
269
|
+
stdout = error.stdout;
|
|
270
|
+
stderr = error.stderr;
|
|
271
|
+
} else if (error instanceof Error) message = error.message;
|
|
272
|
+
else message = "An unknown error occurred.";
|
|
273
|
+
const result = { error: message };
|
|
274
|
+
if (stdout && input.stdout) result.stdout = stdout;
|
|
275
|
+
if (stderr && input.stderr) result.stderr = stderr;
|
|
276
|
+
return { content: [{
|
|
277
|
+
type: "text",
|
|
278
|
+
text: JSON.stringify(result)
|
|
279
|
+
}] };
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//#endregion
|
|
285
|
+
//#region src/lib/mime.ts
|
|
286
|
+
const MIME_TYPES = {
|
|
287
|
+
".png": "image/png",
|
|
288
|
+
".jpg": "image/jpeg",
|
|
289
|
+
".jpeg": "image/jpeg",
|
|
290
|
+
".gif": "image/gif",
|
|
291
|
+
".webp": "image/webp",
|
|
292
|
+
".pdf": "application/pdf"
|
|
293
|
+
};
|
|
294
|
+
function lookupMimeType(filePath) {
|
|
295
|
+
return MIME_TYPES[extname(filePath).toLowerCase()];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
//#endregion
|
|
299
|
+
//#region src/tools/read-image-file.ts
|
|
300
|
+
const MAX_IMAGE_SIZE = 15 * 1024 * 1024;
|
|
301
|
+
async function readImageFile(input) {
|
|
302
|
+
const { path } = input;
|
|
303
|
+
const validatedPath = await validatePath(path);
|
|
304
|
+
if (!existsSync(validatedPath)) throw new Error(`File ${path} does not exist.`);
|
|
305
|
+
const mimeType = lookupMimeType(validatedPath);
|
|
306
|
+
if (!mimeType || ![
|
|
307
|
+
"image/png",
|
|
308
|
+
"image/jpeg",
|
|
309
|
+
"image/gif",
|
|
310
|
+
"image/webp"
|
|
311
|
+
].includes(mimeType)) throw new Error(`File ${path} is not supported.`);
|
|
312
|
+
const fileStats = await stat(validatedPath);
|
|
313
|
+
const fileSizeMB = fileStats.size / (1024 * 1024);
|
|
314
|
+
if (fileStats.size > MAX_IMAGE_SIZE) throw new Error(`Image file too large (${fileSizeMB.toFixed(1)}MB). Maximum supported size is ${MAX_IMAGE_SIZE / (1024 * 1024)}MB. Please use a smaller image file.`);
|
|
315
|
+
return {
|
|
316
|
+
path: validatedPath,
|
|
317
|
+
mimeType,
|
|
318
|
+
size: fileStats.size
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function registerReadImageFile(server) {
|
|
322
|
+
server.registerTool("readImageFile", {
|
|
323
|
+
title: "Read image file",
|
|
324
|
+
description: "Read an image file as base64. Supports PNG, JPEG, GIF, WebP. Max 15MB.",
|
|
325
|
+
inputSchema: { path: z.string() }
|
|
326
|
+
}, async (input) => {
|
|
327
|
+
try {
|
|
328
|
+
return successToolResult(await readImageFile(input));
|
|
329
|
+
} catch (e) {
|
|
330
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
331
|
+
throw e;
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/tools/read-pdf-file.ts
|
|
338
|
+
const MAX_PDF_SIZE = 30 * 1024 * 1024;
|
|
339
|
+
async function readPdfFile(input) {
|
|
340
|
+
const { path } = input;
|
|
341
|
+
const validatedPath = await validatePath(path);
|
|
342
|
+
if (!existsSync(validatedPath)) throw new Error(`File ${path} does not exist.`);
|
|
343
|
+
const mimeType = lookupMimeType(validatedPath);
|
|
344
|
+
if (mimeType !== "application/pdf") throw new Error(`File ${path} is not a PDF file.`);
|
|
345
|
+
const fileStats = await stat(validatedPath);
|
|
346
|
+
const fileSizeMB = fileStats.size / (1024 * 1024);
|
|
347
|
+
if (fileStats.size > MAX_PDF_SIZE) throw new Error(`PDF file too large (${fileSizeMB.toFixed(1)}MB). Maximum supported size is ${MAX_PDF_SIZE / (1024 * 1024)}MB. Please use a smaller PDF file.`);
|
|
348
|
+
return {
|
|
349
|
+
path: validatedPath,
|
|
350
|
+
mimeType,
|
|
351
|
+
size: fileStats.size
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function registerReadPdfFile(server) {
|
|
355
|
+
server.registerTool("readPdfFile", {
|
|
356
|
+
title: "Read PDF file",
|
|
357
|
+
description: "Read a PDF file as base64. Max 30MB.",
|
|
358
|
+
inputSchema: { path: z.string() }
|
|
359
|
+
}, async (input) => {
|
|
360
|
+
try {
|
|
361
|
+
return successToolResult(await readPdfFile(input));
|
|
362
|
+
} catch (e) {
|
|
363
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
364
|
+
throw e;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/tools/read-text-file.ts
|
|
371
|
+
async function readTextFile(input) {
|
|
372
|
+
const { path, from, to } = input;
|
|
373
|
+
const validatedPath = await validatePath(path);
|
|
374
|
+
if (!await stat(validatedPath).catch(() => null)) throw new Error(`File ${path} does not exist.`);
|
|
375
|
+
const lines = (await safeReadFile(validatedPath)).split("\n");
|
|
376
|
+
const fromLine = from ?? 0;
|
|
377
|
+
const toLine = to ?? lines.length;
|
|
378
|
+
return {
|
|
379
|
+
path,
|
|
380
|
+
content: lines.slice(fromLine, toLine).join("\n"),
|
|
381
|
+
from: fromLine,
|
|
382
|
+
to: toLine
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
function registerReadTextFile(server) {
|
|
386
|
+
server.registerTool("readTextFile", {
|
|
387
|
+
title: "Read text file",
|
|
388
|
+
description: "Read a UTF-8 text file. Supports partial reading via line ranges.",
|
|
389
|
+
inputSchema: {
|
|
390
|
+
path: z.string(),
|
|
391
|
+
from: z.number().optional().describe("The line number to start reading from."),
|
|
392
|
+
to: z.number().optional().describe("The line number to stop reading at.")
|
|
393
|
+
}
|
|
394
|
+
}, async (input) => {
|
|
395
|
+
try {
|
|
396
|
+
return successToolResult(await readTextFile(input));
|
|
397
|
+
} catch (e) {
|
|
398
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
399
|
+
throw e;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region src/tools/skill-management.ts
|
|
406
|
+
function registerAddSkill(server, callbacks) {
|
|
407
|
+
server.registerTool("addSkill", {
|
|
408
|
+
title: "Add skill",
|
|
409
|
+
description: "Dynamically add an MCP skill. Returns the list of tool names provided by the new skill.",
|
|
410
|
+
inputSchema: {
|
|
411
|
+
name: z.string().describe("Unique skill name"),
|
|
412
|
+
type: z.enum(["mcpStdioSkill", "mcpSseSkill"]).describe("Skill transport type"),
|
|
413
|
+
command: z.string().optional().describe("Command to execute (for stdio skills)"),
|
|
414
|
+
packageName: z.string().optional().describe("Package name for npx/uvx (for stdio skills)"),
|
|
415
|
+
args: z.array(z.string()).optional().describe("Additional command arguments"),
|
|
416
|
+
requiredEnv: z.array(z.string()).optional().describe("Required environment variable names"),
|
|
417
|
+
endpoint: z.string().optional().describe("SSE endpoint URL (for SSE skills)"),
|
|
418
|
+
description: z.string().optional().describe("Human-readable description"),
|
|
419
|
+
rule: z.string().optional().describe("Usage rules for the LLM"),
|
|
420
|
+
pick: z.array(z.string()).optional().describe("Tool names to include (whitelist)"),
|
|
421
|
+
omit: z.array(z.string()).optional().describe("Tool names to exclude (blacklist)")
|
|
422
|
+
}
|
|
423
|
+
}, async (input) => {
|
|
424
|
+
try {
|
|
425
|
+
return successToolResult(await callbacks.addSkill(input));
|
|
426
|
+
} catch (e) {
|
|
427
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
428
|
+
throw e;
|
|
429
|
+
}
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function registerRemoveSkill(server, callbacks) {
|
|
433
|
+
server.registerTool("removeSkill", {
|
|
434
|
+
title: "Remove skill",
|
|
435
|
+
description: "Dynamically remove an MCP skill by name. Disconnects and removes the skill.",
|
|
436
|
+
inputSchema: { skillName: z.string().describe("Name of the skill to remove") }
|
|
437
|
+
}, async (input) => {
|
|
438
|
+
try {
|
|
439
|
+
await callbacks.removeSkill(input.skillName);
|
|
440
|
+
return successToolResult({ removed: input.skillName });
|
|
441
|
+
} catch (e) {
|
|
442
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
443
|
+
throw e;
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
function registerAddDelegate(server, callbacks) {
|
|
448
|
+
server.registerTool("addDelegate", {
|
|
449
|
+
title: "Add delegate",
|
|
450
|
+
description: "Dynamically add a delegate expert. Returns the delegate tool name so you know what to call.",
|
|
451
|
+
inputSchema: { expertKey: z.string().describe("Key of the expert to add as a delegate") }
|
|
452
|
+
}, async (input) => {
|
|
453
|
+
try {
|
|
454
|
+
return successToolResult(await callbacks.addDelegate(input.expertKey));
|
|
455
|
+
} catch (e) {
|
|
456
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
457
|
+
throw e;
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
function registerRemoveDelegate(server, callbacks) {
|
|
462
|
+
server.registerTool("removeDelegate", {
|
|
463
|
+
title: "Remove delegate",
|
|
464
|
+
description: "Dynamically remove a delegate expert by name.",
|
|
465
|
+
inputSchema: { expertName: z.string().describe("Name of the delegate expert to remove") }
|
|
466
|
+
}, async (input) => {
|
|
467
|
+
try {
|
|
468
|
+
await callbacks.removeDelegate(input.expertName);
|
|
469
|
+
return successToolResult({ removed: input.expertName });
|
|
470
|
+
} catch (e) {
|
|
471
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
472
|
+
throw e;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function registerCreateExpert(server, callbacks) {
|
|
477
|
+
server.registerTool("createExpert", {
|
|
478
|
+
title: "Create expert",
|
|
479
|
+
description: "Dynamically create an expert definition in memory. Returns the expert key so you can add it as a delegate.",
|
|
480
|
+
inputSchema: {
|
|
481
|
+
key: z.string().describe("Unique expert key (kebab-case)"),
|
|
482
|
+
instruction: z.string().describe("System instruction for the expert"),
|
|
483
|
+
description: z.string().optional().describe("Human-readable description"),
|
|
484
|
+
version: z.string().optional().describe("Semantic version (defaults to 1.0.0)"),
|
|
485
|
+
skills: z.record(z.string(), z.object({
|
|
486
|
+
type: z.enum(["mcpStdioSkill", "mcpSseSkill"]).describe("Skill transport type"),
|
|
487
|
+
command: z.string().optional().describe("Command to execute (for stdio skills)"),
|
|
488
|
+
packageName: z.string().optional().describe("Package name for npx/uvx (for stdio skills)"),
|
|
489
|
+
args: z.array(z.string()).optional().describe("Additional command arguments"),
|
|
490
|
+
requiredEnv: z.array(z.string()).optional().describe("Required environment variable names"),
|
|
491
|
+
endpoint: z.string().optional().describe("SSE endpoint URL (for SSE skills)"),
|
|
492
|
+
description: z.string().optional().describe("Human-readable description"),
|
|
493
|
+
rule: z.string().optional().describe("Usage rules for the LLM"),
|
|
494
|
+
pick: z.array(z.string()).optional().describe("Tool names to include (whitelist)"),
|
|
495
|
+
omit: z.array(z.string()).optional().describe("Tool names to exclude (blacklist)"),
|
|
496
|
+
lazyInit: z.boolean().optional().describe("Lazy initialization")
|
|
497
|
+
})).optional().describe("Skills map (defaults to @perstack/base)"),
|
|
498
|
+
delegates: z.array(z.string()).optional().describe("Expert keys to delegate to"),
|
|
499
|
+
tags: z.array(z.string()).optional().describe("Tags for categorization"),
|
|
500
|
+
providerTools: z.array(z.string()).optional().describe("Provider-specific tool names")
|
|
501
|
+
}
|
|
502
|
+
}, async (input) => {
|
|
503
|
+
try {
|
|
504
|
+
return successToolResult(await callbacks.createExpert(input));
|
|
505
|
+
} catch (e) {
|
|
506
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
507
|
+
throw e;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function registerSkillManagementTools(server, callbacks) {
|
|
512
|
+
registerAddSkill(server, callbacks);
|
|
513
|
+
registerRemoveSkill(server, callbacks);
|
|
514
|
+
registerAddDelegate(server, callbacks);
|
|
515
|
+
registerRemoveDelegate(server, callbacks);
|
|
516
|
+
registerCreateExpert(server, callbacks);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
//#endregion
|
|
520
|
+
//#region src/tools/write-text-file.ts
|
|
521
|
+
async function writeTextFile(input) {
|
|
522
|
+
const { path, text } = input;
|
|
523
|
+
const validatedPath = await validatePath(path);
|
|
524
|
+
const stats = await stat(validatedPath).catch(() => null);
|
|
525
|
+
if (stats && !(stats.mode & 128)) throw new Error(`File ${path} is not writable`);
|
|
526
|
+
await mkdir(dirname(validatedPath), { recursive: true });
|
|
527
|
+
await safeWriteFile(validatedPath, text);
|
|
528
|
+
return {
|
|
529
|
+
path: validatedPath,
|
|
530
|
+
text
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function registerWriteTextFile(server) {
|
|
534
|
+
server.registerTool("writeTextFile", {
|
|
535
|
+
title: "writeTextFile",
|
|
536
|
+
description: "Create or overwrite a UTF-8 text file. Creates parent directories as needed.",
|
|
537
|
+
inputSchema: {
|
|
538
|
+
path: z.string().describe("Target file path (relative or absolute)."),
|
|
539
|
+
text: z.string().describe("Text to write to the file.")
|
|
540
|
+
}
|
|
541
|
+
}, async (input) => {
|
|
542
|
+
try {
|
|
543
|
+
return successToolResult(await writeTextFile(input));
|
|
544
|
+
} catch (e) {
|
|
545
|
+
if (e instanceof Error) return errorToolResult(e);
|
|
546
|
+
throw e;
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/server.ts
|
|
553
|
+
/** Base skill name */
|
|
554
|
+
const BASE_SKILL_NAME = name;
|
|
555
|
+
/** Base skill version */
|
|
556
|
+
const BASE_SKILL_VERSION = version;
|
|
557
|
+
/**
|
|
558
|
+
* Register all base skill tools on an MCP server.
|
|
559
|
+
* This is useful for both standalone and in-process server creation.
|
|
560
|
+
*/
|
|
561
|
+
function registerAllTools(server) {
|
|
562
|
+
registerAttemptCompletion(server);
|
|
563
|
+
registerTodo(server);
|
|
564
|
+
registerClearTodo(server);
|
|
565
|
+
registerExec(server);
|
|
566
|
+
registerReadTextFile(server);
|
|
567
|
+
registerReadImageFile(server);
|
|
568
|
+
registerReadPdfFile(server);
|
|
569
|
+
registerWriteTextFile(server);
|
|
570
|
+
registerEditTextFile(server);
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Create a base skill MCP server with all tools registered.
|
|
574
|
+
* Used by the runtime for in-process execution via InMemoryTransport.
|
|
575
|
+
*/
|
|
576
|
+
function createBaseServer(options) {
|
|
577
|
+
const server = new McpServer({
|
|
578
|
+
name: BASE_SKILL_NAME,
|
|
579
|
+
version: BASE_SKILL_VERSION
|
|
580
|
+
}, { capabilities: { tools: {} } });
|
|
581
|
+
registerAllTools(server);
|
|
582
|
+
if (options?.skillManagement) registerSkillManagementTools(server, options.skillManagement);
|
|
583
|
+
return server;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
//#endregion
|
|
587
|
+
export { todo as A, validatePath as C, getRemainingTodos as D, clearTodo as E, version as F, successToolResult as M, description as N, registerClearTodo as O, name as P, registerEditTextFile as S, registerAttemptCompletion as T, readImageFile as _, registerWriteTextFile as a, registerExec as b, registerAddSkill as c, registerRemoveSkill as d, registerSkillManagementTools as f, registerReadPdfFile as g, readPdfFile as h, registerAllTools as i, errorToolResult as j, registerTodo as k, registerCreateExpert as l, registerReadTextFile as m, BASE_SKILL_VERSION as n, writeTextFile as o, readTextFile as p, createBaseServer as r, registerAddDelegate as s, BASE_SKILL_NAME as t, registerRemoveDelegate as u, registerReadImageFile as v, attemptCompletion as w, editTextFile as x, exec as y };
|
|
588
|
+
//# sourceMappingURL=server-BM7K7dr8.js.map
|