Skip to content

Commit f21b014

Browse files
authored
Merge branch 'main' into kerobbi/response-optimisation
2 parents c29fcb5 + a94f95b commit f21b014

File tree

19 files changed

+446
-126
lines changed

19 files changed

+446
-126
lines changed

go.mod

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ require (
1010
github.com/josephburnett/jd/v2 v2.4.0
1111
github.com/lithammer/fuzzysearch v1.1.8
1212
github.com/microcosm-cc/bluemonday v1.0.27
13-
github.com/modelcontextprotocol/go-sdk v1.3.0
13+
github.com/modelcontextprotocol/go-sdk v1.3.1-0.20260220105450-b17143f71798
1414
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021
1515
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
1616
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
@@ -35,6 +35,8 @@ require (
3535
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
3636
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
3737
github.com/sagikazarmark/locafero v0.11.0 // indirect
38+
github.com/segmentio/asm v1.1.3 // indirect
39+
github.com/segmentio/encoding v0.5.3 // indirect
3840
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
3941
github.com/spf13/afero v1.15.0 // indirect
4042
github.com/spf13/cast v1.10.0 // indirect
@@ -43,8 +45,8 @@ require (
4345
go.yaml.in/yaml/v3 v3.0.4 // indirect
4446
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
4547
golang.org/x/net v0.38.0 // indirect
46-
golang.org/x/oauth2 v0.30.0 // indirect
47-
golang.org/x/sys v0.31.0 // indirect
48+
golang.org/x/oauth2 v0.34.0 // indirect
49+
golang.org/x/sys v0.40.0 // indirect
4850
golang.org/x/text v0.28.0 // indirect
4951
gopkg.in/yaml.v3 v3.0.1 // indirect
5052
)

go.sum

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
1515
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
1616
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
1717
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
18-
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
19-
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
18+
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
19+
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
2020
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
2121
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2222
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
@@ -44,8 +44,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
4444
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
4545
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
4646
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
47-
github.com/modelcontextprotocol/go-sdk v1.3.0 h1:gMfZkv3DzQF5q/DcQePo5rahEY+sguyPfXDfNBcT0Zs=
48-
github.com/modelcontextprotocol/go-sdk v1.3.0/go.mod h1:AnQ//Qc6+4nIyyrB4cxBU7UW9VibK4iOZBeyP/rF1IE=
47+
github.com/modelcontextprotocol/go-sdk v1.3.1-0.20260220105450-b17143f71798 h1:ogb5ErmcnxZgfaTeVZnKEMrwdHDpJ3yln5EhCIPcTlY=
48+
github.com/modelcontextprotocol/go-sdk v1.3.1-0.20260220105450-b17143f71798/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
4949
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021 h1:31Y+Yu373ymebRdJN1cWLLooHH8xAr0MhKTEJGV/87g=
5050
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021/go.mod h1:WERUkUryfUWlrHnFSO/BEUZ+7Ns8aZy7iVOGewxKzcc=
5151
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
@@ -57,6 +57,10 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
5757
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5858
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
5959
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
60+
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
61+
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
62+
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
63+
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
6064
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
6165
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
6266
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
@@ -97,8 +101,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
97101
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
98102
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
99103
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
100-
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
101-
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
104+
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
105+
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
102106
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
103107
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
104108
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -108,8 +112,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
108112
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
109113
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
110114
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
111-
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
112-
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
115+
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
116+
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
113117
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
114118
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
115119
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -124,8 +128,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
124128
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
125129
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
126130
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
127-
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
128-
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
131+
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
132+
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
129133
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
130134
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
131135
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=

pkg/context/request.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,19 @@ func GetHeaderFeatures(ctx context.Context) []string {
113113
}
114114
return nil
115115
}
116+
117+
// uiSupportCtxKey is a context key for MCP Apps UI support
118+
type uiSupportCtxKey struct{}
119+
120+
// WithUISupport stores whether the client supports MCP Apps UI in the context.
121+
// This is used by HTTP/stateless servers where the go-sdk session may not
122+
// persist client capabilities across requests.
123+
func WithUISupport(ctx context.Context, supported bool) context.Context {
124+
return context.WithValue(ctx, uiSupportCtxKey{}, supported)
125+
}
126+
127+
// HasUISupport retrieves the MCP Apps UI support flag from context.
128+
func HasUISupport(ctx context.Context) (supported bool, ok bool) {
129+
v, ok := ctx.Value(uiSupportCtxKey{}).(bool)
130+
return v, ok
131+
}

pkg/github/helper_test.go

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -291,10 +291,16 @@ func createMCPRequest(args any) mcp.CallToolRequest {
291291
}
292292
}
293293

