sandbox_root = realpath( MCP_FS_AGENT_ROOT );
// Ensure the root directory exists and is a directory.
return $this->sandbox_root && is_dir( $this->sandbox_root );
}
/**
* The permission callback used for all filesystem abilities.
* Ensures only users with the highest privileges can perform filesystem operations.
*
* @return boolean
*/
public function check_permissions() {
return current_user_can( ‘manage_options’ );
}
/**
* Resolves a relative path provided by the agent against the sandbox root
* and performs rigorous security checks.
*
* @param string $relative_path The path provided by the agent.
* @return string|WP_Error The absolute, validated path, or a WP_Error on failure.
*/
private function resolve_and_validate_path( $relative_path ) {
if ( ! $this->is_enabled() ) {
return new WP_Error( ‘fs_disabled’, ‘Filesystem abilities are not configured or the root directory is invalid.’ );
}
// Normalize the path to prevent directory traversal tricks.
$normalized_path = wp_normalize_path( $relative_path );
// Prevent navigating up from the root.
if ( strpos( $normalized_path, ‘..’ ) !== false ) {
return new WP_Error( ‘fs_path_traversal’, ‘Path traversal is not permitted.’ );
}
// Construct the full path.
$full_path = $this->sandbox_root . ‘/’ . ltrim( $normalized_path, ‘/’ );
// Get the real, canonical path. This resolves symlinks and relative segments.
$real_path = realpath( $full_path );
// If the path doesn’t exist yet (e.g., for write operations), we check its parent.
if ( ! $real_path ) {
$real_parent_path = realpath( dirname( $full_path ) );
if ( strpos( $real_parent_path, $this->sandbox_root ) !== 0 ) {
return new WP_Error( ‘fs_outside_sandbox’, ‘The specified path is outside the configured sandbox.’ );
}
return $full_path; // Return the non-resolved path for creation.
}
// The most important check: ensure the resolved real path is still inside our sandbox root.
if ( strpos( $real_path, $this->sandbox_root ) !== 0 ) {
return new WP_Error( ‘fs_outside_sandbox’, ‘The specified path is outside the configured sandbox.’ );
}
return $real_path;
}
/**
* Registers all the filesystem abilities with the MCP system.
*/
public function register_abilities() {
if ( ! function_exists( ‘wp_register_ability’ ) || ! $this->is_enabled() ) {
return;
}
$category = ‘filesystem’;
// — List Directory —
wp_register_ability( ‘filesystem/list-directory’, [
‘label’ => ‘List Directory’,
‘description’ => ‘Lists files and subdirectories within a specified path in the sandbox.’,
‘category’ => $category,
‘input_schema’ => [
‘type’ => ‘object’,
‘properties’ => [
‘path’ => [ ‘type’ => ‘string’, ‘description’ => ‘The relative path from the sandbox root. Use “/” for the root.’, ‘default’ => ‘/’ ],
],
],
‘output_schema’ => [
‘type’ => ‘object’,
‘properties’ => [
‘items’ => [
‘type’ => ‘array’,
‘items’ => [
‘type’ => ‘object’,
‘properties’ => [
‘name’ => [ ‘type’ => ‘string’ ],
‘type’ => [ ‘type’ => ‘string’, ‘enum’ => [ ‘file’, ‘directory’ ] ],
‘size’ => [ ‘type’ => ‘integer’ ],
‘modified_at’ => [ ‘type’ => ‘string’, ‘format’ => ‘date-time’ ],
],
],
],
],
],
‘execute_callback’ => [ $this, ‘list_directory’ ],
‘permission_callback’ => [ $this, ‘check_permissions’ ],
‘meta’ => [ ‘mcp’ => [ ‘public’ => true, ‘type’ => ‘tool’ ], ‘annotations’ => [ ‘readOnlyHint’ => true ] ],
] );
// — Read File —
wp_register_ability( ‘filesystem/read-file’, [
‘label’ => ‘Read File’,
‘description’ => ‘Reads the content of a specified file from the sandbox.’,
‘category’ => $category,
‘input_schema’ => [
‘type’ => ‘object’,
‘properties’ => [
‘path’ => [ ‘type’ => ‘string’, ‘description’ => ‘The relative path to the file.’ ],
],
‘required’ => [ ‘path’ ],
],
‘output_schema’ => [
‘type’ => ‘object’,
‘properties’ => [
‘content’ => [ ‘type’ => ‘string’, ‘description’ => ‘The content of the file.’ ],
‘encoding’ => [ ‘type’ => ‘string’, ‘description’ => ‘The detected encoding (e.g., utf-8).’, ‘default’ => ‘utf-8’ ],
],
],
‘execute_callback’ => [ $this, ‘read_file’ ],
‘permission_callback’ => [ $this, ‘check_permissions’ ],
‘meta’ => [ ‘mcp’ => [ ‘public’ => true, ‘type’ => ‘tool’ ], ‘annotations’ => [ ‘readOnlyHint’ => true ] ],
] );
// — Write File —
wp_register_ability( ‘filesystem/write-file’, [
‘label’ => ‘Write File’,
‘description’ => ‘Writes content to a file within the sandbox. Disallows writing to sensitive file types.’,
‘category’ => $category,
‘input_schema’ => [
‘type’ => ‘object’,
‘properties’ => [
‘path’ => [ ‘type’ => ‘string’, ‘description’ => ‘The relative path to the file.’ ],
‘content’ => [ ‘type’ => ‘string’, ‘description’ => ‘The content to write.’ ],
‘mode’ => [ ‘type’ => ‘string’, ‘enum’ => [ ‘overwrite’, ‘append’ ], ‘default’ => ‘overwrite’ ],
],
‘required’ => [ ‘path’, ‘content’ ],
],
‘output_schema’ => [ ‘type’ => ‘object’, ‘properties’ => [ ‘success’ => [ ‘type’ => ‘boolean’ ] ] ],
‘execute_callback’ => [ $this, ‘write_file’ ],
‘permission_callback’ => [ $this, ‘check_permissions’ ],
‘meta’ => [ ‘mcp’ => [ ‘public’ => true, ‘type’ => ‘tool’ ] ],
] );
// — Create Directory —
wp_register_ability( ‘filesystem/create-directory’, [
‘label’ => ‘Create Directory’,
‘description’ => ‘Creates a new directory within the sandbox.’,
‘category’ => $category,
‘input_schema’ => [
‘type’ => ‘object’,
‘properties’ => [
‘path’ => [ ‘type’ => ‘string’, ‘description’ => ‘The relative path for the new directory.’ ],
],
‘required’ => [ ‘path’ ],
],
‘output_schema’ => [ ‘type’ => ‘object’, ‘properties’ => [ ‘success’ => [ ‘type’ => ‘boolean’ ] ] ],
‘execute_callback’ => [ $this, ‘create_directory’ ],
‘permission_callback’ => [ $this, ‘check_permissions’ ],
‘meta’ => [ ‘mcp’ => [ ‘public’ => true, ‘type’ => ‘tool’ ] ],
] );
// — Delete Path —
wp_register_ability( ‘filesystem/delete-path’, [
‘label’ => ‘Delete Path’,
‘description’ => ‘Deletes a file or directory from the sandbox.’,
‘category’ => $category,
‘input_schema’ => [
‘type’ => ‘object’,
‘properties’ => [
‘path’ => [ ‘type’ => ‘string’, ‘description’ => ‘The relative path to delete.’ ],
‘recursive’ => [ ‘type’ => ‘boolean’, ‘description’ => ‘If true, deletes directories and their contents. Use with caution.’, ‘default’ => false ],
],
‘required’ => [ ‘path’ ],
],
‘output_schema’ => [ ‘type’ => ‘object’, ‘properties’ => [ ‘success’ => [ ‘type’ => ‘boolean’ ] ] ],
‘execute_callback’ => [ $this, ‘delete_path’ ],
‘permission_callback’ => [ $this, ‘check_permissions’ ],
‘meta’ => [ ‘mcp’ => [ ‘public’ => true, ‘type’ => ‘tool’ ] ],
] );
}
/**
* Execution Callbacks
*/
public function list_directory( $request ) {
$path = $this->resolve_and_validate_path( $request[‘path’] );
if ( is_wp_error( $path ) ) {
return $path;
}
if ( ! is_dir( $path ) ) {
return new WP_Error( ‘fs_not_a_directory’, ‘The specified path is not a directory.’ );
}
$items = [];
$files = scandir( $path );
foreach ( $files as $file ) {
if ( $file === ‘.’ || $file === ‘..’ ) {
continue;
}
$item_path = $path . ‘/’ . $file;
$stat = stat( $item_path );
$items[] = [
‘name’ => $file,
‘type’ => is_dir( $item_path ) ? ‘directory’ : ‘file’,
‘size’ => $stat[‘size’],
‘modified_at’ => gmdate( ‘c’, $stat[‘mtime’] ),
];
}
return [ ‘items’ => $items ];
}
public function read_file( $request ) {
$path = $this->resolve_and_validate_path( $request[‘path’] );
if ( is_wp_error( $path ) ) {
return $path;
}
if ( ! is_file( $path ) ) {
return new WP_Error( ‘fs_not_a_file’, ‘The specified path is not a file.’ );
}
$content = file_get_contents( $path );
if ( $content === false ) {
return new WP_Error( ‘fs_read_failed’, ‘Failed to read file content.’ );
}
return [ ‘content’ => $content, ‘encoding’ => ‘utf-8’ ];
}
public function write_file( $request ) {
// Security: Blacklist dangerous file extensions.
$disallowed_extensions = [ ‘php’, ‘phtml’, ‘php3’, ‘php4’, ‘php5’, ‘php7’, ‘phps’, ‘phar’, ‘htaccess’ ];
$extension = strtolower( pathinfo( $request[‘path’], PATHINFO_EXTENSION ) );
if ( in_array( $extension, $disallowed_extensions, true ) ) {
return new WP_Error( ‘fs_disallowed_file_type’, ‘Writing to this file type is prohibited for security reasons.’ );
}
$path = $this->resolve_and_validate_path( $request[‘path’] );
if ( is_wp_error( $path ) ) {
return $path;
}
$flags = ( $request[‘mode’] === ‘append’ ) ? FILE_APPEND : 0;
$result = file_put_contents( $path, $request[‘content’], $flags );
if ( $result === false ) {
return new WP_Error( ‘fs_write_failed’, ‘Failed to write to the file.’ );
}
return [ ‘success’ => true ];
}
public function create_directory( $request ) {
$path = $this->resolve_and_validate_path( $request[‘path’] );
if ( is_wp_error( $path ) ) {
return $path;
}
if ( file_exists( $path ) ) {
return new WP_Error( ‘fs_path_exists’, ‘The specified path already exists.’ );
}
if ( ! wp_mkdir_p( $path ) ) {
return new WP_Error( ‘fs_mkdir_failed’, ‘Failed to create the directory.’ );
}
return [ ‘success’ => true ];
}
public function delete_path( $request ) {
$path = $this->resolve_and_validate_path( $request[‘path’] );
if ( is_wp_error( $path ) ) {
return $path;
}
if ( ! file_exists( $path ) ) {
return new WP_Error( ‘fs_path_not_found’, ‘The specified path does not exist.’ );
}
if ( is_dir( $path ) ) {
if ( ! $request[‘recursive’] ) {
// Check if directory is empty
if ( count( array_diff( scandir( $path ), [ ‘.’, ‘..’ ] ) ) > 0 ) {
return new WP_Error( ‘fs_dir_not_empty’, ‘Directory is not empty and recursive mode is not enabled.’ );
}
$result = rmdir( $path );
} else {
// A simple recursive delete. For production, a more robust implementation is recommended.
$this->recursive_delete( $path );
$result = ! file_exists( $path );
}
} else {
$result = unlink( $path );
}
if ( ! $result ) {
return new WP_Error( ‘fs_delete_failed’, ‘Failed to delete the specified path.’ );
}
return [ ‘success’ => true ];
}
/**
* Helper for recursive directory deletion.
*
* @param string $dir Path to the directory.
*/
private function recursive_delete( $dir ) {
$files = array_diff( scandir( $dir ), array( ‘.’, ‘..’ ) );
foreach ( $files as $file ) {
( is_dir( “$dir/$file” ) ) ? $this->recursive_delete( “$dir/$file” ) : unlink( “$dir/$file” );
}
return rmdir( $dir );
}
}