Skip to content

Commit 2424d6c

Browse files
Merge branch 'main' into search-commit
2 parents 851bdef + f363fd0 commit 2424d6c

8 files changed

Lines changed: 171 additions & 5 deletions

File tree

.github/workflows/docker-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ jobs:
106106
# https://github.com/docker/build-push-action
107107
- name: Build and push Docker image
108108
id: build-and-push
109-
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
109+
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
110110
with:
111111
context: .
112112
push: ${{ github.event_name != 'pull_request' }}

.github/workflows/license-check.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ jobs:
7777
- name: Check if already commented
7878
if: steps.changes.outcome == 'failure' && steps.push.outcome == 'failure'
7979
id: check_comment
80-
uses: actions/github-script@v8
80+
uses: actions/github-script@v9
8181
with:
8282
script: |
8383
const { data: comments } = await github.rest.issues.listComments({
@@ -95,7 +95,7 @@ jobs:
9595
9696
- name: Comment with instructions if cannot push
9797
if: steps.changes.outcome == 'failure' && steps.push.outcome == 'failure' && steps.check_comment.outputs.already_commented == 'false'
98-
uses: actions/github-script@v8
98+
uses: actions/github-script@v9
9999
with:
100100
script: |
101101
await github.rest.issues.createComment({

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ COPY ui/ ./ui/
77
RUN mkdir -p ./pkg/github/ui_dist && \
88
cd ui && npm run build
99

10-
FROM golang:1.25.9-alpine@sha256:04d017a27c481185c169884328a5761d052910fdced8c3b8edd686474efdf59b AS build
10+
FROM golang:1.25.9-alpine@sha256:5caaf1cca9dc351e13deafbc3879fd4754801acba8653fa9540cea125d01a71f AS build
1111
ARG VERSION="dev"
1212

1313
# Set the working directory

pkg/http/handler.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,10 +223,16 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
223223
return
224224
}
225225

226+
// Bypass cross-origin protection: this server uses bearer tokens (not
227+
// cookies), so Sec-Fetch-Site CSRF checks are unnecessary. See PR #2359.
228+
crossOriginProtection := http.NewCrossOriginProtection()
229+
crossOriginProtection.AddInsecureBypassPattern("/")
230+
226231
mcpHandler := mcp.NewStreamableHTTPHandler(func(_ *http.Request) *mcp.Server {
227232
return ghServer
228233
}, &mcp.StreamableHTTPOptions{
229-
Stateless: true,
234+
Stateless: true,
235+
CrossOriginProtection: crossOriginProtection,
230236
})
231237

232238
mcpHandler.ServeHTTP(w, r)

pkg/http/handler_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,3 +756,72 @@ func buildStaticInventoryFromTools(cfg *ServerConfig, tools []inventory.ServerTo
756756
ctx := context.Background()
757757
return inv.AvailableTools(ctx), inv.AvailableResourceTemplates(ctx), inv.AvailablePrompts(ctx)
758758
}
759+
760+
func TestCrossOriginProtection(t *testing.T) {
761+
jsonRPCBody := `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}`
762+
763+
apiHost, err := utils.NewAPIHost("https://api.githubcopilot.com")
764+
require.NoError(t, err)
765+
766+
handler := NewHTTPMcpHandler(
767+
context.Background(),
768+
&ServerConfig{
769+
Version: "test",
770+
},
771+
nil,
772+
translations.NullTranslationHelper,
773+
slog.Default(),
774+
apiHost,
775+
WithInventoryFactory(func(_ *http.Request) (*inventory.Inventory, error) {
776+
return inventory.NewBuilder().Build()
777+
}),
778+
WithGitHubMCPServerFactory(func(_ *http.Request, _ github.ToolDependencies, _ *inventory.Inventory, _ *github.MCPServerConfig) (*mcp.Server, error) {
779+
return mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil), nil
780+
}),
781+
WithScopeFetcher(allScopesFetcher{}),
782+
)
783+
784+
r := chi.NewRouter()
785+
handler.RegisterMiddleware(r)
786+
handler.RegisterRoutes(r)
787+
788+
tests := []struct {
789+
name string
790+
secFetchSite string
791+
origin string
792+
}{
793+
{
794+
name: "cross-site request with bearer token succeeds",
795+
secFetchSite: "cross-site",
796+
origin: "https://example.com",
797+
},
798+
{
799+
name: "same-origin request succeeds",
800+
secFetchSite: "same-origin",
801+
},
802+
{
803+
name: "native client without Sec-Fetch-Site succeeds",
804+
secFetchSite: "",
805+
},
806+
}
807+
808+
for _, tt := range tests {
809+
t.Run(tt.name, func(t *testing.T) {
810+
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(jsonRPCBody))
811+
req.Header.Set("Content-Type", "application/json")
812+
req.Header.Set("Accept", "application/json, text/event-stream")
813+
req.Header.Set(headers.AuthorizationHeader, "Bearer github_pat_xyz")
814+
if tt.secFetchSite != "" {
815+
req.Header.Set("Sec-Fetch-Site", tt.secFetchSite)
816+
}
817+
if tt.origin != "" {
818+
req.Header.Set("Origin", tt.origin)
819+
}
820+
821+
rr := httptest.NewRecorder()
822+
r.ServeHTTP(rr, req)
823+
824+
assert.Equal(t, http.StatusOK, rr.Code, "unexpected status code; body: %s", rr.Body.String())
825+
})
826+
}
827+
}

pkg/http/middleware/cors.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
7+
"github.com/github/github-mcp-server/pkg/http/headers"
8+
)
9+
10+
// SetCorsHeaders is middleware that sets CORS headers to allow browser-based
11+
// MCP clients to connect from any origin. This is safe because the server
12+
// authenticates via bearer tokens (not cookies), so cross-origin requests
13+
// cannot exploit ambient credentials.
14+
func SetCorsHeaders(h http.Handler) http.Handler {
15+
allowHeaders := strings.Join([]string{
16+
"Content-Type",
17+
"Mcp-Session-Id",
18+
"Mcp-Protocol-Version",
19+
"Last-Event-ID",
20+
headers.AuthorizationHeader,
21+
headers.MCPReadOnlyHeader,
22+
headers.MCPToolsetsHeader,
23+
headers.MCPToolsHeader,
24+
headers.MCPExcludeToolsHeader,
25+
headers.MCPFeaturesHeader,
26+
headers.MCPLockdownHeader,
27+
headers.MCPInsidersHeader,
28+
}, ", ")
29+
30+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
31+
w.Header().Set("Access-Control-Allow-Origin", "*")
32+
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
33+
w.Header().Set("Access-Control-Max-Age", "86400")
34+
w.Header().Set("Access-Control-Expose-Headers", "Mcp-Session-Id, WWW-Authenticate")
35+
w.Header().Set("Access-Control-Allow-Headers", allowHeaders)
36+
37+
if r.Method == http.MethodOptions {
38+
w.WriteHeader(http.StatusOK)
39+
return
40+
}
41+
h.ServeHTTP(w, r)
42+
})
43+
}

