Skip to main content
Glama

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
NameRequiredDescriptionDefault
sourceNoPath to move (deprecated: use sources)
sourcesNoPaths to move
destinationYesNew path

Implementation Reference

  • 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 } : {}),
      });
    }
  • 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;
  • 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
      );
    }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/j0hanz/filesystem-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server