feat(filesystem): add symlink resolution and home directory support to roots protocol
- Add symlink resolution using fs.realpath() for security consistency - Support home directory expansion (~/) in root URI specifications - Improve error handling with null checks, detailed error messages, and informative logging - Change allowedDirectories from constant to variable to support roots protocol directory management
This commit is contained in:
parent
f3891aaf69
commit
2c922a93f9
2 changed files with 40 additions and 22 deletions
|
|
@ -43,7 +43,7 @@ function expandHome(filepath: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store allowed directories in normalized and resolved form
|
// Store allowed directories in normalized and resolved form
|
||||||
const allowedDirectories = await Promise.all(
|
let allowedDirectories = await Promise.all(
|
||||||
args.map(async (dir) => {
|
args.map(async (dir) => {
|
||||||
const expanded = expandHome(dir);
|
const expanded = expandHome(dir);
|
||||||
const absolute = path.resolve(expanded);
|
const absolute = path.resolve(expanded);
|
||||||
|
|
@ -897,10 +897,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Updates allowed directories based on MCP client roots
|
// Updates allowed directories based on MCP client roots
|
||||||
async function updateAllowedDirectoriesFromRoots(roots: Root[]) {
|
async function updateAllowedDirectoriesFromRoots(requestedRoots: Root[]) {
|
||||||
const rootDirs = await getValidRootDirectories(roots);
|
const validatedRootDirs = await getValidRootDirectories(requestedRoots);
|
||||||
if (rootDirs.length > 0) {
|
if (validatedRootDirs.length > 0) {
|
||||||
allowedDirectories.splice(0, allowedDirectories.length, ...rootDirs);
|
allowedDirectories = [...validatedRootDirs];
|
||||||
|
console.error(`Updated allowed directories from MCP roots: ${validatedRootDirs.length} valid directories`);
|
||||||
|
} else {
|
||||||
|
console.error("No valid root directories provided by client");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,26 @@
|
||||||
import { promises as fs, type Stats } from 'fs';
|
import { promises as fs, type Stats } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import os from 'os';
|
||||||
import { normalizePath } from './path-utils.js';
|
import { normalizePath } from './path-utils.js';
|
||||||
import type { Root } from '@modelcontextprotocol/sdk/types.js';
|
import type { Root } from '@modelcontextprotocol/sdk/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a root URI to a normalized directory path.
|
* Converts a root URI to a normalized directory path with basic security validation.
|
||||||
* @param uri - File URI (file://...) or plain directory path
|
* @param rootUri - File URI (file://...) or plain directory path
|
||||||
* @returns Normalized absolute directory path
|
* @returns Promise resolving to validated path or null if invalid
|
||||||
*/
|
*/
|
||||||
function parseRootUri(uri: string): string {
|
async function parseRootUri(rootUri: string): Promise<string | null> {
|
||||||
const rawPath = uri.startsWith('file://') ? uri.slice(7) : uri;
|
try {
|
||||||
return normalizePath(path.resolve(rawPath));
|
const rawPath = rootUri.startsWith('file://') ? rootUri.slice(7) : rootUri;
|
||||||
|
const expandedPath = rawPath.startsWith('~/') || rawPath === '~'
|
||||||
|
? path.join(os.homedir(), rawPath.slice(1))
|
||||||
|
: rawPath;
|
||||||
|
const absolutePath = path.resolve(expandedPath);
|
||||||
|
const resolvedPath = await fs.realpath(absolutePath);
|
||||||
|
return normalizePath(resolvedPath);
|
||||||
|
} catch {
|
||||||
|
return null; // Path doesn't exist or other error
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -29,33 +39,38 @@ function formatDirectoryError(dir: string, error?: unknown, reason?: string): st
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets valid directory paths from MCP root specifications.
|
* Resolves requested root directories from MCP root specifications.
|
||||||
*
|
*
|
||||||
* Converts root URI specifications (file:// URIs or plain paths) into normalized
|
* Converts root URI specifications (file:// URIs or plain paths) into normalized
|
||||||
* directory paths, validating that each path exists and is a directory.
|
* directory paths, validating that each path exists and is a directory.
|
||||||
|
* Includes symlink resolution for security.
|
||||||
*
|
*
|
||||||
* @param roots - Array of root specifications with URI and optional name
|
* @param requestedRoots - Array of root specifications with URI and optional name
|
||||||
* @returns Promise resolving to array of validated directory paths
|
* @returns Promise resolving to array of validated directory paths
|
||||||
*/
|
*/
|
||||||
export async function getValidRootDirectories(
|
export async function getValidRootDirectories(
|
||||||
roots: readonly Root[]
|
requestedRoots: readonly Root[]
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const validDirectories: string[] = [];
|
const validatedDirectories: string[] = [];
|
||||||
|
|
||||||
for (const root of roots) {
|
for (const requestedRoot of requestedRoots) {
|
||||||
const dir = parseRootUri(root.uri);
|
const resolvedPath = await parseRootUri(requestedRoot.uri);
|
||||||
|
if (!resolvedPath) {
|
||||||
|
console.error(formatDirectoryError(requestedRoot.uri, undefined, 'invalid path or inaccessible'));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats: Stats = await fs.stat(dir);
|
const stats: Stats = await fs.stat(resolvedPath);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
validDirectories.push(dir);
|
validatedDirectories.push(resolvedPath);
|
||||||
} else {
|
} else {
|
||||||
console.error(formatDirectoryError(dir, undefined, 'non-directory root'));
|
console.error(formatDirectoryError(resolvedPath, undefined, 'non-directory root'));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(formatDirectoryError(dir, error));
|
console.error(formatDirectoryError(resolvedPath, error));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return validDirectories;
|
return validatedDirectories;
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue