From 7179bbb4c69c41cd4da6d7d65251e6cc25dbdbf3 Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Sat, 30 May 2026 05:38:54 +0800 Subject: [PATCH] fix: hide write UI resources in read-only mode --- pkg/github/server.go | 2 +- pkg/github/ui_resources.go | 6 ++++- pkg/github/ui_resources_test.go | 46 ++++++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/pkg/github/server.go b/pkg/github/server.go index f56ac7d3a..7ec5837c3 100644 --- a/pkg/github/server.go +++ b/pkg/github/server.go @@ -111,7 +111,7 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci // remote/HTTP server also serves them, fixing the "-32002 Resource not // found" error clients hit after the tool returns a ui:// URI. if UIAssetsAvailable() { - RegisterUIResources(ghServer) + RegisterUIResources(ghServer, cfg.ReadOnly) } return ghServer, nil diff --git a/pkg/github/ui_resources.go b/pkg/github/ui_resources.go index ab3ebfd16..28051c0c4 100644 --- a/pkg/github/ui_resources.go +++ b/pkg/github/ui_resources.go @@ -13,7 +13,7 @@ import ( // // Resource metadata follows the stable 2026-01-26 MCP Apps spec: // https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/2026-01-26/apps.mdx -func RegisterUIResources(s *mcp.Server) { +func RegisterUIResources(s *mcp.Server, readOnly bool) { // Register the get_me UI resource s.AddResource( &mcp.Resource{ @@ -46,6 +46,10 @@ func RegisterUIResources(s *mcp.Server) { }, ) + if readOnly { + return + } + // Register the issue_write UI resource s.AddResource( &mcp.Resource{ diff --git a/pkg/github/ui_resources_test.go b/pkg/github/ui_resources_test.go index 928950ac7..7e67d5fae 100644 --- a/pkg/github/ui_resources_test.go +++ b/pkg/github/ui_resources_test.go @@ -2,6 +2,7 @@ package github import ( "context" + "slices" "testing" "github.com/github/github-mcp-server/pkg/inventory" @@ -26,7 +27,7 @@ func TestRegisterUIResources_ReadableViaClient(t *testing.T) { } srv := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil) - RegisterUIResources(srv) + RegisterUIResources(srv, false) // Connect an in-memory client/server pair and read each advertised URI. st, ct := mcp.NewInMemoryTransports() @@ -113,6 +114,49 @@ func TestNewMCPServer_RegistersUIResources(t *testing.T) { assert.Equal(t, MCPAppMIMEType, res.Contents[0].MIMEType) } +func TestRegisterUIResources_ReadOnlySkipsWriteResources(t *testing.T) { + t.Parallel() + + srv := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil) + RegisterUIResources(srv, true) + + st, ct := mcp.NewInMemoryTransports() + + type clientResult struct { + res *mcp.ListResourcesResult + err error + } + clientCh := make(chan clientResult, 1) + go func() { + client := mcp.NewClient(&mcp.Implementation{Name: "test-client"}, nil) + cs, err := client.Connect(context.Background(), ct, nil) + if err != nil { + clientCh <- clientResult{err: err} + return + } + defer func() { _ = cs.Close() }() + + res, err := cs.ListResources(context.Background(), nil) + clientCh <- clientResult{res: res, err: err} + }() + + ss, err := srv.Connect(context.Background(), st, nil) + require.NoError(t, err) + t.Cleanup(func() { _ = ss.Close() }) + + got := <-clientCh + require.NoError(t, got.err) + require.NotNil(t, got.res) + + names := make([]string, 0, len(got.res.Resources)) + for _, res := range got.res.Resources { + names = append(names, res.Name) + } + slices.Sort(names) + + assert.Equal(t, []string{"get_me_ui"}, names) +} + // mustEmptyInventory builds an empty inventory for tests that only care about // resources/prompts registered outside the inventory (such as the UI resources). func mustEmptyInventory(t *testing.T) *inventory.Inventory {