telegram-context-mcp 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- telegram_context_mcp-0.2.0.dist-info/METADATA +187 -0
- telegram_context_mcp-0.2.0.dist-info/RECORD +49 -0
- telegram_context_mcp-0.2.0.dist-info/WHEEL +4 -0
- telegram_context_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- telegram_context_mcp-0.2.0.dist-info/licenses/LICENSE +21 -0
- telegram_mcp/__init__.py +1 -0
- telegram_mcp/adapters/__init__.py +1 -0
- telegram_mcp/adapters/bridge.py +77 -0
- telegram_mcp/adapters/cli.py +215 -0
- telegram_mcp/adapters/clock.py +12 -0
- telegram_mcp/adapters/config.py +169 -0
- telegram_mcp/adapters/cursors.py +58 -0
- telegram_mcp/adapters/http_token.py +76 -0
- telegram_mcp/adapters/mcp.py +363 -0
- telegram_mcp/adapters/session.py +57 -0
- telegram_mcp/adapters/telegram/__init__.py +6 -0
- telegram_mcp/adapters/telegram/auth.py +48 -0
- telegram_mcp/adapters/telegram/backend.py +101 -0
- telegram_mcp/adapters/telegram/chats.py +178 -0
- telegram_mcp/adapters/telegram/discovery.py +50 -0
- telegram_mcp/adapters/telegram/forums.py +124 -0
- telegram_mcp/adapters/telegram/mapping.py +287 -0
- telegram_mcp/adapters/telegram/messages.py +397 -0
- telegram_mcp/adapters/telegram/metadata.py +142 -0
- telegram_mcp/adapters/telegram/runtime.py +138 -0
- telegram_mcp/adapters/tool_gate.py +24 -0
- telegram_mcp/adapters/transport.py +106 -0
- telegram_mcp/adapters/version.py +7 -0
- telegram_mcp/application/__init__.py +1 -0
- telegram_mcp/application/access.py +59 -0
- telegram_mcp/application/catalog.py +76 -0
- telegram_mcp/application/common.py +19 -0
- telegram_mcp/application/discovery.py +50 -0
- telegram_mcp/application/message_pager.py +134 -0
- telegram_mcp/application/messages.py +195 -0
- telegram_mcp/application/ports.py +123 -0
- telegram_mcp/application/queries.py +118 -0
- telegram_mcp/application/query_preparation.py +154 -0
- telegram_mcp/application/requests.py +47 -0
- telegram_mcp/application/retry.py +31 -0
- telegram_mcp/application/service.py +182 -0
- telegram_mcp/application/topics.py +146 -0
- telegram_mcp/bootstrap.py +45 -0
- telegram_mcp/cli.py +6 -0
- telegram_mcp/domain/__init__.py +1 -0
- telegram_mcp/domain/folders.py +93 -0
- telegram_mcp/domain/models.py +319 -0
- telegram_mcp/domain/policy.py +12 -0
- telegram_mcp/domain/text.py +16 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: telegram-context-mcp
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Local, read-only Telegram MCP server with folder-aware retrieval
|
|
5
|
+
Project-URL: Documentation, https://github.com/egor-fedorov/telegram-mcp/blob/main/README.md
|
|
6
|
+
Project-URL: Repository, https://github.com/egor-fedorov/telegram-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/egor-fedorov/telegram-mcp/issues
|
|
8
|
+
Project-URL: Roadmap, https://github.com/egor-fedorov/telegram-mcp/blob/main/ROADMAP.md
|
|
9
|
+
Project-URL: Changelog, https://github.com/egor-fedorov/telegram-mcp/blob/main/CHANGELOG.md
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Operating System :: MacOS
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Requires-Dist: anyio<5,>=4.15.1
|
|
24
|
+
Requires-Dist: httpx2<3,>=2.5
|
|
25
|
+
Requires-Dist: mcp<3,>=2.2
|
|
26
|
+
Requires-Dist: platformdirs<5,>=4
|
|
27
|
+
Requires-Dist: portalocker<5,>=4.4.1
|
|
28
|
+
Requires-Dist: pydantic<3,>=2.14.1
|
|
29
|
+
Requires-Dist: pywin32>=311; sys_platform == 'win32'
|
|
30
|
+
Requires-Dist: starlette<2,>=1.6
|
|
31
|
+
Requires-Dist: telethon<2,>=1.45
|
|
32
|
+
Requires-Dist: uvicorn<1,>=0.52.4
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# Telegram Context MCP
|
|
36
|
+
|
|
37
|
+
[](https://github.com/egor-fedorov/telegram-mcp/actions/workflows/ci.yml)
|
|
38
|
+
[](https://github.com/egor-fedorov/telegram-mcp/blob/main/LICENSE)
|
|
39
|
+
|
|
40
|
+
Read Telegram through your own account from an MCP client. Find permitted conversations,
|
|
41
|
+
search and read original posts, navigate topics and replies, and inspect albums, reactions,
|
|
42
|
+
and polls. Connect several local clients to one session-owning process, or run standalone STDIO.
|
|
43
|
+
|
|
44
|
+
Supports Windows, macOS, and Linux with Python 3.12–3.15. The server uses Telethon and
|
|
45
|
+
preserves original text, dates, links, entities, and available metadata. It does not send
|
|
46
|
+
messages, join channels, mark messages read, or download attachment bytes.
|
|
47
|
+
|
|
48
|
+
## Install and log in
|
|
49
|
+
|
|
50
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then run:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
uv tool install --python 3.12 telegram-context-mcp==0.2.0
|
|
54
|
+
telegram-mcp --version
|
|
55
|
+
telegram-mcp login
|
|
56
|
+
telegram-mcp configure
|
|
57
|
+
telegram-mcp status
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
These examples target release 0.2.0; check [PyPI](https://pypi.org/project/telegram-context-mcp/)
|
|
61
|
+
for published versions. Use the [source setup](#run-from-source) for unreleased revisions.
|
|
62
|
+
|
|
63
|
+
Obtain `api_id` and `api_hash` from [my.telegram.org](https://my.telegram.org), then
|
|
64
|
+
enter them in the local login prompt with your phone number, login code, and any 2FA
|
|
65
|
+
password. Do not send login codes or credentials to the model.
|
|
66
|
+
|
|
67
|
+
`configure` lets you choose folders/chats and separately enable all-chat access,
|
|
68
|
+
explicit public-channel reads, or public-channel discovery. Enter keeps the current
|
|
69
|
+
value. By default, no message sources are permitted. `status` should report
|
|
70
|
+
`authorized: true` after login. Use `catalog` to find folder and chat IDs locally.
|
|
71
|
+
|
|
72
|
+
The package name is **`telegram-context-mcp`**; the executable is **`telegram-mcp`**.
|
|
73
|
+
You can also run a pinned version without installing a persistent command:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
uvx --python 3.12 --from telegram-context-mcp==0.2.0 telegram-mcp --version
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Connect your clients
|
|
80
|
+
|
|
81
|
+
Start one server in a terminal and leave it running:
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
telegram-mcp serve --transport http
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
It listens only on `http://127.0.0.1:8765/mcp`. Configure each MCP client to launch
|
|
88
|
+
`telegram-mcp connect` over STDIO. This small proxy reads the local token and connects
|
|
89
|
+
to the existing server; it does not start a server or silently fall back to another
|
|
90
|
+
Telegram session. Closing a client leaves the shared server running.
|
|
91
|
+
|
|
92
|
+
[Client examples](https://github.com/egor-fedorov/telegram-mcp/blob/main/docs/clients.md)
|
|
93
|
+
cover Codex, Claude Desktop, Claude Code, Cursor, and VS Code, including a pinned `uvx`
|
|
94
|
+
command and executable paths for Windows.
|
|
95
|
+
|
|
96
|
+
For one client, standalone STDIO is still available: configure that client to launch
|
|
97
|
+
`telegram-mcp serve`. Use either mode for a given session. Stop the session-owning
|
|
98
|
+
process before running local `login`, `configure`, `catalog`, or `status`; use the
|
|
99
|
+
`get_status` MCP tool while the server is running.
|
|
100
|
+
|
|
101
|
+
All shared clients use the same Telegram account, read permissions, and cursor store.
|
|
102
|
+
The HTTP bearer token is created in the local user profile and read by `connect`;
|
|
103
|
+
do not paste it into client configuration. This is a local, single-user integration.
|
|
104
|
+
|
|
105
|
+
## Tools
|
|
106
|
+
|
|
107
|
+
| Tool | Purpose |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `get_status` | Authorization, connection, permitted sources |
|
|
110
|
+
| `list_folders` | Folders, IDs, order, inclusion/exclusion rules |
|
|
111
|
+
| `list_chats` | Chats filtered by name, type, or folders |
|
|
112
|
+
| `get_chat` | Description and linked discussion group |
|
|
113
|
+
| `search_public_channels` | Find public broadcast channels by query; metadata only |
|
|
114
|
+
| `get_similar_channels` | Find similar public channels for one specified public source; metadata only |
|
|
115
|
+
| `search_messages` | Keyword search by chats, folders, dates, sender, or content type |
|
|
116
|
+
| `get_history` | Chat or forum-topic history over a time range, in either order |
|
|
117
|
+
| `list_topics` | Forum topics with navigation references, optional title query, and pagination |
|
|
118
|
+
| `get_pinned_messages` | Chat-wide or forum-topic pinned messages, newest first |
|
|
119
|
+
| `get_messages` | Messages by links/IDs with authors, dates, attachments, counters, reactions, and polls |
|
|
120
|
+
| `get_album` | Members of a selected album within a bounded neighborhood of one message |
|
|
121
|
+
| `get_context` | Messages before/after an anchor and a chain of parent replies |
|
|
122
|
+
| `get_thread` | Comments on a channel post or replies in a group thread |
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
## First tasks
|
|
126
|
+
|
|
127
|
+
- Ask the client to list the folders you permitted and read a selected conversation
|
|
128
|
+
over a timezone-qualified date range.
|
|
129
|
+
- Enable discovery to find candidate public channels, select a source, then read its
|
|
130
|
+
posts with a separate read permission.
|
|
131
|
+
- Follow `next_cursor` with the original filters until it is absent. An empty page
|
|
132
|
+
can still have a cursor. Respect `retry_after_seconds` after a temporary failure.
|
|
133
|
+
- Use `get_messages` with `next_text_offset` to finish long text. Album completeness
|
|
134
|
+
is explicitly unknown; each member remains a separate source message.
|
|
135
|
+
|
|
136
|
+
[Usage and response contracts](https://github.com/egor-fedorov/telegram-mcp/blob/main/docs/usage.md)
|
|
137
|
+
explain filters, cursor recovery, topics, public-channel discovery, and message metadata.
|
|
138
|
+
Message text is data for the calling agent, not an instruction source.
|
|
139
|
+
|
|
140
|
+
## Configuration and troubleshooting
|
|
141
|
+
|
|
142
|
+
Configuration, Telegram sessions, and the HTTP token live outside the repository in
|
|
143
|
+
platform-specific user directories. The login command prints the configuration path.
|
|
144
|
+
Use `telegram-mcp --config /absolute/private/config.toml COMMAND` to select another file;
|
|
145
|
+
pass the same file to the shared server and each proxy.
|
|
146
|
+
|
|
147
|
+
On POSIX, private files use restrictive permissions. On Windows, protect them with
|
|
148
|
+
user-profile filesystem ACLs; Unix `0600` is not an ACL guarantee. A Telegram session
|
|
149
|
+
contains account access credentials, and the underlying account can do more than the
|
|
150
|
+
server's read-only tools allow.
|
|
151
|
+
|
|
152
|
+
- [Configuration and permissions](https://github.com/egor-fedorov/telegram-mcp/blob/main/docs/configuration.md)
|
|
153
|
+
- [Troubleshooting](https://github.com/egor-fedorov/telegram-mcp/blob/main/docs/troubleshooting.md)
|
|
154
|
+
- [Example TOML](https://github.com/egor-fedorov/telegram-mcp/blob/main/config.example.toml)
|
|
155
|
+
- [Security reporting](https://github.com/egor-fedorov/telegram-mcp/blob/main/SECURITY.md)
|
|
156
|
+
|
|
157
|
+
## Run from source
|
|
158
|
+
|
|
159
|
+
```sh
|
|
160
|
+
git clone https://github.com/egor-fedorov/telegram-mcp.git
|
|
161
|
+
cd telegram-mcp
|
|
162
|
+
uv sync --locked --python 3.12
|
|
163
|
+
uv run --locked telegram-mcp login
|
|
164
|
+
uv run --locked telegram-mcp configure
|
|
165
|
+
uv run --locked telegram-mcp serve --transport http
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
For client commands, use the checkout's `.venv/bin/telegram-mcp` on POSIX or
|
|
169
|
+
`.venv\Scripts\telegram-mcp.exe` on Windows, with `connect` as the argument.
|
|
170
|
+
Use an absolute executable path so clients do not depend on their working directory.
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
Run `make check` for formatting, lint, types, architecture checks, and offline tests.
|
|
175
|
+
[Contributing](https://github.com/egor-fedorov/telegram-mcp/blob/main/CONTRIBUTING.md)
|
|
176
|
+
includes equivalent commands for Windows without Make. Changes use focused pull requests
|
|
177
|
+
and squash merges after CI. See the
|
|
178
|
+
[roadmap](https://github.com/egor-fedorov/telegram-mcp/blob/main/ROADMAP.md),
|
|
179
|
+
[architecture](https://github.com/egor-fedorov/telegram-mcp/blob/main/docs/architecture.md),
|
|
180
|
+
[changelog](https://github.com/egor-fedorov/telegram-mcp/blob/main/CHANGELOG.md), and
|
|
181
|
+
[release procedure](https://github.com/egor-fedorov/telegram-mcp/blob/main/docs/releasing.md).
|
|
182
|
+
|
|
183
|
+
The code is [MIT licensed](https://github.com/egor-fedorov/telegram-mcp/blob/main/LICENSE).
|
|
184
|
+
This does not grant rights to Telegram content. Secret chats are unsupported; attachment
|
|
185
|
+
downloads, sending, administration, and persistent archives are separate future work.
|
|
186
|
+
Use source material you are entitled to share with your chosen model and follow
|
|
187
|
+
[Telegram's content terms](https://telegram.org/tos/content-licensing).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
telegram_mcp/__init__.py,sha256=LShGvu63tXwxgYmoJpfYFl-uPFp_kjrA3X4vp2AuU3w,55
|
|
2
|
+
telegram_mcp/bootstrap.py,sha256=7Sn-4lhpLmrnPBgFoDDMgUj4C1cBLG08Z5pbRlAWpV4,1616
|
|
3
|
+
telegram_mcp/cli.py,sha256=HfgvjR-uRtY9_M_tf7XckSNmKIoNIp4p-uUM5XxVsqU,135
|
|
4
|
+
telegram_mcp/adapters/__init__.py,sha256=qWfFGpBNefKGiF907eZrtsF5yiW_A3UjZa4Ax-yPy_o,62
|
|
5
|
+
telegram_mcp/adapters/bridge.py,sha256=1gAJWlxrv_1WyMIcw3_9v3fpKc69ml-PYq1U3yUSPe8,2918
|
|
6
|
+
telegram_mcp/adapters/cli.py,sha256=nt1_F03YdmR49VT0p3WGyVwoqHPBV-uC323co0yx148,8536
|
|
7
|
+
telegram_mcp/adapters/clock.py,sha256=YVsCji7YxX2AIHPTLd08GEiW2lsWDUHmsPKB_-GM0Dg,277
|
|
8
|
+
telegram_mcp/adapters/config.py,sha256=hdFkaO1MGsEeft-rqDu81Ehy9m00gKoP_Eh1G3l5Ezs,6984
|
|
9
|
+
telegram_mcp/adapters/cursors.py,sha256=vjKoRZLS_sJ3wxrQXn0zZ8xeZzCDCVLpPXHvEVThtoY,2123
|
|
10
|
+
telegram_mcp/adapters/http_token.py,sha256=EGSNiNqbTPJGXNyr4eQWTfqUn7ICwVs-qxDAR9GoDQM,2603
|
|
11
|
+
telegram_mcp/adapters/mcp.py,sha256=uINuPOScQrN3y5KVmD4VP-o2o25vds7RV8fW6Bg8unE,17061
|
|
12
|
+
telegram_mcp/adapters/session.py,sha256=WB0m-FpEf-Kh2N9lzSM3h6bOwkEdCh10DHrm-A33VMY,1793
|
|
13
|
+
telegram_mcp/adapters/tool_gate.py,sha256=FPAJ7lJfzYjc-eWIqf5dQr5QGZ12Koxo8jXmAUauNNc,774
|
|
14
|
+
telegram_mcp/adapters/transport.py,sha256=ULckM2Aknh-IPp-RgXyE_uobMzmi9oIXln-j_1s1cZk,3712
|
|
15
|
+
telegram_mcp/adapters/version.py,sha256=RcTAIvbXUCC_vGF3IW2XCFVoHZYzbq_qwi_9bY4n3dg,188
|
|
16
|
+
telegram_mcp/adapters/telegram/__init__.py,sha256=wclc3gq9hG6wuzD-mpw1L34iu95GtN0bxepehHQFFpg,243
|
|
17
|
+
telegram_mcp/adapters/telegram/auth.py,sha256=PXScKRl1sG04GJoqFMLSAby35drL40QhGSWj3borTso,1599
|
|
18
|
+
telegram_mcp/adapters/telegram/backend.py,sha256=XwCgNxpRYcL4DRS46B5cIvmphYqN8aeOUspZIFlYp54,4172
|
|
19
|
+
telegram_mcp/adapters/telegram/chats.py,sha256=O9RCRLJYx4NDi8DN-RQ2gbAbMVtee1ZZLQ6zPgnp8uw,7595
|
|
20
|
+
telegram_mcp/adapters/telegram/discovery.py,sha256=AEC0uwE0r8ACrELo4JDvYmkC0gIDd4Ao6tf2YMVQnhE,2356
|
|
21
|
+
telegram_mcp/adapters/telegram/forums.py,sha256=HbXGiHuQi2_nilWjy_lmbPf-B8kuQIqCW_R9qRiGBpk,5290
|
|
22
|
+
telegram_mcp/adapters/telegram/mapping.py,sha256=ZaLjPV5Uu8UoA3G8dROoNhD6BMPWTr1JWOX_wV568zs,11139
|
|
23
|
+
telegram_mcp/adapters/telegram/messages.py,sha256=rXmDOACQrnkJJkbb-i4cnn_qFalU0Ry43awmOmQKde8,18129
|
|
24
|
+
telegram_mcp/adapters/telegram/metadata.py,sha256=C_IhP1HTbQARI1bXad4GkXcCofLHGtpBCpW2Ki8lwYw,5504
|
|
25
|
+
telegram_mcp/adapters/telegram/runtime.py,sha256=By4_rlez1-YReUGEOenrItfN_R_v35SK1Lc-daJd3sA,5579
|
|
26
|
+
telegram_mcp/application/__init__.py,sha256=4fPU_l5oJ3gplKsmc0c-gmcnIqQFWY93u2eMRfWCawk,53
|
|
27
|
+
telegram_mcp/application/access.py,sha256=aiEIDrSh_CCQ4X8WIkUooSGU7A5IY1ZSu7fMhtzaWw4,2276
|
|
28
|
+
telegram_mcp/application/catalog.py,sha256=WnHDgMCIrzjG9cMCVya8zVElLdXltF2G89qzKBRdjpU,2880
|
|
29
|
+
telegram_mcp/application/common.py,sha256=hm41HSWvkOJ-Bz5_02OMFShRTcS_eego5x4vAqgHTQs,632
|
|
30
|
+
telegram_mcp/application/discovery.py,sha256=fbz-BVaCpAjVOddhKNqV2Ko8woFewBXTS4U_sZk52tc,2058
|
|
31
|
+
telegram_mcp/application/message_pager.py,sha256=Q6RgAE9FszpF5VlxvOJfTUReZquP0NV8ybNgbmeiffU,5276
|
|
32
|
+
telegram_mcp/application/messages.py,sha256=nvFm3vyQmGSaF_z5jfcrjsNNLwr4YYDtQOaT9uqHt5w,8828
|
|
33
|
+
telegram_mcp/application/ports.py,sha256=0YGWyxHdsllk4kWa0BZ-uV2UcOgHzfE8Y5kxAdf3gGQ,3574
|
|
34
|
+
telegram_mcp/application/queries.py,sha256=ST_YkuiN0kQgl9w8ayWjOtY-VixrOqR0eiBlWdxyZSI,4194
|
|
35
|
+
telegram_mcp/application/query_preparation.py,sha256=fdNY6YgfqLPW1yVqdyI7l_wNHr3MwET2KL2nVvFpOR0,6190
|
|
36
|
+
telegram_mcp/application/requests.py,sha256=9s-nerhRksF_9ywx26keQPfewO19jipvbuSE7eTrs1w,1128
|
|
37
|
+
telegram_mcp/application/retry.py,sha256=qqHEfAs4avsPQ6bitI9V188qV1TjHG-kzNtBT2a3Y6o,975
|
|
38
|
+
telegram_mcp/application/service.py,sha256=JY3HxSR4XoOmxy3xnR_i2s0rMW_Xv1aDa_SYJxTldaE,6652
|
|
39
|
+
telegram_mcp/application/topics.py,sha256=YcP2lZTDPOsGA6lTx_YqSr97vCfM9ydEIBJmYEb0OZk,5860
|
|
40
|
+
telegram_mcp/domain/__init__.py,sha256=idV106eVVaA2lz2aFmouoa4xiIbJP0h4-yEGQULq6Jo,39
|
|
41
|
+
telegram_mcp/domain/folders.py,sha256=FTbpcpVWH7Sg1nAWEu8Y4l1pqz3qtj69wJimTVE8uNw,3404
|
|
42
|
+
telegram_mcp/domain/models.py,sha256=2drZwa0NRcXxUSW04xVQ2n1jQ0Z-CKWK_cCAbQu9b00,9286
|
|
43
|
+
telegram_mcp/domain/policy.py,sha256=7rIC7Wenh8OfwpipZEyD-CPl7vyfmWGGjoyBAV0WII4,361
|
|
44
|
+
telegram_mcp/domain/text.py,sha256=TGQuEgMJq8qFW6Ju7QGEthiJVZrbA-mF7G7QWi5dleM,515
|
|
45
|
+
telegram_context_mcp-0.2.0.dist-info/METADATA,sha256=DZN4vtGVFAGwJpFgeODKBchgwTz0tT0ErA8t3s_ylxY,9514
|
|
46
|
+
telegram_context_mcp-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
47
|
+
telegram_context_mcp-0.2.0.dist-info/entry_points.txt,sha256=AnkjeB4Oeu6bjL2mNoM3G8alvMfPhcn4fY54jIQm8g4,55
|
|
48
|
+
telegram_context_mcp-0.2.0.dist-info/licenses/LICENSE,sha256=Sb4YOw89QCIIIGM_KRImBZYJdnVqBsmWsxBhz5V3XEw,1070
|
|
49
|
+
telegram_context_mcp-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Egor Fyodorov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
telegram_mcp/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Folder-aware, read-only Telegram context server."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""External transport, persistence, and Telegram adapters."""
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Expose the shared loopback server to clients that launch STDIO subprocesses."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
|
|
6
|
+
import httpx2
|
|
7
|
+
from mcp.client import Client
|
|
8
|
+
from mcp.client.streamable_http import streamable_http_client
|
|
9
|
+
from mcp.server.lowlevel import Server
|
|
10
|
+
from mcp.server.stdio import stdio_server
|
|
11
|
+
from mcp.shared.exceptions import MCPError
|
|
12
|
+
from mcp_types import CallToolRequestParams, CallToolResult, PaginatedRequestParams, TextContent
|
|
13
|
+
|
|
14
|
+
from telegram_mcp.adapters.config import Settings
|
|
15
|
+
from telegram_mcp.adapters.http_token import load_token
|
|
16
|
+
from telegram_mcp.adapters.transport import endpoint
|
|
17
|
+
from telegram_mcp.domain.models import ToolError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def unavailable() -> ToolError:
|
|
21
|
+
return ToolError(
|
|
22
|
+
"SHARED_SERVER_UNAVAILABLE",
|
|
23
|
+
"Cannot connect to the shared Telegram server. Start 'serve --transport http' "
|
|
24
|
+
"and use the same http_port and http_token_path for connect. If it is already running, "
|
|
25
|
+
"check that both commands use the same token file.",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_bridge(upstream: Client) -> Server:
|
|
30
|
+
async def list_tools(_context, params: PaginatedRequestParams | None):
|
|
31
|
+
return await upstream.list_tools(cursor=params.cursor if params is not None else None)
|
|
32
|
+
|
|
33
|
+
async def call_tool(_context, params: CallToolRequestParams) -> CallToolResult:
|
|
34
|
+
try:
|
|
35
|
+
return await upstream.call_tool(params.name, params.arguments, meta=params.meta)
|
|
36
|
+
except MCPError:
|
|
37
|
+
raise
|
|
38
|
+
except Exception:
|
|
39
|
+
return CallToolResult(
|
|
40
|
+
content=[TextContent(type="text", text=unavailable().issue().model_dump_json())],
|
|
41
|
+
is_error=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
info = upstream.server_info
|
|
45
|
+
return Server(
|
|
46
|
+
info.name if info is not None else "Telegram Context",
|
|
47
|
+
version=info.version if info is not None else "",
|
|
48
|
+
instructions=upstream.instructions,
|
|
49
|
+
on_list_tools=list_tools,
|
|
50
|
+
on_call_tool=call_tool,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@asynccontextmanager
|
|
55
|
+
async def shared_client(settings: Settings) -> AsyncIterator[Client]:
|
|
56
|
+
token = load_token(settings.http_token_path)
|
|
57
|
+
try:
|
|
58
|
+
async with httpx2.AsyncClient(
|
|
59
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
60
|
+
timeout=httpx2.Timeout(30, read=300),
|
|
61
|
+
trust_env=False,
|
|
62
|
+
) as http:
|
|
63
|
+
async with Client(
|
|
64
|
+
streamable_http_client(endpoint(settings), http_client=http), cache=None
|
|
65
|
+
) as upstream:
|
|
66
|
+
yield upstream
|
|
67
|
+
except ToolError:
|
|
68
|
+
raise
|
|
69
|
+
except Exception:
|
|
70
|
+
raise unavailable() from None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def connect(settings: Settings) -> None:
|
|
74
|
+
async with shared_client(settings) as upstream:
|
|
75
|
+
proxy = create_bridge(upstream)
|
|
76
|
+
async with stdio_server() as (read_stream, write_stream):
|
|
77
|
+
await proxy.run(read_stream, write_stream, proxy.create_initialization_options())
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""Local commands for login, selecting scope, diagnosis and serving MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import asyncio
|
|
7
|
+
import getpass
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
import sys
|
|
11
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from pydantic import SecretStr, ValidationError
|
|
16
|
+
|
|
17
|
+
from telegram_mcp.adapters.config import (
|
|
18
|
+
Settings,
|
|
19
|
+
config_path,
|
|
20
|
+
load_settings,
|
|
21
|
+
save_settings,
|
|
22
|
+
validate_settings,
|
|
23
|
+
validation_error,
|
|
24
|
+
)
|
|
25
|
+
from telegram_mcp.adapters.mcp import create_server
|
|
26
|
+
from telegram_mcp.adapters.version import get_version
|
|
27
|
+
from telegram_mcp.application.ports import Backend
|
|
28
|
+
from telegram_mcp.application.service import Service
|
|
29
|
+
from telegram_mcp.domain.models import ToolError
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def login(
|
|
33
|
+
settings: Settings, path: Path, authenticate: Callable[..., Awaitable[None]]
|
|
34
|
+
) -> None:
|
|
35
|
+
if not sys.stdin.isatty():
|
|
36
|
+
raise ToolError(
|
|
37
|
+
"INTERACTIVE_LOGIN_REQUIRED", "Run login yourself in an interactive terminal."
|
|
38
|
+
)
|
|
39
|
+
settings = settings.model_copy(deep=True)
|
|
40
|
+
if settings.api_id is None:
|
|
41
|
+
value = (await asyncio.to_thread(input, "Telegram api_id (my.telegram.org): ")).strip()
|
|
42
|
+
try:
|
|
43
|
+
settings.api_id = int(value)
|
|
44
|
+
except ValueError:
|
|
45
|
+
raise ToolError("INVALID_ARGUMENT", "api_id: enter a positive integer.") from None
|
|
46
|
+
if settings.api_hash is None:
|
|
47
|
+
settings.api_hash = SecretStr(getpass.getpass("Telegram api_hash: ").strip())
|
|
48
|
+
settings = validate_settings(settings, "Login input")
|
|
49
|
+
await authenticate(
|
|
50
|
+
settings,
|
|
51
|
+
phone=lambda: input("Phone number: "),
|
|
52
|
+
code=lambda: getpass.getpass("Telegram login code: "),
|
|
53
|
+
password=lambda: getpass.getpass("Telegram 2FA password: "),
|
|
54
|
+
)
|
|
55
|
+
save_settings(settings, path)
|
|
56
|
+
print(f"Session saved. Configuration: {path}")
|
|
57
|
+
print("Next: telegram-mcp configure — choose the folders/chats the server may read.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def prompt_boolean(prompt: str, current: bool) -> bool:
|
|
61
|
+
current_label = "yes" if current else "no"
|
|
62
|
+
while True:
|
|
63
|
+
value = await asyncio.to_thread(input, f"{prompt} [y/n] (Enter: keep {current_label}): ")
|
|
64
|
+
value = value.strip().lower()
|
|
65
|
+
if not value:
|
|
66
|
+
return current
|
|
67
|
+
if value in {"y", "yes", "n", "no"}:
|
|
68
|
+
return value in {"y", "yes"}
|
|
69
|
+
print("Enter y or n, or press Enter to keep the current setting.")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def configure(
|
|
73
|
+
settings: Settings, path: Path, backend_factory: Callable[[Settings], Backend]
|
|
74
|
+
) -> None:
|
|
75
|
+
if not sys.stdin.isatty():
|
|
76
|
+
raise ToolError(
|
|
77
|
+
"INTERACTIVE_CONFIGURATION_REQUIRED", "Run configure in an interactive terminal."
|
|
78
|
+
)
|
|
79
|
+
updated = settings.model_copy(deep=True)
|
|
80
|
+
backend = backend_factory(settings)
|
|
81
|
+
try:
|
|
82
|
+
_, folders = await backend.catalog()
|
|
83
|
+
for folder in folders:
|
|
84
|
+
print(f"{folder.id}: {folder.title}")
|
|
85
|
+
value = (
|
|
86
|
+
await asyncio.to_thread(
|
|
87
|
+
input, "Allowed folder IDs, comma-separated (Enter: keep current): "
|
|
88
|
+
)
|
|
89
|
+
).strip()
|
|
90
|
+
if value:
|
|
91
|
+
try:
|
|
92
|
+
chosen = [int(item.strip()) for item in value.split(",")]
|
|
93
|
+
except ValueError:
|
|
94
|
+
raise ToolError(
|
|
95
|
+
"INVALID_ARGUMENT", "allowed_folder_ids: enter comma-separated integer IDs."
|
|
96
|
+
) from None
|
|
97
|
+
if set(chosen) - {f.id for f in folders}:
|
|
98
|
+
raise ToolError("FOLDER_NOT_FOUND", "A selected folder does not exist.")
|
|
99
|
+
updated.allowed_folder_ids = chosen
|
|
100
|
+
value = (
|
|
101
|
+
await asyncio.to_thread(
|
|
102
|
+
input, "Additional chat IDs, comma-separated (Enter: keep current): "
|
|
103
|
+
)
|
|
104
|
+
).strip()
|
|
105
|
+
if value:
|
|
106
|
+
try:
|
|
107
|
+
updated.allowed_chat_ids = [str(int(item.strip())) for item in value.split(",")]
|
|
108
|
+
except ValueError:
|
|
109
|
+
raise ToolError(
|
|
110
|
+
"INVALID_ARGUMENT", "allowed_chat_ids: enter comma-separated integer IDs."
|
|
111
|
+
) from None
|
|
112
|
+
updated.allow_public_channel_reads = await prompt_boolean(
|
|
113
|
+
"Allow explicitly requested public channels outside this scope?",
|
|
114
|
+
settings.allow_public_channel_reads,
|
|
115
|
+
)
|
|
116
|
+
updated.allow_public_channel_discovery = await prompt_boolean(
|
|
117
|
+
"Allow public-channel search and similar-channel metadata (no post reads)?",
|
|
118
|
+
settings.allow_public_channel_discovery,
|
|
119
|
+
)
|
|
120
|
+
updated.allow_all_chats = await prompt_boolean(
|
|
121
|
+
"Allow reading all account chats instead of limiting reads to selected sources?",
|
|
122
|
+
settings.allow_all_chats,
|
|
123
|
+
)
|
|
124
|
+
save_settings(validate_settings(updated, "Configuration input"), path)
|
|
125
|
+
print(f"Scope saved to {path}. Restart the MCP server to apply configuration changes.")
|
|
126
|
+
finally:
|
|
127
|
+
await backend.close()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
async def diagnose(
|
|
131
|
+
settings: Settings, dependencies: CliDependencies, catalog: bool = False
|
|
132
|
+
) -> None:
|
|
133
|
+
backend = dependencies.backend_factory(settings)
|
|
134
|
+
try:
|
|
135
|
+
if catalog:
|
|
136
|
+
chats, folders = await backend.catalog()
|
|
137
|
+
output: object = {
|
|
138
|
+
"folders": [f.model_dump(mode="json") for f in folders],
|
|
139
|
+
"chats": [c.model_dump(mode="json") for c in chats],
|
|
140
|
+
}
|
|
141
|
+
else:
|
|
142
|
+
output = await dependencies.service_factory(backend, settings).get_status()
|
|
143
|
+
print(json.dumps(output, ensure_ascii=False, indent=2))
|
|
144
|
+
finally:
|
|
145
|
+
await backend.close()
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class CliDependencies:
|
|
150
|
+
backend_factory: Callable[[Settings], Backend]
|
|
151
|
+
service_factory: Callable[[Backend, Settings], Service]
|
|
152
|
+
authenticate: Callable[..., Awaitable[None]]
|
|
153
|
+
serve_http: Callable[[Service, Settings], None]
|
|
154
|
+
connect: Callable[[Settings], Coroutine[object, object, None]]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(dependencies: CliDependencies) -> None:
|
|
158
|
+
parser = argparse.ArgumentParser(
|
|
159
|
+
prog="telegram-mcp", description="Local read-only Telegram MCP server"
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {get_version()}")
|
|
162
|
+
parser.add_argument("--config", type=Path, help="Configuration TOML path")
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"command", choices=["serve", "connect", "login", "configure", "status", "catalog"]
|
|
165
|
+
)
|
|
166
|
+
parser.add_argument(
|
|
167
|
+
"--transport", choices=["stdio", "http"], help="Transport for serve (default: stdio)"
|
|
168
|
+
)
|
|
169
|
+
args = parser.parse_args()
|
|
170
|
+
if args.transport is not None and args.command != "serve":
|
|
171
|
+
parser.error("--transport is supported only with serve")
|
|
172
|
+
logging.basicConfig(level=logging.WARNING, stream=sys.stderr)
|
|
173
|
+
try:
|
|
174
|
+
path = (args.config or config_path()).expanduser()
|
|
175
|
+
settings = load_settings(path)
|
|
176
|
+
if args.command == "serve":
|
|
177
|
+
service = dependencies.service_factory(dependencies.backend_factory(settings), settings)
|
|
178
|
+
if args.transport == "http":
|
|
179
|
+
dependencies.serve_http(service, settings)
|
|
180
|
+
else:
|
|
181
|
+
create_server(service).run(transport="stdio")
|
|
182
|
+
elif args.command == "connect":
|
|
183
|
+
asyncio.run(dependencies.connect(settings))
|
|
184
|
+
elif args.command == "login":
|
|
185
|
+
asyncio.run(login(settings, path, dependencies.authenticate))
|
|
186
|
+
elif args.command == "configure":
|
|
187
|
+
asyncio.run(configure(settings, path, dependencies.backend_factory))
|
|
188
|
+
else:
|
|
189
|
+
asyncio.run(diagnose(settings, dependencies, args.command == "catalog"))
|
|
190
|
+
except ToolError as exc:
|
|
191
|
+
print(str(exc), file=sys.stderr)
|
|
192
|
+
if exc.code == "SESSION_IN_USE":
|
|
193
|
+
print(
|
|
194
|
+
"A running server owns this Telegram session. Use get_status through a connected "
|
|
195
|
+
"MCP app, or stop the server before running direct local commands.",
|
|
196
|
+
file=sys.stderr,
|
|
197
|
+
)
|
|
198
|
+
sys.exit(1)
|
|
199
|
+
except ValidationError as exc:
|
|
200
|
+
print(str(validation_error(exc, "Local command")), file=sys.stderr)
|
|
201
|
+
sys.exit(1)
|
|
202
|
+
except OSError:
|
|
203
|
+
print(
|
|
204
|
+
"LOCAL_IO_ERROR: Local command failed to access a file or device. "
|
|
205
|
+
"Check paths and permissions.",
|
|
206
|
+
file=sys.stderr,
|
|
207
|
+
)
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
except EOFError:
|
|
210
|
+
print(
|
|
211
|
+
"INPUT_CLOSED: Interactive input ended; configuration was not saved.", file=sys.stderr
|
|
212
|
+
)
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
except KeyboardInterrupt:
|
|
215
|
+
sys.exit(130)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Wall and monotonic time for the application's injected clock port."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SystemClock:
|
|
8
|
+
def utc_now(self) -> datetime:
|
|
9
|
+
return datetime.now(UTC)
|
|
10
|
+
|
|
11
|
+
def monotonic(self) -> float:
|
|
12
|
+
return time.monotonic()
|