Build Your Own MCP Server: Complete Guide

By Brent Dunn Jan 24, 2026 18 min read

Build Your First AI Project This Weekend

Stop consuming tutorials. Start creating. Get the free step-by-step guide.

Stop consuming tutorials. Start creating. Get the free step-by-step guide.

You’ve got Claude Code running. You’ve built a website or two. Now you want it to do more.

The problem: Claude can’t see your CRM, your analytics, or that API your business runs on. You’re stuck copy-pasting data between tools like it’s 2015.

MCP servers fix this. They let Claude call external APIs directly.

I built my first MCP server in about two hours, with zero backend experience. It connected Claude to Hunter.io for email prospecting. Now when I need contact info for outreach, I ask Claude. One prompt, data delivered.

This guide walks you through building that exact server. You’ll end up with three working tools: domain search, email finder, and email verification. More importantly, you’ll understand the pattern well enough to connect Claude to any API your business needs.

That’s the real advantage. Custom integrations your competitors can’t access because they’re waiting for someone else to build them.


Quick navigation

SectionWhat you’ll learn
What is an MCP serverThe basics in plain English
Project setupGet your environment ready
Building the serverComplete walkthrough with code
Testing your serverVerify it works
Installing in Claude CodeConnect it to Claude
Using Claude to build MCP serversLet AI do the heavy lifting
Marketing use casesPractical applications

What is an MCP server

MCP (Model Context Protocol) is how you give Claude access to external tools and data.

Without MCP, Claude Code can only see your codebase and whatever you paste into the chat. With MCP, Claude can call APIs, query databases, and pull data from anywhere.

An MCP server is a small program you run locally. It:

  1. Exposes tools (functions Claude can call)
  2. Provides resources (data Claude can read)
  3. Speaks a standard protocol Claude understands

Once you connect the Hunter.io MCP server, you can ask Claude things like:

  • “Find email addresses for decision makers at stripe.com”
  • “Verify these 50 emails and tell me which ones will bounce”
  • “Find the email for Sarah Jones, CMO at Acme Corp”

One prompt. Data delivered. No tab switching, no manual lookups.

MCP server components

ComponentWhat it doesExample
ToolsFunctions Claude can executedomain_search(domain), verify_email(email)
ResourcesData Claude can readDatabase records, API responses
PromptsPre-built templates“Analyze this data and summarize”

For most marketing use cases, you’ll focus on tools. That’s what we’ll build.


Why Hunter.io for this example

I picked Hunter.io because it solves a real problem: finding contact info for outreach.

If you’re doing cold email, affiliate outreach, or partnership development, you need emails. Hunter.io finds them. The free tier gives you 25 searches per month, enough to test this integration.

The tools we’ll build:

  1. domain_search - Find all email addresses at a company
  2. email_finder - Find a specific person’s email
  3. email_verifier - Check if an email will bounce

After this, when you need contact info, you’ll ask Claude instead of logging into another dashboard.

Before you start: Sign up at hunter.io and grab your free API key from the dashboard. You’ll need it.


Project setup

We’ll use TypeScript. It has the best MCP SDK support and most production servers use it.

If you prefer Python, the official MCP SDK supports it. Same concepts, different syntax. But I recommend following along with TypeScript first since that’s what this guide covers.

Prerequisites

  • Node.js 18+ installed
  • A code editor (VS Code recommended)
  • Hunter.io API key (free tier works)

Create the project

# Create project directory
mkdir hunter-mcp
cd hunter-mcp

# Initialize npm project
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node

# Create source directory
mkdir src

Configure TypeScript (tsconfig.json)

Create tsconfig.json in your project root:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Update package.json

Add these fields:

{
  "type": "module",
  "bin": {
    "hunter-mcp": "./build/index.js"
  },
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  }
}

Building the server step by step

Now let’s write the actual server. I’ll break this into sections so you understand what each part does. If you want to skip ahead, the complete code is in the Complete code section.

Step 1: Imports and setup

Create src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Hunter.io API configuration
const HUNTER_API_BASE = "https://api.hunter.io/v2";
const API_KEY = process.env.HUNTER_API_KEY;

