feat: add auto-start, AFK mode, and comprehensive cross-platform setup
Major Features: - Auto-start: MCP server now automatically starts Telegram bridge on demand - AFK Mode: Toggle notifications on/off with /afk slash command - New telegram_toggle_afk MCP tool for controlling notification state - Dynamic enable/disable via new /toggle and /status API endpoints MCP Server Improvements: - Auto-detects if bridge is running and starts it automatically - Monitors bridge process health and logs output - Clean shutdown handling for both MCP server and bridge - Process spawning with proper environment variable passing Telegram Bridge Updates: - Runtime toggle for ENABLED state (was previously static) - POST /toggle endpoint to toggle notifications with Telegram confirmation - GET /status endpoint to check current notification state - Telegram messages sent when state changes (🟢/🔴 indicators) Documentation: - Cross-platform setup instructions (Mac, Linux, Windows) - Claude Code CLI setup guide with claude mcp add commands - Global vs project-specific MCP configuration explained - Troubleshooting section for fixing configuration scope issues - Complete AFK mode usage documentation - All new API endpoints documented Slash Commands: - Created /afk command in .claude/commands/afk.md - Available in both InnerVoice and ESO-MCP projects Files Changed: - src/mcp-server.ts: Auto-start logic and telegram_toggle_afk tool - src/index.ts: Dynamic ENABLED toggle and new API endpoints - README.md: Comprehensive setup and troubleshooting guide - mcp-config.json: Updated with correct absolute path - .claude/commands/afk.md: New slash command for AFK mode 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
36
src/index.ts
36
src/index.ts
@@ -10,7 +10,7 @@ const bot = new Telegraf(process.env.TELEGRAM_BOT_TOKEN!);
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT || '3456');
|
||||
const HOST = process.env.HOST || 'localhost';
|
||||
const ENABLED = process.env.ENABLED !== 'false';
|
||||
let ENABLED = process.env.ENABLED !== 'false'; // Now mutable for runtime toggling
|
||||
|
||||
let chatId: string | null = process.env.TELEGRAM_CHAT_ID || null;
|
||||
const envPath = path.join(process.cwd(), '.env');
|
||||
@@ -256,6 +256,40 @@ app.get('/health', (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle enabled state
|
||||
app.post('/toggle', async (req, res) => {
|
||||
const previousState = ENABLED;
|
||||
ENABLED = !ENABLED;
|
||||
|
||||
const statusMessage = ENABLED
|
||||
? '🟢 InnerVoice notifications ENABLED - You will receive messages'
|
||||
: '🔴 InnerVoice notifications DISABLED - Messages paused';
|
||||
|
||||
// Notify via Telegram if chat ID is set
|
||||
if (chatId) {
|
||||
try {
|
||||
await bot.telegram.sendMessage(chatId, statusMessage, { parse_mode: 'Markdown' });
|
||||
} catch (error) {
|
||||
console.error('Failed to send toggle notification:', error);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
enabled: ENABLED,
|
||||
previousState,
|
||||
message: statusMessage
|
||||
});
|
||||
});
|
||||
|
||||
// Get current enabled state
|
||||
app.get('/status', (req, res) => {
|
||||
res.json({
|
||||
enabled: ENABLED,
|
||||
message: ENABLED ? 'Notifications are ON' : 'Notifications are OFF (AFK mode)'
|
||||
});
|
||||
});
|
||||
|
||||
// Start bot
|
||||
bot.launch().then(() => {
|
||||
console.log('🤖 Telegram bot started');
|
||||
|
||||
@@ -7,8 +7,93 @@ import {
|
||||
ListToolsRequestSchema,
|
||||
Tool,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { spawn, ChildProcess } from 'child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const BRIDGE_URL = process.env.TELEGRAM_BRIDGE_URL || 'http://localhost:3456';
|
||||
const BRIDGE_PORT = new URL(BRIDGE_URL).port || '3456';
|
||||
const BRIDGE_HOST = new URL(BRIDGE_URL).hostname || 'localhost';
|
||||
|
||||
let bridgeProcess: ChildProcess | null = null;
|
||||
|
||||
// Check if the bridge is running
|
||||
async function isBridgeRunning(): Promise<boolean> {
|
||||
try {
|
||||
const response = await fetch(`${BRIDGE_URL}/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return response.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Start the Telegram bridge
|
||||
async function startBridge(): Promise<void> {
|
||||
console.error('🚀 Starting Telegram bridge...');
|
||||
|
||||
// Find the project root (one level up from dist or src)
|
||||
const projectRoot = join(__dirname, '..');
|
||||
const bridgeScript = join(projectRoot, 'dist', 'index.js');
|
||||
|
||||
// Start the bridge process
|
||||
bridgeProcess = spawn('node', [bridgeScript], {
|
||||
env: {
|
||||
...process.env,
|
||||
PORT: BRIDGE_PORT,
|
||||
HOST: BRIDGE_HOST,
|
||||
},
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
});
|
||||
|
||||
// Log bridge output
|
||||
if (bridgeProcess.stdout) {
|
||||
bridgeProcess.stdout.on('data', (data) => {
|
||||
console.error(`[Bridge] ${data.toString().trim()}`);
|
||||
});
|
||||
}
|
||||
|
||||
if (bridgeProcess.stderr) {
|
||||
bridgeProcess.stderr.on('data', (data) => {
|
||||
console.error(`[Bridge] ${data.toString().trim()}`);
|
||||
});
|
||||
}
|
||||
|
||||
bridgeProcess.on('error', (error) => {
|
||||
console.error('❌ Bridge process error:', error);
|
||||
});
|
||||
|
||||
bridgeProcess.on('exit', (code) => {
|
||||
console.error(`⚠️ Bridge process exited with code ${code}`);
|
||||
bridgeProcess = null;
|
||||
});
|
||||
|
||||
// Wait for the bridge to be ready
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
if (await isBridgeRunning()) {
|
||||
console.error('✅ Telegram bridge is ready');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Bridge failed to start after 5 seconds');
|
||||
}
|
||||
|
||||
// Ensure the bridge is running
|
||||
async function ensureBridge(): Promise<void> {
|
||||
if (await isBridgeRunning()) {
|
||||
console.error('✅ Telegram bridge is already running');
|
||||
return;
|
||||
}
|
||||
|
||||
await startBridge();
|
||||
}
|
||||
|
||||
// Define the Telegram bridge tools
|
||||
const TOOLS: Tool[] = [
|
||||
@@ -81,6 +166,14 @@ const TOOLS: Tool[] = [
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'telegram_toggle_afk',
|
||||
description: 'Toggle InnerVoice AFK mode - enables or disables Telegram notifications. Use this when going away from the system to enable notifications, or when back to disable them.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// Create the MCP server
|
||||
@@ -235,6 +328,30 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
};
|
||||
}
|
||||
|
||||
case 'telegram_toggle_afk': {
|
||||
const response = await fetch(`${BRIDGE_URL}/toggle`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to toggle AFK mode');
|
||||
}
|
||||
|
||||
const result: any = await response.json();
|
||||
const icon = result.enabled ? '🟢' : '🔴';
|
||||
const status = result.enabled ? 'ENABLED' : 'DISABLED';
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `${icon} InnerVoice AFK mode toggled: ${status}\n\n${result.message}`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
@@ -252,8 +369,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
function cleanup() {
|
||||
console.error('\n👋 Shutting down MCP server...');
|
||||
if (bridgeProcess) {
|
||||
console.error('🛑 Stopping bridge process...');
|
||||
bridgeProcess.kill('SIGTERM');
|
||||
bridgeProcess = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle shutdown signals
|
||||
process.on('SIGINT', () => {
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
cleanup();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Start the server
|
||||
async function main() {
|
||||
// Ensure the bridge is running before starting the MCP server
|
||||
await ensureBridge();
|
||||
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error('🚀 Telegram MCP server running on stdio');
|
||||
@@ -261,5 +402,6 @@ async function main() {
|
||||
|
||||
main().catch((error) => {
|
||||
console.error('Fatal error:', error);
|
||||
cleanup();
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user