@overpod/mcp-telegram 1.34.0 → 1.36.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.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,89 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.36.0] — 2026-04-28
9
+
10
+ ### Added
11
+
12
+ **Tool manifest export — introspect the catalog without standing up an MCP transport.**
13
+
14
+ A new `@overpod/mcp-telegram/manifest` subpath export and `mcp-telegram-manifest` bin entry let consumers (and downstream cloud distributions) ask the package "what tools do you register, and at what risk tier?" without booting a real Telegram session.
15
+
16
+ ```ts
17
+ import { getToolManifest } from "@overpod/mcp-telegram/manifest";
18
+
19
+ const m = getToolManifest();
20
+ // {
21
+ // generatedAt: "2026-04-28T...Z",
22
+ // toolCount: 181,
23
+ // tiers: { "read-only": 74, write: 96, destructive: 11 },
24
+ // tools: [{ name: "telegram-status", tier: "read-only", description: "...", hasInput: false }, ...]
25
+ // }
26
+ ```
27
+
28
+ CLI variant:
29
+
30
+ ```bash
31
+ mcp-telegram-manifest # writes manifest.json
32
+ mcp-telegram-manifest path/out.json # writes to path/out.json
33
+ mcp-telegram-manifest - # writes JSON to stdout
34
+ ```
35
+
36
+ How it works: instantiates an `McpServer`, calls the existing `registerTools()` with a stub service (only types matter — every `telegram.*` call lives inside async tool callbacks, not the registration phase), then introspects the SDK's registered tools and classifies each by `annotations`:
37
+
38
+ - `destructiveHint: true` → `destructive`
39
+ - `readOnlyHint: true` → `read-only`
40
+ - otherwise → `write`
41
+
42
+ Opt-in env flags (`MCP_TELEGRAM_ENABLE_STARS`, `MCP_TELEGRAM_ENABLE_GROUP_CALLS`, `MCP_TELEGRAM_ENABLE_QUICK_REPLIES`) are forced ON during introspection so consumers always see the full catalog, then restored to the caller's prior values. The result is cached for the process lifetime.
43
+
44
+ This is the foundation for upstream parity gates (e.g. cloud distributions that ship a curated whitelist can detect drift in CI by comparing their whitelist against `getToolManifest().tools`).
45
+
46
+ ### Notes
47
+
48
+ - New public API surface; no breaking changes to existing exports.
49
+ - `src/manifest.ts` and 13 new tests added; total test count: 505.
50
+ - Build now sets executable bits on all `dist/*-cli.js` outputs (was npm-install-time only for `bin` entries).
51
+
52
+ ## [1.35.0] — 2026-04-25
53
+
54
+ ### Changed
55
+
56
+ **Rate limiter — structured stderr events for retries.**
57
+
58
+ The internal `RateLimiter` previously logged `FLOOD_WAIT` and network retries as
59
+ human-readable strings via `console.error`, and the temporary-server-error
60
+ branch (5xx, etc.) was silent. All three retry branches now emit a single
61
+ structured stderr line per event:
62
+
63
+ ```
64
+ [rate-limiter] event {"event":"flood_wait","context":"list-chats","seconds":30,"attempt":1,"maxRetries":3}
65
+ ```
66
+
67
+ Event types: `flood_wait`, `network_retry`, `temporary_retry`. Each event
68
+ carries:
69
+
70
+ - `event` — event class (string literal)
71
+ - `context` — caller-supplied operation name (never user input / PII)
72
+ - `attempt` / `maxRetries` — retry counters
73
+ - `seconds` (flood_wait only) or `delayMs` (network/temporary)
74
+ - `error` (network/temporary only) — the upstream Telegram error message
75
+
76
+ This is a logging contract change, not a behaviour change: retry timing,
77
+ backoff, and error-throwing semantics are identical to 1.34.0.
78
+
79
+ ### Why this matters
80
+
81
+ Downstream log collectors can now aggregate retry rates by event class and
82
+ caller without parsing free-form English. `mcp-telegram-cloud` v1.12.0+ wires
83
+ this into SigNoz directly. Self-hosters can `grep '\[rate-limiter\] event'` or
84
+ pipe into any structured log pipeline.
85
+
86
+ ### Compatibility
87
+
88
+ No breaking changes. No new dependencies. No new environment variables.
89
+ Tests: 490/490 pass.
90
+
8
91
  ## [1.34.0] — 2026-04-24
9
92
 
10
93
  ### Added