// Validate API key exists
if (!API_KEY) {
  console.error("HUNTER_API_KEY environment variable is required");
  console.error("Get your free API key at https://hunter.io");
  process.exit(1);
}

// Create server instance
const server = new McpServer({
  name: "hunter",
  version: "1.0.0",
});

What’s happening here:

  • McpServer is the main class from the MCP SDK
  • StdioServerTransport handles communication via standard input/output
  • z (Zod) is used for input validation and schema generation
  • We read the API key from environment variables (secure, not hardcoded)

Step 2: Helper function for API requests

Add this below your imports:

// Helper function for Hunter API requests
async function hunterRequest<T>(endpoint: string, params: Record<string, string> = {}): Promise<T | null> {
  const url = new URL(`${HUNTER_API_BASE}${endpoint}`);
  url.searchParams.set("api_key", API_KEY!);

  // Add any additional params
  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, value);
  }

  try {
    const response = await fetch(url.toString());

    if (!response.ok) {
      const error = await response.json();
      console.error(`Hunter API error: ${error.errors?.[0]?.details || response.statusText}`);
      return null;
    }

    const result = await response.json();
    return result.data as T;
  } catch (error) {
    console.error("Request failed:", error);
    return null;
  }
}

Critical: Never use console.log() in STDIO servers. It writes to stdout and corrupts the MCP protocol. Use console.error() for debugging instead (it writes to stderr). This trips up more people than you’d expect.

Step 3: Define response types

Add TypeScript interfaces for the API responses:

// Type definitions for Hunter.io API responses
interface DomainSearchResult {
  domain: string;
  disposable: boolean;
  webmail: boolean;
  accept_all: boolean;
  pattern: string | null;
  organization: string | null;
  emails: Array<{
    value: string;
    type: string;
    confidence: number;
    first_name: string | null;
    last_name: string | null;
    position: string | null;
    department: string | null;
    linkedin: string | null;
    twitter: string | null;
    phone_number: string | null;
  }>;
}

interface EmailFinderResult {
  first_name: string;
  last_name: string;
  email: string;
  score: number;
  domain: string;
  position: string | null;
  twitter: string | null;
  linkedin_url: string | null;
  company: string | null;
}

interface EmailVerifierResult {
  email: string;
  status: "valid" | "invalid" | "accept_all" | "webmail" | "disposable" | "unknown";
  result: "deliverable" | "undeliverable" | "risky" | "unknown";
  score: number;
  regexp: boolean;
  gibberish: boolean;
  disposable: boolean;
  webmail: boolean;
  mx_records: boolean;
  smtp_server: boolean;
  smtp_check: boolean;
  accept_all: boolean;
  block: boolean;
}

Step 4: Register the domain search tool

Now add the first tool. This finds all emails at a company:

// Tool 1: Domain Search - Find emails at a company
server.tool(
  "domain_search",
  "Find email addresses associated with a company domain. Returns names, positions, and confidence scores.",
  {
    domain: z.string().describe("Company domain to search (e.g., 'stripe.com')"),
    limit: z.number().optional().default(10).describe("Max results to return (default 10, max 100)")
  },
  async ({ domain, limit }) => {
    const data = await hunterRequest<DomainSearchResult>("/domain-search", {
      domain,
      limit: String(Math.min(limit, 100))
    });

    if (!data) {
      return {
        content: [{ type: "text", text: `Could not search domain: ${domain}. Check if the domain is valid.` }]
      };
    }

    if (data.emails.length === 0) {
      return {
        content: [{ type: "text", text: `No emails found for ${domain}. This could mean:\n- The domain has no public emails\n- Hunter hasn't indexed this domain yet` }]
      };
    }

    // Format the results
    const emailList = data.emails.map(email => {
      const name = [email.first_name, email.last_name].filter(Boolean).join(" ") || "Unknown";
      const role = email.position || email.department || "Unknown role";
      const confidence = email.confidence;

      let extras = [];
      if (email.linkedin) extras.push(`LinkedIn: ${email.linkedin}`);
      if (email.phone_number) extras.push(`Phone: ${email.phone_number}`);

      return `- ${email.value}
  Name: ${name}
  Role: ${role}
  Confidence: ${confidence}%${extras.length ? "\n  " + extras.join("\n  ") : ""}`;
    }).join("\n\n");

    const summary = `
Domain: ${data.domain}
Organization: ${data.organization || "Unknown"}
Email Pattern: ${data.pattern || "Unknown"}
Accept-All: ${data.accept_all ? "Yes (can't verify individual emails)" : "No"}

Found ${data.emails.length} emails:

${emailList}
    `.trim();

    return {
      content: [{ type: "text", text: summary }]
    };
  }
);

