Skip to main content
Glama

Agent MCP

management.ts20.1 kB
// Task management tools for Agent-MCP Node.js // Ported from Python task_tools.py (view_tasks, update_task_status functions) import { z } from 'zod'; import { registerTool } from '../registry.js'; import { getDbConnection } from '../../db/connection.js'; import { MCP_DEBUG } from '../../core/config.js'; import { verifyToken, getAgentId } from '../../core/auth.js'; import { formatTaskSummary, formatTaskDetailed, formatTaskWithDependencies, calculateTaskHealthMetrics, logTaskAction, validateTaskStatus } from './core.js'; import { autoLaunchTestingAgents } from '../../utils/testingAgent.js'; // Helper function to get agent ID from token function getAgentIdFromToken(token: string): string | null { const db = getDbConnection(); try { const agent = db.prepare('SELECT agent_id FROM agents WHERE token = ?').get(token); return agent ? (agent as any).agent_id : null; } catch (error) { console.error('Error getting agent ID:', error); return null; } } // Helper function to verify admin access function verifyAdminToken(token?: string): boolean { if (!token) return false; return verifyToken(token, 'admin'); } // View Tasks Tool registerTool( 'view_tasks', 'View tasks with filtering, sorting and detailed information. Supports multiple display formats.', z.object({ token: z.string().optional().describe('Agent or admin authentication token (optional - uses session context)'), // Filtering options filter_agent_id: z.string().optional().describe('Filter tasks by assigned agent ID'), filter_status: z.enum(['pending', 'in_progress', 'completed', 'cancelled', 'failed']).optional().describe('Filter by task status'), filter_priority: z.enum(['low', 'medium', 'high']).optional().describe('Filter by task priority'), filter_created_by: z.string().optional().describe('Filter by task creator'), filter_parent_task: z.string().optional().describe('Filter by parent task ID'), filter_has_dependencies: z.boolean().optional().describe('Filter tasks that have dependencies'), // Date filtering filter_created_after: z.string().optional().describe('Filter tasks created after date (ISO format)'), filter_created_before: z.string().optional().describe('Filter tasks created before date (ISO format)'), filter_updated_after: z.string().optional().describe('Filter tasks updated after date (ISO format)'), // Text search search_text: z.string().optional().describe('Search in task titles and descriptions'), // Display options display_mode: z.enum(['summary', 'detailed', 'with_dependencies']).default('summary').describe('Display format'), include_completed: z.boolean().default(false).describe('Include completed tasks'), include_cancelled: z.boolean().default(false).describe('Include cancelled tasks'), // Sorting and pagination sort_by: z.enum(['created_at', 'updated_at', 'priority', 'status', 'title']).default('updated_at').describe('Sort field'), sort_order: z.enum(['asc', 'desc']).default('desc').describe('Sort order'), limit: z.number().min(1).max(200).default(50).describe('Maximum number of tasks to return'), offset: z.number().min(0).default(0).describe('Number of tasks to skip for pagination'), // Health metrics include_health_metrics: z.boolean().default(false).describe('Include overall task health metrics') }), async (args, context) => { const { token, filter_agent_id, filter_status, filter_priority, filter_created_by, filter_parent_task, filter_has_dependencies, filter_created_after, filter_created_before, filter_updated_after, search_text, display_mode = 'summary', include_completed = false, include_cancelled = false, sort_by = 'updated_at', sort_order = 'desc', limit = 50, offset = 0, include_health_metrics = false } = args; // Get requesting agent ID const requestingAgentId = getAgentIdFromToken(token); const isAdmin = verifyAdminToken(token); if (!requestingAgentId && !isAdmin) { return { content: [{ type: 'text' as const, text: '❌ Unauthorized: Valid token required' }], isError: true }; } const db = getDbConnection(); try { // Build dynamic query let query = 'SELECT * FROM tasks WHERE 1=1'; const params: any[] = []; // Apply permission filtering - non-admin agents can only see their own tasks or unassigned tasks if (!isAdmin && requestingAgentId) { query += ' AND (assigned_to = ? OR assigned_to IS NULL OR created_by = ?)'; params.push(requestingAgentId, requestingAgentId); } // Apply filters if (filter_agent_id) { query += ' AND assigned_to = ?'; params.push(filter_agent_id); } if (filter_status) { query += ' AND status = ?'; params.push(filter_status); } if (filter_priority) { query += ' AND priority = ?'; params.push(filter_priority); } if (filter_created_by) { query += ' AND created_by = ?'; params.push(filter_created_by); } if (filter_parent_task) { query += ' AND parent_task = ?'; params.push(filter_parent_task); } if (filter_has_dependencies !== undefined) { if (filter_has_dependencies) { query += " AND json_array_length(depends_on_tasks) > 0"; } else { query += " AND (depends_on_tasks = '[]' OR depends_on_tasks IS NULL)"; } } // Date filters if (filter_created_after) { query += ' AND created_at >= ?'; params.push(filter_created_after); } if (filter_created_before) { query += ' AND created_at <= ?'; params.push(filter_created_before); } if (filter_updated_after) { query += ' AND updated_at >= ?'; params.push(filter_updated_after); } // Text search if (search_text) { query += ' AND (title LIKE ? OR description LIKE ?)'; const searchPattern = `%${search_text}%`; params.push(searchPattern, searchPattern); } // Status exclusions if (!include_completed) { query += " AND status != 'completed'"; } if (!include_cancelled) { query += " AND status NOT IN ('cancelled', 'failed')"; } // Sorting const validSortFields = ['created_at', 'updated_at', 'priority', 'status', 'title']; const sortField = validSortFields.includes(sort_by) ? sort_by : 'updated_at'; const sortDirection = sort_order === 'asc' ? 'ASC' : 'DESC'; query += ` ORDER BY ${sortField} ${sortDirection}`; // Pagination query += ' LIMIT ? OFFSET ?'; params.push(limit, offset); // Execute query const tasks = db.prepare(query).all(...params); // Get total count for pagination info let countQuery = query.replace(/SELECT \* FROM/, 'SELECT COUNT(*) as total FROM'); countQuery = countQuery.replace(/ORDER BY.*$/, ''); countQuery = countQuery.replace(/LIMIT.*$/, ''); const countResult = db.prepare(countQuery).get(...params.slice(0, -2)); // Remove limit/offset params const totalCount = (countResult as any)?.total || 0; // Format response const response: string[] = []; if (include_health_metrics && isAdmin) { const allTasks = db.prepare('SELECT * FROM tasks').all(); const healthMetrics = calculateTaskHealthMetrics(allTasks); response.push('📊 **Task Health Metrics**'); response.push(''); response.push(`**Overall Status:**`); response.push(`- Total Tasks: ${healthMetrics.totalTasks}`); response.push(`- Recent Activity: ${healthMetrics.recentActivity} tasks updated in last 24h`); response.push(`- Overdue Tasks: ${healthMetrics.overdueCount}`); response.push(''); response.push(`**By Status:**`); Object.entries(healthMetrics.byStatus).forEach(([status, count]) => { response.push(`- ${status}: ${count}`); }); response.push(''); response.push(`**By Priority:**`); Object.entries(healthMetrics.byPriority).forEach(([priority, count]) => { response.push(`- ${priority}: ${count}`); }); response.push(''); response.push('─'.repeat(50)); response.push(''); } response.push(`🎯 **Task List** (${tasks.length} of ${totalCount} total)`); response.push(''); if (tasks.length === 0) { response.push('No tasks found matching the specified criteria.'); // Provide helpful suggestions if (filter_status || filter_priority || search_text) { response.push(''); response.push('💡 **Suggestions:**'); response.push('- Try removing some filters'); response.push('- Check if tasks exist with different status/priority'); response.push('- Use search_text for broader text matching'); } } else { // Display tasks based on mode tasks.forEach((task: any, index: number) => { switch (display_mode) { case 'detailed': response.push(formatTaskDetailed(task)); break; case 'with_dependencies': response.push(formatTaskWithDependencies(task)); break; default: // summary response.push(formatTaskSummary(task)); break; } if (index < tasks.length - 1) { response.push(''); } }); // Pagination info if (totalCount > limit) { response.push(''); response.push('─'.repeat(30)); response.push(`📄 **Pagination:** Showing ${offset + 1}-${Math.min(offset + limit, totalCount)} of ${totalCount}`); if (offset + limit < totalCount) { response.push(`💡 Use offset=${offset + limit} to see next ${limit} tasks`); } } } // Applied filters summary if (Object.keys(args).some(key => key.startsWith('filter_') && args[key] !== undefined)) { response.push(''); response.push('🔍 **Active Filters:**'); if (filter_agent_id) response.push(`- Agent: ${filter_agent_id}`); if (filter_status) response.push(`- Status: ${filter_status}`); if (filter_priority) response.push(`- Priority: ${filter_priority}`); if (filter_created_by) response.push(`- Created by: ${filter_created_by}`); if (filter_parent_task) response.push(`- Parent: ${filter_parent_task}`); if (search_text) response.push(`- Search: "${search_text}"`); } return { content: [{ type: 'text' as const, text: response.join('\n') }] }; } catch (error) { console.error('Error viewing tasks:', error); return { content: [{ type: 'text' as const, text: `❌ Error retrieving tasks: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); // Update Task Status Tool registerTool( 'update_task_status', 'Update the status of one or more tasks. Agents can update their own tasks, admins can update any task.', z.object({ token: z.string().describe('Agent or admin authentication token'), task_id: z.string().optional().describe('Single task ID to update'), task_ids: z.array(z.string()).optional().describe('Multiple task IDs to update'), new_status: z.enum(['pending', 'in_progress', 'completed', 'cancelled', 'failed']).describe('New status for the task(s)'), notes: z.string().optional().describe('Optional notes about the status change'), // Additional update fields (optional) new_title: z.string().optional().describe('New title for the task'), new_description: z.string().optional().describe('New description for the task'), new_priority: z.enum(['low', 'medium', 'high']).optional().describe('New priority for the task'), new_assigned_to: z.string().optional().describe('New agent assignment (admin only)'), // Completion metadata completion_notes: z.string().optional().describe('Notes for completed tasks'), estimated_hours: z.number().optional().describe('Actual hours worked (for analytics)') }), async (args, context) => { const { token, task_id, task_ids, new_status, notes, new_title, new_description, new_priority, new_assigned_to, completion_notes, estimated_hours } = args; // Get requesting agent ID const requestingAgentId = getAgentIdFromToken(token); const isAdmin = verifyAdminToken(token); if (!requestingAgentId && !isAdmin) { return { content: [{ type: 'text' as const, text: '❌ Unauthorized: Valid token required' }], isError: true }; } // Validate status if (!validateTaskStatus(new_status)) { return { content: [{ type: 'text' as const, text: '❌ Error: Invalid status. Must be one of: pending, in_progress, completed, cancelled, failed' }], isError: true }; } // Determine task IDs to update let targetTaskIds: string[]; if (task_id) { targetTaskIds = [task_id]; } else if (task_ids && task_ids.length > 0) { targetTaskIds = task_ids; } else { return { content: [{ type: 'text' as const, text: '❌ Error: Must provide either task_id or task_ids' }], isError: true }; } const db = getDbConnection(); const results: string[] = []; const timestamp = new Date().toISOString(); try { const transaction = db.transaction(() => { for (const taskId of targetTaskIds) { // Get current task data const task = db.prepare('SELECT * FROM tasks WHERE task_id = ?').get(taskId); if (!task) { results.push(`❌ Task '${taskId}' not found`); continue; } const taskData = task as any; // Check permissions if (!isAdmin && taskData.assigned_to !== requestingAgentId) { results.push(`❌ Unauthorized: Cannot update task '${taskId}' assigned to ${taskData.assigned_to}`); continue; } // Build update query const updateFields = ['status = ?', 'updated_at = ?']; const updateParams = [new_status, timestamp]; // Handle notes const currentNotes = JSON.parse(taskData.notes || '[]'); if (notes || completion_notes) { const noteContent = notes || completion_notes || ''; currentNotes.push({ content: noteContent, timestamp, agent_id: requestingAgentId || 'admin' }); updateFields.push('notes = ?'); updateParams.push(JSON.stringify(currentNotes)); } // Handle optional field updates if (new_title) { updateFields.push('title = ?'); updateParams.push(new_title); } if (new_description) { updateFields.push('description = ?'); updateParams.push(new_description); } if (new_priority) { updateFields.push('priority = ?'); updateParams.push(new_priority); } if (new_assigned_to && isAdmin) { updateFields.push('assigned_to = ?'); updateParams.push(new_assigned_to); } // Execute update const updateQuery = `UPDATE tasks SET ${updateFields.join(', ')} WHERE task_id = ?`; updateParams.push(taskId); const updateResult = db.prepare(updateQuery).run(...updateParams); if (updateResult.changes > 0) { results.push(`✅ Updated '${taskId}': ${taskData.title} → ${new_status}`); // Log the action logTaskAction(requestingAgentId || 'admin', 'updated_task_status', taskId, { old_status: taskData.status, new_status, notes: notes || completion_notes, estimated_hours }); // Special handling for completion if (new_status === 'completed') { results.push(` 🎉 Task completed! ${completion_notes ? `Notes: ${completion_notes}` : ''}`); } } else { results.push(`⚠️ No changes made to task '${taskId}'`); } } }); transaction(); // Auto-launch testing agents for completed tasks const completedTasks: Array<{ task_id: string; completed_by: string }> = []; if (new_status === 'completed') { for (const taskId of targetTaskIds) { const successfulUpdate = results.find(r => r.includes(`✅ Updated '${taskId}'`)); if (successfulUpdate) { completedTasks.push({ task_id: taskId, completed_by: requestingAgentId || 'admin' }); } } if (completedTasks.length > 0) { try { console.log(`🧪 Auto-launching testing agents for ${completedTasks.length} completed task(s)`); const testingAgentResults = await autoLaunchTestingAgents(completedTasks); // Add testing agent launch results to response for (const result of testingAgentResults) { if (result.testing_agent_launched) { results.push(` 🧪 Testing agent '${result.testing_agent_id}' launched for validation`); } else { results.push(` ⚠️ Failed to launch testing agent for task '${result.task_id}': ${result.error || 'Unknown error'}`); } } if (MCP_DEBUG) { const successfulLaunches = testingAgentResults.filter(r => r.testing_agent_launched).length; console.log(`🧪 Testing agent launches: ${successfulLaunches}/${testingAgentResults.length} successful`); } } catch (error) { console.error('❌ Error auto-launching testing agents:', error); results.push(` ⚠️ Failed to auto-launch testing agents: ${error instanceof Error ? error.message : String(error)}`); } } } const response = [ `📝 **Task Status Update Results**`, '', ...results ]; if (targetTaskIds.length > 1) { const successCount = results.filter(r => r.startsWith('✅')).length; response.push(''); response.push(`📊 **Summary:** ${successCount}/${targetTaskIds.length} tasks updated successfully`); } if (MCP_DEBUG) { console.log(`📝 ${requestingAgentId || 'admin'} updated ${targetTaskIds.length} task(s) to ${new_status}`); } return { content: [{ type: 'text' as const, text: response.join('\n') }] }; } catch (error) { console.error('Error updating task status:', error); return { content: [{ type: 'text' as const, text: `❌ Error updating task status: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } ); console.log('✅ Task management tools registered successfully');

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