agent-jmap-mcp 0.2.0__py3-none-any.whl

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,25 @@
1
+ """agent-jmap-mcp: Stateless JMAP client and FastMCP server for AI agents."""
2
+
3
+ from agent_jmap_mcp.client import JMAPClient
4
+ from agent_jmap_mcp.models import (
5
+ EmailAddress,
6
+ EmailHeader,
7
+ EmailMessage,
8
+ EmailThread,
9
+ MailboxInfo,
10
+ TriageCategory,
11
+ TriageResult,
12
+ )
13
+
14
+ __version__ = "0.2.0"
15
+ __all__ = [
16
+ "EmailAddress",
17
+ "EmailHeader",
18
+ "EmailMessage",
19
+ "EmailThread",
20
+ "JMAPClient",
21
+ "MailboxInfo",
22
+ "TriageCategory",
23
+ "TriageResult",
24
+ "__version__",
25
+ ]
agent_jmap_mcp/cli.py ADDED
@@ -0,0 +1,257 @@
1
+ """Command line interface (CLI) for agent-jmap operations and server execution."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.table import Table
10
+
11
+ from agent_jmap_mcp.client import JMAPClient
12
+ from agent_jmap_mcp.server import mcp
13
+ from agent_jmap_mcp.triage import triage_inbox
14
+
15
+ console = Console()
16
+
17
+
18
+ def get_client_from_args(args: argparse.Namespace) -> JMAPClient:
19
+ session_url = args.session_url or os.environ.get("JMAP_SESSION_URL")
20
+ api_token = args.token or os.environ.get("JMAP_API_TOKEN")
21
+ account_id = args.account or os.environ.get("JMAP_ACCOUNT_ID")
22
+
23
+ if not session_url or not api_token:
24
+ console.print(
25
+ "[bold red]Error:[/bold red] Missing JMAP session URL or API token.\n"
26
+ "Set JMAP_SESSION_URL and JMAP_API_TOKEN env vars or pass --session-url and --token."
27
+ )
28
+ sys.exit(1)
29
+
30
+ return JMAPClient(session_url=session_url, token=api_token, account_id=account_id)
31
+
32
+
33
+ def build_parser() -> argparse.ArgumentParser:
34
+ parser = argparse.ArgumentParser(
35
+ prog="agent-jmap",
36
+ description="Stateless JMAP Email Client and Model Context Protocol Server for AI Agents.",
37
+ )
38
+ parser.add_argument("--session-url", help="JMAP session discovery URL")
39
+ parser.add_argument("--token", help="JMAP bearer API token")
40
+ parser.add_argument("--account", help="Specific account ID (optional)")
41
+
42
+ subparsers = parser.add_subparsers(dest="command", help="Available subcommands")
43
+
44
+ # serve
45
+ subparsers.add_parser("serve", help="Run the FastMCP stdio server")
46
+
47
+ # mailboxes
48
+ subparsers.add_parser("mailboxes", help="List all mailboxes/folders")
49
+
50
+ # list
51
+ list_p = subparsers.add_parser("list", help="List emails in a mailbox")
52
+ list_p.add_argument("--mailbox", default="INBOX", help="Target mailbox (default: INBOX)")
53
+ list_p.add_argument("--limit", type=int, default=15, help="Max results (default: 15)")
54
+ list_p.add_argument("--unread", action="store_true", help="Filter unread emails only")
55
+ list_p.add_argument("--query", help="Search text query")
56
+
57
+ # get
58
+ get_p = subparsers.add_parser("get", help="Get full details of an email by ID")
59
+ get_p.add_argument("email_id", help="JMAP Email ID")
60
+ get_p.add_argument("--read", action="store_true", help="Mark email as read")
61
+
62
+ # thread
63
+ thread_p = subparsers.add_parser("thread", help="View full conversation thread by Thread ID")
64
+ thread_p.add_argument("thread_id", help="JMAP Thread ID")
65
+
66
+ # download
67
+ dl_p = subparsers.add_parser("download", help="Download an email attachment by blob ID")
68
+ dl_p.add_argument("blob_id", help="JMAP Binary Blob ID")
69
+ dl_p.add_argument("--name", required=True, help="Target filename for saving")
70
+ dl_p.add_argument("--output", help="Output directory path (default: current directory)")
71
+
72
+ # send
73
+ send_p = subparsers.add_parser("send", help="Send an email atomically or save to Drafts")
74
+ send_p.add_argument("--to", required=True, nargs="+", help="Recipient email address(es)")
75
+ send_p.add_argument("--subject", required=True, help="Email subject")
76
+ send_p.add_argument("--body", required=True, help="Email body text")
77
+ send_p.add_argument("--from-addr", help="Sender email address")
78
+ send_p.add_argument("--cc", nargs="+", help="CC recipient address(es)")
79
+ send_p.add_argument("--bcc", nargs="+", help="BCC recipient address(es)")
80
+ send_p.add_argument("--draft", action="store_true", help="Save to Drafts instead of sending")
81
+
82
+ # triage
83
+ triage_p = subparsers.add_parser("triage", help="Run automated inbox triage digest")
84
+ triage_p.add_argument("--mailbox", default="INBOX", help="Mailbox to triage (default: INBOX)")
85
+ triage_p.add_argument("--limit", type=int, default=20, help="Number of emails to analyze")
86
+ triage_p.add_argument("--all", action="store_true", help="Include read emails in analysis")
87
+
88
+ return parser
89
+
90
+
91
+ def main() -> int:
92
+ parser = build_parser()
93
+ args = parser.parse_args()
94
+
95
+ if not args.command:
96
+ parser.print_help()
97
+ return 0
98
+
99
+ if args.command == "serve":
100
+ mcp.run()
101
+ return 0
102
+
103
+ try:
104
+ client = get_client_from_args(args)
105
+
106
+ if args.command == "mailboxes":
107
+ mailboxes = client.get_mailboxes()
108
+ table = Table(title="JMAP Mailboxes", show_header=True, header_style="bold cyan")
109
+ table.add_column("ID", style="dim")
110
+ table.add_column("Name", style="bold")
111
+ table.add_column("Role", style="magenta")
112
+ table.add_column("Unread", justify="right", style="bold red")
113
+ table.add_column("Total", justify="right")
114
+
115
+ for mb in mailboxes:
116
+ table.add_row(
117
+ mb.id,
118
+ mb.name,
119
+ mb.role or "-",
120
+ str(mb.unread_emails),
121
+ str(mb.total_emails),
122
+ )
123
+ console.print(table)
124
+
125
+ elif args.command == "list":
126
+ headers = client.list_emails(
127
+ mailbox_name=args.mailbox,
128
+ limit=args.limit,
129
+ unread_only=args.unread,
130
+ query=args.query,
131
+ )
132
+ table = Table(
133
+ title=f"Emails in {args.mailbox} ({len(headers)})",
134
+ show_header=True,
135
+ header_style="bold cyan",
136
+ )
137
+ table.add_column("ID", style="dim")
138
+ table.add_column("From", style="green")
139
+ table.add_column("Subject", style="bold")
140
+ table.add_column("Received", style="blue")
141
+ table.add_column("Status")
142
+
143
+ for h in headers:
144
+ sender = h.from_addr[0].format_string() if h.from_addr else "Unknown"
145
+ status = "[bold red]UNREAD[/bold red]" if h.unread else "[dim]READ[/dim]"
146
+ table.add_row(h.id, sender, h.subject, h.received_at[:10], status)
147
+ console.print(table)
148
+
149
+ elif args.command == "get":
150
+ msg = client.get_email(email_id=args.email_id, mark_as_read=args.read)
151
+ if not msg:
152
+ console.print(f"[bold red]Error:[/bold red] Email {args.email_id} not found.")
153
+ return 1
154
+
155
+ sender = msg.from_addr[0].format_string() if msg.from_addr else "Unknown"
156
+ recipients = ", ".join(t.format_string() for t in msg.to)
157
+ header_info = f"[bold]From:[/bold] {sender}\n[bold]To:[/bold] {recipients}\n[bold]Date:[/bold] {msg.received_at}\n[bold]Thread ID:[/bold] {msg.thread_id or '-'}"
158
+ if msg.attachments:
159
+ att_names = ", ".join(a.get("name", "unnamed") for a in msg.attachments)
160
+ header_info += f"\n[bold yellow]Attachments:[/bold yellow] {att_names}"
161
+
162
+ console.print(
163
+ Panel(
164
+ f"{header_info}\n\n{msg.body_text}",
165
+ title=f"[bold]{msg.subject}[/bold]",
166
+ border_style="cyan",
167
+ )
168
+ )
169
+
170
+ elif args.command == "thread":
171
+ thread = client.get_thread(thread_id=args.thread_id)
172
+ if not thread:
173
+ console.print(f"[bold red]Error:[/bold red] Thread {args.thread_id} not found.")
174
+ return 1
175
+
176
+ console.print(
177
+ f"[bold cyan]Thread:[/bold cyan] {thread.subject} ({thread.message_count} messages, Senders: {', '.join(thread.senders)})"
178
+ )
179
+ for i, msg in enumerate(thread.messages, 1):
180
+ sender = msg.from_addr[0].format_string() if msg.from_addr else "Unknown"
181
+ console.print(
182
+ Panel(
183
+ f"[bold]From:[/bold] {sender} | [bold]Date:[/bold] {msg.received_at}\n\n{msg.body_text}",
184
+ title=f"Message #{i} ({msg.id})",
185
+ border_style="blue",
186
+ )
187
+ )
188
+
189
+ elif args.command == "download":
190
+ out_dir = args.output or os.getcwd()
191
+ content = client.download_attachment(
192
+ blob_id=args.blob_id,
193
+ filename=args.name,
194
+ output_path=out_dir,
195
+ )
196
+ console.print(
197
+ f"[bold green]Saved:[/bold green] {args.name} ({len(content)} bytes) to {out_dir}"
198
+ )
199
+
200
+ elif args.command == "send":
201
+ res = client.send_email(
202
+ to=args.to,
203
+ subject=args.subject,
204
+ body=args.body,
205
+ from_addr=args.from_addr,
206
+ cc=args.cc,
207
+ bcc=args.bcc,
208
+ draft_only=args.draft,
209
+ )
210
+ console.print(
211
+ f"[bold green]Success:[/bold green] {res.get('status')} "
212
+ f"(Email ID: {res.get('emailId')}, Submission ID: {res.get('submissionId')})"
213
+ )
214
+
215
+ elif args.command == "triage":
216
+ res = triage_inbox(
217
+ client=client,
218
+ mailbox_name=args.mailbox,
219
+ limit=args.limit,
220
+ unread_only=not args.all,
221
+ )
222
+ table = Table(
223
+ title=f"Triage Digest for {res.mailbox} ({res.analyzed_count} analyzed)",
224
+ show_header=True,
225
+ header_style="bold yellow",
226
+ )
227
+ table.add_column("Prio", justify="center", style="bold")
228
+ table.add_column("Category", style="cyan")
229
+ table.add_column("From", style="green")
230
+ table.add_column("Subject", style="bold")
231
+ table.add_column("Action Summary")
232
+
233
+ for cat in res.categories:
234
+ prio_style = (
235
+ "bold red" if cat.priority <= 2 else "yellow" if cat.priority == 3 else "dim"
236
+ )
237
+ table.add_row(
238
+ f"[{prio_style}]P{cat.priority}[/{prio_style}]",
239
+ cat.category,
240
+ cat.from_address,
241
+ cat.subject[:40],
242
+ cat.summary,
243
+ )
244
+ console.print(table)
245
+ if res.recommended_actions:
246
+ console.print("\n[bold yellow]Recommended Action Items:[/bold yellow]")
247
+ for act in res.recommended_actions:
248
+ console.print(f" • {act}")
249
+
250
+ return 0
251
+ except Exception as e: # noqa: BLE001
252
+ console.print(f"[bold red]Execution Error:[/bold red] {e}")
253
+ return 1
254
+
255
+
256
+ if __name__ == "__main__":
257
+ sys.exit(main())