Breaking this down:

  • server.tool() registers a new tool with Claude
  • First argument: tool name (what Claude calls)
  • Second argument: description (helps Claude know when to use it)
  • Third argument: input schema using Zod (validates inputs)
  • Fourth argument: the actual function that runs

Step 5: Register the email finder tool

This finds a specific person’s email:

// Tool 2: Email Finder - Find a specific person's email
server.tool(
  "email_finder",
  "Find the email address of a specific person at a company. Provide their name and company domain.",
  {
    domain: z.string().describe("Company domain (e.g., 'stripe.com')"),
    first_name: z.string().describe("Person's first name"),
    last_name: z.string().describe("Person's last name")
  },
  async ({ domain, first_name, last_name }) => {
    const data = await hunterRequest<EmailFinderResult>("/email-finder", {
      domain,
      first_name,
      last_name
    });

    if (!data || !data.email) {
      return {
        content: [{
          type: "text",
          text: `Could not find email for ${first_name} ${last_name} at ${domain}.

Possible reasons:
- Person doesn't exist at this company
- Email pattern not found
- Name spelling might be different

Try:
- Check LinkedIn for correct spelling
- Try domain_search to see all emails at ${domain}`
        }]
      };
    }

    let extras = [];
    if (data.position) extras.push(`Position: ${data.position}`);
    if (data.linkedin_url) extras.push(`LinkedIn: ${data.linkedin_url}`);
    if (data.twitter) extras.push(`Twitter: @${data.twitter}`);

    const result = `
Found email for ${data.first_name} ${data.last_name}:

Email: ${data.email}
Confidence: ${data.score}%
Domain: ${data.domain}
${extras.length ? "\n" + extras.join("\n") : ""}

Note: ${data.score >= 90 ? "High confidence - likely accurate" : data.score >= 70 ? "Medium confidence - probably accurate" : "Low confidence - verify before using"}
    `.trim();

    return {
      content: [{ type: "text", text: result }]
    };
  }
);

Step 6: Register the email verifier tool

This checks if an email is deliverable:

// Tool 3: Email Verifier - Check if an email is valid
server.tool(
  "email_verifier",
  "Verify if an email address is valid and deliverable. Returns detailed verification results.",
  {
    email: z.string().email().describe("Email address to verify")
  },
  async ({ email }) => {
    const data = await hunterRequest<EmailVerifierResult>("/email-verifier", { email });

    if (!data) {
      return {
        content: [{ type: "text", text: `Could not verify email: ${email}. Try again later.` }]
      };
    }

    // Build a clear status message
    let statusMessage: string;
    switch (data.result) {
      case "deliverable":
        statusMessage = "VALID - This email should receive messages";
        break;
      case "undeliverable":
        statusMessage = "INVALID - This email will bounce";
        break;
      case "risky":
        statusMessage = "RISKY - May or may not deliver (accept-all server or catch-all)";
        break;
      default:
        statusMessage = "UNKNOWN - Could not determine deliverability";
    }

    // Build details
    const checks = [
      `Format Valid: ${data.regexp ? "Yes" : "No"}`,
      `MX Records: ${data.mx_records ? "Found" : "Missing"}`,
      `SMTP Server: ${data.smtp_server ? "Responding" : "Not responding"}`,
      `SMTP Check: ${data.smtp_check ? "Passed" : "Failed"}`,
      `Gibberish: ${data.gibberish ? "Yes (suspicious)" : "No"}`,
      `Disposable: ${data.disposable ? "Yes (temporary email)" : "No"}`,
      `Webmail: ${data.webmail ? "Yes (Gmail, Yahoo, etc.)" : "No (company email)"}`,
      `Accept-All: ${data.accept_all ? "Yes (can't fully verify)" : "No"}`
    ];

    const result = `
Email: ${data.email}

Status: ${statusMessage}
Score: ${data.score}/100

Verification Details:
${checks.map(c => "- " + c).join("\n")}

Recommendation: ${
  data.result === "deliverable" ? "Safe to send" :
  data.result === "undeliverable" ? "Do NOT send - will bounce" :
  data.result === "risky" ? "Send with caution - monitor bounces" :
  "Verify manually before sending"
}
    `.trim();

    return {
      content: [{ type: "text", text: result }]
    };
  }
);

