Skip to content

Commit 5bbab3c

Browse files
fix(http): reject unsupported subscription streams
Use the Mcp-Method header to reject subscriptions/listen with the spec-defined 404 Method Not Found response instead of opening an idle SSE stream. Preserve SDK validation for missing or mismatched headers. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 06d5dda1-4086-4996-8d18-152e45e611b0
1 parent 0ea1f77 commit 5bbab3c

3 files changed

Lines changed: 97 additions & 0 deletions

File tree

pkg/http/handler.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,20 @@ import (
88

99
ghcontext "github.com/github/github-mcp-server/pkg/context"
1010
"github.com/github/github-mcp-server/pkg/github"
11+
"github.com/github/github-mcp-server/pkg/http/headers"
1112
"github.com/github/github-mcp-server/pkg/http/middleware"
1213
"github.com/github/github-mcp-server/pkg/http/oauth"
1314
"github.com/github/github-mcp-server/pkg/inventory"
1415
"github.com/github/github-mcp-server/pkg/scopes"
1516
"github.com/github/github-mcp-server/pkg/translations"
1617
"github.com/github/github-mcp-server/pkg/utils"
1718
"github.com/go-chi/chi/v5"
19+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
1820
"github.com/modelcontextprotocol/go-sdk/mcp"
1921
)
2022

23+
const subscriptionsListenMethod = "subscriptions/listen"
24+
2125
type InventoryFactoryFunc func(r *http.Request) (*inventory.Inventory, error)
2226

2327
// GitHubMCPServerFactoryFunc is a function type for creating a new MCP Server instance.
@@ -219,6 +223,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
219223
return
220224
}
221225

226+
if r.Header.Get(headers.MCPMethodHeader) == subscriptionsListenMethod {
227+
ghServer.AddReceivingMiddleware(rejectSubscriptionsListen)
228+
}
229+
222230
// Cross-origin protection is intentionally left unset: this server
223231
// authenticates via bearer tokens (not cookies), so Sec-Fetch-Site CSRF
224232
// checks are unnecessary and would block browser-based MCP clients. As of
@@ -233,6 +241,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
233241
mcpHandler.ServeHTTP(w, r)
234242
}
235243

244+
func rejectSubscriptionsListen(next mcp.MethodHandler) mcp.MethodHandler {
245+
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
246+
if method == subscriptionsListenMethod {
247+
return nil, &jsonrpc.Error{
248+
Code: jsonrpc.CodeMethodNotFound,
249+
Message: "method not found",
250+
}
251+
}
252+
return next(ctx, method, req)
253+
}
254+
}
255+
236256
func DefaultGitHubMCPServerFactory(r *http.Request, deps github.ToolDependencies, inventory *inventory.Inventory, cfg *github.MCPServerConfig) (*mcp.Server, error) {
237257
return github.NewMCPServer(r.Context(), cfg, deps, inventory)
238258
}

pkg/http/handler_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package http
22

33
import (
44
"context"
5+
"encoding/json"
56
"log/slog"
67
"net/http"
78
"net/http/httptest"
@@ -906,6 +907,80 @@ func TestCrossOriginProtection(t *testing.T) {
906907
}
907908
}
908909

910+
func TestSubscriptionsListenIsRejected(t *testing.T) {
911+
apiHost, err := utils.NewAPIHost("https://api.githubcopilot.com")
912+
require.NoError(t, err)
913+
914+
handler := NewHTTPMcpHandler(
915+
context.Background(),
916+
&ServerConfig{Version: "test"},
917+
nil,
918+
translations.NullTranslationHelper,
919+
slog.Default(),
920+
apiHost,
921+
WithInventoryFactory(func(_ *http.Request) (*inventory.Inventory, error) {
922+
return inventory.NewBuilder().Build()
923+
}),
924+
WithGitHubMCPServerFactory(func(_ *http.Request, _ github.ToolDependencies, _ *inventory.Inventory, _ *github.MCPServerConfig) (*mcp.Server, error) {
925+
return mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil), nil
926+
}),
927+
)
928+
929+
body := `{"jsonrpc":"2.0","id":1,"method":"subscriptions/listen","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"test","version":"1.0.0"},"io.modelcontextprotocol/clientCapabilities":{}},"notifications":{"toolsListChanged":true}}}`
930+
tests := []struct {
931+
name string
932+
methodHeader string
933+
expectedStatus int
934+
expectedJSONCode int
935+
}{
936+
{
937+
name: "matching method header",
938+
methodHeader: subscriptionsListenMethod,
939+
expectedStatus: http.StatusNotFound,
940+
expectedJSONCode: -32601,
941+
},
942+
{
943+
name: "missing method header",
944+
expectedStatus: http.StatusBadRequest,
945+
expectedJSONCode: -32020,
946+
},
947+
{
948+
name: "mismatched method header",
949+
methodHeader: "tools/list",
950+
expectedStatus: http.StatusBadRequest,
951+
expectedJSONCode: -32020,
952+
},
953+
}
954+
955+
for _, tt := range tests {
956+
t.Run(tt.name, func(t *testing.T) {
957+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
958+
req.Header.Set(headers.ContentTypeHeader, headers.ContentTypeJSON)
959+
req.Header.Set(headers.AcceptHeader, strings.Join([]string{headers.ContentTypeJSON, headers.ContentTypeEventStream}, ", "))
960+
req.Header.Set("MCP-Protocol-Version", "2026-07-28")
961+
if tt.methodHeader != "" {
962+
req.Header.Set(headers.MCPMethodHeader, tt.methodHeader)
963+
}
964+
965+
rr := httptest.NewRecorder()
966+
handler.ServeHTTP(rr, req)
967+
968+
assert.Equal(t, tt.expectedStatus, rr.Code)
969+
assert.Equal(t, headers.ContentTypeJSON, rr.Header().Get(headers.ContentTypeHeader))
970+
971+
var response struct {
972+
ID int `json:"id"`
973+
Error struct {
974+
Code int `json:"code"`
975+
} `json:"error"`
976+
}
977+
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
978+
assert.Equal(t, 1, response.ID)
979+
assert.Equal(t, tt.expectedJSONCode, response.Error.Code)
980+
})
981+
}
982+
}
983+
909984
// TestInsidersRoutePreservesUIMeta is a regression test for the bug where
910985
// _meta.ui was stripped from tools/list responses on the HTTP /insiders route.
911986
//

pkg/http/headers/headers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const (
3131

3232
// MCP-specific headers.
3333

34+
// MCPMethodHeader mirrors the JSON-RPC method for request routing.
35+
MCPMethodHeader = "Mcp-Method"
3436
// MCPReadOnlyHeader indicates whether the MCP is in read-only mode.
3537
MCPReadOnlyHeader = "X-MCP-Readonly"
3638
// MCPToolsetsHeader is a comma-separated list of MCP toolsets that the request is for.

0 commit comments

Comments
 (0)