diff --git a/go.mod b/go.mod index f5fa5ff2e..4252abe37 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/jmoiron/sqlx v1.4.0 github.com/mikefarah/yq/v4 v4.45.4 - github.com/modelcontextprotocol/go-sdk v1.4.1 + github.com/modelcontextprotocol/go-sdk v1.5.0 github.com/modelcontextprotocol/registry v0.0.0-00010101000000-000000000000 github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/image-spec v1.1.1 @@ -37,7 +37,7 @@ require ( go.opentelemetry.io/otel/sdk v1.38.0 go.opentelemetry.io/otel/sdk/metric v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 - golang.org/x/oauth2 v0.34.0 + golang.org/x/oauth2 v0.35.0 golang.org/x/sync v0.19.0 gopkg.in/op/go-logging.v1 v1.0.0-20160211212156-b2cb9fa56473 gopkg.in/yaml.v3 v3.0.1 @@ -200,7 +200,7 @@ require ( golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.30.0 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect diff --git a/go.sum b/go.sum index 6c72db787..473833cc6 100644 --- a/go.sum +++ b/go.sum @@ -374,8 +374,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= -github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang-migrate/migrate/v4 v4.19.0 h1:RcjOnCGz3Or6HQYEJ/EEVLfWnmw9KnoigPSjzhCuaSE= github.com/golang-migrate/migrate/v4 v4.19.0/go.mod h1:9dyEcu+hO+G9hPSw8AIg50yg622pXJsoHItQnDGZkI0= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= @@ -564,8 +564,8 @@ github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7z github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= -github.com/modelcontextprotocol/go-sdk v1.4.1 h1:M4x9GyIPj+HoIlHNGpK2hq5o3BFhC+78PkEaldQRphc= -github.com/modelcontextprotocol/go-sdk v1.4.1/go.mod h1:Bo/mS87hPQqHSRkMv4dQq1XCu6zv4INdXnFZabkNU6s= +github.com/modelcontextprotocol/go-sdk v1.5.0 h1:CHU0FIX9kpueNkxuYtfYQn1Z0slhFzBZuq+x6IiblIU= +github.com/modelcontextprotocol/go-sdk v1.5.0/go.mod h1:gggDIhoemhWs3BGkGwd1umzEXCEMMvAnhTrnbXJKKKA= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -869,8 +869,8 @@ golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= -golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -902,8 +902,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -922,8 +922,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/npm-catalog-check b/npm-catalog-check new file mode 100755 index 000000000..99e8eb10a Binary files /dev/null and b/npm-catalog-check differ diff --git a/npm-catalog-count b/npm-catalog-count new file mode 100755 index 000000000..5ec3f537a Binary files /dev/null and b/npm-catalog-count differ diff --git a/pkg/mcp/remote.go b/pkg/mcp/remote.go index d90b16222..372942ccb 100644 --- a/pkg/mcp/remote.go +++ b/pkg/mcp/remote.go @@ -81,37 +81,51 @@ func (c *remoteMCPClient) Initialize(ctx context.Context, _ *mcp.InitializeParam headers[k] = expandEnv(v, env) } - // Add OAuth token if remote server has OAuth configuration - if c.config.Spec.OAuth != nil && len(c.config.Spec.OAuth.Providers) > 0 { - if verbose { - log.Logf(" - Using OAuth token for: %s", c.config.Name) - } - credHelper := oauth.NewOAuthCredentialHelper() - token, err := credHelper.GetOAuthToken(ctx, c.config.Name) - if err != nil { - log.Logf("Failed to get OAuth token for %s: %v", c.config.Name, err) - } else if token != "" { - headers["Authorization"] = "Bearer " + token - } - } else if c.config.Spec.Remote.URL != "" { - // Community servers may have OAuth tokens via dynamic discovery (DCR) - // without explicit OAuth metadata in the catalog. Try to get a stored token. - // Use per-server mode so community servers read from docker pass. - mode := oauth.DetermineMode(ctx, c.config.Spec.IsCommunity()) - credHelper := oauth.NewOAuthCredentialHelperWithMode(mode) - token, err := credHelper.GetOAuthToken(ctx, c.config.Name) - if err == nil && token != "" { + // Determine OAuth mode for this server. + oauthMode := oauth.DetermineMode(ctx, c.config.Spec.IsCommunity()) + + // For SSE transport, inject OAuth token into headers (SSE lacks OAuthHandler support). + // For streamable transport, the SDK's OAuthHandler handles token injection and 401 retry. + isStreamable := false + switch strings.ToLower(transport) { + case "http", "streamable", "streaming", "streamable-http": + isStreamable = true + } + + if isStreamable { + // Streamable path: the SDK's OAuthHandler manages the Authorization header. + // Remove any user-configured Authorization to avoid conflicts. + delete(headers, "Authorization") + } else { + // SSE path: inject OAuth token directly into headers. + if c.config.Spec.OAuth != nil && len(c.config.Spec.OAuth.Providers) > 0 { if verbose { - log.Logf(" - Using dynamic OAuth token for: %s", c.config.Name) + log.Logf(" - Using OAuth token for: %s", c.config.Name) + } + credHelper := oauth.NewOAuthCredentialHelper() + token, err := credHelper.GetOAuthToken(ctx, c.config.Name) + if err != nil { + log.Logf("Failed to get OAuth token for %s: %v", c.config.Name, err) + } else if token != "" { + headers["Authorization"] = "Bearer " + token + } + } else if c.config.Spec.Remote.URL != "" { + credHelper := oauth.NewOAuthCredentialHelperWithMode(oauthMode) + token, err := credHelper.GetOAuthToken(ctx, c.config.Name) + if err == nil && token != "" { + if verbose { + log.Logf(" - Using dynamic OAuth token for: %s", c.config.Name) + } + headers["Authorization"] = "Bearer " + token } - headers["Authorization"] = "Bearer " + token } } var mcpTransport mcp.Transport var err error - // Create HTTP client with custom headers + // Create HTTP client with custom headers (non-OAuth headers for streamable, + // all headers including OAuth for SSE). httpClient := &http.Client{ Transport: &headerRoundTripper{ base: desktop.ProxyTransport(), @@ -126,9 +140,14 @@ func (c *remoteMCPClient) Initialize(ctx context.Context, _ *mcp.InitializeParam HTTPClient: httpClient, } case "http", "streamable", "streaming", "streamable-http": + sdkOAuthHandler := oauth.NewSDKHandler(c.config.Name, oauthMode) + if verbose { + log.Logf(" - Using SDK OAuthHandler for: %s (mode=%s)", c.config.Name, oauthMode) + } mcpTransport = &mcp.StreamableClientTransport{ - Endpoint: url, - HTTPClient: httpClient, + Endpoint: url, + HTTPClient: httpClient, + OAuthHandler: sdkOAuthHandler, } default: return fmt.Errorf("unsupported remote transport: %s", transport) diff --git a/pkg/oauth/sdk_handler.go b/pkg/oauth/sdk_handler.go new file mode 100644 index 000000000..5f90a1d66 --- /dev/null +++ b/pkg/oauth/sdk_handler.go @@ -0,0 +1,88 @@ +package oauth + +import ( + "context" + "fmt" + "io" + "net/http" + + "golang.org/x/oauth2" + + "github.com/docker/mcp-gateway/pkg/log" +) + +// SDKHandler implements auth.OAuthHandler from the go-sdk, bridging the +// gateway's existing credential storage (CE/Desktop/Community modes) to the +// SDK's transport-level OAuth. This enables automatic Bearer token injection +// and 401/403 retry on StreamableClientTransport. +type SDKHandler struct { + serverName string + mode Mode + credHelper *CredentialHelper +} + +// NewSDKHandler creates a handler for the given server using the specified +// credential storage mode. +func NewSDKHandler(serverName string, mode Mode) *SDKHandler { + return &SDKHandler{ + serverName: serverName, + mode: mode, + credHelper: NewOAuthCredentialHelperWithMode(mode), + } +} + +// TokenSource returns a token source that reads from the gateway's credential +// store on each Token() call. Returns nil (not an error) when no token exists, +// which tells the transport to skip the Authorization header. +func (h *SDKHandler) TokenSource(ctx context.Context) (oauth2.TokenSource, error) { + token, err := h.credHelper.GetOAuthToken(ctx, h.serverName) + if err != nil || token == "" { + return nil, nil //nolint:nilnil // nil token source means "no auth header" + } + return &gatewayTokenSource{ + ctx: ctx, + serverName: h.serverName, + credHelper: h.credHelper, + }, nil +} + +// Authorize is called by the SDK transport on 401/403 responses. The gateway +// runs as a background daemon and cannot interactively open a browser, so this +// method returns an error instructing the user to authorize manually. +// The response body is consumed and closed as required by the interface contract. +func (h *SDKHandler) Authorize(_ context.Context, _ *http.Request, resp *http.Response) error { + statusCode := 0 + if resp != nil { + statusCode = resp.StatusCode + if resp.Body != nil { + _, _ = io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + } + log.Logf("! OAuth authorization required for %s (received %d)", h.serverName, statusCode) + return fmt.Errorf("OAuth authorization required for %s: run 'docker mcp oauth authorize %s' to authenticate", h.serverName, h.serverName) +} + +// gatewayTokenSource implements oauth2.TokenSource by reading the current +// access token from the gateway's credential store. Each call to Token() +// fetches the latest token, so background refreshes are picked up automatically. +type gatewayTokenSource struct { + ctx context.Context //nolint:containedctx // oauth2.TokenSource.Token() has no context param; storing ctx is the only option + serverName string + credHelper *CredentialHelper +} + +func (ts *gatewayTokenSource) Token() (*oauth2.Token, error) { + accessToken, err := ts.credHelper.GetOAuthToken(ts.ctx, ts.serverName) + if err != nil { + return nil, fmt.Errorf("failed to get OAuth token for %s: %w", ts.serverName, err) + } + // Expiry is intentionally unset: the SDK calls TokenSource() on every + // request (no caching), so each call re-reads from the credential store. + // Background refresh is handled by the gateway's Provider refresh loop, + // not by the oauth2 library's built-in expiry logic. + return &oauth2.Token{ + AccessToken: accessToken, + TokenType: "Bearer", + }, nil +} diff --git a/pkg/oauth/sdk_handler_integration_test.go b/pkg/oauth/sdk_handler_integration_test.go new file mode 100644 index 000000000..2d16e2df0 --- /dev/null +++ b/pkg/oauth/sdk_handler_integration_test.go @@ -0,0 +1,233 @@ +package oauth + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "net" + "net/http" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/docker/docker-credential-helpers/credentials" + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestSDKHandler_Integration_TokenInjection verifies that an SDKHandler +// backed by a fake credential store correctly injects a Bearer token into +// requests made by the SDK's StreamableClientTransport. +// +// Flow: +// 1. Start an in-process MCP server that records the Authorization header. +// 2. Populate a fake credential store with a DCR client and token. +// 3. Create a StreamableClientTransport with OAuthHandler = our SDKHandler. +// 4. Connect and call a tool. +// 5. Assert the server received "Bearer ". +func TestSDKHandler_Integration_TokenInjection(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + const serverName = "integ-test-server" + const accessToken = "integ-test-access-token-42" + + // -- fake credential store -- + fake := newFakeCredentialHelper() + dcrJSON, _ := json.Marshal(map[string]string{ + "serverName": serverName, + "providerName": serverName, + "clientId": "c-123", + "authorizationEndpoint": "https://auth.example.com/authorize", + "tokenEndpoint": "https://auth.example.com/token", + }) + _ = fake.Add(&credentials.Credentials{ + ServerURL: fmt.Sprintf("https://%s.mcp-dcr", serverName), + Username: "dcr_client", + Secret: base64.StdEncoding.EncodeToString(dcrJSON), + }) + tokJSON, _ := json.Marshal(map[string]string{ + "access_token": accessToken, + "token_type": "Bearer", + }) + _ = fake.Add(&credentials.Credentials{ + ServerURL: fmt.Sprintf("https://auth.example.com/authorize/%s", serverName), + Username: fmt.Sprintf("oauth2_%s", serverName), + Secret: base64.StdEncoding.EncodeToString(tokJSON), + }) + + handler := &SDKHandler{ + serverName: serverName, + mode: ModeCE, + credHelper: &CredentialHelper{ + credentialHelper: fake, + mode: ModeCE, + }, + } + + // -- in-process MCP server -- + var receivedAuth atomic.Value + receivedAuth.Store("") + + mcpServer := mcp.NewServer(&mcp.Implementation{ + Name: "integ-test-mcp", + Version: "1.0.0", + }, nil) + + mcp.AddTool(mcpServer, &mcp.Tool{ + Name: "echo_auth", + Description: "Returns the Authorization header the server received", + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{}, + }, + }, func(_ context.Context, _ *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: receivedAuth.Load().(string)}, + }, + }, nil, nil + }) + + httpHandler := mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server { + receivedAuth.Store(r.Header.Get("Authorization")) + return mcpServer + }, nil) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + lc := &net.ListenConfig{} + listener, err := lc.Listen(ctx, "tcp", ":0") + require.NoError(t, err) + defer listener.Close() + + srv := &http.Server{Handler: httpHandler} + go func() { _ = srv.Serve(listener) }() + defer func() { _ = srv.Shutdown(context.Background()) }() + time.Sleep(50 * time.Millisecond) + + serverURL := fmt.Sprintf("http://localhost:%d", listener.Addr().(*net.TCPAddr).Port) + + // -- SDK client with OAuthHandler -- + transport := &mcp.StreamableClientTransport{ + Endpoint: serverURL, + OAuthHandler: handler, + } + + client := mcp.NewClient(&mcp.Implementation{ + Name: "integ-test-client", + Version: "1.0.0", + }, nil) + + session, err := client.Connect(ctx, transport, nil) + require.NoError(t, err) + defer session.Close() + + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "echo_auth", + }) + require.NoError(t, err) + require.Len(t, result.Content, 1) + + text, ok := result.Content[0].(*mcp.TextContent) + require.True(t, ok, "expected TextContent") + assert.Equal(t, "Bearer "+accessToken, text.Text, + "OAuthHandler should inject the correct Bearer token") +} + +// TestSDKHandler_Integration_401Retry verifies that the SDK's +// StreamableClientTransport calls Authorize on 401 and that our SDKHandler +// returns an informative error (the gateway cannot interactively auth). +// +// Flow: +// 1. Start an HTTP server that returns 401 on the first POST (the +// initialize request) and proxies subsequent requests to a real MCP server. +// 2. Create a StreamableClientTransport with OAuthHandler = our SDKHandler. +// 3. Attempt to connect. The SDK should call Authorize after the 401. +// 4. Since our Authorize returns an error, the connection should fail +// with the expected error message. +func TestSDKHandler_Integration_401Retry(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + + const serverName = "auth-required-server" + + // SDKHandler with empty credential store (no tokens available). + handler := &SDKHandler{ + serverName: serverName, + mode: ModeCE, + credHelper: &CredentialHelper{ + credentialHelper: newFakeCredentialHelper(), + mode: ModeCE, + }, + } + + // Real MCP server behind the 401 gate. + mcpServer := mcp.NewServer(&mcp.Implementation{ + Name: "gated-mcp", + Version: "1.0.0", + }, nil) + + httpHandler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server { + return mcpServer + }, nil) + + // Wrapper that returns 401 on the first request. + var firstRequest atomic.Bool + firstRequest.Store(true) + var mu sync.Mutex + + gatedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + mu.Lock() + isFirst := firstRequest.Load() + if isFirst { + firstRequest.Store(false) + } + mu.Unlock() + + if isFirst { + w.WriteHeader(http.StatusUnauthorized) + return + } + httpHandler.ServeHTTP(w, r) + }) + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + lc := &net.ListenConfig{} + listener, err := lc.Listen(ctx, "tcp", ":0") + require.NoError(t, err) + defer listener.Close() + + srv := &http.Server{Handler: gatedHandler} + go func() { _ = srv.Serve(listener) }() + defer func() { _ = srv.Shutdown(context.Background()) }() + time.Sleep(50 * time.Millisecond) + + serverURL := fmt.Sprintf("http://localhost:%d", listener.Addr().(*net.TCPAddr).Port) + + transport := &mcp.StreamableClientTransport{ + Endpoint: serverURL, + OAuthHandler: handler, + } + + client := mcp.NewClient(&mcp.Implementation{ + Name: "auth-test-client", + Version: "1.0.0", + }, nil) + + _, err = client.Connect(ctx, transport, nil) + require.Error(t, err, "Connect should fail because Authorize returns an error") + assert.Contains(t, err.Error(), "OAuth authorization required for "+serverName, + "Error should contain the server name and instruction to authenticate") + assert.Contains(t, err.Error(), "docker mcp oauth authorize", + "Error should instruct the user to run the authorize command") +} diff --git a/pkg/oauth/sdk_handler_test.go b/pkg/oauth/sdk_handler_test.go new file mode 100644 index 000000000..53c2967ec --- /dev/null +++ b/pkg/oauth/sdk_handler_test.go @@ -0,0 +1,114 @@ +package oauth + +import ( + "context" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "github.com/docker/docker-credential-helpers/credentials" + "github.com/modelcontextprotocol/go-sdk/auth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Verify SDKHandler satisfies the auth.OAuthHandler interface at compile time. +var _ auth.OAuthHandler = (*SDKHandler)(nil) + +func TestSDKHandler_TokenSource_NoToken(t *testing.T) { + handler := &SDKHandler{ + serverName: "test-server", + mode: ModeCE, + credHelper: &CredentialHelper{ + credentialHelper: newFakeCredentialHelper(), + mode: ModeCE, + }, + } + + ts, err := handler.TokenSource(context.Background()) + require.NoError(t, err) + assert.Nil(t, ts, "TokenSource should return nil when no token exists") +} + +func TestSDKHandler_TokenSource_WithToken(t *testing.T) { + fake := newFakeCredentialHelper() + + // Store a DCR client using the exact key format from dcr.Credentials: + // key: "https://{serverName}.mcp-dcr", username: "dcr_client" + dcrClientJSON, _ := json.Marshal(map[string]string{ + "serverName": "test-server", + "providerName": "test-server", + "clientId": "client123", + "authorizationEndpoint": "https://auth.example.com/authorize", + "tokenEndpoint": "https://auth.example.com/token", + }) + _ = fake.Add(&credentials.Credentials{ + ServerURL: "https://test-server.mcp-dcr", + Username: "dcr_client", + Secret: base64.StdEncoding.EncodeToString(dcrClientJSON), + }) + + // Store a token at the key the CE path expects: {authorizationEndpoint}/{providerName} + tokenJSON, _ := json.Marshal(map[string]string{ + "access_token": "my-access-token", + "token_type": "Bearer", + }) + _ = fake.Add(&credentials.Credentials{ + ServerURL: "https://auth.example.com/authorize/test-server", + Username: "oauth2_test-server", + Secret: base64.StdEncoding.EncodeToString(tokenJSON), + }) + + handler := &SDKHandler{ + serverName: "test-server", + mode: ModeCE, + credHelper: &CredentialHelper{ + credentialHelper: fake, + mode: ModeCE, + }, + } + + ts, err := handler.TokenSource(context.Background()) + require.NoError(t, err) + require.NotNil(t, ts, "TokenSource should return non-nil when token exists") + + tok, err := ts.Token() + require.NoError(t, err) + assert.Equal(t, "my-access-token", tok.AccessToken) + assert.Equal(t, "Bearer", tok.TokenType) +} + +func TestSDKHandler_Authorize_ReturnsError(t *testing.T) { + handler := NewSDKHandler("my-server", ModeCE) + + resp := &http.Response{ + StatusCode: http.StatusUnauthorized, + Body: io.NopCloser(strings.NewReader("Unauthorized")), + } + req, _ := http.NewRequestWithContext(context.Background(), http.MethodPost, "https://example.com/mcp", nil) + + err := handler.Authorize(context.Background(), req, resp) + require.Error(t, err) + assert.Contains(t, err.Error(), "my-server") + assert.Contains(t, err.Error(), "docker mcp oauth authorize") +} + +func TestSDKHandler_Authorize_NilBody(t *testing.T) { + handler := NewSDKHandler("my-server", ModeCE) + + resp := &http.Response{StatusCode: http.StatusForbidden} + err := handler.Authorize(context.Background(), nil, resp) + require.Error(t, err) + assert.Contains(t, err.Error(), "my-server") +} + +func TestSDKHandler_Authorize_NilResponse(t *testing.T) { + handler := NewSDKHandler("my-server", ModeCE) + + err := handler.Authorize(context.Background(), nil, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "my-server") +} diff --git a/scripts/npm-catalog-check/main.go b/scripts/npm-catalog-check/main.go new file mode 100644 index 000000000..7caa6de54 --- /dev/null +++ b/scripts/npm-catalog-check/main.go @@ -0,0 +1,313 @@ +// npm-catalog-check fetches all npm-only servers from the MCP community registry +// and runs TransformToDocker on each one, reporting pass/fail/skip counts. +// +// Usage: +// +// go run ./scripts/npm-catalog-check # all npm servers (no limit) +// go run ./scripts/npm-catalog-check -limit 20 # first 20 npm servers +// go run ./scripts/npm-catalog-check -verbose # print each server result +// go run ./scripts/npm-catalog-check -retries 5 # retry failed page fetches up to 5 times +package main + +import ( + "context" + "crypto/tls" + "encoding/json" + "flag" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "strings" + "time" + + v0 "github.com/modelcontextprotocol/registry/pkg/api/v0" + + "github.com/docker/mcp-gateway/pkg/catalog" +) + +type registryListResponse struct { + Servers []struct { + Server v0.ServerJSON `json:"server"` + } `json:"servers"` + Metadata struct { + NextCursor string `json:"nextCursor"` + Count int `json:"count"` + } `json:"metadata"` +} + +func main() { + limit := flag.Int("limit", 0, "max npm servers to check (0 = unlimited)") + verbose := flag.Bool("verbose", false, "print result for each server") + maxRetries := flag.Int("retries", 5, "max retries per page fetch on timeout/network error") + pageTimeout := flag.Duration("page-timeout", 60*time.Second, "HTTP timeout per page request") + flag.Parse() + + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) + defer cancel() + + // Explicit transport bypassing macOS system/Desktop proxy settings. + httpClient := &http.Client{ + Timeout: *pageTimeout, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{}, + DialContext: (&net.Dialer{Timeout: 15 * time.Second}).DialContext, + TLSHandshakeTimeout: 15 * time.Second, + // Keep connections alive across retries and pages. + MaxIdleConns: 10, + IdleConnTimeout: 90 * time.Second, + DisableKeepAlives: false, + }, + } + + // Also use a direct-connection resolver for npm version lookups. + npmResolver := catalog.NewNPMVersionResolver(httpClient) + + fmt.Println("Fetching npm-only servers from community registry...") + fmt.Printf("Settings: page-timeout=%s, retries=%d\n", *pageTimeout, *maxRetries) + + var ( + passed int + failed int + skipped int + total int + failedNames []string + skippedNames []string + ) + + cursor := "" + baseURL := "https://registry.modelcontextprotocol.io/v0/servers?version=latest&limit=100" + pagesRead := 0 + done := false + + for !done { + reqURL := baseURL + if cursor != "" { + reqURL += "&cursor=" + url.QueryEscape(cursor) + } + + body, err := fetchWithRetry(ctx, httpClient, reqURL, pagesRead+1, *maxRetries) + if err != nil { + fmt.Fprintf(os.Stderr, "Giving up on page %d after %d retries: %v\n", pagesRead+1, *maxRetries, err) + break + } + + var listResp registryListResponse + if err := json.Unmarshal(body, &listResp); err != nil { + fmt.Fprintf(os.Stderr, "Error parsing page %d: %v\n", pagesRead+1, err) + break + } + + pagesRead++ + + for _, s := range listResp.Servers { + // Filter to npm-only stdio servers (no OCI, no PyPI). + pkgs := s.Server.Packages + hasNPMStdio := false + hasOCI := false + hasPyPI := false + for _, p := range pkgs { + switch p.RegistryType { + case "oci": + hasOCI = true + case "pypi": + hasPyPI = true + case "npm": + if p.Transport.Type == "stdio" { + hasNPMStdio = true + } + } + } + if !hasNPMStdio || hasOCI || hasPyPI { + continue + } + + total++ + name := s.Server.Name + + if *limit > 0 && total > *limit { + done = true + total-- // don't count the one we skipped + break + } + + result, source, err := catalog.TransformToDocker(ctx, s.Server, + catalog.WithNPMResolver(npmResolver), + ) + if err != nil { + skipped++ + skippedNames = append(skippedNames, fmt.Sprintf("%s: %v", name, err)) + if *verbose { + fmt.Printf(" SKIP %-60s %v\n", name, err) + } + continue + } + + if source != catalog.TransformSourceNPM { + skipped++ + skippedNames = append(skippedNames, fmt.Sprintf("%s: resolved as %s", name, source)) + if *verbose { + fmt.Printf(" SKIP %-60s resolved as %s\n", name, source) + } + continue + } + + // Validate the transform output. + var problems []string + + if !strings.HasPrefix(result.Image, "node:") { + problems = append(problems, fmt.Sprintf("image %q doesn't start with node:", result.Image)) + } + if !strings.HasSuffix(result.Image, "-bookworm-slim") { + problems = append(problems, fmt.Sprintf("image %q doesn't end with -bookworm-slim", result.Image)) + } + if len(result.Command) < 3 { + problems = append(problems, fmt.Sprintf("command too short: %v", result.Command)) + } else { + if result.Command[0] != "npx" { + problems = append(problems, fmt.Sprintf("command[0]=%q, want npx", result.Command[0])) + } + if result.Command[1] != "--yes" { + problems = append(problems, fmt.Sprintf("command[1]=%q, want --yes", result.Command[1])) + } + } + if result.Type != "server" { + problems = append(problems, fmt.Sprintf("type=%q, want server", result.Type)) + } + if !result.LongLived { + problems = append(problems, "longLived=false, want true") + } + + hasNPMCache := false + for _, v := range result.Volumes { + if strings.Contains(v, ":/root/.npm") && strings.HasPrefix(v, "docker-mcp-npm-cache-") { + hasNPMCache = true + break + } + } + if !hasNPMCache { + problems = append(problems, fmt.Sprintf("no npm cache volume in %v", result.Volumes)) + } + + if result.Metadata == nil || result.Metadata.RegistryURL == "" { + problems = append(problems, "missing metadata.registryUrl") + } + + if len(problems) > 0 { + failed++ + failedNames = append(failedNames, fmt.Sprintf("%s: %s", name, strings.Join(problems, "; "))) + if *verbose { + fmt.Printf(" FAIL %-60s %s\n", name, strings.Join(problems, "; ")) + } + } else { + passed++ + if *verbose { + fmt.Printf(" PASS %-60s %s %v\n", name, result.Image, result.Command) + } + } + + // Progress indicator every 100 servers. + if total%100 == 0 { + fmt.Printf(" ... checked %d npm servers so far (%d passed, %d failed, %d skipped)\n", + total, passed, failed, skipped) + } + } + + if listResp.Metadata.NextCursor == "" || len(listResp.Servers) == 0 { + break + } + cursor = listResp.Metadata.NextCursor + + fmt.Printf(" Page %d done (%d npm servers found so far)\n", pagesRead, total) + } + + // Print summary. + fmt.Println() + fmt.Println("=== NPM Catalog Check Results ===") + fmt.Printf("Pages read: %d\n", pagesRead) + fmt.Printf("Total npm servers: %d\n", total) + fmt.Printf("Passed: %d\n", passed) + fmt.Printf("Failed: %d\n", failed) + fmt.Printf("Skipped (remote/err): %d\n", skipped) + + if passed+failed > 0 { + rate := float64(passed) / float64(passed+failed) * 100 + fmt.Printf("Success rate: %.1f%% (%d/%d transformable)\n", rate, passed, passed+failed) + } + + if len(failedNames) > 0 { + fmt.Println() + fmt.Println("--- Failed servers ---") + for _, f := range failedNames { + fmt.Printf(" %s\n", f) + } + } + + if len(skippedNames) > 0 && *verbose { + fmt.Println() + fmt.Println("--- Skipped servers ---") + for _, s := range skippedNames { + fmt.Printf(" %s\n", s) + } + } + + if failed > 0 { + os.Exit(1) + } +} + +// fetchWithRetry fetches a URL with exponential backoff on timeout/network errors. +// Returns the response body on success, or the last error after all retries are exhausted. +func fetchWithRetry(ctx context.Context, client *http.Client, reqURL string, pageNum int, maxRetries int) ([]byte, error) { + var lastErr error + for attempt := range maxRetries + 1 { + if attempt > 0 { + backoff := time.Duration(attempt) * 5 * time.Second + fmt.Printf(" Page %d: retry %d/%d after %s...\n", pageNum, attempt, maxRetries, backoff) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(backoff): + } + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + lastErr = fmt.Errorf("fetching page %d: %w", pageNum, err) + continue + } + + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + lastErr = fmt.Errorf("reading page %d: %w", pageNum, err) + continue + } + + if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 { + lastErr = fmt.Errorf("page %d returned %d: %s", pageNum, resp.StatusCode, truncate(string(body), 200)) + continue + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("page %d returned %d: %s", pageNum, resp.StatusCode, truncate(string(body), 200)) + } + + return body, nil + } + return nil, lastErr +} + +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "..." +} diff --git a/scripts/npm-catalog-count/main.go b/scripts/npm-catalog-count/main.go new file mode 100644 index 000000000..c84b5d101 --- /dev/null +++ b/scripts/npm-catalog-count/main.go @@ -0,0 +1,242 @@ +// npm-catalog-count does a raw count of all servers in the MCP community registry, +// broken down by package type, to reconcile against the context doc's numbers. +// +// Usage: go run ./scripts/npm-catalog-count/ +package main + +import ( + "context" + "crypto/tls" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "time" +) + +type registryListResponse struct { + Servers []struct { + Server serverJSON `json:"server"` + } `json:"servers"` + Metadata struct { + NextCursor string `json:"nextCursor"` + Count int `json:"count"` + } `json:"metadata"` +} + +type serverJSON struct { + Name string `json:"name"` + Packages []pkgJSON `json:"packages"` + Remotes []remoteJSON `json:"remotes"` +} + +type remoteJSON struct { + Type string `json:"type"` +} + +type pkgJSON struct { + RegistryType string `json:"registryType"` + Transport struct { + Type string `json:"type"` + } `json:"transport"` +} + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) + defer cancel() + + client := &http.Client{ + Timeout: 60 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{}, + DialContext: (&net.Dialer{Timeout: 15 * time.Second}).DialContext, + TLSHandshakeTimeout: 15 * time.Second, + }, + } + + cursor := "" + baseURL := "https://registry.modelcontextprotocol.io/v0/servers?version=latest&limit=100" + pagesRead := 0 + totalServers := 0 + + // Per-server counts (a server can appear in multiple categories). + var ( + hasNPM int // server has at least one npm package + hasNPMStdio int // server has at least one npm+stdio package + hasNPMRemote int // server has at least one npm+non-stdio package + hasOCI int + hasPyPI int + hasRemote int // server has a non-npm/oci/pypi package or remote transport + npmOnly int // npm+stdio, no OCI, no PyPI + npmOnlyStrict int // npm+stdio, no OCI, no PyPI, no remote/sse transport on any package + ) + + for { + reqURL := baseURL + if cursor != "" { + reqURL += "&cursor=" + url.QueryEscape(cursor) + } + + body, err := fetchWithRetry(ctx, client, reqURL, pagesRead+1, 5) + if err != nil { + fmt.Fprintf(os.Stderr, "Failed page %d: %v\n", pagesRead+1, err) + break + } + + var listResp registryListResponse + if err := json.Unmarshal(body, &listResp); err != nil { + fmt.Fprintf(os.Stderr, "Parse error page %d: %v\n", pagesRead+1, err) + break + } + + pagesRead++ + + for _, s := range listResp.Servers { + totalServers++ + + serverHasNPM := false + serverHasNPMStdio := false + serverHasNPMRemote := false + serverHasOCI := false + serverHasPyPI := false + serverHasRemote := false + + for _, p := range s.Server.Packages { + switch p.RegistryType { + case "npm": + serverHasNPM = true + if p.Transport.Type == "stdio" { + serverHasNPMStdio = true + } else { + serverHasNPMRemote = true + } + case "oci": + serverHasOCI = true + case "pypi": + serverHasPyPI = true + default: + serverHasRemote = true + } + // Also count non-stdio transports as "remote-like" + if p.Transport.Type != "stdio" && p.Transport.Type != "" { + serverHasRemote = true + } + } + + // Count remotes field too. + if len(s.Server.Remotes) > 0 { + serverHasRemote = true + } + + if serverHasNPM { + hasNPM++ + } + if serverHasNPMStdio { + hasNPMStdio++ + } + if serverHasNPMRemote { + hasNPMRemote++ + } + if serverHasOCI { + hasOCI++ + } + if serverHasPyPI { + hasPyPI++ + } + if serverHasRemote { + hasRemote++ + } + if serverHasNPMStdio && !serverHasOCI && !serverHasPyPI { + npmOnly++ + } + if serverHasNPMStdio && !serverHasOCI && !serverHasPyPI && !serverHasRemote { + npmOnlyStrict++ + } + } + + if pagesRead%10 == 0 { + fmt.Printf(" ... page %d, %d servers so far\n", pagesRead, totalServers) + } + + if listResp.Metadata.NextCursor == "" || len(listResp.Servers) == 0 { + break + } + cursor = listResp.Metadata.NextCursor + } + + fmt.Println() + fmt.Println("=== Community Registry Breakdown ===") + fmt.Printf("Pages read: %d\n", pagesRead) + fmt.Printf("Total servers: %d\n", totalServers) + fmt.Println() + fmt.Println("--- Servers with package type ---") + fmt.Printf("Has npm (any transport): %d (%.1f%%)\n", hasNPM, pct(hasNPM, totalServers)) + fmt.Printf("Has npm+stdio: %d (%.1f%%)\n", hasNPMStdio, pct(hasNPMStdio, totalServers)) + fmt.Printf("Has npm+non-stdio (SSE/streaming): %d (%.1f%%)\n", hasNPMRemote, pct(hasNPMRemote, totalServers)) + fmt.Printf("Has OCI: %d (%.1f%%)\n", hasOCI, pct(hasOCI, totalServers)) + fmt.Printf("Has PyPI: %d (%.1f%%)\n", hasPyPI, pct(hasPyPI, totalServers)) + fmt.Printf("Has remote/SSE transport: %d (%.1f%%)\n", hasRemote, pct(hasRemote, totalServers)) + fmt.Println() + fmt.Println("--- npm-only (our target) ---") + fmt.Printf("npm+stdio, no OCI, no PyPI: %d (script filter)\n", npmOnly) + fmt.Printf("npm+stdio, no OCI/PyPI/remote: %d (strict: no remote fallback)\n", npmOnlyStrict) + fmt.Println() + fmt.Printf("Context doc claimed: 8387 npm total, 8167 npm-only\n") + fmt.Printf("Delta (total npm): %d\n", hasNPM-8387) + fmt.Printf("Delta (npm-only): %d\n", npmOnly-8167) +} + +func pct(n, total int) float64 { + if total == 0 { + return 0 + } + return float64(n) / float64(total) * 100 +} + +func fetchWithRetry(ctx context.Context, client *http.Client, reqURL string, pageNum int, maxRetries int) ([]byte, error) { + var lastErr error + for attempt := range maxRetries + 1 { + if attempt > 0 { + backoff := time.Duration(attempt) * 5 * time.Second + fmt.Printf(" Page %d: retry %d/%d after %s...\n", pageNum, attempt, maxRetries, backoff) + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(backoff): + } + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + lastErr = fmt.Errorf("fetching page %d: %w", pageNum, err) + continue + } + + body, err := io.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + lastErr = fmt.Errorf("reading page %d: %w", pageNum, err) + continue + } + + if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 { + lastErr = fmt.Errorf("page %d returned %d: %s", pageNum, resp.StatusCode, string(body[:min(200, len(body))])) + continue + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("page %d returned %d: %s", pageNum, resp.StatusCode, string(body[:min(200, len(body))])) + } + + return body, nil + } + return nil, lastErr +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go index 36ff259e9..40fa259fb 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/auth/auth.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "net/http" "slices" "strings" @@ -73,8 +74,17 @@ func RequireBearerToken(verifier TokenVerifier, opts *RequireBearerTokenOptions) tokenInfo, errmsg, code := verify(r, verifier, opts) if code != 0 { if code == http.StatusUnauthorized || code == http.StatusForbidden { - if opts != nil && opts.ResourceMetadataURL != "" { - w.Header().Add("WWW-Authenticate", "Bearer resource_metadata="+opts.ResourceMetadataURL) + if opts != nil { + var params []string + if opts.ResourceMetadataURL != "" { + params = append(params, fmt.Sprintf("resource_metadata=%q", opts.ResourceMetadataURL)) + } + if len(opts.Scopes) > 0 { + params = append(params, fmt.Sprintf("scope=%q", strings.Join(opts.Scopes, " "))) + } + if len(params) > 0 { + w.Header().Add("WWW-Authenticate", "Bearer "+strings.Join(params, ", ")) + } } } http.Error(w, errmsg, code) diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go index 1190836e0..846feb7b9 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/auth/authorization_code.go @@ -2,8 +2,6 @@ // Use of this source code is governed by the license // that can be found in the LICENSE file. -//go:build mcp_go_client_oauth - package auth import ( @@ -11,6 +9,7 @@ import ( "crypto/rand" "errors" "fmt" + "io" "net/http" "net/url" "slices" @@ -20,18 +19,6 @@ import ( "golang.org/x/oauth2" ) -// ClientSecretAuthConfig is used to configure client authentication using client_secret. -// Authentication method will be selected based on the authorization server's supported methods, -// according to the following preference order: -// 1. client_secret_post -// 2. client_secret_basic -type ClientSecretAuthConfig struct { - // ClientID is the client ID to be used for client authentication. - ClientID string - // ClientSecret is the client secret to be used for client authentication. - ClientSecret string -} - // ClientIDMetadataDocumentConfig is used to configure the Client ID Metadata Document // based client registration per // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#client-id-metadata-documents. @@ -42,14 +29,6 @@ type ClientIDMetadataDocumentConfig struct { URL string } -// PreregisteredClientConfig is used to configure a pre-registered client per -// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#preregistration. -// Currently only "client_secret_basic" and "client_secret_post" authentication methods are supported. -type PreregisteredClientConfig struct { - // ClientSecretAuthConfig is the client_secret based configuration to be used for client authentication. - ClientSecretAuthConfig *ClientSecretAuthConfig -} - // DynamicClientRegistrationConfig is used to configure dynamic client registration per // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#dynamic-client-registration. type DynamicClientRegistrationConfig struct { @@ -67,12 +46,18 @@ type AuthorizationResult struct { State string } -// AuthorizationArgs is the input to [AuthorizationCodeHandlerConfig].AuthorizationCodeFetcher. +// AuthorizationArgs is the input to [AuthorizationCodeFetcher]. type AuthorizationArgs struct { // Authorization URL to be opened in a browser for the user to start the authorization process. URL string } +// AuthorizationCodeFetcher is called to initiate the OAuth authorization flow. +// It is responsible for directing the user to the authorization URL (e.g., opening +// in a browser) and returning the authorization code and state once the Authorization +// Server redirects back to the configured RedirectURL. +type AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) + // AuthorizationCodeHandlerConfig is the configuration for [AuthorizationCodeHandler]. type AuthorizationCodeHandlerConfig struct { // Client registration configuration. @@ -82,7 +67,7 @@ type AuthorizationCodeHandlerConfig struct { // 3. Dynamic Client Registration // At least one method must be configured. ClientIDMetadataDocumentConfig *ClientIDMetadataDocumentConfig - PreregisteredClientConfig *PreregisteredClientConfig + PreregisteredClient *oauthex.ClientCredentials DynamicClientRegistrationConfig *DynamicClientRegistrationConfig // RedirectURL is a required URL to redirect to after authorization. @@ -97,10 +82,8 @@ type AuthorizationCodeHandlerConfig struct { RedirectURL string // AuthorizationCodeFetcher is a required function called to initiate the authorization flow. - // It is responsible for opening the URL in a browser for the user to start the authorization process. - // It should return the authorization code and state once the Authorization Server - // redirects back to the RedirectURL. - AuthorizationCodeFetcher func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) + // See [AuthorizationCodeFetcher] for details. + AuthorizationCodeFetcher AuthorizationCodeFetcher // Client is an optional HTTP client to use for HTTP requests. // It is used for the following requests: @@ -127,8 +110,6 @@ type AuthorizationCodeHandler struct { var _ OAuthHandler = (*AuthorizationCodeHandler)(nil) -func (h *AuthorizationCodeHandler) isOAuthHandler() {} - func (h *AuthorizationCodeHandler) TokenSource(ctx context.Context) (oauth2.TokenSource, error) { return h.tokenSource, nil } @@ -141,7 +122,7 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho return nil, errors.New("config must be provided") } if config.ClientIDMetadataDocumentConfig == nil && - config.PreregisteredClientConfig == nil && + config.PreregisteredClient == nil && config.DynamicClientRegistrationConfig == nil { return nil, errors.New("at least one client registration configuration must be provided") } @@ -151,19 +132,15 @@ func NewAuthorizationCodeHandler(config *AuthorizationCodeHandlerConfig) (*Autho if config.ClientIDMetadataDocumentConfig != nil && !isNonRootHTTPSURL(config.ClientIDMetadataDocumentConfig.URL) { return nil, fmt.Errorf("client ID metadata document URL must be a non-root HTTPS URL") } - preCfg := config.PreregisteredClientConfig - if preCfg != nil { - if preCfg.ClientSecretAuthConfig == nil { - return nil, errors.New("ClientSecretAuthConfig is required for pre-registered client") - } - if preCfg.ClientSecretAuthConfig.ClientID == "" || preCfg.ClientSecretAuthConfig.ClientSecret == "" { - return nil, fmt.Errorf("pre-registered client ID or secret is empty") + if config.PreregisteredClient != nil { + if err := config.PreregisteredClient.Validate(); err != nil { + return nil, fmt.Errorf("invalid PreregisteredClient configuration: %w", err) } } dCfg := config.DynamicClientRegistrationConfig if dCfg != nil { if dCfg.Metadata == nil { - return nil, errors.New("Metadata is required for dynamic client registration") + return nil, errors.New("dynamic client registration requires non-nil Metadata") } if len(dCfg.Metadata.RedirectURIs) == 0 { return nil, errors.New("Metadata.RedirectURIs is required for dynamic client registration") @@ -198,6 +175,7 @@ func isNonRootHTTPSURL(u string) bool { // On success, [AuthorizationCodeHandler.TokenSource] will return a token source with the fetched token. func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Request, resp *http.Response) error { defer resp.Body.Close() + defer io.Copy(io.Discard, resp.Body) wwwChallenges, err := oauthex.ParseWWWAuthenticate(resp.Header[http.CanonicalHeaderKey("WWW-Authenticate")]) if err != nil { @@ -218,9 +196,20 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ return err } - asm, err := h.getAuthServerMetadata(ctx, prm) + asm, err := GetAuthServerMetadata(ctx, prm.AuthorizationServers[0], h.config.Client) if err != nil { - return err + return fmt.Errorf("failed to get authorization server metadata: %w", err) + } + if asm == nil { + // Fallback to 2025-03-26 spec: predefined endpoints. + // https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#fallbacks-for-servers-without-metadata-discovery + authServerURL := prm.AuthorizationServers[0] + asm = &oauthex.AuthServerMeta{ + Issuer: authServerURL, + AuthorizationEndpoint: authServerURL + "/authorize", + TokenEndpoint: authServerURL + "/token", + RegistrationEndpoint: authServerURL + "/register", + } } resolvedClientConfig, err := h.handleRegistration(ctx, asm) @@ -246,7 +235,7 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ Scopes: scps, } - authRes, err := h.getAuthorizationCode(ctx, cfg, req.URL.String()) + authRes, err := h.getAuthorizationCode(ctx, cfg, prm.Resource) if err != nil { // Purposefully leaving the error unwrappable so it can be handled by the caller. return err @@ -292,22 +281,34 @@ func errorFromChallenges(cs []oauthex.Challenge) string { // If no metadata was found or the fetched metadata fails security checks, // it returns an error. func (h *AuthorizationCodeHandler) getProtectedResourceMetadata(ctx context.Context, wwwChallenges []oauthex.Challenge, mcpServerURL string) (*oauthex.ProtectedResourceMetadata, error) { - var errs []error // Use MCP server URL as the resource URI per // https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#canonical-server-uri. for _, url := range protectedResourceMetadataURLs(resourceMetadataURLFromChallenges(wwwChallenges), mcpServerURL) { prm, err := oauthex.GetProtectedResourceMetadata(ctx, url.URL, url.Resource, h.config.Client) if err != nil { - errs = append(errs, err) continue } if prm == nil { - errs = append(errs, fmt.Errorf("protected resource metadata is nil")) continue } + if len(prm.AuthorizationServers) == 0 { + // If we found PRM, we enforce the 2025-11-25 spec and not search further. + return nil, fmt.Errorf("protected resource metadata has no authorization servers specified") + } return prm, nil } - return nil, fmt.Errorf("failed to get protected resource metadata: %v", errors.Join(errs...)) + // Fallback to 2025-03-26 spec MCP server root is the Authorization Server: + // https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#server-metadata-discovery + u, err := url.Parse(mcpServerURL) + if err != nil { + return nil, fmt.Errorf("failed to parse MCP server URL: %v", err) + } + u.Path = "" + prm := &oauthex.ProtectedResourceMetadata{ + AuthorizationServers: []string{u.String()}, + Resource: mcpServerURL, + } + return prm, nil } type prmURL struct { @@ -350,82 +351,6 @@ func protectedResourceMetadataURLs(metadataURL, resourceURL string) []prmURL { return urls } -// getAuthServerMetadata returns the authorization server metadata. -// The provided Protected Resource Metadata must not be nil. -// It returns an error if the metadata request fails with non-4xx HTTP status code -// or the fetched metadata fails security checks. -// If no metadata was found, it returns a minimal set of endpoints -// as a fallback to 2025-03-26 spec. -func (h *AuthorizationCodeHandler) getAuthServerMetadata(ctx context.Context, prm *oauthex.ProtectedResourceMetadata) (*oauthex.AuthServerMeta, error) { - var authServerURL string - if len(prm.AuthorizationServers) > 0 { - // Use the first authorization server, similarly to other SDKs. - authServerURL = prm.AuthorizationServers[0] - } else { - // Fallback to 2025-03-26 spec: MCP server base URL acts as Authorization Server. - authURL, err := url.Parse(prm.Resource) - if err != nil { - return nil, fmt.Errorf("failed to parse resource URL: %v", err) - } - authURL.Path = "" - authServerURL = authURL.String() - } - - for _, u := range authorizationServerMetadataURLs(authServerURL) { - asm, err := oauthex.GetAuthServerMeta(ctx, u, authServerURL, h.config.Client) - if err != nil { - return nil, fmt.Errorf("failed to get authorization server metadata: %w", err) - } - if asm != nil { - return asm, nil - } - } - - // Fallback to 2025-03-26 spec: predefined endpoints. - // https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization#fallbacks-for-servers-without-metadata-discovery - asm := &oauthex.AuthServerMeta{ - Issuer: authServerURL, - AuthorizationEndpoint: authServerURL + "/authorize", - TokenEndpoint: authServerURL + "/token", - RegistrationEndpoint: authServerURL + "/register", - } - return asm, nil -} - -// authorizationServerMetadataURLs returns a list of URLs to try when looking for -// authorization server metadata as mandated by the MCP specification: -// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-metadata-discovery. -func authorizationServerMetadataURLs(issuerURL string) []string { - var urls []string - - baseURL, err := url.Parse(issuerURL) - if err != nil { - return nil - } - - if baseURL.Path == "" { - // "OAuth 2.0 Authorization Server Metadata". - baseURL.Path = "/.well-known/oauth-authorization-server" - urls = append(urls, baseURL.String()) - // "OpenID Connect Discovery 1.0". - baseURL.Path = "/.well-known/openid-configuration" - urls = append(urls, baseURL.String()) - return urls - } - - originalPath := baseURL.Path - // "OAuth 2.0 Authorization Server Metadata with path insertion". - baseURL.Path = "/.well-known/oauth-authorization-server/" + strings.TrimLeft(originalPath, "/") - urls = append(urls, baseURL.String()) - // "OpenID Connect Discovery 1.0 with path insertion". - baseURL.Path = "/.well-known/openid-configuration/" + strings.TrimLeft(originalPath, "/") - urls = append(urls, baseURL.String()) - // "OpenID Connect Discovery 1.0 with path appending". - baseURL.Path = "/" + strings.Trim(originalPath, "/") + "/.well-known/openid-configuration" - urls = append(urls, baseURL.String()) - return urls -} - type registrationType int const ( @@ -488,13 +413,17 @@ func (h *AuthorizationCodeHandler) handleRegistration(ctx context.Context, asm * }, nil } // 2. Attempt to use pre-registered client configuration. - pCfg := h.config.PreregisteredClientConfig - if pCfg != nil { + preCfg := h.config.PreregisteredClient + if preCfg != nil { authStyle := selectTokenAuthMethod(asm.TokenEndpointAuthMethodsSupported) + clientSecret := "" + if preCfg.ClientSecretAuth != nil { + clientSecret = preCfg.ClientSecretAuth.ClientSecret + } return &resolvedClientConfig{ registrationType: registrationTypePreregistered, - clientID: pCfg.ClientSecretAuthConfig.ClientID, - clientSecret: pCfg.ClientSecretAuthConfig.ClientSecret, + clientID: preCfg.ClientID, + clientSecret: clientSecret, authStyle: authStyle, }, nil } diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go index 0af6963fc..db32d97a1 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/auth/client.go @@ -22,8 +22,6 @@ import ( // [github.com/modelcontextprotocol/go-sdk/mcp.StreamableClientTransport] // for an example. type OAuthHandler interface { - isOAuthHandler() - // TokenSource returns a token source to be used for outgoing requests. // Returned token source might be nil. In that case, the transport will not // add any authorization headers to the request. diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/client_private.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/client_private.go deleted file mode 100644 index 767c59eea..000000000 --- a/vendor/github.com/modelcontextprotocol/go-sdk/auth/client_private.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2025 The Go MCP SDK Authors. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -//go:build mcp_go_client_oauth - -package auth - -import ( - "bytes" - "errors" - "io" - "net/http" - "sync" - - "golang.org/x/oauth2" -) - -// An OAuthHandlerLegacy conducts an OAuth flow and returns a [oauth2.TokenSource] if the authorization -// is approved, or an error if not. -// The handler receives the HTTP request and response that triggered the authentication flow. -// To obtain the protected resource metadata, call [oauthex.GetProtectedResourceMetadataFromHeader]. -// -// Deprecated: Please use the new [OAuthHandler] abstraction that is built -// into the streamable transport. This struct will be removed in v1.5.0. -type OAuthHandlerLegacy func(req *http.Request, res *http.Response) (oauth2.TokenSource, error) - -// HTTPTransport is an [http.RoundTripper] that follows the MCP -// OAuth protocol when it encounters a 401 Unauthorized response. -// -// Deprecated: Please use the new [OAuthHandler] abstraction that is built -// into the streamable transport. This struct will be removed in v1.5.0. -type HTTPTransport struct { - handler OAuthHandlerLegacy - mu sync.Mutex // protects opts.Base - opts HTTPTransportOptions -} - -// NewHTTPTransport returns a new [*HTTPTransport]. -// The handler is invoked when an HTTP request results in a 401 Unauthorized status. -// It is called only once per transport. Once a TokenSource is obtained, it is used -// for the lifetime of the transport; subsequent 401s are not processed. -// -// Deprecated: Please use the new [OAuthHandler] abstraction that is built -// into the streamable transport. This struct will be removed in v1.5.0. -func NewHTTPTransport(handler OAuthHandlerLegacy, opts *HTTPTransportOptions) (*HTTPTransport, error) { - if handler == nil { - return nil, errors.New("handler cannot be nil") - } - t := &HTTPTransport{ - handler: handler, - } - if opts != nil { - t.opts = *opts - } - if t.opts.Base == nil { - t.opts.Base = http.DefaultTransport - } - return t, nil -} - -// HTTPTransportOptions are options to [NewHTTPTransport]. -// -// Deprecated: Please use the new [OAuthHandler] abstraction that is built -// into the streamable transport. This struct will be removed in v1.5.0. -type HTTPTransportOptions struct { - // Base is the [http.RoundTripper] to use. - // If nil, [http.DefaultTransport] is used. - Base http.RoundTripper -} - -func (t *HTTPTransport) RoundTrip(req *http.Request) (*http.Response, error) { - t.mu.Lock() - base := t.opts.Base - t.mu.Unlock() - - var ( - // If haveBody is set, the request has a nontrivial body, and we need avoid - // reading (or closing) it multiple times. In that case, bodyBytes is its - // content. - haveBody bool - bodyBytes []byte - ) - if req.Body != nil && req.Body != http.NoBody { - // if we're setting Body, we must mutate first. - req = req.Clone(req.Context()) - haveBody = true - var err error - bodyBytes, err = io.ReadAll(req.Body) - if err != nil { - return nil, err - } - // Now that we've read the request body, http.RoundTripper requires that we - // close it. - req.Body.Close() // ignore error - req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - } - - resp, err := base.RoundTrip(req) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusUnauthorized { - return resp, nil - } - if _, ok := base.(*oauth2.Transport); ok { - // We failed to authorize even with a token source; give up. - return resp, nil - } - - resp.Body.Close() - // Try to authorize. - t.mu.Lock() - defer t.mu.Unlock() - // If we don't have a token source, get one by following the OAuth flow. - // (We may have obtained one while t.mu was not held above.) - // TODO: We hold the lock for the entire OAuth flow. This could be a long - // time. Is there a better way? - if _, ok := t.opts.Base.(*oauth2.Transport); !ok { - ts, err := t.handler(req, resp) - if err != nil { - return nil, err - } - t.opts.Base = &oauth2.Transport{Base: t.opts.Base, Source: ts} - } - - // If we don't have a body, the request is reusable, though it will be cloned - // by the base. However, if we've had to read the body, we must clone. - if haveBody { - req = req.Clone(req.Context()) - req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) - } - - return t.opts.Base.RoundTrip(req) -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/auth/shared.go b/vendor/github.com/modelcontextprotocol/go-sdk/auth/shared.go new file mode 100644 index 000000000..8c5e10cfe --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/auth/shared.go @@ -0,0 +1,69 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +// This file contains shared utilities for OAuth handlers. + +package auth + +import ( + "context" + "net/http" + "net/url" + "strings" + + "github.com/modelcontextprotocol/go-sdk/oauthex" +) + +// GetAuthServerMetadata fetches authorization server metadata for the given issuer URL. +// It tries standard well-known endpoints (OAuth 2.0 and OIDC) and returns the first successful result. +// +// Returns (nil, nil) when no metadata endpoints respond (404s), allowing callers to implement +// fallback logic. Returns an error for any non-client error (network failures, invalid JSON, etc.). +func GetAuthServerMetadata(ctx context.Context, issuerURL string, httpClient *http.Client) (*oauthex.AuthServerMeta, error) { + for _, metadataURL := range authorizationServerMetadataURLs(issuerURL) { + asm, err := oauthex.GetAuthServerMeta(ctx, metadataURL, issuerURL, httpClient) + if err != nil { + return nil, err + } + if asm != nil { + return asm, nil + } + } + return nil, nil +} + +// authorizationServerMetadataURLs returns a list of URLs to try when looking for +// authorization server metadata as mandated by the MCP specification: +// https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#authorization-server-metadata-discovery. +func authorizationServerMetadataURLs(issuerURL string) []string { + var urls []string + + baseURL, err := url.Parse(issuerURL) + if err != nil { + return nil + } + + if baseURL.Path == "" { + // "OAuth 2.0 Authorization Server Metadata". + baseURL.Path = "/.well-known/oauth-authorization-server" + urls = append(urls, baseURL.String()) + // "OpenID Connect Discovery 1.0". + baseURL.Path = "/.well-known/openid-configuration" + urls = append(urls, baseURL.String()) + return urls + } + + originalPath := baseURL.Path + // "OAuth 2.0 Authorization Server Metadata with path insertion". + baseURL.Path = "/.well-known/oauth-authorization-server/" + strings.TrimLeft(originalPath, "/") + urls = append(urls, baseURL.String()) + // "OpenID Connect Discovery 1.0 with path insertion". + baseURL.Path = "/.well-known/openid-configuration/" + strings.TrimLeft(originalPath, "/") + urls = append(urls, baseURL.String()) + // "OpenID Connect Discovery 1.0 with path appending". + baseURL.Path = "/" + strings.Trim(originalPath, "/") + "/.well-known/openid-configuration" + urls = append(urls, baseURL.String()) + + return urls +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/internal/json/json.go b/vendor/github.com/modelcontextprotocol/go-sdk/internal/json/json.go index 1148770e3..b4c897fa8 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/internal/json/json.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/internal/json/json.go @@ -8,12 +8,25 @@ package json import ( "bytes" + "io" "github.com/segmentio/encoding/json" ) -func Unmarshal(data []byte, v any) error { - dec := json.NewDecoder(bytes.NewReader(data)) +type Decoder struct { + dec *json.Decoder +} + +func NewDecoder(r io.Reader) *Decoder { + dec := json.NewDecoder(r) dec.DontMatchCaseInsensitiveStructFields() - return dec.Decode(v) + return &Decoder{dec: dec} +} + +func (d *Decoder) Decode(v any) error { + return d.dec.Decode(v) +} + +func Unmarshal(data []byte, v any) error { + return NewDecoder(bytes.NewReader(data)).Decode(v) } diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/client.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/client.go index 74900b1c7..dc7bef1c5 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/client.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/client.go @@ -641,7 +641,7 @@ func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult, } err = resolved.ApplyDefaults(&res.Content) if err != nil { - return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("failed to apply schema defalts to elicitation result: %v", err)} + return nil, &jsonrpc.Error{Code: jsonrpc.CodeInvalidParams, Message: fmt.Sprintf("failed to apply schema defaults to elicitation result: %v", err)} } } return res, nil diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/event.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/event.go index 62dd2ad2b..41ff4aef2 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/event.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/event.go @@ -41,7 +41,7 @@ func (e Event) Empty() bool { } // writeEvent writes the event to w, and flushes. -func writeEvent(w io.Writer, evt Event) (int, error) { +func writeEvent(w http.ResponseWriter, evt Event) (int, error) { var b bytes.Buffer if evt.Name != "" { fmt.Fprintf(&b, "event: %s\n", evt.Name) @@ -54,9 +54,9 @@ func writeEvent(w io.Writer, evt Event) (int, error) { } fmt.Fprintf(&b, "data: %s\n\n", string(evt.Data)) n, err := w.Write(b.Bytes()) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } + rc := http.NewResponseController(w) + // Ignore returned error as flushing is best-effort. + _ = rc.Flush() return n, err } @@ -376,10 +376,11 @@ func (s *MemoryEventStore) purge() { for _, dl := range sm { if dl.size > 0 { r := dl.removeFirst() - if r > 0 { - changed = true - s.nBytes -= r - } + // Even if we remove an empty chunk, that's + // still progress. There may be non-empty + // chunks after it. + changed = true + s.nBytes -= r } } } diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/protocol.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/protocol.go index 837ce7843..0b6bf4704 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/protocol.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/protocol.go @@ -4,12 +4,6 @@ package mcp -// Protocol types for version 2025-06-18. -// To see the schema changes from the previous version, run: -// -// prefix=https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/refs/heads/main/schema -// sdiff -l <(curl $prefix/2025-03-26/schema.ts) <(curl $prefix/2025/06-18/schema.ts) - import ( "encoding/json" "fmt" @@ -1226,7 +1220,7 @@ type ToolChoice struct { // ElicitationCapabilities describes the capabilities for elicitation. // -// If neither Form nor URL is set, the 'Form' capabilitiy is assumed. +// If neither Form nor URL is set, the 'Form' capability is assumed. type ElicitationCapabilities struct { Form *FormElicitationCapabilities `json:"form,omitempty"` URL *URLElicitationCapabilities `json:"url,omitempty"` @@ -1558,7 +1552,7 @@ type ServerCapabilities struct { Logging *LoggingCapabilities `json:"logging,omitempty"` // Prompts is present if the server supports prompts. Prompts *PromptCapabilities `json:"prompts,omitempty"` - // Resources is present if the server supports resourcs. + // Resources is present if the server supports resources. Resources *ResourceCapabilities `json:"resources,omitempty"` // Tools is present if the supports tools. Tools *ToolCapabilities `json:"tools,omitempty"` diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/server.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/server.go index e3c03e278..97b5d4462 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/server.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/server.go @@ -321,15 +321,18 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out], cache *SchemaCa var err error input, err = applySchema(input, inputResolved) if err != nil { - // TODO(#450): should this be considered a tool error? (and similar below) - return nil, fmt.Errorf("%w: validating \"arguments\": %v", jsonrpc2.ErrInvalidParams, err) + var errRes CallToolResult + errRes.SetError(fmt.Errorf("validating \"arguments\": %v", err)) + return &errRes, nil } // Unmarshal and validate args. var in In if input != nil { if err := internaljson.Unmarshal(input, &in); err != nil { - return nil, fmt.Errorf("%w: %v", jsonrpc2.ErrInvalidParams, err) + var errRes CallToolResult + errRes.SetError(err) + return &errRes, nil } } @@ -628,6 +631,14 @@ func (s *Server) changeAndNotify(notification string, change func() bool) { s.mu.Lock() defer s.mu.Unlock() if change() && s.shouldSendListChangedNotification(notification) { + if len(s.sessions) == 0 { + if t := s.pendingNotifications[notification]; t != nil { + t.Stop() + s.pendingNotifications[notification] = nil + } + return + } + // Reset the outstanding delayed call, if any. if t := s.pendingNotifications[notification]; t == nil { s.pendingNotifications[notification] = time.AfterFunc(notificationDelay, func() { s.notifySessions(notification) }) @@ -834,7 +845,7 @@ func (s *Server) lookupResourceHandler(uri string) (ResourceHandler, string, boo // and the current working directory is unavailable, fileResourceHandler panics. // // Lexical path traversal attacks, where the path has ".." elements that escape dir, -// are always caught. Go 1.24 and above also protects against symlink-based attacks, +// are always caught. The SDK also protects against symlink-based attacks, // where symlinks under dir lead out of the tree. func fileResourceHandler(dir string) ResourceHandler { // Convert dir to an absolute path. @@ -1285,7 +1296,7 @@ func (ss *ServerSession) Elicit(ctx context.Context, params *ElicitParams) (*Eli } err = resolved.ApplyDefaults(&res.Content) if err != nil { - return nil, fmt.Errorf("failed to apply schema defalts to elicitation result: %v", err) + return nil, fmt.Errorf("failed to apply schema defaults to elicitation result: %v", err) } return res, nil diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/shared.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/shared.go index bda00c203..5662b121f 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/shared.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/shared.go @@ -34,8 +34,8 @@ const ( // // It is the version that the client sends in the initialization request, and // the default version used by the server. - latestProtocolVersion = protocolVersion20250618 - protocolVersion20251125 = "2025-11-25" // not yet released + latestProtocolVersion = protocolVersion20251125 + protocolVersion20251125 = "2025-11-25" protocolVersion20250618 = "2025-06-18" protocolVersion20250326 = "2025-03-26" protocolVersion20241105 = "2024-11-05" @@ -447,6 +447,7 @@ func setProgressToken(p Params, pt any) { m := p.GetMeta() if m == nil { m = map[string]any{} + p.SetMeta(m) } m[progressTokenKey] = pt } diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable.go index 16bca0702..b0b17956b 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/streamable.go @@ -26,6 +26,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/modelcontextprotocol/go-sdk/auth" @@ -96,7 +97,7 @@ func (i *sessionInfo) startPOST() { i.refs++ } -// endPOST sigals that a request for this session is ending, starting the +// endPOST signals that a request for this session is ending, starting the // timeout if there are no other requests running. func (i *sessionInfo) endPOST() { if i.timeout <= 0 { @@ -273,19 +274,7 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque // Allow multiple 'Accept' headers. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept#syntax - accept := strings.Split(strings.Join(req.Header.Values("Accept"), ","), ",") - var jsonOK, streamOK bool - for _, c := range accept { - switch strings.TrimSpace(c) { - case "application/json", "application/*": - jsonOK = true - case "text/event-stream", "text/*": - streamOK = true - case "*/*": - jsonOK = true - streamOK = true - } - } + jsonOK, streamOK := streamableAccepts(req.Header.Values("Accept")) if req.Method == http.MethodGet { if !streamOK { @@ -550,6 +539,26 @@ func (h *StreamableHTTPHandler) ServeHTTP(w http.ResponseWriter, req *http.Reque sessInfo.transport.ServeHTTP(w, req) } +func streamableAccepts(values []string) (jsonOK, streamOK bool) { + for _, value := range values { + for _, raw := range strings.Split(value, ",") { + token := strings.TrimSpace(raw) + // Ignore Accept parameters like ";charset=utf-8"; match the base media type. + base, _, _ := strings.Cut(token, ";") + switch strings.ToLower(strings.TrimSpace(base)) { + case "application/json", "application/*": + jsonOK = true + case "text/event-stream", "text/*": + streamOK = true + case "*/*": + jsonOK = true + streamOK = true + } + } + } + return jsonOK, streamOK +} + // A StreamableServerTransport implements the server side of the MCP streamable // transport. // @@ -1044,9 +1053,9 @@ func (c *streamableServerConn) acquireStream(ctx context.Context, w http.Respons // Issue #410: the standalone SSE stream is likely not to receive messages // for a long time. Ensure that headers are flushed. w.WriteHeader(http.StatusOK) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } + rc := http.NewResponseController(w) + // Ignore returned error as flushing is best-effort. + _ = rc.Flush() } for _, data := range toReplay { @@ -1202,7 +1211,7 @@ func (c *streamableServerConn) servePOST(w http.ResponseWriter, req *http.Reques // // Create a logical stream to track its responses. // Important: don't publish the incoming messages until the stream is - // registered, as the server may attempt to respond to imcoming messages as + // registered, as the server may attempt to respond to incoming messages as // soon as they're published. stream, err := c.newStream(req.Context(), calls, crand.Text()) if err != nil { @@ -1515,9 +1524,13 @@ var ( // reconnectInitialDelay is the base delay for the first reconnect attempt. // // Mutable for testing. - reconnectInitialDelay = 1 * time.Second + reconnectInitialDelay atomic.Int64 ) +func init() { + reconnectInitialDelay.Store(int64(1 * time.Second)) +} + // Connect implements the [Transport] interface. // // The resulting [Connection] writes messages via POST requests to the @@ -2196,7 +2209,7 @@ func calculateReconnectDelay(attempt int) time.Duration { return 0 } // Calculate the exponential backoff using the grow factor. - backoffDuration := time.Duration(float64(reconnectInitialDelay) * math.Pow(reconnectGrowFactor, float64(attempt-1))) + backoffDuration := time.Duration(float64(reconnectInitialDelay.Load()) * math.Pow(reconnectGrowFactor, float64(attempt-1))) // Cap the backoffDuration at maxDelay. backoffDuration = min(backoffDuration, reconnectMaxDelay) diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/transport.go b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/transport.go index 5f2a50072..23dccf8e1 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/mcp/transport.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/mcp/transport.go @@ -344,7 +344,11 @@ func (r rwc) Close() error { // // See [msgBatch] for more discussion of message batching. type ioConn struct { - protocolVersion string // negotiated version, set during session initialization. + // protocolVersion is the negotiated version of the protocol, + // set during session initialization. + // Since writes may be concurrent to reads, we need to guard this with a mutex. + sessionMu sync.Mutex + protocolVersion string writeMu sync.Mutex // guards Write, which must be concurrency safe. rwc io.ReadWriteCloser // the underlying stream @@ -430,9 +434,15 @@ func (c *ioConn) sessionUpdated(state ServerSessionState) { protocolVersion = state.InitializeParams.ProtocolVersion } if protocolVersion == "" { + // 2025-03-26 is used, because it's the last spec version + // where specifying the protocol version in the HTTP header + // was not required. protocolVersion = protocolVersion20250326 } - c.protocolVersion = negotiatedVersion(protocolVersion) + protocolVersion = negotiatedVersion(protocolVersion) + c.sessionMu.Lock() + c.protocolVersion = protocolVersion + c.sessionMu.Unlock() } // addBatch records a msgBatch for an incoming batch payload. @@ -533,8 +543,12 @@ func (t *ioConn) Read(ctx context.Context) (jsonrpc.Message, error) { if err != nil { return nil, err } - if batch && t.protocolVersion >= protocolVersion20250618 { - return nil, fmt.Errorf("JSON-RPC batching is not supported in %s and later (request version: %s)", protocolVersion20250618, t.protocolVersion) + var protocolVersion string + t.sessionMu.Lock() + protocolVersion = t.protocolVersion + t.sessionMu.Unlock() + if batch && protocolVersion >= protocolVersion20250618 { + return nil, fmt.Errorf("JSON-RPC batching is not supported in %s and later (request version: %s)", protocolVersion20250618, protocolVersion) } t.queue = msgs[1:] diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/auth_meta.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/auth_meta.go index 36210576b..df1a0aa8f 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/auth_meta.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/auth_meta.go @@ -5,8 +5,6 @@ // This file implements Authorization Server Metadata. // See https://www.rfc-editor.org/rfc/rfc8414.html. -//go:build mcp_go_client_oauth - package oauthex import ( diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/client.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/client.go new file mode 100644 index 000000000..e8f991825 --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/client.go @@ -0,0 +1,57 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +package oauthex + +import "errors" + +// ClientCredentials holds client authentication credentials for OAuth token requests. +// It supports multiple authentication methods, but only one method should be set at a time. +// Use the Validate method to ensure proper configuration. +type ClientCredentials struct { + // ClientID is the OAuth2 client identifier. + // REQUIRED for all authentication methods. + ClientID string + + // ClientSecretAuth configures client authentication using a client secret. + // This is the most common authentication method for confidential clients. + // OPTIONAL. If not provided, the client is treated as a public client. + ClientSecretAuth *ClientSecretAuth +} + +// ClientSecretAuth holds client secret authentication credentials. +// This authentication method supports both "client_secret_basic" and "client_secret_post" +// methods as defined in RFC 6749 Section 2.3.1. +type ClientSecretAuth struct { + // ClientSecret is the OAuth2 client secret for confidential clients. + // REQUIRED when using ClientSecretAuth. + ClientSecret string +} + +// Validate checks that the ClientCredentials are properly configured. +// It ensures that: +// - ClientID is not empty. +// - At most one authentication method is configured. +// - If ClientSecretAuth is set, ClientSecret is not empty. +func (c *ClientCredentials) Validate() error { + if c.ClientID == "" { + return errors.New("ClientID is required") + } + + // Count how many auth methods are configured. + authMethodCount := 0 + if c.ClientSecretAuth != nil { + authMethodCount++ + if c.ClientSecretAuth.ClientSecret == "" { + return errors.New("ClientSecret is required when using ClientSecretAuth") + } + } + + // Allow zero auth methods (public client) or exactly one auth method. + if authMethodCount > 1 { + return errors.New("only one client authentication method can be configured") + } + + return nil +} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/dcr.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/dcr.go index 6db30255f..653cf5b11 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/dcr.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/dcr.go @@ -5,8 +5,6 @@ // This file implements Authorization Server Metadata. // See https://www.rfc-editor.org/rfc/rfc8414.html. -//go:build mcp_go_client_oauth - package oauthex import ( diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/oauth2.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/oauth2.go index d8aeb3c27..689f17aea 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/oauth2.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/oauth2.go @@ -4,13 +4,10 @@ // Package oauthex implements extensions to OAuth2. -//go:build mcp_go_client_oauth - package oauthex import ( "context" - "encoding/json" "fmt" "io" "mime" @@ -18,6 +15,7 @@ import ( "net/url" "strings" + "github.com/modelcontextprotocol/go-sdk/internal/json" "github.com/modelcontextprotocol/go-sdk/internal/util" ) @@ -66,6 +64,7 @@ func getJSON[T any](ctx context.Context, c *http.Client, url string, limit int64 // checkURLScheme ensures that its argument is a valid URL with a scheme // that prevents XSS attacks. // See #526. +// Note: a copy of this function exists in auth/extauth/oidc_login.go; keep these in sync. func checkURLScheme(u string) error { if u == "" { return nil diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta.go index 4680c1538..75066345c 100644 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta.go +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta.go @@ -5,8 +5,6 @@ // This file implements Protected Resource Metadata. // See https://www.rfc-editor.org/rfc/rfc9728.html. -//go:build mcp_go_client_oauth - package oauthex import ( @@ -14,81 +12,108 @@ import ( "errors" "fmt" "net/http" - "net/url" - "path" "strings" "unicode" "github.com/modelcontextprotocol/go-sdk/internal/util" ) -const defaultProtectedResourceMetadataURI = "/.well-known/oauth-protected-resource" - -// GetProtectedResourceMetadataFromID issues a GET request to retrieve protected resource -// metadata from a resource server by its ID. -// The resource ID is an HTTPS URL, typically with a host:port and possibly a path. -// For example: -// -// https://example.com/server -// -// This function, following the spec (§3), inserts the default well-known path into the -// URL. In our example, the result would be +// ProtectedResourceMetadata is the metadata for an OAuth 2.0 protected resource, +// as defined in section 2 of https://www.rfc-editor.org/rfc/rfc9728.html. // -// https://example.com/.well-known/oauth-protected-resource/server -// -// It then retrieves the metadata at that location using the given client (or the -// default client if nil) and validates its resource field against resourceID. -// -// Deprecated: Use [GetProtectedResourceMetadata] instead. This function will be removed in v1.5.0. -func GetProtectedResourceMetadataFromID(ctx context.Context, resourceID string, c *http.Client) (_ *ProtectedResourceMetadata, err error) { - defer util.Wrapf(&err, "GetProtectedResourceMetadataFromID(%q)", resourceID) +// The following features are not supported: +// - additional keys (§2, last sentence) +// - human-readable metadata (§2.1) +// - signed metadata (§2.2) +type ProtectedResourceMetadata struct { + // Resource (resource) is the protected resource's resource identifier. + // Required. + Resource string `json:"resource"` - u, err := url.Parse(resourceID) - if err != nil { - return nil, err - } - // Insert well-known URI into URL. - u.Path = path.Join(defaultProtectedResourceMetadataURI, u.Path) - return GetProtectedResourceMetadata(ctx, u.String(), resourceID, c) -} + // AuthorizationServers (authorization_servers) is an optional slice containing a list of + // OAuth authorization server issuer identifiers (as defined in RFC 8414) that can be + // used with this protected resource. + AuthorizationServers []string `json:"authorization_servers,omitempty"` -// GetProtectedResourceMetadataFromHeader retrieves protected resource metadata -// using information in the given header, using the given client (or the default -// client if nil). -// It issues a GET request to a URL discovered by parsing the WWW-Authenticate headers in the given request. -// Per RFC 9728 section 3.3, it validates that the resource field of the resulting metadata -// matches the serverURL (the URL that the client used to make the original request to the resource server). -// If there is no metadata URL in the header, it returns nil, nil. -// -// Deprecated: Use [GetProtectedResourceMetadata] instead. This function will be removed in v1.5.0. -func GetProtectedResourceMetadataFromHeader(ctx context.Context, serverURL string, header http.Header, c *http.Client) (_ *ProtectedResourceMetadata, err error) { - headers := header[http.CanonicalHeaderKey("WWW-Authenticate")] - if len(headers) == 0 { - return nil, nil - } - cs, err := ParseWWWAuthenticate(headers) - if err != nil { - return nil, err - } - metadataURL := resourceMetadataURL(cs) - if metadataURL == "" { - return nil, nil - } - return GetProtectedResourceMetadata(ctx, metadataURL, serverURL, c) + // JWKSURI (jwks_uri) is an optional URL of the protected resource's JSON Web Key (JWK) Set + // document. This contains public keys belonging to the protected resource, such as + // signing key(s) that the resource server uses to sign resource responses. + JWKSURI string `json:"jwks_uri,omitempty"` + + // ScopesSupported (scopes_supported) is a recommended slice containing a list of scope + // values (as defined in RFC 6749) used in authorization requests to request access + // to this protected resource. + ScopesSupported []string `json:"scopes_supported,omitempty"` + + // BearerMethodsSupported (bearer_methods_supported) is an optional slice containing + // a list of the supported methods of sending an OAuth 2.0 bearer token to the + // protected resource. Defined values are "header", "body", and "query". + BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"` + + // ResourceSigningAlgValuesSupported (resource_signing_alg_values_supported) is an optional + // slice of JWS signing algorithms (alg values) supported by the protected + // resource for signing resource responses. + ResourceSigningAlgValuesSupported []string `json:"resource_signing_alg_values_supported,omitempty"` + + // ResourceName (resource_name) is a human-readable name of the protected resource + // intended for display to the end user. It is RECOMMENDED that this field be included. + // This value may be internationalized. + ResourceName string `json:"resource_name,omitempty"` + + // ResourceDocumentation (resource_documentation) is an optional URL of a page containing + // human-readable information for developers using the protected resource. + // This value may be internationalized. + ResourceDocumentation string `json:"resource_documentation,omitempty"` + + // ResourcePolicyURI (resource_policy_uri) is an optional URL of a page containing + // human-readable policy information on how a client can use the data provided. + // This value may be internationalized. + ResourcePolicyURI string `json:"resource_policy_uri,omitempty"` + + // ResourceTOSURI (resource_tos_uri) is an optional URL of a page containing the protected + // resource's human-readable terms of service. This value may be internationalized. + ResourceTOSURI string `json:"resource_tos_uri,omitempty"` + + // TLSClientCertificateBoundAccessTokens (tls_client_certificate_bound_access_tokens) is an + // optional boolean indicating support for mutual-TLS client certificate-bound + // access tokens (RFC 8705). Defaults to false if omitted. + TLSClientCertificateBoundAccessTokens bool `json:"tls_client_certificate_bound_access_tokens,omitempty"` + + // AuthorizationDetailsTypesSupported (authorization_details_types_supported) is an optional + // slice of 'type' values supported by the resource server for the + // 'authorization_details' parameter (RFC 9396). + AuthorizationDetailsTypesSupported []string `json:"authorization_details_types_supported,omitempty"` + + // DPOPSigningAlgValuesSupported (dpop_signing_alg_values_supported) is an optional + // slice of JWS signing algorithms supported by the resource server for validating + // DPoP proof JWTs (RFC 9449). + DPOPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"` + + // DPOPBoundAccessTokensRequired (dpop_bound_access_tokens_required) is an optional boolean + // specifying whether the protected resource always requires the use of DPoP-bound + // access tokens (RFC 9449). Defaults to false if omitted. + DPOPBoundAccessTokensRequired bool `json:"dpop_bound_access_tokens_required,omitempty"` + + // SignedMetadata (signed_metadata) is an optional JWT containing metadata parameters + // about the protected resource as claims. If present, these values take precedence + // over values conveyed in plain JSON. + // TODO:implement. + // Note that §2.2 says it's okay to ignore this. + // SignedMetadata string `json:"signed_metadata,omitempty"` } -// resourceMetadataURL returns a resource metadata URL from the given "WWW-Authenticate" header challenges, -// or the empty string if there is none. -func resourceMetadataURL(cs []Challenge) string { - for _, c := range cs { - if u := c.Params["resource_metadata"]; u != "" { - return u - } - } - return "" +// Challenge represents a single authentication challenge from a WWW-Authenticate header. +// As per RFC 9110, Section 11.6.1, a challenge consists of a scheme and optional parameters. +type Challenge struct { + // Scheme is the authentication scheme (e.g., "Bearer", "Basic"). + // It is case-insensitive. A parsed value will always be lower-case. + Scheme string + // Params is a map of authentication parameters. + // Keys are case-insensitive. Parsed keys are always lower-case. + Params map[string]string } -// GetProtectedResourceMetadataFromID issues a GET request to retrieve protected resource +// GetProtectedResourceMetadata issues a GET request to retrieve protected resource // metadata from a resource server. // The metadataURL is typically a URL with a host:port and possibly a path. // The resourceURL is the resource URI the metadataURL is for. diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta_public.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta_public.go deleted file mode 100644 index 3bf7d9aca..000000000 --- a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/resource_meta_public.go +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright 2025 The Go MCP SDK Authors. All rights reserved. -// Use of this source code is governed by an MIT-style -// license that can be found in the LICENSE file. - -// This file implements Protected Resource Metadata. -// See https://www.rfc-editor.org/rfc/rfc9728.html. - -// This is a temporary file to expose the required objects to the main package. - -package oauthex - -// ProtectedResourceMetadata is the metadata for an OAuth 2.0 protected resource, -// as defined in section 2 of https://www.rfc-editor.org/rfc/rfc9728.html. -// -// The following features are not supported: -// - additional keys (§2, last sentence) -// - human-readable metadata (§2.1) -// - signed metadata (§2.2) -type ProtectedResourceMetadata struct { - // Resource (resource) is the protected resource's resource identifier. - // Required. - Resource string `json:"resource"` - - // AuthorizationServers (authorization_servers) is an optional slice containing a list of - // OAuth authorization server issuer identifiers (as defined in RFC 8414) that can be - // used with this protected resource. - AuthorizationServers []string `json:"authorization_servers,omitempty"` - - // JWKSURI (jwks_uri) is an optional URL of the protected resource's JSON Web Key (JWK) Set - // document. This contains public keys belonging to the protected resource, such as - // signing key(s) that the resource server uses to sign resource responses. - JWKSURI string `json:"jwks_uri,omitempty"` - - // ScopesSupported (scopes_supported) is a recommended slice containing a list of scope - // values (as defined in RFC 6749) used in authorization requests to request access - // to this protected resource. - ScopesSupported []string `json:"scopes_supported,omitempty"` - - // BearerMethodsSupported (bearer_methods_supported) is an optional slice containing - // a list of the supported methods of sending an OAuth 2.0 bearer token to the - // protected resource. Defined values are "header", "body", and "query". - BearerMethodsSupported []string `json:"bearer_methods_supported,omitempty"` - - // ResourceSigningAlgValuesSupported (resource_signing_alg_values_supported) is an optional - // slice of JWS signing algorithms (alg values) supported by the protected - // resource for signing resource responses. - ResourceSigningAlgValuesSupported []string `json:"resource_signing_alg_values_supported,omitempty"` - - // ResourceName (resource_name) is a human-readable name of the protected resource - // intended for display to the end user. It is RECOMMENDED that this field be included. - // This value may be internationalized. - ResourceName string `json:"resource_name,omitempty"` - - // ResourceDocumentation (resource_documentation) is an optional URL of a page containing - // human-readable information for developers using the protected resource. - // This value may be internationalized. - ResourceDocumentation string `json:"resource_documentation,omitempty"` - - // ResourcePolicyURI (resource_policy_uri) is an optional URL of a page containing - // human-readable policy information on how a client can use the data provided. - // This value may be internationalized. - ResourcePolicyURI string `json:"resource_policy_uri,omitempty"` - - // ResourceTOSURI (resource_tos_uri) is an optional URL of a page containing the protected - // resource's human-readable terms of service. This value may be internationalized. - ResourceTOSURI string `json:"resource_tos_uri,omitempty"` - - // TLSClientCertificateBoundAccessTokens (tls_client_certificate_bound_access_tokens) is an - // optional boolean indicating support for mutual-TLS client certificate-bound - // access tokens (RFC 8705). Defaults to false if omitted. - TLSClientCertificateBoundAccessTokens bool `json:"tls_client_certificate_bound_access_tokens,omitempty"` - - // AuthorizationDetailsTypesSupported (authorization_details_types_supported) is an optional - // slice of 'type' values supported by the resource server for the - // 'authorization_details' parameter (RFC 9396). - AuthorizationDetailsTypesSupported []string `json:"authorization_details_types_supported,omitempty"` - - // DPOPSigningAlgValuesSupported (dpop_signing_alg_values_supported) is an optional - // slice of JWS signing algorithms supported by the resource server for validating - // DPoP proof JWTs (RFC 9449). - DPOPSigningAlgValuesSupported []string `json:"dpop_signing_alg_values_supported,omitempty"` - - // DPOPBoundAccessTokensRequired (dpop_bound_access_tokens_required) is an optional boolean - // specifying whether the protected resource always requires the use of DPoP-bound - // access tokens (RFC 9449). Defaults to false if omitted. - DPOPBoundAccessTokensRequired bool `json:"dpop_bound_access_tokens_required,omitempty"` - - // SignedMetadata (signed_metadata) is an optional JWT containing metadata parameters - // about the protected resource as claims. If present, these values take precedence - // over values conveyed in plain JSON. - // TODO:implement. - // Note that §2.2 says it's okay to ignore this. - // SignedMetadata string `json:"signed_metadata,omitempty"` -} - -// Challenge represents a single authentication challenge from a WWW-Authenticate header. -// As per RFC 9110, Section 11.6.1, a challenge consists of a scheme and optional parameters. -type Challenge struct { - // Scheme is the authentication scheme (e.g., "Bearer", "Basic"). - // It is case-insensitive. A parsed value will always be lower-case. - Scheme string - // Params is a map of authentication parameters. - // Keys are case-insensitive. Parsed keys are always lower-case. - Params map[string]string -} diff --git a/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/token_exchange.go b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/token_exchange.go new file mode 100644 index 000000000..0c7b24eb4 --- /dev/null +++ b/vendor/github.com/modelcontextprotocol/go-sdk/oauthex/token_exchange.go @@ -0,0 +1,183 @@ +// Copyright 2026 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +// This file implements Token Exchange (RFC 8693) for Enterprise Managed Authorization. +// See https://datatracker.ietf.org/doc/html/rfc8693 + +package oauthex + +import ( + "context" + "fmt" + "net/http" + "strings" + + "golang.org/x/oauth2" +) + +// Token type identifiers defined by RFC 8693 and SEP-990. +const ( + // TokenTypeIDToken is the URN for OpenID Connect ID Tokens. + TokenTypeIDToken = "urn:ietf:params:oauth:token-type:id_token" + + // TokenTypeSAML2 is the URN for SAML 2.0 assertions. + TokenTypeSAML2 = "urn:ietf:params:oauth:token-type:saml2" + + // TokenTypeIDJAG is the URN for Identity Assertion JWT Authorization Grants. + // This is the token type returned by IdP during token exchange for SEP-990. + TokenTypeIDJAG = "urn:ietf:params:oauth:token-type:id-jag" + + // GrantTypeTokenExchange is the grant type for RFC 8693 token exchange. + GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange" +) + +// TokenExchangeRequest represents a Token Exchange request per RFC 8693. +// This is used for Enterprise Managed Authorization (SEP-990) where an MCP Client +// exchanges an ID Token from an enterprise IdP for an ID-JAG that can be used +// to obtain an access token from an MCP Server's authorization server. +type TokenExchangeRequest struct { + // RequestedTokenType indicates the type of security token being requested. + // For SEP-990, this MUST be TokenTypeIDJAG. + RequestedTokenType string + + // Audience is the logical name of the target service where the client + // intends to use the requested token. For SEP-990, this MUST be the + // Issuer URL of the MCP Server's authorization server. + Audience string + + // Resource is the physical location or identifier of the target resource. + // For SEP-990, this MUST be the RFC9728 Resource Identifier of the MCP Server. + Resource string + + // Scope is a list of space-separated scopes for the requested token. + // This is OPTIONAL per RFC 8693 but commonly used in SEP-990. + Scope []string + + // SubjectToken is the security token that represents the identity of the + // party on behalf of whom the request is being made. For SEP-990, this is + // typically an OpenID Connect ID Token. + SubjectToken string + + // SubjectTokenType is the type of the security token in SubjectToken. + // For SEP-990 with OIDC, this MUST be TokenTypeIDToken. + SubjectTokenType string +} + +// ExchangeToken performs a token exchange request per RFC 8693 for Enterprise +// Managed Authorization (SEP-990). It exchanges an identity assertion (typically +// an ID Token) for an Identity Assertion JWT Authorization Grant (ID-JAG) that +// can be used to obtain an access token from an MCP Server. +// +// The tokenEndpoint parameter should be the IdP's token endpoint (typically +// obtained from the IdP's authorization server metadata). +// +// Returns an oauth2.Token where: +// - Extra("issued_token_type") contains the type of the issued token (e.g., TokenTypeIDJAG) +// - AccessToken contains the ID-JAG JWT (despite the name, this is not an OAuth access token) +// - TokenType is typically "N_A" for SEP-990 +// - Extra("scope") may contain the scope if different from the request +// - Expiry is when the token expires +func ExchangeToken( + ctx context.Context, + tokenEndpoint string, + req *TokenExchangeRequest, + clientCreds *ClientCredentials, + httpClient *http.Client, +) (*oauth2.Token, error) { + if tokenEndpoint == "" { + return nil, fmt.Errorf("token endpoint is required") + } + if req == nil { + return nil, fmt.Errorf("token exchange request is required") + } + if clientCreds == nil { + return nil, fmt.Errorf("client credentials are required") + } + if err := clientCreds.Validate(); err != nil { + return nil, fmt.Errorf("invalid client credentials: %w", err) + } + + // Validate required fields per SEP-990 Section 4. + if req.RequestedTokenType == "" { + return nil, fmt.Errorf("requested_token_type is required") + } + if req.Audience == "" { + return nil, fmt.Errorf("audience is required") + } + if req.Resource == "" { + return nil, fmt.Errorf("resource is required") + } + if req.SubjectToken == "" { + return nil, fmt.Errorf("subject_token is required") + } + if req.SubjectTokenType == "" { + return nil, fmt.Errorf("subject_token_type is required") + } + + // Validate URL schemes to prevent XSS attacks (see #526). + if err := checkURLScheme(tokenEndpoint); err != nil { + return nil, fmt.Errorf("invalid token endpoint: %w", err) + } + if err := checkURLScheme(req.Audience); err != nil { + return nil, fmt.Errorf("invalid audience: %w", err) + } + if err := checkURLScheme(req.Resource); err != nil { + return nil, fmt.Errorf("invalid resource: %w", err) + } + + // Per RFC 6749 Section 3.2, parameters sent without a value (like the empty + // "code" parameter) MUST be treated as if they were omitted from the request. + // The oauth2 library's Exchange method sends an empty code, but compliant + // servers should ignore it. + cfg := &oauth2.Config{ + ClientID: clientCreds.ClientID, + Endpoint: oauth2.Endpoint{ + TokenURL: tokenEndpoint, + AuthStyle: oauth2.AuthStyleInParams, + }, + } + // Set ClientSecret if ClientSecretAuth is configured. + if clientCreds.ClientSecretAuth != nil { + cfg.ClientSecret = clientCreds.ClientSecretAuth.ClientSecret + } + + // Use custom HTTP client if provided. + if httpClient == nil { + httpClient = http.DefaultClient + } + ctxWithClient := context.WithValue(ctx, oauth2.HTTPClient, httpClient) + + // Build token exchange parameters per RFC 8693. + opts := []oauth2.AuthCodeOption{ + oauth2.SetAuthURLParam("grant_type", GrantTypeTokenExchange), + oauth2.SetAuthURLParam("requested_token_type", req.RequestedTokenType), + oauth2.SetAuthURLParam("audience", req.Audience), + oauth2.SetAuthURLParam("resource", req.Resource), + oauth2.SetAuthURLParam("subject_token", req.SubjectToken), + oauth2.SetAuthURLParam("subject_token_type", req.SubjectTokenType), + } + if len(req.Scope) > 0 { + opts = append(opts, oauth2.SetAuthURLParam("scope", strings.Join(req.Scope, " "))) + } + + // Exchange with token exchange grant type. + // SetAuthURLParam overrides the default grant_type and adds all required parameters. + token, err := cfg.Exchange( + ctxWithClient, + "", // empty code - per RFC 6749 Section 3.2, empty params should be ignored + opts..., + ) + if err != nil { + return nil, fmt.Errorf("token exchange request failed: %w", err) + } + + // Validate that issued_token_type is present in the response. + // The oauth2 library stores additional response fields in Extra. + issuedTokenType, _ := token.Extra("issued_token_type").(string) + if issuedTokenType == "" { + return nil, fmt.Errorf("response missing required field: issued_token_type") + } + + return token, nil +} diff --git a/vendor/golang.org/x/sys/cpu/cpu_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_arm64.go index af2aa99f9..6d8eb784b 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_arm64.go @@ -47,7 +47,7 @@ func archInit() { switch runtime.GOOS { case "freebsd": readARM64Registers() - case "linux", "netbsd", "openbsd": + case "linux", "netbsd", "openbsd", "windows": doinit() default: // Many platforms don't seem to allow reading these registers. diff --git a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go index 5341e7f88..ff74d7afa 100644 --- a/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go +++ b/vendor/golang.org/x/sys/cpu/cpu_other_arm64.go @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//go:build !linux && !netbsd && !openbsd && arm64 +//go:build !linux && !netbsd && !openbsd && !windows && arm64 package cpu diff --git a/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go b/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go new file mode 100644 index 000000000..d09e85a36 --- /dev/null +++ b/vendor/golang.org/x/sys/cpu/cpu_windows_arm64.go @@ -0,0 +1,42 @@ +// Copyright 2026 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package cpu + +import ( + "golang.org/x/sys/windows" +) + +func doinit() { + // set HasASIMD and HasFP to true as per + // https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170#base-requirements + // + // The ARM64 version of Windows always presupposes that it's running on an ARMv8 or later architecture. + // Both floating-point and NEON support are presumed to be present in hardware. + // + ARM64.HasASIMD = true + ARM64.HasFP = true + + if windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE) { + ARM64.HasAES = true + ARM64.HasPMULL = true + ARM64.HasSHA1 = true + ARM64.HasSHA2 = true + } + ARM64.HasSHA3 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE) + ARM64.HasCRC32 = windows.IsProcessorFeaturePresent(windows.PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE) + ARM64.HasSHA512 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE) + ARM64.HasATOMICS = windows.IsProcessorFeaturePresent(windows.PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE) + if windows.IsProcessorFeaturePresent(windows.PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE) { + ARM64.HasASIMDDP = true + ARM64.HasASIMDRDM = true + } + if windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE) { + ARM64.HasLRCPC = true + ARM64.HasSM3 = true + } + ARM64.HasSVE = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE_INSTRUCTIONS_AVAILABLE) + ARM64.HasSVE2 = windows.IsProcessorFeaturePresent(windows.PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE) + ARM64.HasJSCVT = windows.IsProcessorFeaturePresent(windows.PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE) +} diff --git a/vendor/golang.org/x/sys/unix/ioctl_signed.go b/vendor/golang.org/x/sys/unix/ioctl_signed.go index 5b0759bd8..be0f3fba6 100644 --- a/vendor/golang.org/x/sys/unix/ioctl_signed.go +++ b/vendor/golang.org/x/sys/unix/ioctl_signed.go @@ -6,9 +6,7 @@ package unix -import ( - "unsafe" -) +import "unsafe" // ioctl itself should not be exposed directly, but additional get/set // functions for specific types are permissible. @@ -28,6 +26,13 @@ func IoctlSetPointerInt(fd int, req int, value int) error { return ioctlPtr(fd, req, unsafe.Pointer(&v)) } +// IoctlSetString performs an ioctl operation which sets a string value +// on fd, using the specified request number. +func IoctlSetString(fd int, req int, value string) error { + bs := append([]byte(value), 0) + return ioctlPtr(fd, req, unsafe.Pointer(&bs[0])) +} + // IoctlSetWinsize performs an ioctl on fd with a *Winsize argument. // // To change fd's window size, the req argument should be TIOCSWINSZ. diff --git a/vendor/golang.org/x/sys/unix/ioctl_unsigned.go b/vendor/golang.org/x/sys/unix/ioctl_unsigned.go index 20f470b9d..f0c282136 100644 --- a/vendor/golang.org/x/sys/unix/ioctl_unsigned.go +++ b/vendor/golang.org/x/sys/unix/ioctl_unsigned.go @@ -6,9 +6,7 @@ package unix -import ( - "unsafe" -) +import "unsafe" // ioctl itself should not be exposed directly, but additional get/set // functions for specific types are permissible. @@ -28,6 +26,13 @@ func IoctlSetPointerInt(fd int, req uint, value int) error { return ioctlPtr(fd, req, unsafe.Pointer(&v)) } +// IoctlSetString performs an ioctl operation which sets a string value +// on fd, using the specified request number. +func IoctlSetString(fd int, req uint, value string) error { + bs := append([]byte(value), 0) + return ioctlPtr(fd, req, unsafe.Pointer(&bs[0])) +} + // IoctlSetWinsize performs an ioctl on fd with a *Winsize argument. // // To change fd's window size, the req argument should be TIOCSWINSZ. diff --git a/vendor/golang.org/x/sys/unix/syscall_solaris.go b/vendor/golang.org/x/sys/unix/syscall_solaris.go index 18a3d9bda..a6a2ea0cc 100644 --- a/vendor/golang.org/x/sys/unix/syscall_solaris.go +++ b/vendor/golang.org/x/sys/unix/syscall_solaris.go @@ -1052,14 +1052,6 @@ func IoctlSetIntRetInt(fd int, req int, arg int) (int, error) { return ioctlRet(fd, req, uintptr(arg)) } -func IoctlSetString(fd int, req int, val string) error { - bs := make([]byte, len(val)+1) - copy(bs[:len(bs)-1], val) - err := ioctlPtr(fd, req, unsafe.Pointer(&bs[0])) - runtime.KeepAlive(&bs[0]) - return err -} - // Lifreq Helpers func (l *Lifreq) SetName(name string) error { diff --git a/vendor/golang.org/x/sys/unix/syscall_unix.go b/vendor/golang.org/x/sys/unix/syscall_unix.go index 4e92e5aa4..de6fccf9a 100644 --- a/vendor/golang.org/x/sys/unix/syscall_unix.go +++ b/vendor/golang.org/x/sys/unix/syscall_unix.go @@ -367,7 +367,9 @@ func Recvmsg(fd int, p, oob []byte, flags int) (n, oobn int, recvflags int, from iov[0].SetLen(len(p)) } var rsa RawSockaddrAny - n, oobn, recvflags, err = recvmsgRaw(fd, iov[:], oob, flags, &rsa) + if n, oobn, recvflags, err = recvmsgRaw(fd, iov[:], oob, flags, &rsa); err != nil { + return + } // source address is only specified if the socket is unconnected if rsa.Addr.Family != AF_UNSPEC { from, err = anyToSockaddr(fd, &rsa) @@ -389,8 +391,10 @@ func RecvmsgBuffers(fd int, buffers [][]byte, oob []byte, flags int) (n, oobn in } } var rsa RawSockaddrAny - n, oobn, recvflags, err = recvmsgRaw(fd, iov, oob, flags, &rsa) - if err == nil && rsa.Addr.Family != AF_UNSPEC { + if n, oobn, recvflags, err = recvmsgRaw(fd, iov, oob, flags, &rsa); err != nil { + return + } + if rsa.Addr.Family != AF_UNSPEC { from, err = anyToSockaddr(fd, &rsa) } return diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index 69439df2a..738a9f212 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -900,6 +900,7 @@ const socket_error = uintptr(^uint32(0)) //sys NotifyRouteChange2(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyRouteChange2 //sys NotifyUnicastIpAddressChange(family uint16, callback uintptr, callerContext unsafe.Pointer, initialNotification bool, notificationHandle *Handle) (errcode error) = iphlpapi.NotifyUnicastIpAddressChange //sys CancelMibChangeNotify2(notificationHandle Handle) (errcode error) = iphlpapi.CancelMibChangeNotify2 +//sys IsProcessorFeaturePresent(ProcessorFeature uint32) (ret bool) = kernel32.IsProcessorFeaturePresent // For testing: clients can set this flag to force // creation of IPv6 sockets to return EAFNOSUPPORT. diff --git a/vendor/golang.org/x/sys/windows/types_windows.go b/vendor/golang.org/x/sys/windows/types_windows.go index 6e4f50eb4..d5658a138 100644 --- a/vendor/golang.org/x/sys/windows/types_windows.go +++ b/vendor/golang.org/x/sys/windows/types_windows.go @@ -3938,3 +3938,88 @@ const ( MOUSE_EVENT = 0x0002 WINDOW_BUFFER_SIZE_EVENT = 0x0004 ) + +// The processor features to be tested for IsProcessorFeaturePresent, see +// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-isprocessorfeaturepresent +const ( + PF_ARM_64BIT_LOADSTORE_ATOMIC = 25 + PF_ARM_DIVIDE_INSTRUCTION_AVAILABLE = 24 + PF_ARM_EXTERNAL_CACHE_AVAILABLE = 26 + PF_ARM_FMAC_INSTRUCTIONS_AVAILABLE = 27 + PF_ARM_VFP_32_REGISTERS_AVAILABLE = 18 + PF_3DNOW_INSTRUCTIONS_AVAILABLE = 7 + PF_CHANNELS_ENABLED = 16 + PF_COMPARE_EXCHANGE_DOUBLE = 2 + PF_COMPARE_EXCHANGE128 = 14 + PF_COMPARE64_EXCHANGE128 = 15 + PF_FASTFAIL_AVAILABLE = 23 + PF_FLOATING_POINT_EMULATED = 1 + PF_FLOATING_POINT_PRECISION_ERRATA = 0 + PF_MMX_INSTRUCTIONS_AVAILABLE = 3 + PF_NX_ENABLED = 12 + PF_PAE_ENABLED = 9 + PF_RDTSC_INSTRUCTION_AVAILABLE = 8 + PF_RDWRFSGSBASE_AVAILABLE = 22 + PF_SECOND_LEVEL_ADDRESS_TRANSLATION = 20 + PF_SSE3_INSTRUCTIONS_AVAILABLE = 13 + PF_SSSE3_INSTRUCTIONS_AVAILABLE = 36 + PF_SSE4_1_INSTRUCTIONS_AVAILABLE = 37 + PF_SSE4_2_INSTRUCTIONS_AVAILABLE = 38 + PF_AVX_INSTRUCTIONS_AVAILABLE = 39 + PF_AVX2_INSTRUCTIONS_AVAILABLE = 40 + PF_AVX512F_INSTRUCTIONS_AVAILABLE = 41 + PF_VIRT_FIRMWARE_ENABLED = 21 + PF_XMMI_INSTRUCTIONS_AVAILABLE = 6 + PF_XMMI64_INSTRUCTIONS_AVAILABLE = 10 + PF_XSAVE_ENABLED = 17 + PF_ARM_V8_INSTRUCTIONS_AVAILABLE = 29 + PF_ARM_V8_CRYPTO_INSTRUCTIONS_AVAILABLE = 30 + PF_ARM_V8_CRC32_INSTRUCTIONS_AVAILABLE = 31 + PF_ARM_V81_ATOMIC_INSTRUCTIONS_AVAILABLE = 34 + PF_ARM_V82_DP_INSTRUCTIONS_AVAILABLE = 43 + PF_ARM_V83_JSCVT_INSTRUCTIONS_AVAILABLE = 44 + PF_ARM_V83_LRCPC_INSTRUCTIONS_AVAILABLE = 45 + PF_ARM_SVE_INSTRUCTIONS_AVAILABLE = 46 + PF_ARM_SVE2_INSTRUCTIONS_AVAILABLE = 47 + PF_ARM_SVE2_1_INSTRUCTIONS_AVAILABLE = 48 + PF_ARM_SVE_AES_INSTRUCTIONS_AVAILABLE = 49 + PF_ARM_SVE_PMULL128_INSTRUCTIONS_AVAILABLE = 50 + PF_ARM_SVE_BITPERM_INSTRUCTIONS_AVAILABLE = 51 + PF_ARM_SVE_BF16_INSTRUCTIONS_AVAILABLE = 52 + PF_ARM_SVE_EBF16_INSTRUCTIONS_AVAILABLE = 53 + PF_ARM_SVE_B16B16_INSTRUCTIONS_AVAILABLE = 54 + PF_ARM_SVE_SHA3_INSTRUCTIONS_AVAILABLE = 55 + PF_ARM_SVE_SM4_INSTRUCTIONS_AVAILABLE = 56 + PF_ARM_SVE_I8MM_INSTRUCTIONS_AVAILABLE = 57 + PF_ARM_SVE_F32MM_INSTRUCTIONS_AVAILABLE = 58 + PF_ARM_SVE_F64MM_INSTRUCTIONS_AVAILABLE = 59 + PF_BMI2_INSTRUCTIONS_AVAILABLE = 60 + PF_MOVDIR64B_INSTRUCTION_AVAILABLE = 61 + PF_ARM_LSE2_AVAILABLE = 62 + PF_ARM_SHA3_INSTRUCTIONS_AVAILABLE = 64 + PF_ARM_SHA512_INSTRUCTIONS_AVAILABLE = 65 + PF_ARM_V82_I8MM_INSTRUCTIONS_AVAILABLE = 66 + PF_ARM_V82_FP16_INSTRUCTIONS_AVAILABLE = 67 + PF_ARM_V86_BF16_INSTRUCTIONS_AVAILABLE = 68 + PF_ARM_V86_EBF16_INSTRUCTIONS_AVAILABLE = 69 + PF_ARM_SME_INSTRUCTIONS_AVAILABLE = 70 + PF_ARM_SME2_INSTRUCTIONS_AVAILABLE = 71 + PF_ARM_SME2_1_INSTRUCTIONS_AVAILABLE = 72 + PF_ARM_SME2_2_INSTRUCTIONS_AVAILABLE = 73 + PF_ARM_SME_AES_INSTRUCTIONS_AVAILABLE = 74 + PF_ARM_SME_SBITPERM_INSTRUCTIONS_AVAILABLE = 75 + PF_ARM_SME_SF8MM4_INSTRUCTIONS_AVAILABLE = 76 + PF_ARM_SME_SF8MM8_INSTRUCTIONS_AVAILABLE = 77 + PF_ARM_SME_SF8DP2_INSTRUCTIONS_AVAILABLE = 78 + PF_ARM_SME_SF8DP4_INSTRUCTIONS_AVAILABLE = 79 + PF_ARM_SME_SF8FMA_INSTRUCTIONS_AVAILABLE = 80 + PF_ARM_SME_F8F32_INSTRUCTIONS_AVAILABLE = 81 + PF_ARM_SME_F8F16_INSTRUCTIONS_AVAILABLE = 82 + PF_ARM_SME_F16F16_INSTRUCTIONS_AVAILABLE = 83 + PF_ARM_SME_B16B16_INSTRUCTIONS_AVAILABLE = 84 + PF_ARM_SME_F64F64_INSTRUCTIONS_AVAILABLE = 85 + PF_ARM_SME_I16I64_INSTRUCTIONS_AVAILABLE = 86 + PF_ARM_SME_LUTv2_INSTRUCTIONS_AVAILABLE = 87 + PF_ARM_SME_FA64_INSTRUCTIONS_AVAILABLE = 88 + PF_UMONITOR_INSTRUCTION_AVAILABLE = 89 +) diff --git a/vendor/golang.org/x/sys/windows/zsyscall_windows.go b/vendor/golang.org/x/sys/windows/zsyscall_windows.go index f25b7308a..fe7a4ea12 100644 --- a/vendor/golang.org/x/sys/windows/zsyscall_windows.go +++ b/vendor/golang.org/x/sys/windows/zsyscall_windows.go @@ -320,6 +320,7 @@ var ( procGetVolumePathNamesForVolumeNameW = modkernel32.NewProc("GetVolumePathNamesForVolumeNameW") procGetWindowsDirectoryW = modkernel32.NewProc("GetWindowsDirectoryW") procInitializeProcThreadAttributeList = modkernel32.NewProc("InitializeProcThreadAttributeList") + procIsProcessorFeaturePresent = modkernel32.NewProc("IsProcessorFeaturePresent") procIsWow64Process = modkernel32.NewProc("IsWow64Process") procIsWow64Process2 = modkernel32.NewProc("IsWow64Process2") procLoadLibraryExW = modkernel32.NewProc("LoadLibraryExW") @@ -2786,6 +2787,12 @@ func initializeProcThreadAttributeList(attrlist *ProcThreadAttributeList, attrco return } +func IsProcessorFeaturePresent(ProcessorFeature uint32) (ret bool) { + r0, _, _ := syscall.SyscallN(procIsProcessorFeaturePresent.Addr(), uintptr(ProcessorFeature)) + ret = r0 != 0 + return +} + func IsWow64Process(handle Handle, isWow64 *bool) (err error) { var _p0 uint32 if *isWow64 { diff --git a/vendor/modules.txt b/vendor/modules.txt index 61cd75e35..806eb7aac 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -502,7 +502,7 @@ github.com/moby/sys/sequential ## explicit; go 1.18 github.com/moby/term github.com/moby/term/windows -# github.com/modelcontextprotocol/go-sdk v1.4.1 +# github.com/modelcontextprotocol/go-sdk v1.5.0 ## explicit; go 1.25.0 github.com/modelcontextprotocol/go-sdk/auth github.com/modelcontextprotocol/go-sdk/internal/json @@ -939,14 +939,14 @@ golang.org/x/net/idna golang.org/x/net/internal/httpcommon golang.org/x/net/internal/timeseries golang.org/x/net/trace -# golang.org/x/oauth2 v0.34.0 +# golang.org/x/oauth2 v0.35.0 ## explicit; go 1.24.0 golang.org/x/oauth2 golang.org/x/oauth2/internal # golang.org/x/sync v0.19.0 ## explicit; go 1.24.0 golang.org/x/sync/errgroup -# golang.org/x/sys v0.40.0 +# golang.org/x/sys v0.41.0 ## explicit; go 1.24.0 golang.org/x/sys/cpu golang.org/x/sys/plan9