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.
- email_verifier_mcp-0.1.0/.claude/settings.local.json +22 -0
- email_verifier_mcp-0.1.0/.gitignore +150 -0
- email_verifier_mcp-0.1.0/PKG-INFO +7 -0
- email_verifier_mcp-0.1.0/app.py +252 -0
- email_verifier_mcp-0.1.0/claude_desktop_config.json +11 -0
- email_verifier_mcp-0.1.0/email_verifier_mcp.py +206 -0
- email_verifier_mcp-0.1.0/pyproject.toml +19 -0
|
@@ -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,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 — 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 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> |
|
|
136
|
+
Status: <strong>${history.status ?? '—'}</strong>
|
|
137
|
+
${timedOut ? ' <span style="color:#dc2626">(timed out — partial results)</span>' : ''}
|
|
138
|
+
${history.created_at ? ' | 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,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"]
|