talon-agent 1.0.0

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.
Files changed (89) hide show
  1. package/README.md +137 -0
  2. package/bin/talon.js +5 -0
  3. package/package.json +86 -0
  4. package/prompts/base.md +13 -0
  5. package/prompts/custom.md.example +22 -0
  6. package/prompts/dream.md +41 -0
  7. package/prompts/identity.md +45 -0
  8. package/prompts/teams.md +52 -0
  9. package/prompts/telegram.md +89 -0
  10. package/prompts/terminal.md +13 -0
  11. package/src/__tests__/chat-id.test.ts +91 -0
  12. package/src/__tests__/chat-settings.test.ts +337 -0
  13. package/src/__tests__/config.test.ts +546 -0
  14. package/src/__tests__/cron-store.test.ts +440 -0
  15. package/src/__tests__/daily-log.test.ts +146 -0
  16. package/src/__tests__/dispatcher.test.ts +383 -0
  17. package/src/__tests__/errors.test.ts +240 -0
  18. package/src/__tests__/fuzz.test.ts +302 -0
  19. package/src/__tests__/gateway-actions.test.ts +1453 -0
  20. package/src/__tests__/gateway-context.test.ts +102 -0
  21. package/src/__tests__/gateway-http.test.ts +245 -0
  22. package/src/__tests__/handlers.test.ts +351 -0
  23. package/src/__tests__/history-persistence.test.ts +172 -0
  24. package/src/__tests__/history.test.ts +659 -0
  25. package/src/__tests__/integration.test.ts +189 -0
  26. package/src/__tests__/log.test.ts +110 -0
  27. package/src/__tests__/media-index.test.ts +277 -0
  28. package/src/__tests__/plugin.test.ts +317 -0
  29. package/src/__tests__/prompt-builder.test.ts +71 -0
  30. package/src/__tests__/sessions.test.ts +594 -0
  31. package/src/__tests__/teams-frontend.test.ts +239 -0
  32. package/src/__tests__/telegram.test.ts +177 -0
  33. package/src/__tests__/terminal-commands.test.ts +367 -0
  34. package/src/__tests__/terminal-frontend.test.ts +141 -0
  35. package/src/__tests__/terminal-renderer.test.ts +278 -0
  36. package/src/__tests__/watchdog.test.ts +287 -0
  37. package/src/__tests__/workspace.test.ts +184 -0
  38. package/src/backend/claude-sdk/index.ts +438 -0
  39. package/src/backend/claude-sdk/tools.ts +605 -0
  40. package/src/backend/opencode/index.ts +252 -0
  41. package/src/bootstrap.ts +134 -0
  42. package/src/cli.ts +611 -0
  43. package/src/core/cron.ts +148 -0
  44. package/src/core/dispatcher.ts +126 -0
  45. package/src/core/dream.ts +295 -0
  46. package/src/core/errors.ts +206 -0
  47. package/src/core/gateway-actions.ts +267 -0
  48. package/src/core/gateway.ts +258 -0
  49. package/src/core/plugin.ts +432 -0
  50. package/src/core/prompt-builder.ts +43 -0
  51. package/src/core/pulse.ts +175 -0
  52. package/src/core/types.ts +85 -0
  53. package/src/frontend/teams/actions.ts +101 -0
  54. package/src/frontend/teams/formatting.ts +220 -0
  55. package/src/frontend/teams/graph.ts +297 -0
  56. package/src/frontend/teams/index.ts +308 -0
  57. package/src/frontend/teams/proxy-fetch.ts +28 -0
  58. package/src/frontend/teams/tools.ts +177 -0
  59. package/src/frontend/telegram/actions.ts +437 -0
  60. package/src/frontend/telegram/admin.ts +178 -0
  61. package/src/frontend/telegram/callbacks.ts +251 -0
  62. package/src/frontend/telegram/commands.ts +543 -0
  63. package/src/frontend/telegram/formatting.ts +101 -0
  64. package/src/frontend/telegram/handlers.ts +1008 -0
  65. package/src/frontend/telegram/helpers.ts +105 -0
  66. package/src/frontend/telegram/index.ts +130 -0
  67. package/src/frontend/telegram/middleware.ts +177 -0
  68. package/src/frontend/telegram/userbot.ts +546 -0
  69. package/src/frontend/terminal/commands.ts +303 -0
  70. package/src/frontend/terminal/index.ts +282 -0
  71. package/src/frontend/terminal/input.ts +297 -0
  72. package/src/frontend/terminal/renderer.ts +248 -0
  73. package/src/index.ts +144 -0
  74. package/src/login.ts +89 -0
  75. package/src/storage/chat-settings.ts +218 -0
  76. package/src/storage/cron-store.ts +165 -0
  77. package/src/storage/daily-log.ts +97 -0
  78. package/src/storage/history.ts +278 -0
  79. package/src/storage/media-index.ts +116 -0
  80. package/src/storage/sessions.ts +328 -0
  81. package/src/util/chat-id.ts +21 -0
  82. package/src/util/config.ts +244 -0
  83. package/src/util/log.ts +122 -0
  84. package/src/util/paths.ts +80 -0
  85. package/src/util/time.ts +86 -0
  86. package/src/util/trace.ts +35 -0
  87. package/src/util/watchdog.ts +108 -0
  88. package/src/util/workspace.ts +208 -0
  89. package/tsconfig.json +13 -0
