AtlatestRepositorytally
1
;;; (tally tools) - MCP tool definitions and shared helpers for tally.2
;;;3
;;; Provides shared Wise/reporting helpers used by both MCP tools and4
;;; CLI commands, plus MCP tool registration.6
(define-library (tally tools)7
(import (sigil core)8
(sigil string)9
(sigil dict)10
(sigil struct)11
(sigil json)12
(sigil time)13
(sigil error)14
(sigil mcp server)15
(wise)16
(ledger)17
(ledger report)18
(tally config)19
(tally sync))20
(export register-tally-tools!21
require-wise-client22
resolve-profile-id23
format-transaction-summary24
format-balance-summary25
run-hledger-report26
default-since-date27
default-until-date28
tool-categorize)29
(begin31
;;; Compute default since date (January 1 of the current year).32
;;; Must be called outside async contexts due to VM yield bug33
;;; with current-second.34
(define (default-since-date)35
(let ((year (number->string (time-year (current-second)))))36
(string-append year "-01-01T00:00:00Z")))38
;;; Compute default until date (December 31 of the current year).39
;;; Must be called outside async contexts due to VM yield bug40
;;; with current-second.41
(define (default-until-date)42
(let ((year (number->string (time-year (current-second)))))43
(string-append year "-12-31T23:59:59Z")))45
;;; Build a wise-client from config, raising if no token.46
(define (require-wise-client config)47
(let ((token (tally-config-wise-token config)))48
(unless token49
(error "WISE_API_TOKEN is not set"))50
(wise-client token: token)))52
;;; Find the profile matching the configured type (personal/business).53
;;; Falls back to the first available profile if no exact match.54
(define (resolve-profile-id client profile-type)55
(let ((profiles (wise-profiles client)))56
(let ((match (find (lambda (p)57
(string-ci=? (wise-profile-type p) profile-type))58
profiles)))59
(if match60
(wise-profile-id match)61
(if (pair? profiles)62
(wise-profile-id (car profiles))63
(error "No Wise profiles found"))))))65
;;; Format a wise-transaction as a summary string with description and ref.66
(define (format-transaction-summary txn)67
(let* ((details (wise-transaction-details txn))68
(desc (transaction-description details))69
(ref (wise-transaction-reference-number txn)))70
(string-append71
(wise-transaction-date txn) " "72
(wise-transaction-type txn) " "73
(number->string (wise-transaction-amount txn)) " "74
(wise-transaction-currency txn) " "75
desc76
(if (and ref (not (string-empty? ref)))77
(string-append " [" ref "]")78
""))))80
;;; Format a wise-balance as a display string with optional reserved amount.81
(define (format-balance-summary bal)82
(string-append83
(wise-balance-currency bal) ": "84
(number->string (wise-balance-amount bal))85
(let ((reserved (wise-balance-reserved-amount bal)))86
(if (and reserved (> reserved 0))87
(string-append " (reserved: " (number->string reserved) ")")88
""))))90
;;; Run an hledger report by type string.91
;;; Dispatches "bal", "reg", "is" to the appropriate hledger function.92
(define (run-hledger-report report-type journal query-args)93
(cond94
((string=? report-type "bal")95
(apply hledger-balance journal query-args))96
((string=? report-type "reg")97
(apply hledger-register journal query-args))98
((string=? report-type "is")99
(apply hledger-income-statement journal query-args))100
(else101
(hledger-command report-type file: journal args: query-args))))103
;; MCP tool handlers105
(define (tool-pull config params default-since default-until)106
(guard (exn (else107
(error (string-append "Failed to fetch transactions: "108
(format-exception exn)))))109
(let* ((client (require-wise-client config))110
(since (dict-ref params since: default-since))111
(until (dict-ref params until: default-until))112
(profile-type (dict-ref params profile:113
(tally-config-profile-type config)))114
(profile-id (resolve-profile-id client profile-type))115
(txns (tally-pull-transactions client profile-id since until))116
(count (length txns)))117
(string-append118
"Pulled " (number->string count) " transactions.\n\n"119
(if (= count 0) ""120
(string-join (map format-transaction-summary txns) "\n"))))))122
(define (tool-import config params default-since default-until)123
(guard (exn (else124
(error (string-append "Failed to import transactions: "125
(format-exception exn)))))126
(let* ((client (require-wise-client config))127
(journal (dict-ref params journal:128
(tally-config-journal-path config)))129
(since (dict-ref params since: default-since))130
(until (dict-ref params until: default-until))131
(profile-type (dict-ref params profile:132
(tally-config-profile-type config)))133
(profile-id (resolve-profile-id client profile-type))134
(count (tally-import client profile-id since until journal)))135
(string-append136
"Imported " (number->string count) " new transactions to " journal "."))))138
(define (tool-balance config params)139
(guard (exn (else140
(error (string-append "Failed to fetch balances: "141
(format-exception exn)))))142
(let* ((client (require-wise-client config))143
(profile-type (dict-ref params profile:144
(tally-config-profile-type config)))145
(profile-id (resolve-profile-id client profile-type))146
(balances (wise-balances client profile-id)))147
(if (null? balances)148
"No balances found."149
(string-join (map format-balance-summary balances) "\n")))))151
(define (tool-report config params)152
(guard (exn (else153
(error (string-append "Failed to run report: "154
(format-exception exn)))))155
(let* ((journal (dict-ref params journal:156
(tally-config-journal-path config)))157
(report-type (dict-ref params type: "bal"))158
(query (dict-ref params query: #f))159
(args (if query (list query) '())))160
(run-hledger-report report-type journal args))))162
(define (tool-categorize config params)163
(let* ((journal-path (dict-ref params journal:164
(tally-config-journal-path config)))165
(txns (guard (exn (else '()))166
(read-journal journal-path)))167
(uncategorized168
(filter (lambda (txn)169
(find (lambda (p)170
(let ((acct (journal-posting-account p)))171
(or (string-contains? acct "uncategorized")172
(string-contains? acct "unknown"))))173
(journal-transaction-postings txn)))174
txns)))175
(if (null? uncategorized)176
"No uncategorized transactions found."177
(string-append178
(number->string (length uncategorized))179
" uncategorized transactions:\n\n"180
(string-join181
(map (lambda (txn)182
(string-append183
(journal-transaction-date txn) " "184
(or (journal-transaction-code txn) "") " "185
(journal-transaction-description txn)))186
uncategorized)187
"\n")))))189
;;; Register all tally MCP tools on the given server.190
;;; Precomputes default dates here (before async context) to avoid191
;;; the VM yield bug with current-second in async goroutines.192
(define (register-tally-tools! server config)193
(let ((since-default (default-since-date))194
(until-default (default-until-date)))196
(mcp-server-register-tool! server197
"fetch-transactions"198
"Pull recent transactions from Wise API. Returns a summary of transactions found."199
#{ type: "object"200
properties: #{201
since: #{ type: "string"202
description: "Start date in ISO 8601 format" }203
until: #{ type: "string"204
description: "End date in ISO 8601 format" }205
profile: #{ type: "string"206
description: "Wise profile type: personal or business" } } }207
(lambda (params) (tool-pull config params since-default until-default)))209
(mcp-server-register-tool! server210
"import-transactions"211
"Pull transactions from Wise and write new ones to the hledger journal file. Deduplicates against existing entries automatically."212
#{ type: "object"213
properties: #{214
journal: #{ type: "string"215
description: "Path to journal file (default: from TALLY_JOURNAL env)" }216
since: #{ type: "string"217
description: "Start date in ISO 8601 format" }218
until: #{ type: "string"219
description: "End date in ISO 8601 format" }220
profile: #{ type: "string"221
description: "Wise profile type: personal or business" } } }222
(lambda (params) (tool-import config params since-default until-default)))224
(mcp-server-register-tool! server225
"wise-balances"226
"Show current Wise account balances across all currencies."227
#{ type: "object"228
properties: #{229
profile: #{ type: "string"230
description: "Wise profile type: personal or business" } } }231
(lambda (params) (tool-balance config params)))233
(mcp-server-register-tool! server234
"journal-report"235
"Run an hledger report on the journal file. Supports balance (bal), register (reg), and income statement (is)."236
#{ type: "object"237
properties: #{238
journal: #{ type: "string"239
description: "Path to journal file" }240
type: #{ type: "string"241
description: "Report type: bal, reg, or is (default: bal)" }242
query: #{ type: "string"243
description: "Optional hledger query (e.g. 'assets', 'expenses:food')" } } }244
(lambda (params) (tool-report config params)))246
(mcp-server-register-tool! server247
"uncategorized-transactions"248
"List uncategorized transactions in the journal. Use this to find transactions that need category assignment."249
#{ type: "object"250
properties: #{251
journal: #{ type: "string"252
description: "Path to journal file" } } }253
(lambda (params) (tool-categorize config params)))))255
))