Commit ·
c6ff5ab
1
Parent(s): 276dcca
feat(email): add PDF attachment support via report_id
Browse files- README.md +16 -1
- app.py +3 -2
- tools/email.py +34 -2
README.md
CHANGED
|
@@ -39,7 +39,7 @@ An MCP (Model Context Protocol) tool server providing NYC infrastructure data fo
|
|
| 39 |
| `weather_get_current` | Get current weather + hazard assessment (real data via Weather.gov) | `lat`, `lon` |
|
| 40 |
| `get_department_info` | Get NYC department contact info and SLA targets (real data via NYC 311 SLA) | `department_code` |
|
| 41 |
| `pdf_generate_report` | Generate real PDF report (via ReportLab) | `issue_type`, `address`, `urgency`, `description` |
|
| 42 |
-
| `sendgrid_send_email` | Send email via Resend API (test mode - routes to test sink) | `to`, `subject`, `body`, `api_key`, `from_email` |
|
| 43 |
|
| 44 |
## Use with Claude Desktop
|
| 45 |
|
|
@@ -129,6 +129,21 @@ Local MCP endpoint: `http://localhost:7860/gradio_api/mcp/sse`
|
|
| 129 |
| `pdf_generate_report` | ReportLab (local generation) | N/A |
|
| 130 |
| `sendgrid_send_email` | Resend API (test sink: `delivered@resend.dev`) | Safe demo mode |
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
## Hackathon Track
|
| 133 |
|
| 134 |
**Track 1: Building MCP** - This server demonstrates how to build MCP tools with Gradio that can be used by any MCP-compatible client (Claude Desktop, AI agents, etc.).
|
|
|
|
| 39 |
| `weather_get_current` | Get current weather + hazard assessment (real data via Weather.gov) | `lat`, `lon` |
|
| 40 |
| `get_department_info` | Get NYC department contact info and SLA targets (real data via NYC 311 SLA) | `department_code` |
|
| 41 |
| `pdf_generate_report` | Generate real PDF report (via ReportLab) | `issue_type`, `address`, `urgency`, `description` |
|
| 42 |
+
| `sendgrid_send_email` | Send email via Resend API (test mode - routes to test sink) | `to`, `subject`, `body`, `api_key`, `from_email`, `report_id` |
|
| 43 |
|
| 44 |
## Use with Claude Desktop
|
| 45 |
|
|
|
|
| 129 |
| `pdf_generate_report` | ReportLab (local generation) | N/A |
|
| 130 |
| `sendgrid_send_email` | Resend API (test sink: `delivered@resend.dev`) | Safe demo mode |
|
| 131 |
|
| 132 |
+
## Complete Report Flow
|
| 133 |
+
|
| 134 |
+
The server supports a production-ready flow with safety guardrails:
|
| 135 |
+
|
| 136 |
+
```
|
| 137 |
+
1. pdf_generate_report() → Creates PDF, returns report_id (e.g., "FMN-20251127050500")
|
| 138 |
+
2. sendgrid_send_email(report_id="FMN-20251127050500") → Attaches PDF to email
|
| 139 |
+
→ Routes to test sink (delivered@resend.dev)
|
| 140 |
+
```
|
| 141 |
+
|
| 142 |
+
**Safety Features:**
|
| 143 |
+
- All emails route to `delivered@resend.dev` (Resend test sink)
|
| 144 |
+
- From address uses `onboarding@resend.dev` (no domain verification needed)
|
| 145 |
+
- Real Resend API is exercised, but no emails reach municipal inboxes
|
| 146 |
+
|
| 147 |
## Hackathon Track
|
| 148 |
|
| 149 |
**Track 1: Building MCP** - This server demonstrates how to build MCP tools with Gradio that can be used by any MCP-compatible client (Claude Desktop, AI agents, etc.).
|
app.py
CHANGED
|
@@ -114,11 +114,12 @@ email_interface = gr.Interface(
|
|
| 114 |
gr.Textbox(label="Subject", value="Infrastructure Issue Report"),
|
| 115 |
gr.Textbox(label="Body", lines=3, value="Please see attached report for infrastructure issue details."),
|
| 116 |
gr.Textbox(label="API Key (optional)", value="", type="password", placeholder="Resend API key (or set RESEND_API_KEY env var)"),
|
| 117 |
-
gr.Textbox(label="From Email (optional)", value="", placeholder="Sender email (or set RESEND_FROM_EMAIL env var)")
|
|
|
|
| 118 |
],
|
| 119 |
outputs=gr.JSON(label="Email Status"),
|
| 120 |
title="Send Email",
|
| 121 |
-
description="Send email report to municipal department
|
| 122 |
api_name="sendgrid_send_email"
|
| 123 |
)
|
| 124 |
|
|
|
|
| 114 |
gr.Textbox(label="Subject", value="Infrastructure Issue Report"),
|
| 115 |
gr.Textbox(label="Body", lines=3, value="Please see attached report for infrastructure issue details."),
|
| 116 |
gr.Textbox(label="API Key (optional)", value="", type="password", placeholder="Resend API key (or set RESEND_API_KEY env var)"),
|
| 117 |
+
gr.Textbox(label="From Email (optional)", value="", placeholder="Sender email (or set RESEND_FROM_EMAIL env var)"),
|
| 118 |
+
gr.Textbox(label="Report ID (optional)", value="", placeholder="Report ID to attach PDF (e.g., FMN-20251127050500)")
|
| 119 |
],
|
| 120 |
outputs=gr.JSON(label="Email Status"),
|
| 121 |
title="Send Email",
|
| 122 |
+
description="Send email report to municipal department with optional PDF attachment.",
|
| 123 |
api_name="sendgrid_send_email"
|
| 124 |
)
|
| 125 |
|
tools/email.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
| 1 |
"""Email notification using Resend API."""
|
|
|
|
|
|
|
|
|
|
| 2 |
from datetime import datetime
|
| 3 |
from tools.email_client import send_email as resend_send_email, is_configured
|
| 4 |
|
|
@@ -8,7 +11,8 @@ def sendgrid_send_email(
|
|
| 8 |
subject: str,
|
| 9 |
body: str,
|
| 10 |
api_key: str = "",
|
| 11 |
-
from_email: str = ""
|
|
|
|
| 12 |
) -> dict:
|
| 13 |
"""
|
| 14 |
Send an email notification using Resend API (demo mode - sends to test address).
|
|
@@ -22,6 +26,7 @@ def sendgrid_send_email(
|
|
| 22 |
body: Email body text
|
| 23 |
api_key: Resend API key (optional, defaults to RESEND_API_KEY env var)
|
| 24 |
from_email: Sender email (optional, defaults to RESEND_FROM_EMAIL env var)
|
|
|
|
| 25 |
|
| 26 |
Returns:
|
| 27 |
dict: Send result with status and message ID
|
|
@@ -29,6 +34,30 @@ def sendgrid_send_email(
|
|
| 29 |
# Test email address that Resend accepts
|
| 30 |
TEST_EMAIL = "delivered@resend.dev"
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
# If no API key provided or in env, return demo mode response
|
| 33 |
if not is_configured(api_key if api_key else None):
|
| 34 |
return {
|
|
@@ -38,6 +67,7 @@ def sendgrid_send_email(
|
|
| 38 |
"actual_recipient": "none (no API key)",
|
| 39 |
"subject": subject,
|
| 40 |
"body_preview": body[:100] + "..." if len(body) > 100 else body,
|
|
|
|
| 41 |
"timestamp": datetime.now().isoformat(),
|
| 42 |
"note": "Demo mode: No API key configured.",
|
| 43 |
"source": "demo"
|
|
@@ -49,12 +79,14 @@ def sendgrid_send_email(
|
|
| 49 |
subject=f"[Test] {subject}",
|
| 50 |
body=f"Intended recipient: {to}\n\n{body}",
|
| 51 |
api_key=api_key if api_key else None,
|
| 52 |
-
from_email=from_email if from_email else None
|
|
|
|
| 53 |
)
|
| 54 |
|
| 55 |
# Add context about test mode
|
| 56 |
result["intended_recipient"] = to
|
| 57 |
result["actual_recipient"] = TEST_EMAIL
|
|
|
|
| 58 |
result["timestamp"] = datetime.now().isoformat()
|
| 59 |
result["body_preview"] = body[:100] + "..." if len(body) > 100 else body
|
| 60 |
result["note"] = "Test mode: Email sent to Resend test address, not actual recipient."
|
|
|
|
| 1 |
"""Email notification using Resend API."""
|
| 2 |
+
import os
|
| 3 |
+
import base64
|
| 4 |
+
import tempfile
|
| 5 |
from datetime import datetime
|
| 6 |
from tools.email_client import send_email as resend_send_email, is_configured
|
| 7 |
|
|
|
|
| 11 |
subject: str,
|
| 12 |
body: str,
|
| 13 |
api_key: str = "",
|
| 14 |
+
from_email: str = "",
|
| 15 |
+
report_id: str = ""
|
| 16 |
) -> dict:
|
| 17 |
"""
|
| 18 |
Send an email notification using Resend API (demo mode - sends to test address).
|
|
|
|
| 26 |
body: Email body text
|
| 27 |
api_key: Resend API key (optional, defaults to RESEND_API_KEY env var)
|
| 28 |
from_email: Sender email (optional, defaults to RESEND_FROM_EMAIL env var)
|
| 29 |
+
report_id: Report ID to attach PDF (optional, e.g., "FMN-20251127050500")
|
| 30 |
|
| 31 |
Returns:
|
| 32 |
dict: Send result with status and message ID
|
|
|
|
| 34 |
# Test email address that Resend accepts
|
| 35 |
TEST_EMAIL = "delivered@resend.dev"
|
| 36 |
|
| 37 |
+
# Look for PDF attachment if report_id provided
|
| 38 |
+
attachments = None
|
| 39 |
+
attachment_info = None
|
| 40 |
+
if report_id:
|
| 41 |
+
pdf_path = os.path.join(tempfile.gettempdir(), f"{report_id}.pdf")
|
| 42 |
+
if os.path.exists(pdf_path):
|
| 43 |
+
with open(pdf_path, "rb") as f:
|
| 44 |
+
pdf_content = base64.b64encode(f.read()).decode("utf-8")
|
| 45 |
+
attachments = [{
|
| 46 |
+
"filename": f"{report_id}.pdf",
|
| 47 |
+
"content": pdf_content
|
| 48 |
+
}]
|
| 49 |
+
attachment_info = {
|
| 50 |
+
"filename": f"{report_id}.pdf",
|
| 51 |
+
"size_bytes": os.path.getsize(pdf_path),
|
| 52 |
+
"attached": True
|
| 53 |
+
}
|
| 54 |
+
else:
|
| 55 |
+
attachment_info = {
|
| 56 |
+
"filename": f"{report_id}.pdf",
|
| 57 |
+
"attached": False,
|
| 58 |
+
"error": "PDF not found - generate report first"
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
# If no API key provided or in env, return demo mode response
|
| 62 |
if not is_configured(api_key if api_key else None):
|
| 63 |
return {
|
|
|
|
| 67 |
"actual_recipient": "none (no API key)",
|
| 68 |
"subject": subject,
|
| 69 |
"body_preview": body[:100] + "..." if len(body) > 100 else body,
|
| 70 |
+
"attachment": attachment_info,
|
| 71 |
"timestamp": datetime.now().isoformat(),
|
| 72 |
"note": "Demo mode: No API key configured.",
|
| 73 |
"source": "demo"
|
|
|
|
| 79 |
subject=f"[Test] {subject}",
|
| 80 |
body=f"Intended recipient: {to}\n\n{body}",
|
| 81 |
api_key=api_key if api_key else None,
|
| 82 |
+
from_email=from_email if from_email else None,
|
| 83 |
+
attachments=attachments
|
| 84 |
)
|
| 85 |
|
| 86 |
# Add context about test mode
|
| 87 |
result["intended_recipient"] = to
|
| 88 |
result["actual_recipient"] = TEST_EMAIL
|
| 89 |
+
result["attachment"] = attachment_info
|
| 90 |
result["timestamp"] = datetime.now().isoformat()
|
| 91 |
result["body_preview"] = body[:100] + "..." if len(body) > 100 else body
|
| 92 |
result["note"] = "Test mode: Email sent to Resend test address, not actual recipient."
|