@@ -11,6 +11,16 @@ import (
1111 "github.com/stacklok/toolhive/pkg/vmcp/config"
1212)
1313
14+ const (
15+ // Type constants for output properties
16+ typeString = "string"
17+ typeInteger = "integer"
18+ typeNumber = "number"
19+ typeBoolean = "boolean"
20+ typeObject = "object"
21+ typeArray = "array"
22+ )
23+
1424// constructOutputFromConfig builds the workflow output from the output configuration.
1525// This expands templates in the Value fields, deserializes JSON for object types,
1626// applies default values on expansion failure, and validates the final output.
@@ -34,12 +44,16 @@ func (e *workflowEngine) constructOutputFromConfig(
3444 output [propertyName ] = value
3545 }
3646
37- // Validate required fields are present
47+ // Validate required fields are present and non-nil
3848 if len (outputConfig .Required ) > 0 {
3949 for _ , requiredField := range outputConfig .Required {
40- if _ , exists := output [requiredField ]; ! exists {
50+ value , exists := output [requiredField ]
51+ if ! exists {
4152 return nil , fmt .Errorf ("required output field %q is missing" , requiredField )
4253 }
54+ if value == nil {
55+ return nil , fmt .Errorf ("required output field %q is nil" , requiredField )
56+ }
4357 }
4458 }
4559
@@ -103,7 +117,7 @@ func (e *workflowEngine) constructOutputPropertyFromValue(
103117 }
104118
105119 // For object types, attempt JSON deserialization
106- if propertyDef .Type == "object" { //nolint:goconst // Type literals are clearer than constants here
120+ if propertyDef .Type == typeObject {
107121 var obj map [string ]any
108122 if err := json .Unmarshal ([]byte (expandedStr ), & obj ); err != nil {
109123 // JSON deserialization failed - try default value
@@ -117,7 +131,7 @@ func (e *workflowEngine) constructOutputPropertyFromValue(
117131 }
118132
119133 // For array types, attempt JSON deserialization
120- if propertyDef .Type == "array" {
134+ if propertyDef .Type == typeArray {
121135 var arr []any
122136 if err := json .Unmarshal ([]byte (expandedStr ), & arr ); err != nil {
123137 // JSON deserialization failed - try default value
@@ -173,26 +187,26 @@ func (e *workflowEngine) constructOutputPropertyFromProperties(
173187// coerceStringToType converts a string value to the specified type.
174188func (* workflowEngine ) coerceStringToType (value string , targetType string ) (any , error ) {
175189 switch targetType {
176- case "string" :
190+ case typeString :
177191 return value , nil
178192
179- case "integer" :
193+ case typeInteger :
180194 // Try to parse as integer
181195 intVal , err := strconv .ParseInt (value , 10 , 64 )
182196 if err != nil {
183197 return nil , fmt .Errorf ("cannot coerce %q to integer: %w" , value , err )
184198 }
185199 return intVal , nil
186200
187- case "number" :
201+ case typeNumber :
188202 // Try to parse as float
189203 floatVal , err := strconv .ParseFloat (value , 64 )
190204 if err != nil {
191205 return nil , fmt .Errorf ("cannot coerce %q to number: %w" , value , err )
192206 }
193207 return floatVal , nil
194208
195- case "boolean" :
209+ case typeBoolean :
196210 // Try to parse as boolean
197211 switch value {
198212 case "true" , "True" , "TRUE" , "1" : //nolint:goconst // Boolean literals are clearer than constants
@@ -220,14 +234,14 @@ func (*workflowEngine) coerceDefaultValue(defaultVal any, targetType string) (an
220234
221235 // If default is already the correct type, return as-is
222236 switch targetType {
223- case "string" :
237+ case typeString :
224238 if str , ok := defaultVal .(string ); ok {
225239 return str , nil
226240 }
227241 // Convert other types to string
228242 return fmt .Sprintf ("%v" , defaultVal ), nil
229243
230- case "integer" :
244+ case typeInteger :
231245 // Handle various integer representations
232246 switch v := defaultVal .(type ) {
233247 case int :
@@ -237,16 +251,26 @@ func (*workflowEngine) coerceDefaultValue(defaultVal any, targetType string) (an
237251 case int64 :
238252 return v , nil
239253 case float64 :
240- return int64 (v ), nil
254+ // Check for potential truncation
255+ intVal := int64 (v )
256+ if float64 (intVal ) != v {
257+ logger .Warnf ("Potential precision loss converting float64 %v to int64 %d" , v , intVal )
258+ }
259+ return intVal , nil
241260 case float32 :
242- return int64 (v ), nil
261+ // Check for potential truncation
262+ intVal := int64 (v )
263+ if float32 (intVal ) != v {
264+ logger .Warnf ("Potential precision loss converting float32 %v to int64 %d" , v , intVal )
265+ }
266+ return intVal , nil
243267 case string :
244268 return strconv .ParseInt (v , 10 , 64 )
245269 default :
246270 return nil , fmt .Errorf ("cannot coerce default value %v (type %T) to integer" , defaultVal , defaultVal )
247271 }
248272
249- case "number" :
273+ case typeNumber :
250274 // Handle various number representations
251275 switch v := defaultVal .(type ) {
252276 case float64 :
@@ -265,7 +289,7 @@ func (*workflowEngine) coerceDefaultValue(defaultVal any, targetType string) (an
265289 return nil , fmt .Errorf ("cannot coerce default value %v (type %T) to number" , defaultVal , defaultVal )
266290 }
267291
268- case "boolean" :
292+ case typeBoolean :
269293 switch v := defaultVal .(type ) {
270294 case bool :
271295 return v , nil
@@ -285,7 +309,7 @@ func (*workflowEngine) coerceDefaultValue(defaultVal any, targetType string) (an
285309 return nil , fmt .Errorf ("cannot coerce default value %v (type %T) to boolean" , defaultVal , defaultVal )
286310 }
287311
288- case "object" :
312+ case typeObject :
289313 // For objects, accept maps or JSON strings
290314 if objMap , ok := defaultVal .(map [string ]any ); ok {
291315 return objMap , nil
@@ -299,7 +323,7 @@ func (*workflowEngine) coerceDefaultValue(defaultVal any, targetType string) (an
299323 }
300324 return nil , fmt .Errorf ("cannot coerce default value %v (type %T) to object" , defaultVal , defaultVal )
301325
302- case "array" :
326+ case typeArray :
303327 // For arrays, accept slices or JSON strings
304328 if arr , ok := defaultVal .([]any ); ok {
305329 return arr , nil
0 commit comments