Skip to content

Commit 4cdbaaf

Browse files
examples: add an example that display header forwarding. (#836)
Created to document the way it can be done discussed in #373. Fixes #373
1 parent 16d990b commit 4cdbaaf

File tree

2 files changed

+193
-0
lines changed

2 files changed

+193
-0
lines changed

examples/server/proxy/README.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# Proxy Header Propagation Example
2+
3+
This example demonstrates how to propagate HTTP headers from an incoming MCP server request to an outgoing MCP client request.
4+
5+
This is particularly useful when building a gateway or proxy server that needs to forward authentication tokens, trace IDs, or other context-sensitive headers to downstream services.
6+
7+
## Architecture
8+
9+
This example runs three components in a single process:
10+
11+
1. **Backend Server** (Port 8082):
12+
- Exposes a tool `echo_headers` that returns the headers it received.
13+
14+
2. **Proxy Server** (Port 8081):
15+
- Exposes a tool `forward_headers`.
16+
- Acts as a client to the Backend Server.
17+
- Uses a custom `http.RoundTripper` (`HeaderForwardingTransport`) to inject headers from the context into outgoing requests.
18+
19+
3. **Client**:
20+
- Connects to the Proxy Server.
21+
- Calls `forward_headers`.
22+
23+
## How it works
24+
25+
1. The Client calls `forward_headers` on the Proxy Server.
26+
2. The Proxy Server receives the request. The request context contains the HTTP headers in `req.Extra.Header`.
27+
3. The Proxy's tool handler extracts these headers and places them into a new `context.Context` using a specific key (`headerContextKey`).
28+
4. The Proxy uses an `mcp.Client` configured with a custom `HTTPClient` that uses `HeaderForwardingTransport`.
29+
5. `HeaderForwardingTransport.RoundTrip` inspects the context of outgoing requests. If it finds headers under `headerContextKey`, it adds them to the HTTP request.
30+
6. The Backend Server receives the request with the propagated headers.
31+
32+
## Running the Example
33+
34+
Run the example with:
35+
36+
```bash
37+
go run main.go
38+
```
39+
40+
You should see output indicating:
41+
1. Backend and Proxy servers starting.
42+
2. Gateway receiving headers.
43+
3. Client receiving the result, which contains the echoed headers.
44+
45+
Example output:
46+
47+
```
48+
2025/08/29 10:00:00 Starting Backend Server on :8082
49+
2025/08/29 10:00:00 Starting Gateway Server on :8081
50+
2025/08/29 10:00:00 Gateway received headers: map[...]
51+
2025/08/29 10:00:00 Client received result: ...
52+
```

examples/server/proxy/main.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package main
6+
7+
import (
8+
"context"
9+
"fmt"
10+
"log"
11+
"net/http"
12+
"time"
13+
14+
"github.com/modelcontextprotocol/go-sdk/mcp"
15+
)
16+
17+
// Define a context key for passing headers.
18+
type contextKey string
19+
20+
const headerContextKey contextKey = "ctx-headers"
21+
22+
// HeaderForwardingTransport is an http.RoundTripper that injects headers
23+
// from the context into the outgoing request.
24+
type HeaderForwardingTransport struct{}
25+
26+
// RoundTrip executes a single HTTP transaction, adding headers from the context.
27+
func (h *HeaderForwardingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
28+
// Clone the request to avoid modifying the original one.
29+
newReq := req.Clone(req.Context())
30+
31+
// Retrieve headers from the context.
32+
if headers, ok := req.Context().Value(headerContextKey).(http.Header); ok {
33+
for key, values := range headers {
34+
// Copy all values for each header.
35+
for _, value := range values {
36+
newReq.Header.Add(key, value)
37+
}
38+
}
39+
}
40+
41+
return http.DefaultTransport.RoundTrip(newReq)
42+
}
43+
44+
// NewHeaderForwardingClient creates a new http.Client that uses HeaderForwardingTransport.
45+
func NewHeaderForwardingClient() *http.Client {
46+
return &http.Client{
47+
Transport: new(HeaderForwardingTransport),
48+
}
49+
}
50+
51+
// Backend Server: Echoes received headers to verify propagation.
52+
func runBackendServer() {
53+
server := mcp.NewServer(&mcp.Implementation{Name: "backend-server", Version: "1.0.0"}, nil)
54+
55+
mcp.AddTool(server, &mcp.Tool{
56+
Name: "echo_headers",
57+
Description: "Returns the headers received by the server",
58+
}, func(ctx context.Context, req *mcp.CallToolRequest, args struct{}) (*mcp.CallToolResult, any, error) {
59+
return nil, req.Extra.Header, nil
60+
})
61+
62+
// Start the backend server on port 8082.
63+
log.Println("Starting Backend Server on :8082")
64+
if err := http.ListenAndServe(":8082", mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
65+
return server
66+
}, nil)); err != nil {
67+
log.Fatal(err)
68+
}
69+
}
70+
71+
// Proxy Server: Forwards requests to the backend with headers.
72+
func runProxyServer(ctx context.Context) {
73+
// Connect to the backend server (acting as a client).
74+
client := mcp.NewClient(&mcp.Implementation{Name: "proxy-client", Version: "1.0.0"}, nil)
75+
clientSession, err := client.Connect(ctx, &mcp.StreamableClientTransport{
76+
Endpoint: "http://localhost:8082/mcp",
77+
HTTPClient: NewHeaderForwardingClient(),
78+
}, nil)
79+
if err != nil {
80+
log.Fatalf("Failed to connect to backend: %v", err)
81+
}
82+
defer clientSession.Close()
83+
84+
server := mcp.NewServer(&mcp.Implementation{Name: "proxy-server", Version: "1.0.0"}, nil)
85+
// Add a tool that forwards the call to the backend.
86+
mcp.AddTool(server, &mcp.Tool{
87+
Name: "forward_headers",
88+
Description: "Calls the backend server, ensuring headers are propagated",
89+
}, func(ctx context.Context, req *mcp.CallToolRequest, args struct{}) (*mcp.CallToolResult, any, error) {
90+
incomingHeaders := req.Extra.Header
91+
log.Printf("Gateway received headers: %v", incomingHeaders)
92+
propagateCtx := context.WithValue(ctx, headerContextKey, incomingHeaders)
93+
result, err := clientSession.CallTool(propagateCtx, &mcp.CallToolParams{
94+
Name: "echo_headers",
95+
})
96+
if err != nil {
97+
return nil, nil, fmt.Errorf("failed to call backend: %w", err)
98+
}
99+
100+
return result, nil, nil
101+
})
102+
103+
// Start the gateway server on port 8081.
104+
log.Println("Starting Gateway Server on :8081")
105+
if err := http.ListenAndServe(":8081", mcp.NewStreamableHTTPHandler(func(r *http.Request) *mcp.Server {
106+
return server
107+
}, nil)); err != nil {
108+
log.Fatal(err)
109+
}
110+
}
111+
112+
func runClient(ctx context.Context) {
113+
// Connect to the proxy server (acting as a client).
114+
client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "1.0.0"}, nil)
115+
clientSession, err := client.Connect(ctx, &mcp.StreamableClientTransport{
116+
Endpoint: "http://localhost:8081/mcp",
117+
}, nil)
118+
if err != nil {
119+
log.Fatalf("Failed to connect to proxy: %v", err)
120+
}
121+
defer clientSession.Close()
122+
123+
result, err := clientSession.CallTool(ctx, &mcp.CallToolParams{
124+
Name: "forward_headers",
125+
})
126+
if err != nil {
127+
log.Fatalf("Failed to call proxy: %v", err)
128+
}
129+
log.Printf("Client received result: %v", result.StructuredContent)
130+
}
131+
132+
func main() {
133+
ctx := context.Background()
134+
go runBackendServer()
135+
// Give the backend a moment to start.
136+
time.Sleep(100 * time.Millisecond)
137+
go runProxyServer(ctx)
138+
// Give the proxy a moment to start.
139+
time.Sleep(100 * time.Millisecond)
140+
runClient(ctx)
141+
}

0 commit comments

Comments
 (0)