From 452674eceecf5226cfb82dc70bb1d027edb85a74 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:00:34 +0200 Subject: [PATCH] Task/improve symbol validation of MCP import tool (#8000) Improve symbol validation --- .../src/app/endpoints/mcp/mcp.schemas.spec.ts | 35 ++++++++++++++++++- apps/api/src/app/endpoints/mcp/mcp.schemas.ts | 16 +++++---- 2 files changed, 43 insertions(+), 8 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 2039e7cd91..a5109d225f 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.spec.ts @@ -2,7 +2,8 @@ import { DEFAULT_DATE_RANGE, MCP_MAX_ACTIVITIES, SEARCH_QUERY_MAXIMUM_LENGTH, - SEARCH_QUERY_MINIMUM_LENGTH + SEARCH_QUERY_MINIMUM_LENGTH, + SYMBOL_MAXIMUM_LENGTH } from '@ghostfolio/common/config'; import { DataSource } from '@prisma/client'; @@ -107,6 +108,38 @@ describe('IMPORT_ACTIVITIES_PARAMETERS', () => { expect(parse([createActivity({ symbol: '' })])).toBe(false); }); + it('Refuses a symbol that contains only spaces', () => { + expect(parse([createActivity({ symbol: ' ' })])).toBe(false); + }); + + it(`Accepts a symbol of ${SYMBOL_MAXIMUM_LENGTH} characters`, () => { + expect( + parse([createActivity({ symbol: 'A'.repeat(SYMBOL_MAXIMUM_LENGTH) })]) + ).toBe(true); + }); + + it(`Accepts a symbol of ${SYMBOL_MAXIMUM_LENGTH} characters with spaces at the start and the end`, () => { + expect( + parse([ + createActivity({ symbol: ` ${'A'.repeat(SYMBOL_MAXIMUM_LENGTH)} ` }) + ]) + ).toBe(true); + }); + + it(`Refuses a symbol longer than ${SYMBOL_MAXIMUM_LENGTH} characters`, () => { + expect( + parse([createActivity({ symbol: 'A'.repeat(SYMBOL_MAXIMUM_LENGTH + 1) })]) + ).toBe(false); + }); + + it('Removes spaces at the start and the end of a symbol', () => { + expect( + IMPORT_ACTIVITIES_PARAMETERS.parse({ + activities: [createActivity({ symbol: ' AAPL ' })] + }).activities[0].symbol + ).toBe('AAPL'); + }); + it('Refuses an empty identifier of an account', () => { expect(parse([createActivity({ accountId: '' })])).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 4bcc385b35..dc075377d0 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.schemas.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.schemas.ts @@ -16,16 +16,18 @@ import { import { AssetClass, DataSource, Type as ActivityType } from '@prisma/client'; import { z } from 'zod'; +const SYMBOL_PARAMETER = z + .string() + .trim() + .min(1) + .max(SYMBOL_MAXIMUM_LENGTH) + .describe('The symbol of the asset profile'); + const HOLDING_PARAMETER = z.object({ dataSource: z .enum(DataSource) .describe('The data source of the asset profile'), - symbol: z - .string() - .trim() - .min(1) - .max(SYMBOL_MAXIMUM_LENGTH) - .describe('The symbol of the asset profile') + symbol: SYMBOL_PARAMETER }); export const GET_ACCOUNTS_PARAMETERS = z.object({ @@ -141,7 +143,7 @@ export const IMPORT_ACTIVITIES_PARAMETERS = z.object({ ), fee: z.number().min(0).describe('The fee of the activity'), quantity: z.number().min(0).describe('The quantity of the activity'), - symbol: z.string().min(1).describe('The symbol of the asset profile'), + symbol: SYMBOL_PARAMETER, type: z.enum(ActivityType).describe('The type of the activity'), unitPrice: z.number().min(0).describe('The unit price of the activity') })