Skip to main content
Glama

OpenFGA MCP

DocumentationTools.php34.4 kB
<?php declare(strict_types=1); namespace OpenFGA\MCP\Tools; use OpenFGA\ClientInterface; use OpenFGA\MCP\Documentation\{DocumentationIndex, DocumentationIndexSingleton}; use PhpMcp\Server\Attributes\McpTool; use RuntimeException; use function array_slice; use function count; use function in_array; use function is_array; use function is_scalar; use function sprintf; use function strlen; final readonly class DocumentationTools extends AbstractTools { public function __construct( /** * @phpstan-ignore property.onlyWritten */ private ClientInterface $client, ) { } /** * Find documentation similar to provided content. * * @param string $content Reference content to find similar documentation * @param string|null $sdk Limit search to specific SDK * @param float $similarity_threshold Minimum similarity score (0.0-1.0, default: 0.5) * @param int $limit Maximum number of results (default: 5) * * @throws RuntimeException If documentation index initialization fails * * @return string Markdown-formatted related documentation */ #[McpTool(name: 'find_similar_documentation')] public function findSimilarDocumentation( string $content, ?string $sdk = null, float $similarity_threshold = 0.5, int $limit = 5, ): string { if ('' === trim($content)) { return '❌ Content cannot be empty'; } if (0.0 > $similarity_threshold || 1.0 < $similarity_threshold) { return '❌ Similarity threshold must be between 0.0 and 1.0'; } if (1 > $limit || 20 < $limit) { return '❌ Limit must be between 1 and 20'; } $validSdks = ['php', 'go', 'python', 'java', 'dotnet', 'js', 'laravel', null]; if (! in_array($sdk, $validSdks, true)) { return '❌ Invalid SDK. Must be one of: php, go, python, java, dotnet, js, laravel'; } $index = DocumentationIndexSingleton::getInstance(); if (! $index->isInitialized()) { $index->initialize(); } // Extract key terms from content for similarity matching $keyTerms = $this->extractKeyTerms($content); if ([] === $keyTerms) { return '❌ Could not extract meaningful terms from the provided content'; } // Search for similar content using key terms /** @var array<string, array{chunk_id: string, sdk: string, score: float, preview: string, metadata: array<mixed>, similarity?: float, content?: string}> $similarChunks */ $similarChunks = []; foreach ($keyTerms as $keyTerm) { $chunks = $index->searchChunks($keyTerm, $sdk, $limit * 2); foreach ($chunks as $chunk) { // Get content from the chunk to calculate similarity // We need to get the actual content - the preview is just a snippet $chunkContent = $this->getChunkContent($chunk['chunk_id'], $index); $similarity = $this->calculateSimilarity($content, $chunkContent); if ($similarity >= $similarity_threshold) { // Add similarity and content to the chunk $chunkWithSimilarity = $chunk; $chunkWithSimilarity['similarity'] = $similarity; $chunkWithSimilarity['content'] = $chunkContent; $chunkKey = $chunk['sdk'] . '::' . $chunk['chunk_id']; if (! isset($similarChunks[$chunkKey])) { $similarChunks[$chunkKey] = $chunkWithSimilarity; } elseif (isset($similarChunks[$chunkKey]['similarity'])) { $existingSim = $similarChunks[$chunkKey]['similarity']; if ($existingSim < $similarity) { $similarChunks[$chunkKey] = $chunkWithSimilarity; } } } } } // Sort by similarity score usort($similarChunks, static function (array $a, array $b): int { // Since we know similarity is added above, we can safely access it $aSimilarity = isset($a['similarity']) && is_numeric($a['similarity']) ? $a['similarity'] : 0.0; $bSimilarity = isset($b['similarity']) && is_numeric($b['similarity']) ? $b['similarity'] : 0.0; return $bSimilarity <=> $aSimilarity; }); // Apply limit $similarChunks = array_slice($similarChunks, 0, $limit); if ([] === $similarChunks) { return "## Similar Documentation\n\nNo similar documentation found (threshold: {$similarity_threshold})" . (null !== $sdk ? ' in SDK: ' . $sdk : '') . "\n\nTry:\n- Lowering the similarity threshold\n- Providing more specific content\n- Removing SDK filter for broader results"; } // Build markdown response $markdown = "## Similar Documentation\n\n"; $markdown .= sprintf('**Similarity Threshold:** %s%s', $similarity_threshold, PHP_EOL); if (null !== $sdk) { $markdown .= sprintf('**SDK Filter:** %s%s', $sdk, PHP_EOL); } $markdown .= '**Found:** ' . count($similarChunks) . " similar document(s)\n\n"; $markdown .= "---\n\n"; foreach ($similarChunks as $chunkIndex => $chunk) { $markdown .= $this->formatSimilarResult($chunk, $chunkIndex + 1); } return $markdown; } /** * Search for code examples in documentation. * * @param string $query Code pattern or concept to find * @param string|null $language Programming language filter (php, go, python, java, csharp, javascript, typescript) * @param bool $include_context Include surrounding explanatory context * @param int $limit Maximum number of examples to return (default: 5) * @param int $offset Pagination offset for results (default: 0) * * @throws RuntimeException If documentation index initialization fails * * @return string Markdown-formatted code examples with descriptions */ #[McpTool(name: 'search_code_examples')] public function searchCodeExamples( string $query, ?string $language = null, bool $include_context = true, int $limit = 5, int $offset = 0, ): string { if ('' === trim($query)) { return '❌ Search query cannot be empty'; } if (1 > $limit || 20 < $limit) { return '❌ Limit must be between 1 and 20'; } if (0 > $offset) { return '❌ Offset cannot be negative'; } $validLanguages = ['php', 'go', 'python', 'java', 'csharp', 'javascript', 'typescript', null]; if (! in_array($language, $validLanguages, true)) { return '❌ Invalid language. Must be one of: php, go, python, java, csharp, javascript, typescript'; } $index = DocumentationIndexSingleton::getInstance(); if (! $index->isInitialized()) { $index->initialize(); } // Map language to SDK if applicable $sdk = $this->mapLanguageToSdk($language); // Search for code-related chunks $allChunks = $index->searchChunks($query, $sdk); $codeExamples = []; foreach ($allChunks as $allChunk) { // Get the full content for this chunk $chunkContent = $this->getChunkContent($allChunk['chunk_id'], $index); $chunkWithContent = $allChunk; $chunkWithContent['content'] = $chunkContent; $examples = $this->extractCodeFromChunk($chunkWithContent, $language); foreach ($examples as $example) { $example['chunk'] = $allChunk; $codeExamples[] = $example; } } $totalExamples = count($codeExamples); if (0 === $totalExamples) { return "## Code Examples No code examples found for: **{$query}**" . (null !== $language ? sprintf(' (language: %s)', $language) : '') . "\n\nTry:\n- Searching for specific method or class names\n- Using OpenFGA terminology (e.g., 'check', 'expand', 'tuples')\n- Removing language filter for broader results"; } // Apply pagination $paginatedExamples = array_slice($codeExamples, $offset, $limit); $currentPage = (int) floor($offset / $limit) + 1; $totalPages = (int) ceil($totalExamples / $limit); // Build markdown response $markdown = "## Code Examples\n\n"; $markdown .= "**Search:** `{$query}`\n"; if (null !== $language) { $markdown .= sprintf('**Language:** %s%s', $language, PHP_EOL); } $markdown .= '**Results:** Showing ' . ($offset + 1) . '-' . min($offset + $limit, $totalExamples) . " of {$totalExamples} examples\n\n"; $markdown .= "---\n\n"; foreach ($paginatedExamples as $exampleIndex => $example) { $exampleNumber = $offset + $exampleIndex + 1; $markdown .= $this->formatCodeExample($example, $exampleNumber, $include_context); } // Add pagination info if (1 < $totalPages) { $markdown .= "\n---\n\n### Pagination\n\n"; if (1 < $currentPage) { $prevOffset = max(0, $offset - $limit); $markdown .= sprintf('- **Previous page:** Use offset=%d%s', $prevOffset, PHP_EOL); } if ($currentPage < $totalPages) { $nextOffset = $offset + $limit; $markdown .= sprintf('- **Next page:** Use offset=%d%s', $nextOffset, PHP_EOL); } } return $markdown; } /** * Advanced documentation search with filtering and pagination. * * @param string $query Search query to find in documentation * @param string|null $sdk Filter by specific SDK (php, go, python, java, dotnet, js, laravel) * @param string $search_type Type of search: content (default), class, method, or section * @param int $limit Maximum number of results to return (default: 10) * @param int $offset Pagination offset for results (default: 0) * * @throws RuntimeException If documentation index initialization fails * * @return string Markdown-formatted search results with pagination metadata */ #[McpTool(name: 'search_documentation')] public function searchDocumentation( string $query, ?string $sdk = null, string $search_type = 'content', int $limit = 10, int $offset = 0, ): string { if ('' === trim($query)) { return '❌ Search query cannot be empty'; } if (1 > $limit || 50 < $limit) { return '❌ Limit must be between 1 and 50'; } if (0 > $offset) { return '❌ Offset cannot be negative'; } $validSearchTypes = ['content', 'class', 'method', 'section']; if (! in_array($search_type, $validSearchTypes, true)) { return '❌ Invalid search_type. Must be one of: ' . implode(', ', $validSearchTypes); } $validSdks = ['php', 'go', 'python', 'java', 'dotnet', 'js', 'laravel', null]; if (! in_array($sdk, $validSdks, true)) { return '❌ Invalid SDK. Must be one of: php, go, python, java, dotnet, js, laravel'; } $index = DocumentationIndexSingleton::getInstance(); if (! $index->isInitialized()) { $index->initialize(); } // Get all results first for total count $allResults = $this->performSearch($index, $query, $sdk, $search_type); $totalResults = count($allResults); if (0 === $totalResults) { $markdown = '## Documentation Search Results '; $markdown .= "**Query:** `{$query}` "; if (null !== $sdk) { $markdown .= sprintf('**SDK Filter:** %s%s', $sdk, PHP_EOL); } $markdown .= sprintf('**Search Type:** %s%s', $search_type, PHP_EOL); $markdown .= "\nNo results found for query: **{$query}**"; if (null !== $sdk) { $markdown .= sprintf(' (filtered by SDK: %s)', $sdk); } return $markdown . "\n\nTry:\n- Using different keywords\n- Checking spelling\n- Using broader search terms"; } // Apply pagination $paginatedResults = array_slice($allResults, $offset, $limit); $currentPage = (int) floor($offset / $limit) + 1; $totalPages = (int) ceil($totalResults / $limit); // Build markdown response $markdown = "## Documentation Search Results\n\n"; $markdown .= "**Query:** `{$query}`\n"; if (null !== $sdk) { $markdown .= sprintf('**SDK Filter:** %s%s', $sdk, PHP_EOL); } $markdown .= sprintf('**Search Type:** %s%s', $search_type, PHP_EOL); $markdown .= '**Results:** Showing ' . ($offset + 1) . '-' . min($offset + $limit, $totalResults) . " of {$totalResults} total results\n"; $markdown .= "**Page:** {$currentPage} of {$totalPages}\n\n"; $markdown .= "---\n\n"; foreach ($paginatedResults as $resultIndex => $result) { $resultNumber = $offset + (int) $resultIndex + 1; $markdown .= $this->formatSearchResult($result, $resultNumber); } // Add pagination info if (1 < $totalPages) { $markdown .= "\n---\n\n### Pagination\n\n"; if (1 < $currentPage) { $prevOffset = max(0, $offset - $limit); $markdown .= sprintf('- **Previous page:** Use offset=%d%s', $prevOffset, PHP_EOL); } if ($currentPage < $totalPages) { $nextOffset = $offset + $limit; $markdown .= sprintf('- **Next page:** Use offset=%d%s', $nextOffset, PHP_EOL); } } return $markdown; } /** * Calculate similarity between two pieces of content. * * @param string $content1 * @param string $content2 * @return float Similarity score between 0 and 1 */ private function calculateSimilarity(string $content1, string $content2): float { if ('' === $content1 || '' === $content2) { return 0.0; } // Extract terms from both contents $terms1 = $this->extractKeyTerms($content1); $terms2 = $this->extractKeyTerms($content2); if ([] === $terms1 || [] === $terms2) { return 0.0; } // Calculate Jaccard similarity $intersection = count(array_intersect($terms1, $terms2)); $union = count(array_unique(array_merge($terms1, $terms2))); // Union will always be at least 1 since we checked both term arrays are non-empty $jaccard = (float) ($intersection / $union); // Also check for exact phrase matches for higher similarity $phrases = [ 'authorization model', 'permission check', 'tuple creation', 'relationship tuples', 'access control', 'openfga', ]; $phraseBonus = 0.0; foreach ($phrases as $phrase) { if (false !== stripos($content1, $phrase) && false !== stripos($content2, $phrase)) { $phraseBonus += 0.1; } } // Combine scores (cap at 1.0) return min(1.0, $jaccard + $phraseBonus); } /** * Extract code examples from a chunk. * * @param array<string, mixed> $chunk * @param string|null $language * @return array<array<string, mixed>> */ private function extractCodeFromChunk(array $chunk, ?string $language): array { $content = isset($chunk['content']) && is_scalar($chunk['content']) ? (string) $chunk['content'] : ''; $examples = []; // Match code blocks with optional language specification $pattern = '/```(\w+)?\n(.*?)\n```/s'; $result = preg_match_all($pattern, $content, $matches, PREG_SET_ORDER); if (false !== $result && 0 < $result) { foreach ($matches as $match) { // When preg_match_all succeeds with PREG_SET_ORDER, capture groups exist // Optional groups will be empty strings if they don't match /** @var array{0: string, 1: string, 2: string} $match */ $codeLang = $match[1]; $codeContent = $match[2]; // Filter by language if specified if (null !== $language) { $langMatch = false; switch ($language) { case 'php': $langMatch = 'php' === $codeLang || false !== stripos($codeContent, '<?php'); break; case 'go': $langMatch = 'go' === $codeLang || false !== stripos($codeContent, 'func '); break; case 'python': $langMatch = 'python' === $codeLang || 'py' === $codeLang || false !== stripos($codeContent, 'def '); break; case 'java': $langMatch = 'java' === $codeLang || false !== stripos($codeContent, 'public class'); break; case 'csharp': $langMatch = 'csharp' === $codeLang || 'cs' === $codeLang || false !== stripos($codeContent, 'using '); break; case 'javascript': case 'typescript': $langMatch = in_array($codeLang, ['javascript', 'js', 'typescript', 'ts'], true); break; } if (! $langMatch) { continue; } } $examples[] = [ 'language' => '' !== $codeLang ? $codeLang : 'unknown', 'code' => $codeContent, 'context' => $this->extractContext($content, $match[0]), ]; } } return $examples; } /** * Extract context around code. * * @param string $content * @param string $codeBlock */ private function extractContext(string $content, string $codeBlock): string { $position = strpos($content, $codeBlock); if (false === $position) { return ''; } // Get text before the code block (up to 200 chars) $beforeStart = max(0, $position - 200); $beforeText = substr($content, $beforeStart, $position - $beforeStart); // Get text after the code block (up to 200 chars) $afterStart = $position + strlen($codeBlock); $afterText = substr($content, $afterStart, 200); // Clean up and combine $beforeText = trim(preg_replace('/\s+/', ' ', $beforeText) ?? ''); $afterText = trim(preg_replace('/\s+/', ' ', $afterText) ?? ''); $context = ''; if ('' !== $beforeText) { $context .= '...' . $beforeText; } $context .= ' [CODE] '; if ('' !== $afterText) { $context .= $afterText . '...'; } return trim($context); } /** * Extract key terms from content for similarity matching. * * @param string $content * @return array<string> */ private function extractKeyTerms(string $content): array { // Remove code blocks and special characters $cleanContent = preg_replace('/```[\s\S]*?```/', '', $content); $cleanContent ??= ''; $cleanContent = preg_replace('/[^a-zA-Z0-9\s]/', ' ', $cleanContent); $cleanContent ??= ''; // Extract words $words = preg_split('/\s+/', strtolower($cleanContent)); $words = false !== $words ? $words : []; // Filter out common words and short words $stopWords = ['the', 'is', 'at', 'which', 'on', 'and', 'a', 'an', 'as', 'are', 'was', 'were', 'been', 'be', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must', 'can', 'this', 'that', 'these', 'those', 'i', 'you', 'he', 'she', 'it', 'we', 'they', 'what', 'which', 'who', 'when', 'where', 'why', 'how', 'all', 'each', 'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'only', 'own', 'same', 'so', 'than', 'too', 'very', 'just', 'in', 'of', 'to', 'for', 'with', 'from', 'up', 'out', 'if', 'about', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'under', 'again', 'further', 'then', 'once']; $termCounts = []; foreach ($words as $word) { if (2 < strlen($word) && ! in_array($word, $stopWords, true)) { if (! isset($termCounts[$word])) { $termCounts[$word] = 0; } ++$termCounts[$word]; } } // Sort by frequency and take top terms arsort($termCounts); $keyTerms = array_keys(array_slice($termCounts, 0, 10)); // Add OpenFGA-specific terms if present $openfgaTerms = ['openfga', 'authorization', 'permission', 'tuple', 'relation', 'check', 'expand', 'store', 'model', 'user', 'object']; foreach ($openfgaTerms as $openfgaTerm) { if (false !== stripos($content, $openfgaTerm) && ! in_array($openfgaTerm, $keyTerms, true)) { $keyTerms[] = $openfgaTerm; } } return array_slice($keyTerms, 0, 15); } /** * Format a code example as markdown. * * @param array<string, mixed> $example * @param int $number * @param bool $includeContext */ private function formatCodeExample(array $example, int $number, bool $includeContext): string { $markdown = "### Example {$number}\n\n"; $chunk = isset($example['chunk']) && is_array($example['chunk']) ? $example['chunk'] : []; // Add metadata if (isset($chunk['sdk']) && '' !== $chunk['sdk']) { $chunkSdk = is_scalar($chunk['sdk']) ? (string) $chunk['sdk'] : ''; $markdown .= "**SDK:** `{$chunkSdk}` \n"; } // Access class and method from metadata $metadata = isset($chunk['metadata']) && is_array($chunk['metadata']) ? $chunk['metadata'] : []; if (isset($metadata['class']) && '' !== $metadata['class']) { $chunkClass = is_scalar($metadata['class']) ? (string) $metadata['class'] : ''; $markdown .= sprintf('**Class:** `%s`', $chunkClass); if (isset($metadata['method']) && '' !== $metadata['method']) { $chunkMethod = is_scalar($metadata['method']) ? (string) $metadata['method'] : ''; $markdown .= sprintf(' **Method:** `%s`', $chunkMethod); } $markdown .= " \n"; } if (isset($example['language']) && '' !== $example['language'] && 'unknown' !== $example['language']) { $exampleLang = is_scalar($example['language']) ? (string) $example['language'] : ''; $markdown .= "**Language:** `{$exampleLang}` \n"; } $markdown .= "\n"; // Add context if requested if ($includeContext && isset($example['context']) && '' !== $example['context']) { $context = is_scalar($example['context']) ? (string) $example['context'] : ''; $markdown .= "**Context:**\n> " . str_replace('[CODE]', '*(see code below)*', $context) . "\n\n"; } // Add code $lang = isset($example['language']) && is_scalar($example['language']) ? (string) $example['language'] : ''; $code = isset($example['code']) && is_scalar($example['code']) ? (string) $example['code'] : ''; $markdown .= "```{$lang}\n{$code}\n```\n"; return $markdown . "\n---\n\n"; } /** * Format a search result as markdown. * * @param array<string, mixed> $result * @param int $number */ private function formatSearchResult(array $result, int $number): string { $markdown = sprintf('### %d. ', $number); // Build title based on available metadata $title = ''; $metadata = isset($result['metadata']) && is_array($result['metadata']) ? $result['metadata'] : []; if (isset($metadata['class']) && '' !== $metadata['class']) { $title .= is_scalar($metadata['class']) ? (string) $metadata['class'] : ''; if (isset($metadata['method']) && '' !== $metadata['method']) { $title .= '::' . (is_scalar($metadata['method']) ? (string) $metadata['method'] : ''); } } elseif (isset($metadata['section']) && '' !== $metadata['section']) { $title .= is_scalar($metadata['section']) ? (string) $metadata['section'] : ''; } else { $title .= 'Documentation Chunk'; } $markdown .= $title . "\n\n"; // Add metadata if (isset($result['sdk']) && '' !== $result['sdk']) { $sdkName = is_scalar($result['sdk']) ? (string) $result['sdk'] : ''; $markdown .= "**SDK:** `{$sdkName}` \n"; } if (isset($result['source']) && '' !== $result['source']) { $sourceName = is_scalar($result['source']) ? (string) $result['source'] : ''; $markdown .= "**Source:** `{$sourceName}` \n"; } if (isset($result['score']) && 0.0 !== $result['score']) { $scoreValue = is_numeric($result['score']) ? (float) $result['score'] : 0.0; $markdown .= '**Relevance:** ' . (string) round($scoreValue * 100.0) . "% \n"; } $markdown .= "\n"; // Add preview if (isset($result['preview']) && '' !== $result['preview']) { $preview = is_scalar($result['preview']) ? (string) $result['preview'] : ''; // Limit preview length if (500 < strlen($preview)) { $preview = substr($preview, 0, 497) . '...'; } $markdown .= "**Preview:**\n```\n{$preview}\n```\n"; } // Add navigation if (isset($result['chunk_id']) && '' !== $result['chunk_id']) { $sdk = isset($result['sdk']) && is_scalar($result['sdk']) ? (string) $result['sdk'] : 'unknown'; $id = isset($result['chunk_id']) && is_scalar($result['chunk_id']) ? (string) $result['chunk_id'] : 'unknown'; $markdown .= "\n**Reference:** `{$sdk}::{$id}`\n"; } return $markdown . "\n---\n\n"; } /** * Format a similar documentation result as markdown. * * @param array<string, mixed> $chunk * @param int $number */ private function formatSimilarResult(array $chunk, int $number): string { $markdown = sprintf('### %d. ', $number); // Build title $title = ''; $metadata = isset($chunk['metadata']) && is_array($chunk['metadata']) ? $chunk['metadata'] : []; if (isset($metadata['class']) && '' !== $metadata['class']) { $title .= is_scalar($metadata['class']) ? (string) $metadata['class'] : ''; if (isset($metadata['method']) && '' !== $metadata['method']) { $title .= '::' . (is_scalar($metadata['method']) ? (string) $metadata['method'] : ''); } } elseif (isset($metadata['section']) && '' !== $metadata['section']) { $title .= is_scalar($metadata['section']) ? (string) $metadata['section'] : ''; } else { $title .= 'Related Documentation'; } $markdown .= $title . "\n\n"; // Add metadata if (isset($chunk['sdk']) && '' !== $chunk['sdk']) { $chunkSdkValue = is_scalar($chunk['sdk']) ? (string) $chunk['sdk'] : ''; $markdown .= "**SDK:** `{$chunkSdkValue}` \n"; } if (isset($chunk['similarity']) && 0.0 !== $chunk['similarity']) { $simScore = is_numeric($chunk['similarity']) ? (float) $chunk['similarity'] : 0.0; $markdown .= '**Similarity Score:** ' . (string) round($simScore * 100.0) . "% \n"; } if (isset($chunk['source']) && '' !== $chunk['source']) { $sourceValue = is_scalar($chunk['source']) ? (string) $chunk['source'] : ''; $markdown .= "**Source:** `{$sourceValue}` \n"; } $markdown .= "\n"; // Add content preview if (isset($chunk['content']) && '' !== $chunk['content']) { $preview = is_scalar($chunk['content']) ? (string) $chunk['content'] : ''; // Limit preview length if (800 < strlen($preview)) { $preview = substr($preview, 0, 797) . '...'; } $markdown .= "**Content:**\n\n" . $preview . "\n"; } // Add reference if (isset($chunk['chunk_id']) && '' !== $chunk['chunk_id']) { $sdk = isset($chunk['sdk']) && is_scalar($chunk['sdk']) ? (string) $chunk['sdk'] : 'unknown'; $id = isset($chunk['chunk_id']) && is_scalar($chunk['chunk_id']) ? (string) $chunk['chunk_id'] : 'unknown'; $markdown .= "\n**Reference:** `{$sdk}::{$id}`\n"; } return $markdown . "\n---\n\n"; } /** * Get the full content of a chunk by its ID. * * @param string $chunkId * @param DocumentationIndex $index */ private function getChunkContent(string $chunkId, DocumentationIndex $index): string { // For now, we'll use the chunk ID to get content // In a real implementation, we'd fetch this from the index $chunk = $index->getChunkById($chunkId); if (null === $chunk) { return ''; } return $chunk['content'] ?? ''; } /** * Map programming language to SDK. * * @param string|null $language */ private function mapLanguageToSdk(?string $language): ?string { if (null === $language) { return null; } $mapping = [ 'php' => 'php', 'go' => 'go', 'python' => 'python', 'java' => 'java', 'csharp' => 'dotnet', 'javascript' => 'js', 'typescript' => 'js', ]; return $mapping[$language] ?? null; } /** * Perform search based on search type. * * @param DocumentationIndex $index * @param string $query * @param string|null $sdk * @param string $searchType * * @throws RuntimeException If search fails * * @return array<array<string, mixed>> */ private function performSearch(DocumentationIndex $index, string $query, ?string $sdk, string $searchType): array { return match ($searchType) { 'class' => $this->searchForClasses($index, $query, $sdk), 'method' => $this->searchForMethods($index, $query, $sdk), 'section' => $this->searchForSections($index, $query, $sdk), default => $index->searchChunks($query, $sdk), }; } /** * Search specifically for classes. * * @param DocumentationIndex $index * @param string $query * @param string|null $sdk * * @throws RuntimeException If search fails * * @return array<array<string, mixed>> */ private function searchForClasses(DocumentationIndex $index, string $query, ?string $sdk): array { $allChunks = $index->searchChunks($query, $sdk); $classResults = []; foreach ($allChunks as $allChunk) { $metadata = $allChunk['metadata']; if (isset($metadata['class']) && '' !== $metadata['class'] && is_scalar($metadata['class']) && false !== stripos((string) $metadata['class'], $query)) { $classResults[] = $allChunk; } } return $classResults; } /** * Search specifically for methods. * * @param DocumentationIndex $index * @param string $query * @param string|null $sdk * * @throws RuntimeException If search fails * * @return array<array<string, mixed>> */ private function searchForMethods(DocumentationIndex $index, string $query, ?string $sdk): array { $allChunks = $index->searchChunks($query, $sdk); $methodResults = []; foreach ($allChunks as $allChunk) { $metadata = $allChunk['metadata']; if (isset($metadata['method']) && '' !== $metadata['method'] && is_scalar($metadata['method']) && false !== stripos((string) $metadata['method'], $query)) { $methodResults[] = $allChunk; } } return $methodResults; } /** * Search specifically for sections. * * @param DocumentationIndex $index * @param string $query * @param string|null $sdk * * @throws RuntimeException If search fails * * @return array<array<string, mixed>> */ private function searchForSections(DocumentationIndex $index, string $query, ?string $sdk): array { $allChunks = $index->searchChunks($query, $sdk); $sectionResults = []; foreach ($allChunks as $allChunk) { $metadata = $allChunk['metadata']; if (isset($metadata['section']) && '' !== $metadata['section'] && is_scalar($metadata['section']) && false !== stripos((string) $metadata['section'], $query)) { $sectionResults[] = $allChunk; } } return $sectionResults; } }

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/evansims/openfga-mcp'

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