import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
Tool,
} from '@modelcontextprotocol/sdk/types.js';
import express, { Request, Response } from 'express';
import {
createMemory,
getAllMemories,
searchMemories,
updateMemory,
deleteMemory,
getMemoryById,
findMemoryByContext,
listMemories,
getMemoryCount,
Memory,
} from './database.js';
const PORT = parseInt(process.env.PORT || '8081', 10);
const app = express();
app.use(express.json());
// Health check endpoint
app.get('/health', (_req: Request, res: Response) => {
res.json({ status: 'healthy', service: 'cursor-memory-mcp', version: '1.0.0' });
});
// Define all memory tools
const tools: Tool[] = [
{
name: 'memory_store',
description: `Store a new memory. Use this when the user says "remember this", "add to memory", "store this", etc.
The memory should be optimized for future retrieval - concise, context-rich, and meaningful.
The agent should ask the user for confirmation before storing.
Returns the stored memory with its assigned ID.`,
inputSchema: {
type: 'object',
properties: {
content: {
type: 'string',
description: 'The memory content to store (max 2000 tokens). Should be optimized and rewritten by the agent for clarity and usefulness.',
},
scope: {
type: 'string',
enum: ['global', 'project'],
description: 'Whether this memory is global (applies to all projects) or project-specific.',
},
project_id: {
type: 'string',
description: 'Required if scope is "project". The workspace/project path this memory belongs to.',
},
tags: {
type: 'array',
items: { type: 'string' },
description: 'Optional tags for categorization (e.g., ["architecture", "preferences", "coding-style"]).',
},
},
required: ['content', 'scope'],
},
},
{
name: 'memory_recall',
description: `Retrieve all memories. Call this at the start of a session to load context.
Returns all global memories plus project-specific memories for the given project.
Use this when starting a new conversation or when instructed to "check your memory".`,
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Optional project path to include project-specific memories.',
},
},
required: [],
},
},
{
name: 'memory_search',
description: `Search memories by keyword, phrase, or tag.
Use this when the user says "check your memory for...", "do you remember...", "what did I say about...", etc.
Searches both memory content and tags.`,
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query - matches against content and tags.',
},
project_id: {
type: 'string',
description: 'Optional project path to include project-specific memories in search.',
},
},
required: ['query'],
},
},
{
name: 'memory_list',
description: `List all memories with their index numbers.
Use this when the user wants to see what memories exist, or before updating/deleting.
Each memory is shown with its index number for reference.`,
inputSchema: {
type: 'object',
properties: {
project_id: {
type: 'string',
description: 'Optional project path to include project-specific memories.',
},
},
required: [],
},
},
{
name: 'memory_update',
description: `Update an existing memory by ID or by context match.
Use this when the user wants to modify a memory. Can specify either:
- id: Direct memory ID (from memory_list)
- context: A description of the memory to update (will find best match)
At least one of id or context must be provided.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'number',
description: 'The memory ID to update (from memory_list).',
},
context: {
type: 'string',
description: 'Description of the memory to update (finds best match).',
},
new_content: {
type: 'string',
description: 'The new content for the memory.',
},
new_tags: {
type: 'array',
items: { type: 'string' },
description: 'New tags to replace existing tags.',
},
project_id: {
type: 'string',
description: 'Project path for context matching.',
},
},
required: [],
},
},
{
name: 'memory_delete',
description: `Delete a memory by ID or by context match.
Use this when the user wants to remove a memory. Can specify either:
- id: Direct memory ID (from memory_list)
- context: A description of the memory to delete (will find best match)
At least one of id or context must be provided.`,
inputSchema: {
type: 'object',
properties: {
id: {
type: 'number',
description: 'The memory ID to delete (from memory_list).',
},
context: {
type: 'string',
description: 'Description of the memory to delete (finds best match).',
},
project_id: {
type: 'string',
description: 'Project path for context matching.',
},
},
required: [],
},
},
];
function createServer(): Server {
const server = new Server(
{
name: 'cursor-memory',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// Handle tool listing
server.setRequestHandler(ListToolsRequestSchema, async () => {
return { tools };
});
// Handle tool execution
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
switch (name) {
case 'memory_store': {
const { content, scope, project_id, tags } = args as {
content: string;
scope: 'global' | 'project';
project_id?: string;
tags?: string[];
};
if (!content || !scope) {
return {
content: [{ type: 'text', text: 'Error: content and scope are required.' }],
isError: true,
};
}
if (scope === 'project' && !project_id) {
return {
content: [{ type: 'text', text: 'Error: project_id is required when scope is "project".' }],
isError: true,
};
}
// Check token limit (rough estimate: 1 token β 4 chars)
if (content.length > 8000) {
return {
content: [{ type: 'text', text: 'Error: Memory content exceeds 2000 token limit (approximately 8000 characters).' }],
isError: true,
};
}
const memory = createMemory({ content, scope, project_id, tags });
return {
content: [{
type: 'text',
text: `Memory stored successfully!\n\nID: ${memory.id}\nScope: ${memory.scope}\nTags: ${memory.tags.length > 0 ? memory.tags.join(', ') : 'none'}\nContent: ${memory.content}`,
}],
};
}
case 'memory_recall': {
const { project_id } = args as { project_id?: string };
const memories = getAllMemories(project_id);
const counts = getMemoryCount();
if (memories.length === 0) {
return {
content: [{
type: 'text',
text: 'No memories stored yet.',
}],
};
}
const formatted = formatMemories(memories);
return {
content: [{
type: 'text',
text: `Found ${memories.length} memories (${counts.global} global, ${counts.project} project-specific):\n\n${formatted}`,
}],
};
}
case 'memory_search': {
const { query, project_id } = args as { query: string; project_id?: string };
if (!query) {
return {
content: [{ type: 'text', text: 'Error: query is required.' }],
isError: true,
};
}
const memories = searchMemories(query, project_id);
if (memories.length === 0) {
return {
content: [{
type: 'text',
text: `No memories found matching "${query}".`,
}],
};
}
const formatted = formatMemories(memories);
return {
content: [{
type: 'text',
text: `Found ${memories.length} memories matching "${query}":\n\n${formatted}`,
}],
};
}
case 'memory_list': {
const { project_id } = args as { project_id?: string };
const memories = listMemories(project_id);
const counts = getMemoryCount();
if (memories.length === 0) {
return {
content: [{
type: 'text',
text: 'No memories stored yet.',
}],
};
}
const formatted = formatMemoriesWithIndex(memories);
return {
content: [{
type: 'text',
text: `Memory list (${counts.global} global, ${counts.project} project-specific):\n\n${formatted}`,
}],
};
}
case 'memory_update': {
const { id, context, new_content, new_tags, project_id } = args as {
id?: number;
context?: string;
new_content?: string;
new_tags?: string[];
project_id?: string;
};
if (!id && !context) {
return {
content: [{ type: 'text', text: 'Error: Either id or context must be provided.' }],
isError: true,
};
}
if (!new_content && !new_tags) {
return {
content: [{ type: 'text', text: 'Error: At least one of new_content or new_tags must be provided.' }],
isError: true,
};
}
let memoryId = id;
if (!memoryId && context) {
const found = findMemoryByContext(context, project_id);
if (!found) {
return {
content: [{ type: 'text', text: `No memory found matching context: "${context}"` }],
isError: true,
};
}
memoryId = found.id;
}
const updated = updateMemory(memoryId!, {
content: new_content,
tags: new_tags,
});
if (!updated) {
return {
content: [{ type: 'text', text: `Memory with ID ${memoryId} not found.` }],
isError: true,
};
}
return {
content: [{
type: 'text',
text: `Memory updated successfully!\n\nID: ${updated.id}\nContent: ${updated.content}\nTags: ${updated.tags.length > 0 ? updated.tags.join(', ') : 'none'}`,
}],
};
}
case 'memory_delete': {
const { id, context, project_id } = args as {
id?: number;
context?: string;
project_id?: string;
};
if (!id && !context) {
return {
content: [{ type: 'text', text: 'Error: Either id or context must be provided.' }],
isError: true,
};
}
let memoryId = id;
let memoryContent = '';
if (!memoryId && context) {
const found = findMemoryByContext(context, project_id);
if (!found) {
return {
content: [{ type: 'text', text: `No memory found matching context: "${context}"` }],
isError: true,
};
}
memoryId = found.id;
memoryContent = found.content;
} else if (memoryId) {
const found = getMemoryById(memoryId);
memoryContent = found?.content || '';
}
const deleted = deleteMemory(memoryId!);
if (!deleted) {
return {
content: [{ type: 'text', text: `Memory with ID ${memoryId} not found.` }],
isError: true,
};
}
return {
content: [{
type: 'text',
text: `Memory deleted successfully!\n\nID: ${memoryId}\nContent: ${memoryContent.substring(0, 100)}${memoryContent.length > 100 ? '...' : ''}`,
}],
};
}
default:
return {
content: [{ type: 'text', text: `Unknown tool: ${name}` }],
isError: true,
};
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
content: [{ type: 'text', text: `Error: ${errorMessage}` }],
isError: true,
};
}
});
return server;
}
function formatMemories(memories: Memory[]): string {
return memories.map(m => {
const scopeLabel = m.scope === 'global' ? 'π Global' : `π Project: ${m.project_id}`;
const tagsLabel = m.tags.length > 0 ? `[${m.tags.join(', ')}]` : '';
return `---\n${scopeLabel} ${tagsLabel}\n${m.content}\n(ID: ${m.id}, Created: ${m.created_at})`;
}).join('\n\n');
}
function formatMemoriesWithIndex(memories: Memory[]): string {
return memories.map((m, index) => {
const scopeLabel = m.scope === 'global' ? 'π Global' : `π Project`;
const tagsLabel = m.tags.length > 0 ? `[${m.tags.join(', ')}]` : '';
const preview = m.content.length > 150 ? m.content.substring(0, 150) + '...' : m.content;
return `[${index + 1}] ID:${m.id} | ${scopeLabel} ${tagsLabel}\n ${preview}`;
}).join('\n\n');
}
// Store active transports for cleanup
const transports = new Map<string, SSEServerTransport>();
// SSE endpoint for MCP
app.get('/sse', async (req: Request, res: Response) => {
console.log('New SSE connection');
const transport = new SSEServerTransport('/message', res);
const server = createServer();
// Generate a unique ID for this connection
const connectionId = Math.random().toString(36).substring(7);
transports.set(connectionId, transport);
// Clean up on close
res.on('close', () => {
console.log('SSE connection closed');
transports.delete(connectionId);
});
await server.connect(transport);
});
// Message endpoint for MCP
app.post('/message', async (req: Request, res: Response) => {
// Find the transport for this session
const sessionId = req.query.sessionId as string;
// For SSE transport, the message handling is done automatically
// This endpoint receives messages and the transport handles them
const transport = Array.from(transports.values())[0];
if (transport) {
await transport.handlePostMessage(req, res);
} else {
res.status(400).json({ error: 'No active SSE connection' });
}
});
// Start the server
app.listen(PORT, '0.0.0.0', () => {
console.log(`Cursor Memory MCP server running on http://0.0.0.0:${PORT}`);
console.log(`SSE endpoint: http://localhost:${PORT}/sse`);
console.log(`Health check: http://localhost:${PORT}/health`);
});