4cddfcf2f3
Backend Tests / Tests (other) (push) Failing after 1s
Backend Tests / Static Checks (push) Failing after 2s
Backend Tests / Tests (internal) (push) Failing after 2s
Backend Tests / Tests (server) (push) Failing after 1s
Backend Tests / Tests (store) (push) Failing after 1s
Build Canary Image / build-frontend (push) Failing after 1s
Frontend Tests / Lint (push) Failing after 1s
Build Canary Image / build-push (linux/amd64) (push) Has been skipped
Build Canary Image / build-push (linux/arm64) (push) Has been skipped
Build Canary Image / merge (push) Has been skipped
Frontend Tests / Build (push) Failing after 1s
Release Please / release-please (push) Failing after 0s
Proto Linter / Lint Protos (push) Failing after 2s
253 lines
7.2 KiB
Go
253 lines
7.2 KiB
Go
package mcp
|
|
|
|
import (
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/pkg/errors"
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
type jsonSchema map[string]any
|
|
|
|
type openAPISpec struct {
|
|
OpenAPI string `yaml:"openapi"`
|
|
Paths map[string]map[string]*openAPIOperation `yaml:"paths"`
|
|
Components openAPIComponents `yaml:"components"`
|
|
}
|
|
|
|
type openAPIComponents struct {
|
|
Schemas map[string]jsonSchema `yaml:"schemas"`
|
|
}
|
|
|
|
type openAPIOperation struct {
|
|
OperationID string `yaml:"operationId"`
|
|
Description string `yaml:"description"`
|
|
Parameters []openAPIParameter `yaml:"parameters"`
|
|
RequestBody *openAPIRequestBody `yaml:"requestBody"`
|
|
Responses map[string]openAPIResponse `yaml:"responses"`
|
|
Method string `yaml:"-"`
|
|
Path string `yaml:"-"`
|
|
ResponseSchema jsonSchema `yaml:"-"`
|
|
RequestBodySchema jsonSchema `yaml:"-"`
|
|
}
|
|
|
|
type openAPIParameter struct {
|
|
Name string `yaml:"name"`
|
|
In string `yaml:"in"`
|
|
Description string `yaml:"description"`
|
|
Required bool `yaml:"required"`
|
|
Schema jsonSchema `yaml:"schema"`
|
|
}
|
|
|
|
type openAPIRequestBody struct {
|
|
Required bool `yaml:"required"`
|
|
Content map[string]openAPIMediaType `yaml:"content"`
|
|
}
|
|
|
|
type openAPIResponse struct {
|
|
Description string `yaml:"description"`
|
|
Content map[string]openAPIMediaType `yaml:"content"`
|
|
}
|
|
|
|
type openAPIMediaType struct {
|
|
Schema jsonSchema `yaml:"schema"`
|
|
}
|
|
|
|
func loadOpenAPISpec(path string) (*openAPISpec, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to read OpenAPI spec")
|
|
}
|
|
|
|
spec := &openAPISpec{}
|
|
if err := yaml.Unmarshal(data, spec); err != nil {
|
|
return nil, errors.Wrap(err, "failed to parse OpenAPI spec")
|
|
}
|
|
if spec.Paths == nil {
|
|
return nil, errors.New("OpenAPI spec has no paths")
|
|
}
|
|
return spec, nil
|
|
}
|
|
|
|
func buildOperationRegistry(spec *openAPISpec) (map[string]*openAPIOperation, error) {
|
|
registry := map[string]*openAPIOperation{}
|
|
for path, pathItem := range spec.Paths {
|
|
for method, operation := range pathItem {
|
|
if operation == nil || operation.OperationID == "" {
|
|
continue
|
|
}
|
|
if _, exists := registry[operation.OperationID]; exists {
|
|
return nil, errors.Errorf("duplicate OpenAPI operationId %q", operation.OperationID)
|
|
}
|
|
|
|
operation.Method = strings.ToUpper(method)
|
|
operation.Path = path
|
|
|
|
responseSchema, err := operationSuccessResponseSchema(spec, operation)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to resolve response schema for %s", operation.OperationID)
|
|
}
|
|
operation.ResponseSchema = responseSchema
|
|
|
|
requestBodySchema, err := operationRequestBodySchema(spec, operation)
|
|
if err != nil {
|
|
return nil, errors.Wrapf(err, "failed to resolve request body schema for %s", operation.OperationID)
|
|
}
|
|
operation.RequestBodySchema = requestBodySchema
|
|
|
|
registry[operation.OperationID] = operation
|
|
}
|
|
}
|
|
return registry, nil
|
|
}
|
|
|
|
func operationSuccessResponseSchema(spec *openAPISpec, operation *openAPIOperation) (jsonSchema, error) {
|
|
response, ok := operation.Responses["200"]
|
|
if !ok || response.Content == nil {
|
|
return okSchema(), nil
|
|
}
|
|
mediaType, ok := response.Content["application/json"]
|
|
if !ok || mediaType.Schema == nil {
|
|
return okSchema(), nil
|
|
}
|
|
return resolveSchemaRef(spec, mediaType.Schema)
|
|
}
|
|
|
|
func operationRequestBodySchema(spec *openAPISpec, operation *openAPIOperation) (jsonSchema, error) {
|
|
if operation.RequestBody == nil {
|
|
return nil, nil
|
|
}
|
|
mediaType, ok := operation.RequestBody.Content["application/json"]
|
|
if !ok || mediaType.Schema == nil {
|
|
return jsonSchema{"type": "object"}, nil
|
|
}
|
|
return resolveSchemaRef(spec, mediaType.Schema)
|
|
}
|
|
|
|
func resolveSchemaRef(spec *openAPISpec, schema jsonSchema) (jsonSchema, error) {
|
|
defs := map[string]any{}
|
|
resolved, err := resolveSchemaValue(spec, schema, defs, map[string]bool{}, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resolvedSchema, ok := resolved.(map[string]any)
|
|
if !ok {
|
|
return nil, errors.New("resolved schema is not an object")
|
|
}
|
|
if len(defs) > 0 {
|
|
resolvedSchema["$defs"] = defs
|
|
}
|
|
return jsonSchema(resolvedSchema), nil
|
|
}
|
|
|
|
func resolveSchemaValue(spec *openAPISpec, value any, defs map[string]any, resolving map[string]bool, inlineRef bool) (any, error) {
|
|
switch typed := value.(type) {
|
|
case jsonSchema:
|
|
return resolveSchemaMap(spec, map[string]any(typed), defs, resolving, inlineRef)
|
|
case map[string]any:
|
|
return resolveSchemaMap(spec, typed, defs, resolving, inlineRef)
|
|
case []any:
|
|
resolved := make([]any, 0, len(typed))
|
|
for _, item := range typed {
|
|
resolvedItem, err := resolveSchemaValue(spec, item, defs, resolving, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolved = append(resolved, resolvedItem)
|
|
}
|
|
return resolved, nil
|
|
default:
|
|
return value, nil
|
|
}
|
|
}
|
|
|
|
func resolveSchemaMap(spec *openAPISpec, schema map[string]any, defs map[string]any, resolving map[string]bool, inlineRef bool) (map[string]any, error) {
|
|
if ref, ok := schema["$ref"].(string); ok && ref != "" {
|
|
name, err := schemaComponentName(ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if inlineRef {
|
|
return resolveComponentSchema(spec, name, defs, resolving)
|
|
}
|
|
if err := addSchemaDef(spec, name, defs, resolving); err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"$ref": "#/$defs/" + name}, nil
|
|
}
|
|
|
|
resolved := make(map[string]any, len(schema))
|
|
for key, value := range schema {
|
|
resolvedValue, err := resolveSchemaValue(spec, value, defs, resolving, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resolved[key] = resolvedValue
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func resolveComponentSchema(spec *openAPISpec, name string, defs map[string]any, resolving map[string]bool) (map[string]any, error) {
|
|
component, ok := spec.Components.Schemas[name]
|
|
if !ok {
|
|
return nil, errors.Errorf("schema ref %q not found", schemaComponentRef(name))
|
|
}
|
|
resolving[name] = true
|
|
resolved, err := resolveSchemaMap(spec, map[string]any(component), defs, resolving, false)
|
|
delete(resolving, name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, ok := defs[name]; ok {
|
|
defs[name] = resolved
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
func addSchemaDef(spec *openAPISpec, name string, defs map[string]any, resolving map[string]bool) error {
|
|
if _, ok := defs[name]; ok {
|
|
return nil
|
|
}
|
|
component, ok := spec.Components.Schemas[name]
|
|
if !ok {
|
|
return errors.Errorf("schema ref %q not found", schemaComponentRef(name))
|
|
}
|
|
|
|
defs[name] = map[string]any{}
|
|
if resolving[name] {
|
|
return nil
|
|
}
|
|
|
|
resolving[name] = true
|
|
resolved, err := resolveSchemaMap(spec, map[string]any(component), defs, resolving, false)
|
|
delete(resolving, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defs[name] = resolved
|
|
return nil
|
|
}
|
|
|
|
func schemaComponentName(ref string) (string, error) {
|
|
const prefix = "#/components/schemas/"
|
|
if !strings.HasPrefix(ref, prefix) {
|
|
return "", errors.Errorf("unsupported schema ref %q", ref)
|
|
}
|
|
return strings.TrimPrefix(ref, prefix), nil
|
|
}
|
|
|
|
func schemaComponentRef(name string) string {
|
|
return "#/components/schemas/" + name
|
|
}
|
|
|
|
func okSchema() jsonSchema {
|
|
return jsonSchema{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"ok": map[string]any{"type": "boolean"},
|
|
},
|
|
}
|
|
}
|