go-sdk icon indicating copy to clipboard operation
go-sdk copied to clipboard

Add client examples for streamable http

Open rdmcguire opened this issue 4 months ago • 5 comments

It would be useful to have a client example for connecting to a streamable http MCP server. A few items that would be great to include:

  • TLS
  • Auth / Custom Headers
  • Middelware

While my basic usage may be wrong, I couldn't find a way to perform even a basic operation using StreamableClientTransport without adding a RoundTripper to add req.Header.Set("Accept", "text/event-stream,application/json").

This seems like something the client should handle, so perhaps I'm using the package wrong (thus the thought that examples may help).

Here is my current (presumably incorrect) usage, which required the roundtripper to make any calls:

func (a *AIWorkerMCP) TestGitToolHandler(ctx context.Context) (*mcp.CallToolResult, error) {
	client := mcp.NewClient(AIWorkerMCPImpl, &mcp.ClientOptions{})

	session, err := client.Connect(ctx, &mcp.StreamableClientTransport{
		Endpoint:   a.getHost() + "/api/mcp/",
		HTTPClient: customHTTPClient,
	}, nil)
	if err != nil {
		return nil, err
	}
	defer session.Close()

	return session.CallTool(ctx, &mcp.CallToolParams{
		Name: "aiworkermcp-git",
		Arguments: &GitToolArgs{
			Repo:         "git@someremote:somerepo.git",
			SourceBranch: "development",
			TargetBranch: "main",
		},
	})
}

type acceptHeaderRoundTripper struct {
	delegate http.RoundTripper
}

func (rt *acceptHeaderRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	req.Header.Set("Accept", "text/event-stream,application/json")
	return rt.delegate.RoundTrip(req)
}

var customHTTPClient = &http.Client{
	Transport: &acceptHeaderRoundTripper{
		delegate: http.DefaultTransport,
	},
}

rdmcguire avatar Aug 25 '25 13:08 rdmcguire

@rdmcguire Looks like examples/http was added since this issue was opened.

Does this solve your case? If so, I think this issue can be closed.

frenchi avatar Sep 12 '25 05:09 frenchi

examples/http is a very useful start, but it's still unclear to me how to easily set HTTP request headers on the client, for instance, to submit an Authorization token.

fgrosse avatar Sep 12 '25 12:09 fgrosse

The design does indeed require a custom http.Client, and that requires a custom http.Transport. Yes, it's annoying and tedious. Yes, we should do something better. If you're getting the token via OAuth, there is a little more help in the auth package. If you're getting it on your own, then what you wrote is right.

But why do you need to set the Accept header anyway? Don't we already do that?

jba avatar Sep 16 '25 17:09 jba

I'm mainly after the use case of setting the Authorization header to authenticate with an MCP server. This is a very common use case, and if we added a better API, it would probably help a lot of people and reduce the number of questions in the future.

The auth package only seems to provide helper functions for the server, not the client. Even if it contained a helper to wrap a round-tripper, it would still be more difficult to discover and understand than I would like.

Regarding the use of a round tripper, I was wondering if this isn't also encouraging bad patterns. At least, we seem to directly be in conflict with the documentation of the Go standard library regarding how to implement an http.RoundTripper: https://github.com/golang/go/blob/master/src/net/http/client.go#L128-L132

// RoundTrip should not modify the request, except for
// consuming and closing the Request's Body. [...]

But I understand that this specific pattern of implementing an HTTP client middleware with a round tripper seems to be used in more places already so maybe that's ok 🤷 .

A better alternative would be to use an interface for the client instead of a concrete *http.Client:

// A Client sends http.Requests and returns http.Responses or errors in case of failure.
// Responses with StatusCode >= 400 are *not* considered a failure.
type Client interface {
	Do(*http.Request) (*http.Response, error)
}

This way, users can choose how they implement middlewares and they can perfectly implement one without learning about http.RoundTripper (or keep doing that if they want to).

There is a good implementation of HTTP middlewares here: https://github.com/classmarkets/cmhttp (I'm an author) which I have used ever since in one way or another. I'm not saying this project should use it but if you could use an interface it would enable other people to use that :)

fgrosse avatar Sep 19 '25 13:09 fgrosse

Just to illustrate the idea: maybe it's not too late for another breaking change? 😅

https://github.com/modelcontextprotocol/go-sdk/compare/main...fgrosse:go-sdk:fgrosse/http-client

fgrosse avatar Sep 19 '25 14:09 fgrosse