Skip to main content
Glama
validation.ts17.4 kB
/** * Zod validation schemas for all input types * Based on data-model.md validation rules and Todoist API constraints */ import { z } from 'zod'; /** * Task-related validation schemas */ /** * T019: Deadline validation schema * Validates date string in YYYY-MM-DD format (RFC 3339 date) * Used for task completion deadlines (when work must be done by) */ export const DeadlineSchema = z .string() .regex( /^\d{4}-\d{2}-\d{2}$/, 'Deadline must be in YYYY-MM-DD format (e.g., 2025-10-15)' ) .describe('Task completion deadline in YYYY-MM-DD format'); /** * T020: Deadline parameter schema for optional deadline input * Supports null to explicitly remove a deadline */ export const DeadlineParameterSchema = z .union([DeadlineSchema, z.null(), z.undefined()]) .describe('Optional deadline (YYYY-MM-DD) or null to remove'); export const CreateTaskSchema = z.object({ content: z .string() .min(1, 'Content is required') .max(500, 'Content must be 500 characters or less'), description: z .string() .max(16384, 'Description must be 16384 characters or less') .optional(), project_id: z.string().min(1, 'Project ID is required'), section_id: z.string().min(1, 'Section ID must not be empty').optional(), parent_id: z.string().min(1, 'Parent ID must not be empty').optional(), priority: z.number().int().min(1).max(4).default(1), labels: z .array(z.string()) .max(100, 'Maximum 100 labels allowed') .default([]), due_string: z.string().optional(), due_date: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, 'Due date must be in YYYY-MM-DD format') .optional(), due_datetime: z .string() .datetime('Due datetime must be in ISO 8601 format') .optional(), assignee_id: z.string().min(1, 'Assignee ID must not be empty').optional(), deadline: DeadlineParameterSchema, // T021: Add deadline parameter support }); export const UpdateTaskSchema = CreateTaskSchema.partial().extend({ task_id: z.string().min(1, 'Task ID is required'), }); export const TaskQuerySchema = z.object({ project_id: z.string().optional(), section_id: z.string().optional(), label_id: z.string().optional(), filter: z.string().optional(), lang: z.string().default('en'), }); export const CompleteTaskSchema = z.object({ id: z.string().min(1, 'Task ID is required'), }); export const DeleteTaskSchema = z.object({ id: z.string().min(1, 'Task ID is required'), }); /** * Project-related validation schemas */ export const CreateProjectSchema = z.object({ name: z .string() .min(1, 'Project name is required') .max(120, 'Project name must be 120 characters or less'), parent_id: z.string().min(1, 'Parent ID must not be empty').optional(), color: z.string().min(1, 'Color is required'), is_favorite: z.boolean().default(false), view_style: z.enum(['list', 'board']).default('list'), }); export const UpdateProjectSchema = CreateProjectSchema.partial().extend({ id: z.string().min(1, 'Project ID is required'), }); export const ArchiveProjectSchema = z.object({ id: z.string().min(1, 'Project ID is required'), }); export const DeleteProjectSchema = z.object({ id: z.string().min(1, 'Project ID is required'), }); /** * Section-related validation schemas */ export const CreateSectionSchema = z.object({ name: z .string() .min(1, 'Section name is required') .max(120, 'Section name must be 120 characters or less'), project_id: z.string().min(1, 'Project ID is required'), order: z.number().int().min(1).optional(), }); export const UpdateSectionSchema = z.object({ id: z.string().min(1, 'Section ID is required'), name: z .string() .min(1, 'Section name is required') .max(120, 'Section name must be 120 characters or less'), }); export const DeleteSectionSchema = z.object({ id: z.string().min(1, 'Section ID is required'), }); export const ReorderSectionSchema = z.object({ id: z.string().min(1, 'Section ID is required'), order: z.number().int().min(1, 'Order must be positive'), }); /** * Comment-related validation schemas */ export const CreateCommentSchema = z .object({ content: z .string() .min(1, 'Comment content is required') .max(15000, 'Comment must be 15,000 characters or less'), task_id: z.string().min(1, 'Task ID must not be empty').optional(), project_id: z.string().min(1, 'Project ID must not be empty').optional(), attachment: z .object({ file_url: z.string().url('File URL must be valid'), file_name: z.string().min(1, 'File name is required'), file_type: z.string().min(1, 'File type is required'), file_size: z.number().int().min(0, 'File size must be non-negative'), }) .strict() .optional(), }) .refine(data => data.task_id || data.project_id, { message: 'Either task_id or project_id is required', path: ['task_id', 'project_id'], }) .refine(data => !(data.task_id && data.project_id), { message: 'Cannot specify both task_id and project_id, only one is allowed', path: ['task_id', 'project_id'], }); export const UpdateCommentSchema = z.object({ id: z.string().min(1, 'Comment ID is required'), content: z .string() .min(1, 'Comment content is required') .max(15000, 'Comment must be 15,000 characters or less'), }); export const DeleteCommentSchema = z.object({ id: z.string().min(1, 'Comment ID is required'), }); /** * Filter-related validation schemas */ export const CreateFilterSchema = z.object({ name: z .string() .min(1, 'Filter name is required') .max(120, 'Filter name must be 120 characters or less'), query: z.string().min(1, 'Query is required'), color: z.string().min(1, 'Color is required').optional(), order: z.number().int().min(1).optional(), is_favorite: z.boolean().default(false), }); export const UpdateFilterSchema = CreateFilterSchema.partial().extend({ id: z.string().min(1, 'Filter ID is required'), }); export const DeleteFilterSchema = z.object({ id: z.string().min(1, 'Filter ID is required'), }); export const QueryFilterSchema = z.object({ filter_id: z.string().min(1, 'Filter ID is required'), }); /** * Label-related validation schemas */ export const CreateLabelSchema = z.object({ name: z .string() .min(1, 'Label name is required') .max(120, 'Label name must be 120 characters or less') .regex( /^[a-zA-Z0-9_-]+$/, 'Label name can only contain alphanumeric characters, hyphens, and underscores' ), order: z.number().int().min(1).optional(), color: z.string().min(1, 'Color is required'), is_favorite: z.boolean().default(false), }); export const UpdateLabelSchema = CreateLabelSchema.partial().extend({ id: z.string().min(1, 'Label ID is required'), }); export const DeleteLabelSchema = z.object({ id: z.string().min(1, 'Label ID is required'), }); /** * Label Tool MCP validation schema * Supports all label actions: create, get, update, delete, list, rename_shared, remove_shared */ export const LabelToolInputSchema = z.object({ action: z.enum([ 'create', 'get', 'update', 'delete', 'list', 'rename_shared', 'remove_shared', ]), label_id: z.string().optional(), name: z .string() .min(1, 'Label name is required') .max(128, 'Label name must be 128 characters or less') .optional(), new_name: z .string() .min(1, 'New name is required') .max(128, 'New name must be 128 characters or less') .optional(), color: z.string().optional(), order: z.number().int().optional(), is_favorite: z.boolean().optional(), cursor: z.string().optional(), limit: z .number() .int() .min(1, 'Limit must be at least 1') .max(200, 'Limit must be between 1 and 200') .optional(), }); /** * Reminder-related validation schemas */ // Due date schema for reminders (must include time) const ReminderDueSchema = z.object({ date: z.string().optional(), // ISO 8601 datetime string: z.string().optional(), // Natural language (e.g., "tomorrow at 10:00", "every day at 9am") timezone: z.string().nullable().optional(), is_recurring: z.boolean().default(false), lang: z.string().default('en'), }); // Base reminder create schema with discriminated union for types export const CreateReminderSchema = z.discriminatedUnion('type', [ // Relative reminder: minutes before task due date z.object({ type: z.literal('relative'), item_id: z.string().min(1, 'Task ID is required'), minute_offset: z .number() .int() .min(0, 'Minute offset must be non-negative') .max(43200, 'Minute offset cannot exceed 30 days (43200 minutes)'), // Max 30 days notify_uid: z.string().optional(), }), // Absolute reminder: specific date and time z.object({ type: z.literal('absolute'), item_id: z.string().min(1, 'Task ID is required'), due: ReminderDueSchema, notify_uid: z.string().optional(), }), // Location reminder: geofenced z.object({ type: z.literal('location'), item_id: z.string().min(1, 'Task ID is required'), name: z.string().min(1, 'Location name is required'), loc_lat: z.string().min(1, 'Latitude is required'), loc_long: z.string().min(1, 'Longitude is required'), loc_trigger: z.enum(['on_enter', 'on_leave']), radius: z .number() .int() .min(1, 'Radius must be at least 1 meter') .max(5000, 'Radius cannot exceed 5000 meters'), notify_uid: z.string().optional(), }), ]); // Update reminder schema allows partial updates export const UpdateReminderSchema = z.object({ id: z.string().min(1, 'Reminder ID is required'), type: z.enum(['relative', 'absolute', 'location']).optional(), notify_uid: z.string().optional(), due: ReminderDueSchema.optional(), minute_offset: z.number().int().min(0).max(43200).optional(), name: z.string().optional(), loc_lat: z.string().optional(), loc_long: z.string().optional(), loc_trigger: z.enum(['on_enter', 'on_leave']).optional(), radius: z.number().int().min(1).max(5000).optional(), }); export const DeleteReminderSchema = z.object({ id: z.string().min(1, 'Reminder ID is required'), }); export const GetReminderSchema = z.object({ id: z.string().min(1, 'Reminder ID is required'), }); export const ListRemindersSchema = z.object({ item_id: z.string().optional(), // Filter by task ID }); /** * Batch operation validation schemas */ export const BatchCommandSchema = z.object({ type: z.enum([ 'item_add', 'item_update', 'item_delete', 'item_complete', 'item_uncomplete', 'item_move', 'project_add', 'project_update', 'project_delete', 'project_archive', 'section_add', 'section_update', 'section_delete', ]), temp_id: z.string().optional(), uuid: z.string().optional(), args: z.record(z.any()), }); export const BatchOperationObjectSchema = z.object({ batch_commands: z .array(BatchCommandSchema) .min(1, 'At least one command is required') .max(100, 'Maximum 100 commands allowed'), }); export const BatchOperationSchema = BatchOperationObjectSchema.refine( data => { // Check temp_id uniqueness const tempIds = data.batch_commands .map(cmd => cmd.temp_id) .filter((id): id is string => id !== undefined && id !== null); if (tempIds.length === 0) return true; // No temp_ids to validate const uniqueTempIds = new Set(tempIds); return uniqueTempIds.size === tempIds.length; }, { message: 'All temp_id values must be unique within the batch', path: ['batch_commands'], } ); /** * Configuration validation schemas */ export const APIConfigurationSchema = z.object({ token: z.string().min(1, 'API token is required'), base_url: z .string() .url('Base URL must be valid') .default('https://api.todoist.com/api/v1'), timeout: z.number().int().min(1000).max(60000).default(10000), retry_attempts: z.number().int().min(0).max(10).default(3), }); /** * Common schemas */ export const PaginationSchema = z.object({ limit: z.number().int().min(1).max(100).default(30), offset: z.number().int().min(0).default(0), }); export const SortSchema = z.object({ sort_by: z .enum(['created', 'updated', 'priority', 'order', 'name']) .default('order'), sort_order: z.enum(['asc', 'desc']).default('asc'), }); /** * Type exports for use in other modules */ export type CreateTaskInput = z.infer<typeof CreateTaskSchema>; export type UpdateTaskInput = z.infer<typeof UpdateTaskSchema>; export type TaskQueryInput = z.infer<typeof TaskQuerySchema>; export type CreateProjectInput = z.infer<typeof CreateProjectSchema>; export type UpdateProjectInput = z.infer<typeof UpdateProjectSchema>; export type CreateSectionInput = z.infer<typeof CreateSectionSchema>; export type UpdateSectionInput = z.infer<typeof UpdateSectionSchema>; export type CreateCommentInput = z.infer<typeof CreateCommentSchema>; export type UpdateCommentInput = z.infer<typeof UpdateCommentSchema>; export type CreateFilterInput = z.infer<typeof CreateFilterSchema>; export type UpdateFilterInput = z.infer<typeof UpdateFilterSchema>; export type CreateLabelInput = z.infer<typeof CreateLabelSchema>; export type UpdateLabelInput = z.infer<typeof UpdateLabelSchema>; export type LabelToolInput = z.infer<typeof LabelToolInputSchema>; export type CreateReminderInput = z.infer<typeof CreateReminderSchema>; export type UpdateReminderInput = z.infer<typeof UpdateReminderSchema>; export type BatchCommandInput = z.infer<typeof BatchCommandSchema>; export type BatchOperationInput = z.infer<typeof BatchOperationSchema>; export type APIConfigurationInput = z.infer<typeof APIConfigurationSchema>; export type PaginationInput = z.infer<typeof PaginationSchema>; export type SortInput = z.infer<typeof SortSchema>; /** * Completed Tasks Query validation schema * Supports querying completed tasks by completion date (3-month window) or due date (6-week window) */ export const CompletedTasksInputSchema = z .object({ action: z.literal('list_completed'), completed_query_type: z.enum(['by_completion_date', 'by_due_date']), since: z.string().datetime('Since must be in ISO 8601 format'), until: z.string().datetime('Until must be in ISO 8601 format'), project_id: z.string().optional(), section_id: z.string().optional(), workspace_id: z.number().optional(), parent_id: z.string().optional(), filter_query: z.string().optional(), filter_lang: z.string().default('en'), cursor: z.string().optional(), limit: z.number().min(1).max(200).default(50), }) .refine( data => { const since = new Date(data.since); const until = new Date(data.until); const daysDiff = Math.ceil( (until.getTime() - since.getTime()) / (1000 * 60 * 60 * 24) ); if (data.completed_query_type === 'by_completion_date') { return daysDiff <= 92; } else { return daysDiff <= 42; } }, data => { const maxDays = data.completed_query_type === 'by_completion_date' ? 92 : 42; const queryType = data.completed_query_type === 'by_completion_date' ? 'completion date' : 'due date'; return { message: `Time window exceeds ${maxDays} days maximum for ${queryType} queries`, }; } ) .refine(data => new Date(data.until) > new Date(data.since), { message: 'Until date must be after since date', }); export type CompletedTasksInput = z.infer<typeof CompletedTasksInputSchema>; /** * Bulk operations validation schema * T001: Schema for bulk task operations (update, complete, uncomplete, move) */ export const bulkActionEnum = z.enum([ 'update', 'complete', 'uncomplete', 'move', ]); export const bulkOperationInputSchema = z .object({ action: bulkActionEnum, task_ids: z .array(z.string()) .min(1, 'At least one task ID required') .max(50, 'Maximum 50 tasks allowed'), // Optional field updates (for update/move actions) project_id: z.string().optional(), section_id: z.string().optional(), parent_id: z.string().optional(), order: z.number().optional(), labels: z.array(z.string()).optional(), priority: z.number().min(1).max(4).optional(), assignee_id: z.number().optional(), due_string: z.string().optional(), due_date: z .string() .regex(/^\d{4}-\d{2}-\d{2}$/, 'Due date must be in YYYY-MM-DD format') .optional(), due_datetime: z .string() .datetime('Due datetime must be in ISO 8601 format') .optional(), due_lang: z.string().optional(), duration: z.number().optional(), duration_unit: z.enum(['minute', 'day']).optional(), deadline_date: z .string() .regex( /^\d{4}-\d{2}-\d{2}$/, 'Deadline date must be in YYYY-MM-DD format' ) .optional(), }) .strict() .refine( data => { // Ensure no disallowed fields const disallowed = ['content', 'description', 'comments']; return !disallowed.some(field => field in data); }, { message: 'Cannot modify content, description, or comments in bulk operations', } ); export type BulkOperationInput = z.infer<typeof bulkOperationInputSchema>;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/shayonpal/mcp-todoist'

If you have feedback or need assistance with the MCP directory API, please join our Discord server