feat: enhance error handling and logging in main.py
- Added logging setup to capture errors in mcp_errors.log. - Improved error handling in get_chats, get_messages, send_message, and other functions to provide clearer feedback. - Added file existence and readability checks for file-related functions. - Updated documentation for function arguments to clarify requirements.
This commit is contained in:
parent
6f25a900f5
commit
710b9fd05c
1 changed files with 94 additions and 40 deletions
134
main.py
134
main.py
|
|
@ -15,6 +15,8 @@ from datetime import datetime, timedelta
|
||||||
import json
|
import json
|
||||||
from typing import List, Dict, Optional, Union, Any
|
from typing import List, Dict, Optional, Union, Any
|
||||||
from telethon import functions
|
from telethon import functions
|
||||||
|
import mimetypes
|
||||||
|
import logging
|
||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
@ -34,6 +36,13 @@ else:
|
||||||
# Use file-based session
|
# Use file-based session
|
||||||
client = TelegramClient(TELEGRAM_SESSION_NAME, TELEGRAM_API_ID, TELEGRAM_API_HASH)
|
client = TelegramClient(TELEGRAM_SESSION_NAME, TELEGRAM_API_ID, TELEGRAM_API_HASH)
|
||||||
|
|
||||||
|
# Setup logger for error reporting
|
||||||
|
logging.basicConfig(
|
||||||
|
filename='mcp_errors.log',
|
||||||
|
level=logging.ERROR,
|
||||||
|
format='%(asctime)s %(levelname)s %(name)s %(message)s'
|
||||||
|
)
|
||||||
|
logger = logging.getLogger("mcp")
|
||||||
|
|
||||||
def format_entity(entity) -> Dict[str, Any]:
|
def format_entity(entity) -> Dict[str, Any]:
|
||||||
"""Helper function to format entity information consistently."""
|
"""Helper function to format entity information consistently."""
|
||||||
|
|
@ -80,32 +89,33 @@ def format_message(message) -> Dict[str, Any]:
|
||||||
async def get_chats(page: int = 1, page_size: int = 20) -> str:
|
async def get_chats(page: int = 1, page_size: int = 20) -> str:
|
||||||
"""
|
"""
|
||||||
Get a paginated list of chats.
|
Get a paginated list of chats.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
page: Page number (1-indexed).
|
page: Page number (1-indexed).
|
||||||
page_size: Number of chats per page.
|
page_size: Number of chats per page.
|
||||||
"""
|
"""
|
||||||
dialogs = await client.get_dialogs()
|
try:
|
||||||
start = (page - 1) * page_size
|
dialogs = await client.get_dialogs()
|
||||||
end = start + page_size
|
start = (page - 1) * page_size
|
||||||
if start >= len(dialogs):
|
end = start + page_size
|
||||||
return "Page out of range."
|
if start >= len(dialogs):
|
||||||
chats = dialogs[start:end]
|
return "Page out of range."
|
||||||
lines = []
|
chats = dialogs[start:end]
|
||||||
for dialog in chats:
|
lines = []
|
||||||
# For groups or channels, use the title; for users, show first name.
|
for dialog in chats:
|
||||||
entity = dialog.entity
|
entity = dialog.entity
|
||||||
chat_id = entity.id
|
chat_id = entity.id
|
||||||
title = getattr(entity, "title", None) or getattr(entity, "first_name", "Unknown")
|
title = getattr(entity, "title", None) or getattr(entity, "first_name", "Unknown")
|
||||||
lines.append(f"Chat ID: {chat_id}, Title: {title}")
|
lines.append(f"Chat ID: {chat_id}, Title: {title}")
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"get_chats failed (page={page}, page_size={page_size})")
|
||||||
|
return "An error occurred (code: GETCHATS-ERR-001). Check mcp_errors.log for details."
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_messages(chat_id: int, page: int = 1, page_size: int = 20) -> str:
|
async def get_messages(chat_id: int, page: int = 1, page_size: int = 20) -> str:
|
||||||
"""
|
"""
|
||||||
Get paginated messages from a specific chat.
|
Get paginated messages from a specific chat.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
chat_id: The ID of the chat.
|
chat_id: The ID of the chat.
|
||||||
page: Page number (1-indexed).
|
page: Page number (1-indexed).
|
||||||
|
|
@ -113,38 +123,34 @@ async def get_messages(chat_id: int, page: int = 1, page_size: int = 20) -> str:
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
entity = await client.get_entity(chat_id)
|
entity = await client.get_entity(chat_id)
|
||||||
|
offset = (page - 1) * page_size
|
||||||
|
messages = await client.get_messages(entity, limit=page_size, add_offset=offset)
|
||||||
|
if not messages:
|
||||||
|
return "No messages found for this page."
|
||||||
|
lines = []
|
||||||
|
for msg in messages:
|
||||||
|
lines.append(f"ID: {msg.id} | Date: {msg.date} | Message: {msg.message}")
|
||||||
|
return "\n".join(lines)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Could not resolve chat with ID {chat_id}: {e}"
|
logger.exception(f"get_messages failed (chat_id={chat_id}, page={page}, page_size={page_size})")
|
||||||
|
return "An error occurred (code: GETMSGS-ERR-001). Check mcp_errors.log for details."
|
||||||
offset = (page - 1) * page_size
|
|
||||||
messages = await client.get_messages(entity, limit=page_size, add_offset=offset)
|
|
||||||
if not messages:
|
|
||||||
return "No messages found for this page."
|
|
||||||
lines = []
|
|
||||||
for msg in messages:
|
|
||||||
lines.append(f"ID: {msg.id} | Date: {msg.date} | Message: {msg.message}")
|
|
||||||
return "\n".join(lines)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def send_message(chat_id: int, message: str) -> str:
|
async def send_message(chat_id: int, message: str) -> str:
|
||||||
"""
|
"""
|
||||||
Send a message to a specific chat.
|
Send a message to a specific chat.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
chat_id: The ID of the chat.
|
chat_id: The ID of the chat.
|
||||||
message: The message content to send.
|
message: The message content to send.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
entity = await client.get_entity(chat_id)
|
entity = await client.get_entity(chat_id)
|
||||||
except Exception as e:
|
|
||||||
return f"Could not resolve chat with ID {chat_id}: {e}"
|
|
||||||
|
|
||||||
try:
|
|
||||||
await client.send_message(entity, message)
|
await client.send_message(entity, message)
|
||||||
return "Message sent successfully."
|
return "Message sent successfully."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Failed to send message: {e}"
|
logger.exception(f"send_message failed (chat_id={chat_id})")
|
||||||
|
return "An error occurred (code: SENDMSG-ERR-001). Check mcp_errors.log for details."
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
|
|
@ -735,10 +741,14 @@ async def send_file(chat_id: int, file_path: str, caption: str = None) -> str:
|
||||||
Send a file to a chat.
|
Send a file to a chat.
|
||||||
Args:
|
Args:
|
||||||
chat_id: The chat ID.
|
chat_id: The chat ID.
|
||||||
file_path: Path to the file to send.
|
file_path: Absolute path to the file to send (must exist and be readable).
|
||||||
caption: Optional caption for the file.
|
caption: Optional caption for the file.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
return f"File not found: {file_path}"
|
||||||
|
if not os.access(file_path, os.R_OK):
|
||||||
|
return f"File is not readable: {file_path}"
|
||||||
entity = await client.get_entity(chat_id)
|
entity = await client.get_entity(chat_id)
|
||||||
await client.send_file(entity, file_path, caption=caption)
|
await client.send_file(entity, file_path, caption=caption)
|
||||||
return f"File sent to chat {chat_id}."
|
return f"File sent to chat {chat_id}."
|
||||||
|
|
@ -753,14 +763,20 @@ async def download_media(chat_id: int, message_id: int, file_path: str) -> str:
|
||||||
Args:
|
Args:
|
||||||
chat_id: The chat ID.
|
chat_id: The chat ID.
|
||||||
message_id: The message ID containing the media.
|
message_id: The message ID containing the media.
|
||||||
file_path: Path to save the downloaded file.
|
file_path: Absolute path to save the downloaded file (must be writable).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
entity = await client.get_entity(chat_id)
|
entity = await client.get_entity(chat_id)
|
||||||
msg = await client.get_messages(entity, ids=message_id)
|
msg = await client.get_messages(entity, ids=message_id)
|
||||||
if not msg or not msg.media:
|
if not msg or not msg.media:
|
||||||
return "No media found in the specified message."
|
return "No media found in the specified message."
|
||||||
|
# Check if directory is writable
|
||||||
|
dir_path = os.path.dirname(file_path) or '.'
|
||||||
|
if not os.access(dir_path, os.W_OK):
|
||||||
|
return f"Directory not writable: {dir_path}"
|
||||||
await client.download_media(msg, file=file_path)
|
await client.download_media(msg, file=file_path)
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
return f"Download failed: file not created at {file_path}"
|
||||||
return f"Media downloaded to {file_path}."
|
return f"Media downloaded to {file_path}."
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error downloading media: {e}"
|
return f"Error downloading media: {e}"
|
||||||
|
|
@ -1088,9 +1104,19 @@ async def import_chat_invite(hash: str) -> str:
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def send_voice(chat_id: int, file_path: str) -> str:
|
async def send_voice(chat_id: int, file_path: str) -> str:
|
||||||
"""
|
"""
|
||||||
Send a voice message to a chat.
|
Send a voice message to a chat. File must be an OGG/OPUS voice note.
|
||||||
|
Args:
|
||||||
|
chat_id: The chat ID.
|
||||||
|
file_path: Absolute path to the OGG/OPUS file.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
return f"File not found: {file_path}"
|
||||||
|
if not os.access(file_path, os.R_OK):
|
||||||
|
return f"File is not readable: {file_path}"
|
||||||
|
mime, _ = mimetypes.guess_type(file_path)
|
||||||
|
if not (mime and (mime == 'audio/ogg' or file_path.lower().endswith('.ogg') or file_path.lower().endswith('.opus'))):
|
||||||
|
return "Voice file must be .ogg or .opus format."
|
||||||
entity = await client.get_entity(chat_id)
|
entity = await client.get_entity(chat_id)
|
||||||
await client.send_file(entity, file_path, voice_note=True)
|
await client.send_file(entity, file_path, voice_note=True)
|
||||||
return f"Voice message sent to chat {chat_id}."
|
return f"Voice message sent to chat {chat_id}."
|
||||||
|
|
@ -1193,9 +1219,15 @@ async def reply_to_message(chat_id: int, message_id: int, text: str) -> str:
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def upload_file(file_path: str) -> str:
|
async def upload_file(file_path: str) -> str:
|
||||||
"""
|
"""
|
||||||
Upload a file to Telegram servers (returns file handle).
|
Upload a file to Telegram servers (returns file handle as string, not a file path).
|
||||||
|
Args:
|
||||||
|
file_path: Absolute path to the file to upload (must exist and be readable).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
return f"File not found: {file_path}"
|
||||||
|
if not os.access(file_path, os.R_OK):
|
||||||
|
return f"File is not readable: {file_path}"
|
||||||
file = await client.upload_file(file_path)
|
file = await client.upload_file(file_path)
|
||||||
return str(file)
|
return str(file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1206,12 +1238,15 @@ async def upload_file(file_path: str) -> str:
|
||||||
async def get_media_info(chat_id: int, message_id: int) -> str:
|
async def get_media_info(chat_id: int, message_id: int) -> str:
|
||||||
"""
|
"""
|
||||||
Get info about media in a message.
|
Get info about media in a message.
|
||||||
|
Args:
|
||||||
|
chat_id: The chat ID.
|
||||||
|
message_id: The message ID.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
entity = await client.get_entity(chat_id)
|
entity = await client.get_entity(chat_id)
|
||||||
msg = await client.get_messages(entity, ids=message_id)
|
msg = await client.get_messages(entity, ids=message_id)
|
||||||
if not msg or not msg.media:
|
if not msg or not msg.media:
|
||||||
return "No media found."
|
return "No media found in the specified message."
|
||||||
return str(msg.media)
|
return str(msg.media)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error getting media info: {e}"
|
return f"Error getting media info: {e}"
|
||||||
|
|
@ -1329,9 +1364,18 @@ async def get_sticker_sets() -> str:
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def send_sticker(chat_id: int, file_path: str) -> str:
|
async def send_sticker(chat_id: int, file_path: str) -> str:
|
||||||
"""
|
"""
|
||||||
Send a sticker to a chat.
|
Send a sticker to a chat. File must be a valid .webp sticker file.
|
||||||
|
Args:
|
||||||
|
chat_id: The chat ID.
|
||||||
|
file_path: Absolute path to the .webp sticker file.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if not os.path.isfile(file_path):
|
||||||
|
return f"Sticker file not found: {file_path}"
|
||||||
|
if not os.access(file_path, os.R_OK):
|
||||||
|
return f"Sticker file is not readable: {file_path}"
|
||||||
|
if not file_path.lower().endswith('.webp'):
|
||||||
|
return "Sticker file must be a .webp file."
|
||||||
entity = await client.get_entity(chat_id)
|
entity = await client.get_entity(chat_id)
|
||||||
await client.send_file(entity, file_path, force_document=False)
|
await client.send_file(entity, file_path, force_document=False)
|
||||||
return f"Sticker sent to chat {chat_id}."
|
return f"Sticker sent to chat {chat_id}."
|
||||||
|
|
@ -1342,10 +1386,15 @@ async def send_sticker(chat_id: int, file_path: str) -> str:
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_gif_search(query: str, limit: int = 10) -> str:
|
async def get_gif_search(query: str, limit: int = 10) -> str:
|
||||||
"""
|
"""
|
||||||
Search for GIFs by query.
|
Search for GIFs by query. Returns a list of Telegram document IDs (not file paths).
|
||||||
|
Args:
|
||||||
|
query: Search term for GIFs.
|
||||||
|
limit: Max number of GIFs to return.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
result = await client(functions.messages.SearchGifsRequest(q=query, offset_id=0, limit=limit))
|
result = await client(functions.messages.SearchGifsRequest(q=query, offset_id=0, limit=limit))
|
||||||
|
if not result.gifs:
|
||||||
|
return "No GIFs found for this query."
|
||||||
return json.dumps([g.document.id for g in result.gifs], indent=2)
|
return json.dumps([g.document.id for g in result.gifs], indent=2)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return f"Error searching GIFs: {e}"
|
return f"Error searching GIFs: {e}"
|
||||||
|
|
@ -1354,9 +1403,14 @@ async def get_gif_search(query: str, limit: int = 10) -> str:
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def send_gif(chat_id: int, gif_id: int) -> str:
|
async def send_gif(chat_id: int, gif_id: int) -> str:
|
||||||
"""
|
"""
|
||||||
Send a GIF to a chat by GIF document ID.
|
Send a GIF to a chat by Telegram GIF document ID (not a file path).
|
||||||
|
Args:
|
||||||
|
chat_id: The chat ID.
|
||||||
|
gif_id: Telegram document ID for the GIF (from get_gif_search).
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
|
if not isinstance(gif_id, int):
|
||||||
|
return "gif_id must be a Telegram document ID (integer), not a file path. Use get_gif_search to find IDs."
|
||||||
entity = await client.get_entity(chat_id)
|
entity = await client.get_entity(chat_id)
|
||||||
await client.send_file(entity, gif_id)
|
await client.send_file(entity, gif_id)
|
||||||
return f"GIF sent to chat {chat_id}."
|
return f"GIF sent to chat {chat_id}."
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue