@fre4x/telegram 1.1.3 → 1.1.5

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 (3) hide show
  1. package/README.md +10 -5
  2. package/dist/index.js +485 -175
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ Telegram MCP server implementing Claude Code Channels for bidirectional communic
4
4
 
5
5
  ## Features
6
6
 
7
- - **Push Notifications:** Sends incoming Telegram messages directly to the Claude Code session via `notifications/push_event`.
7
+ - **Push Notifications:** Sends incoming Telegram messages to the connected MCP client via `notifications/message` (claude/channel protocol).
8
8
  - **Bidirectional Chat:** Allows Claude to send messages to Telegram using the `send_telegram_message` tool.
9
9
  - **Security:** Filters incoming messages based on an allowed user ID.
10
10
  - **Remote Control:** (Planned) Approve or deny Claude's tool-use prompts from your mobile device.
@@ -62,14 +62,19 @@ If you are using this plugin as a bidirectional Claude Code channel rather than
62
62
  claude --channels plugin:telegram --dangerously-load-development-channels
63
63
  ```
64
64
 
65
- ## Development
65
+ ## Mock Mode
66
+
67
+ Run without a real bot token (returns fixture data of identical shape):
66
68
 
67
69
  ```bash
68
- npm run dev
70
+ MOCK=true npx @fre4x/telegram
69
71
  ```
70
72
 
71
- Run the MCP Inspector:
73
+ ## Development
72
74
 
73
75
  ```bash
