@dizzlkheinz/ynab-mcpb 0.17.0 → 0.18.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.
Files changed (182) hide show
  1. package/.env.example +33 -33
  2. package/.github/workflows/ci-tests.yml +45 -45
  3. package/.github/workflows/claude-code-review.yml +57 -57
  4. package/.github/workflows/claude.yml +50 -50
  5. package/.github/workflows/full-integration.yml +22 -22
  6. package/.github/workflows/publish.yml +12 -3
  7. package/.github/workflows/release.yml +2 -2
  8. package/CHANGELOG.md +10 -1
  9. package/CLAUDE.md +16 -12
  10. package/README.md +6 -1
  11. package/dist/bundle/index.cjs +49 -49
  12. package/dist/server/YNABMCPServer.d.ts +125 -54
  13. package/dist/server/YNABMCPServer.js +42 -11
  14. package/dist/server/cacheManager.js +6 -5
  15. package/dist/server/completions.d.ts +25 -0
  16. package/dist/server/completions.js +160 -0
  17. package/dist/server/config.d.ts +2 -2
  18. package/dist/server/errorHandler.js +1 -0
  19. package/dist/server/rateLimiter.js +3 -1
  20. package/dist/server/resources.d.ts +1 -0
  21. package/dist/server/resources.js +33 -16
  22. package/dist/server/securityMiddleware.d.ts +38 -8
  23. package/dist/server/securityMiddleware.js +1 -0
  24. package/dist/server/toolRegistry.d.ts +9 -0
  25. package/dist/server/toolRegistry.js +11 -0
  26. package/dist/tools/adapters.d.ts +3 -1
  27. package/dist/tools/adapters.js +1 -0
  28. package/dist/tools/reconciliation/executor.d.ts +2 -0
  29. package/dist/tools/reconciliation/executor.js +26 -1
  30. package/dist/tools/reconciliation/index.d.ts +3 -2
  31. package/dist/tools/reconciliation/index.js +4 -3
  32. package/dist/tools/schemas/outputs/index.d.ts +2 -2
  33. package/dist/tools/schemas/outputs/index.js +2 -2
  34. package/dist/tools/schemas/outputs/utilityOutputs.d.ts +0 -15
  35. package/dist/tools/schemas/outputs/utilityOutputs.js +0 -9
  36. package/dist/tools/utilityTools.d.ts +0 -7
  37. package/dist/tools/utilityTools.js +1 -50
  38. package/docs/maintainers/npm-publishing.md +27 -0
  39. package/docs/reference/API.md +83 -97
  40. package/docs/technical/reconciliation-system-architecture.md +2251 -2251
  41. package/package.json +6 -6
  42. package/scripts/analyze-bundle.mjs +41 -41
  43. package/scripts/generate-mcpb.ps1 +95 -95
  44. package/scripts/watch-and-restart.ps1 +49 -49
  45. package/src/__tests__/comprehensive.integration.test.ts +4 -32
  46. package/src/__tests__/performance.test.ts +5 -14
  47. package/src/__tests__/setup.ts +45 -14
  48. package/src/__tests__/smoke.e2e.test.ts +70 -0
  49. package/src/__tests__/testUtils.ts +2 -113
  50. package/src/server/YNABMCPServer.ts +64 -10
  51. package/src/server/__tests__/YNABMCPServer.test.ts +0 -1
  52. package/src/server/__tests__/completions.integration.test.ts +117 -0
  53. package/src/server/__tests__/completions.test.ts +319 -0
  54. package/src/server/__tests__/resources.template.test.ts +3 -3
  55. package/src/server/__tests__/resources.test.ts +3 -3
  56. package/src/server/__tests__/toolRegistration.test.ts +3 -3
  57. package/src/server/cacheManager.ts +7 -6
  58. package/src/server/completions.ts +279 -0
  59. package/src/server/errorHandler.ts +1 -0
  60. package/src/server/rateLimiter.ts +4 -1
  61. package/src/server/resources.ts +49 -13
  62. package/src/server/securityMiddleware.ts +1 -0
  63. package/src/server/toolRegistry.ts +42 -0
  64. package/src/tools/__tests__/transactionTools.integration.test.ts +63 -3
  65. package/src/tools/__tests__/utilityTools.integration.test.ts +1 -85
  66. package/src/tools/__tests__/utilityTools.test.ts +1 -123
  67. package/src/tools/adapters.ts +22 -1
  68. package/src/tools/reconciliation/__tests__/executor.progress.test.ts +462 -0
  69. package/src/tools/reconciliation/executor.ts +55 -1
  70. package/src/tools/reconciliation/index.ts +7 -3
  71. package/src/tools/schemas/outputs/index.ts +0 -3
  72. package/src/tools/schemas/outputs/utilityOutputs.ts +2 -43
  73. package/src/tools/toolCategories.ts +0 -1
  74. package/src/tools/utilityTools.ts +5 -76
  75. package/vitest.config.ts +4 -1
  76. package/.chunkhound.json +0 -11
  77. package/.code/agents/0098661e-0fa3-4990-beb9-c0cbf3f123aa/status.txt +0 -1
  78. package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +0 -3
  79. package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +0 -3
  80. package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +0 -1
  81. package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +0 -5
  82. package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +0 -1569
  83. package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +0 -3
  84. package/.code/agents/1324/exec-call_tIpx9uV1TpARbAMZonRQm8AO.txt +0 -757
  85. package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +0 -2832
  86. package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +0 -2709
  87. package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +0 -2832
  88. package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +0 -2832
  89. package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +0 -2709
  90. package/.code/agents/1572/exec-call_GjVFBFOWcY7lE0idc5nWlLNh.txt +0 -781
  91. package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +0 -1
  92. package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +0 -5217
  93. package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +0 -2594
  94. package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +0 -2594
  95. package/.code/agents/1846/exec-call_1YNAVD18RjrMN7JnfkkQhUP3.txt +0 -766
  96. package/.code/agents/1846/exec-call_lh3lDzE4WJAh1lFiomiiZ73D.txt +0 -766
  97. package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +0 -231
  98. package/.code/agents/2038/exec-call_DYwOukaYsL8VCONWmV2rUW5u.txt +0 -766
  99. package/.code/agents/2038/exec-call_c7fOQ7UrpVcTtvdfGBRM146V.txt +0 -652
  100. package/.code/agents/2038/exec-call_ySNyq9Mm55jWE480s54r5QcA.txt +0 -766
  101. package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +0 -2590
  102. package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +0 -5195
  103. package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +0 -286
  104. package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +0 -218
  105. package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +0 -180
  106. package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +0 -3
  107. package/.code/agents/2256/exec-call_AtPcRWPmFPMcmX6qOFm1fCEY.txt +0 -766
  108. package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +0 -1
  109. package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +0 -747
  110. package/.code/agents/2454/exec-call_aFJpupwjfZeOBm7ixI5Vc8z2.txt +0 -766
  111. package/.code/agents/2454/exec-call_wogZ4HfXTodTEXvdgXlVUBpv.txt +0 -766
  112. package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +0 -1
  113. package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +0 -2594
  114. package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +0 -2594
  115. package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +0 -144
  116. package/.code/agents/2e905864-aa07-4314-bcf9-c5b32277e4ac/result.txt +0 -36
  117. package/.code/agents/3073/exec-call_Peeagc9DxGYLgE6pNdMZhqIE.txt +0 -766
  118. package/.code/agents/3073/exec-call_d2YSE3hXF08KRSoUM3qd8Z3x.txt +0 -766
  119. package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +0 -416
  120. package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +0 -2590
  121. package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +0 -2590
  122. package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +0 -2590
  123. package/.code/agents/335aa031-466d-4fb7-925f-3cd864e264d0/result.txt +0 -191
  124. package/.code/agents/3364/exec-call_NbhIrsM5HhyDZDmJZG5CuCYL.txt +0 -766
  125. package/.code/agents/3364/exec-call_cKtJg0NrXiwXEFwlsE3uPZRA.txt +0 -766
  126. package/.code/agents/36d98414-5cde-4d9d-9a67-a240a18c1f07/result.txt +0 -189
  127. package/.code/agents/4604e866-b7b8-44f5-992f-2f683b0a523b/status.txt +0 -1
  128. package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +0 -790
  129. package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +0 -766
  130. package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +0 -790
  131. package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +0 -2594
  132. package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +0 -1000
  133. package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +0 -3489
  134. package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +0 -766
  135. package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +0 -2594
  136. package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +0 -2456
  137. package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +0 -2594
  138. package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +0 -18
  139. package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +0 -48
  140. package/.code/agents/5f8dc01c-47b3-4163-b0b3-aa31be89fcdc/status.txt +0 -1
  141. package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +0 -1
  142. package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +0 -1
  143. package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +0 -1
  144. package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +0 -1
  145. package/.code/agents/7/exec-call_HltHpkDox0Zm1vGEjdksUgpE.txt +0 -1120
  146. package/.code/agents/7/exec-call_LCATrOPPAgbxW9Q1z0XaVi2E.txt +0 -2646
  147. package/.code/agents/7/exec-call_W8DeRfNG9hvbgVFvf0clBf6R.txt +0 -2646
  148. package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +0 -1271
  149. package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +0 -1570
  150. package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +0 -2590
  151. package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +0 -3
  152. package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +0 -3299
  153. package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +0 -3299
  154. package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +0 -1882
  155. package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +0 -2594
  156. package/.code/agents/94a0ddf3-a304-4ec3-913e-3cceef509948/error.txt +0 -1
  157. package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +0 -1
  158. package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +0 -170
  159. package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +0 -1
  160. package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +0 -3
  161. package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +0 -1
  162. package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +0 -1
  163. package/.code/agents/e2c752b7-711d-423a-af57-f53c809deb84/result.txt +0 -160
  164. package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +0 -3
  165. package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +0 -1
  166. package/.code/agents/e6601719-c31f-4a0e-8c71-d70787d0ab71/status.txt +0 -1
  167. package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +0 -1
  168. package/.code/agents/f250b7ed-5bd5-4036-aa8c-ce63caee7d61/result.txt +0 -20
  169. package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +0 -5
  170. package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +0 -1
  171. package/AGENTS.md +0 -1
  172. package/NUL +0 -0
  173. package/package.json.tmp +0 -105
  174. package/src/__tests__/delta.performance.test.ts +0 -80
  175. package/src/__tests__/workflows.e2e.test.ts +0 -1702
  176. package/temp-recon.ts +0 -126
  177. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +0 -23
  178. package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +0 -23
  179. package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +0 -44
  180. package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +0 -58
  181. package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +0 -3662
  182. package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +0 -115