294+
// Well-known MCP client names used in tests.
295+
const (
296+
ClientNameVSCodeInsiders = "Visual Studio Code - Insiders"
297+
ClientNameVSCode = "Visual Studio Code"
298+
)
299+
294300
// createMCPRequestWithSession creates a CallToolRequest with a ServerSession
295-
// that has the given client name in its InitializeParams. This is used to test
296-
// UI capability detection based on ClientInfo.Name.
297-
func createMCPRequestWithSession(t *testing.T, clientName string, args any) mcp.CallToolRequest {
301+
// that has the given client name in its InitializeParams. When withUI is true
302+
// the session advertises MCP Apps UI support via the capability extension.
303+
func createMCPRequestWithSession(t *testing.T, clientName string, withUI bool, args any) mcp.CallToolRequest {
298304
t.Helper()
299305

300306
argsMap, ok := args.(map[string]any)
@@ -306,11 +312,19 @@ func createMCPRequestWithSession(t *testing.T, clientName string, args any) mcp.
306312

307313
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil)
308314

315+
caps := &mcp.ClientCapabilities{}
316+
if withUI {
317+
caps.AddExtension("io.modelcontextprotocol/ui", map[string]any{
318+
"mimeTypes": []string{"text/html;profile=mcp-app"},
319+
})
320+
}
321+
309322
st, _ := mcp.NewInMemoryTransports()
310323
session, err := srv.Connect(context.Background(), st, &mcp.ServerSessionOptions{
311324
State: &mcp.ServerSessionState{
312325
InitializeParams: &mcp.InitializeParams{
313-
ClientInfo: &mcp.Implementation{Name: clientName},
326+
ClientInfo: &mcp.Implementation{Name: clientName},
327+
Capabilities: caps,
314328
},
315329
},
316330
})

pkg/github/issues.go

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

692-
r, err := json.Marshal(createdComment)
692+
minimalResponse := MinimalResponse{
693+
ID: fmt.Sprintf("%d", createdComment.GetID()),
694+
URL: createdComment.GetHTMLURL(),
695+
}
696+
697+
r, err := json.Marshal(minimalResponse)
693698
if err != nil {
694699
return utils.NewToolResultErrorFromErr("failed to marshal response", err), nil, nil
695700
}
@@ -1101,7 +1106,7 @@ Options are:
11011106
// to distinguish form submissions from LLM calls.
11021107
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
11031108

1104-
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(req) && !uiSubmitted {
1109+
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(ctx, req) && !uiSubmitted {
11051110
if method == "update" {
11061111
issueNumber, numErr := RequiredInt(args, "issue_number")
11071112
if numErr != nil {

pkg/github/issues_test.go

Lines changed: 7 additions & 8 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
}
@@ -958,7 +957,7 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
958957
handler := serverTool.Handler(deps)
959958

960959
t.Run("UI client without _ui_submitted returns form message", func(t *testing.T) {
961-
request := createMCPRequestWithSession(t, "Visual Studio Code - Insiders", map[string]any{
960+
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
962961
"method": "create",
963962
"owner": "owner",
964963
"repo": "repo",
@@ -972,7 +971,7 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
972971
})
973972

974973
t.Run("UI client with _ui_submitted executes directly", func(t *testing.T) {
975-
request := createMCPRequestWithSession(t, "Visual Studio Code - Insiders", map[string]any{
974+
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
976975
"method": "create",
977976
"owner": "owner",
978977
"repo": "repo",

pkg/github/minimal_types.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,8 +278,38 @@ type MinimalProjectStatusUpdate struct {
278278
Creator *MinimalUser `json:"creator,omitempty"`
279279
}
280280

281+
// MinimalPullRequestReview is the trimmed output type for pull request review objects to reduce verbosity.
282+
type MinimalPullRequestReview struct {
283+
ID int64 `json:"id"`
284+
State string `json:"state"`
285+
Body string `json:"body,omitempty"`
286+
HTMLURL string `json:"html_url"`
287+
User *MinimalUser `json:"user,omitempty"`
288+
CommitID string `json:"commit_id,omitempty"`
289+
SubmittedAt string `json:"submitted_at,omitempty"`
290+
AuthorAssociation string `json:"author_association,omitempty"`
291+
}
292+
281293
// Helper functions
282294

295+
func convertToMinimalPullRequestReview(review *github.PullRequestReview) MinimalPullRequestReview {
296+
m := MinimalPullRequestReview{
297+
ID: review.GetID(),
298+
State: review.GetState(),
299+
Body: review.GetBody(),
300+
HTMLURL: review.GetHTMLURL(),
301+
User: convertToMinimalUser(review.GetUser()),
302+
CommitID: review.GetCommitID(),
303+
AuthorAssociation: review.GetAuthorAssociation(),
304+
}
305+
306+
if review.SubmittedAt != nil {
307+
m.SubmittedAt = review.SubmittedAt.Format(time.RFC3339)
308+
}
309+
310+
return m
311+
}
312+
283313
func convertToMinimalIssue(issue *github.Issue) MinimalIssue {
284314
m := MinimalIssue{
285315
Number: issue.GetNumber(),

pkg/github/pullrequests.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -455,12 +455,12 @@ func GetPullRequestReviews(ctx context.Context, client *github.Client, deps Tool
455455
}
456456
}
457457

458-
r, err := json.Marshal(reviews)
459-
if err != nil {
460-
return nil, fmt.Errorf("failed to marshal response: %w", err)
458+
minimalReviews := make([]MinimalPullRequestReview, 0, len(reviews))
459+
for _, review := range reviews {
460+
minimalReviews = append(minimalReviews, convertToMinimalPullRequestReview(review))
461461
}
462462

463-
return utils.NewToolResultText(string(r)), nil
463+
return MarshalledTextResult(minimalReviews), nil
464464
}
465465

466466
// PullRequestWriteUIResourceURI is the URI for the create_pull_request tool's MCP App UI resource.
@@ -538,7 +538,7 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
538538
// to distinguish form submissions from LLM calls.
539539
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
540540

541-
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(req) && !uiSubmitted {
541+
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(ctx, req) && !uiSubmitted {
542542
return utils.NewToolResultText(fmt.Sprintf("Ready to create a pull request in %s/%s. The user will review and confirm via the interactive form.", owner, repo)), nil, nil
543543
}
544544

pkg/github/pullrequests_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,18 +1990,18 @@ func Test_GetPullRequestReviews(t *testing.T) {
19901990
textContent := getTextResult(t, result)
19911991

19921992
// Unmarshal and verify the result
1993-
var returnedReviews []*github.PullRequestReview
1993+
var returnedReviews []MinimalPullRequestReview
19941994
err = json.Unmarshal([]byte(textContent.Text), &returnedReviews)
19951995
require.NoError(t, err)
19961996
assert.Len(t, returnedReviews, len(tc.expectedReviews))
19971997
for i, review := range returnedReviews {
1998+
assert.Equal(t, tc.expectedReviews[i].GetID(), review.ID)
1999+
assert.Equal(t, tc.expectedReviews[i].GetState(), review.State)
2000+
assert.Equal(t, tc.expectedReviews[i].GetBody(), review.Body)
19982001
require.NotNil(t, tc.expectedReviews[i].User)
19992002
require.NotNil(t, review.User)
2000-
assert.Equal(t, tc.expectedReviews[i].GetID(), review.GetID())
2001-
assert.Equal(t, tc.expectedReviews[i].GetState(), review.GetState())
2002-
assert.Equal(t, tc.expectedReviews[i].GetBody(), review.GetBody())
2003-
assert.Equal(t, tc.expectedReviews[i].GetUser().GetLogin(), review.GetUser().GetLogin())
2004-
assert.Equal(t, tc.expectedReviews[i].GetHTMLURL(), review.GetHTMLURL())
2003+
assert.Equal(t, tc.expectedReviews[i].GetUser().GetLogin(), review.User.Login)
2004+
assert.Equal(t, tc.expectedReviews[i].GetHTMLURL(), review.HTMLURL)
20052005
}
20062006
})
20072007
}
@@ -2185,7 +2185,7 @@ func Test_CreatePullRequest_InsidersMode_UIGate(t *testing.T) {
21852185
handler := serverTool.Handler(deps)
21862186

21872187
t.Run("UI client without _ui_submitted returns form message", func(t *testing.T) {
2188-
request := createMCPRequestWithSession(t, "Visual Studio Code", map[string]any{
2188+
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
21892189
"owner": "owner",
21902190
"repo": "repo",
21912191
"title": "Test PR",
@@ -2200,7 +2200,7 @@ func Test_CreatePullRequest_InsidersMode_UIGate(t *testing.T) {
22002200
})
22012201

22022202
t.Run("UI client with _ui_submitted executes directly", func(t *testing.T) {
2203-
request := createMCPRequestWithSession(t, "Visual Studio Code", map[string]any{
2203+
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
22042204
"owner": "owner",
22052205
"repo": "repo",
22062206
"title": "Test PR",

pkg/github/ui_capability.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
11
package github
22

3-
import "github.com/modelcontextprotocol/go-sdk/mcp"
3+
import (
4+
"context"
45

5-
// uiSupportedClients lists client names (from ClientInfo.Name) known to
6-
// support MCP Apps UI rendering.
7-
//
8-
// This is a temporary workaround until the Go SDK adds an Extensions field
9-
// to ClientCapabilities (see https://github.com/modelcontextprotocol/go-sdk/issues/777).
10-
// Once that lands, detection should use capabilities.extensions instead.
11-
var uiSupportedClients = map[string]bool{
12-
"Visual Studio Code - Insiders": true,
13-
"Visual Studio Code": true,
14-
}
6+
ghcontext "github.com/github/github-mcp-server/pkg/context"
7+
"github.com/modelcontextprotocol/go-sdk/mcp"
8+
)
9+
10+
// mcpAppsExtensionKey is the capability extension key that clients use to
11+
// advertise MCP Apps UI support.
12+
const mcpAppsExtensionKey = "io.modelcontextprotocol/ui"
1513

1614
// clientSupportsUI reports whether the MCP client that sent this request
17-
// supports MCP Apps UI rendering, based on its ClientInfo.Name.
18-
func clientSupportsUI(req *mcp.CallToolRequest) bool {
19-
if req == nil || req.Session == nil {
20-
return false
15+
// supports MCP Apps UI rendering.
16+
// It checks the context first (set by HTTP/stateless servers from stored
17+
// session capabilities), then falls back to the go-sdk Session (for stdio).
18+
func clientSupportsUI(ctx context.Context, req *mcp.CallToolRequest) bool {
19+
// Check context first (works for HTTP/stateless servers)
20+
if supported, ok := ghcontext.HasUISupport(ctx); ok {
21+
return supported
2122
}
22-
params := req.Session.InitializeParams()
23-
if params == nil || params.ClientInfo == nil {
24-
return false
23+
// Fall back to go-sdk session (works for stdio/stateful servers)
24+
if req != nil && req.Session != nil {
25+
params := req.Session.InitializeParams()
26+
if params != nil && params.Capabilities != nil {
27+
_, hasUI := params.Capabilities.Extensions[mcpAppsExtensionKey]
28+
return hasUI
29+
}
2530
}
26-
return uiSupportedClients[params.ClientInfo.Name]
31+
return false
2732
}

0 commit comments

Comments
 (0)