Skip to content

Commit 45b31af

Browse files
committed
examples: adjust conformance server for auth tests
1 parent 3381035 commit 45b31af

File tree

2 files changed

+146
-10
lines changed

2 files changed

+146
-10
lines changed

examples/server/conformance/main.go

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
// The conformance server implements features required for MCP conformance testing.
66
// It mirrors the functionality of the TypeScript conformance server at
77
// https://github.com/modelcontextprotocol/conformance/blob/main/examples/servers/typescript/everything-server.ts
8+
9+
//go:build mcp_go_client_oauth
10+
811
package main
912

1013
import (
@@ -16,19 +19,28 @@ import (
1619
"fmt"
1720
"log"
1821
"net/http"
22+
"net/url"
1923
"os"
24+
"strings"
2025
"time"
2126

2227
"github.com/google/jsonschema-go/jsonschema"
28+
"github.com/modelcontextprotocol/go-sdk/auth"
2329
"github.com/modelcontextprotocol/go-sdk/mcp"
30+
"github.com/modelcontextprotocol/go-sdk/oauthex"
2431
"github.com/yosida95/uritemplate/v3"
2532
)
2633

2734
var (
28-
httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
35+
httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
36+
enableAuth = flag.Bool("enable_auth", false, "if set, enable OAuth authorization")
2937
)
3038

31-
const watchedResourceURI = "test://watched-resource"
39+
const (
40+
watchedResourceURI = "test://watched-resource"
41+
42+
adminScope = "admin"
43+
)
3244

3345
func main() {
3446
flag.Parse()
@@ -56,11 +68,29 @@ func main() {
5668

5769
// Serve over stdio, or streamable HTTP if -http is set.
5870
if *httpAddr != "" {
59-
handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
71+
mux := http.NewServeMux()
72+
var mcpHandler http.Handler = mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
6073
return server
6174
}, nil)
62-
log.Printf("Conformance server listening at %s", *httpAddr)
63-
log.Fatal(http.ListenAndServe(*httpAddr, handler))
75+
76+
if *enableAuth {
77+
authServerURL := os.Getenv("MCP_CONFORMANCE_AUTH_SERVER_URL")
78+
if authServerURL == "" {
79+
log.Fatal("MCP_CONFORMANCE_AUTH_SERVER_URL environment variable must be set when --enable-auth is true")
80+
}
81+
82+
handlePRM(mux, authServerURL)
83+
84+
var err error
85+
mcpHandler, err = addAuthMiddleware(mcpHandler, authServerURL)
86+
if err != nil {
87+
log.Fatalf("auth middleware: %v", err)
88+
}
89+
}
90+
91+
mux.Handle("/", mcpHandler)
92+
log.Printf("Conformance server listening at http://%s/mcp", *httpAddr)
93+
log.Fatal(http.ListenAndServe(*httpAddr, mux))
6494
} else {
6595
t := &mcp.StdioTransport{}
6696
if err := server.Run(ctx, t); err != nil {
@@ -722,6 +752,96 @@ func promptWithImageHandler(ctx context.Context, req *mcp.GetPromptRequest) (*mc
722752
}, nil
723753
}
724754

755+
// =============================================================================
756+
// Middleware
757+
// =============================================================================
758+
759+
func handlePRM(mux *http.ServeMux, authServerURL string) {
760+
// Host the resource metadata document.
761+
resourceMetadata := &oauthex.ProtectedResourceMetadata{
762+
Resource: "http://" + *httpAddr,
763+
AuthorizationServers: []string{authServerURL},
764+
ScopesSupported: []string{adminScope},
765+
}
766+
mux.Handle("/.well-known/oauth-protected-resource", auth.ProtectedResourceMetadataHandler(resourceMetadata))
767+
}
768+
769+
func addAuthMiddleware(handler http.Handler, authServerURL string) (http.Handler, error) {
770+
771+
log.Printf("Fetching authorization server metadata from %s...", authServerURL)
772+
metadata, err := oauthex.GetAuthServerMeta(context.Background(), authServerURL, http.DefaultClient)
773+
if err != nil {
774+
return nil, fmt.Errorf("fetch auth server metadata: %v", err)
775+
}
776+
if metadata.IntrospectionEndpoint == "" {
777+
return nil, fmt.Errorf("auth server metadata does not contain introspection_endpoint")
778+
}
779+
log.Printf("Using introspection endpoint: %s", metadata.IntrospectionEndpoint)
780+
781+
tokenVerifier := createIntrospectionVerifier(metadata.IntrospectionEndpoint)
782+
verifyAuth := auth.RequireBearerToken(tokenVerifier, &auth.RequireBearerTokenOptions{
783+
ResourceMetadataURL: fmt.Sprintf("http://%s/.well-known/oauth-protected-resource", *httpAddr),
784+
})
785+
786+
return verifyAuth(handler), nil
787+
}
788+
789+
func createIntrospectionVerifier(introspectionEndpoint string) auth.TokenVerifier {
790+
return func(ctx context.Context, token string, req *http.Request) (*auth.TokenInfo, error) {
791+
data := url.Values{}
792+
data.Set("token", token)
793+
794+
req, err := http.NewRequestWithContext(ctx, "POST", introspectionEndpoint, strings.NewReader(data.Encode()))
795+
if err != nil {
796+
return nil, fmt.Errorf("create introspection request: %v", err)
797+
}
798+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
799+
req.Header.Set("Accept", "application/json")
800+
801+
resp, err := http.DefaultClient.Do(req)
802+
if err != nil {
803+
return nil, fmt.Errorf("introspection request failed: %v", err)
804+
}
805+
defer resp.Body.Close()
806+
807+
if resp.StatusCode != http.StatusOK {
808+
return nil, fmt.Errorf("introspection returned status %d", resp.StatusCode)
809+
}
810+
811+
var result struct {
812+
Active bool `json:"active"`
813+
Scope string `json:"scope"`
814+
Expiration int64 `json:"exp"`
815+
ClientID string `json:"client_id"`
816+
}
817+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
818+
return nil, fmt.Errorf("decode introspection response: %v", err)
819+
}
820+
821+
if !result.Active {
822+
return nil, auth.ErrInvalidToken
823+
}
824+
825+
expiration := time.Time{}
826+
if result.Expiration != 0 {
827+
expiration = time.Unix(result.Expiration, 0)
828+
}
829+
830+
var scopes []string
831+
if result.Scope != "" {
832+
scopes = strings.Split(result.Scope, " ")
833+
}
834+
835+
return &auth.TokenInfo{
836+
Scopes: scopes,
837+
Expiration: expiration,
838+
Extra: map[string]any{
839+
"client_id": result.ClientID,
840+
},
841+
}, nil
842+
}
843+
}
844+
725845
// =============================================================================
726846
// Server handlers
727847
// =============================================================================

scripts/conformance.sh

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,11 @@ else
6868
fi
6969

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

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

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

9494
# Run conformance tests from the work directory to avoid writing results to the repo.
95-
echo "Running conformance tests..."
95+
echo "Running 'active' conformance tests..."
9696
if [ -n "$CONFORMANCE_REPO" ]; then
9797
# Run from local checkout using npm run start.
9898
(cd "$WORKDIR" && \
9999
npm --prefix "$CONFORMANCE_REPO" run start -- \
100-
server --url "http://localhost:$PORT")
100+
server --url "http://localhost:$PORT") || true
101101
else
102102
(cd "$WORKDIR" && \
103-
npx @modelcontextprotocol/conformance@latest server --url "http://localhost:$PORT")
103+
npx @modelcontextprotocol/conformance@latest server --url "http://localhost:$PORT") || true
104+
fi
105+
106+
echo ""
107+
if [ -n "$SERVER_PID" ]; then
108+
kill "$SERVER_PID" 2>/dev/null || true
109+
fi
110+
echo "Running 'auth' conformance tests..."
111+
if [ -n "$CONFORMANCE_REPO" ]; then
112+
# Run from local checkout using npm run start.
113+
(cd "$WORKDIR" && \
114+
npm --prefix "$CONFORMANCE_REPO" run start -- \
115+
server --suite auth --command "$WORKDIR/conformance-server --http=\"localhost:$PORT\" --enable_auth") || true
116+
else
117+
(cd "$WORKDIR" && \
118+
npx @modelcontextprotocol/conformance@latest server --suite auth \
119+
--command "$WORKDIR/conformance-server --http=\"localhost:$PORT\" --enable_auth") || true
104120
fi
105121

106122
echo ""

0 commit comments

Comments
 (0)