Crumb runs a Model Context Protocol server, so an AI assistant (Claude Code, Claude Desktop, Cursor, or any MCP host) can connect straight to your workspace and read and triage feedback in plain language. Ask it "what are our highest-ARR accounts asking for?", or tell it to "reply to FB-42 and mark it shipped." It runs through the same code paths as the dashboard, so replies and notifications fire exactly as if you'd done it by hand.
1 · Create an API key
In Settings, API keys, an admin creates a key; a workspace can hold 20 active ones. The full key (crumb_sk_…) is shown once with a Copy button, and Crumb stores only a SHA-256 hash of it, so copy it then and treat it like a password. The same page shows your exact server URL and config snippets.
A key acts as the teammate who created it, with that person's current role: admins and PMs can read and write, and viewers can read and post internal notes. Status changes and replies made with it are attributed to that teammate, and demoting them narrows the key at once. Revoking a key takes effect on the next request, and removing its creator from the team deletes their keys. Keys only work on /api/mcp; the customer API doesn't accept them.
2 · Add it to Claude Code
One command wires it up. Hit the button to copy it, paste it in your terminal, and swap in the key you just created:
claude mcp add --transport http crumb \ https://crumb-app.localhostlabs.net/api/mcp \ --header "Authorization: Bearer crumb_sk_…"
/api/mcp.Any other MCP client
HTTP-capable hosts (Claude Desktop, Cursor, and most others) take a config block:
{
"mcpServers": {
"crumb": {
"type": "http",
"url": "https://crumb-app.localhostlabs.net/api/mcp",
"headers": { "Authorization": "Bearer crumb_sk_…" }
}
}
}For a client that only speaks stdio, bridge with mcp-remote:
npx mcp-remote https://crumb-app.localhostlabs.net/api/mcp \ --header "Authorization: Bearer crumb_sk_…"
What the assistant can do
Every tool reads and writes only the key's workspace. Reading works with any key.
Read tools
| Tool | Takes and returns |
|---|---|
list_items | Takes status, type, account, limit (default 50), and cursor, all optional. Returns items newest first, filtered by status, type (bug, idea, question), or exact account name, each with short_id, title, type, status, account, created_at, and updated_at. |
search_items | Takes query (required), limit (default 25), and cursor. Returns items whose title or body contains the text, case-insensitive, newest first. % and _ match literally. |
get_item | Takes short_id (required), like FB-42. Returns the item with its body, account, submitter, and assignee, every reply (internal notes included, marked internal), and its status timeline. |
list_roadmap | Takes nothing. Returns every initiative, grouped into now, next, later, and unscheduled, with its status and whether it's public. |
list_accounts | Takes limit (default 100) and cursor. Returns customer accounts, highest ARR first: name, arr_usd, and since (when they became a customer, or null). |
Write tools
These need a key whose creator is an admin or PM, except internal notes, which any key can post. They run the same code as the dashboard, so webhooks, Slack and Teams notifications, and customer emails fire exactly as they would there.
| Tool | Takes and does |
|---|---|
update_item_status | Takes short_id and status (both required) and reason. Moves an item to open, review, planned, progress, shipped, declined, deferred, or duplicate. The last three need a reason, which the customer sees. Returns emailed. |
reply_to_item | Takes short_id and body (both required) and internal (default false). Replies to the customer, or with internal: true adds a private team note, which never emails the customer or reaches webhooks. Returns reply_id and emailed. |
create_item | Takes account, submitter_, type, and title (all required), plus body and submitter_. Logs feedback for a customer, creating the account and the person if they're new. type is bug, idea, or question. Returns the new short_id. |
assign_item | Takes short_id (required) and assignee_. Assigns the item to a teammate by email; leave out assignee_, or send it empty, to unassign. |
Only planned, progress, shipped, declined, and deferred email the customer, and only when they can be emailed: they wrote in through the widget, have an address, haven't opted out, and your workspace can send email. A customer-facing reply emails under the same conditions, and emailed in the result says whether the email went out. Items made with create_item never email the customer, since they didn't write in through the widget.
Paging through lists
list_items, search_items, and list_accounts return at most limit rows (200 at most) and a next_cursor. When it isn't null, call the tool again with the same arguments plus "cursor": "<next_cursor>" for the next page. Cursors are opaque and continue strictly after the last row you got, so items created between calls never shift or repeat a page.
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_items","arguments":{"status":"open","limit":20}}}Every tool answers with one text block holding JSON:
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\n \"count\": 20,\n \"items\": [ … ],\n \"next_cursor\": \"MTc2…\"\n}"}]}}Protocol
- Streamable HTTP, stateless. Each
POSTcarries one JSON-RPC 2.0 message and gets one JSON response. There's no SSE stream and no session id, andGETandDELETEanswer405. - Versions:
2025-06-18(latest),2025-03-26, and2024-11-05.initializeanswers with the version the client asked for when it's one of these, and with2025-06-18otherwise. - Methods:
initialize,ping,tools/list, andtools/call. Notifications, likenotifications/initialized, get202 Acceptedwith no body. JSON-RPC batches are refused. - Rate limit: a burst of 120 requests per key, refilling 2 per second.
To check a key by hand, run the handshake:
curl -s https://crumb-app.localhostlabs.net/api/mcp \
-H "Authorization: Bearer $CRUMB_API_KEY" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"crumb","version":"1.0.0"}}}Errors
| Situation | HTTP | Response |
|---|---|---|
| Missing, unknown, or revoked key | 401 | JSON-RPC error -32600, unauthorized |
| Over the rate limit | 429 | -32603, rate_limited |
| The body isn't JSON | 400 | -32700, parse error |
| A batch | 400 | -32600, batch not supported |
| Unknown method | 200 | -32601, method not found: … |
| Unknown tool | 200 | -32602, unknown tool: … |
| Unexpected server failure | 200 | -32603, internal error |
| The tool ran and failed | 200 | A normal result with "isError": true and the reason as its text |
The 401 carries WWW-Authenticate: Bearer, and the 429 a Retry-After header. A tool that fails gives one of these reasons:
| Reason | Means |
|---|---|
forbidden | The key's creator is a viewer. Writes need an admin or PM; viewers can still post internal notes. |
not_found | No item with that short_id in this workspace. |
bad_status | Not a status your team can set. Only customers set resolved. |
reason_required | declined, deferred, and duplicate need a reason. |
invalid_cursor | cursor isn't a next_cursor this server issued. |
assignee_not_found | No teammate in this workspace has that email. |
bad_type | type isn't bug, idea, or question. |
<argument> is required | A required argument is missing or empty. |
| A sentence | A create_item check failed, like "Enter a valid email for the submitter." |