Skip to content

Commit abd080f

Browse files
Register MCP App UI resources in shared server constructor
The remote/HTTP server never called RegisterUIResources, so when the remote_mcp_ui_apps feature flag was enabled per-request, tools like issue_write and create_pull_request would advertise a ui:// resource URI in their _meta.ui block but the resource itself was not registered. The client's follow-up resources/read call then failed with -32002 'Resource not found' (the error surfaced as 'Error loading MCP App: MPC -32002: Resource not found' in VS Code). The stdio bootstrap also gated registration on featureChecker called with context.Background(), which can't see per-request flag overrides. Move RegisterUIResources into pkg/github.NewMCPServer (the shared constructor used by both stdio and HTTP), gated only on UIAssetsAvailable(). The resources are inert static HTML; the inventory still strips _meta.ui from tools per-request via stripMCPAppsMetadata, so the URI is only advertised to clients when the flag is on for that request. Fixes #2467 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f5f9c72 commit abd080f

3 files changed

Lines changed: 135 additions & 9 deletions

File tree

internal/ghmcp/server.go

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,6 @@ func NewStdioMCPServer(ctx context.Context, cfg github.MCPServerConfig) (*mcp.Se
173173
return nil, fmt.Errorf("failed to create GitHub MCP server: %w", err)
174174
}
175175

176-
// Register MCP App UI resources if the remote_mcp_ui_apps feature flag is enabled
177-
// and UI assets are available (requires running script/build-ui).
178-
// We check availability to allow the feature flag to be enabled without
179-
// requiring a UI build (graceful degradation).
180-
mcpAppsEnabled, _ := featureChecker(context.Background(), github.MCPAppsFeatureFlag)
181-
if mcpAppsEnabled && github.UIAssetsAvailable() {
182-
github.RegisterUIResources(ghServer)
183-
}
184-
185176
ghServer.AddReceivingMiddleware(addUserAgentsMiddleware(cfg, clients.restUATransp, clients.gqlHTTP))
186177

187178
return ghServer, nil

pkg/github/server.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,18 @@ func NewMCPServer(ctx context.Context, cfg *MCPServerConfig, deps ToolDependenci
102102
// Register GitHub tools/resources/prompts from the inventory.
103103
inv.RegisterAll(ctx, ghServer, deps)
104104

105+
// Register MCP App UI resources whenever the embedded UI assets are
106+
// available. The resources are static HTML and are only referenced by
107+
// tools when the remote_mcp_ui_apps feature flag is enabled for the
108+
// request (the inventory strips the _meta.ui block otherwise via
109+
// stripMCPAppsMetadata), so registering them unconditionally is safe.
110+
// Registering here — rather than in the stdio bootstrap — ensures the
111+
// remote/HTTP server also serves them, fixing the "-32002 Resource not
112+
// found" error clients hit after the tool returns a ui:// URI.
113+
if UIAssetsAvailable() {
114+
RegisterUIResources(ghServer)
115+
}
116+
105117
return ghServer, nil
106118
}
107119

