Skip to main content
Glama

Agent MCP

activity-broadcaster.js8.99 kB
#!/usr/bin/env node const LockUtils = require('./lock-utils'); const fs = require('fs'); const path = require('path'); /** * Activity Broadcaster Hook for Claude Code PostToolUse * * This hook logs completed file operations for real-time visibility * and releases file locks after successful operations. */ class ActivityBroadcaster { constructor() { this.lockUtils = new LockUtils(); } /** * Parse input from Claude Code hook * @returns {Promise<Object>} Parsed hook input */ async parseInput() { return new Promise((resolve, reject) => { let input = ''; process.stdin.on('data', (chunk) => { input += chunk.toString(); }); process.stdin.on('end', () => { try { const data = JSON.parse(input); resolve(data); } catch (error) { reject(new Error(`Invalid JSON input: ${error.message}`)); } }); process.stdin.on('error', reject); }); } /** * Extract file path from tool input * @param {Object} toolInput - Tool input object * @returns {string|null} File path or null if not found */ extractFilePath(toolInput) { return toolInput.file_path || toolInput.filePath || (toolInput.edits && toolInput.edits[0] && toolInput.edits[0].file_path) || null; } /** * Extract agent ID from hook input * @param {Object} input - Hook input * @returns {string} Agent ID */ extractAgentId(input) { return process.env.MCP_AGENT_ID || input.agent_id || input.session_id || 'unknown-agent'; } /** * Analyze tool response for additional details * @param {Object} toolResponse - Tool response object * @returns {Object} Analysis details */ analyzeToolResponse(toolResponse) { const details = {}; if (toolResponse) { // Extract success status details.success = toolResponse.success !== false; // Extract any error information if (toolResponse.error) { details.error = toolResponse.error; } // Extract file information if available if (toolResponse.filePath) { details.actual_file_path = toolResponse.filePath; } // Extract content length for write operations if (toolResponse.content && typeof toolResponse.content === 'string') { details.content_length = toolResponse.content.length; details.lines_written = toolResponse.content.split('\n').length; } } return details; } /** * Get file statistics * @param {string} filePath - File path * @returns {Object} File statistics */ getFileStats(filePath) { try { if (fs.existsSync(filePath)) { const stats = fs.statSync(filePath); const content = fs.readFileSync(filePath, 'utf8'); return { size_bytes: stats.size, modified_at: stats.mtime.toISOString(), lines_total: content.split('\n').length, file_exists: true }; } else { return { file_exists: false }; } } catch (error) { return { file_exists: false, stat_error: error.message }; } } /** * Update agent status * @param {string} agentId - Agent ID * @param {string} status - Current status * @param {Object} details - Additional details */ updateAgentStatus(agentId, status, details = {}) { try { const statusFile = path.join(this.lockUtils.activityDir, 'agent-status.json'); let agentStatuses = {}; // Load existing statuses if (fs.existsSync(statusFile)) { try { agentStatuses = JSON.parse(fs.readFileSync(statusFile, 'utf8')); } catch (error) { console.warn(`Warning: Could not parse agent status file: ${error.message}`); } } // Update status for this agent agentStatuses[agentId] = { status: status, last_activity: new Date().toISOString(), ...details }; // Write back to file fs.writeFileSync(statusFile, JSON.stringify(agentStatuses, null, 2)); } catch (error) { console.warn(`Warning: Could not update agent status: ${error.message}`); } } /** * Log detailed activity information * @param {Object} activityData - Activity data to log */ logDetailedActivity(activityData) { try { const activityFile = path.join(this.lockUtils.activityDir, 'live-feed.jsonl'); const entry = { timestamp: new Date().toISOString(), ...activityData }; fs.appendFileSync(activityFile, JSON.stringify(entry) + '\n'); } catch (error) { console.warn(`Warning: Could not log detailed activity: ${error.message}`); } } /** * Check if file should be excluded from activity logging * @param {string} filePath - File path to check * @returns {boolean} True if should be excluded */ shouldExcludeFile(filePath) { const excludePatterns = [ /\.log$/, /\.tmp$/, /\.temp$/, /\.lock$/, /\.agent-/, /\.git\//, /node_modules\// ]; return excludePatterns.some(pattern => pattern.test(filePath)); } /** * Main execution logic * @param {Object} input - Hook input from Claude Code * @returns {Object} Hook response */ async execute(input) { try { const { tool_name, tool_input, tool_response, session_id } = input; // Handle all tool operations for visibility const filePath = this.extractFilePath(tool_input); const agentId = this.extractAgentId(input); // Basic activity logging for all operations const baseActivity = { action: 'tool_completed', agent_id: agentId, tool_name: tool_name, session_id: session_id, file_path: filePath }; // If it's a file operation, add detailed information if (filePath && !this.shouldExcludeFile(filePath)) { const responseDetails = this.analyzeToolResponse(tool_response); const fileStats = this.getFileStats(filePath); const detailedActivity = { ...baseActivity, response_details: responseDetails, file_stats: fileStats, operation_successful: responseDetails.success !== false }; this.logDetailedActivity(detailedActivity); // Release file lock for modification operations if (['Edit', 'Write', 'MultiEdit'].includes(tool_name)) { const lockReleased = this.lockUtils.releaseLock(filePath, agentId); if (lockReleased) { this.logDetailedActivity({ action: 'lock_released', agent_id: agentId, file_path: filePath, reason: 'operation_completed', tool_name: tool_name }); } // Update agent status this.updateAgentStatus(agentId, 'idle', { last_file: filePath, last_operation: tool_name.toLowerCase(), operations_completed: (input.operations_completed || 0) + 1 }); } } else { // Log non-file operations too for complete visibility this.logDetailedActivity(baseActivity); } // Update agent status for all operations this.updateAgentStatus(agentId, 'active', { last_tool: tool_name, last_activity_detail: filePath ? `${tool_name} on ${path.basename(filePath)}` : tool_name }); // Always return success for PostToolUse hooks return { success: true, message: `Activity logged for ${tool_name}${filePath ? ` on ${path.basename(filePath)}` : ''}`, agent_id: agentId }; } catch (error) { console.error(`Activity broadcaster error: ${error.message}`); return { success: false, error: error.message }; } } /** * Output result to stdout * @param {Object} result - Hook result */ outputResult(result) { // PostToolUse hooks don't block operations, just log results if (result.success) { // Output success message to stdout for transcript if (result.message) { console.log(result.message); } process.exit(0); } else { // Output error to stderr but don't block console.error(`Activity logging failed: ${result.error}`); process.exit(1); } } } // CLI execution if (require.main === module) { (async () => { try { const broadcaster = new ActivityBroadcaster(); const input = await broadcaster.parseInput(); const result = await broadcaster.execute(input); broadcaster.outputResult(result); } catch (error) { console.error(`Activity broadcaster error: ${error.message}`); process.exit(1); } })(); } module.exports = ActivityBroadcaster;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/rinadelph/Agent-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server