Skip to content

Commit 3a9cbd0

Browse files
committed
examples: add an example to display client OAuth.
1 parent 5be070b commit 3a9cbd0

5 files changed

Lines changed: 299 additions & 1 deletion

File tree

auth/auth.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ func verify(req *http.Request, verifier TokenVerifier, opts *RequireBearerTokenO
106106
}
107107
return nil, err.Error(), http.StatusInternalServerError
108108
}
109+
if tokenInfo == nil {
110+
return nil, "token validation failed", http.StatusInternalServerError
111+
}
109112

110113
// Check scopes. All must be present.
111114
if opts != nil {

auth/authorization_code.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,10 +155,12 @@ func (h *AuthorizationCodeOAuthHandler) Authorize(ctx context.Context, req *http
155155
}
156156
log.Printf("Failed to get protected resource metadata from %q: %v", url, err)
157157
}
158+
// log.Printf("Protected resource metadata: %+v", prm)
158159
asm, err := h.getAuthServerMetadata(ctx, prm, resourceURL)
159160
if err != nil {
160161
return err
161162
}
163+
// log.Printf("Authorization server metadata: %+v", asm)
162164

163165
if err := h.handleRegistration(ctx, asm); err != nil {
164166
return err
@@ -202,6 +204,7 @@ func (h *AuthorizationCodeOAuthHandler) FinalizeAuthorization(code, state string
202204
return nil
203205
}
204206

207+
// TODO: validate on creation.
205208
func (h *AuthorizationCodeOAuthHandler) validate() error {
206209
if h.ClientIDMetadataDocumentConfig == nil &&
207210
h.PreregisteredClientConfig == nil &&
@@ -267,7 +270,6 @@ func (h *AuthorizationCodeOAuthHandler) getAuthServerMetadata(ctx context.Contex
267270
if err != nil {
268271
return nil, fmt.Errorf("failed to get authorization server metadata: %w", err)
269272
}
270-
log.Print("Authorization server medatada fetched")
271273
if asm == nil {
272274
log.Print("Authorization server metadata not found, using fallback")
273275
// Fallback to 2025-03-26 spec: predefined endpoints.

examples/auth/client/main.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2026 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by the license
3+
// that can be found in the LICENSE file.
4+
5+
//go:build mcp_go_client_oauth
6+
7+
package main
8+
9+
import (
10+
"context"
11+
"errors"
12+
"flag"
13+
"fmt"
14+
"log"
15+
"net/http"
16+
17+
"github.com/modelcontextprotocol/go-sdk/auth"
18+
"github.com/modelcontextprotocol/go-sdk/mcp"
19+
)
20+
21+
// Flags.
22+
var (
23+
serverURL = flag.String("server_url", "http://localhost:8000/mcp", "Server URL")
24+
)
25+
26+
// Configuration required for this example.
27+
var (
28+
clientID = ""
29+
clientSecret = ""
30+
)
31+
32+
type authResult struct {
33+
code string
34+
state string
35+
err error
36+
}
37+
38+
type codeReceiver struct {
39+
authChan chan authResult
40+
server *http.Server
41+
}
42+
43+
func (r *codeReceiver) startAuthorizationFlow(ctx context.Context, authorizationURL string) error {
44+
mux := http.NewServeMux()
45+
mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
46+
code := req.URL.Query().Get("code")
47+
state := req.URL.Query().Get("state")
48+
if code == "" {
49+
http.Error(w, "authorization code not found", http.StatusBadRequest)
50+
return
51+
}
52+
53+
r.authChan <- authResult{
54+
code: code,
55+
state: state,
56+
}
57+
fmt.Fprint(w, "Authentication successful. You can close this window.")
58+
})
59+
60+
r.server = &http.Server{
61+
Addr: "localhost:3142",
62+
Handler: mux,
63+
}
64+
65+
go func() {
66+
// We ignore ErrServerClosed as it is returned on Shutdown.
67+
if err := r.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
68+
r.authChan <- authResult{err: fmt.Errorf("server error: %w", err)}
69+
}
70+
}()
71+
72+
fmt.Printf("Please authorize by visiting: %s\n", authorizationURL)
73+
return nil
74+
}
75+
76+
func main() {
77+
flag.Parse()
78+
client := mcp.NewClient(&mcp.Implementation{
79+
Name: "test-client",
80+
Version: "1.0.0",
81+
}, nil)
82+
83+
receiver := &codeReceiver{
84+
authChan: make(chan authResult),
85+
}
86+
87+
authHandler := &auth.AuthorizationCodeOAuthHandler{
88+
RedirectURL: "http://localhost:3142",
89+
PreregisteredClientConfig: &auth.PreregisteredClientConfig{
90+
ClientID: clientID,
91+
ClientSecret: clientSecret,
92+
},
93+
AuthorizationURLHandler: receiver.startAuthorizationFlow,
94+
}
95+
96+
transport := &mcp.StreamableClientTransport{
97+
Endpoint: *serverURL,
98+
OAuthHandler: authHandler,
99+
}
100+
101+
ctx := context.Background()
102+
var session *mcp.ClientSession
103+
var err error
104+
105+
for {
106+
session, err = client.Connect(ctx, transport, nil)
107+
if err == nil {
108+
break
109+
}
110+
// If the error is ErrRedirected, it means the authorization flow has started
111+
// and we need to wait for the code.
112+
if errors.Is(err, auth.ErrRedirected) {
113+
fmt.Println("Authorization flow started. Waiting for authorization code...")
114+
res := <-receiver.authChan
115+
if res.err != nil {
116+
log.Fatalf("Authorization failed: %v", res.err)
117+
}
118+
119+
// Shutdown the temporary server
120+
if err := receiver.server.Shutdown(ctx); err != nil {
121+
log.Printf("Failed to shutdown server: %v", err)
122+
}
123+
124+
if err := authHandler.FinalizeAuthorization(res.code, res.state); err != nil {
125+
log.Fatalf("Failed to finalize authorization: %v", err)
126+
}
127+
continue
128+
}
129+
log.Fatalf("client.Connect(): %v", err)
130+
}
131+
defer session.Close()
132+
133+
if _, err := session.ListTools(ctx, nil); err != nil {
134+
log.Fatalf("session.ListTools(): %v", err)
135+
}
136+
}

examples/auth/server/main.go

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
// Copyright 2026 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by the license
3+
// that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"context"
9+
"encoding/json"
10+
"flag"
11+
"fmt"
12+
"log"
13+
"net/http"
14+
"net/http/httputil"
15+
"net/url"
16+
"strings"
17+
"time"
18+
19+
"github.com/modelcontextprotocol/go-sdk/auth"
20+
"github.com/modelcontextprotocol/go-sdk/mcp"
21+
"github.com/modelcontextprotocol/go-sdk/oauthex"
22+
)
23+
24+
// Flags.
25+
var (
26+
port = flag.Int("port", 8000, "Port to listen on")
27+
)
28+
29+
// Configuration required for this example.
30+
var (
31+
// Authorization server to return in the protected resource metadata.
32+
authorizationServer = ""
33+
// Introspection endpoint for verifying tokens.
34+
introspectionEndpoint = ""
35+
// Client credentials used in the introspection request.
36+
clientID = ""
37+
clientSecret = ""
38+
)
39+
40+
func verifyToken(ctx context.Context, token string, _ *http.Request) (*auth.TokenInfo, error) {
41+
data := url.Values{}
42+
data.Set("token", token)
43+
data.Set("token_type_hint", "access_token")
44+
45+
req, err := http.NewRequestWithContext(ctx, "POST", introspectionEndpoint, strings.NewReader(data.Encode()))
46+
if err != nil {
47+
return nil, err
48+
}
49+
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
50+
req.Header.Set("Accept", "application/json")
51+
req.SetBasicAuth(clientID, clientSecret)
52+
53+
resp, err := http.DefaultClient.Do(req)
54+
if err != nil {
55+
return nil, err
56+
}
57+
defer resp.Body.Close()
58+
59+
if resp.StatusCode != http.StatusOK {
60+
dump, _ := httputil.DumpResponse(resp, true)
61+
log.Printf("Introspection failed: %s", dump)
62+
return nil, fmt.Errorf("introspection failed with status %d", resp.StatusCode)
63+
}
64+
65+
var result struct {
66+
Active bool `json:"active"`
67+
Scope string `json:"scope"`
68+
Exp int64 `json:"exp"`
69+
Sub string `json:"sub"`
70+
}
71+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
72+
return nil, err
73+
}
74+
75+
if !result.Active {
76+
return nil, auth.ErrInvalidToken
77+
}
78+
79+
return &auth.TokenInfo{
80+
Scopes: strings.Fields(result.Scope),
81+
Expiration: time.Unix(result.Exp, 0),
82+
UserID: result.Sub,
83+
}, nil
84+
}
85+
86+
func main() {
87+
flag.Parse()
88+
metadata := &oauthex.ProtectedResourceMetadata{
89+
Resource: fmt.Sprintf("http://localhost:%d/mcp", *port),
90+
AuthorizationServers: []string{authorizationServer},
91+
ScopesSupported: []string{"read"},
92+
}
93+
http.Handle("/.well-known/oauth-protected-resource", auth.ProtectedResourceMetadataHandler(metadata))
94+
95+
server := mcp.NewServer(&mcp.Implementation{
96+
Name: "test-server",
97+
Version: "1.0.0",
98+
}, nil)
99+
server.AddReceivingMiddleware(createLoggingMiddleware())
100+
101+
handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server {
102+
return server
103+
}, nil)
104+
105+
authMiddleware := auth.RequireBearerToken(verifyToken, &auth.RequireBearerTokenOptions{
106+
Scopes: []string{"read"},
107+
ResourceMetadataURL: fmt.Sprintf("http://localhost:%d/.well-known/oauth-protected-resource", *port),
108+
})
109+
110+
http.Handle("/mcp", authMiddleware(handler))
111+
112+
log.Printf("Starting server on http://localhost:%d", *port)
113+
log.Fatal(http.ListenAndServe(fmt.Sprintf("localhost:%d", *port), nil))
114+
}
115+
116+
// createLoggingMiddleware creates an MCP middleware that logs method calls.
117+
func createLoggingMiddleware() mcp.Middleware {
118+
return func(next mcp.MethodHandler) mcp.MethodHandler {
119+
return func(
120+
ctx context.Context,
121+
method string,
122+
req mcp.Request,
123+
) (mcp.Result, error) {
124+
start := time.Now()
125+
sessionID := req.GetSession().ID()
126+
127+
// Log request details.
128+
log.Printf("[REQUEST] Session: %s | Method: %s",
129+
sessionID,
130+
method)
131+
132+
// Call the actual handler.
133+
result, err := next(ctx, method, req)
134+
135+
// Log response details.
136+
duration := time.Since(start)
137+
138+
if err != nil {
139+
log.Printf("[RESPONSE] Session: %s | Method: %s | Status: ERROR | Duration: %v | Error: %v",
140+
sessionID,
141+
method,
142+
duration,
143+
err)
144+
} else {
145+
log.Printf("[RESPONSE] Session: %s | Method: %s | Status: OK | Duration: %v",
146+
sessionID,
147+
method,
148+
duration)
149+
}
150+
151+
return result, err
152+
}
153+
}
154+
}

oauthex/auth_meta.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"context"
1414
"errors"
1515
"fmt"
16+
"log"
1617
"net/http"
1718
"net/url"
1819
"strings"
@@ -134,6 +135,7 @@ func GetAuthServerMeta(ctx context.Context, issuerURL string, c *http.Client) (*
134135
for _, u := range AuthorizationServerMetadataURLs(issuerURL) {
135136
asm, err := getJSON[AuthServerMeta](ctx, c, u, 1<<20)
136137
if err != nil {
138+
log.Printf("Failed to get auth server metadata from %q: %v", u, err)
137139
var httpErr *httpStatusError
138140
if errors.As(err, &httpErr) {
139141
if 400 <= httpErr.StatusCode && httpErr.StatusCode < 500 {
@@ -156,6 +158,7 @@ func GetAuthServerMeta(ctx context.Context, issuerURL string, c *http.Client) (*
156158
if err := validateAuthServerMetaURLs(asm); err != nil {
157159
return nil, err
158160
}
161+
log.Printf("Fetched authorization server metadata from %q", u)
159162

160163
return asm, nil
161164
}

0 commit comments

Comments
 (0)