Skip to content

Commit a94f95b

Browse files
Enhance client support checks for MCP Apps UI rendering (#2051)
* enhance client support checks for MCP Apps UI rendering * update dependencies and enhance MCP Apps UI support handling * chore: regenerate license files Auto-generated by license-check workflow * retrigger CI * update test * introduce constants for client names and remove wrong ide name for mcp apps support --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent c0ba3ed commit a94f95b

File tree

18 files changed

+395
-109
lines changed

18 files changed

+395
-109
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1105,7 +1105,7 @@ Options are:
11051105
// to distinguish form submissions from LLM calls.
11061106
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
11071107

1108-
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(req) && !uiSubmitted {
1108+
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(ctx, req) && !uiSubmitted {
11091109
if method == "update" {
11101110
issueNumber, numErr := RequiredInt(args, "issue_number")
11111111
if numErr != nil {

pkg/github/issues_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,7 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
957957
handler := serverTool.Handler(deps)
958958

959959
t.Run("UI client without _ui_submitted returns form message", func(t *testing.T) {
960-
request := createMCPRequestWithSession(t, "Visual Studio Code - Insiders", map[string]any{
960+
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
961961
"method": "create",
962962
"owner": "owner",
963963
"repo": "repo",
@@ -971,7 +971,7 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
971971
})
972972

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

pkg/github/pullrequests.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
537537
// to distinguish form submissions from LLM calls.
538538
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")
539539

540-
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(req) && !uiSubmitted {
540+
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(ctx, req) && !uiSubmitted {
541541
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
542542
}
543543

pkg/github/pullrequests_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
}

pkg/github/ui_capability_test.go

Lines changed: 59 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,84 @@ import (
44
"context"
55
"testing"
66

7+
ghcontext "github.com/github/github-mcp-server/pkg/context"
78
"github.com/modelcontextprotocol/go-sdk/mcp"
89
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
911
)
1012

13+
func createMCPRequestWithCapabilities(t *testing.T, caps *mcp.ClientCapabilities) mcp.CallToolRequest {
14+
t.Helper()
15+
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil)
16+
st, _ := mcp.NewInMemoryTransports()
17+
session, err := srv.Connect(context.Background(), st, &mcp.ServerSessionOptions{
18+
State: &mcp.ServerSessionState{
19+
InitializeParams: &mcp.InitializeParams{
20+
ClientInfo: &mcp.Implementation{Name: "test-client"},
21+
Capabilities: caps,
22+
},
23+
},
24+
})
25+
require.NoError(t, err)
26+
t.Cleanup(func() { _ = session.Close() })
27+
return mcp.CallToolRequest{Session: session}
28+
}
29+
1130
func Test_clientSupportsUI(t *testing.T) {
1231
t.Parallel()
32+
ctx := context.Background()
1333

14-
tests := []struct {
15-
name string
16-
clientName string
17-
want bool
18-
}{
19-
{name: "VS Code Insiders", clientName: "Visual Studio Code - Insiders", want: true},
20-
{name: "VS Code Stable", clientName: "Visual Studio Code", want: true},
21-
{name: "unknown client", clientName: "some-other-client", want: false},
22-
{name: "empty client name", clientName: "", want: false},
23-
}
24-
25-
for _, tt := range tests {
26-
t.Run(tt.name, func(t *testing.T) {
27-
req := createMCPRequestWithSession(t, tt.clientName, nil)
28-
assert.Equal(t, tt.want, clientSupportsUI(&req))
34+
t.Run("client with UI extension", func(t *testing.T) {
35+
caps := &mcp.ClientCapabilities{}
36+
caps.AddExtension("io.modelcontextprotocol/ui", map[string]any{
37+
"mimeTypes": []string{"text/html;profile=mcp-app"},
2938
})
30-
}
39+
req := createMCPRequestWithCapabilities(t, caps)
40+
assert.True(t, clientSupportsUI(ctx, &req))
41+
})
42+
43+
t.Run("client without UI extension", func(t *testing.T) {
44+
req := createMCPRequestWithCapabilities(t, &mcp.ClientCapabilities{})
45+
assert.False(t, clientSupportsUI(ctx, &req))
46+
})
47+
48+
t.Run("client with nil capabilities", func(t *testing.T) {
49+
req := createMCPRequestWithCapabilities(t, nil)
50+
assert.False(t, clientSupportsUI(ctx, &req))
51+
})
3152

3253
t.Run("nil request", func(t *testing.T) {
33-
assert.False(t, clientSupportsUI(nil))
54+
assert.False(t, clientSupportsUI(ctx, nil))
3455
})
3556

3657
t.Run("nil session", func(t *testing.T) {
3758
req := createMCPRequest(nil)
38-
assert.False(t, clientSupportsUI(&req))
59+
assert.False(t, clientSupportsUI(ctx, &req))
3960
})
4061
}
4162

42-
func Test_clientSupportsUI_nilClientInfo(t *testing.T) {
63+
func Test_clientSupportsUI_fromContext(t *testing.T) {
4364
t.Parallel()
4465

45-
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil)
46-
st, _ := mcp.NewInMemoryTransports()
47-
session, err := srv.Connect(context.Background(), st, &mcp.ServerSessionOptions{
48-
State: &mcp.ServerSessionState{
49-
InitializeParams: &mcp.InitializeParams{
50-
ClientInfo: nil,
51-
},
52-
},
66+
t.Run("UI supported in context", func(t *testing.T) {
67+
ctx := ghcontext.WithUISupport(context.Background(), true)
68+
assert.True(t, clientSupportsUI(ctx, nil))
69+
})
70+
71+
t.Run("UI not supported in context", func(t *testing.T) {
72+
ctx := ghcontext.WithUISupport(context.Background(), false)
73+
assert.False(t, clientSupportsUI(ctx, nil))
5374
})
54-
if err != nil {
55-
t.Fatal(err)
56-
}
57-
t.Cleanup(func() { _ = session.Close() })
5875

59-
req := mcp.CallToolRequest{Session: session}
60-
assert.False(t, clientSupportsUI(&req))
76+
t.Run("context takes precedence over session", func(t *testing.T) {
77+
ctx := ghcontext.WithUISupport(context.Background(), false)
78+
caps := &mcp.ClientCapabilities{}
79+
caps.AddExtension("io.modelcontextprotocol/ui", map[string]any{})
80+
req := createMCPRequestWithCapabilities(t, caps)
81+
assert.False(t, clientSupportsUI(ctx, &req))
82+
})
83+
84+
t.Run("no context or session", func(t *testing.T) {
85+
assert.False(t, clientSupportsUI(context.Background(), nil))
86+
})
6187
}

0 commit comments

Comments
 (0)