# MCP Module Exploration - GoodGo Platform ## 1. MODULE STRUCTURE & SOURCE FILES ### Directory Structure ``` apps/api/src/modules/mcp/ ├── index.ts ├── mcp.module.ts └── presentation/ ├── mcp-transport.controller.ts └── __tests__/ └── mcp-transport.controller.spec.ts ``` ### All Source Files (4 files) #### 1. **apps/api/src/modules/mcp/index.ts** - **Type**: Module entry point (exports) - **Purpose**: Exports the McpIntegrationModule - **Exports**: `{ McpIntegrationModule }` #### 2. **apps/api/src/modules/mcp/mcp.module.ts** - **Type**: NestJS Module configuration (22 lines) - **Key Class**: `McpIntegrationModule implements OnModuleInit` - **Responsibility**: - Sets up MCP core module with configuration - Initializes TypesenseClient for MCP registry - Logs initialized server names on module init - **Dependencies Injected**: - `TypesenseClientService` (from SearchModule) - `McpRegistryService` (from @goodgo/mcp-servers) - `LoggerService` (from SharedModule) - **Imports**: - `SearchModule` - `AuthModule` - `McpCoreModule.forRoot()` with config - **Controllers**: McpTransportController - **Lifecycle**: Implements `onModuleInit()` #### 3. **apps/api/src/modules/mcp/presentation/mcp-transport.controller.ts** - **Type**: NestJS Controller (102 lines) - **Key Class**: `McpTransportController` - **Responsibility**: HTTP transport layer for MCP SSE connections - **Decorators Applied**: - `@ApiTags('mcp')` - `@ApiBearerAuth('JWT')` - `@Controller('mcp')` - `@UseGuards(JwtAuthGuard)` - protects all endpoints - **Properties**: - `transports: Map` - active session management - **Injected Dependencies**: - `registry: McpRegistryService` **Endpoints:** 1. **GET /mcp/servers** (line 27-34) - Summary: List available MCP servers - Throttle: 30 requests per 60s - Returns: `{ servers: string[] }` - Status: 200 (success), 401 (unauthorized) 2. **GET /mcp/:serverName/sse** (line 36-68) - Summary: Open SSE connection to MCP server - Throttle: 5 requests per 60s (stricter) - Params: `serverName` - Returns: SSE stream - Response Handling: - Creates `SSEServerTransport` instance - Stores in map with `sessionId` - Connects to server via `server.connect(transport)` - Cleans up on request close - Status: 200 (stream), 404 (server not found), 401 (unauthorized) 3. **POST /mcp/:serverName/messages** (line 70-102) - Summary: Send message to MCP server session - Throttle: 30 requests per 60s - Params: `serverName` - Query: `sessionId` (required) - Body: Message data passed to transport - Response Handling: - Validates sessionId exists - Delegates to `transport.handlePostMessage(req, res)` - Status: 200 (success), 400 (missing sessionId), 404 (session not found), 401 (unauthorized) --- ## 2. TEST FILES ### Total Test Files: 1 #### **apps/api/src/modules/mcp/presentation/__tests__/mcp-transport.controller.spec.ts** (174 lines) - **Testing Framework**: Vitest - **Subject**: `McpTransportController` - **Test Structure**: Describe blocks + beforeEach setup **Test Suites (4 describe blocks):** 1. **security decorators** (4 tests) - Verifies JwtAuthGuard is applied - Checks Throttle metadata on endpoints - Validates throttle limits (30, 5, 30) 2. **listServers** (2 tests) - Returns server list from registry - Handles empty server list 3. **handleSse** (3 tests) - Throws NOT_FOUND when server doesn't exist - Creates transport and connects to server - Cleans up on connection close (transport removal) 4. **handleMessage** (2 tests) - Throws BAD_REQUEST when sessionId missing - Throws NOT_FOUND when session doesn't exist **Mocking Patterns Used:** - `vi.mock()` for external modules (SSEServerTransport) - `vi.fn()` for service method mocks - `mockReturnValue()`, `mockResolvedValue()`, `mockRejectValue()` - Manual mock objects for requests/responses - Reflection API for metadata checks (`Reflect.getMetadata()`) --- ## 3. DDD LAYER STRUCTURE **Current Status**: MCP module has a **SIMPLIFIED structure** (NOT full DDD yet): ### What EXISTS: - **Presentation Layer** ✅ - `presentation/mcp-transport.controller.ts` - `presentation/__tests__/mcp-transport.controller.spec.ts` - HTTP endpoint definitions - Guard decorators (auth) - Throttle decorators - Swagger decorators ### What DOES NOT EXIST (yet): - **Domain Layer** ❌ - No `domain/` directory - No entities, value objects, or domain events - No domain business logic - **Application Layer** ❌ - No `application/` directory - No commands/handlers (CQRS pattern not used here) - No queries - No DTOs - **Infrastructure Layer** ❌ - No `infrastructure/` directory - No repositories - No external service adapters - No database access ### Architecture Notes: - The MCP module acts as an **integration wrapper** - Delegates to `@goodgo/mcp-servers` library (external dependency) - Simple **presentation-only controller** approach - Focuses on HTTP transport mechanism (SSE connections) - Session management via in-memory Map --- ## 4. KEY CLASSES/HANDLERS NEEDING TESTS ### Current Implementation: 1. **McpIntegrationModule** (mcp.module.ts) - **Status**: Partially tested (initialization logic) - **Methods needing tests**: - `constructor()` - dependency injection - `onModuleInit()` - initialization flow - **Test focus**: Module setup, service integration, logging 2. **McpTransportController** (mcp-transport.controller.ts) - **Status**: Well tested ✅ (174 lines of tests) - **Methods tested**: - `listServers()` - returns available servers - `handleSse()` - establishes SSE connection - `handleMessage()` - routes messages to session - **Test coverage**: Happy path + error cases + security decorators --- ## 5. TESTING PATTERNS FROM OTHER MODULES ### Pattern 1: Auth Module - Handler Testing (Simple) **File**: `apps/api/src/modules/auth/application/__tests__/login-user.handler.spec.ts` ```typescript // KEY PATTERNS: // 1. No explicit imports from vitest (globals enabled) // 2. beforeEach: Create handler with mocked dependencies // 3. vi.fn() for service mocks // 4. mockResolvedValue() for async returns // 5. expect().toHaveBeenCalledWith() for verification // 6. Simple test structure for handlers describe('LoginUserHandler', () => { let handler: LoginUserHandler; let mockTokenService: { generateTokenPair: ReturnType }; const tokenPair = { accessToken: 'access-jwt', refreshToken: 'family.refresh-hex', expiresIn: 900, }; beforeEach(() => { mockTokenService = { generateTokenPair: vi.fn().mockResolvedValue(tokenPair) }; handler = new LoginUserHandler(mockTokenService as any); }); it('generates token pair with correct payload', async () => { const command = new LoginUserCommand('user-1', '0912345678', 'BUYER'); const result = await handler.execute(command); expect(result).toEqual(tokenPair); expect(mockTokenService.generateTokenPair).toHaveBeenCalledWith({ sub: 'user-1', phone: '0912345678', role: 'BUYER', }); }); }); ``` **Key Takeaways**: - ✅ Minimal setup, direct handler instantiation - ✅ Command objects for test input - ✅ Simple mock with typed object - ✅ Focus on behavior verification - ✅ Uses `as any` for type casting ### Pattern 2: Payments Module - Complex Handler Testing (Advanced) **File**: `apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts` ```typescript // KEY PATTERNS: // 1. Multiple mocked dependencies (repo, factory, gateway, event bus) // 2. Complex mock setup with multiple methods // 3. Tests for idempotency handling // 4. Error case validation // 5. Event publishing verification describe('CreatePaymentHandler', () => { let handler: CreatePaymentHandler; let mockPaymentRepo: { [K in keyof IPaymentRepository]: ReturnType }; let mockGatewayFactory: { getGateway: ReturnType }; let mockGateway: { createPaymentUrl: ReturnType; verifyCallback: ReturnType; refund: ReturnType; }; let mockEventBus: { publish: ReturnType }; beforeEach(() => { mockPaymentRepo = { findById: vi.fn(), findByProviderTxId: vi.fn(), findByIdempotencyKey: vi.fn(), findByUserId: vi.fn(), save: vi.fn().mockResolvedValue(undefined), update: vi.fn(), updateIfStatus: vi.fn(), }; mockGateway = { createPaymentUrl: vi.fn().mockResolvedValue({ paymentUrl: 'https://vnpay.vn/pay/123', providerTxId: 'vnpay-tx-1', }), verifyCallback: vi.fn(), refund: vi.fn(), }; mockGatewayFactory = { getGateway: vi.fn().mockReturnValue(mockGateway), }; mockEventBus = { publish: vi.fn() }; const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() }; handler = new CreatePaymentHandler( mockPaymentRepo as any, mockGatewayFactory as any, mockEventBus as any, mockLogger as any, ); }); it('creates payment successfully', async () => { mockPaymentRepo.findByIdempotencyKey.mockResolvedValue(null); const command = new CreatePaymentCommand( 'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n, 'Thanh toán gói Pro', 'https://goodgo.vn/return', '127.0.0.1', undefined, 'idem-key-1', ); const result = await handler.execute(command); expect(result.paymentId).toBeDefined(); expect(result.paymentUrl).toBe('https://vnpay.vn/pay/123'); expect(result.providerTxId).toBe('vnpay-tx-1'); expect(mockPaymentRepo.save).toHaveBeenCalledTimes(1); expect(mockEventBus.publish).toHaveBeenCalled(); expect(mockGatewayFactory.getGateway).toHaveBeenCalledWith('VNPAY'); }); it('throws ConflictException for duplicate idempotency key (pending)', async () => { mockPaymentRepo.findByIdempotencyKey.mockResolvedValue({ status: 'PENDING' }); const command = new CreatePaymentCommand( 'user-1', 'VNPAY', 'SUBSCRIPTION', 500_000n, 'desc', 'https://goodgo.vn/return', '127.0.0.1', undefined, 'existing-key', ); await expect(handler.execute(command)).rejects.toThrow(/idempotency/); }); }); ``` **Key Takeaways**: - ✅ Complex multi-dependency mocking - ✅ Mapped type for repository mocks `[K in keyof IPaymentRepository]` - ✅ Individual method mocks for each dependency - ✅ Tests for business rules (idempotency) - ✅ Error case testing with regex matching - ✅ Logger mock included - ✅ Verify multiple calls and interactions ### Pattern 3: Domain Entity Testing (DDD Style) **File**: `apps/api/src/modules/payments/domain/__tests__/payment.entity.spec.ts` ```typescript // KEY PATTERNS: // 1. Explicit vitest imports at the top // 2. Complex entity with state transitions // 3. Tests for domain events // 4. Tests for value objects // 5. Tests for error handling (Result types) // 6. Helper factory function for test setup import { describe, it, expect } from 'vitest'; import { PaymentEntity } from '../entities/payment.entity'; import { PaymentCompletedEvent } from '../events/payment-completed.event'; import { PaymentCreatedEvent } from '../events/payment-created.event'; import { PaymentFailedEvent } from '../events/payment-failed.event'; import { PaymentRefundedEvent } from '../events/payment-refunded.event'; import { Money } from '../value-objects/money.vo'; describe('PaymentEntity', () => { const createPayment = (status?: string) => { const money = Money.create(500_000n).unwrap(); const payment = PaymentEntity.createNew( 'pay-1', 'user-1', 'VNPAY', 'LISTING_FEE', money, 'txn-1', 'idem-key-1', ); if (status === 'PROCESSING') { payment.markProcessing('vnp-txn-123'); } if (status === 'COMPLETED') { payment.markProcessing('vnp-txn-123'); payment.clearDomainEvents(); payment.markCompleted({ responseCode: '00' }); } return payment; }; it('should create a new payment with domain events', () => { const money = Money.create(500_000n).unwrap(); const payment = PaymentEntity.createNew( 'pay-1', 'user-1', 'VNPAY', 'LISTING_FEE', money, 'txn-1', ); expect(payment.id).toBe('pay-1'); expect(payment.status).toBe('PENDING'); const events = payment.domainEvents; expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(PaymentCreatedEvent); }); it('should mark completed payment as refunded and emit event', () => { const payment = createPayment('COMPLETED'); payment.clearDomainEvents(); const result = payment.markRefunded(); expect(result.isOk).toBe(true); expect(payment.status).toBe('REFUNDED'); const events = payment.domainEvents; expect(events).toHaveLength(1); expect(events[0]).toBeInstanceOf(PaymentRefundedEvent); }); it('should not refund a non-completed payment', () => { const payment = createPayment(); const result = payment.markRefunded(); expect(result.isErr).toBe(true); expect(result.unwrapErr().message).toContain('hoàn tiền'); }); }); ``` **Key Takeaways**: - ✅ Explicit vitest imports (explicit over implicit) - ✅ Helper factory method for complex setup - ✅ Tests for entity behavior (state transitions) - ✅ Tests for domain events - ✅ Tests for error handling (Result pattern) - ✅ No mocking - tests real entity logic - ✅ Value object usage (Money.create().unwrap()) - ✅ Domain event verification ### Pattern 4: Infrastructure Service Testing (Crypto/External) **File**: `apps/api/src/modules/payments/infrastructure/__tests__/zalopay.service.spec.ts` ```typescript // KEY PATTERNS: // 1. Explicit vitest imports // 2. External service simulation (payment gateway) // 3. Crypto/HMAC signature testing // 4. Helper function to build test data // 5. Mocking ConfigService // 6. Tests for security (tamper detection) import * as crypto from 'crypto'; import { type ConfigService } from '@nestjs/config'; import { describe, it, expect, beforeEach, vi } from 'vitest'; import { ZalopayService } from '../services/zalopay.service'; describe('ZalopayService', () => { let service: ZalopayService; const key2 = 'TESTKEY2ABCDEF1234567890ABCDEF12'; beforeEach(() => { const mockConfig = { get: vi.fn((key: string, defaultValue?: string) => { const env: Record = { 'ZALOPAY_APP_ID': '2553', 'ZALOPAY_KEY1': 'TESTKEY1ABCDEF1234567890ABCDEF12', 'ZALOPAY_KEY2': 'TESTKEY2ABCDEF1234567890ABCDEF12', }; return env[key] ?? defaultValue; }), getOrThrow: vi.fn((key: string) => { // ... }), } as unknown as ConfigService; const mockLogger = { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), verbose: vi.fn() }; service = new ZalopayService(mockConfig, mockLogger as any); }); function buildCallbackData( dataPayload: Record = {}, tamperMac = false, ): Record { const payload = { app_id: 2553, app_trans_id: '260408_order-123', zp_trans_id: 'ZLP_TX_456', ...dataPayload, }; const dataStr = JSON.stringify(payload); let mac = crypto .createHmac('sha256', key2) .update(dataStr) .digest('hex'); if (tamperMac) { mac = 'a'.repeat(mac.length); } return { data: dataStr, mac }; } it('should verify a valid callback with timingSafeEqual', () => { const data = buildCallbackData(); const result = service.verifyCallback(data); expect(result.isValid).toBe(true); expect(result.isSuccess).toBe(true); expect(result.orderId).toBe('260408_order-123'); expect(result.providerTxId).toBe('ZLP_TX_456'); }); it('should reject an invalid MAC', () => { const data = buildCallbackData({}, true); const result = service.verifyCallback(data); expect(result.isValid).toBe(false); }); it('should handle invalid JSON in data field gracefully', () => { const mac = crypto .createHmac('sha256', key2) .update('not-json') .digest('hex'); const result = service.verifyCallback({ data: 'not-json', mac }); expect(result.isValid).toBe(false); }); }); ``` **Key Takeaways**: - ✅ Explicit vitest imports (import from 'vitest') - ✅ Tests for security-critical functionality (HMAC verification) - ✅ ConfigService mocking for environment variables - ✅ Helper function for complex test data generation - ✅ Tests for error conditions (invalid JSON, wrong MAC) - ✅ Tests for security edge cases (tampered data) - ✅ Uses Node.js crypto module directly - ✅ Real algorithm testing (SHA256 HMAC) --- ## 6. TESTING CONVENTIONS SUMMARY ### Framework & Setup - **Framework**: Vitest - **Environment**: Node.js - **Globals**: Enabled (`globals: true` in config) - **File Pattern**: `*.spec.ts` (not `.test.ts`) - **Test Organization**: `__tests__/` subdirectory ### Import Styles ```typescript // Style 1: Using globals (in simple tests) describe('MyClass', () => { it('does something', () => { expect(true).toBe(true); }); }); // Style 2: Explicit imports (in complex/domain tests) import { describe, it, expect, beforeEach, vi } from 'vitest'; ``` ### Mocking Patterns 1. **Service Mocks** ```typescript mockService = { method: vi.fn().mockResolvedValue(value), method2: vi.fn().mockReturnValue(value), }; ``` 2. **Module Mocks** ```typescript vi.mock('@goodgo/mcp-servers', () => ({ SSEServerTransport: class MockSSE { /* ... */ }, })); ``` 3. **Configuration Mocks** ```typescript const mockConfig = { get: vi.fn((key) => env[key]), getOrThrow: vi.fn((key) => env[key] || throw), }; ``` ### Test Structure ```typescript describe('ClassName', () => { let instance: ClassName; let mockDep1: Mock; let mockDep2: Mock; beforeEach(() => { // Setup mocks mockDep1 = { method: vi.fn() }; // Instantiate with mocks instance = new ClassName(mockDep1 as any, mockDep2 as any); }); describe('methodName', () => { it('happy path scenario', async () => { // Arrange mockDep1.method.mockResolvedValue(expected); // Act const result = await instance.method(); // Assert expect(result).toEqual(expected); expect(mockDep1.method).toHaveBeenCalledWith(params); }); it('error case', async () => { // Arrange mockDep1.method.mockRejectedValue(new Error('fail')); // Act & Assert await expect(instance.method()).rejects.toThrow('fail'); }); }); }); ``` ### Common Assertions Used - `expect(x).toBe(value)` - strict equality - `expect(x).toEqual(object)` - deep equality - `expect(x).toHaveLength(n)` - array/string length - `expect(x).toBeInstanceOf(Class)` - instance check - `expect(fn).toHaveBeenCalled()` - called verification - `expect(fn).toHaveBeenCalledWith(args)` - call arguments - `expect(fn).toHaveBeenCalledTimes(n)` - call count - `expect(promise).rejects.toThrow(msg)` - error expectation ### Vitest-Specific Features Used - `vi.fn()` - create mock function - `vi.mock()` - module mocking - `mockResolvedValue()` - async return - `mockReturnValue()` - sync return - `mockRejectedValue()` - error simulation - `Reflect.getMetadata()` - decorator inspection (NestJS) --- ## 7. RECOMMENDATIONS FOR MCP MODULE TESTING ### Current Coverage Status - ✅ Controller (presentation layer): Well tested - ⚠️ Module initialization: Basic (mocked) - ❌ Domain layer: Not implemented - ❌ Application layer: Not implemented - ❌ Infrastructure layer: Not implemented ### Recommended Next Steps 1. **Expand Controller Tests** - Integration tests with mock SSE connections - Test session lifecycle (open → message → close) - Test error recovery 2. **Add Module Tests** - Test `McpIntegrationModule.onModuleInit()` - Test registry initialization - Test service injection 3. **Consider Adding Domain Tests** (if business logic added) - Session entity tests - Connection state management - Event handling ### Test Command ```bash # Run all MCP tests pnpm test -- src/modules/mcp # Run with coverage pnpm test -- --coverage src/modules/mcp # Watch mode pnpm test -- --watch src/modules/mcp ```