Skip to content
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
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/josephburnett/jd/v2 v2.4.0
github.com/lithammer/fuzzysearch v1.1.8
github.com/microcosm-cc/bluemonday v1.0.27
github.com/modelcontextprotocol/go-sdk v1.3.0
github.com/modelcontextprotocol/go-sdk v1.3.1-0.20260220105450-b17143f71798
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466
Expand All @@ -35,6 +35,8 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/segmentio/asm v1.1.3 // indirect
github.com/segmentio/encoding v0.5.3 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
Expand All @@ -43,8 +45,8 @@ require (
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.28.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
24 changes: 14 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
Expand Down Expand Up @@ -44,8 +44,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
github.com/modelcontextprotocol/go-sdk v1.3.0 h1:gMfZkv3DzQF5q/DcQePo5rahEY+sguyPfXDfNBcT0Zs=
github.com/modelcontextprotocol/go-sdk v1.3.0/go.mod h1:AnQ//Qc6+4nIyyrB4cxBU7UW9VibK4iOZBeyP/rF1IE=
github.com/modelcontextprotocol/go-sdk v1.3.1-0.20260220105450-b17143f71798 h1:ogb5ErmcnxZgfaTeVZnKEMrwdHDpJ3yln5EhCIPcTlY=
github.com/modelcontextprotocol/go-sdk v1.3.1-0.20260220105450-b17143f71798/go.mod h1:Nxc2n+n/GdCebUaqCOhTetptS17SXXNu9IfNTaLDi1E=
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021 h1:31Y+Yu373ymebRdJN1cWLLooHH8xAr0MhKTEJGV/87g=
github.com/muesli/cache2go v0.0.0-20221011235721-518229cd8021/go.mod h1:WERUkUryfUWlrHnFSO/BEUZ+7Ns8aZy7iVOGewxKzcc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
Expand All @@ -57,6 +57,10 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w=
github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 h1:cYCy18SHPKRkvclm+pWm1Lk4YrREb4IOIb/YdFO0p2M=
github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7/go.mod h1:zqMwyHmnN/eDOZOdiTohqIUKUrTFX62PNlu7IJdu0q8=
github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZVHkmAsGEkcIu0oCe3AM420QDgGwZx0=
Expand Down Expand Up @@ -97,8 +101,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -108,8 +112,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
Expand All @@ -124,8 +128,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
Expand Down
16 changes: 16 additions & 0 deletions pkg/context/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,19 @@ func GetHeaderFeatures(ctx context.Context) []string {
}
return nil
}

// uiSupportCtxKey is a context key for MCP Apps UI support
type uiSupportCtxKey struct{}

// WithUISupport stores whether the client supports MCP Apps UI in the context.
// This is used by HTTP/stateless servers where the go-sdk session may not
// persist client capabilities across requests.
func WithUISupport(ctx context.Context, supported bool) context.Context {
return context.WithValue(ctx, uiSupportCtxKey{}, supported)
}

// HasUISupport retrieves the MCP Apps UI support flag from context.
func HasUISupport(ctx context.Context) (supported bool, ok bool) {
v, ok := ctx.Value(uiSupportCtxKey{}).(bool)
return v, ok
}
22 changes: 18 additions & 4 deletions pkg/github/helper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,10 +291,16 @@ func createMCPRequest(args any) mcp.CallToolRequest {
}
}

// Well-known MCP client names used in tests.
const (
ClientNameVSCodeInsiders = "Visual Studio Code - Insiders"
ClientNameVSCode = "Visual Studio Code"
)

