- Go 99.7%
- Shell 0.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
Fixes #937. ### What this changes Adds a single line — an SSE comment (`: ok\n\n`) — after `WriteHeader` and before `Flush` on the standalone SSE GET stream. SSE comments are explicitly ignored by clients (any line starting with `:` per the SSE spec), but they produce an HTTP/2 DATA frame, which is what HTTP/2 reverse proxies need before they will forward the HEADERS frame. ### Why the existing fix isn't sufficient Issue #410 was closed by #413, which added `WriteHeader(StatusOK)` + `Flush()` to the same site. #870 refactored the `Flush()` to use `http.NewResponseController`. Both correctly address HTTP/1.1: `Flush()` writes the headers as text on the TCP stream and any HTTP/1.1-aware proxy forwards them. In HTTP/2, headers and body are separate frame types (HEADERS, DATA). Reverse proxies batch them for efficiency and won't forward the HEADERS frame on its own. `Flush()` on the response writer only pushes the in-process buffer to the HTTP/2 stack; the proxy still holds the HEADERS frame waiting for DATA. The standalone SSE stream never sends body data on connect (the whole point — it's the long-poll listener for server-pushed notifications), so without a deliberate "kick" the headers never reach the client until the proxy tears down the stream on timeout. The SSE-comment trick is the established mitigation for SSE servers behind HTTP/2 proxies. It's used in Caddy (https://github.com/caddyserver/caddy/issues/4247), referenced in the Go issue tracker (https://github.com/golang/go/issues/31125), and is independent of the proxy implementation. ### The diff ```go if s.id == "" { // Issue #410: the standalone SSE stream is likely not to receive messages // for a long time. Ensure that headers are flushed. + // + // On HTTP/2, headers and body travel as separate frames (HEADERS and + // DATA). Reverse proxies (e.g. Envoy, Caddy, net/http/httputil) + // commonly buffer the HEADERS frame until they have a DATA frame to + // coalesce it with — there is no HTTP/2 equivalent of HTTP/1.1's + // Transfer-Encoding: chunked signal that says "this is streaming, send + // headers now". Calling Flush() alone is not sufficient: it pushes + // the kernel buffer to the proxy, but the proxy still holds the + // HEADERS frame. + // + // Write an SSE comment (lines starting with ":" are ignored by + // clients per RFC) so a DATA frame is produced, which forces the + // proxy to forward both frames. See: + // https://github.com/golang/go/issues/31125 + // https://github.com/caddyserver/caddy/issues/4247 w.WriteHeader(http.StatusOK) + fmt.Fprint(w, ": ok\n\n") rc := http.NewResponseController(w) // Ignore returned error as flushing is best-effort. _ = rc.Flush() } ``` `fmt` is already imported in this file; no other dependencies change. ### Test Added `TestStandaloneSSEEmitsCommentForHTTP2Flush`. It initializes a streamable session via raw POST, then opens the standalone SSE GET and asserts the first body byte starts with `:` (the SSE comment marker, which is what produces the DATA frame HTTP/2 proxies need). The test catches the bug behaviorally without requiring a full HTTP/2 reverse-proxy setup: it verifies the SDK's contract (emit body bytes after headers on the standalone SSE stream). HTTP/2-specific behavior is then a property of any conforming proxy. I confirmed the test fails on `main` without the patch — it times out at 2s waiting for the first body byte — and passes in <1ms with the patch. Full streamable test suite still passes. ### Reproduction matrix (from #937) | Configuration | TTFB | Result | |---|---|---| | HTTP/2 + 30s request timeout | ~31s | Headers only on stream tear-down | | HTTP/2 + no request timeout | never | Headers never arrive | | HTTP/1.1 + 30s request timeout | ~300ms | Headers immediate | | HTTP/1.1 + no request timeout | ~270ms | Headers immediate | | HTTP/2 POST (response has body) | ~350ms | Works (DATA frame coalesces) | | **HTTP/2 + this PR** | ~1ms | Headers immediate | ### Risk - Behavior-preserving for HTTP/1.1 clients (a leading `:` line is a valid SSE comment and is ignored by every conforming SSE client). - No protocol-level concern: SSE explicitly defines comment lines as a no-op for the consumer. - The DATA frame adds 7 bytes per standalone SSE connection, sent once on connect. Co-authored-by: Maciej Kisiel <mkisiel@google.com> |
||
| .agents/skills/fix-bug | ||
| .devcontainer | ||
| .github | ||
| auth | ||
| conformance | ||
| design | ||
| docs | ||
| examples | ||
| internal | ||
| jsonrpc | ||
| mcp | ||
| oauthex | ||
| scripts | ||
| .gitignore | ||
| AGENTS.md | ||
| CONTRIBUTING.md | ||
| copyright_test.go | ||
| go.mod | ||
| go.sum | ||
| LICENSE | ||
| mkdocs.yaml | ||
| README.md | ||
| ROADMAP.md | ||
| SECURITY.md | ||
MCP Go SDK
This repository contains an implementation of the official Go software development kit (SDK) for the Model Context Protocol (MCP).
Package / Feature documentation
The SDK consists of several importable packages:
- The
github.com/modelcontextprotocol/go-sdk/mcppackage defines the primary APIs for constructing and using MCP clients and servers. - The
github.com/modelcontextprotocol/go-sdk/jsonrpcpackage is for users implementing their own transports. - The
github.com/modelcontextprotocol/go-sdk/authpackage provides some primitives for supporting OAuth. - The
github.com/modelcontextprotocol/go-sdk/oauthexpackage provides extensions to the OAuth protocol, such as ProtectedResourceMetadata.
The SDK endeavors to implement the full MCP spec. The docs/ directory
contains feature documentation, mapping the MCP spec to the packages above.
Version Compatibility
The following table shows which versions of the Go SDK support which versions of the MCP specification:
| SDK Version | Latest MCP Spec | All Supported MCP Specs |
|---|---|---|
| v1.4.0+ | 2025-11-25* | 2025-11-25*, 2025-06-18, 2025-03-26, 2024-11-05 |
| v1.2.0 - v1.3.1 | 2025-11-25** | 2025-11-25**, 2025-06-18, 2025-03-26, 2024-11-05 |
| v1.0.0 - v1.1.0 | 2025-06-18 | 2025-06-18, 2025-03-26, 2024-11-05 |
* Client side OAuth has experimental support.
** Partial support for 2025-11-25 (client side OAuth and Sampling with tools not available).
New releases of the SDK target only supported versions of Go. See https://go.dev/doc/devel/release#policy for more information.
Getting started
To get started creating an MCP server, create an mcp.Server instance, add
features to it, and then run it over an mcp.Transport. For example, this
server adds a single simple tool, and then connects it to clients over
stdin/stdout:
package main
import (
"context"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type Input struct {
Name string `json:"name" jsonschema:"the name of the person to greet"`
}
type Output struct {
Greeting string `json:"greeting" jsonschema:"the greeting to tell to the user"`
}
func SayHi(ctx context.Context, req *mcp.CallToolRequest, input Input) (
*mcp.CallToolResult,
Output,
error,
) {
return nil, Output{Greeting: "Hi " + input.Name}, nil
}
func main() {
// Create a server with a single tool.
server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v1.0.0"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
// Run the server over stdin/stdout, until the client disconnects.
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Fatal(err)
}
}
To communicate with that server, create an mcp.Client and connect it to the
corresponding server, by running the server command and communicating over its
stdin/stdout:
package main
import (
"context"
"log"
"os/exec"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
ctx := context.Background()
// Create a new client, with no features.
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
// Connect to a server over stdin/stdout.
transport := &mcp.CommandTransport{Command: exec.Command("myserver")}
session, err := client.Connect(ctx, transport, nil)
if err != nil {
log.Fatal(err)
}
defer session.Close()
// Call a tool on the server.
params := &mcp.CallToolParams{
Name: "greet",
Arguments: map[string]any{"name": "you"},
}
res, err := session.CallTool(ctx, params)
if err != nil {
log.Fatalf("CallTool failed: %v", err)
}
if res.IsError {
log.Fatal("tool failed")
}
for _, c := range res.Content {
log.Print(c.(*mcp.TextContent).Text)
}
}
The examples/ directory contains more example clients and
servers.
Contributing
We welcome contributions to the SDK! Please see CONTRIBUTING.md for details of how to contribute.
Acknowledgements / Alternatives
Several third party Go MCP SDKs inspired the development and design of this official SDK, and continue to be viable alternatives, notably mcp-go, originally authored by Ed Zynda. We are grateful to Ed as well as the other contributors to mcp-go, and to authors and contributors of other SDKs such as mcp-golang and go-mcp. Thanks to their work, there is a thriving ecosystem of Go MCP clients and servers.
License
This project is licensed under Apache 2.0 for new contributions, with existing code under MIT - see the LICENSE file for details.