UNPKG

hellomoon-mcp-server

Version:

A Model Context Protocol server that provides access to HelloMoon's documentation and API information

580 lines (579 loc) 24.2 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import axios from 'axios'; const DOCS_BASE_URL = 'https://docs.hellomoon.io'; class HelloMoonDocsServer { server; axiosInstance; documentationCache = new Map(); constructor() { this.server = new Server({ name: 'hellomoon-docs-server', version: '0.1.0', }, { capabilities: { resources: {}, tools: {}, }, }); this.axiosInstance = axios.create({ baseURL: DOCS_BASE_URL, headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36', 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', }, timeout: 30000, }); this.setupResourceHandlers(); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } setupResourceHandlers() { // List available resources this.server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'hellomoon-docs://quickstart', name: 'HelloMoon Quickstart Guide', mimeType: 'text/plain', description: 'Stream Builder Quickstart documentation', }, { uri: 'hellomoon-docs://datastreams/overview', name: 'Datastreams Overview', mimeType: 'text/plain', description: 'Overview of all available datastreams', }, { uri: 'hellomoon-docs://api/reference', name: 'API Reference', mimeType: 'text/plain', description: 'HelloMoon API reference documentation', }, ], })); // Resource templates for dynamic documentation access this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates: [ { uriTemplate: 'hellomoon-docs://page/{slug}', name: 'Documentation page by slug', mimeType: 'text/plain', description: 'Access any HelloMoon documentation page by its URL slug', }, { uriTemplate: 'hellomoon-docs://datastream/{type}', name: 'Specific datastream documentation', mimeType: 'text/plain', description: 'Documentation for a specific datastream type', }, ], })); // Handle resource reading this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const uri = request.params.uri; if (uri === 'hellomoon-docs://quickstart') { try { const content = await this.fetchDocumentationPage('/docs/your-first-data-stream'); return { contents: [ { uri: request.params.uri, mimeType: 'text/plain', text: content, }, ], }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to fetch quickstart guide: ${error}`); } } if (uri === 'hellomoon-docs://datastreams/overview') { try { const content = await this.fetchDocumentationPage('/docs/all-datastreams'); return { contents: [ { uri: request.params.uri, mimeType: 'text/plain', text: content, }, ], }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to fetch datastreams overview: ${error}`); } } if (uri === 'hellomoon-docs://api/reference') { try { const content = await this.fetchDocumentationPage('/reference'); return { contents: [ { uri: request.params.uri, mimeType: 'text/plain', text: content, }, ], }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to fetch API reference: ${error}`); } } // Handle dynamic resources const pageMatch = uri.match(/^hellomoon-docs:\/\/page\/(.+)$/); if (pageMatch) { const slug = decodeURIComponent(pageMatch[1]); try { const content = await this.fetchDocumentationPage(`/docs/${slug}`); return { contents: [ { uri: request.params.uri, mimeType: 'text/plain', text: content, }, ], }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to fetch page ${slug}: ${error}`); } } const datastreamMatch = uri.match(/^hellomoon-docs:\/\/datastream\/(.+)$/); if (datastreamMatch) { const type = decodeURIComponent(datastreamMatch[1]); try { const content = await this.fetchDocumentationPage(`/docs/${type}`); return { contents: [ { uri: request.params.uri, mimeType: 'text/plain', text: content, }, ], }; } catch (error) { throw new McpError(ErrorCode.InternalError, `Failed to fetch datastream ${type}: ${error}`); } } throw new McpError(ErrorCode.InvalidRequest, `Unknown resource URI: ${uri}`); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'search_documentation', description: 'Search HelloMoon documentation for specific topics', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query for documentation', }, section: { type: 'string', enum: ['setup', 'datastreams', 'api', 'faq', 'all'], description: 'Specific documentation section to search (optional)', }, }, required: ['query'], }, }, { name: 'get_datastream_info', description: 'Get detailed information about a specific datastream type', inputSchema: { type: 'object', properties: { streamType: { type: 'string', description: 'Type of datastream (e.g., token-price, nft-market-actions, balance-change)', }, }, required: ['streamType'], }, }, { name: 'list_available_datastreams', description: 'List all available HelloMoon datastream types', inputSchema: { type: 'object', properties: {}, }, }, { name: 'get_api_endpoints', description: 'Get information about HelloMoon API endpoints', inputSchema: { type: 'object', properties: { category: { type: 'string', description: 'API category to focus on (optional)', }, }, }, }, { name: 'get_setup_instructions', description: 'Get setup and quickstart instructions', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['stream-builder', 'sdk', 'community-sdks'], description: 'Type of setup instructions', }, }, required: ['type'], }, }, { name: 'answer_question', description: 'Answer questions about HelloMoon based on documentation', inputSchema: { type: 'object', properties: { question: { type: 'string', description: 'Question about HelloMoon functionality', }, }, required: ['question'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'search_documentation': return await this.searchDocumentation(args); case 'get_datastream_info': return await this.getDatastreamInfo(args); case 'list_available_datastreams': return await this.listAvailableDatastreams(); case 'get_api_endpoints': return await this.getApiEndpoints(args); case 'get_setup_instructions': return await this.getSetupInstructions(args); case 'answer_question': return await this.answerQuestion(args); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { console.error(`Error in tool ${name}:`, error); return { content: [ { type: 'text', text: `Error executing ${name}: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); } async fetchDocumentationPage(path) { try { const cacheKey = path; if (this.documentationCache.has(cacheKey)) { return this.documentationCache.get(cacheKey).content; } const response = await this.axiosInstance.get(path); const htmlContent = response.data; // Extract text content from HTML (basic text extraction) const textContent = this.extractTextFromHtml(htmlContent); this.documentationCache.set(cacheKey, { title: this.extractTitle(htmlContent), url: `${DOCS_BASE_URL}${path}`, content: textContent, section: this.categorizeContent(path), }); return textContent; } catch (error) { throw new Error(`Failed to fetch documentation from ${path}: ${error}`); } } extractTextFromHtml(html) { // Remove script and style elements let text = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ''); text = text.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ''); // Remove HTML tags text = text.replace(/<[^>]+>/g, ' '); // Decode HTML entities text = text.replace(/&nbsp;/g, ' '); text = text.replace(/&amp;/g, '&'); text = text.replace(/&lt;/g, '<'); text = text.replace(/&gt;/g, '>'); text = text.replace(/&quot;/g, '"'); text = text.replace(/&#x27;/g, "'"); // Clean up whitespace text = text.replace(/\s+/g, ' ').trim(); return text; } extractTitle(html) { const titleMatch = html.match(/<title[^>]*>([^<]+)<\/title>/i); return titleMatch ? titleMatch[1].trim() : 'HelloMoon Documentation'; } categorizeContent(path) { if (path.includes('setup') || path.includes('quickstart')) return 'setup'; if (path.includes('datastream')) return 'datastreams'; if (path.includes('api') || path.includes('reference')) return 'api'; if (path.includes('faq')) return 'faq'; return 'general'; } async searchDocumentation(args) { const { query, section = 'all' } = args; try { // Define key documentation pages to search const pagesToSearch = [ '/docs/your-first-data-stream', '/docs/all-datastreams', '/docs/token-price', '/docs/nft-secondary-market-actions', '/docs/balance-change', '/docs/token-swap', '/docs/token-transfer', '/reference', ]; const results = []; for (const page of pagesToSearch) { try { const content = await this.fetchDocumentationPage(page); const cachedPage = this.documentationCache.get(page); if (section === 'all' || (cachedPage && cachedPage.section === section)) { // Simple search for query terms if (content.toLowerCase().includes(query.toLowerCase())) { const snippet = this.extractSnippet(content, query); results.push({ title: cachedPage?.title || page, url: `${DOCS_BASE_URL}${page}`, snippet, section: cachedPage?.section || 'unknown', }); } } } catch (error) { console.error(`Error fetching ${page}:`, error); } } return { content: [ { type: 'text', text: JSON.stringify({ query, section, results, totalResults: results.length, searchedPages: pagesToSearch.length, }, null, 2), }, ], }; } catch (error) { throw new Error(`Search failed: ${error}`); } } extractSnippet(content, query, maxLength = 200) { const queryIndex = content.toLowerCase().indexOf(query.toLowerCase()); if (queryIndex === -1) return content.substring(0, maxLength) + '...'; const start = Math.max(0, queryIndex - 50); const end = Math.min(content.length, queryIndex + query.length + 150); return (start > 0 ? '...' : '') + content.substring(start, end) + (end < content.length ? '...' : ''); } async getDatastreamInfo(args) { const { streamType } = args; try { const content = await this.fetchDocumentationPage(`/docs/${streamType}`); return { content: [ { type: 'text', text: JSON.stringify({ streamType, documentation: content, url: `${DOCS_BASE_URL}/docs/${streamType}`, timestamp: new Date().toISOString(), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ streamType, error: `Could not find documentation for stream type: ${streamType}`, suggestion: 'Try using list_available_datastreams to see available types', timestamp: new Date().toISOString(), }, null, 2), }, ], }; } } async listAvailableDatastreams() { const knownDatastreams = [ { name: 'token-price', description: 'Token price changes and monitoring' }, { name: 'balance-change', description: 'Account balance changes' }, { name: 'token-swap', description: 'Token swap events' }, { name: 'token-transfer', description: 'Token transfer events' }, { name: 'nft-secondary-market-actions', description: 'NFT secondary market activities' }, { name: 'nft-collection-listing-stats', description: 'NFT collection floor price and listing stats' }, { name: 'lp-balance-changes', description: 'Liquidity pool balance changes' }, { name: 'lp-creations', description: 'Liquidity pool creation events' }, { name: 'lp-deposits-withdrawals', description: 'LP deposit and withdrawal events' }, { name: 'unparsed-transactions', description: 'Raw unparsed transaction data' }, { name: 'parsed-account-updates', description: 'Parsed account update events' }, { name: 'unparsed-account-updates', description: 'Raw account update events' }, ]; return { content: [ { type: 'text', text: JSON.stringify({ availableDatastreams: knownDatastreams, totalCount: knownDatastreams.length, note: 'Use get_datastream_info tool to get detailed documentation for any specific stream type', documentationUrl: `${DOCS_BASE_URL}/docs/all-datastreams`, }, null, 2), }, ], }; } async getApiEndpoints(args) { try { const content = await this.fetchDocumentationPage('/reference'); return { content: [ { type: 'text', text: JSON.stringify({ apiReference: content, url: `${DOCS_BASE_URL}/reference`, note: 'This contains the complete API reference documentation', timestamp: new Date().toISOString(), }, null, 2), }, ], }; } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ error: 'Could not fetch API reference documentation', suggestion: 'Visit https://docs.hellomoon.io/reference directly', timestamp: new Date().toISOString(), }, null, 2), }, ], }; } } async getSetupInstructions(args) { const { type } = args; const setupPages = { 'stream-builder': '/docs/your-first-data-stream', 'sdk': '/docs/your-first-data-stream-1', 'community-sdks': '/docs/community-sdks', }; const page = setupPages[type]; if (!page) { return { content: [ { type: 'text', text: JSON.stringify({ error: `Unknown setup type: ${type}`, availableTypes: Object.keys(setupPages), }, null, 2), }, ], }; } try { const content = await this.fetchDocumentationPage(page); return { content: [ { type: 'text', text: JSON.stringify({ setupType: type, instructions: content, url: `${DOCS_BASE_URL}${page}`, timestamp: new Date().toISOString(), }, null, 2), }, ], }; } catch (error) { throw new Error(`Failed to get setup instructions: ${error}`); } } async answerQuestion(args) { const { question } = args; // Search for relevant content based on the question const searchResult = await this.searchDocumentation({ query: question, section: 'all' }); return { content: [ { type: 'text', text: JSON.stringify({ question, answer: 'Based on HelloMoon documentation search:', relevantContent: JSON.parse(searchResult.content[0].text), note: 'This answer is based on searching HelloMoon documentation. For the most up-to-date information, visit docs.hellomoon.io', timestamp: new Date().toISOString(), }, null, 2), }, ], }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('HelloMoon Documentation MCP server running on stdio'); } } const server = new HelloMoonDocsServer(); server.run().catch(console.error);