// createMCPRequestWithSession creates a CallToolRequest with a ServerSession
// that has the given client name in its InitializeParams. This is used to test
// UI capability detection based on ClientInfo.Name.
func createMCPRequestWithSession(t *testing.T, clientName string, args any) mcp.CallToolRequest {
// that has the given client name in its InitializeParams. When withUI is true
// the session advertises MCP Apps UI support via the capability extension.
func createMCPRequestWithSession(t *testing.T, clientName string, withUI bool, args any) mcp.CallToolRequest {
t.Helper()

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

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

caps := &mcp.ClientCapabilities{}
if withUI {
caps.AddExtension("io.modelcontextprotocol/ui", map[string]any{
"mimeTypes": []string{"text/html;profile=mcp-app"},
})
}
Comment on lines 315 to 320
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This helper now decides whether to add the UI extension by matching on clientName (VS Code names). Since UI support detection has been refactored away from client-name matching, consider making createMCPRequestWithSession accept an explicit *mcp.ClientCapabilities (or a uiSupported bool) so tests don’t implicitly rely on client-name heuristics and can set capabilities directly.

Copilot uses AI. Check for mistakes.

st, _ := mcp.NewInMemoryTransports()
session, err := srv.Connect(context.Background(), st, &mcp.ServerSessionOptions{
State: &mcp.ServerSessionState{
InitializeParams: &mcp.InitializeParams{
ClientInfo: &mcp.Implementation{Name: clientName},
ClientInfo: &mcp.Implementation{Name: clientName},
Capabilities: caps,
},
},
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/issues.go
Original file line number Diff line number Diff line change
Expand Up @@ -1105,7 +1105,7 @@ Options are:
// to distinguish form submissions from LLM calls.
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")

if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(req) && !uiSubmitted {
if deps.GetFlags(ctx).InsidersMode && clientSupportsUI(ctx, req) && !uiSubmitted {
if method == "update" {
issueNumber, numErr := RequiredInt(args, "issue_number")
if numErr != nil {
Expand Down
4 changes: 2 additions & 2 deletions pkg/github/issues_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -957,7 +957,7 @@ func Test_IssueWrite_InsidersMode_UIGate(t *testing.T) {
handler := serverTool.Handler(deps)

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

t.Run("UI client with _ui_submitted executes directly", func(t *testing.T) {
request := createMCPRequestWithSession(t, "Visual Studio Code - Insiders", map[string]any{
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
"method": "create",
"owner": "owner",
"repo": "repo",
Expand Down
2 changes: 1 addition & 1 deletion pkg/github/pullrequests.go
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
// to distinguish form submissions from LLM calls.
uiSubmitted, _ := OptionalParam[bool](args, "_ui_submitted")

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

Expand Down
4 changes: 2 additions & 2 deletions pkg/github/pullrequests_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2185,7 +2185,7 @@ func Test_CreatePullRequest_InsidersMode_UIGate(t *testing.T) {
handler := serverTool.Handler(deps)

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

t.Run("UI client with _ui_submitted executes directly", func(t *testing.T) {
request := createMCPRequestWithSession(t, "Visual Studio Code", map[string]any{
request := createMCPRequestWithSession(t, ClientNameVSCodeInsiders, true, map[string]any{
"owner": "owner",
"repo": "repo",
"title": "Test PR",
Expand Down
43 changes: 24 additions & 19 deletions pkg/github/ui_capability.go
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
package github

import "github.com/modelcontextprotocol/go-sdk/mcp"
import (
"context"

// uiSupportedClients lists client names (from ClientInfo.Name) known to
// support MCP Apps UI rendering.
//
// This is a temporary workaround until the Go SDK adds an Extensions field
// to ClientCapabilities (see https://github.com/modelcontextprotocol/go-sdk/issues/777).
// Once that lands, detection should use capabilities.extensions instead.
var uiSupportedClients = map[string]bool{
"Visual Studio Code - Insiders": true,
"Visual Studio Code": true,
}
ghcontext "github.com/github/github-mcp-server/pkg/context"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

// mcpAppsExtensionKey is the capability extension key that clients use to
// advertise MCP Apps UI support.
const mcpAppsExtensionKey = "io.modelcontextprotocol/ui"

// clientSupportsUI reports whether the MCP client that sent this request
// supports MCP Apps UI rendering, based on its ClientInfo.Name.
func clientSupportsUI(req *mcp.CallToolRequest) bool {
if req == nil || req.Session == nil {
return false
// supports MCP Apps UI rendering.
// It checks the context first (set by HTTP/stateless servers from stored
// session capabilities), then falls back to the go-sdk Session (for stdio).
func clientSupportsUI(ctx context.Context, req *mcp.CallToolRequest) bool {
// Check context first (works for HTTP/stateless servers)
Comment on lines +16 to +19
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clientSupportsUI now prefers a context flag for stateless/HTTP servers, but nothing in this repo currently sets that flag (the only WithUISupport usages are in tests). As a result, stateless HTTP requests will still fall back to the go-sdk session (which may not carry client capabilities across requests) and UI support may never be detected. Either wire WithUISupport into the HTTP request path (e.g., extract UI support from the initialize/capabilities for the request and set it on the request context), or adjust the comment/logic to reflect how this will actually be provided.

Suggested change
// It checks the context first (set by HTTP/stateless servers from stored
// session capabilities), then falls back to the go-sdk Session (for stdio).
func clientSupportsUI(ctx context.Context, req *mcp.CallToolRequest) bool {
// Check context first (works for HTTP/stateless servers)
// It first checks for a UI-support flag on the context (when explicitly set
// by the caller, e.g., HTTP/stateless servers that persist capabilities),
// then falls back to the go-sdk Session (for stdio/stateful servers).
func clientSupportsUI(ctx context.Context, req *mcp.CallToolRequest) bool {
// Prefer explicit context flag when provided (e.g., HTTP/stateless servers)

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a legit issue or not @mattdholloway? Will we only support this in remote and set the context on the request there, or should this also be supported in STDIO?

if supported, ok := ghcontext.HasUISupport(ctx); ok {
return supported
}
params := req.Session.InitializeParams()
if params == nil || params.ClientInfo == nil {
return false
// Fall back to go-sdk session (works for stdio/stateful servers)
if req != nil && req.Session != nil {
params := req.Session.InitializeParams()
if params != nil && params.Capabilities != nil {
_, hasUI := params.Capabilities.Extensions[mcpAppsExtensionKey]
return hasUI
}
}
return uiSupportedClients[params.ClientInfo.Name]
return false
}
92 changes: 59 additions & 33 deletions pkg/github/ui_capability_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,58 +4,84 @@ import (
"context"
"testing"

ghcontext "github.com/github/github-mcp-server/pkg/context"
"github.com/modelcontextprotocol/go-sdk/mcp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func createMCPRequestWithCapabilities(t *testing.T, caps *mcp.ClientCapabilities) mcp.CallToolRequest {
t.Helper()
srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil)
st, _ := mcp.NewInMemoryTransports()
session, err := srv.Connect(context.Background(), st, &mcp.ServerSessionOptions{
State: &mcp.ServerSessionState{
InitializeParams: &mcp.InitializeParams{
ClientInfo: &mcp.Implementation{Name: "test-client"},
Capabilities: caps,
},
},
})
require.NoError(t, err)
t.Cleanup(func() { _ = session.Close() })
return mcp.CallToolRequest{Session: session}
}

func Test_clientSupportsUI(t *testing.T) {
t.Parallel()
ctx := context.Background()

tests := []struct {
name string
clientName string
want bool
}{
{name: "VS Code Insiders", clientName: "Visual Studio Code - Insiders", want: true},
{name: "VS Code Stable", clientName: "Visual Studio Code", want: true},
{name: "unknown client", clientName: "some-other-client", want: false},
{name: "empty client name", clientName: "", want: false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := createMCPRequestWithSession(t, tt.clientName, nil)
assert.Equal(t, tt.want, clientSupportsUI(&req))
t.Run("client with UI extension", func(t *testing.T) {
caps := &mcp.ClientCapabilities{}
caps.AddExtension("io.modelcontextprotocol/ui", map[string]any{
"mimeTypes": []string{"text/html;profile=mcp-app"},
})
}
req := createMCPRequestWithCapabilities(t, caps)
assert.True(t, clientSupportsUI(ctx, &req))
})

t.Run("client without UI extension", func(t *testing.T) {
req := createMCPRequestWithCapabilities(t, &mcp.ClientCapabilities{})
assert.False(t, clientSupportsUI(ctx, &req))
})

t.Run("client with nil capabilities", func(t *testing.T) {
req := createMCPRequestWithCapabilities(t, nil)
assert.False(t, clientSupportsUI(ctx, &req))
})

t.Run("nil request", func(t *testing.T) {
assert.False(t, clientSupportsUI(nil))
assert.False(t, clientSupportsUI(ctx, nil))
})

t.Run("nil session", func(t *testing.T) {
req := createMCPRequest(nil)
assert.False(t, clientSupportsUI(&req))
assert.False(t, clientSupportsUI(ctx, &req))
})
}

func Test_clientSupportsUI_nilClientInfo(t *testing.T) {
func Test_clientSupportsUI_fromContext(t *testing.T) {
t.Parallel()

srv := mcp.NewServer(&mcp.Implementation{Name: "test"}, nil)
st, _ := mcp.NewInMemoryTransports()
session, err := srv.Connect(context.Background(), st, &mcp.ServerSessionOptions{
State: &mcp.ServerSessionState{
InitializeParams: &mcp.InitializeParams{
ClientInfo: nil,
},
},
t.Run("UI supported in context", func(t *testing.T) {
ctx := ghcontext.WithUISupport(context.Background(), true)
assert.True(t, clientSupportsUI(ctx, nil))
})

t.Run("UI not supported in context", func(t *testing.T) {
ctx := ghcontext.WithUISupport(context.Background(), false)
assert.False(t, clientSupportsUI(ctx, nil))
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = session.Close() })

req := mcp.CallToolRequest{Session: session}
assert.False(t, clientSupportsUI(&req))
t.Run("context takes precedence over session", func(t *testing.T) {
ctx := ghcontext.WithUISupport(context.Background(), false)
caps := &mcp.ClientCapabilities{}
caps.AddExtension("io.modelcontextprotocol/ui", map[string]any{})
req := createMCPRequestWithCapabilities(t, caps)
assert.False(t, clientSupportsUI(ctx, &req))
})

t.Run("no context or session", func(t *testing.T) {
assert.False(t, clientSupportsUI(context.Background(), nil))
})
}
Loading