package openapi import ( "context" "encoding/json" "fmt" "reflect" "strings" ) // ReflectRegistry provides helper methods for registering tools using reflection. type ReflectRegistry struct { *MapRegistry } // NewReflectRegistry creates a new ReflectRegistry wrapping a MapRegistry. func NewReflectRegistry(r *MapRegistry) *ReflectRegistry { return &ReflectRegistry{MapRegistry: r} } // RegisterTool registers a tool with a typed handler. // The handler's third argument must be a struct that defines the tool's parameters. func RegisterTool[T any](r *MapRegistry, name, description string, handler func(context.Context, *Session, T) (any, error)) error { var arg T schema, err := generateSchema(reflect.TypeOf(arg)) if err != nil { return fmt.Errorf("generate schema: %w", err) } wrappedHandler := func(ctx context.Context, s *Session, args json.RawMessage) (any, error) { var params T if err := json.Unmarshal(args, ¶ms); err != nil { return nil, fmt.Errorf("unmarshal arguments: %w", err) } return handler(ctx, s, params) } return r.Register(name, description, schema, wrappedHandler) } func generateSchema(t reflect.Type) (map[string]any, error) { if t.Kind() == reflect.Ptr { t = t.Elem() } if t.Kind() != reflect.Struct { return nil, fmt.Errorf("expected struct, got %s", t.Kind()) } properties := make(map[string]any) var required []string for i := 0; i < t.NumField(); i++ { field := t.Field(i) jsonTag := field.Tag.Get("json") if jsonTag == "" || jsonTag == "-" { continue } // Split json tag to handle omitempty etc. parts := strings.Split(jsonTag, ",") name := parts[0] prop := make(map[string]any) switch field.Type.Kind() { case reflect.String: prop["type"] = "string" case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: prop["type"] = "integer" case reflect.Float32, reflect.Float64: prop["type"] = "number" case reflect.Bool: prop["type"] = "boolean" case reflect.Slice: prop["type"] = "array" // Simple slice handling, could be improved itemType, _ := getJsonType(field.Type.Elem()) prop["items"] = map[string]string{"type": itemType} case reflect.Struct: subSchema, err := generateSchema(field.Type) if err != nil { return nil, err } prop = subSchema default: return nil, fmt.Errorf("unsupported field type: %s", field.Type.Kind()) } if desc := field.Tag.Get("desc"); desc != "" { prop["description"] = desc } properties[name] = prop // Determine if field is required. // Priority: 'required' tag > 'omitempty' in 'json' tag. isRequired := true for _, p := range parts[1:] { if p == "omitempty" { isRequired = false break } } if reqTag := field.Tag.Get("required"); reqTag != "" { isRequired = (reqTag == "true") } if isRequired { required = append(required, name) } } schema := map[string]any{ "type": "object", "properties": properties, } if len(required) > 0 { schema["required"] = required } return schema, nil } func getJsonType(t reflect.Type) (string, error) { switch t.Kind() { case reflect.String: return "string", nil case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return "integer", nil case reflect.Float32, reflect.Float64: return "number", nil case reflect.Bool: return "boolean", nil default: return "", fmt.Errorf("unsupported item type: %s", t.Kind()) } }