674 lines
23 KiB
JavaScript
674 lines
23 KiB
JavaScript
#!/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();
|