Skip to main content
Glama
avatarTools.ts14.1 kB
/** * MCP tools for VRChat avatar parameters. */ import { createLogger } from '@vrchat-mcp-osc/utils'; import { AvatarConfig, ParameterValue, ToolContext } from '../types/index.js'; import { WebSocketClient } from '../ws-client.js'; const logger = createLogger('AvatarTools'); /** * Tools for interacting with VRChat avatar parameters. */ export class AvatarTools { private wsClient: WebSocketClient; private logger = createLogger('AvatarTools'); private currentAvatarInfo: { id: string | null; name: string } = { id: null, name: 'Unknown Avatar' }; private avatarConfigs: Map<string, AvatarConfig> = new Map(); /** * Initialize avatar tools. * * @param wsClient - WebSocket client for communication with the relay server */ constructor(wsClient: WebSocketClient) { this.wsClient = wsClient; // Register event handler for avatar changes // this.wsClient.registerEventHandler('avatar/changed', this.onAvatarChange.bind(this)); } /** * Get a list of all available avatars. * * @param ctx - MCP Context (optional) * @returns Promise resolving to an object with avatar IDs as keys and names as values */ public async getAllAvatars(ctx?: ToolContext): Promise<{ [avatarId: string]: string }> { if (ctx) { await ctx.info('Getting list of available avatars'); } try { const avatars = await this.wsClient.getAvatarlist(); if (ctx) { const avatarCount = Object.keys(avatars).length; await ctx.info(`Found ${avatarCount} available avatars`); } return avatars; } catch (error) { const errorMsg = `Error getting avatar list: ${error instanceof Error ? error.message : String(error)}`; this.logger.error(errorMsg); if (ctx) { await ctx.error(errorMsg); } return {}; } } /** * Get the current avatar name. * * @param ctx - MCP Context (optional) * @returns Promise resolving to the name of the current avatar */ public async getAvatarName(ctx?: ToolContext): Promise<string> { if (ctx) { await ctx.info('Getting avatar name'); } try { // Multiple retry attempts let attempts = 0; const maxAttempts = 3; let lastError: Error | null = null; while (attempts < maxAttempts) { attempts++; logger.info(`Getting avatar info (attempt ${attempts}/${maxAttempts})`); try { // Get the latest avatar info with increased timeout const avatarInfo = await this.wsClient.getAvatarInfo(); // Validate the response if (!avatarInfo || !avatarInfo.id) { throw new Error('Invalid avatar info response'); } // Update stored info and return the name this.currentAvatarInfo = avatarInfo; logger.info(`Got avatar info: ${JSON.stringify(this.currentAvatarInfo)}`); // Return result if (ctx) { await ctx.info(`Current avatar: ${this.currentAvatarInfo.name}`); } return this.currentAvatarInfo.name; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); logger.warn(`Attempt ${attempts} failed: ${lastError.message}`); // Wait before retry if (attempts < maxAttempts) { const delay = 500 * attempts; // Increasing delay for each retry logger.info(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // All attempts failed if (lastError) { throw lastError; } else { throw new Error('Failed to get avatar name after multiple attempts'); } } catch (error) { logger.error(`Failed to get avatar info: ${error instanceof Error ? error.message : String(error)}`); // Try to fall back to loaded config info if available const fallbackName = this.getFallbackAvatarName(); if (ctx) { await ctx.warning(`Could not get avatar name: ${error instanceof Error ? error.message : String(error)}. Using fallback: ${fallbackName}`); } return fallbackName; } } /** * Get a fallback avatar name from loaded configs if available * * @returns Best guess at avatar name or default string */ private getFallbackAvatarName(): string { // If we already have a valid name, return it if (this.currentAvatarInfo.id && this.currentAvatarInfo.name !== 'Unknown Avatar') { return this.currentAvatarInfo.name; } // Try to find the config for the current avatar if (this.currentAvatarInfo.id && this.avatarConfigs.has(this.currentAvatarInfo.id)) { const config = this.avatarConfigs.get(this.currentAvatarInfo.id)!; return config.name || 'Unknown Avatar'; } return 'Unknown Avatar'; } /** * Get parameter names for the current avatar. * * @param ctx - MCP Context (optional) * @returns Promise resolving to a list of parameter names */ public async getParameterNames(ctx?: ToolContext): Promise<string[]> { if (ctx) { await ctx.info('Getting avatar parameters'); } try { // Multiple retry attempts let attempts = 0; const maxAttempts = 3; let lastError: Error | null = null; while (attempts < maxAttempts) { attempts++; logger.info(`Getting avatar parameters (attempt ${attempts}/${maxAttempts})`); try { // Get the parameters with increased timeout const parameters = await this.wsClient.getAvatarParameters(); // Validate the response if (!Array.isArray(parameters)) { throw new Error('Invalid parameter response: not an array'); } // Return the list logger.info(`Found ${parameters.length} avatar parameters`); if (parameters.length > 0) { logger.debug(`Parameters: ${parameters.join(', ')}`); } else { logger.warn('No parameters found for current avatar'); } if (ctx) { await ctx.info(`Found ${parameters.length} avatar parameters`); if (parameters.length <= 10 && parameters.length > 0) { await ctx.info(`Parameters: ${parameters.join(', ')}`); } } return parameters; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); logger.warn(`Attempt ${attempts} failed: ${lastError.message}`); // Wait before retry if (attempts < maxAttempts) { const delay = 500 * attempts; // Increasing delay for each retry logger.info(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // All attempts failed if (lastError) { throw lastError; } else { throw new Error('Failed to get avatar parameters after multiple attempts'); } } catch (error) { logger.error(`Failed to get avatar parameters: ${error instanceof Error ? error.message : String(error)}`); // Try to get parameters from the loaded config if we have it const fallbackParams = this.getFallbackParameters(); if (ctx) { await ctx.warning(`Could not get avatar parameters: ${error instanceof Error ? error.message : String(error)}`); if (fallbackParams.length > 0) { await ctx.info(`Using ${fallbackParams.length} parameters from cached config`); } } return fallbackParams; } } /** * Get fallback parameters from loaded config * * @returns Array of parameter names or empty array */ private getFallbackParameters(): string[] { // Try to find the config for the current avatar if (this.currentAvatarInfo.id && this.avatarConfigs.has(this.currentAvatarInfo.id)) { const config = this.avatarConfigs.get(this.currentAvatarInfo.id)!; if (config.parameters && Array.isArray(config.parameters)) { return config.parameters.map(p => p.name); } } // Return common VRChat parameters as absolute fallback return [ 'VRCEmote', 'GestureLeft', 'GestureRight', 'GestureLeftWeight', 'GestureRightWeight' ]; } /** * Set an avatar parameter. * * @param parameterName - Name of the parameter to set * @param value - Value to set * @param ctx - MCP Context (optional) * @returns Promise resolving to a confirmation message */ /** * Change the current avatar. * * @param avatarId - ID of the avatar to change to * @param ctx - MCP Context (optional) * @returns Promise resolving to a confirmation message */ public async setAvatar( avatarId: string, ctx?: ToolContext ): Promise<string> { if (!avatarId) { const errorMsg = 'Missing avatar ID'; logger.error(errorMsg); if (ctx) await ctx.error(errorMsg); return errorMsg; } if (ctx) { await ctx.info(`Changing avatar to ${avatarId}`); } try { // Multiple retry attempts let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { attempts++; logger.info(`Changing avatar to ${avatarId} (attempt ${attempts}/${maxAttempts})`); try { // // Get available avatars // const avatars = await this.wsClient.getAvatarlist(); // if (!avatars || !avatars[avatarId]) { // throw new Error(`Avatar ${avatarId} not found in available avatars`); // } // Send avatar change request using the new method const success = await this.wsClient.setAvatar(avatarId); if (success) { // const successMsg = `Successfully changed avatar to ${avatars[avatarId]} (${avatarId})`; const successMsg = `Successfully changed avatar to (${avatarId})`; logger.info(successMsg); return successMsg; } else { logger.warn(`Failed to change avatar (attempt ${attempts})`); // Try again if we have attempts left if (attempts < maxAttempts) { const delay = 300 * attempts; logger.info(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } catch (error) { logger.warn(`Error changing avatar (attempt ${attempts}): ${error instanceof Error ? error.message : String(error)}`); // Try again if we have attempts left if (attempts < maxAttempts) { const delay = 300 * attempts; logger.info(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // All attempts failed const failMsg = `Failed to change avatar after ${maxAttempts} attempts`; logger.error(failMsg); return failMsg; } catch (error) { const errorMsg = `Error changing avatar: ${error instanceof Error ? error.message : String(error)}`; logger.error(errorMsg); return errorMsg; } } public async setParameter( parameterName: string, value: ParameterValue, ctx?: ToolContext ): Promise<string> { if (!parameterName) { const errorMsg = 'Missing parameter name'; logger.error(errorMsg); if (ctx) await ctx.error(errorMsg); return errorMsg; } // // Validate value type // if (typeof value !== 'number' && typeof value !== 'boolean') { // const errorMsg = `Invalid parameter value type: ${typeof value} (must be number or boolean)`; // logger.error(errorMsg); // if (ctx) await ctx.error(errorMsg); // return errorMsg; // } if (ctx) { await ctx.info(`Setting avatar parameter ${parameterName} to ${value}`); } try { // Multiple retry attempts let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { attempts++; logger.info(`Setting parameter ${parameterName}=${value} (attempt ${attempts}/${maxAttempts})`); try { // Set parameter with timeout const success = await this.wsClient.setAvatarParameter(parameterName, value); if (success) { const successMsg = `Successfully set ${parameterName} to ${value}`; logger.info(successMsg); return successMsg; } else { logger.warn(`Failed to set parameter ${parameterName} (attempt ${attempts})`); // Try again if we have attempts left if (attempts < maxAttempts) { const delay = 300 * attempts; // Increasing delay for each retry logger.info(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } catch (error) { logger.warn(`Error setting parameter ${parameterName} (attempt ${attempts}): ${error instanceof Error ? error.message : String(error)}`); // Try again if we have attempts left if (attempts < maxAttempts) { const delay = 300 * attempts; // Increasing delay for each retry logger.info(`Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // All attempts failed const failMsg = `Failed to set ${parameterName} after ${maxAttempts} attempts`; logger.error(failMsg); return failMsg; } catch (error) { const errorMsg = `Error setting parameter ${parameterName}: ${error instanceof Error ? error.message : String(error)}`; logger.error(errorMsg); return errorMsg; } } }

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