Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Fix subdomain isolation URL parsing #1218

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
tonytrg merged 6 commits into main from tonytrg/fix-subdomain
Oct 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions internal/ghmcp/server.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"os/signal"
"strings"
"syscall"
"time"

"github.com/github/github-mcp-server/pkg/errors"
"github.com/github/github-mcp-server/pkg/github"
Expand Down Expand Up @@ -363,11 +364,30 @@ func newGHESHost(hostname string) (apiHost, error) {
return apiHost{}, fmt.Errorf("failed to parse GHES GraphQL URL: %w", err)
}

uploadURL, err := url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, u.Hostname()))
// Check if subdomain isolation is enabled
// See https://docs.github.com/en/enterprise-server@3.17/admin/configuring-settings/hardening-security-for-your-enterprise/enabling-subdomain-isolation#about-subdomain-isolation
hasSubdomainIsolation := checkSubdomainIsolation(u.Scheme, u.Hostname())

var uploadURL *url.URL
if hasSubdomainIsolation {
// With subdomain isolation: https://uploads.hostname/
uploadURL, err = url.Parse(fmt.Sprintf("%s://uploads.%s/", u.Scheme, u.Hostname()))
} else {
// Without subdomain isolation: https://hostname/api/uploads/
uploadURL, err = url.Parse(fmt.Sprintf("%s://%s/api/uploads/", u.Scheme, u.Hostname()))
}
if err != nil {
return apiHost{}, fmt.Errorf("failed to parse GHES Upload URL: %w", err)
}
rawURL, err := url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname()))

var rawURL *url.URL
if hasSubdomainIsolation {
// With subdomain isolation: https://raw.hostname/
rawURL, err = url.Parse(fmt.Sprintf("%s://raw.%s/", u.Scheme, u.Hostname()))
} else {
// Without subdomain isolation: https://hostname/raw/
rawURL, err = url.Parse(fmt.Sprintf("%s://%s/raw/", u.Scheme, u.Hostname()))
}
if err != nil {
return apiHost{}, fmt.Errorf("failed to parse GHES Raw URL: %w", err)
}
Expand All @@ -380,6 +400,29 @@ func newGHESHost(hostname string) (apiHost, error) {
}, nil
}

// checkSubdomainIsolation detects if GitHub Enterprise Server has subdomain isolation enabled
// by attempting to ping the raw.<host>/_ping endpoint on the subdomain. The raw subdomain must always exist for subdomain isolation.
func checkSubdomainIsolation(scheme, hostname string) bool {
subdomainURL := fmt.Sprintf("%s://raw.%s/_ping", scheme, hostname)

client := &http.Client{
Timeout: 5 * time.Second,
// Don't follow redirects - we just want to check if the endpoint exists
//nolint:revive // parameters are required by http.Client.CheckRedirect signature
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}

resp, err := client.Get(subdomainURL)
if err != nil {
return false
}
defer resp.Body.Close()

return resp.StatusCode == http.StatusOK
}

// Note that this does not handle ports yet, so development environments are out.
func parseAPIHost(s string) (apiHost, error) {
if s == "" {
Expand Down
6 changes: 4 additions & 2 deletions pkg/github/repositories.go
View file Open in desktop
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,8 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t

// If the path is (most likely) not to be a directory, we will
// first try to get the raw content from the GitHub raw content API.

var rawAPIResponseCode int
if path != "" && !strings.HasSuffix(path, "/") {
// First, get file info from Contents API to retrieve SHA
var fileSHA string
Expand Down Expand Up @@ -631,8 +633,8 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
return mcp.NewToolResultResource(fmt.Sprintf("successfully downloaded binary file (SHA: %s)", fileSHA), result), nil
}
return mcp.NewToolResultResource("successfully downloaded binary file", result), nil

}
rawAPIResponseCode = resp.StatusCode
}

if rawOpts.SHA != "" {
Expand Down Expand Up @@ -677,7 +679,7 @@ func GetFileContents(getClient GetClientFn, getRawClient raw.GetRawClientFn, t t
if err != nil {
return mcp.NewToolResultError(fmt.Sprintf("failed to marshal resolved refs: %s", err)), nil
}
return mcp.NewToolResultText(fmt.Sprintf("Path did not point to a file or directory, but resolved git ref to %s with possible path matches: %s", resolvedRefs, matchingFilesJSON)), nil
return mcp.NewToolResultError(fmt.Sprintf("Resolved potential matches in the repository tree (resolved refs: %s, matching files: %s), but the raw content API returned an unexpected status code %d.", string(resolvedRefs), string(matchingFilesJSON), rawAPIResponseCode)), nil
}

return mcp.NewToolResultError("Failed to get file contents. The path does not point to a file or directory, or the file does not exist in the repository."), nil
Expand Down
Loading

AltStyle によって変換されたページ (->オリジナル) /