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

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