tan-en-yao commited on
Commit
c6ff5ab
·
1 Parent(s): 276dcca

feat(email): add PDF attachment support via report_id

Browse files
Files changed (3) hide show
  1. README.md +16 -1
  2. app.py +3 -2
  3. 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. Provide API key or set RESEND_API_KEY env var.",
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."