/** * 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), offset: z.number().int().min(0).default(0), }); /** * 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), limit: z.number().int().min(1).max(100).default(50), 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, pinned: z.boolean().optional(), }); /** * toggle_archive input schema */ export const toggleArchiveSchema = z.object({ id: idSchema, archived: z.boolean().optional(), }); /** * batch_move_notes input schema */ export const batchMoveNotesSchema = z.object({ noteIds: z.array(idSchema).min(1).max(100), notebookId: idSchema.optional().nullable(), }); /** * batch_delete_notes input schema */ export const batchDeleteNotesSchema = z.object({ noteIds: z.array(idSchema).min(1).max(100), }); /** * create_notebook input schema */ export const createNotebookSchema = z.object({ name: z.string().min(1).max(200), color: colorSchema.default('default'), icon: z.string().max(50).optional().nullable(), parentId: idSchema.optional().nullable(), }); /** * get_notebooks input schema */ export const getNotebooksSchema = z.object({ includeHierarchy: boolSchema(false), includeTrashed: boolSchema(false), }); /** * 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: colorSchema.optional(), icon: z.string().max(50).optional().nullable(), 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: colorSchema.default('default'), }); /** * get_labels input schema */ export const getLabelsSchema = z.object({ limit: z.number().int().min(1).max(500).default(100), }); /** * 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({ before: isoDateSchema.optional().nullable(), after: isoDateSchema.optional().nullable(), includeDone: boolSchema(false), limit: z.number().int().min(1).max(500).default(100), }); /** * export_notes input schema */ export const exportNotesSchema = z.object({ notebookId: idSchema.optional().nullable(), includeArchived: boolSchema(false), format: z.enum(['json', 'markdown']).default('json'), }); /** * import_notes input schema */ export const importNotesSchema = z.object({ notes: z.array( z.object({ title: z.string().optional(), content: z.string(), color: colorSchema.optional(), labels: labelsSchema, notebookId: idSchema.optional().nullable(), }) ).min(1).max(100), notebookId: idSchema.optional().nullable(), overwrite: z.boolean().optional().default(false), }); // ═══════════════════════════════════════════════════════════════ // 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, }; // ═══════════════════════════════════════════════════════════════ // 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.errors.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 = [ /