Skip to content

Commit f01ce5f

Browse files
dougborgclaude
andauthored
feat(mcp): render Prefab UI for the find/view/decide/mutate cluster (#20)
* feat(mcp): add Prefab UI foundation (utils, templates, schemas) Lay the scaffolding for MCP-Apps rendering without changing any tool behavior yet. Ported from katana-openapi-client's mature Prefab UI pipeline — specifically the contract established in katana commit 5b373fca (fix(mcp): Prefab UI was built but never rendered in Claude Desktop). Added: - tools/tool_result_utils.py — make_tool_result(), UI_META, format_md_table, iso_or_none, enum_to_str. Pass raw PrefabApp as structured_content; FastMCP's ToolResult.__init__ converts via _prefab_to_json on isinstance check. Do NOT call .to_json() manually (that's the bug katana #350 fixed). - templates/{orders_list,order_detail,viable_statuses,status_change_preview}.md — markdown fallback for the 4 tools that will get UI in a follow-up commit. - tests/tools/test_tool_result_utils.py — pins the contract: Pydantic dump when ui=None, Prefab envelope (with "view" key) when ui is a PrefabApp, and UI_META == {"ui": True}. Prevents silent regressions the next time FastMCP or prefab-ui change shape. Updated: - tools/schemas.py — added typed response models (OrderSummary, OrderDetail, OrderList, HistoryEntry, StatusEntry, ViableStatusesResponse, StatusChangePreview, StatusChangeResult) so make_tool_result's `response` argument has a well-typed home for every UI tool in the follow-up. OrderSummary/StatusEntry are duplicated with orders.py/statuses.py until that commit swaps the imports over. No behavior change to registered tools — follow-up commit wires them up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(mcp): render Prefab UI for find/view/decide/mutate cluster Four tools now emit a PrefabApp via ``make_tool_result`` and are tagged with ``meta=UI_META`` so FastMCP's ``_maybe_apply_prefab_ui`` registers ``ui://prefab/…`` resources with MIME ``text/html;profile=mcp-app`` — the contract Claude Desktop uses to render MCP-Apps UI. Non-Prefab clients still see the markdown fallback rendered from the templates added in the previous commit. Added tools/prefab_ui.py with four builders: - build_orders_table_ui: sortable DataTable, row click fires CallTool("get_order") for drill-down - build_order_detail_ui: Card + history timeline (ForEach) + footer buttons that fire get_viable_statuses / add_order_comment - build_viable_statuses_ui: color-coded status buttons; click sends a SendMessage follow-up asking Claude to update_order_status - build_status_change_preview_ui: current → new status chips, comment with visibility badge, Confirm button that sends the confirm=true follow-up Plus helpers ``_color_to_variant`` (StatusPro's free-form Status.color → Prefab Badge variant; lossy mapping, falls back to ``outline``), ``_status_chip`` (shared status Badge renderer), ``_is_overdue`` (ISO-8601 due-date parser with UTC fallback). Wired in orders.py and statuses.py: - list_orders, get_order, update_order_status (preview branch), get_viable_statuses — return ToolResult and include meta=UI_META - lookup_order, add_order_comment, update_order_due_date, bulk_update_order_status, list_statuses — unchanged behaviour; deferred for a future pass since their payloads are simple enough that a UI adds little over JSON Cleanup bundled in: - Inline ``from statuspro_public_api_client.api.orders import …`` imports (previously inside tool function bodies) lifted to module top so the import graph is obvious and the cache-read tools don't pay import cost per call - OrderSummary, StatusEntry definitions moved into schemas.py (were duplicated locally); typed StatusChangePreview / StatusChangeResult replace the ad-hoc ``dict[str, Any]`` preview/result shapes for the update_order_status flow Verification — server boots cleanly and a probe via mcp.list_tools() + mcp.list_resources() confirms: - 4/9 tools carry meta expanded to the full AppConfig dict (not the raw {"ui": True}), proving FastMCP's _maybe_apply_prefab_ui ran - 4 ``ui://prefab/tool/<hash>/renderer.html`` resources are registered with mime=``text/html;profile=mcp-app`` Plus test_prefab_ui.py with 6 smoke tests exercising every builder against minimal sample dicts; each asserts ``app.to_json()`` returns a dict containing the ``"view"`` envelope key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(mcp): status-change confirm branch gets its own template Two self-review follow-ups before review: 1. update_order_status preview branch was fetching ``statuses.list()`` twice — once via the shared ``_status_color_catalog`` helper and again to resolve the new status's display name. Combine into one pass that builds the ``{code: color}`` map and finds the new name at the same time. 2. Confirm branches were reusing the ``status_change_preview`` template with placeholder dashes for the now-irrelevant preview fields — the resulting markdown said "Preview: …" after the update had already run, which is misleading. Add a dedicated ``status_change_result.md`` and route both the declined-by-user and executed outcomes through it. The preview path is unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(mcp): simplify pass on the Prefab UI wiring Five fixes from the /simplify review pass: 1. Use ``iso_or_none`` (already in tool_result_utils) consistently in ``_to_summary``, ``_to_detail``, and ``_history_entry`` instead of inline ``str()``/``isoformat()`` patterns. ``UNSET`` is falsy so ``iso_or_none(getattr(order, "due_date", None))`` short-circuits correctly for unset attrs fields. 2. Run the two independent fetches in ``get_order`` concurrently via ``asyncio.gather`` — ``orders.get`` and ``_status_color_catalog`` have no dependency on each other, so this halves the latency on uncached catalog calls. 3. ``list_orders`` now serializes ``OrderSummary`` to dict once and reuses the result for both the Prefab DataTable and the markdown table rows. Saves N ``model_dump`` calls per page. 4. ``StatusChangePreview`` gains a ``recipients_text()`` method so both ``orders.py`` (for the markdown template var) and ``prefab_ui.build_status_change_preview_ui`` (for the UI metric) share one implementation. Drops the duplicated three-line ``customer / additional contacts / nobody`` block from each site. 5. ``test_build_orders_table_ui`` and ``test_build_status_change_preview_ui`` now assert against the serialized envelope for the action wiring that drives the loop — ``"get_order"`` for the row-click drill-down, ``"confirm=true"`` and the new status code for the Confirm button. Pure shape checks were silently allowing those follow-up payloads to regress. Also tightened the ``_status_color_catalog`` docstring — the ``ResponseCachingMiddleware`` caches MCP tool calls, not the underlying ``client.statuses.list()`` call this helper makes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 9aa5db8 commit f01ce5f

13 files changed

Lines changed: 1137 additions & 86 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# {order_name} — {customer_name}
2+
3+
- **Order number:** {order_number}
4+
- **Customer:** {customer_name} ({customer_email})
5+
- **Status:** {status_name} ({status_code})
6+
- **Due date:** {due_date}{due_date_range}
7+
8+
## History
9+
10+
{history_table}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Orders ({total})
2+
3+
{filters_line}
4+
5+
{orders_table}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Preview: change order {order_id} status
2+
3+
**{current_status_name}** ({current_status_code}) → **{new_status_name}** ({new_status_code})
4+
5+
{comment_block}
6+
7+
Emails: {recipients}
8+
9+
Rerun with `confirm=true` to apply the change.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Status change for order {order_id}
2+
3+
- **New status:** {new_status_code}
4+
- **Result:** {outcome}
5+
- **HTTP:** {http_status}{message_line}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Valid status transitions for order {order_id}
2+
3+
{status_list}
4+
5+
Call `update_order_status(order_id={order_id}, status_code="…", confirm=false)` to
6+
preview a change before committing.

0 commit comments

Comments
 (0)