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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 125 additions & 5 deletions examples/server/conformance/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
// The conformance server implements features required for MCP conformance testing.
// It mirrors the functionality of the TypeScript conformance server at
// https://github.com/modelcontextprotocol/conformance/blob/main/examples/servers/typescript/everything-server.ts

//go:build mcp_go_client_oauth
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does any of these need the client-side code? I think only the server-side code is used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/modelcontextprotocol/go-sdk/blob/main/oauthex/auth_meta.go has the tag and getting Authorization server metadata is expected by the conformance tests.

I would have a more general question: what's the logic whether something should live in auth vs in oauthex? My naive assumption would be that auth supports the authorization helpers implementing the spec (https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) and oauthex could contain extensions, as proposed in the https://github.com/modelcontextprotocol/ext-auth repo. But currently, many of the basic primitives do live in oauthex. Could you provide me with more context?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oauthex should contain only extensions to the OAuth 2.0 protocol, as described in various RFCs for that protocol. So for us that means Resource Server Metadata, Auth Server Metadata, and Dynamic Client Registration. The test is: could we just move this package under golang.org/x/oauth2 without bringing along anything MCP-specific? Because that is exactly what I'd like to happen someday.

Are we currently violating that?

I don't know why auth_meta.go has the tag. Please confirm that it contains no logic outside of the RFC. Then remove the tag.


package main

