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 ### 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` - Upgraded `uuid` from version `14.0.1` to `14.0.2`
## 3.64.0 - 2026-08-30 ## 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({ function createAccount({
id = 'account-a-id',
isExcluded = false, isExcluded = false,
name = 'Account A' name = 'Account A'
}: { }: {
id?: string;
isExcluded?: boolean; isExcluded?: boolean;
name?: string; name?: string;
} = {}) { } = {}) {
return { return {
id,
name, name,
activitiesCount: 3, activitiesCount: 3,
allocationInPercentage: 0.25, allocationInPercentage: 0.25,
@ -86,6 +89,7 @@ describe('AiService', () => {
describe('getAccountsTableColumnNames', () => { describe('getAccountsTableColumnNames', () => {
it('gives no column with a monetary value', () => { it('gives no column with a monetary value', () => {
expect(AiService.getAccountsTableColumnNames()).toEqual([ expect(AiService.getAccountsTableColumnNames()).toEqual([
'Id',
'Name', 'Name',
'Currency', 'Currency',
'Platform', 'Platform',
@ -121,10 +125,20 @@ describe('AiService', () => {
expect(result).not.toContain('2000'); 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 () => { it('marks an account which is excluded from the analysis', async () => {
const aiService = createAiService([ const aiService = createAiService([
createAccount({ isExcluded: true }), createAccount({ isExcluded: true }),
createAccount({ name: 'Account B' }) createAccount({ id: 'account-b-id', name: 'Account B' })
]); ]);
const result = await aiService.getAccountsTable({ userId: 'user-id' }); const result = await aiService.getAccountsTable({ userId: 'user-id' });
@ -132,7 +146,7 @@ describe('AiService', () => {
const [rowOfAccountA, rowOfAccountB] = result const [rowOfAccountA, rowOfAccountB] = result
.split('\n') .split('\n')
.filter((line) => { .filter((line) => {
return line.startsWith('Account '); return line.startsWith('account-');
}); });
expect(rowOfAccountA).toContain('true'); expect(rowOfAccountA).toContain('true');

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

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

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

@ -8,6 +8,7 @@ import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'
import { import {
DATE_RANGES, DATE_RANGES,
DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_CODE,
MCP_MAX_ACCOUNTS,
MCP_MAX_ACTIVITIES MCP_MAX_ACTIVITIES
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { scopes } from '@ghostfolio/common/scopes'; import { scopes } from '@ghostfolio/common/scopes';
@ -20,6 +21,14 @@ import { McpController, Tool } from '@rekog/mcp-nest';
import { z } from 'zod'; import { z } from 'zod';
const GET_ACCOUNTS_PARAMETERS = z.object({ 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 assetClasses: z
.array(z.enum(AssetClass)) .array(z.enum(AssetClass))
.min(1) .min(1)
@ -104,9 +113,14 @@ export class GhostfolioMcpController {
public async getAccounts( public async getAccounts(
@Impersonation() { userId }: ImpersonationContext, @Impersonation() { userId }: ImpersonationContext,
@Payload() @Payload()
{ assetClasses, holding }: z.infer<typeof GET_ACCOUNTS_PARAMETERS> {
accountIds,
assetClasses,
holding
}: z.infer<typeof GET_ACCOUNTS_PARAMETERS>
) { ) {
const filters = this.apiService.buildFiltersFromQueryParams({ const filters = this.apiService.buildFiltersFromQueryParams({
filterByAccounts: accountIds?.join(','),
filterByAssetClasses: assetClasses?.join(','), filterByAssetClasses: assetClasses?.join(','),
filterByDataSource: holding?.dataSource, filterByDataSource: holding?.dataSource,
filterBySymbol: holding?.symbol 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 MAX_TOP_HOLDINGS = 50;
export const MCP_ENDPOINT = '/mcp'; export const MCP_ENDPOINT = '/mcp';
export const MCP_MAX_ACCOUNTS = 50;
export const MCP_MAX_ACTIVITIES = 100; export const MCP_MAX_ACTIVITIES = 100;
export const MCP_REALM = 'Ghostfolio'; export const MCP_REALM = 'Ghostfolio';

Loading…
Cancel
Save