@dizzlkheinz/ynab-mcpb 0.17.0 → 0.17.1
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/.env.example +33 -33
- package/.github/workflows/ci-tests.yml +45 -45
- package/.github/workflows/claude-code-review.yml +57 -57
- package/.github/workflows/claude.yml +50 -50
- package/.github/workflows/full-integration.yml +22 -22
- package/.github/workflows/publish.yml +11 -2
- package/CLAUDE.md +7 -6
- package/dist/bundle/index.cjs +52 -52
- package/dist/server/YNABMCPServer.d.ts +120 -54
- package/dist/server/securityMiddleware.d.ts +37 -8
- package/dist/tools/schemas/outputs/index.d.ts +2 -2
- package/dist/tools/schemas/outputs/index.js +2 -2
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +0 -15
- package/dist/tools/schemas/outputs/utilityOutputs.js +0 -9
- package/dist/tools/utilityTools.d.ts +0 -7
- package/dist/tools/utilityTools.js +1 -50
- package/docs/maintainers/npm-publishing.md +27 -0
- package/docs/reference/API.md +15 -70
- package/docs/technical/reconciliation-system-architecture.md +2251 -2251
- package/package.json +5 -5
- package/scripts/analyze-bundle.mjs +41 -41
- package/scripts/generate-mcpb.ps1 +95 -95
- package/scripts/watch-and-restart.ps1 +49 -49
- package/src/__tests__/comprehensive.integration.test.ts +0 -28
- package/src/__tests__/performance.test.ts +4 -12
- package/src/__tests__/setup.ts +45 -14
- package/src/__tests__/workflows.e2e.test.ts +0 -44
- package/src/server/__tests__/YNABMCPServer.test.ts +0 -1
- package/src/server/__tests__/toolRegistration.test.ts +2 -2
- package/src/tools/__tests__/transactionTools.integration.test.ts +63 -3
- package/src/tools/__tests__/utilityTools.integration.test.ts +1 -85
- package/src/tools/__tests__/utilityTools.test.ts +1 -123
- package/src/tools/schemas/outputs/index.ts +0 -3
- package/src/tools/schemas/outputs/utilityOutputs.ts +2 -43
- package/src/tools/toolCategories.ts +0 -1
- package/src/tools/utilityTools.ts +5 -76
- package/vitest.config.ts +2 -1
- package/.chunkhound.json +0 -11
- package/.code/agents/0098661e-0fa3-4990-beb9-c0cbf3f123aa/status.txt +0 -1
- package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +0 -3
- package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +0 -3
- package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +0 -1
- package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +0 -5
- package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +0 -1569
- package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +0 -3
- package/.code/agents/1324/exec-call_tIpx9uV1TpARbAMZonRQm8AO.txt +0 -757
- package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +0 -2832
- package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +0 -2709
- package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +0 -2832
- package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +0 -2832
- package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +0 -2709
- package/.code/agents/1572/exec-call_GjVFBFOWcY7lE0idc5nWlLNh.txt +0 -781
- package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +0 -1
- package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +0 -5217
- package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +0 -2594
- package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +0 -2594
- package/.code/agents/1846/exec-call_1YNAVD18RjrMN7JnfkkQhUP3.txt +0 -766
- package/.code/agents/1846/exec-call_lh3lDzE4WJAh1lFiomiiZ73D.txt +0 -766
- package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +0 -231
- package/.code/agents/2038/exec-call_DYwOukaYsL8VCONWmV2rUW5u.txt +0 -766
- package/.code/agents/2038/exec-call_c7fOQ7UrpVcTtvdfGBRM146V.txt +0 -652
- package/.code/agents/2038/exec-call_ySNyq9Mm55jWE480s54r5QcA.txt +0 -766
- package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +0 -2590
- package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +0 -5195
- package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +0 -286
- package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +0 -218
- package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +0 -180
- package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +0 -3
- package/.code/agents/2256/exec-call_AtPcRWPmFPMcmX6qOFm1fCEY.txt +0 -766
- package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +0 -1
- package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +0 -747
- package/.code/agents/2454/exec-call_aFJpupwjfZeOBm7ixI5Vc8z2.txt +0 -766
- package/.code/agents/2454/exec-call_wogZ4HfXTodTEXvdgXlVUBpv.txt +0 -766
- package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +0 -1
- package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +0 -2594
- package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +0 -2594
- package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +0 -144
- package/.code/agents/2e905864-aa07-4314-bcf9-c5b32277e4ac/result.txt +0 -36
- package/.code/agents/3073/exec-call_Peeagc9DxGYLgE6pNdMZhqIE.txt +0 -766
- package/.code/agents/3073/exec-call_d2YSE3hXF08KRSoUM3qd8Z3x.txt +0 -766
- package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +0 -416
- package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +0 -2590
- package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +0 -2590
- package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +0 -2590
- package/.code/agents/335aa031-466d-4fb7-925f-3cd864e264d0/result.txt +0 -191
- package/.code/agents/3364/exec-call_NbhIrsM5HhyDZDmJZG5CuCYL.txt +0 -766
- package/.code/agents/3364/exec-call_cKtJg0NrXiwXEFwlsE3uPZRA.txt +0 -766
- package/.code/agents/36d98414-5cde-4d9d-9a67-a240a18c1f07/result.txt +0 -189
- package/.code/agents/4604e866-b7b8-44f5-992f-2f683b0a523b/status.txt +0 -1
- package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +0 -790
- package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +0 -766
- package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +0 -790
- package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +0 -2594
- package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +0 -1000
- package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +0 -3489
- package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +0 -766
- package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +0 -2594
- package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +0 -2456
- package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +0 -2594
- package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +0 -18
- package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +0 -48
- package/.code/agents/5f8dc01c-47b3-4163-b0b3-aa31be89fcdc/status.txt +0 -1
- package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +0 -1
- package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +0 -1
- package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +0 -1
- package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +0 -1
- package/.code/agents/7/exec-call_HltHpkDox0Zm1vGEjdksUgpE.txt +0 -1120
- package/.code/agents/7/exec-call_LCATrOPPAgbxW9Q1z0XaVi2E.txt +0 -2646
- package/.code/agents/7/exec-call_W8DeRfNG9hvbgVFvf0clBf6R.txt +0 -2646
- package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +0 -1271
- package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +0 -1570
- package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +0 -2590
- package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +0 -3
- package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +0 -3299
- package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +0 -3299
- package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +0 -1882
- package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +0 -2594
- package/.code/agents/94a0ddf3-a304-4ec3-913e-3cceef509948/error.txt +0 -1
- package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +0 -1
- package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +0 -170
- package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +0 -1
- package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +0 -3
- package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +0 -1
- package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +0 -1
- package/.code/agents/e2c752b7-711d-423a-af57-f53c809deb84/result.txt +0 -160
- package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +0 -3
- package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +0 -1
- package/.code/agents/e6601719-c31f-4a0e-8c71-d70787d0ab71/status.txt +0 -1
- package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +0 -1
- package/.code/agents/f250b7ed-5bd5-4036-aa8c-ce63caee7d61/result.txt +0 -20
- package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +0 -5
- package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +0 -1
- package/AGENTS.md +0 -1
- package/NUL +0 -0
- package/package.json.tmp +0 -105
- package/temp-recon.ts +0 -126
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +0 -23
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +0 -23
- package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +0 -44
- package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +0 -58
- package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +0 -3662
- package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +0 -115
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
handleUpdateTransactions,
|
|
10
10
|
CreateTransactionsSchema,
|
|
11
11
|
} from '../transactionTools.js';
|
|
12
|
+
import { waitFor } from '../../__tests__/testUtils.js';
|
|
12
13
|
|
|
13
14
|
const isSkip = ['true', '1', 'yes', 'y', 'on'].includes(
|
|
14
15
|
(process.env['SKIP_E2E_TESTS'] || '').toLowerCase().trim(),
|
|
@@ -325,9 +326,22 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
325
326
|
],
|
|
326
327
|
});
|
|
327
328
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
let transactions: any[] | undefined;
|
|
330
|
+
await waitFor(
|
|
331
|
+
async () => {
|
|
332
|
+
const afterList = await fetchBudgetTransactions();
|
|
333
|
+
transactions =
|
|
334
|
+
afterList.transactions ||
|
|
335
|
+
afterList.preview_transactions ||
|
|
336
|
+
afterList.transaction_preview;
|
|
337
|
+
return (
|
|
338
|
+
(transactions as any[])?.some((transaction) => transaction.memo === memo) ?? false
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
10000,
|
|
342
|
+
500,
|
|
343
|
+
);
|
|
344
|
+
|
|
331
345
|
expect(transactions).toBeDefined();
|
|
332
346
|
expect((transactions as any[]).some((transaction) => transaction.memo === memo)).toBe(true);
|
|
333
347
|
},
|
|
@@ -576,6 +590,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
576
590
|
expect(updateResponse.results[1].correlation_key).toBe(transactionIds[1]);
|
|
577
591
|
|
|
578
592
|
// Verify changes persisted
|
|
593
|
+
await waitFor(
|
|
594
|
+
async () => {
|
|
595
|
+
const getResult1 = await handleGetTransaction(ynabAPI, {
|
|
596
|
+
budget_id: testBudgetId,
|
|
597
|
+
transaction_id: transactionIds[0],
|
|
598
|
+
});
|
|
599
|
+
const transaction1 = parseToolResult(getResult1).transaction;
|
|
600
|
+
return transaction1.amount === -7.5 && transaction1.memo === 'Updated memo 1';
|
|
601
|
+
},
|
|
602
|
+
10000,
|
|
603
|
+
500,
|
|
604
|
+
);
|
|
605
|
+
|
|
579
606
|
const getResult1 = await handleGetTransaction(ynabAPI, {
|
|
580
607
|
budget_id: testBudgetId,
|
|
581
608
|
transaction_id: transactionIds[0],
|
|
@@ -584,6 +611,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
584
611
|
expect(transaction1.amount).toBe(-7.5);
|
|
585
612
|
expect(transaction1.memo).toBe('Updated memo 1');
|
|
586
613
|
|
|
614
|
+
await waitFor(
|
|
615
|
+
async () => {
|
|
616
|
+
const getResult2 = await handleGetTransaction(ynabAPI, {
|
|
617
|
+
budget_id: testBudgetId,
|
|
618
|
+
transaction_id: transactionIds[1],
|
|
619
|
+
});
|
|
620
|
+
const transaction2 = parseToolResult(getResult2).transaction;
|
|
621
|
+
return transaction2.memo === 'Updated memo 2' && transaction2.cleared === 'cleared';
|
|
622
|
+
},
|
|
623
|
+
10000,
|
|
624
|
+
500,
|
|
625
|
+
);
|
|
626
|
+
|
|
587
627
|
const getResult2 = await handleGetTransaction(ynabAPI, {
|
|
588
628
|
budget_id: testBudgetId,
|
|
589
629
|
transaction_id: transactionIds[1],
|
|
@@ -635,6 +675,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
635
675
|
expect(updateResponse.summary.updated).toBe(1);
|
|
636
676
|
|
|
637
677
|
// Verify change
|
|
678
|
+
await waitFor(
|
|
679
|
+
async () => {
|
|
680
|
+
const getResult = await handleGetTransaction(ynabAPI, {
|
|
681
|
+
budget_id: testBudgetId,
|
|
682
|
+
transaction_id: transactionId,
|
|
683
|
+
});
|
|
684
|
+
const transaction = parseToolResult(getResult).transaction;
|
|
685
|
+
return transaction.memo === 'Updated without metadata';
|
|
686
|
+
},
|
|
687
|
+
10000,
|
|
688
|
+
500,
|
|
689
|
+
);
|
|
690
|
+
|
|
638
691
|
const getResult = await handleGetTransaction(ynabAPI, {
|
|
639
692
|
budget_id: testBudgetId,
|
|
640
693
|
transaction_id: transactionId,
|
|
@@ -749,6 +802,13 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
749
802
|
});
|
|
750
803
|
|
|
751
804
|
const updateResponse = parseToolResult(updateResult);
|
|
805
|
+
|
|
806
|
+
if (updateResponse.error) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Tool execution failed unexpectedly: ${JSON.stringify(updateResponse.error)}`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
752
812
|
expect(updateResponse.summary.total_requested).toBe(2);
|
|
753
813
|
expect(updateResponse.summary.updated).toBe(1);
|
|
754
814
|
expect(updateResponse.summary.failed).toBeGreaterThan(0);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
2
|
import * as ynab from 'ynab';
|
|
3
|
-
import { handleGetUser
|
|
3
|
+
import { handleGetUser } from '../utilityTools.js';
|
|
4
4
|
import { skipOnRateLimit } from '../../__tests__/testUtils.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -41,88 +41,4 @@ describeIntegration('Utility Tools Integration Tests', () => {
|
|
|
41
41
|
},
|
|
42
42
|
);
|
|
43
43
|
});
|
|
44
|
-
|
|
45
|
-
describe('handleConvertAmount', () => {
|
|
46
|
-
it(
|
|
47
|
-
'should convert various dollar amounts to milliunits',
|
|
48
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
49
|
-
async () => {
|
|
50
|
-
const testCases = [
|
|
51
|
-
{ dollars: 1.0, expectedMilliunits: 1000 },
|
|
52
|
-
{ dollars: 0.01, expectedMilliunits: 10 },
|
|
53
|
-
{ dollars: 10.5, expectedMilliunits: 10500 },
|
|
54
|
-
{ dollars: 999.99, expectedMilliunits: 999990 },
|
|
55
|
-
{ dollars: 0, expectedMilliunits: 0 },
|
|
56
|
-
{ dollars: -5.25, expectedMilliunits: -5250 },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
for (const testCase of testCases) {
|
|
60
|
-
const result = await handleConvertAmount({
|
|
61
|
-
amount: testCase.dollars,
|
|
62
|
-
to_milliunits: true,
|
|
63
|
-
});
|
|
64
|
-
const response = JSON.parse(result.content[0].text);
|
|
65
|
-
|
|
66
|
-
expect(response.conversion.converted_amount).toBe(testCase.expectedMilliunits);
|
|
67
|
-
expect(response.conversion.to_milliunits).toBe(true);
|
|
68
|
-
expect(response.conversion.description).toContain(`$${testCase.dollars.toFixed(2)}`);
|
|
69
|
-
expect(response.conversion.description).toContain(
|
|
70
|
-
`${testCase.expectedMilliunits} milliunits`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
it(
|
|
77
|
-
'should convert various milliunit amounts to dollars',
|
|
78
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
79
|
-
async () => {
|
|
80
|
-
const testCases = [
|
|
81
|
-
{ milliunits: 1000, expectedDollars: 1.0 },
|
|
82
|
-
{ milliunits: 10, expectedDollars: 0.01 },
|
|
83
|
-
{ milliunits: 10500, expectedDollars: 10.5 },
|
|
84
|
-
{ milliunits: 999990, expectedDollars: 999.99 },
|
|
85
|
-
{ milliunits: 0, expectedDollars: 0 },
|
|
86
|
-
{ milliunits: -5250, expectedDollars: -5.25 },
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
for (const testCase of testCases) {
|
|
90
|
-
const result = await handleConvertAmount({
|
|
91
|
-
amount: testCase.milliunits,
|
|
92
|
-
to_milliunits: false,
|
|
93
|
-
});
|
|
94
|
-
const response = JSON.parse(result.content[0].text);
|
|
95
|
-
|
|
96
|
-
expect(response.conversion.converted_amount).toBe(testCase.expectedDollars);
|
|
97
|
-
expect(response.conversion.to_milliunits).toBe(false);
|
|
98
|
-
expect(response.conversion.description).toContain(`${testCase.milliunits} milliunits`);
|
|
99
|
-
expect(response.conversion.description).toContain(
|
|
100
|
-
`$${testCase.expectedDollars.toFixed(2)}`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
it(
|
|
107
|
-
'should handle precision edge cases',
|
|
108
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
109
|
-
async () => {
|
|
110
|
-
// Test floating-point precision issues
|
|
111
|
-
const precisionTests = [
|
|
112
|
-
{ amount: 0.1 + 0.2, to_milliunits: true }, // Should handle 0.30000000000000004
|
|
113
|
-
{ amount: 1.005, to_milliunits: true }, // Should round correctly
|
|
114
|
-
{ amount: 999.999, to_milliunits: true }, // Should handle near-integer values
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
for (const test of precisionTests) {
|
|
118
|
-
const result = await handleConvertAmount(test);
|
|
119
|
-
const response = JSON.parse(result.content[0].text);
|
|
120
|
-
|
|
121
|
-
expect(response.conversion).toHaveProperty('converted_amount');
|
|
122
|
-
expect(typeof response.conversion.converted_amount).toBe('number');
|
|
123
|
-
expect(Number.isInteger(response.conversion.converted_amount)).toBe(true);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
44
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import * as ynab from 'ynab';
|
|
3
|
-
import { handleGetUser
|
|
3
|
+
import { handleGetUser } from '../utilityTools.js';
|
|
4
4
|
|
|
5
5
|
// Mock the YNAB API
|
|
6
6
|
const mockYnabAPI = {
|
|
@@ -80,126 +80,4 @@ describe('Utility Tools', () => {
|
|
|
80
80
|
expect(result.content[0].text).toContain('Failed to get user information');
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
|
-
|
|
84
|
-
describe('handleConvertAmount', () => {
|
|
85
|
-
it('should convert dollars to milliunits correctly', async () => {
|
|
86
|
-
const params = { amount: 10.5, to_milliunits: true };
|
|
87
|
-
|
|
88
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
89
|
-
const response = JSON.parse(result.content[0].text);
|
|
90
|
-
|
|
91
|
-
expect(response.conversion.original_amount).toBe(10.5);
|
|
92
|
-
expect(response.conversion.converted_amount).toBe(10500);
|
|
93
|
-
expect(response.conversion.to_milliunits).toBe(true);
|
|
94
|
-
expect(response.conversion.description).toBe('$10.50 = 10500 milliunits');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should convert milliunits to dollars correctly', async () => {
|
|
98
|
-
const params = { amount: 10500, to_milliunits: false };
|
|
99
|
-
|
|
100
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
101
|
-
const response = JSON.parse(result.content[0].text);
|
|
102
|
-
|
|
103
|
-
expect(response.conversion.original_amount).toBe(10500);
|
|
104
|
-
expect(response.conversion.converted_amount).toBe(10.5);
|
|
105
|
-
expect(response.conversion.to_milliunits).toBe(false);
|
|
106
|
-
expect(response.conversion.description).toBe('10500 milliunits = $10.50');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should handle zero amounts', async () => {
|
|
110
|
-
const params = { amount: 0, to_milliunits: true };
|
|
111
|
-
|
|
112
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
113
|
-
const response = JSON.parse(result.content[0].text);
|
|
114
|
-
|
|
115
|
-
expect(response.conversion.original_amount).toBe(0);
|
|
116
|
-
expect(response.conversion.converted_amount).toBe(0);
|
|
117
|
-
expect(response.conversion.description).toBe('$0.00 = 0 milliunits');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should handle negative amounts', async () => {
|
|
121
|
-
const params = { amount: -5.25, to_milliunits: true };
|
|
122
|
-
|
|
123
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
124
|
-
const response = JSON.parse(result.content[0].text);
|
|
125
|
-
|
|
126
|
-
expect(response.conversion.original_amount).toBe(-5.25);
|
|
127
|
-
expect(response.conversion.converted_amount).toBe(-5250);
|
|
128
|
-
expect(response.conversion.description).toBe('$-5.25 = -5250 milliunits');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should handle floating-point precision correctly', async () => {
|
|
132
|
-
const params = { amount: 0.01, to_milliunits: true };
|
|
133
|
-
|
|
134
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
135
|
-
const response = JSON.parse(result.content[0].text);
|
|
136
|
-
|
|
137
|
-
expect(response.conversion.converted_amount).toBe(10);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should handle large amounts', async () => {
|
|
141
|
-
const params = { amount: 999999.99, to_milliunits: true };
|
|
142
|
-
|
|
143
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
144
|
-
const response = JSON.parse(result.content[0].text);
|
|
145
|
-
|
|
146
|
-
expect(response.conversion.converted_amount).toBe(999999990);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should round to nearest milliunit when converting from dollars', async () => {
|
|
150
|
-
const params = { amount: 10.5555, to_milliunits: true };
|
|
151
|
-
|
|
152
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
153
|
-
const response = JSON.parse(result.content[0].text);
|
|
154
|
-
|
|
155
|
-
expect(response.conversion.converted_amount).toBe(10556); // Rounded from 10555.5
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('ConvertAmountSchema validation', () => {
|
|
160
|
-
it('should validate correct parameters', () => {
|
|
161
|
-
const validParams = { amount: 10.5, to_milliunits: true };
|
|
162
|
-
const result = ConvertAmountSchema.safeParse(validParams);
|
|
163
|
-
|
|
164
|
-
expect(result.success).toBe(true);
|
|
165
|
-
if (result.success) {
|
|
166
|
-
expect(result.data).toEqual(validParams);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should reject non-finite numbers', () => {
|
|
171
|
-
const invalidParams = { amount: Infinity, to_milliunits: true };
|
|
172
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
173
|
-
|
|
174
|
-
expect(result.success).toBe(false);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should reject NaN values', () => {
|
|
178
|
-
const invalidParams = { amount: NaN, to_milliunits: true };
|
|
179
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
180
|
-
|
|
181
|
-
expect(result.success).toBe(false);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should reject missing amount parameter', () => {
|
|
185
|
-
const invalidParams = { to_milliunits: true };
|
|
186
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
187
|
-
|
|
188
|
-
expect(result.success).toBe(false);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should reject missing to_milliunits parameter', () => {
|
|
192
|
-
const invalidParams = { amount: 10.5 };
|
|
193
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
194
|
-
|
|
195
|
-
expect(result.success).toBe(false);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should reject non-boolean to_milliunits parameter', () => {
|
|
199
|
-
const invalidParams = { amount: 10.5, to_milliunits: 'true' };
|
|
200
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
201
|
-
|
|
202
|
-
expect(result.success).toBe(false);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
83
|
});
|
|
@@ -25,8 +25,6 @@
|
|
|
25
25
|
export {
|
|
26
26
|
GetUserOutputSchema,
|
|
27
27
|
type GetUserOutput,
|
|
28
|
-
ConvertAmountOutputSchema,
|
|
29
|
-
type ConvertAmountOutput,
|
|
30
28
|
GetDefaultBudgetOutputSchema,
|
|
31
29
|
type GetDefaultBudgetOutput,
|
|
32
30
|
SetDefaultBudgetOutputSchema,
|
|
@@ -44,7 +42,6 @@ export {
|
|
|
44
42
|
// Nested schemas that may be useful independently
|
|
45
43
|
export {
|
|
46
44
|
UserSchema,
|
|
47
|
-
ConversionSchema,
|
|
48
45
|
DateFormatSchema,
|
|
49
46
|
CurrencyFormatSchema,
|
|
50
47
|
BudgetDetailSchema,
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* @fileoverview Output schemas for utility tools
|
|
3
3
|
*
|
|
4
4
|
* This file contains comprehensive Zod schemas for validating the output
|
|
5
|
-
* of utility tools including user info,
|
|
6
|
-
*
|
|
5
|
+
* of utility tools including user info, budget defaults, cache management,
|
|
6
|
+
* output formatting, and diagnostic information.
|
|
7
7
|
*
|
|
8
8
|
* All schemas include TypeScript type inference for type-safe usage throughout
|
|
9
9
|
* the codebase. Reference the corresponding handler implementations for
|
|
@@ -46,47 +46,6 @@ export const GetUserOutputSchema = z.object({
|
|
|
46
46
|
|
|
47
47
|
export type GetUserOutput = z.infer<typeof GetUserOutputSchema>;
|
|
48
48
|
|
|
49
|
-
// ============================================================================
|
|
50
|
-
// CONVERT AMOUNT OUTPUT
|
|
51
|
-
// ============================================================================
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Schema for amount conversion details
|
|
55
|
-
*
|
|
56
|
-
* Contains the conversion result between dollars and YNAB milliunits.
|
|
57
|
-
*/
|
|
58
|
-
export const ConversionSchema = z.object({
|
|
59
|
-
original_amount: z.number(),
|
|
60
|
-
converted_amount: z.number(),
|
|
61
|
-
to_milliunits: z.boolean(),
|
|
62
|
-
description: z.string(),
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Output schema for convert_amount tool
|
|
67
|
-
*
|
|
68
|
-
* Converts between dollars and YNAB milliunits (1 dollar = 1000 milliunits).
|
|
69
|
-
*
|
|
70
|
-
* @see src/tools/utilityTools.ts:51-90 - Handler implementation
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```typescript
|
|
74
|
-
* const output: ConvertAmountOutput = {
|
|
75
|
-
* conversion: {
|
|
76
|
-
* original_amount: 25.50,
|
|
77
|
-
* converted_amount: 25500,
|
|
78
|
-
* to_milliunits: true,
|
|
79
|
-
* description: "$25.50 converted to 25500 milliunits"
|
|
80
|
-
* }
|
|
81
|
-
* };
|
|
82
|
-
* ```
|
|
83
|
-
*/
|
|
84
|
-
export const ConvertAmountOutputSchema = z.object({
|
|
85
|
-
conversion: ConversionSchema,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
export type ConvertAmountOutput = z.infer<typeof ConvertAmountOutputSchema>;
|
|
89
|
-
|
|
90
49
|
// ============================================================================
|
|
91
50
|
// GET DEFAULT BUDGET OUTPUT
|
|
92
51
|
// ============================================================================
|
|
@@ -120,7 +120,6 @@ export const ToolAnnotationPresets = {
|
|
|
120
120
|
* interacting with the YNAB API.
|
|
121
121
|
*
|
|
122
122
|
* Examples:
|
|
123
|
-
* - convert_amount: Converts between dollars and milliunits
|
|
124
123
|
* - set_output_format: Configures local output formatting
|
|
125
124
|
* - diagnostic_info: Returns local server diagnostic information
|
|
126
125
|
* - clear_cache: Clears local in-memory cache
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import * as ynab from 'ynab';
|
|
3
|
-
import { z } from 'zod/v4';
|
|
4
3
|
import { responseFormatter } from '../server/responseFormatter.js';
|
|
5
4
|
import { withToolErrorHandling } from '../types/index.js';
|
|
6
5
|
import { createAdapters } from './adapters.js';
|
|
@@ -8,18 +7,6 @@ import { emptyObjectSchema } from './schemas/common.js';
|
|
|
8
7
|
import { ToolAnnotationPresets } from './toolCategories.js';
|
|
9
8
|
import type { ToolFactory } from '../types/toolRegistration.js';
|
|
10
9
|
|
|
11
|
-
/**
|
|
12
|
-
* Schema for ynab:convert_amount tool parameters
|
|
13
|
-
*/
|
|
14
|
-
export const ConvertAmountSchema = z
|
|
15
|
-
.object({
|
|
16
|
-
amount: z.number().finite(),
|
|
17
|
-
to_milliunits: z.boolean(),
|
|
18
|
-
})
|
|
19
|
-
.strict();
|
|
20
|
-
|
|
21
|
-
export type ConvertAmountParams = z.infer<typeof ConvertAmountSchema>;
|
|
22
|
-
|
|
23
10
|
/**
|
|
24
11
|
* Handles the ynab:get_user tool call
|
|
25
12
|
* Gets information about the authenticated user
|
|
@@ -49,58 +36,13 @@ export async function handleGetUser(ynabAPI: ynab.API): Promise<CallToolResult>
|
|
|
49
36
|
}
|
|
50
37
|
|
|
51
38
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
_ynabAPI: ynab.API,
|
|
57
|
-
params: ConvertAmountParams,
|
|
58
|
-
): Promise<CallToolResult> {
|
|
59
|
-
return await withToolErrorHandling(
|
|
60
|
-
async () => {
|
|
61
|
-
const { amount, to_milliunits } = params;
|
|
62
|
-
|
|
63
|
-
let result: number;
|
|
64
|
-
let description: string;
|
|
65
|
-
|
|
66
|
-
if (to_milliunits) {
|
|
67
|
-
// Convert from dollars to milliunits
|
|
68
|
-
// Use integer arithmetic to avoid floating-point precision issues
|
|
69
|
-
result = Math.round(amount * 1000);
|
|
70
|
-
description = `$${amount.toFixed(2)} = ${result} milliunits`;
|
|
71
|
-
} else {
|
|
72
|
-
// Convert from milliunits to dollars
|
|
73
|
-
// Assume input amount is in milliunits
|
|
74
|
-
result = amount / 1000;
|
|
75
|
-
description = `${amount} milliunits = $${result.toFixed(2)}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
content: [
|
|
80
|
-
{
|
|
81
|
-
type: 'text',
|
|
82
|
-
text: responseFormatter.format({
|
|
83
|
-
conversion: {
|
|
84
|
-
original_amount: amount,
|
|
85
|
-
converted_amount: result,
|
|
86
|
-
to_milliunits,
|
|
87
|
-
description,
|
|
88
|
-
},
|
|
89
|
-
}),
|
|
90
|
-
},
|
|
91
|
-
],
|
|
92
|
-
};
|
|
93
|
-
},
|
|
94
|
-
'ynab:convert_amount',
|
|
95
|
-
'converting amount',
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Registers utility tools (get_user, convert_amount) using the shared factory pattern.
|
|
39
|
+
* Registers utility tools (get_user) using the shared factory pattern.
|
|
40
|
+
*
|
|
41
|
+
* Note: convert_amount was removed because all tool responses already return
|
|
42
|
+
* amounts in dollars (converted from YNAB milliunits internally).
|
|
101
43
|
*/
|
|
102
44
|
export const registerUtilityTools: ToolFactory = (registry, context) => {
|
|
103
|
-
const {
|
|
45
|
+
const { adaptNoInput } = createAdapters(context);
|
|
104
46
|
|
|
105
47
|
registry.register({
|
|
106
48
|
name: 'get_user',
|
|
@@ -114,17 +56,4 @@ export const registerUtilityTools: ToolFactory = (registry, context) => {
|
|
|
114
56
|
},
|
|
115
57
|
},
|
|
116
58
|
});
|
|
117
|
-
|
|
118
|
-
registry.register({
|
|
119
|
-
name: 'convert_amount',
|
|
120
|
-
description: 'Convert between dollars and milliunits with integer arithmetic for precision',
|
|
121
|
-
inputSchema: ConvertAmountSchema,
|
|
122
|
-
handler: adapt(handleConvertAmount),
|
|
123
|
-
metadata: {
|
|
124
|
-
annotations: {
|
|
125
|
-
...ToolAnnotationPresets.UTILITY_LOCAL,
|
|
126
|
-
title: 'YNAB: Convert Amount',
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
59
|
};
|
package/vitest.config.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineConfig } from 'vitest/config';
|
|
2
2
|
|
|
3
3
|
const integrationFiles = ['src/**/*.integration.test.ts'];
|
|
4
|
+
const isVerbose = !!process.env['VERBOSE_TESTS'];
|
|
4
5
|
|
|
5
6
|
export default defineConfig({
|
|
6
7
|
test: {
|
|
@@ -83,7 +84,7 @@ export default defineConfig({
|
|
|
83
84
|
},
|
|
84
85
|
},
|
|
85
86
|
},
|
|
86
|
-
reporters: ['verbose', 'html', './vitest-reporters/split-json-reporter.ts'],
|
|
87
|
+
reporters: [isVerbose ? 'verbose' : 'dot', 'html', './vitest-reporters/split-json-reporter.ts'],
|
|
87
88
|
outputFile: {
|
|
88
89
|
html: './test-results/index.html',
|
|
89
90
|
},
|
package/.chunkhound.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Agent cancelled
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Agent cancelled
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
Command failed: [31;1mParserError: [0m
|
|
2
|
-
[31;1m[36;1mLine |[0m
|
|
3
|
-
[31;1m[36;1m[36;1m 5 | [0m [Running[36;1m [0min read-only mode - no modifications allowed][0m
|
|
4
|
-
[31;1m[36;1m[36;1m[0m[36;1m[0m[36;1m | [31;1m ~[0m
|
|
5
|
-
[31;1m[36;1m[36;1m[0m[36;1m[0m[36;1m[31;1m[31;1m[36;1m | [31;1mMissing ] at end of attribute or type literal.[0m
|