Look up your contacts in Windsurf
A contact is one person across everything Launch knows about them. With the MCP server connected, the agent can answer questions that otherwise mean exporting three CSV files and matching emails by hand.
Windsurf (now Devin Desktop) reads MCP servers from one file in your home directory, shared by every project. Remote servers take a serverUrl and optional headers.
Setup
You need a secret key (nv_live_…) from Dashboard → API keys.
01Add the server to mcp_config.json
~/.codeium/windsurf/mcp_config.json
{
"mcpServers": {
"nordva": {
"serverUrl": "https://mcp.nordva.dev",
"headers": {
"Authorization": "Bearer nv_live_..."
}
}
}
}The field is serverUrl, not url. The file is outside your repositories, so the key can sit in it; Windsurf also replaces ${env:NAME} with an environment variable if you would rather keep it out. npx nordva init writes this file for you.
02Check the connection
In Cascade, open the MCP servers settings and refresh. nordva should be listed with its tools. Ask Cascade to run setup_check.
Then ask Windsurf
- Which of my waitlist signups are paying customers now?
- What has [email protected] asked for, and is she paying?
- List paying customers who came from Product Hunt.
- Mark [email protected] as in talks and note that she wants SSO.
- Export everyone who agreed to marketing email as a CSV.
For “Which paying customers left feedback?” Windsurf calls list_contacts with:
{
"paying": true,
"has_feedback": true,
"limit": 10
} Returned: Contacts with id, email, source, waitlist status, subscriber flag, feedback count, Stripe status, plan, MRR, stage, details and consents, plus a cursor when there are more.
Contacts tools and arguments
list_contacts
Read-onlyList people across waitlist, feedback, subscribers and payments
| paying | optional | boolean | |
| has_feedback | optional | boolean | |
| status | optional | "waiting" | "invited" | "unsubscribed" | |
| source | optional | string, max 255 | |
| stage | optional | "potential" | "in_talks" | "customer" | "recurring" | "not_a_fit" | "inactive" | |
| q | optional | string, max 254 | Part of an email address. |
| consent | optional | string | Only people who have granted this consent now, e.g. "email_marketing", or a built-in: "waitlist" and "changelog". |
| detail | optional | string, max 250 | "name" for people who have that detail, or "name=value" for that value (case-insensitive), e.g. "role=founder". |
| limit | optional | number, 1–100 | |
| cursor | optional | string |
get_contact
Read-onlyGet one person across every primitive
| contact_id | required | string, max 64 | Contact id, e.g. "ct_...". |
get_contact_journey
Read-onlyBuilderOne person's visits before they signed up or paid
| contact_id | required | string, max 64 |
update_contact
Set a contact's stage, note, details or consents
| contact_id | required | string, max 64 | |
| stage | optional | "potential" | "in_talks" | "customer" | "recurring" | "not_a_fit" | "inactive" | |
| note | optional | string, max 5,000 | |
| details | optional | object | Name to value; null removes. Reuse names from list_contact_fields, e.g. {"role": "founder"}. |
| consents | optional | object | Purpose to true (granted) or false (withdrawn), only as the person said, e.g. {"email_marketing": true}. |
list_contact_fields
Read-onlyList the detail names and consents in use on contacts
No arguments.
export_contacts
Read-onlyIndie and BuilderExport contacts as a CSV download link
| consent | optional | string | Only people who have granted this consent now, e.g. "email_marketing", or a built-in: "waitlist" and "changelog". |
| detail | optional | string, max 250 | "name" for people who have that detail, or "name=value" for that value (case-insensitive), e.g. "role=founder". |
| paying | optional | boolean | |
| has_feedback | optional | boolean | |
| status | optional | "waiting" | "invited" | "unsubscribed" | |
| source | optional | string, max 255 | |
| stage | optional | "potential" | "in_talks" | "customer" | "recurring" | "not_a_fit" | "inactive" | |
| q | optional | string, max 254 | Part of an email address. |
Behaviour worth knowing
- People are matched on normalised email: lowercase, +alias removed, dots ignored on Gmail.
- update_contact changes the stage, the note, and the person's details and consents. It cannot change an email, a waitlist status or anything in Stripe, and a new note replaces the old one.
- Details are named values about a person (role, company size); consents are permissions they gave (email_marketing), granted or withdrawn, with when and how. The agent is told to reuse the names list_contact_fields returns and to record a consent only when the person gave it.
- export_contacts returns a CSV download link for the same filters as list_contacts, for example everyone who granted email_marketing. The link works for 15 minutes. Indie plan and up.
- It works without Stripe connected; every contact then has Stripe status none.
- On the Free plan only waitlist people are listed, in a single page.
- A contact is removed when the person no longer exists in any source, for example after their signup is deleted.
- The file is shared by all your projects, so it points at one Launch project at a time. Running npx nordva init in another project points it there.