Skip to content

Commit a8a28cc

Browse files
Merge branch 'main' into fix-mcp-apps-update-before-submit
2 parents 066aeba + 3ffc06b commit a8a28cc

File tree

11 files changed

+343
-112
lines changed

11 files changed

+343
-112
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ Example for `https://octocorp.ghe.com` with GitHub PAT token:
153153
```
154154
{
155155
...
156-
"proxima-github": {
156+
"github-octocorp": {
157157
"type": "http",
158158
"url": "https://copilot-api.octocorp.ghe.com/mcp",
159159
"headers": {

pkg/github/issues.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,12 @@ func AddIssueComment(t translations.TranslationHelperFunc) inventory.ServerTool
688688
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to create comment", resp, body), nil, nil
689689
}
690690

691-
r, err := json.Marshal(createdComment)
691+
minimalResponse := MinimalResponse{
692+
ID: fmt.Sprintf("%d", createdComment.GetID()),
693+
URL: createdComment.GetHTMLURL(),
694+
}
695+
696+
r, err := json.Marshal(minimalResponse)
692697
if err != nil {
693698
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
694699
}

pkg/github/issues_test.go

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -458,13 +458,12 @@ func Test_AddIssueComment(t *testing.T) {
458458
// Parse the result and get the text content if no error
459459
textContent := getTextResult(t, result)
460460

461-
// Unmarshal and verify the result
462-
var returnedComment github.IssueComment
463-
err = json.Unmarshal([]byte(textContent.Text), &returnedComment)
461+
// Unmarshal and verify the result contains minimal response
462+
var minimalResponse MinimalResponse
463+
err = json.Unmarshal([]byte(textContent.Text), &minimalResponse)
464464
require.NoError(t, err)
465-
assert.Equal(t, *tc.expectedComment.ID, *returnedComment.ID)
466-
assert.Equal(t, *tc.expectedComment.Body, *returnedComment.Body)
467-
assert.Equal(t, *tc.expectedComment.User.Login, *returnedComment.User.Login)
465+
assert.Equal(t, fmt.Sprintf("%d", tc.expectedComment.GetID()), minimalResponse.ID)
466+
assert.Equal(t, tc.expectedComment.GetHTMLURL(), minimalResponse.URL)
468467

469468
})
470469
}

pkg/github/minimal_types.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,41 @@ func convertToMinimalCommit(commit *github.RepositoryCommit, includeDiffs bool)
612612
return minimalCommit
613613
}
614614

615+
// MinimalPageInfo contains pagination cursor information.
616+
type MinimalPageInfo struct {
617+
HasNextPage bool `json:"has_next_page"`
618+
HasPreviousPage bool `json:"has_previous_page"`
619+
StartCursor string `json:"start_cursor,omitempty"`
620+
EndCursor string `json:"end_cursor,omitempty"`
621+
}
622+
623+
// MinimalReviewComment is the trimmed output type for PR review comment objects.
624+
type MinimalReviewComment struct {
625+
Body string `json:"body,omitempty"`
626+
Path string `json:"path"`
627+
Line *int `json:"line,omitempty"`
628+
Author string `json:"author,omitempty"`
629+
CreatedAt string `json:"created_at,omitempty"`
630+
UpdatedAt string `json:"updated_at,omitempty"`
631+
HTMLURL string `json:"html_url"`
632+
}
633+
634+
// MinimalReviewThread is the trimmed output type for PR review thread objects.
635+
type MinimalReviewThread struct {
636+
IsResolved bool `json:"is_resolved"`
637+
IsOutdated bool `json:"is_outdated"`
638+
IsCollapsed bool `json:"is_collapsed"`
639+
Comments []MinimalReviewComment `json:"comments"`
640+
TotalCount int `json:"total_count"`
641+
}
642+
643+
// MinimalReviewThreadsResponse is the trimmed output for a paginated list of PR review threads.
644+
type MinimalReviewThreadsResponse struct {
645+
ReviewThreads []MinimalReviewThread `json:"review_threads"`
646+
TotalCount int `json:"total_count"`
647+
PageInfo MinimalPageInfo `json:"page_info"`
648+
}
649+
615650
func convertToMinimalPRFiles(files []*github.CommitFile) []MinimalPRFile {
616651
result := make([]MinimalPRFile, 0, len(files))
617652
for _, f := range files {
@@ -636,3 +671,61 @@ func convertToMinimalBranch(branch *github.Branch) MinimalBranch {
636671
Protected: branch.GetProtected(),
637672
}
638673
}
674+
675+
func convertToMinimalReviewThreadsResponse(query reviewThreadsQuery) MinimalReviewThreadsResponse {
676+
threads := query.Repository.PullRequest.ReviewThreads
677+
678+
minimalThreads := make([]MinimalReviewThread, 0, len(threads.Nodes))
679+
for _, thread := range threads.Nodes {
680+
minimalThreads = append(minimalThreads, convertToMinimalReviewThread(thread))
681+
}
682+
683+
return MinimalReviewThreadsResponse{
684+
ReviewThreads: minimalThreads,
685+
TotalCount: int(threads.TotalCount),
686+
PageInfo: MinimalPageInfo{
687+
HasNextPage: bool(threads.PageInfo.HasNextPage),
688+
HasPreviousPage: bool(threads.PageInfo.HasPreviousPage),
689+
StartCursor: string(threads.PageInfo.StartCursor),
690+
EndCursor: string(threads.PageInfo.EndCursor),
691+
},
692+
}
693+
}
694+
695+
func convertToMinimalReviewThread(thread reviewThreadNode) MinimalReviewThread {
696+
comments := make([]MinimalReviewComment, 0, len(thread.Comments.Nodes))
697+
for _, c := range thread.Comments.Nodes {
698+
comments = append(comments, convertToMinimalReviewComment(c))
699+
}
700+
701+
return MinimalReviewThread{
702+
IsResolved: bool(thread.IsResolved),
703+
IsOutdated: bool(thread.IsOutdated),
704+
IsCollapsed: bool(thread.IsCollapsed),
705+
Comments: comments,
706+
TotalCount: int(thread.Comments.TotalCount),
707+
}
708+
}
709+
710+
func convertToMinimalReviewComment(c reviewCommentNode) MinimalReviewComment {
711+
m := MinimalReviewComment{
712+
Body: string(c.Body),
713+
Path: string(c.Path),
714+
Author: string(c.Author.Login),
715+
HTMLURL: c.URL.String(),
716+
}
717+
718+
if c.Line != nil {
719+
line := int(*c.Line)
720+
m.Line = &line
721+
}
722+
723+
if !c.CreatedAt.IsZero() {
724+
m.CreatedAt = c.CreatedAt.Format(time.RFC3339)
725+
}
726+
if !c.UpdatedAt.IsZero() {
727+
m.UpdatedAt = c.UpdatedAt.Format(time.RFC3339)
728+
}
729+
730+
return m
731+
}

pkg/github/pullrequests.go

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -406,24 +406,7 @@ func GetPullRequestReviewComments(ctx context.Context, gqlClient *githubv4.Clien
406406
}
407407
}
408408

409-
// Build response with review threads and pagination info
410-
response := map[string]any{
411-
"reviewThreads": query.Repository.PullRequest.ReviewThreads.Nodes,
412-
"pageInfo": map[string]any{
413-
"hasNextPage": query.Repository.PullRequest.ReviewThreads.PageInfo.HasNextPage,
414-
"hasPreviousPage": query.Repository.PullRequest.ReviewThreads.PageInfo.HasPreviousPage,
415-
"startCursor": string(query.Repository.PullRequest.ReviewThreads.PageInfo.StartCursor),
416-
"endCursor": string(query.Repository.PullRequest.ReviewThreads.PageInfo.EndCursor),
417-
},
418-
"totalCount": int(query.Repository.PullRequest.ReviewThreads.TotalCount),
419-
}
420-
421-
r, err := json.Marshal(response)
422-
if err != nil {
423-
return nil, fmt.Errorf("failed to marshal response: %w", err)
424-
}
425-
426-
return utils.NewToolResultText(string(r)), nil
409+
return MarshalledTextResult(convertToMinimalReviewThreadsResponse(query)), nil
427410
}
428411

429412
func GetPullRequestReviews(ctx context.Context, client *github.Client, deps ToolDependencies, owner, repo string, pullNumber int) (*mcp.CallToolResult, error) {

pkg/github/pullrequests_test.go

Lines changed: 24 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,45 +1619,35 @@ func Test_GetPullRequestComments(t *testing.T) {
16191619
},
16201620
expectError: false,
16211621
validateResult: func(t *testing.T, textContent string) {
1622-
var result map[string]any
1622+
var result MinimalReviewThreadsResponse
16231623
err := json.Unmarshal([]byte(textContent), &result)
16241624
require.NoError(t, err)
16251625

1626-
// Validate response structure
1627-
assert.Contains(t, result, "reviewThreads")
1628-
assert.Contains(t, result, "pageInfo")
1629-
assert.Contains(t, result, "totalCount")
1630-
16311626
// Validate review threads
1632-
threads := result["reviewThreads"].([]any)
1633-
assert.Len(t, threads, 1)
1627+
assert.Len(t, result.ReviewThreads, 1)
16341628

1635-
thread := threads[0].(map[string]any)
1636-
assert.Equal(t, "RT_kwDOA0xdyM4AX1Yz", thread["ID"])
1637-
assert.Equal(t, false, thread["IsResolved"])
1638-
assert.Equal(t, false, thread["IsOutdated"])
1639-
assert.Equal(t, false, thread["IsCollapsed"])
1629+
thread := result.ReviewThreads[0]
1630+
assert.Equal(t, false, thread.IsResolved)
1631+
assert.Equal(t, false, thread.IsOutdated)
1632+
assert.Equal(t, false, thread.IsCollapsed)
16401633

16411634
// Validate comments within thread
1642-
comments := thread["Comments"].(map[string]any)
1643-
commentNodes := comments["Nodes"].([]any)
1644-
assert.Len(t, commentNodes, 2)
1635+
assert.Len(t, thread.Comments, 2)
16451636

16461637
// Validate first comment
1647-
comment1 := commentNodes[0].(map[string]any)
1648-
assert.Equal(t, "PRRC_kwDOA0xdyM4AX1Y0", comment1["ID"])
1649-
assert.Equal(t, "This looks good", comment1["Body"])
1650-
assert.Equal(t, "file1.go", comment1["Path"])
1638+
comment1 := thread.Comments[0]
1639+
assert.Equal(t, "This looks good", comment1.Body)
1640+
assert.Equal(t, "file1.go", comment1.Path)
1641+
assert.Equal(t, "reviewer1", comment1.Author)
16511642

16521643
// Validate pagination info
1653-
pageInfo := result["pageInfo"].(map[string]any)
1654-
assert.Equal(t, false, pageInfo["hasNextPage"])
1655-
assert.Equal(t, false, pageInfo["hasPreviousPage"])
1656-
assert.Equal(t, "cursor1", pageInfo["startCursor"])
1657-
assert.Equal(t, "cursor2", pageInfo["endCursor"])
1644+
assert.Equal(t, false, result.PageInfo.HasNextPage)
1645+
assert.Equal(t, false, result.PageInfo.HasPreviousPage)
1646+
assert.Equal(t, "cursor1", result.PageInfo.StartCursor)
1647+
assert.Equal(t, "cursor2", result.PageInfo.EndCursor)
16581648

16591649
// Validate total count
1660-
assert.Equal(t, float64(1), result["totalCount"])
1650+
assert.Equal(t, 1, result.TotalCount)
16611651
},
16621652
},
16631653
{
@@ -1761,27 +1751,22 @@ func Test_GetPullRequestComments(t *testing.T) {
17611751
expectError: false,
17621752
lockdownEnabled: true,
17631753
validateResult: func(t *testing.T, textContent string) {
1764-
var result map[string]any
1754+
var result MinimalReviewThreadsResponse
17651755
err := json.Unmarshal([]byte(textContent), &result)
17661756
require.NoError(t, err)
17671757

17681758
// Validate that only maintainer comment is returned
1769-
threads := result["reviewThreads"].([]any)
1770-
assert.Len(t, threads, 1)
1759+
assert.Len(t, result.ReviewThreads, 1)
17711760

1772-
thread := threads[0].(map[string]any)
1773-
comments := thread["Comments"].(map[string]any)
1761+
thread := result.ReviewThreads[0]
17741762

17751763
// Should only have 1 comment (maintainer) after filtering
1776-
assert.Equal(t, float64(1), comments["TotalCount"])
1777-
1778-
commentNodes := comments["Nodes"].([]any)
1779-
assert.Len(t, commentNodes, 1)
1764+
assert.Equal(t, 1, thread.TotalCount)
1765+
assert.Len(t, thread.Comments, 1)
17801766

1781-
comment := commentNodes[0].(map[string]any)
1782-
author := comment["Author"].(map[string]any)
1783-
assert.Equal(t, "maintainer", author["Login"])
1784-
assert.Equal(t, "Maintainer review comment", comment["Body"])
1767+
comment := thread.Comments[0]
1768+
assert.Equal(t, "maintainer", comment.Author)
1769+
assert.Equal(t, "Maintainer review comment", comment.Body)
17851770
},
17861771
},
17871772
}

pkg/http/oauth/oauth.go

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99

1010
"github.com/github/github-mcp-server/pkg/http/headers"
11+
"github.com/github/github-mcp-server/pkg/utils"
1112
"github.com/go-chi/chi/v5"
1213
"github.com/modelcontextprotocol/go-sdk/auth"
1314
"github.com/modelcontextprotocol/go-sdk/oauthex"
@@ -16,9 +17,6 @@ import (
1617
const (
1718
// OAuthProtectedResourcePrefix is the well-known path prefix for OAuth protected resource metadata.
1819
OAuthProtectedResourcePrefix = "/.well-known/oauth-protected-resource"
19-
20-
// DefaultAuthorizationServer is GitHub's OAuth authorization server.
21-
DefaultAuthorizationServer = "https://github.com/login/oauth"
2220
)
2321

2422
// SupportedScopes lists all OAuth scopes that may be required by MCP tools.
@@ -55,22 +53,27 @@ type Config struct {
5553

5654
// AuthHandler handles OAuth-related HTTP endpoints.
5755
type AuthHandler struct {
58-
cfg *Config
56+
cfg *Config
57+
apiHost utils.APIHostResolver
5958
}
6059

6160
// NewAuthHandler creates a new OAuth auth handler.
62-
func NewAuthHandler(cfg *Config) (*AuthHandler, error) {
61+
func NewAuthHandler(cfg *Config, apiHost utils.APIHostResolver) (*AuthHandler, error) {
6362
if cfg == nil {
6463
cfg = &Config{}
6564
}
6665

67-
// Default authorization server to GitHub
68-
if cfg.AuthorizationServer == "" {
69-
cfg.AuthorizationServer = DefaultAuthorizationServer
66+
if apiHost == nil {
67+
var err error
68+
apiHost, err = utils.NewAPIHost("https://api.github.com")
69+
if err != nil {
70+
return nil, fmt.Errorf("failed to create default API host: %w", err)
71+
}
7072
}
7173

7274
return &AuthHandler{
73-
cfg: cfg,
75+
cfg: cfg,
76+
apiHost: apiHost,
7477
}, nil
7578
}
7679

@@ -95,15 +98,28 @@ func (h *AuthHandler) RegisterRoutes(r chi.Router) {
9598

9699
func (h *AuthHandler) metadataHandler() http.Handler {
97100
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101+
ctx := r.Context()
98102
resourcePath := resolveResourcePath(
99103
strings.TrimPrefix(r.URL.Path, OAuthProtectedResourcePrefix),
100104
h.cfg.ResourcePath,
101105
)
102106
resourceURL := h.buildResourceURL(r, resourcePath)
103107

108+
var authorizationServerURL string
109+
if h.cfg.AuthorizationServer != "" {
110+
authorizationServerURL = h.cfg.AuthorizationServer
111+
} else {
112+
authURL, err := h.apiHost.AuthorizationServerURL(ctx)
113+
if err != nil {
114+
http.Error(w, fmt.Sprintf("failed to resolve authorization server URL: %v", err), http.StatusInternalServerError)
115+
return
116+
}
117+
authorizationServerURL = authURL.String()
118+
}
119+
104120
metadata := &oauthex.ProtectedResourceMetadata{
105121
Resource: resourceURL,
106-
AuthorizationServers: []string{h.cfg.AuthorizationServer},
122+
AuthorizationServers: []string{authorizationServerURL},
107123
ResourceName: "GitHub MCP Server",
108124
ScopesSupported: SupportedScopes,
109125
BearerMethodsSupported: []string{"header"},

0 commit comments

Comments
 (0)