# MCP Module Exploration - GoodGo Platform ## 1. CẤU TRÚC MODULE & CÁC TỆP NGUỒN ### Cấu trúc thư mục ``` apps/api/src/modules/mcp/ ├── index.ts ├── mcp.module.ts └── presentation/ ├── mcp-transport.controller.ts └── __tests__/ └── mcp-transport.controller.spec.ts ``` ### Tất cả tệp nguồn (4 tệp) #### 1. **apps/api/src/modules/mcp/index.ts** - **Loại**: Điểm vào của module (xuất khẩu) - **Mục đích**: Xuất khẩu McpIntegrationModule - **Xuất khẩu**: `{ McpIntegrationModule }` #### 2. **apps/api/src/modules/mcp/mcp.module.ts** - **Loại**: Cấu hình NestJS Module (22 dòng) - **Lớp chính**: `McpIntegrationModule implements OnModuleInit` - **Trách nhiệm**: - Thiết lập module MCP cốt lõi với cấu hình - Khởi tạo TypesenseClient cho registry MCP - Ghi log tên các server đã khởi tạo khi module được khởi động - **Phụ thuộc được tiêm**: - `TypesenseClientService` (từ SearchModule) - `McpRegistryService` (từ @goodgo/mcp-servers) - `LoggerService` (từ SharedModule) - **Nhập khẩu**: - `SearchModule` - `AuthModule` - `McpCoreModule.forRoot()` với cấu hình - **Controllers**: McpTransportController - **Vòng đời**: Triển khai `onModuleInit()` #### 3. **apps/api/src/modules/mcp/presentation/mcp-transport.controller.ts** - **Loại**: NestJS Controller (102 dòng) - **Lớp chính**: `McpTransportController` - **Trách nhiệm**: Lớp truyền tải HTTP cho các kết nối MCP SSE - **Decorator được áp dụng**: - `@ApiTags('mcp')` - `@ApiBearerAuth('JWT')` - `@Controller('mcp')` - `@UseGuards(JwtAuthGuard)` - bảo vệ tất cả các endpoint - **Thuộc tính**: - `transports: Map` - quản lý phiên hoạt động - **Phụ thuộc được tiêm**: - `registry: McpRegistryService` **Các endpoint:** 1. **GET /mcp/servers** (dòng 27-34) - Tóm tắt: Liệt kê các MCP server khả dụng - Throttle: 30 yêu cầu mỗi 60 giây - Trả về: `{ servers: string[] }` - Trạng thái: 200 (thành công), 401 (không được phép) 2. **GET /mcp/:serverName/sse** (dòng 36-68) - Tóm tắt: Mở kết nối SSE đến MCP server - Throttle: 5 yêu cầu mỗi 60 giây (chặt chẽ hơn) - Tham số: `serverName` - Trả về: Luồng SSE - Xử lý phản hồi: - Tạo instance `SSEServerTransport` - Lưu vào map với `sessionId` - Kết nối đến server qua `server.connect(transport)` - Dọn dẹp khi yêu cầu đóng - Trạng thái: 200 (luồng), 404 (không tìm thấy server), 401 (không được phép) 3. **POST /mcp/:serverName/messages** (dòng 70-102) - Tóm tắt: Gửi tin nhắn đến phiên MCP server - Throttle: 30 yêu cầu mỗi 60 giây - Tham số: `serverName` - Query: `sessionId` (bắt buộc) - Body: Dữ liệu tin nhắn được truyền đến transport - Xử lý phản hồi: - Xác thực sessionId tồn tại - Ủy quyền cho `transport.handlePostMessage(req, res)` - Trạng thái: 200 (thành công), 400 (thiếu sessionId), 404 (không tìm thấy phiên), 401 (không được phép) --- ## 2. CÁC TỆP TEST ### Tổng số tệp test: 1 #### **apps/api/src/modules/mcp/presentation/__tests__/mcp-transport.controller.spec.ts** (174 dòng) - **Framework kiểm thử**: Vitest - **Đối tượng kiểm thử**: `McpTransportController` - **Cấu trúc test**: Các khối Describe + thiết lập beforeEach **Bộ test (4 khối describe):** 1. **security decorators** (4 test) - Xác minh JwtAuthGuard được áp dụng - Kiểm tra metadata Throttle trên các endpoint - Xác nhận giới hạn throttle (30, 5, 30) 2. **listServers** (2 test) - Trả về danh sách server từ registry - Xử lý danh sách server rỗng 3. **handleSse** (3 test) - Ném NOT_FOUND khi server không tồn tại - Tạo transport và kết nối đến server - Dọn dẹp khi đóng kết nối (xóa transport) 4. **handleMessage** (2 test) - Ném BAD_REQUEST khi thiếu sessionId - Ném NOT_FOUND khi phiên không tồn tại **Các mẫu Mock được sử dụng:** - `vi.mock()` cho các module ngoài (SSEServerTransport) - `vi.fn()` cho mock phương thức service - `mockReturnValue()`, `mockResolvedValue()`, `mockRejectValue()` - Đối tượng mock thủ công cho request/response - Reflection API để kiểm tra metadata (`Reflect.getMetadata()`) --- ## 3. CẤU TRÚC LỚP DDD **Trạng thái hiện tại**: Module MCP có **cấu trúc ĐƠN GIẢN HÓA** (chưa áp dụng DDD đầy đủ): ### Những gì ĐÃ TỒN TẠI: - **Lớp Presentation** ✅ - `presentation/mcp-transport.controller.ts` - `presentation/__tests__/mcp-transport.controller.spec.ts` - Định nghĩa HTTP endpoint - Decorator Guard (xác thực) - Decorator Throttle - Decorator Swagger ### Những gì CHƯA TỒN TẠI: - **Lớp Domain** ❌ - Không có thư mục `domain/` - Không có entity, value object hay domain event - Không có logic nghiệp vụ domain - **Lớp Application** ❌ - Không có thư mục `application/` - Không có command/handler (chưa dùng mẫu CQRS) - Không có query - Không có DTO - **Lớp Infrastructure** ❌ - Không có thư mục `infrastructure/` - Không có repository - Không có adapter dịch vụ ngoài - Không có truy cập cơ sở dữ liệu ### Ghi chú kiến trúc: - Module MCP đóng vai trò là một **lớp tích hợp bọc ngoài** - Ủy quyền cho thư viện `@goodgo/mcp-servers` (phụ thuộc ngoài) - Cách tiếp cận **controller chỉ có presentation** đơn giản - Tập trung vào cơ chế truyền tải HTTP (kết nối SSE) - Quản lý phiên qua Map trong bộ nhớ --- ## 4. CÁC LỚP/HANDLER CHÍNH CẦN ĐƯỢC KIỂM THỬ ### Triển khai hiện tại: 1. **McpIntegrationModule** (mcp.module.ts) - **Trạng thái**: Được kiểm thử một phần (logic khởi tạo) - **Các phương thức cần kiểm thử**: - `constructor()` - tiêm phụ thuộc - `onModuleInit()` - luồng khởi tạo - **Trọng tâm kiểm thử**: Thiết lập module, tích hợp service, ghi log 2. **McpTransportController** (mcp-transport.controller.ts) - **Trạng thái**: Được kiểm thử tốt ✅ (174 dòng test) - **Các phương thức được kiểm thử**: - `listServers()` - trả về các server khả dụng - `handleSse()` - thiết lập kết nối SSE - `handleMessage()` - định tuyến tin nhắn đến phiên - **Độ phủ kiểm thử**: Happy path + trường hợp lỗi + decorator bảo mật --- ## 5. CÁC MẪU KIỂM THỬ TỪ CÁC MODULE KHÁC ### Mẫu 1: Module Auth - Kiểm thử Handler (Đơn giản) **Tệp**: `apps/api/src/modules/auth/application/__tests__/login-user.handler.spec.ts` ```typescript // CÁC MẪU CHÍNH: // 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', }); }); }); ``` **Điểm chú ý chính**: - ✅ Thiết lập tối giản, khởi tạo handler trực tiếp - ✅ Đối tượng Command làm đầu vào kiểm thử - ✅ Mock đơn giản với đối tượng có kiểu - ✅ Tập trung vào xác minh hành vi - ✅ Dùng `as any` để ép kiểu ### Mẫu 2: Module Payments - Kiểm thử Handler phức tạp (Nâng cao) **Tệp**: `apps/api/src/modules/payments/application/__tests__/create-payment.handler.spec.ts` ```typescript // CÁC MẪU CHÍNH: // 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/); }); }); ``` **Điểm chú ý chính**: - ✅ Mock nhiều phụ thuộc phức tạp - ✅ Kiểu ánh xạ cho mock repository `[K in keyof IPaymentRepository]` - ✅ Mock từng phương thức riêng lẻ cho mỗi phụ thuộc - ✅ Kiểm thử quy tắc nghiệp vụ (idempotency) - ✅ Kiểm thử trường hợp lỗi với khớp regex - ✅ Bao gồm mock logger - ✅ Xác minh nhiều lần gọi và tương tác ### Mẫu 3: Kiểm thử Entity Domain (Phong cách DDD) **Tệp**: `apps/api/src/modules/payments/domain/__tests__/payment.entity.spec.ts` ```typescript // CÁC MẪU CHÍNH: // 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'); }); }); ``` **Điểm chú ý chính**: - ✅ Import vitest tường minh (tường minh hơn ngầm định) - ✅ Phương thức factory trợ giúp cho thiết lập phức tạp - ✅ Kiểm thử hành vi entity (chuyển đổi trạng thái) - ✅ Kiểm thử domain event - ✅ Kiểm thử xử lý lỗi (mẫu Result) - ✅ Không mock - kiểm thử logic entity thực tế - ✅ Sử dụng value object (Money.create().unwrap()) - ✅ Xác minh domain event ### Mẫu 4: Kiểm thử Service Infrastructure (Crypto/Bên ngoài) **Tệp**: `apps/api/src/modules/payments/infrastructure/__tests__/zalopay.service.spec.ts` ```typescript // CÁC MẪU CHÍNH: // 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); }); }); ``` **Điểm chú ý chính**: - ✅ Import vitest tường minh (import from 'vitest') - ✅ Kiểm thử chức năng quan trọng về bảo mật (xác minh HMAC) - ✅ Mock ConfigService cho biến môi trường - ✅ Hàm trợ giúp để tạo dữ liệu kiểm thử phức tạp - ✅ Kiểm thử các điều kiện lỗi (JSON không hợp lệ, MAC sai) - ✅ Kiểm thử các trường hợp biên về bảo mật (dữ liệu bị giả mạo) - ✅ Sử dụng module crypto của Node.js trực tiếp - ✅ Kiểm thử thuật toán thực (SHA256 HMAC) --- ## 6. TÓM TẮT QUY ƯỚC KIỂM THỬ ### Framework & Thiết lập - **Framework**: Vitest - **Môi trường**: Node.js - **Globals**: Được bật (`globals: true` trong cấu hình) - **Mẫu tên tệp**: `*.spec.ts` (không phải `.test.ts`) - **Tổ chức test**: Thư mục con `__tests__/` ### Phong cách Import ```typescript // Phong cách 1: Dùng globals (trong các test đơn giản) describe('MyClass', () => { it('does something', () => { expect(true).toBe(true); }); }); // Phong cách 2: Import tường minh (trong các test phức tạp/domain) import { describe, it, expect, beforeEach, vi } from 'vitest'; ``` ### Các mẫu Mock 1. **Mock Service** ```typescript mockService = { method: vi.fn().mockResolvedValue(value), method2: vi.fn().mockReturnValue(value), }; ``` 2. **Mock Module** ```typescript vi.mock('@goodgo/mcp-servers', () => ({ SSEServerTransport: class MockSSE { /* ... */ }, })); ``` 3. **Mock Cấu hình** ```typescript const mockConfig = { get: vi.fn((key) => env[key]), getOrThrow: vi.fn((key) => env[key] || throw), }; ``` ### Cấu trúc Test ```typescript describe('ClassName', () => { let instance: ClassName; let mockDep1: Mock; let mockDep2: Mock; beforeEach(() => { // Thiết lập mock mockDep1 = { method: vi.fn() }; // Khởi tạo với mock 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'); }); }); }); ``` ### Các assertion thường dùng - `expect(x).toBe(value)` - so sánh bằng nghiêm ngặt - `expect(x).toEqual(object)` - so sánh bằng sâu - `expect(x).toHaveLength(n)` - độ dài mảng/chuỗi - `expect(x).toBeInstanceOf(Class)` - kiểm tra instance - `expect(fn).toHaveBeenCalled()` - xác minh đã được gọi - `expect(fn).toHaveBeenCalledWith(args)` - tham số khi gọi - `expect(fn).toHaveBeenCalledTimes(n)` - số lần gọi - `expect(promise).rejects.toThrow(msg)` - kỳ vọng lỗi ### Các tính năng đặc trưng của Vitest được dùng - `vi.fn()` - tạo hàm mock - `vi.mock()` - mock module - `mockResolvedValue()` - giá trị trả về bất đồng bộ - `mockReturnValue()` - giá trị trả về đồng bộ - `mockRejectedValue()` - mô phỏng lỗi - `Reflect.getMetadata()` - kiểm tra decorator (NestJS) --- ## 7. KHUYẾN NGHỊ CHO KIỂM THỬ MODULE MCP ### Trạng thái độ phủ hiện tại - ✅ Controller (lớp presentation): Được kiểm thử tốt - ⚠️ Khởi tạo module: Cơ bản (được mock) - ❌ Lớp Domain: Chưa được triển khai - ❌ Lớp Application: Chưa được triển khai - ❌ Lớp Infrastructure: Chưa được triển khai ### Các bước tiếp theo được khuyến nghị 1. **Mở rộng kiểm thử Controller** - Kiểm thử tích hợp với kết nối SSE được mock - Kiểm thử vòng đời phiên (mở → tin nhắn → đóng) - Kiểm thử phục hồi lỗi 2. **Thêm kiểm thử Module** - Kiểm thử `McpIntegrationModule.onModuleInit()` - Kiểm thử khởi tạo registry - Kiểm thử tiêm service 3. **Cân nhắc thêm kiểm thử Domain** (nếu có logic nghiệp vụ) - Kiểm thử entity phiên - Quản lý trạng thái kết nối - Xử lý sự kiện ### Lệnh kiểm thử ```bash # Chạy tất cả test MCP pnpm test -- src/modules/mcp # Chạy với coverage pnpm test -- --coverage src/modules/mcp # Chế độ watch pnpm test -- --watch src/modules/mcp ```