Files
beads/integrations/beads-mcp/src/beads_mcp/mail_tools.py
Steve Yegge 9e57cb69d8 Improve type safety and fix minor issues in beads-mcp
Type Safety Improvements:
- Change dict → dict[str, Any] throughout codebase for explicit typing
- Add PEP 561 py.typed marker file to export type information
- Add types-requests to dev dependencies
- Improve signal handler typing (FrameType | None)
- Improve decorator typing (Callable[..., Awaitable[T]])
- Add quickstart() abstract method to BdClientBase for interface completeness

Bug Fixes:
- Fix variable shadowing: beads_dir → local_beads_dir in bd_client.py
- Improve error handling in mail.py:_call_agent_mail() to prevent undefined error
- Make working_dir required (not Optional) in BdDaemonClient
- Remove unnecessary 'or' defaults for required Pydantic fields

Validation:
- mypy passes with no errors
- All unit tests passing
- Daemon quickstart returns helpful static text (RPC doesn't support this command)
2025-11-20 19:27:56 -05:00

212 lines
5.6 KiB
Python

"""MCP tools for Agent Mail messaging."""
import logging
from typing import Annotated, Any
from .mail import (
MailError,
mail_ack,
mail_delete,
mail_inbox,
mail_read,
mail_reply,
mail_send,
)
from .models import (
MailAckParams,
MailDeleteParams,
MailInboxParams,
MailReadParams,
MailReplyParams,
MailSendParams,
)
logger = logging.getLogger(__name__)
def beads_mail_send(params: MailSendParams) -> dict[str, Any]:
"""Send a message to other agents via Agent Mail.
Requires BEADS_AGENT_MAIL_URL and BEADS_AGENT_NAME environment variables.
Auto-detects project from workspace root.
Example:
mail_send(to=["alice"], subject="Review PR", body="Can you review PR #42?")
Args:
params: Message parameters (to, subject, body, etc.)
Returns:
{message_id: int, thread_id: str, sent_to: int}
Raises:
MailError: On configuration or delivery error
"""
try:
return mail_send(
to=params.to,
subject=params.subject,
body=params.body,
urgent=params.urgent,
cc=params.cc,
project_key=params.project_key,
sender_name=params.sender_name,
)
except MailError as e:
logger.error(f"mail_send failed: {e.message}")
return {"error": e.code, "message": e.message, "data": e.data}
def beads_mail_inbox(
params: Annotated[MailInboxParams, "Parameters"] = MailInboxParams(),
) -> dict[str, Any]:
"""Get messages from Agent Mail inbox.
Requires BEADS_AGENT_MAIL_URL and BEADS_AGENT_NAME environment variables.
Example:
mail_inbox(limit=10, unread_only=True)
Args:
params: Inbox filter parameters
Returns:
{
messages: [{id, thread_id, from, subject, created_ts, unread, ack_required, urgent, preview}, ...],
next_cursor: str | None
}
Raises:
MailError: On configuration or fetch error
"""
try:
return mail_inbox(
limit=params.limit,
urgent_only=params.urgent_only,
unread_only=params.unread_only,
cursor=params.cursor,
agent_name=params.agent_name,
project_key=params.project_key,
)
except MailError as e:
logger.error(f"mail_inbox failed: {e.message}")
return {"error": e.code, "message": e.message, "data": e.data}
def beads_mail_read(params: MailReadParams) -> dict[str, Any]:
"""Read full message with body from Agent Mail.
By default, marks the message as read. Set mark_read=False to preview without marking.
Example:
mail_read(message_id=123)
Args:
params: Read parameters (message_id, mark_read)
Returns:
{
id, thread_id, from, to, subject, body,
created_ts, ack_required, ack_status, read_ts, urgent
}
Raises:
MailError: On configuration or read error
"""
try:
return mail_read(
message_id=params.message_id,
mark_read=params.mark_read,
agent_name=params.agent_name,
project_key=params.project_key,
)
except MailError as e:
logger.error(f"mail_read failed: {e.message}")
return {"error": e.code, "message": e.message, "data": e.data}
def beads_mail_reply(params: MailReplyParams) -> dict[str, Any]:
"""Reply to a message (preserves thread).
Automatically inherits thread_id from the original message.
Example:
mail_reply(message_id=123, body="Thanks, will review today!")
Args:
params: Reply parameters (message_id, body, subject)
Returns:
{message_id: int, thread_id: str}
Raises:
MailError: On configuration or reply error
"""
try:
return mail_reply(
message_id=params.message_id,
body=params.body,
subject=params.subject,
agent_name=params.agent_name,
project_key=params.project_key,
)
except MailError as e:
logger.error(f"mail_reply failed: {e.message}")
return {"error": e.code, "message": e.message, "data": e.data}
def beads_mail_ack(params: MailAckParams) -> dict[str, Any]:
"""Acknowledge a message (for ack_required messages).
Safe to call even if message doesn't require acknowledgement.
Example:
mail_ack(message_id=123)
Args:
params: Acknowledgement parameters (message_id)
Returns:
{acknowledged: True}
Raises:
MailError: On configuration or ack error
"""
try:
return mail_ack(
message_id=params.message_id,
agent_name=params.agent_name,
project_key=params.project_key,
)
except MailError as e:
logger.error(f"mail_ack failed: {e.message}")
return {"error": e.code, "acknowledged": False, "message": e.message}
def beads_mail_delete(params: MailDeleteParams) -> dict[str, Any]:
"""Delete (archive) a message from Agent Mail inbox.
Note: Agent Mail archives messages rather than permanently deleting them.
Example:
mail_delete(message_id=123)
Args:
params: Delete parameters (message_id)
Returns:
{deleted: True} or {archived: True}
Raises:
MailError: On configuration or delete error
"""
try:
return mail_delete(
message_id=params.message_id,
agent_name=params.agent_name,
project_key=params.project_key,
)
except MailError as e:
logger.error(f"mail_delete failed: {e.message}")
return {"error": e.code, "deleted": False, "message": e.message}