UNPKG

@the_cfdude/productboard-mcp

Version:

Model Context Protocol server for Productboard REST API with dynamic tool loading

297 lines (250 loc) โ€ข 12.4 kB
#!/usr/bin/env node /** * PRODUCTBOARD MCP SERVER - DATA WRAPPER COMPATIBILITY TEST * * Comprehensive systematic testing of ALL POST/PUT/PATCH tools for Productboard API data wrapper requirement. * This script should be run after any tool updates or additions to ensure API compatibility. * * Usage: npm run test-data-wrapper * * Follows documented strategy with proper test hierarchy and cleanup. * Automatically identifies tools that need { data: body } wrapper fix. */ const testResults = []; const createdItems = []; // Test tools organized by category for systematic testing const testSuite = { // Creation tools (POST) - highest priority creation: [ { tool: 'create_note', handler: 'handleNotesTool', params: { title: 'TEST-CLEANUP: Note', content: 'Testing data wrapper requirement' } }, { tool: 'create_company', handler: 'handleCompaniesTool', params: { name: 'TEST-CLEANUP: Company' } }, { tool: 'create_user', handler: 'handleCompaniesTool', params: { email: 'test-cleanup@example.com', name: 'TEST CLEANUP User' } }, { tool: 'create_objective', handler: 'handleObjectivesTool', params: { name: 'TEST-CLEANUP: Objective' } }, { tool: 'create_initiative', handler: 'handleObjectivesTool', params: { name: 'TEST-CLEANUP: Initiative' } }, { tool: 'create_key_result', handler: 'handleObjectivesTool', params: { name: 'TEST-CLEANUP: Key Result', type: 'number', targetValue: 100, objectiveId: 'test-obj-id' } }, { tool: 'create_release_group', handler: 'handleReleasesTool', params: { name: 'TEST-CLEANUP: Release Group' } }, { tool: 'create_release', handler: 'handleReleasesTool', params: { name: 'TEST-CLEANUP: Release', releaseGroupId: 'test-group-id' } }, { tool: 'create_webhook', handler: 'handleWebhooksTool', params: { url: 'https://webhook.site/test-cleanup', events: ['note.created'] } }, ], // Update tools (PATCH) - medium priority updates: [ { tool: 'update_note', handler: 'handleNotesTool', params: { id: 'test-note-id', title: 'Updated Note' } }, { tool: 'update_company', handler: 'handleCompaniesTool', params: { id: 'test-company-id', name: 'Updated Company' } }, { tool: 'update_user', handler: 'handleCompaniesTool', params: { id: 'test-user-id', name: 'Updated User' } }, { tool: 'update_feature', handler: 'handleFeaturesTool', params: { id: 'test-feature-id', name: 'Updated Feature' } }, { tool: 'update_component', handler: 'handleFeaturesTool', params: { id: 'test-component-id', name: 'Updated Component' } }, { tool: 'update_product', handler: 'handleFeaturesTool', params: { id: 'test-product-id', name: 'Updated Product' } }, { tool: 'update_objective', handler: 'handleObjectivesTool', params: { id: 'test-objective-id', name: 'Updated Objective' } }, { tool: 'update_initiative', handler: 'handleObjectivesTool', params: { id: 'test-initiative-id', name: 'Updated Initiative' } }, { tool: 'update_key_result', handler: 'handleObjectivesTool', params: { id: 'test-key-result-id', name: 'Updated Key Result' } }, { tool: 'update_release_group', handler: 'handleReleasesTool', params: { id: 'test-release-group-id', name: 'Updated Release Group' } }, { tool: 'update_release', handler: 'handleReleasesTool', params: { id: 'test-release-id', name: 'Updated Release' } }, ], // Special operations (POST/PUT) - lower priority special: [ { tool: 'bulk_add_note_followers', handler: 'handleNotesTool', params: { noteId: 'test-note-id', body: { emails: ['test@example.com'] } } }, { tool: 'create_note_tag', handler: 'handleNotesTool', params: { noteId: 'test-note-id', tagName: 'test-tag' } }, { tool: 'create_link', handler: 'handleNotesTool', params: { noteId: 'test-note-id', entityId: 'test-entity-id' } }, { tool: 'submit_feedback_form', handler: 'handleNotesTool', params: { body: { title: 'Test Feedback' } } }, { tool: 'set_custom_field_value', handler: 'handleCustomFieldsTool', params: { 'customField.id': 'test-field-id', 'hierarchyEntity.id': 'test-entity-id', body: { type: 'text', value: 'test' } } }, ] }; async function runComprehensiveTest() { console.log('๐Ÿงช COMPREHENSIVE SYSTEMATIC TESTING - Following Documented Strategy'); console.log('=' .repeat(80)); console.log('Testing ALL POST/PUT/PATCH tools for Productboard API data wrapper requirement\n'); // Test creation tools first (highest priority) console.log('๐Ÿ“ TESTING CREATION TOOLS (POST) - High Priority'); console.log('-' .repeat(50)); for (const test of testSuite.creation) { await testTool(test.tool, test.handler, test.params); } console.log('\n๐Ÿ”„ TESTING UPDATE TOOLS (PATCH) - Medium Priority'); console.log('-' .repeat(50)); for (const test of testSuite.updates) { await testTool(test.tool, test.handler, test.params); } console.log('\nโš™๏ธ TESTING SPECIAL OPERATIONS (POST/PUT) - Lower Priority'); console.log('-' .repeat(50)); for (const test of testSuite.special) { await testTool(test.tool, test.handler, test.params); } // Print comprehensive results printComprehensiveResults(); // Cleanup any successfully created items await cleanupTestItems(); } async function testTool(toolName, handlerModule, testParams) { try { console.log(`๐Ÿงช Testing ${toolName}...`); // Dynamic import of the handler module const module = await import(`./build/tools/${handlerModule.replace('handle', '').replace('Tool', '').toLowerCase()}.js`); const handler = module[handlerModule]; if (!handler) { console.log(`โŒ ${toolName}: HANDLER NOT FOUND`); testResults.push({ tool: toolName, status: 'HANDLER_NOT_FOUND', category: getCategoryForTool(toolName) }); return; } const result = await handler(toolName, testParams); console.log(`โœ… ${toolName}: SUCCESS`); testResults.push({ tool: toolName, status: 'SUCCESS', category: getCategoryForTool(toolName) }); // Try to extract created item ID for cleanup (only for creation tools) if (toolName.startsWith('create_')) { try { const responseData = JSON.parse(result.content[0].text); const id = extractIdFromResponse(responseData, toolName); if (id) { createdItems.push({ type: toolName.replace('create_', ''), id, deleteFunction: getDeleteFunction(toolName) }); console.log(`๐Ÿ“ Tracked ${toolName.replace('create_', '')} ID ${id} for cleanup`); } } catch (e) { console.log(`โš ๏ธ Could not extract ID for cleanup from ${toolName}`); } } } catch (error) { const isSchemaError = error.message.includes('properties which are not allowed by the schema'); const isDataMissingError = error.message.includes('Object has missing required properties ([\"data\"])'); if (isSchemaError || isDataMissingError) { console.log(`โŒ ${toolName}: SCHEMA ERROR - NEEDS DATA WRAPPER FIX`); testResults.push({ tool: toolName, status: 'NEEDS_DATA_WRAPPER', category: getCategoryForTool(toolName), error: error.message.substring(0, 100) + '...' }); } else { console.log(`โŒ ${toolName}: API ERROR`); testResults.push({ tool: toolName, status: 'API_ERROR', category: getCategoryForTool(toolName), error: error.message.substring(0, 100) + '...' }); } } // Small delay to prevent overwhelming the API await new Promise(resolve => setTimeout(resolve, 100)); } function getCategoryForTool(toolName) { if (testSuite.creation.some(t => t.tool === toolName)) return 'creation'; if (testSuite.updates.some(t => t.tool === toolName)) return 'updates'; if (testSuite.special.some(t => t.tool === toolName)) return 'special'; return 'unknown'; } function extractIdFromResponse(responseData, toolName) { // Extract ID based on tool type const toolType = toolName.replace('create_', ''); if (responseData[toolType]?.id) return responseData[toolType].id; if (responseData.data?.id) return responseData.data.id; if (responseData.id) return responseData.id; return null; } function getDeleteFunction(createToolName) { const deleteMap = { 'create_note': 'delete_note', 'create_company': 'delete_company', 'create_user': 'delete_user', 'create_objective': 'delete_objective', 'create_initiative': 'delete_initiative', 'create_key_result': 'delete_key_result', 'create_release_group': 'delete_release_group', 'create_release': 'delete_release', 'create_webhook': 'delete_webhook', }; return deleteMap[createToolName] || null; } function printComprehensiveResults() { console.log('\n๐Ÿ“Š COMPREHENSIVE TEST RESULTS SUMMARY'); console.log('=' .repeat(80)); const needsDataWrapper = testResults.filter(r => r.status === 'NEEDS_DATA_WRAPPER'); const successful = testResults.filter(r => r.status === 'SUCCESS'); const apiErrors = testResults.filter(r => r.status === 'API_ERROR'); const handlerErrors = testResults.filter(r => r.status === 'HANDLER_NOT_FOUND'); console.log(`โœ… Successful: ${successful.length}`); console.log(`๐Ÿšจ Need Data Wrapper Fix: ${needsDataWrapper.length}`); console.log(`โš ๏ธ API Errors: ${apiErrors.length}`); console.log(`โ“ Handler Not Found: ${handlerErrors.length}`); console.log(`๐Ÿ“Š Total Tested: ${testResults.length}`); console.log(''); // Group by category const categories = ['creation', 'updates', 'special']; categories.forEach(category => { const categoryResults = testResults.filter(r => r.category === category); const categoryWrapper = categoryResults.filter(r => r.status === 'NEEDS_DATA_WRAPPER'); console.log(`๐Ÿ“ ${category.toUpperCase()} TOOLS:`); console.log(` Total: ${categoryResults.length}, Need Data Wrapper: ${categoryWrapper.length}`); if (categoryWrapper.length > 0) { categoryWrapper.forEach(result => { console.log(` ๐Ÿšจ ${result.tool}`); }); } console.log(''); }); if (needsDataWrapper.length > 0) { console.log('๐Ÿšจ ALL TOOLS THAT NEED DATA WRAPPER FIX:'); needsDataWrapper.forEach(result => { console.log(` - ${result.tool} (${result.category})`); }); console.log(''); } console.log(`๐Ÿ“ Created ${createdItems.length} test items for cleanup`); console.log(''); } async function cleanupTestItems() { if (createdItems.length === 0) { console.log('โœ… No test items to clean up'); return; } console.log(`๐Ÿงน Cleaning up ${createdItems.length} test items...`); for (const item of createdItems) { if (!item.deleteFunction) { console.log(`โš ๏ธ No delete function for ${item.type} ${item.id}`); continue; } try { console.log(`๐Ÿ—‘๏ธ Deleting ${item.type} ID: ${item.id}`); // Dynamic import and execution of delete function const handlerModule = getHandlerModuleForDelete(item.deleteFunction); const module = await import(`./build/tools/${handlerModule}.js`); const handler = module[getHandlerFunctionName(handlerModule)]; await handler(item.deleteFunction, { id: item.id }); console.log(`โœ… Deleted ${item.type} ${item.id}`); } catch (error) { console.log(`โŒ Failed to delete ${item.type} ${item.id}: ${error.message.substring(0, 80)}...`); } } console.log('๐Ÿงน Cleanup completed'); } function getHandlerModuleForDelete(deleteFunction) { const moduleMap = { 'delete_note': 'notes', 'delete_company': 'companies', 'delete_user': 'companies', 'delete_objective': 'objectives', 'delete_initiative': 'objectives', 'delete_key_result': 'objectives', 'delete_release_group': 'releases', 'delete_release': 'releases', 'delete_webhook': 'webhooks', }; return moduleMap[deleteFunction] || 'unknown'; } function getHandlerFunctionName(moduleName) { const handlerMap = { 'notes': 'handleNotesTool', 'companies': 'handleCompaniesTool', 'objectives': 'handleObjectivesTool', 'releases': 'handleReleasesTool', 'webhooks': 'handleWebhooksTool', }; return handlerMap[moduleName] || 'handleUnknownTool'; } // Run the comprehensive test runComprehensiveTest().catch(error => { console.error('Comprehensive test execution failed:', error); process.exit(1); });