Step 7: Start the server

Add this at the bottom of your file:

// Start the server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Hunter.io MCP Server running");
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

Complete code

Here’s the full src/index.ts for easy copying:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

const HUNTER_API_BASE = "https://api.hunter.io/v2";
const API_KEY = process.env.HUNTER_API_KEY;

if (!API_KEY) {
  console.error("HUNTER_API_KEY environment variable is required");
  process.exit(1);
}

const server = new McpServer({ name: "hunter", version: "1.0.0" });

// Types
interface DomainSearchResult {
  domain: string;
  organization: string | null;
  pattern: string | null;
  accept_all: boolean;
  emails: Array<{
    value: string;
    confidence: number;
    first_name: string | null;
    last_name: string | null;
    position: string | null;
    department: string | null;
    linkedin: string | null;
    phone_number: string | null;
  }>;
}

interface EmailFinderResult {
  first_name: string;
  last_name: string;
  email: string;
  score: number;
  domain: string;
  position: string | null;
  linkedin_url: string | null;
}

interface EmailVerifierResult {
  email: string;
  result: "deliverable" | "undeliverable" | "risky" | "unknown";
  score: number;
  regexp: boolean;
  gibberish: boolean;
  disposable: boolean;
  webmail: boolean;
  mx_records: boolean;
  smtp_server: boolean;
  smtp_check: boolean;
  accept_all: boolean;
}

// API helper
async function hunterRequest<T>(endpoint: string, params: Record<string, string> = {}): Promise<T | null> {
  const url = new URL(`${HUNTER_API_BASE}${endpoint}`);
  url.searchParams.set("api_key", API_KEY!);
  for (const [key, value] of Object.entries(params)) {
    url.searchParams.set(key, value);
  }

  try {
    const response = await fetch(url.toString());
    if (!response.ok) return null;
    const result = await response.json();
    return result.data as T;
  } catch {
    return null;
  }
}

// Tool 1: Domain Search
server.tool(
  "domain_search",
  "Find email addresses at a company domain",
  {
    domain: z.string().describe("Company domain (e.g., stripe.com)"),
    limit: z.number().optional().default(10).describe("Max results (default 10)")
  },
  async ({ domain, limit }) => {
    const data = await hunterRequest<DomainSearchResult>("/domain-search", {
      domain,
      limit: String(Math.min(limit, 100))
    });

    if (!data || data.emails.length === 0) {
      return { content: [{ type: "text", text: `No emails found for ${domain}` }] };
    }

    const emails = data.emails.map(e => {
      const name = [e.first_name, e.last_name].filter(Boolean).join(" ") || "Unknown";
      return `- ${e.value} | ${name} | ${e.position || "Unknown role"} | ${e.confidence}% confidence`;
    }).join("\n");

    return {
      content: [{
        type: "text",
        text: `Found ${data.emails.length} emails at ${data.domain}:\n\n${emails}`
      }]
    };
  }
);

// Tool 2: Email Finder
server.tool(
  "email_finder",
  "Find a specific person's email at a company",
  {
    domain: z.string().describe("Company domain"),
    first_name: z.string().describe("First name"),
    last_name: z.string().describe("Last name")
  },
  async ({ domain, first_name, last_name }) => {
    const data = await hunterRequest<EmailFinderResult>("/email-finder", {
      domain, first_name, last_name
    });

    if (!data?.email) {
      return { content: [{ type: "text", text: `No email found for ${first_name} ${last_name} at ${domain}` }] };
    }

    return {
      content: [{
        type: "text",
        text: `Found: ${data.email}\nConfidence: ${data.score}%\nPosition: ${data.position || "Unknown"}`
      }]
    };
  }
);

