Skip to main content
Glama

modify_form

Update existing Tally form details including title, description, and field configurations to adapt forms to changing requirements.

Instructions

Modify an existing Tally form

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
formIdYesID of the form to modify
titleNoNew form title
descriptionNoNew form description
fieldsNo

Implementation Reference

  • Registration of the 'modify_form' tool including name, description, and detailed input schema definition
    name: 'modify_form', description: 'Modify an existing Tally form', inputSchema: { type: 'object', properties: { formId: { type: 'string', description: 'ID of the form to modify' }, title: { type: 'string', description: 'New form title' }, description: { type: 'string', description: 'New form description' }, fields: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['text', 'email', 'number', 'textarea', 'select', 'checkbox', 'radio'] }, label: { type: 'string' }, required: { type: 'boolean' }, options: { type: 'array', items: { type: 'string' } } }, required: ['type', 'label'] } } }, required: ['formId'] } },
  • Input schema definition for 'modify_form' tool validating formId (required), title, description, and fields array
    name: 'modify_form', description: 'Modify an existing Tally form', inputSchema: { type: 'object', properties: { formId: { type: 'string', description: 'ID of the form to modify' }, title: { type: 'string', description: 'New form title' }, description: { type: 'string', description: 'New form description' }, fields: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['text', 'email', 'number', 'textarea', 'select', 'checkbox', 'radio'] }, label: { type: 'string' }, required: { type: 'boolean' }, options: { type: 'array', items: { type: 'string' } } }, required: ['type', 'label'] } } }, required: ['formId'] } },
  • FormModificationTool class - primary handler for form modifications. Currently stubs retrieval but intended for full modify_form logic using Tally API
    export class FormModificationTool implements Tool<FormModificationArgs, FormModificationResult> { public readonly name = 'form_modification_tool'; public readonly description = 'Modifies existing Tally forms through natural language commands'; private tallyApiService: TallyApiService; constructor(apiClientConfig: TallyApiClientConfig) { this.tallyApiService = new TallyApiService(apiClientConfig); } /** * Execute the form modification based on natural language command */ public async execute(args: FormModificationArgs): Promise<FormModificationResult> { if (!args.formId) { return { success: false, status: 'error', message: 'Form ID is required', errors: ['Missing form ID'] } as any; } try { const form = await this.tallyApiService.getForm(args.formId); if (!form) { return { success: false, status: 'error', message: `Form with ID ${args.formId} not found`, errors: [`Form ${args.formId} does not exist`] } as any; } return { success: true, status: 'success', message: `Successfully retrieved form "${form.title}"`, modifiedForm: form, // for test compatibility finalFormConfig: undefined, changes: [`Retrieved form: ${form.title}`] } as any; } catch (error) { return { success: false, status: 'error', message: `Form with ID ${args.formId} not found`, errors: [`Form ${args.formId} does not exist`] } as any; } } // Add missing methods for test compatibility public async getForm(formId: string): Promise<TallyForm | null> { try { return await this.tallyApiService.getForm(formId); } catch (e) { return null; } } public async getForms(options: any = {}): Promise<TallyFormsResponse | null> { try { return await this.tallyApiService.getForms(options); } catch (e) { return null; } } public async updateForm(formId: string, config: any): Promise<TallyForm | null> { try { return await this.tallyApiService.updateForm(formId, config); } catch (e) { return null; } } public async patchForm(formId: string, updates: any): Promise<TallyForm | null> { try { return await this.tallyApiService.patchForm(formId, updates); } catch (e) { return null; } } public async validateConnection(): Promise<boolean> { try { const result = await this.getForms({ limit: 1 }); return !!result; } catch (e) { return false; } } }
  • Core helper implementing all form modification operations: add/remove/update/reorder fields, manage required status, title/description changes, option management
    export class FormModificationOperations { /** * Execute a parsed modification command on a form */ public executeOperation( command: ParsedModificationCommand, baseForm: TallyForm | null, currentFormConfig?: FormConfig ): ModificationOperationResult { try { // Convert TallyForm to FormConfig if not provided const formConfig = currentFormConfig || (baseForm ? this.convertTallyFormToFormConfig(baseForm) : undefined); if (!formConfig) { return { success: false, message: 'An error occurred while executing the modification operation', changes: [], errors: ['No form config provided or derivable.'] } } let result: ModificationOperationResult; switch (command.operation) { case ModificationOperation.ADD_FIELD: result = this.addField(formConfig, command.parameters); break; case ModificationOperation.REMOVE_FIELD: result = this.removeField(formConfig, command.parameters); break; case ModificationOperation.MAKE_REQUIRED: result = this.makeFieldRequired(formConfig, command.parameters, true); break; case ModificationOperation.MAKE_OPTIONAL: result = this.makeFieldRequired(formConfig, command.parameters, false); break; case ModificationOperation.UPDATE_TITLE: result = this.updateFormTitle(formConfig, command.parameters); break; case ModificationOperation.UPDATE_DESCRIPTION: result = this.updateFormDescription(formConfig, command.parameters); break; case ModificationOperation.REORDER_FIELD: result = this.reorderField(formConfig, command.parameters); break; case ModificationOperation.ADD_OPTION: result = this.addOption(formConfig, command.parameters); break; case ModificationOperation.MODIFY_FIELD: result = this.modifyField(formConfig, command.parameters); break; default: result = { success: false, message: `Operation ${command.operation} is not yet implemented`, changes: [], errors: [`Unsupported operation: ${command.operation}`] }; } // If modification was successful, update version and timestamp if (result.success && result.modifiedFormConfig) { const originalVersion = formConfig.metadata?.version || 1; if (!result.modifiedFormConfig.metadata) { result.modifiedFormConfig.metadata = {}; } result.modifiedFormConfig.metadata.version = originalVersion + 1; result.modifiedFormConfig.metadata.updatedAt = new Date().toISOString(); // Add version info to changes result.changes.push(`Form version incremented to ${result.modifiedFormConfig.metadata.version}`); } return result; } catch (error) { return { success: false, message: 'An error occurred while executing the modification operation', changes: [], errors: [error instanceof Error ? error.message : 'Unknown error'] }; } } /** * Add a new field to the form */ private addField(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult { if (!params.fieldType) { return { success: false, message: 'Field type is required for adding a field', changes: [], errors: ['Missing field type'] }; } const newFieldId = this.generateNextFieldId(formConfig); const fieldLabel = params.fieldLabel || this.generateDefaultFieldLabel(params.fieldType || QuestionType.TEXT); // Create properly configured question based on type const newField = this.createQuestionConfig( params.fieldType as QuestionType, newFieldId, fieldLabel || 'Untitled Field', params ); const updatedFormConfig: FormConfig = { ...formConfig, questions: [...formConfig.questions, newField] }; return { success: true, modifiedFormConfig: updatedFormConfig, message: `Successfully added ${fieldLabel} field`, changes: [ `Added new field: ${fieldLabel}`, `Field type: ${params.fieldType}`, `Field ID: ${newFieldId}`, `Position: ${updatedFormConfig.questions.length}` ] }; } /** * Remove a field from the form */ private removeField(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult { const lookupOptions: FieldLookupOptions = { formConfig }; if (params.fieldId) lookupOptions.fieldId = params.fieldId; if (params.fieldLabel) lookupOptions.fieldLabel = params.fieldLabel; if (params.fieldNumber) lookupOptions.fieldNumber = params.fieldNumber; const field = this.findField(lookupOptions); if (!field.found || typeof field.index !== 'number') { return { success: false, message: field.error || 'Field not found', changes: [], errors: [field.error || 'Field not found'] }; } const fieldIndex = field.index; const removedField = formConfig.questions[fieldIndex]; if (!removedField?.id) { return { success: false, message: `Field at index ${fieldIndex} is invalid or missing an ID.`, changes: [], errors: [`Invalid field at index ${fieldIndex}`], }; } const fieldIdToRemove = removedField.id; // Dependency Validation const validationErrors = DependencyValidator.validateFieldRemoval(formConfig, fieldIdToRemove); if (validationErrors.length > 0) { return { success: false, message: 'Cannot remove field due to broken dependencies.', changes: [], errors: validationErrors, }; } const updatedQuestions = formConfig.questions.filter((_, index) => index !== fieldIndex); const updatedFormConfig: FormConfig = { ...formConfig, questions: updatedQuestions }; // Post-removal validation const postRemovalErrors = DependencyValidator.validateAllLogic(updatedFormConfig); if (postRemovalErrors.length > 0) { return { success: false, message: 'An unexpected validation error occurred after field removal.', changes: [], errors: postRemovalErrors, }; } return { success: true, modifiedFormConfig: updatedFormConfig, message: `Successfully removed field "${removedField.label}"`, changes: [ `Removed field: ${removedField.label}`, `Field type: ${removedField.type}`, `Previous position: ${fieldIndex + 1}`, `Remaining fields: ${updatedQuestions.length}` ] }; } /** * Update field required status */ private makeFieldRequired( formConfig: FormConfig, params: ModificationParameters, required: boolean ): ModificationOperationResult { const lookupOptions: FieldLookupOptions = { formConfig }; if (params.fieldId) lookupOptions.fieldId = params.fieldId; if (params.fieldLabel) lookupOptions.fieldLabel = params.fieldLabel; if (params.fieldNumber) lookupOptions.fieldNumber = params.fieldNumber; const field = this.findField(lookupOptions); if (!field.found || typeof field.index !== 'number') { return { success: false, message: field.error || 'Field not found', changes: [], errors: [field.error || 'Field not found'] }; } const fieldIndex = field.index; const updatedQuestions = [...formConfig.questions]; const targetQuestion = updatedQuestions[fieldIndex]; if (!targetQuestion) { return { success: false, message: 'Field not found at the specified index.', changes: [], errors: [`Invalid index ${fieldIndex}`] } } updatedQuestions[fieldIndex] = { ...targetQuestion, required }; const updatedFormConfig: FormConfig = { ...formConfig, questions: updatedQuestions }; return { success: true, modifiedFormConfig: updatedFormConfig, message: `Successfully set "required" to ${required} for field "${targetQuestion.label}"`, changes: [ `Field: ${targetQuestion.label}`, `Property: required`, `New value: ${required}`, `Set required: ${required}` ] }; } /** * Update the form title */ private updateFormTitle(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult { if (!params.newValue) { return { success: false, message: 'New title value is required', changes: [], errors: ['Missing new title'] }; } const updatedFormConfig: FormConfig = { ...formConfig, title: params.newValue }; return { success: true, modifiedFormConfig: updatedFormConfig, message: `Successfully updated form title to "${params.newValue}"`, changes: [ `Updated form title`, `Previous title: ${formConfig.title}`, `New title: ${params.newValue}`, `Changed title to: "${params.newValue}"` ] }; } /** * Update the form description */ private updateFormDescription(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult { const newDescription = params.newValue || ''; const updatedFormConfig: FormConfig = { ...formConfig, description: newDescription }; return { success: true, modifiedFormConfig: updatedFormConfig, message: 'Successfully updated form description', changes: [ `Updated form description`, `Previous description: ${formConfig.description || ''}`, `New description: ${newDescription}`, `Changed description to: "${newDescription}"` ] }; } /** * Reorder a field in the form */ private reorderField(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult { const { sourcePosition, targetPosition, fieldId, fieldLabel } = params; if (sourcePosition === undefined && fieldId === undefined && fieldLabel === undefined) { return { success: false, message: 'Source field identifier (position, ID, or label) is required for reordering', changes: [], errors: ['Missing source field identifier'] }; } if (targetPosition === undefined) { return { success: false, message: 'Target position is required for reordering', changes: [], errors: ['Missing target position'] }; } const lookupOptions: FieldLookupOptions = { formConfig }; if (fieldId) lookupOptions.fieldId = fieldId; if (fieldLabel) lookupOptions.fieldLabel = fieldLabel; const sourceIndex = sourcePosition !== undefined ? sourcePosition - 1 : this.findField(lookupOptions).index; if (sourceIndex === undefined || sourceIndex < 0 || sourceIndex >= formConfig.questions.length) { return { success: false, message: 'Source field not found or out of range', changes: [], errors: ['Invalid source field for reorder'] }; } const targetIndex = targetPosition - 1; if (targetIndex < 0 || targetIndex >= formConfig.questions.length) { return { success: false, message: 'Target position is out of range', changes: [], errors: ['Invalid target position for reorder'] }; } const updatedQuestions = [...formConfig.questions]; const [movedField] = updatedQuestions.splice(sourceIndex, 1); if (!movedField) { return { success: false, message: 'Could not extract field to be moved.', changes: [], errors: ['Splice operation failed to return the field.'], }; } updatedQuestions.splice(targetIndex, 0, movedField); const updatedFormConfig: FormConfig = { ...formConfig, questions: updatedQuestions }; return { success: true, modifiedFormConfig: updatedFormConfig, message: `Successfully moved field "${movedField.label}" to position ${targetPosition}`, changes: [ `Moved field: ${movedField.label}`, `Previous position: ${sourcePosition}`, `New position: ${targetPosition}`, `From position: ${sourcePosition}`, `To position: ${targetPosition}` ] }; } /** * Add an option to a choice-type field */ private addOption(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult { const { optionText, fieldId, fieldLabel, fieldNumber } = params; if (!optionText) { return { success: false, message: 'Option text is required', changes: [], errors: ['Missing new option'] }; } const lookupOptions: FieldLookupOptions = { formConfig }; if (fieldId) lookupOptions.fieldId = fieldId; if (fieldLabel) lookupOptions.fieldLabel = fieldLabel; if (fieldNumber) lookupOptions.fieldNumber = fieldNumber; const field = this.findField(lookupOptions); if (!field.found || field.index === undefined) { return { success: false, message: field.error || 'Field not found', changes: [], errors: [field.error || 'Field not found'] }; } const fieldIndex = field.index; const targetField = formConfig.questions[fieldIndex]; if (!targetField) { return { success: false, message: 'Target field is undefined.', changes: [], errors: ['Field not found at the specified index.'], }; } if (!this.isChoiceField(targetField.type)) { return { success: false, message: `Cannot add options to field type ${targetField.type}`, changes: [], errors: [`Invalid field type for adding options: ${targetField.type}`] }; } const updatedQuestions = [...formConfig.questions]; const updatedField = { ...updatedQuestions[fieldIndex] } as any; if (!updatedField.options) { updatedField.options = []; } const newQuestionOption: QuestionOption = { text: optionText, value: optionText.toLowerCase().replace(/\s+/g, '_') }; updatedField.options.push(newQuestionOption); updatedQuestions[fieldIndex] = updatedField; const updatedFormConfig: FormConfig = { ...formConfig, questions: updatedQuestions }; return { success: true, modifiedFormConfig: updatedFormConfig, message: `Successfully added option "${optionText}" to field "${targetField.label}"`, changes: [ `Field: ${targetField.label}`, `Added option: ${optionText}`, `Total options: ${updatedField.options.length}` ] }; } /** * Modify properties of an existing field */ private modifyField(formConfig: FormConfig, params: ModificationParameters): ModificationOperationResult { const lookupOptions: FieldLookupOptions = { formConfig }; if (params.fieldId) lookupOptions.fieldId = params.fieldId; if (params.fieldNumber) lookupOptions.fieldNumber = params.fieldNumber; // Don't use fieldLabel for lookup in modify operations - it's the new value const field = this.findField(lookupOptions); if (!field.found || field.index === undefined) { return { success: false, message: field.error || 'Field not found for modification', changes: [], errors: [field.error || 'Field not found'] }; } const fieldIndex = field.index; const updatedQuestions = [...formConfig.questions]; const currentField = updatedQuestions[fieldIndex]; if (!currentField) { return { success: false, message: `Field at index ${fieldIndex} not found.`, changes: [], errors: [`Invalid field index ${fieldIndex}`], }; } const changes: string[] = []; const updatedField = { ...currentField }; // Handle fieldLabel as the new label value (not for lookup) if (params.fieldLabel) { changes.push(`Updated label from "${currentField.label}" to "${params.fieldLabel}"`); updatedField.label = params.fieldLabel; } if (params.newValue) { changes.push(`Updated label from "${currentField.label}" to "${params.newValue}"`); updatedField.label = params.newValue; } if (params.description !== undefined) { changes.push(`Description updated from "${currentField.description || ''}" to "${params.description}"`); updatedField.description = params.description; } if (params.placeholder !== undefined) { changes.push(`Placeholder updated from "${currentField.placeholder || ''}" to "${params.placeholder}"`); updatedField.placeholder = params.placeholder; } if (params.required !== undefined) { changes.push(`Required updated from ${currentField.required} to ${params.required}`); updatedField.required = params.required; } if (changes.length === 0) { return { success: false, message: 'No modifications specified', changes: [], errors: ['No modification parameters specified'] }; } updatedQuestions[fieldIndex] = updatedField; const updatedFormConfig: FormConfig = { ...formConfig, questions: updatedQuestions }; return { success: true, modifiedFormConfig: updatedFormConfig, message: `Successfully modified field "${updatedField.label}"`, changes }; } /** * Find a field in the form by number, ID, or label */ private findField(options: FieldLookupOptions): { found: boolean; index?: number; error?: string } { const { formConfig, fieldNumber, fieldId, fieldLabel } = options; if (fieldNumber !== undefined) { const index = fieldNumber - 1; if (index < 0 || index >= formConfig.questions.length) { return { found: false, error: `Field number ${fieldNumber} is out of range` }; } return { found: true, index }; } if (fieldId) { const index = formConfig.questions.findIndex(q => q.id === fieldId); if (index === -1) { return { found: false, error: `Field with ID "${fieldId}" not found` }; } return { found: true, index }; } if (fieldLabel) { const lowercasedLabel = fieldLabel.toLowerCase(); const index = formConfig.questions.findIndex(q => q.label.toLowerCase().includes(lowercasedLabel)); if (index === -1) { return { found: false, error: `Field with label "${fieldLabel}" not found` }; } return { found: true, index }; } return { found: false, error: 'No field identifier provided (number, ID, or label)' }; } /** * Generate a unique field ID using UUID * This ensures stable, unique identifiers across all form fields */ private generateNextFieldId(formConfig: FormConfig): string { const existingIds = new Set(formConfig.questions.map(q => q.id).filter(id => id)); // Generate a new UUID and ensure it's unique (extremely unlikely collision) let newId = randomUUID(); while (existingIds.has(newId)) { newId = randomUUID(); } return newId; } /** * Generate a default label for a new field */ private generateDefaultFieldLabel(fieldType: QuestionType | string): string { const labelMap: Record<string, string> = { [QuestionType.TEXT]: 'Text Field', [QuestionType.EMAIL]: 'Email Address', [QuestionType.NUMBER]: 'Number Field', [QuestionType.CHOICE]: 'Choice Field', [QuestionType.RATING]: 'Rating Field', [QuestionType.FILE]: 'File Upload', [QuestionType.DATE]: 'Date Field', [QuestionType.TIME]: 'Time Field', [QuestionType.TEXTAREA]: 'Text Area', [QuestionType.DROPDOWN]: 'Dropdown Field', [QuestionType.CHECKBOXES]: 'Checkboxes Field', [QuestionType.LINEAR_SCALE]: 'Linear Scale', [QuestionType.MULTIPLE_CHOICE]: 'Multiple Choice', [QuestionType.PHONE]: 'Phone Number', [QuestionType.URL]: 'URL Field', [QuestionType.SIGNATURE]: 'Signature Field', [QuestionType.PAYMENT]: 'Payment Field', [QuestionType.MATRIX]: 'Matrix Field', }; return labelMap[fieldType as QuestionType] || `${fieldType.toString().replace(/_/g, ' ')} Field`; } /** * Check if a field type supports options */ private isChoiceField(fieldType: QuestionType | string): boolean { return [ QuestionType.MULTIPLE_CHOICE, QuestionType.DROPDOWN, QuestionType.CHECKBOXES, QuestionType.MATRIX ].includes(fieldType as QuestionType); } /** * Create a properly configured question based on its type */ private createQuestionConfig( fieldType: QuestionType, fieldId: string, fieldLabel: string, params: ModificationParameters ): QuestionConfig { const baseConfig = { id: fieldId, label: fieldLabel, required: false, description: params.description, placeholder: params.placeholder }; switch (fieldType) { case QuestionType.RATING: return this.createRatingQuestionConfig(baseConfig, params); case QuestionType.DATE: return this.createDateQuestionConfig(baseConfig, params); case QuestionType.NUMBER: return this.createNumberQuestionConfig(baseConfig, params); case QuestionType.LINEAR_SCALE: return this.createLinearScaleQuestionConfig(baseConfig, params); case QuestionType.CHOICE: case QuestionType.MULTIPLE_CHOICE: case QuestionType.DROPDOWN: case QuestionType.CHECKBOXES: return this.createChoiceQuestionConfig(fieldType, baseConfig, params); case QuestionType.TIME: return this.createTimeQuestionConfig(baseConfig, params); case QuestionType.FILE: return this.createFileQuestionConfig(baseConfig, params); case QuestionType.SIGNATURE: return this.createSignatureQuestionConfig(baseConfig, params); case QuestionType.PAYMENT: return this.createPaymentQuestionConfig(baseConfig, params); case QuestionType.MATRIX: return this.createMatrixQuestionConfig(baseConfig, params); default: // For other types, return basic configuration return { ...baseConfig, type: fieldType }; } } /** * Create a rating question configuration */ private createRatingQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.RATING, minRating: 1, maxRating: 5, style: 'stars' as any, // Default to stars showNumbers: false, lowLabel: 'Poor', highLabel: 'Excellent' }; } /** * Create a date question configuration */ private createDateQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.DATE, dateFormat: 'MM/DD/YYYY', includeTime: false }; } /** * Create a number question configuration */ private createNumberQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.NUMBER, step: 1, decimalPlaces: 0, useThousandSeparator: false }; } /** * Create a linear scale question configuration */ private createLinearScaleQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.LINEAR_SCALE, minValue: 1, maxValue: 10, step: 1, showNumbers: true, lowLabel: 'Not likely', highLabel: 'Very likely' }; } /** * Create a choice question configuration (multiple choice, dropdown, checkboxes) */ private createChoiceQuestionConfig( fieldType: QuestionType, baseConfig: any, params: ModificationParameters ): QuestionConfig { const defaultOptions = params.options ? params.options.map((option: string) => ({ text: option, value: option.toLowerCase().replace(/\s+/g, '_') })) : [ { text: 'Option 1', value: 'option_1' }, { text: 'Option 2', value: 'option_2' }, { text: 'Option 3', value: 'option_3' } ]; const config = { ...baseConfig, type: fieldType, options: defaultOptions, allowOther: false }; // Add type-specific properties if (fieldType === QuestionType.CHECKBOXES) { return { ...config, minSelections: 0, maxSelections: undefined, // No limit layout: 'vertical' as any }; } if (fieldType === QuestionType.DROPDOWN) { return { ...config, searchable: false, dropdownPlaceholder: 'Select an option...' }; } if (fieldType === QuestionType.MULTIPLE_CHOICE) { return { ...config, randomizeOptions: false, layout: 'vertical' as any }; } return config; } /** * Create a time question configuration */ private createTimeQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.TIME, format: '12' as any, // 12-hour format minuteStep: 15 }; } /** * Create a file question configuration */ private createFileQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.FILE, allowedTypes: ['image/*', 'application/pdf', '.doc', '.docx'], maxFileSize: 10, // 10MB maxFiles: 1, multiple: false, uploadText: 'Click to upload or drag and drop' }; } /** * Create a signature question configuration */ private createSignatureQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.SIGNATURE, canvasWidth: 400, canvasHeight: 200, penColor: '#000000', backgroundColor: '#ffffff' }; } /** * Create a payment question configuration */ private createPaymentQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.PAYMENT, currency: 'USD', fixedAmount: true, amount: 1000, // $10.00 in cents paymentDescription: 'Payment', acceptedMethods: ['card' as any] }; } /** * Create a matrix question configuration */ private createMatrixQuestionConfig( baseConfig: any, _params: ModificationParameters ): QuestionConfig { return { ...baseConfig, type: QuestionType.MATRIX, rows: [ { id: 'row_1', text: 'Row 1', value: 'row_1' }, { id: 'row_2', text: 'Row 2', value: 'row_2' }, { id: 'row_3', text: 'Row 3', value: 'row_3' } ], columns: [ { id: 'col_1', text: 'Column 1', value: 'col_1' }, { id: 'col_2', text: 'Column 2', value: 'col_2' }, { id: 'col_3', text: 'Column 3', value: 'col_3' } ], defaultResponseType: MatrixResponseType.SINGLE_SELECT, allowMultiplePerRow: false, requireAllRows: false, randomizeRows: false, randomizeColumns: false, mobileLayout: 'stacked' as any, showHeadersOnMobile: true }; } /** * Convert a TallyForm API response to a FormConfig object with comprehensive field details */ public convertTallyFormToFormConfig(tallyForm: TallyForm & { questions?: any[], settings?: any }): FormConfig { return { title: tallyForm.title || 'Untitled Form', description: tallyForm.description || undefined, questions: (tallyForm.questions || []).map((field: any) => { // Base question configuration const baseConfig: any = { id: field.id || field.uuid, type: field.type as QuestionType, label: field.label || field.title || 'Untitled Question', description: field.description, required: field.required || field.validations?.some((r: any) => r.type === 'required') || false, placeholder: field.placeholder, order: field.order, }; // Map validation rules if present if (field.validation || field.validations) { baseConfig.validation = this.mapValidationRules(field.validation || field.validations); } // Map conditional logic if present if (field.logic) { baseConfig.logic = field.logic; } // Type-specific configurations const typeSpecificConfig = this.mapTypeSpecificProperties(field); return { ...baseConfig, ...typeSpecificConfig }; }), settings: { submissionBehavior: tallyForm.settings?.redirectOnCompletion ? SubmissionBehavior.REDIRECT : SubmissionBehavior.MESSAGE, redirectUrl: tallyForm.settings?.redirectOnCompletionUrl || undefined, showProgressBar: tallyForm.settings?.showProgressBar, allowDrafts: tallyForm.settings?.allowDrafts, showQuestionNumbers: tallyForm.settings?.showQuestionNumbers, shuffleQuestions: tallyForm.settings?.shuffleQuestions, maxSubmissions: tallyForm.settings?.maxSubmissions, requireAuth: tallyForm.settings?.requireAuth, collectEmail: tallyForm.settings?.collectEmail, closeDate: tallyForm.settings?.closeDate, openDate: tallyForm.settings?.openDate, submissionMessage: tallyForm.settings?.submissionMessage, sendConfirmationEmail: tallyForm.settings?.sendConfirmationEmail, allowMultipleSubmissions: tallyForm.settings?.allowMultipleSubmissions }, metadata: (() => { const meta: any = tallyForm.metadata || {}; return { createdAt: tallyForm.createdAt, updatedAt: tallyForm.updatedAt, version: 1, // Initial version isPublished: tallyForm.isPublished, ...(meta.tags && { tags: meta.tags }), ...(meta.category && { category: meta.category }), ...(meta.workspaceId && { workspaceId: meta.workspaceId }) }; })() }; } /** * Map validation rules from API response format to FormConfig format */ private mapValidationRules(validations: any): any { if (!validations) return undefined; if (Array.isArray(validations)) { return { rules: validations.map((rule: any) => ({ type: rule.type, errorMessage: rule.errorMessage, enabled: rule.enabled, ...rule // Include all other properties })) }; } return validations; } /** * Map type-specific properties from API field to FormConfig question */ private mapTypeSpecificProperties(field: any): any { const config: any = {}; // Choice-based field properties if (field.options) { config.options = field.options.map((opt: any) => ({ id: opt.id, text: opt.text || opt.label, value: opt.value, isDefault: opt.isDefault, imageUrl: opt.imageUrl, metadata: opt.metadata })); } if (field.allowOther !== undefined) config.allowOther = field.allowOther; if (field.randomizeOptions !== undefined) config.randomizeOptions = field.randomizeOptions; if (field.layout) config.layout = field.layout; // Text field properties if (field.minLength !== undefined) config.minLength = field.minLength; if (field.maxLength !== undefined) config.maxLength = field.maxLength; if (field.format) config.format = field.format; if (field.textRows !== undefined) config.rows = field.textRows; // Map back to rows for FormConfig if (field.autoResize !== undefined) config.autoResize = field.autoResize; // Number field properties if (field.min !== undefined) config.min = field.min; if (field.max !== undefined) config.max = field.max; if (field.step !== undefined) config.step = field.step; if (field.decimalPlaces !== undefined) config.decimalPlaces = field.decimalPlaces; if (field.useThousandSeparator !== undefined) config.useThousandSeparator = field.useThousandSeparator; if (field.numberCurrency) config.currency = field.numberCurrency; // Map back to currency for FormConfig // Date/Time field properties if (field.minDate) config.minDate = field.minDate; if (field.maxDate) config.maxDate = field.maxDate; if (field.dateFormat) config.dateFormat = field.dateFormat; if (field.includeTime !== undefined) config.includeTime = field.includeTime; if (field.defaultDate) config.defaultDate = field.defaultDate; if (field.timeFormat) config.format = field.timeFormat; // Time format goes to format field if (field.minuteStep !== undefined) config.minuteStep = field.minuteStep; if (field.defaultTime) config.defaultTime = field.defaultTime; // Rating field properties if (field.minRating !== undefined) config.minRating = field.minRating; if (field.maxRating !== undefined) config.maxRating = field.maxRating; if (field.ratingLabels) config.ratingLabels = field.ratingLabels; if (field.ratingStyle) config.style = field.ratingStyle; if (field.showNumbers !== undefined) config.showNumbers = field.showNumbers; if (field.lowLabel) config.lowLabel = field.lowLabel; if (field.highLabel) config.highLabel = field.highLabel; // Linear scale properties if (field.minValue !== undefined) config.minValue = field.minValue; if (field.maxValue !== undefined) config.maxValue = field.maxValue; // File upload properties if (field.allowedTypes) config.allowedTypes = field.allowedTypes; if (field.maxFileSize !== undefined) config.maxFileSize = field.maxFileSize; if (field.maxFiles !== undefined) config.maxFiles = field.maxFiles; if (field.multiple !== undefined) config.multiple = field.multiple; if (field.uploadText) config.uploadText = field.uploadText; if (field.enableDragDrop !== undefined) config.enableDragDrop = field.enableDragDrop; if (field.dragDropHint) config.dragDropHint = field.dragDropHint; if (field.showProgress !== undefined) config.showProgress = field.showProgress; if (field.showPreview !== undefined) config.showPreview = field.showPreview; if (field.fileRestrictions) config.fileRestrictions = field.fileRestrictions; if (field.sizeConstraints) config.sizeConstraints = field.sizeConstraints; // Dropdown properties if (field.searchable !== undefined) config.searchable = field.searchable; if (field.dropdownPlaceholder) config.dropdownPlaceholder = field.dropdownPlaceholder; if (field.multiSelect !== undefined) config.multiSelect = field.multiSelect; if (field.maxSelections !== undefined) config.maxSelections = field.maxSelections; if (field.minSelections !== undefined) config.minSelections = field.minSelections; if (field.imageOptions !== undefined) config.imageOptions = field.imageOptions; if (field.searchConfig) config.searchConfig = field.searchConfig; // Checkbox properties if (field.selectionConstraints) config.selectionConstraints = field.selectionConstraints; if (field.searchOptions) config.searchOptions = field.searchOptions; // Email field properties if (field.validateFormat !== undefined) config.validateFormat = field.validateFormat; if (field.suggestDomains !== undefined) config.suggestDomains = field.suggestDomains; // Phone field properties if (field.phoneFormat) config.format = field.phoneFormat; if (field.customPattern) config.customPattern = field.customPattern; if (field.autoFormat !== undefined) config.autoFormat = field.autoFormat; // URL field properties if (field.allowedSchemes) config.allowedSchemes = field.allowedSchemes; // Signature field properties if (field.canvasWidth !== undefined) config.canvasWidth = field.canvasWidth; if (field.canvasHeight !== undefined) config.canvasHeight = field.canvasHeight; if (field.penColor) config.penColor = field.penColor; if (field.backgroundColor) config.backgroundColor = field.backgroundColor; // Matrix field properties if (field.rows && Array.isArray(field.rows)) config.rows = field.rows; // Matrix rows, not text rows if (field.columns) config.columns = field.columns; if (field.defaultResponseType) config.defaultResponseType = field.defaultResponseType; if (field.allowMultiplePerRow !== undefined) config.allowMultiplePerRow = field.allowMultiplePerRow; if (field.requireAllRows !== undefined) config.requireAllRows = field.requireAllRows; if (field.randomizeRows !== undefined) config.randomizeRows = field.randomizeRows; if (field.randomizeColumns !== undefined) config.randomizeColumns = field.randomizeColumns; if (field.mobileLayout) config.mobileLayout = field.mobileLayout; if (field.showHeadersOnMobile !== undefined) config.showHeadersOnMobile = field.showHeadersOnMobile; if (field.defaultCellValidation) config.defaultCellValidation = field.defaultCellValidation; if (field.customClasses) config.customClasses = field.customClasses; // Payment field properties if (field.amount !== undefined) config.amount = field.amount; if (field.currency) config.currency = field.currency; // Payment currency if (field.fixedAmount !== undefined) config.fixedAmount = field.fixedAmount; if (field.minAmount !== undefined) config.minAmount = field.minAmount; if (field.maxAmount !== undefined) config.maxAmount = field.maxAmount; if (field.paymentDescription) config.paymentDescription = field.paymentDescription; if (field.acceptedMethods) config.acceptedMethods = field.acceptedMethods; // Custom properties if (field.customProperties) config.customProperties = field.customProperties; if (field.metadata) config.metadata = field.metadata; return config; } /** * Validate the form configuration */ public validateFormConfig(formConfig: FormConfig): { valid: boolean; errors: string[] } { const errors: string[] = []; if (!formConfig.title) { errors.push('Form title is required.'); } if (!formConfig.questions || formConfig.questions.length === 0) { errors.push('Form must have at least one question.'); } // Check for duplicate field IDs const fieldIds = formConfig.questions.map(q => q.id).filter(id => id); const duplicateIds = fieldIds.filter((id, index) => fieldIds.indexOf(id) !== index); if (duplicateIds.length > 0) { const uniqueDuplicates = [...new Set(duplicateIds)]; errors.push(`Duplicate field IDs found: ${uniqueDuplicates.join(', ')}`); } // Check for empty field labels const emptyLabelCount = formConfig.questions.filter(q => !q.label || q.label.trim() === '').length; if (emptyLabelCount > 0) { errors.push(`${emptyLabelCount} field(s) have empty labels`); } // Check for choice fields without options const choiceFieldsWithoutOptions = formConfig.questions.filter(q => this.isChoiceField(q.type) && (!(q as any).options || (q as any).options.length === 0) ).length; if (choiceFieldsWithoutOptions > 0) { errors.push(`${choiceFieldsWithoutOptions} choice field(s) have no options`); } // Add more validation rules as needed... const logicErrors = DependencyValidator.validateAllLogic(formConfig); errors.push(...logicErrors); return { valid: errors.length === 0, errors }; } }
  • NLP parser helper converting natural language modification commands to structured operations for use in form_modification_tool
    export class FormModificationParser { private questionTypeMap!: Map<string, QuestionType>; private commandPatterns!: CommandPattern[]; constructor() { this.initializeQuestionTypeMap(); this.initializeCommandPatterns(); } /** * Parse a natural language command into a structured modification operation */ public parseCommand(command: string): ParsedModificationCommand { const normalizedCommand = this.normalizeCommand(command); for (const pattern of this.commandPatterns) { const match = normalizedCommand.match(pattern.pattern); if (match) { const parameters = pattern.extractor(match); return { operation: pattern.operation, target: pattern.target, parameters, confidence: pattern.confidence, rawCommand: command, ambiguous: this.isAmbiguous(parameters, pattern.operation) }; } } // If no pattern matches, return an ambiguous result return { operation: ModificationOperation.MODIFY_FIELD, target: ModificationTarget.FIELD, parameters: {}, confidence: 0, rawCommand: command, ambiguous: true, clarificationNeeded: `I couldn't understand the command "${command}". Please try rephrasing it using patterns like "add [field type] field", "make [field] required", or "remove question [number]".` }; } /** * Parse multiple commands from a single input */ public parseMultipleCommands(input: string): ParsedModificationCommand[] { const commands = this.splitCommands(input); return commands.map(cmd => this.parseCommand(cmd)); } /** * Check if a command needs clarification */ public needsClarification(parsed: ParsedModificationCommand): boolean { return parsed.ambiguous === true || parsed.confidence < 0.7; } /** * Generate suggestions for ambiguous commands */ public generateSuggestions(command: string): string[] { const suggestions: string[] = []; const normalized = this.normalizeCommand(command); // If it mentions adding, suggest add patterns if (normalized.includes('add')) { suggestions.push('add text field', 'add email field', 'add phone field'); } // If it mentions required, suggest requirement patterns if (normalized.includes('required') || normalized.includes('require')) { suggestions.push('make field 1 required', 'make email required'); } // If it mentions remove, suggest removal patterns if (normalized.includes('remove') || normalized.includes('delete')) { suggestions.push('remove question 3', 'remove field 2'); } return suggestions; } private initializeQuestionTypeMap(): void { this.questionTypeMap = new Map([ ['text', QuestionType.TEXT], ['input', QuestionType.TEXT], ['textarea', QuestionType.TEXTAREA], ['long text', QuestionType.TEXTAREA], ['email', QuestionType.EMAIL], ['email address', QuestionType.EMAIL], ['number', QuestionType.NUMBER], ['numeric', QuestionType.NUMBER], ['phone', QuestionType.PHONE], ['phone number', QuestionType.PHONE], ['url', QuestionType.URL], ['website', QuestionType.URL], ['link', QuestionType.URL], ['date', QuestionType.DATE], ['time', QuestionType.TIME], ['rating', QuestionType.RATING], ['stars', QuestionType.RATING], ['file', QuestionType.FILE], ['upload', QuestionType.FILE], ['attachment', QuestionType.FILE], ['signature', QuestionType.SIGNATURE], ['sign', QuestionType.SIGNATURE], ['payment', QuestionType.PAYMENT], ['pay', QuestionType.PAYMENT], ['choice', QuestionType.MULTIPLE_CHOICE], ['multiple choice', QuestionType.MULTIPLE_CHOICE], ['select', QuestionType.DROPDOWN], ['dropdown', QuestionType.DROPDOWN], ['checkboxes', QuestionType.CHECKBOXES], ['checkbox', QuestionType.CHECKBOXES], ['multi select', QuestionType.CHECKBOXES], ['scale', QuestionType.LINEAR_SCALE], ['linear scale', QuestionType.LINEAR_SCALE], ['slider', QuestionType.LINEAR_SCALE] ]); } private initializeCommandPatterns(): void { this.commandPatterns = [ // Add field patterns { pattern: /add\s+(?:a\s+)?(?:new\s+)?(\w+(?:\s+\w+)*)\s+(?:field|question|input)/i, operation: ModificationOperation.ADD_FIELD, target: ModificationTarget.FIELD, confidence: 0.9, extractor: (match) => { const fieldTypeKey = match[1]?.toLowerCase(); return { fieldType: fieldTypeKey ? this.questionTypeMap.get(fieldTypeKey) : undefined, fieldLabel: fieldTypeKey ? this.generateDefaultLabel(fieldTypeKey) : undefined }; } }, // Remove field by number { pattern: /(?:remove|delete)\s+(?:question|field)\s+(?:number\s+)?(\d+)/i, operation: ModificationOperation.REMOVE_FIELD, target: ModificationTarget.FIELD, confidence: 0.95, extractor: (match) => ({ fieldNumber: match[1] ? parseInt(match[1]) : undefined }) }, // Remove field by label/name { pattern: /(?:remove|delete)\s+(?:the\s+)?(.+?)\s+(?:field|question)/i, operation: ModificationOperation.REMOVE_FIELD, target: ModificationTarget.FIELD, confidence: 0.8, extractor: (match) => ({ fieldLabel: match[1]?.trim() }) }, // Make field required { pattern: /make\s+(?:question|field)\s+(?:number\s+)?(\d+)\s+required/i, operation: ModificationOperation.MAKE_REQUIRED, target: ModificationTarget.FIELD, confidence: 0.9, extractor: (match) => ({ fieldNumber: match[1] ? parseInt(match[1]) : undefined, required: true }) }, // Make field required by name { pattern: /make\s+(?:the\s+)?(.+?)\s+(?:field|question)\s+required/i, operation: ModificationOperation.MAKE_REQUIRED, target: ModificationTarget.FIELD, confidence: 0.85, extractor: (match) => ({ fieldLabel: match[1]?.trim(), required: true }) }, // Make field optional { pattern: /make\s+(?:question|field)\s+(?:number\s+)?(\d+)\s+optional/i, operation: ModificationOperation.MAKE_OPTIONAL, target: ModificationTarget.FIELD, confidence: 0.9, extractor: (match) => ({ fieldNumber: match[1] ? parseInt(match[1]) : undefined, required: false }) }, // Update form title { pattern: /(?:update|change|set)\s+(?:the\s+)?(?:form\s+)?title\s+to\s+"([^"]+)"/i, operation: ModificationOperation.UPDATE_TITLE, target: ModificationTarget.FORM, confidence: 0.95, extractor: (match) => ({ newValue: match[1] }) }, // Update form description { pattern: /(?:update|change|set)\s+(?:the\s+)?(?:form\s+)?description\s+to\s+"([^"]+)"/i, operation: ModificationOperation.UPDATE_DESCRIPTION, target: ModificationTarget.FORM, confidence: 0.95, extractor: (match) => ({ newValue: match[1] }) }, // Reorder fields { pattern: /move\s+(?:question|field)\s+(\d+)\s+(?:to\s+)?(?:position\s+)?(\d+)/i, operation: ModificationOperation.REORDER_FIELD, target: ModificationTarget.FIELD, confidence: 0.9, extractor: (match) => ({ sourcePosition: match[1] ? parseInt(match[1]) : undefined, targetPosition: match[2] ? parseInt(match[2]) : undefined }) }, // Add option to choice field { pattern: /add\s+option\s+"([^"]+)"\s+to\s+(?:question|field)\s+(\d+)/i, operation: ModificationOperation.ADD_OPTION, target: ModificationTarget.OPTION, confidence: 0.9, extractor: (match) => ({ optionText: match[1], fieldNumber: match[2] ? parseInt(match[2]) : undefined }) }, // Generic field modification { pattern: /(?:update|change|modify)\s+(?:question|field)\s+(\d+)/i, operation: ModificationOperation.MODIFY_FIELD, target: ModificationTarget.FIELD, confidence: 0.7, extractor: (match) => ({ fieldNumber: match[1] ? parseInt(match[1]) : undefined }) } ]; } private normalizeCommand(command: string): string { // Preserve quoted strings while normalizing the rest let normalized = command.trim(); const quotedStrings: string[] = []; // Extract quoted strings normalized = normalized.replace(/"([^"]+)"/g, (_match, content) => { quotedStrings.push(content); return `"__QUOTED_${quotedStrings.length - 1}__"`; }); // Normalize non-quoted parts normalized = normalized .toLowerCase() .replace(/[^\w\s"_]/g, ' ') .replace(/\s+/g, ' '); // Restore quoted strings quotedStrings.forEach((content, index) => { normalized = normalized.replace(`"__quoted_${index}__"`, `"${content}"`); }); return normalized; } private splitCommands(input: string): string[] { // Split on common separators while preserving quoted strings const separators = /(?:\s*[,;]\s*)|(?:\s+and\s+)|(?:\s+then\s+)/i; return input.split(separators) .map(cmd => cmd.trim()) .filter(cmd => cmd.length > 0); } private isAmbiguous(parameters: ModificationParameters, operation: ModificationOperation): boolean { switch (operation) { case ModificationOperation.ADD_FIELD: return !parameters.fieldType; case ModificationOperation.REMOVE_FIELD: case ModificationOperation.MODIFY_FIELD: return !parameters.fieldNumber && !parameters.fieldLabel; case ModificationOperation.MAKE_REQUIRED: case ModificationOperation.MAKE_OPTIONAL: return !parameters.fieldNumber && !parameters.fieldLabel; default: return false; } } private generateDefaultLabel(fieldType: string): string { const typeLabel = fieldType.split(' ') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); return `${typeLabel} Field`; } }

Latest Blog Posts

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/learnwithcc/tally-mcp'

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