/**
* LiteFarm MCP Server - Finance Management Tools
*/
import { z } from "zod";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { LiteFarmClient } from "../litefarm-client.js";
import { ResponseFormat, type LiteFarmExpense, type LiteFarmExpenseType } from "../types.js";
import {
createToolResponse,
createErrorResponse,
formatListResponse,
formatDate,
objectToMarkdownTable,
validateRequiredFields
} from "../tool-utils.js";
/**
* Register all finance-related tools
*/
export function registerFinanceTools(server: McpServer, client: LiteFarmClient): void {
// List expense types
server.registerTool(
"litefarm_list_expense_types",
{
title: "List Expense Types",
description: `Get all available expense categories (Seeds, Equipment, Labor, etc.).
This tool retrieves all expense types that can be used when creating expenses.
Use this to prevent errors with invalid expense_type_id values.
Args:
- farm_id (string): Farm ID to list expense types for (optional, uses selected farm if not provided)
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns:
For JSON format: Array of expense type objects with schema:
[{
"expense_type_id": string, // Type ID to use in create_expense
"expense_name": string, // Display name (e.g., "Seeds", "Equipment")
"farm_id": string | null, // NULL = global type, set = farm-specific
"expense_translation_key": string,
"created_at": string,
"updated_at": string
}]
Examples:
- Use when: "What expense categories are available?"
- Use when: Before creating expenses to get valid expense_type_id values
Error Handling:
- Returns empty list if no types are found
- Returns authentication error if login fails`,
inputSchema: z.object({
farm_id: z.string()
.optional()
.describe("Farm ID (optional, uses selected farm if not provided)"),
response_format: z.nativeEnum(ResponseFormat)
.default(ResponseFormat.MARKDOWN)
.describe("Output format: 'markdown' or 'json'")
}).strict(),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
try {
// Select farm if provided
if (params.farm_id) {
await client.selectFarm(params.farm_id);
}
const farmId = client.getSelectedFarmId();
if (!farmId) {
return createErrorResponse("No farm selected. Please provide farm_id or select a farm first.");
}
const types = await client.get<LiteFarmExpenseType[]>("/expense_type");
if (!types || types.length === 0) {
return createToolResponse("No expense types found.");
}
const formatter = (type: LiteFarmExpenseType) => {
const scope = type.farm_id ? "Farm-specific" : "Global";
return `**${type.expense_name}** (ID: ${type.expense_type_id}, ${scope})`;
};
const response = formatListResponse(types, formatter, params.response_format);
return createToolResponse(response.text, response.structuredContent);
} catch (error) {
return createErrorResponse(error);
}
}
);
// List expenses
server.registerTool(
"litefarm_list_expenses",
{
title: "List Expenses",
description: `List all expenses for the currently selected farm.
This tool retrieves all expenses with their type names and values.
Args:
- farm_id (string): Farm ID to list expenses for (optional, uses selected farm if not provided)
- response_format ('markdown' | 'json'): Output format (default: 'markdown')
Returns:
For JSON format: Array of expense objects with schema:
[{
"farm_expense_id": string,
"expense_type_id": string,
"value": number,
"expense_date": string, // ISO 8601
"note": string,
"farm_id": string,
"created_at": string,
"updated_at": string
}]
Examples:
- Use when: "Show me all expenses" or "List farm expenses"
- Use when: Verifying expenses were created successfully
Error Handling:
- Returns empty list if no expenses are found
- Returns error if farm not selected`,
inputSchema: z.object({
farm_id: z.string()
.optional()
.describe("Farm ID (optional, uses selected farm if not provided)"),
response_format: z.nativeEnum(ResponseFormat)
.default(ResponseFormat.MARKDOWN)
.describe("Output format: 'markdown' or 'json'")
}).strict(),
annotations: {
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: true
}
},
async (params) => {
try {
// Select farm if provided
if (params.farm_id) {
await client.selectFarm(params.farm_id);
}
const farmId = client.getSelectedFarmId();
if (!farmId) {
return createErrorResponse("No farm selected. Please provide farm_id or select a farm first.");
}
const expenses = await client.get<LiteFarmExpense[]>(`/expense/farm/${farmId}`);
if (!expenses || expenses.length === 0) {
return createToolResponse("No expenses found for this farm.");
}
const formatter = (expense: LiteFarmExpense) => `**${expense.note}** ($${expense.value.toFixed(2)})
- Expense ID: ${expense.farm_expense_id}
- Type ID: ${expense.expense_type_id}
- Date: ${formatDate(expense.expense_date)}
- Created: ${formatDate(expense.created_at!)}`;
const response = formatListResponse(expenses, formatter, params.response_format);
return createToolResponse(response.text, response.structuredContent);
} catch (error) {
return createErrorResponse(error);
}
}
);
// Create expense
server.registerTool(
"litefarm_create_expense",
{
title: "Create Expense",
description: `Create a new expense in the LiteFarm system.
This tool creates an expense with specified type, value, date, and note.
All fields (expense_type_id, value, expense_date, note) are required.
Args:
- expense_type_id (string): Expense type ID (required, use litefarm_list_expense_types to get valid IDs)
- value (number): Expense amount in dollars (required)
- expense_date (string): Date of expense in ISO 8601 format (required, e.g., "2025-01-01T00:00:00.000Z")
- note (string): Description of expense (required, 1-255 characters)
- picture (string): URL to picture/receipt (optional)
- farm_id (string): Farm ID (optional, uses selected farm if not provided)
Returns:
The newly created expense object with generated farm_expense_id
Examples:
- Use when: "Create expense for $200 of seeds on 2025-01-01"
- Use when: "Log a $50 equipment maintenance expense"
- Don't use when: Tracking revenue (use litefarm_create_revenue if available)
Error Handling:
- Returns validation error if required fields are missing
- Returns error if invalid expense_type_id
- Returns error if note is empty or exceeds 255 characters`,
inputSchema: z.object({
expense_type_id: z.string()
.min(1, "Expense type ID is required")
.describe("Expense type ID (required, use litefarm_list_expense_types to get valid IDs)"),
value: z.number()
.min(0, "Value must be non-negative")
.describe("Expense amount in dollars"),
expense_date: z.string()
.min(1, "Expense date is required")
.describe("Date of expense in ISO 8601 format (e.g., '2025-01-01T00:00:00.000Z')"),
note: z.string()
.min(1, "Note is required")
.max(255, "Note must not exceed 255 characters")
.describe("Description of expense (1-255 characters)"),
picture: z.string()
.optional()
.describe("URL to picture/receipt"),
farm_id: z.string()
.optional()
.describe("Farm ID (optional, uses selected farm if not provided)")
}).strict(),
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: true
}
},
async (params) => {
try {
// Validate required fields
const validation = validateRequiredFields(params, ["expense_type_id", "value", "expense_date", "note"]);
if (!validation.valid) {
return createErrorResponse(`Missing required fields: ${validation.missing.join(", ")}`);
}
// Select farm if provided
if (params.farm_id) {
await client.selectFarm(params.farm_id);
}
const farmId = client.getSelectedFarmId();
if (!farmId) {
return createErrorResponse("No farm selected. Please provide farm_id or select a farm first.");
}
// Build expense payload
const expenseData: Partial<LiteFarmExpense> = {
farm_id: farmId, // REQUIRED: farm_id must be in body per hasFarmAccess middleware
expense_type_id: params.expense_type_id,
value: params.value,
expense_date: params.expense_date,
note: params.note,
...(params.picture && { picture: params.picture })
};
// POST to /expense/farm/:farm_id with array of expenses
// Note: API returns 201 with no body, so we can't get the created expense back
await client.post(`/expense/farm/${farmId}`, [expenseData]);
const markdown = `✅ **Expense Created Successfully!**
${objectToMarkdownTable({
"Type ID": params.expense_type_id,
"Amount": `$${params.value.toFixed(2)}`,
"Date": formatDate(params.expense_date),
"Note": params.note,
"Status": "Created (201)"
})}`;
return createToolResponse(markdown, { created: true, ...expenseData });
} catch (error) {
return createErrorResponse(error);
}
}
);
}