@coldleads/mcp-server 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anton Tkachenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,164 @@
1
- # Temporary Holding Version
1
+ # Cold Leads MCP server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Model Context Protocol server for [Cold Leads](https://coldleads.app/developers?utm_source=github&utm_medium=readme&utm_campaign=mcp-server), the B2B outreach CRM. It exposes the hosted workspace tools to local MCP clients (Claude Desktop, Claude Code, Cursor, Windsurf, VS Code): contacts, imports, synced conversations, templates, campaigns, website capture, lead search, and verification. Without an API key, it can also ask the human owner to approve a subscription.
4
+
5
+ ```bash
6
+ npx -y @coldleads/mcp-server
7
+ ```
8
+
9
+ ## Try it without a key
10
+
11
+ The hosted demo server has the same tools over a sample workspace (fictional contacts on `.example` domains). It never sends e-mail and uses no credits:
12
+
13
+ ```bash
14
+ claude mcp add --transport http coldleads-demo https://coldleads.app/api/mcp/demo
15
+ ```
16
+
17
+ Then ask: *"Verify these three contacts and prepare outreach drafts. Don't send anything until I approve. Jana Dvořák <jana.dvorak@ridgeline.example>, info@harborlane.example, Piotr Kowalski <piotr@quillmetrics.example>"*. Any client that supports remote (Streamable HTTP) MCP servers can use the same URL. A complete agent built on it: [cold-leads-ai-sales-agent](https://github.com/anttka4cz/cold-leads-ai-sales-agent).
18
+
19
+ Demo limits:
20
+
21
+ | Limit | Value |
22
+ | --- | --- |
23
+ | Session | 24 hours from its start (not extended), visible only to that session |
24
+ | Contacts | 10, including the 4 sample contacts |
25
+ | Templates | 1 of your own (besides the sample template; update it with `template_id`) |
26
+ | Campaign drafts | 1 |
27
+ | Text length | notes 100 characters, template body 500, message 200 |
28
+ | Requests | 120 per minute and 20 new sessions per hour per IP address |
29
+ | Verification | sample results for the three demo prospects; other addresses get syntax, disposable-domain and role checks only (no MX or mailbox check) |
30
+ | Sending | never: `send_contact_message` and `launch_campaign` still require approval, then return `demo_not_sent` |
31
+
32
+ ## Tools
33
+
34
+ | Tool | What it does | Cost |
35
+ | --- | --- | --- |
36
+ | `verify_email` | Verification of one address: syntax, disposable domain, role account and MX records, plus an SMTP mailbox and catch-all check when Cold Leads can open an SMTP connection to the recipient's mail server (otherwise `reasons` contains `smtp_unreachable` and `catch_all` is `null`). Returns `valid`, `risky` or `invalid`. Answers within 5 seconds. | 1 credit |
37
+ | `search_leads` | Contacts already in your Cold Leads CRM for a company domain, with an optional role keyword: e-mail, name, company, stage, tags, verification status and a `do_not_contact` flag. Cold Leads has no third-party lead database. | free |
38
+ | `list_contacts`, `import_contacts`, `update_contact` | List, import up to 100 parsed contact rows per call, and update contacts in your own workspace. Imports deduplicate, skip global DNC entries and do not send e-mail. CSV/XLSX parsing is done by the assistant. | free |
39
+ | `get_conversation`, `send_contact_message` | Read mailbox-synced contact conversations or send one e-mail after explicit approval of recipient and content. Opt-outs, bounces and DNC are blocked. | sender/provider limits |
40
+ | `list_templates`, `save_template` | Review, create and update workspace templates; content screening applies. | free |
41
+ | `list_campaigns`, `create_campaign_draft`, `launch_campaign` | Inspect campaigns, prepare drafts and audience estimates, and launch only after human review and explicit confirmation. | plan and recipient limits |
42
+ | `get_lead_context`, `get_sales_next_actions`, `schedule_follow_up`, `get_agent_activity` | Everything known about one lead (contactability, history, last reply class, next action); the queue of replies waiting for an answer and due follow-ups; set a contact's next action (never sends e-mail); the audit log of API and MCP calls. | free |
43
+ | `get_workspace_info`, `setup_website_lead_capture` | Read non-secret workspace/mailbox status or generate a public-only form key/snippet for your website. Website submissions are inquiry leads, not automatic cold-outreach consent. | free |
44
+ | `provision_account_and_get_payment_link` | For agents without a key: creates a pending account for the human owner and returns a Stripe payment link for the Business plan. The human decides and pays. | — |
45
+ | `check_provisioning_status` | After the owner paid: returns the API key exactly once (with the `claim_token`). The server starts using it immediately. | — |
46
+
47
+ Responses are compact JSON in a text block. An illustrative result for an address whose mailbox could not be checked (example.com is reserved for documentation):
48
+
49
+ ```json
50
+ {"status":"success","email":"jane.doe@example.com","validity":"valid","catch_all":null,"score":75,"reasons":["smtp_unreachable"],"disposable":false,"role_account":false}
51
+ ```
52
+
53
+ Errors come back as tool results with `isError: true`, for example `{"status":"error","error":"rate_limited","message":"…","http_status":429,"retry_after_seconds":60}`.
54
+
55
+ ## Requirements
56
+
57
+ - A Cold Leads **secret API key** (`sk_…`) from **Settings → API keys**. The API is included in the **Business** plan (10,000 verification and API credits a month; extra packs available).
58
+ - Node.js 20 or newer (CI tests 20 and 22).
59
+ - No key yet? Start the server without one and let your agent call `provision_account_and_get_payment_link` — see [Agent onboarding](#agent-onboarding-without-a-key).
60
+
61
+ ## Setup
62
+
63
+ ### Claude Desktop
64
+
65
+ `claude_desktop_config.json` (Settings → Developer → Edit Config):
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "coldleads": {
71
+ "command": "npx",
72
+ "args": ["-y", "@coldleads/mcp-server"],
73
+ "env": { "COLDLEADS_API_KEY": "sk_your_secret_key" }
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ ### Cursor / Windsurf
80
+
81
+ `~/.cursor/mcp.json` (Cursor) or `~/.codeium/windsurf/mcp_config.json` (Windsurf) — the same `mcpServers` block as above.
82
+
83
+ ### Claude Code
84
+
85
+ ```bash
86
+ claude mcp add coldleads --env COLDLEADS_API_KEY=sk_your_secret_key -- npx -y @coldleads/mcp-server
87
+ ```
88
+
89
+ ### VS Code
90
+
91
+ `.vscode/mcp.json`:
92
+
93
+ ```json
94
+ {
95
+ "servers": {
96
+ "coldleads": {
97
+ "type": "stdio",
98
+ "command": "npx",
99
+ "args": ["-y", "@coldleads/mcp-server"],
100
+ "env": { "COLDLEADS_API_KEY": "sk_your_secret_key" }
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Hosted endpoint (no install)
107
+
108
+ Clients that support remote servers can connect directly with a secret key:
109
+
110
+ ```json
111
+ {
112
+ "mcpServers": {
113
+ "coldleads": {
114
+ "url": "https://coldleads.app/api/mcp",
115
+ "headers": { "Authorization": "Bearer sk_your_secret_key" }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ The stdio server discovers workspace tools from `https://coldleads.app/api/mcp` and forwards their calls to the hosted MCP service, so the hosted and local tools use the same account, policy and sending safeguards. It also includes keyless onboarding tools. ChatGPT connects to the hosted endpoint with OAuth 2.1; enable Developer mode in ChatGPT, add the endpoint, sign in to Cold Leads and review the requested access. Other remote MCP clients can use the API-key header shown above.
122
+
123
+ ### Docker
124
+
125
+ ```bash
126
+ docker build -t coldleads-mcp-server .
127
+ docker run -i --rm -e COLDLEADS_API_KEY=sk_your_secret_key coldleads-mcp-server
128
+ ```
129
+
130
+ ## Configuration
131
+
132
+ | Variable | Default | Purpose |
133
+ | --- | --- | --- |
134
+ | `COLDLEADS_API_KEY` | — | Secret API key (`sk_…`). Optional at start-up: without it the server still starts, lists tools and can run agent onboarding. |
135
+ | `COLDLEADS_API_BASE` | `https://coldleads.app` | API base URL (for local development against a Cold Leads dev server). |
136
+
137
+ ## Agent onboarding without a key
138
+
139
+ 1. The agent calls `provision_account_and_get_payment_link` with the owner's e-mail.
140
+ 2. It shows the returned `checkout_url` to the human. The human reviews the plan and price on the secure Stripe page and decides whether to pay. The API gives the agent no way to pay, and the agent is told never to open or pay the link.
141
+ 3. The agent polls `check_provisioning_status` with `session_id` and `claim_token` every 15–30 seconds.
142
+ 4. After payment the first call returns `api_key` once; this server switches to it immediately. Store it as `COLDLEADS_API_KEY` for the next start.
143
+ 5. The owner receives an e-mail to open the Cold Leads web app, where they can manage the subscription or rotate the key.
144
+
145
+ Only a request that carries the `claim_token` can collect the key; a status lookup without it never returns the key.
146
+
147
+ ## Responsible use
148
+
149
+ - A verified address is not consent. You need a lawful basis to contact each person.
150
+ - Never e-mail leads with `do_not_contact: true` (opted out, bounced or on the do-not-contact list).
151
+ - Limits: 120 requests per minute per key; each verification costs 1 credit.
152
+
153
+ ## Development
154
+
155
+ ```bash
156
+ npm ci
157
+ npm run build
158
+ npm test # schema, server and stdio child-process tests
159
+ COLDLEADS_API_KEY_FILE=path/to/key COLDLEADS_API_BASE=http://localhost:3000 node scripts/smoke.mjs # live checks
160
+ ```
161
+
162
+ ## License
163
+
164
+ [MIT](LICENSE) © 2026 Anton Tkachenko
package/dist/api.d.ts ADDED
@@ -0,0 +1,42 @@
1
+ export type ApiOk = {
2
+ ok: true;
3
+ status: number;
4
+ data: Record<string, unknown>;
5
+ };
6
+ export type ApiFail = {
7
+ ok: false;
8
+ status: number;
9
+ error: string;
10
+ hint?: string;
11
+ data?: Record<string, unknown>;
12
+ };
13
+ export type ApiResult = ApiOk | ApiFail;
14
+ export type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
15
+ export declare const DEFAULT_BASE = "https://coldleads.app";
16
+ export declare const VERIFY_SERVER_BUDGET_MS = 4000;
17
+ export declare const VERIFY_CLIENT_TIMEOUT_MS = 4900;
18
+ export declare const DEFAULT_TIMEOUT_MS = 15000;
19
+ export declare const USER_AGENT = "coldleads-mcp-server/1.0.0";
20
+ export declare class ColdLeadsClient {
21
+ readonly baseUrl: string;
22
+ private readonly fetchImpl;
23
+ private key;
24
+ private hostedNames;
25
+ constructor(apiKey: string, baseUrl?: string, fetchImpl?: FetchLike);
26
+ get apiKey(): string;
27
+ hasKey(): boolean;
28
+ setKey(apiKey: string): void;
29
+ request(method: "GET" | "POST", path: string, body?: unknown, timeoutMs?: number, auth?: boolean, extraHeaders?: Record<string, string>): Promise<ApiResult>;
30
+ verifyEmail(email: string): Promise<ApiResult>;
31
+ searchLeads(domain: string, role: string | undefined, limit: number): Promise<ApiResult>;
32
+ provision(ownerEmail: string, agentId: string, callbackUrl?: string): Promise<ApiResult>;
33
+ provisioningStatus(sessionId: string, claimToken: string): Promise<ApiResult>;
34
+ hostedTools(): Promise<Record<string, unknown>[]>;
35
+ callHostedTool(name: string, args: unknown): Promise<{
36
+ content: {
37
+ type: "text";
38
+ text: string;
39
+ }[];
40
+ isError?: boolean;
41
+ }>;
42
+ }
package/dist/api.js ADDED
@@ -0,0 +1,96 @@
1
+ export const DEFAULT_BASE = "https://coldleads.app";
2
+ export const VERIFY_SERVER_BUDGET_MS = 4000;
3
+ export const VERIFY_CLIENT_TIMEOUT_MS = 4900;
4
+ export const DEFAULT_TIMEOUT_MS = 15_000;
5
+ export const USER_AGENT = "coldleads-mcp-server/1.0.0";
6
+ export class ColdLeadsClient {
7
+ baseUrl;
8
+ fetchImpl;
9
+ key;
10
+ hostedNames = null;
11
+ constructor(apiKey, baseUrl = DEFAULT_BASE, fetchImpl = (u, i) => fetch(u, i)) {
12
+ this.baseUrl = baseUrl;
13
+ this.fetchImpl = fetchImpl;
14
+ this.key = apiKey.trim();
15
+ }
16
+ get apiKey() {
17
+ return this.key;
18
+ }
19
+ hasKey() {
20
+ return this.key.length > 0;
21
+ }
22
+ // the key issued after the owner paid is used for the rest of this session (the agent should also persist it)
23
+ setKey(apiKey) {
24
+ this.key = apiKey.trim();
25
+ }
26
+ async request(method, path, body, timeoutMs = DEFAULT_TIMEOUT_MS, auth = true, extraHeaders = {}) {
27
+ const url = `${this.baseUrl.replace(/\/+$/, "")}${path}`;
28
+ const headers = { Accept: "application/json", "User-Agent": USER_AGENT, ...extraHeaders };
29
+ if (auth)
30
+ headers.Authorization = `Bearer ${this.key}`;
31
+ if (body !== undefined)
32
+ headers["Content-Type"] = "application/json";
33
+ let res;
34
+ try {
35
+ res = await this.fetchImpl(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(timeoutMs), redirect: "error" });
36
+ }
37
+ catch (e) {
38
+ const name = e instanceof Error ? e.name : "";
39
+ if (name === "TimeoutError" || name === "AbortError")
40
+ return { ok: false, status: 0, error: "timeout", hint: `no answer from Cold Leads within ${timeoutMs} ms` };
41
+ return { ok: false, status: 0, error: "network_error", hint: e instanceof Error ? e.message : String(e) };
42
+ }
43
+ let data = {};
44
+ try {
45
+ data = (await res.json());
46
+ }
47
+ catch {
48
+ data = {};
49
+ }
50
+ if (res.ok)
51
+ return { ok: true, status: res.status, data };
52
+ return { ok: false, status: res.status, error: typeof data.error === "string" ? data.error : `http_${res.status}`, hint: typeof data.hint === "string" ? data.hint : undefined, data };
53
+ }
54
+ verifyEmail(email) {
55
+ return this.request("POST", "/api/v1/verify", { email, timeout_ms: VERIFY_SERVER_BUDGET_MS }, VERIFY_CLIENT_TIMEOUT_MS);
56
+ }
57
+ searchLeads(domain, role, limit) {
58
+ const q = new URLSearchParams({ domain, limit: String(limit) });
59
+ if (role)
60
+ q.set("role", role);
61
+ return this.request("GET", `/api/v1/leads?${q.toString()}`);
62
+ }
63
+ // agent onboarding without a key (no Authorization header)
64
+ provision(ownerEmail, agentId, callbackUrl) {
65
+ return this.request("POST", "/api/agent/provision", { owner_email: ownerEmail, agent_id: agentId, ...(callbackUrl ? { callback_url: callbackUrl } : {}) }, DEFAULT_TIMEOUT_MS, false);
66
+ }
67
+ provisioningStatus(sessionId, claimToken) {
68
+ return this.request("GET", `/api/agent/status?session_id=${encodeURIComponent(sessionId)}`, undefined, DEFAULT_TIMEOUT_MS, false, { "X-Claim-Token": claimToken });
69
+ }
70
+ async hostedTools() {
71
+ const r = await this.request("POST", "/api/mcp", { jsonrpc: "2.0", id: 1, method: "tools/list" }, DEFAULT_TIMEOUT_MS, this.hasKey());
72
+ if (!r.ok) {
73
+ this.hostedNames = new Set();
74
+ return [];
75
+ }
76
+ const result = r.data.result;
77
+ const tools = Array.isArray(result?.tools) ? result.tools : [];
78
+ this.hostedNames = new Set(tools.map((t) => String(t.name ?? "")));
79
+ return tools;
80
+ }
81
+ async callHostedTool(name, args) {
82
+ if (!this.hasKey())
83
+ return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: "missing_api_key", message: "Set COLDLEADS_API_KEY to a secret key (sk_…) to use workspace tools." }) }], isError: true };
84
+ if (!this.hostedNames && !(await this.hostedTools()).length)
85
+ return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: "unknown_tool", message: `Unknown Cold Leads tool: ${name}` }) }], isError: true };
86
+ if (!this.hostedNames?.has(name))
87
+ return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: "unknown_tool", message: `Unknown Cold Leads tool: ${name}` }) }], isError: true };
88
+ const r = await this.request("POST", "/api/mcp", { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }, DEFAULT_TIMEOUT_MS);
89
+ if (!r.ok)
90
+ return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: r.error, message: r.hint ?? r.error, http_status: r.status }) }], isError: true };
91
+ const rpc = r.data.result;
92
+ if (rpc?.content?.length)
93
+ return { content: rpc.content, ...(rpc.isError ? { isError: true } : {}) };
94
+ return { content: [{ type: "text", text: JSON.stringify({ status: "error", error: "invalid_mcp_response" }) }], isError: true };
95
+ }
96
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { ColdLeadsClient, DEFAULT_BASE } from "./api.js";
4
+ import { createServer } from "./server.js";
5
+ // Stdio entry point: `npx -y @coldleads/mcp-server` with COLDLEADS_API_KEY in the environment.
6
+ // Starts and lists tools without a key (so clients and registries can inspect it); calls need the key.
7
+ // Logs go to stderr only — stdout carries JSON-RPC.
8
+ const key = process.env.COLDLEADS_API_KEY ?? "";
9
+ const base = process.env.COLDLEADS_API_BASE || DEFAULT_BASE;
10
+ const server = createServer(new ColdLeadsClient(key, base));
11
+ await server.connect(new StdioServerTransport());
12
+ process.stderr.write(`coldleads MCP server ready (api ${base}, key ${key ? "set" : "missing"})\n`);
@@ -0,0 +1,17 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { ColdLeadsClient } from "./api.js";
3
+ export declare const SERVER_INFO: {
4
+ name: string;
5
+ title: string;
6
+ version: string;
7
+ };
8
+ type ToolResult = {
9
+ content: {
10
+ type: "text";
11
+ text: string;
12
+ }[];
13
+ isError?: boolean;
14
+ };
15
+ export declare function callTool(client: ColdLeadsClient, name: string, rawArgs: unknown, clientName?: string): Promise<ToolResult | null>;
16
+ export declare function createServer(client: ColdLeadsClient): Server;
17
+ export {};
package/dist/server.js ADDED
@@ -0,0 +1,137 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
3
+ import { LEADS_DEFAULT, LEADS_MAX, TOOLS } from "./tools.js";
4
+ export const SERVER_INFO = { name: "coldleads", title: "Cold Leads", version: "1.0.0" };
5
+ const INSTRUCTIONS = "Cold Leads tools: search and verify leads; import and manage workspace contacts; read conversations; send one message only after explicit user confirmation; manage templates and campaign drafts; launch campaigns only after human review and confirmation; check non-secret workspace settings; and set up website lead forms. Only authenticated workspace data is accessible. Never e-mail opted-out, bounced or do-not-contact addresses. Verification is not consent; the user needs a lawful basis to contact each person. Website forms create inquiry contacts and do not establish cold-outreach consent. Without an API key, provision_account_and_get_payment_link creates a payment link for the human owner to approve; check_provisioning_status then returns the key once.";
6
+ const text = (obj, isError = false) => ({ content: [{ type: "text", text: JSON.stringify(obj) }], ...(isError ? { isError: true } : {}) });
7
+ const toolError = (error, message, extra = {}) => text({ status: "error", error, message, ...extra }, true);
8
+ const MESSAGES = {
9
+ missing_api_key: "COLDLEADS_API_KEY is not set. Create a secret key (sk_…) in Cold Leads → Settings → API keys (Business plan) and add it to the MCP server environment.",
10
+ bad_api_key: "The Cold Leads API key was rejected. Check COLDLEADS_API_KEY (a secret key starting with sk_).",
11
+ publishable_key_not_allowed: "A publishable key (lk_…) cannot call the API; use a secret key (sk_…).",
12
+ api_not_in_plan: "This Cold Leads plan does not include the API (Business plan required).",
13
+ account_suspended: "The Cold Leads account is suspended; contact support@coldleads.app.",
14
+ no_credits: "No verification credits left this month; the account owner can add credits in Cold Leads settings.",
15
+ rate_limited: "Rate limit reached (120 requests per minute per key). Wait a minute and retry.",
16
+ timeout: "Cold Leads did not answer in time. Retry later.",
17
+ account_exists: "This e-mail already has a Cold Leads workspace. Ask the owner to create a secret API key in Settings → API keys (Business plan) and set it as COLDLEADS_API_KEY.",
18
+ not_found: "No provisioning found for this session_id.",
19
+ invalid_owner_email: "owner_email must be the human owner's e-mail address.",
20
+ invalid_callback_url: "callback_url must be an https URL on a public host.",
21
+ not_configured: "Payments are not configured on this Cold Leads server.",
22
+ gateway: "The payment provider could not create a checkout; retry later.",
23
+ network_error: "Cold Leads could not be reached.",
24
+ };
25
+ function fromApi(f) {
26
+ const extra = { http_status: f.status };
27
+ if (f.hint)
28
+ extra.hint = f.hint;
29
+ if (f.error === "rate_limited")
30
+ extra.retry_after_seconds = 60;
31
+ return toolError(f.error, MESSAGES[f.error] ?? `Cold Leads API error (${f.error}).`, extra);
32
+ }
33
+ function objectArgs(a) {
34
+ return a && typeof a === "object" && !Array.isArray(a) ? a : null;
35
+ }
36
+ function unknownKeys(a, allowed) {
37
+ return Object.keys(a).filter((k) => !allowed.includes(k));
38
+ }
39
+ const KNOWN = ["verify_email", "search_leads", "provision_account_and_get_payment_link", "check_provisioning_status"];
40
+ export async function callTool(client, name, rawArgs, clientName = "client") {
41
+ if (!KNOWN.includes(name))
42
+ return client.callHostedTool(name, rawArgs ?? {});
43
+ const args = objectArgs(rawArgs ?? {});
44
+ if (!args)
45
+ return toolError("invalid_arguments", "arguments must be an object");
46
+ if (name === "provision_account_and_get_payment_link") {
47
+ const extra = unknownKeys(args, ["owner_email", "agent_id", "callback_url"]);
48
+ if (extra.length)
49
+ return toolError("invalid_arguments", `unknown argument(s): ${extra.join(", ")}`);
50
+ if (typeof args.owner_email !== "string" || args.owner_email.trim().length < 3)
51
+ return toolError("invalid_arguments", "owner_email is required");
52
+ if (args.agent_id !== undefined && (typeof args.agent_id !== "string" || !args.agent_id.trim() || args.agent_id.length > 100))
53
+ return toolError("invalid_arguments", "agent_id must be a non-empty string of up to 100 characters");
54
+ if (args.callback_url !== undefined && (typeof args.callback_url !== "string" || !args.callback_url.startsWith("https://")))
55
+ return toolError("invalid_arguments", "callback_url must start with https://");
56
+ const agentId = args.agent_id?.trim() || `mcp:${clientName}`.slice(0, 100);
57
+ const r = await client.provision(args.owner_email.trim(), agentId, args.callback_url);
58
+ if (!r.ok)
59
+ return fromApi(r);
60
+ return text(r.data);
61
+ }
62
+ if (name === "check_provisioning_status") {
63
+ const extra = unknownKeys(args, ["session_id", "claim_token"]);
64
+ if (extra.length)
65
+ return toolError("invalid_arguments", `unknown argument(s): ${extra.join(", ")}`);
66
+ if (typeof args.session_id !== "string" || typeof args.claim_token !== "string" || args.claim_token.length < 10)
67
+ return toolError("invalid_arguments", "session_id and claim_token are required");
68
+ const r = await client.provisioningStatus(args.session_id, args.claim_token);
69
+ if (!r.ok)
70
+ return fromApi(r);
71
+ const data = { ...r.data };
72
+ if (typeof data.api_key === "string" && data.api_key.startsWith("sk_")) {
73
+ client.setKey(data.api_key);
74
+ data.session_key_updated = true;
75
+ data.message = `${String(data.message ?? "")} This MCP server now uses the new key for the rest of the session; also set COLDLEADS_API_KEY so it is used after a restart.`.trim();
76
+ }
77
+ return text(data);
78
+ }
79
+ if (name === "verify_email") {
80
+ const extra = unknownKeys(args, ["email"]);
81
+ if (extra.length)
82
+ return toolError("invalid_arguments", `unknown argument(s): ${extra.join(", ")}`);
83
+ const email = typeof args.email === "string" ? args.email.trim() : "";
84
+ if (email.length < 3 || email.length > 254)
85
+ return toolError("invalid_arguments", "email is required (3–254 characters)");
86
+ if (!client.hasKey())
87
+ return toolError("missing_api_key", MESSAGES.missing_api_key);
88
+ const r = await client.verifyEmail(email);
89
+ if (!r.ok)
90
+ return fromApi(r);
91
+ const d = r.data;
92
+ const reasons = Array.isArray(d.reasons) ? d.reasons : [];
93
+ return text({
94
+ status: "success",
95
+ email: d.email,
96
+ validity: d.status,
97
+ // catch_all is known only when the SMTP probe got an answer (ok / catch_all / mailbox_missing)
98
+ catch_all: reasons.some((x) => x === "ok" || x === "catch_all" || x === "mailbox_missing") ? d.catch_all === true : null,
99
+ score: d.score,
100
+ reasons,
101
+ disposable: d.disposable === true,
102
+ role_account: d.role === true,
103
+ });
104
+ }
105
+ const extra = unknownKeys(args, ["domain", "role", "limit"]);
106
+ if (extra.length)
107
+ return toolError("invalid_arguments", `unknown argument(s): ${extra.join(", ")}`);
108
+ const domain = typeof args.domain === "string" ? args.domain.trim() : "";
109
+ if (domain.length < 3 || domain.length > 253)
110
+ return toolError("invalid_arguments", "domain is required, e.g. example.com");
111
+ if (args.role !== undefined && (typeof args.role !== "string" || args.role.length > 80))
112
+ return toolError("invalid_arguments", "role must be a string of up to 80 characters");
113
+ if (args.limit !== undefined && (!Number.isInteger(args.limit) || args.limit < 1 || args.limit > LEADS_MAX)) {
114
+ return toolError("invalid_arguments", `limit must be an integer between 1 and ${LEADS_MAX}`);
115
+ }
116
+ if (!client.hasKey())
117
+ return toolError("missing_api_key", MESSAGES.missing_api_key);
118
+ const r = await client.searchLeads(domain, args.role, args.limit ?? LEADS_DEFAULT);
119
+ if (!r.ok)
120
+ return r.error === "domain_required" ? toolError("invalid_arguments", "domain is not a valid company domain, e.g. example.com") : fromApi(r);
121
+ return text({ status: "success", domain: r.data.domain, count: r.data.count, source: r.data.source, leads: r.data.leads });
122
+ }
123
+ export function createServer(client) {
124
+ const server = new Server(SERVER_INFO, { capabilities: { tools: { listChanged: false } }, instructions: INSTRUCTIONS });
125
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
126
+ const hosted = client.hasKey() ? await client.hostedTools() : [];
127
+ const onboarding = TOOLS.filter((t) => t.name === "provision_account_and_get_payment_link" || t.name === "check_provisioning_status");
128
+ return { tools: (hosted.length ? [...hosted, ...onboarding] : TOOLS) };
129
+ });
130
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
131
+ const result = await callTool(client, req.params.name, req.params.arguments, server.getClientVersion()?.name ?? "client");
132
+ if (!result)
133
+ return toolError("unknown_tool", `Unknown tool: ${req.params.name}`);
134
+ return result;
135
+ });
136
+ return server;
137
+ }
@@ -0,0 +1,125 @@
1
+ export declare const LEADS_MAX = 50;
2
+ export declare const LEADS_DEFAULT = 10;
3
+ export declare const TOOLS: readonly [{
4
+ readonly name: "search_leads";
5
+ readonly title: "Search leads by company domain";
6
+ readonly description: "Search the leads already in your Cold Leads CRM for a target company domain: contact e-mail, name, company, phone, stage, tags, verification status and a do_not_contact flag. Covers only contacts in your own workspace (Cold Leads has no third-party lead database). Free, uses no credits.";
7
+ readonly inputSchema: {
8
+ readonly type: "object";
9
+ readonly properties: {
10
+ readonly domain: {
11
+ readonly type: "string";
12
+ readonly minLength: 3;
13
+ readonly maxLength: 253;
14
+ readonly description: "Company domain, e.g. example.com. A URL or an e-mail address is reduced to its domain.";
15
+ };
16
+ readonly role: {
17
+ readonly type: "string";
18
+ readonly maxLength: 80;
19
+ readonly description: "Optional keyword such as sales, ceo or marketing, matched against the contact's name, e-mail local part, tags, notes and type.";
20
+ };
21
+ readonly limit: {
22
+ readonly type: "integer";
23
+ readonly minimum: 1;
24
+ readonly maximum: 50;
25
+ readonly default: 10;
26
+ readonly description: "Maximum number of leads to return (1–50, default 10).";
27
+ };
28
+ };
29
+ readonly required: readonly ["domain"];
30
+ readonly additionalProperties: false;
31
+ };
32
+ readonly annotations: {
33
+ readonly readOnlyHint: true;
34
+ readonly destructiveHint: false;
35
+ readonly idempotentHint: true;
36
+ readonly openWorldHint: false;
37
+ };
38
+ }, {
39
+ readonly name: "verify_email";
40
+ readonly title: "Verify an e-mail address";
41
+ readonly description: "Verifies an e-mail address: syntax, disposable domain, role account and MX records, plus an SMTP mailbox and catch-all check when Cold Leads can open an SMTP connection to the recipient's mail server (otherwise reasons contains smtp_unreachable and catch_all is null). Returns validity (valid, risky or invalid), a score and reason codes. Answers within about 5 seconds; costs 1 credit.";
42
+ readonly inputSchema: {
43
+ readonly type: "object";
44
+ readonly properties: {
45
+ readonly email: {
46
+ readonly type: "string";
47
+ readonly minLength: 3;
48
+ readonly maxLength: 254;
49
+ readonly description: "The e-mail address to verify, e.g. anna@example.com.";
50
+ };
51
+ };
52
+ readonly required: readonly ["email"];
53
+ readonly additionalProperties: false;
54
+ };
55
+ readonly annotations: {
56
+ readonly readOnlyHint: true;
57
+ readonly destructiveHint: false;
58
+ readonly idempotentHint: true;
59
+ readonly openWorldHint: true;
60
+ };
61
+ }, {
62
+ readonly name: "provision_account_and_get_payment_link";
63
+ readonly title: "Set up Cold Leads for the human owner";
64
+ readonly description: "Use only when no working Cold Leads API key is available (other Cold Leads tools return missing_api_key or bad_api_key) and the human wants to use Cold Leads. Creates a pending Cold Leads account for the human owner's e-mail and returns a Stripe payment link for the Business plan with API access (price in plan.price, billed monthly until cancelled). Show checkout_url to the human and let them decide; never open, pay or share the link yourself. Keep claim_token secret, then call check_provisioning_status until status is active to receive the API key once.";
65
+ readonly inputSchema: {
66
+ readonly type: "object";
67
+ readonly properties: {
68
+ readonly owner_email: {
69
+ readonly type: "string";
70
+ readonly minLength: 3;
71
+ readonly maxLength: 254;
72
+ readonly description: "E-mail address of the human who will own and pay for the Cold Leads account.";
73
+ };
74
+ readonly agent_id: {
75
+ readonly type: "string";
76
+ readonly minLength: 1;
77
+ readonly maxLength: 100;
78
+ readonly description: "Name of your agent or product; shown to the human on the payment page.";
79
+ };
80
+ readonly callback_url: {
81
+ readonly type: "string";
82
+ readonly maxLength: 500;
83
+ readonly pattern: "^https://";
84
+ readonly description: "Optional https URL that receives a signed POST when the owner has paid (no secrets in the payload).";
85
+ };
86
+ };
87
+ readonly required: readonly ["owner_email"];
88
+ readonly additionalProperties: false;
89
+ };
90
+ readonly annotations: {
91
+ readonly readOnlyHint: false;
92
+ readonly destructiveHint: false;
93
+ readonly idempotentHint: false;
94
+ readonly openWorldHint: true;
95
+ };
96
+ }, {
97
+ readonly name: "check_provisioning_status";
98
+ readonly title: "Check the owner's payment and get the API key";
99
+ readonly description: "Checks an account created with provision_account_and_get_payment_link. Returns status pending_payment, active, expired or suspended. When active, the first call with the claim_token returns api_key exactly once: store it securely as COLDLEADS_API_KEY and send it as Authorization: Bearer <key>. Poll every 15–30 seconds while pending_payment.";
100
+ readonly inputSchema: {
101
+ readonly type: "object";
102
+ readonly properties: {
103
+ readonly session_id: {
104
+ readonly type: "string";
105
+ readonly minLength: 3;
106
+ readonly maxLength: 200;
107
+ readonly description: "session_id returned by provision_account_and_get_payment_link.";
108
+ };
109
+ readonly claim_token: {
110
+ readonly type: "string";
111
+ readonly minLength: 10;
112
+ readonly maxLength: 200;
113
+ readonly description: "claim_token returned by provision_account_and_get_payment_link.";
114
+ };
115
+ };
116
+ readonly required: readonly ["session_id", "claim_token"];
117
+ readonly additionalProperties: false;
118
+ };
119
+ readonly annotations: {
120
+ readonly readOnlyHint: false;
121
+ readonly destructiveHint: false;
122
+ readonly idempotentHint: false;
123
+ readonly openWorldHint: true;
124
+ };
125
+ }];
package/dist/tools.js ADDED
@@ -0,0 +1,67 @@
1
+ // Tool definitions of the stdio server (JSON Schema draft-07). Workspace tools are loaded from hosted /api/mcp;
2
+ // onboarding remains local because it can run before the account has an API key.
3
+ export const LEADS_MAX = 50;
4
+ export const LEADS_DEFAULT = 10;
5
+ export const TOOLS = [
6
+ {
7
+ name: "search_leads",
8
+ title: "Search leads by company domain",
9
+ description: "Search the leads already in your Cold Leads CRM for a target company domain: contact e-mail, name, company, phone, stage, tags, verification status and a do_not_contact flag. Covers only contacts in your own workspace (Cold Leads has no third-party lead database). Free, uses no credits.",
10
+ inputSchema: {
11
+ type: "object",
12
+ properties: {
13
+ domain: { type: "string", minLength: 3, maxLength: 253, description: "Company domain, e.g. example.com. A URL or an e-mail address is reduced to its domain." },
14
+ role: { type: "string", maxLength: 80, description: "Optional keyword such as sales, ceo or marketing, matched against the contact's name, e-mail local part, tags, notes and type." },
15
+ limit: { type: "integer", minimum: 1, maximum: LEADS_MAX, default: LEADS_DEFAULT, description: `Maximum number of leads to return (1–${LEADS_MAX}, default ${LEADS_DEFAULT}).` },
16
+ },
17
+ required: ["domain"],
18
+ additionalProperties: false,
19
+ },
20
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
21
+ },
22
+ {
23
+ name: "verify_email",
24
+ title: "Verify an e-mail address",
25
+ description: "Verifies an e-mail address: syntax, disposable domain, role account and MX records, plus an SMTP mailbox and catch-all check when Cold Leads can open an SMTP connection to the recipient's mail server (otherwise reasons contains smtp_unreachable and catch_all is null). Returns validity (valid, risky or invalid), a score and reason codes. Answers within about 5 seconds; costs 1 credit.",
26
+ inputSchema: {
27
+ type: "object",
28
+ properties: {
29
+ email: { type: "string", minLength: 3, maxLength: 254, description: "The e-mail address to verify, e.g. anna@example.com." },
30
+ },
31
+ required: ["email"],
32
+ additionalProperties: false,
33
+ },
34
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
35
+ },
36
+ {
37
+ name: "provision_account_and_get_payment_link",
38
+ title: "Set up Cold Leads for the human owner",
39
+ description: "Use only when no working Cold Leads API key is available (other Cold Leads tools return missing_api_key or bad_api_key) and the human wants to use Cold Leads. Creates a pending Cold Leads account for the human owner's e-mail and returns a Stripe payment link for the Business plan with API access (price in plan.price, billed monthly until cancelled). Show checkout_url to the human and let them decide; never open, pay or share the link yourself. Keep claim_token secret, then call check_provisioning_status until status is active to receive the API key once.",
40
+ inputSchema: {
41
+ type: "object",
42
+ properties: {
43
+ owner_email: { type: "string", minLength: 3, maxLength: 254, description: "E-mail address of the human who will own and pay for the Cold Leads account." },
44
+ agent_id: { type: "string", minLength: 1, maxLength: 100, description: "Name of your agent or product; shown to the human on the payment page." },
45
+ callback_url: { type: "string", maxLength: 500, pattern: "^https://", description: "Optional https URL that receives a signed POST when the owner has paid (no secrets in the payload)." },
46
+ },
47
+ required: ["owner_email"],
48
+ additionalProperties: false,
49
+ },
50
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
51
+ },
52
+ {
53
+ name: "check_provisioning_status",
54
+ title: "Check the owner's payment and get the API key",
55
+ description: "Checks an account created with provision_account_and_get_payment_link. Returns status pending_payment, active, expired or suspended. When active, the first call with the claim_token returns api_key exactly once: store it securely as COLDLEADS_API_KEY and send it as Authorization: Bearer <key>. Poll every 15–30 seconds while pending_payment.",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ session_id: { type: "string", minLength: 3, maxLength: 200, description: "session_id returned by provision_account_and_get_payment_link." },
60
+ claim_token: { type: "string", minLength: 10, maxLength: 200, description: "claim_token returned by provision_account_and_get_payment_link." },
61
+ },
62
+ required: ["session_id", "claim_token"],
63
+ additionalProperties: false,
64
+ },
65
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
66
+ },
67
+ ];
package/package.json CHANGED
@@ -1,6 +1,59 @@
1
1
  {
2
2
  "name": "@coldleads/mcp-server",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Model Context Protocol server for Cold Leads (coldleads.app), the B2B outreach CRM: contacts, imports, e-mail verification, templates, campaign drafts, conversations, reply queue and follow-ups in your own workspace; sending needs explicit approval. Includes keyless agent onboarding.",
5
+ "mcpName": "io.github.anttka4cz/mcp-server-coldleads",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-server-coldleads": "dist/index.js"
9
+ },
10
+ "main": "dist/server.js",
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json && chmod +x dist/index.js",
21
+ "typecheck": "tsc -p tsconfig.json --noEmit",
22
+ "test": "vitest run",
23
+ "prepublishOnly": "npm run build && npm test",
24
+ "prepare": "npm run build"
25
+ },
26
+ "keywords": [
27
+ "mcp",
28
+ "model-context-protocol",
29
+ "email-verification",
30
+ "b2b",
31
+ "leads",
32
+ "crm",
33
+ "coldleads",
34
+ "sales",
35
+ "ai-agents"
36
+ ],
37
+ "homepage": "https://coldleads.app/developers?utm_source=npm&utm_medium=readme&utm_campaign=mcp-server",
38
+ "license": "MIT",
39
+ "author": "Anton Tkachenko",
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.31.0"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^22.10.0",
45
+ "ajv": "^8.17.1",
46
+ "typescript": "^5.9.3",
47
+ "vitest": "^3.2.4"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/anttka4cz/mcp-server-coldleads.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/anttka4cz/mcp-server-coldleads/issues"
55
+ },
56
+ "publishConfig": {
57
+ "access": "public"
58
+ }
59
+ }