UNPKG

memberstack-ai-context

Version:

AI context server for Memberstack DOM documentation - provides intelligent access to Memberstack docs for Claude Code, Cursor, and other AI coding assistants

426 lines (369 loc) 13 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListResourcesRequestSchema, ListToolsRequestSchema, McpError, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { DocParser } from './doc-parser.js'; import { join } from 'path'; export class MemberstackDocsServer { private server: Server; private parser: DocParser; constructor(docsPath: string) { this.log('MemberstackDocsServer constructor starting'); this.server = new Server({ name: 'memberstack-docs', version: '1.0.0', }); this.log('Server instance created'); this.log('Creating DocParser...'); this.parser = new DocParser(docsPath); this.log('DocParser created, setting up handlers...'); this.setupHandlers(); this.log('Constructor completed'); } private log(message: string) { try { const { writeFileSync, existsSync, appendFileSync } = require('fs'); const logFile = '/tmp/memberstack-mcp-debug.log'; const timestamp = new Date().toISOString(); const logEntry = `[${timestamp}] ${message}\n`; if (existsSync(logFile)) { appendFileSync(logFile, logEntry); } else { writeFileSync(logFile, logEntry); } } catch {} } private setupHandlers() { this.setupResourceHandlers(); this.setupToolHandlers(); } private setupResourceHandlers() { this.server.setRequestHandler(ListResourcesRequestSchema, async () => { const sections = await this.parser.getAllSections(); return { resources: sections.map(section => ({ uri: `memberstack://docs/${section.id}`, name: section.title, description: `${section.category} - ${section.methods?.join(', ') || 'Documentation section'}`, mimeType: 'text/markdown', })), }; }); this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => { const url = new URL(request.params.uri); if (url.protocol !== 'memberstack:') { throw new McpError(ErrorCode.InvalidRequest, 'Invalid protocol'); } const path = url.pathname; if (path.startsWith('/docs/')) { const sectionId = path.replace('/docs/', ''); const section = await this.parser.getSection(sectionId); if (!section) { throw new McpError(ErrorCode.InvalidRequest, `Section ${sectionId} not found`); } return { contents: [ { uri: request.params.uri, mimeType: 'text/markdown', text: section.content, }, ], }; } throw new McpError(ErrorCode.InvalidRequest, 'Unknown resource path'); }); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'search_memberstack_docs', description: 'Search through Memberstack documentation for specific topics, methods, or concepts', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query (method names, topics, keywords)', }, limit: { type: 'number', description: 'Maximum number of results to return (default: 5)', default: 5, }, }, required: ['query'], }, }, { name: 'get_method_info', description: 'Get detailed information about a specific Memberstack DOM method', inputSchema: { type: 'object', properties: { method_name: { type: 'string', description: 'Name of the method (e.g., loginMemberEmailPassword, getCurrentMember)', }, }, required: ['method_name'], }, }, { name: 'list_methods_by_category', description: 'List all available methods organized by category', inputSchema: { type: 'object', properties: { category: { type: 'string', enum: ['auth', 'members', 'plans', 'ui', 'advanced', 'all'], description: 'Category to filter by, or "all" for everything', default: 'all', }, }, }, }, { name: 'get_section_summary', description: 'Get a summary of a documentation section with key points', inputSchema: { type: 'object', properties: { section_id: { type: 'string', description: 'Section ID (e.g., 02-authentication, 03-member-management)', }, }, required: ['section_id'], }, }, { name: 'get_code_examples', description: 'Get code examples for a specific method or use case', inputSchema: { type: 'object', properties: { topic: { type: 'string', description: 'Method name or use case (e.g., login, social-auth, plan-purchase)', }, }, required: ['topic'], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { switch (request.params.name) { case 'search_memberstack_docs': return await this.handleSearchDocs(request.params.arguments); case 'get_method_info': return await this.handleGetMethodInfo(request.params.arguments); case 'list_methods_by_category': return await this.handleListMethodsByCategory(request.params.arguments); case 'get_section_summary': return await this.handleGetSectionSummary(request.params.arguments); case 'get_code_examples': return await this.handleGetCodeExamples(request.params.arguments); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } }); } private async handleSearchDocs(args: any) { const { query, limit = 5 } = args; const results = await this.parser.search(query, limit); const formattedResults = results.map((result: any) => { const item = result.item; return { title: item.title || item.name, type: item.content ? 'section' : 'method', relevance: (1 - (result.score || 0)) * 100, preview: this.getPreview(item, query), section: item.section || item.id, }; }); return { content: [ { type: 'text', text: `Found ${results.length} results for "${query}":\n\n` + formattedResults.map((r: any) => `**${r.title}** (${r.type}, ${r.relevance.toFixed(0)}% relevant)\n` + `Section: ${r.section}\n` + `Preview: ${r.preview}\n` ).join('\n'), }, ], }; } private async handleGetMethodInfo(args: any) { const { method_name } = args; const method = await this.parser.getMethod(method_name); if (!method) { return { content: [ { type: 'text', text: `Method "${method_name}" not found in documentation.`, }, ], }; } const paramsList = method.parameters.map(p => `- ${p.name}${p.required ? '' : '?'}: ${p.type} ${p.description ? `- ${p.description}` : ''}` ).join('\n'); const examplesList = method.examples.length > 0 ? '\n\n**Examples:**\n```javascript\n' + method.examples.join('\n\n') + '\n```' : ''; return { content: [ { type: 'text', text: `# ${method.name}()\n\n` + `**Signature:**\n\`\`\`typescript\n${method.signature}\n\`\`\`\n\n` + `**Parameters:**\n${paramsList}\n\n` + `**Return Type:** ${method.returnType}\n\n` + `**Documentation Section:** ${method.section}${examplesList}`, }, ], }; } private async handleListMethodsByCategory(args: any) { const { category = 'all' } = args; const sections = await this.parser.getAllSections(); let filteredSections = sections; if (category !== 'all') { filteredSections = sections.filter(s => s.category === category); } const methodsBySection = filteredSections.map(section => ({ section: section.title, category: section.category, methods: section.methods || [], })).filter(s => s.methods.length > 0); const output = methodsBySection.map(s => `## ${s.section} (${s.category})\n${s.methods.map(m => `- ${m}()`).join('\n')}` ).join('\n\n'); return { content: [ { type: 'text', text: `# Memberstack DOM Methods${category !== 'all' ? ` - ${category}` : ''}\n\n${output}`, }, ], }; } private async handleGetSectionSummary(args: any) { const { section_id } = args; const section = await this.parser.getSection(section_id); if (!section) { return { content: [ { type: 'text', text: `Section "${section_id}" not found.`, }, ], }; } // Extract key points (headings and first paragraph of each section) const summary = this.extractSummary(section.content); return { content: [ { type: 'text', text: `# ${section.title}\n\n` + `**Category:** ${section.category}\n` + `**Methods:** ${section.methods?.join(', ') || 'None'}\n\n` + `**Summary:**\n${summary}`, }, ], }; } private async handleGetCodeExamples(args: any) { const { topic } = args; const results = await this.parser.search(topic, 3); const examples: string[] = []; for (const result of results) { const item = result.item; if (item.content) { // Extract code blocks from content const codeBlocks = this.extractCodeBlocks(item.content, topic); examples.push(...codeBlocks); } if (item.examples) { examples.push(...item.examples); } } const uniqueExamples = [...new Set(examples)]; return { content: [ { type: 'text', text: `# Code Examples for "${topic}"\n\n` + uniqueExamples.map(example => `\`\`\`javascript\n${example}\n\`\`\`` ).join('\n\n'), }, ], }; } private getPreview(item: any, query: string): string { const content = item.content || item.signature || ''; const queryIndex = content.toLowerCase().indexOf(query.toLowerCase()); if (queryIndex === -1) { return content.substring(0, 150) + (content.length > 150 ? '...' : ''); } const start = Math.max(0, queryIndex - 50); const end = Math.min(content.length, queryIndex + 100); const preview = content.substring(start, end); return (start > 0 ? '...' : '') + preview + (end < content.length ? '...' : ''); } private extractSummary(content: string): string { const lines = content.split('\n'); const summaryLines: string[] = []; let inCodeBlock = false; for (const line of lines) { if (line.startsWith('```')) { inCodeBlock = !inCodeBlock; continue; } if (inCodeBlock) continue; // Include headings and first paragraph after each heading if (line.startsWith('#') || (line.trim() && !line.startsWith('|') && summaryLines.length < 10)) { summaryLines.push(line); } } return summaryLines.join('\n'); } private extractCodeBlocks(content: string, topic: string): string[] { const codeBlockRegex = /```(?:javascript|typescript)\n([\s\S]*?)\n```/g; const blocks: string[] = []; let match; while ((match = codeBlockRegex.exec(content)) !== null) { const code = match[1]; if (code.toLowerCase().includes(topic.toLowerCase())) { blocks.push(code); } } return blocks; } async run() { this.log('run() method starting'); const transport = new StdioServerTransport(); this.log('StdioServerTransport created'); this.log('Attempting to connect server to transport...'); await this.server.connect(transport); this.log('Server connected to transport successfully - MCP server should be running'); } }