@ebowwa/mcp-telegram 0.1.4

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.
package/src/index.ts ADDED
@@ -0,0 +1,907 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @ebowwa/telegram-mcp - Telegram MTProto Client MCP Server
4
+ *
5
+ * Full-featured Telegram user account integration using GramJS:
6
+ * - Connect/authenticate with API ID and hash
7
+ * - Send messages to users, groups, channels
8
+ * - Get dialogs (chat list)
9
+ * - Get messages from chats
10
+ * - Get entity info (users, chats, channels)
11
+ *
12
+ * Prerequisites:
13
+ * 1. Get API ID and API Hash from https://my.telegram.org/apps
14
+ * 2. First connection requires phone number + verification code
15
+ * 3. Session string is saved for subsequent connections
16
+ */
17
+
18
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
19
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
+ import {
21
+ CallToolRequestSchema,
22
+ ListToolsRequestSchema,
23
+ } from "@modelcontextprotocol/sdk/types.js";
24
+ import { TelegramClient } from "telegram";
25
+ import { StringSession } from "telegram/sessions";
26
+ import { Api } from "telegram";
27
+ import bigInt from "big-integer";
28
+ import { homedir } from "os";
29
+ import { join } from "path";
30
+ import { mkdirSync, existsSync, writeFileSync, readFileSync } from "fs";
31
+ import { execSync } from "child_process";
32
+
33
+ // ==============
34
+ // Configuration
35
+ // ==============
36
+
37
+ const SESSION_DIR = join(homedir(), ".telegram-mcp");
38
+ const SESSION_FILE = join(SESSION_DIR, "session.txt");
39
+
40
+ // Ensure session directory exists
41
+ if (!existsSync(SESSION_DIR)) {
42
+ mkdirSync(SESSION_DIR, { recursive: true });
43
+ }
44
+
45
+ // Global client instance
46
+ let client: TelegramClient | null = null;
47
+ let sessionString: string = "";
48
+
49
+ // ==============
50
+ // Helper Functions
51
+ // ==============
52
+
53
+ async function loadSession(): Promise<string> {
54
+ if (existsSync(SESSION_FILE)) {
55
+ return readFileSync(SESSION_FILE, "utf-8").trim();
56
+ }
57
+ return "";
58
+ }
59
+
60
+ async function saveSession(session: string): Promise<void> {
61
+ writeFileSync(SESSION_FILE, session, "utf-8");
62
+ }
63
+
64
+ async function getClient(): Promise<TelegramClient> {
65
+ if (client && client.connected) {
66
+ return client;
67
+ }
68
+
69
+ // Auto-connect using saved session
70
+ const apiId = parseInt(process.env.TELEGRAM_API_ID || "0");
71
+ const apiHash = process.env.TELEGRAM_API_HASH;
72
+
73
+ if (!apiId || !apiHash) {
74
+ throw new Error("TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables required.");
75
+ }
76
+
77
+ const savedSession = await loadSession();
78
+ if (!savedSession) {
79
+ throw new Error("No saved session. Run telegram_connect with phone number first.");
80
+ }
81
+
82
+ const session = new StringSession(savedSession);
83
+ client = new TelegramClient(session, apiId, apiHash, {
84
+ connectionRetries: 5,
85
+ });
86
+
87
+ await client.connect();
88
+
89
+ if (!(await client.checkAuthorization())) {
90
+ client = null;
91
+ throw new Error("Session expired. Run telegram_connect again.");
92
+ }
93
+
94
+ return client;
95
+ }
96
+
97
+ function formatEntity(entity: any): any {
98
+ if (!entity) return null;
99
+
100
+ const base = {
101
+ id: entity.id?.toString(),
102
+ className: entity.className,
103
+ };
104
+
105
+ if (entity.className === "User") {
106
+ return {
107
+ ...base,
108
+ firstName: entity.firstName,
109
+ lastName: entity.lastName,
110
+ username: entity.username,
111
+ phone: entity.phone,
112
+ bot: entity.bot,
113
+ };
114
+ }
115
+
116
+ if (entity.className === "Chat" || entity.className === "Channel") {
117
+ return {
118
+ ...base,
119
+ title: entity.title,
120
+ username: entity.username,
121
+ megagroup: entity.megagroup,
122
+ };
123
+ }
124
+
125
+ return base;
126
+ }
127
+
128
+ function formatMessage(message: any): any {
129
+ if (!message) return null;
130
+
131
+ return {
132
+ id: message.id,
133
+ date: message.date ? new Date(message.date * 1000).toISOString() : null,
134
+ message: message.message,
135
+ fromId: message.fromId?.userId?.toString() || message.fromId?.toString(),
136
+ peerId: message.peerId?.userId?.toString() || message.peerId?.chatId?.toString() || message.peerId?.channelId?.toString(),
137
+ out: message.out,
138
+ media: message.media?.className,
139
+ };
140
+ }
141
+
142
+ // ==============
143
+ // MCP Server
144
+ // ==============
145
+
146
+ const server = new Server(
147
+ {
148
+ name: "@ebowwa/telegram-mcp",
149
+ version: "0.1.0",
150
+ },
151
+ {
152
+ capabilities: {
153
+ tools: {},
154
+ },
155
+ }
156
+ );
157
+
158
+ // List available tools
159
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
160
+ return {
161
+ tools: [
162
+ // Connection Management
163
+ {
164
+ name: "telegram_connect",
165
+ description: "Connect to Telegram with API credentials. Requires TELEGRAM_API_ID and TELEGRAM_API_HASH env vars. First connection needs phone/code verification.",
166
+ inputSchema: {
167
+ type: "object",
168
+ properties: {
169
+ phoneNumber: {
170
+ type: "string",
171
+ description: "Phone number with country code (e.g., +1234567890). Required for first connection.",
172
+ },
173
+ code: {
174
+ type: "string",
175
+ description: "Verification code received via Telegram. Required after initial phone submission.",
176
+ },
177
+ password: {
178
+ type: "string",
179
+ description: "2FA password if enabled on account.",
180
+ },
181
+ sessionString: {
182
+ type: "string",
183
+ description: "Existing session string to restore (optional, overrides saved session).",
184
+ },
185
+ },
186
+ },
187
+ },
188
+ {
189
+ name: "telegram_disconnect",
190
+ description: "Disconnect from Telegram and clear the session.",
191
+ inputSchema: {
192
+ type: "object",
193
+ properties: {},
194
+ },
195
+ },
196
+ {
197
+ name: "telegram_get_session",
198
+ description: "Get the current session string for backup/restore purposes.",
199
+ inputSchema: {
200
+ type: "object",
201
+ properties: {},
202
+ },
203
+ },
204
+ {
205
+ name: "telegram_get_me",
206
+ description: "Get information about the currently authenticated user.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {},
210
+ },
211
+ },
212
+ {
213
+ name: "telegram_restart",
214
+ description: "Restart the Telegram MCP systemd service. Useful for applying config changes or recovering from errors.",
215
+ inputSchema: {
216
+ type: "object",
217
+ properties: {
218
+ serviceName: {
219
+ type: "string",
220
+ description: "Systemd service name (default: telegram-mcp)",
221
+ },
222
+ },
223
+ },
224
+ },
225
+
226
+ // Messaging
227
+ {
228
+ name: "telegram_send_message",
229
+ description: "Send a message to a user, group, or channel.",
230
+ inputSchema: {
231
+ type: "object",
232
+ properties: {
233
+ entity: {
234
+ type: "string",
235
+ description: "Username, phone, or entity ID to send message to (e.g., 'username', '+1234567890', '123456789')",
236
+ },
237
+ message: {
238
+ type: "string",
239
+ description: "Message text to send",
240
+ },
241
+ parseMode: {
242
+ type: "string",
243
+ enum: ["md", "html", ""],
244
+ description: "Parse mode for formatting (md=Markdown, html=HTML)",
245
+ },
246
+ replyTo: {
247
+ type: "number",
248
+ description: "Message ID to reply to (optional)",
249
+ },
250
+ },
251
+ required: ["entity", "message"],
252
+ },
253
+ },
254
+ {
255
+ name: "telegram_get_dialogs",
256
+ description: "Get list of all chats/conversations (dialogs).",
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {
260
+ limit: {
261
+ type: "number",
262
+ description: "Maximum number of dialogs to return (default: 100)",
263
+ },
264
+ offsetDate: {
265
+ type: "number",
266
+ description: "Offset date for pagination",
267
+ },
268
+ },
269
+ },
270
+ },
271
+ {
272
+ name: "telegram_get_messages",
273
+ description: "Get messages from a chat/conversation.",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ entity: {
278
+ type: "string",
279
+ description: "Username, phone, or entity ID of the chat",
280
+ },
281
+ limit: {
282
+ type: "number",
283
+ description: "Maximum number of messages to return (default: 100)",
284
+ },
285
+ offsetId: {
286
+ type: "number",
287
+ description: "Message ID to start from (for pagination)",
288
+ },
289
+ minId: {
290
+ type: "number",
291
+ description: "Minimum message ID to fetch",
292
+ },
293
+ maxId: {
294
+ type: "number",
295
+ description: "Maximum message ID to fetch",
296
+ },
297
+ reverse: {
298
+ type: "boolean",
299
+ description: "Fetch messages in reverse order (oldest first)",
300
+ },
301
+ },
302
+ required: ["entity"],
303
+ },
304
+ },
305
+ {
306
+ name: "telegram_get_entity",
307
+ description: "Get information about a user, chat, or channel by username or ID.",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ entity: {
312
+ type: "string",
313
+ description: "Username, phone, or entity ID to look up",
314
+ },
315
+ },
316
+ required: ["entity"],
317
+ },
318
+ },
319
+ {
320
+ name: "telegram_mark_read",
321
+ description: "Mark messages as read in a chat.",
322
+ inputSchema: {
323
+ type: "object",
324
+ properties: {
325
+ entity: {
326
+ type: "string",
327
+ description: "Username, phone, or entity ID of the chat",
328
+ },
329
+ maxId: {
330
+ type: "number",
331
+ description: "Maximum message ID to mark as read (default: latest)",
332
+ },
333
+ },
334
+ required: ["entity"],
335
+ },
336
+ },
337
+ {
338
+ name: "telegram_delete_messages",
339
+ description: "Delete messages from a chat.",
340
+ inputSchema: {
341
+ type: "object",
342
+ properties: {
343
+ entity: {
344
+ type: "string",
345
+ description: "Username, phone, or entity ID of the chat",
346
+ },
347
+ messageIds: {
348
+ type: "array",
349
+ items: { type: "number" },
350
+ description: "Array of message IDs to delete",
351
+ },
352
+ revoke: {
353
+ type: "boolean",
354
+ description: "Delete for both parties (default: true)",
355
+ },
356
+ },
357
+ required: ["entity", "messageIds"],
358
+ },
359
+ },
360
+ {
361
+ name: "telegram_edit_message",
362
+ description: "Edit a sent message.",
363
+ inputSchema: {
364
+ type: "object",
365
+ properties: {
366
+ entity: {
367
+ type: "string",
368
+ description: "Username, phone, or entity ID of the chat",
369
+ },
370
+ messageId: {
371
+ type: "number",
372
+ description: "ID of the message to edit",
373
+ },
374
+ message: {
375
+ type: "string",
376
+ description: "New message text",
377
+ },
378
+ parseMode: {
379
+ type: "string",
380
+ enum: ["md", "html", ""],
381
+ description: "Parse mode for formatting",
382
+ },
383
+ },
384
+ required: ["entity", "messageId", "message"],
385
+ },
386
+ },
387
+ {
388
+ name: "telegram_forward_messages",
389
+ description: "Forward messages to another chat.",
390
+ inputSchema: {
391
+ type: "object",
392
+ properties: {
393
+ fromEntity: {
394
+ type: "string",
395
+ description: "Source chat username or ID",
396
+ },
397
+ toEntity: {
398
+ type: "string",
399
+ description: "Destination chat username or ID",
400
+ },
401
+ messageIds: {
402
+ type: "array",
403
+ items: { type: "number" },
404
+ description: "Array of message IDs to forward",
405
+ },
406
+ },
407
+ required: ["fromEntity", "toEntity", "messageIds"],
408
+ },
409
+ },
410
+
411
+ // Contacts & Users
412
+ {
413
+ name: "telegram_get_contacts",
414
+ description: "Get list of contacts.",
415
+ inputSchema: {
416
+ type: "object",
417
+ properties: {
418
+ limit: {
419
+ type: "number",
420
+ description: "Maximum number of contacts to return",
421
+ },
422
+ },
423
+ },
424
+ },
425
+
426
+ // Chats & Channels
427
+ {
428
+ name: "telegram_create_group",
429
+ description: "Create a new group chat.",
430
+ inputSchema: {
431
+ type: "object",
432
+ properties: {
433
+ title: {
434
+ type: "string",
435
+ description: "Group title",
436
+ },
437
+ users: {
438
+ type: "array",
439
+ items: { type: "string" },
440
+ description: "Array of user usernames or IDs to add",
441
+ },
442
+ },
443
+ required: ["title", "users"],
444
+ },
445
+ },
446
+ {
447
+ name: "telegram_get_participants",
448
+ description: "Get participants of a group or channel.",
449
+ inputSchema: {
450
+ type: "object",
451
+ properties: {
452
+ entity: {
453
+ type: "string",
454
+ description: "Group/channel username or ID",
455
+ },
456
+ limit: {
457
+ type: "number",
458
+ description: "Maximum number of participants to return",
459
+ },
460
+ },
461
+ required: ["entity"],
462
+ },
463
+ },
464
+ ],
465
+ };
466
+ });
467
+
468
+ // Handle tool calls
469
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
470
+ const { name, arguments: args } = request.params;
471
+
472
+ try {
473
+ switch (name) {
474
+ // ==================
475
+ // Connection
476
+ // ==================
477
+ case "telegram_connect": {
478
+ const apiId = parseInt(process.env.TELEGRAM_API_ID || "0");
479
+ const apiHash = process.env.TELEGRAM_API_HASH;
480
+
481
+ if (!apiId || !apiHash) {
482
+ throw new Error("TELEGRAM_API_ID and TELEGRAM_API_HASH environment variables required. Get them from https://my.telegram.org/apps");
483
+ }
484
+
485
+ // Use provided session or load saved one
486
+ const savedSession = (args?.sessionString as string) || await loadSession();
487
+ const session = new StringSession(savedSession);
488
+
489
+ client = new TelegramClient(session, apiId, apiHash, {
490
+ connectionRetries: 5,
491
+ });
492
+
493
+ const phoneNumber = args?.phoneNumber as string | undefined;
494
+ const code = args?.code as string | undefined;
495
+ const password = args?.password as string | undefined;
496
+
497
+ // Start the client with auth options
498
+ const startOptions: any = {
499
+ onError: (err: Error) => console.error("Telegram auth error:", err),
500
+ };
501
+
502
+ if (phoneNumber) {
503
+ startOptions.phoneNumber = async () => phoneNumber;
504
+ }
505
+ if (code) {
506
+ startOptions.phoneCode = async () => code;
507
+ }
508
+ if (password) {
509
+ startOptions.password = async () => password;
510
+ }
511
+
512
+ await client.start(startOptions);
513
+
514
+ // Save session for future use
515
+ sessionString = client.session.save() as unknown as string;
516
+ await saveSession(sessionString);
517
+
518
+ const me = await client.getMe();
519
+
520
+ return {
521
+ content: [{
522
+ type: "text",
523
+ text: JSON.stringify({
524
+ success: true,
525
+ message: "Connected to Telegram",
526
+ user: formatEntity(me),
527
+ sessionString: sessionString,
528
+ }, null, 2),
529
+ }],
530
+ };
531
+ }
532
+
533
+ case "telegram_disconnect": {
534
+ if (client) {
535
+ await client.disconnect();
536
+ client = null;
537
+ sessionString = "";
538
+ }
539
+ return {
540
+ content: [{
541
+ type: "text",
542
+ text: JSON.stringify({ success: true, message: "Disconnected from Telegram" }),
543
+ }],
544
+ };
545
+ }
546
+
547
+ case "telegram_get_session": {
548
+ if (!sessionString && existsSync(SESSION_FILE)) {
549
+ sessionString = await loadSession();
550
+ }
551
+ return {
552
+ content: [{
553
+ type: "text",
554
+ text: JSON.stringify({ sessionString: sessionString || null }),
555
+ }],
556
+ };
557
+ }
558
+
559
+ case "telegram_get_me": {
560
+ const c = await getClient();
561
+ const me = await c.getMe();
562
+ return {
563
+ content: [{
564
+ type: "text",
565
+ text: JSON.stringify(formatEntity(me), null, 2),
566
+ }],
567
+ };
568
+ }
569
+
570
+ case "telegram_restart": {
571
+ const serviceName = (args?.serviceName as string) || "telegram-mcp";
572
+ try {
573
+ // Restart the systemd service
574
+ execSync(`sudo systemctl restart ${serviceName}`, {
575
+ encoding: "utf-8",
576
+ timeout: 30000,
577
+ });
578
+
579
+ return {
580
+ content: [{
581
+ type: "text",
582
+ text: JSON.stringify({
583
+ success: true,
584
+ message: `Systemd service '${serviceName}' restarted successfully`,
585
+ serviceName,
586
+ }, null, 2),
587
+ }],
588
+ };
589
+ } catch (error: any) {
590
+ // Check if systemctl is available (not on macOS)
591
+ if (error.message.includes("systemctl: command not found")) {
592
+ return {
593
+ content: [{
594
+ type: "text",
595
+ text: JSON.stringify({
596
+ error: true,
597
+ message: "systemctl not found. This command is only available on Linux systems with systemd.",
598
+ hint: "On macOS, use 'brew services restart' or manual process management.",
599
+ }, null, 2),
600
+ }],
601
+ isError: true,
602
+ };
603
+ }
604
+ throw error;
605
+ }
606
+ }
607
+
608
+ // ==================
609
+ // Messaging
610
+ // ==================
611
+ case "telegram_send_message": {
612
+ if (!args?.entity || !args?.message) {
613
+ throw new Error("entity and message are required");
614
+ }
615
+ const c = await getClient();
616
+ const entity = await c.getInputEntity(args.entity as string);
617
+
618
+ const result = await c.sendMessage(entity, {
619
+ message: args.message as string,
620
+ parseMode: args.parseMode as any,
621
+ replyTo: args.replyTo as number,
622
+ });
623
+
624
+ return {
625
+ content: [{
626
+ type: "text",
627
+ text: JSON.stringify({
628
+ success: true,
629
+ message: formatMessage(result),
630
+ }, null, 2),
631
+ }],
632
+ };
633
+ }
634
+
635
+ case "telegram_get_dialogs": {
636
+ const c = await getClient();
637
+ const limit = (args?.limit as number) || 100;
638
+
639
+ const dialogs = await c.getDialogs({ limit });
640
+
641
+ const formatted = dialogs.map((d: any) => ({
642
+ id: d.id?.toString(),
643
+ name: d.name,
644
+ unreadCount: d.unreadCount,
645
+ archived: d.archived,
646
+ pinned: d.pinned,
647
+ entity: formatEntity(d.entity),
648
+ }));
649
+
650
+ return {
651
+ content: [{
652
+ type: "text",
653
+ text: JSON.stringify(formatted, null, 2),
654
+ }],
655
+ };
656
+ }
657
+
658
+ case "telegram_get_messages": {
659
+ if (!args?.entity) {
660
+ throw new Error("entity is required");
661
+ }
662
+ const c = await getClient();
663
+ const entity = await c.getInputEntity(args.entity as string);
664
+
665
+ const messages = await c.getMessages(entity, {
666
+ limit: (args.limit as number) || 100,
667
+ offsetId: args.offsetId as number,
668
+ minId: args.minId as number,
669
+ maxId: args.maxId as number,
670
+ reverse: args.reverse as boolean,
671
+ });
672
+
673
+ const formatted = messages.map(formatMessage);
674
+
675
+ return {
676
+ content: [{
677
+ type: "text",
678
+ text: JSON.stringify({
679
+ total: messages.total,
680
+ messages: formatted,
681
+ }, null, 2),
682
+ }],
683
+ };
684
+ }
685
+
686
+ case "telegram_get_entity": {
687
+ if (!args?.entity) {
688
+ throw new Error("entity is required");
689
+ }
690
+ const c = await getClient();
691
+ const entity = await c.getEntity(args.entity as string);
692
+ return {
693
+ content: [{
694
+ type: "text",
695
+ text: JSON.stringify(formatEntity(entity), null, 2),
696
+ }],
697
+ };
698
+ }
699
+
700
+ case "telegram_mark_read": {
701
+ if (!args?.entity) {
702
+ throw new Error("entity is required");
703
+ }
704
+ const c = await getClient();
705
+ const entity = await c.getInputEntity(args.entity as string);
706
+
707
+ await c.invoke(
708
+ new Api.messages.ReadHistory({
709
+ peer: entity,
710
+ maxId: (args.maxId as number) || 0,
711
+ })
712
+ );
713
+
714
+ return {
715
+ content: [{
716
+ type: "text",
717
+ text: JSON.stringify({ success: true, message: "Messages marked as read" }),
718
+ }],
719
+ };
720
+ }
721
+
722
+ case "telegram_delete_messages": {
723
+ if (!args?.entity || !args?.messageIds) {
724
+ throw new Error("entity and messageIds are required");
725
+ }
726
+ const c = await getClient();
727
+ const entity = await c.getInputEntity(args.entity as string);
728
+
729
+ await c.deleteMessages(
730
+ entity,
731
+ args.messageIds as number[],
732
+ { revoke: args.revoke !== false }
733
+ );
734
+
735
+ return {
736
+ content: [{
737
+ type: "text",
738
+ text: JSON.stringify({ success: true, message: "Messages deleted" }),
739
+ }],
740
+ };
741
+ }
742
+
743
+ case "telegram_edit_message": {
744
+ if (!args?.entity || !args?.messageId || !args?.message) {
745
+ throw new Error("entity, messageId, and message are required");
746
+ }
747
+ const c = await getClient();
748
+ const entity = await c.getInputEntity(args.entity as string);
749
+
750
+ const result = await c.editMessage(entity, {
751
+ message: args.messageId as number,
752
+ text: args.message as string,
753
+ parseMode: args.parseMode as any,
754
+ });
755
+
756
+ return {
757
+ content: [{
758
+ type: "text",
759
+ text: JSON.stringify({
760
+ success: true,
761
+ message: formatMessage(result),
762
+ }, null, 2),
763
+ }],
764
+ };
765
+ }
766
+
767
+ case "telegram_forward_messages": {
768
+ if (!args?.fromEntity || !args?.toEntity || !args?.messageIds) {
769
+ throw new Error("fromEntity, toEntity, and messageIds are required");
770
+ }
771
+ const c = await getClient();
772
+ const fromEntity = await c.getInputEntity(args.fromEntity as string);
773
+ const toEntity = await c.getInputEntity(args.toEntity as string);
774
+
775
+ const result = await c.forwardMessages(toEntity, {
776
+ messages: args.messageIds as number[],
777
+ fromPeer: fromEntity,
778
+ });
779
+
780
+ return {
781
+ content: [{
782
+ type: "text",
783
+ text: JSON.stringify({
784
+ success: true,
785
+ forwarded: result.length,
786
+ }, null, 2),
787
+ }],
788
+ };
789
+ }
790
+
791
+ // ==================
792
+ // Contacts
793
+ // ==================
794
+ case "telegram_get_contacts": {
795
+ const c = await getClient();
796
+ const result = await c.invoke(
797
+ new Api.contacts.GetContacts({
798
+ hash: bigInt(0),
799
+ })
800
+ );
801
+
802
+ const contacts = (result as any).users?.map(formatEntity) || [];
803
+
804
+ return {
805
+ content: [{
806
+ type: "text",
807
+ text: JSON.stringify(contacts, null, 2),
808
+ }],
809
+ };
810
+ }
811
+
812
+ // ==================
813
+ // Groups & Channels
814
+ // ==================
815
+ case "telegram_create_group": {
816
+ if (!args?.title || !args?.users) {
817
+ throw new Error("title and users are required");
818
+ }
819
+ const c = await getClient();
820
+
821
+ const users = await Promise.all(
822
+ (args.users as string[]).map((u: string) => c.getInputEntity(u))
823
+ );
824
+
825
+ const result = await c.invoke(
826
+ new Api.messages.CreateChat({
827
+ users: users,
828
+ title: args.title as string,
829
+ })
830
+ );
831
+
832
+ return {
833
+ content: [{
834
+ type: "text",
835
+ text: JSON.stringify({
836
+ success: true,
837
+ chat: formatEntity((result as any).chats?.[0]),
838
+ }, null, 2),
839
+ }],
840
+ };
841
+ }
842
+
843
+ case "telegram_get_participants": {
844
+ if (!args?.entity) {
845
+ throw new Error("entity is required");
846
+ }
847
+ const c = await getClient();
848
+ const entity = await c.getInputEntity(args.entity as string);
849
+
850
+ const result = await c.invoke(
851
+ new Api.channels.GetParticipants({
852
+ channel: entity,
853
+ filter: new Api.ChannelParticipantsRecent(),
854
+ limit: (args.limit as number) || 100,
855
+ offset: 0,
856
+ })
857
+ );
858
+
859
+ const participants = (result as any).participants?.map((p: any) => ({
860
+ userId: p.userId?.toString(),
861
+ date: p.date ? new Date(p.date * 1000).toISOString() : null,
862
+ className: p.className,
863
+ })) || [];
864
+
865
+ return {
866
+ content: [{
867
+ type: "text",
868
+ text: JSON.stringify({
869
+ total: (result as any).count,
870
+ participants,
871
+ }, null, 2),
872
+ }],
873
+ };
874
+ }
875
+
876
+ default:
877
+ throw new Error(`Unknown tool: ${name}`);
878
+ }
879
+ } catch (error: any) {
880
+ return {
881
+ content: [{
882
+ type: "text",
883
+ text: JSON.stringify({
884
+ error: true,
885
+ message: error.message,
886
+ stack: error.stack,
887
+ }, null, 2),
888
+ }],
889
+ isError: true,
890
+ };
891
+ }
892
+ });
893
+
894
+ // ==============
895
+ // Start Server
896
+ // ==============
897
+
898
+ async function main() {
899
+ const transport = new StdioServerTransport();
900
+ await server.connect(transport);
901
+ console.error("Telegram MCP server running on stdio");
902
+ }
903
+
904
+ main().catch((error) => {
905
+ console.error("Fatal error:", error);
906
+ process.exit(1);
907
+ });