Browse Source

Bugfix/limits of MCP requests (#8130)

* Add missing check

* Update changelog
pull/8128/head^2
Thomas Kaul 4 days ago
committed by GitHub
parent
commit
b1506c1563
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 59
      apps/api/src/middlewares/mcp-authorization.middleware.ts

1
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 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 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 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 ## 3.83.0 - 2026-10-10

59
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 { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service'; import { ImpersonationService } from '@ghostfolio/api/services/impersonation/impersonation.service';
import { HEADER_KEY_TOKEN, MCP_REALM } from '@ghostfolio/common/config'; import { HEADER_KEY_TOKEN, MCP_REALM } from '@ghostfolio/common/config';
import { hasRole } from '@ghostfolio/common/permissions';
import type { ImpersonationContext } from '@ghostfolio/common/types'; import type { ImpersonationContext } from '@ghostfolio/common/types';
import { RequestClient } from '@prisma/client'; 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 * 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. * 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 * 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. * the access, as the strategies do for the requests of the other clients.
*/ */
@ -47,24 +52,28 @@ export function createMcpAuthorizationMiddleware({
}); });
if (!impersonation.isActive) { 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) { if (!user) {
parameters.push( return sendUnauthorizedResponse({ accessId, response });
'error="invalid_token"',
'error_description="The access cannot be resolved"'
);
} }
response.setHeader('WWW-Authenticate', `Bearer ${parameters.join(', ')}`); const isRequestLimitExceeded =
hasRole(user, 'INACTIVE') ||
(await userService.isDailyRequestLimitExceeded({ user }));
return response.status(StatusCodes.UNAUTHORIZED).json({ if (isRequestLimitExceeded) {
error: getReasonPhrase(StatusCodes.UNAUTHORIZED), return response.status(StatusCodes.TOO_MANY_REQUESTS).json({
message: 'A valid access is required as a bearer token' 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({ await userService.updateAnalytics({
requestClient: RequestClient.MCP, requestClient: RequestClient.MCP,
userId: impersonation.userId userId: impersonation.userId
@ -78,3 +87,27 @@ export function createMcpAuthorizationMiddleware({
return next(); 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'
});
}

Loading…
Cancel
Save