diff --git a/mcpgateway/templates/admin.html b/mcpgateway/templates/admin.html index 91b53d1d2..98630ba79 100644 --- a/mcpgateway/templates/admin.html +++ b/mcpgateway/templates/admin.html @@ -2704,6 +2704,9 @@

+ + +
+
+ + +
+
+ +
+
+ + + {% endfor %} {% else %} +
+

+ No A2A agents registered yet. +

+
+ {% endif %} @@ -7014,7 +6998,7 @@

name="output_schema" id="edit-tool-output-schema" class="mt-1 block w-full rounded-md border border-gray-300 dark:border-gray-600 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-900 dark:placeholder-gray-300 dark:text-gray-300" - placeholder="{"type": "object", "properties"}" + placeholder="{"type": "object", "properties": {}}" >

Optional JSON Schema for validating structured tool output. Per MCP spec, servers should return structured results that conform to this schema. @@ -7800,7 +7784,6 @@

-
- - -
-
@@ -9635,7 +9599,7 @@

} function updateSectionHeaders(teamId) { - const sections = ['tools', 'resources', 'prompts', 'servers', 'gateways', 'a2a-agents']; + const sections = ['tools', 'resources', 'prompts', 'servers', 'gateways']; sections.forEach(section => { const header = document.querySelector('#' + section + '-section h2'); @@ -9729,27 +9693,6 @@

`; } - else if (sectionType === 'a2a-agents') { - // Build a row matching the table columns for A2A agents - row.innerHTML = ` - ${escapeHtml(item.id)} - -
${escapeHtml(item.name || '')}
-
${escapeHtml(item.description || '')}
-
- ${escapeHtml(item.ownerEmail || '')} - ${escapeHtml(item.team || '')} - ${escapeHtml((item.visibility || 'Private').charAt(0).toUpperCase() + (item.visibility || 'private').slice(1))} -
- - ${escapeHtml(item.endpointUrl || item.endpoint || '')} - ${createTagsSpan(item.tags)} - ${escapeHtml(item.agentType || item.type || '')} - ${createStatusSpan(item.enabled || item.isActive || item.is_active)} - ${createActionButtons(sectionType, item)} - `; - } - return row; } @@ -9769,16 +9712,12 @@

} function createActionButtons(sectionType, item) { - // Map UI sectionType to backend path when needed (a2a-agents -> a2a) - const backendPath = sectionType === 'a2a-agents' ? 'a2a' : sectionType; - - const isActiveFlag = item.isActive || item.enabled || item.is_active; - const activeButton = isActiveFlag ? - ` + const activeButton = item.isActive ? + ` ` : - `
+ `
`; @@ -9792,12 +9731,6 @@

buttons += ``; } else if (sectionType === 'servers') { buttons += ``; - } else if (sectionType === 'a2a-agents') { - // Add A2A-specific Edit and Test buttons - const agentName = item.name || ''; - const endpoint = item.endpointUrl || item.endpoint || ''; - buttons += ``; - buttons += ``; } // Show Edit/Delete buttons - check ownership, admin status, and team owner permissions @@ -9821,25 +9754,15 @@

const editDisabled = canEdit ? '' : 'disabled'; const editTitle = canEdit ? '' : 'title="You can only edit resources you own or resources in teams you manage"'; const editClass = canEdit ? 'text-green-600 hover:text-green-900' : 'text-gray-400 cursor-not-allowed'; - // Edit button: special-case for a2a-agents to call editA2AAgent - let editOnclick; - if (sectionType === 'a2a-agents') { - editOnclick = canEdit ? `onclick="editA2AAgent('${item.id}')"` : 'onclick="return false;"'; - } else { - editOnclick = canEdit ? `onclick="edit${sectionType.charAt(0).toUpperCase() + sectionType.slice(1, -1)}('${item.id}')"` : 'onclick="return false;"'; - } + const editOnclick = canEdit ? `onclick="edit${sectionType.charAt(0).toUpperCase() + sectionType.slice(1, -1)}('${item.id}')"` : 'onclick="return false;"'; buttons += ``; // Delete button const deleteDisabled = canEdit ? '' : 'disabled'; const deleteTitle = canEdit ? '' : 'title="You can only delete resources you own or resources in teams you manage"'; const deleteClass = canEdit ? 'text-red-600 hover:text-red-900' : 'text-gray-400 cursor-not-allowed'; - // Delete: use form POST for backend actions; for a2a map to backendPath - if (canEdit) { - buttons += `
`; - } else { - buttons += ``; - } + const deleteOnclick = canEdit ? `onclick="delete${sectionType.charAt(0).toUpperCase() + sectionType.slice(1, -1)}('${item.id}')"` : 'onclick="return false;"'; + buttons += ``; return buttons; } @@ -10291,7 +10214,7 @@

console.log('Exporting permissions...'); // Collect all current permission data - const sections = ['tools', 'resources', 'prompts', 'servers', 'gateways', 'a2a-agents']; + const sections = ['tools', 'resources', 'prompts', 'servers', 'gateways']; let exportData = { export_date: new Date().toISOString(), permissions: [] @@ -10339,7 +10262,7 @@

function auditPermissions() { console.log('Auditing permissions...'); - const sections = ['tools', 'resources', 'prompts', 'servers', 'gateways', 'a2a-agents']; + const sections = ['tools', 'resources', 'prompts', 'servers', 'gateways']; let auditResults = { total_resources: 0, private_resources: 0, @@ -10457,7 +10380,7 @@

Breakdown by Type:

// Initialize tag suggestions when page loads - using functions from admin.js document.addEventListener('DOMContentLoaded', function() { - ['tools', 'resources', 'prompts', 'servers', 'gateways', 'a2a-agents'].forEach(entityType => { + ['tools', 'resources', 'prompts', 'servers', 'gateways'].forEach(entityType => { // Use the corrected functions from admin.js if (typeof updateAvailableTags === 'function') { updateAvailableTags(entityType); @@ -11661,6 +11584,8 @@

window.performDropdownImport = async function (data) { const importBtn = document.getElementById("dropdown-import-btn"); const status = document.getElementById("dropdown-status"); + const sampleSection = document.querySelector('.mt-3.p-3.bg-gray-50'); + const importResultSection = document.getElementById("dropdown-import-result"); importBtn.disabled = true; importBtn.textContent = "Importing..."; @@ -11679,21 +11604,142 @@

const result = await response.json(); - if (response.ok) { - showDropdownStatus( - "success", - `✅ Successfully imported ${result.imported} tools`, - ); - resetDropdownImport(); - setTimeout(() => { - window.location.reload(); - }, 2000); - } else { - showDropdownStatus( - "error", - `❌ Import failed: ${result.detail || "Unknown error"}`, - ); + // Collapse sample template section + if (sampleSection) { + sampleSection.style.display = 'none'; } + + // Show detailed import results + if (importResultSection) { + importResultSection.classList.remove('hidden'); + + const totalTools = data.length; + const imported = result.imported || 0; + const failed = totalTools - imported; + + let resultHTML = ` +
+
📊 Import Results
+
+
+
${totalTools}
+
Total
+
+
+
${imported}
+
Uploaded
+
+
+
${failed}
+
Failed
+
+
+ `; + + if (response.ok && imported > 0) { + resultHTML += `
✅ Successfully imported ${imported} tool${imported !== 1 ? 's' : ''}
`; + } + + if (failed > 0) { + resultHTML += `
❌ ${failed} tool${failed !== 1 ? 's' : ''} failed to import
`; + + // Show detailed error messages for ALL failed tools in scrollable section + if (result.details && result.details.failed && result.details.failed.length > 0) { + resultHTML += ` +
+
Failed Tools (${result.details.failed.length}):
+
+ `; + result.details.failed.forEach((failedTool, idx) => { + const toolName = failedTool.name || 'Unknown'; + let errorMsg = 'Unknown error'; + + // Extract detailed error message + if (failedTool.error) { + if (typeof failedTool.error === 'string') { + errorMsg = failedTool.error; + } else if (failedTool.error.message) { + errorMsg = failedTool.error.message; + } else if (failedTool.error.detail) { + errorMsg = failedTool.error.detail; + } else if (failedTool.error.errors && Array.isArray(failedTool.error.errors)) { + // Handle validation errors array + errorMsg = failedTool.error.errors.map(err => { + if (typeof err === 'string') return err; + if (err.msg) return `${err.loc ? err.loc.join('.') + ': ' : ''}${err.msg}`; + if (err.message) return err.message; + return JSON.stringify(err); + }).join('; '); + } else { + errorMsg = JSON.stringify(failedTool.error); + } + } + + resultHTML += ` +
+
${idx + 1}. ${escapeHtml(toolName)}
+
${escapeHtml(errorMsg)}
+
+ `; + }); + resultHTML += `
`; + } else if (result.errors && result.errors.length > 0) { + // Fallback to errors array if details.failed is not available + resultHTML += ` +
+
Failed Tools (${result.errors.length}):
+
+ `; + result.errors.forEach((error, idx) => { + const toolName = error.name || 'Unknown'; + let errorMsg = 'Unknown error'; + + // Extract detailed error message from errors array + if (error.error) { + if (typeof error.error === 'string') { + errorMsg = error.error; + } else if (error.error.message) { + errorMsg = error.error.message; + } else if (error.error.detail) { + errorMsg = error.error.detail; + } else if (error.error.errors && Array.isArray(error.error.errors)) { + // Handle validation errors array + errorMsg = error.error.errors.map(err => { + if (typeof err === 'string') return err; + if (err.msg) return `${err.loc ? err.loc.join('.') + ': ' : ''}${err.msg}`; + if (err.message) return err.message; + return JSON.stringify(err); + }).join('; '); + } else { + errorMsg = JSON.stringify(error.error); + } + } + + resultHTML += ` +
+
${idx + 1}. ${escapeHtml(toolName)}
+
${escapeHtml(errorMsg)}
+
+ `; + }); + resultHTML += `
`; + } else if (result.detail) { + resultHTML += `
Error: ${escapeHtml(result.detail)}
`; + } + } + + resultHTML += ` + +
+ `; + + importResultSection.innerHTML = resultHTML; + } + + status.classList.add('hidden'); + } catch (error) { showDropdownStatus("error", `❌ Network error: ${error.message}`); } finally { @@ -11702,6 +11748,10 @@

} }; + window.closeImportResult = function() { + window.location.reload(); + }; + // Close dropdown when clicking outside document.addEventListener("click", function (event) { const dropdown = document.getElementById("bulk-import-dropdown");