Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions tools/cli/pkg/openapi/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,21 @@ package openapi

import (
"github.com/mongodb/openapi/tools/cli/internal/openapi"
"github.com/mongodb/openapi/tools/cli/internal/openapi/slice"
"github.com/oasdiff/kin-openapi/openapi3"
"github.com/oasdiff/oasdiff/load"
"github.com/spf13/afero"
)

// SliceCriteria defines the selection criteria for slicing an OpenAPI spec.
// Operations matching ANY of the specified criteria will be included (OR logic).
type SliceCriteria = slice.Criteria

// Slice filters an OpenAPI spec to keep only operations matching the criteria.
func Slice(spec *openapi3.T, criteria *SliceCriteria) error {
return slice.Slice(spec, criteria)
}

// Loader provides methods for loading OpenAPI specifications from files.
type Loader struct {
impl *openapi.OpenAPI3
Expand Down
10 changes: 5 additions & 5 deletions tools/mcp-server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ go 1.26
require (
github.com/modelcontextprotocol/go-sdk v1.5.0
github.com/mongodb/openapi/tools/cli v0.0.0
github.com/oasdiff/kin-openapi v0.136.4
github.com/oasdiff/kin-openapi v0.136.10
github.com/spf13/afero v1.15.0
)

Expand All @@ -19,17 +19,17 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.9.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/oasdiff/oasdiff v1.12.7 // indirect
github.com/oasdiff/yaml v0.0.4 // indirect
github.com/oasdiff/yaml3 v0.0.4 // indirect
github.com/oasdiff/oasdiff v1.13.1 // indirect
github.com/oasdiff/yaml v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.9 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.4 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/wI2L/jsondiff v0.7.0 // indirect
github.com/wI2L/jsondiff v0.7.1 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
github.com/yargevad/filepathx v1.0.0 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
Expand Down
20 changes: 10 additions & 10 deletions tools/mcp-server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzB
github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
github.com/oasdiff/kin-openapi v0.136.4 h1:idO/oW/6liYIOns49liiCN8KbUke7ckWoi+dePDu6Dc=
github.com/oasdiff/kin-openapi v0.136.4/go.mod h1:P7JOZRsZdRww4urEHzu0VFejiJC25RbS7XqQ4h4KDFQ=
github.com/oasdiff/oasdiff v1.12.7 h1:DAbAyqWie1kbk6lXD2ZPME/I3lTXu4U7VR96jmDoQBw=
github.com/oasdiff/oasdiff v1.12.7/go.mod h1:GHJYWhxCAQ7AdpATB9mcTvYe/IrJ423+SdMrQHiPa8Q=
github.com/oasdiff/yaml v0.0.4 h1:airPco4LbUoK4nbVwu+wwkRg2WarLC96cgBhgN93fsE=
github.com/oasdiff/yaml v0.0.4/go.mod h1:EaJ6/lcrRLK+syawtvtrHdbrrln4/SUmQw6aBTIlaMs=
github.com/oasdiff/yaml3 v0.0.4 h1:U5RTQZpBmsbcyCFlzPVuMctk6Jme6lOrbl0jJoOovMw=
github.com/oasdiff/yaml3 v0.0.4/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/oasdiff/kin-openapi v0.136.10 h1:3vKm43dfhRvJerEyijS74bLwLxE7ItB0caMJl+IjGA0=
github.com/oasdiff/kin-openapi v0.136.10/go.mod h1:E7LUU2UyOHENK+Mhu7stMOtUA41LJRp5sFAW+L1Ier4=
github.com/oasdiff/oasdiff v1.13.1 h1:mvEv09sUT0V4926Q+1cwurEYvzV/Lj6d54xINIxcvHA=
github.com/oasdiff/oasdiff v1.13.1/go.mod h1:ZUEb4bzuo+fpHWGLO7f+++Vq7oV2IBgVC9KDRn/z3IA=
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g=
github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
Expand Down Expand Up @@ -66,8 +66,8 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/wI2L/jsondiff v0.7.0 h1:1lH1G37GhBPqCfp/lrs91rf/2j3DktX6qYAKZkLuCQQ=
github.com/wI2L/jsondiff v0.7.0/go.mod h1:KAEIojdQq66oJiHhDyQez2x+sRit0vIzC9KeK0yizxM=
github.com/wI2L/jsondiff v0.7.1 h1:Fg9+yj+1/x3UtPBJhR91TKEzRkrEEWcAcLbg9dzEaNM=
github.com/wI2L/jsondiff v0.7.1/go.mod h1:yAt2W7U6Jd4HK0RA8DGSGk0zDtfEtOUUJVnH/xICpjo=
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc=
Expand Down
140 changes: 0 additions & 140 deletions tools/mcp-server/internal/tools/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,146 +8,6 @@ import (
"github.com/oasdiff/kin-openapi/openapi3"
)

// createTestSpec creates a comprehensive test OpenAPI spec for search testing.
func createTestSpec() *openapi3.T {
spec := &openapi3.T{
OpenAPI: "3.0.0",
Info: &openapi3.Info{
Title: "Test API",
Version: "1.0.0",
},
Paths: &openapi3.Paths{},
Components: &openapi3.Components{},
Tags: []*openapi3.Tag{
{Name: "Users", Description: "User management endpoints"},
{Name: "Clusters", Description: "Cluster operations"},
},
}

// Add operations
spec.Paths.Set("/users", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "getUsers",
Summary: "Get all users",
Description: "Retrieve a list of all users in the system",
Tags: []string{"Users"},
},
Post: &openapi3.Operation{
OperationID: "createUser",
Summary: "Create a user",
Description: "Create a new user account",
Tags: []string{"Users"},
},
})

