Added filesystem module
This commit is contained in:
673
mcpServer/modules/filesystem/index.ts
Normal file
673
mcpServer/modules/filesystem/index.ts
Normal file
@@ -0,0 +1,673 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
||||
import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||
import fs from "fs/promises";
|
||||
import { createReadStream } from "fs";
|
||||
import http from "http";
|
||||
import path from "path";
|
||||
import { URL } from "url";
|
||||
import { z } from "zod";
|
||||
import { minimatch } from "minimatch";
|
||||
import {
|
||||
// Function imports
|
||||
formatSize,
|
||||
validatePath,
|
||||
getFileStats,
|
||||
readFileContent,
|
||||
writeFileContent,
|
||||
searchFilesWithValidation,
|
||||
applyFileEdits,
|
||||
tailFile,
|
||||
headFile,
|
||||
setAllowedDirectories,
|
||||
} from './lib.js';
|
||||
|
||||
// Always use full filesystem (typical for container deployment)
|
||||
const allowedDirectories: string[] = ["/"];
|
||||
setAllowedDirectories(allowedDirectories);
|
||||
|
||||
// Schema definitions
|
||||
const ReadTextFileArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
tail: z.number().optional().describe('If provided, returns only the last N lines of the file'),
|
||||
head: z.number().optional().describe('If provided, returns only the first N lines of the file')
|
||||
});
|
||||
|
||||
const ReadMediaFileArgsSchema = z.object({
|
||||
path: z.string()
|
||||
});
|
||||
|
||||
const ReadMultipleFilesArgsSchema = z.object({
|
||||
paths: z
|
||||
.array(z.string())
|
||||
.min(1, "At least one file path must be provided")
|
||||
.describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories."),
|
||||
});
|
||||
|
||||
const WriteFileArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
content: z.string(),
|
||||
});
|
||||
|
||||
const EditOperation = z.object({
|
||||
oldText: z.string().describe('Text to search for - must match exactly'),
|
||||
newText: z.string().describe('Text to replace with')
|
||||
});
|
||||
|
||||
const EditFileArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
edits: z.array(EditOperation),
|
||||
dryRun: z.boolean().default(false).describe('Preview changes using git-style diff format')
|
||||
});
|
||||
|
||||
const CreateDirectoryArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
const ListDirectoryArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
const ListDirectoryWithSizesArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
sortBy: z.enum(['name', 'size']).optional().default('name').describe('Sort entries by name or size'),
|
||||
});
|
||||
|
||||
const DirectoryTreeArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
excludePatterns: z.array(z.string()).optional().default([])
|
||||
});
|
||||
|
||||
const MoveFileArgsSchema = z.object({
|
||||
source: z.string(),
|
||||
destination: z.string(),
|
||||
});
|
||||
|
||||
const SearchFilesArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
pattern: z.string(),
|
||||
excludePatterns: z.array(z.string()).optional().default([])
|
||||
});
|
||||
|
||||
const GetFileInfoArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
// Server setup
|
||||
const server = new McpServer(
|
||||
{
|
||||
name: "secure-filesystem-server",
|
||||
version: "0.2.0",
|
||||
}
|
||||
);
|
||||
|
||||
// Reads a file as a stream of buffers, concatenates them, and then encodes
|
||||
// the result to a Base64 string. This is a memory-efficient way to handle
|
||||
// binary data from a stream before the final encoding.
|
||||
async function readFileAsBase64Stream(filePath: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const stream = createReadStream(filePath);
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on('data', (chunk) => {
|
||||
chunks.push(chunk as Buffer);
|
||||
});
|
||||
stream.on('end', () => {
|
||||
const finalBuffer = Buffer.concat(chunks);
|
||||
resolve(finalBuffer.toString('base64'));
|
||||
});
|
||||
stream.on('error', (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
// Tool registrations
|
||||
|
||||
// read_file (deprecated) and read_text_file
|
||||
const readTextFileHandler = async (args: z.infer<typeof ReadTextFileArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
|
||||
if (args.head && args.tail) {
|
||||
throw new Error("Cannot specify both head and tail parameters simultaneously");
|
||||
}
|
||||
|
||||
let content: string;
|
||||
if (args.tail) {
|
||||
content = await tailFile(validPath, args.tail);
|
||||
} else if (args.head) {
|
||||
content = await headFile(validPath, args.head);
|
||||
} else {
|
||||
content = await readFileContent(validPath);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: "text" as const, text: content }],
|
||||
structuredContent: { content }
|
||||
};
|
||||
};
|
||||
|
||||
server.registerTool(
|
||||
"read_file",
|
||||
{
|
||||
title: "Read File (Deprecated)",
|
||||
description: "Read the complete contents of a file as text. DEPRECATED: Use read_text_file instead.",
|
||||
inputSchema: ReadTextFileArgsSchema.shape,
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
readTextFileHandler
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"read_text_file",
|
||||
{
|
||||
title: "Read Text File",
|
||||
description:
|
||||
"Read the complete contents of a file from the file system as text. " +
|
||||
"Handles various text encodings and provides detailed error messages " +
|
||||
"if the file cannot be read. Use this tool when you need to examine " +
|
||||
"the contents of a single file. Use the 'head' parameter to read only " +
|
||||
"the first N lines of a file, or the 'tail' parameter to read only " +
|
||||
"the last N lines of a file. Operates on the file as text regardless of extension. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string(),
|
||||
tail: z.number().optional().describe("If provided, returns only the last N lines of the file"),
|
||||
head: z.number().optional().describe("If provided, returns only the first N lines of the file")
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
readTextFileHandler
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"read_media_file",
|
||||
{
|
||||
title: "Read Media File",
|
||||
description:
|
||||
"Read an image or audio file. Returns the base64 encoded data and MIME type. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string()
|
||||
},
|
||||
outputSchema: {
|
||||
content: z.array(z.object({
|
||||
type: z.enum(["image", "audio", "blob"]),
|
||||
data: z.string(),
|
||||
mimeType: z.string()
|
||||
}))
|
||||
},
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof ReadMediaFileArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
const extension = path.extname(validPath).toLowerCase();
|
||||
const mimeTypes: Record<string, string> = {
|
||||
".png": "image/png",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
".bmp": "image/bmp",
|
||||
".svg": "image/svg+xml",
|
||||
".mp3": "audio/mpeg",
|
||||
".wav": "audio/wav",
|
||||
".ogg": "audio/ogg",
|
||||
".flac": "audio/flac",
|
||||
};
|
||||
const mimeType = mimeTypes[extension] || "application/octet-stream";
|
||||
const data = await readFileAsBase64Stream(validPath);
|
||||
|
||||
const type = mimeType.startsWith("image/")
|
||||
? "image"
|
||||
: mimeType.startsWith("audio/")
|
||||
? "audio"
|
||||
// Fallback for other binary types, not officially supported by the spec but has been used for some time
|
||||
: "blob";
|
||||
const contentItem = { type: type as 'image' | 'audio' | 'blob', data, mimeType };
|
||||
return {
|
||||
content: [contentItem],
|
||||
structuredContent: { content: [contentItem] }
|
||||
} as unknown as CallToolResult;
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"read_multiple_files",
|
||||
{
|
||||
title: "Read Multiple Files",
|
||||
description:
|
||||
"Read the contents of multiple files simultaneously. This is more " +
|
||||
"efficient than reading files one by one when you need to analyze " +
|
||||
"or compare multiple files. Each file's content is returned with its " +
|
||||
"path as a reference. Failed reads for individual files won't stop " +
|
||||
"the entire operation. Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
paths: z.array(z.string())
|
||||
.min(1)
|
||||
.describe("Array of file paths to read. Each path must be a string pointing to a valid file within allowed directories.")
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof ReadMultipleFilesArgsSchema>) => {
|
||||
const results = await Promise.all(
|
||||
args.paths.map(async (filePath: string) => {
|
||||
try {
|
||||
const validPath = await validatePath(filePath);
|
||||
const content = await readFileContent(validPath);
|
||||
return `${filePath}:\n${content}\n`;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return `${filePath}: Error - ${errorMessage}`;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const text = results.join("\n---\n");
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: { content: text }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"write_file",
|
||||
{
|
||||
title: "Write File",
|
||||
description:
|
||||
"Create a new file or completely overwrite an existing file with new content. " +
|
||||
"Use with caution as it will overwrite existing files without warning. " +
|
||||
"Handles text content with proper encoding. Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string(),
|
||||
content: z.string()
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof WriteFileArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
await writeFileContent(validPath, args.content);
|
||||
const text = `Successfully wrote to ${args.path}`;
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: { content: text }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"edit_file",
|
||||
{
|
||||
title: "Edit File",
|
||||
description:
|
||||
"Make line-based edits to a text file. Each edit replaces exact line sequences " +
|
||||
"with new content. Returns a git-style diff showing the changes made. " +
|
||||
"Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string(),
|
||||
edits: z.array(z.object({
|
||||
oldText: z.string().describe("Text to search for - must match exactly"),
|
||||
newText: z.string().describe("Text to replace with")
|
||||
})),
|
||||
dryRun: z.boolean().default(false).describe("Preview changes using git-style diff format")
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof EditFileArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
const result = await applyFileEdits(validPath, args.edits, args.dryRun);
|
||||
return {
|
||||
content: [{ type: "text" as const, text: result }],
|
||||
structuredContent: { content: result }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"create_directory",
|
||||
{
|
||||
title: "Create Directory",
|
||||
description:
|
||||
"Create a new directory or ensure a directory exists. Can create multiple " +
|
||||
"nested directories in one operation. If the directory already exists, " +
|
||||
"this operation will succeed silently. Perfect for setting up directory " +
|
||||
"structures for projects or ensuring required paths exist. Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string()
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: false, idempotentHint: true, destructiveHint: false }
|
||||
},
|
||||
async (args: z.infer<typeof CreateDirectoryArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
await fs.mkdir(validPath, { recursive: true });
|
||||
const text = `Successfully created directory ${args.path}`;
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: { content: text }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"list_directory",
|
||||
{
|
||||
title: "List Directory",
|
||||
description:
|
||||
"Get a detailed listing of all files and directories in a specified path. " +
|
||||
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
||||
"prefixes. This tool is essential for understanding directory structure and " +
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string()
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof ListDirectoryArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||
const formatted = entries
|
||||
.map((entry) => `${entry.isDirectory() ? "[DIR]" : "[FILE]"} ${entry.name}`)
|
||||
.join("\n");
|
||||
return {
|
||||
content: [{ type: "text" as const, text: formatted }],
|
||||
structuredContent: { content: formatted }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"list_directory_with_sizes",
|
||||
{
|
||||
title: "List Directory with Sizes",
|
||||
description:
|
||||
"Get a detailed listing of all files and directories in a specified path, including sizes. " +
|
||||
"Results clearly distinguish between files and directories with [FILE] and [DIR] " +
|
||||
"prefixes. This tool is useful for understanding directory structure and " +
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string(),
|
||||
sortBy: z.enum(["name", "size"]).optional().default("name").describe("Sort entries by name or size")
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof ListDirectoryWithSizesArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||
|
||||
// Get detailed information for each entry
|
||||
const detailedEntries = await Promise.all(
|
||||
entries.map(async (entry) => {
|
||||
const entryPath = path.join(validPath, entry.name);
|
||||
try {
|
||||
const stats = await fs.stat(entryPath);
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: stats.size,
|
||||
mtime: stats.mtime
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDirectory(),
|
||||
size: 0,
|
||||
mtime: new Date(0)
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Sort entries based on sortBy parameter
|
||||
const sortedEntries = [...detailedEntries].sort((a, b) => {
|
||||
if (args.sortBy === 'size') {
|
||||
return b.size - a.size; // Descending by size
|
||||
}
|
||||
// Default sort by name
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
// Format the output
|
||||
const formattedEntries = sortedEntries.map(entry =>
|
||||
`${entry.isDirectory ? "[DIR]" : "[FILE]"} ${entry.name.padEnd(30)} ${
|
||||
entry.isDirectory ? "" : formatSize(entry.size).padStart(10)
|
||||
}`
|
||||
);
|
||||
|
||||
// Add summary
|
||||
const totalFiles = detailedEntries.filter(e => !e.isDirectory).length;
|
||||
const totalDirs = detailedEntries.filter(e => e.isDirectory).length;
|
||||
const totalSize = detailedEntries.reduce((sum, entry) => sum + (entry.isDirectory ? 0 : entry.size), 0);
|
||||
|
||||
const summary = [
|
||||
"",
|
||||
`Total: ${totalFiles} files, ${totalDirs} directories`,
|
||||
`Combined size: ${formatSize(totalSize)}`
|
||||
];
|
||||
|
||||
const text = [...formattedEntries, ...summary].join("\n");
|
||||
const contentBlock = { type: "text" as const, text };
|
||||
return {
|
||||
content: [contentBlock],
|
||||
structuredContent: { content: text }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"directory_tree",
|
||||
{
|
||||
title: "Directory Tree",
|
||||
description:
|
||||
"Get a recursive tree view of files and directories as a JSON structure. " +
|
||||
"Each entry includes 'name', 'type' (file/directory), and 'children' for directories. " +
|
||||
"Files have no children array, while directories always have a children array (which may be empty). " +
|
||||
"The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string(),
|
||||
excludePatterns: z.array(z.string()).optional().default([])
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof DirectoryTreeArgsSchema>) => {
|
||||
interface TreeEntry {
|
||||
name: string;
|
||||
type: 'file' | 'directory';
|
||||
children?: TreeEntry[];
|
||||
}
|
||||
const rootPath = args.path;
|
||||
|
||||
async function buildTree(currentPath: string, excludePatterns: string[] = []): Promise<TreeEntry[]> {
|
||||
const validPath = await validatePath(currentPath);
|
||||
const entries = await fs.readdir(validPath, { withFileTypes: true });
|
||||
const result: TreeEntry[] = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const relativePath = path.relative(rootPath, path.join(currentPath, entry.name));
|
||||
const shouldExclude = excludePatterns.some(pattern => {
|
||||
if (pattern.includes('*')) {
|
||||
return minimatch(relativePath, pattern, { dot: true });
|
||||
}
|
||||
// For files: match exact name or as part of path
|
||||
// For directories: match as directory path
|
||||
return minimatch(relativePath, pattern, { dot: true }) ||
|
||||
minimatch(relativePath, `**/${pattern}`, { dot: true }) ||
|
||||
minimatch(relativePath, `**/${pattern}/**`, { dot: true });
|
||||
});
|
||||
if (shouldExclude)
|
||||
continue;
|
||||
|
||||
const entryData: TreeEntry = {
|
||||
name: entry.name,
|
||||
type: entry.isDirectory() ? 'directory' : 'file'
|
||||
};
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const subPath = path.join(currentPath, entry.name);
|
||||
entryData.children = await buildTree(subPath, excludePatterns);
|
||||
}
|
||||
|
||||
result.push(entryData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const treeData = await buildTree(rootPath, args.excludePatterns);
|
||||
const text = JSON.stringify(treeData, null, 2);
|
||||
const contentBlock = { type: "text" as const, text };
|
||||
return {
|
||||
content: [contentBlock],
|
||||
structuredContent: { content: text }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"move_file",
|
||||
{
|
||||
title: "Move File",
|
||||
description:
|
||||
"Move or rename files and directories. Can move files between directories " +
|
||||
"and rename them in a single operation. If the destination exists, the " +
|
||||
"operation will fail. Works across different directories and can be used " +
|
||||
"for simple renaming within the same directory. Both source and destination must be within allowed directories.",
|
||||
inputSchema: {
|
||||
source: z.string(),
|
||||
destination: z.string()
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: false, idempotentHint: false, destructiveHint: false }
|
||||
},
|
||||
async (args: z.infer<typeof MoveFileArgsSchema>) => {
|
||||
const validSourcePath = await validatePath(args.source);
|
||||
const validDestPath = await validatePath(args.destination);
|
||||
await fs.rename(validSourcePath, validDestPath);
|
||||
const text = `Successfully moved ${args.source} to ${args.destination}`;
|
||||
const contentBlock = { type: "text" as const, text };
|
||||
return {
|
||||
content: [contentBlock],
|
||||
structuredContent: { content: text }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"search_files",
|
||||
{
|
||||
title: "Search Files",
|
||||
description:
|
||||
"Recursively search for files and directories matching a pattern. " +
|
||||
"The patterns should be glob-style patterns that match paths relative to the working directory. " +
|
||||
"Use pattern like '*.ext' to match files in current directory, and '**/*.ext' to match files in all subdirectories. " +
|
||||
"Returns full paths to all matching items. Great for finding files when you don't know their exact location. " +
|
||||
"Only searches within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string(),
|
||||
pattern: z.string(),
|
||||
excludePatterns: z.array(z.string()).optional().default([])
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof SearchFilesArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
const results = await searchFilesWithValidation(validPath, args.pattern, allowedDirectories, { excludePatterns: args.excludePatterns });
|
||||
const text = results.length > 0 ? results.join("\n") : "No matches found";
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: { content: text }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
server.registerTool(
|
||||
"get_file_info",
|
||||
{
|
||||
title: "Get File Info",
|
||||
description:
|
||||
"Retrieve detailed metadata about a file or directory. Returns comprehensive " +
|
||||
"information including size, creation time, last modified time, permissions, " +
|
||||
"and type. This tool is perfect for understanding file characteristics " +
|
||||
"without reading the actual content. Only works within allowed directories.",
|
||||
inputSchema: {
|
||||
path: z.string()
|
||||
},
|
||||
outputSchema: { content: z.string() },
|
||||
annotations: { readOnlyHint: true }
|
||||
},
|
||||
async (args: z.infer<typeof GetFileInfoArgsSchema>) => {
|
||||
const validPath = await validatePath(args.path);
|
||||
const info = await getFileStats(validPath);
|
||||
const text = Object.entries(info)
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join("\n");
|
||||
return {
|
||||
content: [{ type: "text" as const, text }],
|
||||
structuredContent: { content: text }
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
// SSE transport session routing (sessionId -> transport)
|
||||
const sseTransportsBySessionId = new Map<string, SSEServerTransport>();
|
||||
|
||||
function runServer() {
|
||||
const port = Number(process.env.MCP_PORT ?? process.env.SSE_PORT ?? 3000);
|
||||
|
||||
const httpServer = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
||||
const pathname = url.pathname;
|
||||
|
||||
if (req.method === "GET" && (pathname === "/sse" || pathname === "/")) {
|
||||
try {
|
||||
const transport = new SSEServerTransport("/messages", res);
|
||||
sseTransportsBySessionId.set(transport.sessionId, transport);
|
||||
transport.onclose = () => {
|
||||
sseTransportsBySessionId.delete(transport.sessionId);
|
||||
};
|
||||
await server.connect(transport);
|
||||
console.error("Secure MCP Filesystem Server: new SSE client connected");
|
||||
} catch (error) {
|
||||
console.error("SSE connection error:", error);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(500).end("Internal server error");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === "POST" && pathname === "/messages") {
|
||||
const sessionId = url.searchParams.get("sessionId");
|
||||
if (!sessionId) {
|
||||
res.writeHead(400).end("Missing sessionId query parameter");
|
||||
return;
|
||||
}
|
||||
const transport = sseTransportsBySessionId.get(sessionId);
|
||||
if (!transport) {
|
||||
res.writeHead(404).end("Unknown session");
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(404).end("Not found");
|
||||
});
|
||||
|
||||
httpServer.listen(port, () => {
|
||||
console.error(`Secure MCP Filesystem Server running on SSE at http://localhost:${port}`);
|
||||
console.error(" GET /sse – open SSE stream (then POST to /messages?sessionId=...)");
|
||||
console.error(" POST /messages?sessionId=<id> – send MCP messages");
|
||||
console.error(" Allowed directory: / (full filesystem)");
|
||||
});
|
||||
}
|
||||
|
||||
runServer();
|
||||
Reference in New Issue
Block a user