74
- npm run inspector
76
+ npm run dev # tsx, no build
77
+ npm run build # esbuild → dist/
78
+ npm test # vitest unit tests (MOCK=true)
79
+ npm run inspector # MCP Inspector (MOCK=true)
75
80
  ```
package/dist/index.js CHANGED
@@ -13,7 +13,11 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
13
13
  throw Error('Dynamic require of "' + x + '" is not supported');
14
14
  });
15
15
  var __commonJS = (cb, mod) => function __require2() {
16
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
16
+ try {
17
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
18
+ } catch (e) {
19
+ throw mod = 0, e;
20
+ }
17
21
  };
18
22
  var __export = (target, all) => {
19
23
  for (var name in all)
@@ -3112,6 +3116,9 @@ var require_utils = __commonJS({
3112
3116
  "use strict";
3113
3117
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3114
3118
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3119
+ var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3120
+ var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3121
+ var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3115
3122
  function stringArrayToHexStripped(input) {
3116
3123
  let acc = "";
3117
3124
  let code = 0;
@@ -3304,27 +3311,77 @@ var require_utils = __commonJS({
3304
3311
  }
3305
3312
  return output.join("");
3306
3313
  }
3307
- function normalizeComponentEncoding(component, esc2) {
3308
- const func = esc2 !== true ? escape : unescape;
3309
- if (component.scheme !== void 0) {
3310
- component.scheme = func(component.scheme);
3311
- }
3312
- if (component.userinfo !== void 0) {
3313
- component.userinfo = func(component.userinfo);
3314
- }
3315
- if (component.host !== void 0) {
3316
- component.host = func(component.host);
3314
+ var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3315
+ var HOST_DELIM_RE = /[@/?#:]/g;
3316
+ var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3317
+ function reescapeHostDelimiters(host, isIP) {
3318
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3319
+ re.lastIndex = 0;
3320
+ return host.replace(re, (ch) => HOST_DELIMS[ch]);
3321
+ }
3322
+ function normalizePercentEncoding(input, decodeUnreserved = false) {
3323
+ if (input.indexOf("%") === -1) {
3324
+ return input;
3317
3325
  }
3318
- if (component.path !== void 0) {
3319
- component.path = func(component.path);
3326
+ let output = "";
3327
+ for (let i = 0; i < input.length; i++) {
3328
+ if (input[i] === "%" && i + 2 < input.length) {
3329
+ const hex3 = input.slice(i + 1, i + 3);
3330
+ if (isHexPair(hex3)) {
3331
+ const normalizedHex = hex3.toUpperCase();
3332
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3333
+ if (decodeUnreserved && isUnreserved(decoded)) {
3334
+ output += decoded;
3335
+ } else {
3336
+ output += "%" + normalizedHex;
3337
+ }
3338
+ i += 2;
3339
+ continue;
3340
+ }
3341
+ }
3342
+ output += input[i];
3320
3343
  }
3321
- if (component.query !== void 0) {
3322
- component.query = func(component.query);
3344
+ return output;
3345
+ }
3346
+ function normalizePathEncoding(input) {
3347
+ let output = "";
3348
+ for (let i = 0; i < input.length; i++) {
3349
+ if (input[i] === "%" && i + 2 < input.length) {
3350
+ const hex3 = input.slice(i + 1, i + 3);
3351
+ if (isHexPair(hex3)) {
3352
+ const normalizedHex = hex3.toUpperCase();
3353
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3354
+ if (decoded !== "." && isUnreserved(decoded)) {
3355
+ output += decoded;
3356
+ } else {
3357
+ output += "%" + normalizedHex;
3358
+ }
3359
+ i += 2;
3360
+ continue;
3361
+ }
3362
+ }
3363
+ if (isPathCharacter(input[i])) {
3364
+ output += input[i];
3365
+ } else {
3366
+ output += escape(input[i]);
3367
+ }
3323
3368
  }
3324
- if (component.fragment !== void 0) {
3325
- component.fragment = func(component.fragment);
3369
+ return output;
3370
+ }
3371
+ function escapePreservingEscapes(input) {
3372
+ let output = "";
3373
+ for (let i = 0; i < input.length; i++) {
3374
+ if (input[i] === "%" && i + 2 < input.length) {
3375
+ const hex3 = input.slice(i + 1, i + 3);
3376
+ if (isHexPair(hex3)) {
3377
+ output += "%" + hex3.toUpperCase();
3378
+ i += 2;
3379
+ continue;
3380
+ }
3381
+ }
3382
+ output += escape(input[i]);
3326
3383
  }
3327
- return component;
3384
+ return output;
3328
3385
  }
3329
3386
  function recomposeAuthority(component) {
3330
3387
  const uriTokens = [];
@@ -3339,7 +3396,7 @@ var require_utils = __commonJS({
3339
3396
  if (ipV6res.isIPV6 === true) {
3340
3397
  host = `[${ipV6res.escapedHost}]`;
3341
3398
  } else {
3342
- host = component.host;
3399
+ host = reescapeHostDelimiters(host, false);
3343
3400
  }
3344
3401
  }
3345
3402
  uriTokens.push(host);
@@ -3353,7 +3410,10 @@ var require_utils = __commonJS({
3353
3410
  module.exports = {
3354
3411
  nonSimpleDomain,
3355
3412
  recomposeAuthority,
3356
- normalizeComponentEncoding,
3413
+ reescapeHostDelimiters,
3414
+ normalizePercentEncoding,
3415
+ normalizePathEncoding,
3416
+ escapePreservingEscapes,
3357
3417
  removeDotSegments,
3358
3418
  isIPv4,
3359
3419
  isUUID,
@@ -3577,12 +3637,12 @@ var require_schemes = __commonJS({
3577
3637
  var require_fast_uri = __commonJS({
3578
3638
  "../node_modules/fast-uri/index.js"(exports, module) {
3579
3639
  "use strict";
3580
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
3640
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3581
3641
  var { SCHEMES, getSchemeHandler } = require_schemes();
3582
3642
  function normalize2(uri, options) {
3583
3643
  if (typeof uri === "string") {
3584
3644
  uri = /** @type {T} */
3585
- serialize(parse3(uri, options), options);
3645
+ normalizeString(uri, options);
3586
3646
  } else if (typeof uri === "object") {
3587
3647
  uri = /** @type {T} */
3588
3648
  parse3(serialize(uri, options), options);
@@ -3649,19 +3709,9 @@ var require_fast_uri = __commonJS({
3649
3709
  return target;
3650
3710
  }
3651
3711
  function equal(uriA, uriB, options) {
3652
- if (typeof uriA === "string") {
3653
- uriA = unescape(uriA);
3654
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
3655
- } else if (typeof uriA === "object") {
3656
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
3657
- }
3658
- if (typeof uriB === "string") {
3659
- uriB = unescape(uriB);
3660
- uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
3661
- } else if (typeof uriB === "object") {
3662
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
3663
- }
3664
- return uriA.toLowerCase() === uriB.toLowerCase();
3712
+ const normalizedA = normalizeComparableURI(uriA, options);
3713
+ const normalizedB = normalizeComparableURI(uriB, options);
3714
+ return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
3665
3715
  }
3666
3716
  function serialize(cmpts, opts) {
3667
3717
  const component = {
@@ -3686,12 +3736,12 @@ var require_fast_uri = __commonJS({
3686
3736
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
3687
3737
  if (component.path !== void 0) {
3688
3738
  if (!options.skipEscape) {
3689
- component.path = escape(component.path);
3739
+ component.path = escapePreservingEscapes(component.path);
3690
3740
  if (component.scheme !== void 0) {
3691
3741
  component.path = component.path.split("%3A").join(":");
3692
3742
  }
3693
3743
  } else {
3694
- component.path = unescape(component.path);
3744
+ component.path = normalizePercentEncoding(component.path);
3695
3745
  }
3696
3746
  }
3697
3747
  if (options.reference !== "suffix" && component.scheme) {
@@ -3726,7 +3776,16 @@ var require_fast_uri = __commonJS({
3726
3776
  return uriTokens.join("");
3727
3777
  }
3728
3778
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3729
- function parse3(uri, opts) {
3779
+ function getParseError(parsed, matches) {
3780
+ if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3781
+ return 'URI path must start with "/" when authority is present.';
3782
+ }
3783
+ if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
3784
+ return "URI port is malformed.";
3785
+ }
3786
+ return void 0;
3787
+ }
3788
+ function parseWithStatus(uri, opts) {
3730
3789
  const options = Object.assign({}, opts);
3731
3790
  const parsed = {
3732
3791
  scheme: void 0,
@@ -3737,6 +3796,7 @@ var require_fast_uri = __commonJS({
3737
3796
  query: void 0,
3738
3797
  fragment: void 0
3739
3798
  };
3799
+ let malformedAuthorityOrPort = false;
3740
3800
  let isIP = false;
3741
3801
  if (options.reference === "suffix") {
3742
3802
  if (options.scheme) {
@@ -3757,6 +3817,11 @@ var require_fast_uri = __commonJS({
3757
3817
  if (isNaN(parsed.port)) {
3758
3818
  parsed.port = matches[5];
3759
3819
  }
3820
+ const parseError = getParseError(parsed, matches);
3821
+ if (parseError !== void 0) {
3822
+ parsed.error = parsed.error || parseError;
3823
+ malformedAuthorityOrPort = true;
3824
+ }
3760
3825
  if (parsed.host) {
3761
3826
  const ipv4result = isIPv4(parsed.host);
3762
3827
  if (ipv4result === false) {
@@ -3795,14 +3860,18 @@ var require_fast_uri = __commonJS({
3795
3860
  parsed.scheme = unescape(parsed.scheme);
3796
3861
  }
3797
3862
  if (parsed.host !== void 0) {
3798
- parsed.host = unescape(parsed.host);
3863
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
3799
3864
  }
3800
3865
  }
3801
3866
  if (parsed.path) {
3802
- parsed.path = escape(unescape(parsed.path));
3867
+ parsed.path = normalizePathEncoding(parsed.path);
3803
3868
  }
3804
3869
  if (parsed.fragment) {
3805
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3870
+ try {
3871
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3872
+ } catch {
3873
+ parsed.error = parsed.error || "URI malformed";
3874
+ }
3806
3875
  }
3807
3876
  }
3808
3877
  if (schemeHandler && schemeHandler.parse) {
@@ -3811,7 +3880,29 @@ var require_fast_uri = __commonJS({
3811
3880
  } else {
3812
3881
  parsed.error = parsed.error || "URI can not be parsed.";
3813
3882
  }
3814
- return parsed;
3883
+ return { parsed, malformedAuthorityOrPort };
3884
+ }
3885
+ function parse3(uri, opts) {
3886
+ return parseWithStatus(uri, opts).parsed;
3887
+ }
3888
+ function normalizeString(uri, opts) {
3889
+ return normalizeStringWithStatus(uri, opts).normalized;
3890
+ }
3891
+ function normalizeStringWithStatus(uri, opts) {
3892
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
3893
+ return {
3894
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
3895
+ malformedAuthorityOrPort
3896
+ };
3897
+ }
3898
+ function normalizeComparableURI(uri, opts) {
3899
+ if (typeof uri === "string") {
3900
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
3901
+ return malformedAuthorityOrPort ? void 0 : normalized;
3902
+ }
3903
+ if (typeof uri === "object") {
3904
+ return serialize(uri, opts);
3905
+ }
3815
3906
  }
3816
3907
  var fastUri = {
3817
3908
  SCHEMES,
@@ -6878,7 +6969,7 @@ var require_main = __commonJS({
6878
6969
  "../node_modules/dotenv/lib/main.js"(exports, module) {
6879
6970
  var fs2 = __require("fs");
6880
6971
  var path2 = __require("path");
6881
- var os = __require("os");
6972
+ var os2 = __require("os");
6882
6973
  var crypto = __require("crypto");
6883
6974
  var packageJson = require_package();
6884
6975
  var version2 = packageJson.version;
@@ -7001,7 +7092,7 @@ var require_main = __commonJS({
7001
7092
  return null;
7002
7093
  }
7003
7094
  function _resolveHome(envPath) {
7004
- return envPath[0] === "~" ? path2.join(os.homedir(), envPath.slice(1)) : envPath;
7095
+ return envPath[0] === "~" ? path2.join(os2.homedir(), envPath.slice(1)) : envPath;
7005
7096
  }
7006
7097
  function _configVault(options) {
7007
7098
  const debug = Boolean(options && options.debug);
@@ -28144,7 +28235,9 @@ var StdioServerTransport = class {
28144
28235
  // src/index.ts
28145
28236
  var import_dotenv = __toESM(require_main(), 1);
28146
28237
  import { Telegraf } from "telegraf";
28238
+ import { createHash } from "node:crypto";
28147
28239
  import * as fs from "fs";
28240
+ import os from "node:os";
28148
28241
  import * as path from "path";
28149
28242
 
28150
28243
  // ../node_modules/zod/index.js
@@ -28496,6 +28589,13 @@ function applyPagination(items, params) {
28496
28589
 
28497
28590
  // src/index.ts
28498
28591
  import_dotenv.default.config();
28592
+ function getStatePath() {
28593
+ if (process.env.TELEGRAM_STATE_PATH) {
28594
+ return process.env.TELEGRAM_STATE_PATH;
28595
+ }
28596
+ const suffix = botToken && !IS_MOCK ? createHash("sha256").update(botToken).digest("hex").slice(0, 12) : "default";
28597
+ return path.join(os.homedir(), ".fre4x", `telegram-mcp-${suffix}.json`);
28598
+ }
28499
28599
  var window = new JSDOM("").window;
28500
28600
  var DOMPurify = createDOMPurify(window);
28501
28601
  var IS_MOCK = process.env.MOCK === "true" || process.env.TELEGRAM_MOCK === "true";
@@ -28569,13 +28669,7 @@ function validateFilePath(filePath) {
28569
28669
  }
28570
28670
  return absolutePath;
28571
28671
  }
28572
- function validateRecipient(targetId) {
28573
- const userPattern = /^\d+$/;
28574
- const groupPattern = /^-?\d+$/;
28575
- const usernamePattern = /^@[a-zA-Z0-9_]+$/;
28576
- if (!userPattern.test(targetId) && !groupPattern.test(targetId) && !usernamePattern.test(targetId)) {
28577
- throw new Error(`Invalid Telegram ID format: ${targetId}`);
28578
- }
28672
+ function assertRecipientAllowed(targetId) {
28579
28673
  if (enableRecipientWhitelist && allowedRecipients.length > 0) {
28580
28674
  if (!allowedRecipients.includes(targetId)) {
28581
28675
  throw new Error(
@@ -28584,6 +28678,18 @@ function validateRecipient(targetId) {
28584
28678
  }
28585
28679
  }
28586
28680
  }
28681
+ function validateSendRecipient(targetId) {
28682
+ if (!/^\d+$/.test(targetId)) {
28683
+ throw new Error(`Invalid Telegram user ID format: ${targetId}`);
28684
+ }
28685
+ assertRecipientAllowed(targetId);
28686
+ }
28687
+ function validateChatId(chatId) {
28688
+ if (!/^-?\d+$/.test(chatId)) {
28689
+ throw new Error(`Invalid Telegram chat ID format: ${chatId}`);
28690
+ }
28691
+ assertRecipientAllowed(chatId);
28692
+ }
28587
28693
  var SendMessageSchema = external_exports3.object({
28588
28694
  content: external_exports3.string().min(1).max(4096),
28589
28695
  recipientId: external_exports3.string().optional(),
@@ -28622,8 +28728,16 @@ var server = new Server(
28622
28728
  },
28623
28729
  {
28624
28730
  capabilities: {
28625
- tools: {}
28626
- }
28731
+ tools: {},
28732
+ logging: {},
28733
+ // Required by SDK for notifications/message
28734
+ experimental: {
28735
+ "claude/channel": {
28736
+ description: "Telegram events are delivered via channel notifications"
28737
+ }
28738
+ }
28739
+ },
28740
+ instructions: "Telegram MCP server. Delivers inbound Telegram messages as channel notifications via notifications/message."
28627
28741
  }
28628
28742
  );
28629
28743
  var isImage = (filePath) => {
@@ -28632,12 +28746,275 @@ var isImage = (filePath) => {
28632
28746
  };
28633
28747
  var messageHistory = [];
28634
28748
  var MAX_HISTORY = 100;
28749
+ var pollingOffset = 0;
28750
+ var knownChats = /* @__PURE__ */ new Map();
28751
+ function loadState() {
28752
+ if (IS_MOCK) return;
28753
+ try {
28754
+ if (!fs.existsSync(getStatePath())) return;
28755
+ const raw = fs.readFileSync(getStatePath(), "utf-8");
28756
+ const state = JSON.parse(raw);
28757
+ for (const chat of state.knownChats ?? []) {
28758
+ if (chat?.id) {
28759
+ knownChats.set(chat.id, chat);
28760
+ }
28761
+ }
28762
+ if (typeof state.pollingOffset === "number") {
28763
+ pollingOffset = state.pollingOffset;
28764
+ }
28765
+ console.error(
28766
+ `[telegram:state] loaded ${knownChats.size} chat(s) from ${getStatePath()}`
28767
+ );
28768
+ } catch (error48) {
28769
+ console.error(`[telegram:state] failed to load ${getStatePath()}:`, error48);
28770
+ }
28771
+ }
28772
+ function persistState() {
28773
+ if (IS_MOCK) return;
28774
+ try {
28775
+ const state = {
28776
+ knownChats: Array.from(knownChats.values()),
28777
+ pollingOffset
28778
+ };
28779
+ fs.mkdirSync(path.dirname(getStatePath()), { recursive: true });
28780
+ fs.writeFileSync(getStatePath(), JSON.stringify(state, null, 2), "utf-8");
28781
+ } catch (error48) {
28782
+ console.error(`[telegram:state] failed to persist ${getStatePath()}:`, error48);
28783
+ }
28784
+ }
28785
+ function reloadState() {
28786
+ if (IS_MOCK) return;
28787
+ try {
28788
+ if (!fs.existsSync(getStatePath())) return;
28789
+ const raw = fs.readFileSync(getStatePath(), "utf-8");
28790
+ const state = JSON.parse(raw);
28791
+ for (const chat of state.knownChats ?? []) {
28792
+ if (chat?.id) {
28793
+ knownChats.set(chat.id, chat);
28794
+ }
28795
+ }
28796
+ if (typeof state.pollingOffset === "number" && state.pollingOffset > pollingOffset) {
28797
+ pollingOffset = state.pollingOffset;
28798
+ }
28799
+ } catch (error48) {
28800
+ console.error(`[telegram:state] failed to reload ${getStatePath()}:`, error48);
28801
+ }
28802
+ }
28803
+ function chatTitleFromTelegramChat(chat) {
28804
+ return chat.title || chat.username || chat.first_name || "Private Chat";
28805
+ }
28806
+ function recordKnownChat(chat) {
28807
+ knownChats.set(chat.id, chat);
28808
+ persistState();
28809
+ }
28810
+ function recordKnownChatFromTelegramMessage(message) {
28811
+ recordKnownChat({
28812
+ id: message.chat.id.toString(),
28813
+ title: chatTitleFromTelegramChat(message.chat),
28814
+ type: message.chat.type ?? "unknown"
28815
+ });
28816
+ }
28817
+ function recordKnownChatFromUpdate(update) {
28818
+ const chat = update.message?.chat ?? update.edited_message?.chat ?? update.channel_post?.chat ?? update.my_chat_member?.chat;
28819
+ if (!chat) return;
28820
+ recordKnownChat({
28821
+ id: chat.id.toString(),
28822
+ title: chatTitleFromTelegramChat(chat),
28823
+ type: chat.type ?? "unknown"
28824
+ });
28825
+ }
28826
+ function logActiveChatsDebug(stage) {
28827
+ const active = getActiveChats();
28828
+ console.error(
28829
+ `[telegram:list_active_chats] ${stage}: knownChats=${knownChats.size} messageHistory=${messageHistory.length} active=${active.length} pollingOffset=${pollingOffset}`
28830
+ );
28831
+ if (knownChats.size > 0) {
28832
+ console.error(
28833
+ `[telegram:list_active_chats] knownChatIds: ${Array.from(knownChats.keys()).join(", ")}`
28834
+ );
28835
+ }
28836
+ if (active.length > 0) {
28837
+ console.error(
28838
+ `[telegram:list_active_chats] activeChatIds: ${active.map((c) => c.id).join(", ")}`
28839
+ );
28840
+ }
28841
+ }
28842
+ async function recordChatInteraction(chatId) {
28843
+ if (IS_MOCK || !bot) {
28844
+ recordKnownChat({
28845
+ id: chatId,
28846
+ title: `Chat ${chatId}`,
28847
+ type: "unknown"
28848
+ });
28849
+ console.error(
28850
+ `[telegram:recordChatInteraction] mock recorded chatId=${chatId} knownChats=${knownChats.size}`
28851
+ );
28852
+ return;
28853
+ }
28854
+ try {
28855
+ const chat = await bot.telegram.getChat(chatId);
28856
+ recordKnownChat({
28857
+ id: chat.id.toString(),
28858
+ title: chatTitleFromTelegramChat(chat),
28859
+ type: chat.type
28860
+ });
28861
+ console.error(
28862
+ `[telegram:recordChatInteraction] getChat ok chatId=${chat.id} knownChats=${knownChats.size}`
28863
+ );
28864
+ } catch (error48) {
28865
+ recordKnownChat({
28866
+ id: chatId,
28867
+ title: chatId,
28868
+ type: "unknown"
28869
+ });
28870
+ console.error(
28871
+ `[telegram:recordChatInteraction] getChat failed for ${chatId}, stored fallback:`,
28872
+ error48
28873
+ );
28874
+ }
28875
+ }
28876
+ function getConfiguredChatCandidates() {
28877
+ const candidates = /* @__PURE__ */ new Set();
28878
+ if (allowedUserId) candidates.add(allowedUserId);
28879
+ for (const id of allowedRecipients) {
28880
+ if (id) candidates.add(id);
28881
+ }
28882
+ for (const chat of knownChats.values()) {
28883
+ candidates.add(chat.id);
28884
+ }
28885
+ for (const msg of messageHistory) {
28886
+ candidates.add(msg.chatId);
28887
+ }
28888
+ return candidates;
28889
+ }
28890
+ async function ensurePollingMode() {
28891
+ if (!bot || IS_MOCK) return;
28892
+ try {
28893
+ const info = await bot.telegram.getWebhookInfo();
28894
+ if (info.url) {
28895
+ console.error(
28896
+ `[telegram] active webhook detected (${info.url}); switching to getUpdates polling`
28897
+ );
28898
+ await bot.telegram.deleteWebhook({ drop_pending_updates: false });
28899
+ }
28900
+ } catch (error48) {
28901
+ console.error("[telegram] ensurePollingMode failed:", error48);
28902
+ }
28903
+ }
28904
+ async function discoverChatsFromApi() {
28905
+ if (IS_MOCK || !bot) return;
28906
+ const candidates = getConfiguredChatCandidates();
28907
+ console.error(
28908
+ `[telegram:list_active_chats] discoverChatsFromApi: probing ${candidates.size} candidate chat(s) from state=${getStatePath()}`
28909
+ );
28910
+ for (const chatId of candidates) {
28911
+ await recordChatInteraction(chatId);
28912
+ }
28913
+ try {
28914
+ await ensurePollingMode();
28915
+ const updates = await bot.telegram.getUpdates(0, 100, pollingOffset, [
28916
+ "message",
28917
+ "edited_message",
28918
+ "channel_post",
28919
+ "my_chat_member",
28920
+ "chat_member"
28921
+ ]);
28922
+ for (const update of updates) {
28923
+ pollingOffset = update.update_id + 1;
28924
+ const msgLike = update;
28925
+ if (!msgLike.message && !msgLike.edited_message && !msgLike.channel_post && !msgLike.my_chat_member)
28926
+ continue;
28927
+ recordKnownChatFromUpdate(msgLike);
28928
+ }
28929
+ console.error(
28930
+ `[telegram:list_active_chats] discoverChatsFromApi: processed ${updates.length} update(s)`
28931
+ );
28932
+ persistState();
28933
+ } catch (error48) {
28934
+ console.error(
28935
+ "[telegram:list_active_chats] discoverChatsFromApi getUpdates failed:",
28936
+ error48
28937
+ );
28938
+ }
28939
+ }
28940
+ function getActiveChats() {
28941
+ const chats = /* @__PURE__ */ new Map();
28942
+ for (const chat of knownChats.values()) {
28943
+ chats.set(chat.id, chat);
28944
+ }
28945
+ for (const msg of messageHistory) {
28946
+ if (chats.has(msg.chatId)) continue;
28947
+ chats.set(msg.chatId, {
28948
+ id: msg.chatId,
28949
+ title: msg.chatTitle || "Unknown",
28950
+ type: msg.metadata?.chatType || "unknown"
28951
+ });
28952
+ }
28953
+ return Array.from(chats.values()).sort(
28954
+ (a, b) => a.title.localeCompare(b.title)
28955
+ );
28956
+ }
28957
+ async function fetchNewUpdates() {
28958
+ if (!bot || IS_MOCK) return;
28959
+ try {
28960
+ await ensurePollingMode();
28961
+ const updates = await bot.telegram.getUpdates(0, 100, pollingOffset, [
28962
+ "message",
28963
+ "edited_message",
28964
+ "channel_post",
28965
+ "my_chat_member"
28966
+ ]);
28967
+ for (const update of updates) {
28968
+ pollingOffset = update.update_id + 1;
28969
+ const msgLike = update;
28970
+ if (!msgLike.message && !msgLike.edited_message && !msgLike.channel_post && !msgLike.my_chat_member)
28971
+ continue;
28972
+ recordKnownChatFromUpdate(msgLike);
28973
+ const msg = msgLike.message;
28974
+ if (!msg?.text) continue;
28975
+ const senderId = msg.from?.id?.toString() ?? "unknown";
28976
+ const chatId = msg.chat.id.toString();
28977
+ const chatTitle = msg.chat.title || msg.chat.username || msg.chat.first_name || "Private Chat";
28978
+ if (messageHistory.some(
28979
+ (m) => m.metadata?.message_id === msg.message_id
28980
+ ))
28981
+ continue;
28982
+ const currentAllowedUserId = process.env.ALLOWED_USER_ID;
28983
+ if (currentAllowedUserId && msg.chat.type === "private") {
28984
+ if (senderId !== currentAllowedUserId) continue;
28985
+ }
28986
+ let content = DOMPurify.sanitize(msg.text, {
28987
+ ALLOWED_TAGS: [],
28988
+ ALLOWED_ATTR: []
28989
+ });
28990
+ content = content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/[*_~]/g, "\\$&");
28991
+ messageHistory.push({
28992
+ senderId,
28993
+ chatId,
28994
+ chatTitle,
28995
+ content,
28996
+ timestamp: new Date(msg.date * 1e3).toISOString(),
28997
+ metadata: {
28998
+ chatId,
28999
+ chatType: msg.chat.type,
29000
+ message_id: msg.message_id
29001
+ }
29002
+ });
29003
+ if (messageHistory.length > MAX_HISTORY) {
29004
+ messageHistory.shift();
29005
+ }
29006
+ }
29007
+ persistState();
29008
+ } catch (error48) {
29009
+ console.error("fetchNewUpdates failed:", error48);
29010
+ }
29011
+ }
28635
29012
  server.setRequestHandler(ListToolsRequestSchema, async () => {
28636
29013
  return {
28637
29014
  tools: [
28638
29015
  {
28639
- name: "send_telegram_message",
28640
- description: "Send a text message, photo, or document to a specific Telegram user, group, or channel.",
29016
+ name: "send_message",
29017
+ description: "Send text, photo, or document to a Telegram user by numeric ID.",
28641
29018
  inputSchema: {
28642
29019
  type: "object",
28643
29020
  properties: {
@@ -28647,7 +29024,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
28647
29024
  },
28648
29025
  recipientId: {
28649
29026
  type: "string",
28650
- description: "Target ID or @username."
29027
+ description: "Numeric Telegram user ID."
28651
29028
  },
28652
29029
  mediaPath: {
28653
29030
  type: "string",
@@ -28675,7 +29052,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
28675
29052
  },
28676
29053
  {
28677
29054
  name: "list_active_chats",
28678
- description: "List all unique Telegram chats the bot has interacted with.",
29055
+ description: "List chats the bot has sent to or received messages from.",
28679
29056
  inputSchema: {
28680
29057
  type: "object",
28681
29058
  properties: {
@@ -28689,16 +29066,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
28689
29066
  description: "Get detailed information about a specific Telegram chat.",
28690
29067
  inputSchema: {
28691
29068
  type: "object",
28692
- properties: { chatId: { type: "string" } },
29069
+ properties: {
29070
+ chatId: {
29071
+ type: "string",
29072
+ description: "Numeric Telegram chat ID."
29073
+ }
29074
+ },
28693
29075
  required: ["chatId"]
28694
29076
  }
28695
29077
  },
28696
29078
  {
28697
- name: "leave_telegram_chat",
29079
+ name: "leave_chat",
28698
29080
  description: "Request the bot to leave a specific group or channel.",
28699
29081
  inputSchema: {
28700
29082
  type: "object",
28701
- properties: { chatId: { type: "string" } },
29083
+ properties: {
29084
+ chatId: {
29085
+ type: "string",
29086
+ description: "Numeric Telegram chat ID."
29087
+ }
29088
+ },
28702
29089
  required: ["chatId"]
28703
29090
  }
28704
29091
  }
@@ -28709,7 +29096,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
28709
29096
  const { name, arguments: args } = request.params;
28710
29097
  const currentAllowedUserId = process.env.ALLOWED_USER_ID;
28711
29098
  try {
28712
- if (name === "send_telegram_message") {
29099
+ if (name === "send_message") {
29100
+ reloadState();
28713
29101
  const { content, recipientId, mediaPath, mediaType } = SendMessageSchema.parse(args);
28714
29102
  const targetId = recipientId || currentAllowedUserId;
28715
29103
  if (!targetId) {
@@ -28724,7 +29112,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
28724
29112
  429
28725
29113
  );
28726
29114
  }
28727
- validateRecipient(targetId);
29115
+ validateSendRecipient(targetId);
28728
29116
  let safePath;
28729
29117
  if (mediaPath) {
28730
29118
  safePath = validateFilePath(mediaPath);
@@ -28734,19 +29122,24 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
28734
29122
  if (mediaPath)
28735
29123
  mockResponse += `
28736
29124
  [Mock] Media: ${mediaPath} (${mediaType || "auto"})`;
29125
+ await recordChatInteraction(targetId);
28737
29126
  return { content: [{ type: "text", text: mockResponse }] };
28738
29127
  }
28739
29128
  if (mediaPath && safePath) {
28740
29129
  const source = { source: safePath };
28741
29130
  const isPhoto = mediaType === "photo" || !mediaType && isImage(safePath);
28742
29131
  if (isPhoto) {
28743
- await bot.telegram.sendPhoto(targetId, source, {
29132
+ const sent = await bot.telegram.sendPhoto(targetId, source, {
28744
29133
  caption: content
28745
29134
  });
29135
+ recordKnownChatFromTelegramMessage(sent);
28746
29136
  } else {
28747
- await bot.telegram.sendDocument(targetId, source, {
28748
- caption: content
28749
- });
29137
+ const sent = await bot.telegram.sendDocument(
29138
+ targetId,
29139
+ source,
29140
+ { caption: content }
29141
+ );
29142
+ recordKnownChatFromTelegramMessage(sent);
28750
29143
  }
28751
29144
  return {
28752
29145
  content: [
@@ -28757,7 +29150,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
28757
29150
  ]
28758
29151
  };
28759
29152
  } else {
28760
- await bot.telegram.sendMessage(targetId, content);
29153
+ const sent = await bot.telegram.sendMessage(targetId, content);
29154
+ recordKnownChatFromTelegramMessage(sent);
28761
29155
  return {
28762
29156
  content: [
28763
29157
  { type: "text", text: `Message sent successfully.` }
@@ -28767,6 +29161,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
28767
29161
  }
28768
29162
  if (name === "get_recent_messages") {
28769
29163
  const { limit, offset } = GetRecentMessagesSchema.parse(args || {});
29164
+ await fetchNewUpdates();
28770
29165
  const paginated = applyPagination(
28771
29166
  messageHistory.slice().reverse(),
28772
29167
  { limit, offset }
@@ -28795,18 +29190,12 @@ ${formatPaginationFooter(offset, limit, messageHistory.length)}`
28795
29190
  }
28796
29191
  if (name === "list_active_chats") {
28797
29192
  const { limit, offset } = GetRecentMessagesSchema.parse(args || {});
28798
- const uniqueChatIds = Array.from(
28799
- new Set(messageHistory.map((m) => m.chatId))
28800
- );
28801
- const chatDetails = uniqueChatIds.map((id) => {
28802
- const lastMsg = messageHistory.slice().reverse().find((m) => m.chatId === id);
28803
- return {
28804
- id,
28805
- title: lastMsg?.chatTitle || "Unknown",
28806
- type: lastMsg?.metadata?.chatType || "unknown"
28807
- };
28808
- });
28809
- const paginated = applyPagination(chatDetails, { limit, offset });
29193
+ reloadState();
29194
+ await fetchNewUpdates();
29195
+ await discoverChatsFromApi();
29196
+ logActiveChatsDebug("after fetch and discover");
29197
+ const activeChats = getActiveChats();
29198
+ const paginated = applyPagination(activeChats, { limit, offset });
28810
29199
  const formatted = paginated.items.map((c) => `- ${c.title} (ID: ${c.id}, Type: ${c.type})`).join("\n");
28811
29200
  return {
28812
29201
  content: [
@@ -28815,14 +29204,14 @@ ${formatPaginationFooter(offset, limit, messageHistory.length)}`
28815
29204
  text: formatted ? `Active chats:
28816
29205
  ${formatted}
28817
29206
 
28818
- ${formatPaginationFooter(offset, limit, uniqueChatIds.length)}` : "No active chats found."
29207
+ ${formatPaginationFooter(offset, limit, activeChats.length)}` : "No active chats found."
28819
29208
  }
28820
29209
  ]
28821
29210
  };
28822
29211
  }
28823
29212
  if (name === "get_chat_info") {
28824
29213
  const { chatId } = GetChatInfoSchema.parse(args);
28825
- validateRecipient(chatId);
29214
+ validateChatId(chatId);
28826
29215
  if (IS_MOCK || !bot) {
28827
29216
  return {
28828
29217
  content: [
@@ -28848,9 +29237,9 @@ ${formatPaginationFooter(offset, limit, uniqueChatIds.length)}` : "No active cha
28848
29237
  ]
28849
29238
  };
28850
29239
  }
28851
- if (name === "leave_telegram_chat") {
29240
+ if (name === "leave_chat") {
28852
29241
  const { chatId } = LeaveChatSchema.parse(args);
28853
- validateRecipient(chatId);
29242
+ validateChatId(chatId);
28854
29243
  if (IS_MOCK || !bot) {
28855
29244
  return {
28856
29245
  content: [
@@ -28892,108 +29281,29 @@ ${formatPaginationFooter(offset, limit, uniqueChatIds.length)}` : "No active cha
28892
29281
  if (msg.includes("limit exceeded")) {
28893
29282
  return createApiError("Rate limit exceeded.", 429);
28894
29283
  }
29284
+ if (msg.includes("invalid telegram user id format")) {
29285
+ return createValidationError(
29286
+ "recipientId",
29287
+ 'Use a numeric user ID only (e.g., "123456789"). @username is not supported.'
29288
+ );
29289
+ }
29290
+ if (msg.includes("invalid telegram chat id format")) {
29291
+ return createValidationError(
29292
+ "chatId",
29293
+ 'Use a numeric chat ID only (e.g., "123456789"). @username is not supported.'
29294
+ );
29295
+ }
28895
29296
  }
28896
29297
  return createInternalError(
28897
29298
  "An unexpected error occurred while processing the request."
28898
29299
  );
28899
29300
  }
28900
29301
  });