spec.Paths.Set("/users/{userId}", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "getUser",
Summary: "Get user by ID",
Description: "Retrieve a specific user by their ID",
Tags: []string{"Users"},
},
})

spec.Paths.Set("/clusters", &openapi3.PathItem{
Post: &openapi3.Operation{
OperationID: "createCluster",
Summary: "Create a new cluster",
Description: "Creates a new cluster in the project",
Tags: []string{"Clusters"},
},
Get: &openapi3.Operation{
OperationID: "listClusters",
Summary: "List clusters",
Description: "Get all clusters in the project",
Tags: []string{"Clusters"},
},
})

spec.Paths.Set("/clusters/{clusterId}", &openapi3.PathItem{
Get: &openapi3.Operation{
OperationID: "getCluster",
Summary: "Get cluster details",
Description: "Retrieve details for a specific cluster",
Tags: []string{"Clusters"},
},
})

// Add schemas
spec.Components.Schemas = make(map[string]*openapi3.SchemaRef)
spec.Components.Schemas["User"] = &openapi3.SchemaRef{
Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Description: "User account information",
Properties: map[string]*openapi3.SchemaRef{
"userId": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}},
"username": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}},
"email": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}},
},
},
}

spec.Components.Schemas["Cluster"] = &openapi3.SchemaRef{
Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Description: "Cluster configuration",
Properties: map[string]*openapi3.SchemaRef{
"clusterId": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}},
"name": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}},
"region": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}},
},
},
}

spec.Components.Schemas["Database"] = &openapi3.SchemaRef{
Value: &openapi3.Schema{
Type: &openapi3.Types{"object"},
Description: "Database information",
Properties: map[string]*openapi3.SchemaRef{
"databaseName": {Value: &openapi3.Schema{Type: &openapi3.Types{"string"}}},
},
},
}

// Add parameters
spec.Components.Parameters = make(map[string]*openapi3.ParameterRef)
spec.Components.Parameters["userId"] = &openapi3.ParameterRef{
Value: &openapi3.Parameter{
Name: "userId",
In: "path",
Description: "Unique identifier for the user",
Required: true,
},
}

spec.Components.Parameters["clusterId"] = &openapi3.ParameterRef{
Value: &openapi3.Parameter{
Name: "clusterId",
In: "path",
Description: "Unique identifier for the cluster",
Required: true,
},
}

// Add responses
spec.Components.Responses = make(map[string]*openapi3.ResponseRef)
notFound := "Not Found"
spec.Components.Responses["NotFound"] = &openapi3.ResponseRef{
Value: &openapi3.Response{
Description: &notFound,
},
}

unauthorized := "Unauthorized"
spec.Components.Responses["Unauthorized"] = &openapi3.ResponseRef{
Value: &openapi3.Response{
Description: &unauthorized,
},
}

return spec
}

