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}