@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.
- package/.env.example +33 -33
- package/.github/workflows/ci-tests.yml +45 -45
- package/.github/workflows/claude-code-review.yml +57 -57
- package/.github/workflows/claude.yml +50 -50
- package/.github/workflows/full-integration.yml +22 -22
- package/.github/workflows/publish.yml +12 -3
- package/.github/workflows/release.yml +2 -2
- package/CHANGELOG.md +10 -1
- package/CLAUDE.md +16 -12
- package/README.md +6 -1
- package/dist/bundle/index.cjs +49 -49
- package/dist/server/YNABMCPServer.d.ts +125 -54
- package/dist/server/YNABMCPServer.js +42 -11
- package/dist/server/cacheManager.js +6 -5
- package/dist/server/completions.d.ts +25 -0
- package/dist/server/completions.js +160 -0
- package/dist/server/config.d.ts +2 -2
- package/dist/server/errorHandler.js +1 -0
- package/dist/server/rateLimiter.js +3 -1
- package/dist/server/resources.d.ts +1 -0
- package/dist/server/resources.js +33 -16
- package/dist/server/securityMiddleware.d.ts +38 -8
- package/dist/server/securityMiddleware.js +1 -0
- package/dist/server/toolRegistry.d.ts +9 -0
- package/dist/server/toolRegistry.js +11 -0
- package/dist/tools/adapters.d.ts +3 -1
- package/dist/tools/adapters.js +1 -0
- package/dist/tools/reconciliation/executor.d.ts +2 -0
- package/dist/tools/reconciliation/executor.js +26 -1
- package/dist/tools/reconciliation/index.d.ts +3 -2
- package/dist/tools/reconciliation/index.js +4 -3
- package/dist/tools/schemas/outputs/index.d.ts +2 -2
- package/dist/tools/schemas/outputs/index.js +2 -2
- package/dist/tools/schemas/outputs/utilityOutputs.d.ts +0 -15
- package/dist/tools/schemas/outputs/utilityOutputs.js +0 -9
- package/dist/tools/utilityTools.d.ts +0 -7
- package/dist/tools/utilityTools.js +1 -50
- package/docs/maintainers/npm-publishing.md +27 -0
- package/docs/reference/API.md +83 -97
- package/docs/technical/reconciliation-system-architecture.md +2251 -2251
- package/package.json +6 -6
- package/scripts/analyze-bundle.mjs +41 -41
- package/scripts/generate-mcpb.ps1 +95 -95
- package/scripts/watch-and-restart.ps1 +49 -49
- package/src/__tests__/comprehensive.integration.test.ts +4 -32
- package/src/__tests__/performance.test.ts +5 -14
- package/src/__tests__/setup.ts +45 -14
- package/src/__tests__/smoke.e2e.test.ts +70 -0
- package/src/__tests__/testUtils.ts +2 -113
- package/src/server/YNABMCPServer.ts +64 -10
- package/src/server/__tests__/YNABMCPServer.test.ts +0 -1
- package/src/server/__tests__/completions.integration.test.ts +117 -0
- package/src/server/__tests__/completions.test.ts +319 -0
- package/src/server/__tests__/resources.template.test.ts +3 -3
- package/src/server/__tests__/resources.test.ts +3 -3
- package/src/server/__tests__/toolRegistration.test.ts +3 -3
- package/src/server/cacheManager.ts +7 -6
- package/src/server/completions.ts +279 -0
- package/src/server/errorHandler.ts +1 -0
- package/src/server/rateLimiter.ts +4 -1
- package/src/server/resources.ts +49 -13
- package/src/server/securityMiddleware.ts +1 -0
- package/src/server/toolRegistry.ts +42 -0
- package/src/tools/__tests__/transactionTools.integration.test.ts +63 -3
- package/src/tools/__tests__/utilityTools.integration.test.ts +1 -85
- package/src/tools/__tests__/utilityTools.test.ts +1 -123
- package/src/tools/adapters.ts +22 -1
- package/src/tools/reconciliation/__tests__/executor.progress.test.ts +462 -0
- package/src/tools/reconciliation/executor.ts +55 -1
- package/src/tools/reconciliation/index.ts +7 -3
- package/src/tools/schemas/outputs/index.ts +0 -3
- package/src/tools/schemas/outputs/utilityOutputs.ts +2 -43
- package/src/tools/toolCategories.ts +0 -1
- package/src/tools/utilityTools.ts +5 -76
- package/vitest.config.ts +4 -1
- package/.chunkhound.json +0 -11
- package/.code/agents/0098661e-0fa3-4990-beb9-c0cbf3f123aa/status.txt +0 -1
- package/.code/agents/01a13ef4-3f23-4f52-b33b-3585b73cfa60/error.txt +0 -3
- package/.code/agents/084fd32f-e298-4728-9103-a78d7dc39613/error.txt +0 -3
- package/.code/agents/0fed51e1-a943-4b97-a2a8-a6f0f27c844d/status.txt +0 -1
- package/.code/agents/1059b6bd-5ccd-4d83-a12c-7c9d89137399/error.txt +0 -5
- package/.code/agents/110/exec-call_F9BDNG7JfxKkq7Vc8ESAvdft.txt +0 -1569
- package/.code/agents/11ebcef3-b13f-4e44-ad80-d94a866804b7/error.txt +0 -3
- package/.code/agents/1324/exec-call_tIpx9uV1TpARbAMZonRQm8AO.txt +0 -757
- package/.code/agents/1398/exec-call_CjItcWMU1G6JoPshX62QvpaR.txt +0 -2832
- package/.code/agents/1398/exec-call_SUVq2ivmONQ5LMCmd7ngmOqr.txt +0 -2709
- package/.code/agents/1398/exec-call_SdNY4NOffdcC5pRYjVXHjPCK.txt +0 -2832
- package/.code/agents/1398/exec-call_qblJo9et1gsFFB63TtLOiji2.txt +0 -2832
- package/.code/agents/1398/exec-call_zaRrzlGz7GJcNzVfkAmML7Zg.txt +0 -2709
- package/.code/agents/1572/exec-call_GjVFBFOWcY7lE0idc5nWlLNh.txt +0 -781
- package/.code/agents/171834fd-5905-42fc-bbcc-2c755145b0fc/status.txt +0 -1
- package/.code/agents/1724/exec-call_HvHQe0w5CCG3T7Q3ULT6MO3g.txt +0 -5217
- package/.code/agents/1724/exec-call_QwUNESVzfxxk78K1frh1Vahb.txt +0 -2594
- package/.code/agents/1724/exec-call_aJ1Xwz71XmIpD4SBxSHERzLe.txt +0 -2594
- package/.code/agents/1846/exec-call_1YNAVD18RjrMN7JnfkkQhUP3.txt +0 -766
- package/.code/agents/1846/exec-call_lh3lDzE4WJAh1lFiomiiZ73D.txt +0 -766
- package/.code/agents/1d7d7ab7-7473-4b69-8b97-6e914f56056a/result.txt +0 -231
- package/.code/agents/2038/exec-call_DYwOukaYsL8VCONWmV2rUW5u.txt +0 -766
- package/.code/agents/2038/exec-call_c7fOQ7UrpVcTtvdfGBRM146V.txt +0 -652
- package/.code/agents/2038/exec-call_ySNyq9Mm55jWE480s54r5QcA.txt +0 -766
- package/.code/agents/210/exec-call_0tQCsKNJ1WTuIchb8wlcFJpW.txt +0 -2590
- package/.code/agents/210/exec-call_8ZlY9cUc8Ft1twi4ch8UJ6IN.txt +0 -5195
- package/.code/agents/2188/exec-call_5HqayBxIteJtoI8oPTiLWgvJ.txt +0 -286
- package/.code/agents/2188/exec-call_XRbBKBq3adZe6dcppAvQtM7G.txt +0 -218
- package/.code/agents/2188/exec-call_ehA0SjpYtrUi6GJXmibLjp4i.txt +0 -180
- package/.code/agents/21902821-ecaf-4759-bb9d-222b90921af5/error.txt +0 -3
- package/.code/agents/2256/exec-call_AtPcRWPmFPMcmX6qOFm1fCEY.txt +0 -766
- package/.code/agents/232073be-aa0e-46da-b478-5b64dbf03cf5/status.txt +0 -1
- package/.code/agents/234ff534-2336-4771-a8d9-aa04421a63be/result.txt +0 -747
- package/.code/agents/2454/exec-call_aFJpupwjfZeOBm7ixI5Vc8z2.txt +0 -766
- package/.code/agents/2454/exec-call_wogZ4HfXTodTEXvdgXlVUBpv.txt +0 -766
- package/.code/agents/253e2695-dc36-4022-b436-27655e0fc6c7/status.txt +0 -1
- package/.code/agents/2583/exec-call_M59I4eDjpjlBIWBiSxyS0YlJ.txt +0 -2594
- package/.code/agents/2583/exec-call_usLRGh7OhVHtsRBL4iUwRhjq.txt +0 -2594
- package/.code/agents/292aa3ff-dbab-470f-97c9-e7e8fd65e0db/result.txt +0 -144
- package/.code/agents/2e905864-aa07-4314-bcf9-c5b32277e4ac/result.txt +0 -36
- package/.code/agents/3073/exec-call_Peeagc9DxGYLgE6pNdMZhqIE.txt +0 -766
- package/.code/agents/3073/exec-call_d2YSE3hXF08KRSoUM3qd8Z3x.txt +0 -766
- package/.code/agents/3134/exec-call_IgCAMGx19lWfuo8zfYIt5FFC.txt +0 -416
- package/.code/agents/3134/exec-call_IxvLR2Oo7kba2QTsI1gHVko8.txt +0 -2590
- package/.code/agents/3134/exec-call_jYvc8hksZChSiysbzKjl2ZbB.txt +0 -2590
- package/.code/agents/329/exec-call_4QdP3SfSO7HGPCwVcqZIth6s.txt +0 -2590
- package/.code/agents/335aa031-466d-4fb7-925f-3cd864e264d0/result.txt +0 -191
- package/.code/agents/3364/exec-call_NbhIrsM5HhyDZDmJZG5CuCYL.txt +0 -766
- package/.code/agents/3364/exec-call_cKtJg0NrXiwXEFwlsE3uPZRA.txt +0 -766
- package/.code/agents/36d98414-5cde-4d9d-9a67-a240a18c1f07/result.txt +0 -189
- package/.code/agents/4604e866-b7b8-44f5-992f-2f683b0a523b/status.txt +0 -1
- package/.code/agents/472/exec-call_4AxzEEcWwkKhpqRB3bE8Ha4L.txt +0 -790
- package/.code/agents/472/exec-call_CB3LPYQA8QIZRi8I6kj4J17A.txt +0 -766
- package/.code/agents/472/exec-call_YeoUWvaFoktay2nqVUsa9KKX.txt +0 -790
- package/.code/agents/472/exec-call_jPWgKVquBBXTg0T3Lks5ZfkK.txt +0 -2594
- package/.code/agents/472/exec-call_qBkvunpGBDEHph2jPmTwtcsb.txt +0 -1000
- package/.code/agents/472/exec-call_v0ffRV1p0kTckBmJPzzHAEy0.txt +0 -3489
- package/.code/agents/472/exec-call_xAX5FXqWIlk02d9WubHbHWh8.txt +0 -766
- package/.code/agents/5346/exec-call_9q0muXUuLaucwEqI51Pt7idT.txt +0 -2594
- package/.code/agents/5346/exec-call_B2el3B79rVkq9LhWTI2VYlz7.txt +0 -2456
- package/.code/agents/5346/exec-call_BfX08f02qkZI9uJD5dvCvuoj.txt +0 -2594
- package/.code/agents/543328d0-61d6-4fd1-a723-bb168656e2e2/error.txt +0 -18
- package/.code/agents/5580c02c-1383-4d18-9cbd-cc8a06e3408d/result.txt +0 -48
- package/.code/agents/5f8dc01c-47b3-4163-b0b3-aa31be89fcdc/status.txt +0 -1
- package/.code/agents/60ce1a22-5126-44b2-b977-1d5b56142a7b/status.txt +0 -1
- package/.code/agents/6215d9db-7fa9-4429-aeec-3835c3212291/error.txt +0 -1
- package/.code/agents/6743db55-30e5-4b4e-9366-a8214fc7f714/error.txt +0 -1
- package/.code/agents/6bf9591b-b9c9-422c-b0a5-e968c7d8422a/status.txt +0 -1
- package/.code/agents/7/exec-call_HltHpkDox0Zm1vGEjdksUgpE.txt +0 -1120
- package/.code/agents/7/exec-call_LCATrOPPAgbxW9Q1z0XaVi2E.txt +0 -2646
- package/.code/agents/7/exec-call_W8DeRfNG9hvbgVFvf0clBf6R.txt +0 -2646
- package/.code/agents/7/exec-call_eww3GfdEiJZx61sJEQ9wNmt3.txt +0 -1271
- package/.code/agents/70/exec-call_owUtDMYiVgqDf8vsz1i32PFf.txt +0 -1570
- package/.code/agents/8/exec-call_UtrjAcLbhYLatxR4O97fZgnm.txt +0 -2590
- package/.code/agents/82490bc9-f34e-4b1b-8a8e-bccc2e6254f5/error.txt +0 -3
- package/.code/agents/841/exec-call_7nTNhSBCNjTDUIJv7py6CepO.txt +0 -3299
- package/.code/agents/841/exec-call_TLI0yUdUijuUAvI4o3DXEvHO.txt +0 -3299
- package/.code/agents/9/exec-call_XaABQT1hIlRpnKZ2uyBMWsTC.txt +0 -1882
- package/.code/agents/941/exec-call_GuGHRx7NNXWIDAnxUG2NEWPa.txt +0 -2594
- package/.code/agents/94a0ddf3-a304-4ec3-913e-3cceef509948/error.txt +0 -1
- package/.code/agents/95d9fbab-19a2-48af-83f9-c792566a347f/error.txt +0 -1
- package/.code/agents/b0098cb8-cb32-4ada-9bc4-37c587518896/result.txt +0 -170
- package/.code/agents/b4fe59a4-81df-42e2-a112-0153e504faca/error.txt +0 -1
- package/.code/agents/bf4ce152-f623-49d7-aa52-c18631625c3c/error.txt +0 -3
- package/.code/agents/d7d1db75-d7eb-468e-adea-4ef4d916d187/status.txt +0 -1
- package/.code/agents/e2baa9c8-bac3-49e3-a39d-024333e6a990/status.txt +0 -1
- package/.code/agents/e2c752b7-711d-423a-af57-f53c809deb84/result.txt +0 -160
- package/.code/agents/e350b8c3-8483-408c-b2bb-94515f492a11/error.txt +0 -3
- package/.code/agents/e63f9919-719f-4ad0-bccf-01b1a596e1e9/status.txt +0 -1
- package/.code/agents/e6601719-c31f-4a0e-8c71-d70787d0ab71/status.txt +0 -1
- package/.code/agents/e71695a8-3044-478d-8f12-ed13d02884c7/status.txt +0 -1
- package/.code/agents/f250b7ed-5bd5-4036-aa8c-ce63caee7d61/result.txt +0 -20
- package/.code/agents/f95b7464-3e25-4897-b153-c8dfd63fd605/error.txt +0 -5
- package/.code/agents/fa3c5ddf-cdf7-47a2-930a-b806c6363689/status.txt +0 -1
- package/AGENTS.md +0 -1
- package/NUL +0 -0
- package/package.json.tmp +0 -105
- package/src/__tests__/delta.performance.test.ts +0 -80
- package/src/__tests__/workflows.e2e.test.ts +0 -1702
- package/temp-recon.ts +0 -126
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_09-04-53.json +0 -23
- package/test-exports/ynab_account_e9ddc2a6_minimal_1items_2025-11-19_10-37-42.json +0 -23
- package/test-exports/ynab_account_e9ddc2a6_minimal_4items_2025-11-19_09-02-09.json +0 -44
- package/test-exports/ynab_account_e9ddc2a6_minimal_6items_2025-11-19_10-37-52.json +0 -58
- package/test-exports/ynab_since_2025-10-16_account_53298e13_238items_2025-11-28_13-46-20.json +0 -3662
- package/test-exports/ynab_since_2025-11-01_account_4c18e9f0_minimal_14items_2025-11-16_10-07-10.json +0 -115
|
@@ -152,5 +152,8 @@ export class RateLimitError extends Error {
|
|
|
152
152
|
* Global rate limiter instance
|
|
153
153
|
*/
|
|
154
154
|
export const globalRateLimiter = new RateLimiter({
|
|
155
|
-
enableLogging:
|
|
155
|
+
enableLogging:
|
|
156
|
+
process.env['RATE_LIMIT_LOGGING'] === 'true' ||
|
|
157
|
+
process.env['LOG_LEVEL'] === 'debug' ||
|
|
158
|
+
process.env['VERBOSE_TESTS'] === 'true',
|
|
156
159
|
});
|
package/src/server/resources.ts
CHANGED
|
@@ -10,9 +10,18 @@ import {
|
|
|
10
10
|
ResourceTemplate as MCPResourceTemplate,
|
|
11
11
|
Resource as MCPResource,
|
|
12
12
|
ResourceContents,
|
|
13
|
+
ErrorCode,
|
|
14
|
+
McpError,
|
|
13
15
|
} from '@modelcontextprotocol/sdk/types.js';
|
|
14
16
|
import { CacheManager, CACHE_TTLS } from './cacheManager.js';
|
|
15
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Custom MCP error code for resource not found.
|
|
20
|
+
* Uses JSON-RPC reserved range (-32000 to -32099) for server errors.
|
|
21
|
+
* @see https://www.jsonrpc.org/specification#error_object
|
|
22
|
+
*/
|
|
23
|
+
const RESOURCE_NOT_FOUND_ERROR_CODE = -32002;
|
|
24
|
+
|
|
16
25
|
/**
|
|
17
26
|
* Response formatter interface to avoid direct dependency on concrete implementation
|
|
18
27
|
*/
|
|
@@ -155,7 +164,9 @@ const defaultResourceTemplates: ResourceTemplateDefinition[] = [
|
|
|
155
164
|
mimeType: 'application/json',
|
|
156
165
|
handler: async (uri, params, { ynabAPI, responseFormatter, cacheManager }) => {
|
|
157
166
|
const budget_id = params['budget_id'];
|
|
158
|
-
if (!budget_id)
|
|
167
|
+
if (!budget_id) {
|
|
168
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing budget_id parameter');
|
|
169
|
+
}
|
|
159
170
|
const cacheKey = CacheManager.generateKey('resources', 'budgets', 'get', budget_id);
|
|
160
171
|
return cacheManager.wrap<ResourceContents[]>(cacheKey, {
|
|
161
172
|
ttl: CACHE_TTLS.BUDGETS,
|
|
@@ -184,7 +195,9 @@ const defaultResourceTemplates: ResourceTemplateDefinition[] = [
|
|
|
184
195
|
mimeType: 'application/json',
|
|
185
196
|
handler: async (uri, params, { ynabAPI, responseFormatter, cacheManager }) => {
|
|
186
197
|
const budget_id = params['budget_id'];
|
|
187
|
-
if (!budget_id)
|
|
198
|
+
if (!budget_id) {
|
|
199
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing budget_id parameter');
|
|
200
|
+
}
|
|
188
201
|
const cacheKey = CacheManager.generateKey('resources', 'accounts', 'list', budget_id);
|
|
189
202
|
return cacheManager.wrap<ResourceContents[]>(cacheKey, {
|
|
190
203
|
ttl: CACHE_TTLS.ACCOUNTS,
|
|
@@ -214,8 +227,12 @@ const defaultResourceTemplates: ResourceTemplateDefinition[] = [
|
|
|
214
227
|
handler: async (uri, params, { ynabAPI, responseFormatter, cacheManager }) => {
|
|
215
228
|
const budget_id = params['budget_id'];
|
|
216
229
|
const account_id = params['account_id'];
|
|
217
|
-
if (!budget_id)
|
|
218
|
-
|
|
230
|
+
if (!budget_id) {
|
|
231
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing budget_id parameter');
|
|
232
|
+
}
|
|
233
|
+
if (!account_id) {
|
|
234
|
+
throw new McpError(ErrorCode.InvalidParams, 'Missing account_id parameter');
|
|
235
|
+
}
|
|
219
236
|
const cacheKey = CacheManager.generateKey(
|
|
220
237
|
'resources',
|
|
221
238
|
'accounts',
|
|
@@ -317,24 +334,43 @@ export class ResourceManager {
|
|
|
317
334
|
// 1. Try exact match first
|
|
318
335
|
const handler = this.resourceHandlers[uri];
|
|
319
336
|
if (handler) {
|
|
320
|
-
return {
|
|
337
|
+
return {
|
|
338
|
+
contents: await this.executeResourceHandler(
|
|
339
|
+
() => handler(uri, this.dependencies),
|
|
340
|
+
`resource ${uri}`,
|
|
341
|
+
),
|
|
342
|
+
};
|
|
321
343
|
}
|
|
322
344
|
|
|
323
345
|
// 2. Try template matching
|
|
324
346
|
for (const template of this.resourceTemplates) {
|
|
325
347
|
const params = this.matchTemplate(template.uriTemplate, uri);
|
|
326
348
|
if (params) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
349
|
+
return {
|
|
350
|
+
contents: await this.executeResourceHandler(
|
|
351
|
+
() => template.handler(uri, params, this.dependencies),
|
|
352
|
+
`resource ${uri}`,
|
|
353
|
+
),
|
|
354
|
+
};
|
|
334
355
|
}
|
|
335
356
|
}
|
|
336
357
|
|
|
337
|
-
throw new
|
|
358
|
+
throw new McpError(RESOURCE_NOT_FOUND_ERROR_CODE, `Resource not found: ${uri}`);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private async executeResourceHandler(
|
|
362
|
+
handler: () => Promise<ResourceContents[]>,
|
|
363
|
+
label: string,
|
|
364
|
+
): Promise<ResourceContents[]> {
|
|
365
|
+
try {
|
|
366
|
+
return await handler();
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (error instanceof McpError) {
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
372
|
+
throw new McpError(ErrorCode.InternalError, `Failed to read ${label}: ${message}`);
|
|
373
|
+
}
|
|
338
374
|
}
|
|
339
375
|
|
|
340
376
|
/**
|
|
@@ -54,12 +54,27 @@ export interface ToolMetadataOptions {
|
|
|
54
54
|
annotations?: MCPToolAnnotations;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Progress notification callback for long-running operations.
|
|
59
|
+
* Follows MCP spec: notifications/progress
|
|
60
|
+
*/
|
|
61
|
+
export type ProgressCallback = (params: {
|
|
62
|
+
progress: number;
|
|
63
|
+
total?: number;
|
|
64
|
+
message?: string;
|
|
65
|
+
}) => Promise<void>;
|
|
66
|
+
|
|
57
67
|
export interface ToolExecutionContext {
|
|
58
68
|
accessToken: string;
|
|
59
69
|
name: string;
|
|
60
70
|
operation: string;
|
|
61
71
|
rawArguments: Record<string, unknown>;
|
|
62
72
|
cache?: ToolRegistryCacheHelpers;
|
|
73
|
+
/**
|
|
74
|
+
* Optional progress callback for emitting MCP progress notifications.
|
|
75
|
+
* Available when the client provides a progressToken in the request.
|
|
76
|
+
*/
|
|
77
|
+
sendProgress?: ProgressCallback;
|
|
63
78
|
}
|
|
64
79
|
|
|
65
80
|
export interface ToolExecutionPayload<TInput extends Record<string, unknown>> {
|
|
@@ -97,6 +112,11 @@ export interface ToolExecutionOptions {
|
|
|
97
112
|
accessToken: string;
|
|
98
113
|
arguments?: Record<string, unknown>;
|
|
99
114
|
minifyOverride?: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* Optional progress callback for emitting MCP progress notifications.
|
|
117
|
+
* Should be provided when the request includes a progressToken.
|
|
118
|
+
*/
|
|
119
|
+
sendProgress?: ProgressCallback;
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
export interface ToolRegistryDependencies {
|
|
@@ -176,6 +196,10 @@ export class ToolRegistry {
|
|
|
176
196
|
});
|
|
177
197
|
}
|
|
178
198
|
|
|
199
|
+
hasTool(name: string): boolean {
|
|
200
|
+
return this.tools.has(name);
|
|
201
|
+
}
|
|
202
|
+
|
|
179
203
|
getToolDefinitions(): ToolDefinition[] {
|
|
180
204
|
return Array.from(this.tools.values()).map((tool) => {
|
|
181
205
|
const definition: ToolDefinition = {
|
|
@@ -269,6 +293,9 @@ export class ToolRegistry {
|
|
|
269
293
|
if (this.deps.cacheHelpers) {
|
|
270
294
|
context.cache = this.deps.cacheHelpers;
|
|
271
295
|
}
|
|
296
|
+
if (options.sendProgress) {
|
|
297
|
+
context.sendProgress = options.sendProgress;
|
|
298
|
+
}
|
|
272
299
|
const handlerResult = await tool.handler({
|
|
273
300
|
input: validated,
|
|
274
301
|
context,
|
|
@@ -353,6 +380,13 @@ export class ToolRegistry {
|
|
|
353
380
|
return undefined;
|
|
354
381
|
}
|
|
355
382
|
|
|
383
|
+
/**
|
|
384
|
+
* Regex pattern for MCP-compliant tool names.
|
|
385
|
+
* Tool names SHOULD be 1-128 chars, case-sensitive, only [a-zA-Z0-9_.-]
|
|
386
|
+
* @see https://spec.modelcontextprotocol.io/specification/2024-11-05/server/tools/
|
|
387
|
+
*/
|
|
388
|
+
private static readonly MCP_TOOL_NAME_REGEX = /^[a-zA-Z0-9_.-]{1,128}$/;
|
|
389
|
+
|
|
356
390
|
private assertValidDefinition<
|
|
357
391
|
TInput extends Record<string, unknown>,
|
|
358
392
|
TOutput extends Record<string, unknown>,
|
|
@@ -365,6 +399,14 @@ export class ToolRegistry {
|
|
|
365
399
|
throw new Error('Tool definition requires a non-empty name');
|
|
366
400
|
}
|
|
367
401
|
|
|
402
|
+
// Validate tool name follows MCP specification guidelines
|
|
403
|
+
if (!ToolRegistry.MCP_TOOL_NAME_REGEX.test(definition.name)) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
`Tool name '${definition.name}' violates MCP guidelines: ` +
|
|
406
|
+
`must be 1-128 chars using only [a-zA-Z0-9_.-]`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
368
410
|
if (!definition.description || typeof definition.description !== 'string') {
|
|
369
411
|
throw new Error(`Tool '${definition.name}' requires a description`);
|
|
370
412
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
handleUpdateTransactions,
|
|
10
10
|
CreateTransactionsSchema,
|
|
11
11
|
} from '../transactionTools.js';
|
|
12
|
+
import { waitFor } from '../../__tests__/testUtils.js';
|
|
12
13
|
|
|
13
14
|
const isSkip = ['true', '1', 'yes', 'y', 'on'].includes(
|
|
14
15
|
(process.env['SKIP_E2E_TESTS'] || '').toLowerCase().trim(),
|
|
@@ -325,9 +326,22 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
325
326
|
],
|
|
326
327
|
});
|
|
327
328
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
329
|
+
let transactions: any[] | undefined;
|
|
330
|
+
await waitFor(
|
|
331
|
+
async () => {
|
|
332
|
+
const afterList = await fetchBudgetTransactions();
|
|
333
|
+
transactions =
|
|
334
|
+
afterList.transactions ||
|
|
335
|
+
afterList.preview_transactions ||
|
|
336
|
+
afterList.transaction_preview;
|
|
337
|
+
return (
|
|
338
|
+
(transactions as any[])?.some((transaction) => transaction.memo === memo) ?? false
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
10000,
|
|
342
|
+
500,
|
|
343
|
+
);
|
|
344
|
+
|
|
331
345
|
expect(transactions).toBeDefined();
|
|
332
346
|
expect((transactions as any[]).some((transaction) => transaction.memo === memo)).toBe(true);
|
|
333
347
|
},
|
|
@@ -576,6 +590,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
576
590
|
expect(updateResponse.results[1].correlation_key).toBe(transactionIds[1]);
|
|
577
591
|
|
|
578
592
|
// Verify changes persisted
|
|
593
|
+
await waitFor(
|
|
594
|
+
async () => {
|
|
595
|
+
const getResult1 = await handleGetTransaction(ynabAPI, {
|
|
596
|
+
budget_id: testBudgetId,
|
|
597
|
+
transaction_id: transactionIds[0],
|
|
598
|
+
});
|
|
599
|
+
const transaction1 = parseToolResult(getResult1).transaction;
|
|
600
|
+
return transaction1.amount === -7.5 && transaction1.memo === 'Updated memo 1';
|
|
601
|
+
},
|
|
602
|
+
10000,
|
|
603
|
+
500,
|
|
604
|
+
);
|
|
605
|
+
|
|
579
606
|
const getResult1 = await handleGetTransaction(ynabAPI, {
|
|
580
607
|
budget_id: testBudgetId,
|
|
581
608
|
transaction_id: transactionIds[0],
|
|
@@ -584,6 +611,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
584
611
|
expect(transaction1.amount).toBe(-7.5);
|
|
585
612
|
expect(transaction1.memo).toBe('Updated memo 1');
|
|
586
613
|
|
|
614
|
+
await waitFor(
|
|
615
|
+
async () => {
|
|
616
|
+
const getResult2 = await handleGetTransaction(ynabAPI, {
|
|
617
|
+
budget_id: testBudgetId,
|
|
618
|
+
transaction_id: transactionIds[1],
|
|
619
|
+
});
|
|
620
|
+
const transaction2 = parseToolResult(getResult2).transaction;
|
|
621
|
+
return transaction2.memo === 'Updated memo 2' && transaction2.cleared === 'cleared';
|
|
622
|
+
},
|
|
623
|
+
10000,
|
|
624
|
+
500,
|
|
625
|
+
);
|
|
626
|
+
|
|
587
627
|
const getResult2 = await handleGetTransaction(ynabAPI, {
|
|
588
628
|
budget_id: testBudgetId,
|
|
589
629
|
transaction_id: transactionIds[1],
|
|
@@ -635,6 +675,19 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
635
675
|
expect(updateResponse.summary.updated).toBe(1);
|
|
636
676
|
|
|
637
677
|
// Verify change
|
|
678
|
+
await waitFor(
|
|
679
|
+
async () => {
|
|
680
|
+
const getResult = await handleGetTransaction(ynabAPI, {
|
|
681
|
+
budget_id: testBudgetId,
|
|
682
|
+
transaction_id: transactionId,
|
|
683
|
+
});
|
|
684
|
+
const transaction = parseToolResult(getResult).transaction;
|
|
685
|
+
return transaction.memo === 'Updated without metadata';
|
|
686
|
+
},
|
|
687
|
+
10000,
|
|
688
|
+
500,
|
|
689
|
+
);
|
|
690
|
+
|
|
638
691
|
const getResult = await handleGetTransaction(ynabAPI, {
|
|
639
692
|
budget_id: testBudgetId,
|
|
640
693
|
transaction_id: transactionId,
|
|
@@ -749,6 +802,13 @@ describeIntegration('Transaction Tools Integration', () => {
|
|
|
749
802
|
});
|
|
750
803
|
|
|
751
804
|
const updateResponse = parseToolResult(updateResult);
|
|
805
|
+
|
|
806
|
+
if (updateResponse.error) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`Tool execution failed unexpectedly: ${JSON.stringify(updateResponse.error)}`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
752
812
|
expect(updateResponse.summary.total_requested).toBe(2);
|
|
753
813
|
expect(updateResponse.summary.updated).toBe(1);
|
|
754
814
|
expect(updateResponse.summary.failed).toBeGreaterThan(0);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
2
|
import * as ynab from 'ynab';
|
|
3
|
-
import { handleGetUser
|
|
3
|
+
import { handleGetUser } from '../utilityTools.js';
|
|
4
4
|
import { skipOnRateLimit } from '../../__tests__/testUtils.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -41,88 +41,4 @@ describeIntegration('Utility Tools Integration Tests', () => {
|
|
|
41
41
|
},
|
|
42
42
|
);
|
|
43
43
|
});
|
|
44
|
-
|
|
45
|
-
describe('handleConvertAmount', () => {
|
|
46
|
-
it(
|
|
47
|
-
'should convert various dollar amounts to milliunits',
|
|
48
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
49
|
-
async () => {
|
|
50
|
-
const testCases = [
|
|
51
|
-
{ dollars: 1.0, expectedMilliunits: 1000 },
|
|
52
|
-
{ dollars: 0.01, expectedMilliunits: 10 },
|
|
53
|
-
{ dollars: 10.5, expectedMilliunits: 10500 },
|
|
54
|
-
{ dollars: 999.99, expectedMilliunits: 999990 },
|
|
55
|
-
{ dollars: 0, expectedMilliunits: 0 },
|
|
56
|
-
{ dollars: -5.25, expectedMilliunits: -5250 },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
|
-
for (const testCase of testCases) {
|
|
60
|
-
const result = await handleConvertAmount({
|
|
61
|
-
amount: testCase.dollars,
|
|
62
|
-
to_milliunits: true,
|
|
63
|
-
});
|
|
64
|
-
const response = JSON.parse(result.content[0].text);
|
|
65
|
-
|
|
66
|
-
expect(response.conversion.converted_amount).toBe(testCase.expectedMilliunits);
|
|
67
|
-
expect(response.conversion.to_milliunits).toBe(true);
|
|
68
|
-
expect(response.conversion.description).toContain(`$${testCase.dollars.toFixed(2)}`);
|
|
69
|
-
expect(response.conversion.description).toContain(
|
|
70
|
-
`${testCase.expectedMilliunits} milliunits`,
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
it(
|
|
77
|
-
'should convert various milliunit amounts to dollars',
|
|
78
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
79
|
-
async () => {
|
|
80
|
-
const testCases = [
|
|
81
|
-
{ milliunits: 1000, expectedDollars: 1.0 },
|
|
82
|
-
{ milliunits: 10, expectedDollars: 0.01 },
|
|
83
|
-
{ milliunits: 10500, expectedDollars: 10.5 },
|
|
84
|
-
{ milliunits: 999990, expectedDollars: 999.99 },
|
|
85
|
-
{ milliunits: 0, expectedDollars: 0 },
|
|
86
|
-
{ milliunits: -5250, expectedDollars: -5.25 },
|
|
87
|
-
];
|
|
88
|
-
|
|
89
|
-
for (const testCase of testCases) {
|
|
90
|
-
const result = await handleConvertAmount({
|
|
91
|
-
amount: testCase.milliunits,
|
|
92
|
-
to_milliunits: false,
|
|
93
|
-
});
|
|
94
|
-
const response = JSON.parse(result.content[0].text);
|
|
95
|
-
|
|
96
|
-
expect(response.conversion.converted_amount).toBe(testCase.expectedDollars);
|
|
97
|
-
expect(response.conversion.to_milliunits).toBe(false);
|
|
98
|
-
expect(response.conversion.description).toContain(`${testCase.milliunits} milliunits`);
|
|
99
|
-
expect(response.conversion.description).toContain(
|
|
100
|
-
`$${testCase.expectedDollars.toFixed(2)}`,
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
it(
|
|
107
|
-
'should handle precision edge cases',
|
|
108
|
-
{ meta: { tier: 'domain', domain: 'utility' } },
|
|
109
|
-
async () => {
|
|
110
|
-
// Test floating-point precision issues
|
|
111
|
-
const precisionTests = [
|
|
112
|
-
{ amount: 0.1 + 0.2, to_milliunits: true }, // Should handle 0.30000000000000004
|
|
113
|
-
{ amount: 1.005, to_milliunits: true }, // Should round correctly
|
|
114
|
-
{ amount: 999.999, to_milliunits: true }, // Should handle near-integer values
|
|
115
|
-
];
|
|
116
|
-
|
|
117
|
-
for (const test of precisionTests) {
|
|
118
|
-
const result = await handleConvertAmount(test);
|
|
119
|
-
const response = JSON.parse(result.content[0].text);
|
|
120
|
-
|
|
121
|
-
expect(response.conversion).toHaveProperty('converted_amount');
|
|
122
|
-
expect(typeof response.conversion.converted_amount).toBe('number');
|
|
123
|
-
expect(Number.isInteger(response.conversion.converted_amount)).toBe(true);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
44
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import * as ynab from 'ynab';
|
|
3
|
-
import { handleGetUser
|
|
3
|
+
import { handleGetUser } from '../utilityTools.js';
|
|
4
4
|
|
|
5
5
|
// Mock the YNAB API
|
|
6
6
|
const mockYnabAPI = {
|
|
@@ -80,126 +80,4 @@ describe('Utility Tools', () => {
|
|
|
80
80
|
expect(result.content[0].text).toContain('Failed to get user information');
|
|
81
81
|
});
|
|
82
82
|
});
|
|
83
|
-
|
|
84
|
-
describe('handleConvertAmount', () => {
|
|
85
|
-
it('should convert dollars to milliunits correctly', async () => {
|
|
86
|
-
const params = { amount: 10.5, to_milliunits: true };
|
|
87
|
-
|
|
88
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
89
|
-
const response = JSON.parse(result.content[0].text);
|
|
90
|
-
|
|
91
|
-
expect(response.conversion.original_amount).toBe(10.5);
|
|
92
|
-
expect(response.conversion.converted_amount).toBe(10500);
|
|
93
|
-
expect(response.conversion.to_milliunits).toBe(true);
|
|
94
|
-
expect(response.conversion.description).toBe('$10.50 = 10500 milliunits');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('should convert milliunits to dollars correctly', async () => {
|
|
98
|
-
const params = { amount: 10500, to_milliunits: false };
|
|
99
|
-
|
|
100
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
101
|
-
const response = JSON.parse(result.content[0].text);
|
|
102
|
-
|
|
103
|
-
expect(response.conversion.original_amount).toBe(10500);
|
|
104
|
-
expect(response.conversion.converted_amount).toBe(10.5);
|
|
105
|
-
expect(response.conversion.to_milliunits).toBe(false);
|
|
106
|
-
expect(response.conversion.description).toBe('10500 milliunits = $10.50');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('should handle zero amounts', async () => {
|
|
110
|
-
const params = { amount: 0, to_milliunits: true };
|
|
111
|
-
|
|
112
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
113
|
-
const response = JSON.parse(result.content[0].text);
|
|
114
|
-
|
|
115
|
-
expect(response.conversion.original_amount).toBe(0);
|
|
116
|
-
expect(response.conversion.converted_amount).toBe(0);
|
|
117
|
-
expect(response.conversion.description).toBe('$0.00 = 0 milliunits');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('should handle negative amounts', async () => {
|
|
121
|
-
const params = { amount: -5.25, to_milliunits: true };
|
|
122
|
-
|
|
123
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
124
|
-
const response = JSON.parse(result.content[0].text);
|
|
125
|
-
|
|
126
|
-
expect(response.conversion.original_amount).toBe(-5.25);
|
|
127
|
-
expect(response.conversion.converted_amount).toBe(-5250);
|
|
128
|
-
expect(response.conversion.description).toBe('$-5.25 = -5250 milliunits');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should handle floating-point precision correctly', async () => {
|
|
132
|
-
const params = { amount: 0.01, to_milliunits: true };
|
|
133
|
-
|
|
134
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
135
|
-
const response = JSON.parse(result.content[0].text);
|
|
136
|
-
|
|
137
|
-
expect(response.conversion.converted_amount).toBe(10);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('should handle large amounts', async () => {
|
|
141
|
-
const params = { amount: 999999.99, to_milliunits: true };
|
|
142
|
-
|
|
143
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
144
|
-
const response = JSON.parse(result.content[0].text);
|
|
145
|
-
|
|
146
|
-
expect(response.conversion.converted_amount).toBe(999999990);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it('should round to nearest milliunit when converting from dollars', async () => {
|
|
150
|
-
const params = { amount: 10.5555, to_milliunits: true };
|
|
151
|
-
|
|
152
|
-
const result = await handleConvertAmount(mockYnabAPI, params);
|
|
153
|
-
const response = JSON.parse(result.content[0].text);
|
|
154
|
-
|
|
155
|
-
expect(response.conversion.converted_amount).toBe(10556); // Rounded from 10555.5
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
describe('ConvertAmountSchema validation', () => {
|
|
160
|
-
it('should validate correct parameters', () => {
|
|
161
|
-
const validParams = { amount: 10.5, to_milliunits: true };
|
|
162
|
-
const result = ConvertAmountSchema.safeParse(validParams);
|
|
163
|
-
|
|
164
|
-
expect(result.success).toBe(true);
|
|
165
|
-
if (result.success) {
|
|
166
|
-
expect(result.data).toEqual(validParams);
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should reject non-finite numbers', () => {
|
|
171
|
-
const invalidParams = { amount: Infinity, to_milliunits: true };
|
|
172
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
173
|
-
|
|
174
|
-
expect(result.success).toBe(false);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should reject NaN values', () => {
|
|
178
|
-
const invalidParams = { amount: NaN, to_milliunits: true };
|
|
179
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
180
|
-
|
|
181
|
-
expect(result.success).toBe(false);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should reject missing amount parameter', () => {
|
|
185
|
-
const invalidParams = { to_milliunits: true };
|
|
186
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
187
|
-
|
|
188
|
-
expect(result.success).toBe(false);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should reject missing to_milliunits parameter', () => {
|
|
192
|
-
const invalidParams = { amount: 10.5 };
|
|
193
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
194
|
-
|
|
195
|
-
expect(result.success).toBe(false);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
it('should reject non-boolean to_milliunits parameter', () => {
|
|
199
|
-
const invalidParams = { amount: 10.5, to_milliunits: 'true' };
|
|
200
|
-
const result = ConvertAmountSchema.safeParse(invalidParams);
|
|
201
|
-
|
|
202
|
-
expect(result.success).toBe(false);
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
83
|
});
|
package/src/tools/adapters.ts
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
ToolExecutionPayload,
|
|
11
|
+
DefaultArgumentResolver,
|
|
12
|
+
ProgressCallback,
|
|
13
|
+
} from '../server/toolRegistry.js';
|
|
10
14
|
import { BudgetResolver } from '../server/budgetResolver.js';
|
|
11
15
|
import { DefaultArgumentResolutionError } from '../server/toolRegistry.js';
|
|
12
16
|
import type {
|
|
@@ -16,6 +20,7 @@ import type {
|
|
|
16
20
|
WriteHandler,
|
|
17
21
|
NoInputHandler,
|
|
18
22
|
} from '../types/toolRegistration.js';
|
|
23
|
+
import type { DeltaFetcher } from './deltaFetcher.js';
|
|
19
24
|
|
|
20
25
|
/**
|
|
21
26
|
* Creates adapter functions bound to the provided context. These helpers reduce
|
|
@@ -41,6 +46,22 @@ export function createAdapters(context: ToolContext) {
|
|
|
41
46
|
async ({ input }: ToolExecutionPayload<TInput>): Promise<CallToolResult> =>
|
|
42
47
|
handler(ynabAPI, deltaFetcher, input),
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Adapter for delta operations that may emit progress notifications.
|
|
51
|
+
* Passes the optional sendProgress callback from the execution context.
|
|
52
|
+
*/
|
|
53
|
+
adaptWithDeltaAndProgress:
|
|
54
|
+
<TInput extends Record<string, unknown>>(
|
|
55
|
+
handler: (
|
|
56
|
+
api: typeof ynabAPI,
|
|
57
|
+
deltaFetcher: DeltaFetcher,
|
|
58
|
+
params: TInput,
|
|
59
|
+
sendProgress?: ProgressCallback,
|
|
60
|
+
) => Promise<CallToolResult>,
|
|
61
|
+
) =>
|
|
62
|
+
async ({ input, context }: ToolExecutionPayload<TInput>): Promise<CallToolResult> =>
|
|
63
|
+
handler(ynabAPI, deltaFetcher, input, context.sendProgress),
|
|
64
|
+
|
|
44
65
|
adaptWrite:
|
|
45
66
|
<TInput extends Record<string, unknown>>(handler: WriteHandler<TInput>) =>
|
|
46
67
|
async ({ input }: ToolExecutionPayload<TInput>): Promise<CallToolResult> =>
|