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:
| 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-writenext_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=1test to check plain branch - [ ] Update
DESIGN-NOTE.relay-card-display.mdin relay-platform - [ ] Add visual example to
relay-creative/prototype /mcp/response-cards.html