From 88ed206acdd13f6992cea3cdbd2862d75bdda654 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:47:01 +0200 Subject: [PATCH] Add tool to update account balance to MCP server --- .../src/app/endpoints/mcp/mcp.schemas.spec.ts | 23 +++++++++++----- apps/api/src/app/endpoints/mcp/mcp.schemas.ts | 26 +++++++++---------- .../src/app/endpoints/mcp/mcp.service.spec.ts | 5 +++- apps/api/src/app/endpoints/mcp/mcp.service.ts | 4 ++- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts index 9969aa7fab..facfe3fd75 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts @@ -7,10 +7,8 @@ import { SEARCH_QUERY_MINIMUM_LENGTH, SYMBOL_MAXIMUM_LENGTH } from '@ghostfolio/common/config'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DataSource } from '@prisma/client'; -import { addDays, format } from 'date-fns'; import { GET_ACCOUNTS_PARAMETERS, @@ -317,6 +315,14 @@ describe('SEARCH_ASSET_PROFILES_PARAMETERS', () => { }); describe('UPDATE_ACCOUNT_BALANCE_PARAMETERS', () => { + beforeAll(() => { + jest.useFakeTimers().setSystemTime(new Date('2026-09-03T23:30:00.000Z')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + function parse(parameters: Record) { return UPDATE_ACCOUNT_BALANCE_PARAMETERS.safeParse({ accountId: 'account-id', @@ -327,7 +333,7 @@ describe('UPDATE_ACCOUNT_BALANCE_PARAMETERS', () => { } it('Accepts a cash balance of today', () => { - expect(parse({ date: format(new Date(), DATE_FORMAT) })).toBe(true); + expect(parse({ date: '2026-09-03' })).toBe(true); }); it('Accepts a negative cash balance', () => { @@ -339,12 +345,15 @@ describe('UPDATE_ACCOUNT_BALANCE_PARAMETERS', () => { }); it('Refuses a date at or before the epoch', () => { - expect(parse({ date: '0000-01-01' })).toBe(false); + expect(parse({ date: '1969-12-31' })).toBe(false); + expect(parse({ date: '1970-01-01' })).toBe(false); + }); + + it('Refuses a date and time', () => { + expect(parse({ date: '2024-01-01T00:30:00+02:00' })).toBe(false); }); it('Refuses a date in the future', () => { - expect(parse({ date: format(addDays(new Date(), 2), DATE_FORMAT) })).toBe( - false - ); + expect(parse({ date: '2026-09-05' })).toBe(false); }); }); diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts index e4028ddda0..1802305ae6 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts @@ -11,14 +11,12 @@ import { SYMBOL_MAXIMUM_LENGTH } from '@ghostfolio/common/config'; import { - isAccountBalanceInFuture, + isValidAccountBalanceDate, isValidCurrencyCode, - isValidDateAfter1970, - resetHours + isValidDateAfter1970 } from '@ghostfolio/common/helper'; import { AssetClass, DataSource, Type as ActivityType } from '@prisma/client'; -import { parseISO } from 'date-fns'; import { z } from 'zod'; const DATA_SOURCE_PARAMETER_ERROR = `Invalid option: expected one of ${Object.values(DataSource).join('|')} or a data source as given by a tool`; @@ -188,14 +186,16 @@ export const UPDATE_ACCOUNT_BALANCE_PARAMETERS = z.object({ balance: z .number() .describe('The cash balance of the account in the currency of the account'), - date: z - .string() - .refine(isValidDateAfter1970) - .refine( - (date) => { - return !isAccountBalanceInFuture({ date: resetHours(parseISO(date)) }); - }, - { error: 'The date must not be in the future' } - ) + date: z.iso + .date({ + abort: true, + error: 'The date must be an ISO 8601 date, for example 2024-01-01' + }) + .refine(isValidDateAfter1970, { + error: 'The date must be after 1970-01-01' + }) + .refine(isValidAccountBalanceDate, { + error: 'The date must not be in the future' + }) .describe('The date of the cash balance, as an ISO 8601 date') }); diff --git a/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts b/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts index 4f622d20e6..841c29ea49 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts @@ -700,7 +700,10 @@ describe('McpService', () => { expect(await mcpService.updateAccountBalance(parameters)).toEqual({ content: [ - { text: 'Updated the cash balance of the account', type: 'text' } + { + text: 'Updated the cash balance of the account at 2024-01-01', + type: 'text' + } ] }); diff --git a/apps/api/src/app/endpoints/mcp/mcp.service.ts b/apps/api/src/app/endpoints/mcp/mcp.service.ts index 3f75b62372..c3aa289dd5 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.service.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.service.ts @@ -280,7 +280,9 @@ export class McpService { userId: account.userId }); - return this.getTextResult('Updated the cash balance of the account'); + return this.getTextResult( + `Updated the cash balance of the account at ${date}` + ); } private getHoldingFilterParameters({