← MCP / Response cards v3.6

The closing rule was eating the card.

On 2026-08-26 the card spec gained a zone model — identity, content, action. It never reached the screen, and the reason turned out not to be styling at all. A card’s closing --- is not a horizontal rule: with no blank line before it, CommonMark reads it as a setext heading underline, and the entire card body — title, anchor, the sender’s prose, the rename — is promoted into one <h2> element. The zones are indistinguishable because they are literally the same element.

That explains every symptom this area has chased for weeks: why the provenance line is the widest thing on the card, why italic “came back heavier than the title” (italic inside an <h2> is large bold-italic), and why the closing rule is invisible — it was consumed as the underline. It also means blank lines alone do not fix it: they split the paragraph, so the heading stops swallowing the whole card and starts swallowing the last block before the rule — the rename. Below: the parser evidence, the one-line fix, the register grammar that becomes possible once the heading is gone, and the 48-site work-list.

Sample data is synthetic. Titles, Codes and names are stand-ins at the real strings’ shape and length — this repo publishes, and every branch gets a preview URL.

zone grammar v3.6 upstream: DESIGN-NOTE.relay-card-display §Zones · SPEC.relay-display-grammar proposal · 2026-08-26
In flight
A fix for half of this is already in flight, and it needs one more line. A parallel relay-platform session shipped blank-line zone separation on claude/claim-2ccc92k9f8-c363h4 — _arrival_bits and _claim_manic_block restored to \n\n, italic banned, a pinning test, two new paragraphs in the card design note. All of that is right and this page treats it as landed. But blank lines alone relocate the setext heading rather than removing it: with the body split into paragraphs, the closing --- promotes the last one, so the rename line becomes an <h2>. The missing line is a blank line before the closing rule — verified against CommonMark in §2.
Z1
Identity renders as data, never as prose

Title and anchor line only: the object’s name, its ⇄ address, and ·-joined facets. Nothing a human wrote in sentences belongs in this zone. A sentence sitting inside the identity block is what makes a card read as a wall.

Z2
A closing --- needs a blank line above it, or it is a heading underline root cause

Not a rule — an underline. text\n--- is setext H2 in CommonMark, and a setext heading spans every line of the paragraph above it. With no blank lines the whole card is one paragraph, so the whole card becomes the heading. This is the defect; everything else on this page is downstream of it.

Z3
Zones are separated by blank lines — necessary, not sufficient shipped

A single \n is not separation: some hosts collapse it and the card renders as a run-on. The rule predates the defect — _claim_manic_block’s docstring has stated it since #1163. It is load-bearing and it is shipped. It is also not enough on its own: without Z2 it just moves the heading onto the rename.

Z4
Narrative is quoted, not stated open

Separation says these are three groups. It does not say this group is quieter than that one — and the sender’s provenance sentence is still the widest thing on the card. Prose the sender wrote (origin, forward note, description) should render as a blockquote: someone else’s voice inside your card, indented rather than weighted.

Z5
Italic is not de-emphasis in this renderer — it is a display treatment

Twice confirmed, expensively. A line that is italic on its own comes back from the claude.ai renderer as large serif bold-italic. It made Origin the heaviest thing on the card, and then made the carried-over lead the same. Any future “make this recede” instinct that reaches for *…* is reaching for the trap. Structure is the only lever left, which is what makes Z4 a quote and not an emphasis.

Z6
One facet, one glyph — collisions are checked, not hoped open

Three helpers independently chose 👤 for org kind, sender kind and visibility. On a personal-org, human-sent, private relay — the most common case there is — all three collapse, and the icons carry zero information exactly when the card renders most often.

Z7
Plain keeps the zone and loses the styling

Quote becomes an indent, sigils become their plain equivalents, the blank line stays a blank line. Plain is an unstyled transcript, not a second design — and it is the branch that gets skipped, which is the most common defect in this area.

The report

relay_receive · owner view · live on main

A receive card as production renders it today, reproduced verbatim. Four registers, one weight, no gaps. The reader’s eye has nowhere to land after the title, so the sender’s provenance sentence — the least important line on the card — occupies the most visual space on it.

Today · live
Relay Receive›
--- 1👤 MCP response cards — envelope, manicure, and composition order 2 FBQJ4HYNP9 · Sent by you · Aug 26 3Packaged out of a relay-platform session that had just shipped the apex-router URL sweep. That work lived in the same card-grammar layer, so this resource map is first-hand. 4✎ Name this chat → “MCP response cards — envelope, manicure, and composition order — FBQJ4HYNP9” · open ↗ --- content loaded — card above 📌 Start here to work on Relay’s MCP card display — the confusions are named with traced evidence.4 registers · 1 weight · 0 blank lines
1
Object identity

The title. Correct, and the only line whose weight is earned.

2
Address and facets

