Skip to content

Commit 8eba6fd

Browse files
Merge remote-tracking branch 'origin/main' into sammorrowdrums-add-delete-repository-tool
# Conflicts: # pkg/http/handler_test.go
2 parents 676a5b2 + 3085e59 commit 8eba6fd

3 files changed

Lines changed: 99 additions & 0 deletions

File tree

pkg/http/handler.go

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

1010
ghcontext "github.com/github/github-mcp-server/pkg/context"
1111
"github.com/github/github-mcp-server/pkg/github"
12+
"github.com/github/github-mcp-server/pkg/http/headers"
1213
"github.com/github/github-mcp-server/pkg/http/middleware"
1314
"github.com/github/github-mcp-server/pkg/http/oauth"
1415
"github.com/github/github-mcp-server/pkg/inventory"
1516
"github.com/github/github-mcp-server/pkg/scopes"
1617
"github.com/github/github-mcp-server/pkg/translations"
1718
"github.com/github/github-mcp-server/pkg/utils"
1819
"github.com/go-chi/chi/v5"
20+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
1921
"github.com/modelcontextprotocol/go-sdk/mcp"
2022
)
2123

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

2428
// GitHubMCPServerFactoryFunc is a function type for creating a new MCP Server instance.
@@ -220,6 +224,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
220224
return
221225
}
222226

227+
// Let the SDK validate missing or mismatched method headers before the
228+
// middleware rejects a well-formed request as unsupported.
229+
if r.Header.Get(headers.MCPMethodHeader) == subscriptionsListenMethod {
230+
ghServer.AddReceivingMiddleware(rejectSubscriptionsListen)
231+
}
232+
223233
// Cross-origin protection is intentionally left unset: this server
224234
// authenticates via bearer tokens (not cookies), so Sec-Fetch-Site CSRF
225235
// checks are unnecessary and would block browser-based MCP clients. As of
@@ -234,6 +244,18 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
234244
mcpHandler.ServeHTTP(w, r)
235245
}
236246

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

pkg/http/handler_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"github.com/github/github-mcp-server/pkg/translations"
2020
"github.com/github/github-mcp-server/pkg/utils"
2121
"github.com/go-chi/chi/v5"
22+
"github.com/modelcontextprotocol/go-sdk/jsonrpc"
2223
"github.com/modelcontextprotocol/go-sdk/mcp"
2324
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
@@ -1044,6 +1045,80 @@ func TestHTTPToolMinimumProtocolVersion(t *testing.T) {
10441045
}
10451046
}
10461047

1048+
func TestSubscriptionsListenIsRejected(t *testing.T) {
1049+
apiHost, err := utils.NewAPIHost("https://api.githubcopilot.com")
1050+
require.NoError(t, err)
1051+
1052+
handler := NewHTTPMcpHandler(
1053+
context.Background(),
1054+
&ServerConfig{Version: "test"},
1055+
nil,
1056+
translations.NullTranslationHelper,
1057+
slog.Default(),
1058+
apiHost,
1059+
WithInventoryFactory(func(_ *http.Request) (*inventory.Inventory, error) {
1060+
return inventory.NewBuilder().Build()
1061+
}),
1062+
WithGitHubMCPServerFactory(func(_ *http.Request, _ github.ToolDependencies, _ *inventory.Inventory, _ *github.MCPServerConfig) (*mcp.Server, error) {
1063+
return mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil), nil
1064+
}),
1065+
)
1066+
1067+
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}}}`
1068+
tests := []struct {
1069+
name string
1070+
methodHeader string
1071+
expectedStatus int
1072+
expectedJSONCode int
1073+
}{
1074+
{
1075+
name: "matching method header",
1076+
methodHeader: subscriptionsListenMethod,
1077+
expectedStatus: http.StatusNotFound,
1078+
expectedJSONCode: jsonrpc.CodeMethodNotFound,
1079+
},
1080+
{
1081+
name: "missing method header",
1082+
expectedStatus: http.StatusBadRequest,
1083+
expectedJSONCode: mcp.CodeHeaderMismatch,
1084+
},
1085+
{
1086+
name: "mismatched method header",
1087+
methodHeader: "tools/list",
1088+
expectedStatus: http.StatusBadRequest,
1089+
expectedJSONCode: mcp.CodeHeaderMismatch,
1090+
},
1091+
}
1092+
1093+
for _, tt := range tests {
1094+
t.Run(tt.name, func(t *testing.T) {
1095+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))
1096+
req.Header.Set(headers.ContentTypeHeader, headers.ContentTypeJSON)
1097+
req.Header.Set(headers.AcceptHeader, strings.Join([]string{headers.ContentTypeJSON, headers.ContentTypeEventStream}, ", "))
1098+
req.Header.Set("MCP-Protocol-Version", "2026-07-28")
1099+
if tt.methodHeader != "" {
1100+
req.Header.Set(headers.MCPMethodHeader, tt.methodHeader)
1101+
}
1102+
1103+
rr := httptest.NewRecorder()
1104+
handler.ServeHTTP(rr, req)
1105+
1106+
assert.Equal(t, tt.expectedStatus, rr.Code)
1107+
assert.Equal(t, headers.ContentTypeJSON, rr.Header().Get(headers.ContentTypeHeader))
1108+
1109+
var response struct {
1110+
ID int `json:"id"`
1111+
Error struct {
1112+
Code int `json:"code"`
1113+
} `json:"error"`
1114+
}
1115+
require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response))
1116+
assert.Equal(t, 1, response.ID)
1117+
assert.Equal(t, tt.expectedJSONCode, response.Error.Code)
1118+
})
1119+
}
1120+
}
1121+
10471122
// TestInsidersRoutePreservesUIMeta is a regression test for the bug where
10481123
// _meta.ui was stripped from tools/list responses on the HTTP /insiders route.
10491124
//

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)