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}