Data: the Code, who sent it, when. Also correct — and it should be structurally unlike everything below it, which is the point.

3
The sender’s narrative — unmarked

Prose, in a data block, with no mark and no gap. Italic was tried and removed because it “came back heavier than the title it sat under”; nothing replaced it. It is now the widest thing on the card.

4
An ask to the reader — flush against the data

The one line the user can act on, rendered adjacent to and at the same weight as the facts about the object. Nothing separates a thing you read from a thing you do.

● The zone model is not wrong — it was unrendered. DESIGN-NOTE.relay-card-display.md §Zones already assigns each of these four lines a zone, and every one of those assignments is right. None of them reached the screen.

The closing rule is an underline

verified against CommonMark

In CommonMark, text followed by --- is not a paragraph and a horizontal rule. It is a setext heading — and a setext heading takes every line of the paragraph above it, not just the last. A card whose lines carry no blank line between them is one paragraph, so the closing rule promotes all of it.

The card the server sends: What the parser builds: --- <hr /> 👤 **Positioning — the frame stack** <h2> ⇄ `JDH6YD5WDV` · Sent by you · Aug 25 👤 Positioning — the frame stack Packaged out of a relay-platform… ⇄ JDH6YD5WDV · Sent by you · Aug 25 ✎ **Name this chat "…"** · open ↗ Packaged out of a relay-platform… --- ← consumed as the underline ✎ Name this chat "…" · open ↗ *content loaded — card above* </h2> <p><em>content loaded — card above</em></p>

Every symptom this area has chased for weeks falls out of that one <h2>:

SymptomExplanation
Zones look identicalThey are the same element. No amount of blank-line grouping changes the fact that a heading renders as a heading throughout.
Provenance is the widest thingIt is the longest line in a heading, so it wraps at heading size across three lines.
Italic “came back heavier”Italic inside an <h2> is large bold-italic. Removing the italic was right; the diagnosis under it — “italic is a display treatment in this renderer” — was one level too shallow. It is a display treatment because it is inside a heading.
No closing rule appearsThere is no closing rule. It was eaten. The card only ever emits one <hr>, the opening one.

Four candidate strings, run through a CommonMark parser. h2 is how many heading elements the card produces; it should be zero.

Varianth2hrWhat the heading swallowsVerdict
A — live on main11The entire card — title, anchor, provenance, renamethe defect
B — blank lines only11The rename line — the last block before the rulerelocated
C — blank line before ---02Nothing. Each zone is its own <p>.fixed
D — close with ***02Nothing — *** can only ever be a rule.fixed
● C is the minimal change — one \n before the closing rule, in relay_card() and in every hand-rolled card that closes its own frame. D is the safer one: *** has no setext meaning at all, so no future edit can re-open this by removing a blank line. Either way the assertion worth pinning in a test is not the string — it is “this card renders zero heading elements”, which stays true however the card is later rearranged.
● Why this hid for so long. Both June 2026 card bugs, the density pass, the zone move and the italic removal were all fixes at the copy layer for a defect at the parse layer. Each one moved real weight around and none could remove it, because the heading was never in the diff. A page-level QA pass reads a card and asks whether it looks right; nobody was asking the parser what it had built.

Layer 2 — separation

shipped · blank lines restored

Three groups, three gaps. Nothing was added and nothing was styled — two single newlines became two blank ones, and the run-on becomes three readable units. The whole fix, and it was a regression being undone rather than a feature being built.

Rich · shipped
Relay Receive›
--- 👤 MCP response cards — envelope, manicure, and composition order FBQJ4HYNP9 · Sent by you · Aug 26 Packaged out of a relay-platform session that had just shipped the apex-router URL sweep. That work lived in the same card-grammar layer, so this resource map is first-hand. ✎ Name this chat → “MCP response cards — … — FBQJ4HYNP9” · open ↗ --- content loaded — card above 📌 Start here to work on Relay’s MCP card display — the confusions are named with traced evidence.grouped — but only once §2 lands; on its own the rename becomes the heading
Plain · RELAY_PLAIN=1
Relay Receive›
⇩ MCP response cards — envelope, manicure, and composition order FBQJ4HYNP9 · Sent by you · Aug 26 Packaged out of a relay-platform session that had just shipped the apex-router URL sweep. That work lived in the same card-grammar layer, so this resource map is first-hand. Name this chat “MCP response cards — … — FBQJ4HYNP9” relayctx.com/FBQJ4HYNP9 content loaded — card above Start here to work on Relay’s MCP card display — the confusions are named with traced evidence.the blank line is the whole mechanism — it survives plain for free
● Necessary, and not sufficient. #1163 established the hard-break rule and _claim_manic_block’s docstring has stated it ever since; the zone move dropped it and this restores it. But on its own it does not remove the heading — it splits the paragraph so the closing rule promotes the last block instead of all of them, which is variant B in §2: the rename becomes the <h2>. Ship it together with the blank line before the closing rule, or the mock above is not what renders.

