Skip to content
Open
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
45 changes: 45 additions & 0 deletions mcp/linker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2026 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by the license
// that can be found in the LICENSE file.

package mcp_test

import (
"os/exec"
"path/filepath"
"strings"
"testing"
)

// TestStdioServerDoesNotLinkTLS builds a stdio-only server and checks that
// the linker drops the TLS client. A package-level initializer that touches
// http.Transport would pull it back in for every program importing mcp.
func TestStdioServerDoesNotLinkTLS(t *testing.T) {
goTool, err := exec.LookPath("go")
if err != nil {
t.Skip("go tool not found")
}
bin := filepath.Join(t.TempDir(), "hello")
if out, err := exec.Command(goTool, "build", "-o", bin, "../examples/server/hello").CombinedOutput(); err != nil {
t.Fatalf("go build: %v\n%s", err, out)
}
out, err := exec.Command(goTool, "tool", "nm", bin).CombinedOutput()
if err != nil {
t.Fatalf("go tool nm: %v\n%s", err, out)
}
wantSym := map[string]bool{
// Sanity check that nm read the server binary.
"github.com/modelcontextprotocol/go-sdk/mcp.(*Server).Run": true,
// The exported Handshake is inlined away, so check its callee.
"crypto/tls.(*Conn).clientHandshake": false,
}
for sym, want := range wantSym {
got := strings.Contains(string(out), sym)
if want && !got {
t.Errorf("symbol %s not found; is this the right binary?", sym)
}
if !want && got {
t.Errorf("stdio-only server links %s; a package initializer likely reaches http.Transport (see go build -ldflags=-dumpdep)", sym)
}
}
}
28 changes: 19 additions & 9 deletions oauthex/oauth2.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,25 @@ import (

const maxDiscoveryRedirects = 10

// Build this on first use, not when the package loads. Building it checks the
// proxy via http.ProxyFromEnvironment, and net/http only reads the proxy
// environment once and then remembers it for the whole run. If that happened
// at import time, it would lock in the proxy setting before the app had a
// chance to set HTTPS_PROXY. sync.OnceValue waits until first use and still
// builds the transport just once. See #1276.
var defaultDiscoveryTransport = sync.OnceValue(func() http.RoundTripper {
return newDiscoveryTransport(http.DefaultTransport)
})
var (
defaultDiscoveryTransportOnce sync.Once
defaultDiscoveryTransportValue http.RoundTripper
)

// defaultDiscoveryTransport is built on first use, not at package init:
// building it consults http.ProxyFromEnvironment, which net/http caches for
// the whole run, so doing it at import time would freeze the proxy settings
// before main could set HTTPS_PROXY (#1276). It is a function rather than a
// package-level sync.OnceValue because a package initializer is a linker
// root: a OnceValue var kept http.Transport.Clone, and through it the HTTP/2
// client and crypto/tls, in every program importing mcp, including stdio-only
// servers that never make an HTTP request.
func defaultDiscoveryTransport() http.RoundTripper {
defaultDiscoveryTransportOnce.Do(func() {
defaultDiscoveryTransportValue = newDiscoveryTransport(http.DefaultTransport)
})
return defaultDiscoveryTransportValue
}

type httpStatusError struct {
StatusCode int
Expand Down