pkg/github/ui_resources_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/github/github-mcp-server/pkg/inventory"
8+
"github.com/modelcontextprotocol/go-sdk/mcp"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
// TestRegisterUIResources_ReadableViaClient verifies that each UI resource URI
14+
// advertised by an MCP App-enabled tool (e.g. issue_write, create_pull_request,
15+
// get_me) actually resolves to a registered resource on the server.
16+
//
17+
// Regression test for the "Error loading MCP App: MPC -32002: Resource not
18+
// found" bug reported in issue #2467, where the HTTP/remote server returned a
19+
// resource URI in the tool's _meta.ui block but never registered the matching
20+
// resource — so the follow-up resources/read call from the client failed.
21+
func TestRegisterUIResources_ReadableViaClient(t *testing.T) {
22+
t.Parallel()
23+
24+
if !UIAssetsAvailable() {
25+
t.Skip("UI assets not built; run script/build-ui to enable this test")
26+
}
27+
28+
srv := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.0.1"}, nil)
29+
RegisterUIResources(srv)
30+
31+
// Connect an in-memory client/server pair and read each advertised URI.
32+
st, ct := mcp.NewInMemoryTransports()
33+
34+
type clientResult struct {
35+
session *mcp.ClientSession
36+
err error
37+
}
38+
clientCh := make(chan clientResult, 1)
39+
go func() {
40+
client := mcp.NewClient(&mcp.Implementation{Name: "test-client"}, nil)
41+
cs, err := client.Connect(context.Background(), ct, nil)
42+
clientCh <- clientResult{session: cs, err: err}
43+
}()
44+
45+
ss, err := srv.Connect(context.Background(), st, nil)
46+
require.NoError(t, err)
47+
t.Cleanup(func() { _ = ss.Close() })
48+
49+
got := <-clientCh
50+
require.NoError(t, got.err)
51+
t.Cleanup(func() { _ = got.session.Close() })
52+
53+
uris := []string{
54+
GetMeUIResourceURI,
55+
IssueWriteUIResourceURI,
56+
PullRequestWriteUIResourceURI,
57+
}
58+
for _, uri := range uris {
59+
t.Run(uri, func(t *testing.T) {
60+
res, err := got.session.ReadResource(context.Background(), &mcp.ReadResourceParams{URI: uri})
61+
require.NoError(t, err, "resource %s should be registered (got -32002 means it isn't)", uri)
62+
require.NotNil(t, res)
63+
require.NotEmpty(t, res.Contents)
64+
assert.Equal(t, uri, res.Contents[0].URI)
65+
assert.Equal(t, MCPAppMIMEType, res.Contents[0].MIMEType)
66+
assert.NotEmpty(t, res.Contents[0].Text, "UI resource should return HTML body")
67+
})
68+
}
69+
}
70+
71+
// TestNewMCPServer_RegistersUIResources verifies that NewMCPServer — the
72+
// shared constructor used by both the stdio and HTTP entry points — registers
73+
// the UI resources when UI assets are embedded. Previously this registration
74+
// only happened in the stdio bootstrap, so remote/HTTP clients hit -32002.
75+
func TestNewMCPServer_RegistersUIResources(t *testing.T) {
76+
t.Parallel()
77+
78+
if !UIAssetsAvailable() {
79+
t.Skip("UI assets not built; run script/build-ui to enable this test")
80+
}
81+
82+
srv, err := NewMCPServer(context.Background(), &MCPServerConfig{
83+
Version: "test",
84+
Translator: stubTranslator,
85+
}, stubDeps{t: stubTranslator}, mustEmptyInventory(t))
86+
require.NoError(t, err)
87+
88+
st, ct := mcp.NewInMemoryTransports()
89+
90+
type clientResult struct {
91+
session *mcp.ClientSession
92+
err error
93+
}
94+
clientCh := make(chan clientResult, 1)
95+
go func() {
96+
client := mcp.NewClient(&mcp.Implementation{Name: "test-client"}, nil)
97+
cs, err := client.Connect(context.Background(), ct, nil)
98+
clientCh <- clientResult{session: cs, err: err}
99+
}()
100+
101+
ss, err := srv.Connect(context.Background(), st, nil)
102+
require.NoError(t, err)
103+
t.Cleanup(func() { _ = ss.Close() })
104+
105+
got := <-clientCh
106+
require.NoError(t, got.err)
107+
t.Cleanup(func() { _ = got.session.Close() })
108+
109+
res, err := got.session.ReadResource(context.Background(), &mcp.ReadResourceParams{URI: IssueWriteUIResourceURI})
110+
require.NoError(t, err)
111+
require.NotNil(t, res)
112+
require.NotEmpty(t, res.Contents)
113+
assert.Equal(t, MCPAppMIMEType, res.Contents[0].MIMEType)
114+
}
115+
116+
// mustEmptyInventory builds an empty inventory for tests that only care about
117+
// resources/prompts registered outside the inventory (such as the UI resources).
118+
func mustEmptyInventory(t *testing.T) *inventory.Inventory {
119+
t.Helper()
120+
inv, err := NewInventory(stubTranslator).WithToolsets([]string{}).Build()
121+
require.NoError(t, err)
122+
return inv
123+
}

0 commit comments

Comments
 (0)