From b1506c1563bbd5e166e9071b493b9b23dcb4ff97 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 11 Oct 2026 16:51:11 +0200 Subject: [PATCH] Bugfix/limits of MCP requests (#8130) * Add missing check * Update changelog --- CHANGELOG.md | 1 + .../mcp-authorization.middleware.ts | 59 +++++++++++++++---- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e0eff1c45..7ec0d3f0d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the dates of the benchmark in the benchmark comparator on the analysis page for instances in time zones with a negative UTC offset - Fixed the value of the accounts by excluding the cash balance when filtering by an asset class other than liquidity on the accounts page and the allocations page - Fixed the value of the accounts by excluding the cash balance when filtering by an asset class other than liquidity in the server of the Model Context Protocol (MCP) (experimental) +- Fixed the missing check of the inactive role and of the daily request limit in the server of the Model Context Protocol (MCP) (experimental) ## 3.83.0 - 2026-10-10 diff --git a/apps/api/src/middlewares/mcp-authorization.middleware.ts b/apps/api/src/middlewares/mcp-authorization.middleware.ts index 47c4332b08..416816f298 100644 --- a/apps/api/src/middlewares/mcp-authorization.middleware.ts +++ b/apps/api/src/middlewares/mcp-authorization.middleware.ts @@ -3,6 +3,7 @@ import { getAccessIdOfBearerToken } from '@ghostfolio/api/helper/bearer-token.he import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { HEADER_KEY_TOKEN, MCP_REALM } from '@ghostfolio/common/config'; +import { hasRole } from '@ghostfolio/common/permissions'; import type { ImpersonationContext } from '@ghostfolio/common/types'; import { RequestClient } from '@prisma/client'; @@ -20,6 +21,10 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes'; * authorization server. A client which reads the challenge asks the user for * the identifier of the access instead of starting a flow which would fail. * + * With the subscription, the user who granted the access has the same limits + * as with the other clients: an inactive user and a user who exceeds the daily + * request limit get the status 429, which also has to come from here. + * * An authorized request is counted in the analytics of the user who granted * the access, as the strategies do for the requests of the other clients. */ @@ -47,24 +52,28 @@ export function createMcpAuthorizationMiddleware({ }); if (!impersonation.isActive) { - const parameters = [`realm="${MCP_REALM}"`]; + return sendUnauthorizedResponse({ accessId, response }); + } + + if (configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { + const user = await userService.user({ id: impersonation.userId }); - if (accessId) { - parameters.push( - 'error="invalid_token"', - 'error_description="The access cannot be resolved"' - ); + if (!user) { + return sendUnauthorizedResponse({ accessId, response }); } - response.setHeader('WWW-Authenticate', `Bearer ${parameters.join(', ')}`); + const isRequestLimitExceeded = + hasRole(user, 'INACTIVE') || + (await userService.isDailyRequestLimitExceeded({ user })); - return response.status(StatusCodes.UNAUTHORIZED).json({ - error: getReasonPhrase(StatusCodes.UNAUTHORIZED), - message: 'A valid access is required as a bearer token' - }); - } + if (isRequestLimitExceeded) { + return response.status(StatusCodes.TOO_MANY_REQUESTS).json({ + error: getReasonPhrase(StatusCodes.TOO_MANY_REQUESTS), + message: + 'The request limit of the user who granted the access is exceeded' + }); + } - if (configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { await userService.updateAnalytics({ requestClient: RequestClient.MCP, userId: impersonation.userId @@ -78,3 +87,27 @@ export function createMcpAuthorizationMiddleware({ return next(); }; } + +function sendUnauthorizedResponse({ + accessId, + response +}: { + accessId: string; + response: Response; +}) { + const parameters = [`realm="${MCP_REALM}"`]; + + if (accessId) { + parameters.push( + 'error="invalid_token"', + 'error_description="The access cannot be resolved"' + ); + } + + response.setHeader('WWW-Authenticate', `Bearer ${parameters.join(', ')}`); + + return response.status(StatusCodes.UNAUTHORIZED).json({ + error: getReasonPhrase(StatusCodes.UNAUTHORIZED), + message: 'A valid access is required as a bearer token' + }); +}