@@ -0,0 +1,70 @@
1
+ /**
2
+ * End-to-end smoke tests for YNAB MCP Server
3
+ *
4
+ * These tests require a real YNAB API key but only perform read operations
5
+ * to verify connectivity and basic functionality without hitting rate limits.
6
+ *
7
+ * NOTE: This file was intentionally reduced from comprehensive CRUD workflow tests
8
+ * to lightweight smoke tests. The full E2E test suite was removed because:
9
+ * 1. YNAB API rate limits (200 requests/hour) made full E2E runs unreliable in CI
10
+ * 2. Comprehensive integration tests with mocked APIs provide better coverage
11
+ * 3. Smoke tests validate real API connectivity without rate limit pressure
12
+ *
13
+ * For full workflow testing, see integration tests in src/tools/__tests__/
14
+ */
15
+
16
+ import { describe, it, expect, beforeAll } from 'vitest';
17
+ import { YNABMCPServer } from '../server/YNABMCPServer.js';
18
+ import {
19
+ getTestConfig,
20
+ createTestServer,
21
+ executeToolCall,
22
+ parseToolResult,
23
+ validateOutputSchema,
24
+ } from './testUtils.js';
25
+
26
+ const runE2ETests = process.env['SKIP_E2E_TESTS'] !== 'true';
27
+ const describeE2E = runE2ETests ? describe : describe.skip;
28
+
29
+ describeE2E('YNAB MCP Server - Smoke Tests', () => {
30
+ let server: YNABMCPServer;
31
+ let testConfig: ReturnType<typeof getTestConfig>;
32
+
33
+ beforeAll(async () => {
34
+ testConfig = getTestConfig();
35
+
36
+ if (testConfig.skipE2ETests) {
37
+ console.warn('Skipping E2E smoke tests - no real API key or SKIP_E2E_TESTS=true');
38
+ return;
39
+ }
40
+
41
+ server = await createTestServer();
42
+ });
43
+
44
+ it('should authenticate and retrieve user information', async () => {
45
+ if (testConfig.skipE2ETests) return;
46
+
47
+ const result = await executeToolCall(server, 'ynab:get_user');
48
+
49
+ // Validate output schema
50
+ const validation = validateOutputSchema(server, 'get_user', result);
51
+ expect(validation.valid).toBe(true);
52
+
53
+ const data = parseToolResult(result);
54
+ expect(data.data.user).toBeDefined();
55
+ expect(data.data.user.id).toBeDefined();
56
+ });
57
+
58
+ it('should list budgets', async () => {
59
+ if (testConfig.skipE2ETests) return;
60
+
61
+ const result = await executeToolCall(server, 'ynab:list_budgets');
62
+
63
+ // Validate output schema
64
+ const validation = validateOutputSchema(server, 'list_budgets', result);
65
+ expect(validation.valid).toBe(true);
66
+
67
+ const data = parseToolResult(result);
68
+ expect(Array.isArray(data.data.budgets)).toBe(true);
69
+ });
70
+ });
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import { expect } from 'vitest';
6
- import { YNABMCPServer } from '../server/YNABMCPServer.js';
6
+ import type { YNABMCPServer } from '../server/YNABMCPServer.js';
7
7
  import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
