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.
- package/hooks/notify-hook.sh +44 -14
- package/hooks/permission-hook.sh +42 -17
- package/package.json +1 -1
- package/src/index.ts +76 -3
package/hooks/notify-hook.sh
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
75
|
+
# Return success immediately
|
|
46
76
|
echo '{"continue": true}'
|
package/hooks/permission-hook.sh
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
|
|
16
|
-
|
|
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,
|
|
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
|
|
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
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(
|
|
131
|
-
console.error(`[HTTP] Hook server listening on port ${
|
|
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:${
|
|
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);
|