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')

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