-
-
Notifications
You must be signed in to change notification settings - Fork 5
MCP Server
MailGrab ships a local MCP (Model Context Protocol) server, mailgrab_mcp_server.py, so AI coding agents — Claude Code, Claude Desktop, Cursor, GitHub Copilot in VS Code, OpenAI's Codex CLI, or any other MCP-speaking client — can harvest emails from a website as a tool call, instead of you (or the agent) shelling out to MailGrab.py by hand.
One tool: crawl_website.
crawl_website(
url: str, # required
depth: int = 30,
same_domain: bool = True,
max_hops: int | None = None,
use_sitemap: bool = False,
ignore_robots: bool = False,
verify_mx: bool = False,
delay: float = 0.0,
timeout: float = 10.0,
concurrency: int = 10,
user_agent: str | None = None,
proxy: str | None = None,
) -> {
emails: [str],
email_count: int,
scrapped_urls: [str],
url_count: int,
sources: {email: [url]},
social_links: [str],
}
Every parameter maps 1:1 to a CLI flag — see that page for what each one actually does. Two defaults are deliberately different from the CLI's own defaults, chosen for safer behavior when an agent (not a human who typed the command) decides to call this:
-
same_domaindefaults toTruehere (the CLI defaults toFalse) — an agent-initiated crawl stays on the seed's own site unless explicitly told not to. -
depthdefaults to30— enough for a typical site's About/Team/Contact pages, without risking an unexpectedly long-running call from a vague request.
A crawl that completes but finds zero emails is a normal, successful result, not an error. A crawl that couldn't run at all (bad URL, invalid proxy, out-of-range depth, a timeout) raises a clear, specific error message instead.
Each tool call runs MailGrab.py as a real subprocess — python MailGrab.py --url ... --depth ... --quiet [your other options] — inside a fresh temporary directory, then reads that directory's _results.json back and returns it. It is not a thin in-process wrapper around the crawl engine. This is deliberate, for two reasons:
- MCP's
stdiotransport (what every client below uses for a local server) reserves this process's stdout exclusively for the JSON-RPC protocol stream.MailGrab.py's own crawl output goes to stdout too; running it in the same process would corrupt that stream. A subprocess has its own stdout, captured separately, so this can't happen. -
MailGrab.pywrites to fixed filenames (_results.jsonand friends) in its working directory. Running each call in its own temp directory means two concurrent tool calls — a real scenario, nothing stops a client from calling the tool in parallel — can never clobber each other's output.
The subprocess timeout itself scales with your own depth/concurrency/timeout choices rather than a flat guess (see Limitations, on purpose), and a clean-but-empty result is retried once automatically — both fixes came out of an adversarial review during development, alongside the stdin-inheritance bug above. See Architecture and CLAUDE.md in the repo for the full story.
The MCP server needs the mcp Python package, pinned to the 1.x line (the 2.x line renamed its main API and mailgrab_mcp_server.py is written against 1.x):
pip install mcp==1.29.1This is already included if you ran MailGrab's own installer (Installation) or pip install -r requirements.txt.
You'll point your AI tool's config at the full path to mailgrab_mcp_server.py inside your MailGrab checkout, e.g. /home/you/MailGrab/mailgrab_mcp_server.py or C:\Users\you\MailGrab\mailgrab_mcp_server.py.
Every client below launches the server the same way — as a local process via python <path> — just with different config file locations and key names.
Project-scoped config, checked into .mcp.json at your project root so the whole team gets it:
{
"mcpServers": {
"mailgrab": {
"command": "python",
"args": ["/absolute/path/to/mailgrab_mcp_server.py"]
}
}
}Or register it without hand-editing JSON — note --scope project, without it claude mcp add defaults to a local, private-to-you scope (stored in ~/.claude.json, not shared via .mcp.json):
claude mcp add mailgrab --scope project -- python /absolute/path/to/mailgrab_mcp_server.py(There's also --scope user for a private server available across all your projects.)
Edit claude_desktop_config.json (Settings → Developer → Edit Config, or find it directly at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS / %APPDATA%\Claude\claude_desktop_config.json on Windows):
{
"mcpServers": {
"mailgrab": {
"command": "python",
"args": ["/absolute/path/to/mailgrab_mcp_server.py"]
}
}
}Restart Claude Desktop after saving.
Project-scoped .cursor/mcp.json (or the global ~/.cursor/mcp.json):
{
"mcpServers": {
"mailgrab": {
"command": "python",
"args": ["/absolute/path/to/mailgrab_mcp_server.py"]
}
}
}Known quirk: Cursor doesn't always pick up config changes live. If
mailgrabdoesn't show up (or a config edit doesn't take effect), toggle it off/on under Settings → MCP, or run Developer: Reload Window, before assuming something's wrong with the server itself.
.vscode/mcp.json in your workspace. The top-level key is "servers" (not "mcpServers") — this is the one client here that differs:
{
"servers": {
"mailgrab": {
"type": "stdio",
"command": "python",
"args": ["/absolute/path/to/mailgrab_mcp_server.py"]
}
}
}"type": "stdio" is technically optional (it's the default when you give a command), but spelling it out costs nothing and matches how non-stdio servers are declared. VS Code will prompt to start the server the first time; you can also run MCP: List Servers from the command palette to start/restart it manually.
The standalone copilot terminal tool (separate from the VS Code extension above) uses its own config file, ~/.copilot/mcp-config.json, with its own schema — notably a tools allowlist with no equivalent in the VS Code version:
{
"mcpServers": {
"mailgrab": {
"type": "local",
"command": "python",
"args": ["/absolute/path/to/mailgrab_mcp_server.py"],
"tools": ["*"]
}
}
}TOML config at ~/.codex/config.toml:
[mcp_servers.mailgrab]
command = "python"
args = ["/absolute/path/to/mailgrab_mcp_server.py"]Ask your agent something like "find contact emails for example.com" — it should recognize crawl_website as the right tool and call it.
-
No
--append/--resume/--config. The tool is deliberately stateless and one-shot — every call gets a fresh temp directory, and there's nowork_dirparameter (yet) for a caller that wants persistence across calls. - No streaming progress. A tool call is request/response; the agent waits for the full result rather than seeing live per-page progress. MCP supports progress notifications for long-running tools if this becomes worth the added complexity later.
-
Depth is still capped by the CLI's own validation (1–500). The subprocess itself is bounded by a timeout computed from your own
depth,concurrency, andtimeoutarguments — roughlyceil(depth / concurrency) * (timeout + 5) + 30seconds, clamped between 60s and 1800s — so a low-concurrency, high-per-request-timeout crawl (a deliberately polite, slow crawl) gets a proportionally longer budget instead of being killed early.