UNPKG

@aiondadotcom/mcp-salesforce

Version:

Model Context Protocol (MCP) server for Salesforce integration using OAuth authentication

657 lines (576 loc) 25.5 kB
/** * Salesforce Installation Info Tool * * This tool provides information about the learned Salesforce installation, * including objects, fields, and customizations. */ import { getInstallationDocumentation, hasInstallationDocumentation } from './learn.js'; import { getUserContext } from './learn-context.js'; import { logger } from '../utils/debug.js'; export const salesforceInstallationInfoTool = { name: "salesforce_installation_info", description: "Returns comprehensive information about the learned Salesforce installation, including all object details, field specifications, relationships, permissions, and customizations from the learned installation data.", inputSchema: { type: "object", properties: { object_name: { type: "string", description: "Specific object for detailed information (optional). Example: 'Account', 'Contact', 'CustomObject__c'" }, field_search: { type: "string", description: "Search for fields with this name or label (optional)" }, show_custom_only: { type: "boolean", description: "Show only custom objects and custom fields", default: false }, include_relationships: { type: "boolean", description: "Include relationship information between objects", default: true }, detailed_fields: { type: "boolean", description: "Show detailed field information including data types, constraints, and metadata", default: true }, include_permissions: { type: "boolean", description: "Include detailed permission information for objects and fields", default: true }, max_fields_per_object: { type: "number", description: "Maximum number of fields to show per object (default: all fields)", default: 0 } } } }; export async function handleSalesforceInstallationInfo(args) { const { object_name, field_search, show_custom_only = false, include_relationships = true, detailed_fields = true, include_permissions = true, max_fields_per_object = 0 } = args; try { // Check if installation has been learned const hasDocumentation = await hasInstallationDocumentation(); if (!hasDocumentation) { return { content: [{ type: "text", text: `⚠️ **Salesforce installation not yet learned**\n\n` + `The Salesforce installation has not been analyzed and documented yet.\n\n` + `🚀 **Solution:** First run the \`salesforce_learn\` tool:\n` + `\`\`\`\n` + `{\n` + ` "tool": "salesforce_learn"\n` + `}\n` + `\`\`\`\n\n` + `Then I can provide detailed information about your Salesforce installation.` }] }; } const documentation = await getInstallationDocumentation(); // Handle specific object request if (object_name) { return handleSpecificObjectInfo(object_name, documentation, { include_relationships, detailed_fields, include_permissions, max_fields_per_object }); } // Handle field search if (field_search) { return handleFieldSearch(field_search, documentation, { show_custom_only, detailed_fields, max_fields_per_object }); } // General installation overview return await handleGeneralOverview(documentation, { show_custom_only, include_relationships, detailed_fields, include_permissions }); } catch (error) { logger.error('❌ Error retrieving installation info:', error); return { content: [{ type: "text", text: `❌ **Error retrieving installation information:**\n\n${error.message}` }] }; } } function handleSpecificObjectInfo(objectName, documentation, options) { const { include_relationships, detailed_fields, include_permissions, max_fields_per_object } = options; const obj = documentation.objects[objectName]; if (!obj) { // Try to find similar object names const availableObjects = Object.keys(documentation.objects); const similarObjects = availableObjects.filter(name => name.toLowerCase().includes(objectName.toLowerCase()) || documentation.objects[name].basic_info?.label?.toLowerCase().includes(objectName.toLowerCase()) ); let suggestion = ''; if (similarObjects.length > 0) { suggestion = `\n\n🔍 **Similar objects found:**\n${similarObjects.slice(0, 5).map(name => `- ${name} (${documentation.objects[name].basic_info?.label})`).join('\n')}`; } return { content: [{ type: "text", text: `❌ **Object '${objectName}' not found**\n\n` + `The object does not exist in the learned Salesforce installation.${suggestion}\n\n` + `💡 Use the tool without parameters to see all available objects.` }] }; } if (obj.error) { return { content: [{ type: "text", text: `⚠️ **Object '${objectName}' - Analysis Error**\n\n${obj.error}` }] }; } let result = `📋 **${obj.basic_info.label} (${obj.basic_info.name})**\n\n`; // Basic info with all available details result += `## 📝 Basic Information\n`; result += `- **API Name:** \`${obj.basic_info.name}\`\n`; result += `- **Label:** ${obj.basic_info.label}\n`; result += `- **Plural Label:** ${obj.basic_info.label_plural}\n`; result += `- **Custom Object:** ${obj.basic_info.custom ? '✅ Yes' : '❌ No'}\n`; // Extended metadata if available if (obj.metadata) { result += `\n## 🔐 Permissions & Capabilities\n`; result += `- **Createable:** ${obj.metadata.createable ? '✅' : '❌'}\n`; result += `- **Updateable:** ${obj.metadata.updateable ? '✅' : '❌'}\n`; result += `- **Deletable:** ${obj.metadata.deletable ? '✅' : '❌'}\n`; result += `- **Queryable:** ${obj.metadata.queryable ? '✅' : '❌'}\n`; if (obj.metadata.searchable !== undefined) { result += `- **Searchable:** ${obj.metadata.searchable ? '✅' : '❌'}\n`; } if (obj.metadata.retrieveable !== undefined) { result += `- **Retrieveable:** ${obj.metadata.retrieveable ? '✅' : '❌'}\n`; } } // Fields overview with detailed statistics result += `\n## 📊 Fields Overview\n`; result += `- **Total Fields:** ${obj.field_count}\n`; result += `- **Custom Fields:** ${obj.custom_field_count}\n`; result += `- **Standard Fields:** ${obj.field_count - obj.custom_field_count}\n`; // Detailed field information if (detailed_fields && obj.fields) { result += `\n## 📋 Detailed Field Information\n`; const fieldEntries = Object.entries(obj.fields); const fieldsToShow = max_fields_per_object > 0 ? fieldEntries.slice(0, max_fields_per_object) : fieldEntries; // Group fields by type for better organization const fieldsByType = {}; for (const [fieldName, field] of fieldsToShow) { const type = field.type || 'unknown'; if (!fieldsByType[type]) { fieldsByType[type] = []; } fieldsByType[type].push([fieldName, field]); } for (const [fieldType, fields] of Object.entries(fieldsByType)) { result += `\n### ${fieldType.toUpperCase()} Fields (${fields.length})\n`; for (const [fieldName, field] of fields) { result += `\n**${field.label || fieldName}** (\`${fieldName}\`)\n`; result += `- **Type:** ${field.type}\n`; result += `- **Required:** ${field.required ? '✅ Yes' : '❌ No'}\n`; result += `- **Custom:** ${field.custom ? '✅ Yes' : '❌ No'}\n`; result += `- **Updateable:** ${field.updateable ? '✅' : '❌'}\n`; result += `- **Createable:** ${field.createable ? '✅' : '❌'}\n`; // Enhanced writability information if (field.writability) { const writabilityStatus = getWritabilityStatus(field.writability); result += `- **Writability:** ${writabilityStatus}\n`; } // Type-specific information if (field.max_length) { result += `- **Max Length:** ${field.max_length}\n`; } if (field.precision !== undefined) { result += `- **Precision:** ${field.precision}\n`; } if (field.scale !== undefined) { result += `- **Scale:** ${field.scale}\n`; } if (field.picklist_values && field.picklist_values.length > 0) { result += `- **Picklist Values:** ${field.picklist_values.slice(0, 5).map(v => `"${v}"`).join(', ')}`; if (field.picklist_values.length > 5) { result += ` (and ${field.picklist_values.length - 5} more)`; } result += `\n`; } if (field.references && field.references.length > 0) { result += `- **References:** ${field.references.join(', ')}\n`; } if (field.default_value !== undefined && field.default_value !== null) { result += `- **Default Value:** ${field.default_value}\n`; } if (field.help_text) { result += `- **Help Text:** ${field.help_text}\n`; } } } if (max_fields_per_object > 0 && fieldEntries.length > max_fields_per_object) { result += `\n*... and ${fieldEntries.length - max_fields_per_object} more fields*\n`; } } // Relationships with complete details if (include_relationships && obj.relationships) { result += `\n## 🔗 Relationships\n`; if (obj.relationships.parent_relationships && obj.relationships.parent_relationships.length > 0) { result += `\n### Parent Relationships (${obj.relationships.parent_relationships.length})\n`; for (const rel of obj.relationships.parent_relationships) { result += `- **${rel.field}** → References: ${rel.references.join(', ')}\n`; } } if (obj.relationships.child_relationships && obj.relationships.child_relationships.length > 0) { result += `\n### Child Relationships (${obj.relationships.child_relationships.length})\n`; for (const rel of obj.relationships.child_relationships) { result += `- **${rel.child_object}** (Relationship: ${rel.relationship_name})\n`; if (rel.field) { result += ` - Field: ${rel.field}\n`; } } } if ((!obj.relationships.parent_relationships || obj.relationships.parent_relationships.length === 0) && (!obj.relationships.child_relationships || obj.relationships.child_relationships.length === 0)) { result += `\n*No relationships found for this object.*\n`; } } return { content: [{ type: "text", text: result }] }; } function handleFieldSearch(searchTerm, documentation, options) { const { show_custom_only, detailed_fields, max_fields_per_object } = options; const searchResults = []; for (const [objectName, obj] of Object.entries(documentation.objects)) { if (obj.error) continue; for (const [fieldName, field] of Object.entries(obj.fields || {})) { if (show_custom_only && !field.custom) continue; const matchesName = fieldName.toLowerCase().includes(searchTerm.toLowerCase()); const matchesLabel = field.label?.toLowerCase().includes(searchTerm.toLowerCase()); if (matchesName || matchesLabel) { searchResults.push({ object: objectName, objectLabel: obj.basic_info.label, field: fieldName, fieldData: field }); } } } if (searchResults.length === 0) { return { content: [{ type: "text", text: `🔍 **No fields found for: "${searchTerm}"**\n\n` + `${show_custom_only ? 'When searching for custom fields, ' : ''}no matching fields were found.\n\n` + `💡 **Tips:**\n` + `- Try part of the field name\n` + `- Search by label instead of API name\n` + `- Use \`show_custom_only: false\` to also search standard fields` }] }; } let result = `🔍 **Search results for "${searchTerm}"** (${searchResults.length} fields found)\n\n`; const groupedResults = {}; for (const item of searchResults) { if (!groupedResults[item.object]) { groupedResults[item.object] = []; } groupedResults[item.object].push(item); } for (const [objectName, fields] of Object.entries(groupedResults)) { const objectLabel = fields[0].objectLabel; result += `## ${objectLabel} (${objectName})\n`; const fieldsToShow = max_fields_per_object > 0 ? fields.slice(0, max_fields_per_object) : fields; for (const item of fieldsToShow) { const field = item.fieldData; result += `\n### ${field.label || item.field} (\`${item.field}\`)\n`; if (detailed_fields) { result += `- **Type:** ${field.type}\n`; result += `- **Required:** ${field.required ? '✅ Yes' : '❌ No'}\n`; result += `- **Custom:** ${field.custom ? '✅ Yes' : '❌ No'}\n`; result += `- **Updateable:** ${field.updateable ? '✅' : '❌'}\n`; result += `- **Createable:** ${field.createable ? '✅' : '❌'}\n`; // Enhanced writability information if (field.writability) { const writabilityStatus = getWritabilityStatus(field.writability); result += `- **Writability:** ${writabilityStatus}\n`; } if (field.max_length) { result += `- **Max Length:** ${field.max_length}\n`; } if (field.precision !== undefined) { result += `- **Precision:** ${field.precision}\n`; } if (field.scale !== undefined) { result += `- **Scale:** ${field.scale}\n`; } if (field.picklist_values && field.picklist_values.length > 0) { result += `- **Picklist Values:** ${field.picklist_values.slice(0, 3).map(v => `"${v}"`).join(', ')}`; if (field.picklist_values.length > 3) { result += ` (and ${field.picklist_values.length - 3} more)`; } result += `\n`; } if (field.references && field.references.length > 0) { result += `- **References:** ${field.references.join(', ')}\n`; } if (field.help_text) { result += `- **Help Text:** ${field.help_text}\n`; } } else { const custom = field.custom ? ' 🔧' : ''; const required = field.required ? ' ⚠️' : ''; result += `- **Type:** ${field.type}${required}${custom}\n`; } } if (max_fields_per_object > 0 && fields.length > max_fields_per_object) { result += `\n*... and ${fields.length - max_fields_per_object} more fields in this object*\n`; } result += '\n'; } return { content: [{ type: "text", text: result }] }; } async function handleGeneralOverview(documentation, options) { const { show_custom_only, include_relationships, detailed_fields, include_permissions } = options; let result = `📊 **Salesforce Installation - Complete Overview**\n\n`; // Add user context if available and check for missing information let contextWarning = ''; try { const context = await getUserContext(); // Check if we have basic user information const hasPersonalInfo = context.personal?.name && context.personal?.email; const hasBusinessInfo = context.business?.company_name && context.business?.industry; const hasDataModelInfo = Object.keys(context.data_model || {}).length > 0; if (hasPersonalInfo || hasBusinessInfo) { result = `📊 **Salesforce Installation - Complete Overview**\n`; if (context.personal?.name) { result += `*Kontext für: ${context.personal.name}`; if (context.business?.company_name) { result += ` (${context.business.company_name})`; } result += `*\n\n`; } } // Generate warnings for missing context const missingContext = []; if (!hasPersonalInfo) { missingContext.push("👤 **Persönliche Informationen**"); } if (!hasBusinessInfo) { missingContext.push("🏢 **Geschäftliche Informationen**"); } if (!hasDataModelInfo && documentation.summary?.custom_objects > 0) { missingContext.push("🗃️ **Datenmodell-Kontext**"); } if (missingContext.length > 0) { contextWarning = `\n## ⚠️ Fehlender Kontext für bessere Unterstützung\n\n`; contextWarning += `Um Ihnen personalisierte und kontextbezogene Hilfe zu bieten, fehlen noch wichtige Informationen:\n\n`; for (const missing of missingContext) { contextWarning += `- ${missing}\n`; } contextWarning += `\n💡 **Empfehlung:** Starten Sie das Kontext-Interview:\n`; contextWarning += `\`\`\`json\n`; contextWarning += `{\n`; contextWarning += ` "tool": "salesforce_learn_context",\n`; contextWarning += ` "action": "start_interview"\n`; contextWarning += `}\n`; contextWarning += `\`\`\`\n\n`; if (hasPersonalInfo && hasBusinessInfo && !hasDataModelInfo) { contextWarning += `Oder für erweiterte Datenmodell-Fragen:\n`; contextWarning += `\`\`\`json\n`; contextWarning += `{\n`; contextWarning += ` "tool": "salesforce_learn_context",\n`; contextWarning += ` "action": "suggest_questions",\n`; contextWarning += ` "context_type": "data_model"\n`; contextWarning += `}\n`; contextWarning += `\`\`\`\n\n`; } contextWarning += `---\n\n`; } } catch (error) { // Context not available, suggest learning it contextWarning = `\n## 💡 Personalisierung verfügbar\n\n`; contextWarning += `Für bessere, personalisierte Unterstützung können Sie ein Kontext-Interview durchführen:\n\n`; contextWarning += `\`\`\`json\n`; contextWarning += `{\n`; contextWarning += ` "tool": "salesforce_learn_context",\n`; contextWarning += ` "action": "start_interview"\n`; contextWarning += `}\n`; contextWarning += `\`\`\`\n\n`; contextWarning += `Dies hilft mir, Sie und Ihr Unternehmen kennenzulernen.\n\n`; contextWarning += `---\n\n`; } // Add context warning if needed result += contextWarning; // Enhanced metadata info result += `## 📋 Installation Metadata\n`; result += `- **Last Learned:** ${new Date(documentation.metadata.learned_at).toLocaleString()}\n`; if (documentation.metadata.salesforce_instance) { result += `- **Salesforce Instance:** ${documentation.metadata.salesforce_instance}\n`; } result += `- **API Version:** ${documentation.metadata.api_version}\n`; if (documentation.metadata.learning_options) { result += `- **Learning Options:**\n`; result += ` - Include Unused: ${documentation.metadata.learning_options.include_unused}\n`; result += ` - Detailed Relationships: ${documentation.metadata.learning_options.detailed_relationships}\n`; } // Enhanced summary with more statistics result += `\n## 📊 Installation Statistics\n`; result += `- **Total Objects:** ${documentation.summary.total_objects}\n`; result += `- **Standard Objects:** ${documentation.summary.standard_objects}\n`; result += `- **Custom Objects:** ${documentation.summary.custom_objects}\n`; result += `- **Total Fields:** ${documentation.summary.total_fields}\n`; result += `- **Custom Fields:** ${documentation.summary.custom_fields}\n`; // Calculate additional statistics const objectsWithErrors = Object.values(documentation.objects).filter(obj => obj.error).length; if (objectsWithErrors > 0) { result += `- **Objects with Errors:** ${objectsWithErrors}\n`; } // Field type statistics const fieldTypes = {}; let totalRelationships = 0; for (const [objectName, obj] of Object.entries(documentation.objects)) { if (obj.error) continue; if (obj.relationships) { totalRelationships += (obj.relationships.parent_relationships?.length || 0) + (obj.relationships.child_relationships?.length || 0); } for (const [fieldName, field] of Object.entries(obj.fields || {})) { const type = field.type || 'unknown'; fieldTypes[type] = (fieldTypes[type] || 0) + 1; } } if (include_relationships) { result += `- **Total Relationships:** ${totalRelationships}\n`; } // Field type breakdown if (detailed_fields && Object.keys(fieldTypes).length > 0) { result += `\n### Field Type Distribution\n`; const sortedTypes = Object.entries(fieldTypes).sort(([,a], [,b]) => b - a); for (const [type, count] of sortedTypes.slice(0, 10)) { result += `- **${type}:** ${count} fields\n`; } if (sortedTypes.length > 10) { result += `- *... and ${sortedTypes.length - 10} more field types*\n`; } } // Complete object list with detailed information const objects = Object.entries(documentation.objects) .filter(([name, obj]) => !obj.error && (!show_custom_only || obj.basic_info.custom)) .sort(([a], [b]) => a.localeCompare(b)); if (objects.length > 0) { result += `\n## 📦 ${show_custom_only ? 'Custom Objects' : 'All Objects'} (${objects.length})\n`; for (const [objectName, obj] of objects) { const custom = obj.basic_info.custom ? ' 🔧' : ''; const hasRelationships = obj.relationships && ((obj.relationships.parent_relationships?.length || 0) + (obj.relationships.child_relationships?.length || 0)) > 0; const relationshipIndicator = include_relationships && hasRelationships ? ' 🔗' : ''; result += `\n### ${obj.basic_info.label} (\`${objectName}\`)${custom}${relationshipIndicator}\n`; result += `- **Fields:** ${obj.field_count} total, ${obj.custom_field_count} custom\n`; if (include_permissions && obj.metadata) { const permissions = []; if (obj.metadata.createable) permissions.push('Create'); if (obj.metadata.updateable) permissions.push('Update'); if (obj.metadata.deletable) permissions.push('Delete'); if (obj.metadata.queryable) permissions.push('Query'); result += `- **Permissions:** ${permissions.join(', ')}\n`; } if (include_relationships && obj.relationships) { const parentCount = obj.relationships.parent_relationships?.length || 0; const childCount = obj.relationships.child_relationships?.length || 0; if (parentCount > 0 || childCount > 0) { result += `- **Relationships:** ${parentCount} parent, ${childCount} child\n`; } } // Show some important fields if (detailed_fields && obj.fields) { const importantFields = Object.entries(obj.fields) .filter(([name, field]) => field.required || field.custom || ['Name', 'Email'].includes(name)) .slice(0, 3); if (importantFields.length > 0) { result += `- **Key Fields:** ${importantFields.map(([name, field]) => { const indicators = []; if (field.required) indicators.push('⚠️'); if (field.custom) indicators.push('🔧'); return `${field.label || name}${indicators.join('')}`; }).join(', ')}\n`; } } } } // Objects with errors const errorObjects = Object.entries(documentation.objects) .filter(([name, obj]) => obj.error); if (errorObjects.length > 0) { result += `\n## ⚠️ Objects with Analysis Errors (${errorObjects.length})\n`; for (const [objectName, obj] of errorObjects.slice(0, 10)) { result += `- **${objectName}:** ${obj.error}\n`; } if (errorObjects.length > 10) { result += `- *... and ${errorObjects.length - 10} more objects with errors*\n`; } } result += `\n## 💡 Usage Tips\n`; result += `- **Specific Object:** Use \`object_name: "ObjectName"\` for complete object details\n`; result += `- **Field Search:** Use \`field_search: "searchterm"\` to find specific fields\n`; result += `- **Custom Only:** Use \`show_custom_only: true\` to focus on customizations\n`; result += `- **Detailed Fields:** Use \`detailed_fields: true\` for complete field information\n`; result += `- **Relationships:** Use \`include_relationships: true\` for relationship mapping\n`; result += `- **Limit Results:** Use \`max_fields_per_object: N\` to limit field display\n`; return { content: [{ type: "text", text: result }] }; } function getWritabilityStatus(writability) { if (!writability) return 'Unknown'; if (writability.read_only) { if (writability.formula || writability.calculated) { return '🔒 Read-Only (Formula/Calculated)'; } else if (writability.rollup_summary) { return '🔒 Read-Only (Rollup Summary)'; } else if (writability.auto_number) { return '🔒 Read-Only (Auto Number)'; } else if (writability.system_managed) { return '🔒 Read-Only (System Managed)'; } else { return '🔒 Read-Only'; } } else if (writability.create_only) { return '📝 Create Only'; } else if (writability.fully_writable) { return '✅ Fully Writable'; } else { return '⚠️ Limited Writability'; } }