@ -22,18 +22,23 @@ import { z } from 'zod';
const GET_ACTIVITIES_PARAMETERS = z . object ( {
const GET_ACTIVITIES_PARAMETERS = z . object ( {
activityTypes : z
activityTypes : z
. array ( z . enum ( ActivityType ) )
. array ( z . enum ( ActivityType ) )
. min ( 1 )
. optional ( )
. optional ( )
. describe ( 'The types of the activities to get' ) ,
. describe ( 'The types of the activities to get' ) ,
assetClasses : z
assetClasses : z
. array ( z . enum ( AssetClass ) )
. array ( z . enum ( AssetClass ) )
. min ( 1 )
. optional ( )
. optional ( )
. describe ( 'The asset classes of the activities to get' ) ,
. describe ( 'The asset classes of the activities to get' ) ,
holding : z
. object ( {
dataSource : z
dataSource : z
. enum ( DataSource )
. enum ( DataSource )
. describe ( 'The data source of the asset profile' ) ,
symbol : z . string ( ) . describe ( 'The symbol of the asset profile' )
} )
. optional ( )
. optional ( )
. describe (
. describe ( 'The asset profile of the activities to get' ) ,
'The data source of the asset profile, which only takes effect together with the symbol'
) ,
range : z
range : z
. string ( )
. string ( )
. regex ( DATE_RANGE_PATTERN )
. regex ( DATE_RANGE_PATTERN )
@ -49,12 +54,6 @@ const GET_ACTIVITIES_PARAMETERS = z.object({
. min ( 0 )
. min ( 0 )
. optional ( )
. optional ( )
. describe ( 'The number of activities to skip' ) ,
. describe ( 'The number of activities to skip' ) ,
symbol : z
. string ( )
. optional ( )
. describe (
'The symbol of the asset profile, which only takes effect together with the data source'
) ,
take : z
take : z
. number ( )
. number ( )
. int ( )
. int ( )
@ -79,9 +78,11 @@ export class GhostfolioMcpController {
readOnlyHint : true ,
readOnlyHint : true ,
title : 'Get activities'
title : 'Get activities'
} ,
} ,
description : ` Gives the activities of the portfolio, the most recent first, with these columns: ${ AiService . getActivitiesTableColumnNames ( ) . join (
description : ` Gives the activities of the portfolio, the most recent first, with these columns: ${ AiService . getActivitiesTableColumnNames (
{ withValues : false }
) . join (
', '
', '
) } . The columns with a monetary value are omitted if the access does not grant to read them . At most $ { MCP_MAX_ACTIVITIES } activities are given per call , hence narrow the result with the parameters or get the further activities with the skip parameter . ` ,
) } . Mor e columns with a monetary value are add ed if the access grants to read them . At most $ { MCP_MAX_ACTIVITIES } activities are given per call , hence narrow the result with the parameters or get the further activities with the skip parameter . ` ,
name : 'get-activities' ,
name : 'get-activities' ,
parameters : GET_ACTIVITIES_PARAMETERS
parameters : GET_ACTIVITIES_PARAMETERS
} )
} )
@ -97,10 +98,9 @@ export class GhostfolioMcpController {
{
{
activityTypes ,
activityTypes ,
assetClasses ,
assetClasses ,
dataSource ,
holding ,
range ,
range ,
skip ,
skip ,
symbol ,
take
take
} : z . infer < typeof GET_ACTIVITIES_PARAMETERS >
} : z . infer < typeof GET_ACTIVITIES_PARAMETERS >
) {
) {
@ -115,23 +115,55 @@ export class GhostfolioMcpController {
const filtersOfAccess = filters ? ? [ ] ;
const filtersOfAccess = filters ? ? [ ] ;
const typesOfFiltersOfAccess = new Set (
const filtersOfTool = this . apiService . buildFiltersFromQueryParams ( {
filtersOfAccess . map ( ( { type } ) = > {
filterByAssetClasses : assetClasses?.join ( ',' ) ,
return type ;
filterByDataSource : holding?.dataSource ,
filterBySymbol : holding?.symbol
} ) ;
// A tool must never widen the access, hence a filter of the tool which
// the access does not permit gives no activity
const filtersOfToolOutsideAccess = filtersOfTool . filter ( ( { id , type } ) = > {
const filtersOfAccessOfType = filtersOfAccess . filter ( ( filter ) = > {
return filter . type === type ;
} ) ;
return (
filtersOfAccessOfType . length > 0 &&
! filtersOfAccessOfType . some ( ( filter ) = > {
return filter . id === id ;
} )
} )
) ;
) ;
} ) ;
// A filter of the tool is dropped if the access already restricts its
if ( filtersOfToolOutsideAccess . length > 0 ) {
// type, because the filters of a type are combined with a logical or and
const valuesOutsideAccess = filtersOfToolOutsideAccess
// a tool must never widen the access
. map ( ( { id } ) = > {
const filtersOfTool = this . apiService
return id ;
. buildFiltersFromQueryParams ( {
filterByAssetClasses : assetClasses?.join ( ',' ) ,
filterByDataSource : dataSource ,
filterBySymbol : symbol
} )
} )
. filter ( ( { type } ) = > {
. join ( ', ' ) ;
return ! typesOfFiltersOfAccess . has ( type ) ;
return {
content : [
{
text : ` No activities found. The access does not permit these values of the parameters: ${ valuesOutsideAccess } . ` ,
type : 'text' as const
}
]
} ;
}
const typesOfFiltersOfTool = new Set (
filtersOfTool . map ( ( { type } ) = > {
return type ;
} )
) ;
// The filters of a type are combined with a logical or, hence a filter of
// the tool replaces the filters of the access of the same type instead of
// joining them
const filtersOfAccessOutsideTool = filtersOfAccess . filter ( ( { type } ) = > {
return ! typesOfFiltersOfTool . has ( type ) ;
} ) ;
} ) ;
const table = await this . aiService . getActivitiesTable ( {
const table = await this . aiService . getActivitiesTable ( {
@ -139,7 +171,7 @@ export class GhostfolioMcpController {
skip ,
skip ,
startDate ,
startDate ,
userId ,
userId ,
filters : [ . . . filtersOfAccess , . . . filtersOfTool ] ,
filters : [ . . . filtersOfAccessOutsideTool , . . . filtersOfTool ] ,
take : take ? ? MCP_MAX_ACTIVITIES ,
take : take ? ? MCP_MAX_ACTIVITIES ,
types : activityTypes ,
types : activityTypes ,
userCurrency : userSettings.baseCurrency ,
userCurrency : userSettings.baseCurrency ,