From ff8bdf4dc2aeb5d9f03bb8316c113837f02412f7 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Fri, 10 Oct 2025 13:11:35 -0500 Subject: [PATCH 1/6] [disable-stream] Add `WithDisableStreaming` option to StreamableHTTP server to allow disabling streaming [Per the spec](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server), a server is allowed to respond with 405 in response to a request for streaming. In our use case, we do not need streaming, and do not want to support it at a network layer. --- go.mod | 2 +- server/streamable_http.go | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 5c8974549..23d69d6fc 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mark3labs/mcp-go -go 1.23 +go 1.23.0 require ( github.com/google/uuid v1.6.0 diff --git a/server/streamable_http.go b/server/streamable_http.go index c97d9b747..a390ad2c3 100644 --- a/server/streamable_http.go +++ b/server/streamable_http.go @@ -69,6 +69,16 @@ func WithHeartbeatInterval(interval time.Duration) StreamableHTTPOption { } } +// WithDisableStreaming prevents the server from responding to GET requests with +// a streaming response. Instead, it will respond with a 403 Forbidden status. +// This can be useful in scenarios where streaming is not desired or supported. +// The default is false, meaning streaming is enabled. +func WithDisableStreaming(disable bool) StreamableHTTPOption { + return func(s *StreamableHTTPServer) { + s.disableStreaming = disable + } +} + // WithHTTPContextFunc sets a function that will be called to customise the context // to the server using the incoming request. // This can be used to inject context values from headers, for example. @@ -141,6 +151,7 @@ type StreamableHTTPServer struct { listenHeartbeatInterval time.Duration logger util.Logger sessionLogLevels *sessionLogLevelsStore + disableStreaming bool tlsCertFile string tlsKeyFile string @@ -400,6 +411,10 @@ func (s *StreamableHTTPServer) handlePost(w http.ResponseWriter, r *http.Request func (s *StreamableHTTPServer) handleGet(w http.ResponseWriter, r *http.Request) { // get request is for listening to notifications // https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server + if s.disableStreaming { + http.Error(w, "Streaming is disabled on this server", http.StatusMethodNotAllowed) + return + } sessionID := r.Header.Get(HeaderKeySessionID) // the specification didn't say we should validate the session id From e09c6e30c65c2bd1b77900f2c8ebd060e76b19e3 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Fri, 10 Oct 2025 13:13:33 -0500 Subject: [PATCH 2/6] [disable-stream] bounce --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 23d69d6fc..5c8974549 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/mark3labs/mcp-go -go 1.23.0 +go 1.23 require ( github.com/google/uuid v1.6.0 From a13ae7188bc1d860db8c1e30bbcc1837e43ed252 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Fri, 10 Oct 2025 14:57:08 -0500 Subject: [PATCH 3/6] [disable-stream] tests --- server/streamable_http_test.go | 94 ++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/server/streamable_http_test.go b/server/streamable_http_test.go index b464e1bdd..619b8a54e 100644 --- a/server/streamable_http_test.go +++ b/server/streamable_http_test.go @@ -914,6 +914,100 @@ func TestStreamableHTTPServer_TLS(t *testing.T) { }) } +func TestStreamableHTTPServer_WithDisableStreaming(t *testing.T) { + t.Run("WithDisableStreaming blocks GET requests", func(t *testing.T) { + mcpServer := NewMCPServer("test-mcp-server", "1.0.0") + server := NewTestStreamableHTTPServer(mcpServer, WithDisableStreaming(true)) + defer server.Close() + + // Attempt a GET request (which should be blocked) + req, err := http.NewRequest(http.MethodGet, server.URL, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "text/event-stream") + + resp, err := server.Client().Do(req) + if err != nil { + t.Fatalf("Failed to send request: %v", err) + } + defer resp.Body.Close() + + // Verify the request is rejected with 405 Method Not Allowed + if resp.StatusCode != http.StatusMethodNotAllowed { + t.Errorf("Expected status 405 Method Not Allowed, got %d", resp.StatusCode) + } + + // Verify the error message + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Failed to read response body: %v", err) + } + + expectedMessage := "Streaming is disabled on this server" + if !strings.Contains(string(bodyBytes), expectedMessage) { + t.Errorf("Expected error message to contain '%s', got '%s'", expectedMessage, string(bodyBytes)) + } + }) + + t.Run("POST requests still work with WithDisableStreaming", func(t *testing.T) { + mcpServer := NewMCPServer("test-mcp-server", "1.0.0") + server := NewTestStreamableHTTPServer(mcpServer, WithDisableStreaming(true)) + defer server.Close() + + // POST requests should still work + resp, err := postJSON(server.URL, initRequest) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + // Verify the response is valid + bodyBytes, _ := io.ReadAll(resp.Body) + var responseMessage jsonRPCResponse + if err := json.Unmarshal(bodyBytes, &responseMessage); err != nil { + t.Fatalf("Failed to unmarshal response: %v", err) + } + if responseMessage.Result["protocolVersion"] != mcp.LATEST_PROTOCOL_VERSION { + t.Errorf("Expected protocol version %s, got %s", mcp.LATEST_PROTOCOL_VERSION, responseMessage.Result["protocolVersion"]) + } + }) + + t.Run("Streaming works when WithDisableStreaming is false", func(t *testing.T) { + mcpServer := NewMCPServer("test-mcp-server", "1.0.0") + server := NewTestStreamableHTTPServer(mcpServer, WithDisableStreaming(false)) + defer server.Close() + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + // GET request should work when streaming is enabled + req, err := http.NewRequestWithContext(ctx, http.MethodGet, server.URL, nil) + if err != nil { + t.Fatalf("Failed to create request: %v", err) + } + req.Header.Set("Content-Type", "text/event-stream") + + resp, err := server.Client().Do(req) + if err != nil { + t.Fatalf("Failed to send request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + if resp.Header.Get("content-type") != "text/event-stream" { + t.Errorf("Expected content-type text/event-stream, got %s", resp.Header.Get("content-type")) + } + }) +} + func postJSON(url string, bodyObject any) (*http.Response, error) { jsonBody, _ := json.Marshal(bodyObject) req, _ := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(jsonBody)) From 08eae1983a18dcc8b9f65618b9609d9d6d850bd6 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Fri, 10 Oct 2025 21:17:24 -0500 Subject: [PATCH 4/6] [disable-stream] fix docstring --- server/streamable_http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/streamable_http.go b/server/streamable_http.go index a390ad2c3..4e523998e 100644 --- a/server/streamable_http.go +++ b/server/streamable_http.go @@ -70,7 +70,7 @@ func WithHeartbeatInterval(interval time.Duration) StreamableHTTPOption { } // WithDisableStreaming prevents the server from responding to GET requests with -// a streaming response. Instead, it will respond with a 403 Forbidden status. +// a streaming response. Instead, it will respond with a 405 Method Not Allowed status. // This can be useful in scenarios where streaming is not desired or supported. // The default is false, meaning streaming is enabled. func WithDisableStreaming(disable bool) StreamableHTTPOption { From 4a969d68a31c5a927e2d21320f6b15823c686936 Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Tue, 14 Oct 2025 12:54:25 -0500 Subject: [PATCH 5/6] [disable-stream] logging --- server/streamable_http.go | 1 + 1 file changed, 1 insertion(+) diff --git a/server/streamable_http.go b/server/streamable_http.go index 4e523998e..8c3bcbe31 100644 --- a/server/streamable_http.go +++ b/server/streamable_http.go @@ -412,6 +412,7 @@ func (s *StreamableHTTPServer) handleGet(w http.ResponseWriter, r *http.Request) // get request is for listening to notifications // https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server if s.disableStreaming { + s.logger.Debugf("Rejected GET request: streaming is disabled (session: %s)", r.Header.Get(HeaderKeySessionID)) http.Error(w, "Streaming is disabled on this server", http.StatusMethodNotAllowed) return } From 01f55625eca8d7bcffb256d45c90a0cefb028ded Mon Sep 17 00:00:00 2001 From: Jared Forsyth Date: Tue, 14 Oct 2025 13:45:24 -0500 Subject: [PATCH 6/6] [disable-stream] bounce --- server/streamable_http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/streamable_http.go b/server/streamable_http.go index 8c3bcbe31..eeb69fe6d 100644 --- a/server/streamable_http.go +++ b/server/streamable_http.go @@ -412,7 +412,7 @@ func (s *StreamableHTTPServer) handleGet(w http.ResponseWriter, r *http.Request) // get request is for listening to notifications // https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#listening-for-messages-from-the-server if s.disableStreaming { - s.logger.Debugf("Rejected GET request: streaming is disabled (session: %s)", r.Header.Get(HeaderKeySessionID)) + s.logger.Infof("Rejected GET request: streaming is disabled (session: %s)", r.Header.Get(HeaderKeySessionID)) http.Error(w, "Streaming is disabled on this server", http.StatusMethodNotAllowed) return }