UNPKG

agent-hub-mcp

Version:

Universal AI agent coordination platform based on Model Context Protocol (MCP)

1 lines • 152 kB
{"version":3,"sources":["../src/index.ts","../src/agents/service.ts","../src/features/service.ts","../src/types/features.types.ts","../src/types/messages.types.ts","../src/messaging/service.ts","../src/servers/mcp.ts","../src/tools/definitions.ts","../src/tools/handlers.ts","../src/validation/schema.ts","../src/validation/security.ts","../src/agents/detection.ts","../src/features/handlers.ts","../src/messaging/handlers.ts","../src/storage/file-storage.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nimport { AgentService } from './agents/service';\nimport { FeaturesService } from './features/service';\nimport { MessageService } from './messaging/service';\nimport { createMcpServer } from './servers/mcp';\nimport { FileStorage } from './storage';\n\n// Use FileStorage directly for simplicity and reliability\nfunction createStorage(): FileStorage {\n const dataDirectory = process.env.AGENT_HUB_DATA_DIR ?? '~/.agent-hub';\n\n return new FileStorage(dataDirectory);\n}\n\nconst storage = createStorage();\n\n// Initialize services\nconst messageService = new MessageService(storage);\nconst featuresService = new FeaturesService(storage);\nconst agentService = new AgentService(storage, featuresService, messageService);\n\nasync function main() {\n await storage.init();\n\n // For stdio transport, we have a single server instance\n let mcpServer: any = null;\n\n // Notification functions for stdio transport\n async function broadcastNotification(method: string, _params: unknown) {\n if (mcpServer) {\n // eslint-disable-next-line no-console\n console.error(`šŸ“” Broadcasting ${method} via stdio transport`);\n\n try {\n // Use MCP's built-in resource list changed notification\n await mcpServer.sendResourceListChanged();\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('Error sending notification via stdio:', error);\n }\n }\n }\n\n async function sendNotificationToAgent(agentId: string, method: string, _params: unknown) {\n if (mcpServer) {\n // eslint-disable-next-line no-console\n console.error(`šŸ“¤ Sending ${method} to agent ${agentId} via stdio transport`);\n\n try {\n // For stdio, we send to the single connected client\n await mcpServer.sendResourceListChanged();\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('Error sending notification to agent via stdio:', error);\n }\n }\n }\n\n async function sendResourceNotification(agentId: string, resourceUri: string) {\n if (mcpServer) {\n // eslint-disable-next-line no-console\n console.error(\n `šŸ”„ Sending resource notification to ${agentId} for ${resourceUri} via stdio transport`,\n );\n\n try {\n // For stdio, we send resources/list_changed to the single connected client\n // The client should then re-read all resources including the changed one\n await mcpServer.sendResourceListChanged();\n } catch (error) {\n // eslint-disable-next-line no-console\n console.error('Error sending resource notification via stdio:', error);\n }\n }\n }\n\n // Create MCP server with proper notification handlers\n const server = createMcpServer({\n storage,\n messageService,\n agentService,\n broadcastNotification,\n getCurrentSession: () => undefined, // No session management for stdio\n sendNotificationToAgent,\n sendResourceNotification,\n });\n\n mcpServer = server;\n\n const transport = new StdioServerTransport();\n\n await server.connect(transport);\n\n // eslint-disable-next-line no-console\n console.error(`Agent Hub MCP server started`);\n}\n\n// eslint-disable-next-line unicorn/prefer-top-level-await\nmain().catch(error => {\n // eslint-disable-next-line no-console\n console.error('Failed to start server:', error);\n process.exit(1);\n});\n","import { FeaturesService } from '~/features/service';\nimport { MessageService } from '~/messaging/service';\n\nimport { AgentRegistration, HubStatusResult, StorageAdapter } from '~/types';\n\nexport class AgentService {\n constructor(\n private readonly storage: StorageAdapter,\n private readonly featuresService: FeaturesService,\n private readonly messageService: MessageService,\n ) {}\n\n async getAllAgents(): Promise<AgentRegistration[]> {\n return this.storage.getAgents();\n }\n\n async getHubStatus(): Promise<HubStatusResult> {\n // Get all agent registration info\n const allAgents = await this.getAllAgents();\n\n // Separate active vs inactive agents (active = last seen within 5 minutes)\n const fiveMinutesAgo = Date.now() - 5 * 60 * 1000;\n const activeAgents = allAgents.filter(agent => agent.lastSeen > fiveMinutesAgo);\n const inactiveAgents = allAgents.filter(agent => agent.lastSeen <= fiveMinutesAgo);\n\n // Get all features and categorize them\n const allFeatures = await this.featuresService.getFeatures();\n const activeFeatures = allFeatures.filter(feature => feature.status === 'active');\n\n const priorityCounts = {\n critical: allFeatures.filter(f => f.priority === 'critical').length,\n high: allFeatures.filter(f => f.priority === 'high').length,\n normal: allFeatures.filter(f => f.priority === 'normal').length,\n low: allFeatures.filter(f => f.priority === 'low').length,\n };\n\n // Get message activity overview\n const allMessages = await this.messageService.getAllMessages();\n const unreadMessages = allMessages.filter((message: any) => !message.read);\n\n // Messages from last hour\n const oneHourAgo = Date.now() - 60 * 60 * 1000;\n const recentMessages = allMessages.filter((message: any) => message.timestamp > oneHourAgo);\n\n return {\n agents: {\n total: allAgents.length,\n active: activeAgents,\n inactive: inactiveAgents,\n },\n features: {\n total: allFeatures.length,\n active: activeFeatures,\n byPriority: priorityCounts,\n },\n messages: {\n totalUnread: unreadMessages.length,\n recentActivity: recentMessages.length,\n },\n };\n }\n}\n","import { createId } from '@paralleldrive/cuid2';\n\nimport {\n AgentWorkload,\n CreateFeatureInput,\n CreateSubtaskServiceInput,\n CreateTaskInput,\n Delegation,\n DelegationStatus,\n Feature,\n FeatureData,\n FeatureFilters,\n FeaturePriority,\n FeatureStatus,\n ParentTask,\n StorageAdapter,\n Subtask,\n SubtaskStatus,\n TaskStatus,\n UpdateSubtaskInput,\n} from '~/types';\n\nexport class FeaturesService {\n constructor(private storage: StorageAdapter) {}\n\n async createFeature(input: CreateFeatureInput, createdBy: string): Promise<Feature> {\n const now = Date.now();\n\n const feature: Feature = {\n id: input.name.toLowerCase().replace(/[^\\da-z]+/g, '-'),\n name: input.name.toLowerCase().replace(/[^\\da-z]+/g, '-'),\n title: input.title,\n description: input.description,\n status: FeatureStatus.PLANNING,\n createdBy,\n priority: input.priority ?? FeaturePriority.NORMAL,\n estimatedAgents: input.estimatedAgents,\n assignedAgents: [],\n createdAt: now,\n updatedAt: now,\n };\n\n await this.storage.createFeature(feature);\n\n return feature;\n }\n\n async getFeatures(filters?: FeatureFilters): Promise<Feature[]> {\n return this.storage.getFeatures(filters);\n }\n\n async getFeature(featureId: string): Promise<Feature | undefined> {\n return this.storage.getFeature(featureId);\n }\n\n async updateFeature(featureId: string, updates: Partial<Feature>): Promise<void> {\n await this.storage.updateFeature(featureId, { ...updates, updatedAt: Date.now() });\n }\n\n async approveFeature(featureId: string): Promise<void> {\n await this.updateFeature(featureId, { status: FeatureStatus.ACTIVE });\n }\n\n async createTask(\n featureId: string,\n input: CreateTaskInput,\n createdBy: string,\n ): Promise<{ delegations: Delegation[]; task: ParentTask }> {\n const feature = await this.storage.getFeature(featureId);\n\n if (!feature) {\n throw new Error(`Feature not found: ${featureId}`);\n }\n\n const now = Date.now();\n const taskId = createId();\n\n const task: ParentTask = {\n id: taskId,\n title: input.title,\n description: input.description,\n status: TaskStatus.PLANNING,\n createdBy,\n createdAt: now,\n updatedAt: now,\n };\n\n // Create delegations for this task\n const delegations: Delegation[] = input.delegations.map((del, index) => ({\n id: `${taskId}-del-${index + 1}`,\n parentTaskId: taskId,\n agent: del.agent,\n scope: del.scope,\n status: DelegationStatus.PENDING,\n subtaskIds: [],\n createdBy,\n createdAt: now,\n updatedAt: now,\n }));\n\n // Save task and delegations\n await this.storage.createTask(featureId, task);\n\n for (const delegation of delegations) {\n await this.storage.createDelegation(featureId, delegation);\n }\n\n // Update feature's assigned agents\n const allAgents = [...(feature.assignedAgents || []), ...delegations.map(d => d.agent)];\n const uniqueAgents = [...new Set(allAgents)];\n\n await this.updateFeature(featureId, { assignedAgents: uniqueAgents });\n\n return { task, delegations };\n }\n\n async acceptDelegation(featureId: string, delegationId: string, agentId: string): Promise<void> {\n const delegation = await this.storage.getDelegation(featureId, delegationId);\n\n if (!delegation) {\n throw new Error(`Delegation not found: ${delegationId} in feature ${featureId}`);\n }\n\n if (delegation.agent !== agentId) {\n throw new Error(`Delegation ${delegationId} is not assigned to agent ${agentId}`);\n }\n\n if (delegation.status !== DelegationStatus.PENDING) {\n throw new Error(`Delegation ${delegationId} has already been ${delegation.status}`);\n }\n\n await this.storage.updateDelegation(featureId, delegationId, {\n status: DelegationStatus.ACCEPTED,\n acceptedAt: Date.now(),\n });\n }\n\n async createSubtask(\n featureId: string,\n delegationId: string,\n input: CreateSubtaskServiceInput,\n createdBy: string,\n ): Promise<Subtask> {\n const delegation = await this.storage.getDelegation(featureId, delegationId);\n\n if (!delegation) {\n throw new Error(`Delegation not found: ${delegationId} in feature ${featureId}`);\n }\n\n if (delegation.agent !== createdBy) {\n throw new Error(\n `Only assigned agent ${delegation.agent} can create subtasks for delegation ${delegationId}`,\n );\n }\n\n const now = Date.now();\n const subtaskId = createId();\n\n const subtask: Subtask = {\n id: subtaskId,\n delegationId,\n parentTaskId: delegation.parentTaskId,\n title: input.title,\n description: input.description,\n status: SubtaskStatus.TODO,\n createdBy,\n dependsOn: input.dependsOn || [],\n createdAt: now,\n updatedAt: now,\n };\n\n await this.storage.createSubtask(featureId, subtask);\n\n // Update delegation to include this subtask\n const updatedSubtaskIds = [...delegation.subtaskIds, subtaskId];\n\n await this.storage.updateDelegation(featureId, delegationId, {\n subtaskIds: updatedSubtaskIds,\n });\n\n // If delegation was pending and now has subtasks, mark as in-progress\n if (\n delegation.status === DelegationStatus.ACCEPTED ||\n delegation.status === DelegationStatus.PENDING\n ) {\n await this.storage.updateDelegation(featureId, delegationId, {\n status: DelegationStatus.IN_PROGRESS,\n });\n }\n\n return subtask;\n }\n\n async updateSubtask(\n featureId: string,\n subtaskId: string,\n updates: UpdateSubtaskInput,\n updatedBy: string,\n ): Promise<void> {\n const subtask = await this.storage.getSubtask(featureId, subtaskId);\n\n if (!subtask) {\n throw new Error(`Subtask not found: ${subtaskId} in feature ${featureId}`);\n }\n\n if (subtask.createdBy !== updatedBy) {\n throw new Error(`Only the creator ${subtask.createdBy} can update subtask ${subtaskId}`);\n }\n\n await this.storage.updateSubtask(featureId, subtaskId, {\n ...updates,\n updatedAt: Date.now(),\n });\n\n // Check if delegation should be marked as completed\n if (updates.status === SubtaskStatus.COMPLETED) {\n await this.checkDelegationCompletion(featureId, subtask.delegationId);\n }\n }\n\n private async checkDelegationCompletion(featureId: string, delegationId: string): Promise<void> {\n const delegation = await this.storage.getDelegation(featureId, delegationId);\n\n if (!delegation) {\n return;\n }\n\n const subtasks = await this.storage.getSubtasks(featureId, delegationId);\n const allCompleted =\n subtasks.length > 0 && subtasks.every(s => s.status === SubtaskStatus.COMPLETED);\n\n if (allCompleted && delegation.status !== DelegationStatus.COMPLETED) {\n await this.storage.updateDelegation(featureId, delegationId, {\n status: DelegationStatus.COMPLETED,\n completedAt: Date.now(),\n });\n\n // Check if parent task should be marked as completed\n await this.checkTaskCompletion(featureId, delegation.parentTaskId);\n }\n }\n\n private async checkTaskCompletion(featureId: string, taskId: string): Promise<void> {\n const task = await this.storage.getTask(featureId, taskId);\n\n if (!task) {\n return;\n }\n\n const delegations = await this.storage.getDelegations(featureId);\n const taskDelegations = delegations.filter(d => d.parentTaskId === taskId);\n const allCompleted =\n taskDelegations.length > 0 &&\n taskDelegations.every(d => d.status === DelegationStatus.COMPLETED);\n\n if (allCompleted && task.status !== TaskStatus.COMPLETED) {\n await this.storage.updateTask(featureId, taskId, {\n status: TaskStatus.COMPLETED,\n });\n\n // Check if feature should be marked as completed\n await this.checkFeatureCompletion(featureId);\n }\n }\n\n private async checkFeatureCompletion(featureId: string): Promise<void> {\n const tasks = await this.storage.getTasksInFeature(featureId);\n const allCompleted = tasks.length > 0 && tasks.every(t => t.status === TaskStatus.COMPLETED);\n\n if (allCompleted) {\n await this.updateFeature(featureId, { status: FeatureStatus.COMPLETED });\n }\n }\n\n async getAgentWorkload(agentId: string): Promise<AgentWorkload> {\n return this.storage.getAgentWorkload(agentId);\n }\n\n async getFeatureData(featureId: string): Promise<FeatureData | undefined> {\n return this.storage.getFeatureData(featureId);\n }\n\n async getFeatureOverview(featureId?: string): Promise<Feature[] | FeatureData> {\n if (featureId) {\n const data = await this.getFeatureData(featureId);\n\n if (!data) {\n throw new Error(`Feature not found: ${featureId}`);\n }\n\n return data;\n }\n\n return this.getFeatures();\n }\n\n // Utility methods for common queries\n\n async getActiveFeatures(): Promise<Feature[]> {\n return this.getFeatures({ status: FeatureStatus.ACTIVE });\n }\n\n async getAgentFeatures(agentId: string): Promise<Feature[]> {\n return this.getFeatures({ agent: agentId });\n }\n\n async getFeaturesByPriority(priority: string): Promise<Feature[]> {\n return this.getFeatures({ priority: priority as any });\n }\n\n async getAgentDelegations(agentId: string, featureId: string): Promise<Delegation[]> {\n return this.storage.getDelegations(featureId, agentId);\n }\n\n async getAgentSubtasks(agentId: string, featureId: string): Promise<Subtask[]> {\n const subtasks = await this.storage.getSubtasks(featureId);\n\n return subtasks.filter(s => s.createdBy === agentId);\n }\n\n // Feature lifecycle management\n\n async pauseFeature(featureId: string): Promise<void> {\n await this.updateFeature(featureId, { status: FeatureStatus.ON_HOLD });\n }\n\n async resumeFeature(featureId: string): Promise<void> {\n await this.updateFeature(featureId, { status: FeatureStatus.ACTIVE });\n }\n\n async cancelFeature(featureId: string): Promise<void> {\n await this.updateFeature(featureId, { status: FeatureStatus.CANCELLED });\n }\n\n // Dependency management\n\n async getSubtaskDependencies(featureId: string, subtaskId: string): Promise<Subtask[]> {\n const subtask = await this.storage.getSubtask(featureId, subtaskId);\n\n if (!subtask || !subtask.dependsOn.length) {\n return [];\n }\n\n const dependencies: Subtask[] = [];\n\n for (const depId of subtask.dependsOn) {\n const dep = await this.storage.getSubtask(featureId, depId);\n\n if (dep) {\n dependencies.push(dep);\n }\n }\n\n return dependencies;\n }\n\n async canStartSubtask(featureId: string, subtaskId: string): Promise<boolean> {\n const dependencies = await this.getSubtaskDependencies(featureId, subtaskId);\n\n return dependencies.every(dep => dep.status === SubtaskStatus.COMPLETED);\n }\n\n // Statistics and monitoring\n\n async getFeatureStats(featureId?: string): Promise<any> {\n if (featureId) {\n const data = await this.getFeatureData(featureId);\n\n if (!data) {\n return null;\n }\n\n return {\n feature: data.feature,\n tasksTotal: data.tasks.length,\n tasksCompleted: data.tasks.filter(t => t.status === TaskStatus.COMPLETED).length,\n delegationsTotal: data.delegations.length,\n delegationsCompleted: data.delegations.filter(d => d.status === DelegationStatus.COMPLETED)\n .length,\n subtasksTotal: data.subtasks.length,\n subtasksCompleted: data.subtasks.filter(s => s.status === SubtaskStatus.COMPLETED).length,\n agents: [...new Set(data.delegations.map(d => d.agent))],\n };\n }\n\n // Global stats\n const features = await this.getFeatures();\n const stats = {\n active: features.filter(f => f.status === FeatureStatus.ACTIVE).length,\n completed: features.filter(f => f.status === FeatureStatus.COMPLETED).length,\n planning: features.filter(f => f.status === FeatureStatus.PLANNING).length,\n onHold: features.filter(f => f.status === FeatureStatus.ON_HOLD).length,\n cancelled: features.filter(f => f.status === FeatureStatus.CANCELLED).length,\n byPriority: {} as Record<string, number>,\n byAgent: {} as Record<string, number>,\n };\n\n // Count by priority\n for (const feature of features) {\n stats.byPriority[feature.priority] = (stats.byPriority[feature.priority] || 0) + 1;\n\n // Count by assigned agents\n for (const agent of feature.assignedAgents || []) {\n stats.byAgent[agent] = (stats.byAgent[agent] || 0) + 1;\n }\n }\n\n return stats;\n }\n}\n","import { AuditableEntity, FilterOptions, StatusEntity } from './common.types';\n\nexport enum DelegationStatus {\n PENDING = 'pending',\n ACCEPTED = 'accepted',\n IN_PROGRESS = 'in-progress',\n COMPLETED = 'completed',\n BLOCKED = 'blocked',\n}\n\nexport enum FeaturePriority {\n CRITICAL = 'critical',\n HIGH = 'high',\n NORMAL = 'normal',\n LOW = 'low',\n}\n\nexport enum FeatureStatus {\n PLANNING = 'planning',\n ACTIVE = 'active',\n COMPLETED = 'completed',\n ON_HOLD = 'on-hold',\n CANCELLED = 'cancelled',\n}\n\nexport enum SubtaskStatus {\n TODO = 'todo',\n IN_PROGRESS = 'in-progress',\n COMPLETED = 'completed',\n BLOCKED = 'blocked',\n}\n\nexport enum TaskStatus {\n PLANNING = 'planning',\n APPROVED = 'approved',\n IN_PROGRESS = 'in-progress',\n COMPLETED = 'completed',\n BLOCKED = 'blocked',\n}\n\n/**\n * Feature list filters\n */\nexport type FeatureFilters = FilterOptions<Pick<Feature, 'status' | 'priority' | 'createdBy'>> & {\n agent?: string;\n};\n\n/**\n * Agent's work within a specific feature\n */\nexport interface AgentFeatureWork {\n feature: Feature;\n featureId: string;\n myDelegations: Delegation[];\n mySubtasks?: Subtask[];\n}\n\n/**\n * Agent's work across all features\n */\nexport interface AgentWorkload {\n activeFeatures: AgentFeatureWork[];\n}\n\n/**\n * Delegation creation input\n */\nexport interface CreateDelegationInput {\n agent: string;\n scope: string;\n}\n\n/**\n * Feature creation input\n */\nexport interface CreateFeatureInput {\n createdBy: string;\n description: string;\n estimatedAgents?: string[];\n name: string;\n priority?: FeaturePriority;\n title: string;\n}\n\n/**\n * Subtask creation input (for MCP tool)\n */\nexport interface CreateSubtaskInput {\n createdBy: string;\n delegationId: string;\n featureId: string;\n subtasks: SubtaskData[];\n}\n\n/**\n * Subtask creation input (for service method)\n */\nexport interface CreateSubtaskServiceInput {\n dependsOn?: string[];\n description?: string;\n title: string;\n}\n\n/**\n * Task creation input within a feature\n */\nexport interface CreateTaskInput {\n createdBy: string;\n delegations: Array<{\n agent: string;\n scope: string;\n }>;\n description: string;\n featureId: string;\n title: string;\n}\n\n/**\n * Work assigned to specific agents within a feature\n */\nexport interface Delegation extends AuditableEntity, StatusEntity<DelegationStatus> {\n acceptedAt?: number;\n agent: string;\n completedAt?: number;\n parentTaskId: string;\n scope: string;\n subtaskIds: string[];\n}\n\n/**\n * Represents an epic or major feature that spans multiple repositories and agents\n */\nexport interface Feature extends AuditableEntity, StatusEntity<FeatureStatus> {\n assignedAgents?: string[];\n description: string;\n estimatedAgents?: string[];\n name: string;\n priority: FeaturePriority;\n title: string;\n}\n\n/**\n * Complete feature data with all related entities\n */\nexport interface FeatureData {\n delegations: Delegation[];\n feature: Feature;\n subtasks: Subtask[];\n tasks: ParentTask[];\n}\n\n/**\n * Represents a major work item within a feature\n */\nexport interface ParentTask extends AuditableEntity, StatusEntity<TaskStatus> {\n approvedAt?: number;\n description: string;\n title: string;\n}\n\n/**\n * Specific implementation work created by domain agents\n */\nexport interface Subtask extends AuditableEntity, StatusEntity<SubtaskStatus> {\n blockedReason?: string;\n delegationId: string;\n dependsOn: string[];\n description?: string;\n output?: string;\n parentTaskId: string;\n title: string;\n}\n\n/**\n * Individual subtask data for creation\n */\nexport interface SubtaskData {\n dependsOn?: string[];\n description?: string;\n title: string;\n}\n\n/**\n * Subtask update input\n */\nexport interface UpdateSubtaskInput {\n blockedReason?: string;\n featureId: string;\n output?: string;\n status?: SubtaskStatus;\n subtaskId: string;\n updatedBy: string;\n}\n\n/**\n * Priority ordering for features\n */\nexport const PRIORITY_ORDER: Record<FeaturePriority, number> = {\n [FeaturePriority.CRITICAL]: 0,\n [FeaturePriority.HIGH]: 1,\n [FeaturePriority.NORMAL]: 2,\n [FeaturePriority.LOW]: 3,\n};\n","export enum MessagePriority {\n URGENT = 'urgent',\n NORMAL = 'normal',\n LOW = 'low',\n}\n\nexport enum MessageType {\n CONTEXT = 'context',\n TASK = 'task',\n QUESTION = 'question',\n COMPLETION = 'completion',\n ERROR = 'error',\n}\n\nexport interface Message {\n content: string;\n from: string;\n id: string;\n metadata?: Record<string, any>;\n priority?: MessagePriority;\n read: boolean;\n threadId?: string;\n timestamp: number;\n to: 'all' | (string & {});\n type: MessageType;\n}\n\nexport interface MessagesResponse {\n count: number;\n messages: Message[];\n}\n","import { createId } from '@paralleldrive/cuid2';\n\nimport { Message, MessagePriority, MessageType, StorageAdapter } from '~/types';\n\nexport class MessageService {\n constructor(private readonly storage: StorageAdapter) {}\n\n async sendMessage(\n from: string,\n to: string,\n type: MessageType,\n content: string,\n options: {\n metadata?: Record<string, any>;\n priority?: MessagePriority;\n threadId?: string;\n } = {},\n ): Promise<string> {\n const message: Message = {\n id: createId(),\n from,\n to,\n type,\n content,\n metadata: options.metadata,\n timestamp: Date.now(),\n read: false,\n threadId: options.threadId,\n priority: options.priority ?? MessagePriority.NORMAL,\n };\n\n await this.storage.saveMessage(message);\n\n return message.id;\n }\n\n async getAllMessages(\n options: {\n since?: number;\n type?: string;\n } = {},\n ): Promise<Message[]> {\n return this.storage.getMessages({\n type: options.type,\n since: options.since,\n });\n }\n\n async getMessages(\n agentId: string,\n options: {\n markAsRead?: boolean;\n since?: number;\n type?: string;\n } = {},\n ): Promise<{ count: number; messages: Message[] }> {\n const messages = await this.storage.getMessages({\n agent: agentId,\n type: options.type,\n since: options.since,\n });\n\n const unreadMessages = messages.filter(\n m => !m.read && (m.to === agentId || (m.to === 'all' && m.from !== agentId)),\n );\n\n if (options.markAsRead !== false) {\n // Mark messages as read atomically to prevent race conditions\n const markAsReadPromises = unreadMessages.map(async message => {\n try {\n await this.storage.markMessageAsRead(message.id);\n } catch (error) {\n // Log error but don't fail the entire operation if one message fails\n // eslint-disable-next-line no-console\n console.error(`Failed to mark message ${message.id} as read:`, error);\n }\n });\n\n // Wait for all messages to be marked as read, but don't fail if some fail\n await Promise.allSettled(markAsReadPromises);\n }\n\n return {\n count: unreadMessages.length,\n messages: unreadMessages,\n };\n }\n\n async getMessageById(messageId: string): Promise<Message | undefined> {\n return this.storage.getMessage(messageId);\n }\n}\n","import { Server } from '@modelcontextprotocol/sdk/server';\nimport {\n CallToolRequestSchema,\n InitializeRequestSchema,\n ListResourcesRequestSchema,\n ListToolsRequestSchema,\n ReadResourceRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js';\n\nimport { AgentService } from '~/agents/service';\nimport { AgentSession } from '~/agents/session';\nimport { MessageService } from '~/messaging/service';\nimport { TOOLS } from '~/tools/definitions';\nimport { createToolHandlers, ToolHandlerServices } from '~/tools/handlers';\n\nimport { FeatureStatus, StorageAdapter } from '~/types';\n\nexport interface McpServerDependencies {\n agentService: AgentService;\n broadcastNotification: (method: string, params: unknown) => Promise<void>;\n getCurrentSession: () => AgentSession | undefined;\n messageService: MessageService;\n sendNotificationToAgent: (agentId: string, method: string, params: unknown) => Promise<void>;\n sendResourceNotification?: (agentId: string, uri: string) => Promise<void>;\n storage: StorageAdapter;\n}\n\nexport function createMcpServer(deps: McpServerDependencies): Server {\n const server = new Server(\n {\n name: 'agent-hub-mcp',\n version: '0.2.0',\n },\n {\n capabilities: {\n tools: {},\n resources: {\n subscribe: true,\n listChanged: true,\n },\n },\n },\n );\n\n // Helper function to update agent's lastSeen timestamp\n async function updateAgentLastSeen(): Promise<void> {\n const currentSession = deps.getCurrentSession();\n\n if (currentSession?.agent) {\n await deps.storage.updateAgent(currentSession.agent.id, {\n lastSeen: Date.now(),\n status: 'active', // Ensure they're marked active when making requests\n });\n }\n }\n\n const toolHandlerServices: ToolHandlerServices = {\n storage: deps.storage,\n messageService: deps.messageService,\n agentService: deps.agentService,\n getCurrentSession: deps.getCurrentSession,\n broadcastNotification: deps.broadcastNotification,\n sendNotificationToAgent: deps.sendNotificationToAgent,\n sendResourceNotification: deps.sendResourceNotification,\n };\n\n const toolHandlers = createToolHandlers(toolHandlerServices);\n\n // Custom initialize handler to include collaboration state\n server.setRequestHandler(InitializeRequestSchema, async request => {\n const agents = await deps.storage.getAgents();\n const activeAgents = agents.filter(a => Date.now() - a.lastSeen < 5 * 60 * 1000);\n const totalMessages = await deps.storage.getMessages({});\n const unreadCount = totalMessages.filter(m => !m.read).length;\n\n return {\n protocolVersion: request.params.protocolVersion,\n capabilities: {\n tools: {},\n resources: {\n subscribe: true,\n listChanged: true,\n },\n },\n serverInfo: {\n name: 'agent-hub-mcp',\n version: '0.2.0',\n activeAgents: activeAgents.length,\n totalMessages: unreadCount,\n collaborationHints: activeAgents.map(a => ({\n id: a.id,\n role: a.role,\n capabilities: a.capabilities,\n })),\n },\n instructions: `🟢 CONNECTED TO AGENT-HUB | Registration Required\n\nāš ļø REGISTRATION PENDING - Complete registration to enable collaboration:\n\nregister_agent({\n \"id\": \"your-project-name\",\n \"projectPath\": \"/full/path/to/your/project\", \n \"role\": \"Your Role\"\n})\n\nQuick Examples:\nregister_agent({\"id\": \"react-app\", \"projectPath\": \"/Users/name/my-react-app\", \"role\": \"Frontend Developer\"})\nregister_agent({\"id\": \"api-server\", \"projectPath\": \"/Users/name/my-api\", \"role\": \"Backend Developer\"})\n\nAfter registration you'll be able to:\nāœ“ Exchange messages with other agents (use sync to stay updated)\nāœ“ Create and collaborate on features (multi-agent projects)\nāœ“ Delegate tasks to specific agents with clear scope\nāœ“ Track implementation progress with subtasks\n\nšŸ“‹ Collaboration Workflow:\n1. create_feature - Start a new multi-agent project\n2. create_task - Break features into tasks with agent delegations\n3. accept_delegation - Accept work assigned to you\n4. create_subtask - Track your implementation steps\n5. update_subtask - Report progress on your work\n\nšŸ’¬ Communication:\n• send_message - Send messages to other agents\n• sync - Get messages, workload, and status in one comprehensive call\n• get_hub_status - See hub activity, agents, and collaboration opportunities\n\nNote: Messages are stored instantly but require manual checking with sync.\nClaude Code uses a pull-only model - no automatic notifications.\n\n${\n activeAgents.length > 0\n ? `šŸ¤ ${activeAgents.length} registered agent(s) waiting to collaborate`\n : 'šŸ‘‹ You are the first agent to connect'\n}`,\n };\n });\n\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: TOOLS,\n }));\n\n // Tool call handler\n server.setRequestHandler(CallToolRequestSchema, async request => {\n const { arguments: arguments_, name } = request.params;\n\n // Update agent's lastSeen timestamp for any tool call\n await updateAgentLastSeen();\n\n if (!arguments_) {\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({ error: 'No arguments provided' }),\n },\n ],\n };\n }\n\n try {\n const handler = toolHandlers[name as keyof typeof toolHandlers];\n\n if (!handler) {\n throw new Error(`Unknown tool: ${name}`);\n }\n\n const result = await handler(arguments_ as any);\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify(result),\n },\n ],\n };\n } catch (error) {\n // Only expose safe error messages to prevent information disclosure\n let safeMessage = 'Operation failed';\n\n // Only show detailed errors for validation failures (safe to expose)\n if (error instanceof Error && error.message.includes('Invalid')) {\n safeMessage = error.message;\n }\n\n // Log the full error for debugging (server-side only)\n // eslint-disable-next-line no-console\n console.error('Tool execution error:', error);\n\n return {\n content: [\n {\n type: 'text',\n text: JSON.stringify({ error: safeMessage }),\n },\n ],\n };\n }\n });\n\n // Resource handlers for agent discovery\n server.setRequestHandler(ListResourcesRequestSchema, async () => {\n const currentSession = deps.getCurrentSession();\n\n const resources = [\n {\n uri: 'agent-hub://agents',\n name: 'Active Agents',\n description: 'List of all active agents in the hub',\n mimeType: 'application/json',\n },\n {\n uri: 'agent-hub://collaboration',\n name: 'Collaboration Opportunities',\n description: 'Current collaboration sessions and opportunities',\n mimeType: 'application/json',\n },\n ];\n\n // Add self resource if agent is registered\n if (currentSession?.agent) {\n resources.push(\n {\n uri: 'agent-hub://self',\n name: 'My Agent Info',\n description: \"This agent's registration and status\",\n mimeType: 'application/json',\n },\n {\n uri: `agent-hub://messages/${currentSession.agent.id}`,\n name: 'My Messages',\n description: 'Messages for this agent',\n mimeType: 'application/json',\n },\n );\n }\n\n return { resources };\n });\n\n server.setRequestHandler(ReadResourceRequestSchema, async request => {\n const { uri } = request.params;\n const currentSession = deps.getCurrentSession();\n\n // Handle self resource\n if (uri === 'agent-hub://self' && currentSession?.agent) {\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify(\n {\n id: currentSession.agent.id,\n role: currentSession.agent.role,\n capabilities: currentSession.agent.capabilities,\n projectPath: currentSession.agent.projectPath,\n status: currentSession.agent.status,\n lastSeen: currentSession.agent.lastSeen,\n collaboratesWith: currentSession.agent.collaboratesWith,\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n // Handle agent-specific messages\n if (uri.startsWith('agent-hub://messages/')) {\n const agentId = uri.replace('agent-hub://messages/', '');\n const messages = await deps.storage.getMessages({ agent: agentId });\n const unreadMessages = messages.filter(m => !m.read && (m.to === agentId || m.to === 'all'));\n\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify(\n {\n agentId,\n totalMessages: messages.length,\n unreadCount: unreadMessages.length,\n messages: unreadMessages.slice(0, 10).map(m => ({\n id: m.id,\n from: m.from,\n type: m.type,\n content: m.content,\n timestamp: m.timestamp,\n priority: m.priority,\n })),\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n if (uri === 'agent-hub://agents') {\n const agents = await deps.storage.getAgents();\n const activeAgents = agents.filter(a => Date.now() - a.lastSeen < 5 * 60 * 1000);\n\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify(\n {\n total: agents.length,\n active: activeAgents.length,\n agents: activeAgents.map(a => ({\n id: a.id,\n role: a.role,\n capabilities: a.capabilities,\n projectPath: a.projectPath,\n lastSeen: a.lastSeen,\n })),\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n if (uri === 'agent-hub://collaboration') {\n const features = await deps.storage.getFeatures();\n const messages = await deps.storage.getMessages({});\n const unreadMessages = messages.filter(m => !m.read);\n\n return {\n contents: [\n {\n uri,\n mimeType: 'application/json',\n text: JSON.stringify(\n {\n activeFeatures: features.filter(t => t.status === FeatureStatus.ACTIVE).length,\n pendingMessages: unreadMessages.length,\n recentActivity: unreadMessages.slice(0, 5).map(m => ({\n from: m.from,\n to: m.to,\n type: m.type,\n preview: m.content.slice(0, 100),\n timestamp: m.timestamp,\n })),\n },\n null,\n 2,\n ),\n },\n ],\n };\n }\n\n throw new Error(`Resource not found: ${uri}`);\n });\n\n return server;\n}\n","import { Tool } from '@modelcontextprotocol/sdk/types.js';\n\nimport { FeaturePriority, FeatureStatus, MessagePriority, MessageType } from '~/types';\n\nexport const TOOLS: Tool[] = [\n {\n name: 'register_agent',\n description: 'Register an agent with the hub',\n inputSchema: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description:\n 'Agent identifier (optional - will be generated from project path if not provided)',\n },\n projectPath: { type: 'string', description: 'Agent working directory' },\n role: { type: 'string', description: 'Agent role description' },\n capabilities: {\n type: 'array',\n items: { type: 'string' },\n description: 'Agent capabilities',\n default: [],\n },\n collaboratesWith: {\n type: 'array',\n items: { type: 'string' },\n description: 'Expected collaborators',\n default: [],\n },\n },\n required: ['projectPath', 'role'],\n },\n },\n {\n name: 'send_message',\n description: 'Send a message to another agent or broadcast to all agents',\n inputSchema: {\n type: 'object',\n properties: {\n from: { type: 'string', description: 'Source agent identifier' },\n to: { type: 'string', description: 'Target agent identifier or \"all\" for broadcast' },\n type: {\n type: 'string',\n enum: Object.values(MessageType),\n description: 'Message type',\n },\n content: { type: 'string', description: 'Message content' },\n metadata: { type: 'object', description: 'Additional structured data' },\n priority: {\n type: 'string',\n enum: Object.values(MessagePriority),\n description: 'Message priority',\n default: MessagePriority.NORMAL,\n },\n threadId: { type: 'string', description: 'Optional conversation thread ID' },\n },\n required: ['from', 'to', 'type', 'content'],\n },\n },\n {\n name: 'get_messages',\n description: 'Retrieve messages for an agent',\n inputSchema: {\n type: 'object',\n properties: {\n agent: { type: 'string', description: 'Agent identifier to get messages for' },\n markAsRead: {\n type: 'boolean',\n description: 'Mark retrieved messages as read',\n default: true,\n },\n type: {\n type: 'string',\n enum: Object.values(MessageType),\n description: 'Filter by message type',\n },\n since: { type: 'number', description: 'Get messages since timestamp' },\n },\n required: ['agent'],\n },\n },\n {\n name: 'get_hub_status',\n description: 'Get overview of hub activity, agents, and collaboration opportunities',\n inputSchema: {\n type: 'object',\n properties: {},\n },\n },\n {\n name: 'create_feature',\n description: 'Create a new feature for multi-agent collaboration',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string', description: 'Feature name (will be converted to kebab-case ID)' },\n title: { type: 'string', description: 'Human-readable feature title' },\n description: { type: 'string', description: 'Detailed feature requirements and context' },\n priority: {\n type: 'string',\n enum: Object.values(FeaturePriority),\n description: 'Feature priority level',\n default: FeaturePriority.NORMAL,\n },\n estimatedAgents: {\n type: 'array',\n items: { type: 'string' },\n description: 'Agents expected to be needed for this feature',\n default: [],\n },\n createdBy: { type: 'string', description: 'Agent creating this feature' },\n },\n required: ['name', 'title', 'description', 'createdBy'],\n },\n },\n {\n name: 'create_task',\n description: 'Create a task within a feature with agent delegations',\n inputSchema: {\n type: 'object',\n properties: {\n featureId: { type: 'string', description: 'Feature ID to create task in' },\n title: { type: 'string', description: 'Task title' },\n description: { type: 'string', description: 'Detailed task requirements' },\n delegations: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n agent: { type: 'string', description: 'Agent ID to delegate to' },\n scope: { type: 'string', description: 'What this agent should accomplish' },\n },\n required: ['agent', 'scope'],\n },\n description: 'Agent delegations for this task',\n },\n createdBy: { type: 'string', description: 'Agent creating this task' },\n },\n required: ['featureId', 'title', 'description', 'delegations', 'createdBy'],\n },\n },\n {\n name: 'create_subtask',\n description: 'Create implementation subtasks within a delegation',\n inputSchema: {\n type: 'object',\n properties: {\n featureId: { type: 'string', description: 'Feature ID' },\n delegationId: { type: 'string', description: 'Delegation ID to create subtasks for' },\n subtasks: {\n type: 'array',\n items: {\n type: 'object',\n properties: {\n title: { type: 'string', description: 'Subtask title' },\n description: { type: 'string', description: 'Subtask description' },\n dependsOn: {\n type: 'array',\n items: { type: 'string' },\n description: 'Subtask IDs this depends on',\n default: [],\n },\n },\n required: ['title'],\n },\n description: 'Subtasks to create',\n },\n createdBy: { type: 'string', description: 'Agent creating these subtasks' },\n },\n required: ['featureId', 'delegationId', 'subtasks', 'createdBy'],\n },\n },\n {\n name: 'get_features',\n description: 'Get list of features with optional filtering',\n inputSchema: {\n type: 'object',\n properties: {\n status: {\n type: 'string',\n enum: Object.values(FeatureStatus),\n description: 'Filter by feature status',\n },\n priority: {\n type: 'string',\n enum: Object.values(FeaturePriority),\n description: 'Filter by feature priority',\n },\n agent: { type: 'string', description: 'Filter features assigned to this agent' },\n createdBy: { type: 'string', description: 'Filter features created by this agent' },\n },\n },\n },\n {\n name: 'get_feature',\n description: 'Get complete feature data including tasks, delegations, and subtasks',\n inputSchema: {\n type: 'object',\n properties: {\n featureId: { type: 'string', description: 'Feature ID to retrieve' },\n },\n required: ['featureId'],\n },\n },\n {\n name: 'accept_delegation',\n description: 'Accept a delegation assigned to an agent',\n inputSchema: {\n type: 'object',\n properties: {\n featureId: { type: 'string', description: 'Feature ID' },\n delegationId: { type: 'string', description: 'Delegation ID to accept' },\n agentId: { type: 'string', description: 'Agent accepting the delegation' },\n },\n required: ['featureId', 'delegationId', 'agentId'],\n },\n },\n {\n name: 'update_subtask',\n description: 'Update subtask status and provide output/context',\n inputSchema: {\n type: 'object',\n properties: {\n featureId: { type: 'string', description: 'Feature ID' },\n subtaskId: { type: 'string', description: 'Subtask ID to update' },\n status: {\n type: 'string',\n enum: ['todo', 'in-progress', 'completed', 'blocked'],\n description: 'New subtask status',\n },\n output: { type: 'string', description: 'Output or context for other agents' },\n blockedReason: { type: 'string', description: 'Reason if status is blocked' },\n updatedBy: { type: 'string', description: 'Agent updating this subtask' },\n },\n required: ['featureId', 'subtaskId', 'updatedBy'],\n },\n },\n {\n name: 'sync',\n description: 'Comprehensive sync with the hub - get messages, workload, and status in one call',\n inputSchema: {\n type: 'object',\n properties: {\n agentId: { type: 'string', description: 'Agent ID to sync for' },\n markAsRead: {\n type: 'boolean',\n description: 'Mark retrieved messages as read',\n default: true,\n },\n },\n required: ['agentId'],\n },\n },\n];\n","import path from 'path';\n\nimport { createAgentFromProjectPath } from '~/agents/detection';\nimport { AgentService } from '~/agents/service';\nimport { AgentSession } from '~/agents/session';\nimport { FeaturesHandler } from '~/features/handlers';\nimport { createMessageHandlers } from '~/messaging/handlers';\nimport { MessageService } from '~/messaging/service';\nimport { validateToolInput } from '~/validation';\n\nimport {\n AcceptDelegationInput,\n AgentRegistration,\n CreateFeatureInput,\n CreateSubtaskInput,\n CreateTaskInput,\n GetFeatureInput,\n GetFeaturesInput,\n GetMessagesInput,\n RegisterAgentInput,\n SendMessageInput,\n StorageAdapter,\n SyncErrorResult,\n SyncInput,\n SyncResult,\n UpdateSubtaskInput,\n} from '~/types';\n\nexport interface ToolHandlerServices {\n agentService: AgentService;\n broadcastNotification: (method: string, params: unknown) => Promise<void>;\n getCurrentSession: () => AgentSession | undefined;\n messageService: MessageService;\n sendNotificationToAgent: (agentId: string, method: string, params: unknown) => Promise<void>;\n sendResourceNotification?: (agentId: string, uri: string) => Promise<void>;\n storage: StorageAdapter;\n}\n\nexport function createToolHandlers(services: ToolHandlerServices) {\n const messageHandlers = createMessageHandlers(\n services.messageService,\n services.storage,\n services.sendNotificationToAgent,\n services.sendResourceNotification,\n );\n const featuresHandler = new FeaturesHandler(services.storage);\n\n return {\n async send_message(arguments_: SendMessageInput) {\n const validatedArguments = validateToolInput('send_message', arguments_);\n\n // Instant notification is now handled directly in the message handler\n return messageHandlers.send_message(validatedArguments);\n },\n\n async get_messages(arguments_: GetMessagesInput) {\n const validatedArguments = validateToolInput('get_messages', arguments_);\n\n return messageHandlers.get_messages(validatedArguments);\n },\n\n async register_agent(arguments_: RegisterAgentInput) {\n const validatedArguments = validateToolInput('register_agent', arguments_);\n const currentSession = services.getCurrentSession();\n let agent: AgentRegistration;\n let isExistingAgent = false;\n\n const { projectPath } = validatedArguments;\n\n // Determine the agent ID that would be used\n const proposedAgentId = validatedArguments.id\n ? validatedArguments.id\n : path.basename(projectPath);\n\n // Check for conflicts: existing agent ID with different project path\n if (validatedArguments.id) {\n const existingAgentById = await services.storage.findAgentById(proposedAgentId);\n\n if (existingAgentById && existingAgentById.projectPath !== projectPath) {\n return {\n success: false,\n error: 'AGENT_ID_CONFLICT',\n message: `āŒ Agent ID '${proposedAgentId}' is alread