import (
Expand All @@ -16,19 +19,28 @@ import (
"fmt"
"log"
"net/http"
"net/url"
"os"
"strings"
"time"

"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/auth"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/modelcontextprotocol/go-sdk/oauthex"
"github.com/yosida95/uritemplate/v3"
)

var (
httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
enableAuth = flag.Bool("enable_auth", false, "if set, enable OAuth authorization")
)

const watchedResourceURI = "test://watched-resource"
const (
watchedResourceURI = "test://watched-resource"

adminScope = "admin"
)

func main() {
flag.Parse()
Expand Down Expand Up @@ -56,11 +68,29 @@ func main() {

// Serve over stdio, or streamable HTTP if -http is set.
if *httpAddr != "" {
handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
mux := http.NewServeMux()
var mcpHandler http.Handler = mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
return server
}, nil)
log.Printf("Conformance server listening at %s", *httpAddr)
log.Fatal(http.ListenAndServe(*httpAddr, handler))

if *enableAuth {
authServerURL := os.Getenv("MCP_CONFORMANCE_AUTH_SERVER_URL")
if authServerURL == "" {
log.Fatal("MCP_CONFORMANCE_AUTH_SERVER_URL environment variable must be set when --enable-auth is true")
}

handlePRM(mux, authServerURL)

var err error
mcpHandler, err = addAuthMiddleware(mcpHandler, authServerURL)
if err != nil {
log.Fatalf("auth middleware: %v", err)
}
}

mux.Handle("/mcp", mcpHandler)
log.Printf("Conformance server listening at http://%s/mcp", *httpAddr)
log.Fatal(http.ListenAndServe(*httpAddr, mux))
} else {
t := &mcp.StdioTransport{}
if err := server.Run(ctx, t); err != nil {
Expand Down Expand Up @@ -722,6 +752,96 @@ func promptWithImageHandler(ctx context.Context, req *mcp.GetPromptRequest) (*mc
}, nil
}

// =============================================================================
// Middleware
// =============================================================================

func handlePRM(mux *http.ServeMux, authServerURL string) {
// Host the resource metadata document.
resourceMetadata := &oauthex.ProtectedResourceMetadata{
Resource: "http://" + *httpAddr,
AuthorizationServers: []string{authServerURL},
ScopesSupported: []string{adminScope},
}
mux.Handle("/.well-known/oauth-protected-resource", auth.ProtectedResourceMetadataHandler(resourceMetadata))
}

func addAuthMiddleware(handler http.Handler, authServerURL string) (http.Handler, error) {

log.Printf("Fetching authorization server metadata from %s...", authServerURL)
metadata, err := oauthex.GetAuthServerMeta(context.Background(), authServerURL, http.DefaultClient)
if err != nil {
return nil, fmt.Errorf("fetch auth server metadata: %v", err)
}
if metadata.IntrospectionEndpoint == "" {
return nil, fmt.Errorf("auth server metadata does not contain introspection_endpoint")
}
log.Printf("Using introspection endpoint: %s", metadata.IntrospectionEndpoint)

tokenVerifier := createIntrospectionVerifier(metadata.IntrospectionEndpoint)
verifyAuth := auth.RequireBearerToken(tokenVerifier, &auth.RequireBearerTokenOptions{
ResourceMetadataURL: fmt.Sprintf("http://%s/.well-known/oauth-protected-resource", *httpAddr),
})

return verifyAuth(handler), nil
}

func createIntrospectionVerifier(introspectionEndpoint string) auth.TokenVerifier {
return func(ctx context.Context, token string, req *http.Request) (*auth.TokenInfo, error) {
data := url.Values{}
data.Set("token", token)

req, err := http.NewRequestWithContext(ctx, "POST", introspectionEndpoint, strings.NewReader(data.Encode()))
if err != nil {
return nil, fmt.Errorf("create introspection request: %v", err)
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("introspection request failed: %v", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("introspection returned status %d", resp.StatusCode)
}

var result struct {
Active bool `json:"active"`
Scope string `json:"scope"`
Expiration int64 `json:"exp"`
ClientID string `json:"client_id"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decode introspection response: %v", err)
}

if !result.Active {
return nil, auth.ErrInvalidToken
}

expiration := time.Time{}
if result.Expiration != 0 {
expiration = time.Unix(result.Expiration, 0)
}

var scopes []string
if result.Scope != "" {
scopes = strings.Split(result.Scope, " ")
}

return &auth.TokenInfo{
Scopes: scopes,
Expiration: expiration,
Extra: map[string]any{
"client_id": result.ClientID,
},
}, nil
}
}

// =============================================================================
// Server handlers
// =============================================================================
Expand Down
26 changes: 21 additions & 5 deletions scripts/conformance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ else
fi

# Build the conformance server.
go build -o "$WORKDIR/conformance-server" ./examples/server/conformance
go build -tags mcp_go_client_oauth -o "$WORKDIR/conformance-server" ./examples/server/conformance

# Start the server in the background
echo "Starting conformance server on port $PORT..."
"$WORKDIR/conformance-server" -http=":$PORT" &
"$WORKDIR/conformance-server" -http="localhost:$PORT" &
SERVER_PID=$!

echo "Server pid is $SERVER_PID"
Expand All @@ -92,15 +92,31 @@ for i in {1..30}; do
done

# Run conformance tests from the work directory to avoid writing results to the repo.
echo "Running conformance tests..."
echo "Running 'active' conformance tests..."
if [ -n "$CONFORMANCE_REPO" ]; then
# Run from local checkout using npm run start.
(cd "$WORKDIR" && \
npm --prefix "$CONFORMANCE_REPO" run start -- \
server --url "http://localhost:$PORT")
server --url "http://localhost:$PORT/mcp") || true
else
(cd "$WORKDIR" && \
npx @modelcontextprotocol/conformance@latest server --url "http://localhost:$PORT")
npx @modelcontextprotocol/conformance@latest server --url "http://localhost:$PORT/mcp") || true
fi

echo ""
if [ -n "$SERVER_PID" ]; then
kill "$SERVER_PID" 2>/dev/null || true
fi
echo "Running 'auth' conformance tests..."
if [ -n "$CONFORMANCE_REPO" ]; then
# Run from local checkout using npm run start.
(cd "$WORKDIR" && \
npm --prefix "$CONFORMANCE_REPO" run start -- \
server --suite auth --command "$WORKDIR/conformance-server --http=\"localhost:$PORT\" --enable_auth") || true
else
(cd "$WORKDIR" && \
npx @modelcontextprotocol/conformance@latest server --suite auth \
--command "$WORKDIR/conformance-server --http=\"localhost:$PORT\" --enable_auth") || true
fi

echo ""
Expand Down
Loading