package/dist/cli.js CHANGED
File without changes
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ export type ToolTier = "read-only" | "write" | "destructive";
3
+ export interface ToolManifestEntry {
4
+ name: string;
5
+ tier: ToolTier;
6
+ description: string;
7
+ hasInput: boolean;
8
+ }
9
+ export interface ToolManifest {
10
+ generatedAt: string;
11
+ toolCount: number;
12
+ tiers: {
13
+ "read-only": number;
14
+ write: number;
15
+ destructive: number;
16
+ };
17
+ tools: ToolManifestEntry[];
18
+ }
19
+ /**
20
+ * Build a manifest of every tool the package can register. Forces all opt-in
21
+ * env flags ON during introspection so consumers see the full catalog, not
22
+ * the runtime-filtered subset. Cached for the process lifetime — invocations
23
+ * are cheap and idempotent.
24
+ */
25
+ export declare function getToolManifest(): ToolManifest;
26
+ /** Test-only: discard cache and force a fresh introspection. */
27
+ export declare function _resetManifestCache(): void;
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
+ import { registerTools } from "./tools/index.js";
6
+ function classify(name, annotations) {
7
+ if (annotations === undefined) {
8
+ console.warn(`[manifest] Tool '${name}' has no annotations — defaulting to 'write'. Add READ_ONLY/WRITE/DESTRUCTIVE.`);
9
+ return "write";
10
+ }
11
+ if (annotations.destructiveHint === true)
12
+ return "destructive";
13
+ if (annotations.readOnlyHint === true)
14
+ return "read-only";
15
+ return "write";
16
+ }
17
+ const OPT_IN_FLAGS = [
18
+ "MCP_TELEGRAM_ENABLE_STARS",
19
+ "MCP_TELEGRAM_ENABLE_GROUP_CALLS",
20
+ "MCP_TELEGRAM_ENABLE_QUICK_REPLIES",
21
+ ];
22
+ /** Cache: introspection is deterministic + cheap, but env save/restore is not reentrant. */
23
+ let cached = null;
24
+ /**
25
+ * Build a manifest of every tool the package can register. Forces all opt-in
26
+ * env flags ON during introspection so consumers see the full catalog, not
27
+ * the runtime-filtered subset. Cached for the process lifetime — invocations
28
+ * are cheap and idempotent.
29
+ */
30
+ export function getToolManifest() {
31
+ if (cached)
32
+ return cached;
33
+ cached = introspect();
34
+ return cached;
35
+ }
36
+ /** Test-only: discard cache and force a fresh introspection. */
37
+ export function _resetManifestCache() {
38
+ cached = null;
39
+ }
40
+ function introspect() {
41
+ const restore = {};
42
+ for (const key of OPT_IN_FLAGS) {
43
+ restore[key] = process.env[key];
44
+ process.env[key] = "1";
45
+ }
46
+ try {
47
+ const server = new McpServer({ name: "manifest-introspect", version: "0.0.0" });
48
+ registerTools(server, {});
49
+ return buildManifest(server);
50
+ }
51
+ finally {
52
+ for (const key of OPT_IN_FLAGS) {
53
+ if (restore[key] === undefined)
54
+ delete process.env[key];
55
+ else
56
+ process.env[key] = restore[key];
57
+ }
58
+ }
59
+ }
60
+ function buildManifest(server) {
61
+ const registered = server._registeredTools;
62
+ if (!registered || typeof registered !== "object") {
63
+ throw new Error("Failed to introspect MCP server: _registeredTools is missing. " +
64
+ "The @modelcontextprotocol/sdk shape may have changed — please file an issue at " +
65
+ "https://github.com/mcp-telegram/mcp-telegram/issues");
66
+ }
67
+ const tools = Object.entries(registered)
68
+ .map(([name, tool]) => ({
69
+ name,
70
+ tier: classify(name, tool.annotations),
71
+ description: tool.description ?? "",
72
+ hasInput: tool.inputSchema !== undefined,
73
+ }))
74
+ .sort((a, b) => a.name.localeCompare(b.name));
75
+ const tiers = { "read-only": 0, write: 0, destructive: 0 };
76
+ for (const t of tools)
77
+ tiers[t.tier]++;
78
+ return {
79
+ generatedAt: new Date().toISOString(),
80
+ toolCount: tools.length,
81
+ tiers,
82
+ tools,
83
+ };
84
+ }
85
+ if (import.meta.url === `file://${process.argv[1]}` || fileURLToPath(import.meta.url) === process.argv[1]) {
86
+ const manifest = getToolManifest();
87
+ const output = process.argv[2] ?? "manifest.json";
88
+ if (output === "-") {
89
+ process.stdout.write(`${JSON.stringify(manifest, null, 2)}\n`);
90
+ }
91
+ else {
92
+ writeFileSync(output, `${JSON.stringify(manifest, null, 2)}\n`);
93
+ console.error(`[manifest] Wrote ${manifest.toolCount} tools to ${output}`);
94
+ }
95
+ }
File without changes
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Rate limiter and retry logic for Telegram API calls.
3
3
  * Handles FLOOD_WAIT errors and implements exponential backoff.
4
+ *
5
+ * Emits structured events on stderr so downstream log collectors (e.g. cloud
6
+ * SigNoz) can aggregate by `event` and `context`. Format:
7
+ * [rate-limiter] event {"event":"flood_wait","context":"X","seconds":N,...}
4
8
  */
