Skip to main content
Glama

Motion.dev MCP Server

search.ts9.04 kB
/** * Documentation search tool implementation * Provides full-text search across Motion.dev documentation */ import Fuse from 'fuse.js'; import { DocumentationEndpoint, CategorizedEndpoints, Framework, DocumentationCategory } from '../types/motion.js'; import { SearchMotionDocsParams } from './documentation.js'; import { logger } from '../utils/logger.js'; import { MotionMCPError, createValidationError } from '../utils/errors.js'; export interface SearchResult { endpoint: DocumentationEndpoint; score: number; matches: Array<{ field: string; value: string; indices: Array<[number, number]>; }>; } export interface SearchMotionDocsResponse { success: boolean; results: SearchResult[]; totalFound: number; searchTime: number; query: string; filters?: { framework?: string; category?: string; }; } export class SearchTool { private fuse: Fuse<DocumentationEndpoint> | null = null; private endpoints: DocumentationEndpoint[] = []; // private categorizedEndpoints: CategorizedEndpoints = { // Reserved for future use // react: [], // js: [], // vue: [], // general: [] // }; constructor() { this.initializeFuse(); } private initializeFuse(): void { const fuseOptions: any = { keys: [ { name: 'title', weight: 0.6 }, { name: 'url', weight: 0.3 }, { name: 'framework', weight: 0.1 } ], threshold: 0.3, includeScore: true, includeMatches: true, minMatchCharLength: 2, ignoreLocation: true }; this.fuse = new Fuse([], fuseOptions); } updateEndpoints(endpoints: DocumentationEndpoint[]): void { this.endpoints = endpoints; if (this.fuse) { this.fuse.setCollection(endpoints); } logger.debug(`Search index updated with ${endpoints.length} endpoints`); } updateCategorizedEndpoints(_categorized: CategorizedEndpoints): void { // this.categorizedEndpoints = categorized; // Reserved for future use } async searchMotionDocs(params: SearchMotionDocsParams): Promise<SearchMotionDocsResponse> { const startTime = Date.now(); logger.logToolExecution('search_motion_docs', params); try { // Validate parameters if (!params.query || params.query.trim().length === 0) { throw createValidationError('query', params.query, 'Search query cannot be empty'); } if (params.limit && (params.limit < 1 || params.limit > 100)) { throw createValidationError('limit', params.limit, 'Search limit must be between 1 and 100'); } const limit = params.limit || 10; let searchEndpoints = this.endpoints; // Apply framework filter if (params.framework) { searchEndpoints = searchEndpoints.filter( endpoint => endpoint.framework === params.framework ); } // Apply category filter if (params.category) { searchEndpoints = searchEndpoints.filter( endpoint => endpoint.category === params.category ); } // Perform search let searchResults: SearchResult[]; if (!this.fuse) { this.initializeFuse(); } // Update search collection with filtered endpoints this.fuse!.setCollection(searchEndpoints); const fuseResults = this.fuse!.search(params.query.trim()); searchResults = fuseResults .slice(0, limit) .map((result): SearchResult => ({ endpoint: result.item, score: result.score || 0, matches: result.matches?.map(match => ({ field: match.key || '', value: match.value || '', indices: match.indices?.map(index => [index[0], index[1]] as [number, number]) || [] })) || [] })); const response: SearchMotionDocsResponse = { success: true, results: searchResults, totalFound: fuseResults.length, searchTime: Date.now() - startTime, query: params.query, filters: { framework: params.framework, category: params.category } }; logger.logPerformanceMetric('search_motion_docs', response.searchTime, 'ms'); logger.info(`Search completed: "${params.query}" returned ${response.results.length} results`); return response; } catch (error) { logger.error(`Search failed for query: "${params.query}"`, error as Error); return { success: false, results: [], totalFound: 0, searchTime: Date.now() - startTime, query: params.query, error: error instanceof MotionMCPError ? error.message : String(error) } as SearchMotionDocsResponse & { error: string }; } } async getSearchSuggestions(query: string, limit: number = 5): Promise<string[]> { try { if (!query || query.trim().length < 2) { return []; } // Extract common terms from endpoint titles const terms = this.endpoints .flatMap(endpoint => endpoint.title.toLowerCase().split(/\s+/) .filter(term => term.length > 2) ) .filter(term => term.includes(query.toLowerCase())) .slice(0, limit); return [...new Set(terms)]; // Remove duplicates } catch (error) { logger.warn(`Failed to generate search suggestions for: "${query}"`, { error: (error as Error).message }); return []; } } async getPopularSearches(): Promise<Array<{ query: string; category: string }>> { // Return popular/common searches for Motion.dev return [ { query: 'animation', category: 'animation' }, { query: 'spring', category: 'animation' }, { query: 'gesture', category: 'gestures' }, { query: 'scroll', category: 'scroll-animations' }, { query: 'layout', category: 'layout-animations' }, { query: 'transition', category: 'animation' }, { query: 'drag', category: 'gestures' }, { query: 'hover', category: 'gestures' }, { query: 'keyframes', category: 'animation' }, { query: 'variants', category: 'animation' } ]; } async searchByCategory(category: DocumentationCategory, limit: number = 20): Promise<DocumentationEndpoint[]> { try { const categoryEndpoints = this.endpoints.filter( endpoint => endpoint.category === category ); return categoryEndpoints.slice(0, limit); } catch (error) { logger.error(`Category search failed: ${category}`, error as Error); return []; } } async searchByFramework(framework: Framework | 'general', limit: number = 50): Promise<DocumentationEndpoint[]> { try { const frameworkEndpoints = this.endpoints.filter( endpoint => endpoint.framework === framework ); return frameworkEndpoints.slice(0, limit); } catch (error) { logger.error(`Framework search failed: ${framework}`, error as Error); return []; } } async getSearchStats(): Promise<{ totalEndpoints: number; byFramework: Record<string, number>; byCategory: Record<DocumentationCategory, number>; searchCapabilities: string[]; }> { const byFramework: Record<string, number> = {}; const byCategory: Record<DocumentationCategory, number> = { 'animation': 0, 'gestures': 0, 'layout-animations': 0, 'scroll-animations': 0, 'components': 0, 'api-reference': 0, 'guides': 0, 'examples': 0, 'best-practices': 0 }; for (const endpoint of this.endpoints) { byFramework[endpoint.framework] = (byFramework[endpoint.framework] || 0) + 1; byCategory[endpoint.category] = (byCategory[endpoint.category] || 0) + 1; } return { totalEndpoints: this.endpoints.length, byFramework, byCategory, searchCapabilities: [ 'Full-text search across titles and URLs', 'Framework filtering (react, js, vue, general)', 'Category filtering by animation type', 'Fuzzy matching with relevance scoring', 'Search suggestions and popular queries', 'Batch operations and bulk search' ] }; } async bulkSearch(queries: string[], options?: { framework?: Framework | 'general'; category?: DocumentationCategory; limit?: number; }): Promise<Map<string, SearchResult[]>> { const results = new Map<string, SearchResult[]>(); try { for (const query of queries) { const searchResponse = await this.searchMotionDocs({ query, framework: options?.framework, category: options?.category, limit: options?.limit || 5 }); if (searchResponse.success) { results.set(query, searchResponse.results); } else { results.set(query, []); } } logger.info(`Bulk search completed for ${queries.length} queries`); return results; } catch (error) { logger.error('Bulk search failed', error as Error); return results; } } }

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/Abhishekrajpurohit/motion-dev-mcp'

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