/** * Memento MCP Server - Input Validation * * Validates all tool inputs before processing. * Uses Zod schemas for type-safe validation. */ import { z } from 'zod'; // ═══════════════════════════════════════════════════════════════ // Common Validators // ═══════════════════════════════════════════════════════════════ /** * Safe string validator (no HTML tags, limited length) */ export const safeStringSchema = z .string() .max(10000) .transform((s) => s.trim()) .refine((s) => !/<[^>]*>/g.test(s), { message: 'String must not contain HTML tags', }); /** * ID validator (UUID or custom ID format) */ export const idSchema = z .string() .min(1) .max(100) .regex(/^[a-zA-Z0-9_-]+$/, { message: 'ID must contain only alphanumeric characters, hyphens, and underscores', }); /** * UUID validator */ export const uuidSchema = z.string().uuid(); /** * Title validator (with emoji support) */ export const titleSchema = z .string() .min(1) .max(500) .transform((s) => s.trim()); /** * Content validator (Markdown-safe, reasonable length) */ export const contentSchema = z .string() .max(1000000) // 1MB limit .transform((s) => s.trim()); /** * Color validator */ export const colorSchema = z.enum([ 'default', 'red', 'orange', 'yellow', 'green', 'teal', 'blue', 'purple', 'pink', 'gray', ]); /** * Note type validator */ export const noteTypeSchema = z.enum(['text', 'markdown', 'richtext', 'checklist']); /** * Size validator */ export const sizeSchema = z.enum(['small', 'medium', 'large']); /** * Boolean validator with default */ export const boolSchema = (defaultValue = false) => z .boolean() .optional() .default(defaultValue) .transform((v) => typeof v === 'boolean' ? v : defaultValue); /** * Date/ISO string validator */ export const isoDateSchema = z .string() .datetime() .optional() .nullable(); /** * Labels array validator */ export const labelsSchema = z .array(z.string().min(1).max(100)) .max(50) .optional() .nullable(); /** * CheckItems validator */ export const checkItemSchema = z.object({ id: z.string().min(1).max(100), text: z.string().min(1).max(1000), checked: z.boolean(), }); export const checkItemsSchema = z.array(checkItemSchema).max(100).optional().nullable(); /** * Images array validator */ export const imagesSchema = z .array(z.string().url().max(2000)) .max(50) .optional() .nullable(); /** * Links array validator */ export const linksSchema = z .array(z.string().url().max(2000)) .max(50) .optional() .nullable(); /** * Reminder recurrence validator */ export const recurrenceSchema = z.enum(['daily', 'weekly', 'monthly', 'yearly']).optional().nullable(); // ═══════════════════════════════════════════════════════════════ // Tool-specific Schemas // ═══════════════════════════════════════════════════════════════ /** * create_note input schema */ export const createNoteSchema = z.object({ title: titleSchema.optional(), content: contentSchema, color: colorSchema.default('default'), type: noteTypeSchema.default('richtext'), checkItems: checkItemsSchema, labels: labelsSchema, isPinned: boolSchema(false), isArchived: boolSchema(false), images: imagesSchema, links: linksSchema, reminder: isoDateSchema, isReminderDone: boolSchema(false), reminderRecurrence: recurrenceSchema, reminderLocation: z.string().max(500).optional().nullable(), isMarkdown: boolSchema(false), // Deprecated size: sizeSchema.default('small'), notebookId: idSchema.optional().nullable(), }); /** * get_notes input schema */ export const getNotesSchema = z.object({ includeArchived: boolSchema(false), search: z.string().max(500).optional().nullable(), notebookId: idSchema.optional().nullable(), fullDetails: boolSchema(false), limit: z.number().int().min(1).max(500).default(100), }); /** * get_note input schema */ export const getNoteSchema = z.object({ id: idSchema, }); /** * update_note input schema */ export const updateNoteSchema = z.object({ id: idSchema, title: titleSchema.optional(), content: contentSchema.optional(), color: colorSchema.optional(), type: noteTypeSchema.optional(), checkItems: checkItemsSchema, labels: labelsSchema, isPinned: z.boolean().optional(), isArchived: z.boolean().optional(), images: imagesSchema, links: linksSchema, reminder: isoDateSchema, isReminderDone: z.boolean().optional(), reminderRecurrence: recurrenceSchema, reminderLocation: z.string().max(500).optional().nullable(), isMarkdown: z.boolean().optional(), size: sizeSchema.optional(), notebookId: idSchema.optional().nullable(), }); /** * delete_note input schema */ export const deleteNoteSchema = z.object({ id: idSchema, }); /** * search_notes input schema */ export const searchNotesSchema = z.object({ query: z.string().min(1).max(500), notebookId: idSchema.optional().nullable(), includeArchived: boolSchema(false), }); /** * move_note input schema */ export const moveNoteSchema = z.object({ id: idSchema, notebookId: idSchema.optional().nullable(), }); /** * toggle_pin input schema */ export const togglePinSchema = z.object({ id: idSchema, }); /** * toggle_archive input schema */ export const toggleArchiveSchema = z.object({ id: idSchema, }); /** * batch_move_notes input schema */ export const batchMoveNotesSchema = z.object({ ids: z.array(idSchema).min(1).max(100), notebookId: idSchema.optional().nullable(), }); /** * batch_delete_notes input schema */ export const batchDeleteNotesSchema = z.object({ ids: z.array(idSchema).min(1).max(100), }); /** * create_notebook input schema */ export const createNotebookSchema = z.object({ name: z.string().min(1).max(200), color: z.string().max(50).default('#3B82F6'), icon: z.string().max(50).optional().nullable(), parentId: idSchema.optional().nullable(), order: z.number().int().optional(), }); /** * get_notebooks input schema */ export const getNotebooksSchema = z.object({}); /** * get_notebook input schema */ export const getNotebookSchema = z.object({ id: idSchema, }); /** * update_notebook input schema */ export const updateNotebookSchema = z.object({ id: idSchema, name: z.string().min(1).max(200).optional(), color: z.string().max(50).optional().nullable(), icon: z.string().max(50).optional().nullable(), order: z.number().int().optional(), parentId: idSchema.optional().nullable(), }); /** * delete_notebook input schema */ export const deleteNotebookSchema = z.object({ id: idSchema, force: z.boolean().optional().default(false), }); /** * reorder_notebooks input schema */ export const reorderNotebooksSchema = z.object({ notebookIds: z.array(idSchema).min(1).max(500), }); /** * get_notebook_hierarchy input schema */ export const getNotebookHierarchySchema = z.object({ rootId: idSchema.optional().nullable(), }); /** * create_label input schema */ export const createLabelSchema = z.object({ name: z.string().min(1).max(100), color: z.string().max(50).optional().nullable(), notebookId: idSchema, }); /** * get_labels input schema */ export const getLabelsSchema = z.object({ notebookId: idSchema.optional().nullable(), }); /** * update_label input schema */ export const updateLabelSchema = z.object({ id: idSchema, name: z.string().min(1).max(100).optional(), color: colorSchema.optional(), }); /** * delete_label input schema */ export const deleteLabelSchema = z.object({ id: idSchema, }); /** * get_due_reminders input schema */ export const getDueRemindersSchema = z.object({}); /** * export_notes input schema */ export const exportNotesSchema = z.object({}); /** * import_notes input schema */ export const importNotesSchema = z.object({ data: z.object({ version: z.string().optional(), data: z.object({ notes: z.array(z.any()).optional(), labels: z.array(z.any()).optional(), notebooks: z.array(z.any()).optional(), }).optional(), }), }); /** * find_similar_notes input schema */ export const findSimilarNotesSchema = z.object({ id: idSchema, limit: z.number().int().min(1).max(50).default(10), threshold: z.number().min(0).max(1).default(0.7), }); /** * get_memory_echo_insights input schema */ export const getMemoryEchoInsightsSchema = z.object({ includeViewed: boolSchema(false), includeDismissed: boolSchema(false), limit: z.number().int().min(1).max(100).default(20), }); /** * dismiss_memory_echo_insight input schema */ export const dismissMemoryEchoInsightSchema = z.object({ id: idSchema, viewed: boolSchema(true), dismissed: boolSchema(true), }); /** * get_notes_by_label input schema */ export const getNotesByLabelSchema = z.object({ label: z.string().min(1).max(100), notebookId: idSchema.optional().nullable(), includeArchived: boolSchema(false), limit: z.number().int().min(1).max(500).default(100), }); /** * get_note_statistics input schema */ export const getNoteStatisticsSchema = z.object({ includeArchived: boolSchema(false), includeTrashed: boolSchema(false), }); /** * get_upcoming_reminders input schema */ export const getUpcomingRemindersSchema = z.object({ hours: z.number().int().min(1).max(168).default(24), includeDone: boolSchema(false), limit: z.number().int().min(1).max(500).default(100), }); /** * update_reminder input schema */ export const updateReminderSchema = z.object({ id: idSchema, reminder: isoDateSchema, isReminderDone: z.boolean().optional(), reminderRecurrence: recurrenceSchema, reminderLocation: z.string().max(500).optional().nullable(), }); // ═══════════════════════════════════════════════════════════════ // Schema Registry // ═══════════════════════════════════════════════════════════════ /** * Tool schema registry */ export const toolSchemas = { create_note: createNoteSchema, get_notes: getNotesSchema, get_note: getNoteSchema, update_note: updateNoteSchema, delete_note: deleteNoteSchema, search_notes: searchNotesSchema, move_note: moveNoteSchema, toggle_pin: togglePinSchema, toggle_archive: toggleArchiveSchema, batch_move_notes: batchMoveNotesSchema, batch_delete_notes: batchDeleteNotesSchema, create_notebook: createNotebookSchema, get_notebooks: getNotebooksSchema, get_notebook: getNotebookSchema, update_notebook: updateNotebookSchema, delete_notebook: deleteNotebookSchema, reorder_notebooks: reorderNotebooksSchema, get_notebook_hierarchy: getNotebookHierarchySchema, create_label: createLabelSchema, get_labels: getLabelsSchema, update_label: updateLabelSchema, delete_label: deleteLabelSchema, get_due_reminders: getDueRemindersSchema, export_notes: exportNotesSchema, import_notes: importNotesSchema, find_similar_notes: findSimilarNotesSchema, get_memory_echo_insights: getMemoryEchoInsightsSchema, dismiss_memory_echo_insight: dismissMemoryEchoInsightSchema, get_notes_by_label: getNotesByLabelSchema, get_note_statistics: getNoteStatisticsSchema, get_upcoming_reminders: getUpcomingRemindersSchema, update_reminder: updateReminderSchema, }; // ═══════════════════════════════════════════════════════════════ // Validation Functions // ═══════════════════════════════════════════════════════════════ /** * Validate tool input against its schema * * @param {string} toolName - Name of the tool * @param {object} input - Input data to validate * @returns {{ success: boolean, data?: object, errors?: array }} */ export function validateToolInput(toolName, input) { const schema = toolSchemas[toolName]; if (!schema) { return { success: false, errors: [{ message: `Unknown tool: ${toolName}` }], }; } try { const data = schema.parse(input); return { success: true, data }; } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.issues.map((e) => ({ field: e.path.join('.'), message: e.message, code: e.code, })), }; } return { success: false, errors: [{ message: error.message || 'Validation failed' }], }; } } /** * Sanitize HTML tags from string * (Basic sanitization - for production, use a library like sanitize-html) */ export function sanitizeHtml(input) { if (typeof input !== 'string') return input; return input.replace(/<[^>]*>/g, ''); } /** * Sanitize object recursively */ export function sanitizeObject(obj, options = {}) { const { allowedTags = [], allowedAttributes = {} } = options; if (typeof obj === 'string') { return sanitizeHtml(obj); } if (Array.isArray(obj)) { return obj.map((item) => sanitizeObject(item, options)); } if (obj && typeof obj === 'object') { const sanitized = {}; for (const [key, value] of Object.entries(obj)) { sanitized[key] = sanitizeObject(value, options); } return sanitized; } return obj; } /** * Check for potential XSS attacks */ export function checkXSS(input) { const xssPatterns = [ /