diff --git a/mcp-server/index-sse.js b/mcp-server/index-sse.js index df32806..6c281fb 100644 --- a/mcp-server/index-sse.js +++ b/mcp-server/index-sse.js @@ -232,9 +232,7 @@ const server = new Server( }, ); -registerTools(server, prisma, { - appBaseUrl, -}); +registerTools(server, prisma); // ── HTTP Endpoints ────────────────────────────────────────────────────────── @@ -252,13 +250,11 @@ app.get('/', (req, res) => { method: 'x-api-key or x-user-id header', }, tools: { - notes: 12, + notes: 11, notebooks: 6, labels: 4, - ai: 11, reminders: 1, - apiKeys: 3, - total: 37, + total: 22, }, performance: { optimizations: [ @@ -358,11 +354,11 @@ Performance Optimizations: ✅ Request timeout handling ✅ Session cleanup -Tools (37 total): - Notes (12): +Tools (22 total): + Notes (11): create_note, get_notes, get_note, update_note, delete_note, - delete_all_notes, search_notes, move_note, toggle_pin, - toggle_archive, export_notes, import_notes + search_notes, move_note, toggle_pin, toggle_archive, + export_notes, import_notes Notebooks (6): create_notebook, get_notebooks, get_notebook, update_notebook, @@ -371,18 +367,9 @@ Tools (37 total): Labels (4): create_label, get_labels, update_label, delete_label - AI (11): - generate_title_suggestions, reformulate_text, generate_tags, - suggest_notebook, get_notebook_summary, get_memory_echo, - get_note_connections, dismiss_connection, fuse_notes, - batch_organize, suggest_auto_labels - Reminders (1): get_due_reminders - API Key Management (3): - generate_api_key, list_api_keys, revoke_api_key - N8N config: SSE endpoint http://YOUR_IP:${PORT}/mcp Headers: x-api-key or x-user-id `); diff --git a/mcp-server/tools.js b/mcp-server/tools.js index 96fc66a..9470184 100644 --- a/mcp-server/tools.js +++ b/mcp-server/tools.js @@ -10,8 +10,6 @@ * - Connection pooling */ -import { generateApiKey, listApiKeys, revokeApiKey, resolveUser, validateApiKey, clearAuthCaches } from './auth.js'; - import { CallToolRequestSchema, ErrorCode, @@ -22,36 +20,11 @@ import { requestContext } from './request-context.js'; // ─── Configuration ───────────────────────────────────────────────────────── -const DEFAULT_TIMEOUT = 10000; // 10 seconds for HTTP requests const DEFAULT_SEARCH_LIMIT = 50; const DEFAULT_NOTES_LIMIT = 100; // ─── Helpers ──────────────────────────────────────────────────────────────── -/** - * Fetch with timeout wrapper - * Prevents hanging on slow/unresponsive endpoints - */ -async function fetchWithTimeout(url, options = {}, timeoutMs = DEFAULT_TIMEOUT) { - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeoutMs); - - try { - const response = await fetch(url, { - ...options, - signal: controller.signal, - }); - clearTimeout(timeoutId); - return response; - } catch (error) { - clearTimeout(timeoutId); - if (error.name === 'AbortError') { - throw new Error(`Request timeout after ${timeoutMs}ms`); - } - throw error; - } -} - export function parseNote(dbNote) { if (!dbNote) return null; return { @@ -207,11 +180,6 @@ const toolDefinitions = [ required: ['id'], }, }, - { - name: 'delete_all_notes', - description: 'Delete ALL notes, labels, and notebooks for the configured user. Use with caution.', - inputSchema: { type: 'object', properties: {} }, - }, { name: 'search_notes', description: 'Search notes by keyword in title, content, and labels. Returns lightweight format.', @@ -406,145 +374,6 @@ const toolDefinitions = [ }, }, - // ═══════════════════════════════════════════════════════════ - // AI TOOLS - // ═══════════════════════════════════════════════════════════ - { - name: 'generate_title_suggestions', - description: 'Use AI to generate 3 title suggestions for a note based on its content.', - inputSchema: { - type: 'object', - properties: { - content: { type: 'string', description: 'Note content (at least 10 words recommended)' }, - }, - required: ['content'], - }, - }, - { - name: 'reformulate_text', - description: 'Use AI to reformulate text: clarify meaning, shorten, or improve writing style.', - inputSchema: { - type: 'object', - properties: { - text: { type: 'string', description: 'Text to reformulate' }, - option: { type: 'string', enum: ['clarify', 'shorten', 'improve'], description: 'Reformulation mode' }, - }, - required: ['text', 'option'], - }, - }, - { - name: 'generate_tags', - description: 'Use AI to generate tags/labels for content. Optionally contextual to a notebook\'s existing labels.', - inputSchema: { - type: 'object', - properties: { - content: { type: 'string', description: 'Content to analyze' }, - notebookId: { type: 'string', description: 'Notebook ID for contextual tagging (uses existing labels)' }, - language: { type: 'string', description: 'Language hint (e.g. "en", "fr")', default: 'en' }, - }, - required: ['content'], - }, - }, - { - name: 'suggest_notebook', - description: 'Use AI to suggest which notebook a note should belong to based on its content.', - inputSchema: { - type: 'object', - properties: { - noteContent: { type: 'string', description: 'Note content (at least 20 words recommended)' }, - language: { type: 'string', default: 'en' }, - }, - required: ['noteContent'], - }, - }, - { - name: 'get_notebook_summary', - description: 'Generate an AI summary of all notes in a notebook.', - inputSchema: { - type: 'object', - properties: { - notebookId: { type: 'string', description: 'Notebook ID' }, - language: { type: 'string', default: 'en' }, - }, - required: ['notebookId'], - }, - }, - { - name: 'get_memory_echo', - description: 'Get the next Memory Echo insight — AI-discovered connections between your notes based on semantic similarity.', - inputSchema: { type: 'object', properties: {} }, - }, - { - name: 'get_note_connections', - description: 'Get all semantically related notes (Memory Echo connections) for a specific note. Supports pagination.', - inputSchema: { - type: 'object', - properties: { - noteId: { type: 'string', description: 'Note ID' }, - page: { type: 'number', description: 'Page number (default 1)', default: 1 }, - limit: { type: 'number', description: 'Results per page (default 10, max 50)', default: 10 }, - }, - required: ['noteId'], - }, - }, - { - name: 'dismiss_connection', - description: 'Dismiss a Memory Echo connection between two notes.', - inputSchema: { - type: 'object', - properties: { - noteId: { type: 'string', description: 'First note ID' }, - connectedNoteId: { type: 'string', description: 'Connected note ID to dismiss' }, - }, - required: ['noteId', 'connectedNoteId'], - }, - }, - { - name: 'fuse_notes', - description: 'Use AI to merge/fuse multiple notes into a single unified note.', - inputSchema: { - type: 'object', - properties: { - noteIds: { - type: 'array', - description: 'Array of note IDs to fuse (minimum 2)', - items: { type: 'string' }, - }, - prompt: { type: 'string', description: 'Optional instructions for the fusion' }, - }, - required: ['noteIds'], - }, - }, - { - name: 'batch_organize', - description: 'Create an AI-powered organization plan to move inbox notes into appropriate notebooks, or apply a previously created plan.', - inputSchema: { - type: 'object', - properties: { - action: { type: 'string', enum: ['create_plan', 'apply_plan'], description: 'Create a plan or apply it' }, - language: { type: 'string', default: 'en' }, - plan: { type: 'object', description: 'The plan object (required for apply_plan)' }, - selectedNoteIds: { type: 'array', items: { type: 'string' }, description: 'Note IDs to move (required for apply_plan)' }, - }, - required: ['action'], - }, - }, - { - name: 'suggest_auto_labels', - description: 'Use AI to suggest new labels for a notebook based on its notes\' content (requires 15+ notes), or create selected labels.', - inputSchema: { - type: 'object', - properties: { - action: { type: 'string', enum: ['suggest', 'create'], description: 'Suggest labels or create selected ones' }, - notebookId: { type: 'string', description: 'Notebook ID' }, - language: { type: 'string', default: 'en' }, - selectedLabels: { type: 'array', items: { type: 'string' }, description: 'Label names to create (for create action)' }, - suggestions: { type: 'object', description: 'Suggestions object from suggest step (for create action)' }, - }, - required: ['action', 'notebookId'], - }, - }, - // ═══════════════════════════════════════════════════════════ // REMINDER TOOLS // ═══════════════════════════════════════════════════════════ @@ -553,43 +382,6 @@ const toolDefinitions = [ description: 'Get all notes with due reminders that haven\'t been processed yet. Designed for cron/automation use.', inputSchema: { type: 'object', properties: {} }, }, - - // ═══════════════════════════════════════════════════════════ - // API KEY MANAGEMENT - // ═══════════════════════════════════════════════════════════ - { - name: 'generate_api_key', - description: 'Generate a new API key for a user. The raw key is only shown once — store it securely.', - inputSchema: { - type: 'object', - properties: { - name: { type: 'string', description: 'Human-readable name for this key (e.g. "N8N Automation")' }, - userId: { type: 'string', description: 'User ID to link this key to. If omitted, uses the default user.' }, - userEmail: { type: 'string', description: 'Alternatively, identify the user by email.' }, - }, - }, - }, - { - name: 'list_api_keys', - description: 'List all API keys (without revealing the actual keys). Can filter by user.', - inputSchema: { - type: 'object', - properties: { - userId: { type: 'string', description: 'Filter keys by user ID' }, - }, - }, - }, - { - name: 'revoke_api_key', - description: 'Revoke an API key by its short ID. The key will no longer be usable.', - inputSchema: { - type: 'object', - properties: { - shortId: { type: 'string', description: 'The short ID of the key (e.g. "a1b2c3d4")' }, - }, - required: ['shortId'], - }, - }, ]; // ─── Tool Handlers ────────────────────────────────────────────────────────── @@ -599,12 +391,8 @@ const toolDefinitions = [ * * @param {import('@modelcontextprotocol/sdk/server/index.js').Server} server * @param {import('@prisma/client').PrismaClient} prisma - * @param {object} [options] - * @param {string} [options.userId] - Optional user ID for multi-user filtering - * @param {string} [options.appBaseUrl] - Optional base URL of the Next.js app for AI API calls */ -export function registerTools(server, prisma, options = {}) { - const { appBaseUrl = null } = options; +export function registerTools(server, prisma) { // Resolve userId per-request from AsyncLocalStorage (set by auth middleware) const getResolvedUserId = () => { @@ -733,23 +521,6 @@ export function registerTools(server, prisma, options = {}) { return textResult({ success: true, message: 'Note deleted' }); } - case 'delete_all_notes': { - const where = {}; - if (resolvedUserId) where.userId = resolvedUserId; - - const [deletedNotes] = await prisma.$transaction([ - prisma.note.deleteMany({ where }), - resolvedUserId - ? prisma.label.deleteMany({ where: { notebook: { userId: resolvedUserId } } }) - : prisma.label.deleteMany({}), - resolvedUserId - ? prisma.notebook.deleteMany({ where: { userId: resolvedUserId } }) - : prisma.notebook.deleteMany({}), - ]); - - return textResult({ success: true, deletedNotes: deletedNotes.count }); - } - case 'search_notes': { const where = { isArchived: args.includeArchived || false, @@ -1170,165 +941,6 @@ export function registerTools(server, prisma, options = {}) { return textResult({ success: true, message: 'Label deleted' }); } - // ═══════════════════════════════════════════════════════ - // AI TOOLS - OPTIMIZED with timeout - // ═══════════════════════════════════════════════════════ - case 'generate_title_suggestions': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/title-suggestions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content: args.content }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Title suggestion failed'); - return textResult(data); - } - - case 'reformulate_text': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/reformulate`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ text: args.text, option: args.option }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Reformulation failed'); - return textResult(data); - } - - case 'generate_tags': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/tags`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ content: args.content, notebookId: args.notebookId, language: args.language || 'en' }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Tag generation failed'); - return textResult(data); - } - - case 'suggest_notebook': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/suggest-notebook`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ noteContent: args.noteContent, language: args.language || 'en' }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Notebook suggestion failed'); - return textResult(data); - } - - case 'get_notebook_summary': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/notebook-summary`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ notebookId: args.notebookId, language: args.language || 'en' }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Summary generation failed'); - return textResult(data); - } - - case 'get_memory_echo': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo`, { - method: 'GET', - headers: { 'Content-Type': 'application/json' }, - }); - const data = await resp.json(); - return textResult(data); - } - - case 'get_note_connections': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - const params = new URLSearchParams({ noteId: args.noteId, page: String(args.page || 1), limit: String(Math.min(args.limit || 10, 50)) }); - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/connections?${params}`); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Connection fetch failed'); - return textResult(data); - } - - case 'dismiss_connection': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/dismiss`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ noteId: args.noteId, connectedNoteId: args.connectedNoteId }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Dismiss failed'); - return textResult(data); - } - - case 'fuse_notes': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - if (!args.noteIds || args.noteIds.length < 2) { - throw new McpError(ErrorCode.InvalidRequest, 'At least 2 note IDs required'); - } - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/echo/fusion`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ noteIds: args.noteIds, prompt: args.prompt }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Fusion failed'); - return textResult(data); - } - - case 'batch_organize': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - if (args.action === 'create_plan') { - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/batch-organize`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ language: args.language || 'en' }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Plan creation failed'); - return textResult(data); - } else if (args.action === 'apply_plan') { - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/batch-organize`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ plan: args.plan, selectedNoteIds: args.selectedNoteIds }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Plan application failed'); - return textResult(data); - } else { - throw new McpError(ErrorCode.InvalidRequest, 'Invalid action. Use "create_plan" or "apply_plan"'); - } - } - - case 'suggest_auto_labels': { - if (!appBaseUrl) throw new McpError(ErrorCode.InternalError, 'App base URL not configured for AI features'); - if (args.action === 'suggest') { - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/auto-labels`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ notebookId: args.notebookId, language: args.language || 'en' }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Label suggestion failed'); - return textResult(data); - } else if (args.action === 'create') { - const resp = await fetchWithTimeout(`${appBaseUrl}/api/ai/auto-labels`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ suggestions: args.suggestions, selectedLabels: args.selectedLabels }), - }); - const data = await resp.json(); - if (!resp.ok) throw new McpError(ErrorCode.InternalError, data.error || 'Label creation failed'); - return textResult(data); - } else { - throw new McpError(ErrorCode.InvalidRequest, 'Invalid action. Use "suggest" or "create"'); - } - } - // ═══════════════════════════════════════════════════════ // REMINDERS // ═══════════════════════════════════════════════════════ @@ -1357,41 +969,6 @@ export function registerTools(server, prisma, options = {}) { return textResult({ success: true, count: reminders.length, reminders }); } - // ═══════════════════════════════════════════════════════ - // API KEY MANAGEMENT - // ═══════════════════════════════════════════════════════ - case 'generate_api_key': { - // Resolve target user - let targetUserId = args?.userId || resolvedUserId; - if (!targetUserId && args?.userEmail) { - const user = await resolveUser(prisma, args.userEmail); - if (user) targetUserId = user.id; - } - if (!targetUserId) targetUserId = await ensureUserId(); - - const result = await generateApiKey(prisma, { - name: args?.name, - userId: targetUserId, - }); - - return textResult({ - warning: 'Store this raw key securely — it will NOT be shown again.', - rawKey: result.rawKey, - info: result.info, - }); - } - - case 'list_api_keys': { - const keys = await listApiKeys(prisma, { userId: args?.userId }); - return textResult(keys); - } - - case 'revoke_api_key': { - const revoked = await revokeApiKey(prisma, args.shortId); - if (!revoked) throw new McpError(ErrorCode.InvalidRequest, 'Key not found or already revoked'); - return textResult({ success: true, message: `Key ${args.shortId} revoked` }); - } - default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } @@ -1400,7 +977,4 @@ export function registerTools(server, prisma, options = {}) { throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error.message}`); } }); -} - -// Export clear caches function for testing -export { clearAuthCaches }; +} \ No newline at end of file