0503f2f2c5
* fix(ui): render success view when an MCP App tool executed up-front The create_pull_request / issue_write / update_pull_request Views decided form-vs-success from in-app submit state only, ignoring the tool-result the host delivers on render. Per the MCP Apps 2026-01-26 spec the host renders a View whenever the tool carries _meta.ui.resourceUri — independent of whether the server deferred or executed. So when the server executed up-front (e.g. show_ui=false, or parameters the form can't represent) the View still showed its "Create pull request" input form over an already-created PR, which reads as a bug (it even shows a PR number). Drive the Views off the result instead: a new shared completedToolResult() helper returns parsed data only for a genuine completed success, and returns null for the awaiting_user_submission deferral sentinel, errors, or no result. Each write View now shows its success card when that completed result is present, so the form is only ever shown while the action is genuinely deferred. Reconciles the show/defer state machine at the View (decision layer that the host result feeds). See github/copilot-mcp-core#1864. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix(ui): scope tool-result to the current invocation Address review feedback: the write Views derive their success card from `toolResult`, but it wasn't cleared when a new invocation arrived (only the in-app `successPR`/`successIssue` was reset on `toolInput` change). A completed result from a previous invocation could briefly render a stale success card over the next, still-deferred form. Clear `toolResult` whenever a new `tool-input` notification arrives. The spec guarantees `tool-input` precedes that invocation's `tool-result`, so this scopes the result to the current invocation centrally in the hook — fixing all three Views without per-app invocation keys. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(mcp-apps): remove show_ui — it can't suppress app rendering show_ui promised "skip the form and execute directly", but it can't deliver: the host renders an MCP App for any tool that carries _meta.ui.resourceUri, and the 2026-01-26 MCP Apps spec has no per-call/per-result way to opt out of rendering. show_ui only flipped the server's defer decision, so show_ui=false created the PR/issue up-front yet the host still rendered the app — exactly the contradiction this work set out to fix. And show_ui is only ever exposed to clients that support UI, i.e. precisely the clients that always render the app. Remove it entirely: - Drop the show_ui schema property, the form-param allowlist entry, and the showUI term from the defer predicate in create_pull_request and issue_write. The gate is now FF && clientSupportsUI && !_ui_submitted && !hasNonFormParams. - Delete the now-unused UI-only schema-property strip machinery in pkg/inventory (uiOnlySchemaProperties, stripUIOnlySchemaProperties, stripSchemaProperties) and the exported ConditionalSchemaPropertyDescriptions, which existed solely to surface show_ui to UI-capable clients. _meta.ui stripping is untouched. - Drop the conditional-property annotation from the docs generator. - Update toolsnaps, generated docs, and tests. With the up-front-execution Views now rendering the result (success card), the remaining contract is simple: when MCP Apps are enabled the form is the path, and the form is only shown while the action is genuinely deferred. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * refactor(mcp-apps): centralize the show/defer decision (single source of truth) The defer-to-form predicate was triplicated across create_pull_request, update_pull_request, and issue_write, each with its own near-identical *HasNonFormParams function. As more MCP App tools are added this duplication would grow and the copies could silently drift. Extract one shared gate in ui_capability.go: - shouldDeferToForm(ctx, deps, req, args, formParams) — the single show/defer decision (MCP Apps enabled, client supports UI, not a form submission, and no non-form params). - hasNonFormParams(args, formParams) — one generic helper replacing the three per-tool functions. - uiSubmitted(args) — small shared predicate. Each handler is now a one-line `if shouldDeferToForm(...) { return awaiting }`. The per-tool form-parameter allowlists and the user-facing messages stay per-tool (that is the genuine per-tool config). Pure refactor — behavior unchanged; existing tests now exercise the generic helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
77 lines
3.0 KiB
Go
77 lines
3.0 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
|
|
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"
|
|
|
|
// MCPAppMIMEType is the MIME type for MCP App UI resources.
|
|
const MCPAppMIMEType = "text/html;profile=mcp-app"
|
|
|
|
// clientSupportsUI reports whether the MCP client that sent this request
|
|
// 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)
|
|
if supported, ok := ghcontext.HasUISupport(ctx); ok {
|
|
return supported
|
|
}
|
|
// 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 false
|
|
}
|
|
|
|
// uiSubmitted reports whether the call is itself an MCP App form submission.
|
|
// The form re-invokes its tool with _ui_submitted=true; such calls must execute
|
|
// rather than re-render the form.
|
|
func uiSubmitted(args map[string]any) bool {
|
|
submitted, _ := OptionalParam[bool](args, "_ui_submitted")
|
|
return submitted
|
|
}
|
|
|
|
// hasNonFormParams reports whether the call carries any parameter the tool's MCP
|
|
// App form cannot represent (anything outside formParams). Such calls must
|
|
// bypass the form and execute directly so the supplied values aren't silently
|
|
// dropped. formParams is the set of parameters the form collects and re-sends
|
|
// on submit.
|
|
func hasNonFormParams(args map[string]any, formParams map[string]struct{}) bool {
|
|
for key, value := range args {
|
|
if value == nil {
|
|
continue
|
|
}
|
|
if _, ok := formParams[key]; !ok {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// shouldDeferToForm is the single source of truth for the show/defer decision
|
|
// shared by the form-backed write tools (create_pull_request,
|
|
// update_pull_request, issue_write). It reports whether a call should be handed
|
|
// off to its MCP App form instead of executing now: defer only when MCP Apps
|
|
// are enabled, the client can render UI, the call is not itself a form
|
|
// submission, and every supplied parameter can be represented by the form
|
|
// (formParams is the tool's form-parameter allowlist). When it returns false
|
|
// the handler executes directly; the host may still render the tool's view,
|
|
// which renders the result rather than an input form.
|
|
func shouldDeferToForm(ctx context.Context, deps ToolDependencies, req *mcp.CallToolRequest, args map[string]any, formParams map[string]struct{}) bool {
|
|
return deps.IsFeatureEnabled(ctx, MCPAppsFeatureFlag) &&
|
|
clientSupportsUI(ctx, req) &&
|
|
!uiSubmitted(args) &&
|
|
!hasNonFormParams(args, formParams)
|
|
}
|