// Tool 3: Email Verifier
server.tool(
  "email_verifier",
  "Verify if an email address is valid and deliverable",
  {
    email: z.string().email().describe("Email to verify")
  },
  async ({ email }) => {
    const data = await hunterRequest<EmailVerifierResult>("/email-verifier", { email });

    if (!data) {
      return { content: [{ type: "text", text: `Could not verify: ${email}` }] };
    }

    const status = data.result === "deliverable" ? "VALID" :
                   data.result === "undeliverable" ? "INVALID" :
                   data.result === "risky" ? "RISKY" : "UNKNOWN";

    return {
      content: [{
        type: "text",
        text: `${email}: ${status} (Score: ${data.score}/100)\nDisposable: ${data.disposable ? "Yes" : "No"}\nWebmail: ${data.webmail ? "Yes" : "No"}`
      }]
    };
  }
);

// Start server
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Hunter MCP Server running");
}

main().catch(console.error);

Testing your server

Build the server

npm run build

This compiles TypeScript to JavaScript in the build/ directory.

Test with MCP inspector

The MCP Inspector lets you test your server before connecting it to Claude:

HUNTER_API_KEY=your-key-here npx @modelcontextprotocol/inspector node build/index.js

This opens a web interface where you can:

  1. See all registered tools
  2. Call tools with test inputs
  3. View responses

Test queries to try:

  • domain_search with domain: “hubspot.com”
  • email_finder with domain: “stripe.com”, first_name: “Patrick”, last_name: “Collison”
  • email_verifier with a known email address

Installing in Claude Code

Once your server works, connect it to Claude Code. If you haven’t set up Claude Code yet, see the Claude Code guide first.

Add to Claude Code

claude mcp add hunter --env HUNTER_API_KEY=your-key-here -- node /ABSOLUTE/PATH/TO/hunter-mcp/build/index.js

Replace:

  • your-key-here with your actual Hunter.io API key
  • /ABSOLUTE/PATH/TO/ with the actual path to your project

Verify installation

In Claude Code, run:

/mcp

You should see hunter listed with 3 tools.

Test it

Ask Claude something like:

Find email addresses for decision makers at notion.so

Or:

Find the email for Brian Chesky at airbnb.com and verify if it's deliverable

Claude calls your MCP server, pulls the data, and returns actionable results. No more logging into Hunter.io every time you need a contact.


Using Claude Code to build your MCP server

Here’s the shortcut most people miss.

You don’t need to write MCP servers yourself. Claude Code builds them for you.

I’ve used this approach to create servers in under an hour that would have taken a full day writing from scratch. It works because MCP servers follow a predictable structure, and Claude knows the pattern.

The prompt template

Build an MCP server in TypeScript for [API NAME].

API Documentation: [URL or paste docs]

The server should have these tools:

1. [TOOL NAME]
   - Description: [what it does]
   - Inputs: [parameters needed]
   - Output: [what to return]

2. [TOOL NAME]
   - Description: [what it does]
   - Inputs: [parameters needed]
   - Output: [what to return]

Requirements:
- Use the @modelcontextprotocol/sdk package
- Use Zod for input validation
- Handle errors gracefully
- Use STDIO transport
- Never use console.log (use console.error for debugging)
- Read API key from environment variable

Create all files including package.json and tsconfig.json.
Include installation and usage instructions.

Real example: Ahrefs MCP server

Build an MCP server in TypeScript for the Ahrefs API.

API Docs: https://ahrefs.com/api/documentation

The server should have these tools:

1. backlinks_one_per_domain
   - Description: Get backlinks to a URL (one per referring domain)
   - Inputs: target (URL), limit (number)
   - Output: List of backlinks with DR, traffic, anchor text

2. organic_keywords
   - Description: Get organic keywords a URL ranks for
   - Inputs: target (URL), country (string), limit (number)
   - Output: Keywords with position, volume, traffic

3. domain_rating
   - Description: Get domain rating and metrics
   - Inputs: target (domain)
   - Output: DR, referring domains, backlinks count

Requirements:
- Use env variable AHREFS_API_KEY for authentication
- Use the @modelcontextprotocol/sdk package
- Use Zod for input validation
- Handle rate limits and errors gracefully
- Use STDIO transport

