Skip to content
This repository was archived by the owner on Feb 18, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 0 additions & 75 deletions internal/mcp/tools/customers.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package tools
import (
"context"
"fmt"
"strings"

"github.com/beetlebugorg/tekmetric-mcp/pkg/tekmetric"
"github.com/mark3labs/mcp-go/mcp"
Expand Down Expand Up @@ -131,77 +130,3 @@ func (r *Registry) handleCustomers(arguments map[string]interface{}) (*mcp.CallT
)
}

// formatCustomerSummary creates a formatted summary of a customer
func (r *Registry) formatCustomerSummary(c *tekmetric.Customer) (*mcp.CallToolResult, error) {
var summary strings.Builder

// Header
summary.WriteString(fmt.Sprintf("%s %s", c.FirstName, c.LastName))
if c.CustomerType != nil {
summary.WriteString(fmt.Sprintf(" (%s)", c.CustomerType.Name))
}
summary.WriteString(fmt.Sprintf("\nCustomer ID: %d\n\n", c.ID))

// Contact Information
if c.Email != "" {
summary.WriteString(fmt.Sprintf("Email: %s\n", c.Email))
}

if len(c.Phone) > 0 {
for _, phone := range c.Phone {
phoneType := phone.Type
if phoneType == "" {
phoneType = "Phone"
}
primary := ""
if phone.Primary {
primary = " (Primary)"
}
summary.WriteString(fmt.Sprintf("%s: %s%s\n", phoneType, phone.Number, primary))
}
}

if c.Address != nil && (c.Address.Address1 != "" || c.Address.City != "") {
summary.WriteString("\nAddress:\n")
if c.Address.Address1 != "" {
summary.WriteString(fmt.Sprintf(" %s\n", c.Address.Address1))
}
if c.Address.Address2 != "" {
summary.WriteString(fmt.Sprintf(" %s\n", c.Address.Address2))
}
if c.Address.City != "" {
cityLine := fmt.Sprintf(" %s", c.Address.City)
if c.Address.State != "" {
cityLine += fmt.Sprintf(", %s", c.Address.State)
}
if c.Address.Zip != "" {
cityLine += fmt.Sprintf(" %s", c.Address.Zip)
}
summary.WriteString(cityLine + "\n")
}
}

// Account Information
if c.EligibleForAccountsReceivable || c.CreditLimit > 0 || c.OkForMarketing {
summary.WriteString("\n")
if c.EligibleForAccountsReceivable {
summary.WriteString("Accounts Receivable: Yes\n")
}
if c.CreditLimit > 0 {
summary.WriteString(fmt.Sprintf("Credit Limit: $%.2f\n", c.CreditLimit))
}
if c.OkForMarketing {
summary.WriteString("Marketing: Yes\n")
}
}

// Notes
if c.Notes != "" {
summary.WriteString(fmt.Sprintf("\nNotes: %s\n", c.Notes))
}

// Metadata
summary.WriteString(fmt.Sprintf("\nCustomer Since: %s", c.CreatedDate.Format("January 2, 2006")))

return formatRichResult(summary.String(), c)
}
10 changes: 2 additions & 8 deletions internal/mcp/tools/employees.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
func (r *Registry) RegisterEmployeeTools(s *server.MCPServer) {
s.AddTool(
mcp.NewTool("employees",
mcp.WithDescription("Search and filter employees/technicians, or get a specific employee by ID. Supports filtering by active status, role, and more."),
mcp.WithDescription("Search employees/technicians by name or email, or get a specific employee by ID. Returns employee details including name, email, phone, role, and active status. Note: The Tekmetric API does not support filtering by role or active status - use search to find employees by name, then filter the results yourself."),
mcp.WithNumber("id",
mcp.Description("Get specific employee by ID"),
),
Expand All @@ -23,20 +23,14 @@ func (r *Registry) RegisterEmployeeTools(s *server.MCPServer) {
mcp.WithNumber("shop",
mcp.Description("Shop ID (defaults to TEKMETRIC_DEFAULT_SHOP_ID)"),
),
mcp.WithBoolean("active",
mcp.Description("Filter by active status (true for active employees only, false for inactive)"),
),
mcp.WithString("role",
mcp.Description("Filter by employee role (e.g., technician, service advisor, manager)"),
),
mcp.WithString("sort",
mcp.Description("Property to sort results by (e.g., firstName, lastName, email)"),
),
mcp.WithString("sort_direction",
mcp.Description("Sort direction (ASC or DESC)"),
),
mcp.WithNumber("limit",
mcp.Description("Maximum number of results to return (max: 100, default: 20)"),
mcp.Description("Maximum number of results to return (max: 100, default: 10)"),
),
mcp.WithNumber("page",
mcp.Description("Page number for pagination (default: 0)"),
Expand Down
76 changes: 0 additions & 76 deletions internal/mcp/tools/repair_orders.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,79 +177,3 @@ func (r *Registry) handleRepairOrders(arguments map[string]interface{}) (*mcp.Ca

return formatJSON(response)
}

// formatRepairOrderSummary creates a rich formatted summary of a repair order
func (r *Registry) formatRepairOrderSummary(ro *tekmetric.RepairOrder) (*mcp.CallToolResult, error) {
var summary strings.Builder

// Header
summary.WriteString(fmt.Sprintf("# Repair Order #%d\n\n", ro.RepairOrderNumber))

// Status
summary.WriteString(fmt.Sprintf("**Status:** %s\n", ro.RepairOrderStatus.Name))
if ro.CompletedDate != nil {
summary.WriteString(fmt.Sprintf("**Completed:** %s\n", ro.CompletedDate.Format("2006-01-02 15:04")))
}
if ro.PostedDate != nil {
summary.WriteString(fmt.Sprintf("**Posted:** %s\n", ro.PostedDate.Format("2006-01-02 15:04")))
}
summary.WriteString("\n")

// Financial Summary
summary.WriteString("## Financial Summary\n\n")
summary.WriteString(fmt.Sprintf("- **Labor:** %s\n", formatCurrency(ro.LaborSales)))
summary.WriteString(fmt.Sprintf("- **Parts:** %s\n", formatCurrency(ro.PartsSales)))
summary.WriteString(fmt.Sprintf("- **Sublet:** %s\n", formatCurrency(ro.SubletSales)))
summary.WriteString(fmt.Sprintf("- **Fees:** %s\n", formatCurrency(ro.FeeTotal)))
summary.WriteString(fmt.Sprintf("- **Discounts:** -%s\n", formatCurrency(ro.DiscountTotal)))
summary.WriteString(fmt.Sprintf("- **Taxes:** %s\n", formatCurrency(ro.Taxes)))
summary.WriteString(fmt.Sprintf("- **Total:** %s\n", formatCurrency(ro.TotalSales)))
summary.WriteString(fmt.Sprintf("- **Amount Paid:** %s\n", formatCurrency(ro.AmountPaid)))

balance := ro.TotalSales - ro.AmountPaid
if balance > 0 {
summary.WriteString(fmt.Sprintf("- **Balance Due:** %s\n", formatCurrency(balance)))
}
summary.WriteString("\n")

// Jobs
if len(ro.Jobs) > 0 {
summary.WriteString(fmt.Sprintf("## Jobs (%d)\n\n", len(ro.Jobs)))
for i, job := range ro.Jobs {
summary.WriteString(fmt.Sprintf("%d. **%s**\n", i+1, job.Name))
if job.Note != "" {
summary.WriteString(fmt.Sprintf(" - %s\n", job.Note))
}
if job.JobCategoryName != "" {
summary.WriteString(fmt.Sprintf(" - Category: %s\n", job.JobCategoryName))
}
summary.WriteString(fmt.Sprintf(" - Labor: %s | Parts: %s | Subtotal: %s\n",
formatCurrency(job.LaborTotal),
formatCurrency(job.PartsTotal),
formatCurrency(job.Subtotal)))
if job.Authorized {
summary.WriteString(" - ✓ Authorized\n")
}
}
summary.WriteString("\n")
}

// Customer Concerns
if len(ro.CustomerConcerns) > 0 {
summary.WriteString("## Customer Concerns\n\n")
for _, concern := range ro.CustomerConcerns {
summary.WriteString(fmt.Sprintf("- %s\n", concern.Concern))
}
summary.WriteString("\n")
}

// Vehicle Info
if ro.MilesIn != nil {
summary.WriteString(fmt.Sprintf("**Mileage In:** %.0f\n", *ro.MilesIn))
}
if ro.MilesOut != nil {
summary.WriteString(fmt.Sprintf("**Mileage Out:** %.0f\n", *ro.MilesOut))
}

return formatRichResult(summary.String(), ro)
}
57 changes: 0 additions & 57 deletions internal/mcp/tools/vehicles.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package tools
import (
"context"
"fmt"
"strings"

"github.com/beetlebugorg/tekmetric-mcp/pkg/tekmetric"
"github.com/mark3labs/mcp-go/mcp"
Expand Down Expand Up @@ -117,59 +116,3 @@ func (r *Registry) handleVehicles(arguments map[string]interface{}) (*mcp.CallTo
"VEHICLES",
)
}

// formatVehicleSummary creates a formatted summary of a vehicle
func (r *Registry) formatVehicleSummary(v *tekmetric.Vehicle) (*mcp.CallToolResult, error) {
var summary strings.Builder

// Header
vehicleName := fmt.Sprintf("%d %s %s", v.Year, v.Make, v.Model)
if v.SubModel != "" {
vehicleName += fmt.Sprintf(" %s", v.SubModel)
}
if v.Color != "" {
vehicleName += fmt.Sprintf(" (%s)", v.Color)
}
summary.WriteString(vehicleName + "\n")
summary.WriteString(fmt.Sprintf("Vehicle ID: %d\n\n", v.ID))

// Identification
if v.VIN != "" {
summary.WriteString(fmt.Sprintf("VIN: %s\n", v.VIN))
}
if v.LicensePlate != "" {
summary.WriteString(fmt.Sprintf("License Plate: %s\n", v.LicensePlate))
}
if v.UnitNumber != "" {
summary.WriteString(fmt.Sprintf("Unit Number: %s\n", v.UnitNumber))
}

// Mileage
if v.Mileage > 0 {
summary.WriteString(fmt.Sprintf("Current Mileage: %.0f miles\n", v.Mileage))
}

// Technical Specifications
if v.Engine != "" || v.Transmission != "" || v.DriveType != "" {
summary.WriteString("\n")
if v.Engine != "" {
summary.WriteString(fmt.Sprintf("Engine: %s\n", v.Engine))
}
if v.Transmission != "" {
summary.WriteString(fmt.Sprintf("Transmission: %s\n", v.Transmission))
}
if v.DriveType != "" {
summary.WriteString(fmt.Sprintf("Drive Type: %s\n", v.DriveType))
}
}

// Notes
if v.Notes != "" {
summary.WriteString(fmt.Sprintf("\nNotes: %s\n", v.Notes))
}

// Metadata
summary.WriteString(fmt.Sprintf("\nAdded: %s", v.CreatedDate.Format("January 2, 2006")))

return formatRichResult(summary.String(), v)
}
34 changes: 34 additions & 0 deletions pkg/tekmetric/appointments.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,42 @@ import (
"context"
"fmt"
"net/url"
"time"
)

// ============================================================================
// Models
// ============================================================================

// Appointment represents an appointment
type Appointment struct {
ID int `json:"id"`
ShopID int `json:"shopId"`
CustomerID int `json:"customerId"`
VehicleID int `json:"vehicleId"`
ServiceWriterID *int `json:"serviceWriterId"`
TechnicianID *int `json:"technicianId"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Status string `json:"status"`
CustomerConcerns string `json:"customerConcerns,omitempty"`
Notes string `json:"notes,omitempty"`
CreatedDate time.Time `json:"createdDate"`
UpdatedDate time.Time `json:"updatedDate"`
DeletedDate *time.Time `json:"deletedDate"`
}

// EnrichedAppointment represents an appointment with customer and vehicle details
type EnrichedAppointment struct {
Appointment
Customer *Customer `json:"customer,omitempty"`
Vehicle *Vehicle `json:"vehicle,omitempty"`
}

// ============================================================================
// API Methods
// ============================================================================

// AppointmentQueryParams holds query parameters for appointment searches
type AppointmentQueryParams struct {
Shop int `url:"shop,omitempty"`
Expand Down
37 changes: 37 additions & 0 deletions pkg/tekmetric/customers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,45 @@ import (
"context"
"fmt"
"net/url"
"time"
)

// ============================================================================
// Models
// ============================================================================

// CustomerType represents the type of customer
type CustomerType struct {
ID int `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
}

// Customer represents a Tekmetric customer
type Customer struct {
ID int `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
Phone []Phone `json:"phone"`
CustomerType *CustomerType `json:"customerType,omitempty"`
ContactFirstName *string `json:"contactFirstName"`
ContactLastName *string `json:"contactLastName"`
Address *Address `json:"address"`
ShopID int `json:"shopId"`
EligibleForAccountsReceivable bool `json:"eligibleForAccountsReceivable"`
CreditLimit float64 `json:"creditLimit"`
OkForMarketing bool `json:"okForMarketing"`
Notes string `json:"notes,omitempty"`
CreatedDate time.Time `json:"createdDate"`
UpdatedDate time.Time `json:"updatedDate"`
DeletedDate *time.Time `json:"deletedDate"`
}

// ============================================================================
// API Methods
// ============================================================================

// CustomerQueryParams holds query parameters for customer searches
// Note: By default, deleted records are excluded unless DeletedDateStart/DeletedDateEnd are explicitly set
type CustomerQueryParams struct {
Expand Down
32 changes: 32 additions & 0 deletions pkg/tekmetric/employees.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,40 @@ import (
"context"
"fmt"
"net/url"
"time"
)

// ============================================================================
// Models
// ============================================================================

// EmployeeRole represents an employee's role
type EmployeeRole struct {
ID int `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
}

// Employee represents an employee
type Employee struct {
ID int `json:"id"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
Phone string `json:"phone,omitempty"`
EmployeeRole *EmployeeRole `json:"employeeRole,omitempty"`
CanPerformWork bool `json:"canPerformWork"`
CertificationNumber string `json:"certificationNumber,omitempty"`
ShopID int `json:"shopId"`
CreatedDate time.Time `json:"createdDate"`
UpdatedDate time.Time `json:"updatedDate"`
DeletedDate *time.Time `json:"deletedDate"`
}

// ============================================================================
// API Methods
// ============================================================================

// EmployeeQueryParams holds query parameters for employee searches
type EmployeeQueryParams struct {
Shop int `url:"shop,omitempty"`
Expand Down
Loading