Skip to main content
Glama
comprehensive-symbol-index.ts6.85 kB
import {ErrorCode, McpError} from '@modelcontextprotocol/sdk/types.js'; import type {ServerContext} from '../context.js'; import { type SymbolData, type ReferenceData, type FrameworkData, type AppleDevDocsClient, } from '../../apple-client.js'; export type SymbolIndexEntry = { id: string; title: string; path: string; kind: string; abstract: string; platforms: string[]; tokens: string[]; }; export class ComprehensiveSymbolIndex { private readonly symbols = new Map<string, SymbolIndexEntry>(); constructor(private readonly client: AppleDevDocsClient) {} async buildIndex(context: ServerContext): Promise<void> { const {state} = context; const activeTechnology = state.getActiveTechnology(); if (!activeTechnology) { throw new McpError( ErrorCode.InvalidRequest, 'No technology selected. Use `discover_technologies` then `choose_technology` first.', ); } // Load framework data const frameworkData = await this.client.getFramework(activeTechnology.title); this.processSymbolData(frameworkData, activeTechnology.title); // Load all topic sections and their identifiers const allIdentifiers = frameworkData.topicSections.flatMap(section => section.identifiers ?? []); // Process identifiers in batches to avoid overwhelming the API const batchSize = 20; const batches = []; for (let i = 0; i < allIdentifiers.length; i += batchSize) { batches.push(allIdentifiers.slice(i, i + batchSize)); } const batchPromises = batches.map(async batch => { const promises = batch.map(async identifier => { try { const symbolPath = identifier .replace('doc://com.apple.documentation/', '') .replace(/^documentation\//, 'documentation/'); const data = await this.client.getSymbol(symbolPath); this.processSymbolData(data, activeTechnology.title); } catch (error) { console.warn(`Failed to load symbol ${identifier}:`, error instanceof Error ? error.message : String(error)); } }); await Promise.all(promises); }); await Promise.all(batchPromises); } search(query: string, maxResults = 20): SymbolIndexEntry[] { const results: Array<{entry: SymbolIndexEntry; score: number}> = []; const queryTokens = this.tokenize(query); // Check if query contains wildcards const hasWildcards = query.includes('*') || query.includes('?'); for (const [id, entry] of this.symbols.entries()) { let score = 0; if (hasWildcards) { // Wildcard matching const pattern = query .replaceAll('*', '.*') .replaceAll('?', '.') .toLowerCase(); const regex = new RegExp(`^${pattern}$`); if (regex.test(entry.title.toLowerCase()) || regex.test(entry.path.toLowerCase()) || entry.tokens.some(token => regex.test(token))) { score = 100; // High score for wildcard matches } } else { // Regular token-based matching for (const queryToken of queryTokens) { if (entry.title.toLowerCase().includes(queryToken.toLowerCase())) { score += 50; } if (entry.tokens.includes(queryToken)) { score += 30; } if (entry.abstract.toLowerCase().includes(queryToken.toLowerCase())) { score += 10; } } } if (score > 0) { results.push({entry, score}); } } return results .sort((a, b) => b.score - a.score) .slice(0, maxResults) .map(result => result.entry); } getSymbolCount(): number { return this.symbols.size; } clear(): void { this.symbols.clear(); } private tokenize(text: string): string[] { if (!text) { return []; } const tokens = new Set<string>(); // Split on common delimiters const basicTokens = text.split(/[\s/._-]+/).filter(Boolean); for (const token of basicTokens) { // Add lowercase version tokens.add(token.toLowerCase()); // Add original case version for exact matches tokens.add(token); // Handle camelCase/PascalCase (e.g., GridItem -> grid, item, griditem) const camelParts = token.split(/(?=[A-Z])/).filter(Boolean); if (camelParts.length > 1) { for (const part of camelParts) { tokens.add(part.toLowerCase()); tokens.add(part); } // Add concatenated lowercase version tokens.add(camelParts.join('').toLowerCase()); } } return [...tokens]; } private processSymbolData(data: SymbolData | FrameworkData, frameworkName: string): void { const metadata = this.extractMetadata(data); const tokens = this.createTokens(metadata.title, metadata.abstract, metadata.path, metadata.platforms); const entry: SymbolIndexEntry = { id: metadata.path || metadata.title, title: metadata.title, path: metadata.path, kind: metadata.kind, abstract: metadata.abstract, platforms: metadata.platforms, tokens, }; this.symbols.set(metadata.path || metadata.title, entry); this.processReferences(data.references); } private extractMetadata(data: SymbolData | FrameworkData) { const title = data.metadata?.title || 'Unknown'; const path = (data.metadata && 'url' in data.metadata && typeof data.metadata.url === 'string') ? data.metadata.url : ''; const kind = (data.metadata && 'symbolKind' in data.metadata && typeof data.metadata.symbolKind === 'string') ? data.metadata.symbolKind : 'framework'; const abstract = this.client.extractText(data.abstract); const platforms = data.metadata?.platforms?.map(p => p.name).filter(Boolean) || []; return { title, path, kind, abstract, platforms, }; } private createTokens(title: string, abstract: string, path: string, platforms: string[]): string[] { const tokens = new Set<string>(); for (const token of this.tokenize(title)) { tokens.add(token); } for (const token of this.tokenize(abstract)) { tokens.add(token); } for (const token of this.tokenize(path)) { tokens.add(token); } for (const platform of platforms) { for (const token of this.tokenize(platform)) { tokens.add(token); } } return [...tokens]; } private processReferences(references: Record<string, ReferenceData> | undefined): void { if (!references) { return; } for (const [refId, ref] of Object.entries(references)) { if (ref.kind === 'symbol' && ref.title) { this.processReference(refId, ref); } } } private processReference(refId: string, ref: ReferenceData): void { const refTokens = this.createTokens( ref.title, this.client.extractText(ref.abstract ?? []), ref.url || '', ref.platforms?.map(p => p.name).filter(Boolean) ?? [], ); const refEntry: SymbolIndexEntry = { id: refId, title: ref.title, path: ref.url || '', kind: ref.kind ?? 'symbol', abstract: this.client.extractText(ref.abstract ?? []), platforms: ref.platforms?.map(p => p.name).filter(Boolean) ?? [], tokens: refTokens, }; this.symbols.set(refId, refEntry); } }

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/MightyDillah/apple-doc-mcp'

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