pkg/http/middleware/cors_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package middleware_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/github/github-mcp-server/pkg/http/middleware"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestSetCorsHeaders(t *testing.T) {
13+
inner := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
14+
w.WriteHeader(http.StatusOK)
15+
})
16+
handler := middleware.SetCorsHeaders(inner)
17+
18+
t.Run("OPTIONS preflight returns 200 with CORS headers", func(t *testing.T) {
19+
req := httptest.NewRequest(http.MethodOptions, "/", nil)
20+
req.Header.Set("Origin", "http://localhost:6274")
21+
rr := httptest.NewRecorder()
22+
handler.ServeHTTP(rr, req)
23+
24+
assert.Equal(t, http.StatusOK, rr.Code)
25+
assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
26+
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Methods"), "POST")
27+
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Headers"), "Authorization")
28+
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Headers"), "Content-Type")
29+
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Headers"), "Mcp-Session-Id")
30+
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Headers"), "X-MCP-Lockdown")
31+
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Headers"), "X-MCP-Insiders")
32+
assert.Contains(t, rr.Header().Get("Access-Control-Expose-Headers"), "Mcp-Session-Id")
33+
assert.Contains(t, rr.Header().Get("Access-Control-Expose-Headers"), "WWW-Authenticate")
34+
})
35+
36+
t.Run("POST request includes CORS headers", func(t *testing.T) {
37+
req := httptest.NewRequest(http.MethodPost, "/", nil)
38+
req.Header.Set("Origin", "http://localhost:6274")
39+
rr := httptest.NewRecorder()
40+
handler.ServeHTTP(rr, req)
41+
42+
assert.Equal(t, http.StatusOK, rr.Code)
43+
assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
44+
})
45+
}

pkg/http/server.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
ghcontext "github.com/github/github-mcp-server/pkg/context"
1515
"github.com/github/github-mcp-server/pkg/github"
16+
"github.com/github/github-mcp-server/pkg/http/middleware"
1617
"github.com/github/github-mcp-server/pkg/http/oauth"
1718
"github.com/github/github-mcp-server/pkg/inventory"
1819
"github.com/github/github-mcp-server/pkg/lockdown"
@@ -167,6 +168,8 @@ func RunHTTPServer(cfg ServerConfig) error {
167168
}
168169

169170
r.Group(func(r chi.Router) {
171+
r.Use(middleware.SetCorsHeaders)
172+
170173
// Register Middleware First, needs to be before route registration
171174
handler.RegisterMiddleware(r)
172175

0 commit comments

Comments
 (0)