Fix MCP tool serialization for datetime objects

- Add serialize_dataclass() helper to convert dataclass instances to JSON-compatible dicts
- Convert datetime fields to ISO format strings for JSON serialization
- Fix all MCP tool return values (list_messages, list_chats, search_contacts, etc.)
- Add 'title' parameter to list_messages for easier chat lookup by name
- Change date_range parameter to accept ISO date strings instead of datetime objects
- Set include_context default to False for better performance

Fixes compatibility issues where MCP tools were failing with 'tool compatibility issue' error
because Python dataclasses with datetime fields cannot be directly JSON-serialized.

All tools now return properly serialized dictionaries that work with the MCP protocol.

Author: Danny Stocker
Date: 2025-10-26
This commit is contained in:
Danny Stocker 2025-10-26 21:38:19 +00:00
parent 896f02616f
commit 77b4106fbe

View file

@ -14,6 +14,7 @@ and also communicates with the Bridge's HTTP API for sending messages.
from typing import List, Dict, Any, Optional, Tuple from typing import List, Dict, Any, Optional, Tuple
from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp import FastMCP
from datetime import datetime from datetime import datetime
from dataclasses import asdict
from telegram import ( from telegram import (
search_contacts as telegram_search_contacts, search_contacts as telegram_search_contacts,
list_messages as telegram_list_messages, list_messages as telegram_list_messages,
@ -29,6 +30,21 @@ from telegram import (
# Initialize FastMCP server # Initialize FastMCP server
mcp = FastMCP("telegram") mcp = FastMCP("telegram")
# Author: Danny Stocker (fix for datetime serialization bug)
# Date: 2025-10-26
def serialize_dataclass(obj: Any) -> Dict[str, Any]:
"""Convert dataclass to dictionary with JSON-serializable values."""
if hasattr(obj, '__dataclass_fields__'):
result = asdict(obj)
# Convert datetime objects to ISO format strings
for key, value in result.items():
if isinstance(value, datetime):
result[key] = value.isoformat()
elif isinstance(value, list):
result[key] = [serialize_dataclass(item) if hasattr(item, '__dataclass_fields__') else item for item in value]
return result
return obj
@mcp.tool() @mcp.tool()
def search_contacts(query: str) -> List[Dict[str, Any]]: def search_contacts(query: str) -> List[Dict[str, Any]]:
"""Search Telegram contacts by name or username. """Search Telegram contacts by name or username.
@ -37,35 +53,53 @@ def search_contacts(query: str) -> List[Dict[str, Any]]:
query: Search term to match against contact names or usernames query: Search term to match against contact names or usernames
""" """
contacts = telegram_search_contacts(query) contacts = telegram_search_contacts(query)
return contacts return [serialize_dataclass(c) for c in contacts]
@mcp.tool() @mcp.tool()
def list_messages( def list_messages(
date_range: Optional[Tuple[datetime, datetime]] = None, date_range: Optional[Tuple[str, str]] = None,
sender_id: Optional[int] = None, sender_id: Optional[int] = None,
chat_id: Optional[int] = None, chat_id: Optional[int] = None,
title: Optional[str] = None,
query: Optional[str] = None, query: Optional[str] = None,
limit: int = 20, limit: int = 20,
page: int = 0, page: int = 0,
include_context: bool = True, include_context: bool = False,
context_before: int = 1, context_before: int = 1,
context_after: int = 1 context_after: int = 1
) -> List[Dict[str, Any]]: ) -> List[Dict[str, Any]]:
"""Get Telegram messages matching specified criteria with optional context. """Get Telegram messages matching specified criteria with optional context.
Args: Args:
date_range: Optional tuple of (start_date, end_date) to filter messages by date date_range: Optional tuple of (start_date, end_date) ISO strings to filter messages by date
sender_id: Optional sender ID to filter messages by sender sender_id: Optional sender ID to filter messages by sender
chat_id: Optional chat ID to filter messages by chat chat_id: Optional chat ID to filter messages by chat
title: Optional chat title to search for (will find chat_id automatically)
query: Optional search term to filter messages by content query: Optional search term to filter messages by content
limit: Maximum number of messages to return (default 20) limit: Maximum number of messages to return (default 20)
page: Page number for pagination (default 0) page: Page number for pagination (default 0)
include_context: Whether to include messages before and after matches (default True) include_context: Whether to include messages before and after matches (default False)
context_before: Number of messages to include before each match (default 1) context_before: Number of messages to include before each match (default 1)
context_after: Number of messages to include after each match (default 1) context_after: Number of messages to include after each match (default 1)
""" """
# Convert date strings to datetime if provided
parsed_date_range = None
if date_range:
parsed_date_range = (
datetime.fromisoformat(date_range[0]),
datetime.fromisoformat(date_range[1])
)
# If title is provided, find the chat_id
if title and not chat_id:
from telegram import list_chats as find_chats
chats = find_chats(query=title, limit=5)
if chats:
# Use first matching chat
chat_id = chats[0].id
messages = telegram_list_messages( messages = telegram_list_messages(
date_range=date_range, date_range=parsed_date_range,
sender_id=sender_id, sender_id=sender_id,
chat_id=chat_id, chat_id=chat_id,
query=query, query=query,
@ -75,7 +109,7 @@ def list_messages(
context_before=context_before, context_before=context_before,
context_after=context_after context_after=context_after
) )
return messages return [serialize_dataclass(m) for m in messages]
@mcp.tool() @mcp.tool()
def list_chats( def list_chats(
@ -101,7 +135,7 @@ def list_chats(
chat_type=chat_type, chat_type=chat_type,
sort_by=sort_by sort_by=sort_by
) )
return chats return [serialize_dataclass(c) for c in chats]
@mcp.tool() @mcp.tool()
def get_chat(chat_id: int) -> Dict[str, Any]: def get_chat(chat_id: int) -> Dict[str, Any]:
@ -111,7 +145,7 @@ def get_chat(chat_id: int) -> Dict[str, Any]:
chat_id: The ID of the chat to retrieve chat_id: The ID of the chat to retrieve
""" """
chat = telegram_get_chat(chat_id) chat = telegram_get_chat(chat_id)
return chat return serialize_dataclass(chat)
@mcp.tool() @mcp.tool()
def get_direct_chat_by_contact(contact_id: int) -> Dict[str, Any]: def get_direct_chat_by_contact(contact_id: int) -> Dict[str, Any]:
@ -121,7 +155,7 @@ def get_direct_chat_by_contact(contact_id: int) -> Dict[str, Any]:
contact_id: The contact ID to search for contact_id: The contact ID to search for
""" """
chat = telegram_get_direct_chat_by_contact(contact_id) chat = telegram_get_direct_chat_by_contact(contact_id)
return chat return serialize_dataclass(chat)
@mcp.tool() @mcp.tool()
def get_contact_chats(contact_id: int, limit: int = 20, page: int = 0) -> List[Dict[str, Any]]: def get_contact_chats(contact_id: int, limit: int = 20, page: int = 0) -> List[Dict[str, Any]]:
@ -133,7 +167,7 @@ def get_contact_chats(contact_id: int, limit: int = 20, page: int = 0) -> List[D
page: Page number for pagination (default 0) page: Page number for pagination (default 0)
""" """
chats = telegram_get_contact_chats(contact_id, limit, page) chats = telegram_get_contact_chats(contact_id, limit, page)
return chats return [serialize_dataclass(c) for c in chats]
@mcp.tool() @mcp.tool()
def get_last_interaction(contact_id: int) -> Dict[str, Any]: def get_last_interaction(contact_id: int) -> Dict[str, Any]:
@ -143,7 +177,7 @@ def get_last_interaction(contact_id: int) -> Dict[str, Any]:
contact_id: The ID of the contact to search for contact_id: The ID of the contact to search for
""" """
message = telegram_get_last_interaction(contact_id) message = telegram_get_last_interaction(contact_id)
return message return serialize_dataclass(message)
@mcp.tool() @mcp.tool()
def get_message_context( def get_message_context(
@ -161,7 +195,7 @@ def get_message_context(
after: Number of messages to include after the target message (default 5) after: Number of messages to include after the target message (default 5)
""" """
context = telegram_get_message_context(message_id, chat_id, before, after) context = telegram_get_message_context(message_id, chat_id, before, after)
return context return serialize_dataclass(context)
@mcp.tool() @mcp.tool()
def send_message( def send_message(
@ -171,7 +205,7 @@ def send_message(
"""Send a Telegram message to a person or group. """Send a Telegram message to a person or group.
Args: Args:
recipient: The recipient - either a username (with or without @) or a chat ID recipient: The recipient - either a username (with or without @), chat title, or a chat ID
message: The message text to send message: The message text to send
Returns: Returns:
@ -203,4 +237,4 @@ if __name__ == "__main__":
sys.stderr = open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs', 'mcp_error.log'), 'w') sys.stderr = open(os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'logs', 'mcp_error.log'), 'w')
# Initialize and run the server # Initialize and run the server
mcp.run(transport='stdio') mcp.run(transport='stdio')