Handing a report to your coding agent
Every report your clients file already contains what a coding agent needs: the page and the element they pointed at, their browser and viewport, the console messages and failed requests from that moment, the screenshot they marked up, and their own words.
There are two ways to get it there. Copy as prompt is the manual one: a button on every issue that copies a self-contained Markdown briefing you paste into whatever you use. The MCP server is the automatic one: your agent connects to Client Screen directly, lists your open reports, reads one in full, fixes it, and sets it to “Please check” — which emails your client and asks them to verify, without you touching the board.
Copy as prompt
On an issue page, press Copy as prompt. You get a Markdown block: the issue key and
status, the page and route (and the path the client took to get there), the element’s
selector and HTML, the environment, the console and network buffers, anything your app
attached through ClientScreen.setContext(), the client’s raw words, the client-visible
part of the conversation, and links to both screenshots.
Those screenshot links are signed and expire 24 hours after the block was produced.
They need no login, which is exactly what makes them usable by a coding agent — and exactly
why they are short-lived. If your deployment has no FILE_SIGNING_SECRET set, the block
says so in place of the links rather than quietly leaving them out.
The MCP server
Client Screen speaks the Model Context Protocol over streamable HTTP at the address below. Settings → API keys in the dashboard shows the same command with your key already in it.
https://scintillating-civet-800.eu-west-1.convex.site/mcp
It is stateless: every call is one authenticated HTTP request. Any MCP client that can send
an Authorization header will work; two are spelled out below.
1. Create an API key
Dashboard → Settings → API keys → New key. Give it a name you will recognise later and pick its scopes:
- read —
list_projects,list_issues,get_issue. - write —
add_comment,set_status,ask_reporter.
The key is shown once, at creation. Copy it then; there is no way to read it back, because only its SHA-256 is stored. Lost one? Revoke it and make another. Revoking takes effect on the next call.
A key belongs to a workspace and can reach nothing outside it. Anything it writes is recorded as yours, with a note in the issue’s activity saying which key did it.
2. Point your agent at it
Claude Code — one command:
claude mcp add --transport http client-screen \
https://scintillating-civet-800.eu-west-1.convex.site/mcp \
--header "Authorization: Bearer cs_your_key"
Add --scope project to share the server with everyone working in that repository (the
key then lands in .mcp.json, so use --scope local, the default, if the repo is public).
Cursor — .cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project:
{
"mcpServers": {
"client-screen": {
"url": "https://scintillating-civet-800.eu-west-1.convex.site/mcp",
"headers": { "Authorization": "Bearer cs_your_key" }
}
}
}
Any other MCP client — give it the URL above as a streamable HTTP (not SSE, not
stdio) server, with one header: Authorization: Bearer cs_your_key. The server answers
POST only; a GET or DELETE is a 405, because there is no session to stream or close.
3. The tools
| Tool | Scope | What it does |
|---|---|---|
list_projects |
read | Your client sites, with how many reports are open on each. Start here: every other tool names a project by the prefix this returns. |
list_issues(project, status?) |
read | The 50 newest reports in a project, optionally in one status. |
get_issue(project, number) |
read | The whole briefing — the same content as “Copy as prompt” — plus the marked-up screenshot as an image (over 1 MB it is left at the signed link instead). |
add_comment(project, number, body, visibility) |
write | internal is a private note; client is a reply the reporter receives by email. |
set_status(project, number, status) |
write | new, accepted, in_progress, ready_for_check, done, wont_do. |
ask_reporter(project, number, question) |
write | A client-visible question; the reporter is emailed and their answer lands on the issue. |
A project is named by its prefix (WEB, case-insensitive) or by its id. An issue is named
by its number within the project — the 3 of WEB-3.
The loop worth knowing: after fixing something, set_status to ready_for_check. The
client gets an email asking them to verify, and your board moves without you.
What it will refuse
- An unknown or revoked key:
401. - A read-only key on a write tool: a tool error saying so, not a broken connection — the agent keeps the read tools.
- A project in another workspace: not found, whether named by prefix or by id.
- Too many calls:
429with aRetry-Afterheader, at 300 calls per 10 minutes per key. Calls that present no working key are counted per IP address instead, against the same budget as the reporting endpoints, and that budget is checked before a key is looked up.
Trying it by hand
curl -s https://scintillating-civet-800.eu-west-1.convex.site/mcp \
-H "Authorization: Bearer cs_your_key" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
The Accept header carrying both types is required by the protocol; without it the
transport answers 406.
For maintainers
- The server is
convex/mcp.ts: the official TypeScript MCP SDK’sMcpServerand itsWebStandardStreamableHTTPServerTransport, running inside a Convex HTTP action. One server is built per request; nothing is kept between calls. - The briefing is built by one pure function,
buildPromptinconvex/lib/prompt.ts, from data gathered byissues.promptData(the dashboard) orissues.forAgent(the MCP server). The two paths render the same block by construction. - Signed links are
convex/lib/sign.ts: HMAC-SHA256 over<storageId>.<expiry>with the deployment’sFILE_SIGNING_SECRET. Rotating that secret invalidates every outstanding link at once. Set it withnpx convex env set FILE_SIGNING_SECRET "$(openssl rand -hex 32)".