This PR introduces client-side OAuth support. # Overall design philosophy The main new interface introduced to support client-side OAuth is the `OAuthHandler`. Contrary to its counter-parts in some other SDKs, it is intentionally small and generic so that it can support most of the OAuth authorization flow variants. The main `mcp` package purposefully delegates almost all auth handling to `OAuthHandler` implementations. This way, the `mcp` package continues to be largely OAuth agnostic. The `auth` package comes with a default `OAuthHandler` implementation – `AuthorizationCodeHandler`, which implements the authorization flow for the `authorization_code` grant type, as defined in the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). It is expected that the `OAuthHandler` implementation granularity will be roughly per grant type, as this parameter influences the authorization flow the most. # Detailed design ## `OAuthHandler` (auth/client.go) The main interface for the integration consists of two methods: ```go type OAuthHandler interface { // 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. TokenSource(context.Context) (oauth2.TokenSource, error) // Authorize is called when an HTTP request results in an error that may // be addressed by the authorization flow (currently 401 Unauthorized and 403 Forbidden). // It is responsible for performing the OAuth flow to obtain an access token. // The arguments are the request that failed and the response that was received for it. // The headers of the request are available, but the body will have already been consumed // when Authorize is called. // If the returned error is nil, TokenSource is expected to return a non-nil token source. // After a successful call to Authorize, the HTTP request will be retried by the transport. // The function is responsible for closing the response body. Authorize(context.Context, *http.Request, *http.Response) error } ``` `StreamableClientTransport` accepts this interface as a field and uses the `TokenSource` method to add an `Authorization: Bearer` header to outgoing requests to the MCP server. If the request fails with `401: Unauthorized` or `403: Forbidden`, the transport calls `Authorize` to perform the authorization flow. If `Authorize` returns a non-nil error, the request is retried once. `Authorize` errors do not result in termination of the client session (unless they happen during connection initialization), as some OAuth flows are multi-legged and it should be acceptable to retry for example when the authorization grant was received. *Note:* There was already an `OAuthHandler` type defined as part of a previous iteration of OAuth support. It was renamed to `OAuthHandlerLegacy`. This is the only backwards incompatible API change in the experimental client `auth` part (protected by the `mcp_go_client_oauth` Go build tag). The cost of replacing previous usages is deemed worthwhile to maintain clear naming, under the assumption that the experimental client `auth` part was not widely used. ## `AuthorizationCodeHandler` (auth/authorization_code.go) This is the `OAuthHandler` implementation that fulfills the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization). In particular, in supports: - Protected Resource Metadata discovery and fetching - Authorization Server Metadata discovery and fetching - Client registration - OpenID Connect metadata document - Pre-registration - Dynamic Client Registration - PKCE - `client_secret_post`, `client_secret_basic` token endpoint auth methods - Automatic refresh of tokens ### Authorization Server redirect As part of the flow for the `authorization_code` grant type, the client needs to point the user to the authorization URL, so that they confirm the access request. This design proposes that it will be supported via a pluggable dependency that will initiate this process in an application-specific way: ```go type AuthorizationCodeHandler struct { ... // 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) ... } ``` The function is supposed perform any application specific logic to handle the redirect from the Authorization Server and return the resulting code and state. ## `oauthex` changes Some additional/adjusted building blocks that may be useful when creating `OAuthHandler` implementations are present in the `oauthex` package: - `auth_meta.go`: Authorization Server Metadata utilities: - `GetAuthServerMeta`: get Authorization Server Metadata from a URL - `dcr.go`: Dynamic Client Registration utilities (unchanged) - `resource_meta.go`: Protected Resource Metadata utilities: - `GetProtectedResourceMetadata`: get Protected Resource Metadata from a URL - `ParseWWWAuthenticate` – as per name (unchanged) - [deprecated] `GetProtectedResourceMetadataFromID` and `GetProtectedResourceMetadataFromHeader` in favor of more generic `GetProtectedResourceMetadata`. They will be kept under the Go build tag and removed one version of the SDK in the future.
92 lines
2.5 KiB
Shell
Executable file
92 lines
2.5 KiB
Shell
Executable file
#!/bin/bash
|
|
# 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.
|
|
|
|
# Run MCP conformance tests against the Go SDK conformance client.
|
|
|
|
set -e
|
|
|
|
RESULT_DIR=""
|
|
WORKDIR=""
|
|
CONFORMANCE_REPO=""
|
|
SUITE="core"
|
|
FINAL_EXIT_CODE=0
|
|
|
|
usage() {
|
|
echo "Usage: $0 [options]"
|
|
echo ""
|
|
echo "Run MCP conformance tests against the Go SDK conformance client."
|
|
echo ""
|
|
echo "Options:"
|
|
echo " --result_dir <dir> Save results to the specified directory"
|
|
echo " --conformance_repo <dir> Run conformance tests from a local checkout"
|
|
echo " instead of using the latest npm release"
|
|
echo " --suite <name> Which suite to run (default: core)"
|
|
echo " --help Show this help message"
|
|
}
|
|
|
|
|
|
# Parse arguments.
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
--result_dir)
|
|
RESULT_DIR="$2"
|
|
shift 2
|
|
;;
|
|
--conformance_repo)
|
|
CONFORMANCE_REPO="$2"
|
|
shift 2
|
|
;;
|
|
--suite)
|
|
SUITE="$2"
|
|
shift 2
|
|
;;
|
|
--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Set up the work directory.
|
|
if [ -n "$RESULT_DIR" ]; then
|
|
mkdir -p "$RESULT_DIR"
|
|
WORKDIR="$RESULT_DIR"
|
|
else
|
|
WORKDIR=$(mktemp -d)
|
|
fi
|
|
|
|
# Build the conformance server.
|
|
go build -tags mcp_go_client_oauth -o "$WORKDIR/conformance-client" ./conformance/everything-client
|
|
|
|
# Run conformance tests from the work directory to avoid writing results to the repo.
|
|
echo "Running conformance tests..."
|
|
if [ -n "$CONFORMANCE_REPO" ]; then
|
|
# Run from local checkout using npm run start.
|
|
(cd "$WORKDIR" && \
|
|
npm --prefix "$CONFORMANCE_REPO" run start -- \
|
|
client --command "$WORKDIR/conformance-client" \
|
|
--suite "$SUITE" \
|
|
${RESULT_DIR:+--output-dir "$RESULT_DIR"}) || FINAL_EXIT_CODE=$?
|
|
else
|
|
(cd "$WORKDIR" && \
|
|
npx @modelcontextprotocol/conformance@latest \
|
|
client --command "$WORKDIR/conformance-client" \
|
|
--suite "$SUITE" \
|
|
${RESULT_DIR:+--output-dir "$RESULT_DIR"}) || FINAL_EXIT_CODE=$?
|
|
fi
|
|
|
|
echo ""
|
|
if [ -n "$RESULT_DIR" ]; then
|
|
echo "See $RESULT_DIR for details."
|
|
else
|
|
echo "Run with --result_dir to save results."
|
|
fi
|
|
|
|
exit $FINAL_EXIT_CODE
|