A side-by-side look at the Agent Email 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 Agent Email 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.
list_emails, read_email, search_emails.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 | 6 | 10 |
| Directory activity | 3 views 0 installs 0 upvotes | 5 views 0 installs 0 upvotes |
| Tools | 27 | 42 |
| Full listing | View Agent Email | View Cv MCP Server |
list_emailsList recent emails with filtering by unread status, folder, sender, and limit. Use offset for pagination. A row with `isDraft: true` is an unsent draft — it has NOT been sent, and its `receivedAt` is provider-supplied metadata, not evidence of delivery. Never describe such a row as a sent, delivered, or received email.read_emailRead the full content of an email by ID, transformed to token-efficient markdown. When the response has `isDraft: true` the message is an unsent draft — it has NOT been sent, and `receivedAt` is provider-supplied metadata, not evidence of delivery; never describe it as a sent, delivered, or received email. Set format to 'html' to get the raw body HTML instead of markdown — use this when you need to preserve inline styling (colour, background-colour, underline, strikethrough) that the markdown conversion discards, e.g. to change one sentence of a formatted body and leave the rest alone. Raw HTML costs far more tokens than markdown, so leave the default alone unless you need the styling; check `bodyFormat` (`text` means the message had no HTML part, so do not send it as HTML) before writing a body back, and never write one back with `bodyTruncated: true` — you do not have the whole body. strip_quoted_history and strip_signatures are markdown-shaped text transforms and are NOT applied when format is 'html'; the HTML is returned verbatim. To write it back, call create_draft/update_draft with format: 'html' AND force_black: false — force_black defaults to true and wraps your HTML in a force-black div, so leaving it on nests another wrapper on every round trip. Set strip_quoted_history to true to drop the terminal "On … wrote:" / Outlook-header / `>`-prefix reply chain and replace it with a short marker. Set strip_signatures to true to remove detected signatures and legal disclaimers; it defaults to false here for MCP compatibility even though the core action defaults to true.search_emailsSearch emails using full-text query across one or all mailboxes. Use offset for pagination. Results include unsent drafts: a row with `isDraft: true` has NOT been sent — its `receivedAt` is provider-supplied metadata, not evidence of delivery. Never describe such a row as a sent, delivered, or received email.get_mailbox_statusGet mailbox connection status, unread count, and warningslist_mailboxesList every configured mailbox: its logical `name` (pass this as the `mailbox` argument to other tools), its `emailAddress`, provider, connection status, and which one is the default. Reports mailboxes that failed to authenticate too, so it stays useful for diagnosing a broken setup.get_threadRetrieve all messages in a conversation thread by message ID. A thread can end with an unsent draft reply: a message with `isDraft: true` has NOT been sent, so it is not a sent reply — though earlier sent replies from the same author may exist elsewhere in the thread. Never describe such a message as a sent, delivered, or received email.send_emailCompose and send a new email. Gated by send allowlist. Draft mode bypasses allowlist. If a send fails with SEND_STATUS_UNKNOWN, the message may already have been delivered; do not resend without checking Sent Items. An identical send repeated within the duplicate window is refused with a DUPLICATE_SEND_* code instead of delivering twice; pass allow_duplicate: true only when a human has decided to send the same message again.reply_to_emailReply to an email within an existing thread. Default reply_all=true cc's the original thread; pass reply_all=false to reply only to the sender. Send path validates all effective recipients against the send allowlist; draft path bypasses. If a send fails with SEND_STATUS_UNKNOWN, the message may already have been delivered; do not resend without checking Sent Items. An identical reply repeated within the duplicate window is refused with a DUPLICATE_SEND_* code instead of delivering twice; pass allow_duplicate: true only when a human has decided to send the same reply again.create_draftCreate an email draft with to/cc/bcc recipients. Supports body_file with YAML frontmatter (to, cc, bcc, subject, reply_to, format). Use reply_to for threaded reply drafts, where to and subject are derived from the parent message and may be omitted; pass reply_all=false with reply_to to draft a sender-only reply.send_draftSend a previously created draft. Enforces send allowlist before sending. Rate-limited. If a send fails with SEND_STATUS_UNKNOWN, the message may already have been delivered; do not resend without checking Sent Items. Sending the same draft id twice within the duplicate window is refused with a DUPLICATE_SEND_* code instead of delivering twice; pass allow_duplicate: true only when a human has decided to send it again.update_draftUpdate a draft email. Body edits are refused for reply drafts; non-reply body edits require replace_body=true and replace the body wholesale. Subject, recipients (to/cc/bcc), and attachments remain editable. Allowlist is enforced at send_draft time, not here.cancel_scheduled_sendCancel a pending provider-held scheduled send. Only verified scheduled drafts can be cancelled.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": {
"agent-email": {
"command": "npx",
"args": [
"-y",
"email-agent-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"
}
}
}
}