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	ValidateModifyTaskRequest(req ModifyTaskRequest) error
 23}
 24
 25// PlanningValidator implements the Validator interface with configuration-based validation
 26type PlanningValidator struct {
 27	config *config.Config
 28}
 29
 30// NewPlanningValidator creates a new PlanningValidator instance
 31func NewPlanningValidator(cfg *config.Config) *PlanningValidator {
 32	return &PlanningValidator{config: cfg}
 33}
 34
 35// ValidateSetGoalRequest validates a set goal request
 36func (v *PlanningValidator) ValidateSetGoalRequest(req SetGoalRequest) error {
 37	if req.Title == "" {
 38		return errors.New("title is required")
 39	}
 40	if len(req.Title) > v.config.Planning.MaxGoalLength {
 41		return fmt.Errorf("title too long (max %d characters)", v.config.Planning.MaxGoalLength)
 42	}
 43	if req.Description == "" {
 44		return errors.New("description is required")
 45	}
 46	if len(req.Description) > v.config.Planning.MaxGoalLength {
 47		return fmt.Errorf("description too long (max %d characters)", v.config.Planning.MaxGoalLength)
 48	}
 49	return nil
 50}
 51
 52// ValidateChangeGoalRequest validates a change goal request
 53func (v *PlanningValidator) ValidateChangeGoalRequest(req ChangeGoalRequest) error {
 54	if req.Title == "" {
 55		return errors.New("title is required")
 56	}
 57	if len(req.Title) > v.config.Planning.MaxGoalLength {
 58		return fmt.Errorf("title too long (max %d characters)", v.config.Planning.MaxGoalLength)
 59	}
 60	if req.Description == "" {
 61		return errors.New("description is required")
 62	}
 63	if len(req.Description) > v.config.Planning.MaxGoalLength {
 64		return fmt.Errorf("description too long (max %d characters)", v.config.Planning.MaxGoalLength)
 65	}
 66	if req.Reason == "" {
 67		return errors.New("reason is required")
 68	}
 69	if len(req.Reason) > v.config.Planning.MaxGoalLength {
 70		return fmt.Errorf("reason too long (max %d characters)", v.config.Planning.MaxGoalLength)
 71	}
 72	return nil
 73}
 74
 75// ValidateAddTasksRequest validates an add tasks request
 76func (v *PlanningValidator) ValidateAddTasksRequest(req AddTasksRequest) error {
 77	if len(req.Tasks) == 0 {
 78		return errors.New("at least one task is required")
 79	}
 80
 81	for i, task := range req.Tasks {
 82		if task.Title == "" {
 83			return fmt.Errorf("task %d: title is required", i)
 84		}
 85		if len(task.Title) > v.config.Planning.MaxTaskLength {
 86			return fmt.Errorf("task %d: title too long (max %d characters)", i, v.config.Planning.MaxTaskLength)
 87		}
 88		if len(task.Description) > v.config.Planning.MaxTaskLength {
 89			return fmt.Errorf("task %d: description too long (max %d characters)", i, v.config.Planning.MaxTaskLength)
 90		}
 91	}
 92	return nil
 93}
 94
 95// ValidateGetTasksRequest validates a get tasks request
 96func (v *PlanningValidator) ValidateGetTasksRequest(req GetTasksRequest) error {
 97	if req.Status != "" {
 98		validStatuses := map[string]bool{
 99			"all":         true,
100			"pending":     true,
101			"in_progress": true,
102			"completed":   true,
103			"cancelled":   true,
104			"failed":      true,
105		}
106
107		if !validStatuses[req.Status] {
108			return fmt.Errorf("invalid status '%s', must be one of: all, pending, in_progress, completed, cancelled, failed", req.Status)
109		}
110	}
111	return nil
112}
113
114// ValidateUpdateTaskStatusesRequest validates an update task statuses request
115func (v *PlanningValidator) ValidateUpdateTaskStatusesRequest(req UpdateTaskStatusesRequest) error {
116	if len(req.Tasks) == 0 {
117		return errors.New("at least one task update is required")
118	}
119
120	validStatuses := map[string]bool{
121		"pending":     true,
122		"in_progress": true,
123		"completed":   true,
124		"cancelled":   true,
125		"failed":      true,
126	}
127
128	for i, update := range req.Tasks {
129		if update.TaskID == "" {
130			return fmt.Errorf("task update %d: task_id is required", i)
131		}
132		if update.Status == "" {
133			return fmt.Errorf("task update %d: status is required", i)
134		}
135		if !validStatuses[update.Status] {
136			return fmt.Errorf("task update %d: invalid status '%s', must be one of: pending, in_progress, completed, cancelled, failed", i, update.Status)
137		}
138	}
139	return nil
140}
141
142// ValidateDeleteTasksRequest validates a delete tasks request
143func (v *PlanningValidator) ValidateDeleteTasksRequest(req DeleteTasksRequest) error {
144	if len(req.TaskIDs) == 0 {
145		return errors.New("at least one task ID is required")
146	}
147
148	for i, taskID := range req.TaskIDs {
149		if taskID == "" {
150			return fmt.Errorf("task ID %d is empty", i)
151		}
152	}
153	return nil
154}
155
156// ValidateModifyTaskRequest validates a modify task request
157func (v *PlanningValidator) ValidateModifyTaskRequest(req ModifyTaskRequest) error {
158	if req.TaskID == "" {
159		return errors.New("task_id is required")
160	}
161
162	if req.Title == "" && req.Description == "" {
163		return errors.New("at least one of title or description must be provided")
164	}
165
166	if req.Title != "" && len(req.Title) > v.config.Planning.MaxTaskLength {
167		return fmt.Errorf("title too long (max %d characters)", v.config.Planning.MaxTaskLength)
168	}
169
170	if req.Description != "" && len(req.Description) > v.config.Planning.MaxTaskLength {
171		return fmt.Errorf("description too long (max %d characters)", v.config.Planning.MaxTaskLength)
172	}
173
174	return nil
175}