Jira Insights MCP

by aaronsb
Verified
# Jira Insights MCP Implementation Plan This document provides a detailed implementation plan for the high-priority improvements to the Jira Insights MCP. ## 1. Enhanced Error Handling ### Create Error Handling Utility (`src/utils/error-handler.ts`) ```typescript import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { ToolResponse } from '../types/index.js'; // Common error types for standardization export enum JiraInsightsErrorType { VALIDATION_ERROR = 'VALIDATION_ERROR', API_ERROR = 'API_ERROR', NOT_FOUND = 'NOT_FOUND', PERMISSION_ERROR = 'PERMISSION_ERROR', SCHEMA_ERROR = 'SCHEMA_ERROR', QUERY_ERROR = 'QUERY_ERROR', } // Error handling utility export function handleError( error: any, operation: string, context?: Record<string, any> ): ToolResponse { console.error(`Error in ${operation}:`, error); // Determine error type const errorType = determineErrorType(error); // Get suggested fix based on error type and context const suggestedFix = getSuggestedFix(errorType, error, operation, context); // Get examples for the operation const examples = getExamplesForOperation(operation, context); return { content: [ { type: 'text', text: JSON.stringify({ error: `Failed to perform operation: ${operation}`, errorType, message: (error as Error).message, details: error.response?.data || 'No additional details available', suggestedFix, examples, context: context || {}, }, null, 2), }, ], isError: true, }; } // Determine the type of error function determineErrorType(error: any): JiraInsightsErrorType { if (error.response?.status === 400) { return JiraInsightsErrorType.VALIDATION_ERROR; } if (error.response?.status === 404) { return JiraInsightsErrorType.NOT_FOUND; } if (error.response?.status === 403) { return JiraInsightsErrorType.PERMISSION_ERROR; } if (error.message?.includes('AQL')) { return JiraInsightsErrorType.QUERY_ERROR; } if (error.message?.includes('schema')) { return JiraInsightsErrorType.SCHEMA_ERROR; } return JiraInsightsErrorType.API_ERROR; } // Get suggested fix based on error type function getSuggestedFix( errorType: JiraInsightsErrorType, error: any, operation: string, context?: Record<string, any> ): string { switch (errorType) { case JiraInsightsErrorType.VALIDATION_ERROR: return getValidationErrorFix(error, operation, context); case JiraInsightsErrorType.QUERY_ERROR: return getQueryErrorFix(error, context); case JiraInsightsErrorType.NOT_FOUND: return getNotFoundErrorFix(error, operation, context); case JiraInsightsErrorType.PERMISSION_ERROR: return 'Ensure you have the necessary permissions to perform this operation.'; case JiraInsightsErrorType.SCHEMA_ERROR: return 'Check that the schema exists and is properly configured.'; default: return 'Try again with modified parameters or contact support if the issue persists.'; } } // Get fix for validation errors function getValidationErrorFix( error: any, operation: string, context?: Record<string, any> ): string { const errorMessage = error.message || ''; if (operation === 'query' && context?.aql) { return `Check your AQL syntax. Ensure object types and attributes exist. Try a simpler query first like: objectType = "Application"`; } if (errorMessage.includes('required')) { return 'Ensure all required parameters are provided.'; } if (errorMessage.includes('format')) { return 'Check the format of your parameters.'; } return 'Validate your input parameters against the API documentation.'; } // Get fix for query errors function getQueryErrorFix( error: any, context?: Record<string, any> ): string { const aql = context?.aql || ''; if (aql.includes('==')) { return 'Note: == is case-sensitive equality, = is case-insensitive. Try using = instead.'; } if (!aql.includes('=') && !aql.includes('>') && !aql.includes('<') && !aql.includes('like') && !aql.includes('in')) { return 'Your query is missing a comparison operator (=, >, <, like, in, etc.). Try a basic query like: objectType = "Application"'; } if (aql.includes('"') && aql.includes("'")) { return 'Mixing quote types can cause issues. Stick to one quote type (preferably double quotes).'; } return 'Check your AQL syntax. Ensure object types and attributes exist. Try a simpler query first.'; } // Get fix for not found errors function getNotFoundErrorFix( error: any, operation: string, context?: Record<string, any> ): string { if (operation === 'get' && context?.objectTypeId) { return `Object type with ID ${context.objectTypeId} not found. Verify the ID is correct.`; } if (operation === 'get' && context?.objectId) { return `Object with ID ${context.objectId} not found. Verify the ID is correct.`; } if (operation === 'get' && context?.schemaId) { return `Schema with ID ${context.schemaId} not found. Verify the ID is correct.`; } return 'The requested resource was not found. Verify all IDs are correct.'; } // Get examples for the operation function getExamplesForOperation( operation: string, context?: Record<string, any> ): Record<string, any>[] { switch (operation) { case 'query': return [ { aql: 'objectType = "Application"', description: 'Find all Applications' }, { aql: 'objectType = "Server" AND "Operating System" like "Linux"', description: 'Find Linux servers' }, { aql: 'objectType = "Database" AND Status = "Production"', description: 'Find production databases' }, ]; case 'get': return [ { description: 'Get object by ID', parameters: { objectId: '123' } }, { description: 'Get object type by ID', parameters: { objectTypeId: '456' } }, { description: 'Get schema by ID', parameters: { schemaId: '789' } }, ]; case 'list': return [ { description: 'List objects of type', parameters: { objectTypeId: '123', maxResults: 10 } }, { description: 'List object types in schema', parameters: { schemaId: '456', maxResults: 20 } }, { description: 'List all schemas', parameters: { maxResults: 50 } }, ]; case 'create': return [ { description: 'Create a new object', parameters: { objectTypeId: '123', name: 'New Object', attributes: { 'attr1': 'value1' } } }, ]; case 'update': return [ { description: 'Update an object', parameters: { objectId: '123', name: 'Updated Object', attributes: { 'attr1': 'new value' } } }, ]; default: return [{ description: 'No examples available for this operation' }]; } } ``` ### Update Handler Files to Use the Error Handler Update all handler files (`schema-handlers.ts`, `object-type-handlers.ts`, `object-handlers.ts`) to use the new error handling utility. ## 2. Fix AQL Query Format Issues ### Create AQL Utility (`src/utils/aql-utils.ts`) ```typescript // AQL query utilities export function validateAqlQuery(aql: string): { isValid: boolean; errors: string[]; suggestions: string[] } { const errors = []; const suggestions = []; // Basic syntax validation if (!aql.includes('=') && !aql.includes('>') && !aql.includes('<') && !aql.includes('like') && !aql.includes('in')) { errors.push('Query missing comparison operator (=, >, <, like, in, etc.)'); suggestions.push('Try a basic query like: objectType = "Application"'); } // Check for common mistakes if (aql.includes('==')) { suggestions.push('Note: == is case-sensitive equality, = is case-insensitive'); } // Check for missing quotes around values with spaces const valueWithSpaceRegex = /=\s*(\w+\s+\w+)(?!\s*")/g; if (valueWithSpaceRegex.test(aql)) { errors.push('Values with spaces must be enclosed in quotes'); suggestions.push('Example: Name = "John Doe" (not Name = John Doe)'); } // Check for proper quoting const quoteCount = (aql.match(/"/g) || []).length; if (quoteCount % 2 !== 0) { errors.push('Unbalanced quotes in query'); suggestions.push('Ensure all opening quotes have matching closing quotes'); } // Check for AND/OR operators if (aql.includes(' and ') || aql.includes(' or ')) { errors.push('Logical operators should be uppercase'); suggestions.push('Use AND instead of and, OR instead of or'); } return { isValid: errors.length === 0, errors, suggestions }; } // Format AQL query for API request export function formatAqlForRequest(aql: string): string { // Trim whitespace let formattedAql = aql.trim(); // Ensure AND/OR operators are uppercase formattedAql = formattedAql.replace(/\s+and\s+/gi, ' AND '); formattedAql = formattedAql.replace(/\s+or\s+/gi, ' OR '); // Ensure spaces around operators formattedAql = formattedAql.replace(/([=<>])/g, ' $1 ').replace(/\s+/g, ' '); // Ensure proper quoting for values with spaces // This is a simplified approach - a more robust solution would use a parser return formattedAql; } // Get example AQL queries for a schema export function getExampleQueriesForSchema(schemaId: string): string[] { const schemaExamples: Record<string, string[]> = { // Application Portfolio schema examples '2': [ 'objectType = "Application"', 'objectType = "Application" AND Status = "Active"', 'objectType = "Application" AND "Business Criticality" = "High"' ], // Engineering Support schema examples '4': [ 'objectType = "Server"', 'objectType = "Server" AND "Operating System" like "Linux"', 'objectType = "Database" AND Status = "Production"' ], // Default examples for any schema 'default': [ 'objectType = "[ObjectTypeName]"', 'objectType = "[ObjectTypeName]" AND [AttributeName] = "[Value]"', 'objectType = "[ObjectTypeName]" AND [AttributeName] like "[Value]"' ] }; return schemaExamples[schemaId] || schemaExamples['default']; } ``` ### Update Object Handlers to Use AQL Utilities Update `object-handlers.ts` to use the new AQL utilities for query validation and formatting. ## 3. Attribute Discovery Enhancement ### Create Attribute Discovery Utility (`src/utils/attribute-utils.ts`) ```typescript import { JiraClient } from '../client/jira-client.js'; // Cache for attributes to improve performance const attributeCache = new Map<string, { attributes: any[]; timestamp: number; }>(); // Cache expiration time (5 minutes) const CACHE_EXPIRATION = 5 * 60 * 1000; /** * Get attributes for an object type with caching * @param jiraClient The Jira client * @param objectTypeId The object type ID * @param forceRefresh Force a refresh of the cache * @returns The attributes for the object type */ export async function getObjectTypeAttributes( jiraClient: JiraClient, objectTypeId: string, forceRefresh = false ): Promise<{ attributes: any[]; count: number; _cached?: boolean; _cachedAt?: string; }> { // Check cache first if not forcing refresh if (!forceRefresh && attributeCache.has(objectTypeId)) { const cached = attributeCache.get(objectTypeId)!; // Check if cache is still valid if (Date.now() - cached.timestamp < CACHE_EXPIRATION) { return { attributes: cached.attributes, count: cached.attributes.length, _cached: true, _cachedAt: new Date(cached.timestamp).toISOString() }; } } try { const assetsApi = await jiraClient.getAssetsApi(); const attributesList = await assetsApi.objectTypeFindAllAttributes({ id: objectTypeId, onlyValueEditable: false, orderByName: false, query: '""', includeValuesExist: false, excludeParentAttributes: false, includeChildren: false, orderByRequired: false }); const attributes = attributesList.values || []; // Update cache attributeCache.set(objectTypeId, { attributes, timestamp: Date.now() }); return { attributes, count: attributes.length, _cached: false }; } catch (error) { console.error(`Error fetching attributes for object type ${objectTypeId}:`, error); throw error; } } /** * Clear the attribute cache * @param objectTypeId Optional object type ID to clear specific cache entry */ export function clearAttributeCache(objectTypeId?: string): void { if (objectTypeId) { attributeCache.delete(objectTypeId); } else { attributeCache.clear(); } } /** * Format attributes for object creation/update * @param attributes The attributes as key-value pairs * @returns Formatted attributes for the API */ export function formatAttributes(attributes: Record<string, any>): any[] { return Object.entries(attributes).map(([key, value]) => ({ objectTypeAttributeId: key, objectAttributeValues: Array.isArray(value) ? value.map(v => ({ value: v })) : [{ value }], })); } ``` ### Update Object Type Handlers to Use Attribute Utilities Update `object-type-handlers.ts` to use the new attribute utilities for attribute discovery. ## 4. Integration and Testing ### Create Test Script (`scripts/test-api.js`) ```javascript // Test script for API operations const { execSync } = require('child_process'); // Configuration const HOST = process.env.JIRA_HOST || 'https://your-domain.atlassian.net'; const EMAIL = process.env.JIRA_EMAIL || 'your-email@example.com'; const API_TOKEN = process.env.JIRA_API_TOKEN || 'your-api-token'; // Test schema operations console.log('Testing schema operations...'); execSync(`JIRA_HOST=${HOST} JIRA_EMAIL=${EMAIL} JIRA_API_TOKEN=${API_TOKEN} node -e " const { initAssetsApiClient } = require('jira-insights-api'); async function testSchemaOperations() { try { const instance = '${HOST}'.match(/https?:\\/\\/([^/]+)/)[1].split('.')[0]; const client = await initAssetsApiClient({ email: '${EMAIL}', apiToken: '${API_TOKEN}', instance }); console.log('Listing schemas...'); const schemas = await client.DefaultService.schemaList({ maxResults: 10 }); console.log(\`Found \${schemas.values.length} schemas\`); if (schemas.values.length > 0) { const schemaId = schemas.values[0].id; console.log(\`Getting schema \${schemaId}...\`); const schema = await client.DefaultService.schemaFind({ id: schemaId }); console.log(\`Schema name: \${schema.name}\`); console.log(\`Listing object types for schema \${schemaId}...\`); const objectTypes = await client.DefaultService.schemaFindAllObjectTypes({ id: schemaId, excludeAbstract: false }); console.log(\`Found \${objectTypes.values.length} object types\`); if (objectTypes.values.length > 0) { const objectTypeId = objectTypes.values[0].id; console.log(\`Getting object type \${objectTypeId}...\`); const objectType = await client.DefaultService.objectTypeFind({ id: objectTypeId }); console.log(\`Object type name: \${objectType.name}\`); console.log(\`Listing attributes for object type \${objectTypeId}...\`); const attributes = await client.DefaultService.objectTypeFindAllAttributes({ id: objectTypeId, onlyValueEditable: false, orderByName: false, query: '\"\"', includeValuesExist: false, excludeParentAttributes: false, includeChildren: false, orderByRequired: false }); console.log(\`Found \${attributes.values.length} attributes\`); console.log('Testing AQL query...'); try { const aql = \`objectType = \${objectTypeId}\`; console.log(\`Query: \${aql}\`); const objects = await client.DefaultService.objectsByAql({ requestBody: { aql }, startAt: 0, maxResults: 10, includeAttributes: true }); console.log(\`Found \${objects.values.length} objects\`); } catch (error) { console.error('AQL query error:', error.message); } } } } catch (error) { console.error('Error:', error); } } testSchemaOperations(); "`, { stdio: 'inherit' }); ``` ### Update API Migration TODO List Update `docs/API_MIGRATION_TODO.md` with the progress made on the improvements.