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:
RichardDillman
2025-11-23 14:15:46 -05:00
parent c4cfcf6eb8
commit c45fe4d509
5 changed files with 338 additions and 15 deletions

View File

@@ -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);
});