agentpay-email-finder-mcp 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.
Files changed (2) hide show
  1. package/package.json +12 -0
  2. package/server.py +257 -0
package/package.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "agentpay-email-finder-mcp",
3
+ "version": "1.0.0",
4
+ "description": "Email Finder MCP - find/verify emails by name+domain. 50 free lookups, then PRO $19/mo.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "keywords": [],
10
+ "author": "",
11
+ "license": "MIT"
12
+ }
package/server.py ADDED
@@ -0,0 +1,257 @@
1
+ """Email Finder MCP Server - Main entry point with MCP tools."""
2
+
3
+ import sys
4
+ from typing import Optional
5
+
6
+ from mcp.server import Server
7
+ from mcp.server.stdio import stdio_server
8
+ from mcp.types import (
9
+ Tool,
10
+ TextContent,
11
+ CallToolResult,
12
+ )
13
+
14
+ from .email_utils import find_email, verify_email, find_company_emails
15
+ from .rate_limiter import check_rate_limit, increment_usage, FREE_LIMIT, STRIPE_LINK
16
+
17
+
18
+ server = Server("email-finder")
19
+
20
+
21
+ @server.list_tools()
22
+ async def list_tools() -> list[Tool]:
23
+ """List available email finding tools."""
24
+ return [
25
+ Tool(
26
+ name="find_email",
27
+ description="Find the most likely email address for a person given their name and company domain. "
28
+ f"Free limit: {FREE_LIMIT} lookups. See https://rumblingb.github.io/email-finder-mcp for pro access.",
29
+ inputSchema={
30
+ "type": "object",
31
+ "properties": {
32
+ "first_name": {
33
+ "type": "string",
34
+ "description": "First name of the person",
35
+ },
36
+ "last_name": {
37
+ "type": "string",
38
+ "description": "Last name of the person",
39
+ },
40
+ "domain": {
41
+ "type": "string",
42
+ "description": "Company domain (e.g., example.com)",
43
+ },
44
+ "api_key": {
45
+ "type": "string",
46
+ "description": "Optional PRO API key for rate limit bypass. Get one at " + STRIPE_LINK,
47
+ },
48
+ },
49
+ "required": ["first_name", "last_name", "domain"],
50
+ },
51
+ ),
52
+ Tool(
53
+ name="verify_email",
54
+ description="Verify if an email address exists using MX record lookup and SMTP verification. "
55
+ f"Free limit: {FREE_LIMIT} verifications. See https://rumblingb.github.io/email-finder-mcp for pro access.",
56
+ inputSchema={
57
+ "type": "object",
58
+ "properties": {
59
+ "email": {
60
+ "type": "string",
61
+ "description": "Email address to verify",
62
+ },
63
+ "api_key": {
64
+ "type": "string",
65
+ "description": "Optional PRO API key for rate limit bypass. Get one at " + STRIPE_LINK,
66
+ },
67
+ },
68
+ "required": ["email"],
69
+ },
70
+ ),
71
+ Tool(
72
+ name="find_company_emails",
73
+ description="Find common email patterns and formats used at a company domain. "
74
+ f"Free limit: {FREE_LIMIT} lookups. See https://rumblingb.github.io/email-finder-mcp for pro access.",
75
+ inputSchema={
76
+ "type": "object",
77
+ "properties": {
78
+ "company_name": {
79
+ "type": "string",
80
+ "description": "Company name (e.g., Acme Corp)",
81
+ },
82
+ "domain": {
83
+ "type": "string",
84
+ "description": "Company domain (optional - will guess from name if not provided)",
85
+ },
86
+ "api_key": {
87
+ "type": "string",
88
+ "description": "Optional PRO API key for rate limit bypass. Get one at " + STRIPE_LINK,
89
+ },
90
+ },
91
+ "required": ["company_name"],
92
+ },
93
+ ),
94
+ Tool(
95
+ name="check_rate_limit",
96
+ description=f"Check remaining free API calls. Free limit is {FREE_LIMIT}. Upgrade at {STRIPE_LINK}",
97
+ inputSchema={
98
+ "type": "object",
99
+ "properties": {
100
+ "client_info": {
101
+ "type": "string",
102
+ "description": "Client identifier (IP, username, or session ID) for tracking usage",
103
+ },
104
+ "api_key": {
105
+ "type": "string",
106
+ "description": "Optional PRO API key. If valid, shows unlimited access.",
107
+ },
108
+ },
109
+ "required": ["client_info"],
110
+ },
111
+ ),
112
+ ]
113
+
114
+
115
+ @server.call_tool()
116
+ async def call_tool(name: str, arguments: dict) -> list[TextContent]:
117
+ """Handle tool calls."""
118
+ client_info = arguments.get("client_info", "anonymous")
119
+ api_key = arguments.get("api_key", None)
120
+
121
+ # Check rate limit for all non-check_rate_limit tools
122
+ if name != "check_rate_limit":
123
+ # Use client_id from api_key if provided, otherwise from arguments
124
+ rl_client = api_key or str(arguments)
125
+ rate_result = check_rate_limit(rl_client, api_key)
126
+
127
+ if not rate_result["allowed"]:
128
+ return [TextContent(
129
+ type="text",
130
+ text=f"Rate limit exceeded. {rate_result['message']}\n\n"
131
+ f"Upgrade to PRO at: {STRIPE_LINK}\n"
132
+ f"Free limit: {FREE_LIMIT} lookups"
133
+ )]
134
+
135
+ # Track usage for free tier
136
+ if not rate_result["is_pro"]:
137
+ increment_usage(rl_client)
138
+
139
+ if name == "find_email":
140
+ return await handle_find_email(arguments)
141
+ elif name == "verify_email":
142
+ return await handle_verify_email(arguments)
143
+ elif name == "find_company_emails":
144
+ return await handle_find_company_emails(arguments)
145
+ elif name == "check_rate_limit":
146
+ return await handle_check_rate_limit(arguments)
147
+ else:
148
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
149
+
150
+
151
+ async def handle_find_email(args: dict) -> list[TextContent]:
152
+ """Handle find_email tool call."""
153
+ first_name = args.get("first_name", "")
154
+ last_name = args.get("last_name", "")
155
+ domain = args.get("domain", "")
156
+
157
+ result = find_email(first_name, last_name, domain)
158
+
159
+ if result.get("error"):
160
+ return [TextContent(type="text", text=f"Error: {result['error']}")]
161
+
162
+ output = (
163
+ f"Email Finder Results\n"
164
+ f"{'=' * 50}\n"
165
+ f"Name: {first_name} {last_name}\n"
166
+ f"Domain: {domain}\n"
167
+ f"Most Likely Email: {result['email']}\n"
168
+ f"Confidence: {result['confidence']}\n\n"
169
+ f"All Generated Patterns:\n"
170
+ )
171
+ for i, pattern in enumerate(result.get("all_patterns", []), 1):
172
+ marker = " ->" if pattern == result["email"] else " "
173
+ output += f"{marker} {i}. {pattern}\n"
174
+
175
+ return [TextContent(type="text", text=output)]
176
+
177
+
178
+ async def handle_verify_email(args: dict) -> list[TextContent]:
179
+ """Handle verify_email tool call."""
180
+ email = args.get("email", "")
181
+
182
+ result = verify_email(email)
183
+
184
+ status_icon = {
185
+ "valid": "VALID",
186
+ "invalid": "INVALID",
187
+ "unknown": "UNKNOWN",
188
+ }.get(result["verdict"], "UNKNOWN")
189
+
190
+ output = (
191
+ f"Email Verification Results\n"
192
+ f"{'=' * 50}\n"
193
+ f"Email: {result['email']}\n"
194
+ f"Verdict: {status_icon}\n"
195
+ f"Format Valid: {'Yes' if result['format_valid'] else 'No'}\n"
196
+ f"MX Record: {result['mx_record'] or 'Not found'}\n"
197
+ f"SMTP Verified: {'Yes' if result['smtp_verified'] else 'No'}\n\n"
198
+ f"Details:\n"
199
+ )
200
+ for detail in result["details"]:
201
+ output += f" - {detail}\n"
202
+
203
+ return [TextContent(type="text", text=output)]
204
+
205
+
206
+ async def handle_find_company_emails(args: dict) -> list[TextContent]:
207
+ """Handle find_company_emails tool call."""
208
+ company_name = args.get("company_name", "")
209
+ domain = args.get("domain", None)
210
+
211
+ result = find_company_emails(company_name, domain)
212
+
213
+ if result.get("error"):
214
+ return [TextContent(type="text", text=f"Error: {result['error']}")]
215
+
216
+ output = (
217
+ f"Company Email Patterns\n"
218
+ f"{'=' * 50}\n"
219
+ f"Company: {result['company']}\n"
220
+ f"Domain: {result['domain']}\n"
221
+ f"MX Active: {'Yes' if result['mx_record']['exists'] else 'No/Unknown'}\n\n"
222
+ f"Common Email Patterns:\n"
223
+ )
224
+ for p in result["patterns"]:
225
+ output += f" [{p['frequency']}] {p['example']}\n"
226
+ output += (
227
+ f"\nPro Tip: Use find_email with first/last name + domain to get exact addresses.\n"
228
+ f"Upgrade to PRO at: {STRIPE_LINK}"
229
+ )
230
+
231
+ return [TextContent(type="text", text=output)]
232
+
233
+
234
+ async def handle_check_rate_limit(args: dict) -> list[TextContent]:
235
+ """Handle check_rate_limit tool call."""
236
+ client_info = args.get("client_info", "anonymous")
237
+ api_key = args.get("api_key", None)
238
+
239
+ result = check_rate_limit(client_info, api_key)
240
+
241
+ if result["is_pro"]:
242
+ output = (
243
+ f"Rate Limit Status\n"
244
+ f"{'=' * 50}\n"
245
+ f"Status: PRO (Unlimited)\n"
246
+ f"Thank you for being a PRO subscriber!\n"
247
+ )
248
+ else:
249
+ output = (
250
+ f"Rate Limit Status\n"
251
+ f"{'=' * 50}\n"
252
+ f"Tier: Free\n"
253
+ f"Remaining: {result['remaining']}/{FREE_LIMIT}\n"
254
+ f"Upgrade to PRO at: {STRIPE_LINK}\n"
255
+ )
256
+
257
+ return [TextContent(type="text", text=output)]