Layer 3 — register

open · the proposal

Grouping is not differentiation. Three groups at one weight read as three paragraphs of the same kind of thing, and the eye still lands on the longest one. The remaining question is narrow and specific: what does “quieter” look like when italic is a trap and bold is spoken for?

Rich · proposed
Relay Receive›
--- 👤 MCP response cards — envelope, manicure, and composition order FBQJ4HYNP9 · Sent by you · Aug 26
Packaged out of a relay-platform session that had just shipped the apex-router URL sweep. That work lived in the same card-grammar layer, so this resource map is first-hand.
✎ Name this chat → “MCP response cards — … — FBQJ4HYNP9” · open ↗ --- content loaded — card above 📌 Start here to work on Relay’s MCP card display — the confusions are named with traced evidence.the eye lands on the title, then the Code, then the ask
Plain · proposed
Relay Receive›
⇩ MCP response cards — envelope, manicure, and composition order FBQJ4HYNP9 · Sent by you · Aug 26 Packaged out of a relay-platform session that had just shipped the apex-router URL sweep. That work lived in the same card-grammar layer, so this resource map is first-hand. > Name this chat “MCP response cards — … — FBQJ4HYNP9” relayctx.com/FBQJ4HYNP9 content loaded — card above * Start here to work on Relay’s MCP card display — the confusions are named with traced evidence.quote → indent · sigil → ascii · the blank lines are untouched

The zone table the renderer would implement. Separation is layer 1 and is shipped; mark and treatment are the open part.

ZoneHoldsSeparationMarkRichPlain
Identitytitle, ⇄ anchor, facetsopens the frameobject sigil + ⇄bold title, mono Code, ·-joinedsigil, bare Code, ·-joined
Narrativeorigin, forward note, description\n\n> quoteblockquote, no emphasis4-space indent
Reader askrename, gates, advisories\n\n✎ 🔔bold verb, quoted value> + verb
Signal“content loaded”closing rule above it—italic, after ---plain line
Sender askthe context_ask\n\n📌own line, no emphasis* + text
● Why a quote and not another glyph. A sigil marks a line; narrative is a paragraph and wraps. A leading glyph only marks the first wrapped line, so a three-line origin returns to bare text on lines two and three — the defect, two-thirds intact. The quote rule is the only structure that spans the wrap. It is also already in this grammar for stream item bodies (object envelope v2, ACTIVE section), so it extends a rule rather than inventing one.
● And the reason to be careful. Blockquote is a bet on host rendering, and this exact area has lost that bet twice with italic. If a host renders > with a tint or a heavier face, it reproduces the defect it was meant to fix. The fallback is a four-space indent — no host emphasises it — and the decision belongs to the host-matrix pass, not to this page.

The mint envelope

relay_send · created · the three-glyph collision

A separate defect in the same area, untouched by either layer above. The send card does not use the shared renderer at all — it is hand-built in both branches. Its anchor line runs six facets and uses ✦ where the canonical anchor is ⇄. Three of those facets independently resolved to 👤.

Today
Relay Send›
--- 📤 Identity × Sharing contract — shared-object UI for multi-profile people 7HZ3Q9Y3CG · 👤 @northgate · 👤 erik · Aug 26 · 👤 Private · unlimited claims 🔔 Anyone you’re not already connected to will need your approval each time before they can receive it. ↗ relayctx.com/7HZ3Q9Y3CG — sent. Share this Code with the recipient. ✎ Adjust anytime — rename, retag, or change who can receive it; or expire it if this was the wrong direction. ---3 identical glyphs, 3 meanings · wrong anchor · 1 facet with no glyph · no gaps · no signal
Rich · v3.6
Relay Send›
--- 📤 Identity × Sharing contract — shared-object UI for multi-profile people 7HZ3Q9Y3CG · Aug 26 · 🔒 Private · ∞ receives 👤 erik · in 🏢 @northgate 🔔 Anyone you’re not already connected to needs your approval before they can receive it. ✎ Adjust anytime — rename, retag, or change who can receive it. --- sent · relayctx.com/7HZ3Q9Y3CG — share this Code with the recipientone glyph per facet · canonical anchor · sender split from org · signal line

Five changes, each traceable to a named helper.

