Sales Prep MCP
# Sales Prep and Follow-up
Pre-call briefs, follow-up emails, enriched lead sheets and payment links, from your calendar.
An MCP server with **14 workflows** across Google Calendar, Tavily, Firecrawl, Gmail, Slack, Granola, Notion, Google Sheets, Google Forms, Stripe, Google Docs, Linear and Google Drive. Each workflow is a prompt your agent runs as a slash command, over the 33 tools it needs and no others.
```bash
uv tool install https://github.com/r28ai/sales-prep-mcp/releases/download/v0.1.0/sales_prep_mcp-0.1.0-py3-none-any.whl
claude mcp add sales -- sales-prep-mcp
```
It installs with [uv](https://docs.astral.sh/uv/) from this repository's release, with no git and nothing to build; nothing but Charter and the libraries it uses comes from PyPI. To update, run the install line from the [latest release](https://github.com/r28ai/sales-prep-mcp/releases/latest). If a desktop app cannot find `sales-prep-mcp`, give it the full path from `which sales-prep-mcp` (`where sales-prep-mcp` on Windows).
Then ask your agent to **connect your apps**, or run `/mcp__sales__setup`.
## Connect your apps
Ask the agent to connect one ("connect Linear"). It tells you where to get that app's key and the command that stores it, and the next call works, with no restart. The agent never asks for a key in the chat.
Or connect everything this server uses from a terminal:
```bash
sales-prep-mcp login # each app in turn
sales-prep-mcp login tavily # just one
sales-prep-mcp status # what is connected
```
Tokens and keys go to your operating system's keychain (macOS Keychain, Windows Credential Manager, the Secret Service on Linux), and are checked with one read-only call to the app's own API before they are kept. Every key, token and OAuth client is yours: we register no app with any of these services, and nothing passes through a server of ours, because there isn't one.
| App | How it connects | Or set |
|---|---|---|
| Google | Browser sign-in, over your own OAuth client ([make one](https://docs.r28.ai/charter/auth/setup/google)). | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET` |
| Tavily | Your own key ([get one](https://app.tavily.com/home)), entered once. | `TAVILY_API_KEY` |
| Firecrawl | Your own key ([get one](https://www.firecrawl.dev/app/api-keys)), entered once. | `FIRECRAWL_API_KEY` |
| Slack | Your own key ([get one](https://docs.r28.ai/charter/auth/setup/slack)), entered once. A bot token from your own Slack app, which the guide sets up in about three minutes. | `SLACK_BOT_TOKEN` |
| Granola | Your own key ([get one](https://docs.granola.ai/help-center/sharing/integrations/granola-api)), entered once. In the Granola app: Settings → Connectors → API keys. Business plan or above. | `GRANOLA_API_KEY` |
| Notion | Your own key ([get one](https://www.notion.so/profile/integrations)), entered once. Then share the pages it should see with the integration. | `NOTION_API_KEY` |
| Stripe | Your own key ([get one](https://dashboard.stripe.com/apikeys)), entered once. | `STRIPE_API_KEY` |
| Linear | Your own key ([get one](https://linear.app/settings/account/security)), entered once. | `LINEAR_API_KEY` |
A variable set in your client's config always wins over the keychain.
## Workflows
| Workflow | What you get | Apps |
|---|---|---|
| **Pre-call brief for tomorrow's external meetings** <br>`pre_call_brief_for_tomorrow_s_external_meetings` | Company news, what they sell and the last email thread, in a DM the night before. | Google Calendar, Tavily, Firecrawl, Gmail, Slack |
| **Call → follow-up email and next step** <br>`call_to_follow_up_email_and_next_step` | The recap email is drafted, the next meeting held and the CRM updated before the rep leaves the room. | Granola, Gmail, Google Calendar, Notion |
| **Lead list enrichment** <br>`lead_list_enrichment` | A column of domains becomes size, product, pricing model and a recent signal per row. | Google Sheets, Firecrawl, Tavily |
| **Inbound demo request → booked** <br>`inbound_demo_request_to_booked` | A qualified request gets a meeting and a researched AE within the hour. | Google Forms, Firecrawl, Google Calendar, Gmail, Slack |
| **Scope agreed on a call → payment link** <br>`scope_agreed_on_a_call_to_payment_link` | The deal closes while the buyer still remembers saying yes. | Granola, Stripe, Gmail |
| **Signed quote → Stripe invoice** <br>`signed_quote_to_stripe_invoice` | The quote's line items become the invoice's, with no retyping. | Google Docs, Stripe |
| **Closed-won handoff** <br>`closed_won_handoff` | Account page, customer record and internal channel exist the day the deal closes. | Stripe, Notion, Linear, Slack |
| **Target account research** <br>`target_account_research` | Every account in the target list gets a cited brief on its page. | Notion, Tavily |
| **Pipeline digest** <br>`pipeline_digest` | Deals with no meeting booked in two weeks get flagged to their owner. | Notion, Google Calendar, Slack |
| **Trial ending → rep alert and nudge** <br>`trial_ending_to_rep_alert_and_nudge` | Trials that end in three days get a human touch instead of a silent expiry. | Stripe, Gmail, Slack |
| **No-show recovery** <br>`no_show_recovery` | A meeting with no notes was a no-show, and it gets a reschedule email. | Google Calendar, Granola, Gmail |
| **Security questionnaire, prefilled** <br>`security_questionnaire_prefilled` | Two hundred rows answered from your own policies, for a human to review. | Google Sheets, Notion, Google Drive |
| **New thread from an unknown domain → CRM** <br>`new_thread_from_an_unknown_domain_to_crm` | Inbound that didn't come through a form still lands in the CRM with context. | Gmail, Notion, Firecrawl |
| **Mail merge from a sheet** <br>`mail_merge_from_a_sheet` | Personalised drafts per row, status written back, nothing sent without review. | Google Sheets, Gmail |
Every prompt takes one optional argument, `details`: the repo, team, channel, customer or date range you mean, so the agent does not have to ask. In Claude Code, put it in quotes, or only its first word arrives:
```
/mcp__sales__pre_call_brief_for_tomorrow_s_external_meetings "tomorrow's calls only, skip internal ones"
```
Reads run without asking. Before anything that creates, sends, changes or deletes, the prompt tells the agent to show you the call and wait.
2 of the 14 workflows need no Google or Granola credential.
## Other clients
**Claude Desktop**: install [uv](https://docs.astral.sh/uv/getting-started/installation/) if you have not, since Claude Desktop starts the server with it, then open the `.mcpb` from the [latest release](https://github.com/r28ai/sales-prep-mcp/releases/latest). Claude asks for any keys in its own settings and keeps them in your keychain. The first start takes a few seconds longer, while uv installs it.
**VS Code** (`.vscode/mcp.json`): VS Code asks for each key the first time the server starts and stores it securely. Leave out any you stored with `login`.
```json
{
"inputs": [
{
"type": "promptString",
"id": "google-client-secret",
"description": "Google: OAuth client secret",
"password": true
},
{
"type": "promptString",
"id": "tavily-api-key",
"description": "Tavily: API key",
"password": true
},
{
"type": "promptString",
"id": "firecrawl-api-key",
"description": "Firecrawl: API key",
"password": true
},
{
"type": "promptString",
"id": "slack-bot-token",
"description": "Slack: Bot token (xoxb-\u2026)",
"password": true
},
{
"type": "promptString",
"id": "granola-api-key",
"description": "Granola: API key",
"password": true
},
{
"type": "promptString",
"id": "notion-api-key",
"description": "Notion: Integration secret (ntn_\u2026)",
"password": true
},
{
"type": "promptString",
"id": "stripe-api-key",
"description": "Stripe: Secret or restricted key",
"password": true
},
{
"type": "promptString",
"id": "linear-api-key",
"description": "Linear: Personal API key",
"password": true
}
],
"servers": {
"sales": {
"type": "stdio",
"command": "sales-prep-mcp",
"env": {
"GOOGLE_CLIENT_SECRET": "${input:google-client-secret}",
"TAVILY_API_KEY": "${input:tavily-api-key}",
"FIRECRAWL_API_KEY": "${input:firecrawl-api-key}",
"SLACK_BOT_TOKEN": "${input:slack-bot-token}",
"GRANOLA_API_KEY": "${input:granola-api-key}",
"NOTION_API_KEY": "${input:notion-api-key}",
"STRIPE_API_KEY": "${input:stripe-api-key}",
"LINEAR_API_KEY": "${input:linear-api-key}",
"GOOGLE_CLIENT_ID": ""
}
}
}
}
```
**Cursor** (`.cursor/mcp.json`) starts it the same way:
```json
{
"mcpServers": {
"sales": {
"command": "sales-prep-mcp"
}
}
}
```
**Codex** (`~/.codex/config.toml`) starts a turn without waiting for a server unless it is `required`, and then the agent has none of its tools. `required = true` makes the session wait for it, and `startup_readiness = "catalog"` waits for its tool list rather than just its connection:
```toml
[mcp_servers.sales]
command = "sales-prep-mcp"
required = true
startup_readiness = "catalog"
startup_timeout_sec = 30
```
Name the server `sales`. A host builds each tool's name from that key, and a longer one can push a tool past the 64 characters a function name allows.
## Built with Charter
Every tool here is a [Charter](https://github.com/r28ai/charter) declaration: a Pydantic schema saying where each field goes on the wire. Charter's runtime builds the request, attaches and refreshes the credential, and trims the response before the model reads it. It runs in your process, with no proxy and no telemetry.
The 33 tool schemas come to 47,240 tokens.
The same tools work in your own agent, without MCP:
```python
from charter.adapters.openai import to_openai_tools
from charter_packs_mcp import FAMILIES
tools = FAMILIES["sales"].tools()
definitions = to_openai_tools(tools) # or charter.adapters.langchain
```
Need an API that isn't here? [Write a pack](https://docs.r28.ai/charter/start/coding-agents): your coding agent writes the declarations, and Charter's conformance suite checks them.
<details>
<summary>All 33 tools</summary>
- **Google Calendar**: `gcalendar_events_list`, `gcalendar_events_insert`
- **Tavily**: `tavily_search`, `tavily_research_create`, `tavily_research_get`
- **Firecrawl**: `firecrawl_scrape`, `firecrawl_extract`
- **Gmail**: `gmail_threads_list`, `gmail_drafts_create`, `gmail_messages_send`
- **Slack**: `slack_chat_post_message`, `slack_conversations_create`
- **Granola**: `granola_notes_get`, `granola_notes_list`
- **Notion**: `notion_pages_update`, `notion_pages_create`, `notion_data_sources_query`, `notion_search`
- **Google Sheets**: `gsheets_spreadsheets_values_get`, `gsheets_spreadsheets_values_update`
- **Google Forms**: `gforms_forms_responses_list`
- **Stripe**: `stripe_prices_list`, `stripe_checkout_sessions_create`, `stripe_customers_create`, `stripe_invoice_items_create`, `stripe_invoices_create`, `stripe_invoices_finalize`, `stripe_subscriptions_list`, `stripe_customers_retrieve`
- **Google Docs**: `gdocs_documents_get`
- **Linear**: `linear_customer_create`
- **Google Drive**: `gdrive_files_list`, `gdrive_files_export`
</details>
## License
Apache 2.0.
TDQS
Scored across 35 tools
Most tools are clearly separated by app prefix and action, e.g. gmail_messages_send vs gmail_drafts_create. Some overlap remains among tavily_search, tavily_research_create, firecrawl_scrape, and firecrawl_extract, which could be confused for general web research tasks. Overall the set is mostly distinct but not perfectly unambiguous.
Tool names follow a highly predictable snake_case pattern, usually app/service/resource/action, such as notion_pages_create, stripe_invoices_finalize, and gcalendar_events_list. Minor deviations like connect and connection_status are meta-tools and do not break the pattern. The naming convention is consistent throughout.
With 35 tools, this is well above the typical 3-15 well-scoped range and exceeds the 25+ threshold for being too many. Although the server spans many integrations, the surface feels heavy and likely includes tools that could be consolidated or conditionally exposed. The count is not well-scoped for a single sales-prep agent.
Coverage is broad across apps but shallow and has significant gaps: Gmail can list threads but not read message bodies, Calendar can list/insert but not update/delete, and Linear only creates customers. granola_notes_get even references a notes_transcript_get tool that is not present, creating a dead end. These gaps would likely cause agent failures in real sales-prep workflows.