@the_cfdude/productboard-mcp
Version:
Model Context Protocol server for Productboard REST API with dynamic tool loading
297 lines (250 loc) โข 12.4 kB
JavaScript
/**
* 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);
});