Skip to main content
Glama
andymillar84-cyber

mcp-cliniko

demo_invoice_generation

Create test patients and appointments, then learn how to generate invoices (note: invoices are created via the web interface, not API).

Instructions

Demo: Generate test patients and appointments, then show how to create invoices. NOTE: Cliniko API is READ-ONLY for invoices - they must be created via the web interface.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
target_dateNoTarget date for appointments (YYYY-MM-DD format). Defaults to today
num_patientsNoNumber of test patients to generate (max 10 for rate limits)
num_appointmentsNoNumber of appointments to generate (max 20 for rate limits)
clear_existingNoClear existing test data before generating new data
display_formatNoHow to display the resultsdetailed

Implementation Reference

  • The function `registerDemoInvoiceTools` is called from `src/index.ts` (line 63) with `toolRegistry` and `clinikoClient`. It registers the tool 'demo_invoice_generation' on the registry via `toolRegistry.tool(...)`.
    export function registerDemoInvoiceTools(toolRegistry: any, clinikoClient: ClinikoClient) {
      toolRegistry.tool(
        'demo_invoice_generation',
        {
          description: 'Demo: Generate test patients and appointments, then show how to create invoices. NOTE: Cliniko API is READ-ONLY for invoices - they must be created via the web interface.',
          inputSchema: {
            type: 'object',
            properties: {
              target_date: {
                type: 'string',
                description: 'Target date for appointments (YYYY-MM-DD format). Defaults to today'
              },
              num_patients: {
                type: 'number',
                minimum: 1,
                maximum: 10,
                default: 5,
                description: 'Number of test patients to generate (max 10 for rate limits)'
              },
              num_appointments: {
                type: 'number',
                minimum: 1,
                maximum: 20,
                default: 10,
                description: 'Number of appointments to generate (max 20 for rate limits)'
              },
              clear_existing: {
                type: 'boolean',
                default: true,
                description: 'Clear existing test data before generating new data'
              },
              display_format: {
                type: 'string',
                enum: ['summary', 'detailed', 'json'],
                default: 'detailed',
                description: 'How to display the results'
              }
            }
          }
        },
        async (args: any) => {
          const startTime = Date.now();
          const target_date = args.target_date || new Date().toISOString().split('T')[0];
          const num_patients = args.num_patients || 5;
          const num_appointments = args.num_appointments || 10;
          
          const results = {
            phase: '',
            cleared_data: false,
            generated: {
              patients: 0,
              appointments: 0,
              invoices_found: 0,
            },
            invoices: [] as any[],
            errors: [] as string[],
            execution_time_ms: 0,
            invoice_creation_instructions: true
          };
    
          try {
            // Phase 1: Clear existing test data if requested
            if (args.clear_existing) {
              console.log('šŸ“§ Phase 1: Clearing existing test data...\n');
              results.phase = 'Clearing existing test data';
              
              try {
                const testPatientsResponse = await clinikoClient.listPatients({ per_page: 100 });
                const testPatients = (testPatientsResponse.patients || []).filter((p: any) => 
                  p.email && (p.email.includes('@test.cliniko.com') || p.last_name?.includes('_TEST'))
                );
    
                console.log(`  Found ${testPatients.length} test patients to remove`);
    
                for (const patient of testPatients) {
                  try {
                    await delay(500); // Rate limiting
                    await clinikoClient.deletePatient(patient.id);
                    console.log(`  āœ… Deleted patient: ${patient.first_name} ${patient.last_name}`);
                  } catch (e) {
                    // Continue
                  }
                }
    
                results.cleared_data = true;
                console.log('  āœ… Test data cleared\n');
              } catch (error: any) {
                console.log('  āš ļø Warning: Could not clear all test data\n');
              }
            }
    
            // Phase 2: Get required reference data
            console.log('šŸ“Š Phase 2: Fetching reference data...\n');
            results.phase = 'Fetching reference data';
            
            await delay(1000);
            const [practResponse, apptTypeResponse, businessResponse] = await Promise.all([
              clinikoClient.listPractitioners({ per_page: 20 }),
              clinikoClient.listAppointmentTypes({ per_page: 20 }),
              clinikoClient.listBusinesses()
            ]);
            
            const practitioners = practResponse.practitioners || [];
            const appointmentTypes = apptTypeResponse.appointment_types || [];
            const businesses = businessResponse.businesses || [];
    
            console.log(`  Found ${practitioners.length} practitioners`);
            console.log(`  Found ${appointmentTypes.length} appointment types`);
            console.log(`  Found ${businesses.length} businesses\n`);
    
            if (practitioners.length === 0 || appointmentTypes.length === 0 || businesses.length === 0) {
              throw new Error('Missing required data: practitioners, appointment types, or businesses');
            }
    
            // Phase 3: Generate test patients
            console.log(`šŸ‘„ Phase 3: Generating ${num_patients} test patients...\n`);
            results.phase = 'Generating test patients';
            
            const createdPatients: any[] = [];
            const firstNames = ['John', 'Jane', 'Bob', 'Alice', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry'];
            const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez'];
            
            for (let i = 0; i < num_patients; i++) {
              const firstName = firstNames[i % firstNames.length];
              const lastName = lastNames[i % lastNames.length];
              const timestamp = Date.now();
              
              const patientData = {
                first_name: firstName,
                last_name: `${lastName}_TEST_${timestamp}`,
                email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${timestamp}@test.cliniko.com`,
                date_of_birth: `1980-01-${String(i + 1).padStart(2, '0')}`,
              };
    
              try {
                console.log(`  Creating patient ${i + 1}/${num_patients}...`);
                await delay(1000); // Rate limiting
                const patient = await clinikoClient.createPatient(patientData);
                createdPatients.push(patient);
                results.generated.patients++;
                console.log(`  āœ… Created: ${firstName} ${patientData.last_name}`);
              } catch (error: any) {
                console.log(`  āŒ Failed to create patient ${i + 1}: ${error.message}`);
                if (error.message.includes('429')) {
                  console.log('  āš ļø Rate limit hit - waiting 5 seconds...');
                  await delay(5000);
                }
              }
            }
            console.log('');
    
            if (createdPatients.length === 0) {
              throw new Error('Could not create any test patients');
            }
    
            // Phase 4: Generate test appointments
            console.log(`šŸ“… Phase 4: Generating ${num_appointments} test appointments for ${target_date}...\n`);
            results.phase = 'Generating test appointments';
            
            const createdAppointments: any[] = [];
            const startHour = 9;
            const minuteInterval = 30;
            
            for (let i = 0; i < num_appointments; i++) {
              const patient = createdPatients[i % createdPatients.length];
              const practitioner = practitioners[i % practitioners.length];
              const appointmentType = appointmentTypes[i % appointmentTypes.length];
              const business = businesses[0];
              
              const totalMinutes = i * minuteInterval;
              const appointmentHour = startHour + Math.floor(totalMinutes / 60);
              const appointmentMinute = totalMinutes % 60;
              
              const startsAt = `${target_date}T${String(appointmentHour).padStart(2, '0')}:${String(appointmentMinute).padStart(2, '0')}:00Z`;
              
              const appointmentData = {
                patient_id: patient.id,
                practitioner_id: practitioner.id,
                appointment_type_id: appointmentType.id,
                business_id: business.id,
                starts_at: startsAt,
                notes: `Test appointment for invoice demo`
              };
    
              try {
                console.log(`  Creating appointment ${i + 1}/${num_appointments} at ${appointmentHour}:${String(appointmentMinute).padStart(2, '0')}...`);
                await delay(1000); // Rate limiting
                const appointment = await clinikoClient.createAppointment(appointmentData);
                createdAppointments.push(appointment);
                results.generated.appointments++;
                console.log(`  āœ… Created appointment at ${appointmentHour}:${String(appointmentMinute).padStart(2, '0')}`);
              } catch (error: any) {
                console.log(`  āŒ Failed to create appointment ${i + 1}: ${error.message}`);
                if (error.message.includes('429')) {
                  console.log('  āš ļø Rate limit hit - waiting 5 seconds...');
                  await delay(5000);
                }
              }
            }
            console.log('');
    
            // Phase 5: Invoice Creation Instructions
            console.log('šŸ’° Phase 5: Invoice Creation Instructions\n');
            console.log('=' .repeat(60));
            console.log('āš ļø  IMPORTANT: CLINIKO API LIMITATIONS');
            console.log('=' .repeat(60));
            console.log('\n  The Cliniko API is READ-ONLY for invoices.');
            console.log('  Invoices CANNOT be created programmatically via the API.\n');
            
            console.log('šŸ“ HOW TO CREATE INVOICES IN CLINIKO:\n');
            console.log('  Option 1: From an Appointment');
            console.log('  --------------------------------');
            console.log('  1. Go to the appointment in Cliniko');
            console.log('  2. Click "Create Invoice" button');
            console.log('  3. Review and save the invoice\n');
            
            console.log('  Option 2: Bulk Invoice Creation');
            console.log('  --------------------------------');
            console.log('  1. Go to Invoices → Bulk Invoice');
            console.log('  2. Select date range and filters');
            console.log('  3. Select appointments to invoice');
            console.log('  4. Generate invoices in bulk\n');
            
            console.log('  Option 3: Manual Invoice Creation');
            console.log('  ---------------------------------');
            console.log('  1. Go to Invoices → New Invoice');
            console.log('  2. Select patient and practitioner');
            console.log('  3. Add appointment(s) or line items');
            console.log('  4. Set payment terms and save\n');
            
            console.log('šŸ”„ AUTOMATION OPTIONS:\n');
            console.log('  • Set up automatic invoice creation rules in Settings');
            console.log('  • Use third-party integrations (Zapier, Pipedream)');
            console.log('  • Configure appointment type billing defaults\n');
    
            // Phase 6: Check for existing invoices
            console.log('šŸ” Phase 6: Checking for existing invoices...\n');
            results.phase = 'Checking for invoices';
            
            let totalInvoicesFound = 0;
            for (const appointment of createdAppointments) {
              try {
                await delay(500);
                const invoicesResponse = await clinikoClient.getAppointmentInvoices(appointment.id);
                if (invoicesResponse.invoices && invoicesResponse.invoices.length > 0) {
                  results.invoices.push(...invoicesResponse.invoices);
                  totalInvoicesFound += invoicesResponse.invoices.length;
                  console.log(`  āœ… Found ${invoicesResponse.invoices.length} invoice(s) for appointment ${appointment.id}`);
                }
              } catch (error: any) {
                // Silently skip 404s
              }
            }
            
            results.generated.invoices_found = totalInvoicesFound;
            
            if (totalInvoicesFound === 0) {
              console.log('  ā„¹ļø No invoices found for the created appointments.');
              console.log('  This is expected - please create them manually in Cliniko.\n');
            }
    
            // Calculate execution time
            results.execution_time_ms = Date.now() - startTime;
            
            // Final summary
            console.log('\n' + '='.repeat(60));
            console.log('šŸŽ‰ DEMO COMPLETE!');
            console.log('='.repeat(60) + '\n');
            
            console.log('šŸ“Š Summary:');
            console.log(`  āœ… Test data cleared: ${results.cleared_data ? 'Yes' : 'No'}`);
            console.log(`  šŸ‘„ Patients created: ${results.generated.patients}/${num_patients}`);
            console.log(`  šŸ“… Appointments created: ${results.generated.appointments}/${num_appointments}`);
            console.log(`  šŸ“„ Existing invoices found: ${results.generated.invoices_found}`);
            console.log(`  ā±ļø Execution time: ${(results.execution_time_ms / 1000).toFixed(2)} seconds`);
            console.log(`  šŸ“† Target Date: ${target_date}\n`);
            
            console.log('✨ Next Steps:');
            console.log('  1. Log into Cliniko web interface');
            console.log('  2. Navigate to the appointments for ' + target_date);
            console.log('  3. Create invoices using one of the methods above');
            console.log('  4. Use display_invoices_for_date tool to view them\n');
    
            return {
              success: true,
              message: `Demo complete. Created ${results.generated.patients} patients and ${results.generated.appointments} appointments. Please create invoices manually in Cliniko.`,
              results
            };
    
          } catch (error: any) {
            console.error(`\nāŒ Demo failed: ${error.message}\n`);
            
            if (error.message.includes('429')) {
              console.log('šŸ’” Tip: Cliniko has strict rate limits (200 requests per 5 min)');
              console.log('   Wait a few minutes before trying again.\n');
            }
            
            results.errors.push(error.message);
            results.execution_time_ms = Date.now() - startTime;
            
            return {
              success: false,
              message: `Demo failed: ${error.message}`,
              results
            };
          }
        }
      );
  • The async handler function that executes the 'demo_invoice_generation' tool logic. It generates test patients and appointments, then provides instructions for creating invoices (since the Cliniko API is read-only for invoices).
    async (args: any) => {
      const startTime = Date.now();
      const target_date = args.target_date || new Date().toISOString().split('T')[0];
      const num_patients = args.num_patients || 5;
      const num_appointments = args.num_appointments || 10;
      
      const results = {
        phase: '',
        cleared_data: false,
        generated: {
          patients: 0,
          appointments: 0,
          invoices_found: 0,
        },
        invoices: [] as any[],
        errors: [] as string[],
        execution_time_ms: 0,
        invoice_creation_instructions: true
      };
    
      try {
        // Phase 1: Clear existing test data if requested
        if (args.clear_existing) {
          console.log('šŸ“§ Phase 1: Clearing existing test data...\n');
          results.phase = 'Clearing existing test data';
          
          try {
            const testPatientsResponse = await clinikoClient.listPatients({ per_page: 100 });
            const testPatients = (testPatientsResponse.patients || []).filter((p: any) => 
              p.email && (p.email.includes('@test.cliniko.com') || p.last_name?.includes('_TEST'))
            );
    
            console.log(`  Found ${testPatients.length} test patients to remove`);
    
            for (const patient of testPatients) {
              try {
                await delay(500); // Rate limiting
                await clinikoClient.deletePatient(patient.id);
                console.log(`  āœ… Deleted patient: ${patient.first_name} ${patient.last_name}`);
              } catch (e) {
                // Continue
              }
            }
    
            results.cleared_data = true;
            console.log('  āœ… Test data cleared\n');
          } catch (error: any) {
            console.log('  āš ļø Warning: Could not clear all test data\n');
          }
        }
    
        // Phase 2: Get required reference data
        console.log('šŸ“Š Phase 2: Fetching reference data...\n');
        results.phase = 'Fetching reference data';
        
        await delay(1000);
        const [practResponse, apptTypeResponse, businessResponse] = await Promise.all([
          clinikoClient.listPractitioners({ per_page: 20 }),
          clinikoClient.listAppointmentTypes({ per_page: 20 }),
          clinikoClient.listBusinesses()
        ]);
        
        const practitioners = practResponse.practitioners || [];
        const appointmentTypes = apptTypeResponse.appointment_types || [];
        const businesses = businessResponse.businesses || [];
    
        console.log(`  Found ${practitioners.length} practitioners`);
        console.log(`  Found ${appointmentTypes.length} appointment types`);
        console.log(`  Found ${businesses.length} businesses\n`);
    
        if (practitioners.length === 0 || appointmentTypes.length === 0 || businesses.length === 0) {
          throw new Error('Missing required data: practitioners, appointment types, or businesses');
        }
    
        // Phase 3: Generate test patients
        console.log(`šŸ‘„ Phase 3: Generating ${num_patients} test patients...\n`);
        results.phase = 'Generating test patients';
        
        const createdPatients: any[] = [];
        const firstNames = ['John', 'Jane', 'Bob', 'Alice', 'Charlie', 'Diana', 'Eve', 'Frank', 'Grace', 'Henry'];
        const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez'];
        
        for (let i = 0; i < num_patients; i++) {
          const firstName = firstNames[i % firstNames.length];
          const lastName = lastNames[i % lastNames.length];
          const timestamp = Date.now();
          
          const patientData = {
            first_name: firstName,
            last_name: `${lastName}_TEST_${timestamp}`,
            email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}${timestamp}@test.cliniko.com`,
            date_of_birth: `1980-01-${String(i + 1).padStart(2, '0')}`,
          };
    
          try {
            console.log(`  Creating patient ${i + 1}/${num_patients}...`);
            await delay(1000); // Rate limiting
            const patient = await clinikoClient.createPatient(patientData);
            createdPatients.push(patient);
            results.generated.patients++;
            console.log(`  āœ… Created: ${firstName} ${patientData.last_name}`);
          } catch (error: any) {
            console.log(`  āŒ Failed to create patient ${i + 1}: ${error.message}`);
            if (error.message.includes('429')) {
              console.log('  āš ļø Rate limit hit - waiting 5 seconds...');
              await delay(5000);
            }
          }
        }
        console.log('');
    
        if (createdPatients.length === 0) {
          throw new Error('Could not create any test patients');
        }
    
        // Phase 4: Generate test appointments
        console.log(`šŸ“… Phase 4: Generating ${num_appointments} test appointments for ${target_date}...\n`);
        results.phase = 'Generating test appointments';
        
        const createdAppointments: any[] = [];
        const startHour = 9;
        const minuteInterval = 30;
        
        for (let i = 0; i < num_appointments; i++) {
          const patient = createdPatients[i % createdPatients.length];
          const practitioner = practitioners[i % practitioners.length];
          const appointmentType = appointmentTypes[i % appointmentTypes.length];
          const business = businesses[0];
          
          const totalMinutes = i * minuteInterval;
          const appointmentHour = startHour + Math.floor(totalMinutes / 60);
          const appointmentMinute = totalMinutes % 60;
          
          const startsAt = `${target_date}T${String(appointmentHour).padStart(2, '0')}:${String(appointmentMinute).padStart(2, '0')}:00Z`;
          
          const appointmentData = {
            patient_id: patient.id,
            practitioner_id: practitioner.id,
            appointment_type_id: appointmentType.id,
            business_id: business.id,
            starts_at: startsAt,
            notes: `Test appointment for invoice demo`
          };
    
          try {
            console.log(`  Creating appointment ${i + 1}/${num_appointments} at ${appointmentHour}:${String(appointmentMinute).padStart(2, '0')}...`);
            await delay(1000); // Rate limiting
            const appointment = await clinikoClient.createAppointment(appointmentData);
            createdAppointments.push(appointment);
            results.generated.appointments++;
            console.log(`  āœ… Created appointment at ${appointmentHour}:${String(appointmentMinute).padStart(2, '0')}`);
          } catch (error: any) {
            console.log(`  āŒ Failed to create appointment ${i + 1}: ${error.message}`);
            if (error.message.includes('429')) {
              console.log('  āš ļø Rate limit hit - waiting 5 seconds...');
              await delay(5000);
            }
          }
        }
        console.log('');
    
        // Phase 5: Invoice Creation Instructions
        console.log('šŸ’° Phase 5: Invoice Creation Instructions\n');
        console.log('=' .repeat(60));
        console.log('āš ļø  IMPORTANT: CLINIKO API LIMITATIONS');
        console.log('=' .repeat(60));
        console.log('\n  The Cliniko API is READ-ONLY for invoices.');
        console.log('  Invoices CANNOT be created programmatically via the API.\n');
        
        console.log('šŸ“ HOW TO CREATE INVOICES IN CLINIKO:\n');
        console.log('  Option 1: From an Appointment');
        console.log('  --------------------------------');
        console.log('  1. Go to the appointment in Cliniko');
        console.log('  2. Click "Create Invoice" button');
        console.log('  3. Review and save the invoice\n');
        
        console.log('  Option 2: Bulk Invoice Creation');
        console.log('  --------------------------------');
        console.log('  1. Go to Invoices → Bulk Invoice');
        console.log('  2. Select date range and filters');
        console.log('  3. Select appointments to invoice');
        console.log('  4. Generate invoices in bulk\n');
        
        console.log('  Option 3: Manual Invoice Creation');
        console.log('  ---------------------------------');
        console.log('  1. Go to Invoices → New Invoice');
        console.log('  2. Select patient and practitioner');
        console.log('  3. Add appointment(s) or line items');
        console.log('  4. Set payment terms and save\n');
        
        console.log('šŸ”„ AUTOMATION OPTIONS:\n');
        console.log('  • Set up automatic invoice creation rules in Settings');
        console.log('  • Use third-party integrations (Zapier, Pipedream)');
        console.log('  • Configure appointment type billing defaults\n');
    
        // Phase 6: Check for existing invoices
        console.log('šŸ” Phase 6: Checking for existing invoices...\n');
        results.phase = 'Checking for invoices';
        
        let totalInvoicesFound = 0;
        for (const appointment of createdAppointments) {
          try {
            await delay(500);
            const invoicesResponse = await clinikoClient.getAppointmentInvoices(appointment.id);
            if (invoicesResponse.invoices && invoicesResponse.invoices.length > 0) {
              results.invoices.push(...invoicesResponse.invoices);
              totalInvoicesFound += invoicesResponse.invoices.length;
              console.log(`  āœ… Found ${invoicesResponse.invoices.length} invoice(s) for appointment ${appointment.id}`);
            }
          } catch (error: any) {
            // Silently skip 404s
          }
        }
        
        results.generated.invoices_found = totalInvoicesFound;
        
        if (totalInvoicesFound === 0) {
          console.log('  ā„¹ļø No invoices found for the created appointments.');
          console.log('  This is expected - please create them manually in Cliniko.\n');
        }
    
        // Calculate execution time
        results.execution_time_ms = Date.now() - startTime;
        
        // Final summary
        console.log('\n' + '='.repeat(60));
        console.log('šŸŽ‰ DEMO COMPLETE!');
        console.log('='.repeat(60) + '\n');
        
        console.log('šŸ“Š Summary:');
        console.log(`  āœ… Test data cleared: ${results.cleared_data ? 'Yes' : 'No'}`);
        console.log(`  šŸ‘„ Patients created: ${results.generated.patients}/${num_patients}`);
        console.log(`  šŸ“… Appointments created: ${results.generated.appointments}/${num_appointments}`);
        console.log(`  šŸ“„ Existing invoices found: ${results.generated.invoices_found}`);
        console.log(`  ā±ļø Execution time: ${(results.execution_time_ms / 1000).toFixed(2)} seconds`);
        console.log(`  šŸ“† Target Date: ${target_date}\n`);
        
        console.log('✨ Next Steps:');
        console.log('  1. Log into Cliniko web interface');
        console.log('  2. Navigate to the appointments for ' + target_date);
        console.log('  3. Create invoices using one of the methods above');
        console.log('  4. Use display_invoices_for_date tool to view them\n');
    
        return {
          success: true,
          message: `Demo complete. Created ${results.generated.patients} patients and ${results.generated.appointments} appointments. Please create invoices manually in Cliniko.`,
          results
        };
    
      } catch (error: any) {
        console.error(`\nāŒ Demo failed: ${error.message}\n`);
        
        if (error.message.includes('429')) {
          console.log('šŸ’” Tip: Cliniko has strict rate limits (200 requests per 5 min)');
          console.log('   Wait a few minutes before trying again.\n');
        }
        
        results.errors.push(error.message);
        results.execution_time_ms = Date.now() - startTime;
        
        return {
          success: false,
          message: `Demo failed: ${error.message}`,
          results
        };
      }
    }
  • The input schema for the 'demo_invoice_generation' tool, defining parameters: target_date (string), num_patients (number 1-10), num_appointments (number 1-20), clear_existing (boolean), display_format (enum: summary/detailed/json).
    description: 'Demo: Generate test patients and appointments, then show how to create invoices. NOTE: Cliniko API is READ-ONLY for invoices - they must be created via the web interface.',
    inputSchema: {
      type: 'object',
      properties: {
        target_date: {
          type: 'string',
          description: 'Target date for appointments (YYYY-MM-DD format). Defaults to today'
        },
        num_patients: {
          type: 'number',
          minimum: 1,
          maximum: 10,
          default: 5,
          description: 'Number of test patients to generate (max 10 for rate limits)'
        },
        num_appointments: {
          type: 'number',
          minimum: 1,
          maximum: 20,
          default: 10,
          description: 'Number of appointments to generate (max 20 for rate limits)'
        },
        clear_existing: {
          type: 'boolean',
          default: true,
          description: 'Clear existing test data before generating new data'
        },
        display_format: {
          type: 'string',
          enum: ['summary', 'detailed', 'json'],
          default: 'detailed',
          description: 'How to display the results'
        }
      }
    }
  • A secondary tool 'display_invoices_for_date' also registered in the same file, which displays existing invoices for a given date (read-only).
    toolRegistry.tool(
      'display_invoices_for_date',
      {
        description: 'Display all existing invoices for a specific date (READ-ONLY). Invoices must be created manually in Cliniko.',
        inputSchema: {
          type: 'object',
          properties: {
            target_date: {
              type: 'string',
              description: 'Date to display invoices for (YYYY-MM-DD format)'
            },
            display_format: {
              type: 'string',
              enum: ['summary', 'detailed', 'json'],
              default: 'detailed',
              description: 'How to display the invoices'
            }
          },
          required: ['target_date']
        }
      },
      async (args: any) => {
        const { target_date, display_format = 'detailed' } = args;
        
        try {
          console.log(`šŸ” Fetching existing invoices for ${target_date}...\n`);
          console.log('āš ļø NOTE: Invoices are read-only via API.');
          console.log('   Any invoices shown were created manually in Cliniko.\n');
          
          const invoicesResponse = await clinikoClient.listInvoices({
            issued_at_from: target_date,
            issued_at_to: target_date,
            per_page: 100
          });
          
          const invoices = invoicesResponse.invoices || [];
          
          if (invoices.length === 0) {
            return {
              success: false,
              message: `No invoices found for ${target_date}. Invoices must be created manually in the Cliniko web interface.`,
              results: {
                target_date,
                invoice_count: 0,
                display_format,
                note: 'The Cliniko API is read-only for invoices. Please create them via the web interface.'
              }
            };
          }
    
          console.log(`Found ${invoices.length} invoice(s) for ${target_date}\n`);
          
          if (display_format === 'json') {
            return {
              success: true,
              message: `Found ${invoices.length} invoices`,
              results: {
                target_date,
                invoice_count: invoices.length,
                invoices
              }
            };
          }
    
          let totalValue = 0;
          
          if (display_format === 'summary') {
            console.log('šŸ“„ Invoice Summary:');
            console.log('-'.repeat(60));
            
            invoices.forEach((inv: any, idx: number) => {
              console.log(`\nInvoice ${idx + 1}: #${inv.invoice_number || inv.id}`);
              console.log(`  Patient: ${inv.patient?.first_name} ${inv.patient?.last_name}`);
              console.log(`  Total: $${inv.total || 0}`);
              console.log(`  Status: ${inv.status}`);
              totalValue += (inv.total || 0);
            });
          } else {
            // Detailed format
            console.log('šŸ“„ Detailed Invoices:');
            console.log('='.repeat(60));
            
            invoices.forEach((inv: any, idx: number) => {
              console.log(`\nInvoice ${idx + 1}:`);
              console.log(`  Number: #${inv.invoice_number || inv.id}`);
              console.log(`  Patient: ${inv.patient?.first_name} ${inv.patient?.last_name}`);
              console.log(`  Practitioner: ${inv.practitioner?.name || 'Unknown'}`);
              console.log(`  Issue Date: ${inv.issued_at}`);
              console.log(`  Status: ${inv.status}`);
              console.log(`  Payment Terms: ${inv.payment_terms || 'Not specified'}`);
              console.log(`  Total: $${inv.total || 0}`);
              
              if (inv.invoice_items && inv.invoice_items.length > 0) {
                console.log(`  Items:`);
                inv.invoice_items.forEach((item: any) => {
                  console.log(`    - ${item.description}: $${item.unit_price} x ${item.quantity}`);
                });
              }
              
              if (inv.notes) {
                console.log(`  Notes: ${inv.notes}`);
              }
              
              totalValue += (inv.total || 0);
            });
          }
          
          console.log('\n' + '-'.repeat(60));
          console.log(`šŸ’° Total Invoice Value: $${totalValue.toFixed(2)}\n`);
    
          return {
            success: true,
            message: `Displayed ${invoices.length} invoices totaling $${totalValue.toFixed(2)}`,
            results: {
              target_date,
              invoice_count: invoices.length,
              total_value: totalValue,
              display_format
            }
          };
    
        } catch (error: any) {
          console.error(`āŒ Error fetching invoices: ${error.message}\n`);
          
          return {
            success: false,
            message: `Failed to fetch invoices: ${error.message}`,
            error: error.message
          };
        }
      }
    );
  • The `getAppointmentInvoices` method on ClinikoClient used by the demo to check for invoices linked to created appointments.
    async getAppointmentInvoices(appointmentId: number): Promise<ClinikoListResponse<Invoice>> {
      return this.request<ClinikoListResponse<Invoice>>(`/appointments/${appointmentId}/invoices`);
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, so description must disclose all behaviors. It mentions generating test data and the invoice limitation, but fails to state that clear_existing defaults to true, which could delete existing data. The demo nature is implicit, but side effects are not fully transparent.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise with two short sentences, no filler, and front-loads the purpose. Every part is necessary and immediately informative.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a demo tool with no output schema and no annotations, the description covers the core purpose and key limitation. It lacks detail on return value format or expected output, but the demo context makes this acceptable. It is sufficiently complete for its intended use.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so baseline is 3. The description adds no parameter-specific meaning beyond the schema. It repeats the general demo context but does not elaborate on how parameters affect the outcome.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it generates test patients/appointments and demonstrates invoice creation, noting the API's read-only limitation. This distinguishes it as a demo tool, but its purpose overlaps with sibling generate_test_data tools, lacking explicit differentiation.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description indicates it's a demo for invoice generation, but provides no guidance on when to use this tool vs. alternatives like generate_test_data or how_to_create_invoices. Context from sibling tools suggests overlap, but no exclusions or recommendations are given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/andymillar84-cyber/mcp-cliniko'

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