Skip to main content
Glama
scene.ts6.79 kB
import { z } from 'zod'; import { FoundryClient } from '../foundry-client.js'; import { Logger } from '../logger.js'; export interface SceneToolsOptions { foundryClient: FoundryClient; logger: Logger; } export class SceneTools { private foundryClient: FoundryClient; private logger: Logger; constructor({ foundryClient, logger }: SceneToolsOptions) { this.foundryClient = foundryClient; this.logger = logger.child({ component: 'SceneTools' }); } /** * Tool definitions for scene operations */ getToolDefinitions() { return [ { name: 'get-current-scene', description: 'Get information about the currently active scene, including tokens and layout', inputSchema: { type: 'object', properties: { includeTokens: { type: 'boolean', description: 'Whether to include detailed token information (default: true)', default: true, }, includeHidden: { type: 'boolean', description: 'Whether to include hidden tokens and elements (default: false)', default: false, }, }, }, }, { name: 'get-world-info', description: 'Get basic information about the Foundry world and system', inputSchema: { type: 'object', properties: {}, }, }, ]; } async handleGetCurrentScene(args: any): Promise<any> { const schema = z.object({ includeTokens: z.boolean().default(true), includeHidden: z.boolean().default(false), }); const { includeTokens, includeHidden } = schema.parse(args); this.logger.info('Getting current scene information', { includeTokens, includeHidden }); try { const sceneData = await this.foundryClient.query('foundry-mcp-bridge.getActiveScene'); this.logger.debug('Successfully retrieved scene data', { sceneId: sceneData.id, sceneName: sceneData.name, tokenCount: sceneData.tokens?.length || 0, }); return this.formatSceneResponse(sceneData, includeTokens, includeHidden); } catch (error) { this.logger.error('Failed to get current scene', error); throw new Error(`Failed to get current scene: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async handleGetWorldInfo(_args: any): Promise<any> { this.logger.info('Getting world information'); try { const worldData = await this.foundryClient.query('foundry-mcp-bridge.getWorldInfo'); this.logger.debug('Successfully retrieved world data', { worldId: worldData.id, system: worldData.system, }); return this.formatWorldResponse(worldData); } catch (error) { this.logger.error('Failed to get world information', error); throw new Error(`Failed to get world information: ${error instanceof Error ? error.message : 'Unknown error'}`); } } private formatSceneResponse(sceneData: any, includeTokens: boolean, includeHidden: boolean): any { const response: any = { id: sceneData.id, name: sceneData.name, active: sceneData.active, dimensions: { width: sceneData.width, height: sceneData.height, padding: sceneData.padding, }, hasBackground: !!sceneData.background, navigation: sceneData.navigation, elements: { walls: sceneData.walls || 0, lights: sceneData.lights || 0, sounds: sceneData.sounds || 0, notes: sceneData.notes?.length || 0, }, }; if (includeTokens && sceneData.tokens) { response.tokens = this.formatTokens(sceneData.tokens, includeHidden); response.tokenSummary = this.createTokenSummary(sceneData.tokens, includeHidden); } if (sceneData.notes && sceneData.notes.length > 0) { response.notes = sceneData.notes.map((note: any) => ({ id: note.id, text: this.truncateText(note.text, 100), position: { x: note.x, y: note.y }, })); } return response; } private formatTokens(tokens: any[], includeHidden: boolean): any[] { return tokens .filter(token => includeHidden || !token.hidden) .map(token => ({ id: token.id, name: token.name, position: { x: token.x, y: token.y, }, size: { width: token.width, height: token.height, }, actorId: token.actorId, disposition: this.getDispositionName(token.disposition), hidden: token.hidden, hasImage: !!token.img, })); } private createTokenSummary(tokens: any[], includeHidden: boolean): any { const visibleTokens = includeHidden ? tokens : tokens.filter(t => !t.hidden); const summary = { total: visibleTokens.length, byDisposition: { friendly: 0, neutral: 0, hostile: 0, unknown: 0, }, hasActors: 0, withoutActors: 0, }; visibleTokens.forEach(token => { // Count by disposition const disposition = this.getDispositionName(token.disposition); if (disposition in summary.byDisposition) { summary.byDisposition[disposition as keyof typeof summary.byDisposition]++; } else { summary.byDisposition.unknown++; } // Count actor association if (token.actorId) { summary.hasActors++; } else { summary.withoutActors++; } }); return summary; } private formatWorldResponse(worldData: any): any { return { id: worldData.id, title: worldData.title, system: { id: worldData.system, version: worldData.systemVersion, }, foundry: { version: worldData.foundryVersion, }, users: { total: worldData.users?.length || 0, active: worldData.users?.filter((u: any) => u.active).length || 0, gms: worldData.users?.filter((u: any) => u.isGM).length || 0, players: worldData.users?.filter((u: any) => !u.isGM).length || 0, }, activeUsers: worldData.users ?.filter((u: any) => u.active) .map((u: any) => ({ id: u.id, name: u.name, isGM: u.isGM, })) || [], }; } private getDispositionName(disposition: number): string { switch (disposition) { case -1: return 'hostile'; case 0: return 'neutral'; case 1: return 'friendly'; default: return 'unknown'; } } private truncateText(text: string, maxLength: number): string { if (!text || text.length <= maxLength) { return text; } return text.substring(0, maxLength - 3) + '...'; } }

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/adambdooley/foundry-vtt-mcp'

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