// setupTestRegistry creates a registry with the test spec loaded.
func setupTestRegistry(t *testing.T) *registry.Registry {
t.Helper()
Expand Down
129 changes: 129 additions & 0 deletions tools/mcp-server/internal/tools/slice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package tools

import (
"fmt"
"strings"

"github.com/mongodb/openapi/tools/cli/pkg/openapi"
"github.com/mongodb/openapi/tools/mcp-server/internal/registry"
"github.com/oasdiff/kin-openapi/openapi3"
)

// SliceParams are the parameters for the slice tool.
type SliceParams struct {
SourceAlias string `json:"sourceAlias" jsonschema:"Alias of the source spec to slice"`
SaveAs string `json:"saveAs" jsonschema:"Alias for the resulting virtual spec (e.g. 'my-api-users')"`
Tags []string `json:"tags,omitempty" jsonschema:"Optional: filter by tags"`
OperationIDs []string `json:"operationIds,omitempty" jsonschema:"Optional: filter by operation IDs"`
Paths []string `json:"paths,omitempty" jsonschema:"Optional: filter by path patterns"`
}

// SliceResult is the response from the slice tool.
type SliceResult struct {
Success bool `json:"success"`
Message string `json:"message,omitempty"`
Alias string `json:"alias,omitempty"`
Error string `json:"error,omitempty"`
}

func handleSlice(reg *registry.Registry, params *SliceParams) (SliceResult, error) {
if params.SaveAs == "" {
return SliceResult{
Success: false,
Error: "saveAs is required: provide an alias for the resulting virtual spec (e.g. 'my-api-users')",
}, nil
}

if !isValidAlias(params.SaveAs) {
return SliceResult{
Success: false,
Error: fmt.Sprintf("invalid saveAs alias '%s': only lowercase letters, numbers, and hyphens allowed", params.SaveAs),
}, nil
}

if len(params.Tags) == 0 && len(params.OperationIDs) == 0 && len(params.Paths) == 0 {
return SliceResult{
Success: false,
Error: "at least one of tags, operationIds, or paths must be specified",
}, nil
}

if _, err := reg.GetByAlias(params.SaveAs); err == nil {
return SliceResult{
Success: false,
Error: fmt.Sprintf("alias '%s' is already in use, choose a different saveAs alias", params.SaveAs),
}, nil
}

entry, err := reg.GetByAlias(params.SourceAlias)
if err != nil {
return SliceResult{
Success: false,
Error: fmt.Sprintf("spec with alias '%s' not found", params.SourceAlias),
}, nil
}

specCopy, err := copySpec(entry.Spec)
if err != nil {
return SliceResult{
Success: false,
Error: fmt.Sprintf("failed to copy spec: %v", err),
}, nil
}

criteria := &openapi.SliceCriteria{
Tags: params.Tags,
OperationIDs: params.OperationIDs,
Paths: params.Paths,
}

if sliceErr := openapi.Slice(specCopy, criteria); sliceErr != nil {
return SliceResult{
Success: false,
Error: fmt.Sprintf("failed to slice spec: %v", sliceErr),
}, nil
}

if addErr := reg.Add(params.SaveAs, "", specCopy, entry.Metadata); addErr != nil {
return SliceResult{
Success: false,
Error: fmt.Sprintf("failed to save virtual spec: %v", addErr),
}, nil
}

return SliceResult{
Success: true,
Message: "Successfully created sliced spec filtered by " + buildFilterDescription(params),
Alias: params.SaveAs,
}, nil
}

func buildFilterDescription(params *SliceParams) string {
filters := []string{}
if len(params.Tags) > 0 {
filters = append(filters, fmt.Sprintf("tags: %v", params.Tags))
}
if len(params.OperationIDs) > 0 {
filters = append(filters, fmt.Sprintf("operation IDs: %v", params.OperationIDs))
}
if len(params.Paths) > 0 {
filters = append(filters, fmt.Sprintf("paths: %v", params.Paths))
}

return strings.Join(filters, ", ")
}

// copySpec creates a deep copy of an OpenAPI spec by marshaling and unmarshaling.
func copySpec(spec *openapi3.T) (*openapi3.T, error) {
data, err := spec.MarshalJSON()
if err != nil {
return nil, fmt.Errorf("failed to marshal spec: %w", err)
}

specCopy, err := openapi3.NewLoader().LoadFromData(data)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal spec: %w", err)
}

return specCopy, nil
}
Loading
Loading