Four Tools
Detailed explanation of the four core tools in the mcp-server, including their handlers and design rationale.
Four Tools
The mcp-server module serves as the central processing unit for various business operations, exposing a set of well-defined tools that encapsulate specific functionalities. This document delves into the implementation details of these tools, explaining their purpose, how they interact with the database, and the design choices that shaped their current form. Understanding these tools is crucial for anyone looking to extend or integrate with the mcp (Money App Core Platform) backend.
Overview
The mcp-server currently defines a total of 4 distinct tools, each designed to address a specific business need. These tools are declared in mcp-server/src/tools.ts and are intended to be callable by external systems, potentially including AI agents or other microservices. The tools are:
get_appointments_todayget_customer_by_phoneget_revenue_summaryupdate_appointment_status
Each tool comes with a clear description and a structured input schema, making them self-documenting and easy to integrate.
Tool Implementations
The mcp-server/src/tools.ts file is the heart of these tool definitions. It not only defines the input schemas using Zod but also provides the asynchronous handler functions that execute the actual business logic, primarily interacting with the Drizzle ORM for database operations.
get_appointments_today
This tool is designed to retrieve all appointments scheduled for a specific professional on the current day, taking into account a given IANA timezone.
The input for this tool is validated by GetAppointmentsTodayInput, which expects a professionalId (a UUID string) and an optional timezone (defaulting to "America/Sao_Paulo"). The TOOL_DEFINITIONS array provides a corresponding JSON Schema representation for external consumption.
The getAppointmentsToday handler function first parses its input. A key challenge in this function is accurately determining "today" across different timezones. It achieves this by:
- Getting the current
Date. - Formatting this date into a
YYYY-MM-DDstring usingIntl.DateTimeFormatwith the specifiedtimezone. This gives a "naive" date string for the target timezone. - Constructing "naive" start and end
Dateobjects (e.g.,2024-01-01T00:00:00Zand2024-01-01T23:59:59.999Z). - Calculating the timezone offset using the custom
tzOffsetMshelper function. This function determines the difference in milliseconds between a UTC date and its representation in the target timezone. - Adjusting the "naive" start and end dates by this offset to get the correct UTC timestamps that correspond to the beginning and end of "today" in the specified timezone.
With the correct start and end timestamps, the function then queries the database using Drizzle ORM. It selects id, startsAt, status from appointments, name from clients, and profession from professionals. It performs innerJoin operations between appointments, clients, and professionals tables. The where clause filters appointments by professionalId and ensures startsAt falls within the calculated start and end range. Results are ordered by startsAt.
The function returns a structured object containing the professionalId, timezone, the date string, and an array of appointments, each mapped to a consistent output format. Basic try/catch error handling is in place to return an error message if a database issue occurs.
get_customer_by_phone
This tool is designed to look up customer records based on a phone number, providing flexibility for various input formats.
The input is validated by GetCustomerByPhoneInput, which requires a phone string with a minimum length of 4 characters.
The getCustomerByPhone handler function first normalizes the input phone number using normalizeBrazilianPhone from mcp-server/src/phone.ts. This helper function strips non-digit characters and ensures the number is prefixed with "55" if it's a Brazilian number and not already in E.164 format. It then generates variants of the phone number using getPhoneVariants, which includes both the full E.164 format (e.g., "5511987654321") and a shorter version without the "55" prefix (e.g., "11987654321") if applicable. This is crucial for matching phone numbers that might be stored in different formats in the database.
The function then queries the clients table, using inArray to match any of the generated phone variants. If no clients are found, it returns an empty matches array. If clients are found, it iterates through each matching client. For each client, it performs a separate query to fetch their last 5 appointments, ordered by startsAt in descending order. This results in a list of client details along with their recent appointment history.
The output includes the normalized phone number and an array of matches, each containing client details and their appointments. Error handling is present to catch database issues.
get_revenue_summary
This tool provides a summary of revenue for a given professional over a specified rolling period (week, month, or year).
The input is validated by GetRevenueSummaryInput, requiring a professionalId and a period enum value ("week", "month", or "year").
The getRevenueSummary handler function calculates the start date for the summary period based on the period input. "Week" covers the last 7 days, "month" the last 30, and "year" the last 365.
It first fetches the sessionPrice for the specified professionalId from the professionals table. This is a critical step, as the revenue calculation relies on this value. If the professional is not found, it returns an error.
Next, it performs an aggregate query on the appointments table. It counts appointments that belong to the professionalId, have a paymentStatus of "captured", and fall within the calculated start and now date range. The sql<number> template literal is used to cast the count(*) result to an integer.
The appointmentCount is then used, along with the professional.sessionPrice, to calculate totalRevenueCents and averageTicketCents. A check prevents division by zero for averageTicketCents if no appointments are found.
The function returns a summary object including professionalId, period, windowStart, windowEnd, appointmentCount, totalRevenueCents, averageTicketCents, and a fixed currency of "BRL". Error handling is included for database operations or if the professional is not found.
update_appointment_status
This tool allows for updating the status of a specific appointment, with built-in ownership verification and idempotency.
The input is validated by UpdateAppointmentStatusInput, which requires appointmentId, professionalId, and a status from the APPOINTMENT_STATUSES enum. An optional reason string can also be provided.
The updateAppointmentStatus handler function first parses its input. It then fetches the existing appointment record using the appointmentId. Several checks are performed:
- Appointment existence: If no appointment is found, an error is returned.
- Ownership verification: It checks if the
professionalIdprovided in the input matches theprofessionalIdof the existing appointment. If they don't match, an "Unauthorized" error is returned, preventing a professional from modifying another's appointments. - Idempotency: If the
existing.statusis already the same as the targetstatus, the function returnssuccess: trueandnoChange: true, indicating that no database write was necessary.
If the status needs updating, the function proceeds to update the appointments table. It sets the status to the new value. If a reason is provided, it also updates the notes column of the appointment. The returning() clause ensures that the updated record is returned from the database operation.
Finally, it returns success: true along with the appointment details, serialized using the serializeAppointment helper function. Error handling is in place for database issues.
Design Decisions
Several key design decisions underpin the structure and functionality of these tools:
-
Separation of Concerns (Zod vs.
TOOL_DEFINITIONS): The input validation schemas are defined using Zod (e.g.,GetAppointmentsTodayInput). This provides robust runtime validation for incoming requests. Separately, theTOOL_DEFINITIONSarray provides a JSON Schema-like representation of these inputs and tool descriptions. This separation allows the Zod schemas to be used directly in TypeScript for type safety and validation, whileTOOL_DEFINITIONScan be exposed to external systems (like an LLM agent or an OpenAPI specification) that require a standardized, descriptive format for tool discovery and invocation. While this introduces some duplication, it caters to different consumers effectively. -
Database Interaction with Drizzle ORM: All database operations are performed using Drizzle ORM. This choice provides a type-safe and fluent API for interacting with the PostgreSQL database, reducing the likelihood of SQL injection vulnerabilities and improving developer experience compared to raw SQL queries. The use of
dbandschemafrommcp-server/src/db.jscentralizes database access. -
Explicit Timezone Handling: The
getAppointmentsTodaytool explicitly handles timezones usingIntl.DateTimeFormatand a customtzOffsetMsfunction. This is a deliberate choice to ensure that "today" is resolved correctly based on the professional's local time, rather than relying on server-local time or UTC, which could lead to incorrect results for users in different geographical locations. -
Robust Phone Number Matching: The
getCustomerByPhonetool leveragesnormalizeBrazilianPhoneandgetPhoneVariantsfrommcp-server/src/phone.ts. This acknowledges the real-world complexity of phone number formats and aims to maximize match rates by trying multiple common representations, improving the user experience for customer lookups. -
Idempotency in
updateAppointmentStatus: TheupdateAppointmentStatustool includes an explicit check (if (existing.status === status)) to prevent unnecessary database writes if the appointment's status is already the target status. This makes the operation idempotent, which is a good practice for API design, especially in distributed systems where retries might occur. -
Centralized Status Definitions: The
APPOINTMENT_STATUSESarray is defined once as aconstand then used to create a Zod enum (z.enum(APPOINTMENT_STATUSES)). This ensures consistency across the application regarding valid appointment statuses and provides type safety.
Potential Improvements
While the current implementation is functional, several areas could be improved to enhance robustness, maintainability, and performance:
-
Automate
TOOL_DEFINITIONSGeneration: TheTOOL_DEFINITIONSarray manually duplicates information already present in the Zod schemas (e.g.,GetAppointmentsTodayInput). This is a source of potential inconsistencies. A build-time script or a utility function could automatically derive the JSON Schema representations forTOOL_DEFINITIONSdirectly from the Zod schemas, ensuring they always stay in sync. This would reduce boilerplate and the risk of human error inmcp-server/src/tools.ts. -
Optimize
getCustomerByPhonefor N+1 Queries: IngetCustomerByPhone, when multiple clients match a phone number, the code performs a separate database query for each client to fetch their last 5 appointments (clients.map(async (client) => { ... await db.select(...) })). This leads to an N+1 query problem, where N is the number of matching clients. A more efficient approach would be to fetch all relevant appointments in a single query using aWHERE INclause onclientIdafter retrieving the initial clients, and then group them by client in application code. This would significantly reduce database round trips and improve performance, especially for phone numbers that might match many clients. -
Refine Timezone Handling with a Library: The custom
tzOffsetMsfunction inmcp-server/src/tools.tsis a bespoke solution for timezone offset calculation. While it works, timezone logic can be notoriously complex due to daylight saving changes, historical data, and edge cases. Replacing this custom function with a well-tested, dedicated date-time library (likedate-fns-tzorluxon) would likely provide more robust and accurate timezone calculations, reducing the risk of subtle bugs and improving maintainability. -
Centralize Error Handling: The
try/catchblocks in each tool handler (e.g.,getAppointmentsToday,getCustomerByPhone) are largely repetitive, returning an object with anerrorproperty. A higher-order function or a centralized error handling utility could wrap the core logic of each handler, abstracting away the repetitivetry/catchboilerplate and ensuring consistent error response formats across all tools.
References
mcp-server/src/tools.tsmcp-server/src/phone.tsmcp-server/src/db.js