1
1
package openapi
2
2
3
3
import (
4
+ "encoding/json"
4
5
"fmt"
6
+ "io"
7
+ "net/http"
8
+ "net/url"
9
+ "strings"
10
+
11
+ "gopkg.in/yaml.v3"
5
12
6
13
. "github.com/universal-tool-calling-protocol/go-utcp/src/providers/base"
7
14
. "github.com/universal-tool-calling-protocol/go-utcp/src/providers/http"
8
15
9
16
. "github.com/universal-tool-calling-protocol/go-utcp/src/auth"
10
17
. "github.com/universal-tool-calling-protocol/go-utcp/src/manual"
11
18
. "github.com/universal-tool-calling-protocol/go-utcp/src/tools"
12
-
13
- "net/url"
14
- "strings"
15
19
)
16
20
17
- // OpenApiConverter converts an OpenAPI JSON spec into a UtcpManual.
21
+ // OpenApiConverter converts an OpenAPI JSON/YAML spec into a UtcpManual.
18
22
type OpenApiConverter struct {
19
23
spec map [string ]interface {}
20
24
specURL string
@@ -51,6 +55,58 @@ func NewConverter(
51
55
}
52
56
}
53
57
58
+ // NewConverterFromURL fetches the spec (YAML or JSON) from the given URL and returns a converter.
59
+ // providerName can be empty to auto-derive from the spec.
60
+ func NewConverterFromURL (specURL string , providerName string ) (* OpenApiConverter , error ) {
61
+ spec , finalURL , err := LoadSpecFromURL (specURL )
62
+ if err != nil {
63
+ return nil , fmt .Errorf ("failed to load spec from URL %s: %w" , specURL , err )
64
+ }
65
+ return NewConverter (spec , finalURL , providerName ), nil
66
+ }
67
+
68
+ // LoadSpecFromURL fetches the content at the URL and attempts to decode it first as JSON,
69
+ // and if that fails, as YAML. Returns the spec as a map and the normalized URL used.
70
+ func LoadSpecFromURL (rawURL string ) (map [string ]interface {}, string , error ) {
71
+ resp , err := http .Get (rawURL )
72
+ if err != nil {
73
+ return nil , rawURL , fmt .Errorf ("http GET failed: %w" , err )
74
+ }
75
+ defer resp .Body .Close ()
76
+
77
+ if resp .StatusCode < 200 || resp .StatusCode >= 300 {
78
+ return nil , rawURL , fmt .Errorf ("unexpected HTTP status: %s" , resp .Status )
79
+ }
80
+
81
+ bodyBytes , err := io .ReadAll (resp .Body )
82
+ if err != nil {
83
+ return nil , rawURL , fmt .Errorf ("reading body failed: %w" , err )
84
+ }
85
+
86
+ var spec map [string ]interface {}
87
+ // Try JSON first
88
+ if err := json .Unmarshal (bodyBytes , & spec ); err == nil {
89
+ return spec , resp .Request .URL .String (), nil
90
+ }
91
+
92
+ // Fallback to YAML
93
+ var yamlRaw interface {}
94
+ if err := yaml .Unmarshal (bodyBytes , & yamlRaw ); err != nil {
95
+ return nil , rawURL , fmt .Errorf ("failed to parse as JSON (%v) or YAML (%v)" , err , err )
96
+ }
97
+
98
+ // Convert YAML parsed structure into map[string]interface{} with proper types (via intermediate JSON).
99
+ intermediate , err := json .Marshal (yamlRaw )
100
+ if err != nil {
101
+ return nil , rawURL , fmt .Errorf ("failed to re-marshal YAML content: %w" , err )
102
+ }
103
+ if err := json .Unmarshal (intermediate , & spec ); err != nil {
104
+ return nil , rawURL , fmt .Errorf ("failed to unmarshal intermediate YAML->JSON: %w" , err )
105
+ }
106
+
107
+ return spec , resp .Request .URL .String (), nil
108
+ }
109
+
54
110
// Convert parses the OpenAPI spec and builds a UtcpManual.
55
111
func (c * OpenApiConverter ) Convert () UtcpManual {
56
112
var tools []Tool
@@ -63,6 +119,15 @@ func (c *OpenApiConverter) Convert() UtcpManual {
63
119
baseURL = u
64
120
}
65
121
}
122
+ } else if host , ok := c .spec ["host" ].(string ); ok {
123
+ scheme := "https"
124
+ if schemes , ok := c .spec ["schemes" ].([]interface {}); ok && len (schemes ) > 0 {
125
+ if s , _ := schemes [0 ].(string ); s != "" {
126
+ scheme = s
127
+ }
128
+ }
129
+ basePath , _ := c .spec ["basePath" ].(string )
130
+ baseURL = fmt .Sprintf ("%s://%s%s" , scheme , host , basePath )
66
131
} else if c .specURL != "" {
67
132
if pu , err := url .Parse (c .specURL ); err == nil {
68
133
baseURL = fmt .Sprintf ("%s://%s" , pu .Scheme , pu .Host )
@@ -280,7 +345,9 @@ func (c *OpenApiConverter) createTool(
280
345
) (* Tool , error ) {
281
346
opID , _ := op ["operationId" ].(string )
282
347
if opID == "" {
283
- return nil , nil // skip unnamed ops
348
+ // fallback: derive from method and path, e.g., POST /request -> post_request
349
+ sanitizedPath := strings .ReplaceAll (strings .Trim (path , "/" ), "/" , "_" )
350
+ opID = fmt .Sprintf ("%s_%s" , strings .ToLower (method ), sanitizedPath )
284
351
}
285
352
286
353
desc , _ := op ["summary" ].(string )
@@ -346,6 +413,9 @@ func (c *OpenApiConverter) extractInputs(
346
413
if loc == "header" {
347
414
headers = append (headers , name )
348
415
}
416
+ if loc == "body" {
417
+ bodyField = & name
418
+ }
349
419
sch := c .resolveSchema (param ["schema" ]).(map [string ]interface {})
350
420
entry := map [string ]interface {}{
351
421
"type" : sch ["type" ],
@@ -438,6 +508,33 @@ func (c *OpenApiConverter) extractOutputs(
438
508
return out
439
509
}
440
510
}
511
+ } else if schema , ok := resp ["schema" ].(map [string ]interface {}); ok {
512
+ sch := c .resolveSchema (schema ).(map [string ]interface {})
513
+ out := ToolInputOutputSchema {
514
+ Type : castString (sch ["type" ], "object" ),
515
+ Properties : castMap (sch ["properties" ]),
516
+ Required : castStringSlice (sch ["required" ]),
517
+ Description : castString (sch ["description" ], castString (resp ["description" ], "" )),
518
+ Title : castString (sch ["title" ], "" ),
519
+ }
520
+ if out .Type == "array" {
521
+ out .Items = castMap (sch ["items" ])
522
+ }
523
+ for _ , attr := range []string {"enum" , "minimum" , "maximum" , "format" } {
524
+ if v , ok := sch [attr ]; ok {
525
+ switch attr {
526
+ case "enum" :
527
+ out .Enum = castInterfaceSlice (v )
528
+ case "minimum" :
529
+ out .Minimum = castFloat (v )
530
+ case "maximum" :
531
+ out .Maximum = castFloat (v )
532
+ case "format" :
533
+ out .Format = castString (v , "" )
534
+ }
535
+ }
536
+ }
537
+ return out
441
538
}
442
539
return ToolInputOutputSchema {}
443
540
}
0 commit comments