mv
Move or rename files and directories within the filesystem. Specify source paths and a destination to organize or relocate items.
Instructions
Move or rename a file or directory.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| source | No | Path to move (deprecated: use sources) | |
| sources | No | Paths to move | |
| destination | Yes | New path |
Implementation Reference
- src/tools/move-file.ts:62-209 (handler)The core handler function that performs the move/rename operation.
async function handleMoveFile( args: z.infer<typeof MoveFileInputSchema>, signal?: AbortSignal ): Promise<ToolResponse<z.infer<typeof MoveFileOutputSchema>>> { const sources = args.sources ?? (args.source ? [args.source] : []); if (sources.length === 0) { throw new McpError(ErrorCode.E_INVALID_INPUT, 'No sources provided.'); } const validDest = await validatePathForWrite(args.destination, signal); // Check if destination exists and is a directory let destIsDirectory = false; try { const stats = await fs.stat(validDest); destIsDirectory = stats.isDirectory(); } catch (error) { if (isNodeError(error) && error.code !== 'ENOENT') { throw error; } } if (sources.length > 1 && !destIsDirectory) { throw new McpError( ErrorCode.E_INVALID_INPUT, 'Destination must be an existing directory when moving multiple files.' ); } // Ensure destination parent directory exists if it's not an existing directory if (!destIsDirectory) { await withAbort( fs.mkdir(path.dirname(validDest), { recursive: true }), signal ); } const movedSources: string[] = []; const failed: NonNullable<z.infer<typeof MoveFileOutputSchema>['failed']> = []; for (const src of sources) { let validSource: string; try { validSource = await validateExistingPath(src, signal); assertAllowedFileAccess(src, validSource); } catch (error) { failed.push(toMoveFailure(src, error, ErrorCode.E_ACCESS_DENIED)); continue; } const targetPath = destIsDirectory ? path.join(validDest, path.basename(validSource)) : validDest; // Prevent moving a file onto itself if (path.resolve(validSource) === path.resolve(targetPath)) { continue; } // Prevent moving a directory into its own subdirectory // Fixes "Missing validation for moving directory into its own subdirectory" finding if ( path.resolve(targetPath).startsWith(path.resolve(validSource) + path.sep) ) { failed.push( toMoveFailure( src, new McpError( ErrorCode.E_INVALID_INPUT, `Cannot move directory '${src}' into its own subdirectory '${targetPath}'`, src ), ErrorCode.E_INVALID_INPUT ) ); continue; } try { await withAbort(fs.rename(validSource, targetPath), signal); movedSources.push(validSource); } catch (error: unknown) { if (isNodeError(error) && error.code === 'EXDEV') { // Cross-device link, fallback to copy + delete try { await withAbort( fs.cp(validSource, targetPath, { recursive: true }), signal ); } catch (copyError) { failed.push(toMoveFailure(src, copyError)); continue; } // Copy succeeded — now remove source try { await withAbort( fs.rm(validSource, { recursive: true, force: true }), signal ); movedSources.push(validSource); } catch (deleteError) { // Source delete failed after copy — rollback by removing the copy try { await fs.rm(targetPath, { recursive: true, force: true }); } catch { // Rollback failed — data exists in both locations failed.push( toMoveFailure( src, new McpError( ErrorCode.E_UNKNOWN, `Cross-device move partially failed: data exists at both '${validSource}' and '${targetPath}'. ${formatUnknownErrorMessage(deleteError)}`, src ) ) ); continue; } failed.push( toMoveFailure( src, new McpError( ErrorCode.E_UNKNOWN, `Cross-device move failed: could not remove source. ${formatUnknownErrorMessage(deleteError)}`, src ) ) ); } } else { failed.push(toMoveFailure(src, error)); } } } const message = failed.length > 0 ? `Moved ${movedSources.length} item${movedSources.length === 1 ? '' : 's'}; failed to move ${failed.length} item${failed.length === 1 ? '' : 's'}` : `Successfully moved ${movedSources.length} item${movedSources.length === 1 ? '' : 's'} to ${args.destination}`; return buildToolResponse(message, { ok: failed.length === 0, sources: movedSources, destination: validDest, ...(failed.length > 0 ? { failed } : {}), }); } - src/tools/move-file.ts:38-49 (schema)Tool contract definition for 'mv', including schemas.
export const MOVE_FILE_TOOL: ToolContract = { name: 'mv', title: 'Move File', description: 'Move or rename a file or directory.', inputSchema: MoveFileInputSchema, outputSchema: MoveFileOutputSchema, annotations: DESTRUCTIVE_WRITE_TOOL_ANNOTATIONS, gotchas: [ 'On POSIX, an existing destination is silently overwritten; on Windows, rename fails with EEXIST if destination exists.', ], taskSupport: 'forbidden', } as const; - src/tools/move-file.ts:211-278 (registration)Tool registration function for 'mv'.
export function registerMoveFileTool( server: McpServer, options: ToolRegistrationOptions = {} ): void { const handler = ( args: z.infer<typeof MoveFileInputSchema>, extra: ToolExtra ): Promise<ToolResult<z.infer<typeof MoveFileOutputSchema>>> => executeToolWithDiagnostics({ toolName: 'mv', extra, outputSchema: MoveFileOutputSchema, timedSignal: {}, context: { path: args.source ?? args.sources?.[0] }, run: (signal) => handleMoveFile(args, signal), onError: (error) => buildToolErrorResponse( error, ErrorCode.E_UNKNOWN, args.source ?? args.sources?.[0] ), }); const wrappedHandler = wrapToolHandler(handler, { guard: options.isInitialized, progressMessage: (args) => { const dest = path.basename(args.destination); if (args.source && !args.sources?.length) { return `🛠 mv: ${path.basename(args.source)} → ${dest}`; } const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0); return `🛠 mv: ${count} items → ${dest}`; }, completionMessage: (args, result) => { const dest = path.basename(args.destination); if (args.source && !args.sources?.length) { const src = path.basename(args.source); if (result.isError) return `🛠 mv: ${src} → ${dest} • failed`; return `🛠 mv: ${src} → ${dest}`; } const count = (args.source ? 1 : 0) + (args.sources?.length ?? 0); if (result.isError) return `🛠 mv: ${count} items → ${dest} • failed`; return `🛠 mv: ${count} items → ${dest}`; }, }); const validatedHandler = withValidatedArgs( MoveFileInputSchema, wrappedHandler ); if ( registerToolTaskIfAvailable( server, 'mv', MOVE_FILE_TOOL, validatedHandler, options.iconInfo, options.isInitialized ) ) return; server.registerTool( 'mv', withDefaultIcons({ ...MOVE_FILE_TOOL }, options.iconInfo), validatedHandler ); }