feat(mcp): add rate limiting and auth guard tests for MCP transport controller
MCP endpoints already had JwtAuthGuard applied but lacked per-route rate limiting and test coverage for security behavior. Add @Throttle decorators with appropriate limits (5 req/min for SSE connections, 30 req/min for server list and messages), unit tests verifying guard/throttle metadata, and E2E tests confirming 401 rejection for unauthenticated requests. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -28,6 +28,41 @@ describe('McpTransportController', () => {
|
||||
controller = new McpTransportController(mockRegistry as any);
|
||||
});
|
||||
|
||||
describe('security decorators', () => {
|
||||
it('has JwtAuthGuard applied at controller level', () => {
|
||||
const guards = Reflect.getMetadata('__guards__', McpTransportController);
|
||||
expect(guards).toBeDefined();
|
||||
expect(guards).toHaveLength(1);
|
||||
// The guard is applied via @UseGuards(JwtAuthGuard)
|
||||
const guardNames = guards.map((g: any) => g.name || g.constructor?.name);
|
||||
expect(guardNames).toContain('JwtAuthGuard');
|
||||
});
|
||||
|
||||
it('has Throttle metadata on listServers endpoint', () => {
|
||||
const limit = Reflect.getMetadata(
|
||||
'THROTTLER:LIMITdefault',
|
||||
McpTransportController.prototype.listServers,
|
||||
);
|
||||
expect(limit).toBe(30);
|
||||
});
|
||||
|
||||
it('has Throttle metadata on handleSse endpoint with low limit', () => {
|
||||
const limit = Reflect.getMetadata(
|
||||
'THROTTLER:LIMITdefault',
|
||||
McpTransportController.prototype.handleSse,
|
||||
);
|
||||
expect(limit).toBe(5);
|
||||
});
|
||||
|
||||
it('has Throttle metadata on handleMessage endpoint', () => {
|
||||
const limit = Reflect.getMetadata(
|
||||
'THROTTLER:LIMITdefault',
|
||||
McpTransportController.prototype.handleMessage,
|
||||
);
|
||||
expect(limit).toBe(30);
|
||||
});
|
||||
});
|
||||
|
||||
describe('listServers', () => {
|
||||
it('returns list of server names from registry', () => {
|
||||
mockRegistry.getServerNames.mockReturnValue(['search', 'listings']);
|
||||
@@ -82,6 +117,24 @@ describe('McpTransportController', () => {
|
||||
expect(mockServer.connect).toHaveBeenCalledOnce();
|
||||
expect(mockReq.on).toHaveBeenCalledWith('close', expect.any(Function));
|
||||
});
|
||||
|
||||
it('cleans up transport on connection close', async () => {
|
||||
const mockServer = { connect: vi.fn().mockResolvedValue(undefined) };
|
||||
mockRegistry.getServer.mockReturnValue(mockServer);
|
||||
|
||||
await controller.handleSse('search', mockUser as any, mockReq as any, mockRes as any);
|
||||
|
||||
// Simulate connection close
|
||||
const closeHandler = mockReq.on.mock.calls[0]![1] as () => void;
|
||||
closeHandler();
|
||||
|
||||
// Transport should be removed — subsequent message calls should fail
|
||||
const msgReq = { query: { sessionId: 'mock-session-id' } } as any;
|
||||
const msgRes = {} as any;
|
||||
await expect(
|
||||
controller.handleMessage('search', mockUser as any, msgReq, msgRes),
|
||||
).rejects.toThrow(HttpException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleMessage', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
import type { Request, Response } from 'express';
|
||||
import { JwtAuthGuard, CurrentUser, type JwtPayload } from '@modules/auth';
|
||||
|
||||
@@ -24,6 +25,7 @@ export class McpTransportController {
|
||||
constructor(private readonly registry: McpRegistryService) {}
|
||||
|
||||
@Get('servers')
|
||||
@Throttle({ default: { ttl: 60_000, limit: 30 } })
|
||||
@ApiOperation({ summary: 'List available MCP servers' })
|
||||
@ApiResponse({ status: 200, description: 'List of registered MCP server names' })
|
||||
@ApiResponse({ status: 401, description: 'Unauthorized' })
|
||||
@@ -32,6 +34,7 @@ export class McpTransportController {
|
||||
}
|
||||
|
||||
@Get(':serverName/sse')
|
||||
@Throttle({ default: { ttl: 60_000, limit: 5 } })
|
||||
@ApiOperation({ summary: 'Open SSE connection to an MCP server' })
|
||||
@ApiParam({ name: 'serverName', description: 'Name of the MCP server to connect to' })
|
||||
@ApiResponse({ status: 200, description: 'SSE stream established' })
|
||||
@@ -65,6 +68,7 @@ export class McpTransportController {
|
||||
}
|
||||
|
||||
@Post(':serverName/messages')
|
||||
@Throttle({ default: { ttl: 60_000, limit: 30 } })
|
||||
@ApiOperation({ summary: 'Send a message to an MCP server session' })
|
||||
@ApiParam({ name: 'serverName', description: 'Name of the MCP server' })
|
||||
@ApiResponse({ status: 200, description: 'Message processed' })
|
||||
|
||||
Reference in New Issue
Block a user