From 70250561258969f0a69f5ddbc9a20d9812977278 Mon Sep 17 00:00:00 2001 From: Stephen Waits <1+swaits@noreply.git.swaits.com> Date: Sun, 11 Oct 2026 03:01:32 +0000 Subject: [PATCH] Next steps 5-10: resources, cash flow, QB interop, backfill, HTTP, parallel-run plan --- crates/ledger-mcp/src/types.rs | 206 ++++++++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 5 deletions(-) diff --git a/crates/ledger-mcp/src/types.rs b/crates/ledger-mcp/src/types.rs index 296a5de..28aa983 100644 --- a/crates/ledger-mcp/src/types.rs +++ b/crates/ledger-mcp/src/types.rs @@ -113,6 +113,153 @@ pub struct ProfitAndLossOut { pub net_income: String, } +/// Parameters for `books.cash_flow`. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct CashFlowParams { + /// Period start, YYYY-MM-DD. Defaults to the beginning of time. + pub start: Option, + /// Period end, YYYY-MM-DD. Defaults to today. + pub end: Option, + /// Account codes treated as cash (e.g. `["1000", "1010"]`). Required: + /// the statement reconciles to the net change in these accounts. + pub cash_accounts: Vec, + /// Account codes for liabilities treated as financing (debt). + /// Defaults to empty: liabilities are operating working capital. + #[serde(default)] + pub financing_accounts: Vec, +} + +/// One cash flow line. +#[derive(Debug, Serialize, JsonSchema)] +pub struct CashFlowLineOut { + /// Human label. + pub label: String, + /// Signed amount, decimal string. Positive = inflow. + pub amount: String, +} + +/// Result of `books.cash_flow`. +#[derive(Debug, Serialize, JsonSchema)] +pub struct CashFlowOut { + /// Period start, YYYY-MM-DD. + pub start: String, + /// Period end, YYYY-MM-DD. + pub end: String, + /// Period net income, decimal string. + pub net_income: String, + /// Non-cash depreciation added back, decimal string. + pub depreciation: String, + /// Working-capital adjustments. + pub working_capital: Vec, + /// Cash from operations, decimal string. + pub cash_from_operations: String, + /// Investing flows (purchases, disposal proceeds). + pub investing: Vec, + /// Cash from investing, decimal string. + pub cash_from_investing: String, + /// Financing flows (equity, designated debt). + pub financing: Vec, + /// Cash from financing, decimal string. + pub cash_from_financing: String, + /// Net change in cash, decimal string. + pub net_change_in_cash: String, + /// Cash balance at period start, decimal string. + pub cash_beginning: String, + /// Cash balance at period end, decimal string. + pub cash_ending: String, +} + +/// Parameters for `books.import_bank_csv`. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ImportBankCsvParams { + /// The CSV text (QBO bank upload layout: Date,Description,Amount or + /// Date,Description,Expense,Income). + pub csv_text: String, + /// Account code for the bank account (the offset side of each entry). + pub bank_account_code: String, + /// Account code for the default offset (e.g. an expense account). + /// Used when no rule matches. + pub default_offset_code: String, + /// Optional categorization rules: { "pattern": "account_code" }. + /// First case-insensitive substring match on the description wins. + #[serde(default)] + pub rules: std::collections::HashMap, +} + +/// One imported bank transaction (as a draft entry). +#[derive(Debug, Serialize, JsonSchema)] +pub struct ImportedTxnOut { + /// Draft entry id. + pub entry_id: u64, + /// Transaction date, YYYY-MM-DD. + pub date: String, + /// Bank description. + pub description: String, + /// Signed amount, decimal string. + pub amount: String, + /// Account code used for the offset. + pub offset_account: String, +} + +/// Result of `books.import_bank_csv`. +#[derive(Debug, Serialize, JsonSchema)] +pub struct ImportBankCsvOut { + /// Drafts created (one per transaction). + pub imported: Vec, + /// Number of transactions imported. + pub count: usize, +} + +/// Parameters for `books.import_chart_csv`. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ImportChartCsvParams { + /// The CSV text: code,name,type,parent_code with a header row. + pub csv_text: String, +} + +/// Result of `books.import_chart_csv`. +#[derive(Debug, Serialize, JsonSchema)] +pub struct ImportChartCsvOut { + /// Account codes created, in order. + pub created: Vec, + /// Number of accounts created. + pub count: usize, +} + +/// Parameters for `books.import_local_file`. +#[derive(Debug, Deserialize, JsonSchema)] +pub struct ImportLocalFileParams { + /// Absolute path to the file on the server's filesystem. + pub path: String, + /// "receipt" or "document". + pub category: String, + /// Entry id to attach to (receipts only). Omit to file unlinked. + #[serde(default)] + pub entry_id: Option, + /// Document category for documents: formation, legal, tax, accounting, other. + #[serde(default)] + pub document_category: Option, + /// Override the file name (defaults to the path's file name). + #[serde(default)] + pub file_name: Option, + /// Optional description. + #[serde(default)] + pub description: Option, +} + +/// Result of `books.import_local_file`. +#[derive(Debug, Serialize, JsonSchema)] +pub struct ImportLocalFileOut { + /// Attachment id. + pub attachment_id: u64, + /// SHA-256 hex of the content. + pub sha256: String, + /// True if the content was already stored (deduplicated). + pub deduplicated: bool, + /// File name used. + pub file_name: String, +} + /// Parameters for `books.balance_sheet`. #[derive(Debug, Deserialize, JsonSchema)] pub struct BalanceSheetParams { @@ -244,6 +391,10 @@ pub struct PostEntryOut { /// True when a receipt is required but none is attached. Posting with /// confirmed=true fails in this state. pub receipt_missing: bool, + /// Posted entry ids that look like they split this entry to dodge the + /// receipt threshold (same date, description, and debit accounts). + /// Empty when no split is detected. + pub split_detected: Vec, } /// Parameters for `books.reverse_entry`. @@ -544,16 +695,21 @@ pub struct DigestOut { /// Parameters for `books.export`. #[derive(Debug, Deserialize, JsonSchema)] pub struct ExportParams { - /// "csv" or "json". + /// "csv", "json", "xlsx", or "iif". XLSX is base64-encoded; IIF is only + /// valid with report "journal". pub format: String, - /// "journal", "trial_balance", "profit_and_loss", or "balance_sheet". + /// "journal", "trial_balance", "profit_and_loss", "balance_sheet", or + /// "cash_flow". pub report: String, - /// Period start for profit_and_loss, YYYY-MM-DD. + /// Period start for profit_and_loss and cash_flow, YYYY-MM-DD. pub start: Option, - /// Period end for profit_and_loss, YYYY-MM-DD. + /// Period end for profit_and_loss and cash_flow, YYYY-MM-DD. pub end: Option, /// As-of date for balance_sheet, YYYY-MM-DD. pub as_of: Option, + /// Cash account codes for cash_flow (required for that report). + #[serde(default)] + pub cash_accounts: Vec, } /// Result of `books.export`. @@ -563,8 +719,12 @@ pub struct ExportOut { pub format: String, /// The requested report. pub report: String, - /// The exported content. + /// The exported content. Base64-encoded when `content_encoding` is + /// "base64" (XLSX); otherwise raw text. pub content: String, + /// "base64" for XLSX, otherwise absent. + #[serde(skip_serializing_if = "Option::is_none")] + pub content_encoding: Option, } // --------------------------------------------------------------------------- @@ -763,3 +923,39 @@ pub struct DisposeAssetOut { /// True when posting requires a receipt (bill of sale) first. pub receipt_required: bool, } + +/// Structured machine-readable tool error for receipt policy rejections. +/// +/// Tool errors surface as strings in MCP; this serializes to JSON so +/// clients can match on `code` instead of parsing prose. The human-readable +/// `message` is still included for logs and chat. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct ReceiptRequiredError { + /// Stable machine-readable code: always `"receipt_required"`. + pub code: String, + /// Human-readable explanation with the remedy. + pub message: String, + /// The entry (or asset) that needs the receipt. Set for entries. + pub entry_id: Option, + /// The asset being disposed. Set for disposals. + pub asset_id: Option, + /// Total debits moved, decimal string. + pub debits: String, + /// The policy that triggered, e.g. "strictly greater than $25.00". + pub policy: String, + /// Tool to call to satisfy the requirement. + pub remedy_tool: String, + /// Concrete next step for the agent. + pub remedy_hint: String, + /// When the requirement was triggered by anti-splitting detection: the + /// ids of the posted entries this entry appears to split. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub split_group: Vec, +} + +impl ReceiptRequiredError { + /// Serialize to the string form returned as the MCP tool error. + pub fn to_tool_error(&self) -> String { + serde_json::to_string(self).unwrap_or_else(|_| self.message.clone()) + } +}