@oliverames/ynab-mcp-server 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -9
- package/index.js +1140 -1169
- package/package.json +2 -3
package/index.js
CHANGED
|
@@ -1,1169 +1,1140 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { execFileSync } from "node:child_process";
|
|
4
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import * as ynab from "ynab";
|
|
8
|
-
|
|
9
|
-
// --- Init ---
|
|
10
|
-
|
|
11
|
-
let API_TOKEN = process.env.YNAB_API_TOKEN;
|
|
12
|
-
if (!API_TOKEN) {
|
|
13
|
-
try {
|
|
14
|
-
API_TOKEN = execFileSync(
|
|
15
|
-
"op", ["read",
|
|
16
|
-
{ encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] }
|
|
17
|
-
).trim();
|
|
18
|
-
} catch {
|
|
19
|
-
// 1Password CLI unavailable or item not found
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
if (!API_TOKEN) {
|
|
23
|
-
console.error("YNAB_API_TOKEN environment variable is required
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const api = new ynab.API(API_TOKEN);
|
|
28
|
-
const DEFAULT_BUDGET_ID = process.env.YNAB_BUDGET_ID;
|
|
29
|
-
|
|
30
|
-
// --- Helpers ---
|
|
31
|
-
|
|
32
|
-
function resolveBudgetId(input) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
async function
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"
|
|
318
|
-
{
|
|
319
|
-
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
"
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
)
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
)
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
)
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
({ budgetId
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
)
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
"
|
|
628
|
-
{
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
{
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
)
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
)
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
)
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
)
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
)
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
)
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
});
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
);
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
)
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
)
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
"
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
const isCategorized = (t) => (t.category_id && t.category_name !== "Uncategorized")
|
|
1142
|
-
|| (t.subtransactions && t.subtransactions.length > 0) // split transactions are categorized via subtransactions
|
|
1143
|
-
|| t.transfer_account_id; // transfers don't need categories
|
|
1144
|
-
const categorized = txns.filter(isCategorized);
|
|
1145
|
-
const uncategorized = txns.filter((t) => !isCategorized(t));
|
|
1146
|
-
return ok({
|
|
1147
|
-
total: txns.length,
|
|
1148
|
-
ready_to_approve: {
|
|
1149
|
-
count: categorized.length,
|
|
1150
|
-
transactions: categorized,
|
|
1151
|
-
},
|
|
1152
|
-
needs_category_first: {
|
|
1153
|
-
count: uncategorized.length,
|
|
1154
|
-
warning: "Do NOT approve these without assigning a category first",
|
|
1155
|
-
transactions: uncategorized,
|
|
1156
|
-
},
|
|
1157
|
-
});
|
|
1158
|
-
})
|
|
1159
|
-
);
|
|
1160
|
-
|
|
1161
|
-
// --- Start ---
|
|
1162
|
-
|
|
1163
|
-
process.on("uncaughtException", (err) => {
|
|
1164
|
-
console.error("Uncaught exception:", err);
|
|
1165
|
-
process.exit(1);
|
|
1166
|
-
});
|
|
1167
|
-
|
|
1168
|
-
const transport = new StdioServerTransport();
|
|
1169
|
-
await server.connect(transport);
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import * as ynab from "ynab";
|
|
8
|
+
|
|
9
|
+
// --- Init ---
|
|
10
|
+
|
|
11
|
+
let API_TOKEN = process.env.YNAB_API_TOKEN;
|
|
12
|
+
if (!API_TOKEN && process.env.YNAB_OP_PATH) {
|
|
13
|
+
try {
|
|
14
|
+
API_TOKEN = execFileSync(
|
|
15
|
+
"op", ["read", process.env.YNAB_OP_PATH],
|
|
16
|
+
{ encoding: "utf-8", timeout: 10000, stdio: ["pipe", "pipe", "pipe"] }
|
|
17
|
+
).trim();
|
|
18
|
+
} catch {
|
|
19
|
+
// 1Password CLI unavailable or item not found at YNAB_OP_PATH
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!API_TOKEN) {
|
|
23
|
+
console.error("YNAB_API_TOKEN environment variable is required. Set YNAB_OP_PATH to enable 1Password CLI fallback (e.g. op://Vault/Item/credential).");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const api = new ynab.API(API_TOKEN);
|
|
28
|
+
const DEFAULT_BUDGET_ID = process.env.YNAB_BUDGET_ID;
|
|
29
|
+
|
|
30
|
+
// --- Helpers ---
|
|
31
|
+
|
|
32
|
+
function resolveBudgetId(input) {
|
|
33
|
+
return input || DEFAULT_BUDGET_ID || "last-used";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function dollars(milliunits) {
|
|
37
|
+
return milliunits == null ? null : milliunits / 1000;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function milliunits(dollars) {
|
|
41
|
+
return Math.round(dollars * 1000);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function dollarsMap(obj) {
|
|
45
|
+
return obj ? Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, dollars(v)])) : obj;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mapTransactionInput(t) {
|
|
49
|
+
const out = {
|
|
50
|
+
account_id: t.accountId,
|
|
51
|
+
date: t.date,
|
|
52
|
+
amount: milliunits(t.amount),
|
|
53
|
+
payee_id: t.payeeId,
|
|
54
|
+
payee_name: t.payeeName,
|
|
55
|
+
category_id: t.categoryId,
|
|
56
|
+
memo: t.memo,
|
|
57
|
+
cleared: t.cleared,
|
|
58
|
+
approved: t.approved,
|
|
59
|
+
flag_color: t.flagColor,
|
|
60
|
+
import_id: t.importId,
|
|
61
|
+
};
|
|
62
|
+
if (t.subtransactions) {
|
|
63
|
+
out.subtransactions = t.subtransactions.map((s) => ({
|
|
64
|
+
amount: milliunits(s.amount),
|
|
65
|
+
category_id: s.categoryId,
|
|
66
|
+
payee_id: s.payeeId,
|
|
67
|
+
payee_name: s.payeeName,
|
|
68
|
+
memo: s.memo,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Sparse patch mapper for update_transaction / update_transactions — only includes fields that were explicitly provided
|
|
75
|
+
function mapTransactionUpdate(t) {
|
|
76
|
+
const out = {};
|
|
77
|
+
if (t.accountId !== undefined) out.account_id = t.accountId;
|
|
78
|
+
if (t.date !== undefined) out.date = t.date;
|
|
79
|
+
if (t.amount !== undefined) out.amount = milliunits(t.amount);
|
|
80
|
+
if (t.payeeId !== undefined) out.payee_id = t.payeeId;
|
|
81
|
+
if (t.payeeName !== undefined) out.payee_name = t.payeeName;
|
|
82
|
+
if (t.categoryId !== undefined) out.category_id = t.categoryId;
|
|
83
|
+
if (t.memo !== undefined) out.memo = t.memo;
|
|
84
|
+
if (t.cleared !== undefined) out.cleared = t.cleared;
|
|
85
|
+
if (t.approved !== undefined) out.approved = t.approved;
|
|
86
|
+
if (t.flagColor !== undefined) out.flag_color = t.flagColor;
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ok(data) {
|
|
91
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function run(fn) {
|
|
95
|
+
try {
|
|
96
|
+
return await fn();
|
|
97
|
+
} catch (e) {
|
|
98
|
+
const detail = e?.error?.detail;
|
|
99
|
+
const name = e?.error?.name;
|
|
100
|
+
const msg = detail
|
|
101
|
+
? (name ? `${name}: ${detail}` : detail)
|
|
102
|
+
: (e?.message || String(e));
|
|
103
|
+
return { content: [{ type: "text", text: `Error: ${msg}` }], isError: true };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Direct API helper for endpoints not yet in the ynab SDK
|
|
108
|
+
const BASE_URL = "https://api.ynab.com/v1";
|
|
109
|
+
async function ynabFetch(path, { method = "GET", body } = {}) {
|
|
110
|
+
const opts = {
|
|
111
|
+
method,
|
|
112
|
+
headers: { Authorization: `Bearer ${API_TOKEN}`, "Content-Type": "application/json" },
|
|
113
|
+
};
|
|
114
|
+
if (body) opts.body = JSON.stringify(body);
|
|
115
|
+
const res = await fetch(`${BASE_URL}${path}`, opts);
|
|
116
|
+
const json = await res.json();
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const err = new Error(json?.error?.detail || `HTTP ${res.status}`);
|
|
119
|
+
err.error = json?.error;
|
|
120
|
+
throw err;
|
|
121
|
+
}
|
|
122
|
+
return json.data;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- Server ---
|
|
126
|
+
|
|
127
|
+
const server = new McpServer({
|
|
128
|
+
name: "ynab-mcp-server",
|
|
129
|
+
version: "1.3.0",
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// ==================== User & Budgets ====================
|
|
133
|
+
|
|
134
|
+
server.registerTool(
|
|
135
|
+
"get_user",
|
|
136
|
+
{ description: "Get the authenticated user" },
|
|
137
|
+
() =>
|
|
138
|
+
run(async () => {
|
|
139
|
+
const { data } = await api.user.getUser();
|
|
140
|
+
return ok(data.user);
|
|
141
|
+
})
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
server.registerTool(
|
|
145
|
+
"list_budgets",
|
|
146
|
+
{ description: "List all budgets. Use a budget ID from the results in other tools, or omit budgetId to use the last-used budget." },
|
|
147
|
+
() =>
|
|
148
|
+
run(async () => {
|
|
149
|
+
const { data } = await api.budgets.getBudgets();
|
|
150
|
+
const result = {
|
|
151
|
+
budgets: data.budgets.map((b) => ({ id: b.id, name: b.name, last_modified_on: b.last_modified_on, first_month: b.first_month, last_month: b.last_month, date_format: b.date_format, currency_format: b.currency_format })),
|
|
152
|
+
};
|
|
153
|
+
if (data.default_budget) {
|
|
154
|
+
result.default_budget = { id: data.default_budget.id, name: data.default_budget.name };
|
|
155
|
+
}
|
|
156
|
+
return ok(result);
|
|
157
|
+
})
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
server.registerTool(
|
|
161
|
+
"get_budget",
|
|
162
|
+
{ description: "Get a budget summary including name, currency format, and account/category/payee counts", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
163
|
+
({ budgetId }) =>
|
|
164
|
+
run(async () => {
|
|
165
|
+
const { data } = await api.budgets.getBudgetById(resolveBudgetId(budgetId));
|
|
166
|
+
const b = data.budget;
|
|
167
|
+
return ok({
|
|
168
|
+
id: b.id,
|
|
169
|
+
name: b.name,
|
|
170
|
+
last_modified_on: b.last_modified_on,
|
|
171
|
+
first_month: b.first_month,
|
|
172
|
+
last_month: b.last_month,
|
|
173
|
+
date_format: b.date_format,
|
|
174
|
+
currency_format: b.currency_format,
|
|
175
|
+
accounts: b.accounts?.length,
|
|
176
|
+
categories: b.categories?.length,
|
|
177
|
+
payees: b.payees?.length,
|
|
178
|
+
});
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
server.registerTool(
|
|
183
|
+
"get_budget_settings",
|
|
184
|
+
{ description: "Get budget settings (currency format, date format)", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
185
|
+
({ budgetId }) =>
|
|
186
|
+
run(async () => {
|
|
187
|
+
const { data } = await api.budgets.getBudgetSettingsById(resolveBudgetId(budgetId));
|
|
188
|
+
return ok(data.settings);
|
|
189
|
+
})
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
// ==================== Accounts ====================
|
|
193
|
+
|
|
194
|
+
function formatAccount(a) {
|
|
195
|
+
const out = {
|
|
196
|
+
id: a.id,
|
|
197
|
+
name: a.name,
|
|
198
|
+
type: a.type,
|
|
199
|
+
on_budget: a.on_budget,
|
|
200
|
+
closed: a.closed,
|
|
201
|
+
balance: dollars(a.balance),
|
|
202
|
+
cleared_balance: dollars(a.cleared_balance),
|
|
203
|
+
uncleared_balance: dollars(a.uncleared_balance),
|
|
204
|
+
transfer_payee_id: a.transfer_payee_id,
|
|
205
|
+
direct_import_linked: a.direct_import_linked,
|
|
206
|
+
direct_import_in_error: a.direct_import_in_error,
|
|
207
|
+
last_reconciled_at: a.last_reconciled_at,
|
|
208
|
+
debt_original_balance: dollars(a.debt_original_balance),
|
|
209
|
+
debt_interest_rates: a.debt_interest_rates,
|
|
210
|
+
debt_minimum_payments: dollarsMap(a.debt_minimum_payments),
|
|
211
|
+
debt_escrow_amounts: dollarsMap(a.debt_escrow_amounts),
|
|
212
|
+
deleted: a.deleted,
|
|
213
|
+
};
|
|
214
|
+
if ("note" in a) out.note = a.note;
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
server.registerTool(
|
|
219
|
+
"list_accounts",
|
|
220
|
+
{ description: "List all accounts in a budget", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
221
|
+
({ budgetId }) =>
|
|
222
|
+
run(async () => {
|
|
223
|
+
const { data } = await api.accounts.getAccounts(resolveBudgetId(budgetId));
|
|
224
|
+
return ok(data.accounts.map(formatAccount));
|
|
225
|
+
})
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
server.registerTool(
|
|
229
|
+
"get_account",
|
|
230
|
+
{ description: "Get details for a specific account", inputSchema: {
|
|
231
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
232
|
+
accountId: z.string().describe("Account ID"),
|
|
233
|
+
} },
|
|
234
|
+
({ budgetId, accountId }) =>
|
|
235
|
+
run(async () => {
|
|
236
|
+
const { data } = await api.accounts.getAccountById(resolveBudgetId(budgetId), accountId);
|
|
237
|
+
return ok(formatAccount(data.account));
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
server.registerTool(
|
|
242
|
+
"create_account",
|
|
243
|
+
{ description: "Create a new account", inputSchema: {
|
|
244
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
245
|
+
name: z.string().describe("Account name"),
|
|
246
|
+
type: z.enum(["checking", "savings", "cash", "creditCard", "lineOfCredit", "otherAsset", "otherLiability", "mortgage", "autoLoan", "studentLoan", "personalLoan", "medicalDebt", "otherDebt"]).describe("Account type"),
|
|
247
|
+
balance: z.number().describe("Starting balance in dollars"),
|
|
248
|
+
} },
|
|
249
|
+
({ budgetId, name, type, balance: bal }) =>
|
|
250
|
+
run(async () => {
|
|
251
|
+
const { data } = await api.accounts.createAccount(resolveBudgetId(budgetId), {
|
|
252
|
+
account: { name, type, balance: milliunits(bal) },
|
|
253
|
+
});
|
|
254
|
+
return ok(formatAccount(data.account));
|
|
255
|
+
})
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
// ==================== Categories ====================
|
|
259
|
+
|
|
260
|
+
function formatCategory(c) {
|
|
261
|
+
return {
|
|
262
|
+
id: c.id,
|
|
263
|
+
category_group_id: c.category_group_id,
|
|
264
|
+
category_group_name: c.category_group_name,
|
|
265
|
+
original_category_group_id: c.original_category_group_id,
|
|
266
|
+
name: c.name,
|
|
267
|
+
hidden: c.hidden,
|
|
268
|
+
note: c.note,
|
|
269
|
+
budgeted: dollars(c.budgeted),
|
|
270
|
+
activity: dollars(c.activity),
|
|
271
|
+
balance: dollars(c.balance),
|
|
272
|
+
goal_type: c.goal_type,
|
|
273
|
+
goal_day: c.goal_day,
|
|
274
|
+
goal_cadence: c.goal_cadence,
|
|
275
|
+
goal_cadence_frequency: c.goal_cadence_frequency,
|
|
276
|
+
goal_creation_month: c.goal_creation_month,
|
|
277
|
+
goal_target: dollars(c.goal_target),
|
|
278
|
+
goal_target_date: c.goal_target_date,
|
|
279
|
+
goal_percentage_complete: c.goal_percentage_complete,
|
|
280
|
+
goal_months_to_budget: c.goal_months_to_budget,
|
|
281
|
+
goal_under_funded: dollars(c.goal_under_funded),
|
|
282
|
+
goal_overall_funded: dollars(c.goal_overall_funded),
|
|
283
|
+
goal_overall_left: dollars(c.goal_overall_left),
|
|
284
|
+
goal_needs_whole_amount: c.goal_needs_whole_amount,
|
|
285
|
+
deleted: c.deleted,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
server.registerTool(
|
|
290
|
+
"list_categories",
|
|
291
|
+
{ description: "List all category groups and their categories", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
292
|
+
({ budgetId }) =>
|
|
293
|
+
run(async () => {
|
|
294
|
+
const { data } = await api.categories.getCategories(resolveBudgetId(budgetId));
|
|
295
|
+
return ok(
|
|
296
|
+
data.category_groups.map((g) => ({
|
|
297
|
+
id: g.id,
|
|
298
|
+
name: g.name,
|
|
299
|
+
hidden: g.hidden,
|
|
300
|
+
deleted: g.deleted,
|
|
301
|
+
categories: g.categories.map((c) => ({
|
|
302
|
+
id: c.id,
|
|
303
|
+
name: c.name,
|
|
304
|
+
hidden: c.hidden,
|
|
305
|
+
budgeted: dollars(c.budgeted),
|
|
306
|
+
activity: dollars(c.activity),
|
|
307
|
+
balance: dollars(c.balance),
|
|
308
|
+
goal_type: c.goal_type,
|
|
309
|
+
deleted: c.deleted,
|
|
310
|
+
})),
|
|
311
|
+
}))
|
|
312
|
+
);
|
|
313
|
+
})
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
server.registerTool(
|
|
317
|
+
"get_category",
|
|
318
|
+
{ description: "Get a specific category", inputSchema: {
|
|
319
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
320
|
+
categoryId: z.string().describe("Category ID"),
|
|
321
|
+
} },
|
|
322
|
+
({ budgetId, categoryId }) =>
|
|
323
|
+
run(async () => {
|
|
324
|
+
const { data } = await api.categories.getCategoryById(resolveBudgetId(budgetId), categoryId);
|
|
325
|
+
return ok(formatCategory(data.category));
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
server.registerTool(
|
|
330
|
+
"get_month_category",
|
|
331
|
+
{ description: "Get category budget for a specific month", inputSchema: {
|
|
332
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
333
|
+
month: z.string().describe("Month in YYYY-MM-DD format (first of month)"),
|
|
334
|
+
categoryId: z.string().describe("Category ID"),
|
|
335
|
+
} },
|
|
336
|
+
({ budgetId, month, categoryId }) =>
|
|
337
|
+
run(async () => {
|
|
338
|
+
const { data } = await api.categories.getMonthCategoryById(resolveBudgetId(budgetId), month, categoryId);
|
|
339
|
+
return ok(formatCategory(data.category));
|
|
340
|
+
})
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
server.registerTool(
|
|
344
|
+
"update_month_category",
|
|
345
|
+
{ description: "Set the budgeted amount for a category in a specific month", inputSchema: {
|
|
346
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
347
|
+
month: z.string().describe("Month in YYYY-MM-DD format (first of month)"),
|
|
348
|
+
categoryId: z.string().describe("Category ID"),
|
|
349
|
+
budgeted: z.number().describe("Amount to budget in dollars"),
|
|
350
|
+
} },
|
|
351
|
+
({ budgetId, month, categoryId, budgeted }) =>
|
|
352
|
+
run(async () => {
|
|
353
|
+
const { data } = await api.categories.updateMonthCategory(resolveBudgetId(budgetId), month, categoryId, {
|
|
354
|
+
category: { budgeted: milliunits(budgeted) },
|
|
355
|
+
});
|
|
356
|
+
return ok(formatCategory(data.category));
|
|
357
|
+
})
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
server.registerTool(
|
|
361
|
+
"update_category",
|
|
362
|
+
{ description: "Update a category's name, note, goal target, or move it to a different group", inputSchema: {
|
|
363
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
364
|
+
categoryId: z.string().describe("Category ID"),
|
|
365
|
+
name: z.string().optional().describe("New category name"),
|
|
366
|
+
note: z.string().nullable().optional().describe("Category note (null to clear)"),
|
|
367
|
+
categoryGroupId: z.string().optional().describe("Move to a different category group"),
|
|
368
|
+
goalTarget: z.number().nullable().optional().describe("Goal target amount in dollars (only if category already has a goal)"),
|
|
369
|
+
goalTargetDate: z.string().nullable().optional().describe("Goal target date in ISO format (e.g. 2026-12-01, null to clear)"),
|
|
370
|
+
} },
|
|
371
|
+
({ budgetId, categoryId, name, note, categoryGroupId, goalTarget, goalTargetDate }) =>
|
|
372
|
+
run(async () => {
|
|
373
|
+
const cat = {};
|
|
374
|
+
if (name !== undefined) cat.name = name;
|
|
375
|
+
if (note !== undefined) cat.note = note;
|
|
376
|
+
if (categoryGroupId !== undefined) cat.category_group_id = categoryGroupId;
|
|
377
|
+
if (goalTarget !== undefined) cat.goal_target = goalTarget != null ? milliunits(goalTarget) : null;
|
|
378
|
+
if (goalTargetDate !== undefined) cat.goal_target_date = goalTargetDate;
|
|
379
|
+
|
|
380
|
+
const { data } = await api.categories.updateCategory(resolveBudgetId(budgetId), categoryId, {
|
|
381
|
+
category: cat,
|
|
382
|
+
});
|
|
383
|
+
return ok(formatCategory(data.category));
|
|
384
|
+
})
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
server.registerTool(
|
|
388
|
+
"create_category",
|
|
389
|
+
{ description: "Create a new category in a category group", inputSchema: {
|
|
390
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
391
|
+
categoryGroupId: z.string().describe("Category group ID to create the category in"),
|
|
392
|
+
name: z.string().describe("Category name"),
|
|
393
|
+
note: z.string().optional().describe("Category note"),
|
|
394
|
+
goalTarget: z.number().optional().describe("Goal target amount in dollars (creates a 'Needed for Spending' goal)"),
|
|
395
|
+
goalTargetDate: z.string().optional().describe("Goal target date in ISO format (e.g. 2026-12-01)"),
|
|
396
|
+
} },
|
|
397
|
+
({ budgetId, categoryGroupId, name, note, goalTarget, goalTargetDate }) =>
|
|
398
|
+
run(async () => {
|
|
399
|
+
const bid = resolveBudgetId(budgetId);
|
|
400
|
+
const cat = { category_group_id: categoryGroupId, name };
|
|
401
|
+
if (note !== undefined) cat.note = note;
|
|
402
|
+
if (goalTarget !== undefined) cat.goal_target = milliunits(goalTarget);
|
|
403
|
+
if (goalTargetDate !== undefined) cat.goal_target_date = goalTargetDate;
|
|
404
|
+
const data = await ynabFetch(`/budgets/${bid}/categories`, {
|
|
405
|
+
method: "POST",
|
|
406
|
+
body: { category: cat },
|
|
407
|
+
});
|
|
408
|
+
return ok(formatCategory(data.category));
|
|
409
|
+
})
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
server.registerTool(
|
|
413
|
+
"create_category_group",
|
|
414
|
+
{ description: "Create a new category group", inputSchema: {
|
|
415
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
416
|
+
name: z.string().describe("Category group name (max 50 characters)"),
|
|
417
|
+
} },
|
|
418
|
+
({ budgetId, name }) =>
|
|
419
|
+
run(async () => {
|
|
420
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/category_groups`, {
|
|
421
|
+
method: "POST",
|
|
422
|
+
body: { category_group: { name } },
|
|
423
|
+
});
|
|
424
|
+
return ok(data.category_group);
|
|
425
|
+
})
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
server.registerTool(
|
|
429
|
+
"update_category_group",
|
|
430
|
+
{ description: "Rename a category group", inputSchema: {
|
|
431
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
432
|
+
categoryGroupId: z.string().describe("Category group ID"),
|
|
433
|
+
name: z.string().describe("New category group name (max 50 characters)"),
|
|
434
|
+
} },
|
|
435
|
+
({ budgetId, categoryGroupId, name }) =>
|
|
436
|
+
run(async () => {
|
|
437
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/category_groups/${categoryGroupId}`, {
|
|
438
|
+
method: "PATCH",
|
|
439
|
+
body: { category_group: { name } },
|
|
440
|
+
});
|
|
441
|
+
return ok(data.category_group);
|
|
442
|
+
})
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// ==================== Payees ====================
|
|
446
|
+
|
|
447
|
+
server.registerTool(
|
|
448
|
+
"list_payees",
|
|
449
|
+
{ description: "List all payees", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
450
|
+
({ budgetId }) =>
|
|
451
|
+
run(async () => {
|
|
452
|
+
const { data } = await api.payees.getPayees(resolveBudgetId(budgetId));
|
|
453
|
+
return ok(data.payees.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id, deleted: p.deleted })));
|
|
454
|
+
})
|
|
455
|
+
);
|
|
456
|
+
|
|
457
|
+
server.registerTool(
|
|
458
|
+
"get_payee",
|
|
459
|
+
{ description: "Get a specific payee", inputSchema: {
|
|
460
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
461
|
+
payeeId: z.string().describe("Payee ID"),
|
|
462
|
+
} },
|
|
463
|
+
({ budgetId, payeeId }) =>
|
|
464
|
+
run(async () => {
|
|
465
|
+
const { data } = await api.payees.getPayeeById(resolveBudgetId(budgetId), payeeId);
|
|
466
|
+
return ok(data.payee);
|
|
467
|
+
})
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
server.registerTool(
|
|
471
|
+
"update_payee",
|
|
472
|
+
{ description: "Rename a payee", inputSchema: {
|
|
473
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
474
|
+
payeeId: z.string().describe("Payee ID"),
|
|
475
|
+
name: z.string().describe("New payee name"),
|
|
476
|
+
} },
|
|
477
|
+
({ budgetId, payeeId, name }) =>
|
|
478
|
+
run(async () => {
|
|
479
|
+
const { data } = await api.payees.updatePayee(resolveBudgetId(budgetId), payeeId, {
|
|
480
|
+
payee: { name },
|
|
481
|
+
});
|
|
482
|
+
return ok(data.payee);
|
|
483
|
+
})
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
server.registerTool(
|
|
487
|
+
"create_payee",
|
|
488
|
+
{ description: "Create a new payee", inputSchema: {
|
|
489
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
490
|
+
name: z.string().max(500).describe("Payee name (max 500 characters)"),
|
|
491
|
+
} },
|
|
492
|
+
({ budgetId, name }) =>
|
|
493
|
+
run(async () => {
|
|
494
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/payees`, {
|
|
495
|
+
method: "POST",
|
|
496
|
+
body: { payee: { name } },
|
|
497
|
+
});
|
|
498
|
+
return ok(data.payee);
|
|
499
|
+
})
|
|
500
|
+
);
|
|
501
|
+
|
|
502
|
+
// ==================== Payee Locations ====================
|
|
503
|
+
|
|
504
|
+
server.registerTool(
|
|
505
|
+
"list_payee_locations",
|
|
506
|
+
{ description: "List all payee locations (GPS coordinates where transactions occurred)", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
507
|
+
({ budgetId }) =>
|
|
508
|
+
run(async () => {
|
|
509
|
+
const { data } = await api.payeeLocations.getPayeeLocations(resolveBudgetId(budgetId));
|
|
510
|
+
return ok(data.payee_locations);
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
server.registerTool(
|
|
515
|
+
"get_payee_location",
|
|
516
|
+
{ description: "Get a specific payee location", inputSchema: {
|
|
517
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
518
|
+
payeeLocationId: z.string().describe("Payee location ID"),
|
|
519
|
+
} },
|
|
520
|
+
({ budgetId, payeeLocationId }) =>
|
|
521
|
+
run(async () => {
|
|
522
|
+
const { data } = await api.payeeLocations.getPayeeLocationById(resolveBudgetId(budgetId), payeeLocationId);
|
|
523
|
+
return ok(data.payee_location);
|
|
524
|
+
})
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
server.registerTool(
|
|
528
|
+
"get_payee_locations_by_payee",
|
|
529
|
+
{ description: "Get all locations for a specific payee", inputSchema: {
|
|
530
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
531
|
+
payeeId: z.string().describe("Payee ID"),
|
|
532
|
+
} },
|
|
533
|
+
({ budgetId, payeeId }) =>
|
|
534
|
+
run(async () => {
|
|
535
|
+
const { data } = await api.payeeLocations.getPayeeLocationsByPayee(resolveBudgetId(budgetId), payeeId);
|
|
536
|
+
return ok(data.payee_locations);
|
|
537
|
+
})
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
// ==================== Months ====================
|
|
541
|
+
|
|
542
|
+
server.registerTool(
|
|
543
|
+
"list_months",
|
|
544
|
+
{ description: "List all budget months", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
545
|
+
({ budgetId }) =>
|
|
546
|
+
run(async () => {
|
|
547
|
+
const { data } = await api.months.getBudgetMonths(resolveBudgetId(budgetId));
|
|
548
|
+
return ok(
|
|
549
|
+
data.months.map((m) => ({
|
|
550
|
+
month: m.month,
|
|
551
|
+
note: m.note,
|
|
552
|
+
income: dollars(m.income),
|
|
553
|
+
budgeted: dollars(m.budgeted),
|
|
554
|
+
activity: dollars(m.activity),
|
|
555
|
+
to_be_budgeted: dollars(m.to_be_budgeted),
|
|
556
|
+
age_of_money: m.age_of_money,
|
|
557
|
+
deleted: m.deleted,
|
|
558
|
+
}))
|
|
559
|
+
);
|
|
560
|
+
})
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
server.registerTool(
|
|
564
|
+
"get_month",
|
|
565
|
+
{ description: "Get budget month detail with per-category breakdown", inputSchema: {
|
|
566
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
567
|
+
month: z.string().describe("Month in YYYY-MM-DD format (first of month)"),
|
|
568
|
+
} },
|
|
569
|
+
({ budgetId, month }) =>
|
|
570
|
+
run(async () => {
|
|
571
|
+
const { data } = await api.months.getBudgetMonth(resolveBudgetId(budgetId), month);
|
|
572
|
+
const m = data.month;
|
|
573
|
+
return ok({
|
|
574
|
+
month: m.month,
|
|
575
|
+
note: m.note,
|
|
576
|
+
income: dollars(m.income),
|
|
577
|
+
budgeted: dollars(m.budgeted),
|
|
578
|
+
activity: dollars(m.activity),
|
|
579
|
+
to_be_budgeted: dollars(m.to_be_budgeted),
|
|
580
|
+
age_of_money: m.age_of_money,
|
|
581
|
+
deleted: m.deleted,
|
|
582
|
+
categories: m.categories?.map((c) => ({
|
|
583
|
+
id: c.id,
|
|
584
|
+
name: c.name,
|
|
585
|
+
hidden: c.hidden,
|
|
586
|
+
category_group_name: c.category_group_name,
|
|
587
|
+
budgeted: dollars(c.budgeted),
|
|
588
|
+
activity: dollars(c.activity),
|
|
589
|
+
balance: dollars(c.balance),
|
|
590
|
+
goal_type: c.goal_type,
|
|
591
|
+
goal_target: dollars(c.goal_target),
|
|
592
|
+
goal_under_funded: dollars(c.goal_under_funded),
|
|
593
|
+
deleted: c.deleted,
|
|
594
|
+
})),
|
|
595
|
+
});
|
|
596
|
+
})
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
// ==================== Money Movements ====================
|
|
600
|
+
|
|
601
|
+
function formatMoneyMovement(m) {
|
|
602
|
+
return {
|
|
603
|
+
id: m.id,
|
|
604
|
+
month: m.month,
|
|
605
|
+
moved_at: m.moved_at,
|
|
606
|
+
note: m.note,
|
|
607
|
+
money_movement_group_id: m.money_movement_group_id,
|
|
608
|
+
performed_by_user_id: m.performed_by_user_id,
|
|
609
|
+
from_category_id: m.from_category_id,
|
|
610
|
+
to_category_id: m.to_category_id,
|
|
611
|
+
amount: dollars(m.amount),
|
|
612
|
+
deleted: m.deleted,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
server.registerTool(
|
|
617
|
+
"list_money_movements",
|
|
618
|
+
{ description: "List all money movements (budget re-allocations between categories)", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
619
|
+
({ budgetId }) =>
|
|
620
|
+
run(async () => {
|
|
621
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/money_movements`);
|
|
622
|
+
return ok(data.money_movements.map(formatMoneyMovement));
|
|
623
|
+
})
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
server.registerTool(
|
|
627
|
+
"get_money_movements_by_month",
|
|
628
|
+
{ description: "Get money movements for a specific month", inputSchema: {
|
|
629
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
630
|
+
month: z.string().describe("Month in YYYY-MM-DD format (first of month), or 'current'"),
|
|
631
|
+
} },
|
|
632
|
+
({ budgetId, month }) =>
|
|
633
|
+
run(async () => {
|
|
634
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/months/${month}/money_movements`);
|
|
635
|
+
return ok(data.money_movements.map(formatMoneyMovement));
|
|
636
|
+
})
|
|
637
|
+
);
|
|
638
|
+
|
|
639
|
+
server.registerTool(
|
|
640
|
+
"list_money_movement_groups",
|
|
641
|
+
{ description: "List all money movement groups (batches of related money movements)", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
642
|
+
({ budgetId }) =>
|
|
643
|
+
run(async () => {
|
|
644
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/money_movement_groups`);
|
|
645
|
+
return ok(data.money_movement_groups);
|
|
646
|
+
})
|
|
647
|
+
);
|
|
648
|
+
|
|
649
|
+
server.registerTool(
|
|
650
|
+
"get_money_movement_groups_by_month",
|
|
651
|
+
{ description: "Get money movement groups for a specific month", inputSchema: {
|
|
652
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
653
|
+
month: z.string().describe("Month in YYYY-MM-DD format (first of month), or 'current'"),
|
|
654
|
+
} },
|
|
655
|
+
({ budgetId, month }) =>
|
|
656
|
+
run(async () => {
|
|
657
|
+
const data = await ynabFetch(`/budgets/${resolveBudgetId(budgetId)}/months/${month}/money_movement_groups`);
|
|
658
|
+
return ok(data.money_movement_groups);
|
|
659
|
+
})
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
// ==================== Transactions ====================
|
|
663
|
+
|
|
664
|
+
function formatTransaction(t) {
|
|
665
|
+
return {
|
|
666
|
+
id: t.id,
|
|
667
|
+
date: t.date,
|
|
668
|
+
amount: dollars(t.amount),
|
|
669
|
+
memo: t.memo,
|
|
670
|
+
cleared: t.cleared,
|
|
671
|
+
approved: t.approved,
|
|
672
|
+
flag_color: t.flag_color,
|
|
673
|
+
flag_name: t.flag_name,
|
|
674
|
+
account_id: t.account_id,
|
|
675
|
+
account_name: t.account_name,
|
|
676
|
+
payee_id: t.payee_id,
|
|
677
|
+
payee_name: t.payee_name,
|
|
678
|
+
category_id: t.category_id,
|
|
679
|
+
category_name: t.category_name,
|
|
680
|
+
transfer_account_id: t.transfer_account_id,
|
|
681
|
+
transfer_transaction_id: t.transfer_transaction_id,
|
|
682
|
+
matched_transaction_id: t.matched_transaction_id,
|
|
683
|
+
import_id: t.import_id,
|
|
684
|
+
import_payee_name: t.import_payee_name,
|
|
685
|
+
import_payee_name_original: t.import_payee_name_original,
|
|
686
|
+
debt_transaction_type: t.debt_transaction_type,
|
|
687
|
+
deleted: t.deleted,
|
|
688
|
+
subtransactions: t.subtransactions?.map((s) => ({
|
|
689
|
+
id: s.id,
|
|
690
|
+
transaction_id: s.transaction_id,
|
|
691
|
+
amount: dollars(s.amount),
|
|
692
|
+
memo: s.memo,
|
|
693
|
+
payee_id: s.payee_id,
|
|
694
|
+
payee_name: s.payee_name,
|
|
695
|
+
category_id: s.category_id,
|
|
696
|
+
category_name: s.category_name,
|
|
697
|
+
transfer_account_id: s.transfer_account_id,
|
|
698
|
+
transfer_transaction_id: s.transfer_transaction_id,
|
|
699
|
+
deleted: s.deleted,
|
|
700
|
+
})),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
server.registerTool(
|
|
705
|
+
"get_transactions",
|
|
706
|
+
{ description: "Get transactions with optional filters. Use type='unapproved' or type='uncategorized' to filter. Optionally filter by account, category, payee, or month.", inputSchema: {
|
|
707
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
708
|
+
sinceDate: z.string().optional().describe("Only return transactions on or after this date (YYYY-MM-DD)"),
|
|
709
|
+
type: z.enum(["unapproved", "uncategorized"]).optional().describe("Filter by approval/categorization status"),
|
|
710
|
+
accountId: z.string().optional().describe("Filter by account ID"),
|
|
711
|
+
categoryId: z.string().optional().describe("Filter by category ID"),
|
|
712
|
+
payeeId: z.string().optional().describe("Filter by payee ID"),
|
|
713
|
+
month: z.string().optional().describe("Filter by month (YYYY-MM-DD, first of month)"),
|
|
714
|
+
} },
|
|
715
|
+
({ budgetId, sinceDate, type, accountId, categoryId, payeeId, month }) =>
|
|
716
|
+
run(async () => {
|
|
717
|
+
const bid = resolveBudgetId(budgetId);
|
|
718
|
+
let transactions;
|
|
719
|
+
|
|
720
|
+
if (accountId) {
|
|
721
|
+
const { data } = await api.transactions.getTransactionsByAccount(bid, accountId, sinceDate, type);
|
|
722
|
+
transactions = data.transactions;
|
|
723
|
+
} else if (categoryId) {
|
|
724
|
+
const { data } = await api.transactions.getTransactionsByCategory(bid, categoryId, sinceDate, type);
|
|
725
|
+
transactions = data.transactions;
|
|
726
|
+
} else if (payeeId) {
|
|
727
|
+
const { data } = await api.transactions.getTransactionsByPayee(bid, payeeId, sinceDate, type);
|
|
728
|
+
transactions = data.transactions;
|
|
729
|
+
} else if (month) {
|
|
730
|
+
const { data } = await api.transactions.getTransactionsByMonth(bid, month, sinceDate, type);
|
|
731
|
+
transactions = data.transactions;
|
|
732
|
+
} else {
|
|
733
|
+
const { data } = await api.transactions.getTransactions(bid, sinceDate, type);
|
|
734
|
+
transactions = data.transactions;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
return ok(transactions.map(formatTransaction));
|
|
738
|
+
})
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
server.registerTool(
|
|
742
|
+
"get_transaction",
|
|
743
|
+
{ description: "Get a single transaction by ID", inputSchema: {
|
|
744
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
745
|
+
transactionId: z.string().describe("Transaction ID"),
|
|
746
|
+
} },
|
|
747
|
+
({ budgetId, transactionId }) =>
|
|
748
|
+
run(async () => {
|
|
749
|
+
const { data } = await api.transactions.getTransactionById(resolveBudgetId(budgetId), transactionId);
|
|
750
|
+
return ok(formatTransaction(data.transaction));
|
|
751
|
+
})
|
|
752
|
+
);
|
|
753
|
+
|
|
754
|
+
server.registerTool(
|
|
755
|
+
"create_transaction",
|
|
756
|
+
{ description: "Create a new transaction. Amounts are in dollars (positive for inflows, negative for outflows). Note: future-dated transactions cannot be created here — use create_scheduled_transaction instead.", inputSchema: {
|
|
757
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
758
|
+
accountId: z.string().describe("Account ID"),
|
|
759
|
+
date: z.string().describe("Transaction date (YYYY-MM-DD)"),
|
|
760
|
+
amount: z.number().describe("Amount in dollars (negative for outflows, positive for inflows)"),
|
|
761
|
+
payeeId: z.string().optional().describe("Payee ID"),
|
|
762
|
+
payeeName: z.string().optional().describe("Payee name (creates new payee if no payeeId)"),
|
|
763
|
+
categoryId: z.string().optional().describe("Category ID"),
|
|
764
|
+
memo: z.string().optional().describe("Transaction memo"),
|
|
765
|
+
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
766
|
+
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
767
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color"),
|
|
768
|
+
importId: z.string().optional().describe("Unique import ID for deduplication (max 36 chars). If omitted and the transaction is later imported, duplicates may be created."),
|
|
769
|
+
subtransactions: z.array(z.object({
|
|
770
|
+
amount: z.number().describe("Subtransaction amount in dollars"),
|
|
771
|
+
categoryId: z.string().optional().describe("Category ID"),
|
|
772
|
+
payeeId: z.string().optional().describe("Payee ID"),
|
|
773
|
+
payeeName: z.string().optional().describe("Payee name"),
|
|
774
|
+
memo: z.string().optional().describe("Memo"),
|
|
775
|
+
})).optional().describe("Split transaction into subtransactions. The subtransaction amounts must sum to the total transaction amount."),
|
|
776
|
+
} },
|
|
777
|
+
({ budgetId, ...txnFields }) =>
|
|
778
|
+
run(async () => {
|
|
779
|
+
const { data } = await api.transactions.createTransaction(resolveBudgetId(budgetId), {
|
|
780
|
+
transaction: mapTransactionInput(txnFields),
|
|
781
|
+
});
|
|
782
|
+
return ok(formatTransaction(data.transaction));
|
|
783
|
+
})
|
|
784
|
+
);
|
|
785
|
+
|
|
786
|
+
server.registerTool(
|
|
787
|
+
"create_transactions",
|
|
788
|
+
{ description: "Create multiple transactions at once. Amounts are in dollars. Returns created transactions and any duplicate import IDs. Future-dated transactions are not supported — use create_scheduled_transaction instead.", inputSchema: {
|
|
789
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
790
|
+
transactions: z.array(z.object({
|
|
791
|
+
accountId: z.string().describe("Account ID"),
|
|
792
|
+
date: z.string().describe("Transaction date (YYYY-MM-DD)"),
|
|
793
|
+
amount: z.number().describe("Amount in dollars (negative for outflows, positive for inflows)"),
|
|
794
|
+
payeeId: z.string().optional().describe("Payee ID"),
|
|
795
|
+
payeeName: z.string().optional().describe("Payee name (creates new payee if no payeeId)"),
|
|
796
|
+
categoryId: z.string().optional().describe("Category ID"),
|
|
797
|
+
memo: z.string().optional().describe("Transaction memo"),
|
|
798
|
+
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
799
|
+
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
800
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color"),
|
|
801
|
+
importId: z.string().optional().describe("Unique import ID for deduplication (max 36 chars)"),
|
|
802
|
+
subtransactions: z.array(z.object({
|
|
803
|
+
amount: z.number().describe("Subtransaction amount in dollars"),
|
|
804
|
+
categoryId: z.string().optional().describe("Category ID"),
|
|
805
|
+
payeeId: z.string().optional().describe("Payee ID"),
|
|
806
|
+
payeeName: z.string().optional().describe("Payee name"),
|
|
807
|
+
memo: z.string().optional().describe("Memo"),
|
|
808
|
+
})).optional().describe("Split transaction into subtransactions"),
|
|
809
|
+
})).describe("Array of transactions to create"),
|
|
810
|
+
} },
|
|
811
|
+
({ budgetId, transactions: txns }) =>
|
|
812
|
+
run(async () => {
|
|
813
|
+
const { data } = await api.transactions.createTransactions(resolveBudgetId(budgetId), {
|
|
814
|
+
transactions: txns.map(mapTransactionInput),
|
|
815
|
+
});
|
|
816
|
+
return ok({
|
|
817
|
+
created: data.transactions?.map(formatTransaction),
|
|
818
|
+
duplicate_import_ids: data.duplicate_import_ids,
|
|
819
|
+
});
|
|
820
|
+
})
|
|
821
|
+
);
|
|
822
|
+
|
|
823
|
+
server.registerTool(
|
|
824
|
+
"update_transaction",
|
|
825
|
+
{ description: "Update an existing transaction. Only provided fields are changed. Amounts in dollars.", inputSchema: {
|
|
826
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
827
|
+
transactionId: z.string().describe("Transaction ID"),
|
|
828
|
+
accountId: z.string().optional().describe("Account ID"),
|
|
829
|
+
date: z.string().optional().describe("Transaction date (YYYY-MM-DD)"),
|
|
830
|
+
amount: z.number().optional().describe("Amount in dollars"),
|
|
831
|
+
payeeId: z.string().nullable().optional().describe("Payee ID (null to remove)"),
|
|
832
|
+
payeeName: z.string().nullable().optional().describe("Payee name (null to clear)"),
|
|
833
|
+
categoryId: z.string().nullable().optional().describe("Category ID (null to uncategorize)"),
|
|
834
|
+
memo: z.string().nullable().optional().describe("Transaction memo (null to clear)"),
|
|
835
|
+
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
836
|
+
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
837
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).nullable().optional().describe("Flag color (null to remove)"),
|
|
838
|
+
} },
|
|
839
|
+
({ budgetId, transactionId, accountId, date, amount, payeeId, payeeName, categoryId, memo, cleared, approved, flagColor }) =>
|
|
840
|
+
run(async () => {
|
|
841
|
+
const { data } = await api.transactions.updateTransaction(resolveBudgetId(budgetId), transactionId, {
|
|
842
|
+
transaction: mapTransactionUpdate({ accountId, date, amount, payeeId, payeeName, categoryId, memo, cleared, approved, flagColor }),
|
|
843
|
+
});
|
|
844
|
+
return ok(formatTransaction(data.transaction));
|
|
845
|
+
})
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
server.registerTool(
|
|
849
|
+
"delete_transaction",
|
|
850
|
+
{ description: "Delete a transaction", inputSchema: {
|
|
851
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
852
|
+
transactionId: z.string().describe("Transaction ID"),
|
|
853
|
+
} },
|
|
854
|
+
({ budgetId, transactionId }) =>
|
|
855
|
+
run(async () => {
|
|
856
|
+
const { data } = await api.transactions.deleteTransaction(resolveBudgetId(budgetId), transactionId);
|
|
857
|
+
return ok(formatTransaction(data.transaction));
|
|
858
|
+
})
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
server.registerTool(
|
|
862
|
+
"update_transactions",
|
|
863
|
+
{ description: "Batch update multiple transactions. Each transaction object must include its id and the fields to update.", inputSchema: {
|
|
864
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
865
|
+
transactions: z
|
|
866
|
+
.array(
|
|
867
|
+
z.object({
|
|
868
|
+
id: z.string().describe("Transaction ID"),
|
|
869
|
+
accountId: z.string().optional().describe("Account ID"),
|
|
870
|
+
date: z.string().optional().describe("Transaction date (YYYY-MM-DD)"),
|
|
871
|
+
amount: z.number().optional().describe("Amount in dollars"),
|
|
872
|
+
payeeId: z.string().nullable().optional().describe("Payee ID (null to remove)"),
|
|
873
|
+
payeeName: z.string().nullable().optional().describe("Payee name (null to clear)"),
|
|
874
|
+
categoryId: z.string().nullable().optional().describe("Category ID (null to uncategorize)"),
|
|
875
|
+
memo: z.string().nullable().optional().describe("Transaction memo (null to clear)"),
|
|
876
|
+
cleared: z.enum(["cleared", "uncleared", "reconciled"]).optional().describe("Cleared status"),
|
|
877
|
+
approved: z.boolean().optional().describe("Whether transaction is approved"),
|
|
878
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).nullable().optional().describe("Flag color (null to remove)"),
|
|
879
|
+
})
|
|
880
|
+
)
|
|
881
|
+
.describe("Array of transaction updates"),
|
|
882
|
+
} },
|
|
883
|
+
({ budgetId, transactions: txns }) =>
|
|
884
|
+
run(async () => {
|
|
885
|
+
const mapped = txns.map((t) => ({ id: t.id, ...mapTransactionUpdate(t) }));
|
|
886
|
+
const { data } = await api.transactions.updateTransactions(resolveBudgetId(budgetId), {
|
|
887
|
+
transactions: mapped,
|
|
888
|
+
});
|
|
889
|
+
return ok({
|
|
890
|
+
updated: data.transactions?.map(formatTransaction),
|
|
891
|
+
duplicate_import_ids: data.duplicate_import_ids,
|
|
892
|
+
});
|
|
893
|
+
})
|
|
894
|
+
);
|
|
895
|
+
|
|
896
|
+
server.registerTool(
|
|
897
|
+
"import_transactions",
|
|
898
|
+
{ description: "Trigger import of linked account transactions", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
899
|
+
({ budgetId }) =>
|
|
900
|
+
run(async () => {
|
|
901
|
+
const { data } = await api.transactions.importTransactions(resolveBudgetId(budgetId));
|
|
902
|
+
return ok(data);
|
|
903
|
+
})
|
|
904
|
+
);
|
|
905
|
+
|
|
906
|
+
// ==================== Scheduled Transactions ====================
|
|
907
|
+
|
|
908
|
+
function formatScheduledTransaction(t) {
|
|
909
|
+
return {
|
|
910
|
+
id: t.id,
|
|
911
|
+
date_first: t.date_first,
|
|
912
|
+
date_next: t.date_next,
|
|
913
|
+
frequency: t.frequency,
|
|
914
|
+
amount: dollars(t.amount),
|
|
915
|
+
memo: t.memo,
|
|
916
|
+
flag_color: t.flag_color,
|
|
917
|
+
flag_name: t.flag_name,
|
|
918
|
+
account_id: t.account_id,
|
|
919
|
+
account_name: t.account_name,
|
|
920
|
+
payee_id: t.payee_id,
|
|
921
|
+
payee_name: t.payee_name,
|
|
922
|
+
category_id: t.category_id,
|
|
923
|
+
category_name: t.category_name,
|
|
924
|
+
transfer_account_id: t.transfer_account_id,
|
|
925
|
+
deleted: t.deleted,
|
|
926
|
+
subtransactions: t.subtransactions?.map((s) => ({
|
|
927
|
+
id: s.id,
|
|
928
|
+
scheduled_transaction_id: s.scheduled_transaction_id,
|
|
929
|
+
amount: dollars(s.amount),
|
|
930
|
+
memo: s.memo,
|
|
931
|
+
payee_id: s.payee_id,
|
|
932
|
+
payee_name: s.payee_name,
|
|
933
|
+
category_id: s.category_id,
|
|
934
|
+
category_name: s.category_name,
|
|
935
|
+
transfer_account_id: s.transfer_account_id,
|
|
936
|
+
deleted: s.deleted,
|
|
937
|
+
})),
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
server.registerTool(
|
|
942
|
+
"list_scheduled_transactions",
|
|
943
|
+
{ description: "List all scheduled (recurring) transactions", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
944
|
+
({ budgetId }) =>
|
|
945
|
+
run(async () => {
|
|
946
|
+
const { data } = await api.scheduledTransactions.getScheduledTransactions(resolveBudgetId(budgetId));
|
|
947
|
+
return ok(data.scheduled_transactions.map(formatScheduledTransaction));
|
|
948
|
+
})
|
|
949
|
+
);
|
|
950
|
+
|
|
951
|
+
server.registerTool(
|
|
952
|
+
"get_scheduled_transaction",
|
|
953
|
+
{ description: "Get a specific scheduled transaction", inputSchema: {
|
|
954
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
955
|
+
scheduledTransactionId: z.string().describe("Scheduled transaction ID"),
|
|
956
|
+
} },
|
|
957
|
+
({ budgetId, scheduledTransactionId }) =>
|
|
958
|
+
run(async () => {
|
|
959
|
+
const { data } = await api.scheduledTransactions.getScheduledTransactionById(resolveBudgetId(budgetId), scheduledTransactionId);
|
|
960
|
+
return ok(formatScheduledTransaction(data.scheduled_transaction));
|
|
961
|
+
})
|
|
962
|
+
);
|
|
963
|
+
|
|
964
|
+
server.registerTool(
|
|
965
|
+
"create_scheduled_transaction",
|
|
966
|
+
{ description: "Create a new scheduled (recurring) transaction", inputSchema: {
|
|
967
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
968
|
+
accountId: z.string().describe("Account ID"),
|
|
969
|
+
dateFirst: z.string().describe("First occurrence date (YYYY-MM-DD)"),
|
|
970
|
+
frequency: z.enum(["never", "daily", "weekly", "everyOtherWeek", "twiceAMonth", "every4Weeks", "monthly", "everyOtherMonth", "every3Months", "every4Months", "twiceAYear", "yearly", "everyOtherYear"]).describe("Recurrence frequency"),
|
|
971
|
+
amount: z.number().describe("Amount in dollars (negative for outflows)"),
|
|
972
|
+
payeeId: z.string().optional().describe("Payee ID"),
|
|
973
|
+
payeeName: z.string().optional().describe("Payee name"),
|
|
974
|
+
categoryId: z.string().optional().describe("Category ID"),
|
|
975
|
+
memo: z.string().optional().describe("Memo"),
|
|
976
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).optional().describe("Flag color"),
|
|
977
|
+
} },
|
|
978
|
+
({ budgetId, accountId, dateFirst, frequency, amount, payeeId, payeeName, categoryId, memo, flagColor }) =>
|
|
979
|
+
run(async () => {
|
|
980
|
+
const { data } = await api.scheduledTransactions.createScheduledTransaction(resolveBudgetId(budgetId), {
|
|
981
|
+
scheduled_transaction: {
|
|
982
|
+
account_id: accountId,
|
|
983
|
+
date: dateFirst,
|
|
984
|
+
frequency,
|
|
985
|
+
amount: milliunits(amount),
|
|
986
|
+
payee_id: payeeId,
|
|
987
|
+
payee_name: payeeName,
|
|
988
|
+
category_id: categoryId,
|
|
989
|
+
memo,
|
|
990
|
+
flag_color: flagColor,
|
|
991
|
+
},
|
|
992
|
+
});
|
|
993
|
+
return ok(formatScheduledTransaction(data.scheduled_transaction));
|
|
994
|
+
})
|
|
995
|
+
);
|
|
996
|
+
|
|
997
|
+
server.registerTool(
|
|
998
|
+
"update_scheduled_transaction",
|
|
999
|
+
{ description: "Update an existing scheduled transaction. Only provided fields are changed. Amounts in dollars.", inputSchema: {
|
|
1000
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
1001
|
+
scheduledTransactionId: z.string().describe("Scheduled transaction ID"),
|
|
1002
|
+
accountId: z.string().optional().describe("Account ID"),
|
|
1003
|
+
date: z.string().optional().describe("Next occurrence date (YYYY-MM-DD)"),
|
|
1004
|
+
frequency: z.enum(["never", "daily", "weekly", "everyOtherWeek", "twiceAMonth", "every4Weeks", "monthly", "everyOtherMonth", "every3Months", "every4Months", "twiceAYear", "yearly", "everyOtherYear"]).optional().describe("Recurrence frequency"),
|
|
1005
|
+
amount: z.number().optional().describe("Amount in dollars (negative for outflows)"),
|
|
1006
|
+
payeeId: z.string().nullable().optional().describe("Payee ID"),
|
|
1007
|
+
payeeName: z.string().nullable().optional().describe("Payee name"),
|
|
1008
|
+
categoryId: z.string().nullable().optional().describe("Category ID"),
|
|
1009
|
+
memo: z.string().nullable().optional().describe("Memo"),
|
|
1010
|
+
flagColor: z.enum(["red", "orange", "yellow", "green", "blue", "purple"]).nullable().optional().describe("Flag color"),
|
|
1011
|
+
} },
|
|
1012
|
+
({ budgetId, scheduledTransactionId, accountId, date, frequency, amount, payeeId, payeeName, categoryId, memo, flagColor }) =>
|
|
1013
|
+
run(async () => {
|
|
1014
|
+
const bid = resolveBudgetId(budgetId);
|
|
1015
|
+
// PUT replaces the full resource — fetch current values to merge with updates
|
|
1016
|
+
const { data: current } = await api.scheduledTransactions.getScheduledTransactionById(bid, scheduledTransactionId);
|
|
1017
|
+
const existing = current.scheduled_transaction;
|
|
1018
|
+
|
|
1019
|
+
const st = {
|
|
1020
|
+
account_id: accountId ?? existing.account_id,
|
|
1021
|
+
date: date ?? existing.date_next,
|
|
1022
|
+
frequency: frequency ?? existing.frequency,
|
|
1023
|
+
amount: amount !== undefined ? milliunits(amount) : existing.amount,
|
|
1024
|
+
payee_id: payeeId !== undefined ? payeeId : existing.payee_id,
|
|
1025
|
+
payee_name: payeeName !== undefined ? payeeName : existing.payee_name,
|
|
1026
|
+
category_id: categoryId !== undefined ? categoryId : existing.category_id,
|
|
1027
|
+
memo: memo !== undefined ? memo : existing.memo,
|
|
1028
|
+
flag_color: flagColor !== undefined ? flagColor : existing.flag_color,
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
const { data } = await api.scheduledTransactions.updateScheduledTransaction(
|
|
1032
|
+
bid,
|
|
1033
|
+
scheduledTransactionId,
|
|
1034
|
+
{ scheduled_transaction: st }
|
|
1035
|
+
);
|
|
1036
|
+
return ok(formatScheduledTransaction(data.scheduled_transaction));
|
|
1037
|
+
})
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
server.registerTool(
|
|
1041
|
+
"delete_scheduled_transaction",
|
|
1042
|
+
{ description: "Delete a scheduled transaction", inputSchema: {
|
|
1043
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
1044
|
+
scheduledTransactionId: z.string().describe("Scheduled transaction ID"),
|
|
1045
|
+
} },
|
|
1046
|
+
({ budgetId, scheduledTransactionId }) =>
|
|
1047
|
+
run(async () => {
|
|
1048
|
+
const { data } = await api.scheduledTransactions.deleteScheduledTransaction(resolveBudgetId(budgetId), scheduledTransactionId);
|
|
1049
|
+
return ok(formatScheduledTransaction(data.scheduled_transaction));
|
|
1050
|
+
})
|
|
1051
|
+
);
|
|
1052
|
+
|
|
1053
|
+
// ==================== Convenience Tools ====================
|
|
1054
|
+
|
|
1055
|
+
server.registerTool(
|
|
1056
|
+
"search_categories",
|
|
1057
|
+
{ description: "Search categories by partial name match (case-insensitive). Useful for finding category IDs when you only know part of the name.", inputSchema: {
|
|
1058
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
1059
|
+
query: z.string().describe("Partial category name to search for (e.g. 'work' matches '💻 Work Expenses (Oliver LLC)')"),
|
|
1060
|
+
} },
|
|
1061
|
+
({ budgetId, query }) =>
|
|
1062
|
+
run(async () => {
|
|
1063
|
+
const { data } = await api.categories.getCategories(resolveBudgetId(budgetId));
|
|
1064
|
+
const q = query.toLowerCase();
|
|
1065
|
+
const matches = [];
|
|
1066
|
+
for (const g of data.category_groups) {
|
|
1067
|
+
if (g.hidden) continue;
|
|
1068
|
+
for (const c of g.categories) {
|
|
1069
|
+
if (c.hidden) continue;
|
|
1070
|
+
if (c.name.toLowerCase().includes(q)) {
|
|
1071
|
+
matches.push({
|
|
1072
|
+
id: c.id,
|
|
1073
|
+
name: c.name,
|
|
1074
|
+
group: g.name,
|
|
1075
|
+
budgeted: dollars(c.budgeted),
|
|
1076
|
+
activity: dollars(c.activity),
|
|
1077
|
+
balance: dollars(c.balance),
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
if (matches.length === 0) return ok({ message: `No categories matching "${query}"`, suggestions: "Try a shorter search term" });
|
|
1083
|
+
return ok(matches);
|
|
1084
|
+
})
|
|
1085
|
+
);
|
|
1086
|
+
|
|
1087
|
+
server.registerTool(
|
|
1088
|
+
"search_payees",
|
|
1089
|
+
{ description: "Search payees by partial name match (case-insensitive). Useful for finding payee IDs.", inputSchema: {
|
|
1090
|
+
budgetId: z.string().optional().describe("Budget ID (uses default if not provided)"),
|
|
1091
|
+
query: z.string().describe("Partial payee name to search for"),
|
|
1092
|
+
} },
|
|
1093
|
+
({ budgetId, query }) =>
|
|
1094
|
+
run(async () => {
|
|
1095
|
+
const { data } = await api.payees.getPayees(resolveBudgetId(budgetId));
|
|
1096
|
+
const q = query.toLowerCase();
|
|
1097
|
+
const matches = data.payees
|
|
1098
|
+
.filter((p) => p.name.toLowerCase().includes(q))
|
|
1099
|
+
.map((p) => ({ id: p.id, name: p.name, transfer_account_id: p.transfer_account_id, deleted: p.deleted }));
|
|
1100
|
+
if (matches.length === 0) return ok({ message: `No payees matching "${query}"` });
|
|
1101
|
+
return ok(matches);
|
|
1102
|
+
})
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1105
|
+
server.registerTool(
|
|
1106
|
+
"review_unapproved",
|
|
1107
|
+
{ description: "Get all unapproved transactions grouped by status: those already categorized (ready to approve) and those still uncategorized (need category first). Never approve uncategorized transactions without explicit user instruction.", inputSchema: { budgetId: z.string().optional().describe("Budget ID (uses default if not provided)") } },
|
|
1108
|
+
({ budgetId }) =>
|
|
1109
|
+
run(async () => {
|
|
1110
|
+
const { data } = await api.transactions.getTransactions(resolveBudgetId(budgetId), undefined, "unapproved");
|
|
1111
|
+
const txns = data.transactions.map(formatTransaction);
|
|
1112
|
+
const isCategorized = (t) => (t.category_id && t.category_name !== "Uncategorized")
|
|
1113
|
+
|| (t.subtransactions && t.subtransactions.length > 0) // split transactions are categorized via subtransactions
|
|
1114
|
+
|| t.transfer_account_id; // transfers don't need categories
|
|
1115
|
+
const categorized = [], uncategorized = [];
|
|
1116
|
+
for (const t of txns) (isCategorized(t) ? categorized : uncategorized).push(t);
|
|
1117
|
+
return ok({
|
|
1118
|
+
total: txns.length,
|
|
1119
|
+
ready_to_approve: {
|
|
1120
|
+
count: categorized.length,
|
|
1121
|
+
transactions: categorized,
|
|
1122
|
+
},
|
|
1123
|
+
needs_category_first: {
|
|
1124
|
+
count: uncategorized.length,
|
|
1125
|
+
warning: "Do NOT approve these without assigning a category first",
|
|
1126
|
+
transactions: uncategorized,
|
|
1127
|
+
},
|
|
1128
|
+
});
|
|
1129
|
+
})
|
|
1130
|
+
);
|
|
1131
|
+
|
|
1132
|
+
// --- Start ---
|
|
1133
|
+
|
|
1134
|
+
process.on("uncaughtException", (err) => {
|
|
1135
|
+
console.error("Uncaught exception:", err);
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
});
|
|
1138
|
+
|
|
1139
|
+
const transport = new StdioServerTransport();
|
|
1140
|
+
await server.connect(transport);
|