Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
81e2a5f
Give hey mcp its own sign-in, and send a draft only on the person's c…
jeremy Oct 23, 2026
e2be2e5
Close the gaps review found in send_draft's safeguards
jeremy Oct 23, 2026
e803547
Close round two's gaps in send_draft and the device sign-in
jeremy Oct 23, 2026
dabe40d
Date the test's earlier mail so creation and delivery fall on either …
jeremy Oct 23, 2026
7976311
Deliver an attachment as its identity alone, and pin hey mcp to its s…
jeremy Oct 23, 2026
e81f465
Show delivered header text whole, escape every invisible character, a…
jeremy Oct 23, 2026
b24812e
Measure every delivered space, and key claims by the canonical origin
jeremy Oct 24, 2026
faa326b
Escape every default-ignorable character in the confirmation, not onl…
jeremy Oct 24, 2026
27c9b87
Send a draft in one request: no refresh-and-resend after a 401
jeremy Oct 24, 2026
47ef176
To someone new, send exactly the plain text the confirmation shows
jeremy Oct 24, 2026
dd14287
Revalidate the grant just before the claim, and drop invisibles befor…
jeremy Oct 24, 2026
198a20d
Refuse on a failed renewal, acknowledge applied writes, keep spacing,…
jeremy Oct 24, 2026
c472c97
Normalize the plain path's text and subject, and check acknowledgemen…
jeremy Oct 24, 2026
4f1f80b
Say a Keychain refusal isn't a missing sign-in when hey mcp starts
jeremy Oct 28, 2026
94847ae
Name no custom element in a send refusal
jeremy Oct 28, 2026
dd964be
Drop D4 and D6, offer hey_send only where the person can be asked, an…
jeremy Nov 5, 2026
82595aa
Report an unreadable hey mcp credential in styled auth status, not as…
jeremy Nov 5, 2026
ccef71e
Answer cubic's review: cut the prompt between escapes, show textarea …
jeremy Nov 5, 2026
bd992a7
Spell the test's zero-width space as an escape
jeremy Nov 5, 2026
172c1bc
Renew before choosing hey mcp's tools, show the body's whitespace as …
jeremy Nov 5, 2026
f18a2a2
Collapse a whitespace run across inline markup, and keep a listing's …
jeremy Nov 5, 2026
b6483ca
Treat a listing as a block shown whole, like pre
jeremy Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .surface
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ hey attachment save --output
hey auth
hey auth login
hey auth login --cookie
hey auth login --mcp
hey auth login --no-browser
hey auth login --token
hey auth logout
hey auth logout --mcp
hey auth refresh
hey auth status
hey auth token
Expand Down Expand Up @@ -281,9 +283,11 @@ hey label view --limit
hey label view --page
hey login
hey login --cookie
hey login --mcp
hey login --no-browser
hey login --token
hey logout
hey logout --mcp
hey mcp
hey mcp --domains
hey mcp --read-only
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,10 @@ The setup wizard connects the agents it finds. To do it yourself:
```bash
hey setup claude # skill + the hey@37signals plugin for Claude Code
hey setup codex # skill for Codex
claude mcp add hey -- hey mcp # HEY as MCP tools, on your signed-in account
hey auth login --mcp && claude mcp add hey -- hey mcp # HEY as MCP tools, on hey mcp's own sign-in
```

`hey mcp` saves drafts and never sends mail. You send a draft from HEY or with `hey draft send <id>`.
`hey mcp` saves drafts, and sends one only when you confirm that email in your MCP client.

## Docs

Expand Down
70 changes: 59 additions & 11 deletions docs/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,39 @@ Codex keeps in its log.
## MCP server

`hey mcp` runs an MCP (Model Context Protocol) server on stdin/stdout, serving HEY
boxes, search, threads, contacts, todos, calendars, and your identity as tools
backed by your signed-in account — the same keychain-stored credentials every other command uses.
Register it with any MCP client as a stdio server:
boxes, search, threads, contacts, todos, calendars, and your identity as tools.
Sign it in once, then register it with any MCP client as a stdio server:

```bash
hey auth login --mcp # hey mcp's own sign-in (read, write and send)
claude mcp add hey -- hey mcp # Claude Code
hey mcp --read-only # serve only read-only actions
hey mcp --domains boxes,search # narrow the served tool surface
```

### Its own sign-in

`hey mcp` doesn't use the CLI's credential. `hey auth login --mcp` signs it in as its own
app, "HEY MCP (local)" in HEY's Connected Apps, asking to read, write and send but nothing
more: a model driving `hey mcp` can't reach your settings, forwarding, trash or HEY World,
which the CLI's own sign-in can. The credential is kept apart from the CLI's, in its own
keychain item (or its own entry in `credentials.json` when there is no keychain), and it
refreshes as that app. `HEY_TOKEN` never stands in for it. Without it, `hey mcp` exits and
says to run `hey auth login --mcp`; it doesn't fall back to the CLI's credential, even
read-only, because that one holds full access and `--read-only` is hey mcp's own rule,
not HEY's.

