@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
@@ -16,6 +16,7 @@ export declare class YNABMCPServer {
16
16
  private deltaFetcher;
17
17
  private diagnosticManager;
18
18
  private errorHandler;
19
+ private completionsManager;
19
20
  constructor(exitOnError?: boolean);
20
21
  validateToken(): Promise<boolean>;
21
22
  private isMalformedTokenResponse;
@@ -33,39 +34,44 @@ export declare class YNABMCPServer {
33
34
  private warmCacheForBudget;
34
35
  handleListTools(): Promise<{
35
36
  tools: {
36
- name: string;
37
37
  inputSchema: {
38
+ [x: string]: unknown;
38
39
  type: "object";
39
- required?: string[] | undefined;
40
40
  properties?: {
41
41
  [x: string]: object;
42
42
  } | undefined;
43
+ required?: string[] | undefined;
43
44
  };
44
- _meta?: {
45
- [x: string]: unknown;
46
- } | undefined;
47
- icons?: {
48
- src: string;
49
- mimeType?: string | undefined | undefined;
50
- sizes?: string[] | undefined;
51
- }[] | undefined;
52
- title?: string | undefined | undefined;
53
- description?: string | undefined | undefined;
45
+ name: string;
46
+ description?: string | undefined;
54
47
  outputSchema?: {
48
+ [x: string]: unknown;
55
49
  type: "object";
56
- required?: string[] | undefined;
57
50
  properties?: {
58
51
  [x: string]: object;
59
52
  } | undefined;
60
- additionalProperties?: boolean | undefined | undefined;
53
+ required?: string[] | undefined;
61
54
  } | undefined;
62
55
  annotations?: {
63
- title?: string | undefined | undefined;
64
- readOnlyHint?: boolean | undefined | undefined;
65
- destructiveHint?: boolean | undefined | undefined;
66
- idempotentHint?: boolean | undefined | undefined;
67
- openWorldHint?: boolean | undefined | undefined;
56
+ title?: string | undefined;
57
+ readOnlyHint?: boolean | undefined;
58
+ destructiveHint?: boolean | undefined;
59
+ idempotentHint?: boolean | undefined;
60
+ openWorldHint?: boolean | undefined;
61
+ } | undefined;
62
+ execution?: {
63
+ taskSupport?: "optional" | "forbidden" | "required" | undefined;
64
+ } | undefined;
65
+ _meta?: {
66
+ [x: string]: unknown;
68
67
  } | undefined;
68
+ icons?: {
69
+ src: string;
70
+ mimeType?: string | undefined;
71
+ sizes?: string[] | undefined;
72
+ theme?: "light" | "dark" | undefined;
73
+ }[] | undefined;
74
+ title?: string | undefined;
69
75
  }[];
70
76
  }>;
71
77
  handleListResources(): Promise<{
@@ -78,6 +84,11 @@ export declare class YNABMCPServer {
78
84
  content: ({
79
85
  type: "text";
80
86
  text: string;
87
+ annotations?: {
88
+ audience?: ("user" | "assistant")[] | undefined;
89
+ priority?: number | undefined;
90
+ lastModified?: string | undefined;
91
+ } | undefined;
81
92
  _meta?: {
82
93
  [x: string]: unknown;
83
94
  } | undefined;
@@ -85,6 +96,11 @@ export declare class YNABMCPServer {
85
96
  type: "image";
86
97
  data: string;
87
98
  mimeType: string;
99
+ annotations?: {
100
+ audience?: ("user" | "assistant")[] | undefined;
101
+ priority?: number | undefined;
102
+ lastModified?: string | undefined;
103
+ } | undefined;
88
104
  _meta?: {
89
105
  [x: string]: unknown;
90
106
  } | undefined;
@@ -92,47 +108,67 @@ export declare class YNABMCPServer {
92
108
  type: "audio";
93
109
  data: string;
94
110
  mimeType: string;
111
+ annotations?: {
112
+ audience?: ("user" | "assistant")[] | undefined;
113
+ priority?: number | undefined;
114
+ lastModified?: string | undefined;
115
+ } | undefined;
95
116
  _meta?: {
96
117
  [x: string]: unknown;
97
118
  } | undefined;
98
119
  } | {
99
- type: "resource_link";
100
- name: string;
101
120
  uri: string;
121
+ name: string;
122
+ type: "resource_link";
123
+ description?: string | undefined;
124
+ mimeType?: string | undefined;
125
+ annotations?: {
126
+ audience?: ("user" | "assistant")[] | undefined;
127
+ priority?: number | undefined;
128
+ lastModified?: string | undefined;
129
+ } | undefined;
102
130
  _meta?: {
103
131
  [x: string]: unknown;
104
132
  } | undefined;
105
- mimeType?: string | undefined | undefined;
106
133
  icons?: {
107
134
  src: string;
108
- mimeType?: string | undefined | undefined;
135
+ mimeType?: string | undefined;
109
136
  sizes?: string[] | undefined;
137
+ theme?: "light" | "dark" | undefined;
110
138
  }[] | undefined;
111
- title?: string | undefined | undefined;
112
- description?: string | undefined | undefined;
139
+ title?: string | undefined;
113
140
  } | {
114
141
  type: "resource";
115
142
  resource: {
116
143
  uri: string;
117
144
  text: string;
145
+ mimeType?: string | undefined;
118
146
  _meta?: {
119
147
  [x: string]: unknown;
120
148
  } | undefined;
121
- mimeType?: string | undefined | undefined;
122
149
  } | {
123
150
  uri: string;
124
151
  blob: string;
152
+ mimeType?: string | undefined;
125
153
  _meta?: {
126
154
  [x: string]: unknown;
127
155
  } | undefined;
128
- mimeType?: string | undefined | undefined;
129
156
  };
157
+ annotations?: {
158
+ audience?: ("user" | "assistant")[] | undefined;
159
+ priority?: number | undefined;
160
+ lastModified?: string | undefined;
161
+ } | undefined;
130
162
  _meta?: {
131
163
  [x: string]: unknown;
132
164
  } | undefined;
133
165
  })[];
134
166
  _meta?: {
135
167
  [x: string]: unknown;
168
+ progressToken?: string | number | undefined;
169
+ "io.modelcontextprotocol/related-task"?: {
170
+ taskId: string;
171
+ } | undefined;
136
172
  } | undefined;
137
173
  structuredContent?: {
138
174
  [x: string]: unknown;
@@ -152,6 +188,11 @@ export declare class YNABMCPServer {
152
188
  content: ({
153
189
  type: "text";
154
190
  text: string;
191
+ annotations?: {
192
+ audience?: ("user" | "assistant")[] | undefined;
193
+ priority?: number | undefined;
194
+ lastModified?: string | undefined;
195
+ } | undefined;
155
196
  _meta?: {
156
197
  [x: string]: unknown;
157
198
  } | undefined;
@@ -159,6 +200,11 @@ export declare class YNABMCPServer {
159
200
  type: "image";
160
201
  data: string;
161
202
  mimeType: string;
203
+ annotations?: {
204
+ audience?: ("user" | "assistant")[] | undefined;
205
+ priority?: number | undefined;
206
+ lastModified?: string | undefined;
207
+ } | undefined;
162
208
  _meta?: {
163
209
  [x: string]: unknown;
164
210
  } | undefined;
@@ -166,47 +212,67 @@ export declare class YNABMCPServer {
166
212
  type: "audio";
167
213
  data: string;
168
214
  mimeType: string;
215
+ annotations?: {
216
+ audience?: ("user" | "assistant")[] | undefined;
217
+ priority?: number | undefined;
218
+ lastModified?: string | undefined;
219
+ } | undefined;
169
220
  _meta?: {
170
221
  [x: string]: unknown;
171
222
  } | undefined;
172
223
  } | {
173
- type: "resource_link";
174
- name: string;
175
224
  uri: string;
225
+ name: string;
226
+ type: "resource_link";
227
+ description?: string | undefined;
228
+ mimeType?: string | undefined;
229
+ annotations?: {
230
+ audience?: ("user" | "assistant")[] | undefined;
231
+ priority?: number | undefined;
232
+ lastModified?: string | undefined;
233
+ } | undefined;
176
234
  _meta?: {
177
235
  [x: string]: unknown;
178
236
  } | undefined;
179
- mimeType?: string | undefined | undefined;
180
237
  icons?: {
181
238
  src: string;
182
- mimeType?: string | undefined | undefined;
239
+ mimeType?: string | undefined;
183
240
  sizes?: string[] | undefined;
241
+ theme?: "light" | "dark" | undefined;
184
242
  }[] | undefined;
185
- title?: string | undefined | undefined;
186
- description?: string | undefined | undefined;
243
+ title?: string | undefined;
187
244
  } | {
188
245
  type: "resource";
189
246
  resource: {
190
247
  uri: string;
191
248
  text: string;
249
+ mimeType?: string | undefined;
192
250
  _meta?: {
193
251
  [x: string]: unknown;
194
252
  } | undefined;
195
- mimeType?: string | undefined | undefined;
196
253
  } | {
197
254
  uri: string;
198
255
  blob: string;
256
+ mimeType?: string | undefined;
199
257
  _meta?: {
200
258
  [x: string]: unknown;
201
259
  } | undefined;
202
- mimeType?: string | undefined | undefined;
203
260
  };
261
+ annotations?: {
262
+ audience?: ("user" | "assistant")[] | undefined;
263
+ priority?: number | undefined;
264
+ lastModified?: string | undefined;
265
+ } | undefined;
204
266
  _meta?: {
205
267
  [x: string]: unknown;
206
268
  } | undefined;
207
269
  })[];
208
270
  _meta?: {
209
271
  [x: string]: unknown;
272
+ progressToken?: string | number | undefined;
273
+ "io.modelcontextprotocol/related-task"?: {
274
+ taskId: string;
275
+ } | undefined;
210
276
  } | undefined;
211
277
  structuredContent?: {
212
278
  [x: string]: unknown;
@@ -214,39 +280,44 @@ export declare class YNABMCPServer {
214
280
  isError?: boolean | undefined;
215
281
  } | import("./prompts.js").PromptResponse | {
216
282
  tools: {
217
- name: string;
218
283
  inputSchema: {
284
+ [x: string]: unknown;
219
285
  type: "object";
220
- required?: string[] | undefined;
221
286
  properties?: {
222
287
  [x: string]: object;
223
288
  } | undefined;
289
+ required?: string[] | undefined;
224
290
  };
225
- _meta?: {
226
- [x: string]: unknown;
227
- } | undefined;
228
- icons?: {
229
- src: string;
230
- mimeType?: string | undefined | undefined;
231
- sizes?: string[] | undefined;
232
- }[] | undefined;
233
- title?: string | undefined | undefined;
234
- description?: string | undefined | undefined;
291
+ name: string;
292
+ description?: string | undefined;
235
293
  outputSchema?: {
294
+ [x: string]: unknown;
236
295
  type: "object";
237
- required?: string[] | undefined;
238
296
  properties?: {
239
297
  [x: string]: object;
240
298
  } | undefined;
241
- additionalProperties?: boolean | undefined | undefined;
299
+ required?: string[] | undefined;
242
300
  } | undefined;
243
301
  annotations?: {
244
- title?: string | undefined | undefined;
245
- readOnlyHint?: boolean | undefined | undefined;
246
- destructiveHint?: boolean | undefined | undefined;
247
- idempotentHint?: boolean | undefined | undefined;
248
- openWorldHint?: boolean | undefined | undefined;
302
+ title?: string | undefined;
303
+ readOnlyHint?: boolean | undefined;
304
+ destructiveHint?: boolean | undefined;
305
+ idempotentHint?: boolean | undefined;
306
+ openWorldHint?: boolean | undefined;
307
+ } | undefined;
308
+ execution?: {
309
+ taskSupport?: "optional" | "forbidden" | "required" | undefined;
310
+ } | undefined;
311
+ _meta?: {
312
+ [x: string]: unknown;
249
313
  } | undefined;
314
+ icons?: {
315
+ src: string;
316
+ mimeType?: string | undefined;
317
+ sizes?: string[] | undefined;
318
+ theme?: "light" | "dark" | undefined;
319
+ }[] | undefined;
320
+ title?: string | undefined;
250
321
  }[];
251
322
  description: string;
252
323
  messages: {
@@ -3,7 +3,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
3
  import fs from 'fs';
4
4
  import path from 'path';
5
5
  import { z } from 'zod';
6
- import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, ReadResourceRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
6
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListPromptsRequestSchema, ReadResourceRequestSchema, GetPromptRequestSchema, CompleteRequestSchema, ErrorCode, McpError, } from '@modelcontextprotocol/sdk/types.js';
7
7
  import * as ynab from 'ynab';
8
8
  import { AuthenticationError, ConfigurationError, ValidationError as ConfigValidationError, } from '../utils/errors.js';
9
9
  import { ValidationError } from '../types/index.js';
@@ -30,6 +30,7 @@ import { ServerKnowledgeStore } from './serverKnowledgeStore.js';
30
30
  import { DeltaCache } from './deltaCache.js';
31
31
  import { DeltaFetcher } from '../tools/deltaFetcher.js';
32
32
  import { ToolAnnotationPresets } from '../tools/toolCategories.js';
33
+ import { CompletionsManager } from './completions.js';
33
34
  export class YNABMCPServer {
34
35
  constructor(exitOnError = true) {
35
36
  this.exitOnError = exitOnError;
@@ -42,9 +43,13 @@ export class YNABMCPServer {
42
43
  version: this.serverVersion,
43
44
  }, {
44
45
  capabilities: {
45
- tools: { listChanged: true },
46
- resources: { listChanged: true },
47
- prompts: { listChanged: true },
46
+ tools: { listChanged: false },
47
+ resources: {
48
+ subscribe: false,
49
+ listChanged: false,
50
+ },
51
+ prompts: { listChanged: false },
52
+ completions: {},
48
53
  },
49
54
  });
50
55
  this.errorHandler = createErrorHandler(responseFormatter);
@@ -111,6 +116,7 @@ export class YNABMCPServer {
111
116
  serverKnowledgeStore: this.serverKnowledgeStore,
112
117
  deltaCache: this.deltaCache,
113
118
  });
119
+ this.completionsManager = new CompletionsManager(this.ynabAPI, cacheManager, () => this.defaultBudgetId);
114
120
  this.setupToolRegistry();
115
121
  this.setupHandlers();
116
122
  }
@@ -162,12 +168,7 @@ export class YNABMCPServer {
162
168
  });
163
169
  this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
164
170
  const { uri } = request.params;
165
- try {
166
- return await this.resourceManager.readResource(uri);
167
- }
168
- catch (error) {
169
- return this.errorHandler.handleError(error, `reading resource: ${uri}`);
170
- }
171
+ return await this.resourceManager.readResource(uri);
171
172
  });
172
173
  this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
173
174
  return this.promptManager.listPrompts();
@@ -182,7 +183,10 @@ export class YNABMCPServer {
182
183
  tools: this.toolRegistry.listTools(),
183
184
  };
184
185
  });
185
- this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
186
+ this.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
187
+ if (!this.toolRegistry.hasTool(request.params.name)) {
188
+ throw new McpError(ErrorCode.InvalidParams, `Unknown tool: ${request.params.name}`);
189
+ }
186
190
  const rawArgs = (request.params.arguments ?? undefined);
187
191
  const minifyOverride = this.extractMinifyOverride(rawArgs);
188
192
  const sanitizedArgs = rawArgs
@@ -202,8 +206,35 @@ export class YNABMCPServer {
202
206
  if (minifyOverride !== undefined) {
203
207
  executionOptions.minifyOverride = minifyOverride;
204
208
  }
209
+ const progressToken = request.params
210
+ ._meta?.progressToken;
211
+ if (progressToken !== undefined && extra.sendNotification) {
212
+ executionOptions.sendProgress = async (params) => {
213
+ try {
214
+ await extra.sendNotification({
215
+ method: 'notifications/progress',
216
+ params: {
217
+ progressToken,
218
+ progress: params.progress,
219
+ ...(params.total !== undefined && { total: params.total }),
220
+ ...(params.message !== undefined && { message: params.message }),
221
+ },
222
+ });
223
+ }
224
+ catch {
225
+ }
226
+ };
227
+ }
205
228
  return await this.toolRegistry.executeTool(executionOptions);
206
229
  });
230
+ this.server.setRequestHandler(CompleteRequestSchema, async (request) => {
231
+ const { argument, context } = request.params;
232
+ const completionContext = context?.arguments ? { arguments: context.arguments } : undefined;
233
+ const result = await this.completionsManager.getCompletions(argument.name, argument.value, completionContext);
234
+ return {
235
+ completion: result.completion,
236
+ };
237
+ });
207
238
  }
208
239
  setupToolRegistry() {
209
240
  const register = (definition) => {
@@ -79,14 +79,15 @@ export class CacheManager {
79
79
  else {
80
80
  const providedTtl = ttlOrOptions?.ttl;
81
81
  ttl = providedTtl !== undefined ? providedTtl : this.defaultTTL;
82
- if (ttlOrOptions && 'staleWhileRevalidate' in ttlOrOptions) {
82
+ const hasStaleWhileRevalidate = ttlOrOptions !== undefined && 'staleWhileRevalidate' in ttlOrOptions;
83
+ if (hasStaleWhileRevalidate) {
83
84
  staleWhileRevalidate = ttlOrOptions.staleWhileRevalidate;
85
+ if (staleWhileRevalidate === undefined && this.defaultStaleWindow > 0) {
86
+ staleWhileRevalidate = this.defaultStaleWindow;
87
+ }
84
88
  }
85
89
  else {
86
- staleWhileRevalidate = ttlOrOptions?.staleWhileRevalidate;
87
- }
88
- if (staleWhileRevalidate === undefined && this.defaultStaleWindow > 0) {
89
- staleWhileRevalidate = this.defaultStaleWindow;
90
+ staleWhileRevalidate = undefined;
90
91
  }
91
92
  }
92
93
  const entry = {
@@ -0,0 +1,25 @@
1
+ import type * as ynab from 'ynab';
2
+ import type { CacheManager } from './cacheManager.js';
3
+ export interface CompletionResult {
4
+ completion: {
5
+ values: string[];
6
+ total?: number;
7
+ hasMore?: boolean;
8
+ };
9
+ }
10
+ interface CompletionContext {
11
+ arguments?: Record<string, string> | undefined;
12
+ }
13
+ export declare class CompletionsManager {
14
+ private readonly ynabAPI;
15
+ private readonly cacheManager;
16
+ private readonly getDefaultBudgetId;
17
+ constructor(ynabAPI: ynab.API, cacheManager: CacheManager, getDefaultBudgetId: () => string | undefined);
18
+ getCompletions(argumentName: string, value: string, context?: CompletionContext): Promise<CompletionResult>;
19
+ private completeBudgets;
20
+ private completeAccounts;
21
+ private completeCategories;
22
+ private completePayees;
23
+ private filterAndFormat;
24
+ }
25
+ export {};
@@ -0,0 +1,160 @@
1
+ import { CACHE_TTLS } from './cacheManager.js';
2
+ const MAX_COMPLETIONS = 100;
3
+ export class CompletionsManager {
4
+ constructor(ynabAPI, cacheManager, getDefaultBudgetId) {
5
+ this.ynabAPI = ynabAPI;
6
+ this.cacheManager = cacheManager;
7
+ this.getDefaultBudgetId = getDefaultBudgetId;
8
+ }
9
+ async getCompletions(argumentName, value, context) {
10
+ const normalizedName = argumentName.toLowerCase();
11
+ switch (normalizedName) {
12
+ case 'budget_id':
13
+ return this.completeBudgets(value);
14
+ case 'account_id':
15
+ case 'account_name':
16
+ return this.completeAccounts(value, context);
17
+ case 'category':
18
+ case 'category_id':
19
+ return this.completeCategories(value, context);
20
+ case 'payee':
21
+ case 'payee_id':
22
+ return this.completePayees(value, context);
23
+ default:
24
+ return { completion: { values: [], total: 0, hasMore: false } };
25
+ }
26
+ }
27
+ async completeBudgets(value) {
28
+ const budgets = await this.cacheManager.wrap('completions:budgets', {
29
+ ttl: CACHE_TTLS.BUDGETS,
30
+ loader: async () => {
31
+ const response = await this.ynabAPI.budgets.getBudgets();
32
+ return response.data.budgets.map((b) => ({
33
+ id: b.id,
34
+ name: b.name,
35
+ }));
36
+ },
37
+ });
38
+ return this.filterAndFormat(budgets, value, (b) => [b.name, b.id]);
39
+ }
40
+ async completeAccounts(value, context) {
41
+ const budgetId = context?.arguments?.['budget_id'] ?? this.getDefaultBudgetId();
42
+ if (!budgetId) {
43
+ return { completion: { values: [], total: 0, hasMore: false } };
44
+ }
45
+ const accounts = await this.cacheManager.wrap(`completions:accounts:${budgetId}`, {
46
+ ttl: CACHE_TTLS.ACCOUNTS,
47
+ loader: async () => {
48
+ const response = await this.ynabAPI.accounts.getAccounts(budgetId);
49
+ return response.data.accounts
50
+ .filter((a) => !a.deleted && !a.closed)
51
+ .map((a) => ({
52
+ id: a.id,
53
+ name: a.name,
54
+ }));
55
+ },
56
+ });
57
+ return this.filterAndFormat(accounts, value, (a) => [a.name, a.id]);
58
+ }
59
+ async completeCategories(value, context) {
60
+ const budgetId = context?.arguments?.['budget_id'] ?? this.getDefaultBudgetId();
61
+ if (!budgetId) {
62
+ return { completion: { values: [], total: 0, hasMore: false } };
63
+ }
64
+ const categories = await this.cacheManager.wrap(`completions:categories:${budgetId}`, {
65
+ ttl: CACHE_TTLS.CATEGORIES,
66
+ loader: async () => {
67
+ const response = await this.ynabAPI.categories.getCategories(budgetId);
68
+ const result = [];
69
+ for (const group of response.data.category_groups) {
70
+ if (group.hidden || group.deleted)
71
+ continue;
72
+ for (const cat of group.categories) {
73
+ if (cat.hidden || cat.deleted)
74
+ continue;
75
+ result.push({
76
+ id: cat.id,
77
+ name: cat.name,
78
+ group: group.name,
79
+ });
80
+ }
81
+ }
82
+ return result;
83
+ },
84
+ });
85
+ return this.filterAndFormat(categories, value, (c) => [c.name, `${c.group}: ${c.name}`, c.id]);
86
+ }
87
+ async completePayees(value, context) {
88
+ const budgetId = context?.arguments?.['budget_id'] ?? this.getDefaultBudgetId();
89
+ if (!budgetId) {
90
+ return { completion: { values: [], total: 0, hasMore: false } };
91
+ }
92
+ const payees = await this.cacheManager.wrap(`completions:payees:${budgetId}`, {
93
+ ttl: CACHE_TTLS.PAYEES,
94
+ loader: async () => {
95
+ const response = await this.ynabAPI.payees.getPayees(budgetId);
96
+ return response.data.payees
97
+ .filter((p) => !p.deleted)
98
+ .map((p) => ({
99
+ id: p.id,
100
+ name: p.name,
101
+ }));
102
+ },
103
+ });
104
+ return this.filterAndFormat(payees, value, (p) => [p.name, p.id]);
105
+ }
106
+ filterAndFormat(items, value, getSearchableValues) {
107
+ const lowerValue = value.toLowerCase();
108
+ const itemCache = new Map();
109
+ const getCachedValues = (item) => {
110
+ let cached = itemCache.get(item);
111
+ if (!cached) {
112
+ const values = getSearchableValues(item);
113
+ cached = { values, lowerValues: values.map((v) => v.toLowerCase()) };
114
+ itemCache.set(item, cached);
115
+ }
116
+ return cached;
117
+ };
118
+ const matches = items.filter((item) => {
119
+ const { lowerValues } = getCachedValues(item);
120
+ return lowerValues.some((v) => v.includes(lowerValue));
121
+ });
122
+ matches.sort((a, b) => {
123
+ const aCache = getCachedValues(a);
124
+ const bCache = getCachedValues(b);
125
+ const aStartsWith = aCache.lowerValues.some((v) => v.startsWith(lowerValue));
126
+ const bStartsWith = bCache.lowerValues.some((v) => v.startsWith(lowerValue));
127
+ if (aStartsWith && !bStartsWith)
128
+ return -1;
129
+ if (!aStartsWith && bStartsWith)
130
+ return 1;
131
+ return (aCache.values[0] ?? '').localeCompare(bCache.values[0] ?? '');
132
+ });
133
+ const uniqueValues = new Set();
134
+ for (const item of matches) {
135
+ const { values, lowerValues } = getCachedValues(item);
136
+ let selectedValue;
137
+ for (let i = 0; i < values.length; i++) {
138
+ if (lowerValues[i]?.includes(lowerValue)) {
139
+ if (selectedValue === undefined || i < values.indexOf(selectedValue)) {
140
+ selectedValue = values[i];
141
+ }
142
+ break;
143
+ }
144
+ }
145
+ if (selectedValue) {
146
+ uniqueValues.add(selectedValue);
147
+ }
148
+ if (uniqueValues.size >= MAX_COMPLETIONS)
149
+ break;
150
+ }
151
+ const resultValues = Array.from(uniqueValues).slice(0, MAX_COMPLETIONS);
152
+ return {
153
+ completion: {
154
+ values: resultValues,
155
+ total: matches.length,
156
+ hasMore: matches.length > MAX_COMPLETIONS,
157
+ },
158
+ };
159
+ }
160
+ }
@@ -5,10 +5,10 @@ declare const envSchema: z.ZodObject<{
5
5
  YNAB_DEFAULT_BUDGET_ID: z.ZodOptional<z.ZodString>;
6
6
  MCP_PORT: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
7
7
  LOG_LEVEL: z.ZodDefault<z.ZodEnum<{
8
+ debug: "debug";
8
9
  error: "error";
9
10
  warn: "warn";
10
11
  info: "info";
11
- debug: "debug";
12
12
  trace: "trace";
13
13
  fatal: "fatal";
14
14
  }>>;
@@ -17,7 +17,7 @@ export type AppConfig = z.infer<typeof envSchema>;
17
17
  export declare function loadConfig(env?: NodeJS.ProcessEnv): AppConfig;
18
18
  export declare const config: {
19
19
  YNAB_ACCESS_TOKEN: string;
20
- LOG_LEVEL: "error" | "warn" | "info" | "debug" | "trace" | "fatal";
20
+ LOG_LEVEL: "debug" | "error" | "warn" | "info" | "trace" | "fatal";
21
21
  YNAB_DEFAULT_BUDGET_ID?: string | undefined;
22
22
  MCP_PORT?: number | undefined;
23
23
  };
@@ -44,6 +44,7 @@ export class ErrorHandler {
44
44
  }
45
45
  }
46
46
  return {
47
+ isError: true,
47
48
  content: [
48
49
  {
49
50
  type: 'text',
@@ -78,5 +78,7 @@ export class RateLimitError extends Error {
78
78
  }
79
79
  }
80
80
  export const globalRateLimiter = new RateLimiter({
81
- enableLogging: process.env['NODE_ENV'] !== 'production',
81
+ enableLogging: process.env['RATE_LIMIT_LOGGING'] === 'true' ||
82
+ process.env['LOG_LEVEL'] === 'debug' ||
83
+ process.env['VERBOSE_TESTS'] === 'true',
82
84
  });