28901
- if (bot) {
28902
- bot.on("text", async (ctx) => {
28903
- const senderId = ctx.from.id.toString();
28904
- const chatId = ctx.chat.id.toString();
28905
- const chatTitle = ctx.chat.title || ctx.chat.username || ctx.chat.first_name || "Private Chat";
28906
- const currentAllowedUserId = process.env.ALLOWED_USER_ID;
28907
- if (currentAllowedUserId) {
28908
- if (ctx.chat.type === "private") {
28909
- if (senderId !== currentAllowedUserId) {
28910
- console.warn(
28911
- `Unauthorized private message from ${senderId}`
28912
- );
28913
- await ctx.reply(
28914
- "\u274C Access denied. Only the authorized user can interact with this bot."
28915
- ).catch(() => {
28916
- });
28917
- return;
28918
- }
28919
- } else if (ctx.chat.type === "group" || ctx.chat.type === "supergroup") {
28920
- try {
28921
- const chatMember = await ctx.getChatMember(ctx.from.id);
28922
- const isAdmin = ["creator", "administrator"].includes(
28923
- chatMember.status
28924
- );
28925
- if (!isAdmin && senderId !== currentAllowedUserId) {
28926
- console.warn(
28927
- `Unauthorized group message from ${senderId} in ${chatTitle}`
28928
- );
28929
- return;
28930
- }
28931
- } catch (error48) {
28932
- console.error(
28933
- "Failed to check group member status:",
28934
- error48
28935
- );
28936
- return;
28937
- }
28938
- } else if (ctx.chat.type === "channel") {
28939
- return;
28940
- }
28941
- }
28942
- let messageContent = ctx.message.text;
28943
- messageContent = DOMPurify.sanitize(messageContent, {
28944
- ALLOWED_TAGS: [],
28945
- ALLOWED_ATTR: []
28946
- });
28947
- messageContent = messageContent.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/[*_~]/g, "\\$&");
28948
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
28949
- const metadata = {
28950
- chatId,
28951
- chatType: ctx.chat?.type,
28952
- message_id: ctx.message.message_id
28953
- };
28954
- messageHistory.push({
28955
- senderId,
28956
- chatId,
28957
- chatTitle,
28958
- content: messageContent,
28959
- timestamp,
28960
- metadata
28961
- });
28962
- if (messageHistory.length > MAX_HISTORY) {
28963
- messageHistory.shift();
28964
- }
28965
- try {
28966
- await server.notification({
28967
- method: "notifications/push_event",
28968
- params: {
28969
- type: "message",
28970
- senderId,
28971
- content: messageContent,
28972
- metadata
28973
- }
28974
- });
28975
- } catch (error48) {
28976
- console.error("Failed to push notification:", error48);
28977
- }
28978
- });
28979
- }
28980
29302
  async function run() {
29303
+ loadState();
28981
29304
  const transport = new StdioServerTransport();
28982
29305
  await server.connect(transport);
28983
29306
  console.error("Telegram MCP Server running on stdio");
28984
- if (bot) {
28985
- bot.launch().catch((err) => {
28986
- console.error("Failed to launch Telegram bot:", err);
28987
- });
28988
- process.once("SIGINT", () => {
28989
- bot.stop("SIGINT");
28990
- process.exit(0);
28991
- });
28992
- process.once("SIGTERM", () => {
28993
- bot.stop("SIGTERM");
28994
- process.exit(0);
28995
- });
28996
- }
28997
29307
  }
28998
29308
  if (process.env.NODE_ENV !== "test") {
28999
29309
  run().catch((error48) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fre4x/telegram",
3
- "version": "1.1.3",
3
+ "version": "1.1.5",
4
4
  "description": "Telegram MCP server implementing Claude Code Channels for bidirectional communication and remote permission relay.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",