validator.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: AGPL-3.0-or-later
  4
  5package mcp
  6
  7import (
  8	"errors"
  9	"fmt"
 10
 11	"git.sr.ht/~amolith/planning-mcp-server/internal/config"
 12)
 13
 14// Validator defines the interface for validating MCP request inputs
 15type Validator interface {
 16	ValidateSetGoalRequest(req SetGoalRequest) error
 17	ValidateChangeGoalRequest(req ChangeGoalRequest) error
 18	ValidateAddTasksRequest(req AddTasksRequest) error
 19	ValidateGetTasksRequest(req GetTasksRequest) error
 20	ValidateUpdateTaskStatusesRequest(req UpdateTaskStatusesRequest) error
 21	ValidateDeleteTasksRequest(req DeleteTasksRequest) error
 22}
 23
 24// PlanningValidator implements the Validator interface with configuration-based validation
 25type PlanningValidator struct {
 26	config *config.Config
 27}
 28
 29// NewPlanningValidator creates a new PlanningValidator instance
 30func NewPlanningValidator(cfg *config.Config) *PlanningValidator {
 31	return &PlanningValidator{config: cfg}
 32}
 33
 34// ValidateSetGoalRequest validates a set goal request
 35func (v *PlanningValidator) ValidateSetGoalRequest(req SetGoalRequest) error {
 36	if req.Title == "" {
 37		return errors.New("title is required")
 38	}
 39	if len(req.Title) > v.config.Planning.MaxGoalLength {
 40		return fmt.Errorf("title too long (max %d characters)", v.config.Planning.MaxGoalLength)
 41	}
 42	if req.Description == "" {
 43		return errors.New("description is required")
 44	}
 45	if len(req.Description) > v.config.Planning.MaxGoalLength {
 46		return fmt.Errorf("description too long (max %d characters)", v.config.Planning.MaxGoalLength)
 47	}
 48	return nil
 49}
 50
 51// ValidateChangeGoalRequest validates a change goal request
 52func (v *PlanningValidator) ValidateChangeGoalRequest(req ChangeGoalRequest) error {
 53	if req.Title == "" {
 54		return errors.New("title is required")
 55	}
 56	if len(req.Title) > v.config.Planning.MaxGoalLength {
 57		return fmt.Errorf("title too long (max %d characters)", v.config.Planning.MaxGoalLength)
 58	}
 59	if req.Description == "" {
 60		return errors.New("description is required")
 61	}
 62	if len(req.Description) > v.config.Planning.MaxGoalLength {
 63		return fmt.Errorf("description too long (max %d characters)", v.config.Planning.MaxGoalLength)
 64	}
 65	if req.Reason == "" {
 66		return errors.New("reason is required")
 67	}
 68	if len(req.Reason) > v.config.Planning.MaxGoalLength {
 69		return fmt.Errorf("reason too long (max %d characters)", v.config.Planning.MaxGoalLength)
 70	}
 71	return nil
 72}
 73
 74// ValidateAddTasksRequest validates an add tasks request
 75func (v *PlanningValidator) ValidateAddTasksRequest(req AddTasksRequest) error {
 76	if len(req.Tasks) == 0 {
 77		return errors.New("at least one task is required")
 78	}
 79
 80	for i, task := range req.Tasks {
 81		if task.Title == "" {
 82			return fmt.Errorf("task %d: title is required", i)
 83		}
 84		if len(task.Title) > v.config.Planning.MaxTaskLength {
 85			return fmt.Errorf("task %d: title too long (max %d characters)", i, v.config.Planning.MaxTaskLength)
 86		}
 87		if len(task.Description) > v.config.Planning.MaxTaskLength {
 88			return fmt.Errorf("task %d: description too long (max %d characters)", i, v.config.Planning.MaxTaskLength)
 89		}
 90	}
 91	return nil
 92}
 93
 94// ValidateGetTasksRequest validates a get tasks request
 95func (v *PlanningValidator) ValidateGetTasksRequest(req GetTasksRequest) error {
 96	if req.Status != "" {
 97		validStatuses := map[string]bool{
 98			"all":         true,
 99			"pending":     true,
100			"in_progress": true,
101			"completed":   true,
102			"cancelled":   true,
103			"failed":      true,
104		}
105
106		if !validStatuses[req.Status] {
107			return fmt.Errorf("invalid status '%s', must be one of: all, pending, in_progress, completed, cancelled, failed", req.Status)
108		}
109	}
110	return nil
111}
112
113// ValidateUpdateTaskStatusesRequest validates an update task statuses request
114func (v *PlanningValidator) ValidateUpdateTaskStatusesRequest(req UpdateTaskStatusesRequest) error {
115	if len(req.Tasks) == 0 {
116		return errors.New("at least one task update is required")
117	}
118
119	validStatuses := map[string]bool{
120		"pending":     true,
121		"in_progress": true,
122		"completed":   true,
123		"cancelled":   true,
124		"failed":      true,
125	}
126
127	for i, update := range req.Tasks {
128		if update.TaskID == "" {
129			return fmt.Errorf("task update %d: task_id is required", i)
130		}
131		if update.Status == "" {
132			return fmt.Errorf("task update %d: status is required", i)
133		}
134		if !validStatuses[update.Status] {
135			return fmt.Errorf("task update %d: invalid status '%s', must be one of: pending, in_progress, completed, cancelled, failed", i, update.Status)
136		}
137	}
138	return nil
139}
140
141// ValidateDeleteTasksRequest validates a delete tasks request
142func (v *PlanningValidator) ValidateDeleteTasksRequest(req DeleteTasksRequest) error {
143	if len(req.TaskIDs) == 0 {
144		return errors.New("at least one task ID is required")
145	}
146
147	for i, taskID := range req.TaskIDs {
148		if taskID == "" {
149			return fmt.Errorf("task ID %d is empty", i)
150		}
151	}
152	return nil
153}