@universal-mcp-toolkit/server-filesystem 0.1.0

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.
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/json",
3
+ "name": "filesystem",
4
+ "title": "FileSystem MCP Server",
5
+ "description": "Safe, allowlisted file listing, reading, and writing tools.",
6
+ "version": "0.1.0",
7
+ "packageName": "@universal-mcp-toolkit/server-filesystem",
8
+ "homepage": "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit",
9
+ "transports": [
10
+ "stdio",
11
+ "sse"
12
+ ],
13
+ "authentication": {
14
+ "mode": "environment-variables",
15
+ "required": [
16
+ "FILESYSTEM_ROOTS"
17
+ ]
18
+ },
19
+ "capabilities": {
20
+ "tools": true,
21
+ "resources": true,
22
+ "prompts": true
23
+ },
24
+ "tools": [
25
+ "list_files",
26
+ "read_file",
27
+ "write_file"
28
+ ],
29
+ "resources": [
30
+ "workspace"
31
+ ],
32
+ "prompts": [
33
+ "change-plan"
34
+ ]
35
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 universal-mcp-toolkit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,137 @@
1
+ import * as _universal_mcp_toolkit_core from '@universal-mcp-toolkit/core';
2
+ import { ToolkitServer, ToolkitServerMetadata } from '@universal-mcp-toolkit/core';
3
+ import { z } from 'zod';
4
+
5
+ declare const metadata: ToolkitServerMetadata;
6
+ declare const serverCard: _universal_mcp_toolkit_core.ToolkitServerCard;
7
+ type FileEncoding = "utf8" | "base64";
8
+ declare const fileEntrySchema: z.ZodObject<{
9
+ path: z.ZodString;
10
+ absolutePath: z.ZodString;
11
+ type: z.ZodEnum<{
12
+ file: "file";
13
+ directory: "directory";
14
+ }>;
15
+ sizeBytes: z.ZodNumber;
16
+ modifiedAt: z.ZodNullable<z.ZodString>;
17
+ }, z.core.$strip>;
18
+ declare const workspaceSummarySchema: z.ZodObject<{
19
+ roots: z.ZodArray<z.ZodObject<{
20
+ root: z.ZodString;
21
+ available: z.ZodBoolean;
22
+ }, z.core.$strip>>;
23
+ maxReadBytes: z.ZodNumber;
24
+ maxWriteBytes: z.ZodNumber;
25
+ sampleEntries: z.ZodArray<z.ZodObject<{
26
+ path: z.ZodString;
27
+ absolutePath: z.ZodString;
28
+ type: z.ZodEnum<{
29
+ file: "file";
30
+ directory: "directory";
31
+ }>;
32
+ sizeBytes: z.ZodNumber;
33
+ modifiedAt: z.ZodNullable<z.ZodString>;
34
+ }, z.core.$strip>>;
35
+ }, z.core.$strip>;
36
+ declare const listFilesOutputSchema: z.ZodObject<{
37
+ root: z.ZodString;
38
+ directory: z.ZodString;
39
+ entries: z.ZodArray<z.ZodObject<{
40
+ path: z.ZodString;
41
+ absolutePath: z.ZodString;
42
+ type: z.ZodEnum<{
43
+ file: "file";
44
+ directory: "directory";
45
+ }>;
46
+ sizeBytes: z.ZodNumber;
47
+ modifiedAt: z.ZodNullable<z.ZodString>;
48
+ }, z.core.$strip>>;
49
+ truncated: z.ZodBoolean;
50
+ }, z.core.$strip>;
51
+ declare const readFileOutputSchema: z.ZodObject<{
52
+ path: z.ZodString;
53
+ absolutePath: z.ZodString;
54
+ encoding: z.ZodEnum<{
55
+ utf8: "utf8";
56
+ base64: "base64";
57
+ }>;
58
+ content: z.ZodString;
59
+ bytes: z.ZodNumber;
60
+ }, z.core.$strip>;
61
+ declare const writeFileOutputSchema: z.ZodObject<{
62
+ path: z.ZodString;
63
+ absolutePath: z.ZodString;
64
+ bytesWritten: z.ZodNumber;
65
+ created: z.ZodBoolean;
66
+ overwritten: z.ZodBoolean;
67
+ }, z.core.$strip>;
68
+ type FileEntry = z.infer<typeof fileEntrySchema>;
69
+ type WorkspaceSummary = z.infer<typeof workspaceSummarySchema>;
70
+ type ListFilesOutput = z.infer<typeof listFilesOutputSchema>;
71
+ type ReadFileOutput = z.infer<typeof readFileOutputSchema>;
72
+ type WriteFileOutput = z.infer<typeof writeFileOutputSchema>;
73
+ interface FileSystemService {
74
+ listFiles(input: {
75
+ path?: string;
76
+ root?: string;
77
+ recursive: boolean;
78
+ maxEntries: number;
79
+ }): Promise<ListFilesOutput>;
80
+ readFile(input: {
81
+ path: string;
82
+ root?: string;
83
+ encoding: FileEncoding;
84
+ maxBytes?: number;
85
+ }): Promise<ReadFileOutput>;
86
+ writeFile(input: {
87
+ path: string;
88
+ root?: string;
89
+ content: string;
90
+ encoding: FileEncoding;
91
+ overwrite: boolean;
92
+ createDirectories: boolean;
93
+ }): Promise<WriteFileOutput>;
94
+ getWorkspaceSummary(): Promise<WorkspaceSummary>;
95
+ }
96
+ interface CreateFileSystemServerOptions {
97
+ service?: FileSystemService;
98
+ env?: NodeJS.ProcessEnv;
99
+ }
100
+ declare class DefaultFileSystemService implements FileSystemService {
101
+ private readonly roots;
102
+ private readonly maxReadBytes;
103
+ private readonly maxWriteBytes;
104
+ private constructor();
105
+ static fromEnv(source?: NodeJS.ProcessEnv): Promise<DefaultFileSystemService>;
106
+ listFiles(input: {
107
+ path?: string;
108
+ root?: string;
109
+ recursive: boolean;
110
+ maxEntries: number;
111
+ }): Promise<ListFilesOutput>;
112
+ readFile(input: {
113
+ path: string;
114
+ root?: string;
115
+ encoding: FileEncoding;
116
+ maxBytes?: number;
117
+ }): Promise<ReadFileOutput>;
118
+ writeFile(input: {
119
+ path: string;
120
+ root?: string;
121
+ content: string;
122
+ encoding: FileEncoding;
123
+ overwrite: boolean;
124
+ createDirectories: boolean;
125
+ }): Promise<WriteFileOutput>;
126
+ getWorkspaceSummary(): Promise<WorkspaceSummary>;
127
+ private resolveSafePath;
128
+ private selectRoot;
129
+ }
130
+ declare class FileSystemServer extends ToolkitServer {
131
+ private readonly service;
132
+ constructor(service: FileSystemService);
133
+ }
134
+ declare function createServer(options?: CreateFileSystemServerOptions): Promise<FileSystemServer>;
135
+ declare function main(argv?: readonly string[]): Promise<void>;
136
+
137
+ export { type CreateFileSystemServerOptions, DefaultFileSystemService, type FileEntry, FileSystemServer, type FileSystemService, type ListFilesOutput, type ReadFileOutput, type WorkspaceSummary, type WriteFileOutput, createServer, main, metadata, serverCard };
package/dist/index.js ADDED
@@ -0,0 +1,480 @@
1
+ // src/index.ts
2
+ import {
3
+ ConfigurationError,
4
+ createServerCard,
5
+ defineTool,
6
+ loadEnv,
7
+ parseRuntimeOptions,
8
+ runToolkitServer,
9
+ ToolkitServer,
10
+ ValidationError
11
+ } from "@universal-mcp-toolkit/core";
12
+ import { promises as fs } from "fs";
13
+ import { dirname, isAbsolute, join, normalize, relative, resolve as resolvePath } from "path";
14
+ import { fileURLToPath } from "url";
15
+ import { z } from "zod";
16
+ var toolNames = ["list_files", "read_file", "write_file"];
17
+ var resourceNames = ["workspace"];
18
+ var promptNames = ["change-plan"];
19
+ var metadata = {
20
+ id: "filesystem",
21
+ title: "FileSystem MCP Server",
22
+ description: "Safe, allowlisted file listing, reading, and writing tools.",
23
+ version: "0.1.0",
24
+ packageName: "@universal-mcp-toolkit/server-filesystem",
25
+ homepage: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit",
26
+ repositoryUrl: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit",
27
+ documentationUrl: "https://github.com/universal-mcp-toolkit/universal-mcp-toolkit/tree/main/servers/filesystem",
28
+ envVarNames: ["FILESYSTEM_ROOTS"],
29
+ transports: ["stdio", "sse"],
30
+ toolNames,
31
+ resourceNames,
32
+ promptNames
33
+ };
34
+ var serverCard = createServerCard(metadata);
35
+ var envShape = {
36
+ FILESYSTEM_ROOTS: z.string().min(1),
37
+ FILESYSTEM_MAX_READ_BYTES: z.string().regex(/^\d+$/).optional(),
38
+ FILESYSTEM_MAX_WRITE_BYTES: z.string().regex(/^\d+$/).optional()
39
+ };
40
+ var encodingSchema = z.enum(["utf8", "base64"]);
41
+ var fileEntrySchema = z.object({
42
+ path: z.string(),
43
+ absolutePath: z.string(),
44
+ type: z.enum(["file", "directory"]),
45
+ sizeBytes: z.number().int().nonnegative(),
46
+ modifiedAt: z.string().nullable()
47
+ });
48
+ var workspaceRootSchema = z.object({
49
+ root: z.string(),
50
+ available: z.boolean()
51
+ });
52
+ var workspaceSummarySchema = z.object({
53
+ roots: z.array(workspaceRootSchema),
54
+ maxReadBytes: z.number().int().positive(),
55
+ maxWriteBytes: z.number().int().positive(),
56
+ sampleEntries: z.array(fileEntrySchema)
57
+ });
58
+ var listFilesOutputSchema = z.object({
59
+ root: z.string(),
60
+ directory: z.string(),
61
+ entries: z.array(fileEntrySchema),
62
+ truncated: z.boolean()
63
+ });
64
+ var readFileOutputSchema = z.object({
65
+ path: z.string(),
66
+ absolutePath: z.string(),
67
+ encoding: encodingSchema,
68
+ content: z.string(),
69
+ bytes: z.number().int().nonnegative()
70
+ });
71
+ var writeFileOutputSchema = z.object({
72
+ path: z.string(),
73
+ absolutePath: z.string(),
74
+ bytesWritten: z.number().int().nonnegative(),
75
+ created: z.boolean(),
76
+ overwritten: z.boolean()
77
+ });
78
+ function resolveEnv(source = process.env) {
79
+ return loadEnv(envShape, source);
80
+ }
81
+ function parseLimit(raw, fallback) {
82
+ if (raw === void 0) {
83
+ return fallback;
84
+ }
85
+ const parsed = Number.parseInt(raw, 10);
86
+ if (!Number.isInteger(parsed) || parsed <= 0) {
87
+ throw new ConfigurationError(`Expected a positive integer but received '${raw}'.`);
88
+ }
89
+ return parsed;
90
+ }
91
+ function normalizeForComparison(value) {
92
+ const resolved = normalize(resolvePath(value));
93
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
94
+ }
95
+ function isPathInside(root, candidate) {
96
+ const relation = relative(root, candidate);
97
+ return relation === "" || !relation.startsWith("..") && !isAbsolute(relation);
98
+ }
99
+ async function ensureDirectory(path) {
100
+ const stat = await fs.stat(path).catch(() => void 0);
101
+ if (!stat || !stat.isDirectory()) {
102
+ throw new ConfigurationError(`Allowlisted root '${path}' must exist and be a directory.`);
103
+ }
104
+ }
105
+ async function findNearestExistingAncestor(path) {
106
+ let current = path;
107
+ while (true) {
108
+ const stat = await fs.lstat(current).catch(() => void 0);
109
+ if (stat) {
110
+ return current;
111
+ }
112
+ const parent = dirname(current);
113
+ if (parent === current) {
114
+ return current;
115
+ }
116
+ current = parent;
117
+ }
118
+ }
119
+ function toRelativePath(root, absolutePath) {
120
+ const relativePath = relative(root, absolutePath);
121
+ return relativePath.length === 0 ? "." : relativePath;
122
+ }
123
+ function toTimestamp(date) {
124
+ return date.toISOString();
125
+ }
126
+ function splitRoots(raw) {
127
+ return raw.split(";").map((entry) => entry.trim()).filter((entry) => entry.length > 0);
128
+ }
129
+ var DefaultFileSystemService = class _DefaultFileSystemService {
130
+ roots;
131
+ maxReadBytes;
132
+ maxWriteBytes;
133
+ constructor(roots, maxReadBytes, maxWriteBytes) {
134
+ this.roots = roots;
135
+ this.maxReadBytes = maxReadBytes;
136
+ this.maxWriteBytes = maxWriteBytes;
137
+ }
138
+ static async fromEnv(source = process.env) {
139
+ const env = resolveEnv(source);
140
+ const roots = splitRoots(env.FILESYSTEM_ROOTS);
141
+ if (roots.length === 0) {
142
+ throw new ConfigurationError("FILESYSTEM_ROOTS must contain at least one absolute path.");
143
+ }
144
+ const normalizedRoots = [];
145
+ for (const root of roots) {
146
+ if (!isAbsolute(root)) {
147
+ throw new ConfigurationError(`FILESYSTEM_ROOTS entry '${root}' must be an absolute path.`);
148
+ }
149
+ await ensureDirectory(root);
150
+ normalizedRoots.push({
151
+ root: resolvePath(root),
152
+ realRoot: await fs.realpath(root)
153
+ });
154
+ }
155
+ return new _DefaultFileSystemService(
156
+ normalizedRoots,
157
+ parseLimit(env.FILESYSTEM_MAX_READ_BYTES, 1024 * 1024),
158
+ parseLimit(env.FILESYSTEM_MAX_WRITE_BYTES, 1024 * 1024)
159
+ );
160
+ }
161
+ async listFiles(input) {
162
+ const resolvedPath = await this.resolveSafePath(input.path ?? ".", input.root);
163
+ const stat = await fs.stat(resolvedPath.absolutePath).catch(() => void 0);
164
+ if (!stat || !stat.isDirectory()) {
165
+ throw new ValidationError(`Path '${resolvedPath.absolutePath}' is not a directory.`);
166
+ }
167
+ const queue = [resolvedPath.absolutePath];
168
+ const entries = [];
169
+ let truncated = false;
170
+ while (queue.length > 0 && entries.length < input.maxEntries) {
171
+ const current = queue.shift();
172
+ if (!current) {
173
+ break;
174
+ }
175
+ const children = await fs.readdir(current, { withFileTypes: true });
176
+ for (const child of children) {
177
+ if (child.isSymbolicLink()) {
178
+ continue;
179
+ }
180
+ const childPath = join(current, child.name);
181
+ const childStat = await fs.stat(childPath);
182
+ const entry = {
183
+ path: toRelativePath(resolvedPath.root, childPath),
184
+ absolutePath: childPath,
185
+ type: child.isDirectory() ? "directory" : "file",
186
+ sizeBytes: child.isDirectory() ? 0 : childStat.size,
187
+ modifiedAt: toTimestamp(childStat.mtime)
188
+ };
189
+ entries.push(entry);
190
+ if (entries.length >= input.maxEntries) {
191
+ truncated = true;
192
+ break;
193
+ }
194
+ if (input.recursive && child.isDirectory()) {
195
+ queue.push(childPath);
196
+ }
197
+ }
198
+ }
199
+ if (queue.length > 0) {
200
+ truncated = true;
201
+ }
202
+ return {
203
+ root: resolvedPath.root,
204
+ directory: resolvedPath.absolutePath,
205
+ entries,
206
+ truncated
207
+ };
208
+ }
209
+ async readFile(input) {
210
+ const resolvedPath = await this.resolveSafePath(input.path, input.root);
211
+ const stat = await fs.stat(resolvedPath.absolutePath).catch(() => void 0);
212
+ if (!stat || !stat.isFile()) {
213
+ throw new ValidationError(`Path '${resolvedPath.absolutePath}' is not a file.`);
214
+ }
215
+ const effectiveMaxBytes = input.maxBytes ?? this.maxReadBytes;
216
+ if (stat.size > effectiveMaxBytes) {
217
+ throw new ValidationError(`File exceeds the allowed read size of ${effectiveMaxBytes} bytes.`);
218
+ }
219
+ const buffer = await fs.readFile(resolvedPath.absolutePath);
220
+ const content = input.encoding === "base64" ? buffer.toString("base64") : buffer.toString("utf8");
221
+ return {
222
+ path: resolvedPath.requestedPath,
223
+ absolutePath: resolvedPath.absolutePath,
224
+ encoding: input.encoding,
225
+ content,
226
+ bytes: buffer.byteLength
227
+ };
228
+ }
229
+ async writeFile(input) {
230
+ const resolvedPath = await this.resolveSafePath(input.path, input.root);
231
+ const buffer = input.encoding === "base64" ? Buffer.from(input.content, "base64") : Buffer.from(input.content, "utf8");
232
+ if (buffer.byteLength > this.maxWriteBytes) {
233
+ throw new ValidationError(`Content exceeds the allowed write size of ${this.maxWriteBytes} bytes.`);
234
+ }
235
+ const existingStat = await fs.stat(resolvedPath.absolutePath).catch(() => void 0);
236
+ if (existingStat?.isDirectory()) {
237
+ throw new ValidationError(`Path '${resolvedPath.absolutePath}' is a directory.`);
238
+ }
239
+ if (existingStat && !input.overwrite) {
240
+ throw new ValidationError(`Path '${resolvedPath.absolutePath}' already exists and overwrite=false.`);
241
+ }
242
+ const parent = dirname(resolvedPath.absolutePath);
243
+ if (input.createDirectories) {
244
+ await fs.mkdir(parent, { recursive: true });
245
+ } else {
246
+ const parentStat = await fs.stat(parent).catch(() => void 0);
247
+ if (!parentStat || !parentStat.isDirectory()) {
248
+ throw new ValidationError(`Parent directory '${parent}' does not exist.`);
249
+ }
250
+ }
251
+ await fs.writeFile(resolvedPath.absolutePath, buffer, { flag: input.overwrite ? "w" : "wx" });
252
+ return {
253
+ path: resolvedPath.requestedPath,
254
+ absolutePath: resolvedPath.absolutePath,
255
+ bytesWritten: buffer.byteLength,
256
+ created: !existingStat,
257
+ overwritten: Boolean(existingStat)
258
+ };
259
+ }
260
+ async getWorkspaceSummary() {
261
+ const sampleRoot = this.roots[0]?.root;
262
+ const sampleEntries = sampleRoot === void 0 ? [] : (await this.listFiles({ path: sampleRoot, recursive: false, maxEntries: 10 })).entries;
263
+ return {
264
+ roots: await Promise.all(
265
+ this.roots.map(async (root) => ({
266
+ root: root.root,
267
+ available: Boolean(await fs.stat(root.root).catch(() => void 0))
268
+ }))
269
+ ),
270
+ maxReadBytes: this.maxReadBytes,
271
+ maxWriteBytes: this.maxWriteBytes,
272
+ sampleEntries
273
+ };
274
+ }
275
+ async resolveSafePath(requestedPath, requestedRoot) {
276
+ const rootConfig = this.selectRoot(requestedRoot);
277
+ const candidate = isAbsolute(requestedPath) ? resolvePath(requestedPath) : resolvePath(rootConfig.root, requestedPath);
278
+ const normalizedRoot = normalizeForComparison(rootConfig.root);
279
+ const normalizedCandidate = normalizeForComparison(candidate);
280
+ if (!isPathInside(normalizedRoot, normalizedCandidate)) {
281
+ throw new ValidationError(`Path '${requestedPath}' resolves outside the allowlisted roots.`);
282
+ }
283
+ const ancestor = await findNearestExistingAncestor(candidate);
284
+ const realAncestor = await fs.realpath(ancestor).catch(() => ancestor);
285
+ const normalizedRealRoot = normalizeForComparison(rootConfig.realRoot);
286
+ const normalizedRealAncestor = normalizeForComparison(realAncestor);
287
+ if (!isPathInside(normalizedRealRoot, normalizedRealAncestor)) {
288
+ throw new ValidationError(`Path '${requestedPath}' resolves through an unsafe symlinked location.`);
289
+ }
290
+ const existingTarget = await fs.lstat(candidate).catch(() => void 0);
291
+ if (existingTarget) {
292
+ const realTarget = await fs.realpath(candidate).catch(() => candidate);
293
+ if (!isPathInside(normalizedRealRoot, normalizeForComparison(realTarget))) {
294
+ throw new ValidationError(`Path '${requestedPath}' resolves outside the allowlisted roots.`);
295
+ }
296
+ }
297
+ return {
298
+ root: rootConfig.root,
299
+ absolutePath: candidate,
300
+ requestedPath
301
+ };
302
+ }
303
+ selectRoot(requestedRoot) {
304
+ if (requestedRoot === void 0 || requestedRoot.trim().length === 0) {
305
+ const fallback = this.roots[0];
306
+ if (!fallback) {
307
+ throw new ConfigurationError("No filesystem roots are configured.");
308
+ }
309
+ return fallback;
310
+ }
311
+ const normalizedRequested = normalizeForComparison(requestedRoot);
312
+ const root = this.roots.find((entry) => normalizeForComparison(entry.root) === normalizedRequested);
313
+ if (!root) {
314
+ throw new ValidationError(`Root '${requestedRoot}' is not allowlisted.`);
315
+ }
316
+ return root;
317
+ }
318
+ };
319
+ var FileSystemServer = class extends ToolkitServer {
320
+ service;
321
+ constructor(service) {
322
+ super(metadata);
323
+ this.service = service;
324
+ this.registerTool(
325
+ defineTool({
326
+ name: "list_files",
327
+ title: "List files",
328
+ description: "List files under an allowlisted root directory.",
329
+ inputSchema: {
330
+ path: z.string().trim().min(1).optional(),
331
+ root: z.string().trim().min(1).optional(),
332
+ recursive: z.boolean().default(false),
333
+ maxEntries: z.number().int().min(1).max(200).default(50)
334
+ },
335
+ outputSchema: {
336
+ root: z.string(),
337
+ directory: z.string(),
338
+ entries: z.array(fileEntrySchema),
339
+ truncated: z.boolean()
340
+ },
341
+ handler: async ({ path, root, recursive, maxEntries }, context) => {
342
+ await context.log("info", "Listing files from an allowlisted root");
343
+ const request = { recursive, maxEntries };
344
+ if (path !== void 0) {
345
+ request.path = path;
346
+ }
347
+ if (root !== void 0) {
348
+ request.root = root;
349
+ }
350
+ return this.service.listFiles(request);
351
+ },
352
+ renderText: ({ directory, entries }) => `${directory}
353
+ ${entries.map((entry) => `${entry.type}: ${entry.path}`).join("\n")}`
354
+ })
355
+ );
356
+ this.registerTool(
357
+ defineTool({
358
+ name: "read_file",
359
+ title: "Read file",
360
+ description: "Read a file from an allowlisted root.",
361
+ inputSchema: {
362
+ path: z.string().trim().min(1),
363
+ root: z.string().trim().min(1).optional(),
364
+ encoding: encodingSchema.default("utf8"),
365
+ maxBytes: z.number().int().min(1).max(10 * 1024 * 1024).optional()
366
+ },
367
+ outputSchema: {
368
+ path: z.string(),
369
+ absolutePath: z.string(),
370
+ encoding: encodingSchema,
371
+ content: z.string(),
372
+ bytes: z.number().int().nonnegative()
373
+ },
374
+ handler: async ({ path, root, encoding, maxBytes }, context) => {
375
+ await context.log("info", `Reading file ${path}`);
376
+ const request = { path, encoding };
377
+ if (root !== void 0) {
378
+ request.root = root;
379
+ }
380
+ if (maxBytes !== void 0) {
381
+ request.maxBytes = maxBytes;
382
+ }
383
+ return this.service.readFile(request);
384
+ },
385
+ renderText: ({ absolutePath, content }) => `${absolutePath}
386
+ ${content}`
387
+ })
388
+ );
389
+ this.registerTool(
390
+ defineTool({
391
+ name: "write_file",
392
+ title: "Write file",
393
+ description: "Write a file inside an allowlisted root.",
394
+ inputSchema: {
395
+ path: z.string().trim().min(1),
396
+ root: z.string().trim().min(1).optional(),
397
+ content: z.string(),
398
+ encoding: encodingSchema.default("utf8"),
399
+ overwrite: z.boolean().default(false),
400
+ createDirectories: z.boolean().default(true)
401
+ },
402
+ outputSchema: {
403
+ path: z.string(),
404
+ absolutePath: z.string(),
405
+ bytesWritten: z.number().int().nonnegative(),
406
+ created: z.boolean(),
407
+ overwritten: z.boolean()
408
+ },
409
+ handler: async ({ path, root, content, encoding, overwrite, createDirectories }, context) => {
410
+ await context.log("info", `Writing file ${path}`);
411
+ const request = { path, content, encoding, overwrite, createDirectories };
412
+ if (root !== void 0) {
413
+ request.root = root;
414
+ }
415
+ return this.service.writeFile(request);
416
+ },
417
+ renderText: ({ absolutePath, bytesWritten }) => `Wrote ${bytesWritten} bytes to ${absolutePath}`
418
+ })
419
+ );
420
+ this.registerStaticResource(
421
+ "workspace",
422
+ "filesystem://workspace",
423
+ {
424
+ title: "Workspace summary",
425
+ description: "Configured allowlisted roots and a sample directory listing.",
426
+ mimeType: "application/json"
427
+ },
428
+ async (uri) => this.createJsonResource(uri.toString(), await this.service.getWorkspaceSummary())
429
+ );
430
+ this.registerPrompt(
431
+ "change-plan",
432
+ {
433
+ title: "Change plan prompt",
434
+ description: "Draft a careful filesystem change plan before editing local files.",
435
+ argsSchema: {
436
+ targetPath: z.string().trim().min(1),
437
+ goal: z.string().trim().min(1),
438
+ constraints: z.string().trim().min(1).optional()
439
+ }
440
+ },
441
+ async ({ targetPath, goal, constraints }) => this.createTextPrompt(
442
+ [
443
+ `Plan a safe file-system change for ${targetPath}.`,
444
+ `Goal: ${goal}.`,
445
+ constraints ? `Constraints: ${constraints}.` : "Include backups, validation, rollback, and scope-control steps.",
446
+ "Keep all work within allowlisted roots and call out any irreversible operations."
447
+ ].join(" ")
448
+ )
449
+ );
450
+ }
451
+ };
452
+ async function createServer(options = {}) {
453
+ const service = options.service ?? await DefaultFileSystemService.fromEnv(options.env);
454
+ return new FileSystemServer(service);
455
+ }
456
+ function isMainModule(metaUrl) {
457
+ const entry = process.argv[1];
458
+ return typeof entry === "string" && fileURLToPath(metaUrl) === resolvePath(entry);
459
+ }
460
+ async function main(argv = process.argv.slice(2)) {
461
+ const runtimeOptions = parseRuntimeOptions(argv);
462
+ await runToolkitServer(
463
+ {
464
+ createServer: () => createServer(),
465
+ serverCard
466
+ },
467
+ runtimeOptions
468
+ );
469
+ }
470
+ if (isMainModule(import.meta.url)) {
471
+ await main();
472
+ }
473
+ export {
474
+ DefaultFileSystemService,
475
+ FileSystemServer,
476
+ createServer,
477
+ main,
478
+ metadata,
479
+ serverCard
480
+ };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@universal-mcp-toolkit/server-filesystem",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Safe sandboxed file read and write tools for local workflows.",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "server-filesystem": "./dist/index.js",
9
+ "umt-filesystem": "./dist/index.js"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/Markgatcha/universal-mcp-toolkit.git"
14
+ },
15
+ "homepage": "https://github.com/Markgatcha/universal-mcp-toolkit#readme",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./package.json": "./package.json"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ ".well-known"
26
+ ],
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "ai",
31
+ "developer-tools",
32
+ "typescript",
33
+ "filesystem",
34
+ "files",
35
+ "local",
36
+ "sandbox"
37
+ ],
38
+ "dependencies": {
39
+ "@universal-mcp-toolkit/core": "0.1.0",
40
+ "zod": "^4.3.6"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup src/index.ts --format esm --dts --clean",
47
+ "dev": "tsx watch src/index.ts",
48
+ "lint": "tsc --noEmit",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run --passWithNoTests",
51
+ "clean": "rimraf dist"
52
+ }
53
+ }