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
JavaScript
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);
});