FacetHelperWasIsWhy
Anchortransfer.py inline✦⇄The canonical anchor, described in _display.py as “the universal Code-line anchor … so the Code is never buried”. The send card is the surface that most needs it and the one that opted out.
Visibility_vis_icon()👤🔒Private is a lock, not a person. Frees 👤 for the only facet that means a human. Org stays 👥, broadcast stays 📢.
Org kindorg_display_icon()👤🏢Moves off the anchor line onto the sender line and takes the building glyph in both cases — a personal org is still a container, and the word in is what says so.
Sender_sender_emoji()👤👤Keeps 👤 — the one facet the glyph was always right for. 🤖 for an agent sender is unchanged.
Receives_claims_label()—∞The only fragment on the line with no glyph, so it read as a continuation of the facet before it. ∞ receives / 1 receive / 3 receives.
● This collision survives a naive migration. relay_card() takes meta as pre-formatted strings, so moving the send card onto the shared renderer would render the same three glyphs. The fix has to land in the facet helpers, and the renderer has to start taking typed facets — which is the change that lets a uniqueness test exist at all.

What a response is made of

the missing grammar

There is a grammar for what a card looks like and none for what a response is made of. Six independent pieces compete for position, and the order they land in today is a set of individually-argued local decisions — each with a good reason, none written down together. Naming the order is the prerequisite for rearranging any of it.

1
carddisplay · in frame

Identity → narrative → reader ask. The thing agents are instructed to render verbatim, and therefore the best-protected slot in the response. Anything that must survive a summarising agent belongs inside it.

2
renamedisplay · in frame

Moved inside the frame on 2026-08-26, beside the title it renames. It held the closing line as a robustness hack, not because it earned it.

3
signaldisplay · closes

“content loaded — card above”. The cue that the card ended and prose may begin. A card with no signal is one the model reads as a preamble and talks over — the documented cause of both June 2026 card bugs.

4
nudgetail · placed

Passed an explicit placement on the receive path, because a generic “surface after the result” fought the receive’s own tail for last place.

5
sender asktail

One field, two placements: above the excerpt on a preview (decision support — is this worth receiving?), below the signal on a receive (a directive, next to the thing responding to it).

6
agent offertail · last

The action CTA is inserted before the rename CTA on all five content-loading receive branches, so the instruction reads offer-then-rename. Reordering buries the regression that ordering exists to prevent.

● One open decision this page does not settle. The rename exists twice by design — inside the card, and again in next_step as a robustness hack for agents that summarise instead of rendering. An agent that does both shows the user the same ask twice. That is a structural problem wearing a wording problem’s clothes, and it needs a real either/or mechanism before any copy moves.

The work-list

48 sites · 5 on the shared renderer

Every "display" site in src/relay/mcp/, tiered by exposure. Tier 1 carries the narrative zone and is where both the report and the shipped fix landed. 43 of 48 hand-build their card string, so most of tier 4 is a migration, not an edit — which is the right order: fix the grammar once in _display.py and the rest inherit it.

TierCardsSitesLayer 1Layer 2
T1receive — owner view, re-receive, preview, the received card, self view, preview-new; series receive7shippedZ4 quote on the narrative zone
T2the rename block, on receive and series receive5shippedThe card-vs-next_step either/or decision
T3send — created, staged, forwarded3Not yet — hand-built, no gapsZ6 glyph table, canonical anchor, sender/org split, signal line
T4session (10) · series (6) · stream (4) · save (3) · request (3) · profile (3) · nudges (2) · manicure (2) · feedback (1) · approve/deny (2) · other transfer (8)33Not yetMigrate onto relay_card() and inherit Z1–Z7. Both branches, one commit.
● The conformance audit carrying the per-site scores is stale by 15 sites — generated 2026-08-12, before the zone move — and it is the artifact this migration is supposed to burn down. Regenerating it with zone columns is step one, not step last.

What has to be verified before layer 2 ships

host matrix · manual by necessity

The server cannot read a client’s negotiated capabilities at tool-call time, and real hosts diverge from spec — which is why the display QA runbook is manual. Every rule on this page is a bet on how a host renders a markdown structure, and this area has already lost that bet twice with italic.

BetRiskFallback if it fails
Blockquote reads quieterA host that renders > with a tinted ground or a heavier face reproduces the original defect — heavier, not quieter. Precisely how italic failed, twice.Four-space indent: every host treats it as preformatted and none emphasises it. Loses the wrap-spanning rule, keeps the register.
Blank lines surviveAlready the shipped mechanism, so this one is load-bearing for layer 1 too. A host that collapses \n\n erases the grouping entirely.None — this is why the pinning test exists. A host that collapses hard breaks needs the thin-rule treatment instead.
New glyphs render🔒 🏢 ∞ are broadly safe; a terminal without emoji falls back to boxes.Plain mode already has an ASCII table — extend it rather than restrict the rich set.
● Results get appended to the QA runbook, never rewritten — the record of what a host did last time is the only evidence base this area has.
Grammar: relay-platform SPEC.relay-display-grammar.md · DESIGN-NOTE.relay-card-display.md §Zones Work-list: relay-platform REFERENCE.card-conformance-audit.md Siblings: Object envelope v2 · Relay card display · Code presence