Files
goodgo-platform/docs/audits/MCP_MODULE_EXPLORATION.md
Ho Ngoc Hai 642b593884 docs: move remaining analysis docs to docs/audits/
Move MCP module exploration, quick reference, and inquiries exploration
documents to the centralized audit directory.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-11 01:38:14 +07:00

20 KiB

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<string, SSEServerTransport> - 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

// 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<typeof vi.fn> };

  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

// 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<typeof vi.fn> };
  let mockGatewayFactory: { getGateway: ReturnType<typeof vi.fn> };
  let mockGateway: { 
    createPaymentUrl: ReturnType<typeof vi.fn>;
    verifyCallback: ReturnType<typeof vi.fn>;
    refund: ReturnType<typeof vi.fn>;
  };
  let mockEventBus: { publish: ReturnType<typeof vi.fn> };

  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

// 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

// 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<string, string> = {
          '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<string, unknown> = {},
    tamperMac = false,
  ): Record<string, string> {
    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

// 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

    mockService = {
      method: vi.fn().mockResolvedValue(value),
      method2: vi.fn().mockReturnValue(value),
    };
    
  2. Module Mocks

    vi.mock('@goodgo/mcp-servers', () => ({
      SSEServerTransport: class MockSSE { /* ... */ },
    }));
    
  3. Configuration Mocks

    const mockConfig = {
      get: vi.fn((key) => env[key]),
      getOrThrow: vi.fn((key) => env[key] || throw),
    };
    

Test Structure

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
  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

# 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