Skip to content

feat(mcp): preview-card confirm rail + fix ForEach history templating#83

Merged
dougborg merged 1 commit into
mainfrom
feat/mcp-prefab-confirm-rail
May 20, 2026
Merged

feat(mcp): preview-card confirm rail + fix ForEach history templating#83
dougborg merged 1 commit into
mainfrom
feat/mcp-prefab-confirm-rail

Conversation

@dougborg
Copy link
Copy Markdown
Owner

Summary

Fixes two issues surfaced by Claude Desktop rendering of MCP-Apps preview cards.

Confirm-button feedback + double-click safety. The four preview cards (status change, comment, due date, bulk status) now render a footer state machine: Preview → Pending… → Applied / Retry / Cancelled. SetState("pending", True) flips synchronously before the apply CallTool fires, so a rapid second click can't trigger a duplicate mutation. disabled=Rx("pending") | Rx("cancelled") on the button is the belt-and-suspenders binding. on_success pushes the structured result into the agent's model context via UpdateContext; on_error surfaces the failure inline via a shadcn Alert (matching katana's pattern) and swaps the primary button to a Retry affordance.

ForEach history templating. build_order_detail_ui was rendering raw {{ created_at }} text literals because inside ForEach the binding scope is $item, not parent state. Capture the loop via as entry: so each cell binds to the row.

Ports the direct-apply confirmation rail from sibling katana-openapi-client.

Test plan

  • uv run poe check — full validation (test + lint + typecheck + format) passes
  • 14/14 tests in test_prefab_ui.py pass, including two new ones:
    • test_preview_cards_wire_double_click_guard — pins the SetState guard and seeded state across all four preview cards
    • test_status_change_preview_invalid_does_not_seed_apply_state — invalid-transition path doesn't seed pending/applied
  • Manual: install MCPB bundle in Claude Desktop, trigger an update_order_status preview, double-click Confirm rapidly — only one CallTool fires
  • Manual: get_order on an order with history — history rows render with actual timestamps/events, not raw {{ field }} literals

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 20, 2026 19:39
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the StatusPro MCP server’s Prefab UI builders to improve mutation-preview UX in MCP Apps clients (notably Claude Desktop) by adding a confirm/apply “rail” with pending/applied/error/cancelled states, and fixes templating in order-history rendering inside ForEach.

Changes:

  • Add shared preview-card state initialization plus reusable confirm/cancel action builders and a footer state machine (Preview → Pending → Applied/Retry/Cancelled).
  • Render inline apply errors via a destructive Alert and push apply results into agent context via UpdateContext.
  • Fix build_order_detail_ui history row bindings by capturing ForEach(... ) as entry and binding cells to the loop item.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
statuspro_mcp_server/src/statuspro_mcp/tools/prefab_ui.py Adds confirm/footer state machine, inline error rendering, context updates, and fixes ForEach history bindings.
statuspro_mcp_server/tests/tools/test_prefab_ui.py Updates toolCall assertions for the new UI structure and adds tests for the new preview-card guard/state seeding behavior.
Comments suppressed due to low confidence (2)

statuspro_mcp_server/src/statuspro_mcp/tools/prefab_ui.py:268

  • Cancel can’t reach the intended terminal cancelled state after an apply error: the cancel action only sets cancelled=True, but the footer checks error before cancelled, so the UI will keep rendering the Retry state as long as error is set. Either clear error in the cancel action or make the cancelled branch take precedence over the error branch so cancelling reliably locks out further actions.
        with Elif("applied"):
            Button(
                label=applied_label,
                variant="success",
                icon="check",
                disabled=True,
            )
        with Elif("error"):
            Button(
                label="Retry",
                variant="warning",
                icon="rotate-cw",
                on_click=confirm_action,
            )
        with Elif("cancelled"):
            Button(label="Cancelled", variant="outline", disabled=True)
        with Else():
            Button(
                label=confirm_label,
                variant="default",
                on_click=confirm_action,
                disabled=Rx("pending") | Rx("cancelled"),
            )

        Button(
            label="Cancel",
            variant="outline",
            on_click=cancel_action,
            disabled=Rx("pending") | Rx("applied") | Rx("cancelled"),
        )

statuspro_mcp_server/src/statuspro_mcp/tools/prefab_ui.py:252

  • The Retry button (error state) is not disabled based on pending/cancelled, unlike the initial Confirm button. This undermines the double-click safety goal in the error/retry path because the button can still be clicked rapidly while a retry call is in-flight. Bind disabled for Retry as well (and/or rely on an immediate SetState(pending, True) plus UI disable) to prevent duplicate retries.
        with Elif("error"):
            Button(
                label="Retry",
                variant="warning",
                icon="rotate-cw",
                on_click=confirm_action,
            )

Comment thread statuspro_mcp_server/src/statuspro_mcp/tools/prefab_ui.py
Comment thread statuspro_mcp_server/tests/tools/test_prefab_ui.py Outdated
@dougborg dougborg force-pushed the feat/mcp-prefab-confirm-rail branch 2 times, most recently from 515649f to 5b75d4b Compare May 20, 2026 20:12
Two related issues surfaced by Claude Desktop rendering:

**Confirm-button click feedback + double-click safety.** The four preview
cards (status change, comment, due date, bulk status) now render a footer
state machine that morphs through Preview → Pending… → Applied / Retry /
Cancelled. `SetState("pending", True)` fires synchronously before the
apply CallTool so a rapid second click can't trigger a duplicate
mutation, with `disabled=Rx("pending") | Rx("cancelled")` as the
belt-and-suspenders binding. `on_success` pushes the structured result
into the agent's model context via `UpdateContext`; `on_error` surfaces
the failure inline via a shadcn `Alert` (matching katana's pattern) and
swaps the primary button to Retry.

**ForEach history templating.** `build_order_detail_ui` was emitting raw
`{{ created_at }}` literals — the renderer can't resolve them because
inside `ForEach` the binding scope is `$item`, not parent state. Capture
the loop via `as entry:` and bind to `entry.created_at` etc.

Ports the confirmation pattern from katana-openapi-client's prefab_ui.

Co-Authored-By: Claude <noreply@anthropic.com>
@dougborg dougborg force-pushed the feat/mcp-prefab-confirm-rail branch from 5b75d4b to b5f6a8a Compare May 20, 2026 20:15
@dougborg
Copy link
Copy Markdown
Owner Author

Addressing the two findings from the suppressed-low-confidence section of @copilot-pull-request-reviewer's review:

1. Cancel after error doesn't lock out further actions — fixed by reordering the footer If/Elif chain so cancelled is checked before error. Previously error won (still truthy after an apply failure), so a Cancel click set cancelled=True but the Retry button kept rendering. The new order is pending → cancelled → applied → error → else. Pinned with test_confirm_footer_state_precedence.

2. Retry has no disabled belt-and-suspenders — fixed by mirroring the initial Confirm button's binding (disabled=Rx("pending") | Rx("cancelled")). The SetState("pending", True) in confirm_action flips state before re-render, but the explicit binding closes any iframe propagation gap during a rapid double-click on Retry. Pinned with test_retry_button_has_double_click_guard.

Both in b5f6a8a.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@dougborg dougborg merged commit f53a14b into main May 20, 2026
14 checks passed
dougborg added a commit that referenced this pull request May 20, 2026
…sage on Cancel

Closes #54, closes #53.

**#54 — Typed Rx refs for CallTool template args.** The four Confirm-button
``CallTool`` actions used bare-string templates like
``"{{ preview.order_id }}"`` to bind iframe state into tool arguments.
A typo (``"{{ preivew.order_id }}"``) would silently expand to empty and
the host would call the tool with a missing arg, with no signal on our
side. Added a ``_preview_ref(field) -> Rx`` helper next to the existing
action builders; the four builders now go through it:

    "order_id": _preview_ref("order_id"),
    "status_code": _preview_ref("new_status_code"),
    ...

Wire shape is identical (``Rx`` serializes to the same template string),
so all existing tests continue to pass. The win is a single namespaced
choke-point and refactor-safe references — the ``preview.`` prefix can
no longer be typo'd, and field names are greppable.

**#53 — Pin no-SendMessage on Cancel.** PR #83 already replaced the
``SendMessage("Cancel the X")`` chain with
``[SetState("cancelled", True), ShowToast(...)]``, but no test enforced
that contract. Added ``test_cancel_buttons_do_not_send_chat_messages``
which walks every preview card's envelope, finds each Cancel button,
and asserts no ``sendMessage`` action in the on_click chain. Pinning so
a future regression that re-introduces SendMessage on Cancel surfaces
here instead of as chat-noise in production.

Co-Authored-By: Claude <noreply@anthropic.com>
dougborg added a commit that referenced this pull request May 20, 2026
…sage on Cancel

Closes #54, closes #53.

**#54 — Typed Rx refs for CallTool template args.** The four Confirm-button
``CallTool`` actions used bare-string templates like
``"{{ preview.order_id }}"`` to bind iframe state into tool arguments.
A typo (``"{{ preivew.order_id }}"``) would silently expand to empty and
the host would call the tool with a missing arg, with no signal on our
side. Added a ``_preview_ref(field) -> Rx`` helper next to the existing
action builders; the four builders now go through it:

    "order_id": _preview_ref("order_id"),
    "status_code": _preview_ref("new_status_code"),
    ...

Wire shape is identical (``Rx`` serializes to the same template string),
so all existing tests continue to pass. The win is a single namespaced
choke-point and refactor-safe references — the ``preview.`` prefix can
no longer be typo'd, and field names are greppable.

**#53 — Pin no-SendMessage on Cancel.** PR #83 already replaced the
``SendMessage("Cancel the X")`` chain with
``[SetState("cancelled", True), ShowToast(...)]``, but no test enforced
that contract. Added ``test_cancel_buttons_do_not_send_chat_messages``
which walks every preview card's envelope, finds each Cancel button,
and asserts no ``sendMessage`` action in the on_click chain. Pinning so
a future regression that re-introduces SendMessage on Cancel surfaces
here instead of as chat-noise in production.

Co-Authored-By: Claude <noreply@anthropic.com>
dougborg added a commit that referenced this pull request May 20, 2026
…sage on Cancel (#85)

Closes #54, closes #53.

**#54 — Typed Rx refs for CallTool template args.** The four Confirm-button
``CallTool`` actions used bare-string templates like
``"{{ preview.order_id }}"`` to bind iframe state into tool arguments.
A typo (``"{{ preivew.order_id }}"``) would silently expand to empty and
the host would call the tool with a missing arg, with no signal on our
side. Added a ``_preview_ref(field) -> Rx`` helper next to the existing
action builders; the four builders now go through it:

    "order_id": _preview_ref("order_id"),
    "status_code": _preview_ref("new_status_code"),
    ...

Wire shape is identical (``Rx`` serializes to the same template string),
so all existing tests continue to pass. The win is a single namespaced
choke-point and refactor-safe references — the ``preview.`` prefix can
no longer be typo'd, and field names are greppable.

**#53 — Pin no-SendMessage on Cancel.** PR #83 already replaced the
``SendMessage("Cancel the X")`` chain with
``[SetState("cancelled", True), ShowToast(...)]``, but no test enforced
that contract. Added ``test_cancel_buttons_do_not_send_chat_messages``
which walks every preview card's envelope, finds each Cancel button,
and asserts no ``sendMessage`` action in the on_click chain. Pinning so
a future regression that re-introduces SendMessage on Cancel surfaces
here instead of as chat-noise in production.

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants