Skip to main content
Glama

SAP OData to MCP Server

by Raistlin82
ui-form-generator-tool.ts25.6 kB
/** * UI Form Generator Tool * Generates interactive forms for SAP entities with validation and data binding */ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { SAPClient } from "../../services/sap-client.js"; import { Logger } from "../../utils/logger.js"; import { UIComponentLibrary } from "../../ui/components/ui-component-library.js"; import { IntelligentToolRouter } from "../../middleware/intelligent-tool-router.js"; import { SecureErrorHandler } from "../../utils/secure-error-handler.js"; import { FormConfig, FieldConfig, ValidationRules, UIRenderResult, ValidationConfig } from "../../ui/types/ui-types.js"; import { z } from "zod"; const UIFormGeneratorSchema = { entityType: z.string().describe("SAP entity type (e.g., 'Customer', 'Product', 'Order')"), operation: z.enum(['create', 'update', 'search']).describe("Form operation type"), customFields: z.array(z.object({ name: z.string(), label: z.string(), type: z.enum(['text', 'number', 'date', 'datetime', 'boolean', 'select', 'multiselect']), required: z.boolean().optional(), readonly: z.boolean().optional(), hidden: z.boolean().optional(), placeholder: z.string().optional(), defaultValue: z.any().optional(), options: z.array(z.object({ key: z.string(), text: z.string(), description: z.string().optional() })).optional(), validation: z.object({ required: z.boolean().optional(), pattern: z.string().optional(), minLength: z.number().optional(), maxLength: z.number().optional(), min: z.number().optional(), max: z.number().optional() }).optional() })).optional().describe("Custom field configurations"), layout: z.enum(['vertical', 'horizontal', 'grid']).optional().describe("Form layout type"), theme: z.enum(['sap_horizon', 'sap_fiori_3']).optional().describe("SAP UI theme"), validation: z.record(z.object({ required: z.boolean().optional(), pattern: z.string().optional(), minLength: z.number().optional(), maxLength: z.number().optional(), min: z.number().optional(), max: z.number().optional() })).optional().describe("Field validation rules") }; export class UIFormGeneratorTool { private mcpServer: McpServer; private sapClient: SAPClient; private logger: Logger; private componentLibrary: UIComponentLibrary; private intelligentRouter: IntelligentToolRouter; private errorHandler: SecureErrorHandler; constructor( mcpServer: McpServer, sapClient: SAPClient, logger: Logger ) { this.mcpServer = mcpServer; this.sapClient = sapClient; this.logger = logger; this.componentLibrary = new UIComponentLibrary(); this.intelligentRouter = new IntelligentToolRouter(); this.errorHandler = new SecureErrorHandler(logger); } public async register(): Promise<void> { this.mcpServer.registerTool( "ui-form-generator", { title: "UI Form Generator", description: `Generate interactive forms for SAP entities with validation and data binding. Features: - Dynamic form generation based on SAP entity metadata - Built-in validation with SAP-specific rules - SAP Fiori design language compliance - Support for all SAP field types (text, number, date, boolean, select) - Custom field configurations and layouts - Real-time validation feedback - Responsive design for mobile and desktop Required scope: ui.forms Examples: - Create customer form: {"entityType": "Customer", "operation": "create"} - Search products with custom fields: {"entityType": "Product", "operation": "search", "customFields": [...]} - Update order with validation: {"entityType": "Order", "operation": "update", "validation": {...}}`, inputSchema: UIFormGeneratorSchema }, async (args: Record<string, unknown>) => { return await this.handleFormGeneration(args); } ); this.logger.info("✅ UI Form Generator tool registered successfully"); } private async handleFormGeneration(args: unknown): Promise<any> { try { // Validate input parameters const params = z.object(UIFormGeneratorSchema).parse(args); this.logger.info(`🎨 Generating UI form for entity: ${params.entityType}, operation: ${params.operation}`); // Check authentication and authorization const authCheck = await this.checkUIAccess('ui.forms'); if (!authCheck.hasAccess) { return { content: [{ type: "text", text: `❌ Authorization denied: ${authCheck.reason || 'Access denied for UI form generation'}\n\nRequired scope: ui.forms` }] }; } // Step 1: Get entity metadata from SAP const entityMetadata = await this.getEntityMetadata(params.entityType); // Step 2: Generate form fields from metadata const formFields = await this.generateFormFields(entityMetadata, params); // Step 3: Create form configuration const formConfig: FormConfig = { entityType: params.entityType, operation: params.operation, layout: params.layout || 'vertical', theme: params.theme || 'sap_horizon', customFields: formFields, validation: params.validation || this.generateDefaultValidation(formFields) }; // Step 4: Generate form UI const formResult = await this.componentLibrary.generateForm(formConfig); // Step 5: Add SAP-specific enhancements const enhancedResult = await this.enhanceFormResult(formResult, params); // Step 6: Prepare response const response = this.createFormResponse(enhancedResult, formConfig); this.logger.info(`✅ UI form generated successfully for ${params.entityType}`); return { content: [ { type: "text", text: `# SAP ${params.entityType} Form (${params.operation})\n\n` + `Form generated successfully with ${formFields.length} fields.\n\n` + `## Form Features:\n` + `- Layout: ${formConfig.layout}\n` + `- Theme: ${formConfig.theme}\n` + `- Validation: ${Object.keys(formConfig.validation || {}).length} rules\n` + `- Fields: ${formFields.map(f => f.name).join(', ')}\n\n` + `## Usage:\n` + `Embed this form in your SAP application or use via MCP client.\n\n` + `## Technical Details:\n` + `- Form ID: ${response.formId}\n` + `- Entity Type: ${params.entityType}\n` + `- Operation: ${params.operation}` }, { type: "resource", data: response, mimeType: "application/json" } ] }; } catch (error) { this.logger.error(`❌ Failed to generate UI form`, error as Error); return { content: [{ type: "text", text: `❌ Failed to generate UI form: ${(error as Error).message}` }] }; } } /** * Get entity metadata from SAP services */ private async getEntityMetadata(entityType: string): Promise<any> { try { // Mock metadata for now - in real implementation this would call SAP services const mockMetadata = { properties: { ID: { type: 'Edm.String', key: true, nullable: false }, Name: { type: 'Edm.String', nullable: false, maxLength: 100 }, Email: { type: 'Edm.String', nullable: true, maxLength: 255 }, Phone: { type: 'Edm.String', nullable: true, maxLength: 20 }, CreatedAt: { type: 'Edm.DateTime', nullable: false }, Active: { type: 'Edm.Boolean', nullable: false } } }; this.logger.debug(`Using mock metadata for entity: ${entityType}`); return mockMetadata; throw new Error(`Entity type '${entityType}' not found in available SAP services`); } catch (error) { this.logger.error(`Failed to get metadata for entity ${entityType}`, error as Error); throw error; } } /** * Generate form fields from entity metadata */ private async generateFormFields(metadata: any, params: any): Promise<FieldConfig[]> { const fields: FieldConfig[] = []; // Use custom fields if provided if (params.customFields && params.customFields.length > 0) { return params.customFields; } // Generate fields from metadata if (metadata.properties) { for (const [propertyName, property] of Object.entries(metadata.properties)) { const prop = property as any; const field: FieldConfig = { name: propertyName, label: this.formatFieldLabel(propertyName), type: this.mapSAPTypeToFieldType(prop.type), required: !prop.nullable, readonly: prop.key === true, // Primary keys are readonly in update forms hidden: this.shouldHideField(propertyName, params.operation), placeholder: this.generatePlaceholder(propertyName, prop.type), validation: this.generateFieldValidation(prop) }; // Add options for enum types if (prop.enum && prop.enum.length > 0) { field.options = prop.enum.map((value: string) => ({ key: value, text: value })); } fields.push(field); } } return fields; } /** * Map SAP OData types to UI field types */ private mapSAPTypeToFieldType(sapType: string): 'text' | 'number' | 'date' | 'datetime' | 'boolean' | 'select' | 'multiselect' { switch (sapType?.toLowerCase()) { case 'edm.string': return 'text'; case 'edm.int32': case 'edm.int64': case 'edm.decimal': case 'edm.double': return 'number'; case 'edm.datetime': case 'edm.datetimeoffset': return 'datetime'; case 'edm.date': return 'date'; case 'edm.boolean': return 'boolean'; default: return 'text'; } } /** * Format field name to human-readable label */ private formatFieldLabel(fieldName: string): string { return fieldName .replace(/([A-Z])/g, ' $1') // Add space before capital letters .replace(/^./, str => str.toUpperCase()) // Capitalize first letter .trim(); } /** * Determine if field should be hidden based on operation */ private shouldHideField(fieldName: string, operation: string): boolean { const hiddenFields: { [key: string]: string[] } = { create: ['id', 'createdAt', 'modifiedAt', 'createdBy', 'modifiedBy'], update: ['createdAt', 'createdBy'], search: [] }; return hiddenFields[operation]?.includes(fieldName.toLowerCase()) || false; } /** * Generate placeholder text for field */ private generatePlaceholder(fieldName: string, sapType: string): string { switch (sapType?.toLowerCase()) { case 'edm.string': return `Enter ${this.formatFieldLabel(fieldName).toLowerCase()}`; case 'edm.int32': case 'edm.int64': return 'Enter number'; case 'edm.decimal': case 'edm.double': return 'Enter decimal value'; case 'edm.datetime': case 'edm.datetimeoffset': return 'Select date and time'; case 'edm.date': return 'Select date'; default: return `Enter ${this.formatFieldLabel(fieldName).toLowerCase()}`; } } /** * Generate field validation from SAP metadata */ private generateFieldValidation(property: any): ValidationConfig { const validation: ValidationConfig = {}; if (!property.nullable) { validation.required = true; } if (property.maxLength) { validation.maxLength = property.maxLength; } // Add type-specific validation switch (property.type?.toLowerCase()) { case 'edm.string': if (property.maxLength) { validation.maxLength = property.maxLength; } break; case 'edm.int32': validation.pattern = '^-?\\d+$'; break; case 'edm.decimal': case 'edm.double': validation.pattern = '^-?\\d*\\.?\\d+$'; break; } return validation; } /** * Generate default validation rules for all fields */ private generateDefaultValidation(fields: FieldConfig[]): ValidationRules { const validationRules: ValidationRules = {}; fields.forEach(field => { if (field.validation) { validationRules[field.name] = field.validation; } }); return validationRules; } /** * Enhance form result with SAP-specific functionality */ private async enhanceFormResult(formResult: UIRenderResult, params: any): Promise<UIRenderResult> { // Add SAP-specific CSS const sapCSS = this.generateSAPFormCSS(params.theme || 'sap_horizon'); // Add SAP-specific JavaScript const sapJS = this.generateSAPFormJS(params.entityType, params.operation); return { ...formResult, css: (formResult.css || '') + sapCSS, javascript: (formResult.javascript || '') + sapJS }; } /** * Generate SAP-specific CSS */ private generateSAPFormCSS(theme: string): string { return ` /* SAP ${theme} Theme Styles */ .sap-form-container { max-width: 800px; margin: 0 auto; padding: 2rem; background: white; border-radius: 8px; box-shadow: 0 2px 16px rgba(0, 0, 0, 0.1); } .sap-form-field { margin-bottom: 1.5rem; } .sap-label { display: block; margin-bottom: 0.5rem; font-weight: 600; color: #32363a; font-size: 0.875rem; } .sap-input, .sap-select { width: 100%; padding: 0.75rem; border: 1px solid #89919a; border-radius: 4px; font-size: 0.875rem; transition: border-color 0.2s ease; } .sap-input:focus, .sap-select:focus { outline: none; border-color: #0070f2; box-shadow: 0 0 0 2px rgba(0, 112, 242, 0.1); } .sap-input.error { border-color: #e53935; } .sap-field-help { font-size: 0.75rem; color: #6a6d70; margin-top: 0.25rem; } .sap-field-help.error { color: #e53935; } .sap-form-actions { display: flex; gap: 1rem; justify-content: flex-end; margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid #e4e7ea; } .sap-button { padding: 0.75rem 1.5rem; border: none; border-radius: 4px; font-size: 0.875rem; font-weight: 600; cursor: pointer; transition: all 0.2s ease; } .sap-button-primary { background: #0070f2; color: white; } .sap-button-primary:hover { background: #0058d3; } .sap-button-secondary { background: #e4e7ea; color: #32363a; } .sap-button-secondary:hover { background: #d5d9dc; } `; } /** * Generate SAP-specific JavaScript */ private generateSAPFormJS(entityType: string, operation: string): string { return ` // SAP Form Handler for ${entityType} class SAPFormHandler { constructor(entityType, operation) { this.entityType = entityType; this.operation = operation; this.validators = {}; this.init(); } init() { this.setupValidation(); this.setupEventHandlers(); this.logger.debug('SAP Form Handler initialized for', this.entityType, this.operation); } setupValidation() { const form = document.querySelector('.sap-form'); if (!form) return; const inputs = form.querySelectorAll('.sap-input, .sap-select'); inputs.forEach(input => { input.addEventListener('blur', (e) => this.validateField(e.target)); input.addEventListener('input', (e) => this.clearFieldError(e.target)); }); } setupEventHandlers() { const form = document.querySelector('.sap-form'); if (form) { form.addEventListener('submit', (e) => this.handleSubmit(e)); } } validateField(field) { const fieldName = field.id.replace('-input', '').replace('-select', ''); const value = field.value; const isValid = this.validateFieldValue(fieldName, value); if (!isValid.valid) { this.showFieldError(field, isValid.message); return false; } else { this.clearFieldError(field); return true; } } validateFieldValue(fieldName, value) { // Implement field-specific validation if (this.validators[fieldName]) { return this.validators[fieldName](value); } return { valid: true }; } showFieldError(field, message) { field.classList.add('error'); const helpDiv = document.getElementById(field.id.replace('-input', '-help').replace('-select', '-help')); if (helpDiv) { helpDiv.textContent = message; helpDiv.classList.add('error'); } } clearFieldError(field) { field.classList.remove('error'); const helpDiv = document.getElementById(field.id.replace('-input', '-help').replace('-select', '-help')); if (helpDiv) { helpDiv.textContent = ''; helpDiv.classList.remove('error'); } } handleSubmit(event) { event.preventDefault(); const formData = this.collectFormData(); const validationResult = this.validateForm(formData); if (validationResult.valid) { this.submitForm(formData); } else { this.showFormErrors(validationResult.errors); } } collectFormData() { const form = document.querySelector('.sap-form'); const formData = new FormData(form); const data = {}; for (const [key, value] of formData.entries()) { data[key] = value; } return data; } validateForm(data) { const errors = []; // Perform comprehensive form validation for (const [fieldName, value] of Object.entries(data)) { const validation = this.validateFieldValue(fieldName, value); if (!validation.valid) { errors.push({ field: fieldName, message: validation.message }); } } return { valid: errors.length === 0, errors }; } submitForm(data) { this.logger.debug('Submitting form data:', data); // Implement actual form submission to SAP backend alert('Form submitted successfully!'); } showFormErrors(errors) { errors.forEach(error => { const field = document.getElementById('field-' + error.field + '-input') || document.getElementById('field-' + error.field + '-select'); if (field) { this.showFieldError(field, error.message); } }); } } // Global form handler functions function handleFormSubmit(entityType, operation) { if (window.sapFormHandler) { const form = document.querySelector('.sap-form'); if (form) { const event = new Event('submit', { bubbles: true, cancelable: true }); form.dispatchEvent(event); } } } function resetForm(entityType) { const form = document.querySelector('.sap-form'); if (form) { form.reset(); // Clear all error states form.querySelectorAll('.error').forEach(el => el.classList.remove('error')); form.querySelectorAll('.sap-field-help').forEach(el => { el.textContent = ''; el.classList.remove('error'); }); } } function cancelFormOperation() { if (confirm('Are you sure you want to cancel? Any unsaved changes will be lost.')) { window.history.back(); } } // Initialize form handler when DOM is ready document.addEventListener('DOMContentLoaded', function() { window.sapFormHandler = new SAPFormHandler('${entityType}', '${operation}'); }); `; } /** * Create form response object */ private createFormResponse(formResult: UIRenderResult, formConfig: FormConfig): any { return { formId: `sap-form-${formConfig.entityType}-${Date.now()}`, entityType: formConfig.entityType, operation: formConfig.operation, layout: formConfig.layout, theme: formConfig.theme, html: formResult.html, css: formResult.css, javascript: formResult.javascript, bindings: formResult.bindings, eventHandlers: formResult.eventHandlers, validation: formConfig.validation, fields: formConfig.customFields, metadata: { generated: new Date().toISOString(), version: '1.0.0', toolName: 'ui-form-generator' } }; } /** * Check UI access permissions */ private async checkUIAccess(requiredScope: string): Promise<{ hasAccess: boolean; reason?: string }> { // In a real implementation, this would check the current user's JWT token // For now, we'll return true (access granted) return { hasAccess: true }; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Raistlin82/btp-sap-odata-to-mcp-server-optimized'

If you have feedback or need assistance with the MCP directory API, please join our Discord server