- Commented out all @mcp.tool decorators without readOnlyHint=True
- 47 write tools disabled (send_message, create_group, ban_user, etc.)
- 47 read-only tools remain active (get_chats, get_messages, search_messages, etc.)
- Created Python script to automate disabling process
- Now completely read-only - no accidental message sending possible
Telethon's client.get_messages(entity, ids=N) sometimes returns bot messages
with buttons=None and reply_markup=None, even when the message has inline
keyboard buttons. The same message fetched via history (limit=N) includes
the full ReplyInlineMarkup.
Changes:
- Add fallback: when message fetched by ID has no inline buttons, re-fetch
via history and match by ID
- Check both msg.buttons and msg.reply_markup.rows when searching for
messages with buttons
- Access .data/.url/.text directly on button objects (works for both
MessageButton wrappers and raw KeyboardButtonCallback objects)
Replace the JSON output approach with a non-breaking fix: escape newlines
in message text (\n → \\n) to preserve the existing pipe-delimited format.
This fixes multi-line message parsing without breaking downstream consumers.
The pipe-delimited text format (ID: N | ... | Message: <text>) breaks when
messages contain newlines, since \n serves as both record separator and can
appear inside message text. Downstream parsers that split on \n only capture
the first line
Switch to a JSON array output with fields: id, sender, date, text,
reply_to (optional), and engagement (optional, with views/forwards/reactions)
This is a breaking change for MCP clients that parse the old text format.
Since MCP tool results are typically consumed by LLMs which handle JSON
natively, this should be transparent for most consumers
Without calling `client.disconnect()`, the TCP connection to Telegram's
servers is abandoned when the process exits. If the server restarts
quickly (common with MCP stdio servers), Telegram sees two connections
using the same auth key simultaneously and permanently revokes it with
`AuthKeyDuplicatedError`, requiring the user to regenerate their session.
Add a `finally` block to `_main()` so `client.disconnect()` is always
called on exit, allowing Telegram's servers to clean up the connection
state promptly.
When a caller passes a file_path with an explicit extension (e.g. ticket.jpg),
Telethon honours that extension verbatim even if the actual media is a PDF.
Stripping the suffix before calling client.download_media() lets Telethon
append the correct extension based on the real file content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Display unread_mentions_count for each chat when it is greater than zero.
The value is already available on the dialog object from Telegram API,
so this adds no extra API calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add filtering capabilities to the list_chats tool:
- unread_only: filter to show only chats with unread messages
- unmuted_only: filter to show only unmuted chats
- Display mute status in chat info output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address review feedback from @chigwell and @l1v0n1:
- Restore parse_mode=parse_mode in send_message and reply_to_message
- Restore _resolve_readable_file_path() in send_file, send_voice,
send_sticker, and edit_chat_photo
- Use str(safe_path) instead of raw file_path in upload_file calls
The entity cache fix (resolve_entity/resolve_input_entity) is preserved
in all affected functions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
StringSession has no persistent entity cache. After each MCP server
restart, client.get_entity(positive_int) fails for basic group chats
because Telethon interprets positive integers as PeerUser IDs when
they're not in the cache.
Fix:
- Add resolve_entity() / resolve_input_entity() helpers that retry
after warming cache via get_dialogs() on ValueError
- Warm entity cache on startup in _main()
- Replace all ~72 client.get_entity() / client.get_input_entity()
call sites with the new helpers
Tested: 60/60 pass across 6 basic group chats (send_message,
get_history, reply, edit, draft, reactions, search, mark_as_read,
delete) with cold cache -> warm cache transition verified.
Fixeschigwell/telegram-mcp#76
contacts.SearchRequest returns both result.users and result.chats,
but only users were returned. Chats (channels, groups) are now
included, making search work by channel/group title as intended.
Shared folders in Telegram use DialogFilterChatlist type instead of
DialogFilter. Previously, all folder operations (list, get, create,
add/remove chat) silently ignored shared folders. This fix ensures
they are recognized and handled correctly alongside regular folders.
MCP clients (e.g. ZeroClaw) communicate via JSON-RPC over stdin/stdout.
Startup messages and warnings written to stdout corrupt the protocol
stream, causing parse failures. Redirect all print() calls to stderr.
Telethon's DeletePhotosRequest requires InputPhoto objects, which it derives
from Photo objects via get_input_photo(). Passing photo.id (an int) causes:
AttributeError: 'int' object has no attribute 'SUBCLASS_OF_ID'
TypeError: Cannot cast int to any kind of InputPhoto.
Fix: pass the Photo object directly (photos.photos[0]) so Telethon can
convert it to InputPhoto internally.