5
9
  export interface RateLimiterOptions {
6
10
  /** Maximum number of requests per second (default: 20) */
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Rate limiter and retry logic for Telegram API calls.
3
3
  * Handles FLOOD_WAIT errors and implements exponential backoff.
4
+ *
5
+ * Emits structured events on stderr so downstream log collectors (e.g. cloud
6
+ * SigNoz) can aggregate by `event` and `context`. Format:
7
+ * [rate-limiter] event {"event":"flood_wait","context":"X","seconds":N,...}
4
8
  */
5
9
  export class RateLimiter {
6
10
  minInterval;
@@ -41,7 +45,13 @@ export class RateLimiter {
41
45
  if (attempt >= this.maxRetries) {
42
46
  throw new Error(`Rate limit exceeded after ${this.maxRetries} retries. Telegram requires ${waitSeconds}s wait. Try again later.`);
43
47
  }
44
- console.error(`[rate-limiter] FLOOD_WAIT for ${context}. Waiting ${waitSeconds}s (attempt ${attempt + 1}/${this.maxRetries})`);
48
+ logEvent({
49
+ event: "flood_wait",
50
+ context,
51
+ seconds: waitSeconds,
52
+ attempt: attempt + 1,
53
+ maxRetries: this.maxRetries,
54
+ });
45
55
  await sleep(waitSeconds * 1000);
46
56
  return this.executeWithRetry(fn, context, attempt + 1, options);
47
57
  }
@@ -51,7 +61,14 @@ export class RateLimiter {
51
61
  throw new Error(`Network error after ${this.maxRetries} retries: ${errorMessage}. Check your connection.`);
52
62
  }
53
63
  const delay = Math.min(this.initialRetryDelay * 2 ** attempt, this.maxRetryDelay);
54
- console.error(`[rate-limiter] Network error for ${context}. Retrying in ${delay}ms (attempt ${attempt + 1}/${this.maxRetries})`);
64
+ logEvent({
65
+ event: "network_retry",
66
+ context,
67
+ delayMs: delay,
68
+ attempt: attempt + 1,
69
+ maxRetries: this.maxRetries,
70
+ error: errorMessage,
71
+ });
55
72
  await sleep(delay);
56
73
  return this.executeWithRetry(fn, context, attempt + 1, options);
57
74
  }
@@ -61,6 +78,14 @@ export class RateLimiter {
61
78
  throw new Error(`Temporary error after ${this.maxRetries} retries: ${errorMessage}`);
62
79
  }
63
80
  const delay = Math.min(this.initialRetryDelay * 2 ** attempt, this.maxRetryDelay);
81
+ logEvent({
82
+ event: "temporary_retry",
83
+ context,
84
+ delayMs: delay,
85
+ attempt: attempt + 1,
86
+ maxRetries: this.maxRetries,
87
+ error: errorMessage,
88
+ });
64
89
  await sleep(delay);
65
90
  return this.executeWithRetry(fn, context, attempt + 1, options);
66
91
  }
@@ -85,3 +110,6 @@ function isTemporaryError(msg) {
85
110
  function sleep(ms) {
86
111
  return new Promise((resolve) => setTimeout(resolve, ms));
87
112
  }
113
+ function logEvent(payload) {
114
+ console.error(`[rate-limiter] event ${JSON.stringify(payload)}`);
115
+ }
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "@overpod/mcp-telegram",
3
- "version": "1.34.0",
3
+ "version": "1.36.0",
4
4
  "description": "MCP server for Telegram userbot — messages, media, reactions, polls & more. Built on GramJS/MTProto.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "exports": {
8
8
  ".": "./dist/index.js",
9
- "./service": "./dist/telegram-client.js"
9
+ "./service": "./dist/telegram-client.js",
10
+ "./manifest": "./dist/manifest.js"
10
11
  },
11
12
  "bin": {
12
- "mcp-telegram": "dist/cli.js"
13
+ "mcp-telegram": "dist/cli.js",
14
+ "mcp-telegram-manifest": "dist/manifest.js"
13
15
  },
14
16
  "files": [
15
17
  "dist",
@@ -21,7 +23,7 @@
21
23
  "dev": "tsx watch src/index.ts",
22
24
  "start": "node dist/index.js",
23
25
  "login": "node dist/qr-login-cli.js",
24
- "build": "tsc",
26
+ "build": "tsc && chmod +x dist/cli.js dist/manifest.js dist/qr-login-cli.js",
25
27
  "typecheck": "tsc --noEmit",
26
28
  "prepublishOnly": "npm run build",
27
29
  "lint": "biome check src/",
@@ -63,7 +65,7 @@
63
65
  "zod": "^4.3.6"
64
66
  },
65
67
  "devDependencies": {
66
- "@biomejs/biome": "^2.4.12",
68
+ "@biomejs/biome": "^2.4.13",
67
69
  "@types/node": "^25.6.0",
68
70
  "@types/qrcode": "^1.5.6",
69
71
  "c8": "^11.0.0",