Revise contacts and email tools
All checks were successful
Build And Test / publish (push) Successful in 48s
All checks were successful
Build And Test / publish (push) Successful in 48s
This commit is contained in:
@@ -54,6 +54,7 @@ CALDAV_PASSWORD=your-caldav-password
|
|||||||
CARDDAV_URL=https://carddav.example.com/dav
|
CARDDAV_URL=https://carddav.example.com/dav
|
||||||
CARDDAV_USERNAME=user@example.com
|
CARDDAV_USERNAME=user@example.com
|
||||||
CARDDAV_PASSWORD=your-carddav-password
|
CARDDAV_PASSWORD=your-carddav-password
|
||||||
|
CONTACTS_ADDRESSBOOK_ID=/dav/addressbooks/users/user@example.com/contacts/
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Cache Configuration
|
# Cache Configuration
|
||||||
|
|||||||
34
README.md
34
README.md
@@ -4,7 +4,7 @@ A self-hosted MCP server that connects IMAP/SMTP, CalDAV, and CardDAV to MCP-com
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Email tools over IMAP/SMTP (list, read, search, send, drafts, flags, unsubscribe)
|
- Email tools over IMAP/SMTP (list, read, search, drafts, send drafts, flags, unsubscribe)
|
||||||
- Calendar tools over CalDAV (list, create, update, delete)
|
- Calendar tools over CalDAV (list, create, update, delete)
|
||||||
- Contacts tools over CardDAV (list, create, update, delete)
|
- Contacts tools over CardDAV (list, create, update, delete)
|
||||||
- Optional email notifications via webhook with IMAP IDLE or polling
|
- Optional email notifications via webhook with IMAP IDLE or polling
|
||||||
@@ -94,8 +94,11 @@ ICS_CALENDAR_TIMEOUT=20
|
|||||||
CARDDAV_URL=https://carddav.example.com/dav
|
CARDDAV_URL=https://carddav.example.com/dav
|
||||||
CARDDAV_USERNAME=you@example.com
|
CARDDAV_USERNAME=you@example.com
|
||||||
CARDDAV_PASSWORD=your-password
|
CARDDAV_PASSWORD=your-password
|
||||||
|
CONTACTS_ADDRESSBOOK_ID=/dav/addressbooks/users/you@example.com/contacts/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Contacts tools always use `CONTACTS_ADDRESSBOOK_ID`. Listing address books is not exposed via MCP.
|
||||||
|
|
||||||
ICS calendars are optional and read-only. Set `ICS_CALENDARS` to a comma-separated list of entries, each as `name|url` or just `url` if you want the name inferred.
|
ICS calendars are optional and read-only. Set `ICS_CALENDARS` to a comma-separated list of entries, each as `name|url` or just `url` if you want the name inferred.
|
||||||
|
|
||||||
### Email notifications (Poke webhook)
|
### Email notifications (Poke webhook)
|
||||||
@@ -145,32 +148,47 @@ Add your MCP endpoint at https://poke.com/settings/connections.
|
|||||||
|
|
||||||
| Category | Tools |
|
| Category | Tools |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Email | `list_mailboxes`, `list_emails`, `list_drafts`, `read_email`, `search_emails`, `move_email`, `delete_email`, `delete_draft`, `save_draft`, `edit_draft`, `send_email`, `set_email_flags`, `unsubscribe_email` |
|
| Email | `list_mailboxes`, `list_emails`, `list_drafts`, `read_email`, `search_emails`, `move_email`, `delete_email`, `delete_draft`, `save_draft`, `edit_draft`, `send_draft`, `set_email_flags`, `unsubscribe_maillist` |
|
||||||
| Calendar | `list_calendars`, `list_events`, `get_event`, `create_event`, `update_event`, `delete_event` |
|
| Calendar | `list_calendars`, `list_events`, `get_event`, `create_event`, `update_event`, `delete_event` |
|
||||||
| Contacts | `list_addressbooks`, `list_contacts`, `get_contact`, `create_contact`, `update_contact`, `delete_contact` |
|
| Contacts | `list_contacts`, `get_contact`, `create_contact`, `update_contact`, `delete_contact` |
|
||||||
| System | `get_server_info` |
|
| System | `get_server_info` |
|
||||||
|
|
||||||
|
### Sending email
|
||||||
|
|
||||||
|
Emails are sent only from drafts. Create or edit a draft with `save_draft`/`edit_draft`, then send it with `send_draft` using the returned draft ID.
|
||||||
|
|
||||||
### Replying to an email
|
### Replying to an email
|
||||||
|
|
||||||
Use `reply_to_email_id` on `save_draft`, `edit_draft`, or `send_email` to create a reply without a separate tool.
|
Use `in_reply_to_email_id` on `save_draft` or `edit_draft` to create a reply without a separate tool. Then send it with `send_draft`.
|
||||||
|
|
||||||
- Provide `reply_to_email_id` (and optionally `reply_mailbox`, default `INBOX`).
|
- Provide `in_reply_to_email_id` (and optionally `in_reply_to_mailbox`, default `INBOX`).
|
||||||
- `reply_all=true` includes original recipients; otherwise it replies to the sender/Reply-To.
|
- `reply_all=true` includes original recipients; otherwise it replies to the sender/Reply-To.
|
||||||
- If `to`/`subject` are omitted, they are derived from the original email; `body` is still required.
|
- If `to`/`subject` are omitted, they are derived from the original email; `body` is still required.
|
||||||
|
- `in_reply_to_email_id` is the email UID from `list_emails`/`read_email`, not the RFC Message-ID header.
|
||||||
|
|
||||||
Example (send a reply):
|
Example (send a reply):
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"tool": "send_email",
|
"tool": "save_draft",
|
||||||
"args": {
|
"args": {
|
||||||
"reply_to_email_id": "12345",
|
"in_reply_to_email_id": "12345",
|
||||||
"reply_mailbox": "INBOX",
|
"in_reply_to_mailbox": "INBOX",
|
||||||
"reply_all": true,
|
"reply_all": true,
|
||||||
"body": "Thanks — sounds good to me."
|
"body": "Thanks — sounds good to me."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Then send the draft by its returned ID:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool": "send_draft",
|
||||||
|
"args": {
|
||||||
|
"email_id": "67890"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Database and Migrations
|
## Database and Migrations
|
||||||
|
|
||||||
The server uses SQLite (default: `/data/cache.db`) and Alembic.
|
The server uses SQLite (default: `/data/cache.db`) and Alembic.
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ class Settings(BaseSettings):
|
|||||||
carddav_url: Optional[str] = Field(default=None, alias="CARDDAV_URL")
|
carddav_url: Optional[str] = Field(default=None, alias="CARDDAV_URL")
|
||||||
carddav_username: Optional[str] = Field(default=None, alias="CARDDAV_USERNAME")
|
carddav_username: Optional[str] = Field(default=None, alias="CARDDAV_USERNAME")
|
||||||
carddav_password: Optional[SecretStr] = Field(default=None, alias="CARDDAV_PASSWORD")
|
carddav_password: Optional[SecretStr] = Field(default=None, alias="CARDDAV_PASSWORD")
|
||||||
|
contacts_addressbook_id: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
alias="CONTACTS_ADDRESSBOOK_ID",
|
||||||
|
)
|
||||||
|
|
||||||
# SQLite Cache
|
# SQLite Cache
|
||||||
sqlite_path: str = Field(default="/data/cache.db", alias="SQLITE_PATH")
|
sqlite_path: str = Field(default="/data/cache.db", alias="SQLITE_PATH")
|
||||||
@@ -150,6 +154,7 @@ class Settings(BaseSettings):
|
|||||||
self.carddav_url,
|
self.carddav_url,
|
||||||
self.carddav_username,
|
self.carddav_username,
|
||||||
self.carddav_password,
|
self.carddav_password,
|
||||||
|
self.contacts_addressbook_id,
|
||||||
])
|
])
|
||||||
|
|
||||||
def is_notification_configured(self) -> bool:
|
def is_notification_configured(self) -> bool:
|
||||||
|
|||||||
@@ -114,12 +114,13 @@ class ContactsService:
|
|||||||
|
|
||||||
def list_contacts(
|
def list_contacts(
|
||||||
self,
|
self,
|
||||||
addressbook_id: str,
|
addressbook_id: Optional[str] = None,
|
||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> ContactList:
|
) -> ContactList:
|
||||||
client = self._get_client()
|
client = self._get_client()
|
||||||
|
addressbook_id = self._resolve_addressbook_id(addressbook_id)
|
||||||
|
|
||||||
# Build URL
|
# Build URL
|
||||||
addressbook_url = self._build_url(addressbook_id)
|
addressbook_url = self._build_url(addressbook_id)
|
||||||
@@ -188,8 +189,9 @@ class ContactsService:
|
|||||||
offset=offset,
|
offset=offset,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_contact(self, addressbook_id: str, contact_id: str) -> Optional[Contact]:
|
def get_contact(self, contact_id: str, addressbook_id: Optional[str] = None) -> Optional[Contact]:
|
||||||
client = self._get_client()
|
client = self._get_client()
|
||||||
|
addressbook_id = self._resolve_addressbook_id(addressbook_id)
|
||||||
|
|
||||||
# Build URL
|
# Build URL
|
||||||
contact_url = self._build_url(contact_id)
|
contact_url = self._build_url(contact_id)
|
||||||
@@ -206,7 +208,7 @@ class ContactsService:
|
|||||||
|
|
||||||
def create_contact(
|
def create_contact(
|
||||||
self,
|
self,
|
||||||
addressbook_id: str,
|
addressbook_id: Optional[str] = None,
|
||||||
first_name: Optional[str] = None,
|
first_name: Optional[str] = None,
|
||||||
last_name: Optional[str] = None,
|
last_name: Optional[str] = None,
|
||||||
display_name: Optional[str] = None,
|
display_name: Optional[str] = None,
|
||||||
@@ -219,6 +221,7 @@ class ContactsService:
|
|||||||
birthday: Optional[str] = None,
|
birthday: Optional[str] = None,
|
||||||
) -> Contact:
|
) -> Contact:
|
||||||
client = self._get_client()
|
client = self._get_client()
|
||||||
|
addressbook_id = self._resolve_addressbook_id(addressbook_id)
|
||||||
|
|
||||||
# Create vCard
|
# Create vCard
|
||||||
vcard = vobject.vCard()
|
vcard = vobject.vCard()
|
||||||
@@ -311,8 +314,8 @@ class ContactsService:
|
|||||||
|
|
||||||
def update_contact(
|
def update_contact(
|
||||||
self,
|
self,
|
||||||
addressbook_id: str,
|
|
||||||
contact_id: str,
|
contact_id: str,
|
||||||
|
addressbook_id: Optional[str] = None,
|
||||||
first_name: Optional[str] = None,
|
first_name: Optional[str] = None,
|
||||||
last_name: Optional[str] = None,
|
last_name: Optional[str] = None,
|
||||||
display_name: Optional[str] = None,
|
display_name: Optional[str] = None,
|
||||||
@@ -324,7 +327,7 @@ class ContactsService:
|
|||||||
notes: Optional[str] = None,
|
notes: Optional[str] = None,
|
||||||
) -> Optional[Contact]:
|
) -> Optional[Contact]:
|
||||||
# Get existing contact
|
# Get existing contact
|
||||||
existing = self.get_contact(addressbook_id, contact_id)
|
existing = self.get_contact(contact_id, addressbook_id)
|
||||||
if not existing:
|
if not existing:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -342,12 +345,13 @@ class ContactsService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Delete and recreate (simpler than partial update)
|
# Delete and recreate (simpler than partial update)
|
||||||
self.delete_contact(addressbook_id, contact_id)
|
self.delete_contact(contact_id, addressbook_id)
|
||||||
return self.create_contact(addressbook_id, **updated_data)
|
return self.create_contact(addressbook_id, **updated_data)
|
||||||
|
|
||||||
def delete_contact(self, addressbook_id: str, contact_id: str) -> OperationResult:
|
def delete_contact(self, contact_id: str, addressbook_id: Optional[str] = None) -> OperationResult:
|
||||||
try:
|
try:
|
||||||
client = self._get_client()
|
client = self._get_client()
|
||||||
|
addressbook_id = self._resolve_addressbook_id(addressbook_id)
|
||||||
|
|
||||||
# Build URL
|
# Build URL
|
||||||
contact_url = self._build_url(contact_id)
|
contact_url = self._build_url(contact_id)
|
||||||
@@ -481,3 +485,10 @@ class ContactsService:
|
|||||||
notes=notes,
|
notes=notes,
|
||||||
birthday=birthday,
|
birthday=birthday,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _resolve_addressbook_id(self, addressbook_id: Optional[str]) -> str:
|
||||||
|
if addressbook_id:
|
||||||
|
return addressbook_id
|
||||||
|
if self.settings.contacts_addressbook_id:
|
||||||
|
return self.settings.contacts_addressbook_id
|
||||||
|
raise ValueError("CONTACTS_ADDRESSBOOK_ID must be set to use contacts tools")
|
||||||
|
|||||||
@@ -438,19 +438,18 @@ class EmailService:
|
|||||||
body: Optional[str] = None,
|
body: Optional[str] = None,
|
||||||
cc: Optional[list[str]] = None,
|
cc: Optional[list[str]] = None,
|
||||||
bcc: Optional[list[str]] = None,
|
bcc: Optional[list[str]] = None,
|
||||||
reply_to: Optional[str] = None,
|
|
||||||
html_body: Optional[str] = None,
|
html_body: Optional[str] = None,
|
||||||
mailbox: Optional[str] = None,
|
mailbox: Optional[str] = None,
|
||||||
reply_to_email_id: Optional[str] = None,
|
in_reply_to_email_id: Optional[str] = None,
|
||||||
reply_mailbox: Optional[str] = None,
|
in_reply_to_mailbox: Optional[str] = None,
|
||||||
reply_all: bool = False,
|
reply_all: bool = False,
|
||||||
) -> OperationResult:
|
) -> OperationResult:
|
||||||
try:
|
try:
|
||||||
draft_mailbox = mailbox or self._find_drafts_folder() or "Drafts"
|
draft_mailbox = mailbox or self._find_drafts_folder() or "Drafts"
|
||||||
if reply_to_email_id:
|
if in_reply_to_email_id:
|
||||||
context, error = self._get_reply_context(
|
context, error = self._get_reply_context(
|
||||||
reply_mailbox or "INBOX",
|
in_reply_to_mailbox or "INBOX",
|
||||||
reply_to_email_id,
|
in_reply_to_email_id,
|
||||||
reply_all,
|
reply_all,
|
||||||
cc,
|
cc,
|
||||||
self.settings.smtp_from_email,
|
self.settings.smtp_from_email,
|
||||||
@@ -482,7 +481,6 @@ class EmailService:
|
|||||||
body=body,
|
body=body,
|
||||||
cc=cc,
|
cc=cc,
|
||||||
bcc=bcc,
|
bcc=bcc,
|
||||||
reply_to=reply_to,
|
|
||||||
html_body=html_body,
|
html_body=html_body,
|
||||||
in_reply_to=in_reply_to,
|
in_reply_to=in_reply_to,
|
||||||
references=references,
|
references=references,
|
||||||
@@ -513,10 +511,9 @@ class EmailService:
|
|||||||
body: Optional[str] = None,
|
body: Optional[str] = None,
|
||||||
cc: Optional[list[str]] = None,
|
cc: Optional[list[str]] = None,
|
||||||
bcc: Optional[list[str]] = None,
|
bcc: Optional[list[str]] = None,
|
||||||
reply_to: Optional[str] = None,
|
|
||||||
html_body: Optional[str] = None,
|
html_body: Optional[str] = None,
|
||||||
reply_to_email_id: Optional[str] = None,
|
in_reply_to_email_id: Optional[str] = None,
|
||||||
reply_mailbox: Optional[str] = None,
|
in_reply_to_mailbox: Optional[str] = None,
|
||||||
reply_all: bool = False,
|
reply_all: bool = False,
|
||||||
) -> OperationResult:
|
) -> OperationResult:
|
||||||
draft_mailbox = mailbox or self._find_drafts_folder() or "Drafts"
|
draft_mailbox = mailbox or self._find_drafts_folder() or "Drafts"
|
||||||
@@ -539,10 +536,10 @@ class EmailService:
|
|||||||
resolved_body = body if body is not None else (existing.body_text or "")
|
resolved_body = body if body is not None else (existing.body_text or "")
|
||||||
resolved_html = html_body if html_body is not None else existing.body_html
|
resolved_html = html_body if html_body is not None else existing.body_html
|
||||||
|
|
||||||
if reply_to_email_id:
|
if in_reply_to_email_id:
|
||||||
context, error = self._get_reply_context(
|
context, error = self._get_reply_context(
|
||||||
reply_mailbox or "INBOX",
|
in_reply_to_mailbox or "INBOX",
|
||||||
reply_to_email_id,
|
in_reply_to_email_id,
|
||||||
reply_all,
|
reply_all,
|
||||||
cc,
|
cc,
|
||||||
self.settings.smtp_from_email,
|
self.settings.smtp_from_email,
|
||||||
@@ -574,7 +571,6 @@ class EmailService:
|
|||||||
body=resolved_body,
|
body=resolved_body,
|
||||||
cc=resolved_cc,
|
cc=resolved_cc,
|
||||||
bcc=resolved_bcc,
|
bcc=resolved_bcc,
|
||||||
reply_to=reply_to,
|
|
||||||
html_body=resolved_html,
|
html_body=resolved_html,
|
||||||
in_reply_to=in_reply_to,
|
in_reply_to=in_reply_to,
|
||||||
references=references,
|
references=references,
|
||||||
@@ -602,22 +598,21 @@ class EmailService:
|
|||||||
body: Optional[str] = None,
|
body: Optional[str] = None,
|
||||||
cc: Optional[list[str]] = None,
|
cc: Optional[list[str]] = None,
|
||||||
bcc: Optional[list[str]] = None,
|
bcc: Optional[list[str]] = None,
|
||||||
reply_to: Optional[str] = None,
|
|
||||||
html_body: Optional[str] = None,
|
html_body: Optional[str] = None,
|
||||||
sender_email: Optional[str] = None,
|
sender_email: Optional[str] = None,
|
||||||
sender_name: Optional[str] = None,
|
sender_name: Optional[str] = None,
|
||||||
in_reply_to: Optional[str] = None,
|
in_reply_to: Optional[str] = None,
|
||||||
references: Optional[list[str]] = None,
|
references: Optional[list[str]] = None,
|
||||||
reply_to_email_id: Optional[str] = None,
|
in_reply_to_email_id: Optional[str] = None,
|
||||||
reply_mailbox: Optional[str] = None,
|
in_reply_to_mailbox: Optional[str] = None,
|
||||||
reply_all: bool = False,
|
reply_all: bool = False,
|
||||||
) -> OperationResult:
|
) -> OperationResult:
|
||||||
try:
|
try:
|
||||||
if reply_to_email_id:
|
if in_reply_to_email_id:
|
||||||
resolved_name, resolved_email = self._resolve_sender(sender_email, sender_name)
|
resolved_name, resolved_email = self._resolve_sender(sender_email, sender_name)
|
||||||
context, error = self._get_reply_context(
|
context, error = self._get_reply_context(
|
||||||
reply_mailbox or "INBOX",
|
in_reply_to_mailbox or "INBOX",
|
||||||
reply_to_email_id,
|
in_reply_to_email_id,
|
||||||
reply_all,
|
reply_all,
|
||||||
cc,
|
cc,
|
||||||
resolved_email,
|
resolved_email,
|
||||||
@@ -648,8 +643,6 @@ class EmailService:
|
|||||||
|
|
||||||
if cc:
|
if cc:
|
||||||
msg["Cc"] = ", ".join(cc)
|
msg["Cc"] = ", ".join(cc)
|
||||||
if reply_to:
|
|
||||||
msg["Reply-To"] = reply_to
|
|
||||||
if in_reply_to:
|
if in_reply_to:
|
||||||
msg["In-Reply-To"] = in_reply_to
|
msg["In-Reply-To"] = in_reply_to
|
||||||
if references:
|
if references:
|
||||||
@@ -687,6 +680,62 @@ class EmailService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return OperationResult(success=False, message=str(e))
|
return OperationResult(success=False, message=str(e))
|
||||||
|
|
||||||
|
async def send_draft(
|
||||||
|
self,
|
||||||
|
email_id: str,
|
||||||
|
mailbox: Optional[str] = None,
|
||||||
|
) -> OperationResult:
|
||||||
|
draft_mailbox = mailbox or self._find_drafts_folder() or "Drafts"
|
||||||
|
try:
|
||||||
|
draft = self.read_email(draft_mailbox, email_id, format="both")
|
||||||
|
except Exception as e:
|
||||||
|
return OperationResult(success=False, message=str(e), id=email_id)
|
||||||
|
|
||||||
|
if not draft:
|
||||||
|
return OperationResult(
|
||||||
|
success=False,
|
||||||
|
message=f"Draft {email_id} not found in {draft_mailbox}",
|
||||||
|
id=email_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
to = [addr.email for addr in draft.to_addresses]
|
||||||
|
cc = [addr.email for addr in draft.cc_addresses]
|
||||||
|
bcc = [addr.email for addr in draft.bcc_addresses]
|
||||||
|
|
||||||
|
if not to and not cc and not bcc:
|
||||||
|
return OperationResult(
|
||||||
|
success=False,
|
||||||
|
message="Draft has no recipients",
|
||||||
|
id=email_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = draft.subject or "(No Subject)"
|
||||||
|
body = draft.body_text or ""
|
||||||
|
html_body = draft.body_html
|
||||||
|
|
||||||
|
result = await self.send_email(
|
||||||
|
to=to or None,
|
||||||
|
subject=subject,
|
||||||
|
body=body,
|
||||||
|
cc=cc or None,
|
||||||
|
bcc=bcc or None,
|
||||||
|
html_body=html_body,
|
||||||
|
in_reply_to=draft.in_reply_to,
|
||||||
|
references=draft.references or None,
|
||||||
|
)
|
||||||
|
if not result.success:
|
||||||
|
return result
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = self._get_imap_client()
|
||||||
|
client.select_folder(draft_mailbox)
|
||||||
|
client.delete_messages([int(email_id)])
|
||||||
|
client.expunge()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
async def reply_email(
|
async def reply_email(
|
||||||
self,
|
self,
|
||||||
mailbox: str,
|
mailbox: str,
|
||||||
@@ -695,7 +744,6 @@ class EmailService:
|
|||||||
reply_all: bool = False,
|
reply_all: bool = False,
|
||||||
cc: Optional[list[str]] = None,
|
cc: Optional[list[str]] = None,
|
||||||
bcc: Optional[list[str]] = None,
|
bcc: Optional[list[str]] = None,
|
||||||
reply_to: Optional[str] = None,
|
|
||||||
html_body: Optional[str] = None,
|
html_body: Optional[str] = None,
|
||||||
sender_email: Optional[str] = None,
|
sender_email: Optional[str] = None,
|
||||||
sender_name: Optional[str] = None,
|
sender_name: Optional[str] = None,
|
||||||
@@ -703,12 +751,11 @@ class EmailService:
|
|||||||
return await self.send_email(
|
return await self.send_email(
|
||||||
body=body,
|
body=body,
|
||||||
bcc=bcc,
|
bcc=bcc,
|
||||||
reply_to=reply_to,
|
|
||||||
html_body=html_body,
|
html_body=html_body,
|
||||||
sender_email=sender_email,
|
sender_email=sender_email,
|
||||||
sender_name=sender_name,
|
sender_name=sender_name,
|
||||||
reply_to_email_id=email_id,
|
in_reply_to_email_id=email_id,
|
||||||
reply_mailbox=mailbox,
|
in_reply_to_mailbox=mailbox,
|
||||||
reply_all=reply_all,
|
reply_all=reply_all,
|
||||||
cc=cc,
|
cc=cc,
|
||||||
)
|
)
|
||||||
@@ -844,7 +891,6 @@ class EmailService:
|
|||||||
body: str,
|
body: str,
|
||||||
cc: Optional[list[str]] = None,
|
cc: Optional[list[str]] = None,
|
||||||
bcc: Optional[list[str]] = None,
|
bcc: Optional[list[str]] = None,
|
||||||
reply_to: Optional[str] = None,
|
|
||||||
html_body: Optional[str] = None,
|
html_body: Optional[str] = None,
|
||||||
in_reply_to: Optional[str] = None,
|
in_reply_to: Optional[str] = None,
|
||||||
references: Optional[list[str]] = None,
|
references: Optional[list[str]] = None,
|
||||||
@@ -862,8 +908,6 @@ class EmailService:
|
|||||||
msg["Cc"] = ", ".join(cc)
|
msg["Cc"] = ", ".join(cc)
|
||||||
if bcc:
|
if bcc:
|
||||||
msg["Bcc"] = ", ".join(bcc)
|
msg["Bcc"] = ", ".join(bcc)
|
||||||
if reply_to:
|
|
||||||
msg["Reply-To"] = reply_to
|
|
||||||
if in_reply_to:
|
if in_reply_to:
|
||||||
msg["In-Reply-To"] = in_reply_to
|
msg["In-Reply-To"] = in_reply_to
|
||||||
if references:
|
if references:
|
||||||
|
|||||||
@@ -8,53 +8,41 @@ from tools.logging_utils import log_tool_call
|
|||||||
def register_contacts_tools(mcp: FastMCP, service: ContactsService):
|
def register_contacts_tools(mcp: FastMCP, service: ContactsService):
|
||||||
"""Register all contacts-related MCP tools."""
|
"""Register all contacts-related MCP tools."""
|
||||||
|
|
||||||
@mcp.tool(description="List all available address books from the CardDAV server.")
|
@mcp.tool(description="List contacts in the configured address book with optional search filtering and pagination.")
|
||||||
@log_tool_call
|
|
||||||
def list_addressbooks() -> list[dict]:
|
|
||||||
"""List all address books."""
|
|
||||||
addressbooks = service.list_addressbooks()
|
|
||||||
return [a.model_dump() for a in addressbooks]
|
|
||||||
|
|
||||||
@mcp.tool(description="List contacts in an address book with optional search filtering and pagination.")
|
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
def list_contacts(
|
def list_contacts(
|
||||||
addressbook_id: str,
|
|
||||||
search: Optional[str] = None,
|
search: Optional[str] = None,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
List contacts in an address book.
|
List contacts in the configured address book.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
addressbook_id: The address book ID (URL path) to query
|
|
||||||
search: Optional search term to filter contacts by name or email
|
search: Optional search term to filter contacts by name or email
|
||||||
limit: Maximum number of contacts to return (default: 100)
|
limit: Maximum number of contacts to return (default: 100)
|
||||||
offset: Number of contacts to skip for pagination (default: 0)
|
offset: Number of contacts to skip for pagination (default: 0)
|
||||||
"""
|
"""
|
||||||
result = service.list_contacts(addressbook_id, search, limit, offset)
|
result = service.list_contacts(search=search, limit=limit, offset=offset)
|
||||||
return result.model_dump()
|
return result.model_dump()
|
||||||
|
|
||||||
@mcp.tool(description="Get detailed information about a specific contact including all fields.")
|
@mcp.tool(description="Get detailed information about a specific contact including all fields.")
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
def get_contact(
|
def get_contact(
|
||||||
addressbook_id: str,
|
|
||||||
contact_id: str,
|
contact_id: str,
|
||||||
) -> Optional[dict]:
|
) -> Optional[dict]:
|
||||||
"""
|
"""
|
||||||
Get a specific contact.
|
Get a specific contact.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
addressbook_id: The address book containing the contact
|
|
||||||
contact_id: The unique ID (URL) of the contact
|
contact_id: The unique ID (URL) of the contact
|
||||||
"""
|
"""
|
||||||
result = service.get_contact(addressbook_id, contact_id)
|
result = service.get_contact(contact_id)
|
||||||
return result.model_dump() if result else None
|
return result.model_dump() if result else None
|
||||||
|
|
||||||
@mcp.tool(description="Create a new contact with name, emails, phones, addresses, and other details.")
|
@mcp.tool(description="Create a new contact with name, emails, phones, addresses, and other details.")
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
def create_contact(
|
def create_contact(
|
||||||
addressbook_id: str,
|
|
||||||
first_name: Optional[str] = None,
|
first_name: Optional[str] = None,
|
||||||
last_name: Optional[str] = None,
|
last_name: Optional[str] = None,
|
||||||
display_name: Optional[str] = None,
|
display_name: Optional[str] = None,
|
||||||
@@ -70,7 +58,6 @@ def register_contacts_tools(mcp: FastMCP, service: ContactsService):
|
|||||||
Create a new contact.
|
Create a new contact.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
addressbook_id: The address book ID to create the contact in
|
|
||||||
first_name: Contact's first/given name
|
first_name: Contact's first/given name
|
||||||
last_name: Contact's last/family name
|
last_name: Contact's last/family name
|
||||||
display_name: Full display name (auto-generated if not provided)
|
display_name: Full display name (auto-generated if not provided)
|
||||||
@@ -83,24 +70,23 @@ def register_contacts_tools(mcp: FastMCP, service: ContactsService):
|
|||||||
birthday: Birthday in ISO format (YYYY-MM-DD)
|
birthday: Birthday in ISO format (YYYY-MM-DD)
|
||||||
"""
|
"""
|
||||||
result = service.create_contact(
|
result = service.create_contact(
|
||||||
addressbook_id,
|
addressbook_id=None,
|
||||||
first_name,
|
first_name=first_name,
|
||||||
last_name,
|
last_name=last_name,
|
||||||
display_name,
|
display_name=display_name,
|
||||||
emails,
|
emails=emails,
|
||||||
phones,
|
phones=phones,
|
||||||
addresses,
|
addresses=addresses,
|
||||||
organization,
|
organization=organization,
|
||||||
title,
|
title=title,
|
||||||
notes,
|
notes=notes,
|
||||||
birthday,
|
birthday=birthday,
|
||||||
)
|
)
|
||||||
return result.model_dump()
|
return result.model_dump()
|
||||||
|
|
||||||
@mcp.tool(description="Update an existing contact. Only provided fields will be modified.")
|
@mcp.tool(description="Update an existing contact. Only provided fields will be modified.")
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
def update_contact(
|
def update_contact(
|
||||||
addressbook_id: str,
|
|
||||||
contact_id: str,
|
contact_id: str,
|
||||||
first_name: Optional[str] = None,
|
first_name: Optional[str] = None,
|
||||||
last_name: Optional[str] = None,
|
last_name: Optional[str] = None,
|
||||||
@@ -116,7 +102,6 @@ def register_contacts_tools(mcp: FastMCP, service: ContactsService):
|
|||||||
Update an existing contact.
|
Update an existing contact.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
addressbook_id: The address book containing the contact
|
|
||||||
contact_id: The unique ID of the contact to update
|
contact_id: The unique ID of the contact to update
|
||||||
first_name: New first name (optional)
|
first_name: New first name (optional)
|
||||||
last_name: New last name (optional)
|
last_name: New last name (optional)
|
||||||
@@ -129,32 +114,30 @@ def register_contacts_tools(mcp: FastMCP, service: ContactsService):
|
|||||||
notes: New notes (optional)
|
notes: New notes (optional)
|
||||||
"""
|
"""
|
||||||
result = service.update_contact(
|
result = service.update_contact(
|
||||||
addressbook_id,
|
|
||||||
contact_id,
|
contact_id,
|
||||||
first_name,
|
addressbook_id=None,
|
||||||
last_name,
|
first_name=first_name,
|
||||||
display_name,
|
last_name=last_name,
|
||||||
emails,
|
display_name=display_name,
|
||||||
phones,
|
emails=emails,
|
||||||
addresses,
|
phones=phones,
|
||||||
organization,
|
addresses=addresses,
|
||||||
title,
|
organization=organization,
|
||||||
notes,
|
title=title,
|
||||||
|
notes=notes,
|
||||||
)
|
)
|
||||||
return result.model_dump() if result else None
|
return result.model_dump() if result else None
|
||||||
|
|
||||||
@mcp.tool(description="Delete a contact from an address book.")
|
@mcp.tool(description="Delete a contact from an address book.")
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
def delete_contact(
|
def delete_contact(
|
||||||
addressbook_id: str,
|
|
||||||
contact_id: str,
|
contact_id: str,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Delete a contact.
|
Delete a contact.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
addressbook_id: The address book containing the contact
|
|
||||||
contact_id: The unique ID of the contact to delete
|
contact_id: The unique ID of the contact to delete
|
||||||
"""
|
"""
|
||||||
result = service.delete_contact(addressbook_id, contact_id)
|
result = service.delete_contact(contact_id)
|
||||||
return result.model_dump()
|
return result.model_dump()
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
|||||||
result = service.delete_draft(email_id, mailbox, permanent)
|
result = service.delete_draft(email_id, mailbox, permanent)
|
||||||
return result.model_dump()
|
return result.model_dump()
|
||||||
|
|
||||||
@mcp.tool(description="Save a new draft email to the Drafts mailbox.")
|
@mcp.tool(description="Save a new draft email to the Drafts mailbox. Supports reply threading via in_reply_to_email_id.")
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
def save_draft(
|
def save_draft(
|
||||||
to: Optional[list[str]] = None,
|
to: Optional[list[str]] = None,
|
||||||
@@ -161,27 +161,23 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
|||||||
body: Optional[str] = None,
|
body: Optional[str] = None,
|
||||||
cc: Optional[list[str]] = None,
|
cc: Optional[list[str]] = None,
|
||||||
bcc: Optional[list[str]] = None,
|
bcc: Optional[list[str]] = None,
|
||||||
reply_to: Optional[str] = None,
|
|
||||||
html_body: Optional[str] = None,
|
|
||||||
mailbox: Optional[str] = None,
|
mailbox: Optional[str] = None,
|
||||||
reply_to_email_id: Optional[str] = None,
|
in_reply_to_email_id: Optional[str] = None,
|
||||||
reply_mailbox: Optional[str] = None,
|
in_reply_to_mailbox: Optional[str] = None,
|
||||||
reply_all: bool = False,
|
reply_all: bool = False,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Save a new email draft.
|
Save a new email draft.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
to: List of recipient email addresses (required unless reply_to_email_id is set)
|
to: List of recipient email addresses (required unless in_reply_to_email_id is set)
|
||||||
subject: Email subject line (required unless reply_to_email_id is set)
|
subject: Email subject line (required unless in_reply_to_email_id is set)
|
||||||
body: Plain text email body (required unless reply_to_email_id is set)
|
body: Plain text email body (required unless in_reply_to_email_id is set)
|
||||||
cc: List of CC recipients (optional)
|
cc: List of CC recipients (optional)
|
||||||
bcc: List of BCC recipients (optional)
|
bcc: List of BCC recipients (optional)
|
||||||
reply_to: Reply-to address (optional)
|
|
||||||
html_body: HTML version of the email body (optional)
|
|
||||||
mailbox: Drafts mailbox/folder override (default: auto-detect)
|
mailbox: Drafts mailbox/folder override (default: auto-detect)
|
||||||
reply_to_email_id: Email ID to reply to (optional)
|
in_reply_to_email_id: Email UID to reply to (optional, derives recipients/subject and sets threading headers)
|
||||||
reply_mailbox: Mailbox containing the reply_to_email_id (default: INBOX)
|
in_reply_to_mailbox: Mailbox containing the in_reply_to_email_id (default: INBOX)
|
||||||
reply_all: Whether to include original recipients when replying (default: False)
|
reply_all: Whether to include original recipients when replying (default: False)
|
||||||
"""
|
"""
|
||||||
result = service.save_draft(
|
result = service.save_draft(
|
||||||
@@ -190,16 +186,14 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
|||||||
body,
|
body,
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
reply_to,
|
|
||||||
html_body,
|
|
||||||
mailbox,
|
mailbox,
|
||||||
reply_to_email_id,
|
in_reply_to_email_id,
|
||||||
reply_mailbox,
|
in_reply_to_mailbox,
|
||||||
reply_all,
|
reply_all,
|
||||||
)
|
)
|
||||||
return result.model_dump()
|
return result.model_dump()
|
||||||
|
|
||||||
@mcp.tool(description="Edit an existing draft email. Only provided fields will be modified.")
|
@mcp.tool(description="Edit an existing draft email. Only provided fields will be modified. Supports reply threading via in_reply_to_email_id.")
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
def edit_draft(
|
def edit_draft(
|
||||||
email_id: str,
|
email_id: str,
|
||||||
@@ -209,10 +203,8 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
|||||||
body: Optional[str] = None,
|
body: Optional[str] = None,
|
||||||
cc: Optional[list[str]] = None,
|
cc: Optional[list[str]] = None,
|
||||||
bcc: Optional[list[str]] = None,
|
bcc: Optional[list[str]] = None,
|
||||||
reply_to: Optional[str] = None,
|
in_reply_to_email_id: Optional[str] = None,
|
||||||
html_body: Optional[str] = None,
|
in_reply_to_mailbox: Optional[str] = None,
|
||||||
reply_to_email_id: Optional[str] = None,
|
|
||||||
reply_mailbox: Optional[str] = None,
|
|
||||||
reply_all: bool = False,
|
reply_all: bool = False,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -226,10 +218,8 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
|||||||
body: Plain text email body
|
body: Plain text email body
|
||||||
cc: List of CC recipients (optional)
|
cc: List of CC recipients (optional)
|
||||||
bcc: List of BCC recipients (optional)
|
bcc: List of BCC recipients (optional)
|
||||||
reply_to: Reply-to address (optional)
|
in_reply_to_email_id: Email UID to reply to (optional, derives recipients/subject and sets threading headers)
|
||||||
html_body: HTML version of the email body (optional)
|
in_reply_to_mailbox: Mailbox containing the in_reply_to_email_id (default: INBOX)
|
||||||
reply_to_email_id: Email ID to reply to (optional)
|
|
||||||
reply_mailbox: Mailbox containing the reply_to_email_id (default: INBOX)
|
|
||||||
reply_all: Whether to include original recipients when replying (default: False)
|
reply_all: Whether to include original recipients when replying (default: False)
|
||||||
"""
|
"""
|
||||||
result = service.update_draft(
|
result = service.update_draft(
|
||||||
@@ -240,63 +230,26 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
|||||||
body,
|
body,
|
||||||
cc,
|
cc,
|
||||||
bcc,
|
bcc,
|
||||||
reply_to,
|
in_reply_to_email_id,
|
||||||
html_body,
|
in_reply_to_mailbox,
|
||||||
reply_to_email_id,
|
|
||||||
reply_mailbox,
|
|
||||||
reply_all,
|
reply_all,
|
||||||
)
|
)
|
||||||
return result.model_dump()
|
return result.model_dump()
|
||||||
|
|
||||||
@mcp.tool(description="Send a new email via SMTP. Supports plain text and HTML content, CC, BCC, reply-to, and custom sender.")
|
@mcp.tool(description="Send an existing draft by ID. Only drafts can be sent.")
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
async def send_email(
|
async def send_draft(
|
||||||
to: Optional[list[str]] = None,
|
email_id: str,
|
||||||
subject: Optional[str] = None,
|
mailbox: Optional[str] = None,
|
||||||
body: Optional[str] = None,
|
|
||||||
cc: Optional[list[str]] = None,
|
|
||||||
bcc: Optional[list[str]] = None,
|
|
||||||
reply_to: Optional[str] = None,
|
|
||||||
html_body: Optional[str] = None,
|
|
||||||
sender_email: Optional[str] = None,
|
|
||||||
sender_name: Optional[str] = None,
|
|
||||||
reply_to_email_id: Optional[str] = None,
|
|
||||||
reply_mailbox: Optional[str] = None,
|
|
||||||
reply_all: bool = False,
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Send a new email.
|
Send a draft email.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
to: List of recipient email addresses (required unless reply_to_email_id is set)
|
email_id: The unique ID of the draft to send
|
||||||
subject: Email subject line (required unless reply_to_email_id is set)
|
mailbox: Drafts mailbox/folder override (default: auto-detect)
|
||||||
body: Plain text email body (required unless reply_to_email_id is set)
|
|
||||||
cc: List of CC recipients (optional)
|
|
||||||
bcc: List of BCC recipients (optional)
|
|
||||||
reply_to: Reply-to address (optional)
|
|
||||||
html_body: HTML version of the email body (optional)
|
|
||||||
sender_email: Sender email address (optional, defaults to SMTP_FROM_EMAIL)
|
|
||||||
sender_name: Sender display name (optional, defaults to SMTP_FROM_NAME)
|
|
||||||
reply_to_email_id: Email ID to reply to (optional)
|
|
||||||
reply_mailbox: Mailbox containing the reply_to_email_id (default: INBOX)
|
|
||||||
reply_all: Whether to include original recipients when replying (default: False)
|
|
||||||
"""
|
"""
|
||||||
result = await service.send_email(
|
result = await service.send_draft(email_id, mailbox)
|
||||||
to,
|
|
||||||
subject,
|
|
||||||
body,
|
|
||||||
cc,
|
|
||||||
bcc,
|
|
||||||
reply_to,
|
|
||||||
html_body,
|
|
||||||
sender_email,
|
|
||||||
sender_name,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
reply_to_email_id,
|
|
||||||
reply_mailbox,
|
|
||||||
reply_all,
|
|
||||||
)
|
|
||||||
return result.model_dump()
|
return result.model_dump()
|
||||||
|
|
||||||
@mcp.tool(description="Set or remove IMAP flags on an email. Standard flags: \\Seen, \\Answered, \\Flagged, \\Deleted, \\Draft. Custom keywords are also supported.")
|
@mcp.tool(description="Set or remove IMAP flags on an email. Standard flags: \\Seen, \\Answered, \\Flagged, \\Deleted, \\Draft. Custom keywords are also supported.")
|
||||||
@@ -321,7 +274,7 @@ def register_email_tools(mcp: FastMCP, service: EmailService):
|
|||||||
|
|
||||||
@mcp.tool(description="Unsubscribe from a mailing list. Parses List-Unsubscribe headers and attempts automatic unsubscribe via HTTP or provides mailto instructions.")
|
@mcp.tool(description="Unsubscribe from a mailing list. Parses List-Unsubscribe headers and attempts automatic unsubscribe via HTTP or provides mailto instructions.")
|
||||||
@log_tool_call
|
@log_tool_call
|
||||||
async def unsubscribe_email(
|
async def unsubscribe_maillist(
|
||||||
email_id: str,
|
email_id: str,
|
||||||
mailbox: str = "INBOX",
|
mailbox: str = "INBOX",
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
|||||||
30
test.sh
30
test.sh
@@ -17,12 +17,12 @@ echo "Testing MCP server at $BASE_URL"
|
|||||||
echo "================================"
|
echo "================================"
|
||||||
|
|
||||||
# Test health endpoint
|
# Test health endpoint
|
||||||
echo -e "\n[1/5] Testing health endpoint..."
|
echo -e "\n[1/6] Testing health endpoint..."
|
||||||
HEALTH=$(curl -s "$BASE_URL/health")
|
HEALTH=$(curl -s "$BASE_URL/health")
|
||||||
echo "Response: $HEALTH"
|
echo "Response: $HEALTH"
|
||||||
|
|
||||||
# Initialize session and capture session ID
|
# Initialize session and capture session ID
|
||||||
echo -e "\n[2/5] Initializing MCP session..."
|
echo -e "\n[2/6] Initializing MCP session..."
|
||||||
INIT_RESPONSE=$(curl -s -D - -X POST "$MCP_ENDPOINT" \
|
INIT_RESPONSE=$(curl -s -D - -X POST "$MCP_ENDPOINT" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-H "Accept: application/json, text/event-stream" \
|
-H "Accept: application/json, text/event-stream" \
|
||||||
@@ -55,12 +55,12 @@ mcp_request() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# List available tools
|
# List available tools
|
||||||
echo -e "\n[3/5] Listing available tools..."
|
echo -e "\n[3/6] Listing available tools..."
|
||||||
TOOLS=$(mcp_request 2 "tools/list" "{}")
|
TOOLS=$(mcp_request 2 "tools/list" "{}")
|
||||||
echo "$TOOLS" | grep -o '"name":"[^"]*"' | head -20 || echo "$TOOLS"
|
echo "$TOOLS" | grep -o '"name":"[^"]*"' | head -20 || echo "$TOOLS"
|
||||||
|
|
||||||
# Get server info
|
# Get server info
|
||||||
echo -e "\n[4/5] Getting server info..."
|
echo -e "\n[4/6] Getting server info..."
|
||||||
SERVER_INFO=$(mcp_request 3 "tools/call" '{"name":"get_server_info","arguments":{}}')
|
SERVER_INFO=$(mcp_request 3 "tools/call" '{"name":"get_server_info","arguments":{}}')
|
||||||
echo "$SERVER_INFO"
|
echo "$SERVER_INFO"
|
||||||
|
|
||||||
@@ -69,20 +69,18 @@ echo -e "\n[5/7] Listing mailboxes..."
|
|||||||
MAILBOXES=$(mcp_request 4 "tools/call" '{"name":"list_mailboxes","arguments":{}}')
|
MAILBOXES=$(mcp_request 4 "tools/call" '{"name":"list_mailboxes","arguments":{}}')
|
||||||
echo "$MAILBOXES"
|
echo "$MAILBOXES"
|
||||||
|
|
||||||
# List address books
|
# List contacts
|
||||||
echo -e "\n[6/7] Listing address books..."
|
echo -e "\n[6/7] Listing contacts..."
|
||||||
ADDRESSBOOKS=$(mcp_request 5 "tools/call" '{"name":"list_addressbooks","arguments":{}}')
|
CONTACTS=$(mcp_request 5 "tools/call" '{"name":"list_contacts","arguments":{"limit":10}}')
|
||||||
echo "$ADDRESSBOOKS"
|
|
||||||
|
|
||||||
# List contacts (using first addressbook from previous response)
|
|
||||||
echo -e "\n[7/7] Listing contacts..."
|
|
||||||
# Extract first addressbook ID from previous response
|
|
||||||
ADDRESSBOOK_ID=$(echo "$ADDRESSBOOKS" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
||||||
if [ -n "$ADDRESSBOOK_ID" ]; then
|
|
||||||
CONTACTS=$(mcp_request 6 "tools/call" "{\"name\":\"list_contacts\",\"arguments\":{\"addressbook_id\":\"$ADDRESSBOOK_ID\",\"limit\":10}}")
|
|
||||||
echo "$CONTACTS"
|
echo "$CONTACTS"
|
||||||
|
|
||||||
|
# Draft a reply (requires REPLY_EMAIL_ID)
|
||||||
|
echo -e "\n[7/7] Drafting reply..."
|
||||||
|
if [ -n "$REPLY_EMAIL_ID" ]; then
|
||||||
|
DRAFT_REPLY=$(mcp_request 6 "tools/call" "{\"name\":\"save_draft\",\"arguments\":{\"in_reply_to_email_id\":\"$REPLY_EMAIL_ID\",\"in_reply_to_mailbox\":\"INBOX\",\"reply_all\":false,\"body\":\"Test reply draft\"}}")
|
||||||
|
echo "$DRAFT_REPLY"
|
||||||
else
|
else
|
||||||
echo "No addressbook found to list contacts from"
|
echo "Skipping reply draft: set REPLY_EMAIL_ID to an email UID to test threading"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo -e "\n================================"
|
echo -e "\n================================"
|
||||||
|
|||||||
Reference in New Issue
Block a user