Skip to content
Open
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
21 changes: 14 additions & 7 deletions tools/cli/pkg/apiversion/apiversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,29 @@
package apiversion

import (
"github.com/getkin/kin-openapi/openapi3"
"github.com/mongodb/openapi/tools/cli/internal/apiversion"
)

// Parse extracts the API version string from a versioned media type
// (e.g. "application/vnd.atlas.2024-01-01+json" → "2024-01-01").
// Returns an error if the media type does not match the expected pattern.
func Parse(contentType string) (string, error) {
return apiversion.Parse(contentType)
// APIVersion is a parsed Atlas API version with its stability level.
type APIVersion = apiversion.APIVersion

const (
StableStabilityLevel = apiversion.StableStabilityLevel
PreviewStabilityLevel = apiversion.PreviewStabilityLevel
UpcomingStabilityLevel = apiversion.UpcomingStabilityLevel
)

// ParseVersionFromContentType parses a versioned Atlas media type string (e.g. application/vnd.atlas.2024-01-01+json)
// and its OpenAPI media type value into an APIVersion. Handles stable dates, preview, and upcoming stability levels.
func ParseVersionFromContentType(contentType string, mediaType *openapi3.MediaType) (*APIVersion, error) {
return apiversion.New(apiversion.WithFullContent(contentType, mediaType))
}

// IsPreviewStabilityLevel reports whether the given version string represents a preview release.
func IsPreviewStabilityLevel(version string) bool {
return apiversion.IsPreviewStabilityLevel(version)
}

// IsUpcomingStabilityLevel reports whether the given version string represents an upcoming release.
func IsUpcomingStabilityLevel(version string) bool {
return apiversion.IsUpcomingStabilityLevel(version)
}
7 changes: 4 additions & 3 deletions tools/mcp-server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/mongodb/openapi/tools/mcp-server
go 1.26

require (
github.com/getkin/kin-openapi v0.135.0
github.com/getkin/kin-openapi v0.137.0
github.com/modelcontextprotocol/go-sdk v1.5.0
github.com/mongodb/openapi/tools/cli v0.0.0
github.com/spf13/afero v1.15.0
Expand All @@ -21,11 +21,12 @@ 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.14.0 // indirect
github.com/oasdiff/oasdiff v1.15.0 // indirect
github.com/oasdiff/yaml v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.12 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // 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
Expand Down
16 changes: 10 additions & 6 deletions tools/mcp-server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTA
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/getkin/kin-openapi v0.135.0 h1:751SjYfbiwqukYuVjwYEIKNfrSwS5YpA7DZnKSwQgtg=
github.com/getkin/kin-openapi v0.135.0/go.mod h1:6dd5FJl6RdX4usBtFBaQhk9q62Yb2J0Mk5IhUO/QqFI=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/getkin/kin-openapi v0.137.0 h1:Q3HhawNQV0GfvO2mIYMUBUSEFrDsVlzcYz4VydL9YEo=
github.com/getkin/kin-openapi v0.137.0/go.mod h1:vUYWaKyMqj7PfTybelXtLuLN9tReS12vxnzMRK+z2GY=
github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA=
github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0=
github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo=
Expand Down Expand Up @@ -33,18 +35,20 @@ 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/oasdiff v1.14.0 h1:R9nDNGHDoBpPUs/TthVDHGsJ63mnlL/vWUo252RRCgo=
github.com/oasdiff/oasdiff v1.14.0/go.mod h1:9Im1HSDZzkao0yWGiy/Crr6cPvUvPBiCRxzVtHtqIKA=
github.com/oasdiff/oasdiff v1.15.0 h1:K6rzxcVmSl0Tq5l9eJD4TGre5xK1n7HXEF7oRi5MHR0=
github.com/oasdiff/oasdiff v1.15.0/go.mod h1:rFbmb3hJSyrkxkriZqzjXRH1+SIwHr2m+jMIucg6lrQ=
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/oasdiff/yaml3 v0.0.12 h1:75urAtPeDg2/iDEWwzNrLOWxI9N/dCh81nTTJtokt2M=
github.com/oasdiff/yaml3 v0.0.12/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=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.4 h1:OW1VRern8Nw6ITAtwSZ7Idrl3MXCFwXHPgqESYfvNt0=
Expand Down
3 changes: 1 addition & 2 deletions tools/mcp-server/internal/resources/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package resources

import (
"encoding/json"
"fmt"

"github.com/getkin/kin-openapi/openapi3"
"github.com/modelcontextprotocol/go-sdk/mcp"
Expand Down Expand Up @@ -40,7 +39,7 @@ func handleAlias(reg *registry.Registry, req *mcp.ReadResourceRequest) (*mcp.Rea

entry, err := reg.GetByAlias(alias)
if err != nil {
return nil, fmt.Errorf("spec with alias %q not found", alias)
return nil, err
}

overview := buildSpecOverview(entry)
Expand Down
6 changes: 3 additions & 3 deletions tools/mcp-server/internal/resources/alias_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ func TestHandleAlias_Overview(t *testing.T) {
assert.Equal(t, "Test API", body.Title)
assert.Equal(t, registry.SourceTypeFile, body.SourceType)
assert.Equal(t, 4, body.Stats.Paths)
assert.Equal(t, 6, body.Stats.Operations)
assert.Equal(t, 7, body.Stats.Operations)
assert.Equal(t, 2, body.Stats.Tags)
assert.Equal(t, 2, body.Stats.Schemas)
assert.Equal(t, 3, body.Stats.Schemas)
assert.Equal(t, "2025-01-01", body.LatestStableVersion)
assert.Equal(t, []string{"2024-01-01", "2025-01-01"}, body.AvailableVersions)
assert.Equal(t, []string{"2023-02-01", "2024-01-01", "2025-01-01"}, body.AvailableVersions)
assert.True(t, body.HasPreview)
assert.True(t, body.HasUpcoming)
}
Expand Down
108 changes: 108 additions & 0 deletions tools/mcp-server/internal/resources/operations.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package resources

import (
"encoding/json"
"fmt"

"github.com/getkin/kin-openapi/openapi3"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/mongodb/openapi/tools/mcp-server/internal/registry"
)

// OperationParam represents a single parameter (path, query, header, or cookie).
type OperationParam struct {
Name string `json:"name"`
In string `json:"in"`
Description string `json:"description,omitempty"`
Required bool `json:"required"`
}

// ContentEntry represents a single versioned content type present in a request body or response.
type ContentEntry struct {
ContentType string `json:"contentType"`
Version string `json:"version,omitempty"`
Schema *openapi3.SchemaRef `json:"schema,omitempty"`
}

// OperationResponseDetail describes a single HTTP response with its versioned content types.
type OperationResponseDetail struct {
StatusCode string `json:"statusCode"`
Description string `json:"description,omitempty"`
Content []ContentEntry `json:"content"`
}

// RequestBodyDetail describes the request body with its versioned content types.
type RequestBodyDetail struct {
Required bool `json:"required"`
Content []ContentEntry `json:"content"`
}

// OperationDetail is the response body for the openapi://specs/{alias}/operations/{operationId} resource.
type OperationDetail struct {
OperationID string `json:"operationId"`
Method string `json:"method"`
Path string `json:"path"`
Summary string `json:"summary"`
Description string `json:"description,omitempty"`
LatestStableVersion string `json:"latestStableVersion"`
AvailableVersions []string `json:"availableVersions"`
HasPreview bool `json:"hasPreview"`
HasUpcoming bool `json:"hasUpcoming"`
Parameters []OperationParam `json:"parameters"`
RequestBody *RequestBodyDetail `json:"requestBody,omitempty"`
Responses []OperationResponseDetail `json:"responses"`
}

func handleOperation(reg *registry.Registry, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
alias, operationID, versionFilter, err := aliasAndOperationFromURI(req.Params.URI)
if err != nil {
return nil, err
}

entry, err := reg.GetByAlias(alias)
if err != nil {
return nil, err
}

method, path, op, err := findOperation(entry.Spec, operationID)
if err != nil {
return nil, err
}

detail, err := buildOperationDetail(op, method, path, versionFilter)
if err != nil {
return nil, err
}

data, err := json.Marshal(detail)
if err != nil {
return nil, err
}

return &mcp.ReadResourceResult{
Contents: []*mcp.ResourceContents{
{URI: req.Params.URI, MIMEType: mimeTypeJSON, Text: string(data)},
},
}, nil
}

// findOperation searches all paths in the spec for an operation matching operationID.
// Returns the HTTP method (uppercase), path string, and operation pointer.
func findOperation(spec *openapi3.T, operationID string) (method, path string, op *openapi3.Operation, err error) {
if spec == nil || spec.Paths == nil {
return "", "", nil, fmt.Errorf("operation %q not found", operationID)
}

for p, item := range spec.Paths.Map() {
if item == nil {
continue
}
for m, o := range item.Operations() {
if o != nil && o.OperationID == operationID {
return m, p, o, nil
}
}
}

return "", "", nil, fmt.Errorf("operation %q not found", operationID)
}
Loading
Loading