telegram-claude-mcp 1.1.0 → 1.2.1

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.
@@ -5,19 +5,56 @@
5
5
  # This script receives notification events from Claude Code (stop, session events)
6
6
  # and forwards them to the Telegram MCP server.
7
7
  #
8
- # Usage: Configure this script in your Claude Code hooks settings for Stop event
9
- #
10
- # Environment variables:
11
- # TELEGRAM_HOOK_PORT - Port of the MCP server (default: 3333)
12
- # TELEGRAM_HOOK_HOST - Host of the MCP server (default: localhost)
13
- # TELEGRAM_HOOK_EVENT - Event type override (default: from input or 'stop')
8
+ # The script auto-discovers the MCP server port from session info files.
14
9
  #
15
10
 
16
- HOOK_PORT="${TELEGRAM_HOOK_PORT:-3333}"
17
- HOOK_HOST="${TELEGRAM_HOOK_HOST:-localhost}"
18
- HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/notify"
11
+ SESSION_DIR="/tmp/telegram-claude-sessions"
19
12
  EVENT_TYPE="${TELEGRAM_HOOK_EVENT:-stop}"
20
13
 
14
+ # Function to find the most recent active session
15
+ find_active_session() {
16
+ local latest_file=""
17
+ local latest_time=0
18
+
19
+ # Check if directory exists
20
+ [ -d "$SESSION_DIR" ] || return
21
+
22
+ for info_file in "$SESSION_DIR"/*.info; do
23
+ # Skip if no matches (glob didn't expand)
24
+ [ -e "$info_file" ] || continue
25
+
26
+ # Check if the process is still running
27
+ local pid
28
+ pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
29
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
30
+ local file_time
31
+ file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
32
+ if [ "$file_time" -gt "$latest_time" ]; then
33
+ latest_time=$file_time
34
+ latest_file=$info_file
35
+ fi
36
+ fi
37
+ done
38
+
39
+ echo "$latest_file"
40
+ }
41
+
42
+ # Find active session info
43
+ INFO_FILE=$(find_active_session)
44
+
45
+ if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
46
+ echo "[notify-hook] No active session found, skipping notification" >&2
47
+ echo '{"continue": true}'
48
+ exit 0
49
+ fi
50
+
51
+ # Read port from session info
52
+ HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
53
+ HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
54
+ HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/notify"
55
+
56
+ echo "[notify-hook] Using session: $INFO_FILE (port $HOOK_PORT)" >&2
57
+
21
58
  # Read the hook input from stdin
22
59
  INPUT=$(cat)
23
60
 
@@ -25,9 +62,7 @@ INPUT=$(cat)
25
62
  SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
26
63
  MESSAGE=$(echo "$INPUT" | jq -r '.message // .stop_reason // "Agent stopped"')
27
64
 
28
- # Log for debugging
29
- echo "[notify-hook] Event: $EVENT_TYPE" >&2
30
- echo "[notify-hook] Message: $MESSAGE" >&2
65
+ echo "[notify-hook] Event: $EVENT_TYPE, Message: $MESSAGE" >&2
31
66
 
32
67
  # Build the request payload
33
68
  PAYLOAD=$(jq -n \
@@ -36,11 +71,11 @@ PAYLOAD=$(jq -n \
36
71
  --arg session_id "$SESSION_ID" \
37
72
  '{type: $type, message: $message, session_id: $session_id}')
38
73
 
39
- # Send the notification (non-blocking, we don't need to wait)
74
+ # Send the notification (non-blocking)
40
75
  curl -s -X POST "$HOOK_URL" \
41
76
  -H "Content-Type: application/json" \
42
77
  -d "$PAYLOAD" \
43
78
  --max-time 10 >/dev/null 2>&1 &
44
79
 
45
- # Return success immediately (hook doesn't need to block)
80
+ # Return success immediately
46
81
  echo '{"continue": true}'
@@ -5,17 +5,55 @@
5
5
  # This script receives permission requests from Claude Code and forwards them
6
6
  # to the Telegram MCP server for interactive approval via Telegram.
7
7
  #
8
- # Usage: Configure this script in your Claude Code hooks settings
9
- #
10
- # Environment variables:
11
- # TELEGRAM_HOOK_PORT - Port of the MCP server (default: 3333)
12
- # TELEGRAM_HOOK_HOST - Host of the MCP server (default: localhost)
8
+ # The script auto-discovers the MCP server port from session info files.
13
9
  #
14
10
 
15
- HOOK_PORT="${TELEGRAM_HOOK_PORT:-3333}"
16
- HOOK_HOST="${TELEGRAM_HOOK_HOST:-localhost}"
11
+ SESSION_DIR="/tmp/telegram-claude-sessions"
12
+
13
+ # Function to find the most recent active session
14
+ find_active_session() {
15
+ local latest_file=""
16
+ local latest_time=0
17
+
18
+ # Check if directory exists
19
+ [ -d "$SESSION_DIR" ] || return
20
+
21
+ for info_file in "$SESSION_DIR"/*.info; do
22
+ # Skip if no matches (glob didn't expand)
23
+ [ -e "$info_file" ] || continue
24
+
25
+ # Check if the process is still running
26
+ local pid
27
+ pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
28
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
29
+ local file_time
30
+ file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
31
+ if [ "$file_time" -gt "$latest_time" ]; then
32
+ latest_time=$file_time
33
+ latest_file=$info_file
34
+ fi
35
+ fi
36
+ done
37
+
38
+ echo "$latest_file"
39
+ }
40
+
41
+ # Find active session info
42
+ INFO_FILE=$(find_active_session)
43
+
44
+ if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
45
+ echo "[permission-hook] No active session found" >&2
46
+ # Return empty to fall back to default behavior (terminal prompt)
47
+ exit 0
48
+ fi
49
+
50
+ # Read port from session info
51
+ HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
52
+ HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
17
53
  HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/permission"
18
54
 
55
+ echo "[permission-hook] Using session: $INFO_FILE (port $HOOK_PORT)" >&2
56
+
19
57
  # Read the hook input from stdin
20
58
  INPUT=$(cat)
21
59
 
@@ -23,16 +61,13 @@ INPUT=$(cat)
23
61
  TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
24
62
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
25
63
 
26
- # If no tool_name, this might be a different format - try to extract from the input
64
+ # If no tool_name, try alternative paths
27
65
  if [ -z "$TOOL_NAME" ]; then
28
- # Claude Code might send the data differently, try alternative paths
29
66
  TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName // .name // empty')
30
67
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.toolInput // .input // .arguments // {}')
31
68
  fi
32
69
 
33
- # Log for debugging (goes to stderr, not stdout)
34
70
  echo "[permission-hook] Tool: $TOOL_NAME" >&2
35
- echo "[permission-hook] Sending to: $HOOK_URL" >&2
36
71
 
37
72
  # Build the request payload
38
73
  PAYLOAD=$(jq -n \
@@ -46,16 +81,11 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" \
46
81
  -d "$PAYLOAD" \
47
82
  --max-time 600)
48
83
 
49
- # Check if curl succeeded
50
84
  if [ $? -ne 0 ]; then
51
85
  echo "[permission-hook] Error: Failed to connect to hook server" >&2
52
- # Return deny on error for safety
53
- echo '{"hookSpecificOutput":{"decision":{"behavior":"deny","message":"Failed to connect to Telegram hook server"}}}'
86
+ # Return empty to fall back to default behavior
54
87
  exit 0
55
88
  fi
56
89
 
57
- # Log the response
58
90
  echo "[permission-hook] Response: $RESPONSE" >&2
59
-
60
- # Output the response (this goes back to Claude Code)
61
91
  echo "$RESPONSE"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "telegram-claude-mcp",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "MCP server that lets Claude message you on Telegram with hooks support",
5
5
  "author": "Geravant",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -16,6 +16,70 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
16
16
  import { TelegramManager, type HookEvent, type PermissionDecision } from './telegram.js';
17
17
  import { loadAppConfig, validateAppConfig } from './providers/index.js';
18
18
  import { createServer, type IncomingMessage, type ServerResponse } from 'http';
19
+ import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
20
+ import { join } from 'path';
21
+
22
+ // Session info directory (shared with hooks)
23
+ const SESSION_DIR = '/tmp/telegram-claude-sessions';
24
+
25
+ /**
26
+ * Find an available port starting from the preferred port
27
+ */
28
+ async function findAvailablePort(preferredPort: number): Promise<number> {
29
+ const net = await import('net');
30
+
31
+ return new Promise((resolve) => {
32
+ const tryPort = (port: number) => {
33
+ const server = net.createServer();
34
+ server.listen(port, '127.0.0.1');
35
+
36
+ server.on('listening', () => {
37
+ server.close(() => resolve(port));
38
+ });
39
+
40
+ server.on('error', () => {
41
+ // Port in use, try next
42
+ tryPort(port + 1);
43
+ });
44
+ };
45
+
46
+ tryPort(preferredPort);
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Write session info file for hooks to discover
52
+ */
53
+ function writeSessionInfo(sessionName: string, port: number): string {
54
+ if (!existsSync(SESSION_DIR)) {
55
+ mkdirSync(SESSION_DIR, { recursive: true });
56
+ }
57
+
58
+ const infoFile = join(SESSION_DIR, `${sessionName}.info`);
59
+ const info = {
60
+ port,
61
+ host: 'localhost',
62
+ pid: process.pid,
63
+ startedAt: new Date().toISOString(),
64
+ };
65
+
66
+ writeFileSync(infoFile, JSON.stringify(info, null, 2));
67
+ return infoFile;
68
+ }
69
+
70
+ /**
71
+ * Clean up session info file
72
+ */
73
+ function cleanupSessionInfo(sessionName: string): void {
74
+ const infoFile = join(SESSION_DIR, `${sessionName}.info`);
75
+ try {
76
+ if (existsSync(infoFile)) {
77
+ unlinkSync(infoFile);
78
+ }
79
+ } catch {
80
+ // Ignore cleanup errors
81
+ }
82
+ }
19
83
 
20
84
  async function main() {
21
85
  // Load configuration
@@ -39,6 +103,10 @@ async function main() {
39
103
 
40
104
  telegram.start();
41
105
 
106
+ // Find available port and start HTTP server for hooks
107
+ const actualPort = await findAvailablePort(config.sessionPort);
108
+ const sessionInfoFile = writeSessionInfo(config.sessionName, actualPort);
109
+
42
110
  // Start HTTP server for hooks
43
111
  const httpServer = createServer(async (req: IncomingMessage, res: ServerResponse) => {
44
112
  // CORS headers
@@ -127,8 +195,11 @@ async function main() {
127
195
  }
128
196
  });
129
197
 
130
- httpServer.listen(config.sessionPort, () => {
131
- console.error(`[HTTP] Hook server listening on port ${config.sessionPort}`);
198
+ httpServer.listen(actualPort, () => {
199
+ console.error(`[HTTP] Hook server listening on port ${actualPort}`);
200
+ if (actualPort !== config.sessionPort) {
201
+ console.error(`[HTTP] (preferred port ${config.sessionPort} was in use)`);
202
+ }
132
203
  });
133
204
 
134
205
  // Track active chat sessions
@@ -282,12 +353,14 @@ async function main() {
282
353
  console.error('Telegram Claude MCP server ready');
283
354
  console.error(`Session: ${config.sessionName}`);
284
355
  console.error(`Chat ID: ${config.telegramChatId}`);
285
- console.error(`Hook server: http://localhost:${config.sessionPort}`);
356
+ console.error(`Hook server: http://localhost:${actualPort}`);
357
+ console.error(`Session info: ${sessionInfoFile}`);
286
358
  console.error('');
287
359
 
288
360
  // Graceful shutdown
289
361
  const shutdown = async () => {
290
362
  console.error('\nShutting down...');
363
+ cleanupSessionInfo(config.sessionName);
291
364
  httpServer.close();
292
365
  telegram.stop();
293
366
  process.exit(0);