Create all necessary files.

Claude Code will generate the entire server, ready to build and use.


MCP servers worth building for your business

Here are the integrations that actually matter for marketing and lead generation:

Lead generation and outreach

APIWhat you could do
Hunter.ioFind emails, verify contacts (we just built this)
ClearbitCompany enrichment, lead scoring
Apollo.ioProspect search, sequence management
LemlistManage outreach campaigns

SEO and content

APIWhat you could do
AhrefsBacklink analysis, keyword research
SEMrushCompetitor analysis, rank tracking
SurferSEOContent optimization scores
ClearscopeContent grading and recommendations

Analytics and data

APIWhat you could do
Google AnalyticsPull reports, analyze traffic
MixpanelUser behavior, funnel analysis
AmplitudeProduct analytics queries
Your DatabaseQuery customers, segments, revenue

Advertising

APIWhat you could do
Meta Marketing APICampaign data, audience insights
Google Ads APIPerformance data, keyword ideas
TikTok AdsCampaign metrics, creative performance
LinkedIn AdsB2B campaign management

The pattern is always the same: find the API docs, define your tools, let Claude build it. Most APIs have enough documentation for Claude to generate a working server on the first try.


Common patterns

Authentication

Most APIs need auth. Here’s the pattern:

const API_KEY = process.env.MY_API_KEY;

if (!API_KEY) {
  console.error("MY_API_KEY environment variable is required");
  process.exit(1);
}

async function makeRequest(url: string) {
  const response = await fetch(url, {
    headers: {
      "Authorization": `Bearer ${API_KEY}`,
      "Content-Type": "application/json"
    }
  });
  return response.json();
}

Then when adding to Claude Code:

claude mcp add myserver --env MY_API_KEY=your-key -- node /path/to/build/index.js

Rate limiting

Add simple rate limiting to avoid hitting API limits:

let lastRequestTime = 0;
const MIN_INTERVAL = 1000; // 1 second

async function rateLimitedFetch(url: string) {
  const elapsed = Date.now() - lastRequestTime;
  if (elapsed < MIN_INTERVAL) {
    await new Promise(r => setTimeout(r, MIN_INTERVAL - elapsed));
  }
  lastRequestTime = Date.now();
  return fetch(url);
}

Helpful error messages

Return error messages that help Claude (and you) debug issues:

if (!data) {
  return {
    content: [{
      type: "text",
      text: `Error: Could not fetch data.

Possible causes:
- Invalid API key
- Rate limit exceeded
- Network issue

Try again in a few seconds.`
    }]
  };
}

Troubleshooting

Server doesn’t show up in Claude Code:

  1. Check the path is absolute, not relative
  2. Run npm run build to compile (easy to forget)
  3. Test with MCP Inspector first to verify the server works
  4. Run claude mcp list to see if there are errors

Tools fail silently:

  1. Add console.error() statements to debug (never console.log)
  2. Test API responses manually with curl
  3. Verify environment variables are set correctly

API key errors:

  1. Make sure you passed --env when adding the server
  2. Check the key is valid in the API’s dashboard
  3. Verify you haven’t exceeded rate limits

“Fatal error in main()”:

  1. Check your TypeScript compiled successfully (npm run build)
  2. Look for syntax errors in your code
  3. Verify all imports are correct and packages installed

What to do next

You now have two options:

Option 1: Build the Hunter.io server yourself. Follow the steps above. Test it with MCP Inspector. Connect it to Claude Code. You’ll have working email prospecting in a couple hours.

Option 2: Let Claude build the MCP server you actually need. Use the prompt template from this guide. Point it at your CRM’s API docs, your analytics platform, or whatever tool you wish Claude could access. Review the code, deploy it, and you’re connected.

Either way, you’re no longer waiting for someone else to build your integrations. That’s a meaningful advantage over competitors still stuck with disconnected tools.

The pattern is always the same: create the project, define tools with Zod schemas, implement API calls, handle errors, connect to Claude Code. Once you’ve done it once, every future MCP server gets easier.

Start now: Build the Hunter.io server, or pick the API you actually need and ask Claude to build it for you.


Building something custom and need help? Contact me.