@dizzlkheinz/ynab-mcpb 0.12.2 → 0.15.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/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +3 -0
- package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +3 -0
- package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +1 -0
- package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +5 -0
- package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +1569 -0
- package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +3 -0
- package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +2832 -0
- package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +2709 -0
- package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +2832 -0
- package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +2832 -0
- package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +2709 -0
- package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +1 -0
- package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +5217 -0
- package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +2594 -0
- package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +2594 -0
- package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +231 -0
- package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +2590 -0
- package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +5195 -0
- package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +286 -0
- package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +218 -0
- package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +180 -0
- package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +3 -0
- package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +1 -0
- package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +747 -0
- package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +1 -0
- package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +2594 -0
- package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +2594 -0
- package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +144 -0
- package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +416 -0
- package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +2590 -0
- package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +2590 -0
- package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +2590 -0
- package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +790 -0
- package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +766 -0
- package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +790 -0
- package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +2594 -0
- package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +1000 -0
- package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +3489 -0
- package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +766 -0
- package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +2594 -0
- package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +2456 -0
- package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +2594 -0
- package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +18 -0
- package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +48 -0
- package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +1 -0
- package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +1 -0
- package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +1 -0
- package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +1 -0
- package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +1271 -0
- package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +1570 -0
- package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +2590 -0
- package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +3 -0
- package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +3299 -0
- package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +3299 -0
- package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +1882 -0
- package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +2594 -0
- package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +1 -0
- package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +170 -0
- package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +1 -0
- package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +3 -0
- package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +1 -0
- package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +1 -0
- package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +3 -0
- package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +1 -0
- package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +1 -0
- package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +5 -0
- package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +1 -0
- package/.github/workflows/ci-tests.yml +6 -2
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/release.yml +4 -0
- package/CHANGELOG.md +89 -1
- package/NUL +1 -1
- package/README.md +36 -10
- package/dist/bundle/index.cjs +65 -42
- package/dist/index.js +9 -20
- package/dist/server/YNABMCPServer.d.ts +2 -1
- package/dist/server/YNABMCPServer.js +61 -27
- package/dist/server/cacheKeys.d.ts +8 -0
- package/dist/server/cacheKeys.js +8 -0
- package/dist/server/config.d.ts +22 -3
- package/dist/server/config.js +16 -17
- package/dist/server/errorHandler.d.ts +2 -0
- package/dist/server/errorHandler.js +49 -5
- package/dist/server/securityMiddleware.js +3 -6
- package/dist/server/toolRegistry.js +8 -10
- package/dist/tools/accountTools.js +4 -3
- package/dist/tools/categoryTools.js +8 -7
- package/dist/tools/monthTools.js +2 -1
- package/dist/tools/payeeTools.js +2 -1
- package/dist/tools/reconcileAdapter.js +10 -5
- package/dist/tools/reconciliation/analyzer.d.ts +4 -2
- package/dist/tools/reconciliation/analyzer.js +120 -404
- package/dist/tools/reconciliation/csvParser.d.ts +51 -0
- package/dist/tools/reconciliation/csvParser.js +413 -0
- package/dist/tools/reconciliation/executor.d.ts +8 -0
- package/dist/tools/reconciliation/executor.js +277 -50
- package/dist/tools/reconciliation/index.d.ts +7 -7
- package/dist/tools/reconciliation/index.js +115 -39
- package/dist/tools/reconciliation/matcher.d.ts +24 -3
- package/dist/tools/reconciliation/matcher.js +175 -133
- package/dist/tools/reconciliation/recommendationEngine.js +22 -18
- package/dist/tools/reconciliation/reportFormatter.js +9 -8
- package/dist/tools/reconciliation/signDetector.d.ts +2 -0
- package/dist/tools/reconciliation/signDetector.js +54 -0
- package/dist/tools/reconciliation/types.d.ts +20 -34
- package/dist/tools/reconciliation/types.js +1 -7
- package/dist/tools/reconciliation/ynabAdapter.d.ts +4 -0
- package/dist/tools/reconciliation/ynabAdapter.js +15 -0
- package/dist/tools/transactionTools.d.ts +3 -17
- package/dist/tools/transactionTools.js +5 -17
- package/dist/types/reconciliation.d.ts +24 -0
- package/dist/types/reconciliation.js +1 -0
- package/dist/utils/baseError.d.ts +3 -0
- package/dist/utils/baseError.js +7 -0
- package/dist/utils/errors.d.ts +13 -0
- package/dist/utils/errors.js +15 -0
- package/dist/utils/validationError.d.ts +3 -0
- package/dist/utils/validationError.js +3 -0
- package/docs/guides/ARCHITECTURE.md +12 -129
- package/docs/plans/2025-11-20-reloadable-config-token-validation.md +93 -0
- package/docs/plans/2025-11-21-fix-transaction-cached-property.md +362 -0
- package/docs/plans/2025-11-21-reconciliation-error-handling.md +90 -0
- package/docs/plans/2025-11-21-v014-hardening.md +153 -0
- package/docs/plans/reconciliation-v2-redesign.md +1571 -0
- package/package.json +8 -2
- package/scripts/run-throttled-integration-tests.js +9 -3
- package/scripts/test-recommendations.ts +1 -1
- package/src/__tests__/performance.test.ts +12 -5
- package/src/__tests__/testUtils.ts +62 -5
- package/src/__tests__/tools/reconciliation/csvParser.integration.test.ts +129 -0
- package/src/__tests__/tools/reconciliation/real-world.integration.test.ts +53 -0
- package/src/__tests__/workflows.e2e.test.ts +33 -0
- package/src/index.ts +8 -31
- package/src/server/YNABMCPServer.ts +81 -42
- package/src/server/__tests__/YNABMCPServer.integration.test.ts +10 -12
- package/src/server/__tests__/YNABMCPServer.test.ts +27 -15
- package/src/server/__tests__/config.test.ts +76 -152
- package/src/server/__tests__/server-startup.integration.test.ts +42 -14
- package/src/server/__tests__/toolRegistry.test.ts +1 -1
- package/src/server/cacheKeys.ts +8 -0
- package/src/server/config.ts +20 -38
- package/src/server/errorHandler.ts +52 -5
- package/src/server/securityMiddleware.ts +3 -7
- package/src/server/toolRegistry.ts +14 -10
- package/src/tools/__tests__/categoryTools.test.ts +37 -19
- package/src/tools/__tests__/transactionTools.test.ts +58 -2
- package/src/tools/accountTools.ts +8 -3
- package/src/tools/categoryTools.ts +12 -7
- package/src/tools/monthTools.ts +7 -1
- package/src/tools/payeeTools.ts +7 -1
- package/src/tools/reconcileAdapter.ts +10 -5
- package/src/tools/reconciliation/__tests__/adapter.test.ts +28 -22
- package/src/tools/reconciliation/__tests__/analyzer.test.ts +114 -180
- package/src/tools/reconciliation/__tests__/csvParser.test.ts +87 -0
- package/src/tools/reconciliation/__tests__/executor.integration.test.ts +26 -6
- package/src/tools/reconciliation/__tests__/executor.test.ts +133 -60
- package/src/tools/reconciliation/__tests__/matcher.test.ts +68 -54
- package/src/tools/reconciliation/__tests__/recommendationEngine.test.ts +37 -30
- package/src/tools/reconciliation/__tests__/reportFormatter.test.ts +6 -5
- package/src/tools/reconciliation/__tests__/scenarios/extremes.scenario.test.ts +30 -11
- package/src/tools/reconciliation/__tests__/scenarios/repeatAmount.scenario.test.ts +50 -15
- package/src/tools/reconciliation/__tests__/signDetector.test.ts +211 -0
- package/src/tools/reconciliation/__tests__/ynabAdapter.test.ts +61 -0
- package/src/tools/reconciliation/analyzer.ts +174 -545
- package/src/tools/reconciliation/csvParser.ts +617 -0
- package/src/tools/reconciliation/executor.ts +344 -58
- package/src/tools/reconciliation/index.ts +141 -48
- package/src/tools/reconciliation/matcher.ts +234 -214
- package/src/tools/reconciliation/recommendationEngine.ts +23 -19
- package/src/tools/reconciliation/reportFormatter.ts +16 -11
- package/src/tools/reconciliation/signDetector.ts +117 -0
- package/src/tools/reconciliation/types.ts +39 -61
- package/src/tools/reconciliation/ynabAdapter.ts +33 -0
- package/src/tools/schemas/outputs/utilityOutputs.ts +1 -1
- package/src/tools/transactionTools.ts +7 -18
- package/src/types/reconciliation.ts +49 -0
- package/src/utils/baseError.ts +7 -0
- package/src/utils/errors.ts +21 -0
- package/src/utils/validationError.ts +3 -0
- package/temp-recon.ts +126 -0
- package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +3662 -0
- package/test_mcp_tools.mjs +75 -0
- package/.code/agents/0427d95e-edca-431f-a214-5e53264e29c4/error.txt +0 -8
- package/.code/agents/0d675174-d1e1-41c3-9975-4c2e275819a9/error.txt +0 -3
- package/.code/agents/0d8c5afd-4787-422b-abf8-2e5943fc7e67/error.txt +0 -3
- package/.code/agents/0ec34a70-ed5d-4b9e-bee4-bb0e4cccbc4b/error.txt +0 -1
- package/.code/agents/0ef51a21-1ab1-49d7-9561-0eaa43875ebc/error.txt +0 -12
- package/.code/agents/15db95d7-abad-4b4d-9c3b-8446089cb61d/error.txt +0 -1
- package/.code/agents/19ab9acb-f675-4ff0-902a-09a5476f8149/error.txt +0 -1
- package/.code/agents/1ef7e12d-f6ff-4897-8a9b-152d523d898e/error.txt +0 -5
- package/.code/agents/2465/exec-call_lroN9KKzJVWC7t5423DK1nT9.txt +0 -1453
- package/.code/agents/28edb6fe-95a9-41a0-ae69-aa0100d26c0c/error.txt +0 -8
- package/.code/agents/2ae40cf5-b4bf-42e2-92bf-7ea350a7755e/error.txt +0 -9
- package/.code/agents/2bfc4e1f-ac4b-45a5-b6df-bf89d4dbb54c/error.txt +0 -1
- package/.code/agents/2e2e1134-eff0-49be-ba25-8e2c3468a564/error.txt +0 -5
- package/.code/agents/3/exec-call_203OC4TNVkLxW7z2HCVEQ1cM.txt +0 -81
- package/.code/agents/3/exec-call_SS5T0XSiXB4LSNzUKTl75wkh.txt +0 -610
- package/.code/agents/3322c003-ce5e-48e3-a342-f5049c5bf9a2/error.txt +0 -1
- package/.code/agents/391e9b08-1ebc-468c-9bcd-6d0cc3193b37/error.txt +0 -1
- package/.code/agents/3ab0aa84-b7bb-4054-afa3-40b8fd7d3be0/error.txt +0 -1
- package/.code/agents/3bed368d-50fe-477e-aee3-a6707eaa1ab9/error.txt +0 -3
- package/.code/agents/3e40b925-db12-442f-8d7a-a25fc69a6672/error.txt +0 -8
- package/.code/agents/414d5776-cf58-41f3-9328-a6daed503a50/error.txt +0 -5
- package/.code/agents/42687751-4565-4610-b240-67835b17d861/error.txt +0 -1
- package/.code/agents/46b98876-1a39-43c9-9e2f-507ca6d47335/error.txt +0 -9
- package/.code/agents/4a7d9491-b26f-43dd-850d-2ecdc49b5d1b/error.txt +0 -1
- package/.code/agents/4e60f00a-1b3e-447f-87f3-7faf9deddec3/error.txt +0 -13
- package/.code/agents/5138fc1c-4d49-4b74-a7da-ccdb3a8e44e7/error.txt +0 -14
- package/.code/agents/521cff39-a7a3-42e5-a557-134f0f7daaa0/error.txt +0 -5
- package/.code/agents/53302dc5-3857-4413-9a47-9e0f64a51dc4/error.txt +0 -5
- package/.code/agents/567c7c2e-6a6f-4761-a08d-d36deeb2e0ac/error.txt +0 -5
- package/.code/agents/57b00845-80dc-47c9-953c-3028d16275d6/error.txt +0 -3
- package/.code/agents/593d9005-c2a5-48fd-8813-ece0d3f2de96/error.txt +0 -1
- package/.code/agents/5a112e66-0e1a-42f9-877c-53af56ea3551/error.txt +0 -1
- package/.code/agents/5b05e8ed-7788-4738-b7ee-9faa8180f992/error.txt +0 -5
- package/.code/agents/5f888d6f-d7ca-4ac8-be23-9ea1bf753951/error.txt +0 -5
- package/.code/agents/607db3ab-e4b0-435b-b497-93e9aa525549/error.txt +0 -8
- package/.code/agents/67dcb2a2-900f-4c78-b3fc-80b5213e0ddf/error.txt +0 -8
- package/.code/agents/69ad848c-4e98-49b3-b16c-0094ac2d1759/error.txt +0 -5
- package/.code/agents/6c9cfc5f-0d0b-445c-b121-9f60082c4f70/error.txt +0 -1
- package/.code/agents/6f6f8f77-4ab0-4f6e-9f30-40e8be0bd8f5/error.txt +0 -1
- package/.code/agents/72a7cde4-fa8a-4024-9038-27faa550539b/error.txt +0 -1
- package/.code/agents/7b48335c-8247-43aa-9949-5f820ba8e199/error.txt +0 -1
- package/.code/agents/80944249-bea9-4ac5-87de-a666c4df306e/error.txt +0 -1
- package/.code/agents/826099df-1b66-4186-a915-7eb59f9db19d/error.txt +0 -5
- package/.code/agents/8291d158-18a8-4a92-b799-4e9a4d9cce88/error.txt +0 -1
- package/.code/agents/82fb71a3-20fb-4341-804a-a2fc900f95bc/error.txt +0 -1
- package/.code/agents/855790ea-54ee-43e4-8209-a66994e37590/error.txt +0 -1
- package/.code/agents/88ce3a2e-04f2-42be-9062-bf97aa798da0/error.txt +0 -3
- package/.code/agents/9a17e398-b6ed-4218-bb55-bc64a8d38ce8/error.txt +0 -8
- package/.code/agents/9a4f4bfc-a2a6-4f40-a896-9335b41a7ed1/error.txt +0 -1
- package/.code/agents/9b633e55-ef84-47d6-94bb-fd3dd172ad97/error.txt +0 -1
- package/.code/agents/9b81f3ab-c72b-4a81-9a8f-28a49ddba84a/error.txt +0 -8
- package/.code/agents/a35daf29-b2d1-4aef-9b42-dad63a76bd47/error.txt +0 -3
- package/.code/agents/a81990cc-69ee-44d2-b907-17403c9bc5d7/error.txt +0 -5
- package/.code/agents/ab56260a-4a83-4ad4-9410-f88a23d6520a/error.txt +0 -1
- package/.code/agents/ad722c31-2d1d-45f7-bae2-3f02ca455b60/error.txt +0 -1
- package/.code/agents/b62e8690-3324-4b97-9309-731bee79416b/error.txt +0 -5
- package/.code/agents/baf60a3a-752b-4ad8-99d6-df32423ed2eb/error.txt +0 -1
- package/.code/agents/be049042-7dcb-4ac8-9beb-c8f1aea67742/error.txt +0 -14
- package/.code/agents/bed1dcb4-bfce-4a9f-8594-0f994962aafd/error.txt +0 -1
- package/.code/agents/c324a6cf-e935-4ede-9529-b3ebc18e8d6b/error.txt +0 -5
- package/.code/agents/c37c06ff-dfe3-43f2-9bbc-3ec73ec8f41d/error.txt +0 -5
- package/.code/agents/c8cd6671-433a-456b-9f88-e51cb2df6bfc/error.txt +0 -11
- package/.code/agents/ca2ccb67-2f24-428e-b27d-9365beadd140/error.txt +0 -1
- package/.code/agents/cf08c0c8-e7f0-423e-93ba-547e8e818340/error.txt +0 -8
- package/.code/agents/d579c74f-874b-40a4-9d56-ced1eb6a701d/error.txt +0 -1
- package/.code/agents/df412c98-7378-4deb-8e1e-76c416931181/error.txt +0 -3
- package/.code/agents/e5134eb3-2af4-45b0-8998-051cb4afdb45/error.txt +0 -3
- package/.code/agents/e6308471-aa45-4e9e-9496-2e9404164d97/error.txt +0 -8
- package/.code/agents/e7bd8bc7-23fb-4f46-98dc-b0dcf11b75a1/error.txt +0 -1
- package/.code/agents/e92bec35-378d-4fe1-8ac0-6e1bb3c86911/error.txt +0 -5
- package/.code/agents/ed918fbf-2dc4-4aa2-bfc5-04b65d9471ea/error.txt +0 -1
- package/.code/agents/ef1d756f-b272-48fc-8729-f05c494674f7/error.txt +0 -1
- package/.code/agents/ef359853-0249-4e41-a804-c0fc459fe456/error.txt +0 -1
- package/.code/agents/effc7b4a-4b90-40a0-8c86-a7a99d2d5fd2/error.txt +0 -1
- package/.code/agents/fa15f8d5-8359-4a8b-83a3-2f2056b3ff40/error.txt +0 -3
- package/.code/agents/fbef4193-eadf-4c8a-83ff-4878a6310f25/error.txt +0 -8
- package/.code/agents/fd0a4b4a-fda4-4964-a6d6-2b8a2da387c6/error.txt +0 -1
- package/.gemini/settings.json +0 -8
- package/ADOS-2-Module-1-Complete-Manual.md +0 -757
- package/WARP.md +0 -245
|
@@ -1,166 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
* Unit tests for config module
|
|
3
|
-
*
|
|
4
|
-
* Tests environment validation and server configuration.
|
|
5
|
-
*/
|
|
1
|
+
import { describe, it, expect, vi, afterEach, beforeEach } from 'vitest';
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
import { validateEnvironment } from '../config.js';
|
|
9
|
-
import { ConfigurationError } from '../../types/index.js';
|
|
10
|
-
|
|
11
|
-
describe('config module', () => {
|
|
12
|
-
const originalEnv = process.env;
|
|
3
|
+
const originalEnv = { ...process.env };
|
|
13
4
|
|
|
5
|
+
describe('Config Module', () => {
|
|
14
6
|
beforeEach(() => {
|
|
15
|
-
// Reset modules and environment
|
|
16
7
|
vi.resetModules();
|
|
17
8
|
process.env = { ...originalEnv };
|
|
9
|
+
if (!process.env.YNAB_ACCESS_TOKEN) {
|
|
10
|
+
process.env.YNAB_ACCESS_TOKEN = 'test-token-placeholder';
|
|
11
|
+
}
|
|
18
12
|
});
|
|
19
13
|
|
|
20
14
|
afterEach(() => {
|
|
21
|
-
|
|
22
|
-
process.env = originalEnv;
|
|
15
|
+
process.env = { ...originalEnv };
|
|
23
16
|
});
|
|
24
17
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
expect(result).toEqual({
|
|
33
|
-
accessToken: testToken,
|
|
34
|
-
defaultBudgetId: undefined,
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should trim whitespace from access token', () => {
|
|
39
|
-
const testToken = ' test-token-with-spaces ';
|
|
40
|
-
const expectedToken = 'test-token-with-spaces';
|
|
41
|
-
process.env.YNAB_ACCESS_TOKEN = testToken;
|
|
42
|
-
|
|
43
|
-
const result = validateEnvironment();
|
|
44
|
-
|
|
45
|
-
expect(result).toEqual({
|
|
46
|
-
accessToken: expectedToken,
|
|
47
|
-
defaultBudgetId: undefined,
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is not set', () => {
|
|
52
|
-
delete process.env.YNAB_ACCESS_TOKEN;
|
|
53
|
-
|
|
54
|
-
expect(() => validateEnvironment()).toThrow(ConfigurationError);
|
|
55
|
-
expect(() => validateEnvironment()).toThrow(
|
|
56
|
-
'YNAB_ACCESS_TOKEN environment variable is required but not set',
|
|
57
|
-
);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is undefined', () => {
|
|
61
|
-
delete process.env.YNAB_ACCESS_TOKEN;
|
|
62
|
-
|
|
63
|
-
expect(() => validateEnvironment()).toThrow(ConfigurationError);
|
|
64
|
-
expect(() => validateEnvironment()).toThrow(
|
|
65
|
-
'YNAB_ACCESS_TOKEN environment variable is required but not set',
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is empty string', () => {
|
|
70
|
-
process.env.YNAB_ACCESS_TOKEN = '';
|
|
71
|
-
|
|
72
|
-
expect(() => validateEnvironment()).toThrow(ConfigurationError);
|
|
73
|
-
expect(() => validateEnvironment()).toThrow('YNAB_ACCESS_TOKEN must be a non-empty string');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is only whitespace', () => {
|
|
77
|
-
process.env.YNAB_ACCESS_TOKEN = ' ';
|
|
78
|
-
|
|
79
|
-
expect(() => validateEnvironment()).toThrow(ConfigurationError);
|
|
80
|
-
expect(() => validateEnvironment()).toThrow('YNAB_ACCESS_TOKEN must be a non-empty string');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should throw ConfigurationError when YNAB_ACCESS_TOKEN is not a string', () => {
|
|
84
|
-
// TypeScript normally prevents this, but test runtime validation
|
|
85
|
-
(process.env as any).YNAB_ACCESS_TOKEN = 123;
|
|
86
|
-
|
|
87
|
-
expect(() => validateEnvironment()).toThrow(ConfigurationError);
|
|
88
|
-
expect(() => validateEnvironment()).toThrow('YNAB_ACCESS_TOKEN must be a non-empty string');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('should handle various valid token formats', () => {
|
|
92
|
-
const validTokens = [
|
|
93
|
-
'abc123',
|
|
94
|
-
'token-with-dashes',
|
|
95
|
-
'token_with_underscores',
|
|
96
|
-
'MixedCaseToken',
|
|
97
|
-
'1234567890',
|
|
98
|
-
'very-long-token-with-many-characters-abcdefghijklmnopqrstuvwxyz',
|
|
99
|
-
];
|
|
100
|
-
|
|
101
|
-
validTokens.forEach((token) => {
|
|
102
|
-
process.env.YNAB_ACCESS_TOKEN = token;
|
|
103
|
-
const result = validateEnvironment();
|
|
104
|
-
expect(result.accessToken).toBe(token);
|
|
105
|
-
expect(result.defaultBudgetId).toBeUndefined();
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should handle edge cases with leading and trailing whitespace', () => {
|
|
110
|
-
const testCases = [
|
|
111
|
-
{ input: '\ntest-token\n', expected: 'test-token' },
|
|
112
|
-
{ input: '\ttest-token\t', expected: 'test-token' },
|
|
113
|
-
{ input: ' \t\ntest-token \t\n', expected: 'test-token' },
|
|
114
|
-
];
|
|
115
|
-
|
|
116
|
-
testCases.forEach(({ input, expected }) => {
|
|
117
|
-
process.env.YNAB_ACCESS_TOKEN = input;
|
|
118
|
-
const result = validateEnvironment();
|
|
119
|
-
expect(result.accessToken).toBe(expected);
|
|
120
|
-
expect(result.defaultBudgetId).toBeUndefined();
|
|
121
|
-
});
|
|
122
|
-
});
|
|
18
|
+
it('reloads environment variables on each loadConfig call', async () => {
|
|
19
|
+
const { loadConfig } = await import('../config');
|
|
20
|
+
process.env.YNAB_ACCESS_TOKEN = 'test-token-123';
|
|
21
|
+
expect(loadConfig().YNAB_ACCESS_TOKEN).toBe('test-token-123');
|
|
22
|
+
|
|
23
|
+
process.env.YNAB_ACCESS_TOKEN = 'updated-token-456';
|
|
24
|
+
expect(loadConfig().YNAB_ACCESS_TOKEN).toBe('updated-token-456');
|
|
123
25
|
});
|
|
124
26
|
|
|
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
|
-
|
|
27
|
+
it('keeps the config singleton as a one-time parse', async () => {
|
|
28
|
+
process.env.YNAB_ACCESS_TOKEN = 'initial-token';
|
|
29
|
+
const { config, loadConfig } = await import('../config');
|
|
30
|
+
expect(config.YNAB_ACCESS_TOKEN).toBe('initial-token');
|
|
31
|
+
|
|
32
|
+
process.env.YNAB_ACCESS_TOKEN = 'later-token';
|
|
33
|
+
expect(config.YNAB_ACCESS_TOKEN).toBe('initial-token');
|
|
34
|
+
expect(loadConfig().YNAB_ACCESS_TOKEN).toBe('later-token');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('throws a detailed error if YNAB_ACCESS_TOKEN is missing', async () => {
|
|
38
|
+
const { loadConfig } = await import('../config');
|
|
39
|
+
const env = { ...process.env };
|
|
40
|
+
delete env.YNAB_ACCESS_TOKEN;
|
|
41
|
+
|
|
42
|
+
expect.assertions(2);
|
|
43
|
+
try {
|
|
44
|
+
loadConfig(env);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
expect((error as { name?: string }).name).toBe('ValidationError');
|
|
47
|
+
expect((error as Error).message).toMatch(/YNAB_ACCESS_TOKEN/i);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('parses optional MCP_PORT correctly', async () => {
|
|
52
|
+
const { loadConfig } = await import('../config');
|
|
53
|
+
const env = { ...process.env, YNAB_ACCESS_TOKEN: 'token', MCP_PORT: '8080' };
|
|
54
|
+
|
|
55
|
+
const parsed = loadConfig(env);
|
|
56
|
+
expect(parsed.MCP_PORT).toBe(8080);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('handles missing optional MCP_PORT', async () => {
|
|
60
|
+
const { loadConfig } = await import('../config');
|
|
61
|
+
const env = { ...process.env, YNAB_ACCESS_TOKEN: 'token' };
|
|
62
|
+
delete env.MCP_PORT;
|
|
63
|
+
|
|
64
|
+
const parsed = loadConfig(env);
|
|
65
|
+
expect(parsed.MCP_PORT).toBeUndefined();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('throws an error for an invalid MCP_PORT', async () => {
|
|
69
|
+
const { loadConfig } = await import('../config');
|
|
70
|
+
const env = { ...process.env, YNAB_ACCESS_TOKEN: 'token', MCP_PORT: 'invalid-port' };
|
|
71
|
+
|
|
72
|
+
expect.assertions(2);
|
|
73
|
+
try {
|
|
74
|
+
loadConfig(env);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
expect((error as { name?: string }).name).toBe('ValidationError');
|
|
77
|
+
expect((error as Error).message).toMatch(/MCP_PORT/i);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('parses LOG_LEVEL and defaults to info', async () => {
|
|
82
|
+
const { loadConfig } = await import('../config');
|
|
83
|
+
const envWithLog = { ...process.env, YNAB_ACCESS_TOKEN: 'token', LOG_LEVEL: 'debug' };
|
|
84
|
+
expect(loadConfig(envWithLog).LOG_LEVEL).toBe('debug');
|
|
85
|
+
|
|
86
|
+
const envWithoutLog = { ...envWithLog };
|
|
87
|
+
delete envWithoutLog.LOG_LEVEL; // Ensure LOG_LEVEL is not set
|
|
88
|
+
expect(loadConfig(envWithoutLog).LOG_LEVEL).toBe('info');
|
|
165
89
|
});
|
|
166
90
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
2
|
import { YNABMCPServer } from '../YNABMCPServer';
|
|
3
|
-
import {
|
|
3
|
+
import { ValidationError } from '../../types/index';
|
|
4
4
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
5
5
|
import { skipOnRateLimit } from '../../__tests__/testUtils.js';
|
|
6
6
|
// StdioServerTransport import removed as it's not used in tests
|
|
@@ -55,10 +55,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
55
55
|
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
56
56
|
delete process.env['YNAB_ACCESS_TOKEN'];
|
|
57
57
|
|
|
58
|
-
expect(() => new YNABMCPServer(false)).toThrow(
|
|
59
|
-
expect(() => new YNABMCPServer(false)).toThrow(
|
|
60
|
-
'YNAB_ACCESS_TOKEN environment variable is required but not set',
|
|
61
|
-
);
|
|
58
|
+
expect(() => new YNABMCPServer(false)).toThrow(/YNAB_ACCESS_TOKEN/i);
|
|
62
59
|
|
|
63
60
|
// Restore token
|
|
64
61
|
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
@@ -72,7 +69,6 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
72
69
|
const originalToken = process.env['YNAB_ACCESS_TOKEN'];
|
|
73
70
|
process.env['YNAB_ACCESS_TOKEN'] = '';
|
|
74
71
|
|
|
75
|
-
expect(() => new YNABMCPServer(false)).toThrow(ConfigurationError);
|
|
76
72
|
expect(() => new YNABMCPServer(false)).toThrow(
|
|
77
73
|
'YNAB_ACCESS_TOKEN must be a non-empty string',
|
|
78
74
|
);
|
|
@@ -111,7 +107,10 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
111
107
|
|
|
112
108
|
try {
|
|
113
109
|
const invalidServer = new YNABMCPServer(false);
|
|
114
|
-
await expect(invalidServer.validateToken()).rejects.
|
|
110
|
+
await expect(invalidServer.validateToken()).rejects.toHaveProperty(
|
|
111
|
+
'name',
|
|
112
|
+
'AuthenticationError',
|
|
113
|
+
);
|
|
115
114
|
} finally {
|
|
116
115
|
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
117
116
|
}
|
|
@@ -129,13 +128,16 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
129
128
|
|
|
130
129
|
try {
|
|
131
130
|
const invalidServer = new YNABMCPServer(false);
|
|
132
|
-
await expect(invalidServer.validateToken()).rejects.
|
|
131
|
+
await expect(invalidServer.validateToken()).rejects.toHaveProperty(
|
|
132
|
+
'name',
|
|
133
|
+
'AuthenticationError',
|
|
134
|
+
);
|
|
133
135
|
|
|
134
136
|
// Verify the error message contains relevant information
|
|
135
137
|
try {
|
|
136
138
|
await invalidServer.validateToken();
|
|
137
139
|
} catch (error) {
|
|
138
|
-
expect(error).
|
|
140
|
+
expect((error as { name?: string }).name).toBe('AuthenticationError');
|
|
139
141
|
expect(error.message).toContain('Token validation failed');
|
|
140
142
|
}
|
|
141
143
|
} finally {
|
|
@@ -144,6 +146,29 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
144
146
|
}, ctx);
|
|
145
147
|
},
|
|
146
148
|
);
|
|
149
|
+
|
|
150
|
+
it(
|
|
151
|
+
'should surface malformed token responses as AuthenticationError',
|
|
152
|
+
{ meta: { tier: 'domain', domain: 'server' } },
|
|
153
|
+
async () => {
|
|
154
|
+
const syntaxError = new SyntaxError('Unexpected token < in JSON at position 0');
|
|
155
|
+
const getUserSpy = vi
|
|
156
|
+
.spyOn(server.getYNABAPI().user, 'getUser')
|
|
157
|
+
.mockRejectedValue(syntaxError);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
await expect(server.validateToken()).rejects.toHaveProperty(
|
|
161
|
+
'name',
|
|
162
|
+
'AuthenticationError',
|
|
163
|
+
);
|
|
164
|
+
await expect(server.validateToken()).rejects.toThrow(
|
|
165
|
+
'Unexpected response from YNAB during token validation',
|
|
166
|
+
);
|
|
167
|
+
} finally {
|
|
168
|
+
getUserSpy.mockRestore();
|
|
169
|
+
}
|
|
170
|
+
},
|
|
171
|
+
);
|
|
147
172
|
});
|
|
148
173
|
|
|
149
174
|
describe('Tool Registration', () => {
|
|
@@ -262,7 +287,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
262
287
|
} catch (error) {
|
|
263
288
|
// Expected to fail on stdio connection in test environment
|
|
264
289
|
// Token was already validated above, so this error should be transport-related
|
|
265
|
-
expect(error).not.toBeInstanceOf(
|
|
290
|
+
expect(error).not.toBeInstanceOf(ValidationError);
|
|
266
291
|
}
|
|
267
292
|
|
|
268
293
|
consoleSpy.mockRestore();
|
|
@@ -325,7 +350,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
325
350
|
|
|
326
351
|
expect(() => new YNABMCPServer(false)).toThrow(
|
|
327
352
|
expect.objectContaining({
|
|
328
|
-
message:
|
|
353
|
+
message: expect.stringMatching(/YNAB_ACCESS_TOKEN/i),
|
|
329
354
|
}),
|
|
330
355
|
);
|
|
331
356
|
|
|
@@ -343,7 +368,10 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
343
368
|
|
|
344
369
|
try {
|
|
345
370
|
const server = new YNABMCPServer(false);
|
|
346
|
-
await expect(server.validateToken()).rejects.
|
|
371
|
+
await expect(server.validateToken()).rejects.toHaveProperty(
|
|
372
|
+
'name',
|
|
373
|
+
'AuthenticationError',
|
|
374
|
+
);
|
|
347
375
|
} finally {
|
|
348
376
|
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
349
377
|
}
|
|
@@ -448,7 +476,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
448
476
|
delete process.env['YNAB_ACCESS_TOKEN'];
|
|
449
477
|
|
|
450
478
|
// Should fail immediately on construction, not during run()
|
|
451
|
-
expect(() => new YNABMCPServer(false)).toThrow(
|
|
479
|
+
expect(() => new YNABMCPServer(false)).toThrow(/YNAB_ACCESS_TOKEN/i);
|
|
452
480
|
|
|
453
481
|
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
454
482
|
},
|
|
@@ -466,7 +494,7 @@ describeIntegration('Server Startup and Transport Integration', () => {
|
|
|
466
494
|
const server = new YNABMCPServer(false);
|
|
467
495
|
|
|
468
496
|
// Should fail on token validation, before transport setup
|
|
469
|
-
await expect(server.run()).rejects.
|
|
497
|
+
await expect(server.run()).rejects.toHaveProperty('name', 'AuthenticationError');
|
|
470
498
|
} finally {
|
|
471
499
|
process.env['YNAB_ACCESS_TOKEN'] = originalToken;
|
|
472
500
|
}
|
|
@@ -169,9 +169,9 @@ describe('ToolRegistry', () => {
|
|
|
169
169
|
expect(tools[0]?.name).toBe('sample_tool');
|
|
170
170
|
const schema = tools[0]?.inputSchema as Record<string, unknown> | undefined;
|
|
171
171
|
expect(schema).toBeDefined();
|
|
172
|
+
// Input schemas use io:'input' mode which doesn't set additionalProperties
|
|
172
173
|
expect(schema).toMatchObject({
|
|
173
174
|
type: 'object',
|
|
174
|
-
additionalProperties: false,
|
|
175
175
|
properties: expect.objectContaining({
|
|
176
176
|
id: expect.objectContaining({ type: 'string' }),
|
|
177
177
|
minify: expect.objectContaining({ type: 'boolean' }),
|
package/src/server/config.ts
CHANGED
|
@@ -1,41 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (accessToken === undefined) {
|
|
21
|
-
throw new ConfigurationError('YNAB_ACCESS_TOKEN environment variable is required but not set');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (typeof accessToken !== 'string' || accessToken.trim().length === 0) {
|
|
25
|
-
throw new ConfigurationError('YNAB_ACCESS_TOKEN must be a non-empty string');
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { fromZodError } from 'zod-validation-error';
|
|
4
|
+
import { ValidationError } from '../utils/errors.js';
|
|
5
|
+
|
|
6
|
+
const envSchema = z.object({
|
|
7
|
+
YNAB_ACCESS_TOKEN: z.string().trim().min(1, 'YNAB_ACCESS_TOKEN must be a non-empty string'),
|
|
8
|
+
MCP_PORT: z.coerce.number().int().positive().optional(),
|
|
9
|
+
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error', 'fatal']).default('info'),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export type AppConfig = z.infer<typeof envSchema>;
|
|
13
|
+
|
|
14
|
+
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
|
15
|
+
const result = envSchema.safeParse(env);
|
|
16
|
+
if (!result.success) {
|
|
17
|
+
const validationError = fromZodError(result.error);
|
|
18
|
+
throw new ValidationError(validationError.toString());
|
|
26
19
|
}
|
|
27
|
-
|
|
28
|
-
const trimmedDefaultBudgetId = defaultBudgetId?.trim();
|
|
29
|
-
|
|
30
|
-
const config: ServerConfig = {
|
|
31
|
-
accessToken: accessToken.trim(),
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
if (trimmedDefaultBudgetId && trimmedDefaultBudgetId.length > 0) {
|
|
35
|
-
config.defaultBudgetId = trimmedDefaultBudgetId;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return config;
|
|
20
|
+
return result.data;
|
|
39
21
|
}
|
|
40
22
|
|
|
41
|
-
export
|
|
23
|
+
export const config = loadConfig();
|
|
@@ -12,6 +12,7 @@ interface ErrorResponseFormatter {
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
export const enum YNABErrorCode {
|
|
15
|
+
BAD_REQUEST = 400,
|
|
15
16
|
UNAUTHORIZED = 401,
|
|
16
17
|
FORBIDDEN = 403,
|
|
17
18
|
NOT_FOUND = 404,
|
|
@@ -54,6 +55,11 @@ export class YNABAPIError extends Error {
|
|
|
54
55
|
this.code = code;
|
|
55
56
|
this.originalError = originalError;
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
// Expose status as an alias for code for backward compatibility with tests
|
|
60
|
+
get status(): YNABErrorCode {
|
|
61
|
+
return this.code;
|
|
62
|
+
}
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
export class ValidationError extends Error {
|
|
@@ -106,7 +112,12 @@ export class ErrorHandler {
|
|
|
106
112
|
formattedText = this.formatter.format(errorResponse);
|
|
107
113
|
} catch {
|
|
108
114
|
// Fallback to JSON.stringify if formatter fails
|
|
109
|
-
|
|
115
|
+
try {
|
|
116
|
+
formattedText = JSON.stringify(errorResponse, null, 2);
|
|
117
|
+
} catch {
|
|
118
|
+
// Final fallback if JSON serialization fails (e.g. circular references)
|
|
119
|
+
formattedText = `Error processing request: ${this.getGenericErrorMessage(context)}`;
|
|
120
|
+
}
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
return {
|
|
@@ -135,7 +146,9 @@ export class ErrorHandler {
|
|
|
135
146
|
private createErrorResponse(error: unknown, context: string): ErrorResponse {
|
|
136
147
|
// Handle custom error types
|
|
137
148
|
if (error instanceof YNABAPIError) {
|
|
138
|
-
const
|
|
149
|
+
const ynabDetails = this.extractYNABApiError(error.originalError);
|
|
150
|
+
const detailsToSanitize = ynabDetails?.details || error.originalError;
|
|
151
|
+
const sanitizedDetails = this.sanitizeErrorDetails(detailsToSanitize);
|
|
139
152
|
return {
|
|
140
153
|
error: {
|
|
141
154
|
code: error.code,
|
|
@@ -216,7 +229,22 @@ export class ErrorHandler {
|
|
|
216
229
|
|
|
217
230
|
// Fallback for unknown errors
|
|
218
231
|
// Preserve the original error message for debugging while sanitizing sensitive data
|
|
219
|
-
|
|
232
|
+
let errorMessage: string;
|
|
233
|
+
if (error instanceof Error) {
|
|
234
|
+
errorMessage = error.message;
|
|
235
|
+
} else if (typeof error === 'string') {
|
|
236
|
+
errorMessage = error;
|
|
237
|
+
} else if (error && typeof error === 'object') {
|
|
238
|
+
// Handle plain objects (e.g., YNAB SDK errors that aren't Error instances)
|
|
239
|
+
try {
|
|
240
|
+
errorMessage = JSON.stringify(error, null, 2);
|
|
241
|
+
} catch {
|
|
242
|
+
// Circular reference or other JSON issue
|
|
243
|
+
errorMessage = Object.prototype.toString.call(error);
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
errorMessage = String(error);
|
|
247
|
+
}
|
|
220
248
|
const sanitizedDetails = this.sanitizeErrorDetails(errorMessage);
|
|
221
249
|
|
|
222
250
|
return {
|
|
@@ -264,6 +292,8 @@ export class ErrorHandler {
|
|
|
264
292
|
*/
|
|
265
293
|
private getUserFriendlyMessage(code: YNABErrorCode | SecurityErrorCode, context: string): string {
|
|
266
294
|
switch (code) {
|
|
295
|
+
case YNABErrorCode.BAD_REQUEST:
|
|
296
|
+
return 'The request was invalid. Please check your input data.';
|
|
267
297
|
case YNABErrorCode.UNAUTHORIZED:
|
|
268
298
|
return 'Your YNAB access token is invalid or has expired. Please check your token and try again.';
|
|
269
299
|
case YNABErrorCode.FORBIDDEN:
|
|
@@ -288,6 +318,12 @@ export class ErrorHandler {
|
|
|
288
318
|
*/
|
|
289
319
|
private getErrorSuggestions(code: YNABErrorCode | SecurityErrorCode, context: string): string[] {
|
|
290
320
|
switch (code) {
|
|
321
|
+
case YNABErrorCode.BAD_REQUEST:
|
|
322
|
+
return [
|
|
323
|
+
'Check that all required fields are correct',
|
|
324
|
+
'Verify that dates are in the correct format (ISO 8601)',
|
|
325
|
+
'Ensure amounts are valid numbers',
|
|
326
|
+
];
|
|
291
327
|
case YNABErrorCode.UNAUTHORIZED:
|
|
292
328
|
return [
|
|
293
329
|
'Go to https://app.youneedabudget.com/settings/developer to generate a new access token',
|
|
@@ -401,6 +437,8 @@ export class ErrorHandler {
|
|
|
401
437
|
*/
|
|
402
438
|
private getErrorMessage(code: YNABErrorCode, context: string): string {
|
|
403
439
|
switch (code) {
|
|
440
|
+
case YNABErrorCode.BAD_REQUEST:
|
|
441
|
+
return 'Bad request - invalid parameters';
|
|
404
442
|
case YNABErrorCode.UNAUTHORIZED:
|
|
405
443
|
return 'Invalid or expired YNAB access token';
|
|
406
444
|
case YNABErrorCode.FORBIDDEN:
|
|
@@ -535,6 +573,7 @@ export class ErrorHandler {
|
|
|
535
573
|
*/
|
|
536
574
|
private mapHttpStatusToErrorCode(status: number): YNABErrorCode | null {
|
|
537
575
|
switch (status) {
|
|
576
|
+
case YNABErrorCode.BAD_REQUEST:
|
|
538
577
|
case YNABErrorCode.UNAUTHORIZED:
|
|
539
578
|
case YNABErrorCode.FORBIDDEN:
|
|
540
579
|
case YNABErrorCode.NOT_FOUND:
|
|
@@ -571,11 +610,19 @@ export class ErrorHandler {
|
|
|
571
610
|
* Extracts structured YNAB API error information
|
|
572
611
|
*/
|
|
573
612
|
private extractYNABApiError(error: unknown): { code: YNABErrorCode; details?: string } | null {
|
|
574
|
-
if (!error || typeof error !== 'object'
|
|
613
|
+
if (!error || typeof error !== 'object') {
|
|
575
614
|
return null;
|
|
576
615
|
}
|
|
577
616
|
|
|
578
|
-
|
|
617
|
+
let payload = (error as { error?: unknown }).error;
|
|
618
|
+
|
|
619
|
+
if (!payload) {
|
|
620
|
+
const responseData = (error as { response?: { data?: unknown } }).response?.data;
|
|
621
|
+
if (responseData && typeof responseData === 'object') {
|
|
622
|
+
payload = (responseData as { error?: unknown }).error;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
579
626
|
if (!payload || typeof payload !== 'object') {
|
|
580
627
|
return null;
|
|
581
628
|
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import { z } from 'zod/v4';
|
|
7
|
+
import { fromZodError } from 'zod-validation-error';
|
|
7
8
|
import { globalRateLimiter, RateLimitError } from './rateLimiter.js';
|
|
8
9
|
import { globalRequestLogger } from './requestLogger.js';
|
|
9
10
|
import { ErrorHandler } from './errorHandler.js';
|
|
@@ -112,13 +113,8 @@ export class SecurityMiddleware {
|
|
|
112
113
|
return schema.parse(parameters);
|
|
113
114
|
} catch (error) {
|
|
114
115
|
if (error instanceof z.ZodError) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
? error.issues
|
|
118
|
-
.map((err: z.ZodIssue) => `${err.path.join('.')}: ${err.message}`)
|
|
119
|
-
.join(', ')
|
|
120
|
-
: error.message || 'Validation failed';
|
|
121
|
-
throw new Error(`Validation failed: ${errorMessage}`);
|
|
116
|
+
const validationError = fromZodError(error);
|
|
117
|
+
throw new Error(`Validation failed: ${validationError.message}`);
|
|
122
118
|
}
|
|
123
119
|
throw error;
|
|
124
120
|
}
|