Skip to main content
Glama

move_avatar

Direct avatar movement in VRChat by specifying direction (forward, backward, left, right) and duration using the VRChat MCP OSC server.

Instructions

Move the avatar in a specific direction.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
directionYesDirection to move
durationNoDuration in seconds

Implementation Reference

  • Registers the 'move_avatar' MCP tool with input schema (direction: enum['forward','backward','left','right'], duration: number default 1.0) and thin handler delegating to InputTools.move via ToolContext.
    server.tool(
      'move_avatar',
      'Move the avatar in a specific direction.',
      {
        direction: z.enum(['forward', 'backward', 'left', 'right']).describe('Direction to move'),
        duration: z.number().default(1.0).describe('Duration in seconds')
      },
      async ({ direction, duration }, extra) => {
        try {
          const ctx = createToolContext(extra);
          const result = await inputTools.move(direction as MovementDirection, duration, ctx);
          return { content: [{ type: 'text', text: result }] };
        } catch (error) {
          return { 
            content: [{ 
              type: 'text', 
              text: `Error moving avatar: ${error instanceof Error ? error.message : String(error)}` 
            }],
            isError: true
          };
        }
      }
    );
  • Core handler logic: Maps direction to OSC input (MoveForward/Backward/left/right), sends value=1 via wsClient.sendInput with retries, delays for duration, releases to 0 with safety timeout and multiple retry attempts to ensure input release.
    public async move(
      direction: MovementDirection,
      duration: number,
      ctx?: ToolContext
    ): Promise<string> {
      if (ctx) {
        await ctx.info(`Moving ${direction} for ${duration} seconds`);
      }
      
      let inputName: string;
      let value: number;
      let releaseTimer: NodeJS.Timeout | null = null;
      
      try {
        // Map direction to input name and value
        switch (direction) {
          case 'forward':
            inputName = 'MoveForward';
            value = 1;
            break;
          case 'backward':
            inputName = 'MoveBackward';
            value = 1;
            break;
          case 'left':
            inputName = 'Moveleft';
            value = 1;
            break;
          case 'right':
            inputName = 'MoveRight';
            value = 1;
            break;
          default:
            return `Unknown direction: ${direction}`;
        }
        
        // Log the action
        logger.info(`Starting movement: ${direction} (${inputName}=${value}) for ${duration} seconds`);
        
        // Helper function to ensure input release
        const ensureInputRelease = async (): Promise<boolean> => {
          // Try up to 3 times to release the input
          for (let attempt = 1; attempt <= 3; attempt++) {
            try {
              logger.debug(`Releasing input ${inputName} (attempt ${attempt})`);
              const success = await this.wsClient.sendInput(inputName, 0.0);
              if (success) {
                logger.info(`Successfully released input ${inputName}`);
                return true;
              } else {
                logger.warn(`Failed to release input ${inputName} (attempt ${attempt})`);
                // Wait a short time before retry
                if (attempt < 3) await delay(100);
              }
            } catch (error) {
              logger.error(`Error releasing input ${inputName} (attempt ${attempt}): ${error instanceof Error ? error.message : String(error)}`);
              // Wait a short time before retry
              if (attempt < 3) await delay(200);
            }
          }
          return false;
        };
        
        // Always ensure input is released, even if there's an error
        const cleanupAndExit = async (message: string): Promise<string> => {
          if (releaseTimer) {
            clearTimeout(releaseTimer);
            releaseTimer = null;
          }
          
          await ensureInputRelease();
          return message;
        };
        
        // Set up a safety release timer that's longer than the requested duration
        // This ensures movement stops even if there's a problem during the normal flow
        releaseTimer = setTimeout(async () => {
          logger.warn(`Safety timeout for ${inputName} movement triggered`);
          await ensureInputRelease();
        }, (duration + 0.5) * 1000);
        
        // Send input press - use a retry mechanism
        let pressSuccess = false;
        for (let attempt = 1; attempt <= 3; attempt++) {
          try {
            logger.debug(`Sending input ${inputName}=${value} (attempt ${attempt})`);
            pressSuccess = await this.wsClient.sendInput(inputName, value);
            if (pressSuccess) {
              logger.info(`Successfully sent input ${inputName}=${value}`);
              break;
            } else {
              logger.warn(`Failed to send input ${inputName}=${value} (attempt ${attempt})`);
              if (attempt < 3) await delay(100);
            }
          } catch (error) {
            logger.error(`Error sending input ${inputName}=${value} (attempt ${attempt}): ${error instanceof Error ? error.message : String(error)}`);
            if (attempt < 3) await delay(200);
          }
        }
        
        if (!pressSuccess) {
          return await cleanupAndExit(`Failed to start movement: ${direction}`);
        }
        
        // Wait for duration
        logger.debug(`Waiting for ${duration} seconds before releasing ${inputName}`);
        await delay(duration * 1000);
        
        // Send input release
        const releaseSuccess = await ensureInputRelease();
        
        // Clean up the safety timer
        if (releaseTimer) {
          clearTimeout(releaseTimer);
          releaseTimer = null;
        }
        
        if (!releaseSuccess) {
          return `Started moving ${direction} but failed to stop completely`;
        }
        
        return `Moved ${direction} for ${duration} seconds`;
      } catch (error) {
        const errorMsg = `Error during ${direction} movement: ${error instanceof Error ? error.message : String(error)}`;
        logger.error(errorMsg);
        
        // Ensure cleanup happens even on error
        if (releaseTimer) {
          clearTimeout(releaseTimer);
        }
        
        // Try to release the input if we got far enough to define inputName
        if (inputName!) {
          try {
            await this.wsClient.sendInput(inputName, 0.0);
            logger.info(`Emergency release of ${inputName} after error`);
          } catch (releaseError) {
            logger.error(`Failed emergency release: ${releaseError instanceof Error ? releaseError.message : String(releaseError)}`);
          }
        }
        
        return errorMsg;
      }
    }
  • Zod input schema validation for move_avatar tool parameters.
    {
      direction: z.enum(['forward', 'backward', 'left', 'right']).describe('Direction to move'),
      duration: z.number().default(1.0).describe('Duration in seconds')
Install Server

Other Tools

Related Tools

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/Krekun/vrchat-mcp-osc'

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