From b3ba1b5aab698cfdc63be383bf65944fbaf26398 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 1 Oct 2026 21:19:10 +0200 Subject: [PATCH] Fix filtering by holding --- .../src/app/endpoints/mcp/mcp.schemas.spec.ts | 16 +++++++++++ apps/api/src/app/endpoints/mcp/mcp.schemas.ts | 10 +++++-- .../src/app/endpoints/mcp/mcp.service.spec.ts | 27 ++++++++++++++++++ apps/api/src/app/endpoints/mcp/mcp.service.ts | 28 +++++++++++-------- 4 files changed, 67 insertions(+), 14 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 c98f428612..6fb9086d49 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts @@ -22,6 +22,14 @@ describe('GET_ACCOUNTS_PARAMETERS', () => { }).success ).toBe(false); }); + + it('Refuses a symbol of the holding that contains only spaces', () => { + expect( + GET_ACCOUNTS_PARAMETERS.safeParse({ + holding: { dataSource: DataSource.YAHOO, symbol: ' ' } + }).success + ).toBe(false); + }); }); describe('GET_ACTIVITIES_PARAMETERS', () => { @@ -32,6 +40,14 @@ describe('GET_ACTIVITIES_PARAMETERS', () => { }).success ).toBe(false); }); + + it('Refuses a symbol of the holding that contains only spaces', () => { + expect( + GET_ACTIVITIES_PARAMETERS.safeParse({ + holding: { dataSource: DataSource.YAHOO, symbol: ' ' } + }).success + ).toBe(false); + }); }); describe('IMPORT_ACTIVITIES_PARAMETERS', () => { diff --git a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts index 03528b771b..52c95471f2 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts @@ -4,7 +4,8 @@ import { MCP_MAX_ACCOUNTS, MCP_MAX_ACTIVITIES, SEARCH_QUERY_MAXIMUM_LENGTH, - SEARCH_QUERY_MINIMUM_LENGTH + SEARCH_QUERY_MINIMUM_LENGTH, + SYMBOL_MAXIMUM_LENGTH } from '@ghostfolio/common/config'; import { isValidCurrencyCode, @@ -18,7 +19,12 @@ const HOLDING_PARAMETER = z.object({ dataSource: z .enum(DataSource) .describe('The data source of the asset profile'), - symbol: z.string().min(1).describe('The symbol of the asset profile') + symbol: z + .string() + .trim() + .min(1) + .max(SYMBOL_MAXIMUM_LENGTH) + .describe('The symbol of the asset profile') }); export const GET_ACCOUNTS_PARAMETERS = z.object({ 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 d6263ff1f1..6167d0421a 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.service.spec.ts @@ -127,6 +127,20 @@ describe('McpService', () => { ); }); + it('Keeps the data source if the subscription is not enabled', async () => { + configuration.DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER = [DataSource.YAHOO]; + configuration.ENABLE_FEATURE_SUBSCRIPTION = false; + + await mcpService.getAccounts({ + userId, + holding: { dataSource: DataSource.GHOSTFOLIO, symbol: 'AAPL' } + }); + + expect(apiService.buildFiltersFromQueryParams).toHaveBeenCalledWith( + expect.objectContaining({ filterByDataSource: DataSource.GHOSTFOLIO }) + ); + }); + it('Gives the table of the accounts of the filters', async () => { expect(await mcpService.getAccounts({ userId })).toEqual({ content: [{ text: '## Accounts', type: 'text' }] @@ -177,6 +191,19 @@ describe('McpService', () => { ); }); + it('Keeps the data source if the subscription is not enabled', async () => { + configuration.DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER = [DataSource.YAHOO]; + configuration.ENABLE_FEATURE_SUBSCRIPTION = false; + + await getActivities({ + holding: { dataSource: DataSource.GHOSTFOLIO, symbol: 'AAPL' } + }); + + expect(apiService.buildFiltersFromQueryParams).toHaveBeenCalledWith( + expect.objectContaining({ filterByDataSource: DataSource.GHOSTFOLIO }) + ); + }); + it('Changes the range into the start date and the end date', async () => { await getActivities({ range: '2024' }); diff --git a/apps/api/src/app/endpoints/mcp/mcp.service.ts b/apps/api/src/app/endpoints/mcp/mcp.service.ts index 2a43e39209..cd0edb548d 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.service.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.service.ts @@ -41,13 +41,9 @@ export class McpService { userId }: z.infer & { userId: string }) { const filters = this.apiService.buildFiltersFromQueryParams({ + ...this.getHoldingFilterParameters({ holding }), filterByAccounts: accountIds, - filterByAssetClasses: assetClasses, - filterByDataSource: getUnmaskedGhostfolioDataSource({ - dataSource: holding?.dataSource, - ghostfolioDataSources: this.getGhostfolioDataSources() - }), - filterBySymbol: holding?.symbol + filterByAssetClasses: assetClasses }); const table = await this.portfolioTableService.getAccountsTable({ @@ -81,12 +77,8 @@ export class McpService { } const filters = this.apiService.buildFiltersFromQueryParams({ - filterByAssetClasses: assetClasses, - filterByDataSource: getUnmaskedGhostfolioDataSource({ - dataSource: holding?.dataSource, - ghostfolioDataSources: this.getGhostfolioDataSources() - }), - filterBySymbol: holding?.symbol + ...this.getHoldingFilterParameters({ holding }), + filterByAssetClasses: assetClasses }); const table = await this.portfolioTableService.getActivitiesTable({ @@ -214,6 +206,18 @@ export class McpService { : []; } + private getHoldingFilterParameters({ + holding + }: Pick, 'holding'>) { + return { + filterByDataSource: getUnmaskedGhostfolioDataSource({ + dataSource: holding?.dataSource, + ghostfolioDataSources: this.getGhostfolioDataSources() + }), + filterBySymbol: holding?.symbol + }; + } + private getTextResult(text: string) { return { content: [{ text, type: 'text' as const }] }; }