telegram-claude-mcp 1.1.0 → 1.2.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.
@@ -5,19 +5,51 @@
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.
9
+ # No configuration needed - it finds running MCP servers automatically.
14
10
  #
15
11
 
16
- HOOK_PORT="${TELEGRAM_HOOK_PORT:-3333}"
17
- HOOK_HOST="${TELEGRAM_HOOK_HOST:-localhost}"
18
- HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/notify"
12
+ SESSION_DIR="/tmp/telegram-claude-sessions"
19
13
  EVENT_TYPE="${TELEGRAM_HOOK_EVENT:-stop}"
20
14
 
15
+ # Function to find the most recent active session
16
+ find_active_session() {
17
+ local latest_file=""
18
+ local latest_time=0
19
+
20
+ for info_file in "$SESSION_DIR"/*.info 2>/dev/null; do
21
+ [ -f "$info_file" ] || continue
22
+
23
+ # Check if the process is still running
24
+ local pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
25
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
26
+ local file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
27
+ if [ "$file_time" -gt "$latest_time" ]; then
28
+ latest_time=$file_time
29
+ latest_file=$info_file
30
+ fi
31
+ fi
32
+ done
33
+
34
+ echo "$latest_file"
35
+ }
36
+
37
+ # Find active session info
38
+ INFO_FILE=$(find_active_session)
39
+
40
+ if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
41
+ echo "[notify-hook] No active session found, skipping notification" >&2
42
+ echo '{"continue": true}'
43
+ exit 0
44
+ fi
45
+
46
+ # Read port from session info
47
+ HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
48
+ HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
49
+ HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/notify"
50
+
51
+ echo "[notify-hook] Using session: $INFO_FILE (port $HOOK_PORT)" >&2
52
+
21
53
  # Read the hook input from stdin
22
54
  INPUT=$(cat)
23
55
 
@@ -25,9 +57,7 @@ INPUT=$(cat)
25
57
  SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty')
26
58
  MESSAGE=$(echo "$INPUT" | jq -r '.message // .stop_reason // "Agent stopped"')
27
59
 
28
- # Log for debugging
29
- echo "[notify-hook] Event: $EVENT_TYPE" >&2
30
- echo "[notify-hook] Message: $MESSAGE" >&2
60
+ echo "[notify-hook] Event: $EVENT_TYPE, Message: $MESSAGE" >&2
31
61
 
32
62
  # Build the request payload
33
63
  PAYLOAD=$(jq -n \
@@ -36,11 +66,11 @@ PAYLOAD=$(jq -n \
36
66
  --arg session_id "$SESSION_ID" \
37
67
  '{type: $type, message: $message, session_id: $session_id}')
38
68
 
39
- # Send the notification (non-blocking, we don't need to wait)
69
+ # Send the notification (non-blocking)
40
70
  curl -s -X POST "$HOOK_URL" \
41
71
  -H "Content-Type: application/json" \
42
72
  -d "$PAYLOAD" \
43
73
  --max-time 10 >/dev/null 2>&1 &
44
74
 
45
- # Return success immediately (hook doesn't need to block)
75
+ # Return success immediately
46
76
  echo '{"continue": true}'
@@ -5,17 +5,50 @@
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.
9
+ # No configuration needed - it finds running MCP servers automatically.
13
10
  #
14
11
 
15
- HOOK_PORT="${TELEGRAM_HOOK_PORT:-3333}"
16
- HOOK_HOST="${TELEGRAM_HOOK_HOST:-localhost}"
12
+ SESSION_DIR="/tmp/telegram-claude-sessions"
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
+ for info_file in "$SESSION_DIR"/*.info 2>/dev/null; do
20
+ [ -f "$info_file" ] || continue
21
+
22
+ # Check if the process is still running
23
+ local pid=$(jq -r '.pid // empty' "$info_file" 2>/dev/null)
24
+ if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
25
+ local file_time=$(stat -f %m "$info_file" 2>/dev/null || stat -c %Y "$info_file" 2>/dev/null)
26
+ if [ "$file_time" -gt "$latest_time" ]; then
27
+ latest_time=$file_time
28
+ latest_file=$info_file
29
+ fi
30
+ fi
31
+ done
32
+
33
+ echo "$latest_file"
34
+ }
35
+
36
+ # Find active session info
37
+ INFO_FILE=$(find_active_session)
38
+
39
+ if [ -z "$INFO_FILE" ] || [ ! -f "$INFO_FILE" ]; then
40
+ echo "[permission-hook] No active session found" >&2
41
+ # Return empty to fall back to default behavior (terminal prompt)
42
+ exit 0
43
+ fi
44
+
45
+ # Read port from session info
46
+ HOOK_PORT=$(jq -r '.port' "$INFO_FILE")
47
+ HOOK_HOST=$(jq -r '.host // "localhost"' "$INFO_FILE")
17
48
  HOOK_URL="http://${HOOK_HOST}:${HOOK_PORT}/permission"
18
49
 
50
+ echo "[permission-hook] Using session: $INFO_FILE (port $HOOK_PORT)" >&2
51
+
19
52
  # Read the hook input from stdin
20
53
  INPUT=$(cat)
21
54
 
@@ -23,16 +56,13 @@ INPUT=$(cat)
23
56
  TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
24
57
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.tool_input // {}')
25
58
 
26
- # If no tool_name, this might be a different format - try to extract from the input
59
+ # If no tool_name, try alternative paths
27
60
  if [ -z "$TOOL_NAME" ]; then
28
- # Claude Code might send the data differently, try alternative paths
29
61
  TOOL_NAME=$(echo "$INPUT" | jq -r '.toolName // .name // empty')
30
62
  TOOL_INPUT=$(echo "$INPUT" | jq -c '.toolInput // .input // .arguments // {}')
31
63
  fi
32
64
 
33
- # Log for debugging (goes to stderr, not stdout)
34
65
  echo "[permission-hook] Tool: $TOOL_NAME" >&2
35
- echo "[permission-hook] Sending to: $HOOK_URL" >&2
36
66
 
37
67
  # Build the request payload
38
68
  PAYLOAD=$(jq -n \
@@ -46,16 +76,11 @@ RESPONSE=$(curl -s -X POST "$HOOK_URL" \
46
76
  -d "$PAYLOAD" \
47
77
  --max-time 600)
48
78
 
49
- # Check if curl succeeded
50
79
  if [ $? -ne 0 ]; then
51
80
  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"}}}'
81
+ # Return empty to fall back to default behavior
54
82
  exit 0
55
83
  fi
56
84
 
57
- # Log the response
58
85
  echo "[permission-hook] Response: $RESPONSE" >&2
59
-
60
- # Output the response (this goes back to Claude Code)
61
86
  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.0",
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);