@@ -0,0 +1,605 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server — Telegram tools for the Claude Agent SDK.
4
+ * Communicates with the main bot process via HTTP bridge.
5
+ */
6
+
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import { z } from "zod";
10
+
11
+ const BRIDGE_URL = process.env.TALON_BRIDGE_URL || "http://127.0.0.1:19876";
12
+ const CHAT_ID = process.env.TALON_CHAT_ID || "";
13
+
14
+ async function callBridge(
15
+ action: string,
16
+ params: Record<string, unknown>,
17
+ ): Promise<unknown> {
18
+ const resp = await fetch(`${BRIDGE_URL}/action`, {
19
+ method: "POST",
20
+ headers: { "Content-Type": "application/json" },
21
+ // Include chatId so bridge can verify it matches the active context
22
+ body: JSON.stringify({ action, _chatId: CHAT_ID, ...params }),
23
+ signal: AbortSignal.timeout(120_000), // 2-minute timeout prevents hanging
24
+ });
25
+ if (!resp.ok) {
26
+ const text = await resp.text();
27
+ throw new Error(`Bridge error (${resp.status}): ${text}`);
28
+ }
29
+ return resp.json();
30
+ }
31
+
32
+ function textResult(result: unknown): {
33
+ content: Array<{ type: "text"; text: string }>;
34
+ } {
35
+ const r = result as { text?: string; error?: string };
36
+ return {
37
+ content: [
38
+ { type: "text" as const, text: r.text ?? JSON.stringify(result) },
39
+ ],
40
+ };
41
+ }
42
+
43
+ const server = new McpServer({ name: "telegram-tools", version: "2.0.0" });
44
+
45
+ // ── Unified send tool ────────────────────────────────────────────────────────
46
+
47
+ server.tool(
48
+ "send",
49
+ `Send content to the current Telegram chat. Supports text, photos, videos, files, audio, voice, stickers, polls, locations, contacts, dice, and GIFs.
50
+
51
+ Examples:
52
+ Text: send(type="text", text="Hello!")
53
+ Reply: send(type="text", text="Yes!", reply_to=12345)
54
+ With buttons: send(type="text", text="Pick one", buttons=[[{"text":"A","callback_data":"a"}]])
55
+ Photo: send(type="photo", file_path="/path/to/img.jpg", caption="Look!")
56
+ File: send(type="file", file_path="/path/to/report.pdf")
57
+ Audio: send(type="audio", file_path="/path/to/song.mp3", title="Song Name", performer="Artist")
58
+ Poll: send(type="poll", question="Best language?", options=["Rust","Go","TS"])
59
+ Dice: send(type="dice")
60
+ Location: send(type="location", latitude=37.7749, longitude=-122.4194)
61
+ Sticker: send(type="sticker", file_id="CAACAgI...")`,
62
+ {
63
+ type: z
64
+ .enum([
65
+ "text",
66
+ "photo",
67
+ "file",
68
+ "video",
69
+ "voice",
70
+ "audio",
71
+ "animation",
72
+ "sticker",
73
+ "poll",
74
+ "location",
75
+ "contact",
76
+ "dice",
77
+ ])
78
+ .describe("Content type to send"),
79
+ text: z
80
+ .string()
81
+ .optional()
82
+ .describe("Message text (for type=text). Supports Markdown."),
83
+ reply_to: z.number().optional().describe("Message ID to reply to"),
84
+ file_path: z
85
+ .string()
86
+ .optional()
87
+ .describe("Workspace file path (for photo/file/video/voice/animation)"),
88
+ file_id: z.string().optional().describe("Telegram file_id (for sticker)"),
89
+ caption: z.string().optional().describe("Caption for media"),
90
+ buttons: z
91
+ .array(
92
+ z.array(
93
+ z.object({
94
+ text: z.string(),
95
+ url: z.string().optional(),
96
+ callback_data: z.string().optional(),
97
+ }),
98
+ ),
99
+ )
100
+ .optional()
101
+ .describe("Inline keyboard button rows"),
102
+ question: z.string().optional().describe("Poll question"),
103
+ options: z.array(z.string()).optional().describe("Poll options"),
104
+ is_anonymous: z.boolean().optional().describe("Anonymous poll"),
105
+ correct_option_id: z
106
+ .number()
107
+ .optional()
108
+ .describe("Quiz correct answer index"),
109
+ explanation: z.string().optional().describe("Quiz explanation"),
110
+ latitude: z.number().optional().describe("Location latitude"),
111
+ longitude: z.number().optional().describe("Location longitude"),
112
+ phone_number: z.string().optional().describe("Contact phone"),
113
+ first_name: z.string().optional().describe("Contact first name"),
114
+ last_name: z.string().optional().describe("Contact last name"),
115
+ title: z.string().optional().describe("Audio title (for type=audio)"),
116
+ performer: z.string().optional().describe("Audio performer/artist (for type=audio)"),
117
+ emoji: z.string().optional().describe("Dice emoji (🎲🎯🏀⚽🎳🎰)"),
118
+ delay_seconds: z
119
+ .number()
120
+ .optional()
121
+ .describe("Schedule: delay before sending (1-3600)"),
122
+ },
123
+ async (params) => {
124
+ const { type } = params;
125
+ switch (type) {
126
+ case "text": {
127
+ if (params.delay_seconds) {
128
+ const result = await callBridge("schedule_message", {
129
+ text: params.text,
130
+ delay_seconds: params.delay_seconds,
131
+ });
132
+ return textResult(result);
133
+ }
134
+ if (params.buttons) {
135
+ const result = await callBridge("send_message_with_buttons", {
136
+ text: params.text,
137
+ rows: params.buttons,
138
+ reply_to_message_id: params.reply_to,
139
+ });
140
+ return textResult(result);
141
+ }
142
+ const result = await callBridge("send_message", {
143
+ text: params.text,
144
+ reply_to_message_id: params.reply_to,
145
+ });
146
+ return textResult(result);
147
+ }
148
+ case "photo":
149
+ return textResult(
150
+ await callBridge("send_photo", {
151
+ file_path: params.file_path,
152
+ caption: params.caption,
153
+ reply_to: params.reply_to,
154
+ }),
155
+ );
156
+ case "file":
157
+ return textResult(
158
+ await callBridge("send_file", {
159
+ file_path: params.file_path,
160
+ caption: params.caption,
161
+ reply_to: params.reply_to,
162
+ }),
163
+ );
164
+ case "video":
165
+ return textResult(
166
+ await callBridge("send_video", {
167
+ file_path: params.file_path,
168
+ caption: params.caption,
169
+ reply_to: params.reply_to,
170
+ }),
171
+ );
172
+ case "voice":
173
+ return textResult(
174
+ await callBridge("send_voice", {
175
+ file_path: params.file_path,
176
+ caption: params.caption,
177
+ reply_to: params.reply_to,
178
+ }),
179
+ );
180
+ case "audio":
181
+ return textResult(
182
+ await callBridge("send_audio", {
183
+ file_path: params.file_path,
184
+ caption: params.caption,
185
+ title: params.title,
186
+ performer: params.performer,
187
+ reply_to: params.reply_to,
188
+ }),
189
+ );
190
+ case "animation":
191
+ return textResult(
192
+ await callBridge("send_animation", {
193
+ file_path: params.file_path,
194
+ caption: params.caption,
195
+ reply_to: params.reply_to,
196
+ }),
197
+ );
198
+ case "sticker":
199
+ return textResult(
200
+ await callBridge("send_sticker", {
201
+ file_id: params.file_id,
202
+ reply_to: params.reply_to,
203
+ }),
204
+ );
205
+ case "poll":
206
+ return textResult(
207
+ await callBridge("send_poll", {
208
+ question: params.question,
209
+ options: params.options,
210
+ is_anonymous: params.is_anonymous,
211
+ correct_option_id: params.correct_option_id,
212
+ explanation: params.explanation,
213
+ type: params.correct_option_id !== undefined ? "quiz" : "regular",
214
+ }),
215
+ );
216
+ case "location":
217
+ return textResult(
218
+ await callBridge("send_location", {
219
+ latitude: params.latitude,
220
+ longitude: params.longitude,
221
+ }),
222
+ );
223
+ case "contact":
224
+ return textResult(
225
+ await callBridge("send_contact", {
226
+ phone_number: params.phone_number,
227
+ first_name: params.first_name,
228
+ last_name: params.last_name,
229
+ }),
230
+ );
231
+ case "dice":
232
+ return textResult(
233
+ await callBridge("send_dice", { emoji: params.emoji }),
234
+ );
235
+ default:
236
+ return textResult({ ok: false, error: `Unknown type: ${type}` });
237
+ }
238
+ },
239
+ );
240
+
241
+ // ── Message actions ──────────────────────────────────────────────────────────
242
+
243
+ server.tool(
244
+ "react",
245
+ "Add an emoji reaction to a message. Valid: 👍 👎 ❤ 🔥 🥰 👏 😁 🤔 🤯 😱 🤬 😢 🎉 🤩 🤮 💩 🙏 👌 🕊 🤡 🥱 🥴 😍 🐳 ❤‍🔥 🌚 🌭 💯 🤣 ⚡ 🍌 🏆 💔 🤨 😐 🍓 🍾 💋 🖕 😈 😴 😭 🤓 👻 👨‍💻 👀 🎃 🙈 😇 😨 🤝 ✍ 🤗 🫡 🎅 🎄 ☃ 💅 🤪 🗿 🆒 💘 🙉 🦄 😘 💊 🙊 😎 👾 🤷 🤷‍♂ 🤷‍♀ 😡",
246
+ {
247
+ message_id: z.number().describe("Message ID"),
248
+ emoji: z.string().describe("Reaction emoji"),
249
+ },
250
+ async (params) => textResult(await callBridge("react", params)),
251
+ );
252
+
253
+ server.tool(
254
+ "edit_message",
255
+ "Edit a previously sent message.",
256
+ { message_id: z.number(), text: z.string() },
257
+ async (params) => textResult(await callBridge("edit_message", params)),
258
+ );
259
+
260
+ server.tool(
261
+ "delete_message",
262
+ "Delete a message.",
263
+ { message_id: z.number() },
264
+ async (params) => textResult(await callBridge("delete_message", params)),
265
+ );
266
+
267
+ server.tool(
268
+ "forward_message",
269
+ "Forward a message within the chat.",
270
+ { message_id: z.number() },
271
+ async (params) => textResult(await callBridge("forward_message", params)),
272
+ );
273
+
274
+ server.tool(
275
+ "pin_message",
276
+ "Pin a message.",
277
+ { message_id: z.number() },
278
+ async (params) => textResult(await callBridge("pin_message", params)),
279
+ );
280
+
281
+ server.tool(
282
+ "unpin_message",
283
+ "Unpin a message.",
284
+ { message_id: z.number().optional() },
285
+ async (params) => textResult(await callBridge("unpin_message", params)),
286
+ );
287
+
288
+ server.tool(
289
+ "stop_poll",
290
+ "Stop an active poll and get the final results. Returns vote counts for each option.",
291
+ { message_id: z.number().describe("Message ID of the poll to stop") },
292
+ async (params) => textResult(await callBridge("stop_poll", params)),
293
+ );
294
+
295
+ // ── Chat info ────────────────────────────────────────────────────────────────
296
+
297
+ server.tool(
298
+ "get_chat_info",
299
+ "Get chat title, type, member count.",
300
+ {},
301
+ async () => textResult(await callBridge("get_chat_info", {})),
302
+ );
303
+ server.tool("get_chat_admins", "List chat administrators.", {}, async () =>
304
+ textResult(await callBridge("get_chat_admins", {})),
305
+ );
306
+ server.tool("get_chat_member_count", "Get total member count.", {}, async () =>
307
+ textResult(await callBridge("get_chat_member_count", {})),
308
+ );
309
+ server.tool(
310
+ "set_chat_title",
311
+ "Change chat title (admin).",
312
+ { title: z.string() },
313
+ async (p) => textResult(await callBridge("set_chat_title", p)),
314
+ );
315
+ server.tool(
316
+ "set_chat_description",
317
+ "Change chat description (admin).",
318
+ { description: z.string() },
319
+ async (p) => textResult(await callBridge("set_chat_description", p)),
320
+ );
321
+
322
+ // ── Chat history ─────────────────────────────────────────────────────────────
323
+
324
+ server.tool(
325
+ "read_chat_history",
326
+ "Read messages from the chat. Use 'before' to go back in time (e.g. '2026-03-13').",
327
+ {
328
+ limit: z
329
+ .number()
330
+ .optional()
331
+ .describe("Number of messages (default 30, max 100)"),
332
+ before: z
333
+ .string()
334
+ .optional()
335
+ .describe("Fetch messages before this date (ISO format)"),
336
+ offset_id: z.number().optional().describe("Fetch before this message ID"),
337
+ },
338
+ async (params) =>
339
+ textResult(
340
+ await callBridge("read_history", {
341
+ limit: params.limit ?? 30,
342
+ before: params.before,
343
+ offset_id: params.offset_id,
344
+ }),
345
+ ),
346
+ );
347
+
348
+ server.tool(
349
+ "search_chat_history",
350
+ "Search messages by keyword.",
351
+ { query: z.string(), limit: z.number().optional() },
352
+ async (params) => textResult(await callBridge("search_history", params)),
353
+ );
354
+
355
+ server.tool(
356
+ "get_user_messages",
357
+ "Get messages from a specific user.",
358
+ { user_name: z.string(), limit: z.number().optional() },
359
+ async (params) => textResult(await callBridge("get_user_messages", params)),
360
+ );
361
+
362
+ server.tool(
363
+ "get_message_by_id",
364
+ "Get a specific message by ID.",
365
+ { message_id: z.number() },
366
+ async (params) => textResult(await callBridge("get_message_by_id", params)),
367
+ );
368
+
369
+ server.tool(
370
+ "download_media",
371
+ "Download a photo, document, or other media from a message by its ID. Saves the file to the workspace and returns the file path so you can read/analyze it. Use this when you see a [photo] or [document] in chat history but don't have the file.",
372
+ {
373
+ message_id: z.number().describe("Message ID containing the media to download"),
374
+ },
375
+ async (params) => textResult(await callBridge("download_media", params)),
376
+ );
377
+
378
+ server.tool(
379
+ "get_sticker_pack",
380
+ "Get all stickers in a sticker pack by its name. Returns emoji + file_id for each sticker so you can send them. Use when you see a sticker set name in chat history.",
381
+ {
382
+ set_name: z.string().describe("Sticker set name (e.g. 'AnimatedEmojies' or from sticker metadata)"),
383
+ },
384
+ async (params) => textResult(await callBridge("get_sticker_pack", params)),
385
+ );
386
+
387
+ server.tool(
388
+ "download_sticker",
389
+ "Download a sticker image to workspace so you can view its contents. Returns the file path.",
390
+ {
391
+ file_id: z.string().describe("Sticker file_id from chat history or sticker pack listing"),
392
+ },
393
+ async (params) => textResult(await callBridge("download_sticker", params)),
394
+ );
395
+
396
+ // ── Members ──────────────────────────────────────────────────────────────────
397
+
398
+ server.tool(
399
+ "list_chat_members",
400
+ "List chat members with names, IDs, online status, badges.",
401
+ { limit: z.number().optional() },
402
+ async (params) =>
403
+ textResult(await callBridge("list_known_users", { limit: params.limit })),
404
+ );
405
+
406
+ server.tool(
407
+ "get_member_info",
408
+ "Get detailed info about a user by ID.",
409
+ { user_id: z.number() },
410
+ async (params) => textResult(await callBridge("get_member_info", params)),
411
+ );
412
+
413
+ // ── Scheduling ───────────────────────────────────────────────────────────────
414
+
415
+ server.tool(
416
+ "cancel_scheduled",
417
+ "Cancel a scheduled message.",
418
+ { schedule_id: z.string() },
419
+ async (params) => textResult(await callBridge("cancel_scheduled", params)),
420
+ );
421
+
422
+ // ── Cron jobs ────────────────────────────────────────────────────────────────
423
+
424
+ server.tool(
425
+ "create_cron_job",
426
+ `Create a persistent recurring scheduled job. Jobs survive restarts.
427
+
428
+ Cron format: "minute hour day month weekday" (5 fields)
429
+ Examples:
430
+ "0 9 * * *" = every day at 9:00 AM
431
+ "30 14 * * 1-5" = weekdays at 2:30 PM
432
+ "*/15 * * * *" = every 15 minutes
433
+ "0 0 1 * *" = first day of every month at midnight
434
+ "0 8 * * 1" = every Monday at 8:00 AM
435
+
436
+ Type "message" sends the content as a text message.
437
+ Type "query" runs the content as a Claude prompt with full tool access (can search, create files, send messages, etc).`,
438
+ {
439
+ name: z.string().describe("Human-readable name for the job"),
440
+ schedule: z.string().describe("Cron expression (5-field: minute hour day month weekday)"),
441
+ type: z.enum(["message", "query"]).describe("Job type: 'message' sends text, 'query' runs a Claude prompt"),
442
+ content: z.string().describe("Message text or query prompt"),
443
+ timezone: z.string().optional().describe("IANA timezone (e.g. 'America/New_York'). Defaults to system timezone."),
444
+ },
445
+ async (params) => textResult(await callBridge("create_cron_job", params)),
446
+ );
447
+
448
+ server.tool(
449
+ "list_cron_jobs",
450
+ "List all cron jobs in the current chat with their status, schedule, run count, and next run time.",
451
+ {},
452
+ async () => textResult(await callBridge("list_cron_jobs", {})),
453
+ );
454
+
455
+ server.tool(
456
+ "edit_cron_job",
457
+ "Edit an existing cron job. Only provide the fields you want to change.",
458
+ {
459
+ job_id: z.string().describe("Job ID to edit"),
460
+ name: z.string().optional().describe("New name"),
461
+ schedule: z.string().optional().describe("New cron expression"),
462
+ type: z.enum(["message", "query"]).optional().describe("New job type"),
463
+ content: z.string().optional().describe("New content"),
464
+ enabled: z.boolean().optional().describe("Enable or disable the job"),
465
+ timezone: z.string().optional().describe("New IANA timezone"),
466
+ },
467
+ async (params) => textResult(await callBridge("edit_cron_job", params)),
468
+ );
469
+
470
+ server.tool(
471
+ "delete_cron_job",
472
+ "Delete a cron job permanently.",
473
+ {
474
+ job_id: z.string().describe("Job ID to delete"),
475
+ },
476
+ async (params) => textResult(await callBridge("delete_cron_job", params)),
477
+ );
478
+
479
+ server.tool(
480
+ "save_sticker_pack",
481
+ "Save a sticker pack's file_ids to workspace for quick reuse. Once saved, you can read the JSON file to find stickers by emoji and send them instantly.",
482
+ {
483
+ set_name: z.string().describe("Sticker set name"),
484
+ },
485
+ async (params) => textResult(await callBridge("save_sticker_pack", params)),
486
+ );
487
+
488
+ // ── Sticker pack management ──────────────────────────────────────────────────
489
+
490
+ server.tool(
491
+ "create_sticker_set",
492
+ `Create a new sticker pack owned by a user. The bot will be the creator.
493
+ Sticker images must be PNG/WEBP, max 512x512px for static stickers.
494
+ The set name will automatically get "_by_<botname>" appended if needed.
495
+
496
+ Example: create_sticker_set(user_id=123, name="cool_pack", title="Cool Stickers", file_path="/path/to/sticker.png", emoji_list=["😎"])`,
497
+ {
498
+ user_id: z.number().describe("Telegram user ID who will own the pack"),
499
+ name: z.string().describe("Short name for the pack (a-z, 0-9, underscores). Will get _by_<botname> appended."),
500
+ title: z.string().describe("Display title for the pack (1-64 chars)"),
501
+ file_path: z.string().describe("Path to the sticker image file (PNG/WEBP, 512x512 max)"),
502
+ emoji_list: z.array(z.string()).optional().describe("Emojis for this sticker (default: ['🎨'])"),
503
+ format: z.enum(["static", "animated", "video"]).optional().describe("Sticker format (default: static)"),
504
+ },
505
+ async (params) => textResult(await callBridge("create_sticker_set", params)),
506
+ );
507
+
508
+ server.tool(
509
+ "add_sticker_to_set",
510
+ "Add a new sticker to an existing sticker pack created by the bot.",
511
+ {
512
+ user_id: z.number().describe("Telegram user ID who owns the pack"),
513
+ name: z.string().describe("Sticker set name (including _by_<botname>)"),
514
+ file_path: z.string().describe("Path to the sticker image file"),
515
+ emoji_list: z.array(z.string()).optional().describe("Emojis for this sticker (default: ['🎨'])"),
516
+ format: z.enum(["static", "animated", "video"]).optional().describe("Sticker format (default: static)"),
517
+ },
518
+ async (params) => textResult(await callBridge("add_sticker_to_set", params)),
519
+ );
520
+
521
+ server.tool(
522
+ "delete_sticker_from_set",
523
+ "Remove a specific sticker from a pack by its file_id.",
524
+ {
525
+ sticker_file_id: z.string().describe("file_id of the sticker to remove (get from get_sticker_pack)"),
526
+ },
527
+ async (params) => textResult(await callBridge("delete_sticker_from_set", params)),
528
+ );
529
+
530
+ server.tool(
531
+ "set_sticker_set_title",
532
+ "Change the title of a sticker pack created by the bot.",
533
+ {
534
+ name: z.string().describe("Sticker set name"),
535
+ title: z.string().describe("New title (1-64 chars)"),
536
+ },
537
+ async (params) => textResult(await callBridge("set_sticker_set_title", params)),
538
+ );
539
+
540
+ server.tool(
541
+ "delete_sticker_set",
542
+ "Permanently delete an entire sticker pack created by the bot.",
543
+ {
544
+ name: z.string().describe("Sticker set name to delete"),
545
+ },
546
+ async (params) => textResult(await callBridge("delete_sticker_set", params)),
547
+ );
548
+
549
+ // ── Chat analytics ───────────────────────────────────────────────────────────
550
+
551
+ server.tool(
552
+ "get_pinned_messages",
553
+ "Get all pinned messages in the current chat.",
554
+ {},
555
+ async () => textResult(await callBridge("get_pinned_messages", {})),
556
+ );
557
+
558
+ server.tool(
559
+ "online_count",
560
+ "Get how many members are currently online or recently active.",
561
+ {},
562
+ async () => textResult(await callBridge("online_count", {})),
563
+ );
564
+
565
+ server.tool(
566
+ "list_media",
567
+ "List recent photos, documents, and other media in the current chat with file paths. Use this to find a previously sent photo or file to re-read or reference.",
568
+ {
569
+ limit: z.number().optional().describe("Number of entries (default 10, max 20)"),
570
+ },
571
+ async (params) => textResult(await callBridge("list_media", { limit: params.limit })),
572
+ );
573
+
574
+ // ── Web ─────────────────────────────────────────────────────────────────────
575
+
576
+ server.tool(
577
+ "web_search",
578
+ "Search the web using SearXNG. Returns titles, URLs, and snippets. Use for current events, facts, or finding URLs to fetch.",
579
+ {
580
+ query: z.string().describe("Search query"),
581
+ limit: z.number().optional().describe("Max results (default 5, max 10)"),
582
+ },
583
+ async (params) => textResult(await callBridge("web_search", params)),
584
+ );
585
+
586
+ server.tool(
587
+ "fetch_url",
588
+ "Fetch a URL — web pages return text content, image URLs are downloaded to workspace. Use to read articles, download images, or fetch any URL.",
589
+ {
590
+ url: z.string().describe("The URL to fetch"),
591
+ },
592
+ async (params) => textResult(await callBridge("fetch_url", params)),
593
+ );
594
+
595
+ // ── Start ────────────────────────────────────────────────────────────────────
596
+
597
+ async function main() {
598
+ const transport = new StdioServerTransport();
599
+ await server.connect(transport);
600
+ }
601
+
602
+ main().catch((err) => {
603
+ console.error("MCP server error:", err);
604
+ process.exit(1);
605
+ });