Skip to content

MCP Card Display

How Relay MCP tool responses render in the agent chat interface. Every tool that returns a relay object emits a display field — a short markdown block the agent renders verbatim as a visual card.

Source of truth

Implementation lives in relay.mcp._display (canonical symbols, relay_card(), card_result()). Full spec: docs/DESIGN-NOTE.relay-card-display.md in relay-platform. Visual reference: prototype.relayctx.com/mcp/response-cards.


The card grammar

Rich-mode relay cards use horizontal-rule framing:

---
{sigil} **{name}**
⇄ `{code}`  ·  {status}  ·  {meta…}
---
*{signal line}*
Element Purpose
Opening --- Visually separates the card from prior conversation
Sigil + name Object identity on one line
Code line ⇄ `CODE` anchor — always on its own line, led by
Closing --- Marks the card complete; what follows is content
Signal line Tells the agent whether content is loaded and what to do next

Without the framing and signal line, models skip the card and summarise the content in their own voice (confirmed by two #1163-class incidents).


Entity symbols

Single source of truth in relay.mcp._display:

Symbol Entity Usage
Relay / transfer Code-line prefix — on every card
Pulse context ◎ **[Name](url)** title line
📂 Session 📂 **Name** title line
Series ⧉ **series-name** title line

Signal lines by state

State Signal line
Confirmed receive *content loaded — see below*
Owner / already received *content loaded — card above*
Series detail loaded *series loaded*
Session detail loaded *session loaded*
Transfer detail loaded *transfer loaded*
Pulse No trailing signal — content is inline inside the card

card_result() — the only correct way to set next_step

All card-returning tools must use card_result() from relay.mcp._display. It bakes in _RENDER_CARD_VERBATIM — the strong verbatim instruction — so the next_step field is never accidentally weakened:

from relay.mcp._display import card_result

return json.dumps(card_result(
    display_string,               # the rendered card markdown
    extra="Tool-specific hint.",  # optional sentence appended after the instruction
    ok=True,                      # any additional response fields as kwargs
    data=payload,
), indent=2)

Never hand-write next_step for card tools

"next_step": "RENDER the display field verbatim as markdown." is the weak form — it was the original pattern and models drift from it as conversation depth grows. Use card_result() instead. The weak form is acceptable for list/meta payloads (search results, transfer lists) where there is no framed card to render.


Card examples by tool

relay_transfer_receive — confirmed receive

---
✅ **App Sprint Handoff — May 7, 2026**
⇄ `7JAR42`  ·  👤 Erik  ·  May 7
---
*content loaded — see below*

With series:

---
✅ **Frontend Audit — app.relayctx.com**
⇄ `K2SCWM`  ·  👤 Cowork Director  ·  May 7  ·  series `relay-platform-dev`
---
*content loaded — see below*

Owner / self-view (no confirm step):

---
👤 **App Sprint Handoff — May 7, 2026**
⇄ `7JAR42`  ·  Sent by you  ·  May 7
---
*content loaded — card above*

relay_transfer_get — transfer detail

---
⇄ **App Sprint Handoff — May 7, 2026**
⇄ `7JAR42`  ·  owner  ·  pending  ·  Private
---
*transfer loaded*

relay_series_get — series detail

---
⧉ **relay-platform-dev**
5 relay(s) — 2 pending · 3 received

✅ `7JAR42` App Sprint Handoff — May 7, 2026
✅ `FWJE46` Console + App Fix Sprint — May 2026
✅ `K2SCWM` Frontend Audit — app.relayctx.com
⏳ `B9XK21` Product Brief — Q3 2026
⏳ `M4RJ07` Onboarding Handoff
---
*series loaded*

relay_session_get — session detail

Open:

---
📂 **relay-platform / MCP card-envelope schema**  ·  2026-06-14  ·  🟢 open
⇄ `JSKEAW`  ·  Series: relay-platform-dev
---
*session loaded*

Closed:

---
📂 **App Sprint Handoff — May 7, 2026**  ·  2026-05-07  ·  ✓ closed
Packaged as `7JAR42`
---
*session loaded*

relay_pulse_active / relay_pulse_get — pulse

Pulse cards use _build_pulse_card(), which produces --- / header / --- / content inline (no trailing signal):

---
◎ **[Relay Product Development](https://app.relayctx.com/p/pc_78b145fbb371)**
**62%**  ▓▓▓▓▓▓░░░░  5 live  2 stale 🕐
---

━━━━━ ACTIVE (2) ━━━━━

> **Card-envelope schema — PR #1286**
> `pi_a1b2c3`
> Migrate 4 card sites to card_result() helper

> **Nudge v2 reach experiment**
> `pi_d4e5f6`
>

Adding a new card tool — checklist

  • [ ] Build display string with relay_card() from _display.py, or hand-roll with correct --- framing
  • [ ] Rich mode: --- / sigil + name / code line / --- / *{signal}*
  • [ ] Plain mode: [LABEL] Name, code, text signal ("Context loaded below.")
  • [ ] Assemble response with card_result(display, extra='…', **fields) — never hand-write next_step
  • [ ] Signal line matches the state table above; if new, add it to this doc
  • [ ] _plain_mode() branch is present
  • [ ] RELAY_SYM () used for code line, not a plain character
  • [ ] _date_label() used, not raw ISO date
  • [ ] Verify: RELAY_PLAIN=1 test to check plain branch
  • [ ] Update DESIGN-NOTE.relay-card-display.md in relay-platform
  • [ ] Add visual example to relay-creative/prototype /mcp/response-cards.html