UNPKG

memory-api-mcp-server

Version:

MCP Server for Memory API integration with Cursor - enables automatic code context saving and intelligent memory management

619 lines (614 loc) 26.4 kB
#!/usr/bin/env node import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; import path from 'path'; import crypto from 'crypto'; import os from 'os'; const API_BASE_URL = process.env.MEMORY_API_URL || 'https://memory-api-mvp.fly.dev'; const API_TOKEN = process.env.MEMORY_API_TOKEN || ''; // Gerar hash único baseado na máquina function generateDevUserHash() { try { const machineInfo = `${os.hostname()}${os.arch()}${os.platform()}${os.type()}`; const hash = crypto.createHash('sha256').update(machineInfo).digest('hex'); return `dev_user_${hash.substring(0, 12)}`; } catch { // Fallback case const randomHash = crypto.randomBytes(8).toString('hex'); return `dev_user_${randomHash}`; } } const DEV_USER_ID = generateDevUserHash(); class MemoryAPIClient { baseUrl; token; constructor(baseUrl = API_BASE_URL, token = API_TOKEN) { this.baseUrl = baseUrl; this.token = token; } getHeaders() { const headers = { 'Content-Type': 'application/json' }; if (this.token) { headers['Authorization'] = `Bearer ${this.token}`; } return headers; } async createMemory(content, title, tags = []) { const response = await fetch(`${this.baseUrl}/v3/memories`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ userId: DEV_USER_ID, content: content, containerTags: tags }) }); if (!response.ok) { throw new Error(`Failed to create memory: ${response.statusText}`); } return response.json(); } async searchMemories(query, limit = 10, documentThreshold = 0.7) { const response = await fetch(`${this.baseUrl}/v3/search`, { method: 'POST', headers: this.getHeaders(), body: JSON.stringify({ q: query, userId: DEV_USER_ID, limit: limit, documentThreshold: documentThreshold }) }); if (!response.ok) { throw new Error(`Failed to search memories: ${response.statusText}`); } return response.json(); } async getMemory(memoryId) { const response = await fetch(`${this.baseUrl}/v3/memories/${memoryId}/content?userId=${DEV_USER_ID}`, { headers: this.getHeaders() }); if (!response.ok) { throw new Error(`Failed to get memory: ${response.statusText}`); } return response.json(); } async listMemories(limit = 10) { const response = await fetch(`${this.baseUrl}/v3/memories?userId=${DEV_USER_ID}&limit=${limit}`, { headers: this.getHeaders() }); if (!response.ok) { throw new Error(`Failed to list memories: ${response.statusText}`); } return response.json(); } async deleteMemory(memoryId) { const response = await fetch(`${this.baseUrl}/v3/memories/${memoryId}?userId=${DEV_USER_ID}`, { method: 'DELETE', headers: this.getHeaders() }); if (!response.ok) { throw new Error(`Failed to delete memory: ${response.statusText}`); } return response.json(); } } const server = new Server({ name: 'memory-api-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, }); let memoryClient; server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'create_memory', description: 'Cria uma nova memória', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Conteúdo da memória' }, title: { type: 'string', description: 'Título da memória (opcional)' }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags para categorizar a memória' } }, required: ['content'] } }, { name: 'search_memories', description: 'Busca memórias existentes', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Termo de busca' }, limit: { type: 'number', description: 'Número máximo de resultados (padrão: 10)' } }, required: ['query'] } }, { name: 'list_memories', description: 'Lista todas as memórias', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Número máximo de resultados (padrão: 10)' } } } }, { name: 'get_memory', description: 'Recupera uma memória específica pelo ID', inputSchema: { type: 'object', properties: { memory_id: { type: 'string', description: 'ID da memória' } }, required: ['memory_id'] } }, { name: 'delete_memory', description: 'Deleta uma memória', inputSchema: { type: 'object', properties: { memory_id: { type: 'string', description: 'ID da memória a deletar' } }, required: ['memory_id'] } }, { name: 'smart_auto_save', description: 'Salva automaticamente quando o Cursor detecta algo interessante/útil', inputSchema: { type: 'object', properties: { content: { type: 'string', description: 'Conteúdo que o Cursor achou interessante' }, context_type: { type: 'string', description: 'Tipo de contexto (solution, pattern, bug_fix, optimization, etc.)' }, file_path: { type: 'string', description: 'Arquivo onde foi encontrado' }, language: { type: 'string', description: 'Linguagem de programação' }, complexity: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Complexidade da solução' }, reusability: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Potencial de reutilização' } }, required: ['content', 'context_type'] } }, { name: 'smart_auto_search', description: 'Busca automaticamente memórias relevantes baseado no contexto', inputSchema: { type: 'object', properties: { context: { type: 'string', description: 'Contexto atual da conversa ou problema' }, technologies: { type: 'array', items: { type: 'string' }, description: 'Tecnologias mencionadas' }, problem_type: { type: 'string', description: 'Tipo de problema (bug_fix, optimization, implementation, etc.)' }, current_file: { type: 'string', description: 'Arquivo atual sendo trabalhado' }, auto_search: { type: 'boolean', description: 'Se deve fazer busca automática inteligente' } }, required: ['context'] } } ] }; }); server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (!args) { throw new McpError(ErrorCode.InvalidParams, 'Missing arguments'); } try { switch (name) { case 'create_memory': const createResult = await memoryClient.createMemory(args.content, args.title, args.tags); return { content: [ { type: 'text', text: `Memória criada com sucesso! ID: ${createResult.id}` } ] }; case 'search_memories': const searchResult = await memoryClient.searchMemories(args.query, args.limit || 10); const memoriesText = searchResult.results.map((mem) => `ID: ${mem.id}\nConteúdo: ${mem.content.substring(0, 200)}...\nScore: ${mem.score}\n---`).join('\n\n'); return { content: [ { type: 'text', text: `Encontradas ${searchResult.total} memórias:\n\n${memoriesText}` } ] }; case 'list_memories': const listResult = await memoryClient.listMemories(args.limit || 10); const listText = listResult.memories.map((mem) => `ID: ${mem.id}\nConteúdo: ${mem.content}\nCriado: ${mem.created_at}\n---`).join('\n\n'); return { content: [ { type: 'text', text: `Total: ${listResult.total} memórias:\n\n${listText}` } ] }; case 'get_memory': const getResult = await memoryClient.getMemory(args.memory_id); return { content: [ { type: 'text', text: `Memória: ${getResult.id}\nConteúdo: ${getResult.content}\nTags: ${getResult.containerTags?.join(', ') || 'Nenhuma'}` } ] }; case 'delete_memory': await memoryClient.deleteMemory(args.memory_id); return { content: [ { type: 'text', text: `Memória ${args.memory_id} deletada com sucesso!` } ] }; case 'smart_auto_save': const smartOptions = { content: args.content, context_type: args.context_type, file_path: args.file_path, language: args.language, complexity: args.complexity, reusability: args.reusability }; const smartResult = await smartAutoSave(smartOptions); if (smartResult) { return { content: [ { type: 'text', text: `🧠 Memória salva automaticamente! ID: ${smartResult.id}\nTipo: ${args.context_type}\nTags: ${args.context_type === 'solution' ? args.language : args.context_type === 'pattern' ? args.language : args.context_type === 'optimization' ? args.language : args.context_type === 'config' ? args.language : args.context_type === 'function' ? args.language : args.context_type === 'class' ? args.language : 'unknown'}` } ] }; } else { return { content: [ { type: 'text', text: '🧠 Memória não salva devido à baixa relevância' } ] }; } case 'smart_auto_search': const smartSearchOptions = { context: args.context, technologies: args.technologies || [], problem_type: args.problem_type, current_file: args.current_file, auto_search: args.auto_search || true }; const smartSearchResult = await smartAutoSearch(smartSearchOptions); if (smartSearchResult.length === 0) { return { content: [ { type: 'text', text: '🔍 Nenhuma memória relevante encontrada para o contexto atual.' } ] }; } const resultsText = smartSearchResult.map((mem, index) => `🧠 **Memória ${index + 1}** (Score: ${mem.relevance_score.toFixed(2)}) ID: ${mem.id} Contexto: ${mem.context_type || 'N/A'} Linguagem: ${mem.language || 'N/A'} ${mem.content.substring(0, 300)}${mem.content.length > 300 ? '...' : ''} ---`).join('\n\n'); return { content: [ { type: 'text', text: `🔍 **Encontrei ${smartSearchResult.length} memórias relevantes:**\n\n${resultsText}` } ] }; default: throw new McpError(ErrorCode.MethodNotFound, `Ferramenta desconhecida: ${name}`); } } catch (error) { throw new McpError(ErrorCode.InternalError, `Erro ao executar ferramenta: ${error instanceof Error ? error.message : String(error)}`); } }); async function smartAutoSave(options) { try { // Análise de relevância const relevanceScore = calculateRelevance(options); if (relevanceScore < 0.5) { return null; // Não salvar se não for relevante o suficiente } // Gerar tags inteligentes const smartTags = generateSmartTags(options); // Estruturar conteúdo const structuredContent = ` CONTEXTO: ${options.context_type} ARQUIVO: ${options.file_path || 'N/A'} LINGUAGEM: ${options.language || 'unknown'} ${options.content} METADATA: Complexidade ${options.complexity || 'unknown'}, Reusabilidade ${options.reusability || 'unknown'} `.trim(); // Criar memória com metadata enriquecida const createData = { userId: DEV_USER_ID, content: structuredContent, containerTags: smartTags, metadata: { category: options.context_type, language: options.language, file_path: options.file_path, complexity: options.complexity, reusability: options.reusability, auto_saved: true, timestamp: new Date().toISOString(), relevance_score: relevanceScore } }; return await memoryClient.createMemory(createData.content, undefined, createData.containerTags); } catch (error) { console.error('Error in smartAutoSave:', error); return null; } } function calculateRelevance(options) { let score = 0; // Análise de complexidade if (options.complexity === 'high') score += 0.4; else if (options.complexity === 'medium') score += 0.2; // Análise de reusabilidade if (options.reusability === 'high') score += 0.4; else if (options.reusability === 'medium') score += 0.2; // Análise de contexto if (['solution', 'pattern', 'optimization'].includes(options.context_type)) { score += 0.2; } return Math.min(1, score); } function generateSmartTags(options) { const tags = []; if (options.context_type) tags.push(options.context_type); if (options.language) tags.push(options.language); if (options.complexity) tags.push(`complexity_${options.complexity}`); if (options.reusability) tags.push(`reusability_${options.reusability}`); // Adicionar tags específicas baseadas no contexto if (options.file_path) { const ext = path.extname(options.file_path).slice(1); if (ext) tags.push(`file_type_${ext}`); } return tags; } async function smartAutoSearch(options) { try { console.log('🔍 Iniciando busca inteligente:', options); // 1. Extrair palavras-chave do contexto const keywords = extractKeywords(options.context); console.log('📝 Keywords extraídas:', keywords); // 2. Gerar múltiplas queries de busca const searchQueries = generateSearchQueries(keywords, options); console.log('🔍 Queries geradas:', searchQueries); // 3. Executar buscas paralelas (threshold reduzido para 0.4) const searchPromises = searchQueries.map(query => memoryClient.searchMemories(query, 5, 0.4).catch(err => { console.error(`Erro na busca por "${query}":`, err); return { memories: [], total: 0 }; })); const searchResults = await Promise.all(searchPromises); console.log('📊 Resultados brutos:', searchResults.map(r => r.total)); // 4. Consolidar e ranquear resultados const consolidatedResults = consolidateResults(searchResults, options); console.log('🔗 Resultados consolidados:', consolidatedResults.length); // 5. Aplicar scoring inteligente const scoredResults = applyIntelligentScoring(consolidatedResults, options); console.log('⭐ Resultados com score:', scoredResults.length); return scoredResults.slice(0, 3); // Top 3 mais relevantes } catch (error) { console.error('❌ Erro em smartAutoSearch:', error); return []; } } function extractKeywords(context) { // Extrair tecnologias, padrões e conceitos do contexto const techPatterns = /\b(react|vue|angular|node|express|postgresql|mongodb|docker|kubernetes|aws|azure|typescript|javascript|python|java|css|html|api|rest|graphql|auth|cors|jwt|oauth|redis|nginx|webpack|vite|tailwind|bootstrap|supabase|vercel|netlify|git|github|ci|cd|testing|jest|cypress|storybook|figma|sass|less|styled|emotion|mui|antd|chakra|mit|license|apache|gpl|bsd|cc0|unlicense|proprietary)\b/gi; const problemPatterns = /\b(error|bug|fix|problem|issue|optimize|performance|security|validation|authentication|authorization|parsing|serialization|async|await|promise|callback|event|state|component|hook|middleware|route|query|migration|deploy|build|test|debug|cache|session|token|hash|encrypt|decrypt|compress|minify|bundle|import|export|module|package|install|update|upgrade|refactor|clean|format|lint|type|interface|class|function|method|variable|constant|enum|union|generic|extends|implements|licença|licenciamento|copyright|legal|projeto|arquivo|documento|tarefa|todo|concluído|completado|adicionar|remover|criar|configurar|implementar)\b/gi; // Adicionar palavras importantes do contexto (fallback para qualquer palavra significativa) const contextWords = context.toLowerCase().split(/\s+/).filter(word => word.length > 2 && !['the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'que', 'para', 'com', 'uma', 'dos', 'das', 'isto', 'isso', 'este', 'esta'].includes(word)); const techMatches = context.match(techPatterns) || []; const problemMatches = context.match(problemPatterns) || []; // Combinar todas as keywords e pegar as mais relevantes const allKeywords = [...new Set([...techMatches, ...problemMatches, ...contextWords.slice(0, 5)])]; return allKeywords.map(k => k.toLowerCase()); } function generateSearchQueries(keywords, options) { const queries = []; // 1. Query do contexto completo (fallback mais importante) const contextQuery = options.context.trim(); if (contextQuery) { queries.push(contextQuery); } // 2. Query combinada das top 3 keywords if (keywords.length > 0) { queries.push(keywords.slice(0, 3).join(' ')); } // 3. Queries específicas por keyword individual keywords.slice(0, 3).forEach(keyword => { queries.push(keyword); }); // 4. Query baseada no tipo de problema if (options.problem_type) { queries.push(options.problem_type); } // 5. Query baseada no arquivo atual if (options.current_file) { const fileExt = path.extname(options.current_file).slice(1); if (fileExt) { queries.push(fileExt); } } // 6. Queries das tecnologias explícitas if (options.technologies && options.technologies.length > 0) { queries.push(options.technologies.join(' ')); // Tecnologias individuais também options.technologies.forEach(tech => queries.push(tech)); } return [...new Set(queries)].filter(Boolean); } function consolidateResults(searchResults, options) { const memoryMap = new Map(); searchResults.forEach(result => { if (result.memories) { result.memories.forEach((memory) => { if (!memoryMap.has(memory.id)) { memoryMap.set(memory.id, { ...memory, search_hits: 1, search_score: memory.score || 0 }); } else { const existing = memoryMap.get(memory.id); existing.search_hits += 1; existing.search_score = Math.max(existing.search_score, memory.score || 0); } }); } }); return Array.from(memoryMap.values()); } function applyIntelligentScoring(results, options) { return results.map(memory => { let relevanceScore = memory.search_score || 0; // Boost por múltiplas ocorrências relevanceScore += (memory.search_hits - 1) * 0.1; // Boost por tecnologias mencionadas if (options.technologies) { const memoryContent = memory.content.toLowerCase(); options.technologies.forEach(tech => { if (memoryContent.includes(tech.toLowerCase())) { relevanceScore += 0.15; } }); } // Boost por contexto similar if (options.problem_type && memory.content.toLowerCase().includes(options.problem_type.toLowerCase())) { relevanceScore += 0.2; } // Boost por arquivo similar if (options.current_file && memory.content.includes(path.extname(options.current_file))) { relevanceScore += 0.1; } // Extract context metadata const contextMatch = memory.content.match(/CONTEXTO:\s*(\w+)/); const languageMatch = memory.content.match(/LINGUAGEM:\s*(\w+)/); return { ...memory, relevance_score: Math.min(1, relevanceScore), context_type: contextMatch ? contextMatch[1] : 'unknown', language: languageMatch ? languageMatch[1] : 'unknown' }; }).sort((a, b) => b.relevance_score - a.relevance_score); } async function main() { const args = process.argv.slice(2); // Inicializar cliente memoryClient = new MemoryAPIClient(); // Iniciar servidor MCP const transport = new StdioServerTransport(); await server.connect(transport); console.log('Memory API MCP Server rodando via stdio'); } main().catch((error) => { console.error('Erro:', error); process.exit(1); });