Skip to main content
Glama
doc-handler.ts7.21 kB
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; import * as cheerio from 'cheerio'; import { DocSection, CacheEntry, SearchResult } from './types.js'; export class DocHandler { private cache: Map<string, CacheEntry> = new Map(); private sections: DocSection[] = []; private sectionsLoaded = false; async discoverDocs(): Promise<CallToolResult> { try { const response = await fetch('https://docs.tambo.co/'); if (!response.ok) { throw new Error(`Failed to fetch main docs page: ${response.status}`); } const html = await response.text(); const $ = cheerio.load(html); const discoveredSections: DocSection[] = []; $('a[href]').each((_, element) => { const href = $(element).attr('href'); const text = $(element).text().trim(); if (href && href.startsWith('/') && !href.startsWith('//') && text) { if (href.includes('/docs/') || href.match(/^\/(concepts?|api|cli|examples?|getting-started|guides?)/)) { const category = this.extractCategory(href); discoveredSections.push({ path: href, title: text, category }); } } }); const uniqueSections = discoveredSections .filter((section, index, self) => index === self.findIndex(s => s.path === section.path) ) .sort((a, b) => a.path.localeCompare(b.path)); this.sections = uniqueSections; this.sectionsLoaded = true; return { content: [ { type: 'text', text: `Discovered ${uniqueSections.length} documentation sections:\n\n${ uniqueSections.map(s => `• **${s.title}** - ${s.path}${s.category ? ` (${s.category})` : ''}` ).join('\n') }`, }, ], }; } catch (error) { throw new Error(`Failed to discover documentation: ${error instanceof Error ? error.message : String(error)}`); } } private extractCategory(path: string): string | undefined { const segments = path.split('/').filter(Boolean); return segments.length > 0 ? segments[0] : undefined; } async ensureSectionsLoaded(): Promise<void> { if (!this.sectionsLoaded) { await this.discoverDocs(); } } async fetchDocs(path: string): Promise<CallToolResult> { if (!path) { throw new Error('Path is required'); } const url = `https://docs.tambo.co${path}`; const cacheKey = `docs:${path}`; const cached = this.cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < 10 * 60 * 1000) { return cached.content; } try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch ${url}: ${response.status}`); } const html = await response.text(); const $ = cheerio.load(html); const title = $('h1').first().text().trim() || $('[data-title]').first().text().trim() || $('title').text().trim(); let contentElement = $('main').first(); if (contentElement.length === 0) { contentElement = $('article').first(); } if (contentElement.length === 0) { contentElement = $('.content, [data-content], .markdown-body').first(); } const content = contentElement.length > 0 ? contentElement.html() : ''; const cleanContent = content ? cheerio.load(content).text().replace(/\s+/g, ' ').trim() : 'No content found'; const result: CallToolResult = { content: [ { type: 'text', text: `# ${title}\n\nPath: ${path}\nURL: ${url}\n\n${cleanContent}`, }, ], }; this.cache.set(cacheKey, { content: result, timestamp: Date.now() }); return result; } catch (error) { throw new Error(`Failed to fetch documentation: ${error instanceof Error ? error.message : String(error)}`); } } async searchDocs(query: string): Promise<CallToolResult> { if (!query) { throw new Error('Search query is required'); } await this.ensureSectionsLoaded(); const results: SearchResult[] = []; const sectionsToSearch = this.sections.length > 0 ? this.sections : [ { path: '/getting-started/quickstart', title: 'Quickstart' }, { path: '/concepts/components', title: 'Components' }, { path: '/api-reference/react-hooks', title: 'React Hooks' }, ]; for (const section of sectionsToSearch) { try { const response = await this.fetchDocs(section.path); const textContent = response.content[0] && 'text' in response.content[0] ? response.content[0].text : ''; const content = String(textContent || '').toLowerCase(); if (content.includes(query.toLowerCase())) { results.push({ path: section.path, title: section.title, category: section.category, snippet: this.extractSnippet(content, query) }); } } catch (error) { console.error(`Error searching ${section.path}:`, error instanceof Error ? error.message : String(error)); } } return { content: [ { type: 'text', text: results.length > 0 ? `Found ${results.length} results for "${query}":\n\n${results.map(r => `**${r.title}** (${r.path})${r.category ? ` [${r.category}]` : ''}\n${r.snippet}\n` ).join('\n')}` : `No results found for "${query}"`, }, ], }; } async listSections(): Promise<CallToolResult> { await this.ensureSectionsLoaded(); if (this.sections.length === 0) { return { content: [ { type: 'text', text: 'No documentation sections discovered. Try running discover_docs first.', }, ], }; } const grouped = this.sections.reduce((acc, section) => { const category = section.category || 'Other'; if (!acc[category]) acc[category] = []; acc[category].push(section); return acc; }, {} as Record<string, DocSection[]>); const output = Object.entries(grouped) .map(([category, sections]) => `## ${category}\n${sections.map(s => `• **${s.title}** - ${s.path}`).join('\n')}` ) .join('\n\n'); return { content: [ { type: 'text', text: `Available documentation sections (${this.sections.length} total):\n\n${output}`, }, ], }; } private extractSnippet(text: string, query: string, length = 150): string { const lowerText = text.toLowerCase(); const lowerQuery = query.toLowerCase(); const index = lowerText.indexOf(lowerQuery); if (index === -1) return text.substring(0, length) + '...'; const start = Math.max(0, index - 50); const end = Math.min(text.length, index + query.length + 100); return '...' + text.substring(start, end) + '...'; } }

Implementation Reference

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/kylegrahammatzen/tambo-mcp-server'

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