mirror of
https://github.com/modelcontextprotocol/servers.git
synced 2026-04-17 23:53:24 +02:00
Merge pull request #213 from lamemind/main
[FileSystem] directory_tree base implementation
This commit is contained in:
@@ -35,7 +35,7 @@ function expandHome(filepath: string): string {
|
||||
}
|
||||
|
||||
// Store allowed directories in normalized form
|
||||
const allowedDirectories = args.map(dir =>
|
||||
const allowedDirectories = args.map(dir =>
|
||||
normalizePath(path.resolve(expandHome(dir)))
|
||||
);
|
||||
|
||||
@@ -59,7 +59,7 @@ async function validatePath(requestedPath: string): Promise<string> {
|
||||
const absolute = path.isAbsolute(expandedPath)
|
||||
? path.resolve(expandedPath)
|
||||
: path.resolve(process.cwd(), expandedPath);
|
||||
|
||||
|
||||
const normalizedRequested = normalizePath(absolute);
|
||||
|
||||
// Check if path is within allowed directories
|
||||
@@ -127,6 +127,10 @@ const ListDirectoryArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
const DirectoryTreeArgsSchema = z.object({
|
||||
path: z.string(),
|
||||
});
|
||||
|
||||
const MoveFileArgsSchema = z.object({
|
||||
source: z.string(),
|
||||
destination: z.string(),
|
||||
@@ -237,7 +241,7 @@ function createUnifiedDiff(originalContent: string, newContent: string, filepath
|
||||
// Ensure consistent line endings for diff
|
||||
const normalizedOriginal = normalizeLineEndings(originalContent);
|
||||
const normalizedNew = normalizeLineEndings(newContent);
|
||||
|
||||
|
||||
return createTwoFilesPatch(
|
||||
filepath,
|
||||
filepath,
|
||||
@@ -255,33 +259,33 @@ async function applyFileEdits(
|
||||
): Promise<string> {
|
||||
// Read file content and normalize line endings
|
||||
const content = normalizeLineEndings(await fs.readFile(filePath, 'utf-8'));
|
||||
|
||||
|
||||
// Apply edits sequentially
|
||||
let modifiedContent = content;
|
||||
for (const edit of edits) {
|
||||
const normalizedOld = normalizeLineEndings(edit.oldText);
|
||||
const normalizedNew = normalizeLineEndings(edit.newText);
|
||||
|
||||
|
||||
// If exact match exists, use it
|
||||
if (modifiedContent.includes(normalizedOld)) {
|
||||
modifiedContent = modifiedContent.replace(normalizedOld, normalizedNew);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
// Otherwise, try line-by-line matching with flexibility for whitespace
|
||||
const oldLines = normalizedOld.split('\n');
|
||||
const contentLines = modifiedContent.split('\n');
|
||||
let matchFound = false;
|
||||
|
||||
|
||||
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
||||
const potentialMatch = contentLines.slice(i, i + oldLines.length);
|
||||
|
||||
|
||||
// Compare lines with normalized whitespace
|
||||
const isMatch = oldLines.every((oldLine, j) => {
|
||||
const contentLine = potentialMatch[j];
|
||||
return oldLine.trim() === contentLine.trim();
|
||||
});
|
||||
|
||||
|
||||
if (isMatch) {
|
||||
// Preserve original indentation of first line
|
||||
const originalIndent = contentLines[i].match(/^\s*/)?.[0] || '';
|
||||
@@ -296,33 +300,33 @@ async function applyFileEdits(
|
||||
}
|
||||
return line;
|
||||
});
|
||||
|
||||
|
||||
contentLines.splice(i, oldLines.length, ...newLines);
|
||||
modifiedContent = contentLines.join('\n');
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!matchFound) {
|
||||
throw new Error(`Could not find exact match for edit:\n${edit.oldText}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Create unified diff
|
||||
const diff = createUnifiedDiff(content, modifiedContent, filePath);
|
||||
|
||||
|
||||
// Format diff with appropriate number of backticks
|
||||
let numBackticks = 3;
|
||||
while (diff.includes('`'.repeat(numBackticks))) {
|
||||
numBackticks++;
|
||||
}
|
||||
const formattedDiff = `${'`'.repeat(numBackticks)}diff\n${diff}${'`'.repeat(numBackticks)}\n\n`;
|
||||
|
||||
|
||||
if (!dryRun) {
|
||||
await fs.writeFile(filePath, modifiedContent, 'utf-8');
|
||||
}
|
||||
|
||||
|
||||
return formattedDiff;
|
||||
}
|
||||
|
||||
@@ -383,6 +387,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
"finding specific files within a directory. Only works within allowed directories.",
|
||||
inputSchema: zodToJsonSchema(ListDirectoryArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "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: zodToJsonSchema(DirectoryTreeArgsSchema) as ToolInput,
|
||||
},
|
||||
{
|
||||
name: "move_file",
|
||||
description:
|
||||
@@ -413,7 +426,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
},
|
||||
{
|
||||
name: "list_allowed_directories",
|
||||
description:
|
||||
description:
|
||||
"Returns the list of directories that this server is allowed to access. " +
|
||||
"Use this to understand which directories are available before trying to access files.",
|
||||
inputSchema: {
|
||||
@@ -517,6 +530,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
};
|
||||
}
|
||||
|
||||
case "directory_tree": {
|
||||
const parsed = DirectoryTreeArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
throw new Error(`Invalid arguments for directory_tree: ${parsed.error}`);
|
||||
}
|
||||
|
||||
interface TreeEntry {
|
||||
name: string;
|
||||
type: 'file' | 'directory';
|
||||
children?: TreeEntry[];
|
||||
}
|
||||
|
||||
async function buildTree(currentPath: 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 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);
|
||||
}
|
||||
|
||||
result.push(entryData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const treeData = await buildTree(parsed.data.path);
|
||||
return {
|
||||
content: [{
|
||||
type: "text",
|
||||
text: JSON.stringify(treeData, null, 2)
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
case "move_file": {
|
||||
const parsed = MoveFileArgsSchema.safeParse(args);
|
||||
if (!parsed.success) {
|
||||
|
||||
Reference in New Issue
Block a user