email-verifier-mcp 0.1.0__tar.gz

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.
@@ -0,0 +1,22 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(pip install *)",
5
+ "Bash(python3 -c ' *)",
6
+ "Bash(echo \"PID: $!\")",
7
+ "Bash(curl -s http://localhost:8000/)",
8
+ "Read(//home/kareem/.config/**)",
9
+ "Read(//home/kareem/snap/**)",
10
+ "Read(//home/kareem/**)",
11
+ "Bash(python3 -m json.tool)",
12
+ "Bash(python3 -c \"import json,sys; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d.get\\('mcpServers', d.get\\('projects', {}\\).get\\('mcp', {}\\)\\), indent=2\\)\\)\")",
13
+ "Bash(claude mcp *)",
14
+ "Bash(find /home/kareem -name \"claude\" -type f 2>/dev/null | head -5 && find /usr/local/bin -name \"claude*\" 2>/dev/null | head -5)",
15
+ "Read(//usr/local/bin/**)",
16
+ "Bash(/home/kareem/.vscode/extensions/anthropic.claude-code-2.1.186-linux-x64/resources/native-binary/claude mcp *)",
17
+ "Bash(uv --version)",
18
+ "Bash(python3 -c \"import mcp\")",
19
+ "Bash(python3 -c \"import httpx\")"
20
+ ]
21
+ }
22
+ }
@@ -0,0 +1,150 @@
1
+ .idea
2
+ .idea_modules
3
+ .vscode
4
+ build-all.sh
5
+ public
6
+ AAA/logs
7
+ AAA/*.iml
8
+ AAA\target
9
+ AAA/target/*
10
+ AAA/.idea
11
+ AAA/.idea_modules
12
+ AAA/.vscode
13
+ AAA/RUNNING_PID
14
+ AAA/generated.keystore
15
+ AAA/generated.truststore
16
+ AAA/*.log
17
+ AAA/public/logs/*.log
18
+
19
+ #Build Server Protocol
20
+ AAA/.bsp/
21
+
22
+ AAA/.bloop
23
+ AAA/.metals
24
+ AAA/metals.sbt
25
+
26
+ # Scala-IDE specific
27
+ AAA/.scala_dependencies
28
+ AAA/.classpath
29
+ # .project
30
+ AAA/.settings/
31
+ AAA/.cache-main
32
+ AAA/.cache-tests
33
+ AAA/bin/
34
+
35
+
36
+ ApiGateway/logs
37
+ ApiGateway/*.iml
38
+ ApiGateway/target
39
+ ApiGateway/.idea
40
+ ApiGateway/.idea_modules
41
+ ApiGateway/.vscode
42
+ ApiGateway/RUNNING_PID
43
+ ApiGateway/generated.keystore
44
+ ApiGateway/generated.truststore
45
+ ApiGateway/*.log
46
+ ApiGateway/public/logs/*.log
47
+
48
+ # Build Server Protocol
49
+ ApiGateway/.bsp/
50
+
51
+ ApiGateway/.bloop
52
+ ApiGateway/.metals
53
+ ApiGateway/metals.sbt
54
+
55
+ # Scala-IDE specific
56
+ ApiGateway/.scala_dependencies
57
+ ApiGateway/.classpath
58
+ # .project
59
+ ApiGateway/.settings/
60
+ ApiGateway/.cache-main
61
+ ApiGateway/.cache-tests
62
+ ApiGateway/bin/
63
+
64
+
65
+ Pricing/logs
66
+ Pricing/*.iml
67
+ Pricing/target
68
+ Pricing/.idea
69
+ Pricing/.idea_modules
70
+ Pricing/.vscode
71
+ Pricing/RUNNING_PID
72
+ Pricing/generated.keystore
73
+ Pricing/generated.truststore
74
+ Pricing/*.log
75
+ Pricing/public/logs/*.log
76
+
77
+ # Build Server Protocol
78
+ Pricing/.bsp/
79
+
80
+ Pricing/.bloop
81
+ Pricing/.metals
82
+ Pricing/metals.sbt
83
+
84
+ # Scala-IDE specific
85
+ Pricing/.scala_dependencies
86
+ Pricing/.classpath
87
+ # .project
88
+ Pricing/.settings/
89
+ Pricing/.cache-main
90
+ Pricing/.cache-tests
91
+ Pricing/bin/
92
+
93
+
94
+ Search/logs
95
+ Search/*.iml
96
+ Search/target
97
+ Search/.idea
98
+ Search/.idea_modules
99
+ Search/.vscode
100
+ Search/RUNNING_PID
101
+ Search/generated.keystore
102
+ Search/generated.truststore
103
+ Search/*.log
104
+ Search/public/logs/*.log
105
+
106
+ # Build Server Protocol
107
+ Search/.bsp/
108
+
109
+ Search/.bloop
110
+ Search/.metals
111
+ Search/metals.sbt
112
+
113
+ # Scala-IDE specific
114
+ Search/.scala_dependencies
115
+ Search/.classpath
116
+ # .project
117
+ Search/.settings/
118
+ Search/.cache-main
119
+ Search/.cache-tests
120
+ Search/bin/
121
+
122
+ target
123
+ target/*
124
+ .target
125
+ /target
126
+
127
+ /test
128
+
129
+ AAA/gatling
130
+ ApiGateway/gatling
131
+ Pricing/gatling
132
+ Search/gatling
133
+
134
+ # Search/conf/hibernate.prod.cfg.xml
135
+ # Search/conf/hibernate.staging.cfg.xml
136
+
137
+ # Pricing/conf/hibernate.prod.cfg.xml
138
+ # Pricing/conf/hibernate.staging.cfg.xml
139
+
140
+ # AAA/conf/hibernate.prod.cfg.xml
141
+ # AAA/conf/hibernate.staging.cfg.xml
142
+
143
+ # ApiGateway/conf/hibernate.prod.cfg.xml
144
+ # ApiGateway/conf/hibernate.staging.cfg.xml
145
+
146
+ vector_service/venv
147
+ vector_service/app/__pycache__
148
+ vector_service/.pytest_cache
149
+ vector_service/.mypy_cache
150
+ vector_service/.vscode
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: email-verifier-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for verifying email addresses via the RightEmails API
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: httpx>=0.27.0
7
+ Requires-Dist: mcp>=1.0.0
@@ -0,0 +1,252 @@
1
+ """
2
+ Browser UI for the RightEmails email verifier API.
3
+ Run with: python3 app.py
4
+ Then open: http://localhost:8000
5
+ """
6
+
7
+ import os
8
+ import time
9
+ import httpx
10
+ from fastapi import FastAPI, Request
11
+ from fastapi.responses import HTMLResponse, JSONResponse
12
+ import uvicorn
13
+
14
+ app = FastAPI()
15
+
16
+ BASE_URL = os.environ.get("RIGHTEMAILS_BASE_URL", "https://rightemails.reachstream.com")
17
+ API_KEY = os.environ.get("RIGHTEMAILS_API_KEY", "d8397d6e-5d19-46c6-9340-ee5f95d80b85")
18
+ DEFAULT_REQUEST_ORIGIN = os.environ.get("RIGHTEMAILS_REQUEST_ORIGIN", "abdul.kareem@insnap.in")
19
+
20
+ CREATE_ENDPOINT = "/api/v1/api-rightemails/request/"
21
+ GET_ENDPOINT = "/api/v1/api-rightemails/request/{id}"
22
+ BATCH_ENDPOINT = "/api/v1/api-rightemails/request/batch"
23
+
24
+ HTML = """<!DOCTYPE html>
25
+ <html lang="en">
26
+ <head>
27
+ <meta charset="UTF-8">
28
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
29
+ <title>Email Verifier</title>
30
+ <style>
31
+ * { box-sizing: border-box; margin: 0; padding: 0; }
32
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f7fa; color: #333; min-height: 100vh; padding: 40px 20px; }
33
+ .container { max-width: 800px; margin: 0 auto; }
34
+ h1 { font-size: 1.8rem; font-weight: 700; margin-bottom: 4px; }
35
+ .subtitle { color: #666; margin-bottom: 32px; font-size: 0.95rem; }
36
+ .card { background: white; border-radius: 12px; padding: 28px; box-shadow: 0 1px 4px rgba(0,0,0,0.08); margin-bottom: 24px; }
37
+ label { display: block; font-weight: 600; font-size: 0.9rem; margin-bottom: 8px; }
38
+ textarea { width: 100%; border: 1px solid #ddd; border-radius: 8px; padding: 12px; font-size: 0.95rem; resize: vertical; min-height: 120px; outline: none; font-family: inherit; }
39
+ textarea:focus { border-color: #4f46e5; box-shadow: 0 0 0 3px rgba(79,70,229,0.1); }
40
+ .row { display: flex; gap: 12px; margin-top: 16px; align-items: center; }
41
+ button { background: #4f46e5; color: white; border: none; border-radius: 8px; padding: 10px 24px; font-size: 0.95rem; font-weight: 600; cursor: pointer; transition: background 0.15s; }
42
+ button:hover { background: #4338ca; }
43
+ button:disabled { background: #a5b4fc; cursor: not-allowed; }
44
+ .wait-label { font-size: 0.85rem; color: #666; }
45
+ input[type=number] { border: 1px solid #ddd; border-radius: 6px; padding: 8px 10px; width: 70px; font-size: 0.9rem; text-align: center; }
46
+ #status-bar { display: none; align-items: center; gap: 10px; padding: 12px 16px; background: #eef2ff; border-radius: 8px; font-size: 0.9rem; color: #4f46e5; }
47
+ .spinner { width: 18px; height: 18px; border: 2px solid #c7d2fe; border-top-color: #4f46e5; border-radius: 50%; animation: spin 0.7s linear infinite; flex-shrink: 0; }
48
+ @keyframes spin { to { transform: rotate(360deg); } }
49
+ table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
50
+ th { text-align: left; padding: 10px 14px; background: #f8fafc; border-bottom: 1px solid #e5e7eb; font-weight: 600; color: #555; font-size: 0.82rem; text-transform: uppercase; letter-spacing: 0.04em; }
51
+ td { padding: 10px 14px; border-bottom: 1px solid #f1f3f5; }
52
+ tr:last-child td { border-bottom: none; }
53
+ .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 600; text-transform: uppercase; }
54
+ .badge-valid { background: #dcfce7; color: #16a34a; }
55
+ .badge-invalid { background: #fee2e2; color: #dc2626; }
56
+ .badge-risky { background: #fef9c3; color: #ca8a04; }
57
+ .badge-unknown { background: #f1f5f9; color: #64748b; }
58
+ .badge-pending { background: #e0e7ff; color: #4f46e5; }
59
+ .meta { font-size: 0.8rem; color: #888; margin-top: 4px; }
60
+ .error-box { background: #fee2e2; color: #dc2626; padding: 12px 16px; border-radius: 8px; font-size: 0.9rem; }
61
+ </style>
62
+ </head>
63
+ <body>
64
+ <div class="container">
65
+ <h1>Email Verifier</h1>
66
+ <p class="subtitle">Powered by RightEmails &mdash; rightemails.reachstream.com</p>
67
+
68
+ <div class="card">
69
+ <label for="emails">Email addresses (one per line)</label>
70
+ <textarea id="emails" placeholder="user@example.com&#10;another@domain.com"></textarea>
71
+ <div class="row">
72
+ <button id="btn" onclick="verify()">Verify</button>
73
+ <span class="wait-label">Wait up to</span>
74
+ <input type="number" id="timeout" value="60" min="5" max="300"> seconds
75
+ </div>
76
+ </div>
77
+
78
+ <div id="status-bar">
79
+ <div class="spinner"></div>
80
+ <span id="status-msg">Submitting...</span>
81
+ </div>
82
+
83
+ <div id="results"></div>
84
+ </div>
85
+
86
+ <script>
87
+ function badgeClass(status) {
88
+ if (!status) return 'badge-unknown';
89
+ const s = status.toLowerCase();
90
+ if (s === 'valid' || s === 'deliverable') return 'badge-valid';
91
+ if (s === 'invalid' || s === 'undeliverable') return 'badge-invalid';
92
+ if (s === 'risky' || s === 'catch_all' || s === 'catch-all') return 'badge-risky';
93
+ if (s === 'pending' || s === 'processing') return 'badge-pending';
94
+ return 'badge-unknown';
95
+ }
96
+
97
+ async function verify() {
98
+ const raw = document.getElementById('emails').value.trim();
99
+ const timeout = parseInt(document.getElementById('timeout').value) || 60;
100
+ if (!raw) return;
101
+
102
+ const emails = raw.split(/\n/).map(e => e.trim()).filter(Boolean);
103
+ const btn = document.getElementById('btn');
104
+ const bar = document.getElementById('status-bar');
105
+ const msg = document.getElementById('status-msg');
106
+ const results = document.getElementById('results');
107
+
108
+ btn.disabled = true;
109
+ results.innerHTML = '';
110
+ bar.style.display = 'flex';
111
+ msg.textContent = `Submitting ${emails.length} email(s)...`;
112
+
113
+ try {
114
+ const res = await fetch('/verify', {
115
+ method: 'POST',
116
+ headers: { 'Content-Type': 'application/json' },
117
+ body: JSON.stringify({ emails, max_wait_seconds: timeout })
118
+ });
119
+ const data = await res.json();
120
+
121
+ bar.style.display = 'none';
122
+
123
+ if (data.error) {
124
+ results.innerHTML = `<div class="error-box">${data.error}${data.detail ? ': ' + JSON.stringify(data.detail) : ''}</div>`;
125
+ return;
126
+ }
127
+
128
+ const history = data.history || {};
129
+ const detail = data.details || data.detail || {};
130
+ const items = detail.results || detail.result || [];
131
+ const timedOut = data.timed_out;
132
+
133
+ let html = '<div class="card">';
134
+ html += `<div class="meta" style="margin-bottom:14px">
135
+ Request ID: <strong>${history.id ?? '—'}</strong> &nbsp;|&nbsp;
136
+ Status: <strong>${history.status ?? '—'}</strong>
137
+ ${timedOut ? ' &nbsp;<span style="color:#dc2626">(timed out — partial results)</span>' : ''}
138
+ ${history.created_at ? ' &nbsp;|&nbsp; Created: ' + history.created_at : ''}
139
+ </div>`;
140
+
141
+ if (items.length > 0) {
142
+ html += `<table>
143
+ <thead><tr><th>#</th><th>Email</th><th>Status</th><th>Sub-status</th></tr></thead><tbody>`;
144
+ items.forEach((item, i) => {
145
+ const s = item.status || item.result || '';
146
+ html += `<tr>
147
+ <td style="color:#aaa">${i + 1}</td>
148
+ <td>${item.email || '—'}</td>
149
+ <td><span class="badge ${badgeClass(s)}">${s || '—'}</span></td>
150
+ <td style="color:#888;font-size:0.85rem">${item.sub_status || item.substatus || '—'}</td>
151
+ </tr>`;
152
+ });
153
+ html += '</tbody></table>';
154
+ } else {
155
+ html += `<pre style="font-size:0.8rem;color:#555;overflow:auto">${JSON.stringify(data, null, 2)}</pre>`;
156
+ }
157
+ html += '</div>';
158
+ results.innerHTML = html;
159
+ } catch (err) {
160
+ bar.style.display = 'none';
161
+ results.innerHTML = `<div class="error-box">Network error: ${err.message}</div>`;
162
+ } finally {
163
+ btn.disabled = false;
164
+ }
165
+ }
166
+ </script>
167
+ </body>
168
+ </html>"""
169
+
170
+
171
+ def _headers():
172
+ return {"X-API-Key": API_KEY, "Content-Type": "application/json"}
173
+
174
+
175
+ def _handle_response(response: httpx.Response):
176
+ if response.status_code in (200, 201):
177
+ try:
178
+ return response.json()
179
+ except Exception:
180
+ return {"status": "ok", "raw": response.text}
181
+ if response.status_code == 401:
182
+ return {"error": "API key required"}
183
+ if response.status_code == 403:
184
+ return {"error": "Invalid API key or mismatched request origin"}
185
+ if response.status_code == 404:
186
+ return {"error": "Not found"}
187
+ if response.status_code == 422:
188
+ return {"error": "Validation error", "detail": response.json()}
189
+ return {"error": f"Unexpected status {response.status_code}", "detail": response.text}
190
+
191
+
192
+ @app.get("/", response_class=HTMLResponse)
193
+ async def index():
194
+ return HTML
195
+
196
+
197
+ @app.post("/verify")
198
+ async def verify(request: Request):
199
+ body = await request.json()
200
+ emails = body.get("emails", [])
201
+ max_wait = body.get("max_wait_seconds", 60)
202
+ poll_interval = 3
203
+
204
+ if not emails:
205
+ return JSONResponse({"error": "No emails provided"})
206
+
207
+ payload = {
208
+ "request_type": "verifier",
209
+ "request_origin": DEFAULT_REQUEST_ORIGIN,
210
+ "request_body": [{"email": e} for e in emails],
211
+ }
212
+
213
+ try:
214
+ with httpx.Client(timeout=30.0) as client:
215
+ resp = client.post(f"{BASE_URL}{CREATE_ENDPOINT}", headers=_headers(), json=payload)
216
+ submitted = _handle_response(resp)
217
+ except httpx.RequestError as e:
218
+ return JSONResponse({"error": "Request failed", "detail": str(e)})
219
+
220
+ if "error" in submitted:
221
+ return JSONResponse(submitted)
222
+
223
+ request_id = submitted.get("history", {}).get("id")
224
+ if request_id is None:
225
+ return JSONResponse({"error": "No request id in response", "raw": submitted})
226
+
227
+ elapsed = 0
228
+ last = submitted
229
+ while elapsed < max_wait:
230
+ time.sleep(poll_interval)
231
+ elapsed += poll_interval
232
+ try:
233
+ with httpx.Client(timeout=15.0) as client:
234
+ resp = client.get(
235
+ f"{BASE_URL}{GET_ENDPOINT.format(id=request_id)}",
236
+ headers=_headers(),
237
+ )
238
+ last = _handle_response(resp)
239
+ except httpx.RequestError as e:
240
+ return JSONResponse({"error": "Poll failed", "detail": str(e)})
241
+
242
+ if "error" in last:
243
+ return JSONResponse(last)
244
+ if last.get("history", {}).get("status") in ("completed", "failed"):
245
+ return JSONResponse(last)
246
+
247
+ last["timed_out"] = True
248
+ return JSONResponse(last)
249
+
250
+
251
+ if __name__ == "__main__":
252
+ uvicorn.run(app, host="0.0.0.0", port=8001)
@@ -0,0 +1,11 @@
1
+ {
2
+ "mcpServers": {
3
+ "email-verifier": {
4
+ "command": "/usr/bin/python3",
5
+ "args": ["/home/kareem/MCPRE/email_verifier_mcp.py"],
6
+ "env": {
7
+ "RIGHTEMAILS_API_KEY": "d8397d6e-5d19-46c6-9340-ee5f95d80b85"
8
+ }
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,206 @@
1
+ """
2
+ MCP server for the RightEmails API (rightemails.reachstream.com).
3
+
4
+ This API is async/job-based:
5
+ 1. POST a list of emails -> get back a request_history_id + initial status
6
+ 2. GET that id (or POST a batch of ids) -> poll until status is "completed"
7
+
8
+ Setup:
9
+ pip install mcp httpx (use --break-system-packages or a venv if needed)
10
+
11
+ Environment variables:
12
+ RIGHTEMAILS_API_KEY - your X-API-Key value (required)
13
+ RIGHTEMAILS_BASE_URL - defaults to https://rightemails.reachstream.com
14
+ """
15
+
16
+ import os
17
+ import time
18
+ import httpx
19
+ from mcp.server.fastmcp import FastMCP
20
+
21
+ mcp = FastMCP("email-verifier")
22
+
23
+ BASE_URL = os.environ.get("RIGHTEMAILS_BASE_URL", "https://rightemails.reachstream.com")
24
+ # API_KEY = os.environ.get("RIGHTEMAILS_API_KEY", "d8397d6e-5d19-46c6-9340-ee5f95d80b85")
25
+ API_KEY = os.environ.get("RIGHTEMAILS_API_KEY")
26
+
27
+ # RightEmails ties each API key to a specific registered "request_origin" value
28
+ # (in this account's case, the account email). Requests with a mismatched
29
+ # origin are rejected with 403 "Request origin does not match API key user".
30
+ DEFAULT_REQUEST_ORIGIN = os.environ.get("RIGHTEMAILS_REQUEST_ORIGIN")
31
+
32
+ CREATE_ENDPOINT = "/api/v1/api-rightemails/request/"
33
+ GET_ENDPOINT = "/api/v1/api-rightemails/request/{id}"
34
+ BATCH_ENDPOINT = "/api/v1/api-rightemails/request/batch"
35
+
36
+
37
+ def _headers() -> dict:
38
+ return {
39
+ "X-API-Key": API_KEY,
40
+ "Content-Type": "application/json",
41
+ }
42
+
43
+
44
+ def _handle_response(response: httpx.Response) -> dict:
45
+ if response.status_code in (200, 201):
46
+ try:
47
+ return response.json()
48
+ except Exception:
49
+ return {"status": "ok", "raw": response.text}
50
+ if response.status_code == 401:
51
+ return {"error": "API key required (missing X-API-Key header)"}
52
+ if response.status_code == 403:
53
+ return {"error": "Invalid API key"}
54
+ if response.status_code == 404:
55
+ return {"error": "Not found"}
56
+ if response.status_code == 422:
57
+ return {"error": "Validation error", "detail": response.json()}
58
+ return {"error": f"Unexpected status {response.status_code}", "detail": response.text}
59
+
60
+
61
+ @mcp.tool()
62
+ def submit_email_verification(emails: list[str], request_origin: str = None) -> dict:
63
+ """Submit one or more email addresses to RightEmails for verification.
64
+
65
+ This starts an async verification job and returns immediately with a
66
+ request_history_id and initial status (usually 'pending' or 'processing').
67
+ Use check_email_verification_status with that id to poll for results.
68
+
69
+ Args:
70
+ emails: List of email addresses to verify.
71
+ request_origin: Optional label for this request. Defaults to the
72
+ account's registered origin if not provided — usually fine to omit.
73
+
74
+ Returns:
75
+ The created request, including history.id (use this to poll) and history.status.
76
+ """
77
+ if not emails:
78
+ return {"error": "No emails provided"}
79
+
80
+ origin = request_origin or DEFAULT_REQUEST_ORIGIN
81
+
82
+ request_body = [{"email": e} for e in emails]
83
+
84
+ payload = {
85
+ "request_type": "verifier",
86
+ "request_origin": origin,
87
+ "request_body": request_body,
88
+ }
89
+
90
+ try:
91
+ with httpx.Client(timeout=30.0) as client:
92
+ response = client.post(
93
+ f"{BASE_URL}{CREATE_ENDPOINT}",
94
+ headers=_headers(),
95
+ json=payload,
96
+ )
97
+ return _handle_response(response)
98
+ except httpx.RequestError as e:
99
+ return {"error": "Request failed", "detail": str(e)}
100
+
101
+
102
+ @mcp.tool()
103
+ def check_email_verification_status(request_history_id: int) -> dict:
104
+ """Check the status/results of a previously submitted email verification request.
105
+
106
+ Args:
107
+ request_history_id: The id returned by submit_email_verification (history.id).
108
+
109
+ Returns:
110
+ The current state: history.status (pending/processing/completed/failed/etc.)
111
+ and details.results (list of {email, status}) once completed, plus
112
+ details.progress (0-1 fraction complete).
113
+ """
114
+ try:
115
+ with httpx.Client(timeout=15.0) as client:
116
+ response = client.get(
117
+ f"{BASE_URL}{GET_ENDPOINT.format(id=request_history_id)}",
118
+ headers=_headers(),
119
+ )
120
+ return _handle_response(response)
121
+ except httpx.RequestError as e:
122
+ return {"error": "Request failed", "detail": str(e)}
123
+
124
+
125
+ @mcp.tool()
126
+ def check_multiple_verification_statuses(request_history_ids: list[int]) -> dict:
127
+ """Check the status/results of multiple verification requests at once.
128
+
129
+ Args:
130
+ request_history_ids: List of ids returned by previous submit_email_verification calls.
131
+
132
+ Returns:
133
+ A list of request states, one per id, in the same shape as
134
+ check_email_verification_status.
135
+ """
136
+ if not request_history_ids:
137
+ return {"error": "No request_history_ids provided"}
138
+
139
+ try:
140
+ with httpx.Client(timeout=30.0) as client:
141
+ response = client.post(
142
+ f"{BASE_URL}{BATCH_ENDPOINT}",
143
+ headers=_headers(),
144
+ json={"request_history_ids": request_history_ids},
145
+ )
146
+ return _handle_response(response)
147
+ except httpx.RequestError as e:
148
+ return {"error": "Request failed", "detail": str(e)}
149
+
150
+
151
+ @mcp.tool()
152
+ def verify_emails_and_wait(
153
+ emails: list[str],
154
+ request_origin: str = None,
155
+ max_wait_seconds: int = 60,
156
+ poll_interval_seconds: int = 3,
157
+ ) -> dict:
158
+ """Submit emails for verification and poll until the job completes (or times out).
159
+
160
+ Convenience tool that combines submit + poll in one call, so you don't have
161
+ to manually call submit then check status yourself. Useful for small batches
162
+ where you want the final results directly.
163
+
164
+ Args:
165
+ emails: List of email addresses to verify.
166
+ request_origin: A label identifying the source of this request.
167
+ max_wait_seconds: Give up polling after this many seconds (default 60).
168
+ poll_interval_seconds: Seconds between status checks (default 3).
169
+
170
+ Returns:
171
+ The final request state once completed/failed, or the last known state
172
+ if max_wait_seconds is exceeded (with a 'timed_out': true flag).
173
+ """
174
+ submitted = submit_email_verification(emails, request_origin)
175
+ if "error" in submitted:
176
+ return submitted
177
+
178
+ history = submitted.get("history", {})
179
+ request_id = history.get("id")
180
+ if request_id is None:
181
+ return {"error": "No request id returned from submission", "raw": submitted}
182
+
183
+ elapsed = 0
184
+ last_state = submitted
185
+ while elapsed < max_wait_seconds:
186
+ time.sleep(poll_interval_seconds)
187
+ elapsed += poll_interval_seconds
188
+
189
+ last_state = check_email_verification_status(request_id)
190
+ if "error" in last_state:
191
+ return last_state
192
+
193
+ status = last_state.get("history", {}).get("status")
194
+ if status in ("completed", "failed"):
195
+ return last_state
196
+
197
+ last_state["timed_out"] = True
198
+ return last_state
199
+
200
+
201
+ def main():
202
+ mcp.run()
203
+
204
+
205
+ if __name__ == "__main__":
206
+ main()
@@ -0,0 +1,19 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "email-verifier-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for verifying email addresses via the RightEmails API"
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "mcp>=1.0.0",
12
+ "httpx>=0.27.0",
13
+ ]
14
+
15
+ [project.scripts]
16
+ email-verifier-mcp = "email_verifier_mcp:main"
17
+
18
+ [tool.hatch.build.targets.wheel]
19
+ include = ["email_verifier_mcp.py"]