8
8
  import { z } from 'zod';
9
9
 
@@ -40,6 +40,7 @@ export async function createTestServer(): Promise<YNABMCPServer> {
40
40
  throw new Error('YNAB_ACCESS_TOKEN is required for testing');
41
41
  }
42
42
 
43
+ const { YNABMCPServer } = await import('../server/YNABMCPServer.js');
43
44
  return new YNABMCPServer();
44
45
  }
45
46
 
@@ -366,118 +367,6 @@ export const TestData = {
366
367
  },
367
368
  };
368
369
 
369
- /**
370
- * Test data cleanup utilities
371
- */
372
- export class TestDataCleanup {
373
- private createdAccountIds: string[] = [];
374
- private createdTransactionIds: string[] = [];
375
-
376
- /**
377
- * Track created account for cleanup
378
- */
379
- trackAccount(accountId: string): void {
380
- this.createdAccountIds.push(accountId);
381
- }
382
-
383
- /**
384
- * Track created transaction for cleanup
385
- */
386
- trackTransaction(transactionId: string): void {
387
- this.createdTransactionIds.push(transactionId);
388
- }
389
-
390
- /**
391
- * Clean up all tracked test data
392
- */
393
- async cleanup(server: YNABMCPServer, budgetId: string): Promise<void> {
394
- // Clean up transactions first (they depend on accounts)
395
- for (const transactionId of this.createdTransactionIds) {
396
- try {
397
- await executeToolCall(server, 'ynab:delete_transaction', {
398
- budget_id: budgetId,
399
- transaction_id: transactionId,
400
- });
401
- } catch (error) {
402
- console.warn(`Failed to cleanup transaction ${transactionId}:`, error);
403
- }
404
- }
405
-
406
- // Note: YNAB API doesn't support deleting accounts via API
407
- // Accounts created during testing will need manual cleanup
408
- if (this.createdAccountIds.length > 0) {
409
- console.warn(
410
- `Created ${this.createdAccountIds.length} test accounts that need manual cleanup:`,
411
- this.createdAccountIds,
412
- );
413
- }
414
-
415
- this.createdAccountIds = [];
416
- this.createdTransactionIds = [];
417
- }
418
- }
419
-
420
- /**
421
- * Assertion helpers for YNAB data
422
- */
423
- export const YNABAssertions = {
424
- /**
425
- * Assert budget structure
426
- */
427
- assertBudget(budget: any): void {
428
- expect(budget).toBeDefined();
429
- expect(typeof budget.id).toBe('string');
430
- expect(typeof budget.name).toBe('string');
431
- expect(typeof budget.last_modified_on).toBe('string');
432
- },
433
-
434
- /**
435
- * Assert account structure
436
- */
437
- assertAccount(account: any): void {
438
- expect(account).toBeDefined();
439
- expect(typeof account.id).toBe('string');
440
- expect(typeof account.name).toBe('string');
441
- expect(typeof account.type).toBe('string');
442
- expect(typeof account.on_budget).toBe('boolean');
443
- expect(typeof account.closed).toBe('boolean');
444
- expect(typeof account.balance).toBe('number');
445
- },
446
-
447
- /**
448
- * Assert transaction structure
449
- */
450
- assertTransaction(transaction: any): void {
451
- expect(transaction).toBeDefined();
452
- expect(typeof transaction.id).toBe('string');
453
- expect(typeof transaction.date).toBe('string');
454
- expect(typeof transaction.amount).toBe('number');
455
- expect(typeof transaction.account_id).toBe('string');
456
- expect(['cleared', 'uncleared', 'reconciled']).toContain(transaction.cleared);
457
- },
458
-
459
- /**
460
- * Assert category structure
461
- */
462
- assertCategory(category: any): void {
463
- expect(category).toBeDefined();
464
- expect(typeof category.id).toBe('string');
465
- expect(typeof category.name).toBe('string');
466
- expect(typeof category.category_group_id).toBe('string');
467
- expect(typeof category.budgeted).toBe('number');
468
- expect(typeof category.activity).toBe('number');
469
- expect(typeof category.balance).toBe('number');
470
- },
471
-
472
- /**
473
- * Assert payee structure
474
- */
475
- assertPayee(payee: any): void {
476
- expect(payee).toBeDefined();
477
- expect(typeof payee.id).toBe('string');
478
- expect(typeof payee.name).toBe('string');
479
- },
480
- };
481
370
  /**
482
371
  * Determine whether a value represents a rate-limit (HTTP 429 / "too many requests") error.
483
372
  *
@@ -11,6 +11,9 @@ import {
11
11
  ListPromptsRequestSchema,
12
12
  ReadResourceRequestSchema,
13
13
  GetPromptRequestSchema,
14
+ CompleteRequestSchema,
15
+ ErrorCode,
16
+ McpError,
14
17
  } from '@modelcontextprotocol/sdk/types.js';
15
18
  import type { Tool } from '@modelcontextprotocol/sdk/types.js';
16
19
  import * as ynab from 'ynab';
@@ -36,7 +39,7 @@ import { registerUtilityTools } from '../tools/utilityTools.js';
36
39
  import { emptyObjectSchema } from '../tools/schemas/common.js';
37
40
  import { cacheManager, CacheManager } from './cacheManager.js';
38
41
  import { responseFormatter } from './responseFormatter.js';
39
- import { ToolRegistry, type ToolDefinition } from './toolRegistry.js';
42
+ import { ToolRegistry, type ToolDefinition, type ProgressCallback } from './toolRegistry.js';
40
43
  import { ResourceManager } from './resources.js';
41
44
  import { PromptManager } from './prompts.js';
42
45
  import { DiagnosticManager } from './diagnostics.js';
@@ -44,6 +47,7 @@ import { ServerKnowledgeStore } from './serverKnowledgeStore.js';
44
47
  import { DeltaCache } from './deltaCache.js';
45
48
  import { DeltaFetcher } from '../tools/deltaFetcher.js';
46
49
  import { ToolAnnotationPresets } from '../tools/toolCategories.js';
50
+ import { CompletionsManager } from './completions.js';
47
51
 
48
52
  /**
49
53
  * YNAB MCP Server class that provides integration with You Need A Budget API
@@ -63,6 +67,7 @@ export class YNABMCPServer {
63
67
  private deltaFetcher: DeltaFetcher;
64
68
  private diagnosticManager: DiagnosticManager;
65
69
  private errorHandler: ErrorHandler;
70
+ private completionsManager: CompletionsManager;
66
71
 
67
72
  constructor(exitOnError: boolean = true) {
68
73
  this.exitOnError = exitOnError;
@@ -84,9 +89,13 @@ export class YNABMCPServer {
84
89
  },
85
90
  {
86
91
  capabilities: {
87
- tools: { listChanged: true },
88
- resources: { listChanged: true },
89
- prompts: { listChanged: true },
92
+ tools: { listChanged: false },
93
+ resources: {
94
+ subscribe: false, // YNAB API has no webhooks; subscriptions not applicable
95
+ listChanged: false,
96
+ },
97
+ prompts: { listChanged: false },
98
+ completions: {},
90
99
  },
91
100
  },
92
101
  );
@@ -173,6 +182,12 @@ export class YNABMCPServer {
173
182
  deltaCache: this.deltaCache,
174
183
  });
175
184
 
185
+ this.completionsManager = new CompletionsManager(
186
+ this.ynabAPI,
187
+ cacheManager,
188
+ () => this.defaultBudgetId,
189
+ );
190
+
176
191
  this.setupToolRegistry();
177
192
  this.setupHandlers();
178
193
  }
@@ -247,11 +262,7 @@ export class YNABMCPServer {
247
262
  // Handle read resource requests
248
263
  this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
249
264
  const { uri } = request.params;
250
- try {
251
- return await this.resourceManager.readResource(uri);
252
- } catch (error) {
253
- return this.errorHandler.handleError(error, `reading resource: ${uri}`);
254
- }
265
+ return await this.resourceManager.readResource(uri);
255
266
  });
256
267
 
257
268
  // Handle list prompts requests
@@ -275,7 +286,10 @@ export class YNABMCPServer {
275
286
  });
276
287
 
277
288
  // Handle tool call requests
278
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
289
+ this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
290
+ if (!this.toolRegistry.hasTool(request.params.name)) {
291
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
292
+ }
279
293
  const rawArgs = (request.params.arguments ?? undefined) as
280
294
  | Record<string, unknown>
281
295
  | undefined;
@@ -296,6 +310,7 @@ export class YNABMCPServer {
296
310
  accessToken: string;
297
311
  arguments: Record<string, unknown>;
298
312
  minifyOverride?: boolean;
313
+ sendProgress?: ProgressCallback;
299
314
  } = {
300
315
  name: request.params.name,
301
316
  accessToken: this.configInstance.YNAB_ACCESS_TOKEN,
@@ -306,8 +321,47 @@ export class YNABMCPServer {
306
321
  executionOptions.minifyOverride = minifyOverride;
307
322
  }
308
323
 
324
+ // Create progress callback if client provided a progressToken
325
+ const progressToken = (request.params as { _meta?: { progressToken?: string | number } })
326
+ ._meta?.progressToken;
327
+ if (progressToken !== undefined && extra.sendNotification) {
328
+ executionOptions.sendProgress = async (params) => {
329
+ try {
330
+ await extra.sendNotification({
331
+ method: 'notifications/progress',
332
+ params: {
333
+ progressToken,
334
+ progress: params.progress,
335
+ ...(params.total !== undefined && { total: params.total }),
336
+ ...(params.message !== undefined && { message: params.message }),
337
+ },
338
+ });
339
+ } catch {
340
+ // Progress notifications are non-critical; allow tool execution to continue.
341
+ }
342
+ };
343
+ }
344
+
309
345
  return await this.toolRegistry.executeTool(executionOptions);
310
346
  });
347
+
348
+ // Handle completion requests for autocomplete
349
+ this.server.setRequestHandler(CompleteRequestSchema, async (request) => {
350
+ const { argument, context } = request.params;
351
+
352
+ // Get completions from the manager, handling optional context
353
+ const completionContext = context?.arguments ? { arguments: context.arguments } : undefined;
354
+ const result = await this.completionsManager.getCompletions(
355
+ argument.name,
356
+ argument.value,
357
+ completionContext,
358
+ );
359
+
360
+ // Return in MCP-compliant format
361
+ return {
362
+ completion: result.completion,
363
+ };
364
+ });
311
365
  }
312
366
 
313
367
  /**
@@ -45,7 +45,6 @@ describe('YNABMCPServer', () => {
45
45
  'get_month',
46
46
  'list_months',
47
47
  'get_user',
48
- 'convert_amount',
49
48
  'diagnostic_info',
50
49
  'clear_cache',
51
50
  'set_output_format',
@@ -0,0 +1,117 @@
1
+ import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
2
+ import type { YNABMCPServer } from '../YNABMCPServer.js';
3
+ import { getTestConfig, createTestServer } from '../../__tests__/testUtils.js';
4
+
5
+ /**
6
+ * Integration tests for CompletionsManager
7
+ * Tests completions functionality with real or mocked YNAB API
8
+ */
9
+ describe('CompletionsManager Integration', () => {
10
+ let server: YNABMCPServer;
11
+ let testConfig: ReturnType<typeof getTestConfig>;
12
+
13
+ beforeAll(async () => {
14
+ testConfig = getTestConfig();
15
+
16
+ if (testConfig.skipE2ETests) {
17
+ console.warn('Skipping CompletionsManager integration tests - no real API key');
18
+ return;
19
+ }
20
+
21
+ server = await createTestServer();
22
+ });
23
+
24
+ beforeEach(() => {
25
+ if (testConfig.skipE2ETests) {
26
+ return;
27
+ }
28
+ });
29
+
30
+ describe('Budget Completions', () => {
31
+ it('should complete budget names from real API', async () => {
32
+ if (testConfig.skipE2ETests) return;
33
+
34
+ const completionsManager = (
35
+ server as unknown as {
36
+ completionsManager: {
37
+ getCompletions: (
38
+ arg: string,
39
+ val: string,
40
+ ) => Promise<{ completion: { values: string[]; total: number } }>;
41
+ };
42
+ }
43
+ ).completionsManager;
44
+
45
+ if (!completionsManager) {
46
+ console.warn('CompletionsManager not exposed on server - skipping test');
47
+ return;
48
+ }
49
+
50
+ const result = await completionsManager.getCompletions('budget_id', '');
51
+
52
+ expect(result.completion).toBeDefined();
53
+ expect(Array.isArray(result.completion.values)).toBe(true);
54
+ // Should have at least one budget
55
+ expect(result.completion.total).toBeGreaterThanOrEqual(0);
56
+ });
57
+ });
58
+
59
+ describe('Account Completions', () => {
60
+ it('should require budget context for account completions', async () => {
61
+ if (testConfig.skipE2ETests) return;
62
+
63
+ const completionsManager = (
64
+ server as unknown as {
65
+ completionsManager: {
66
+ getCompletions: (
67
+ arg: string,
68
+ val: string,
69
+ ctx?: unknown,
70
+ ) => Promise<{ completion: { values: string[]; total: number } }>;
71
+ };
72
+ }
73
+ ).completionsManager;
74
+
75
+ if (!completionsManager) {
76
+ console.warn('CompletionsManager not exposed on server - skipping test');
77
+ return;
78
+ }
79
+
80
+ // Without budget context, should return empty
81
+ const result = await completionsManager.getCompletions('account_id', 'check');
82
+
83
+ expect(result.completion).toBeDefined();
84
+ expect(Array.isArray(result.completion.values)).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('MCP Completion Handler Integration', () => {
89
+ it('should handle completion requests through MCP server', async () => {
90
+ if (testConfig.skipE2ETests) return;
91
+
92
+ const mcpServer = server.getServer();
93
+
94
+ // The MCP server should have completion capability
95
+ expect(mcpServer).toBeDefined();
96
+
97
+ // Note: Full MCP completion request testing would require
98
+ // setting up the complete MCP request/response flow
99
+ // This is a basic smoke test for integration
100
+ });
101
+ });
102
+ });
103
+
104
+ /**
105
+ * Mock-based integration tests that don't require real API
106
+ */
107
+ describe('CompletionsManager Mock Integration', () => {
108
+ it('should be importable and constructible', async () => {
109
+ const { CompletionsManager } = await import('../completions.js');
110
+ expect(CompletionsManager).toBeDefined();
111
+ });
112
+
113
+ it('should export correct types', async () => {
114
+ const module = await import('../completions.js');
115
+ expect(module.CompletionsManager).toBeDefined();
116
+ });
117
+ });