Skip to main content
Glama
tcsenpai

Universal Documentation MCP Server

by tcsenpai
api.ts11 kB
#!/usr/bin/env node import express from 'express'; import cors from 'cors'; import { GitBookScraper } from './scraper.js'; import { ContentStore } from './store.js'; import { gitBookConfig, validateConfig } from './config.js'; import { DomainDetector, DomainInfo } from './domainDetector.js'; export class GitBookRestAPI { private app: express.Application; private scraper: GitBookScraper; private store: ContentStore; private domainInfo: DomainInfo; private port: number; constructor(port: number = 3000) { this.port = port; this.app = express(); this.scraper = new GitBookScraper(gitBookConfig.gitbookUrl); this.store = new ContentStore(); // Initialize with default domain info this.domainInfo = { name: gitBookConfig.serverName, description: gitBookConfig.serverDescription, keywords: gitBookConfig.domainKeywords, toolPrefix: gitBookConfig.toolPrefix }; this.setupMiddleware(); this.setupRoutes(); } private setupMiddleware(): void { this.app.use(cors()); this.app.use(express.json()); this.app.use(express.urlencoded({ extended: true })); } private setupRoutes(): void { // Health check this.app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString() }); }); // API info this.app.get('/api', (req, res) => { res.json({ name: this.domainInfo.name, description: this.domainInfo.description, version: gitBookConfig.serverVersion, endpoints: { search: '/api/search?q=query', page: '/api/page/:path', sections: '/api/sections', sectionPages: '/api/sections/:section/pages', codeBlocks: '/api/page/:path/code', markdown: '/api/page/:path/markdown', refresh: '/api/refresh', status: '/api/status' } }); }); // Search content this.app.get('/api/search', async (req, res) => { try { const query = req.query.q as string; if (!query) { return res.status(400).json({ error: 'Query parameter "q" is required' }); } const limit = Math.min(parseInt(req.query.limit as string) || 20, 100); const offset = parseInt(req.query.offset as string) || 0; const results = await (this.store as any).searchContent(query, limit, offset); const totalResults = await (this.store as any).searchContentCount ? await (this.store as any).searchContentCount(query) : results.length; res.json({ query, results, pagination: { total: totalResults, limit, offset, hasMore: offset + limit < totalResults, nextOffset: offset + limit < totalResults ? offset + limit : null } }); } catch (error) { res.status(500).json({ error: 'Search failed', details: error instanceof Error ? error.message : String(error) }); } }); // Get specific page this.app.get('/api/page/*', async (req, res) => { try { const path = '/' + (req.params as any)[0]; const page = await this.store.getPage(path); if (!page) { return res.status(404).json({ error: 'Page not found', path }); } res.json(page); } catch (error) { res.status(500).json({ error: 'Failed to get page', details: error instanceof Error ? error.message : String(error) }); } }); // Get page markdown this.app.get('/api/page/*/markdown', async (req, res) => { try { const path = '/' + (req.params as any)[0].replace('/markdown', ''); const page = await this.store.getPage(path); if (!page) { return res.status(404).json({ error: 'Page not found', path }); } res.json({ page: { title: page.title, path: page.path, section: page.section, subsection: page.subsection }, markdown: page.markdown, metadata: { markdownLength: page.markdown.length, markdownLines: page.markdown.split('\n').length, hasCodeBlocks: page.codeBlocks.length > 0, codeBlockCount: page.codeBlocks.length } }); } catch (error) { res.status(500).json({ error: 'Failed to get markdown', details: error instanceof Error ? error.message : String(error) }); } }); // Get page code blocks this.app.get('/api/page/*/code', async (req, res) => { try { const path = '/' + (req.params as any)[0].replace('/code', ''); const page = await this.store.getPage(path); if (!page) { return res.status(404).json({ error: 'Page not found', path }); } res.json({ page: { title: page.title, path: page.path, section: page.section }, codeBlocks: page.codeBlocks.map((block, index) => ({ index: index + 1, language: block.language, title: block.title, lineNumbers: block.lineNumbers, code: block.code, codeLength: block.code.length, lineCount: block.code.split('\n').length })), summary: { totalBlocks: page.codeBlocks.length, languages: [...new Set(page.codeBlocks.map(b => b.language))], totalLines: page.codeBlocks.reduce((sum, b) => sum + b.code.split('\n').length, 0) } }); } catch (error) { res.status(500).json({ error: 'Failed to get code blocks', details: error instanceof Error ? error.message : String(error) }); } }); // List sections this.app.get('/api/sections', async (req, res) => { try { const sections = await this.store.listSections(); res.json({ sections: Object.keys(sections).length, data: sections }); } catch (error) { res.status(500).json({ error: 'Failed to list sections', details: error instanceof Error ? error.message : String(error) }); } }); // Get section pages this.app.get('/api/sections/:section/pages', async (req, res) => { try { const section = req.params.section; const pages = await this.store.getSectionPages(section); res.json({ section, pages: pages.length, data: pages }); } catch (error) { res.status(500).json({ error: 'Failed to get section pages', details: error instanceof Error ? error.message : String(error) }); } }); // Refresh content this.app.post('/api/refresh', async (req, res) => { try { await this.scraper.scrapeAll(); const content = this.scraper.getContent(); await this.store.updateContent(content); const failureStats = this.scraper.getFailureStats(); res.json({ success: true, timestamp: new Date().toISOString(), refreshed: Object.keys(content).length, failures: failureStats.failedPages.length, failedPages: failureStats.failedPages, totalRetries: failureStats.totalRetries }); } catch (error) { res.status(500).json({ error: 'Failed to refresh content', details: error instanceof Error ? error.message : String(error) }); } }); // Get status this.app.get('/api/status', async (req, res) => { try { const stats = this.store.getStats(); const failureStats = this.scraper.getFailureStats(); res.json({ server: { name: this.domainInfo.name, description: this.domainInfo.description, version: gitBookConfig.serverVersion, url: gitBookConfig.gitbookUrl, uptime: process.uptime() }, content: { totalPages: stats.totalPages, sections: stats.sections, lastUpdated: stats.lastUpdated, avgContentAgeHours: stats.avgContentAge }, failures: { failedPages: failureStats.failedPages, totalFailures: failureStats.failedPages.length, totalRetries: failureStats.totalRetries }, config: { cacheFile: gitBookConfig.cacheFile, cacheTtlHours: gitBookConfig.cacheTtlHours, maxRetries: gitBookConfig.maxRetries, maxConcurrentRequests: gitBookConfig.maxConcurrentRequests, scrapingDelayMs: gitBookConfig.scrapingDelayMs } }); } catch (error) { res.status(500).json({ error: 'Failed to get status', details: error instanceof Error ? error.message : String(error) }); } }); // 404 handler this.app.use((req, res) => { res.status(404).json({ error: 'Endpoint not found', path: req.path, availableEndpoints: [ '/health', '/api', '/api/search?q=query', '/api/page/:path', '/api/page/:path/markdown', '/api/page/:path/code', '/api/sections', '/api/sections/:section/pages', '/api/refresh', '/api/status' ] }); }); } async start(): Promise<void> { // Validate configuration validateConfig(); // Initial content load console.log('Loading initial content...'); await this.scraper.scrapeAll(); const content = this.scraper.getContent(); await this.store.updateContent(content); // Detect domain after content is loaded this.domainInfo = DomainDetector.detectDomain(content); // Start server this.app.listen(this.port, () => { console.log(`GitBook REST API v${gitBookConfig.serverVersion} running on port ${this.port}`); console.log(`Source: ${gitBookConfig.gitbookUrl}`); console.log(`Domain: ${this.domainInfo.description}`); console.log(`Loaded ${Object.keys(content).length} pages`); console.log(`\nAPI endpoints:`); console.log(` Health: http://localhost:${this.port}/health`); console.log(` API info: http://localhost:${this.port}/api`); console.log(` Search: http://localhost:${this.port}/api/search?q=query`); console.log(` Status: http://localhost:${this.port}/api/status`); }); } } // CLI usage if (require.main === module) { const port = process.env.PORT ? parseInt(process.env.PORT) : 3000; const api = new GitBookRestAPI(port); api.start().catch(console.error); }

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/tcsenpai/mcpbook'

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