The sign-in uses a device code where HEY offers one (it prints a link and a code to check),
and the browser otherwise. If you uncheck sending on HEY's page, `hey mcp` saves drafts
but serves no way to send them; sign in again to allow it. Without write it serves only
reads. `hey auth status` shows both sign-ins, and `hey auth logout --mcp` revokes hey mcp's
at HEY and removes it, leaving the CLI signed in.

The app's client id is the same in every HEY. `HEY_MCP_CLIENT_ID`, or `mcp_client_ids`
in `~/.config/hey-cli/config.json` keyed by HEY origin
(`{"mcp_client_ids": {"https://app.hey.com": "..."}}`), overrides it. A repository's
`.hey/config.json` can't set it.

Each domain is one gateway tool (`hey_boxes`, `hey_search`, `hey_threads`,
`hey_contacts`, `hey_todos`, `hey_calendar`, `hey_identity`) dispatching actions
derived from the HEY SDK's API model; call an action named `describe` for any
Expand All @@ -85,10 +108,10 @@ the caller rather than risking a duplicate delivery, so retry a failed write
yourself once you know it did not land. Logs go to stderr — stdout carries
the MCP wire protocol.

### Drafts, never sends
### Drafts first, sends confirmed

`hey mcp` never delivers mail. `create_message`, `create_reply` and `update_message`,
the three actions that can send on HEY's API, always save a draft: the entry's status is
`create_message`, `create_reply` and `update_message`, the three actions that can send
on HEY's API, always save a draft: the entry's status is
forced to `drafted`, and the entry takes only its recipients (`entry.addressed`). A
call that asks to send (any other `entry.status`), schedule (`entry.scheduled_delivery`
and its date and hour) or triage the thread in the same request (`mark_done`, `pop`,
Expand All @@ -103,8 +126,33 @@ mail may have been sent, so check HEY before trying again. A save that fails wit
answer from HEY (a dropped connection, a timeout, a 5xx) may still have been saved, and
its error says to check Drafts before retrying.

There is no send action yet. A person reviews the draft and sends it from HEY, or with
`hey draft send <draft_id>`. Earlier releases delivered mail when a call omitted
`entry.status`. An MCP client that relied on that now gets a draft. `describe` for these
actions serves the draft-only schema, and the `hey_threads` tool description says the same.
`--read-only` still drops every write action, drafts included.
Delivery is one action in a tool of its own: `hey_send`'s `send_draft`, with the draft's
id. Each call asks you to confirm the email in your MCP client (an MCP elicitation),
showing the sending address, To, Cc and Bcc (a dash when empty), the subject, attachments,
images and the body's first 1,500 characters, with links' targets beside their text and
every control and invisible character escaped so a display name can't forge a line.
Whatever it can't show (the rest of a long body, what attachments hold, images, hidden
parts or formatting the text leaves out) is named on a `Not shown here` line beside the
draft's link in HEY. What you approve is what goes: the draft is fetched again after you
answer, and if its recipients, sender, subject, body or attachments changed it isn't
sent. Each draft is sent through a `hey mcp` process once; another attempt in the next 24
hours, even after a failure, is refused, and the send is never retried, so an unclear
outcome says to check Sent.

So when you ask your agent to send a draft, it calls `send_draft` straight away: the
confirmation is where you check what goes out, and saying yes in the chat doesn't send
anything. You can still have it show or revise the draft first as you like.

`hey_send` is served only when the sign-in was granted sending, never under `--read-only`,
and is listed only to an MCP client that can show a confirmation. A client that can't is
told to send each draft from its page in HEY (the draft save's `app_url`) or with
`hey draft send <id>`, and a call to `send_draft` from it answers with that link, as
`edit_url`, and sends nothing. `send_draft` also refuses a draft that is already
scheduled, has no recipients, or has a recipient with no address. Nothing
else a draft holds refuses it: how much your agent sends, and to whom, is yours to decide
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
Resolved
once you've granted sending.

Earlier releases delivered mail when a call omitted `entry.status`; these actions now
save drafts only. `describe` for the draft actions serves the draft-only schema, and the
`hey_threads` tool description says the same. A person can still send any draft from HEY
or with `hey draft send <draft_id>`.
10 changes: 10 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ hey auth logout # clear credentials

`hey login` and `hey logout` are top-level shortcuts for `hey auth login` and `hey auth logout`.

`hey mcp` has a sign-in of its own, kept apart from the CLI's and limited to reading,
writing and sending (see [agents.md](agents.md#its-own-sign-in)):

```bash
hey auth login --mcp # sign hey mcp in
hey auth logout --mcp # revoke hey mcp's sign-in and remove it
```

`hey auth status` reports both.

### Linked accounts

One HEY login exposes every mail account linked to that identity. List the available
Expand Down
Loading
Loading