UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

519 lines (462 loc) 14.3 kB
import { ToolResult } from './types.js'; /** * MCP Response format expected by the SDK */ export interface MCPResponse { [key: string]: unknown; content: Array<{ type: string; text: string; }>; structured?: any; } /** * Simplified MCP Tool Response Schema */ export interface MCPToolResponse<T = any> { status: 'success' | 'error' | 'partial'; message: string; data?: T; metadata?: { operation: string; timestamp: string; dataType?: string; relatedTools?: string[]; }; error?: { code: string; category: 'validation' | 'system' | 'permission' | 'resource' | 'execution'; details?: any; suggestions?: string[]; }; warnings?: string[]; } /** * Convert our internal ToolResult format to MCP SDK format with new schema */ export function convertToolResultToMCP<T>(result: ToolResult<T>, operation?: string): MCPResponse { // Check if data already follows the new MCPToolResponse format if (result.success && result.data && typeof result.data === 'object' && 'status' in result.data && 'message' in result.data) { // Data is already in MCPToolResponse format const response = result.data as MCPToolResponse; return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ], structured: response }; } // Convert legacy format to new schema const timestamp = new Date().toISOString(); if (result.success) { const response: MCPToolResponse<T> = { status: 'success', message: formatSuccessMessage(result.data, operation), data: result.data, metadata: { operation: operation || 'unknown', timestamp, ...getOperationMetadata(operation, result.data) } }; return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ], structured: response }; } else { const response: MCPToolResponse = { status: 'error', message: result.error?.message || 'An unknown error occurred', error: { code: result.error?.code || 'UNKNOWN_ERROR', category: result.error?.category || 'system', details: result.error?.details, suggestions: result.error?.suggestions }, metadata: { operation: operation || 'unknown', timestamp } }; return { content: [ { type: 'text', text: JSON.stringify(response, null, 2) } ], structured: response }; } } /** * Format success data as a readable string */ function formatSuccessMessage(data: any, operation?: string): string { if (data === null || data === undefined) { return '✅ Operation completed successfully'; } if (typeof data === 'string') { return data; } if (typeof data === 'number' || typeof data === 'boolean') { return String(data); } // Check if data has a message property for better formatting if (data && typeof data === 'object' && 'message' in data) { return data.message; } // For complex objects, provide a summary instead of full JSON if (data && typeof data === 'object') { const summary = formatDataSummary(data, operation); if (summary) { return summary; } } // For objects and arrays, pretty print as JSON try { return JSON.stringify(data, null, 2); } catch (error) { return '✅ Operation completed successfully (data available in structured field)'; } } /** * Format complex data as a human-readable summary */ function formatDataSummary(data: any, operation?: string): string | null { // Handle list responses if (data.stories && Array.isArray(data.stories)) { const count = data.stories.length; const summary = data.summary; return `✅ Retrieved ${count} user ${count === 1 ? 'story' : 'stories'}${ summary ? `\n\nSummary:\n${JSON.stringify(summary, null, 2)}` : '' }`; } if (data.sprints && Array.isArray(data.sprints)) { return `✅ Retrieved ${data.sprints.length} sprint(s)`; } if (data.epics && Array.isArray(data.epics)) { return `✅ Retrieved ${data.epics.length} epic(s)`; } if (data.boards && Array.isArray(data.boards)) { return `✅ Retrieved ${data.boards.length} kanban board(s)`; } if (data.tasks && Array.isArray(data.tasks)) { return `✅ Retrieved ${data.tasks.length} task(s)`; } // Handle single entity responses based on operation if (data.sprint && data.sprint.id) { const verb = getOperationVerb(operation); return `✅ ${verb} sprint "${data.sprint.name}" (ID: ${data.sprint.id})${ data.nextSteps ? '\n\nNext steps:\n' + data.nextSteps.map((s: string) => `• ${s}`).join('\n') : '' }`; } if (data.story && data.story.id) { const verb = getOperationVerb(operation); return `✅ ${verb} user story "${data.story.title}" (ID: ${data.story.id})${ data.nextSteps ? '\n\nNext steps:\n' + data.nextSteps.map((s: string) => `• ${s}`).join('\n') : '' }`; } if (data.epic && data.epic.id) { const verb = getOperationVerb(operation); return `✅ ${verb} epic "${data.epic.title}" (ID: ${data.epic.id})`; } // Handle board responses if (data.board && data.board.id) { const verb = getOperationVerb(operation); return `✅ ${verb} kanban board "${data.board.name}" (ID: ${data.board.id})`; } // Handle task responses if (data.task && data.task.id) { const verb = getOperationVerb(operation); return `✅ ${verb} task "${data.task.title}" (ID: ${data.task.id})`; } // Handle specific agile operations if (operation === 'update_story_status' && data.oldStatus && data.newStatus) { return `✅ Updated story status from "${data.oldStatus}" to "${data.newStatus}"${ data.storyId ? ` (ID: ${data.storyId})` : '' }`; } if (operation === 'add_story_to_sprint' && data.message) { return `✅ ${data.message}`; } if (operation === 'assign_story' && data.storyId && data.assignee) { return `✅ Assigned story to ${data.assignee}${data.storyTitle ? ` - "${data.storyTitle}"` : ''}`; } if (operation === 'block_story' && data.blocker) { return `✅ Blocked story "${data.blocker.storyTitle || data.blocker.storyId}" - ${data.blocker.reason}`; } if (operation === 'complete_sprint' && data.sprint) { return `✅ Completed sprint "${data.sprint.name}" with ${data.completedStories || 0} stories done`; } // Handle generic update responses if (data.updated && typeof data.updated === 'object') { const type = data.updated.type || 'item'; const id = data.updated.id; return `✅ Updated ${type}${id ? ` (ID: ${id})` : ''}`; } // Handle update responses with entity data if (data.updated === true && (data.story || data.epic || data.sprint || data.task || data.board)) { const entity = data.story || data.epic || data.sprint || data.task || data.board; const entityType = data.story ? 'story' : data.epic ? 'epic' : data.sprint ? 'sprint' : data.task ? 'task' : 'board'; const title = entity.title || entity.name || entity.id; return `✅ Updated ${entityType} "${title}"${entity.id ? ` (ID: ${entity.id})` : ''}`; } // Handle deletion responses if (data.deleted) { return `✅ Deleted successfully`; } // Handle other responses with a result count const keys = Object.keys(data); if (keys.length === 1 && Array.isArray(data[keys[0]])) { const items = data[keys[0]]; return `✅ Retrieved ${items.length} ${keys[0]}`; } return null; } /** * Format error information as a readable string */ function formatErrorMessage(error?: any): string { if (!error) { return '❌ An unknown error occurred'; } const parts: string[] = []; // Add main error message parts.push(`❌ Error: ${error.message || 'Unknown error'}`); // Add error code if available if (error.code) { parts.push(`Code: ${error.code}`); } // Add category if available if (error.category) { parts.push(`Category: ${error.category}`); } // Add suggestions if available if (error.suggestions && Array.isArray(error.suggestions) && error.suggestions.length > 0) { parts.push('\nSuggestions:'); error.suggestions.forEach((suggestion: string, index: number) => { parts.push(`${index + 1}. ${suggestion}`); }); } return parts.join('\n'); } /** * Create a simple MCP response with just text */ export function createMCPTextResponse(text: string): MCPResponse { return { content: [ { type: 'text', text: text } ] }; } /** * Create an MCP error response */ export function createMCPErrorResponse(message: string, details?: any): MCPResponse { return { content: [ { type: 'text', text: `❌ Error: ${message}` } ], structured: details }; } /** * Helper to create a standardized success response */ export function createMCPSuccessResponse<T>( message: string, data: T, metadata: { operation: string; dataType?: string; relatedTools?: string[]; } ): MCPToolResponse<T> { return { status: 'success', message, data, metadata: { ...metadata, timestamp: new Date().toISOString() } }; } /** * Helper to create a standardized error response */ export function createMCPErrorResponseV2( message: string, error: { code: string; category: 'validation' | 'system' | 'permission' | 'resource' | 'execution'; details?: any; suggestions?: string[]; }, operation: string ): MCPToolResponse { return { status: 'error', message, error, metadata: { operation, timestamp: new Date().toISOString() } }; } /** * Get appropriate verb for an operation */ function getOperationVerb(operation?: string): string { if (!operation) return 'Processed'; // Check for specific operation types if (operation.startsWith('create_')) return 'Created'; if (operation.startsWith('get_')) return 'Retrieved'; if (operation.startsWith('list_')) return 'Listed'; if (operation.startsWith('update_')) return 'Updated'; if (operation.startsWith('delete_')) return 'Deleted'; if (operation.startsWith('add_')) return 'Added'; if (operation.startsWith('remove_')) return 'Removed'; if (operation.startsWith('move_')) return 'Moved'; if (operation.startsWith('assign_')) return 'Assigned'; if (operation.startsWith('complete_')) return 'Completed'; if (operation.startsWith('close_')) return 'Closed'; if (operation.startsWith('block_')) return 'Blocked'; if (operation.startsWith('split_')) return 'Split'; if (operation.startsWith('link_')) return 'Linked'; if (operation.startsWith('generate_')) return 'Generated'; if (operation.startsWith('validate_')) return 'Validated'; if (operation.startsWith('analyze_')) return 'Analyzed'; // Default return 'Processed'; } /** * Get operation-specific metadata based on tool name and data */ function getOperationMetadata(operation?: string, data?: any): { dataType?: string; relatedTools?: string[] } { if (!operation) return {}; // Agile management tools if (operation.includes('sprint')) { if (operation === 'create_agile_sprint') { return { dataType: 'sprint', relatedTools: ['add_story_to_sprint', 'list_stories', 'get_sprint_stats'] }; } if (operation === 'list_agile_sprints') { return { dataType: 'sprint_list', relatedTools: ['get_sprint', 'create_agile_sprint', 'get_sprint_stats'] }; } } if (operation.includes('story')) { if (operation === 'create_user_story') { return { dataType: 'story', relatedTools: ['add_story_to_sprint', 'update_story_status', 'list_stories'] }; } if (operation === 'list_stories') { return { dataType: 'story_list', relatedTools: ['create_user_story', 'get_story', 'update_story_status'] }; } } if (operation.includes('epic')) { if (operation === 'create_epic') { return { dataType: 'epic', relatedTools: ['list_epics', 'create_user_story', 'update_epic'] }; } if (operation === 'list_epics') { return { dataType: 'epic_list', relatedTools: ['create_epic', 'get_epic', 'update_epic'] }; } } // Task management tools if (operation.includes('board')) { if (operation === 'create_kanban_board') { return { dataType: 'board', relatedTools: ['create_kanban_task', 'get_kanban_board', 'list_boards'] }; } if (operation === 'list_boards') { return { dataType: 'board_list', relatedTools: ['create_kanban_board', 'get_kanban_board', 'get_kanban_analytics'] }; } } if (operation.includes('task')) { if (operation === 'create_kanban_task') { return { dataType: 'task', relatedTools: ['move_kanban_task', 'update_task', 'get_tasks'] }; } if (operation === 'get_tasks' || operation === 'list_tasks') { return { dataType: 'task_list', relatedTools: ['create_kanban_task', 'update_task', 'move_kanban_task'] }; } } // Memory management if (operation.includes('memory')) { if (operation === 'store_memory') { return { dataType: 'memory', relatedTools: ['search_memories', 'get_memory', 'get_memory_stats'] }; } if (operation === 'search_memories') { return { dataType: 'memory_list', relatedTools: ['store_memory', 'get_memory', 'get_memory_stats'] }; } } // Business management if (operation.includes('business')) { if (operation === 'generate_business_plan') { return { dataType: 'business_plan', relatedTools: ['analyze_market', 'competitor_research', 'financial_projections'] }; } if (operation === 'perform_business_review') { return { dataType: 'business_review', relatedTools: ['track_startup_metrics', 'generate_business_plan', 'startup_assessment'] }; } } // Default metadata return { dataType: 'generic_response' }; }