Browse Source

Feature/extend MCP tool to get accounts by account id (#7765)

* Extend MCP tool to get accounts by account id

* Update changelog
pull/7759/head^2
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
a36b7f9ea4
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 18
      apps/api/src/app/endpoints/ai/ai.service.spec.ts
  3. 7
      apps/api/src/app/endpoints/ai/ai.service.ts
  4. 16
      apps/api/src/app/endpoints/mcp/mcp.controller.ts
  5. 1
      libs/common/src/lib/config.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Extended the tool to get the accounts of the portfolio in the server of the Model Context Protocol (MCP) to support the filtering by account (experimental)
- Upgraded `uuid` from version `14.0.1` to `14.0.2`
## 3.64.0 - 2026-08-30

18
apps/api/src/app/endpoints/ai/ai.service.spec.ts

@ -29,13 +29,16 @@ interface AiServiceWithMarkdownTable {
}
function createAccount({
id = 'account-a-id',
isExcluded = false,
name = 'Account A'
}: {
id?: string;
isExcluded?: boolean;
name?: string;
} = {}) {
return {
id,
name,
activitiesCount: 3,
allocationInPercentage: 0.25,
@ -86,6 +89,7 @@ describe('AiService', () => {
describe('getAccountsTableColumnNames', () => {
it('gives no column with a monetary value', () => {
expect(AiService.getAccountsTableColumnNames()).toEqual([
'Id',
'Name',
'Currency',
'Platform',
@ -121,10 +125,20 @@ describe('AiService', () => {
expect(result).not.toContain('2000');
});
// The accountIds parameter of the tool takes the identifiers, hence the
// table has to give them
it('gives the identifier of an account', async () => {
const aiService = createAiService([createAccount()]);
const result = await aiService.getAccountsTable({ userId: 'user-id' });
expect(result).toContain('account-a-id');
});
it('marks an account which is excluded from the analysis', async () => {
const aiService = createAiService([
createAccount({ isExcluded: true }),
createAccount({ name: 'Account B' })
createAccount({ id: 'account-b-id', name: 'Account B' })
]);
const result = await aiService.getAccountsTable({ userId: 'user-id' });
@ -132,7 +146,7 @@ describe('AiService', () => {
const [rowOfAccountA, rowOfAccountB] = result
.split('\n')
.filter((line) => {
return line.startsWith('Account ');
return line.startsWith('account-');
});
expect(rowOfAccountA).toContain('true');

7
apps/api/src/app/endpoints/ai/ai.service.ts

@ -30,9 +30,11 @@ export class AiService {
| 'ALLOCATION_PERCENTAGE'
| 'CURRENCY'
| 'EXCLUDED_FROM_ANALYSIS'
| 'ID'
| 'NAME'
| 'PLATFORM';
} & ColumnDescriptor)[] = [
{ key: 'ID', name: 'Id' },
{ key: 'NAME', name: 'Name' },
{ key: 'CURRENCY', name: 'Currency' },
{ key: 'PLATFORM', name: 'Platform' },
@ -160,6 +162,7 @@ export class AiService {
activitiesCount,
allocationInPercentage,
currency,
id,
name: label,
platform,
tags
@ -183,6 +186,10 @@ export class AiService {
row[name] = isAccountExcluded({ tags }).toString();
break;
case 'ID':
row[name] = id;
break;
case 'NAME':
row[name] = label ?? '';
break;

16
apps/api/src/app/endpoints/mcp/mcp.controller.ts

@ -8,6 +8,7 @@ import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'
import {
DATE_RANGES,
DEFAULT_LANGUAGE_CODE,
MCP_MAX_ACCOUNTS,
MCP_MAX_ACTIVITIES
} from '@ghostfolio/common/config';
import { scopes } from '@ghostfolio/common/scopes';
@ -20,6 +21,14 @@ import { McpController, Tool } from '@rekog/mcp-nest';
import { z } from 'zod';
const GET_ACCOUNTS_PARAMETERS = z.object({
accountIds: z
.array(z.string().min(1))
.min(1)
.max(MCP_MAX_ACCOUNTS)
.optional()
.describe(
`The identifiers of the accounts to get, at most ${MCP_MAX_ACCOUNTS}`
),
assetClasses: z
.array(z.enum(AssetClass))
.min(1)
@ -104,9 +113,14 @@ export class GhostfolioMcpController {
public async getAccounts(
@Impersonation() { userId }: ImpersonationContext,
@Payload()
{ assetClasses, holding }: z.infer<typeof GET_ACCOUNTS_PARAMETERS>
{
accountIds,
assetClasses,
holding
}: z.infer<typeof GET_ACCOUNTS_PARAMETERS>
) {
const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts: accountIds?.join(','),
filterByAssetClasses: assetClasses?.join(','),
filterByDataSource: holding?.dataSource,
filterBySymbol: holding?.symbol

1
libs/common/src/lib/config.ts

@ -277,6 +277,7 @@ export const HTTP_RESPONSE_MESSAGE_IMPERSONATION_UNRESOLVED =
export const MAX_TOP_HOLDINGS = 50;
export const MCP_ENDPOINT = '/mcp';
export const MCP_MAX_ACCOUNTS = 50;
export const MCP_MAX_ACTIVITIES = 100;
export const MCP_REALM = 'Ghostfolio';

Loading…
Cancel
Save