A side-by-side look at the EmptyInbox and Cv MCP Server MCP servers: transport, auth, tools, quality signals, and the config to install either one in Claude, Cursor, Windsurf or VS Code.
Verdict: choose EmptyInbox for communication work as a local process; choose Cv MCP Server for communication as a local process. You can also run both side by side in one mcpServers block.
register_account, get_quota, create_inbox.CARBON_VOICE_API_KEY, LOG_LEVEL, LOG_DIR.list_messages, get_message, get_recent_messages.Rows where the two servers differ are marked.
| Spec | ||
|---|---|---|
| Category | Communication | Communication |
| Quality signal | 63/100 · Good | 64/100 · Good |
| Install command | Confirmed | Confirmed |
| Transport | Local subprocess (stdio) | Local subprocess (stdio) |
| Auth | Not declared | other |
| Pricing | Not declared | BYOK (pay the provider) |
| Required env vars | None listed | CARBON_VOICE_API_KEYLOG_LEVELLOG_DIR |
| Verified owner | Community listing | Community listing |
| GitHub stars | 4 | 10 |
| Directory activity | 1 views 0 installs 0 upvotes | 5 views 0 installs 0 upvotes |
| Tools | 12 | 42 |
| Full listing | View EmptyInbox | View Cv MCP Server |
register_accountRegister a new EmptyInbox account and get an API key. Use this only if there is no account configured yet, or if authentication is failing. Prefer reusing an existing key: repeatedly registering from one network reduces the free credits each new account receives. Saves the key locally for future sessions.get_quotaCheck how many inbox credits remain on this account. When credits are low, the result says how to buy more with Bitcoin.create_inboxCreate a new disposable email inbox. Returns the email address and when it expires (30 days). Use this before triggering any signup or email verification flow.keep_inboxMake an inbox permanent so it never expires, for 30 credits. Use when the address was used to sign up for an account the user will keep (password resets go there). Ask the user first. Also brings back an expired inbox: addresses are never given to anyone else. Free if the inbox is already permanent.list_inboxesList all email inboxes on this account, with when each expires (null = permanent) and whether it already has.list_messagesList received email messages, newest first, already parsed: each entry carries the extracted one-time code, the link to open, and a text preview — no HTML parsing needed. Filter by inbox and use limit to keep responses small.get_messageGet one email message by ID. Returns the parsed message: extracted code, action_url, every link with its anchor text, and the body as plain text. Set format='text' for a flat rendering to read directly, or include_html=true only if the raw HTML is genuinely needed.wait_for_messagePoll an inbox until a new message arrives. Use after triggering an email verification, OTP, or signup confirmation. Returns the parsed message — including the extracted code and the link to open — as soon as it lands, or a timeout result.buy_quotaBuy inbox credits with Bitcoin. Only call this after the user has chosen a bundle or amount. Returns a pay_url to give the user: a page with a QR code, the exact amount and a live payment status, which works with any Bitcoin wallet. Also returns the raw address, amount and BIP21 URI for paying directly. Credits are added as soon as the payment is seen on the network. Call check_payment once the user has paid.list_bundlesList the inbox credit bundles for sale and their USD prices, paid in Bitcoin. Show these to the user before calling buy_quota.check_paymentCheck whether a Bitcoin payment from buy_quota has landed. Pass wait_seconds (around 120) once the user says they have paid, to wait for the credits in one call. Credit usually arrives within seconds of broadcast, well before confirmation.report_issueSend feedback to the EmptyInbox team: a bug, a failed or abandoned payment, confusing docs, or a missing feature. A human reads every report. Use it when a tool fails in a way retrying will not fix, or when the user decides not to buy and says why. Never include passwords, verification codes, API keys or email contents.list_messagesList messages with filtering by date, conversation, folder, workspace, creator or language.
USE WHEN: The general-purpose message reader; full bodies incl. transcript and AI summary. Max date span 183 days. `user_ids` filters by SENDER — for messages exchanged WITH someone, pass `conversation_id` from `list_conversations`. Use presigned URLs as-is.
USE INSTEAD: `get_recent_messages` for a quick look at the latest few (hard-capped at 10, no paging). `search_message_ids` for filters this cannot express — notified state, mentions or labels.
FIRST: `workspace_id` comes from `get_workspaces_basic_info` (field `id`) — call it first if you don't have one.
EXAMPLE: {"workspace_id":"ws-abc","start_date":"2026-09-01T00:00:00Z","size":25}
RETURNS: `{page, size, sort_direction, total, results_count, has_next_page, filters, results: [{id, transcript?, ai_summary?, audio_url?, creator_id, conversation_id?, duration_ms, reply_count, status, type, created_at, ...}]}`. Page on `has_next_page`/`total` — do not guess.
NARROW: pass response_fields ["total","has_next_page","results.id","results.transcript","results.created_at"] unless you need more — the full payload is much larger.get_messageGet one message by ID, optionally expanded with related records.
USE WHEN: You have a message ID. `fields` ADDS related data (`conversation`, `creator`, `labels`) — it does not narrow the response. Use `response_fields` to narrow.
USE INSTEAD: `list_messages` when you do not have an ID, or need several messages — it already returns full bodies, so fetching each one again is wasted.
FIRST: `id` comes from `list_messages` (field `results[].id`) — call it first if you don't have one.
EXAMPLE: {"id":"msg-abc","fields":"creator"}
RETURNS: `{message: {id, transcript?, ai_summary?, audio_url?, creator_id, duration_ms, status, type, attachments?, created_at, ...}}`.get_recent_messagesGet up to 10 of the most recent messages, each with its conversation, creator and labels.
USE WHEN: A quick "what just happened" glance. Pre-joined, so no follow-up calls for creator or conversation names.
USE INSTEAD: `list_messages` whenever you need more than 10, any date range, paging, or a filter other than conversation and language — this tool supports none of those.
EXAMPLE: {"conversation_id":"conv-abc"}
RETURNS: `{results: [{message: {...}, conversation: {...}, creator: {...}, labels: [...]}]}`. No total and no paging: the cap of 10 is the whole answer.create_conversation_messagePost a message into an existing conversation, or reply in a thread.
USE WHEN: You have a `conversation_id`. Pass `parent_id` (a message ID) to reply as a thread. Either `transcript` or `links` is required — the schema marks neither individually, so both param descriptions say so.
USE INSTEAD: `create_direct_message` to reach people who are not already in a conversation. `create_voicememo_message` for a standalone memo.
FIRST: `id` comes from `list_conversations` (field `results[].id`) — call it first if you don't have one.
EXAMPLE: {"id":"conv-abc","transcript":"Agreed, shipping Friday."}
RETURNS: `{message: {id, link, transcript?, status, type, conversation_id, created_at, ...}}`.
ERROR BAD_REQUEST: Neither `transcript` nor `links` was provided. — Pass at least one of them.create_direct_messageSend a direct message to one or more people, by user ID or email.
USE WHEN: Reaching people outside an existing conversation. Address it with `to.user_ids` or `to.emails`. Requires `transcript` or `links`.
USE INSTEAD: `create_conversation_message` when a conversation already exists — a DM starts a separate thread rather than joining it.
FIRST: `to.user_ids` comes from `search_users` (field `id`) — call it first if you don't have one.
EXAMPLE: {"to":{"user_ids":["user-abc"]},"transcript":"Quick question about the deck."}
RETURNS: `{message: {id, link, transcript?, status, conversation_id, created_at, ...}}`.
ERROR BAD_REQUEST: A user ID is invalid, or neither `transcript` nor `links` was provided. — Resolve people with `search_users` — never pass a display name as a user ID — and include a transcript or links.create_voicememo_messageCreate a voice memo, either from text (spoken via text-to-speech) or from an audio file at a URL.
USE WHEN: Pass `transcript` (2-5000 chars) to have Carbon Voice speak the text, or an https `audio_url` to upload audio, which wins over `transcript`. File it with `folder_id`, whose type must match, or a `workspace_id`.
USE INSTEAD: `create_conversation_message` to post into an existing conversation, or `create_direct_message` to send to specific people. A voice memo is standalone and lives in a folder or workspace.
EXAMPLE: {"transcript":"Reminder to review the pricing deck before Friday."}
RETURNS: `{message: {id, link, transcript?, audio_url?, duration_ms, status, type, created_at, ...}}`. `status` is often `processing` at first.
ERROR INVALID_AUDIO_URL: `audio_url` is not https, unreachable, too large, timed out, embeds credentials, or resolves to a private address. — The message gives the reason. Use a public https URL, or pass `transcript` instead.
ERROR BAD_REQUEST: None of `transcript`, `audio_url` or `links` was provided, or the transcript is outside 2-5000 characters. — Provide one of the three, and keep the transcript within the length limits.add_attachments_to_messageAttach one or more link URLs to an existing message.
USE WHEN: Adding external links to a message that already exists.
USE INSTEAD: `create_message_share_link` to share a Carbon Voice message outward — that produces a link, this consumes them.
FIRST: `id` comes from `list_messages` (field `results[].id`) — call it first if you don't have one.
EXAMPLE: {"id":"msg-abc","links":["https://example.com/spec"]}
RETURNS: `{...}` confirmation with the resulting attachments.get_userGet a user's full profile by ID — names, languages, voice settings, workspace roles.
USE WHEN: You already have a user ID and need complete details.
USE INSTEAD: `search_user` / `search_users` to FIND someone by email, phone or name. `get_current_user` for the caller — this tool needs an explicit ID and will not default to you.
FIRST: `id` comes from `search_users` (field `id`) — call it first if you don't have one.
EXAMPLE: {"id":"user-abc"}
RETURNS: `{id, first_name, last_name?, languages, voice_gender, workspace_ids, workspace_roles, user_type, created_at, ...}`.
NARROW: pass response_fields ["id","first_name","last_name","workspace_ids"] unless you need more — the full payload is much larger.search_userFind a single user by email, phone or name.
USE WHEN: Resolving ONE person. Supply exactly one of `email`, `phone` or `name`. Name search only matches your own contacts.
USE INSTEAD: `search_users` for several people in one call — it takes arrays and saves a round trip per person. `get_user` when you already have the ID.
EXAMPLE: {"email":"someone@example.com"}
RETURNS: `{id, full_name, first_name, last_name?, link, image_url?, languages?, ...}`. Use `id` wherever another tool asks for a user ID.
ERROR NOT_FOUND: Nobody matched, or a name search hit a non-contact. — Try an email or phone instead of a name; name search is limited to your contacts.search_usersResolve several users at once by emails, phones, IDs or names.
USE WHEN: Turning a list of people into user IDs in one call — the right first step before any tool that takes user IDs. Name search only matches your contacts.
USE INSTEAD: `search_user` for a single lookup. `get_user` for a full profile once you have the ID.
EXAMPLE: {"names":["Brett"]}
RETURNS: Array of `{id, full_name, first_name, last_name?, link, languages?, ...}`. If a name returns more than one candidate, ask which person was meant rather than guessing.
NARROW: pass response_fields ["id","full_name"] unless you need more — the full payload is much larger.get_current_userGet the calling user's own identity, workspaces and settings.
USE WHEN: Establishing who you are acting as, or finding the caller’s workspace IDs before a workspace-scoped call. Takes no arguments.
USE INSTEAD: `get_user` for somebody else (it requires an explicit ID). `get_workspaces_basic_info` if you only need workspace IDs and names — it is far smaller than this response.
EXAMPLE: {}
RETURNS: `{success, user: {user_guid, first_name, last_name?, email_txt?, phone_txt?, workspace_guids, identities, entries, environments, lifecycle_events, notification_settings, settings, ...}, settings: {...}}`. This payload is LARGE — several unbounded arrays and an open settings map.
NARROW: pass response_fields ["user.user_guid","user.first_name","user.email_txt","user.workspace_guids"] unless you need more — the full payload is much larger.list_conversationsList your conversations from the last 6 months, optionally filtered by participants, type and name.
USE WHEN: Finding a `conversation_id`. Filter with `user_ids` plus `match`, `types` and `name` — YOUR DM with someone is `user_ids: ["<their id>"], types: ["directMessage"]`. You are always an implicit participant, so never pass your own ID. Filters AND together; if several match, ask which was meant.
USE INSTEAD: `get_conversation` when you already have an ID and want full detail — this returns only id, name, workspace_id and type.
EXAMPLE: {"user_ids":["user-abc"],"types":["directMessage"]}
RETURNS: `{results_count, results: [{id, name, workspace_id, type}]}` where type is `directMessage` | `customerConversation` | `namedConversation` | `asyncMeeting`. No paging: `results_count` is what is returned, after filtering. `name` adds `unfiltered_count`: rows the name was matched against, AFTER `user_ids`/`types`. ALWAYS check it before saying no such conversation exists. 0 of 47 means none matched that string (misspelt, or >6 months old); 0 of 0 means your other filters left nothing — not that the caller has none. Widen the filters or ask; never report it does not exist.Paste either block (or both) into your client config, e.g. claude_desktop_config.json or ~/.cursor/mcp.json. We only show a config when the install command is confirmed.
{
"mcpServers": {
"emptyinbox": {
"command": "npx",
"args": [
"-y",
"emptyinbox-mcp"
]
}
}
}
{
"mcpServers": {
"phononx-cv-mcp-server": {
"command": "npx",
"args": [
"-y",
"@carbonvoice/cv-mcp-server"
],
"env": {
"CARBON_VOICE_API_KEY": "YOUR_CARBON_VOICE_API_KEY_HERE",
"LOG_LEVEL": "YOUR_LOG_LEVEL_HERE",
"LOG_DIR": "YOUR_LOG_DIR_HERE"
}
}
}
}