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
JavaScript
#!/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(/ /g, ' ');
text = text.replace(/&/g, '&');
text = text.replace(/</g, '<');
text = text.replace(/>/g, '>');
text = text.replace(/"/g, '"');
text = text.replace(/'/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);