Thanks to visit codestin.com
Credit goes to github.com

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
26 changes: 23 additions & 3 deletions coderd/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -1702,9 +1702,29 @@ type mcpOAuth2Discovery struct {
// protectedResourceMetadata represents the response from a
// Protected Resource Metadata endpoint per RFC 9728 §2.
type protectedResourceMetadata struct {
Resource string `json:"resource"`
AuthorizationServers []string `json:"authorization_servers"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
Resource resourceIdentifiers `json:"resource"`
AuthorizationServers []string `json:"authorization_servers"`
ScopesSupported []string `json:"scopes_supported,omitempty"`
}

// resourceIdentifiers tolerates both a single JSON string and an
// array of strings. RFC 9728 §2 defines "resource" as a string, but
// some servers (e.g. GitLab's official MCP server) return an array
// when the metadata document covers multiple resources.
type resourceIdentifiers []string

func (r *resourceIdentifiers) UnmarshalJSON(data []byte) error {
var single string
if err := json.Unmarshal(data, &single); err == nil {
*r = resourceIdentifiers{single}
return nil
}
var many []string
if err := json.Unmarshal(data, &many); err != nil {
return xerrors.New("resource must be a string or an array of strings")
}
*r = resourceIdentifiers(many)
return nil
}

// authServerMetadata represents the response from an Authorization
Expand Down
53 changes: 53 additions & 0 deletions coderd/mcp_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package coderd

import (
"context"
"encoding/json"
"sync/atomic"
"testing"
"time"
Expand Down Expand Up @@ -214,3 +215,55 @@ func TestOIDCMCPTokenSource(t *testing.T) {
require.Empty(t, tok)
})
}

func TestProtectedResourceMetadataResourceUnmarshal(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
want resourceIdentifiers
wantErr bool
}{
{
name: "String",
input: `{"resource": "https://gitlab.example.com/api/v4/mcp", "authorization_servers": ["https://gitlab.example.com"]}`,
want: resourceIdentifiers{"https://gitlab.example.com/api/v4/mcp"},
},
{
// GitLab's official MCP server returns an array of
// resources despite RFC 9728 defining a string.
name: "Array",
input: `{"resource": ["https://gitlab.example.com/api/v4/mcp", "https://gitlab.example.com/api/v4/orbit/mcp"], "authorization_servers": ["https://gitlab.example.com"]}`,
want: resourceIdentifiers{
"https://gitlab.example.com/api/v4/mcp",
"https://gitlab.example.com/api/v4/orbit/mcp",
},
},
{
name: "Absent",
input: `{"authorization_servers": ["https://gitlab.example.com"]}`,
want: nil,
},
{
name: "InvalidType",
input: `{"resource": 42, "authorization_servers": ["https://gitlab.example.com"]}`,
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

var meta protectedResourceMetadata
err := json.Unmarshal([]byte(tt.input), &meta)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, meta.Resource)
})
}
}
Loading