|
7 | 7 | "net/http/httptest" |
8 | 8 | "slices" |
9 | 9 | "sort" |
| 10 | + "strings" |
10 | 11 | "testing" |
11 | 12 |
|
12 | 13 | ghcontext "github.com/github/github-mcp-server/pkg/context" |
@@ -631,6 +632,101 @@ func TestStaticConfigEnforcement(t *testing.T) { |
631 | 632 | } |
632 | 633 | } |
633 | 634 |
|
| 635 | +// TestContentTypeHandling verifies that the MCP StreamableHTTP handler |
| 636 | +// accepts Content-Type values with additional parameters like charset=utf-8. |
| 637 | +// This is a regression test for https://github.com/github/github-mcp-server/issues/2333 |
| 638 | +// where the Go SDK performs strict string matching against "application/json" |
| 639 | +// and rejects requests with "application/json; charset=utf-8". |
| 640 | +func TestContentTypeHandling(t *testing.T) { |
| 641 | + tests := []struct { |
| 642 | + name string |
| 643 | + contentType string |
| 644 | + expectUnsupportedMedia bool |
| 645 | + }{ |
| 646 | + { |
| 647 | + name: "exact application/json is accepted", |
| 648 | + contentType: "application/json", |
| 649 | + expectUnsupportedMedia: false, |
| 650 | + }, |
| 651 | + { |
| 652 | + name: "application/json with charset=utf-8 should be accepted", |
| 653 | + contentType: "application/json; charset=utf-8", |
| 654 | + expectUnsupportedMedia: false, |
| 655 | + }, |
| 656 | + { |
| 657 | + name: "application/json with charset=UTF-8 should be accepted", |
| 658 | + contentType: "application/json; charset=UTF-8", |
| 659 | + expectUnsupportedMedia: false, |
| 660 | + }, |
| 661 | + { |
| 662 | + name: "completely wrong content type is rejected", |
| 663 | + contentType: "text/plain", |
| 664 | + expectUnsupportedMedia: true, |
| 665 | + }, |
| 666 | + { |
| 667 | + name: "empty content type is rejected", |
| 668 | + contentType: "", |
| 669 | + expectUnsupportedMedia: true, |
| 670 | + }, |
| 671 | + } |
| 672 | + |
| 673 | + for _, tt := range tests { |
| 674 | + t.Run(tt.name, func(t *testing.T) { |
| 675 | + // Create a minimal MCP server factory |
| 676 | + mcpServerFactory := func(_ *http.Request, _ github.ToolDependencies, _ *inventory.Inventory, _ *github.MCPServerConfig) (*mcp.Server, error) { |
| 677 | + return mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil), nil |
| 678 | + } |
| 679 | + |
| 680 | + // Create a simple inventory factory |
| 681 | + inventoryFactory := func(_ *http.Request) (*inventory.Inventory, error) { |
| 682 | + return inventory.NewBuilder(). |
| 683 | + SetTools(testTools()). |
| 684 | + WithToolsets([]string{"all"}). |
| 685 | + Build() |
| 686 | + } |
| 687 | + |
| 688 | + apiHost, err := utils.NewAPIHost("https://api.github.com") |
| 689 | + require.NoError(t, err) |
| 690 | + |
| 691 | + handler := NewHTTPMcpHandler( |
| 692 | + context.Background(), |
| 693 | + &ServerConfig{Version: "test"}, |
| 694 | + nil, |
| 695 | + translations.NullTranslationHelper, |
| 696 | + slog.Default(), |
| 697 | + apiHost, |
| 698 | + WithInventoryFactory(inventoryFactory), |
| 699 | + WithGitHubMCPServerFactory(mcpServerFactory), |
| 700 | + WithScopeFetcher(allScopesFetcher{}), |
| 701 | + ) |
| 702 | + |
| 703 | + r := chi.NewRouter() |
| 704 | + handler.RegisterMiddleware(r) |
| 705 | + handler.RegisterRoutes(r) |
| 706 | + |
| 707 | + // Send an MCP initialize request as a POST with the given Content-Type |
| 708 | + body := `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}` |
| 709 | + req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) |
| 710 | + req.Header.Set(headers.AuthorizationHeader, "Bearer ghp_testtoken") |
| 711 | + req.Header.Set("Accept", "application/json, text/event-stream") |
| 712 | + if tt.contentType != "" { |
| 713 | + req.Header.Set(headers.ContentTypeHeader, tt.contentType) |
| 714 | + } |
| 715 | + |
| 716 | + rr := httptest.NewRecorder() |
| 717 | + r.ServeHTTP(rr, req) |
| 718 | + |
| 719 | + if tt.expectUnsupportedMedia { |
| 720 | + assert.Equal(t, http.StatusUnsupportedMediaType, rr.Code, |
| 721 | + "expected 415 Unsupported Media Type for Content-Type: %q", tt.contentType) |
| 722 | + } else { |
| 723 | + assert.NotEqual(t, http.StatusUnsupportedMediaType, rr.Code, |
| 724 | + "should not get 415 for Content-Type: %q, got status %d", tt.contentType, rr.Code) |
| 725 | + } |
| 726 | + }) |
| 727 | + } |
| 728 | +} |
| 729 | + |
634 | 730 | // buildStaticInventoryFromTools is a test helper that mirrors buildStaticInventory |
635 | 731 | // but uses the provided mock tools instead of calling github.AllTools. |
636 | 732 | func buildStaticInventoryFromTools(cfg *ServerConfig, tools []inventory.ServerTool, featureChecker inventory.FeatureFlagChecker) ([]inventory.ServerTool, []inventory.ServerResourceTemplate, []inventory.ServerPrompt) { |
|
0 commit comments