#!/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 { PrismaClient } from '@prisma/client'; import { fileURLToPath } from 'url'; import { dirname, join } from 'path'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Initialize Prisma Client const prisma = new PrismaClient({ datasources: { db: { url: `file:${join(__dirname, '../keep-notes/prisma/dev.db')}` } } }); // Helper to parse JSON fields function parseNote(dbNote) { return { ...dbNote, checkItems: dbNote.checkItems ? JSON.parse(dbNote.checkItems) : null, labels: dbNote.labels ? JSON.parse(dbNote.labels) : null, images: dbNote.images ? JSON.parse(dbNote.images) : null, }; } // Create MCP server const server = new Server( { name: 'memento-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'create_note', description: 'Create a new note in Memento', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Note title (optional)', }, content: { type: 'string', description: 'Note content', }, color: { type: 'string', description: 'Note color (default, red, orange, yellow, green, teal, blue, purple, pink, gray)', default: 'default', }, type: { type: 'string', enum: ['text', 'checklist'], description: 'Note type', default: 'text', }, checkItems: { type: 'array', description: 'Checklist items (if type is checklist)', items: { type: 'object', properties: { id: { type: 'string' }, text: { type: 'string' }, checked: { type: 'boolean' }, }, required: ['id', 'text', 'checked'], }, }, labels: { type: 'array', description: 'Note labels/tags', items: { type: 'string' }, }, isPinned: { type: 'boolean', description: 'Pin the note', default: false, }, isArchived: { type: 'boolean', description: 'Archive the note', default: false, }, images: { type: 'array', description: 'Note images as base64 encoded strings', items: { type: 'string' }, }, }, required: ['content'], }, }, { name: 'get_notes', description: 'Get all notes from Memento', inputSchema: { type: 'object', properties: { includeArchived: { type: 'boolean', description: 'Include archived notes', default: false, }, search: { type: 'string', description: 'Search query to filter notes', }, }, }, }, { name: 'get_note', description: 'Get a specific note by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Note ID', }, }, required: ['id'], }, }, { name: 'update_note', description: 'Update an existing note', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Note ID', }, title: { type: 'string', description: 'Note title', }, content: { type: 'string', description: 'Note content', }, color: { type: 'string', description: 'Note color', }, checkItems: { type: 'array', description: 'Checklist items', items: { type: 'object', properties: { id: { type: 'string' }, text: { type: 'string' }, checked: { type: 'boolean' }, }, }, }, labels: { type: 'array', description: 'Note labels', items: { type: 'string' }, }, isPinned: { type: 'boolean', description: 'Pin status', }, isArchived: { type: 'boolean', description: 'Archive status', }, images: { type: 'array', description: 'Note images as base64 encoded strings', items: { type: 'string' }, }, }, required: ['id'], }, }, { name: 'delete_note', description: 'Delete a note by ID', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Note ID', }, }, required: ['id'], }, }, { name: 'search_notes', description: 'Search notes by query', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query', }, }, required: ['query'], }, }, { name: 'get_labels', description: 'Get all unique labels from notes', inputSchema: { type: 'object', properties: {}, }, }, { name: 'toggle_pin', description: 'Toggle pin status of a note', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Note ID', }, }, required: ['id'], }, }, { name: 'toggle_archive', description: 'Toggle archive status of a note', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Note ID', }, }, required: ['id'], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'create_note': { const note = await prisma.note.create({ data: { title: args.title || null, content: args.content, color: args.color || 'default', type: args.type || 'text', checkItems: args.checkItems ? JSON.stringify(args.checkItems) : null, labels: args.labels ? JSON.stringify(args.labels) : null, isPinned: args.isPinned || false, isArchived: args.isArchived || false, images: args.images ? JSON.stringify(args.images) : null, }, }); return { content: [ { type: 'text', text: JSON.stringify(parseNote(note), null, 2), }, ], }; } case 'get_notes': { let where = {}; if (!args.includeArchived) { where.isArchived = false; } if (args.search) { where.OR = [ { title: { contains: args.search, mode: 'insensitive' } }, { content: { contains: args.search, mode: 'insensitive' } }, ]; } const notes = await prisma.note.findMany({ where, orderBy: [ { isPinned: 'desc' }, { order: 'asc' }, { updatedAt: 'desc' }, ], }); return { content: [ { type: 'text', text: JSON.stringify(notes.map(parseNote), null, 2), }, ], }; } case 'get_note': { const note = await prisma.note.findUnique({ where: { id: args.id }, }); if (!note) { throw new McpError(ErrorCode.InvalidRequest, 'Note not found'); } return { content: [ { type: 'text', text: JSON.stringify(parseNote(note), null, 2), }, ], }; } case 'update_note': { const updateData = { ...args }; delete updateData.id; if ('checkItems' in args) { updateData.checkItems = args.checkItems ? JSON.stringify(args.checkItems) : null; } if ('labels' in args) { updateData.labels = args.labels ? JSON.stringify(args.labels) : null; } if ('images' in args) { updateData.images = args.images ? JSON.stringify(args.images) : null; } updateData.updatedAt = new Date(); const note = await prisma.note.update({ where: { id: args.id }, data: updateData, }); return { content: [ { type: 'text', text: JSON.stringify(parseNote(note), null, 2), }, ], }; } case 'delete_note': { await prisma.note.delete({ where: { id: args.id }, }); return { content: [ { type: 'text', text: JSON.stringify({ success: true, message: 'Note deleted' }), }, ], }; } case 'search_notes': { const notes = await prisma.note.findMany({ where: { isArchived: false, OR: [ { title: { contains: args.query, mode: 'insensitive' } }, { content: { contains: args.query, mode: 'insensitive' } }, ], }, orderBy: [ { isPinned: 'desc' }, { updatedAt: 'desc' }, ], }); return { content: [ { type: 'text', text: JSON.stringify(notes.map(parseNote), null, 2), }, ], }; } case 'get_labels': { const notes = await prisma.note.findMany({ select: { labels: true }, }); const labelsSet = new Set(); notes.forEach((note) => { const labels = note.labels ? JSON.parse(note.labels) : null; if (labels) { labels.forEach((label) => labelsSet.add(label)); } }); return { content: [ { type: 'text', text: JSON.stringify(Array.from(labelsSet).sort(), null, 2), }, ], }; } case 'toggle_pin': { const note = await prisma.note.findUnique({ where: { id: args.id } }); if (!note) { throw new McpError(ErrorCode.InvalidRequest, 'Note not found'); } const updated = await prisma.note.update({ where: { id: args.id }, data: { isPinned: !note.isPinned }, }); return { content: [ { type: 'text', text: JSON.stringify(parseNote(updated), null, 2), }, ], }; } case 'toggle_archive': { const note = await prisma.note.findUnique({ where: { id: args.id } }); if (!note) { throw new McpError(ErrorCode.InvalidRequest, 'Note not found'); } const updated = await prisma.note.update({ where: { id: args.id }, data: { isArchived: !note.isArchived }, }); return { content: [ { type: 'text', text: JSON.stringify(parseNote(updated), null, 2), }, ], }; } default: throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${name}` ); } } catch (error) { if (error instanceof McpError) { throw error; } throw new McpError( ErrorCode.InternalError, `Tool execution failed: ${error.message}` ); } }); // Start server async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error('Memento MCP server running on stdio'); } main().catch((error) => { console.error('Server error:', error); process.exit(1); });