extension MCP Curriculum
translate KO / EN
code Module 08

Best Practices

MCP Development Best Practices

_(Click the image above to view video of this lesson)_

Overview

This lesson focuses on advanced best practices for developing, testing, and deploying MCP servers and features in production environments.

As MCP ecosystems grow in complexity and importance, following established patterns ensures reliability, maintainability, and interoperability.

This lesson consolidates practical wisdom gained from real-world MCP implementations to guide you in creating robust, efficient servers with effective resources, prompts, and tools.

Learning Objectives

By the end of this lesson, you will be able to:

  • Apply industry best practices in MCP server and feature design
  • Create comprehensive testing strategies for MCP servers
  • Design efficient, reusable workflow patterns for complex MCP applications
  • Implement proper error handling, logging, and observability in MCP servers
  • Optimize MCP implementations for performance, security, and maintainability
  • MCP Core Principles

    Before diving into specific implementation practices, it's important to understand the core principles that guide effective MCP development:

    1. Standardized Communication: MCP uses JSON-RPC 2.0 as its foundation, providing a consistent format for requests, responses, and error handling across all implementations.

    2. User-Centric Design: Always prioritize user consent, control, and transparency in your MCP implementations.

    3. Security First: Implement robust security measures including authentication, authorization, validation, and rate limiting.

    4. Modular Architecture: Design your MCP servers with a modular approach, where each tool and resource has a clear, focused purpose.

    5. Stateful Connections: Leverage MCP's ability to maintain state across multiple requests for more coherent and context-aware interactions.

    Official MCP Best Practices

    The following best practices are derived from the official Model Context Protocol documentation:

    Security Best Practices

    1. User Consent and Control: Always require explicit user consent before accessing data or performing operations. Provide clear control over what data is shared and which actions are authorized.

    2. Data Privacy: Only expose user data with explicit consent and protect it with appropriate access controls. Safeguard against unauthorized data transmission.

    3. Tool Safety: Require explicit user consent before invoking any tool. Ensure users understand each tool's functionality and enforce robust security boundaries.

    4. Tool Permission Control: Configure which tools a model is allowed to use during a session, ensuring only explicitly authorized tools are accessible.

    5. Authentication: Require proper authentication before granting access to tools, resources, or sensitive operations using API keys, OAuth tokens, or other secure authentication methods.

    6. Parameter Validation: Enforce validation for all tool invocations to prevent malformed or malicious input from reaching tool implementations.

    7. Rate Limiting: Implement rate limiting to prevent abuse and ensure fair usage of server resources.

    Implementation Best Practices

    1. Capability Negotiation: During connection setup, exchange information about supported features, protocol versions, available tools, and resources.

    2. Tool Design: Create focused tools that do one thing well, rather than monolithic tools that handle multiple concerns.

    3. Error Handling: Implement standardized error messages and codes to help diagnose issues, handle failures gracefully, and provide actionable feedback.

    4. Logging: Configure structured logs for auditing, debugging, and monitoring protocol interactions.

    5. Progress Tracking: For long-running operations, report progress updates to enable responsive user interfaces.

    6. Request Cancellation: Allow clients to cancel in-flight requests that are no longer needed or taking too long.

    Additional References

    For the most up-to-date information on MCP best practices, refer to:

  • MCP Documentation
  • MCP Specification (2025-11-25)
  • GitHub Repository
  • Security Best Practices
  • OWASP MCP Top 10 - Security risks and mitigations
  • MCP Security Summit Workshop (Sherpa) - Hands-on security training
  • Practical Implementation Examples

    Tool Design Best Practices

    1. Single Responsibility Principle

    Each MCP tool should have a clear, focused purpose. Rather than creating monolithic tools that attempt to handle multiple concerns, develop specialized tools that excel at specific tasks.

    
    // A focused tool that does one thing well
    
    public class WeatherForecastTool : ITool
    
    {
    
        private readonly IWeatherService _weatherService;
    
        
    
        public WeatherForecastTool(IWeatherService weatherService)
    
        {
    
            _weatherService = weatherService;
    
        }
    
        
    
        public string Name => "weatherForecast";
    
        public string Description => "Gets weather forecast for a specific location";
    
        
    
        public ToolDefinition GetDefinition()
    
        {
    
            return new ToolDefinition
    
            {
    
                Name = Name,
    
                Description = Description,
    
                Parameters = new Dictionary<string, ParameterDefinition>
    
                {
    
                    ["location"] = new ParameterDefinition
    
                    {
    
                        Type = ParameterType.String,
    
                        Description = "City or location name"
    
                    },
    
                    ["days"] = new ParameterDefinition
    
                    {
    
                        Type = ParameterType.Integer,
    
                        Description = "Number of forecast days",
    
                        Default = 3
    
                    }
    
                },
    
                Required = new[] { "location" }
    
            };
    
        }
    
        
    
        public async Task<ToolResponse> ExecuteAsync(IDictionary<string, object> parameters)
    
        {
    
            var location = parameters["location"].ToString();
    
            var days = parameters.ContainsKey("days") 
    
                ? Convert.ToInt32(parameters["days"]) 
    
                : 3;
    
                
    
            var forecast = await _weatherService.GetForecastAsync(location, days);
    
            
    
            return new ToolResponse
    
            {
    
                Content = new List<ContentItem>
    
                {
    
                    new TextContent(JsonSerializer.Serialize(forecast))
    
                }
    
            };
    
        }
    
    }
    
    
    2. Consistent Error Handling

    Implement robust error handling with informative error messages and appropriate recovery mechanisms.

    
    # Python example with comprehensive error handling
    
    class DataQueryTool:
    
        def get_name(self):
    
            return "dataQuery"
    
            
    
        def get_description(self):
    
            return "Queries data from specified database tables"
    
        
    
        async def execute(self, parameters):
    
            try:
    
                # Parameter validation
    
                if "query" not in parameters:
    
                    raise ToolParameterError("Missing required parameter: query")
    
                    
    
                query = parameters["query"]
    
                
    
                # Security validation
    
                if self._contains_unsafe_sql(query):
    
                    raise ToolSecurityError("Query contains potentially unsafe SQL")
    
                
    
                try:
    
                    # Database operation with timeout
    
                    async with timeout(10):  # 10 second timeout
    
                        result = await self._database.execute_query(query)
    
                        
    
                    return ToolResponse(
    
                        content=[TextContent(json.dumps(result))]
    
                    )
    
                except asyncio.TimeoutError:
    
                    raise ToolExecutionError("Database query timed out after 10 seconds")
    
                except DatabaseConnectionError as e:
    
                    # Connection errors might be transient
    
                    self._log_error("Database connection error", e)
    
                    raise ToolExecutionError(f"Database connection error: {str(e)}")
    
                except DatabaseQueryError as e:
    
                    # Query errors are likely client errors
    
                    self._log_error("Database query error", e)
    
                    raise ToolExecutionError(f"Invalid query: {str(e)}")
    
                    
    
            except ToolError:
    
                # Let tool-specific errors pass through
    
                raise
    
            except Exception as e:
    
                # Catch-all for unexpected errors
    
                self._log_error("Unexpected error in DataQueryTool", e)
    
                raise ToolExecutionError(f"An unexpected error occurred: {str(e)}")
    
        
    
        def _contains_unsafe_sql(self, query):
    
            # Implementation of SQL injection detection
    
            pass
    
            
    
        def _log_error(self, message, error):
    
            # Implementation of error logging
    
            pass
    
    
    3. Parameter Validation

    Always validate parameters thoroughly to prevent malformed or malicious input.

    
    // JavaScript/TypeScript example with detailed parameter validation
    
    class FileOperationTool {
    
      getName() {
    
        return "fileOperation";
    
      }
    
      
    
      getDescription() {
    
        return "Performs file operations like read, write, and delete";
    
      }
    
      
    
      getDefinition() {
    
        return {
    
          name: this.getName(),
    
          description: this.getDescription(),
    
          parameters: {
    
            operation: {
    
              type: "string",
    
              description: "Operation to perform",
    
              enum: ["read", "write", "delete"]
    
            },
    
            path: {
    
              type: "string",
    
              description: "File path (must be within allowed directories)"
    
            },
    
            content: {
    
              type: "string",
    
              description: "Content to write (only for write operation)",
    
              optional: true
    
            }
    
          },
    
          required: ["operation", "path"]
    
        };
    
      }
    
      
    
      async execute(parameters) {
    
        // 1. Validate parameter presence
    
        if (!parameters.operation) {
    
          throw new ToolError("Missing required parameter: operation");
    
        }
    
        
    
        if (!parameters.path) {
    
          throw new ToolError("Missing required parameter: path");
    
        }
    
        
    
        // 2. Validate parameter types
    
        if (typeof parameters.operation !== "string") {
    
          throw new ToolError("Parameter 'operation' must be a string");
    
        }
    
        
    
        if (typeof parameters.path !== "string") {
    
          throw new ToolError("Parameter 'path' must be a string");
    
        }
    
        
    
        // 3. Validate parameter values
    
        const validOperations = ["read", "write", "delete"];
    
        if (!validOperations.includes(parameters.operation)) {
    
          throw new ToolError(`Invalid operation. Must be one of: ${validOperations.join(", ")}`);
    
        }
    
        
    
        // 4. Validate content presence for write operation
    
        if (parameters.operation === "write" && !parameters.content) {
    
          throw new ToolError("Content parameter is required for write operation");
    
        }
    
        
    
        // 5. Path safety validation
    
        if (!this.isPathWithinAllowedDirectories(parameters.path)) {
    
          throw new ToolError("Access denied: path is outside of allowed directories");
    
        }
    
        
    
        // Implementation based on validated parameters
    
        // ...
    
      }
    
      
    
      isPathWithinAllowedDirectories(path) {
    
        // Implementation of path safety check
    
        // ...
    
      }
    
    }
    
    

    Security Implementation Examples

    1. Authentication and Authorization
    
    // Java example with authentication and authorization
    
    public class SecureDataAccessTool implements Tool {
    
        private final AuthenticationService authService;
    
        private final AuthorizationService authzService;
    
        private final DataService dataService;
    
        
    
        // Dependency injection
    
        public SecureDataAccessTool(
    
                AuthenticationService authService,
    
                AuthorizationService authzService,
    
                DataService dataService) {
    
            this.authService = authService;
    
            this.authzService = authzService;
    
            this.dataService = dataService;
    
        }
    
        
    
        @Override
    
        public String getName() {
    
            return "secureDataAccess";
    
        }
    
        
    
        @Override
    
        public ToolResponse execute(ToolRequest request) {
    
            // 1. Extract authentication context
    
            String authToken = request.getContext().getAuthToken();
    
            
    
            // 2. Authenticate user
    
            UserIdentity user;
    
            try {
    
                user = authService.validateToken(authToken);
    
            } catch (AuthenticationException e) {
    
                return ToolResponse.error("Authentication failed: " + e.getMessage());
    
            }
    
            
    
            // 3. Check authorization for the specific operation
    
            String dataId = request.getParameters().get("dataId").getAsString();
    
            String operation = request.getParameters().get("operation").getAsString();
    
            
    
            boolean isAuthorized = authzService.isAuthorized(user, "data:" + dataId, operation);
    
            if (!isAuthorized) {
    
                return ToolResponse.error("Access denied: Insufficient permissions for this operation");
    
            }
    
            
    
            // 4. Proceed with authorized operation
    
            try {
    
                switch (operation) {
    
                    case "read":
    
                        Object data = dataService.getData(dataId, user.getId());
    
                        return ToolResponse.success(data);
    
                    case "update":
    
                        JsonNode newData = request.getParameters().get("newData");
    
                        dataService.updateData(dataId, newData, user.getId());
    
                        return ToolResponse.success("Data updated successfully");
    
                    default:
    
                        return ToolResponse.error("Unsupported operation: " + operation);
    
                }
    
            } catch (Exception e) {
    
                return ToolResponse.error("Operation failed: " + e.getMessage());
    
            }
    
        }
    
    }
    
    
    2. Rate Limiting
    
    // C# rate limiting implementation
    
    public class RateLimitingMiddleware
    
    {
    
        private readonly RequestDelegate _next;
    
        private readonly IMemoryCache _cache;
    
        private readonly ILogger<RateLimitingMiddleware> _logger;
    
        
    
        // Configuration options
    
        private readonly int _maxRequestsPerMinute;
    
        
    
        public RateLimitingMiddleware(
    
            RequestDelegate next,
    
            IMemoryCache cache,
    
            ILogger<RateLimitingMiddleware> logger,
    
            IConfiguration config)
    
        {
    
            _next = next;
    
            _cache = cache;
    
            _logger = logger;
    
            _maxRequestsPerMinute = config.GetValue<int>("RateLimit:MaxRequestsPerMinute", 60);
    
        }
    
        
    
        public async Task InvokeAsync(HttpContext context)
    
        {
    
            // 1. Get client identifier (API key or user ID)
    
            string clientId = GetClientIdentifier(context);
    
            
    
            // 2. Get rate limiting key for this minute
    
            string cacheKey = $"rate_limit:{clientId}:{DateTime.UtcNow:yyyyMMddHHmm}";
    
            
    
            // 3. Check current request count
    
            if (!_cache.TryGetValue(cacheKey, out int requestCount))
    
            {
    
                requestCount = 0;
    
            }
    
            
    
            // 4. Enforce rate limit
    
            if (requestCount >= _maxRequestsPerMinute)
    
            {
    
                _logger.LogWarning("Rate limit exceeded for client {ClientId}", clientId);
    
                
    
                context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
    
                context.Response.Headers.Add("Retry-After", "60");
    
                
    
                await context.Response.WriteAsJsonAsync(new
    
                {
    
                    error = "Rate limit exceeded",
    
                    message = "Too many requests. Please try again later.",
    
                    retryAfterSeconds = 60
    
                });
    
                
    
                return;
    
            }
    
            
    
            // 5. Increment request count
    
            _cache.Set(cacheKey, requestCount + 1, TimeSpan.FromMinutes(2));
    
            
    
            // 6. Add rate limit headers
    
            context.Response.Headers.Add("X-RateLimit-Limit", _maxRequestsPerMinute.ToString());
    
            context.Response.Headers.Add("X-RateLimit-Remaining", (_maxRequestsPerMinute - requestCount - 1).ToString());
    
            
    
            // 7. Continue with the request
    
            await _next(context);
    
        }
    
        
    
        private string GetClientIdentifier(HttpContext context)
    
        {
    
            // Implementation to extract API key or user ID
    
            // ...
    
        }
    
    }
    
    

    Testing Best Practices

    1. Unit Testing MCP Tools

    Always test your tools in isolation, mocking external dependencies:

    
    // TypeScript example of a tool unit test
    
    describe('WeatherForecastTool', () => {
    
      let tool: WeatherForecastTool;
    
      let mockWeatherService: jest.Mocked<IWeatherService>;
    
      
    
      beforeEach(() => {
    
        // Create a mock weather service
    
        mockWeatherService = {
    
          getForecasts: jest.fn()
    
        } as any;
    
        
    
        // Create the tool with the mock dependency
    
        tool = new WeatherForecastTool(mockWeatherService);
    
      });
    
      
    
      it('should return weather forecast for a location', async () => {
    
        // Arrange
    
        const mockForecast = {
    
          location: 'Seattle',
    
          forecasts: [
    
            { date: '2025-07-16', temperature: 72, conditions: 'Sunny' },
    
            { date: '2025-07-17', temperature: 68, conditions: 'Partly Cloudy' },
    
            { date: '2025-07-18', temperature: 65, conditions: 'Rain' }
    
          ]
    
        };
    
        
    
        mockWeatherService.getForecasts.mockResolvedValue(mockForecast);
    
        
    
        // Act
    
        const response = await tool.execute({
    
          location: 'Seattle',
    
          days: 3
    
        });
    
        
    
        // Assert
    
        expect(mockWeatherService.getForecasts).toHaveBeenCalledWith('Seattle', 3);
    
        expect(response.content[0].text).toContain('Seattle');
    
        expect(response.content[0].text).toContain('Sunny');
    
      });
    
      
    
      it('should handle errors from the weather service', async () => {
    
        // Arrange
    
        mockWeatherService.getForecasts.mockRejectedValue(new Error('Service unavailable'));
    
        
    
        // Act & Assert
    
        await expect(tool.execute({
    
          location: 'Seattle',
    
          days: 3
    
        })).rejects.toThrow('Weather service error: Service unavailable');
    
      });
    
    });
    
    

    2. Integration Testing

    Test the complete flow from client requests to server responses:

    
    # Python integration test example
    
    @pytest.mark.asyncio
    
    async def test_mcp_server_integration():
    
        # Start a test server
    
        server = McpServer()
    
        server.register_tool(WeatherForecastTool(MockWeatherService()))
    
        await server.start(port=5000)
    
        
    
        try:
    
            # Create a client
    
            client = McpClient("http://localhost:5000")
    
            
    
            # Test tool discovery
    
            tools = await client.discover_tools()
    
            assert "weatherForecast" in [t.name for t in tools]
    
            
    
            # Test tool execution
    
            response = await client.execute_tool("weatherForecast", {
    
                "location": "Seattle",
    
                "days": 3
    
            })
    
            
    
            # Verify response
    
            assert response.status_code == 200
    
            assert "Seattle" in response.content[0].text
    
            assert len(json.loads(response.content[0].text)["forecasts"]) == 3
    
            
    
        finally:
    
            # Clean up
    
            await server.stop()
    
    

    Performance Optimization

    1. Caching Strategies

    Implement appropriate caching to reduce latency and resource usage:

    
    // C# example with caching
    
    public class CachedWeatherTool : ITool
    
    {
    
        private readonly IWeatherService _weatherService;
    
        private readonly IDistributedCache _cache;
    
        private readonly ILogger<CachedWeatherTool> _logger;
    
        
    
        public CachedWeatherTool(
    
            IWeatherService weatherService,
    
            IDistributedCache cache,
    
            ILogger<CachedWeatherTool> logger)
    
        {
    
            _weatherService = weatherService;
    
            _cache = cache;
    
            _logger = logger;
    
        }
    
        
    
        public string Name => "weatherForecast";
    
        
    
        public async Task<ToolResponse> ExecuteAsync(IDictionary<string, object> parameters)
    
        {
    
            var location = parameters["location"].ToString();
    
            var days = Convert.ToInt32(parameters.GetValueOrDefault("days", 3));
    
            
    
            // Create cache key
    
            string cacheKey = $"weather:{location}:{days}";
    
            
    
            // Try to get from cache
    
            string cachedForecast = await _cache.GetStringAsync(cacheKey);
    
            if (!string.IsNullOrEmpty(cachedForecast))
    
            {
    
                _logger.LogInformation("Cache hit for weather forecast: {Location}", location);
    
                return new ToolResponse
    
                {
    
                    Content = new List<ContentItem>
    
                    {
    
                        new TextContent(cachedForecast)
    
                    }
    
                };
    
            }
    
            
    
            // Cache miss - get from service
    
            _logger.LogInformation("Cache miss for weather forecast: {Location}", location);
    
            var forecast = await _weatherService.GetForecastAsync(location, days);
    
            string forecastJson = JsonSerializer.Serialize(forecast);
    
            
    
            // Store in cache (weather forecasts valid for 1 hour)
    
            await _cache.SetStringAsync(
    
                cacheKey,
    
                forecastJson,
    
                new DistributedCacheEntryOptions
    
                {
    
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
    
                });
    
            
    
            return new ToolResponse
    
            {
    
                Content = new List<ContentItem>
    
                {
    
                    new TextContent(forecastJson)
    
                }
    
            };
    
        }
    
    }
    
    
    2. Dependency Injection and Testability

    Design tools to receive their dependencies through constructor injection, making them testable and configurable:

    
    // Java example with dependency injection
    
    public class CurrencyConversionTool implements Tool {
    
        private final ExchangeRateService exchangeService;
    
        private final CacheService cacheService;
    
        private final Logger logger;
    
        
    
        // Dependencies injected through constructor
    
        public CurrencyConversionTool(
    
                ExchangeRateService exchangeService,
    
                CacheService cacheService,
    
                Logger logger) {
    
            this.exchangeService = exchangeService;
    
            this.cacheService = cacheService;
    
            this.logger = logger;
    
        }
    
        
    
        // Tool implementation
    
        // ...
    
    }
    
    
    3. Composable Tools

    Design tools that can be composed together to create more complex workflows:

    
    # Python example showing composable tools
    
    class DataFetchTool(Tool):
    
        def get_name(self):
    
            return "dataFetch"
    
        
    
        # Implementation...
    
    
    
    class DataAnalysisTool(Tool):
    
        def get_name(self):
    
            return "dataAnalysis"
    
        
    
        # This tool can use results from the dataFetch tool
    
        async def execute_async(self, request):
    
            # Implementation...
    
            pass
    
    
    
    class DataVisualizationTool(Tool):
    
        def get_name(self):
    
            return "dataVisualize"
    
        
    
        # This tool can use results from the dataAnalysis tool
    
        async def execute_async(self, request):
    
            # Implementation...
    
            pass
    
    
    
    # These tools can be used independently or as part of a workflow
    
    

    Schema Design Best Practices

    The schema is the contract between the model and your tool. Well-designed schemas lead to better tool usability.

    1. Clear Parameter Descriptions

    Always include descriptive information for each parameter:

    
    public object GetSchema()
    
    {
    
        return new {
    
            type = "object",
    
            properties = new {
    
                query = new { 
    
                    type = "string", 
    
                    description = "Search query text. Use precise keywords for better results." 
    
                },
    
                filters = new {
    
                    type = "object",
    
                    description = "Optional filters to narrow down search results",
    
                    properties = new {
    
                        dateRange = new { 
    
                            type = "string", 
    
                            description = "Date range in format YYYY-MM-DD:YYYY-MM-DD" 
    
                        },
    
                        category = new { 
    
                            type = "string", 
    
                            description = "Category name to filter by" 
    
                        }
    
                    }
    
                },
    
                limit = new { 
    
                    type = "integer", 
    
                    description = "Maximum number of results to return (1-50)",
    
                    default = 10
    
                }
    
            },
    
            required = new[] { "query" }
    
        };
    
    }
    
    
    2. Validation Constraints

    Include validation constraints to prevent invalid inputs:

    
    Map<String, Object> getSchema() {
    
        Map<String, Object> schema = new HashMap<>();
    
        schema.put("type", "object");
    
        
    
        Map<String, Object> properties = new HashMap<>();
    
        
    
        // Email property with format validation
    
        Map<String, Object> email = new HashMap<>();
    
        email.put("type", "string");
    
        email.put("format", "email");
    
        email.put("description", "User email address");
    
        
    
        // Age property with numeric constraints
    
        Map<String, Object> age = new HashMap<>();
    
        age.put("type", "integer");
    
        age.put("minimum", 13);
    
        age.put("maximum", 120);
    
        age.put("description", "User age in years");
    
        
    
        // Enumerated property
    
        Map<String, Object> subscription = new HashMap<>();
    
        subscription.put("type", "string");
    
        subscription.put("enum", Arrays.asList("free", "basic", "premium"));
    
        subscription.put("default", "free");
    
        subscription.put("description", "Subscription tier");
    
        
    
        properties.put("email", email);
    
        properties.put("age", age);
    
        properties.put("subscription", subscription);
    
        
    
        schema.put("properties", properties);
    
        schema.put("required", Arrays.asList("email"));
    
        
    
        return schema;
    
    }
    
    
    3. Consistent Return Structures

    Maintain consistency in your response structures to make it easier for models to interpret results:

    
    async def execute_async(self, request):
    
        try:
    
            # Process request
    
            results = await self._search_database(request.parameters["query"])
    
            
    
            # Always return a consistent structure
    
            return ToolResponse(
    
                result={
    
                    "matches": [self._format_item(item) for item in results],
    
                    "totalCount": len(results),
    
                    "queryTime": calculation_time_ms,
    
                    "status": "success"
    
                }
    
            )
    
        except Exception as e:
    
            return ToolResponse(
    
                result={
    
                    "matches": [],
    
                    "totalCount": 0,
    
                    "queryTime": 0,
    
                    "status": "error",
    
                    "error": str(e)
    
                }
    
            )
    
        
    
    def _format_item(self, item):
    
        """Ensures each item has a consistent structure"""
    
        return {
    
            "id": item.id,
    
            "title": item.title,
    
            "summary": item.summary[:100] + "..." if len(item.summary) > 100 else item.summary,
    
            "url": item.url,
    
            "relevance": item.score
    
        }
    
    

    Error Handling

    Robust error handling is crucial for MCP tools to maintain reliability.

    1. Graceful Error Handling

    Handle errors at appropriate levels and provide informative messages:

    
    public async Task<ToolResponse> ExecuteAsync(ToolRequest request)
    
    {
    
        try
    
        {
    
            string fileId = request.Parameters.GetProperty("fileId").GetString();
    
            
    
            try
    
            {
    
                var fileData = await _fileService.GetFileAsync(fileId);
    
                return new ToolResponse { 
    
                    Result = JsonSerializer.SerializeToElement(fileData) 
    
                };
    
            }
    
            catch (FileNotFoundException)
    
            {
    
                throw new ToolExecutionException($"File not found: {fileId}");
    
            }
    
            catch (UnauthorizedAccessException)
    
            {
    
                throw new ToolExecutionException("You don't have permission to access this file");
    
            }
    
            catch (Exception ex) when (ex is IOException || ex is TimeoutException)
    
            {
    
                _logger.LogError(ex, "Error accessing file {FileId}", fileId);
    
                throw new ToolExecutionException("Error accessing file: The service is temporarily unavailable");
    
            }
    
        }
    
        catch (JsonException)
    
        {
    
            throw new ToolExecutionException("Invalid file ID format");
    
        }
    
        catch (Exception ex)
    
        {
    
            _logger.LogError(ex, "Unexpected error in FileAccessTool");
    
            throw new ToolExecutionException("An unexpected error occurred");
    
        }
    
    }
    
    
    2. Structured Error Responses

    Return structured error information when possible:

    
    @Override
    
    public ToolResponse execute(ToolRequest request) {
    
        try {
    
            // Implementation
    
        } catch (Exception ex) {
    
            Map<String, Object> errorResult = new HashMap<>();
    
            
    
            errorResult.put("success", false);
    
            
    
            if (ex instanceof ValidationException) {
    
                ValidationException validationEx = (ValidationException) ex;
    
                
    
                errorResult.put("errorType", "validation");
    
                errorResult.put("errorMessage", validationEx.getMessage());
    
                errorResult.put("validationErrors", validationEx.getErrors());
    
                
    
                return new ToolResponse.Builder()
    
                    .setResult(errorResult)
    
                    .build();
    
            }
    
            
    
            // Re-throw other exceptions as ToolExecutionException
    
            throw new ToolExecutionException("Tool execution failed: " + ex.getMessage(), ex);
    
        }
    
    }
    
    
    3. Retry Logic

    Implement appropriate retry logic for transient failures:

    
    async def execute_async(self, request):
    
        max_retries = 3
    
        retry_count = 0
    
        base_delay = 1  # seconds
    
        
    
        while retry_count < max_retries:
    
            try:
    
                # Call external API
    
                return await self._call_api(request.parameters)
    
            except TransientError as e:
    
                retry_count += 1
    
                if retry_count >= max_retries:
    
                    raise ToolExecutionException(f"Operation failed after {max_retries} attempts: {str(e)}")
    
                    
    
                # Exponential backoff
    
                delay = base_delay * (2 ** (retry_count - 1))
    
                logging.warning(f"Transient error, retrying in {delay}s: {str(e)}")
    
                await asyncio.sleep(delay)
    
            except Exception as e:
    
                # Non-transient error, don't retry
    
                raise ToolExecutionException(f"Operation failed: {str(e)}")
    
    

    Performance Optimization

    1. Caching

    Implement caching for expensive operations:

    
    public class CachedDataTool : IMcpTool
    
    {
    
        private readonly IDatabase _database;
    
        private readonly IMemoryCache _cache;
    
        
    
        public CachedDataTool(IDatabase database, IMemoryCache cache)
    
        {
    
            _database = database;
    
            _cache = cache;
    
        }
    
        
    
        public async Task<ToolResponse> ExecuteAsync(ToolRequest request)
    
        {
    
            var query = request.Parameters.GetProperty("query").GetString();
    
            
    
            // Create cache key based on parameters
    
            var cacheKey = $"data_query_{ComputeHash(query)}";
    
            
    
            // Try to get from cache first
    
            if (_cache.TryGetValue(cacheKey, out var cachedResult))
    
            {
    
                return new ToolResponse { Result = cachedResult };
    
            }
    
            
    
            // Cache miss - perform actual query
    
            var result = await _database.QueryAsync(query);
    
            
    
            // Store in cache with expiration
    
            var cacheOptions = new MemoryCacheEntryOptions()
    
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(15));
    
                
    
            _cache.Set(cacheKey, JsonSerializer.SerializeToElement(result), cacheOptions);
    
            
    
            return new ToolResponse { Result = JsonSerializer.SerializeToElement(result) };
    
        }
    
        
    
        private string ComputeHash(string input)
    
        {
    
            // Implementation to generate stable hash for cache key
    
        }
    
    }
    
    
    2. Asynchronous Processing

    Use asynchronous programming patterns for I/O-bound operations:

    
    public class AsyncDocumentProcessingTool implements Tool {
    
        private final DocumentService documentService;
    
        private final ExecutorService executorService;
    
        
    
        @Override
    
        public ToolResponse execute(ToolRequest request) {
    
            String documentId = request.getParameters().get("documentId").asText();
    
            
    
            // For long-running operations, return a processing ID immediately
    
            String processId = UUID.randomUUID().toString();
    
            
    
            // Start async processing
    
            CompletableFuture.runAsync(() -> {
    
                try {
    
                    // Perform long-running operation
    
                    documentService.processDocument(documentId);
    
                    
    
                    // Update status (would typically be stored in a database)
    
                    processStatusRepository.updateStatus(processId, "completed");
    
                } catch (Exception ex) {
    
                    processStatusRepository.updateStatus(processId, "failed", ex.getMessage());
    
                }
    
            }, executorService);
    
            
    
            // Return immediate response with process ID
    
            Map<String, Object> result = new HashMap<>();
    
            result.put("processId", processId);
    
            result.put("status", "processing");
    
            result.put("estimatedCompletionTime", ZonedDateTime.now().plusMinutes(5));
    
            
    
            return new ToolResponse.Builder().setResult(result).build();
    
        }
    
        
    
        // Companion status check tool
    
        public class ProcessStatusTool implements Tool {
    
            @Override
    
            public ToolResponse execute(ToolRequest request) {
    
                String processId = request.getParameters().get("processId").asText();
    
                ProcessStatus status = processStatusRepository.getStatus(processId);
    
                
    
                return new ToolResponse.Builder().setResult(status).build();
    
            }
    
        }
    
    }
    
    
    3. Resource Throttling

    Implement resource throttling to prevent overload:

    
    class ThrottledApiTool(Tool):
    
        def __init__(self):
    
            self.rate_limiter = TokenBucketRateLimiter(
    
                tokens_per_second=5,  # Allow 5 requests per second
    
                bucket_size=10        # Allow bursts up to 10 requests
    
            )
    
        
    
        async def execute_async(self, request):
    
            # Check if we can proceed or need to wait
    
            delay = self.rate_limiter.get_delay_time()
    
            
    
            if delay > 0:
    
                if delay > 2.0:  # If wait is too long
    
                    raise ToolExecutionException(
    
                        f"Rate limit exceeded. Please try again in {delay:.1f} seconds."
    
                    )
    
                else:
    
                    # Wait for the appropriate delay time
    
                    await asyncio.sleep(delay)
    
            
    
            # Consume a token and proceed with the request
    
            self.rate_limiter.consume()
    
            
    
            # Call API
    
            result = await self._call_api(request.parameters)
    
            return ToolResponse(result=result)
    
    
    
    class TokenBucketRateLimiter:
    
        def __init__(self, tokens_per_second, bucket_size):
    
            self.tokens_per_second = tokens_per_second
    
            self.bucket_size = bucket_size
    
            self.tokens = bucket_size
    
            self.last_refill = time.time()
    
            self.lock = asyncio.Lock()
    
        
    
        async def get_delay_time(self):
    
            async with self.lock:
    
                self._refill()
    
                if self.tokens >= 1:
    
                    return 0
    
                
    
                # Calculate time until next token available
    
                return (1 - self.tokens) / self.tokens_per_second
    
        
    
        async def consume(self):
    
            async with self.lock:
    
                self._refill()
    
                self.tokens -= 1
    
        
    
        def _refill(self):
    
            now = time.time()
    
            elapsed = now - self.last_refill
    
            
    
            # Add new tokens based on elapsed time
    
            new_tokens = elapsed * self.tokens_per_second
    
            self.tokens = min(self.bucket_size, self.tokens + new_tokens)
    
            self.last_refill = now
    
    

    Security Best Practices

    1. Input Validation

    Always validate input parameters thoroughly:

    
    public async Task<ToolResponse> ExecuteAsync(ToolRequest request)
    
    {
    
        // Validate parameters exist
    
        if (!request.Parameters.TryGetProperty("query", out var queryProp))
    
        {
    
            throw new ToolExecutionException("Missing required parameter: query");
    
        }
    
        
    
        // Validate correct type
    
        if (queryProp.ValueKind != JsonValueKind.String)
    
        {
    
            throw new ToolExecutionException("Query parameter must be a string");
    
        }
    
        
    
        var query = queryProp.GetString();
    
        
    
        // Validate string content
    
        if (string.IsNullOrWhiteSpace(query))
    
        {
    
            throw new ToolExecutionException("Query parameter cannot be empty");
    
        }
    
        
    
        if (query.Length > 500)
    
        {
    
            throw new ToolExecutionException("Query parameter exceeds maximum length of 500 characters");
    
        }
    
        
    
        // Check for SQL injection attacks if applicable
    
        if (ContainsSqlInjection(query))
    
        {
    
            throw new ToolExecutionException("Invalid query: contains potentially unsafe SQL");
    
        }
    
        
    
        // Proceed with execution
    
        // ...
    
    }
    
    
    2. Authorization Checks

    Implement proper authorization checks:

    
    @Override
    
    public ToolResponse execute(ToolRequest request) {
    
        // Get user context from request
    
        UserContext user = request.getContext().getUserContext();
    
        
    
        // Check if user has required permissions
    
        if (!authorizationService.hasPermission(user, "documents:read")) {
    
            throw new ToolExecutionException("User does not have permission to access documents");
    
        }
    
        
    
        // For specific resources, check access to that resource
    
        String documentId = request.getParameters().get("documentId").asText();
    
        if (!documentService.canUserAccess(user.getId(), documentId)) {
    
            throw new ToolExecutionException("Access denied to the requested document");
    
        }
    
        
    
        // Proceed with tool execution
    
        // ...
    
    }
    
    
    3. Sensitive Data Handling

    Handle sensitive data carefully:

    
    class SecureDataTool(Tool):
    
        def get_schema(self):
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "userId": {"type": "string"},
    
                    "includeSensitiveData": {"type": "boolean", "default": False}
    
                },
    
                "required": ["userId"]
    
            }
    
        
    
        async def execute_async(self, request):
    
            user_id = request.parameters["userId"]
    
            include_sensitive = request.parameters.get("includeSensitiveData", False)
    
            
    
            # Get user data
    
            user_data = await self.user_service.get_user_data(user_id)
    
            
    
            # Filter sensitive fields unless explicitly requested AND authorized
    
            if not include_sensitive or not self._is_authorized_for_sensitive_data(request):
    
                user_data = self._redact_sensitive_fields(user_data)
    
            
    
            return ToolResponse(result=user_data)
    
        
    
        def _is_authorized_for_sensitive_data(self, request):
    
            # Check authorization level in request context
    
            auth_level = request.context.get("authorizationLevel")
    
            return auth_level == "admin"
    
        
    
        def _redact_sensitive_fields(self, user_data):
    
            # Create a copy to avoid modifying the original
    
            redacted = user_data.copy()
    
            
    
            # Redact specific sensitive fields
    
            sensitive_fields = ["ssn", "creditCardNumber", "password"]
    
            for field in sensitive_fields:
    
                if field in redacted:
    
                    redacted[field] = "REDACTED"
    
            
    
            # Redact nested sensitive data
    
            if "financialInfo" in redacted:
    
                redacted["financialInfo"] = {"available": True, "accessRestricted": True}
    
            
    
            return redacted
    
    

    Testing Best Practices for MCP Tools

    Comprehensive testing ensures that MCP tools function correctly, handle edge cases, and integrate properly with the rest of the system.

    Unit Testing

    1. Test Each Tool in Isolation

    Create focused tests for each tool's functionality:

    
    [Fact]
    
    public async Task WeatherTool_ValidLocation_ReturnsCorrectForecast()
    
    {
    
        // Arrange
    
        var mockWeatherService = new Mock<IWeatherService>();
    
        mockWeatherService
    
            .Setup(s => s.GetForecastAsync("Seattle", 3))
    
            .ReturnsAsync(new WeatherForecast(/* test data */));
    
        
    
        var tool = new WeatherForecastTool(mockWeatherService.Object);
    
        
    
        var request = new ToolRequest(
    
            toolName: "weatherForecast",
    
            parameters: JsonSerializer.SerializeToElement(new { 
    
                location = "Seattle", 
    
                days = 3 
    
            })
    
        );
    
        
    
        // Act
    
        var response = await tool.ExecuteAsync(request);
    
        
    
        // Assert
    
        Assert.NotNull(response);
    
        var result = JsonSerializer.Deserialize<WeatherForecast>(response.Result);
    
        Assert.Equal("Seattle", result.Location);
    
        Assert.Equal(3, result.DailyForecasts.Count);
    
    }
    
    
    
    [Fact]
    
    public async Task WeatherTool_InvalidLocation_ThrowsToolExecutionException()
    
    {
    
        // Arrange
    
        var mockWeatherService = new Mock<IWeatherService>();
    
        mockWeatherService
    
            .Setup(s => s.GetForecastAsync("InvalidLocation", It.IsAny<int>()))
    
            .ThrowsAsync(new LocationNotFoundException("Location not found"));
    
        
    
        var tool = new WeatherForecastTool(mockWeatherService.Object);
    
        
    
        var request = new ToolRequest(
    
            toolName: "weatherForecast",
    
            parameters: JsonSerializer.SerializeToElement(new { 
    
                location = "InvalidLocation", 
    
                days = 3 
    
            })
    
        );
    
        
    
        // Act & Assert
    
        var exception = await Assert.ThrowsAsync<ToolExecutionException>(
    
            () => tool.ExecuteAsync(request)
    
        );
    
        
    
        Assert.Contains("Location not found", exception.Message);
    
    }
    
    
    2. Schema Validation Testing

    Test that schemas are valid and properly enforce constraints:

    
    @Test
    
    public void testSchemaValidation() {
    
        // Create tool instance
    
        SearchTool searchTool = new SearchTool();
    
        
    
        // Get schema
    
        Object schema = searchTool.getSchema();
    
        
    
        // Convert schema to JSON for validation
    
        String schemaJson = objectMapper.writeValueAsString(schema);
    
        
    
        // Validate schema is valid JSONSchema
    
        JsonSchemaFactory factory = JsonSchemaFactory.byDefault();
    
        JsonSchema jsonSchema = factory.getJsonSchema(schemaJson);
    
        
    
        // Test valid parameters
    
        JsonNode validParams = objectMapper.createObjectNode()
    
            .put("query", "test query")
    
            .put("limit", 5);
    
            
    
        ProcessingReport validReport = jsonSchema.validate(validParams);
    
        assertTrue(validReport.isSuccess());
    
        
    
        // Test missing required parameter
    
        JsonNode missingRequired = objectMapper.createObjectNode()
    
            .put("limit", 5);
    
            
    
        ProcessingReport missingReport = jsonSchema.validate(missingRequired);
    
        assertFalse(missingReport.isSuccess());
    
        
    
        // Test invalid parameter type
    
        JsonNode invalidType = objectMapper.createObjectNode()
    
            .put("query", "test")
    
            .put("limit", "not-a-number");
    
            
    
        ProcessingReport invalidReport = jsonSchema.validate(invalidType);
    
        assertFalse(invalidReport.isSuccess());
    
    }
    
    
    3. Error Handling Tests

    Create specific tests for error conditions:

    
    @pytest.mark.asyncio
    
    async def test_api_tool_handles_timeout():
    
        # Arrange
    
        tool = ApiTool(timeout=0.1)  # Very short timeout
    
        
    
        # Mock a request that will time out
    
        with aioresponses() as mocked:
    
            mocked.get(
    
                "https://api.example.com/data",
    
                callback=lambda *args, **kwargs: asyncio.sleep(0.5)  # Longer than timeout
    
            )
    
            
    
            request = ToolRequest(
    
                tool_name="apiTool",
    
                parameters={"url": "https://api.example.com/data"}
    
            )
    
            
    
            # Act & Assert
    
            with pytest.raises(ToolExecutionException) as exc_info:
    
                await tool.execute_async(request)
    
            
    
            # Verify exception message
    
            assert "timed out" in str(exc_info.value).lower()
    
    
    
    @pytest.mark.asyncio
    
    async def test_api_tool_handles_rate_limiting():
    
        # Arrange
    
        tool = ApiTool()
    
        
    
        # Mock a rate-limited response
    
        with aioresponses() as mocked:
    
            mocked.get(
    
                "https://api.example.com/data",
    
                status=429,
    
                headers={"Retry-After": "2"},
    
                body=json.dumps({"error": "Rate limit exceeded"})
    
            )
    
            
    
            request = ToolRequest(
    
                tool_name="apiTool",
    
                parameters={"url": "https://api.example.com/data"}
    
            )
    
            
    
            # Act & Assert
    
            with pytest.raises(ToolExecutionException) as exc_info:
    
                await tool.execute_async(request)
    
            
    
            # Verify exception contains rate limit information
    
            error_msg = str(exc_info.value).lower()
    
            assert "rate limit" in error_msg
    
            assert "try again" in error_msg
    
    

    Integration Testing

    1. Tool Chain Testing

    Test tools working together in expected combinations:

    
    [Fact]
    
    public async Task DataProcessingWorkflow_CompletesSuccessfully()
    
    {
    
        // Arrange
    
        var dataFetchTool = new DataFetchTool(mockDataService.Object);
    
        var analysisTools = new DataAnalysisTool(mockAnalysisService.Object);
    
        var visualizationTool = new DataVisualizationTool(mockVisualizationService.Object);
    
        
    
        var toolRegistry = new ToolRegistry();
    
        toolRegistry.RegisterTool(dataFetchTool);
    
        toolRegistry.RegisterTool(analysisTools);
    
        toolRegistry.RegisterTool(visualizationTool);
    
        
    
        var workflowExecutor = new WorkflowExecutor(toolRegistry);
    
        
    
        // Act
    
        var result = await workflowExecutor.ExecuteWorkflowAsync(new[] {
    
            new ToolCall("dataFetch", new { source = "sales2023" }),
    
            new ToolCall("dataAnalysis", ctx => new { 
    
                data = ctx.GetResult("dataFetch"),
    
                analysis = "trend" 
    
            }),
    
            new ToolCall("dataVisualize", ctx => new {
    
                analysisResult = ctx.GetResult("dataAnalysis"),
    
                type = "line-chart"
    
            })
    
        });
    
        
    
        // Assert
    
        Assert.NotNull(result);
    
        Assert.True(result.Success);
    
        Assert.NotNull(result.GetResult("dataVisualize"));
    
        Assert.Contains("chartUrl", result.GetResult("dataVisualize").ToString());
    
    }
    
    
    2. MCP Server Testing

    Test the MCP server with full tool registration and execution:

    
    @SpringBootTest
    
    @AutoConfigureMockMvc
    
    public class McpServerIntegrationTest {
    
        
    
        @Autowired
    
        private MockMvc mockMvc;
    
        
    
        @Autowired
    
        private ObjectMapper objectMapper;
    
        
    
        @Test
    
        public void testToolDiscovery() throws Exception {
    
            // Test the discovery endpoint
    
            mockMvc.perform(get("/mcp/tools"))
    
                .andExpect(status().isOk())
    
                .andExpect(jsonPath("$.tools").isArray())
    
                .andExpect(jsonPath("$.tools[*].name").value(hasItems(
    
                    "weatherForecast", "calculator", "documentSearch"
    
                )));
    
        }
    
        
    
        @Test
    
        public void testToolExecution() throws Exception {
    
            // Create tool request
    
            Map<String, Object> request = new HashMap<>();
    
            request.put("toolName", "calculator");
    
            
    
            Map<String, Object> parameters = new HashMap<>();
    
            parameters.put("operation", "add");
    
            parameters.put("a", 5);
    
            parameters.put("b", 7);
    
            request.put("parameters", parameters);
    
            
    
            // Send request and verify response
    
            mockMvc.perform(post("/mcp/execute")
    
                .contentType(MediaType.APPLICATION_JSON)
    
                .content(objectMapper.writeValueAsString(request)))
    
                .andExpect(status().isOk())
    
                .andExpect(jsonPath("$.result.value").value(12));
    
        }
    
        
    
        @Test
    
        public void testToolValidation() throws Exception {
    
            // Create invalid tool request
    
            Map<String, Object> request = new HashMap<>();
    
            request.put("toolName", "calculator");
    
            
    
            Map<String, Object> parameters = new HashMap<>();
    
            parameters.put("operation", "divide");
    
            parameters.put("a", 10);
    
            // Missing parameter "b"
    
            request.put("parameters", parameters);
    
            
    
            // Send request and verify error response
    
            mockMvc.perform(post("/mcp/execute")
    
                .contentType(MediaType.APPLICATION_JSON)
    
                .content(objectMapper.writeValueAsString(request)))
    
                .andExpect(status().isBadRequest())
    
                .andExpect(jsonPath("$.error").exists());
    
        }
    
    }
    
    
    3. End-to-End Testing

    Test complete workflows from model prompt to tool execution:

    
    @pytest.mark.asyncio
    
    async def test_model_interaction_with_tool():
    
        # Arrange - Set up MCP client and mock model
    
        mcp_client = McpClient(server_url="http://localhost:5000")
    
        
    
        # Mock model responses
    
        mock_model = MockLanguageModel([
    
            MockResponse(
    
                "What's the weather in Seattle?",
    
                tool_calls=[{
    
                    "tool_name": "weatherForecast",
    
                    "parameters": {"location": "Seattle", "days": 3}
    
                }]
    
            ),
    
            MockResponse(
    
                "Here's the weather forecast for Seattle:\n- Today: 65ยฐF, Partly Cloudy\n- Tomorrow: 68ยฐF, Sunny\n- Day after: 62ยฐF, Rain",
    
                tool_calls=[]
    
            )
    
        ])
    
        
    
        # Mock weather tool response
    
        with aioresponses() as mocked:
    
            mocked.post(
    
                "http://localhost:5000/mcp/execute",
    
                payload={
    
                    "result": {
    
                        "location": "Seattle",
    
                        "forecast": [
    
                            {"date": "2023-06-01", "temperature": 65, "conditions": "Partly Cloudy"},
    
                            {"date": "2023-06-02", "temperature": 68, "conditions": "Sunny"},
    
                            {"date": "2023-06-03", "temperature": 62, "conditions": "Rain"}
    
                        ]
    
                    }
    
                }
    
            )
    
            
    
            # Act
    
            response = await mcp_client.send_prompt(
    
                "What's the weather in Seattle?",
    
                model=mock_model,
    
                allowed_tools=["weatherForecast"]
    
            )
    
            
    
            # Assert
    
            assert "Seattle" in response.generated_text
    
            assert "65" in response.generated_text
    
            assert "Sunny" in response.generated_text
    
            assert "Rain" in response.generated_text
    
            assert len(response.tool_calls) == 1
    
            assert response.tool_calls[0].tool_name == "weatherForecast"
    
    

    Performance Testing

    1. Load Testing

    Test how many concurrent requests your MCP server can handle:

    
    [Fact]
    
    public async Task McpServer_HandlesHighConcurrency()
    
    {
    
        // Arrange
    
        var server = new McpServer(
    
            name: "TestServer",
    
            version: "1.0",
    
            maxConcurrentRequests: 100
    
        );
    
        
    
        server.RegisterTool(new FastExecutingTool());
    
        await server.StartAsync();
    
        
    
        var client = new McpClient("http://localhost:5000");
    
        
    
        // Act
    
        var tasks = new List<Task<McpResponse>>();
    
        for (int i = 0; i < 1000; i++)
    
        {
    
            tasks.Add(client.ExecuteToolAsync("fastTool", new { iteration = i }));
    
        }
    
        
    
        var results = await Task.WhenAll(tasks);
    
        
    
        // Assert
    
        Assert.Equal(1000, results.Length);
    
        Assert.All(results, r => Assert.NotNull(r));
    
    }
    
    
    2. Stress Testing

    Test the system under extreme load:

    
    @Test
    
    public void testServerUnderStress() {
    
        int maxUsers = 1000;
    
        int rampUpTimeSeconds = 60;
    
        int testDurationSeconds = 300;
    
        
    
        // Set up JMeter for stress testing
    
        StandardJMeterEngine jmeter = new StandardJMeterEngine();
    
        
    
        // Configure JMeter test plan
    
        HashTree testPlanTree = new HashTree();
    
        
    
        // Create test plan, thread group, samplers, etc.
    
        TestPlan testPlan = new TestPlan("MCP Server Stress Test");
    
        testPlanTree.add(testPlan);
    
        
    
        ThreadGroup threadGroup = new ThreadGroup();
    
        threadGroup.setNumThreads(maxUsers);
    
        threadGroup.setRampUp(rampUpTimeSeconds);
    
        threadGroup.setScheduler(true);
    
        threadGroup.setDuration(testDurationSeconds);
    
        
    
        testPlanTree.add(threadGroup);
    
        
    
        // Add HTTP sampler for tool execution
    
        HTTPSampler toolExecutionSampler = new HTTPSampler();
    
        toolExecutionSampler.setDomain("localhost");
    
        toolExecutionSampler.setPort(5000);
    
        toolExecutionSampler.setPath("/mcp/execute");
    
        toolExecutionSampler.setMethod("POST");
    
        toolExecutionSampler.addArgument("toolName", "calculator");
    
        toolExecutionSampler.addArgument("parameters", "{\"operation\":\"add\",\"a\":5,\"b\":7}");
    
        
    
        threadGroup.add(toolExecutionSampler);
    
        
    
        // Add listeners
    
        SummaryReport summaryReport = new SummaryReport();
    
        threadGroup.add(summaryReport);
    
        
    
        // Run test
    
        jmeter.configure(testPlanTree);
    
        jmeter.run();
    
        
    
        // Validate results
    
        assertEquals(0, summaryReport.getErrorCount());
    
        assertTrue(summaryReport.getAverage() < 200); // Average response time < 200ms
    
        assertTrue(summaryReport.getPercentile(90.0) < 500); // 90th percentile < 500ms
    
    }
    
    
    3. Monitoring and Profiling

    Set up monitoring for long-term performance analysis:

    
    # Configure monitoring for an MCP server
    
    def configure_monitoring(server):
    
        # Set up Prometheus metrics
    
        prometheus_metrics = {
    
            "request_count": Counter("mcp_requests_total", "Total MCP requests"),
    
            "request_latency": Histogram(
    
                "mcp_request_duration_seconds", 
    
                "Request duration in seconds",
    
                buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 10.0]
    
            ),
    
            "tool_execution_count": Counter(
    
                "mcp_tool_executions_total", 
    
                "Tool execution count",
    
                labelnames=["tool_name"]
    
            ),
    
            "tool_execution_latency": Histogram(
    
                "mcp_tool_duration_seconds", 
    
                "Tool execution duration in seconds",
    
                labelnames=["tool_name"],
    
                buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 10.0]
    
            ),
    
            "tool_errors": Counter(
    
                "mcp_tool_errors_total",
    
                "Tool execution errors",
    
                labelnames=["tool_name", "error_type"]
    
            )
    
        }
    
        
    
        # Add middleware for timing and recording metrics
    
        server.add_middleware(PrometheusMiddleware(prometheus_metrics))
    
        
    
        # Expose metrics endpoint
    
        @server.router.get("/metrics")
    
        async def metrics():
    
            return generate_latest()
    
        
    
        return server
    
    

    MCP Workflow Design Patterns

    Well-designed MCP workflows improve efficiency, reliability, and maintainability. Here are key patterns to follow:

    1. Chain of Tools Pattern

    Connect multiple tools in a sequence where each tool's output becomes the input for the next:

    
    # Python Chain of Tools implementation
    
    class ChainWorkflow:
    
        def __init__(self, tools_chain):
    
            self.tools_chain = tools_chain  # List of tool names to execute in sequence
    
        
    
        async def execute(self, mcp_client, initial_input):
    
            current_result = initial_input
    
            all_results = {"input": initial_input}
    
            
    
            for tool_name in self.tools_chain:
    
                # Execute each tool in the chain, passing previous result
    
                response = await mcp_client.execute_tool(tool_name, current_result)
    
                
    
                # Store result and use as input for next tool
    
                all_results[tool_name] = response.result
    
                current_result = response.result
    
            
    
            return {
    
                "final_result": current_result,
    
                "all_results": all_results
    
            }
    
    
    
    # Example usage
    
    data_processing_chain = ChainWorkflow([
    
        "dataFetch",
    
        "dataCleaner",
    
        "dataAnalyzer",
    
        "dataVisualizer"
    
    ])
    
    
    
    result = await data_processing_chain.execute(
    
        mcp_client,
    
        {"source": "sales_database", "table": "transactions"}
    
    )
    
    

    2. Dispatcher Pattern

    Use a central tool that dispatches to specialized tools based on input:

    
    public class ContentDispatcherTool : IMcpTool
    
    {
    
        private readonly IMcpClient _mcpClient;
    
        
    
        public ContentDispatcherTool(IMcpClient mcpClient)
    
        {
    
            _mcpClient = mcpClient;
    
        }
    
        
    
        public string Name => "contentProcessor";
    
        public string Description => "Processes content of various types";
    
        
    
        public object GetSchema()
    
        {
    
            return new {
    
                type = "object",
    
                properties = new {
    
                    content = new { type = "string" },
    
                    contentType = new { 
    
                        type = "string",
    
                        enum = new[] { "text", "html", "markdown", "csv", "code" }
    
                    },
    
                    operation = new { 
    
                        type = "string",
    
                        enum = new[] { "summarize", "analyze", "extract", "convert" }
    
                    }
    
                },
    
                required = new[] { "content", "contentType", "operation" }
    
            };
    
        }
    
        
    
        public async Task<ToolResponse> ExecuteAsync(ToolRequest request)
    
        {
    
            var content = request.Parameters.GetProperty("content").GetString();
    
            var contentType = request.Parameters.GetProperty("contentType").GetString();
    
            var operation = request.Parameters.GetProperty("operation").GetString();
    
            
    
            // Determine which specialized tool to use
    
            string targetTool = DetermineTargetTool(contentType, operation);
    
            
    
            // Forward to the specialized tool
    
            var specializedResponse = await _mcpClient.ExecuteToolAsync(
    
                targetTool,
    
                new { content, options = GetOptionsForTool(targetTool, operation) }
    
            );
    
            
    
            return new ToolResponse { Result = specializedResponse.Result };
    
        }
    
        
    
        private string DetermineTargetTool(string contentType, string operation)
    
        {
    
            return (contentType, operation) switch
    
            {
    
                ("text", "summarize") => "textSummarizer",
    
                ("text", "analyze") => "textAnalyzer",
    
                ("html", _) => "htmlProcessor",
    
                ("markdown", _) => "markdownProcessor",
    
                ("csv", _) => "csvProcessor",
    
                ("code", _) => "codeAnalyzer",
    
                _ => throw new ToolExecutionException($"No tool available for {contentType}/{operation}")
    
            };
    
        }
    
        
    
        private object GetOptionsForTool(string toolName, string operation)
    
        {
    
            // Return appropriate options for each specialized tool
    
            return toolName switch
    
            {
    
                "textSummarizer" => new { length = "medium" },
    
                "htmlProcessor" => new { cleanUp = true, operation },
    
                // Options for other tools...
    
                _ => new { }
    
            };
    
        }
    
    }
    
    

    3. Parallel Processing Pattern

    Execute multiple tools simultaneously for efficiency:

    
    public class ParallelDataProcessingWorkflow {
    
        private final McpClient mcpClient;
    
        
    
        public ParallelDataProcessingWorkflow(McpClient mcpClient) {
    
            this.mcpClient = mcpClient;
    
        }
    
        
    
        public WorkflowResult execute(String datasetId) {
    
            // Step 1: Fetch dataset metadata (synchronous)
    
            ToolResponse metadataResponse = mcpClient.executeTool("datasetMetadata", 
    
                Map.of("datasetId", datasetId));
    
            
    
            // Step 2: Launch multiple analyses in parallel
    
            CompletableFuture<ToolResponse> statisticalAnalysis = CompletableFuture.supplyAsync(() ->
    
                mcpClient.executeTool("statisticalAnalysis", Map.of(
    
                    "datasetId", datasetId,
    
                    "type", "comprehensive"
    
                ))
    
            );
    
            
    
            CompletableFuture<ToolResponse> correlationAnalysis = CompletableFuture.supplyAsync(() ->
    
                mcpClient.executeTool("correlationAnalysis", Map.of(
    
                    "datasetId", datasetId,
    
                    "method", "pearson"
    
                ))
    
            );
    
            
    
            CompletableFuture<ToolResponse> outlierDetection = CompletableFuture.supplyAsync(() ->
    
                mcpClient.executeTool("outlierDetection", Map.of(
    
                    "datasetId", datasetId,
    
                    "sensitivity", "medium"
    
                ))
    
            );
    
            
    
            // Wait for all parallel tasks to complete
    
            CompletableFuture<Void> allAnalyses = CompletableFuture.allOf(
    
                statisticalAnalysis, correlationAnalysis, outlierDetection
    
            );
    
            
    
            allAnalyses.join();  // Wait for completion
    
            
    
            // Step 3: Combine results
    
            Map<String, Object> combinedResults = new HashMap<>();
    
            combinedResults.put("metadata", metadataResponse.getResult());
    
            combinedResults.put("statistics", statisticalAnalysis.join().getResult());
    
            combinedResults.put("correlations", correlationAnalysis.join().getResult());
    
            combinedResults.put("outliers", outlierDetection.join().getResult());
    
            
    
            // Step 4: Generate summary report
    
            ToolResponse summaryResponse = mcpClient.executeTool("reportGenerator", 
    
                Map.of("analysisResults", combinedResults));
    
            
    
            // Return complete workflow result
    
            WorkflowResult result = new WorkflowResult();
    
            result.setDatasetId(datasetId);
    
            result.setAnalysisResults(combinedResults);
    
            result.setSummaryReport(summaryResponse.getResult());
    
            
    
            return result;
    
        }
    
    }
    
    

    4. Error Recovery Pattern

    Implement graceful fallbacks for tool failures:

    
    class ResilientWorkflow:
    
        def __init__(self, mcp_client):
    
            self.client = mcp_client
    
        
    
        async def execute_with_fallback(self, primary_tool, fallback_tool, parameters):
    
            try:
    
                # Try primary tool first
    
                response = await self.client.execute_tool(primary_tool, parameters)
    
                return {
    
                    "result": response.result,
    
                    "source": "primary",
    
                    "tool": primary_tool
    
                }
    
            except ToolExecutionException as e:
    
                # Log the failure
    
                logging.warning(f"Primary tool '{primary_tool}' failed: {str(e)}")
    
                
    
                # Fall back to secondary tool
    
                try:
    
                    # Might need to transform parameters for fallback tool
    
                    fallback_params = self._adapt_parameters(parameters, primary_tool, fallback_tool)
    
                    
    
                    response = await self.client.execute_tool(fallback_tool, fallback_params)
    
                    return {
    
                        "result": response.result,
    
                        "source": "fallback",
    
                        "tool": fallback_tool,
    
                        "primaryError": str(e)
    
                    }
    
                except ToolExecutionException as fallback_error:
    
                    # Both tools failed
    
                    logging.error(f"Both primary and fallback tools failed. Fallback error: {str(fallback_error)}")
    
                    raise WorkflowExecutionException(
    
                        f"Workflow failed: primary error: {str(e)}; fallback error: {str(fallback_error)}"
    
                    )
    
        
    
        def _adapt_parameters(self, params, from_tool, to_tool):
    
            """Adapt parameters between different tools if needed"""
    
            # This implementation would depend on the specific tools
    
            # For this example, we'll just return the original parameters
    
            return params
    
    
    
    # Example usage
    
    async def get_weather(workflow, location):
    
        return await workflow.execute_with_fallback(
    
            "premiumWeatherService",  # Primary (paid) weather API
    
            "basicWeatherService",    # Fallback (free) weather API
    
            {"location": location}
    
        )
    
    

    5. Workflow Composition Pattern

    Build complex workflows by composing simpler ones:

    
    public class CompositeWorkflow : IWorkflow
    
    {
    
        private readonly List<IWorkflow> _workflows;
    
        
    
        public CompositeWorkflow(IEnumerable<IWorkflow> workflows)
    
        {
    
            _workflows = new List<IWorkflow>(workflows);
    
        }
    
        
    
        public async Task<WorkflowResult> ExecuteAsync(WorkflowContext context)
    
        {
    
            var results = new Dictionary<string, object>();
    
            
    
            foreach (var workflow in _workflows)
    
            {
    
                var workflowResult = await workflow.ExecuteAsync(context);
    
                
    
                // Store each workflow's result
    
                results[workflow.Name] = workflowResult;
    
                
    
                // Update context with the result for the next workflow
    
                context = context.WithResult(workflow.Name, workflowResult);
    
            }
    
            
    
            return new WorkflowResult(results);
    
        }
    
        
    
        public string Name => "CompositeWorkflow";
    
        public string Description => "Executes multiple workflows in sequence";
    
    }
    
    
    
    // Example usage
    
    var documentWorkflow = new CompositeWorkflow(new IWorkflow[] {
    
        new DocumentFetchWorkflow(),
    
        new DocumentProcessingWorkflow(),
    
        new InsightGenerationWorkflow(),
    
        new ReportGenerationWorkflow()
    
    });
    
    
    
    var result = await documentWorkflow.ExecuteAsync(new WorkflowContext {
    
        Parameters = new { documentId = "12345" }
    
    });
    
    

    Testing MCP Servers: Best Practices and Top Tips

    Overview

    Testing is a critical aspect of developing reliable, high-quality MCP servers.

    This guide provides comprehensive best practices and tips for testing your MCP servers throughout the development lifecycle, from unit tests to integration tests and end-to-end validation.

    Why Testing Matters for MCP Servers

    MCP servers serve as crucial middleware between AI models and client applications. Thorough testing ensures:

  • Reliability in production environments
  • Accurate handling of requests and responses
  • Proper implementation of MCP specifications
  • Resilience against failures and edge cases
  • Consistent performance under various loads
  • Unit Testing for MCP Servers

    Unit Testing (Foundation)

    Unit tests verify individual components of your MCP server in isolation.

    What to Test

    1. Resource Handlers: Test each resource handler's logic independently

    2. Tool Implementations: Verify tool behavior with various inputs

    3. Prompt Templates: Ensure prompt templates render correctly

    4. Schema Validation: Test parameter validation logic

    5. Error Handling: Verify error responses for invalid inputs

    Best Practices for Unit Testing
    
    // Example unit test for a calculator tool in C#
    
    [Fact]
    
    public async Task CalculatorTool_Add_ReturnsCorrectSum()
    
    {
    
        // Arrange
    
        var calculator = new CalculatorTool();
    
        var parameters = new Dictionary<string, object>
    
        {
    
            ["operation"] = "add",
    
            ["a"] = 5,
    
            ["b"] = 7
    
        };
    
        
    
        // Act
    
        var response = await calculator.ExecuteAsync(parameters);
    
        var result = JsonSerializer.Deserialize<CalculationResult>(response.Content[0].ToString());
    
        
    
        // Assert
    
        Assert.Equal(12, result.Value);
    
    }
    
    
    
    # Example unit test for a calculator tool in Python
    
    def test_calculator_tool_add():
    
        # Arrange
    
        calculator = CalculatorTool()
    
        parameters = {
    
            "operation": "add",
    
            "a": 5,
    
            "b": 7
    
        }
    
        
    
        # Act
    
        response = calculator.execute(parameters)
    
        result = json.loads(response.content[0].text)
    
        
    
        # Assert
    
        assert result["value"] == 12
    
    

    Integration Testing (Middle Layer)

    Integration tests verify interactions between components of your MCP server.

    What to Test

    1. Server Initialization: Test server startup with various configurations

    2. Route Registration: Verify all endpoints are correctly registered

    3. Request Processing: Test the full request-response cycle

    4. Error Propagation: Ensure errors are properly handled across components

    5. Authentication & Authorization: Test security mechanisms

    Best Practices for Integration Testing
    
    // Example integration test for MCP server in C#
    
    [Fact]
    
    public async Task Server_ProcessToolRequest_ReturnsValidResponse()
    
    {
    
        // Arrange
    
        var server = new McpServer();
    
        server.RegisterTool(new CalculatorTool());
    
        await server.StartAsync();
    
        
    
        var request = new McpRequest
    
        {
    
            Tool = "calculator",
    
            Parameters = new Dictionary<string, object>
    
            {
    
                ["operation"] = "multiply",
    
                ["a"] = 6,
    
                ["b"] = 7
    
            }
    
        };
    
        
    
        // Act
    
        var response = await server.ProcessRequestAsync(request);
    
        
    
        // Assert
    
        Assert.NotNull(response);
    
        Assert.Equal(McpStatusCodes.Success, response.StatusCode);
    
        // Additional assertions for response content
    
        
    
        // Cleanup
    
        await server.StopAsync();
    
    }
    
    

    End-to-End Testing (Top Layer)

    End-to-end tests verify the complete system behavior from client to server.

    What to Test

    1. Client-Server Communication: Test complete request-response cycles

    2. Real Client SDKs: Test with actual client implementations

    3. Performance Under Load: Verify behavior with multiple concurrent requests

    4. Error Recovery: Test system recovery from failures

    5. Long-Running Operations: Verify handling of streaming and long operations

    Best Practices for E2E Testing
    
    // Example E2E test with a client in TypeScript
    
    describe('MCP Server E2E Tests', () => {
    
      let client: McpClient;
    
      
    
      beforeAll(async () => {
    
        // Start server in test environment
    
        await startTestServer();
    
        client = new McpClient('http://localhost:5000');
    
      });
    
      
    
      afterAll(async () => {
    
        await stopTestServer();
    
      });
    
      
    
      test('Client can invoke calculator tool and get correct result', async () => {
    
        // Act
    
        const response = await client.invokeToolAsync('calculator', {
    
          operation: 'divide',
    
          a: 20,
    
          b: 4
    
        });
    
        
    
        // Assert
    
        expect(response.statusCode).toBe(200);
    
        expect(response.content[0].text).toContain('5');
    
      });
    
    });
    
    

    Mocking Strategies for MCP Testing

    Mocking is essential for isolating components during testing.

    Components to Mock

    1. External AI Models: Mock model responses for predictable testing

    2. External Services: Mock API dependencies (databases, third-party services)

    3. Authentication Services: Mock identity providers

    4. Resource Providers: Mock expensive resource handlers

    Example: Mocking an AI Model Response

    
    // C# example with Moq
    
    var mockModel = new Mock<ILanguageModel>();
    
    mockModel
    
        .Setup(m => m.GenerateResponseAsync(
    
            It.IsAny<string>(),
    
            It.IsAny<McpRequestContext>()))
    
        .ReturnsAsync(new ModelResponse { 
    
            Text = "Mocked model response",
    
            FinishReason = FinishReason.Completed
    
        });
    
    
    
    var server = new McpServer(modelClient: mockModel.Object);
    
    
    
    # Python example with unittest.mock
    
    @patch('mcp_server.models.OpenAIModel')
    
    def test_with_mock_model(mock_model):
    
        # Configure mock
    
        mock_model.return_value.generate_response.return_value = {
    
            "text": "Mocked model response",
    
            "finish_reason": "completed"
    
        }
    
        
    
        # Use mock in test
    
        server = McpServer(model_client=mock_model)
    
        # Continue with test
    
    

    Performance Testing

    Performance testing is crucial for production MCP servers.

    What to Measure

    1. Latency: Response time for requests

    2. Throughput: Requests handled per second

    3. Resource Utilization: CPU, memory, network usage

    4. Concurrency Handling: Behavior under parallel requests

    5. Scaling Characteristics: Performance as load increases

    Tools for Performance Testing

  • k6: Open-source load testing tool
  • JMeter: Comprehensive performance testing
  • Locust: Python-based load testing
  • Azure Load Testing: Cloud-based performance testing
  • Example: Basic Load Test with k6

    
    // k6 script for load testing MCP server
    
    import http from 'k6/http';
    
    import { check, sleep } from 'k6';
    
    
    
    export const options = {
    
      vus: 10,  // 10 virtual users
    
      duration: '30s',
    
    };
    
    
    
    export default function () {
    
      const payload = JSON.stringify({
    
        tool: 'calculator',
    
        parameters: {
    
          operation: 'add',
    
          a: Math.floor(Math.random() * 100),
    
          b: Math.floor(Math.random() * 100)
    
        }
    
      });
    
    
    
      const params = {
    
        headers: {
    
          'Content-Type': 'application/json',
    
          'Authorization': 'Bearer test-token'
    
        },
    
      };
    
    
    
      const res = http.post('http://localhost:5000/api/tools/invoke', payload, params);
    
      
    
      check(res, {
    
        'status is 200': (r) => r.status === 200,
    
        'response time < 500ms': (r) => r.timings.duration < 500,
    
      });
    
      
    
      sleep(1);
    
    }
    
    

    Test Automation for MCP Servers

    Automating your tests ensures consistent quality and faster feedback loops.

    CI/CD Integration

    1. Run Unit Tests on Pull Requests: Ensure code changes don't break existing functionality

    2. Integration Tests in Staging: Run integration tests in pre-production environments

    3. Performance Baselines: Maintain performance benchmarks to catch regressions

    4. Security Scans: Automate security testing as part of the pipeline

    Example CI Pipeline (GitHub Actions)

    
    name: MCP Server Tests
    
    
    
    on:
    
      push:
    
        branches: [ main ]
    
      pull_request:
    
        branches: [ main ]
    
    
    
    jobs:
    
      test:
    
        runs-on: ubuntu-latest
    
        
    
        steps:
    
        - uses: actions/checkout@v2
    
        
    
        - name: Set up Runtime
    
          uses: actions/setup-dotnet@v1
    
          with:
    
            dotnet-version: '8.0.x'
    
        
    
        - name: Restore dependencies
    
          run: dotnet restore
    
        
    
        - name: Build
    
          run: dotnet build --no-restore
    
        
    
        - name: Unit Tests
    
          run: dotnet test --no-build --filter Category=Unit
    
        
    
        - name: Integration Tests
    
          run: dotnet test --no-build --filter Category=Integration
    
          
    
        - name: Performance Tests
    
          run: dotnet run --project tests/PerformanceTests/PerformanceTests.csproj
    
    

    Testing for Compliance with MCP Specification

    Verify your server correctly implements the MCP specification.

    Key Compliance Areas

    1. API Endpoints: Test required endpoints (/resources, /tools, etc.)

    2. Request/Response Format: Validate schema compliance

    3. Error Codes: Verify correct status codes for various scenarios

    4. Content Types: Test handling of different content types

    5. Authentication Flow: Verify spec-compliant auth mechanisms

    Compliance Test Suite

    
    [Fact]
    
    public async Task Server_ResourceEndpoint_ReturnsCorrectSchema()
    
    {
    
        // Arrange
    
        var client = new HttpClient();
    
        client.DefaultRequestHeaders.Add("Authorization", "Bearer test-token");
    
        
    
        // Act
    
        var response = await client.GetAsync("http://localhost:5000/api/resources");
    
        var content = await response.Content.ReadAsStringAsync();
    
        var resources = JsonSerializer.Deserialize<ResourceList>(content);
    
        
    
        // Assert
    
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    
        Assert.NotNull(resources);
    
        Assert.All(resources.Resources, resource => 
    
        {
    
            Assert.NotNull(resource.Id);
    
            Assert.NotNull(resource.Type);
    
            // Additional schema validation
    
        });
    
    }
    
    

    Top 10 Tips for Effective MCP Server Testing

    1. Test Tool Definitions Separately: Verify schema definitions independently from tool logic

    2. Use Parameterized Tests: Test tools with a variety of inputs, including edge cases

    3. Check Error Responses: Verify proper error handling for all possible error conditions

    4. Test Authorization Logic: Ensure proper access control for different user roles

    5. Monitor Test Coverage: Aim for high coverage of critical path code

    6. Test Streaming Responses: Verify proper handling of streaming content

    7. Simulate Network Issues: Test behavior under poor network conditions

    8. Test Resource Limits: Verify behavior when reaching quotas or rate limits

    9. Automate Regression Tests: Build a suite that runs on every code change

    10. Document Test Cases: Maintain clear documentation of test scenarios

    Common Testing Pitfalls

  • Over-reliance on happy path testing: Make sure to test error cases thoroughly
  • Ignoring performance testing: Identify bottlenecks before they affect production
  • Testing in isolation only: Combine unit, integration, and E2E tests
  • Incomplete API coverage: Ensure all endpoints and features are tested
  • Inconsistent test environments: Use containers to ensure consistent test environments
  • Conclusion

    A comprehensive testing strategy is essential for developing reliable, high-quality MCP servers.

    By implementing the best practices and tips outlined in this guide, you can ensure your MCP implementations meet the highest standards of quality, reliability, and performance.

    Key Takeaways

    1. Tool Design: Follow single responsibility principle, use dependency injection, and design for composability

    2. Schema Design: Create clear, well-documented schemas with proper validation constraints

    3. Error Handling: Implement graceful error handling, structured error responses, and retry logic

    4. Performance: Use caching, asynchronous processing, and resource throttling

    5. Security: Apply thorough input validation, authorization checks, and sensitive data handling

    6. Testing: Create comprehensive unit, integration, and end-to-end tests

    7. Workflow Patterns: Apply established patterns like chains, dispatchers, and parallel processing

    Exercise

    Design an MCP tool and workflow for a document processing system that:

    1. Accepts documents in multiple formats (PDF, DOCX, TXT)

    2. Extracts text and key information from the documents

    3. Classifies documents by type and content

    4. Generates a summary of each document

    Implement the tool schemas, error handling, and a workflow pattern that best suits this scenario. Consider how you would test this implementation.

    Resources

    1. Join the MCP community on the Azure AI Foundry Discord Community to stay updated on the latest developments

    2. Contribute to open-source MCP projects

    3. Apply MCP principles in your own organization's AI initiatives

    4. Explore specialized MCP implementations for your industry.

    5. Consider taking advanced courses on specific MCP topics, such as multi-modal integration or enterprise application integration.

    6. Experiment with building your own MCP tools and workflows using the principles learned through the Hands on Lab

    What's Next

    Next: Case Studies

    code Module 09

    Case Study

    MCP in Action: Real-World Case Studies

    _(Click the image above to view video of this lesson)_

    The Model Context Protocol (MCP) is transforming how AI applications interact with data, tools, and services. This section presents real-world case studies that demonstrate practical applications of MCP in various enterprise scenarios.

    Overview

    This section showcases concrete examples of MCP implementations, highlighting how organizations are leveraging this protocol to solve complex business challenges.

    By examining these case studies, you'll gain insights into the versatility, scalability, and practical benefits of MCP in real-world scenarios.

    Key Learning Objectives

    By exploring these case studies, you will:

  • Understand how MCP can be applied to solve specific business problems
  • Learn about different integration patterns and architectural approaches
  • Recognize best practices for implementing MCP in enterprise environments
  • Gain insights into the challenges and solutions encountered in real-world implementations
  • Identify opportunities to apply similar patterns in your own projects
  • Featured Case Studies

    1. Azure AI Travel Agents โ€“ Reference Implementation

    This case study examines Microsoft's comprehensive reference solution that demonstrates how to build a multi-agent, AI-powered travel planning application using MCP, Azure OpenAI, and Azure AI Search. The project showcases:

  • Multi-agent orchestration through MCP
  • Enterprise data integration with Azure AI Search
  • Secure, scalable architecture using Azure services
  • Extensible tooling with reusable MCP components
  • Conversational user experience powered by Azure OpenAI
  • The architecture and implementation details provide valuable insights into building complex, multi-agent systems with MCP as the coordination layer.

    2. Updating Azure DevOps Items from YouTube Data

    This case study demonstrates a practical application of MCP for automating workflow processes. It shows how MCP tools can be used to:

  • Extract data from online platforms (YouTube)
  • Update work items in Azure DevOps systems
  • Create repeatable automation workflows
  • Integrate data across disparate systems
  • This example illustrates how even relatively simple MCP implementations can provide significant efficiency gains by automating routine tasks and improving data consistency across systems.

    3. Real-Time Documentation Retrieval with MCP

    Case Study: Connecting to the Microsoft Learn Docs MCP Server from a Client

    Have you ever found yourself juggling between documentation sites, Stack Overflow, and endless search engine tabs, all while trying to solve a problem in your code?

    Maybe you keep a second monitor just for docs, or youโ€™re constantly alt-tabbing between your IDE and a browser.

    Wouldnโ€™t it be better if you could bring the documentation right into your workflowโ€”integrated into your apps, your IDE, or even your own custom tools?

    In this case study, weโ€™ll explore how to do exactly that by connecting directly to the Microsoft Learn Docs MCP server from your own client application.

    Overview

    Modern development is more than just writing codeโ€”itโ€™s about finding the right information at the right time.

    Documentation is everywhere, but itโ€™s rarely where you need it most: inside your tools and workflows.

    By integrating documentation retrieval directly into your applications, you can save time, reduce context switching, and boost productivity.

    In this section, weโ€™ll show you how to connect a client to the Microsoft Learn Docs MCP server, so you can access real-time, context-aware documentation without ever leaving your app.

    Weโ€™ll walk through the process of establishing a connection, sending a request, and handling streaming responses efficiently. This approach not only streamlines your workflow but also opens the door to building smarter, more helpful developer tools.

    Learning Objectives

    Why are we doing this?

    Because the best developer experiences are those that remove friction.

    Imagine a world where your code editor, chatbot, or web app can answer your documentation questions instantly, using the latest content from Microsoft Learn.

    By the end of this chapter, youโ€™ll know how to:

  • Understand the basics of MCP server-client communication for documentation
  • Implement a console or web application to connect to the Microsoft Learn Docs MCP server
  • Use streaming HTTP clients for real-time documentation retrieval
  • Log and interpret documentation responses in your application
  • Youโ€™ll see how these skills can help you build tools that are not just reactive, but truly interactive and context-aware.

    Scenario 1 - Real-Time Documentation Retrieval with MCP

    In this scenario, weโ€™ll show you how to connect a client to the Microsoft Learn Docs MCP server, so you can access real-time, context-aware documentation without ever leaving your app.

    Letโ€™s put this into practice.

    Your task is to write an app that connects to the Microsoft Learn Docs MCP server, invokes the microsoft_docs_search tool, and logs the streaming response to the console.

    Why this approach?

    Because itโ€™s the foundation for building more advanced integrationsโ€”whether you want to power a chatbot, an IDE extension, or a web dashboard.

    You'll find the code and instructions for this scenario in the solution folder within this case study.

    The steps will guide you through setting up the connection:

  • Use the official MCP SDK and streamable HTTP client for connection
  • Call the microsoft_docs_search tool with a query parameter to retrieve documentation
  • Implement proper logging and error handling
  • Create an interactive console interface to allow users to enter multiple search queries
  • This scenario demonstrates how to:

  • Connect to the Docs MCP server
  • Send a query
  • Parse and print the results
  • Hereโ€™s what running the solution might look like:

    
    Prompt> What is Azure Key Vault?
    
    Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
    
    

    Below is a minimal sample solution. The full code and details are available in the solution folder.

    Python

    
    import asyncio
    
    from mcp.client.streamable_http import streamablehttp_client
    
    from mcp import ClientSession
    
    
    
    async def main():
    
        async with streamablehttp_client("https://learn.microsoft.com/api/mcp") as (read_stream, write_stream, _):
    
            async with ClientSession(read_stream, write_stream) as session:
    
                await session.initialize()
    
                result = await session.call_tool("microsoft_docs_search", {"query": "Azure Functions best practices"})
    
                print(result.content)
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
  • For the complete implementation and logging, see scenario1.py.
  • For installation and usage instructions, see the README.md file in the same folder.
  • Scenario 2 - Interactive Study Plan Generator Web App with MCP

    In this scenario, youโ€™ll learn how to integrate Docs MCP into a web development project.

    The goal is to enable users to search Microsoft Learn documentation directly from a web interface, making documentation instantly accessible within your app or site.

    Youโ€™ll see how to:

  • Set up a web app
  • Connect to the Docs MCP server
  • Handle user input and display results
  • Hereโ€™s what running the solution might look like:

    
    User> I want to learn about AI102 - so suggest the roadmap to get it started from learn for 6 weeks
    
    
    
    Assistant> Hereโ€™s a detailed 6-week roadmap to start your preparation for the AI-102: Designing and Implementing a Microsoft Azure AI Solution certification, using official Microsoft resources and focusing on exam skills areas:
    
    
    
    ---
    
    ## Week 1: Introduction & Fundamentals
    
    - **Understand the Exam**: Review the [AI-102 exam skills outline](https://learn.microsoft.com/en-us/credentials/certifications/exams/ai-102/).
    
    - **Set up Azure**: Sign up for a free Azure account if you don't have one.
    
    - **Learning Path**: [Introduction to Azure AI services](https://learn.microsoft.com/en-us/training/modules/intro-to-azure-ai/)
    
    - **Focus**: Get familiar with Azure portal, AI capabilities, and necessary tools.
    
    
    
    ....more weeks of the roadmap...
    
    
    
    Let me know if you want module-specific recommendations or need more customized weekly tasks!
    
    

    Below is a minimal sample solution. The full code and details are available in the solution folder.

    Python (Chainlit)

    Chainlit is a framework for building conversational AI web apps. It makes it easy to create interactive chatbots and assistants that can call MCP tools and display results in real time. Itโ€™s ideal for rapid prototyping and user-friendly interfaces.

    
    import chainlit as cl
    
    import requests
    
    
    
    MCP_URL = "https://learn.microsoft.com/api/mcp"
    
    
    
    @cl.on_message
    
    def handle_message(message):
    
        query = {"question": message}
    
        response = requests.post(MCP_URL, json=query)
    
        if response.ok:
    
            result = response.json()
    
            cl.Message(content=result.get("answer", "No answer found.")).send()
    
        else:
    
            cl.Message(content="Error: " + response.text).send()
    
    
  • For the complete implementation, see scenario2.py.
  • For setup and running instructions, see the README.md.
  • Scenario 3: In-Editor Docs with MCP Server in VS Code

    If you want to get Microsoft Learn Docs directly inside your VS Code (instead of switching browser tabs), you can use the MCP server in your editor. This allows you to:

  • Search and read docs in VS Code without leaving your coding environment.
  • Reference documentation and insert links directly into your README or course files.
  • Leverage GitHub Copilot and MCP together for a seamless, AI-powered documentation workflow.
  • You'll see how to:

  • Add a valid .vscode/mcp.json file to your workspace root (see example below).
  • Open the MCP panel or use the command palette in VS Code to search and insert docs.
  • Reference documentation directly in your markdown files as you work.
  • Combine this workflow with GitHub Copilot for even greater productivity.
  • Hereโ€™s a example of how to set up the MCP server in VS Code:

    
    {
    
      "servers": {
    
        "LearnDocsMCP": {
    
          "url": "https://learn.microsoft.com/api/mcp"
    
        }
    
      }
    
    }
    
    

    > For a detailed walkthrough with screenshots and step-by-step guide, see README.md.

    This approach is ideal for anyone building technical courses, writing documentation, or developing code with frequent reference needs.

    Key Takeaways

    Integrating documentation directly into your tools isnโ€™t just a convenienceโ€”itโ€™s a game changer for productivity. By connecting to the Microsoft Learn Docs MCP server from your client, you can:

  • Eliminate context switching between your code and documentation
  • Retrieve up-to-date, context-aware docs in real time
  • Build smarter, more interactive developer tools
  • These skills will help you create solutions that are not only efficient, but also delightful to use.

    Additional Resources

    To deepen your understanding, explore these official resources:

  • Microsoft Learn Docs MCP Server (GitHub)
  • Get started with Azure MCP Server (mcp-python)
  • What is the Azure MCP Server?
  • Model Context Protocol (MCP) Introduction
  • Add plugins from a MCP Server (Python)
  • What's Next

  • Back to: Case Studies Overview
  • Continue to: Module 10: Streamlining AI Workflows with AI Toolkit
  • This case study guides you through connecting a Python console client to a Model Context Protocol (MCP) server to retrieve and log real-time, context-aware Microsoft documentation. You'll learn how to:

  • Connect to an MCP server using a Python client and the official MCP SDK
  • Use streaming HTTP clients for efficient, real-time data retrieval
  • Call documentation tools on the server and log responses directly to the console
  • Integrate up-to-date Microsoft documentation into your workflow without leaving the terminal
  • The chapter includes a hands-on assignment, a minimal working code sample, and links to additional resources for deeper learning.

    See the full walkthrough and code in the linked chapter to understand how MCP can transform documentation access and developer productivity in console-based environments.

    4. Interactive Study Plan Generator Web App with MCP

    Case Study: Connecting to the Microsoft Learn Docs MCP Server from a Client

    Have you ever found yourself juggling between documentation sites, Stack Overflow, and endless search engine tabs, all while trying to solve a problem in your code?

    Maybe you keep a second monitor just for docs, or youโ€™re constantly alt-tabbing between your IDE and a browser.

    Wouldnโ€™t it be better if you could bring the documentation right into your workflowโ€”integrated into your apps, your IDE, or even your own custom tools?

    In this case study, weโ€™ll explore how to do exactly that by connecting directly to the Microsoft Learn Docs MCP server from your own client application.

    Overview

    Modern development is more than just writing codeโ€”itโ€™s about finding the right information at the right time.

    Documentation is everywhere, but itโ€™s rarely where you need it most: inside your tools and workflows.

    By integrating documentation retrieval directly into your applications, you can save time, reduce context switching, and boost productivity.

    In this section, weโ€™ll show you how to connect a client to the Microsoft Learn Docs MCP server, so you can access real-time, context-aware documentation without ever leaving your app.

    Weโ€™ll walk through the process of establishing a connection, sending a request, and handling streaming responses efficiently. This approach not only streamlines your workflow but also opens the door to building smarter, more helpful developer tools.

    Learning Objectives

    Why are we doing this?

    Because the best developer experiences are those that remove friction.

    Imagine a world where your code editor, chatbot, or web app can answer your documentation questions instantly, using the latest content from Microsoft Learn.

    By the end of this chapter, youโ€™ll know how to:

  • Understand the basics of MCP server-client communication for documentation
  • Implement a console or web application to connect to the Microsoft Learn Docs MCP server
  • Use streaming HTTP clients for real-time documentation retrieval
  • Log and interpret documentation responses in your application
  • Youโ€™ll see how these skills can help you build tools that are not just reactive, but truly interactive and context-aware.

    Scenario 1 - Real-Time Documentation Retrieval with MCP

    In this scenario, weโ€™ll show you how to connect a client to the Microsoft Learn Docs MCP server, so you can access real-time, context-aware documentation without ever leaving your app.

    Letโ€™s put this into practice.

    Your task is to write an app that connects to the Microsoft Learn Docs MCP server, invokes the microsoft_docs_search tool, and logs the streaming response to the console.

    Why this approach?

    Because itโ€™s the foundation for building more advanced integrationsโ€”whether you want to power a chatbot, an IDE extension, or a web dashboard.

    You'll find the code and instructions for this scenario in the solution folder within this case study.

    The steps will guide you through setting up the connection:

  • Use the official MCP SDK and streamable HTTP client for connection
  • Call the microsoft_docs_search tool with a query parameter to retrieve documentation
  • Implement proper logging and error handling
  • Create an interactive console interface to allow users to enter multiple search queries
  • This scenario demonstrates how to:

  • Connect to the Docs MCP server
  • Send a query
  • Parse and print the results
  • Hereโ€™s what running the solution might look like:

    
    Prompt> What is Azure Key Vault?
    
    Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
    
    

    Below is a minimal sample solution. The full code and details are available in the solution folder.

    Python

    
    import asyncio
    
    from mcp.client.streamable_http import streamablehttp_client
    
    from mcp import ClientSession
    
    
    
    async def main():
    
        async with streamablehttp_client("https://learn.microsoft.com/api/mcp") as (read_stream, write_stream, _):
    
            async with ClientSession(read_stream, write_stream) as session:
    
                await session.initialize()
    
                result = await session.call_tool("microsoft_docs_search", {"query": "Azure Functions best practices"})
    
                print(result.content)
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
  • For the complete implementation and logging, see scenario1.py.
  • For installation and usage instructions, see the README.md file in the same folder.
  • Scenario 2 - Interactive Study Plan Generator Web App with MCP

    In this scenario, youโ€™ll learn how to integrate Docs MCP into a web development project.

    The goal is to enable users to search Microsoft Learn documentation directly from a web interface, making documentation instantly accessible within your app or site.

    Youโ€™ll see how to:

  • Set up a web app
  • Connect to the Docs MCP server
  • Handle user input and display results
  • Hereโ€™s what running the solution might look like:

    
    User> I want to learn about AI102 - so suggest the roadmap to get it started from learn for 6 weeks
    
    
    
    Assistant> Hereโ€™s a detailed 6-week roadmap to start your preparation for the AI-102: Designing and Implementing a Microsoft Azure AI Solution certification, using official Microsoft resources and focusing on exam skills areas:
    
    
    
    ---
    
    ## Week 1: Introduction & Fundamentals
    
    - **Understand the Exam**: Review the [AI-102 exam skills outline](https://learn.microsoft.com/en-us/credentials/certifications/exams/ai-102/).
    
    - **Set up Azure**: Sign up for a free Azure account if you don't have one.
    
    - **Learning Path**: [Introduction to Azure AI services](https://learn.microsoft.com/en-us/training/modules/intro-to-azure-ai/)
    
    - **Focus**: Get familiar with Azure portal, AI capabilities, and necessary tools.
    
    
    
    ....more weeks of the roadmap...
    
    
    
    Let me know if you want module-specific recommendations or need more customized weekly tasks!
    
    

    Below is a minimal sample solution. The full code and details are available in the solution folder.

    Python (Chainlit)

    Chainlit is a framework for building conversational AI web apps. It makes it easy to create interactive chatbots and assistants that can call MCP tools and display results in real time. Itโ€™s ideal for rapid prototyping and user-friendly interfaces.

    
    import chainlit as cl
    
    import requests
    
    
    
    MCP_URL = "https://learn.microsoft.com/api/mcp"
    
    
    
    @cl.on_message
    
    def handle_message(message):
    
        query = {"question": message}
    
        response = requests.post(MCP_URL, json=query)
    
        if response.ok:
    
            result = response.json()
    
            cl.Message(content=result.get("answer", "No answer found.")).send()
    
        else:
    
            cl.Message(content="Error: " + response.text).send()
    
    
  • For the complete implementation, see scenario2.py.
  • For setup and running instructions, see the README.md.
  • Scenario 3: In-Editor Docs with MCP Server in VS Code

    If you want to get Microsoft Learn Docs directly inside your VS Code (instead of switching browser tabs), you can use the MCP server in your editor. This allows you to:

  • Search and read docs in VS Code without leaving your coding environment.
  • Reference documentation and insert links directly into your README or course files.
  • Leverage GitHub Copilot and MCP together for a seamless, AI-powered documentation workflow.
  • You'll see how to:

  • Add a valid .vscode/mcp.json file to your workspace root (see example below).
  • Open the MCP panel or use the command palette in VS Code to search and insert docs.
  • Reference documentation directly in your markdown files as you work.
  • Combine this workflow with GitHub Copilot for even greater productivity.
  • Hereโ€™s a example of how to set up the MCP server in VS Code:

    
    {
    
      "servers": {
    
        "LearnDocsMCP": {
    
          "url": "https://learn.microsoft.com/api/mcp"
    
        }
    
      }
    
    }
    
    

    > For a detailed walkthrough with screenshots and step-by-step guide, see README.md.

    This approach is ideal for anyone building technical courses, writing documentation, or developing code with frequent reference needs.

    Key Takeaways

    Integrating documentation directly into your tools isnโ€™t just a convenienceโ€”itโ€™s a game changer for productivity. By connecting to the Microsoft Learn Docs MCP server from your client, you can:

  • Eliminate context switching between your code and documentation
  • Retrieve up-to-date, context-aware docs in real time
  • Build smarter, more interactive developer tools
  • These skills will help you create solutions that are not only efficient, but also delightful to use.

    Additional Resources

    To deepen your understanding, explore these official resources:

  • Microsoft Learn Docs MCP Server (GitHub)
  • Get started with Azure MCP Server (mcp-python)
  • What is the Azure MCP Server?
  • Model Context Protocol (MCP) Introduction
  • Add plugins from a MCP Server (Python)
  • What's Next

  • Back to: Case Studies Overview
  • Continue to: Module 10: Streamlining AI Workflows with AI Toolkit
  • This case study demonstrates how to build an interactive web application using Chainlit and the Model Context Protocol (MCP) to generate personalized study plans for any topic.

    Users can specify a subject (such as "AI-900 certification") and a study duration (e.g., 8 weeks), and the app will provide a week-by-week breakdown of recommended content.

    Chainlit enables a conversational chat interface, making the experience engaging and adaptive.

  • Conversational web app powered by Chainlit
  • User-driven prompts for topic and duration
  • Week-by-week content recommendations using MCP
  • Real-time, adaptive responses in a chat interface
  • The project illustrates how conversational AI and MCP can be combined to create dynamic, user-driven educational tools in a modern web environment.

    5. In-Editor Docs with MCP Server in VS Code

    Case Study: Connecting to the Microsoft Learn Docs MCP Server from a Client

    Have you ever found yourself juggling between documentation sites, Stack Overflow, and endless search engine tabs, all while trying to solve a problem in your code?

    Maybe you keep a second monitor just for docs, or youโ€™re constantly alt-tabbing between your IDE and a browser.

    Wouldnโ€™t it be better if you could bring the documentation right into your workflowโ€”integrated into your apps, your IDE, or even your own custom tools?

    In this case study, weโ€™ll explore how to do exactly that by connecting directly to the Microsoft Learn Docs MCP server from your own client application.

    Overview

    Modern development is more than just writing codeโ€”itโ€™s about finding the right information at the right time.

    Documentation is everywhere, but itโ€™s rarely where you need it most: inside your tools and workflows.

    By integrating documentation retrieval directly into your applications, you can save time, reduce context switching, and boost productivity.

    In this section, weโ€™ll show you how to connect a client to the Microsoft Learn Docs MCP server, so you can access real-time, context-aware documentation without ever leaving your app.

    Weโ€™ll walk through the process of establishing a connection, sending a request, and handling streaming responses efficiently. This approach not only streamlines your workflow but also opens the door to building smarter, more helpful developer tools.

    Learning Objectives

    Why are we doing this?

    Because the best developer experiences are those that remove friction.

    Imagine a world where your code editor, chatbot, or web app can answer your documentation questions instantly, using the latest content from Microsoft Learn.

    By the end of this chapter, youโ€™ll know how to:

  • Understand the basics of MCP server-client communication for documentation
  • Implement a console or web application to connect to the Microsoft Learn Docs MCP server
  • Use streaming HTTP clients for real-time documentation retrieval
  • Log and interpret documentation responses in your application
  • Youโ€™ll see how these skills can help you build tools that are not just reactive, but truly interactive and context-aware.

    Scenario 1 - Real-Time Documentation Retrieval with MCP

    In this scenario, weโ€™ll show you how to connect a client to the Microsoft Learn Docs MCP server, so you can access real-time, context-aware documentation without ever leaving your app.

    Letโ€™s put this into practice.

    Your task is to write an app that connects to the Microsoft Learn Docs MCP server, invokes the microsoft_docs_search tool, and logs the streaming response to the console.

    Why this approach?

    Because itโ€™s the foundation for building more advanced integrationsโ€”whether you want to power a chatbot, an IDE extension, or a web dashboard.

    You'll find the code and instructions for this scenario in the solution folder within this case study.

    The steps will guide you through setting up the connection:

  • Use the official MCP SDK and streamable HTTP client for connection
  • Call the microsoft_docs_search tool with a query parameter to retrieve documentation
  • Implement proper logging and error handling
  • Create an interactive console interface to allow users to enter multiple search queries
  • This scenario demonstrates how to:

  • Connect to the Docs MCP server
  • Send a query
  • Parse and print the results
  • Hereโ€™s what running the solution might look like:

    
    Prompt> What is Azure Key Vault?
    
    Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
    
    

    Below is a minimal sample solution. The full code and details are available in the solution folder.

    Python

    
    import asyncio
    
    from mcp.client.streamable_http import streamablehttp_client
    
    from mcp import ClientSession
    
    
    
    async def main():
    
        async with streamablehttp_client("https://learn.microsoft.com/api/mcp") as (read_stream, write_stream, _):
    
            async with ClientSession(read_stream, write_stream) as session:
    
                await session.initialize()
    
                result = await session.call_tool("microsoft_docs_search", {"query": "Azure Functions best practices"})
    
                print(result.content)
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
  • For the complete implementation and logging, see scenario1.py.
  • For installation and usage instructions, see the README.md file in the same folder.
  • Scenario 2 - Interactive Study Plan Generator Web App with MCP

    In this scenario, youโ€™ll learn how to integrate Docs MCP into a web development project.

    The goal is to enable users to search Microsoft Learn documentation directly from a web interface, making documentation instantly accessible within your app or site.

    Youโ€™ll see how to:

  • Set up a web app
  • Connect to the Docs MCP server
  • Handle user input and display results
  • Hereโ€™s what running the solution might look like:

    
    User> I want to learn about AI102 - so suggest the roadmap to get it started from learn for 6 weeks
    
    
    
    Assistant> Hereโ€™s a detailed 6-week roadmap to start your preparation for the AI-102: Designing and Implementing a Microsoft Azure AI Solution certification, using official Microsoft resources and focusing on exam skills areas:
    
    
    
    ---
    
    ## Week 1: Introduction & Fundamentals
    
    - **Understand the Exam**: Review the [AI-102 exam skills outline](https://learn.microsoft.com/en-us/credentials/certifications/exams/ai-102/).
    
    - **Set up Azure**: Sign up for a free Azure account if you don't have one.
    
    - **Learning Path**: [Introduction to Azure AI services](https://learn.microsoft.com/en-us/training/modules/intro-to-azure-ai/)
    
    - **Focus**: Get familiar with Azure portal, AI capabilities, and necessary tools.
    
    
    
    ....more weeks of the roadmap...
    
    
    
    Let me know if you want module-specific recommendations or need more customized weekly tasks!
    
    

    Below is a minimal sample solution. The full code and details are available in the solution folder.

    Python (Chainlit)

    Chainlit is a framework for building conversational AI web apps. It makes it easy to create interactive chatbots and assistants that can call MCP tools and display results in real time. Itโ€™s ideal for rapid prototyping and user-friendly interfaces.

    
    import chainlit as cl
    
    import requests
    
    
    
    MCP_URL = "https://learn.microsoft.com/api/mcp"
    
    
    
    @cl.on_message
    
    def handle_message(message):
    
        query = {"question": message}
    
        response = requests.post(MCP_URL, json=query)
    
        if response.ok:
    
            result = response.json()
    
            cl.Message(content=result.get("answer", "No answer found.")).send()
    
        else:
    
            cl.Message(content="Error: " + response.text).send()
    
    
  • For the complete implementation, see scenario2.py.
  • For setup and running instructions, see the README.md.
  • Scenario 3: In-Editor Docs with MCP Server in VS Code

    If you want to get Microsoft Learn Docs directly inside your VS Code (instead of switching browser tabs), you can use the MCP server in your editor. This allows you to:

  • Search and read docs in VS Code without leaving your coding environment.
  • Reference documentation and insert links directly into your README or course files.
  • Leverage GitHub Copilot and MCP together for a seamless, AI-powered documentation workflow.
  • You'll see how to:

  • Add a valid .vscode/mcp.json file to your workspace root (see example below).
  • Open the MCP panel or use the command palette in VS Code to search and insert docs.
  • Reference documentation directly in your markdown files as you work.
  • Combine this workflow with GitHub Copilot for even greater productivity.
  • Hereโ€™s a example of how to set up the MCP server in VS Code:

    
    {
    
      "servers": {
    
        "LearnDocsMCP": {
    
          "url": "https://learn.microsoft.com/api/mcp"
    
        }
    
      }
    
    }
    
    

    > For a detailed walkthrough with screenshots and step-by-step guide, see README.md.

    This approach is ideal for anyone building technical courses, writing documentation, or developing code with frequent reference needs.

    Key Takeaways

    Integrating documentation directly into your tools isnโ€™t just a convenienceโ€”itโ€™s a game changer for productivity. By connecting to the Microsoft Learn Docs MCP server from your client, you can:

  • Eliminate context switching between your code and documentation
  • Retrieve up-to-date, context-aware docs in real time
  • Build smarter, more interactive developer tools
  • These skills will help you create solutions that are not only efficient, but also delightful to use.

    Additional Resources

    To deepen your understanding, explore these official resources:

  • Microsoft Learn Docs MCP Server (GitHub)
  • Get started with Azure MCP Server (mcp-python)
  • What is the Azure MCP Server?
  • Model Context Protocol (MCP) Introduction
  • Add plugins from a MCP Server (Python)
  • What's Next

  • Back to: Case Studies Overview
  • Continue to: Module 10: Streamlining AI Workflows with AI Toolkit
  • This case study demonstrates how you can bring Microsoft Learn Docs directly into your VS Code environment using the MCP serverโ€”no more switching browser tabs! You'll see how to:

  • Instantly search and read docs inside VS Code using the MCP panel or command palette
  • Reference documentation and insert links directly into your README or course markdown files
  • Use GitHub Copilot and MCP together for seamless, AI-powered documentation and code workflows
  • Validate and enhance your documentation with real-time feedback and Microsoft-sourced accuracy
  • Integrate MCP with GitHub workflows for continuous documentation validation
  • The implementation includes:

  • Example .vscode/mcp.json configuration for easy setup
  • Screenshot-based walkthroughs of the in-editor experience
  • Tips for combining Copilot and MCP for maximum productivity
  • This scenario is ideal for course authors, documentation writers, and developers who want to stay focused in their editor while working with docs, Copilot, and validation toolsโ€”all powered by MCP.

    6. APIM MCP Server Creation

    This case study provides a step-by-step guide on how to create an MCP server using Azure API Management (APIM). It covers:

  • Setting up an MCP server in Azure API Management
  • Exposing API operations as MCP tools
  • Configuring policies for rate limiting and security
  • Testing the MCP server using Visual Studio Code and GitHub Copilot
  • This example illustrates how to leverage Azure's capabilities to create a robust MCP server that can be used in various applications, enhancing the integration of AI systems with enterprise APIs.

    7. GitHub MCP Registry โ€” Accelerating Agentic Integration

    This case study examines how GitHub's MCP Registry, launched in September 2025, addresses a critical challenge in the AI ecosystem: the fragmented discovery and deployment of Model Context Protocol (MCP) servers.

    Overview

    The MCP Registry solves the growing pain of scattered MCP servers across repositories and registries, which previously made integration slow and error-prone.

    These servers enable AI agents to interact with external systems like APIs, databases, and documentation sources.

    Problem Statement

    Developers building agentic workflows faced several challenges:

  • Poor discoverability of MCP servers across different platforms
  • Redundant setup questions scattered across forums and documentation
  • Security risks from unverified and untrusted sources
  • Lack of standardization in server quality and compatibility
  • Solution Architecture

    GitHub's MCP Registry centralizes trusted MCP servers with key features:

  • One-click install integration via VS Code for streamlined setup
  • Signal-over-noise sorting by stars, activity, and community validation
  • Direct integration with GitHub Copilot and other MCP-compatible tools
  • Open contribution model enabling both community and enterprise partners to contribute
  • Business Impact

    The registry has delivered measurable improvements:

  • Faster onboarding for developers using tools like the Microsoft Learn MCP Server, which streams official documentation directly into agents
  • Improved productivity via specialized servers like github-mcp-server, enabling natural language GitHub automation (PR creation, CI reruns, code scanning)
  • Stronger ecosystem trust through curated listings and transparent configuration standards
  • Strategic Value

    For practitioners specializing in agent lifecycle management and reproducible workflows, the MCP Registry provides:

  • Modular agent deployment capabilities with standardized components
  • Registry-backed evaluation pipelines for consistent testing and validation
  • Cross-tool interoperability enabling seamless integration across different AI platforms
  • This case study demonstrates that the MCP Registry is more than just a directoryโ€”it's a foundational platform for scalable, real-world model integration and agentic system deployment.

    Conclusion

    These seven comprehensive case studies demonstrate the remarkable versatility and practical applications of the Model Context Protocol across diverse real-world scenarios.

    From complex multi-agent travel planning systems and enterprise API management to streamlined documentation workflows and the revolutionary GitHub MCP Registry, these examples showcase how MCP provides a standardized, scalable way to connect AI systems with the tools, data, and services they need to deliver exceptional value.

    The case studies span multiple dimensions of MCP implementation:

  • Enterprise Integration: Azure API Management and Azure DevOps automation
  • Multi-Agent Orchestration: Travel planning with coordinated AI agents
  • Developer Productivity: VS Code integration and real-time documentation access
  • Ecosystem Development: GitHub's MCP Registry as a foundational platform
  • Educational Applications: Interactive study plan generators and conversational interfaces
  • By studying these implementations, you gain critical insights into:

  • Architectural patterns for different scales and use cases
  • Implementation strategies that balance functionality with maintainability
  • Security and scalability considerations for production deployments
  • Best practices for MCP server development and client integration
  • Ecosystem thinking for building interconnected AI-powered solutions
  • These examples collectively demonstrate that MCP is not merely a theoretical framework but a mature, production-ready protocol enabling practical solutions to complex business challenges.

    Whether you're building simple automation tools or sophisticated multi-agent systems, the patterns and approaches illustrated here provide a solid foundation for your own MCP projects.

    Additional Resources

  • Azure AI Travel Agents GitHub Repository
  • Azure DevOps MCP Tool
  • Playwright MCP Tool
  • Microsoft Docs MCP Server
  • GitHub MCP Registry โ€” Accelerating Agentic Integration
  • MCP Community Examples
  • What's Next

  • Previous: Module 8: Best Practices
  • Next: Module 10: Streamlining AI Workflows: Building an MCP Server with AI Toolkit
  • code Module 10

    AI Toolkit

    Streamlining AI Workflows: Building an MCP Server with AI Toolkit

    ๐ŸŽฏ Overview

    _(Click the image above to view video of this lesson)_

    Welcome to the Model Context Protocol (MCP) Workshop! This comprehensive hands-on workshop combines two cutting-edge technologies to revolutionize AI application development:

  • ๐Ÿ”— Model Context Protocol (MCP): An open standard for seamless AI-tool integration
  • ๐Ÿ› ๏ธ AI Toolkit for Visual Studio Code (AITK): Microsoft's powerful AI development extension
  • ๐ŸŽ“ What You'll Learn

    By the end of this workshop, you'll master the art of building intelligent applications that bridge AI models with real-world tools and services.

    From automated testing to custom API integrations, you'll gain practical skills to solve complex business challenges.

    ๐Ÿ—๏ธ Technology Stack

    ๐Ÿ”Œ Model Context Protocol (MCP)

    MCP is the "USB-C for AI" - a universal standard that connects AI models to external tools and data sources.

    โœจ Key Features:

  • ๐Ÿ”„ Standardized Integration: Universal interface for AI-tool connections
  • ๐Ÿ›๏ธ Flexible Architecture: Local & remote servers via stdio/SSE transport
  • ๐Ÿงฐ Rich Ecosystem: Tools, prompts, and resources in one protocol
  • ๐Ÿ”’ Enterprise-Ready: Built-in security and reliability
  • ๐ŸŽฏ Why MCP Matters:

    Just like USB-C eliminated cable chaos, MCP eliminates the complexity of AI integrations. One protocol, infinite possibilities.

    ๐Ÿค– AI Toolkit for Visual Studio Code (AITK)

    Microsoft's flagship AI development extension that transforms VS Code into an AI powerhouse.

    ๐Ÿš€ Core Capabilities:

  • ๐Ÿ“ฆ Model Catalog: Access models from Azure AI, GitHub, Hugging Face, Ollama
  • โšก Local Inference: ONNX-optimized CPU/GPU/NPU execution
  • ๐Ÿ—๏ธ Agent Builder: Visual AI agent development with MCP integration
  • ๐ŸŽญ Multi-Modal: Text, vision, and structured output support
  • ๐Ÿ’ก Development Benefits:

  • Zero-config model deployment
  • Visual prompt engineering
  • Real-time testing playground
  • Seamless MCP server integration
  • ๐Ÿ“š Learning Journey

    ๐Ÿš€ Module 1: AI Toolkit Fundamentals

    ๐Ÿš€ Module 1: AI Toolkit Fundamentals

    ๐Ÿ“‹ Learning Objectives

    By the end of this module, you will be able to:

  • โœ… Install and configure AI Toolkit for Visual Studio Code
  • โœ… Navigate the Model Catalog and understand different model sources
  • โœ… Use the Playground for model testing and experimentation
  • โœ… Create custom AI agents using Agent Builder
  • โœ… Compare model performance across different providers
  • โœ… Apply best practices for prompt engineering
  • ๐Ÿง  Introduction to AI Toolkit (AITK)

    The AI Toolkit for Visual Studio Code is Microsoft's flagship extension that transforms VS Code into a comprehensive AI development environment.

    It bridges the gap between AI research and practical application development, making generative AI accessible to developers of all skill levels.

    ๐ŸŒŸ Key Capabilities

    Feature Description Use Case --------- ------------- ---------- ๐Ÿ—‚๏ธ Model Catalog Access 100+ models from GitHub, ONNX, OpenAI, Anthropic, Google Model discovery and selection ๐Ÿ”Œ BYOM Support Integrate your own models (local/remote) Custom model deployment ๐ŸŽฎ Interactive Playground Real-time model testing with chat interface Rapid prototyping and testing ๐Ÿ“Ž Multi-Modal Support Handle text, images, and attachments Complex AI applications โšก Batch Processing Run multiple prompts simultaneously Efficient testing workflows ๐Ÿ“Š Model Evaluation Built-in metrics (F1, relevance, similarity, coherence) Performance assessment

    ๐ŸŽฏ Why AI Toolkit Matters

  • ๐Ÿš€ Accelerated Development: From idea to prototype in minutes
  • ๐Ÿ”„ Unified Workflow: One interface for multiple AI providers
  • ๐Ÿงช Easy Experimentation: Compare models without complex setup
  • ๐Ÿ“ˆ Production Ready: Seamless transition from prototype to deployment
  • ๐Ÿ› ๏ธ Prerequisites & Setup

    ๐Ÿ“ฆ Install AI Toolkit Extension

    Step 1: Access Extensions Marketplace

    1. Open Visual Studio Code

    2. Navigate to the Extensions view (Ctrl+Shift+X or Cmd+Shift+X)

    3. Search for "AI Toolkit"

    Step 2: Choose Your Version

  • ๐ŸŸข Release: Recommended for production use
  • ๐Ÿ”ถ Pre-release: Early access to cutting-edge features
  • Step 3: Install and Activate

    โœ… Verification Checklist

  • [ ] AI Toolkit icon appears in the VS Code sidebar
  • [ ] Extension is enabled and activated
  • [ ] No installation errors in the output panel
  • ๐Ÿงช Hands-on Exercise 1: Exploring GitHub Models

    ๐ŸŽฏ Objective: Master the Model Catalog and test your first AI model

    ๐Ÿ“Š Step 1: Navigate the Model Catalog

    The Model Catalog is your gateway to the AI ecosystem. It aggregates models from multiple providers, making it easy to discover and compare options.

    ๐Ÿ” Navigation Guide:

    Click on MODELS - Catalog in the AI Toolkit sidebar

    ๐Ÿ’ก Pro Tip: Look for models with specific capabilities that match your use case (e.g., code generation, creative writing, analysis).

    โš ๏ธ Note: GitHub-hosted models (i.e.

    GitHub Models) are free to use but are subject to rate limits on requests and tokens.

    If you want to access non-GitHub models (that is, external models hosted via Azure AI or other endpoints), you'll need to supply the appropriate API key or authentication.

    ๐Ÿš€ Step 2: Add and Configure Your First Model

    Model Selection Strategy:

  • GPT-4.1: Best for complex reasoning and analysis
  • Phi-4-mini: Lightweight, fast responses for simple tasks
  • ๐Ÿ”ง Configuration Process:

    1. Select OpenAI GPT-4.1 from the catalog

    2. Click Add to My Models - this registers the model for use

    3. Choose Try in Playground to launch the testing environment

    4. Wait for model initialization (first-time setup may take a moment)

    โš™๏ธ Understanding Model Parameters:

  • Temperature: Controls creativity (0 = deterministic, 1 = creative)
  • Max Tokens: Maximum response length
  • Top-p: Nucleus sampling for response diversity
  • ๐ŸŽฏ Step 3: Master the Playground Interface

    The Playground is your AI experimentation lab. Here's how to maximize its potential:

    ๐ŸŽจ Prompt Engineering Best Practices:

    1. Be Specific: Clear, detailed instructions yield better results

    2. Provide Context: Include relevant background information

    3. Use Examples: Show the model what you want with examples

    4. Iterate: Refine prompts based on initial results

    ๐Ÿงช Testing Scenarios:

    
    # Example 1: Code Generation
    
    "Write a Python function that calculates the factorial of a number using recursion. Include error handling and docstrings."
    
    
    
    # Example 2: Creative Writing
    
    "Write a professional email to a client explaining a project delay, maintaining a positive tone while being transparent about challenges."
    
    
    
    # Example 3: Data Analysis
    
    "Analyze this sales data and provide insights: [paste your data]. Focus on trends, anomalies, and actionable recommendations."
    
    

    ๐Ÿ† Challenge Exercise: Model Performance Comparison

    ๐ŸŽฏ Goal: Compare different models using identical prompts to understand their strengths

    ๐Ÿ“‹ Instructions:

    1. Add Phi-4-mini to your workspace

    2. Use the same prompt for both GPT-4.1 and Phi-4-mini

    3. Compare response quality, speed, and accuracy

    4. Document your findings in the results section

    ๐Ÿ’ก Key Insights to Discover:

  • When to use LLM vs SLM
  • Cost vs. performance trade-offs
  • Specialized capabilities of different models
  • ๐Ÿค– Hands-on Exercise 2: Building Custom Agents with Agent Builder

    ๐ŸŽฏ Objective: Create specialized AI agents tailored for specific tasks and workflows

    ๐Ÿ—๏ธ Step 1: Understanding Agent Builder

    Agent Builder is where AI Toolkit truly shines. It allows you to create purpose-built AI assistants that combine the power of large language models with custom instructions, specific parameters, and specialized knowledge.

    ๐Ÿง  Agent Architecture Components:

  • Core Model: The foundation LLM (GPT-4, Groks, Phi, etc.)
  • System Prompt: Defines agent personality and behavior
  • Parameters: Fine-tuned settings for optimal performance
  • Tools Integration: Connect to external APIs and MCP services
  • Memory: Conversation context and session persistence
  • โš™๏ธ Step 2: Agent Configuration Deep Dive

    ๐ŸŽจ Creating Effective System Prompts:

    
    # Template Structure:
    
    ## Role Definition
    
    You are a [specific role] with expertise in [domain].
    
    
    
    ## Capabilities
    
    - List specific abilities
    
    - Define scope of knowledge
    
    - Clarify limitations
    
    
    
    ## Behavior Guidelines
    
    - Response style (formal, casual, technical)
    
    - Output format preferences
    
    - Error handling approach
    
    
    
    ## Examples
    
    Provide 2-3 examples of ideal interactions
    
    

    *Of course, you can also use Generate System Prompt to use AI to help you generate and optimize prompts*

    ๐Ÿ”ง Parameter Optimization:

    Parameter Recommended Range Use Case ----------- ------------------ ---------- Temperature 0.1-0.3 Technical/factual responses Temperature 0.7-0.9 Creative/brainstorming tasks Max Tokens 500-1000 Concise responses Max Tokens 2000-4000 Detailed explanations

    ๐Ÿ Step 3: Practical Exercise - Python Programming Agent

    ๐ŸŽฏ Mission: Create a specialized Python coding assistant

    ๐Ÿ“‹ Configuration Steps:

    1. Model Selection: Choose Claude 3.5 Sonnet (excellent for code)

    2. System Prompt Design:

    
    # Python Programming Expert Agent
    
    
    
    ## Role
    
    You are a senior Python developer with 10+ years of experience. You excel at writing clean, efficient, and well-documented Python code.
    
    
    
    ## Capabilities
    
    - Write production-ready Python code
    
    - Debug complex issues
    
    - Explain code concepts clearly
    
    - Suggest best practices and optimizations
    
    - Provide complete working examples
    
    
    
    ## Response Format
    
    - Always include docstrings
    
    - Add inline comments for complex logic
    
    - Suggest testing approaches
    
    - Mention relevant libraries when applicable
    
    
    
    ## Code Quality Standards
    
    - Follow PEP 8 style guidelines
    
    - Use type hints where appropriate
    
    - Handle exceptions gracefully
    
    - Write readable, maintainable code
    
    

    3. Parameter Configuration:

    - Temperature: 0.2 (for consistent, reliable code)

    - Max Tokens: 2000 (detailed explanations)

    - Top-p: 0.9 (balanced creativity)

    ๐Ÿงช Step 4: Testing Your Python Agent

    Test Scenarios:

    1. Basic Function: "Create a function to find prime numbers"

    2. Complex Algorithm: "Implement a binary search tree with insert, delete, and search methods"

    3. Real-world Problem: "Build a web scraper that handles rate limiting and retries"

    4. Debugging: "Fix this code [paste buggy code]"

    ๐Ÿ† Success Criteria:

  • โœ… Code runs without errors
  • โœ… Includes proper documentation
  • โœ… Follows Python best practices
  • โœ… Provides clear explanations
  • โœ… Suggests improvements
  • ๐ŸŽ“ Module 1 Wrap-Up & Next Steps

    ๐Ÿ“Š Knowledge Check

    Test your understanding:

  • [ ] Can you explain the difference between models in the catalog?
  • [ ] Have you successfully created and tested a custom agent?
  • [ ] Do you understand how to optimize parameters for different use cases?
  • [ ] Can you design effective system prompts?
  • ๐Ÿ“š Additional Resources

  • AI Toolkit Documentation: Official Microsoft Docs
  • Prompt Engineering Guide: Best Practices
  • Models in AI Toolkit: Models in Develpment
  • ๐ŸŽ‰ Congratulations! You've mastered the fundamentals of AI Toolkit and are ready to build more advanced AI applications!

    ๐Ÿ”œ Continue to Next Module

    Ready for more advanced capabilities? Continue to Module 2: MCP with AI Toolkit Fundamentals where you'll learn how to:

  • Connect your agents to external tools using Model Context Protocol (MCP)
  • Build browser automation agents with Playwright
  • Integrate MCP servers with your AI Toolkit agents
  • Supercharge your agents with external data and capabilities
  • Duration: 15 minutes

  • ๐Ÿ› ๏ธ Install and configure AI Toolkit for VS Code
  • ๐Ÿ—‚๏ธ Explore the Model Catalog (100+ models from GitHub, ONNX, OpenAI, Anthropic, Google)
  • ๐ŸŽฎ Master the Interactive Playground for real-time model testing
  • ๐Ÿค– Build your first AI agent with Agent Builder
  • ๐Ÿ“Š Evaluate model performance with built-in metrics (F1, relevance, similarity, coherence)
  • โšก Learn batch processing and multi-modal support capabilities
  • ๐ŸŽฏ Learning Outcome: Create a functional AI agent with comprehensive understanding of AITK capabilities

    ๐ŸŒ Module 2: MCP with AI Toolkit Fundamentals

    ๐ŸŒ Module 2: MCP with AI Toolkit Fundamentals

    ๐Ÿ“‹ Learning Objectives

    By the end of this module, you will be able to:

  • โœ… Understand Model Context Protocol (MCP) architecture and benefits
  • โœ… Explore Microsoft's MCP server ecosystem
  • โœ… Integrate MCP servers with AI Toolkit Agent Builder
  • โœ… Build a functional browser automation agent using Playwright MCP
  • โœ… Configure and test MCP tools within your agents
  • โœ… Export and deploy MCP-powered agents for production use
  • ๐ŸŽฏ Building on Module 1

    In Module 1, we mastered AI Toolkit basics and created our first Python Agent.

    Now we'll supercharge your agents by connecting them to external tools and services through the revolutionary Model Context Protocol (MCP).

    Think of this as upgrading from a basic calculator to a full computer - your AI agents will gain the ability to:

  • ๐ŸŒ Browse and interact with websites
  • ๐Ÿ“ Access and manipulate files
  • ๐Ÿ”ง Integrate with enterprise systems
  • ๐Ÿ“Š Process real-time data from APIs
  • ๐Ÿง  Understanding Model Context Protocol (MCP)

    ๐Ÿ” What is MCP?

    Model Context Protocol (MCP) is the "USB-C for AI applications" - a revolutionary open standard that connects Large Language Models (LLMs) to external tools, data sources, and services.

    Just as USB-C eliminated cable chaos by providing one universal connector, MCP eliminates AI integration complexity with one standardized protocol.

    ๐ŸŽฏ The Problem MCP Solves

    Before MCP:

  • ๐Ÿ”ง Custom integrations for every tool
  • ๐Ÿ”„ Vendor lock-in with proprietary solutions
  • ๐Ÿ”’ Security vulnerabilities from ad-hoc connections
  • โฑ๏ธ Months of development for basic integrations
  • With MCP:

  • โšก Plug-and-play tool integration
  • ๐Ÿ”„ Vendor-agnostic architecture
  • ๐Ÿ›ก๏ธ Built-in security best practices
  • ๐Ÿš€ Minutes to add new capabilities
  • ๐Ÿ—๏ธ MCP Architecture Deep Dive

    MCP follows a client-server architecture that creates a secure, scalable ecosystem:

    
    graph TB
    
        A[AI Application/Agent] --> B[MCP Client]
    
        B --> C[MCP Server 1: Files]
    
        B --> D[MCP Server 2: Web APIs]
    
        B --> E[MCP Server 3: Database]
    
        B --> F[MCP Server N: Custom Tools]
    
        
    
        C --> G[Local File System]
    
        D --> H[External APIs]
    
        E --> I[Database Systems]
    
        F --> J[Enterprise Systems]
    
    

    ๐Ÿ”ง Core Components:

    Component Role Examples ----------- ------ ---------- MCP Hosts Applications that consume MCP services Claude Desktop, VS Code, AI Toolkit MCP Clients Protocol handlers (1:1 with servers) Built into host applications MCP Servers Expose capabilities via standard protocol Playwright, Files, Azure, GitHub Transport Layer Communication methods stdio, HTTP, WebSockets

    ๐Ÿข Microsoft's MCP Server Ecosystem

    Microsoft leads the MCP ecosystem with a comprehensive suite of enterprise-grade servers that address real-world business needs.

    ๐ŸŒŸ Featured Microsoft MCP Servers

    1. โ˜๏ธ Azure MCP Server

    ๐Ÿ”— Repository: azure/azure-mcp

    ๐ŸŽฏ Purpose: Comprehensive Azure resource management with AI integration

    โœจ Key Features:

  • Declarative infrastructure provisioning
  • Real-time resource monitoring
  • Cost optimization recommendations
  • Security compliance checking
  • ๐Ÿš€ Use Cases:

  • Infrastructure-as-Code with AI assistance
  • Automated resource scaling
  • Cloud cost optimization
  • DevOps workflow automation
  • 2. ๐Ÿ“Š Microsoft Dataverse MCP

    ๐Ÿ“š Documentation: Microsoft Dataverse Integration

    ๐ŸŽฏ Purpose: Natural language interface for business data

    โœจ Key Features:

  • Natural language database queries
  • Business context understanding
  • Custom prompt templates
  • Enterprise data governance
  • ๐Ÿš€ Use Cases:

  • Business intelligence reporting
  • Customer data analysis
  • Sales pipeline insights
  • Compliance data queries
  • 3. ๐ŸŒ Playwright MCP Server

    ๐Ÿ”— Repository: microsoft/playwright-mcp

    ๐ŸŽฏ Purpose: Browser automation and web interaction capabilities

    โœจ Key Features:

  • Cross-browser automation (Chrome, Firefox, Safari)
  • Intelligent element detection
  • Screenshot and PDF generation
  • Network traffic monitoring
  • ๐Ÿš€ Use Cases:

  • Automated testing workflows
  • Web scraping and data extraction
  • UI/UX monitoring
  • Competitive analysis automation
  • 4. ๐Ÿ“ Files MCP Server

    ๐Ÿ”— Repository: microsoft/files-mcp-server

    ๐ŸŽฏ Purpose: Intelligent file system operations

    โœจ Key Features:

  • Declarative file management
  • Content synchronization
  • Version control integration
  • Metadata extraction
  • ๐Ÿš€ Use Cases:

  • Documentation management
  • Code repository organization
  • Content publishing workflows
  • Data pipeline file handling
  • 5. ๐Ÿ“ MarkItDown MCP Server

    ๐Ÿ”— Repository: microsoft/markitdown

    ๐ŸŽฏ Purpose: Advanced Markdown processing and manipulation

    โœจ Key Features:

  • Rich Markdown parsing
  • Format conversion (MD โ†” HTML โ†” PDF)
  • Content structure analysis
  • Template processing
  • ๐Ÿš€ Use Cases:

  • Technical documentation workflows
  • Content management systems
  • Report generation
  • Knowledge base automation
  • 6. ๐Ÿ“ˆ Clarity MCP Server

    ๐Ÿ“ฆ Package: @microsoft/clarity-mcp-server

    ๐ŸŽฏ Purpose: Web analytics and user behavior insights

    โœจ Key Features:

  • Heatmap data analysis
  • User session recordings
  • Performance metrics
  • Conversion funnel analysis
  • ๐Ÿš€ Use Cases:

  • Website optimization
  • User experience research
  • A/B testing analysis
  • Business intelligence dashboards
  • ๐ŸŒ Community Ecosystem

    Beyond Microsoft's servers, the MCP ecosystem includes:

  • ๐Ÿ™ GitHub MCP: Repository management and code analysis
  • ๐Ÿ—„๏ธ Database MCPs: PostgreSQL, MySQL, MongoDB integrations
  • โ˜๏ธ Cloud Provider MCPs: AWS, GCP, Digital Ocean tools
  • ๐Ÿ“ง Communication MCPs: Slack, Teams, Email integrations
  • ๐Ÿ› ๏ธ Hands-On Lab: Building a Browser Automation Agent

    ๐ŸŽฏ Project Goal: Create an intelligent browser automation agent using Playwright MCP server that can navigate websites, extract information, and perform complex web interactions.

    ๐Ÿš€ Phase 1: Agent Foundation Setup

    Step 1: Initialize Your Agent

    1. Open AI Toolkit Agent Builder

    2. Create New Agent with the following configuration:

    - Name: BrowserAgent

    - Model: Choose GPT-4o

    ๐Ÿ”ง Phase 2: MCP Integration Workflow

    Step 3: Add MCP Server Integration

    1. Navigate to Tools Section in Agent Builder

    2. Click "Add Tool" to open the integration menu

    3. Select "MCP Server" from available options

    ๐Ÿ” Understanding Tool Types:

  • Built-in Tools: Pre-configured AI Toolkit functions
  • MCP Servers: External service integrations
  • Custom APIs: Your own service endpoints
  • Function Calling: Direct model function access
  • Step 4: MCP Server Selection

    1. Choose "MCP Server" option to proceed

    2. Browse MCP Catalog to explore available integrations

    ๐ŸŽฎ Phase 3: Playwright MCP Configuration

    Step 5: Select and Configure Playwright

    1. Click "Use Featured MCP Servers" to access Microsoft's verified servers

    2. Select "Playwright" from the featured list

    3. Accept Default MCP ID or customize for your environment

    Step 6: Enable Playwright Capabilities

    ๐Ÿ”‘ Critical Step: Select ALL available Playwright methods for maximum functionality

    ๐Ÿ› ๏ธ Essential Playwright Tools:

  • Navigation: goto, goBack, goForward, reload
  • Interaction: click, fill, press, hover, drag
  • Extraction: textContent, innerHTML, getAttribute
  • Validation: isVisible, isEnabled, waitForSelector
  • Capture: screenshot, pdf, video
  • Network: setExtraHTTPHeaders, route, waitForResponse
  • Step 7: Verify Integration Success

    โœ… Success Indicators:

  • All tools appear in Agent Builder interface
  • No error messages in the integration panel
  • Playwright server status shows "Connected"
  • ๐Ÿ”ง Troubleshooting Common Issues:

  • Connection Failed: Check internet connectivity and firewall settings
  • Missing Tools: Ensure all capabilities were selected during setup
  • Permission Errors: Verify VS Code has necessary system permissions
  • ๐ŸŽฏ Phase 4: Advanced Prompt Engineering

    Step 8: Design Intelligent System Prompts

    Create sophisticated prompts that leverage Playwright's full capabilities:

    
    # Web Automation Expert System Prompt
    
    
    
    ## Core Identity
    
    You are an advanced web automation specialist with deep expertise in browser automation, web scraping, and user experience analysis. You have access to Playwright tools for comprehensive browser control.
    
    
    
    ## Capabilities & Approach
    
    ### Navigation Strategy
    
    - Always start with screenshots to understand page layout
    
    - Use semantic selectors (text content, labels) when possible
    
    - Implement wait strategies for dynamic content
    
    - Handle single-page applications (SPAs) effectively
    
    
    
    ### Error Handling
    
    - Retry failed operations with exponential backoff
    
    - Provide clear error descriptions and solutions
    
    - Suggest alternative approaches when primary methods fail
    
    - Always capture diagnostic screenshots on errors
    
    
    
    ### Data Extraction
    
    - Extract structured data in JSON format when possible
    
    - Provide confidence scores for extracted information
    
    - Validate data completeness and accuracy
    
    - Handle pagination and infinite scroll scenarios
    
    
    
    ### Reporting
    
    - Include step-by-step execution logs
    
    - Provide before/after screenshots for verification
    
    - Suggest optimizations and alternative approaches
    
    - Document any limitations or edge cases encountered
    
    
    
    ## Ethical Guidelines
    
    - Respect robots.txt and rate limiting
    
    - Avoid overloading target servers
    
    - Only extract publicly available information
    
    - Follow website terms of service
    
    
    Step 9: Create Dynamic User Prompts

    Design prompts that demonstrate various capabilities:

    ๐ŸŒ Web Analysis Example:

    
    Navigate to github.com/kinfey and provide a comprehensive analysis including:
    
    1. Repository structure and organization
    
    2. Recent activity and contribution patterns  
    
    3. Documentation quality assessment
    
    4. Technology stack identification
    
    5. Community engagement metrics
    
    6. Notable projects and their purposes
    
    
    
    Include screenshots at key steps and provide actionable insights.
    
    

    ๐Ÿš€ Phase 5: Execution and Testing

    Step 10: Execute Your First Automation

    1. Click "Run" to launch the automation sequence

    2. Monitor Real-time Execution:

    - Chrome browser launches automatically

    - Agent navigates to target website

    - Screenshots capture each major step

    - Analysis results stream in real-time

    Step 11: Analyze Results and Insights

    Review comprehensive analysis in Agent Builder's interface:

    ๐ŸŒŸ Phase 6: Advanced Capabilities and Deployment

    Step 12: Export and Production Deployment

    Agent Builder supports multiple deployment options:

    ๐ŸŽ“ Module 2 Summary & Next Steps

    ๐Ÿ† Achievement Unlocked: MCP Integration Master

    โœ… Skills Mastered:

  • [ ] Understanding MCP architecture and benefits
  • [ ] Navigating Microsoft's MCP server ecosystem
  • [ ] Integrating Playwright MCP with AI Toolkit
  • [ ] Building sophisticated browser automation agents
  • [ ] Advanced prompt engineering for web automation
  • ๐Ÿ“š Additional Resources

  • ๐Ÿ”— MCP Specification: Official Protocol Documentation
  • ๐Ÿ› ๏ธ Playwright API: Complete Method Reference
  • ๐Ÿข Microsoft MCP Servers: Enterprise Integration Guide
  • ๐ŸŒ Community Examples: MCP Server Gallery
  • ๐ŸŽ‰ Congratulations! You've successfully mastered MCP integration and can now build production-ready AI agents with external tool capabilities!

    ๐Ÿ”œ Continue to Next Module

    Ready to take your MCP skills to the next level? Proceed to Module 3: Advanced MCP Development with AI Toolkit where you'll learn how to:

  • Create your own custom MCP servers
  • Configure and use the latest MCP Python SDK
  • Set up the MCP Inspector for debugging
  • Master advanced MCP server development workflows
  • Build a Weather MCP Server from scratch
  • Duration: 20 minutes

  • ๐Ÿง  Master Model Context Protocol (MCP) architecture and concepts
  • ๐ŸŒ Explore Microsoft's MCP server ecosystem
  • ๐Ÿค– Build a browser automation agent using Playwright MCP server
  • ๐Ÿ”ง Integrate MCP servers with AI Toolkit Agent Builder
  • ๐Ÿ“Š Configure and test MCP tools within your agents
  • ๐Ÿš€ Export and deploy MCP-powered agents for production use
  • ๐ŸŽฏ Learning Outcome: Deploy an AI agent supercharged with external tools through MCP

    ๐Ÿ”ง Module 3: Advanced MCP Development with AI Toolkit

    ๐Ÿ”ง Module 3: Advanced MCP Development with AI Toolkit

    ๐ŸŽฏ Learning Objectives

    By the end of this lab, you will be able to:

  • โœ… Create custom MCP servers using the AI Toolkit
  • โœ… Configure and use the latest MCP Python SDK (v1.9.3)
  • โœ… Set up and utilize the MCP Inspector for debugging
  • โœ… Debug MCP servers in both Agent Builder and Inspector environments
  • โœ… Understand advanced MCP server development workflows
  • ๐Ÿ“‹ Prerequisites

  • Completion of Lab 2 (MCP Fundamentals)
  • VS Code with AI Toolkit extension installed
  • Python 3.10+ environment
  • Node.js and npm for Inspector setup
  • ๐Ÿ—๏ธ What You'll Build

    In this lab, you'll create a Weather MCP Server that demonstrates:

  • Custom MCP server implementation
  • Integration with AI Toolkit Agent Builder
  • Professional debugging workflows
  • Modern MCP SDK usage patterns
  • ---

    ๐Ÿ”ง Core Components Overview

    ๐Ÿ MCP Python SDK

    The Model Context Protocol Python SDK provides the foundation for building custom MCP servers. You'll use version 1.9.3 with enhanced debugging capabilities.

    ๐Ÿ” MCP Inspector

    A powerful debugging tool that provides:

  • Real-time server monitoring
  • Tool execution visualization
  • Network request/response inspection
  • Interactive testing environment
  • ---

    ๐Ÿ“– Step-by-Step Implementation

    Step 1: Create a WeatherAgent in Agent Builder

    1. Launch Agent Builder in VS Code through the AI Toolkit extension

    2. Create a new agent with the following configuration:

    - Agent Name: WeatherAgent

    Step 2: Initialize MCP Server Project

    1. Navigate to Tools โ†’ Add Tool in Agent Builder

    2. Select "MCP Server" from the available options

    3. Choose "Create A new MCP Server"

    4. Select the python-weather template

    5. Name your server: weather_mcp

    Step 3: Open and Examine the Project

    1. Open the generated project in VS Code

    2. Review the project structure:

    ```

    weather_mcp/

    โ”œโ”€โ”€ src/

    โ”‚ โ”œโ”€โ”€ __init__.py

    โ”‚ โ””โ”€โ”€ server.py

    โ”œโ”€โ”€ inspector/

    โ”‚ โ”œโ”€โ”€ package.json

    โ”‚ โ””โ”€โ”€ package-lock.json

    โ”œโ”€โ”€ .vscode/

    โ”‚ โ”œโ”€โ”€ launch.json

    โ”‚ โ””โ”€โ”€ tasks.json

    โ”œโ”€โ”€ pyproject.toml

    โ””โ”€โ”€ README.md

    ```

    Step 4: Upgrade to Latest MCP SDK

    > ๐Ÿ” Why Upgrade? We want to use the latest MCP SDK (v1.9.3) and Inspector service (0.14.0) for enhanced features and better debugging capabilities.

    4a. Update Python Dependencies

    Edit pyproject.toml: update ./code/weather_mcp/pyproject.toml

    4b. Update Inspector Configuration

    Edit inspector/package.json: update ./code/weather_mcp/inspector/package.json

    4c. Update Inspector Dependencies

    Edit inspector/package-lock.json: update ./code/weather_mcp/inspector/package-lock.json

    > ๐Ÿ“ Note: This file contains extensive dependency definitions. Below is the essential structure - the full content ensures proper dependency resolution.

    > โšก Full Package Lock: The complete package-lock.json contains ~3000 lines of dependency definitions. The above shows the key structure - use the provided file for complete dependency resolution.

    Step 5: Configure VS Code Debugging

    *Note: Please copy the file in the specified path to replace the corresponding local file*

    5a. Update Launch Configuration

    Edit .vscode/launch.json:

    
    {
    
      "version": "0.2.0",
    
      "configurations": [
    
        {
    
          "name": "Attach to Local MCP",
    
          "type": "debugpy",
    
          "request": "attach",
    
          "connect": {
    
            "host": "localhost",
    
            "port": 5678
    
          },
    
          "presentation": {
    
            "hidden": true
    
          },
    
          "internalConsoleOptions": "neverOpen",
    
          "postDebugTask": "Terminate All Tasks"
    
        },
    
        {
    
          "name": "Launch Inspector (Edge)",
    
          "type": "msedge",
    
          "request": "launch",
    
          "url": "http://localhost:6274?timeout=60000&serverUrl=http://localhost:3001/sse#tools",
    
          "cascadeTerminateToConfigurations": [
    
            "Attach to Local MCP"
    
          ],
    
          "presentation": {
    
            "hidden": true
    
          },
    
          "internalConsoleOptions": "neverOpen"
    
        },
    
        {
    
          "name": "Launch Inspector (Chrome)",
    
          "type": "chrome",
    
          "request": "launch",
    
          "url": "http://localhost:6274?timeout=60000&serverUrl=http://localhost:3001/sse#tools",
    
          "cascadeTerminateToConfigurations": [
    
            "Attach to Local MCP"
    
          ],
    
          "presentation": {
    
            "hidden": true
    
          },
    
          "internalConsoleOptions": "neverOpen"
    
        }
    
      ],
    
      "compounds": [
    
        {
    
          "name": "Debug in Agent Builder",
    
          "configurations": [
    
            "Attach to Local MCP"
    
          ],
    
          "preLaunchTask": "Open Agent Builder",
    
        },
    
        {
    
          "name": "Debug in Inspector (Edge)",
    
          "configurations": [
    
            "Launch Inspector (Edge)",
    
            "Attach to Local MCP"
    
          ],
    
          "preLaunchTask": "Start MCP Inspector",
    
          "stopAll": true
    
        },
    
        {
    
          "name": "Debug in Inspector (Chrome)",
    
          "configurations": [
    
            "Launch Inspector (Chrome)",
    
            "Attach to Local MCP"
    
          ],
    
          "preLaunchTask": "Start MCP Inspector",
    
          "stopAll": true
    
        }
    
      ]
    
    }
    
    

    Edit .vscode/tasks.json:

    
    {
    
      "version": "2.0.0",
    
      "tasks": [
    
        {
    
          "label": "Start MCP Server",
    
          "type": "shell",
    
          "command": "python -m debugpy --listen 127.0.0.1:5678 src/__init__.py sse",
    
          "isBackground": true,
    
          "options": {
    
            "cwd": "${workspaceFolder}",
    
            "env": {
    
              "PORT": "3001"
    
            }
    
          },
    
          "problemMatcher": {
    
            "pattern": [
    
              {
    
                "regexp": "^.*$",
    
                "file": 0,
    
                "location": 1,
    
                "message": 2
    
              }
    
            ],
    
            "background": {
    
              "activeOnStart": true,
    
              "beginsPattern": ".*",
    
              "endsPattern": "Application startup complete|running"
    
            }
    
          }
    
        },
    
        {
    
          "label": "Start MCP Inspector",
    
          "type": "shell",
    
          "command": "npm run dev:inspector",
    
          "isBackground": true,
    
          "options": {
    
            "cwd": "${workspaceFolder}/inspector",
    
            "env": {
    
              "CLIENT_PORT": "6274",
    
              "SERVER_PORT": "6277",
    
            }
    
          },
    
          "problemMatcher": {
    
            "pattern": [
    
              {
    
                "regexp": "^.*$",
    
                "file": 0,
    
                "location": 1,
    
                "message": 2
    
              }
    
            ],
    
            "background": {
    
              "activeOnStart": true,
    
              "beginsPattern": "Starting MCP inspector",
    
              "endsPattern": "Proxy server listening on port"
    
            }
    
          },
    
          "dependsOn": [
    
            "Start MCP Server"
    
          ]
    
        },
    
        {
    
          "label": "Open Agent Builder",
    
          "type": "shell",
    
          "command": "echo ${input:openAgentBuilder}",
    
          "presentation": {
    
            "reveal": "never"
    
          },
    
          "dependsOn": [
    
            "Start MCP Server"
    
          ],
    
        },
    
        {
    
          "label": "Terminate All Tasks",
    
          "command": "echo ${input:terminate}",
    
          "type": "shell",
    
          "problemMatcher": []
    
        }
    
      ],
    
      "inputs": [
    
        {
    
          "id": "openAgentBuilder",
    
          "type": "command",
    
          "command": "ai-mlstudio.agentBuilder",
    
          "args": {
    
            "initialMCPs": [ "local-server-weather_mcp" ],
    
            "triggeredFrom": "vsc-tasks"
    
          }
    
        },
    
        {
    
          "id": "terminate",
    
          "type": "command",
    
          "command": "workbench.action.tasks.terminate",
    
          "args": "terminateAll"
    
        }
    
      ]
    
    }
    
    

    ---

    ๐Ÿš€ Running and Testing Your MCP Server

    Step 6: Install Dependencies

    After making the configuration changes, run the following commands:

    Install Python dependencies:

    
    uv sync
    
    

    Install Inspector dependencies:

    
    cd inspector
    
    npm install
    
    

    Step 7: Debug with Agent Builder

    1. Press F5 or use the "Debug in Agent Builder" configuration

    2. Select the compound configuration from the debug panel

    3. Wait for the server to start and Agent Builder to open

    4. Test your weather MCP server with natural language queries

    Input prompt like this

    SYSTEM_PROMPT

    
    You are my weather assistant
    
    

    USER_PROMPT

    
    How's the weather like in Seattle
    
    

    Step 8: Debug with MCP Inspector

    1. Use the "Debug in Inspector" configuration (Edge or Chrome)

    2. Open the Inspector interface at http://localhost:6274

    3. Explore the interactive testing environment:

    - View available tools

    - Test tool execution

    - Monitor network requests

    - Debug server responses

    ---

    ๐ŸŽฏ Key Learning Outcomes

    By completing this lab, you have:

  • [x] Created a custom MCP server using AI Toolkit templates
  • [x] Upgraded to the latest MCP SDK (v1.9.3) for enhanced functionality
  • [x] Configured professional debugging workflows for both Agent Builder and Inspector
  • [x] Set up the MCP Inspector for interactive server testing
  • [x] Mastered VS Code debugging configurations for MCP development
  • ๐Ÿ”ง Advanced Features Explored

    Feature Description Use Case --------- ------------- ---------- MCP Python SDK v1.9.3 Latest protocol implementation Modern server development MCP Inspector 0.14.0 Interactive debugging tool Real-time server testing VS Code Debugging Integrated development environment Professional debugging workflow Agent Builder Integration Direct AI Toolkit connection End-to-end agent testing

    ๐Ÿ“š Additional Resources

  • MCP Python SDK Documentation
  • AI Toolkit Extension Guide
  • VS Code Debugging Documentation
  • Model Context Protocol Specification
  • ---

    ๐ŸŽ‰ Congratulations! You've successfully completed Lab 3 and can now create, debug, and deploy custom MCP servers using professional development workflows.

    ๐Ÿ”œ Continue to Next Module

    Ready to apply your MCP skills to a real-world development workflow? Continue to Module 4: Practical MCP Development - Custom GitHub Clone Server where you'll:

  • Build a production-ready MCP server that automates GitHub repository operations
  • Implement GitHub repository cloning functionality via MCP
  • Integrate custom MCP servers with VS Code and GitHub Copilot Agent Mode
  • Test and deploy custom MCP servers in production environments
  • Learn practical workflow automation for developers
  • Duration: 20 minutes

  • ๐Ÿ’ป Create custom MCP servers using AI Toolkit
  • ๐Ÿ Configure and use the latest MCP Python SDK (v1.9.3)
  • ๐Ÿ” Set up and utilize MCP Inspector for debugging
  • ๐Ÿ› ๏ธ Build a Weather MCP Server with professional debugging workflows
  • ๐Ÿงช Debug MCP servers in both Agent Builder and Inspector environments
  • ๐ŸŽฏ Learning Outcome: Develop and debug custom MCP servers with modern tooling

    ๐Ÿ™ Module 4: Practical MCP Development - Custom GitHub Clone Server

    ๐Ÿ™ Module 4: Practical MCP Development - Custom GitHub Clone Server

    > โšก Quick Start: Build a production-ready MCP server that automates GitHub repository cloning and VS Code integration in just 30 minutes!

    ๐ŸŽฏ Learning Objectives

    By the end of this lab, you will be able to:

  • โœ… Create a custom MCP server for real-world development workflows
  • โœ… Implement GitHub repository cloning functionality via MCP
  • โœ… Integrate custom MCP servers with VS Code and Agent Builder
  • โœ… Use GitHub Copilot Agent Mode with custom MCP tools
  • โœ… Test and deploy custom MCP servers in production environments
  • ๐Ÿ“‹ Prerequisites

  • Completion of Labs 1-3 (MCP fundamentals and advanced development)
  • GitHub Copilot subscription (free signup available)
  • VS Code with AI Toolkit and GitHub Copilot extensions
  • Git CLI installed and configured
  • ๐Ÿ—๏ธ Project Overview

    Real-World Development Challenge

    As developers, we frequently use GitHub to clone repositories and open them in VS Code or VS Code Insiders. This manual process involves:

    1. Opening terminal/command prompt

    2. Navigating to the desired directory

    3. Running git clone command

    4. Opening VS Code in the cloned directory

    Our MCP solution streamlines this into a single intelligent command!

    What You'll Build

    A GitHub Clone MCP Server (git_mcp_server) that provides:

    Feature Description Benefit --------- ------------- --------- ๐Ÿ”„ Smart Repository Cloning Clone GitHub repos with validation Automated error checking ๐Ÿ“ Intelligent Directory Management Check and create directories safely Prevents overwriting ๐Ÿš€ Cross-Platform VS Code Integration Open projects in VS Code/Insiders Seamless workflow transition ๐Ÿ›ก๏ธ Robust Error Handling Handle network, permission, and path issues Production-ready reliability

    ---

    ๐Ÿ“– Step-by-Step Implementation

    Step 1: Create GitHub Agent in Agent Builder

    1. Launch Agent Builder through the AI Toolkit extension

    2. Create a new agent with the following configuration:

    ```

    Agent Name: GitHubAgent

    ```

    3. Initialize custom MCP server:

    - Navigate to Tools โ†’ Add Tool โ†’ MCP Server

    - Select "Create A new MCP Server"

    - Choose Python template for maximum flexibility

    - Server Name: git_mcp_server

    Step 2: Configure GitHub Copilot Agent Mode

    1. Open GitHub Copilot in VS Code (Ctrl/Cmd + Shift + P โ†’ "GitHub Copilot: Open")

    2. Select Agent Model in the Copilot interface

    3. Choose Claude 3.7 model for enhanced reasoning capabilities

    4. Enable MCP integration for tool access

    > ๐Ÿ’ก Pro Tip: Claude 3.7 provides superior understanding of development workflows and error handling patterns.

    Step 3: Implement Core MCP Server Functionality

    Use the following detailed prompt with GitHub Copilot Agent Mode:

    
    Create two MCP tools with the following comprehensive requirements:
    
    
    
    ๐Ÿ”ง TOOL A: clone_repository
    
    Requirements:
    
    - Clone any GitHub repository to a specified local folder
    
    - Return the absolute path of the successfully cloned project
    
    - Implement comprehensive validation:
    
      โœ“ Check if target directory already exists (return error if exists)
    
      โœ“ Validate GitHub URL format (https://github.com/user/repo)
    
      โœ“ Verify git command availability (prompt installation if missing)
    
      โœ“ Handle network connectivity issues
    
      โœ“ Provide clear error messages for all failure scenarios
    
    
    
    ๐Ÿš€ TOOL B: open_in_vscode
    
    Requirements:
    
    - Open specified folder in VS Code or VS Code Insiders
    
    - Cross-platform compatibility (Windows/Linux/macOS)
    
    - Use direct application launch (not terminal commands)
    
    - Auto-detect available VS Code installations
    
    - Handle cases where VS Code is not installed
    
    - Provide user-friendly error messages
    
    
    
    Additional Requirements:
    
    - Follow MCP 1.9.3 best practices
    
    - Include proper type hints and documentation
    
    - Implement logging for debugging purposes
    
    - Add input validation for all parameters
    
    - Include comprehensive error handling
    
    

    Step 4: Test Your MCP Server

    4a. Test in Agent Builder

    1. Launch the debug configuration for Agent Builder

    2. Configure your agent with this system prompt:

    
    SYSTEM_PROMPT:
    
    You are my intelligent coding repository assistant. You help developers efficiently clone GitHub repositories and set up their development environment. Always provide clear feedback about operations and handle errors gracefully.
    
    

    3. Test with realistic user scenarios:

    
    USER_PROMPT EXAMPLES:
    
    
    
    Scenario : Basic Clone and Open
    
    "Clone {Your GitHub Repo link such as https://github.com/kinfey/GHCAgentWorkshop
    
     } and save to {The global path you specify}, then open it with VS Code Insiders"
    
    

    Expected Results:

  • โœ… Successful cloning with path confirmation
  • โœ… Automatic VS Code launch
  • โœ… Clear error messages for invalid scenarios
  • โœ… Proper handling of edge cases
  • 4b. Test in MCP Inspector

    ---

    ๐ŸŽ‰ Congratulations! You've successfully created a practical, production-ready MCP server that solves real development workflow challenges.

    Your custom GitHub clone server demonstrates the power of MCP for automating and enhancing developer productivity.

    ๐Ÿ† Achievement Unlocked:

  • โœ… MCP Developer - Created custom MCP server
  • โœ… Workflow Automator - Streamlined development processes
  • โœ… Integration Expert - Connected multiple development tools
  • โœ… Production Ready - Built deployable solutions
  • ---

    ๐ŸŽ“ Workshop Completion: Your Journey with Model Context Protocol

    Dear Workshop Participant,

    Congratulations on completing all four modules of the Model Context Protocol workshop! You've come a long way from understanding basic AI Toolkit concepts to building production-ready MCP servers that solve real-world development challenges.

    ๐Ÿš€ Your Learning Path Recap:

    Module 1: You began by exploring AI Toolkit fundamentals, model testing, and creating your first AI agent.

    Module 2: You learned MCP architecture, integrated Playwright MCP, and built your first browser automation agent.

    Module 3: You advanced to custom MCP server development with the Weather MCP server and mastered debugging tools.

    Module 4: You've now applied everything to create a practical GitHub repository workflow automation tool.

    ๐ŸŒŸ What You've Mastered:

  • โœ… AI Toolkit Ecosystem: Models, agents, and integration patterns
  • โœ… MCP Architecture: Client-server design, transport protocols, and security
  • โœ… Developer Tools: From Playground to Inspector to production deployment
  • โœ… Custom Development: Building, testing, and deploying your own MCP servers
  • โœ… Practical Applications: Solving real-world workflow challenges with AI
  • ๐Ÿ”ฎ Your Next Steps:

    1. Build Your Own MCP Server: Apply these skills to automate your unique workflows

    2. Join the MCP Community: Share your creations and learn from others

    3. Explore Advanced Integration: Connect MCP servers to enterprise systems

    4. Contribute to Open Source: Help improve MCP tooling and documentation

    Remember, this workshop is just the beginning. The Model Context Protocol ecosystem is rapidly evolving, and you're now equipped to be at the forefront of AI-powered development tools.

    Thank you for your participation and dedication to learning!

    We hope this workshop has sparked ideas that will transform how you build and interact with AI tools in your development journey.

    Happy coding!

    ---

    What's Next

    Congratulations on completing all labs in Module 10!

  • Back to: Module 10 Overview
  • Continue to: Module 11: MCP Server Hands-On Labs
  • Duration: 30 minutes

  • ๐Ÿ—๏ธ Build a real-world GitHub Clone MCP Server for development workflows
  • ๐Ÿ”„ Implement smart repository cloning with validation and error handling
  • ๐Ÿ“ Create intelligent directory management and VS Code integration
  • ๐Ÿค– Use GitHub Copilot Agent Mode with custom MCP tools
  • ๐Ÿ›ก๏ธ Apply production-ready reliability and cross-platform compatibility
  • ๐ŸŽฏ Learning Outcome: Deploy a production-ready MCP server that streamlines real development workflows

    ๐Ÿ’ก Real-World Applications & Impact

    ๐Ÿข Enterprise Use Cases

    ๐Ÿ”„ DevOps Automation

    Transform your development workflow with intelligent automation:

  • Smart Repository Management: AI-driven code review and merge decisions
  • Intelligent CI/CD: Automated pipeline optimization based on code changes
  • Issue Triage: Automatic bug classification and assignment
  • ๐Ÿงช Quality Assurance Revolution

    Elevate testing with AI-powered automation:

  • Intelligent Test Generation: Create comprehensive test suites automatically
  • Visual Regression Testing: AI-powered UI change detection
  • Performance Monitoring: Proactive issue identification and resolution
  • ๐Ÿ“Š Data Pipeline Intelligence

    Build smarter data processing workflows:

  • Adaptive ETL Processes: Self-optimizing data transformations
  • Anomaly Detection: Real-time data quality monitoring
  • Intelligent Routing: Smart data flow management
  • ๐ŸŽง Customer Experience Enhancement

    Create exceptional customer interactions:

  • Context-Aware Support: AI agents with access to customer history
  • Proactive Issue Resolution: Predictive customer service
  • Multi-Channel Integration: Unified AI experience across platforms
  • ๐Ÿ› ๏ธ Prerequisites & Setup

    ๐Ÿ’ป System Requirements

    Component Requirement Notes ----------- ------------- ------- Operating System Windows 10+, macOS 10.15+, Linux Any modern OS Visual Studio Code Latest stable version Required for AITK Node.js v18.0+ and npm For MCP server development Python 3.10+ Optional for Python MCP servers Memory 8GB RAM minimum 16GB recommended for local models

    ๐Ÿ”ง Development Environment

    Recommended VS Code Extensions
  • AI Toolkit (ms-windows-ai-studio.windows-ai-studio)
  • Python (ms-python.python)
  • Python Debugger (ms-python.debugpy)
  • GitHub Copilot (GitHub.copilot) - Optional but helpful
  • Optional Tools
  • uv: Modern Python package manager
  • MCP Inspector: Visual debugging tool for MCP servers
  • Playwright: For web automation examples
  • ๐ŸŽ–๏ธ Learning Outcomes & Certification Path

    ๐Ÿ† Skill Mastery Checklist

    By completing this workshop, you will achieve mastery in:

    ๐ŸŽฏ Core Competencies
  • [ ] MCP Protocol Mastery: Deep understanding of architecture and implementation patterns
  • [ ] AITK Proficiency: Expert-level usage of AI Toolkit for rapid development
  • [ ] Custom Server Development: Build, deploy, and maintain production MCP servers
  • [ ] Tool Integration Excellence: Seamlessly connect AI with existing development workflows
  • [ ] Problem-Solving Application: Apply learned skills to real business challenges
  • ๐Ÿ”ง Technical Skills
  • [ ] Set up and configure AI Toolkit in VS Code
  • [ ] Design and implement custom MCP servers
  • [ ] Integrate GitHub Models with MCP architecture
  • [ ] Build automated testing workflows with Playwright
  • [ ] Deploy AI agents for production use
  • [ ] Debug and optimize MCP server performance
  • ๐Ÿš€ Advanced Capabilities
  • [ ] Architect enterprise-scale AI integrations
  • [ ] Implement security best practices for AI applications
  • [ ] Design scalable MCP server architectures
  • [ ] Create custom tool chains for specific domains
  • [ ] Mentor others in AI-native development
  • ๐Ÿ“– Additional Resources

  • MCP Specification (2025-11-25)
  • AI Toolkit GitHub Repository
  • Sample MCP Servers Collection
  • Best Practices Guide
  • OWASP MCP Top 10 - Security best practices
  • ---

    ๐Ÿš€ Ready to revolutionize your AI development workflow?

    Let's build the future of intelligent applications together with MCP and AI Toolkit!

    What's Next

    Continue to: Module 11: MCP Server Hands-On Labs

    code Module 11

    PostgreSQL

    ๐Ÿš€ MCP Server with PostgreSQL - Complete Learning Guide

    ๐Ÿง  Overview of the MCP Database Integration Learning Path

    This comprehensive learning guide teaches you how to build production-ready Model Context Protocol (MCP) servers that integrate with databases through a practical retail analytics implementation.

    You'll learn enterprise-grade patterns including Row Level Security (RLS), semantic search, Azure AI integration, and multi-tenant data access.

    Whether you're a backend developer, AI engineer, or data architect, this guide provides structured learning with real-world examples and hands-on exercises which walks you through the following MCP server https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail.

    ๐Ÿ”— Official MCP Resources

  • ๐Ÿ“˜ MCP Documentation โ€“ Detailed tutorials and user guides
  • ๐Ÿ“œ MCP Specification (2025-11-25) โ€“ Protocol architecture and technical references
  • ๐Ÿง‘โ€๐Ÿ’ป MCP GitHub Repository โ€“ Open-source SDKs, tools, and code samples
  • ๐ŸŒ MCP Community โ€“ Join discussions and contribute to the community
  • ๐Ÿ”’ OWASP MCP Top 10 โ€“ Security best practices and risk mitigations
  • ๐Ÿงญ MCP Database Integration Learning Path

    ๐Ÿ“š Complete Learning Structure for https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail

    Lab Topic Description Link -------- ------- ------------- ------ Lab 1-3: Foundations 00 Introduction to MCP Database Integration

    Introduction to MCP Database Integration

    ๐ŸŽฏ What This Lab Covers

    This introduction lab provides a comprehensive overview of building Model Context Protocol (MCP) servers with database integration.

    You'll understand the business case, technical architecture, and real-world applications through the Zava Retail analytics use case at https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail.

    Overview

    Model Context Protocol (MCP) enables AI assistants to securely access and interact with external data sources in real-time. When combined with database integration, MCP unlocks powerful capabilities for data-driven AI applications.

    This learning path teaches you to build production-ready MCP servers that connect AI assistants to retail sales data through PostgreSQL, implementing enterprise patterns like Row Level Security, semantic search, and multi-tenant data access.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Define Model Context Protocol and its core benefits for database integration
  • Identify key components of an MCP server architecture with databases
  • Understand the Zava Retail use case and its business requirements
  • Recognize enterprise patterns for secure, scalable database access
  • List the tools and technologies used throughout this learning path
  • ๐Ÿงญ The Challenge: AI Meets Real-World Data

    Traditional AI Limitations

    Modern AI assistants are incredibly powerful but face significant limitations when working with real-world business data:

    Challenge Description Business Impact --------------- ----------------- ------------------- Static Knowledge AI models trained on fixed datasets can't access current business data Outdated insights, missed opportunities Data Silos Information locked in databases, APIs, and systems AI can't reach Incomplete analysis, fragmented workflows Security Constraints Direct database access raises security and compliance concerns Limited deployment, manual data preparation Complex Queries Business users need technical knowledge to extract data insights Reduced adoption, inefficient processes

    The MCP Solution

    Model Context Protocol addresses these challenges by providing:

  • Real-time Data Access: AI assistants query live databases and APIs
  • Secure Integration: Controlled access with authentication and permissions
  • Natural Language Interface: Business users ask questions in plain English
  • Standardized Protocol: Works across different AI platforms and tools
  • ๐Ÿช Meet Zava Retail: Our Learning Case Study https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail

    Throughout this learning path, we'll build an MCP server for Zava Retail, a fictional DIY retail chain with multiple store locations. This realistic scenario demonstrates enterprise-grade MCP implementation.

    Business Context

    Zava Retail operates:

  • 8 physical stores across Washington state (Seattle, Bellevue, Tacoma, Spokane, Everett, Redmond, Kirkland)
  • 1 online store for e-commerce sales
  • Diverse product catalog including tools, hardware, garden supplies, and building materials
  • Multi-level management with store managers, regional managers, and executives
  • Business Requirements

    Store managers and executives need AI-powered analytics to:

    1. Analyze sales performance across stores and time periods

    2. Track inventory levels and identify restocking needs

    3. Understand customer behavior and purchasing patterns

    4. Discover product insights through semantic search

    5. Generate reports with natural language queries

    6. Maintain data security with role-based access control

    Technical Requirements

    The MCP server must provide:

  • Multi-tenant data access where store managers see only their store's data
  • Flexible querying supporting complex SQL operations
  • Semantic search for product discovery and recommendations
  • Real-time data reflecting current business state
  • Secure authentication with row-level security
  • Scalable architecture supporting multiple concurrent users
  • ๐Ÿ—๏ธ MCP Server Architecture Overview

    Our MCP server implements a layered architecture optimized for database integration:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                    VS Code AI Client                       โ”‚
    
    โ”‚                  (Natural Language Queries)                โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ HTTP/SSE
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                     MCP Server                             โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚   Tool Layer    โ”‚ โ”‚  Security Layer โ”‚ โ”‚  Config Layer โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Query Tools   โ”‚ โ”‚ โ€ข RLS Context   โ”‚ โ”‚ โ€ข Environment โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Schema Tools  โ”‚ โ”‚ โ€ข User Identity โ”‚ โ”‚ โ€ข Connections โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Search Tools  โ”‚ โ”‚ โ€ข Access Controlโ”‚ โ”‚ โ€ข Validation  โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ asyncpg
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                PostgreSQL Database                         โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚  Retail Schema  โ”‚ โ”‚   RLS Policies  โ”‚ โ”‚   pgvector    โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Stores        โ”‚ โ”‚ โ€ข Store-based   โ”‚ โ”‚ โ€ข Embeddings  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Customers     โ”‚ โ”‚   Isolation     โ”‚ โ”‚ โ€ข Similarity  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Products      โ”‚ โ”‚ โ€ข Role Control  โ”‚ โ”‚   Search      โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Orders        โ”‚ โ”‚ โ€ข Audit Logs    โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ REST API
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                  Azure OpenAI                              โ”‚
    
    โ”‚               (Text Embeddings)                            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Key Components

    1. MCP Server Layer
  • FastMCP Framework: Modern Python MCP server implementation
  • Tool Registration: Declarative tool definitions with type safety
  • Request Context: User identity and session management
  • Error Handling: Robust error management and logging
  • 2. Database Integration Layer
  • Connection Pooling: Efficient asyncpg connection management
  • Schema Provider: Dynamic table schema discovery
  • Query Executor: Secure SQL execution with RLS context
  • Transaction Management: ACID compliance and rollback handling
  • 3. Security Layer
  • Row Level Security: PostgreSQL RLS for multi-tenant data isolation
  • User Identity: Store manager authentication and authorization
  • Access Control: Fine-grained permissions and audit trails
  • Input Validation: SQL injection prevention and query validation
  • 4. AI Enhancement Layer
  • Semantic Search: Vector embeddings for product discovery
  • Azure OpenAI Integration: Text embedding generation
  • Similarity Algorithms: pgvector cosine similarity search
  • Search Optimization: Indexing and performance tuning
  • ๐Ÿ”ง Technology Stack

    Core Technologies

    Component Technology Purpose --------------- ---------------- ------------- MCP Framework FastMCP (Python) Modern MCP server implementation Database PostgreSQL 17 + pgvector Relational data with vector search AI Services Azure OpenAI Text embeddings and language models Containerization Docker + Docker Compose Development environment Cloud Platform Microsoft Azure Production deployment IDE Integration VS Code AI Chat and development workflow

    Development Tools

    Tool Purpose ---------- ------------- asyncpg High-performance PostgreSQL driver Pydantic Data validation and serialization Azure SDK Cloud service integration pytest Testing framework Docker Containerization and deployment

    Production Stack

    Service Azure Resource Purpose ------------- ------------------- ------------- Database Azure Database for PostgreSQL Managed database service Container Azure Container Apps Serverless container hosting AI Services Azure AI Foundry OpenAI models and endpoints Monitoring Application Insights Observability and diagnostics Security Azure Key Vault Secrets and configuration management

    ๐ŸŽฌ Real-World Usage Scenarios

    Let's explore how different users interact with our MCP server:

    Scenario 1: Store Manager Performance Review

    User: Sarah, Seattle Store Manager

    Goal: Analyze last quarter's sales performance

    Natural Language Query:

    > "Show me the top 10 products by revenue for my store in Q4 2024"

    What Happens:

    1. VS Code AI Chat sends query to MCP server

    2. MCP server identifies Sarah's store context (Seattle)

    3. RLS policies filter data to Seattle store only

    4. SQL query generated and executed

    5. Results formatted and returned to AI Chat

    6. AI provides analysis and insights

    Scenario 2: Product Discovery with Semantic Search

    User: Mike, Inventory Manager

    Goal: Find products similar to a customer request

    Natural Language Query:

    > "What products do we sell that are similar to 'waterproof electrical connectors for outdoor use'?"

    What Happens:

    1. Query processed by semantic search tool

    2. Azure OpenAI generates embedding vector

    3. pgvector performs similarity search

    4. Related products ranked by relevance

    5. Results include product details and availability

    6. AI suggests alternatives and bundling opportunities

    Scenario 3: Cross-Store Analytics

    User: Jennifer, Regional Manager

    Goal: Compare performance across all stores

    Natural Language Query:

    > "Compare sales by category for all stores in the last 6 months"

    What Happens:

    1. RLS context set for regional manager access

    2. Complex multi-store query generated

    3. Data aggregated across store locations

    4. Results include trends and comparisons

    5. AI identifies insights and recommendations

    ๐Ÿ”’ Security and Multi-Tenancy Deep Dive

    Our implementation prioritizes enterprise-grade security:

    Row Level Security (RLS)

    PostgreSQL RLS ensures data isolation:

    
    -- Store managers see only their store's data
    
    CREATE POLICY store_manager_policy ON retail.orders
    
      FOR ALL TO store_managers
    
      USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_policy ON retail.orders
    
      FOR ALL TO regional_managers
    
      USING (store_id = ANY(get_user_store_list()));
    
    

    User Identity Management

    Each MCP connection includes:

  • Store Manager ID: Unique identifier for RLS context
  • Role Assignment: Permissions and access levels
  • Session Management: Secure authentication tokens
  • Audit Logging: Complete access history
  • Data Protection

    Multiple layers of security:

  • Connection Encryption: TLS for all database connections
  • SQL Injection Prevention: Parameterized queries only
  • Input Validation: Comprehensive request validation
  • Error Handling: No sensitive data in error messages
  • ๐ŸŽฏ Key Takeaways

    After completing this introduction, you should understand:

    โœ… MCP Value Proposition: How MCP bridges AI assistants and real-world data

    โœ… Business Context: Zava Retail's requirements and challenges

    โœ… Architecture Overview: Key components and their interactions

    โœ… Technology Stack: Tools and frameworks used throughout

    โœ… Security Model: Multi-tenant data access and protection

    โœ… Usage Patterns: Real-world query scenarios and workflows

    ๐Ÿš€ What's Next

    Ready to dive deeper? Continue with:

    Lab 01: Core Architecture Concepts

    Learn about MCP server architecture patterns, database design principles, and the detailed technical implementation that powers our retail analytics solution.

    ๐Ÿ“š Additional Resources

    MCP Documentation

  • MCP Specification - Official protocol documentation
  • MCP for Beginners - Comprehensive MCP learning guide
  • FastMCP Documentation - Python SDK documentation
  • Database Integration

  • PostgreSQL Documentation - Complete PostgreSQL reference
  • pgvector Guide - Vector extension documentation
  • Row Level Security - PostgreSQL RLS guide
  • Azure Services

  • Azure OpenAI Documentation - AI service integration
  • Azure Database for PostgreSQL - Managed database service
  • Azure Container Apps - Serverless containers
  • ---

    Disclaimer: This is a learning exercise using fictional retail data. Always follow your organization's data governance and security policies when implementing similar solutions in production environments.

    Overview of MCP with database integration and retail analytics use case Start Here

    Introduction to MCP Database Integration

    ๐ŸŽฏ What This Lab Covers

    This introduction lab provides a comprehensive overview of building Model Context Protocol (MCP) servers with database integration.

    You'll understand the business case, technical architecture, and real-world applications through the Zava Retail analytics use case at https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail.

    Overview

    Model Context Protocol (MCP) enables AI assistants to securely access and interact with external data sources in real-time. When combined with database integration, MCP unlocks powerful capabilities for data-driven AI applications.

    This learning path teaches you to build production-ready MCP servers that connect AI assistants to retail sales data through PostgreSQL, implementing enterprise patterns like Row Level Security, semantic search, and multi-tenant data access.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Define Model Context Protocol and its core benefits for database integration
  • Identify key components of an MCP server architecture with databases
  • Understand the Zava Retail use case and its business requirements
  • Recognize enterprise patterns for secure, scalable database access
  • List the tools and technologies used throughout this learning path
  • ๐Ÿงญ The Challenge: AI Meets Real-World Data

    Traditional AI Limitations

    Modern AI assistants are incredibly powerful but face significant limitations when working with real-world business data:

    Challenge Description Business Impact --------------- ----------------- ------------------- Static Knowledge AI models trained on fixed datasets can't access current business data Outdated insights, missed opportunities Data Silos Information locked in databases, APIs, and systems AI can't reach Incomplete analysis, fragmented workflows Security Constraints Direct database access raises security and compliance concerns Limited deployment, manual data preparation Complex Queries Business users need technical knowledge to extract data insights Reduced adoption, inefficient processes

    The MCP Solution

    Model Context Protocol addresses these challenges by providing:

  • Real-time Data Access: AI assistants query live databases and APIs
  • Secure Integration: Controlled access with authentication and permissions
  • Natural Language Interface: Business users ask questions in plain English
  • Standardized Protocol: Works across different AI platforms and tools
  • ๐Ÿช Meet Zava Retail: Our Learning Case Study https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail

    Throughout this learning path, we'll build an MCP server for Zava Retail, a fictional DIY retail chain with multiple store locations. This realistic scenario demonstrates enterprise-grade MCP implementation.

    Business Context

    Zava Retail operates:

  • 8 physical stores across Washington state (Seattle, Bellevue, Tacoma, Spokane, Everett, Redmond, Kirkland)
  • 1 online store for e-commerce sales
  • Diverse product catalog including tools, hardware, garden supplies, and building materials
  • Multi-level management with store managers, regional managers, and executives
  • Business Requirements

    Store managers and executives need AI-powered analytics to:

    1. Analyze sales performance across stores and time periods

    2. Track inventory levels and identify restocking needs

    3. Understand customer behavior and purchasing patterns

    4. Discover product insights through semantic search

    5. Generate reports with natural language queries

    6. Maintain data security with role-based access control

    Technical Requirements

    The MCP server must provide:

  • Multi-tenant data access where store managers see only their store's data
  • Flexible querying supporting complex SQL operations
  • Semantic search for product discovery and recommendations
  • Real-time data reflecting current business state
  • Secure authentication with row-level security
  • Scalable architecture supporting multiple concurrent users
  • ๐Ÿ—๏ธ MCP Server Architecture Overview

    Our MCP server implements a layered architecture optimized for database integration:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                    VS Code AI Client                       โ”‚
    
    โ”‚                  (Natural Language Queries)                โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ HTTP/SSE
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                     MCP Server                             โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚   Tool Layer    โ”‚ โ”‚  Security Layer โ”‚ โ”‚  Config Layer โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Query Tools   โ”‚ โ”‚ โ€ข RLS Context   โ”‚ โ”‚ โ€ข Environment โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Schema Tools  โ”‚ โ”‚ โ€ข User Identity โ”‚ โ”‚ โ€ข Connections โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Search Tools  โ”‚ โ”‚ โ€ข Access Controlโ”‚ โ”‚ โ€ข Validation  โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ asyncpg
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                PostgreSQL Database                         โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚  Retail Schema  โ”‚ โ”‚   RLS Policies  โ”‚ โ”‚   pgvector    โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Stores        โ”‚ โ”‚ โ€ข Store-based   โ”‚ โ”‚ โ€ข Embeddings  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Customers     โ”‚ โ”‚   Isolation     โ”‚ โ”‚ โ€ข Similarity  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Products      โ”‚ โ”‚ โ€ข Role Control  โ”‚ โ”‚   Search      โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Orders        โ”‚ โ”‚ โ€ข Audit Logs    โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ REST API
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                  Azure OpenAI                              โ”‚
    
    โ”‚               (Text Embeddings)                            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Key Components

    1. MCP Server Layer
  • FastMCP Framework: Modern Python MCP server implementation
  • Tool Registration: Declarative tool definitions with type safety
  • Request Context: User identity and session management
  • Error Handling: Robust error management and logging
  • 2. Database Integration Layer
  • Connection Pooling: Efficient asyncpg connection management
  • Schema Provider: Dynamic table schema discovery
  • Query Executor: Secure SQL execution with RLS context
  • Transaction Management: ACID compliance and rollback handling
  • 3. Security Layer
  • Row Level Security: PostgreSQL RLS for multi-tenant data isolation
  • User Identity: Store manager authentication and authorization
  • Access Control: Fine-grained permissions and audit trails
  • Input Validation: SQL injection prevention and query validation
  • 4. AI Enhancement Layer
  • Semantic Search: Vector embeddings for product discovery
  • Azure OpenAI Integration: Text embedding generation
  • Similarity Algorithms: pgvector cosine similarity search
  • Search Optimization: Indexing and performance tuning
  • ๐Ÿ”ง Technology Stack

    Core Technologies

    Component Technology Purpose --------------- ---------------- ------------- MCP Framework FastMCP (Python) Modern MCP server implementation Database PostgreSQL 17 + pgvector Relational data with vector search AI Services Azure OpenAI Text embeddings and language models Containerization Docker + Docker Compose Development environment Cloud Platform Microsoft Azure Production deployment IDE Integration VS Code AI Chat and development workflow

    Development Tools

    Tool Purpose ---------- ------------- asyncpg High-performance PostgreSQL driver Pydantic Data validation and serialization Azure SDK Cloud service integration pytest Testing framework Docker Containerization and deployment

    Production Stack

    Service Azure Resource Purpose ------------- ------------------- ------------- Database Azure Database for PostgreSQL Managed database service Container Azure Container Apps Serverless container hosting AI Services Azure AI Foundry OpenAI models and endpoints Monitoring Application Insights Observability and diagnostics Security Azure Key Vault Secrets and configuration management

    ๐ŸŽฌ Real-World Usage Scenarios

    Let's explore how different users interact with our MCP server:

    Scenario 1: Store Manager Performance Review

    User: Sarah, Seattle Store Manager

    Goal: Analyze last quarter's sales performance

    Natural Language Query:

    > "Show me the top 10 products by revenue for my store in Q4 2024"

    What Happens:

    1. VS Code AI Chat sends query to MCP server

    2. MCP server identifies Sarah's store context (Seattle)

    3. RLS policies filter data to Seattle store only

    4. SQL query generated and executed

    5. Results formatted and returned to AI Chat

    6. AI provides analysis and insights

    Scenario 2: Product Discovery with Semantic Search

    User: Mike, Inventory Manager

    Goal: Find products similar to a customer request

    Natural Language Query:

    > "What products do we sell that are similar to 'waterproof electrical connectors for outdoor use'?"

    What Happens:

    1. Query processed by semantic search tool

    2. Azure OpenAI generates embedding vector

    3. pgvector performs similarity search

    4. Related products ranked by relevance

    5. Results include product details and availability

    6. AI suggests alternatives and bundling opportunities

    Scenario 3: Cross-Store Analytics

    User: Jennifer, Regional Manager

    Goal: Compare performance across all stores

    Natural Language Query:

    > "Compare sales by category for all stores in the last 6 months"

    What Happens:

    1. RLS context set for regional manager access

    2. Complex multi-store query generated

    3. Data aggregated across store locations

    4. Results include trends and comparisons

    5. AI identifies insights and recommendations

    ๐Ÿ”’ Security and Multi-Tenancy Deep Dive

    Our implementation prioritizes enterprise-grade security:

    Row Level Security (RLS)

    PostgreSQL RLS ensures data isolation:

    
    -- Store managers see only their store's data
    
    CREATE POLICY store_manager_policy ON retail.orders
    
      FOR ALL TO store_managers
    
      USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_policy ON retail.orders
    
      FOR ALL TO regional_managers
    
      USING (store_id = ANY(get_user_store_list()));
    
    

    User Identity Management

    Each MCP connection includes:

  • Store Manager ID: Unique identifier for RLS context
  • Role Assignment: Permissions and access levels
  • Session Management: Secure authentication tokens
  • Audit Logging: Complete access history
  • Data Protection

    Multiple layers of security:

  • Connection Encryption: TLS for all database connections
  • SQL Injection Prevention: Parameterized queries only
  • Input Validation: Comprehensive request validation
  • Error Handling: No sensitive data in error messages
  • ๐ŸŽฏ Key Takeaways

    After completing this introduction, you should understand:

    โœ… MCP Value Proposition: How MCP bridges AI assistants and real-world data

    โœ… Business Context: Zava Retail's requirements and challenges

    โœ… Architecture Overview: Key components and their interactions

    โœ… Technology Stack: Tools and frameworks used throughout

    โœ… Security Model: Multi-tenant data access and protection

    โœ… Usage Patterns: Real-world query scenarios and workflows

    ๐Ÿš€ What's Next

    Ready to dive deeper? Continue with:

    Lab 01: Core Architecture Concepts

    Learn about MCP server architecture patterns, database design principles, and the detailed technical implementation that powers our retail analytics solution.

    ๐Ÿ“š Additional Resources

    MCP Documentation

  • MCP Specification - Official protocol documentation
  • MCP for Beginners - Comprehensive MCP learning guide
  • FastMCP Documentation - Python SDK documentation
  • Database Integration

  • PostgreSQL Documentation - Complete PostgreSQL reference
  • pgvector Guide - Vector extension documentation
  • Row Level Security - PostgreSQL RLS guide
  • Azure Services

  • Azure OpenAI Documentation - AI service integration
  • Azure Database for PostgreSQL - Managed database service
  • Azure Container Apps - Serverless containers
  • ---

    Disclaimer: This is a learning exercise using fictional retail data. Always follow your organization's data governance and security policies when implementing similar solutions in production environments.

    01 Core Architecture Concepts

    Core Architecture Concepts

    ๐ŸŽฏ What This Lab Covers

    This lab provides an in-depth exploration of MCP server architecture patterns, database design principles, and the technical implementation strategies that power robust, scalable database-integrated AI applications.

    Overview

    Building a production-ready MCP server with database integration requires careful architectural decisions.

    This lab breaks down the key components, design patterns, and technical considerations that make our Zava Retail analytics solution robust, secure, and scalable.

    You'll understand how each layer interacts, why specific technologies were chosen, and how to apply these patterns to your own MCP implementations.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Analyze the layered architecture of an MCP server with database integration
  • Understand the role and responsibilities of each architectural component
  • Design database schemas that support multi-tenant MCP applications
  • Implement connection pooling and resource management strategies
  • Apply error handling and logging patterns for production systems
  • Evaluate trade-offs between different architectural approaches
  • ๐Ÿ—๏ธ MCP Server Architecture Layers

    Our MCP server implements a layered architecture that separates concerns and promotes maintainability:

    Layer 1: Protocol Layer (FastMCP)

    Responsibility: Handle MCP protocol communication and message routing

    
    # FastMCP server setup
    
    from fastmcp import FastMCP
    
    
    
    mcp = FastMCP("Zava Retail Analytics")
    
    
    
    # Tool registration with type safety
    
    @mcp.tool()
    
    async def execute_sales_query(
    
        ctx: Context,
    
        postgresql_query: Annotated[str, Field(description="Well-formed PostgreSQL query")]
    
    ) -> str:
    
        """Execute PostgreSQL queries with Row Level Security."""
    
        return await query_executor.execute(postgresql_query, ctx)
    
    

    Key Features:

  • Protocol Compliance: Full MCP specification support
  • Type Safety: Pydantic models for request/response validation
  • Async Support: Non-blocking I/O for high concurrency
  • Error Handling: Standardized error responses
  • Layer 2: Business Logic Layer

    Responsibility: Implement business rules and coordinate between protocol and data layers

    
    class SalesAnalyticsService:
    
        """Business logic for retail analytics operations."""
    
        
    
        async def get_store_performance(
    
            self, 
    
            store_id: str, 
    
            time_period: str
    
        ) -> Dict[str, Any]:
    
            """Calculate store performance metrics."""
    
            
    
            # Validate business rules
    
            if not self._validate_store_access(store_id):
    
                raise UnauthorizedError("Access denied for store")
    
            
    
            # Coordinate data retrieval
    
            sales_data = await self.db_provider.get_sales_data(store_id, time_period)
    
            metrics = self._calculate_metrics(sales_data)
    
            
    
            return {
    
                "store_id": store_id,
    
                "period": time_period,
    
                "metrics": metrics,
    
                "insights": self._generate_insights(metrics)
    
            }
    
    

    Key Features:

  • Business Rule Enforcement: Store access validation and data integrity
  • Service Coordination: Orchestration between database and AI services
  • Data Transformation: Converting raw data to business insights
  • Caching Strategy: Performance optimization for frequent queries
  • Layer 3: Data Access Layer

    Responsibility: Manage database connections, query execution, and data mapping

    
    class PostgreSQLProvider:
    
        """Data access layer for PostgreSQL operations."""
    
        
    
        def __init__(self, connection_config: Dict[str, Any]):
    
            self.connection_pool: Optional[Pool] = None
    
            self.config = connection_config
    
        
    
        async def execute_query(
    
            self, 
    
            query: str, 
    
            rls_user_id: str
    
        ) -> List[Dict[str, Any]]:
    
            """Execute query with RLS context."""
    
            
    
            async with self.connection_pool.acquire() as conn:
    
                # Set RLS context
    
                await conn.execute(
    
                    "SELECT set_config('app.current_rls_user_id', $1, false)",
    
                    rls_user_id
    
                )
    
                
    
                # Execute query with timeout
    
                try:
    
                    rows = await asyncio.wait_for(
    
                        conn.fetch(query),
    
                        timeout=30.0
    
                    )
    
                    return [dict(row) for row in rows]
    
                except asyncio.TimeoutError:
    
                    raise QueryTimeoutError("Query execution exceeded timeout")
    
    

    Key Features:

  • Connection Pooling: Efficient resource management
  • Transaction Management: ACID compliance and rollback handling
  • Query Optimization: Performance monitoring and optimization
  • RLS Integration: Row-level security context management
  • Layer 4: Infrastructure Layer

    Responsibility: Handle cross-cutting concerns like logging, monitoring, and configuration

    
    class InfrastructureManager:
    
        """Infrastructure concerns management."""
    
        
    
        def __init__(self):
    
            self.logger = self._setup_logging()
    
            self.metrics = self._setup_metrics()
    
            self.config = self._load_configuration()
    
        
    
        def _setup_logging(self) -> Logger:
    
            """Configure structured logging."""
    
            logging.basicConfig(
    
                level=logging.INFO,
    
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    
                handlers=[
    
                    logging.StreamHandler(),
    
                    logging.FileHandler('mcp_server.log')
    
                ]
    
            )
    
            return logging.getLogger(__name__)
    
        
    
        async def track_query_execution(
    
            self, 
    
            query_type: str, 
    
            duration: float, 
    
            success: bool
    
        ):
    
            """Track query performance metrics."""
    
            self.metrics.counter('query_total').labels(
    
                type=query_type,
    
                status='success' if success else 'error'
    
            ).inc()
    
            
    
            self.metrics.histogram('query_duration').labels(
    
                type=query_type
    
            ).observe(duration)
    
    

    ๐Ÿ—„๏ธ Database Design Patterns

    Our PostgreSQL schema implements several key patterns for multi-tenant MCP applications:

    1. Multi-Tenant Schema Design

    
    -- Core retail entities with store-based partitioning
    
    CREATE TABLE retail.stores (
    
        store_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        name VARCHAR(100) NOT NULL,
    
        location VARCHAR(200) NOT NULL,
    
        manager_id UUID NOT NULL,
    
        created_at TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    CREATE TABLE retail.customers (
    
        customer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        store_id UUID REFERENCES retail.stores(store_id),
    
        first_name VARCHAR(50) NOT NULL,
    
        last_name VARCHAR(50) NOT NULL,
    
        email VARCHAR(100) UNIQUE,
    
        created_at TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    CREATE TABLE retail.orders (
    
        order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        customer_id UUID REFERENCES retail.customers(customer_id),
    
        store_id UUID REFERENCES retail.stores(store_id),
    
        order_date TIMESTAMP DEFAULT NOW(),
    
        total_amount DECIMAL(10,2) NOT NULL,
    
        status VARCHAR(20) DEFAULT 'pending'
    
    );
    
    

    Design Principles:

  • Foreign Key Consistency: Ensure data integrity across tables
  • Store ID Propagation: Every transactional table includes store_id
  • UUID Primary Keys: Globally unique identifiers for distributed systems
  • Timestamp Tracking: Audit trail for all data changes
  • 2. Row Level Security Implementation

    
    -- Enable RLS on multi-tenant tables
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.orders ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.order_items ENABLE ROW LEVEL SECURITY;
    
    
    
    -- Store manager can only see their store's data
    
    CREATE POLICY store_manager_customers ON retail.customers
    
        FOR ALL TO store_managers
    
        USING (store_id = get_current_user_store());
    
    
    
    CREATE POLICY store_manager_orders ON retail.orders
    
        FOR ALL TO store_managers
    
        USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_orders ON retail.orders
    
        FOR ALL TO regional_managers
    
        USING (store_id = ANY(get_user_store_list()));
    
    
    
    -- Support function for RLS context
    
    CREATE OR REPLACE FUNCTION get_current_user_store()
    
    RETURNS UUID AS $$
    
    BEGIN
    
        RETURN current_setting('app.current_rls_user_id')::UUID;
    
    EXCEPTION WHEN OTHERS THEN
    
        RETURN '00000000-0000-0000-0000-000000000000'::UUID;
    
    END;
    
    $$ LANGUAGE plpgsql SECURITY DEFINER;
    
    

    RLS Benefits:

  • Automatic Filtering: Database enforces data isolation
  • Application Simplicity: No complex WHERE clauses needed
  • Security by Default: Impossible to accidentally access wrong data
  • Audit Compliance: Clear data access boundaries
  • 3. Vector Search Schema

    
    -- Product embeddings for semantic search
    
    CREATE TABLE retail.product_description_embeddings (
    
        product_id UUID PRIMARY KEY REFERENCES retail.products(product_id),
    
        description_embedding vector(1536),
    
        last_updated TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    -- Optimize vector similarity search
    
    CREATE INDEX idx_product_embeddings_vector 
    
    ON retail.product_description_embeddings 
    
    USING ivfflat (description_embedding vector_cosine_ops);
    
    
    
    -- Semantic search function
    
    CREATE OR REPLACE FUNCTION search_products_by_description(
    
        query_embedding vector(1536),
    
        similarity_threshold FLOAT DEFAULT 0.7,
    
        max_results INTEGER DEFAULT 20
    
    )
    
    RETURNS TABLE(
    
        product_id UUID,
    
        name VARCHAR,
    
        description TEXT,
    
        similarity_score FLOAT
    
    ) AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            p.product_id,
    
            p.name,
    
            p.description,
    
            (1 - (pde.description_embedding <=> query_embedding)) AS similarity_score
    
        FROM retail.products p
    
        JOIN retail.product_description_embeddings pde ON p.product_id = pde.product_id
    
        WHERE (pde.description_embedding <=> query_embedding) <= (1 - similarity_threshold)
    
        ORDER BY similarity_score DESC
    
        LIMIT max_results;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    

    ๐Ÿ”Œ Connection Management Patterns

    Efficient database connection management is critical for MCP server performance:

    Connection Pool Configuration

    
    class ConnectionPoolManager:
    
        """Manages PostgreSQL connection pools."""
    
        
    
        async def create_pool(self) -> Pool:
    
            """Create optimized connection pool."""
    
            return await asyncpg.create_pool(
    
                host=self.config.db_host,
    
                port=self.config.db_port,
    
                database=self.config.db_name,
    
                user=self.config.db_user,
    
                password=self.config.db_password,
    
                
    
                # Pool configuration
    
                min_size=2,          # Minimum connections
    
                max_size=10,         # Maximum connections
    
                max_inactive_connection_lifetime=300,  # 5 minutes
    
                
    
                # Query configuration
    
                command_timeout=30,   # Query timeout
    
                server_settings={
    
                    "application_name": "zava-mcp-server",
    
                    "jit": "off",          # Disable JIT for stability
    
                    "work_mem": "4MB",     # Limit work memory
    
                    "statement_timeout": "30s"
    
                }
    
            )
    
        
    
        async def execute_with_retry(
    
            self, 
    
            query: str, 
    
            params: Tuple = None,
    
            max_retries: int = 3
    
        ) -> List[Dict[str, Any]]:
    
            """Execute query with automatic retry logic."""
    
            
    
            for attempt in range(max_retries):
    
                try:
    
                    async with self.pool.acquire() as conn:
    
                        if params:
    
                            rows = await conn.fetch(query, *params)
    
                        else:
    
                            rows = await conn.fetch(query)
    
                        return [dict(row) for row in rows]
    
                        
    
                except (ConnectionError, InterfaceError) as e:
    
                    if attempt == max_retries - 1:
    
                        raise
    
                    
    
                    # Exponential backoff
    
                    await asyncio.sleep(2 ** attempt)
    
                    logger.warning(f"Database connection failed, retrying ({attempt + 1}/{max_retries})")
    
    

    Resource Lifecycle Management

    
    class MCPServerManager:
    
        """Manages MCP server lifecycle and resources."""
    
        
    
        async def startup(self):
    
            """Initialize server resources."""
    
            # Create database connection pool
    
            self.db_pool = await self.pool_manager.create_pool()
    
            
    
            # Initialize AI services
    
            self.ai_client = await self.create_ai_client()
    
            
    
            # Setup monitoring
    
            self.metrics_collector = MetricsCollector()
    
            
    
            logger.info("MCP server startup complete")
    
        
    
        async def shutdown(self):
    
            """Cleanup server resources."""
    
            try:
    
                # Close database connections
    
                if self.db_pool:
    
                    await self.db_pool.close()
    
                
    
                # Cleanup AI client
    
                if self.ai_client:
    
                    await self.ai_client.close()
    
                
    
                # Flush metrics
    
                await self.metrics_collector.flush()
    
                
    
                logger.info("MCP server shutdown complete")
    
                
    
            except Exception as e:
    
                logger.error(f"Error during shutdown: {e}")
    
        
    
        async def health_check(self) -> Dict[str, str]:
    
            """Verify server health status."""
    
            status = {}
    
            
    
            # Check database connection
    
            try:
    
                async with self.db_pool.acquire() as conn:
    
                    await conn.fetchval("SELECT 1")
    
                status["database"] = "healthy"
    
            except Exception as e:
    
                status["database"] = f"unhealthy: {e}"
    
            
    
            # Check AI service
    
            try:
    
                await self.ai_client.health_check()
    
                status["ai_service"] = "healthy"
    
            except Exception as e:
    
                status["ai_service"] = f"unhealthy: {e}"
    
            
    
            return status
    
    

    ๐Ÿ›ก๏ธ Error Handling and Resilience Patterns

    Robust error handling ensures reliable MCP server operation:

    Hierarchical Error Types

    
    class MCPError(Exception):
    
        """Base MCP server error."""
    
        def __init__(self, message: str, error_code: str = "MCP_ERROR"):
    
            self.message = message
    
            self.error_code = error_code
    
            super().__init__(message)
    
    
    
    class DatabaseError(MCPError):
    
        """Database operation errors."""
    
        def __init__(self, message: str, query: str = None):
    
            super().__init__(message, "DATABASE_ERROR")
    
            self.query = query
    
    
    
    class AuthorizationError(MCPError):
    
        """Access control errors."""
    
        def __init__(self, message: str, user_id: str = None):
    
            super().__init__(message, "AUTHORIZATION_ERROR")
    
            self.user_id = user_id
    
    
    
    class QueryTimeoutError(DatabaseError):
    
        """Query execution timeout."""
    
        def __init__(self, query: str):
    
            super().__init__(f"Query timeout: {query[:100]}...", query)
    
            self.error_code = "QUERY_TIMEOUT"
    
    
    
    class ValidationError(MCPError):
    
        """Input validation errors."""
    
        def __init__(self, field: str, value: Any, constraint: str):
    
            message = f"Validation failed for {field}: {constraint}"
    
            super().__init__(message, "VALIDATION_ERROR")
    
            self.field = field
    
            self.value = value
    
    

    Error Handling Middleware

    
    @contextmanager
    
    async def error_handling_context(operation_name: str, user_id: str = None):
    
        """Centralized error handling for operations."""
    
        start_time = time.time()
    
        
    
        try:
    
            yield
    
            
    
            # Success metrics
    
            duration = time.time() - start_time
    
            metrics.operation_success.labels(operation=operation_name).inc()
    
            metrics.operation_duration.labels(operation=operation_name).observe(duration)
    
            
    
        except ValidationError as e:
    
            logger.warning(f"Validation error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "validation",
    
                "field": e.field
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="validation").inc()
    
            raise
    
            
    
        except AuthorizationError as e:
    
            logger.warning(f"Authorization error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "authorization"
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="authorization").inc()
    
            raise
    
            
    
        except DatabaseError as e:
    
            logger.error(f"Database error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "database",
    
                "query": e.query[:100] if e.query else None
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="database").inc()
    
            raise
    
            
    
        except Exception as e:
    
            logger.error(f"Unexpected error in {operation_name}: {str(e)}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "unexpected"
    
            }, exc_info=True)
    
            metrics.operation_error.labels(operation=operation_name, type="unexpected").inc()
    
            raise MCPError(f"Internal server error in {operation_name}")
    
    

    ๐Ÿ“Š Performance Optimization Strategies

    Query Performance Monitoring

    
    class QueryPerformanceMonitor:
    
        """Monitor and optimize query performance."""
    
        
    
        def __init__(self):
    
            self.slow_query_threshold = 1.0  # seconds
    
            self.query_stats = defaultdict(list)
    
        
    
        @contextmanager
    
        async def monitor_query(self, query: str, operation_type: str = "unknown"):
    
            """Monitor query execution time and performance."""
    
            start_time = time.time()
    
            query_hash = hashlib.md5(query.encode()).hexdigest()[:8]
    
            
    
            try:
    
                yield
    
                
    
                duration = time.time() - start_time
    
                
    
                # Record performance metrics
    
                self.query_stats[operation_type].append(duration)
    
                
    
                # Log slow queries
    
                if duration > self.slow_query_threshold:
    
                    logger.warning(f"Slow query detected", extra={
    
                        "query_hash": query_hash,
    
                        "duration": duration,
    
                        "operation_type": operation_type,
    
                        "query": query[:200]
    
                    })
    
                
    
                # Update metrics
    
                metrics.query_duration.labels(type=operation_type).observe(duration)
    
                
    
            except Exception as e:
    
                duration = time.time() - start_time
    
                logger.error(f"Query failed", extra={
    
                    "query_hash": query_hash,
    
                    "duration": duration,
    
                    "operation_type": operation_type,
    
                    "error": str(e)
    
                })
    
                raise
    
        
    
        def get_performance_summary(self) -> Dict[str, Any]:
    
            """Generate performance summary report."""
    
            summary = {}
    
            
    
            for operation_type, durations in self.query_stats.items():
    
                if durations:
    
                    summary[operation_type] = {
    
                        "count": len(durations),
    
                        "avg_duration": sum(durations) / len(durations),
    
                        "max_duration": max(durations),
    
                        "min_duration": min(durations),
    
                        "slow_queries": len([d for d in durations if d > self.slow_query_threshold])
    
                    }
    
            
    
            return summary
    
    

    Caching Strategy

    
    class QueryCache:
    
        """Intelligent query result caching."""
    
        
    
        def __init__(self, redis_url: str = None):
    
            self.cache = {}  # In-memory fallback
    
            self.redis_client = redis.Redis.from_url(redis_url) if redis_url else None
    
            self.cache_ttl = 300  # 5 minutes default
    
        
    
        async def get_cached_result(
    
            self, 
    
            cache_key: str, 
    
            query_func: Callable,
    
            ttl: int = None
    
        ) -> Any:
    
            """Get result from cache or execute query."""
    
            ttl = ttl or self.cache_ttl
    
            
    
            # Try cache first
    
            cached_result = await self._get_from_cache(cache_key)
    
            if cached_result is not None:
    
                metrics.cache_hit.labels(type="query").inc()
    
                return cached_result
    
            
    
            # Execute query
    
            metrics.cache_miss.labels(type="query").inc()
    
            result = await query_func()
    
            
    
            # Cache result
    
            await self._set_in_cache(cache_key, result, ttl)
    
            
    
            return result
    
        
    
        def _generate_cache_key(self, query: str, user_context: str) -> str:
    
            """Generate consistent cache key."""
    
            key_data = f"{query}:{user_context}"
    
            return hashlib.sha256(key_data.encode()).hexdigest()
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should understand:

    โœ… Layered Architecture: How to separate concerns in MCP server design

    โœ… Database Patterns: Multi-tenant schema design and RLS implementation

    โœ… Connection Management: Efficient pooling and resource lifecycle

    โœ… Error Handling: Hierarchical error types and resilience patterns

    โœ… Performance Optimization: Monitoring, caching, and query optimization

    โœ… Production Readiness: Infrastructure concerns and operational patterns

    ๐Ÿš€ What's Next

    Continue with Lab 02: Security and Multi-Tenancy to dive deep into:

  • Row Level Security implementation details
  • Authentication and authorization patterns
  • Multi-tenant data isolation strategies
  • Security audit and compliance considerations
  • ๐Ÿ“š Additional Resources

    Architecture Patterns

  • Clean Architecture in Python - Architectural patterns for Python applications
  • Database Design Patterns - Relational database design principles
  • Microservices Patterns - Service architecture patterns
  • PostgreSQL Advanced Topics

  • PostgreSQL Performance Tuning - Database optimization guide
  • Connection Pooling Best Practices - Connection management
  • Query Planning and Optimization - Query performance
  • Python Async Patterns

  • AsyncIO Best Practices - Async programming patterns
  • FastAPI Architecture - Modern Python web architecture
  • Pydantic Models - Data validation and serialization
  • ---

    Next: Ready to explore security patterns? Continue with Lab 02: Security and Multi-Tenancy

    Understanding MCP server architecture, database layers, and security patterns Learn

    Core Architecture Concepts

    ๐ŸŽฏ What This Lab Covers

    This lab provides an in-depth exploration of MCP server architecture patterns, database design principles, and the technical implementation strategies that power robust, scalable database-integrated AI applications.

    Overview

    Building a production-ready MCP server with database integration requires careful architectural decisions.

    This lab breaks down the key components, design patterns, and technical considerations that make our Zava Retail analytics solution robust, secure, and scalable.

    You'll understand how each layer interacts, why specific technologies were chosen, and how to apply these patterns to your own MCP implementations.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Analyze the layered architecture of an MCP server with database integration
  • Understand the role and responsibilities of each architectural component
  • Design database schemas that support multi-tenant MCP applications
  • Implement connection pooling and resource management strategies
  • Apply error handling and logging patterns for production systems
  • Evaluate trade-offs between different architectural approaches
  • ๐Ÿ—๏ธ MCP Server Architecture Layers

    Our MCP server implements a layered architecture that separates concerns and promotes maintainability:

    Layer 1: Protocol Layer (FastMCP)

    Responsibility: Handle MCP protocol communication and message routing

    
    # FastMCP server setup
    
    from fastmcp import FastMCP
    
    
    
    mcp = FastMCP("Zava Retail Analytics")
    
    
    
    # Tool registration with type safety
    
    @mcp.tool()
    
    async def execute_sales_query(
    
        ctx: Context,
    
        postgresql_query: Annotated[str, Field(description="Well-formed PostgreSQL query")]
    
    ) -> str:
    
        """Execute PostgreSQL queries with Row Level Security."""
    
        return await query_executor.execute(postgresql_query, ctx)
    
    

    Key Features:

  • Protocol Compliance: Full MCP specification support
  • Type Safety: Pydantic models for request/response validation
  • Async Support: Non-blocking I/O for high concurrency
  • Error Handling: Standardized error responses
  • Layer 2: Business Logic Layer

    Responsibility: Implement business rules and coordinate between protocol and data layers

    
    class SalesAnalyticsService:
    
        """Business logic for retail analytics operations."""
    
        
    
        async def get_store_performance(
    
            self, 
    
            store_id: str, 
    
            time_period: str
    
        ) -> Dict[str, Any]:
    
            """Calculate store performance metrics."""
    
            
    
            # Validate business rules
    
            if not self._validate_store_access(store_id):
    
                raise UnauthorizedError("Access denied for store")
    
            
    
            # Coordinate data retrieval
    
            sales_data = await self.db_provider.get_sales_data(store_id, time_period)
    
            metrics = self._calculate_metrics(sales_data)
    
            
    
            return {
    
                "store_id": store_id,
    
                "period": time_period,
    
                "metrics": metrics,
    
                "insights": self._generate_insights(metrics)
    
            }
    
    

    Key Features:

  • Business Rule Enforcement: Store access validation and data integrity
  • Service Coordination: Orchestration between database and AI services
  • Data Transformation: Converting raw data to business insights
  • Caching Strategy: Performance optimization for frequent queries
  • Layer 3: Data Access Layer

    Responsibility: Manage database connections, query execution, and data mapping

    
    class PostgreSQLProvider:
    
        """Data access layer for PostgreSQL operations."""
    
        
    
        def __init__(self, connection_config: Dict[str, Any]):
    
            self.connection_pool: Optional[Pool] = None
    
            self.config = connection_config
    
        
    
        async def execute_query(
    
            self, 
    
            query: str, 
    
            rls_user_id: str
    
        ) -> List[Dict[str, Any]]:
    
            """Execute query with RLS context."""
    
            
    
            async with self.connection_pool.acquire() as conn:
    
                # Set RLS context
    
                await conn.execute(
    
                    "SELECT set_config('app.current_rls_user_id', $1, false)",
    
                    rls_user_id
    
                )
    
                
    
                # Execute query with timeout
    
                try:
    
                    rows = await asyncio.wait_for(
    
                        conn.fetch(query),
    
                        timeout=30.0
    
                    )
    
                    return [dict(row) for row in rows]
    
                except asyncio.TimeoutError:
    
                    raise QueryTimeoutError("Query execution exceeded timeout")
    
    

    Key Features:

  • Connection Pooling: Efficient resource management
  • Transaction Management: ACID compliance and rollback handling
  • Query Optimization: Performance monitoring and optimization
  • RLS Integration: Row-level security context management
  • Layer 4: Infrastructure Layer

    Responsibility: Handle cross-cutting concerns like logging, monitoring, and configuration

    
    class InfrastructureManager:
    
        """Infrastructure concerns management."""
    
        
    
        def __init__(self):
    
            self.logger = self._setup_logging()
    
            self.metrics = self._setup_metrics()
    
            self.config = self._load_configuration()
    
        
    
        def _setup_logging(self) -> Logger:
    
            """Configure structured logging."""
    
            logging.basicConfig(
    
                level=logging.INFO,
    
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    
                handlers=[
    
                    logging.StreamHandler(),
    
                    logging.FileHandler('mcp_server.log')
    
                ]
    
            )
    
            return logging.getLogger(__name__)
    
        
    
        async def track_query_execution(
    
            self, 
    
            query_type: str, 
    
            duration: float, 
    
            success: bool
    
        ):
    
            """Track query performance metrics."""
    
            self.metrics.counter('query_total').labels(
    
                type=query_type,
    
                status='success' if success else 'error'
    
            ).inc()
    
            
    
            self.metrics.histogram('query_duration').labels(
    
                type=query_type
    
            ).observe(duration)
    
    

    ๐Ÿ—„๏ธ Database Design Patterns

    Our PostgreSQL schema implements several key patterns for multi-tenant MCP applications:

    1. Multi-Tenant Schema Design

    
    -- Core retail entities with store-based partitioning
    
    CREATE TABLE retail.stores (
    
        store_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        name VARCHAR(100) NOT NULL,
    
        location VARCHAR(200) NOT NULL,
    
        manager_id UUID NOT NULL,
    
        created_at TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    CREATE TABLE retail.customers (
    
        customer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        store_id UUID REFERENCES retail.stores(store_id),
    
        first_name VARCHAR(50) NOT NULL,
    
        last_name VARCHAR(50) NOT NULL,
    
        email VARCHAR(100) UNIQUE,
    
        created_at TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    CREATE TABLE retail.orders (
    
        order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        customer_id UUID REFERENCES retail.customers(customer_id),
    
        store_id UUID REFERENCES retail.stores(store_id),
    
        order_date TIMESTAMP DEFAULT NOW(),
    
        total_amount DECIMAL(10,2) NOT NULL,
    
        status VARCHAR(20) DEFAULT 'pending'
    
    );
    
    

    Design Principles:

  • Foreign Key Consistency: Ensure data integrity across tables
  • Store ID Propagation: Every transactional table includes store_id
  • UUID Primary Keys: Globally unique identifiers for distributed systems
  • Timestamp Tracking: Audit trail for all data changes
  • 2. Row Level Security Implementation

    
    -- Enable RLS on multi-tenant tables
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.orders ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.order_items ENABLE ROW LEVEL SECURITY;
    
    
    
    -- Store manager can only see their store's data
    
    CREATE POLICY store_manager_customers ON retail.customers
    
        FOR ALL TO store_managers
    
        USING (store_id = get_current_user_store());
    
    
    
    CREATE POLICY store_manager_orders ON retail.orders
    
        FOR ALL TO store_managers
    
        USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_orders ON retail.orders
    
        FOR ALL TO regional_managers
    
        USING (store_id = ANY(get_user_store_list()));
    
    
    
    -- Support function for RLS context
    
    CREATE OR REPLACE FUNCTION get_current_user_store()
    
    RETURNS UUID AS $$
    
    BEGIN
    
        RETURN current_setting('app.current_rls_user_id')::UUID;
    
    EXCEPTION WHEN OTHERS THEN
    
        RETURN '00000000-0000-0000-0000-000000000000'::UUID;
    
    END;
    
    $$ LANGUAGE plpgsql SECURITY DEFINER;
    
    

    RLS Benefits:

  • Automatic Filtering: Database enforces data isolation
  • Application Simplicity: No complex WHERE clauses needed
  • Security by Default: Impossible to accidentally access wrong data
  • Audit Compliance: Clear data access boundaries
  • 3. Vector Search Schema

    
    -- Product embeddings for semantic search
    
    CREATE TABLE retail.product_description_embeddings (
    
        product_id UUID PRIMARY KEY REFERENCES retail.products(product_id),
    
        description_embedding vector(1536),
    
        last_updated TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    -- Optimize vector similarity search
    
    CREATE INDEX idx_product_embeddings_vector 
    
    ON retail.product_description_embeddings 
    
    USING ivfflat (description_embedding vector_cosine_ops);
    
    
    
    -- Semantic search function
    
    CREATE OR REPLACE FUNCTION search_products_by_description(
    
        query_embedding vector(1536),
    
        similarity_threshold FLOAT DEFAULT 0.7,
    
        max_results INTEGER DEFAULT 20
    
    )
    
    RETURNS TABLE(
    
        product_id UUID,
    
        name VARCHAR,
    
        description TEXT,
    
        similarity_score FLOAT
    
    ) AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            p.product_id,
    
            p.name,
    
            p.description,
    
            (1 - (pde.description_embedding <=> query_embedding)) AS similarity_score
    
        FROM retail.products p
    
        JOIN retail.product_description_embeddings pde ON p.product_id = pde.product_id
    
        WHERE (pde.description_embedding <=> query_embedding) <= (1 - similarity_threshold)
    
        ORDER BY similarity_score DESC
    
        LIMIT max_results;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    

    ๐Ÿ”Œ Connection Management Patterns

    Efficient database connection management is critical for MCP server performance:

    Connection Pool Configuration

    
    class ConnectionPoolManager:
    
        """Manages PostgreSQL connection pools."""
    
        
    
        async def create_pool(self) -> Pool:
    
            """Create optimized connection pool."""
    
            return await asyncpg.create_pool(
    
                host=self.config.db_host,
    
                port=self.config.db_port,
    
                database=self.config.db_name,
    
                user=self.config.db_user,
    
                password=self.config.db_password,
    
                
    
                # Pool configuration
    
                min_size=2,          # Minimum connections
    
                max_size=10,         # Maximum connections
    
                max_inactive_connection_lifetime=300,  # 5 minutes
    
                
    
                # Query configuration
    
                command_timeout=30,   # Query timeout
    
                server_settings={
    
                    "application_name": "zava-mcp-server",
    
                    "jit": "off",          # Disable JIT for stability
    
                    "work_mem": "4MB",     # Limit work memory
    
                    "statement_timeout": "30s"
    
                }
    
            )
    
        
    
        async def execute_with_retry(
    
            self, 
    
            query: str, 
    
            params: Tuple = None,
    
            max_retries: int = 3
    
        ) -> List[Dict[str, Any]]:
    
            """Execute query with automatic retry logic."""
    
            
    
            for attempt in range(max_retries):
    
                try:
    
                    async with self.pool.acquire() as conn:
    
                        if params:
    
                            rows = await conn.fetch(query, *params)
    
                        else:
    
                            rows = await conn.fetch(query)
    
                        return [dict(row) for row in rows]
    
                        
    
                except (ConnectionError, InterfaceError) as e:
    
                    if attempt == max_retries - 1:
    
                        raise
    
                    
    
                    # Exponential backoff
    
                    await asyncio.sleep(2 ** attempt)
    
                    logger.warning(f"Database connection failed, retrying ({attempt + 1}/{max_retries})")
    
    

    Resource Lifecycle Management

    
    class MCPServerManager:
    
        """Manages MCP server lifecycle and resources."""
    
        
    
        async def startup(self):
    
            """Initialize server resources."""
    
            # Create database connection pool
    
            self.db_pool = await self.pool_manager.create_pool()
    
            
    
            # Initialize AI services
    
            self.ai_client = await self.create_ai_client()
    
            
    
            # Setup monitoring
    
            self.metrics_collector = MetricsCollector()
    
            
    
            logger.info("MCP server startup complete")
    
        
    
        async def shutdown(self):
    
            """Cleanup server resources."""
    
            try:
    
                # Close database connections
    
                if self.db_pool:
    
                    await self.db_pool.close()
    
                
    
                # Cleanup AI client
    
                if self.ai_client:
    
                    await self.ai_client.close()
    
                
    
                # Flush metrics
    
                await self.metrics_collector.flush()
    
                
    
                logger.info("MCP server shutdown complete")
    
                
    
            except Exception as e:
    
                logger.error(f"Error during shutdown: {e}")
    
        
    
        async def health_check(self) -> Dict[str, str]:
    
            """Verify server health status."""
    
            status = {}
    
            
    
            # Check database connection
    
            try:
    
                async with self.db_pool.acquire() as conn:
    
                    await conn.fetchval("SELECT 1")
    
                status["database"] = "healthy"
    
            except Exception as e:
    
                status["database"] = f"unhealthy: {e}"
    
            
    
            # Check AI service
    
            try:
    
                await self.ai_client.health_check()
    
                status["ai_service"] = "healthy"
    
            except Exception as e:
    
                status["ai_service"] = f"unhealthy: {e}"
    
            
    
            return status
    
    

    ๐Ÿ›ก๏ธ Error Handling and Resilience Patterns

    Robust error handling ensures reliable MCP server operation:

    Hierarchical Error Types

    
    class MCPError(Exception):
    
        """Base MCP server error."""
    
        def __init__(self, message: str, error_code: str = "MCP_ERROR"):
    
            self.message = message
    
            self.error_code = error_code
    
            super().__init__(message)
    
    
    
    class DatabaseError(MCPError):
    
        """Database operation errors."""
    
        def __init__(self, message: str, query: str = None):
    
            super().__init__(message, "DATABASE_ERROR")
    
            self.query = query
    
    
    
    class AuthorizationError(MCPError):
    
        """Access control errors."""
    
        def __init__(self, message: str, user_id: str = None):
    
            super().__init__(message, "AUTHORIZATION_ERROR")
    
            self.user_id = user_id
    
    
    
    class QueryTimeoutError(DatabaseError):
    
        """Query execution timeout."""
    
        def __init__(self, query: str):
    
            super().__init__(f"Query timeout: {query[:100]}...", query)
    
            self.error_code = "QUERY_TIMEOUT"
    
    
    
    class ValidationError(MCPError):
    
        """Input validation errors."""
    
        def __init__(self, field: str, value: Any, constraint: str):
    
            message = f"Validation failed for {field}: {constraint}"
    
            super().__init__(message, "VALIDATION_ERROR")
    
            self.field = field
    
            self.value = value
    
    

    Error Handling Middleware

    
    @contextmanager
    
    async def error_handling_context(operation_name: str, user_id: str = None):
    
        """Centralized error handling for operations."""
    
        start_time = time.time()
    
        
    
        try:
    
            yield
    
            
    
            # Success metrics
    
            duration = time.time() - start_time
    
            metrics.operation_success.labels(operation=operation_name).inc()
    
            metrics.operation_duration.labels(operation=operation_name).observe(duration)
    
            
    
        except ValidationError as e:
    
            logger.warning(f"Validation error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "validation",
    
                "field": e.field
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="validation").inc()
    
            raise
    
            
    
        except AuthorizationError as e:
    
            logger.warning(f"Authorization error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "authorization"
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="authorization").inc()
    
            raise
    
            
    
        except DatabaseError as e:
    
            logger.error(f"Database error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "database",
    
                "query": e.query[:100] if e.query else None
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="database").inc()
    
            raise
    
            
    
        except Exception as e:
    
            logger.error(f"Unexpected error in {operation_name}: {str(e)}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "unexpected"
    
            }, exc_info=True)
    
            metrics.operation_error.labels(operation=operation_name, type="unexpected").inc()
    
            raise MCPError(f"Internal server error in {operation_name}")
    
    

    ๐Ÿ“Š Performance Optimization Strategies

    Query Performance Monitoring

    
    class QueryPerformanceMonitor:
    
        """Monitor and optimize query performance."""
    
        
    
        def __init__(self):
    
            self.slow_query_threshold = 1.0  # seconds
    
            self.query_stats = defaultdict(list)
    
        
    
        @contextmanager
    
        async def monitor_query(self, query: str, operation_type: str = "unknown"):
    
            """Monitor query execution time and performance."""
    
            start_time = time.time()
    
            query_hash = hashlib.md5(query.encode()).hexdigest()[:8]
    
            
    
            try:
    
                yield
    
                
    
                duration = time.time() - start_time
    
                
    
                # Record performance metrics
    
                self.query_stats[operation_type].append(duration)
    
                
    
                # Log slow queries
    
                if duration > self.slow_query_threshold:
    
                    logger.warning(f"Slow query detected", extra={
    
                        "query_hash": query_hash,
    
                        "duration": duration,
    
                        "operation_type": operation_type,
    
                        "query": query[:200]
    
                    })
    
                
    
                # Update metrics
    
                metrics.query_duration.labels(type=operation_type).observe(duration)
    
                
    
            except Exception as e:
    
                duration = time.time() - start_time
    
                logger.error(f"Query failed", extra={
    
                    "query_hash": query_hash,
    
                    "duration": duration,
    
                    "operation_type": operation_type,
    
                    "error": str(e)
    
                })
    
                raise
    
        
    
        def get_performance_summary(self) -> Dict[str, Any]:
    
            """Generate performance summary report."""
    
            summary = {}
    
            
    
            for operation_type, durations in self.query_stats.items():
    
                if durations:
    
                    summary[operation_type] = {
    
                        "count": len(durations),
    
                        "avg_duration": sum(durations) / len(durations),
    
                        "max_duration": max(durations),
    
                        "min_duration": min(durations),
    
                        "slow_queries": len([d for d in durations if d > self.slow_query_threshold])
    
                    }
    
            
    
            return summary
    
    

    Caching Strategy

    
    class QueryCache:
    
        """Intelligent query result caching."""
    
        
    
        def __init__(self, redis_url: str = None):
    
            self.cache = {}  # In-memory fallback
    
            self.redis_client = redis.Redis.from_url(redis_url) if redis_url else None
    
            self.cache_ttl = 300  # 5 minutes default
    
        
    
        async def get_cached_result(
    
            self, 
    
            cache_key: str, 
    
            query_func: Callable,
    
            ttl: int = None
    
        ) -> Any:
    
            """Get result from cache or execute query."""
    
            ttl = ttl or self.cache_ttl
    
            
    
            # Try cache first
    
            cached_result = await self._get_from_cache(cache_key)
    
            if cached_result is not None:
    
                metrics.cache_hit.labels(type="query").inc()
    
                return cached_result
    
            
    
            # Execute query
    
            metrics.cache_miss.labels(type="query").inc()
    
            result = await query_func()
    
            
    
            # Cache result
    
            await self._set_in_cache(cache_key, result, ttl)
    
            
    
            return result
    
        
    
        def _generate_cache_key(self, query: str, user_context: str) -> str:
    
            """Generate consistent cache key."""
    
            key_data = f"{query}:{user_context}"
    
            return hashlib.sha256(key_data.encode()).hexdigest()
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should understand:

    โœ… Layered Architecture: How to separate concerns in MCP server design

    โœ… Database Patterns: Multi-tenant schema design and RLS implementation

    โœ… Connection Management: Efficient pooling and resource lifecycle

    โœ… Error Handling: Hierarchical error types and resilience patterns

    โœ… Performance Optimization: Monitoring, caching, and query optimization

    โœ… Production Readiness: Infrastructure concerns and operational patterns

    ๐Ÿš€ What's Next

    Continue with Lab 02: Security and Multi-Tenancy to dive deep into:

  • Row Level Security implementation details
  • Authentication and authorization patterns
  • Multi-tenant data isolation strategies
  • Security audit and compliance considerations
  • ๐Ÿ“š Additional Resources

    Architecture Patterns

  • Clean Architecture in Python - Architectural patterns for Python applications
  • Database Design Patterns - Relational database design principles
  • Microservices Patterns - Service architecture patterns
  • PostgreSQL Advanced Topics

  • PostgreSQL Performance Tuning - Database optimization guide
  • Connection Pooling Best Practices - Connection management
  • Query Planning and Optimization - Query performance
  • Python Async Patterns

  • AsyncIO Best Practices - Async programming patterns
  • FastAPI Architecture - Modern Python web architecture
  • Pydantic Models - Data validation and serialization
  • ---

    Next: Ready to explore security patterns? Continue with Lab 02: Security and Multi-Tenancy

    02 Security and Multi-Tenancy

    Security and Multi-Tenancy

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on implementing enterprise-grade security and multi-tenancy for MCP servers.

    You'll learn to design secure, compliant systems that protect sensitive retail data while enabling flexible access patterns across multiple tenants.

    Overview

    Security is paramount in retail applications that handle customer data, payment information, and business intelligence.

    This lab covers the complete security architecture from authentication and authorization to data isolation and compliance monitoring.

    We implement a defense-in-depth strategy combining Azure identity services, PostgreSQL Row Level Security, application-level controls, and comprehensive audit logging to create a robust, compliant platform.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Implement enterprise-grade Row Level Security for multi-tenant data isolation
  • Design secure authentication and authorization patterns with Azure
  • Configure comprehensive audit logging for compliance requirements
  • Apply defense-in-depth security strategies across all application layers
  • Validate security implementations through systematic testing
  • Monitor security events and respond to potential threats
  • ๐Ÿ” Multi-Tenant Security Architecture

    Security Layers Overview

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚               Azure Front Door                  โ”‚ โ† WAF, DDoS Protection
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚              Application Gateway                โ”‚ โ† SSL Termination, Rate Limiting
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚                MCP Server                       โ”‚ โ† Authentication, Authorization
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚           Connection Layer                  โ”‚ โ† Connection Pooling, Circuit Breakers
    
    โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚         Business Logic Layer               โ”‚ โ† Input Validation, Business Rules
    
    โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚           Data Access Layer                โ”‚ โ† Query Sanitization, RLS Context
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚              PostgreSQL RLS                    โ”‚ โ† Row Level Security, Audit Triggers
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Multi-Tenancy Models

    Our implementation uses the Shared Database, Shared Schema model with Row Level Security:

    Benefits:

  • Cost-effective resource utilization
  • Simplified maintenance and updates
  • Strong data isolation through RLS
  • Compliance-friendly audit trails
  • Trade-offs:

  • Requires careful RLS policy design
  • Schema changes affect all tenants
  • Need robust backup/restore procedures
  • ๐Ÿ›ก๏ธ Row Level Security Implementation

    RLS Foundation

    
    -- Enable RLS on all multi-tenant tables
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.products ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.sales_transactions ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.sales_transaction_items ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.product_embeddings ENABLE ROW LEVEL SECURITY;
    
    
    
    -- Create application role for MCP server
    
    CREATE ROLE mcp_user LOGIN;
    
    GRANT USAGE ON SCHEMA retail TO mcp_user;
    
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA retail TO mcp_user;
    
    

    Store Context Management

    
    -- Function to securely set store context
    
    CREATE OR REPLACE FUNCTION retail.set_store_context(store_id_param VARCHAR(50))
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    SET search_path = retail, pg_temp
    
    AS $$
    
    DECLARE
    
        user_info RECORD;
    
    BEGIN
    
        -- Validate store exists and is active
    
        SELECT store_id, store_name, is_active 
    
        INTO user_info
    
        FROM retail.stores 
    
        WHERE store_id = store_id_param;
    
        
    
        IF NOT FOUND THEN
    
            RAISE EXCEPTION 'Store not found: %', store_id_param
    
                USING ERRCODE = 'invalid_parameter_value',
    
                      HINT = 'Verify store ID and ensure it exists in the system';
    
        END IF;
    
        
    
        IF NOT user_info.is_active THEN
    
            RAISE EXCEPTION 'Store is inactive: %', store_id_param
    
                USING ERRCODE = 'insufficient_privilege',
    
                      HINT = 'Contact administrator to activate store';
    
        END IF;
    
        
    
        -- Set the secure context
    
        PERFORM set_config('app.current_store_id', store_id_param, false);
    
        PERFORM set_config('app.store_name', user_info.store_name, false);
    
        PERFORM set_config('app.context_set_at', extract(epoch from current_timestamp)::text, false);
    
        
    
        -- Log context change for audit
    
        INSERT INTO retail.security_audit_log (
    
            event_type,
    
            user_name,
    
            store_id,
    
            ip_address,
    
            user_agent,
    
            details,
    
            severity
    
        ) VALUES (
    
            'store_context_set',
    
            current_user,
    
            store_id_param,
    
            inet_client_addr()::text,
    
            current_setting('application_name', true),
    
            jsonb_build_object(
    
                'store_name', user_info.store_name,
    
                'timestamp', current_timestamp,
    
                'session_id', pg_backend_pid()
    
            ),
    
            'INFO'
    
        );
    
    END;
    
    $$;
    
    
    
    -- Grant execute to MCP user
    
    GRANT EXECUTE ON FUNCTION retail.set_store_context TO mcp_user;
    
    

    RLS Policies

    
    -- Customers RLS Policy
    
    CREATE POLICY customers_store_isolation ON retail.customers
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Products RLS Policy with additional business rules
    
    CREATE POLICY products_store_isolation ON retail.products
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
            AND is_active = TRUE  -- Additional business rule
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Sales Transactions RLS Policy
    
    CREATE POLICY sales_transactions_store_isolation ON retail.sales_transactions
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Transaction Items RLS Policy (via join)
    
    CREATE POLICY sales_transaction_items_store_isolation ON retail.sales_transaction_items
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        )
    
        WITH CHECK (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        );
    
    
    
    -- Product Embeddings RLS Policy
    
    CREATE POLICY product_embeddings_store_isolation ON retail.product_embeddings
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    

    RLS Testing and Validation

    
    -- Test RLS policies with different store contexts
    
    DO $$
    
    DECLARE
    
        test_result RECORD;
    
        customer_count INTEGER;
    
        product_count INTEGER;
    
    BEGIN
    
        -- Test Seattle store context
    
        PERFORM retail.set_store_context('seattle');
    
        
    
        SELECT COUNT(*) INTO customer_count FROM retail.customers;
    
        SELECT COUNT(*) INTO product_count FROM retail.products;
    
        
    
        RAISE NOTICE 'Seattle store - Customers: %, Products: %', customer_count, product_count;
    
        
    
        -- Test Redmond store context
    
        PERFORM retail.set_store_context('redmond');
    
        
    
        SELECT COUNT(*) INTO customer_count FROM retail.customers;
    
        SELECT COUNT(*) INTO product_count FROM retail.products;
    
        
    
        RAISE NOTICE 'Redmond store - Customers: %, Products: %', customer_count, product_count;
    
        
    
        -- Verify data isolation
    
        IF customer_count > 0 AND product_count > 0 THEN
    
            RAISE NOTICE 'RLS policies are working correctly';
    
        ELSE
    
            RAISE WARNING 'RLS policies may not be configured correctly';
    
        END IF;
    
    END;
    
    $$;
    
    

    ๐Ÿ”‘ Authentication and Authorization

    Azure Entra ID Integration

    
    # mcp_server/security/authentication.py
    
    """
    
    Azure Entra ID authentication for MCP server.
    
    """
    
    import os
    
    import jwt
    
    import aiohttp
    
    import asyncio
    
    from typing import Dict, Optional, List
    
    from datetime import datetime, timezone
    
    from azure.identity.aio import DefaultAzureCredential
    
    from azure.keyvault.secrets.aio import SecretClient
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class AzureAuthenticator:
    
        """Handle Azure Entra ID authentication and token validation."""
    
        
    
        def __init__(self):
    
            self.tenant_id = os.getenv('AZURE_TENANT_ID')
    
            self.client_id = os.getenv('AZURE_CLIENT_ID')
    
            self.audience = os.getenv('AZURE_AUDIENCE', self.client_id)
    
            self.issuer = f"https://login.microsoftonline.com/{self.tenant_id}/v2.0"
    
            
    
            # Cache for JWKS (JSON Web Key Set)
    
            self._jwks_cache = None
    
            self._jwks_cache_expiry = None
    
            
    
            # Key Vault for secrets
    
            self.key_vault_url = os.getenv('AZURE_KEY_VAULT_URL')
    
            self.credential = DefaultAzureCredential()
    
            
    
            if self.key_vault_url:
    
                self.secret_client = SecretClient(
    
                    vault_url=self.key_vault_url,
    
                    credential=self.credential
    
                )
    
        
    
        async def validate_token(self, token: str) -> Dict:
    
            """Validate JWT token from Azure Entra ID."""
    
            
    
            try:
    
                # Get signing keys
    
                signing_keys = await self._get_signing_keys()
    
                
    
                # Decode token header to get key ID
    
                unverified_header = jwt.get_unverified_header(token)
    
                key_id = unverified_header.get('kid')
    
                
    
                if not key_id:
    
                    raise ValueError("Token missing key ID")
    
                
    
                # Find the corresponding key
    
                signing_key = None
    
                for key in signing_keys:
    
                    if key['kid'] == key_id:
    
                        signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
    
                        break
    
                
    
                if not signing_key:
    
                    raise ValueError(f"Unable to find signing key for kid: {key_id}")
    
                
    
                # Validate and decode token
    
                payload = jwt.decode(
    
                    token,
    
                    signing_key,
    
                    algorithms=['RS256'],
    
                    audience=self.audience,
    
                    issuer=self.issuer,
    
                    options={
    
                        'verify_exp': True,
    
                        'verify_aud': True,
    
                        'verify_iss': True
    
                    }
    
                )
    
                
    
                # Extract user information
    
                user_info = self._extract_user_info(payload)
    
                
    
                # Log successful authentication
    
                logger.info(
    
                    "User authenticated successfully",
    
                    extra={
    
                        'user_id': user_info['user_id'],
    
                        'email': user_info.get('email'),
    
                        'tenant_id': payload.get('tid')
    
                    }
    
                )
    
                
    
                return user_info
    
                
    
            except jwt.ExpiredSignatureError:
    
                logger.warning("Token has expired")
    
                raise ValueError("Token has expired")
    
            except jwt.InvalidAudienceError:
    
                logger.warning(f"Invalid audience in token. Expected: {self.audience}")
    
                raise ValueError("Invalid token audience")
    
            except jwt.InvalidIssuerError:
    
                logger.warning(f"Invalid issuer in token. Expected: {self.issuer}")
    
                raise ValueError("Invalid token issuer")
    
            except Exception as e:
    
                logger.error(f"Token validation failed: {str(e)}")
    
                raise ValueError(f"Token validation failed: {str(e)}")
    
        
    
        async def _get_signing_keys(self) -> List[Dict]:
    
            """Get JWKS from Azure Entra ID with caching."""
    
            
    
            current_time = datetime.now(timezone.utc)
    
            
    
            # Check if cache is valid
    
            if (self._jwks_cache and self._jwks_cache_expiry and 
    
                current_time < self._jwks_cache_expiry):
    
                return self._jwks_cache
    
            
    
            # Fetch new JWKS
    
            jwks_url = f"{self.issuer}/keys"
    
            
    
            async with aiohttp.ClientSession() as session:
    
                async with session.get(jwks_url) as response:
    
                    if response.status != 200:
    
                        raise Exception(f"Failed to fetch JWKS: {response.status}")
    
                    
    
                    jwks_data = await response.json()
    
                    
    
            # Cache for 1 hour
    
            self._jwks_cache = jwks_data['keys']
    
            self._jwks_cache_expiry = current_time.replace(
    
                hour=current_time.hour + 1
    
            )
    
            
    
            return self._jwks_cache
    
        
    
        def _extract_user_info(self, payload: Dict) -> Dict:
    
            """Extract user information from JWT payload."""
    
            
    
            return {
    
                'user_id': payload.get('oid') or payload.get('sub'),
    
                'email': payload.get('email') or payload.get('preferred_username'),
    
                'name': payload.get('name'),
    
                'tenant_id': payload.get('tid'),
    
                'roles': payload.get('roles', []),
    
                'groups': payload.get('groups', []),
    
                'app_roles': payload.get('app_roles', []),
    
                'scope': payload.get('scp', '').split() if payload.get('scp') else [],
    
                'expires_at': datetime.fromtimestamp(payload['exp'], timezone.utc),
    
                'issued_at': datetime.fromtimestamp(payload['iat'], timezone.utc)
    
            }
    
        
    
        async def get_user_store_access(self, user_id: str) -> List[str]:
    
            """Get list of stores the user has access to."""
    
            
    
            try:
    
                # This would typically query your user/store mapping
    
                # For demo, we'll use a simple Key Vault secret
    
                secret_name = f"user-{user_id}-stores"
    
                
    
                if self.secret_client:
    
                    secret = await self.secret_client.get_secret(secret_name)
    
                    store_list = secret.value.split(',')
    
                    return [store.strip() for store in store_list if store.strip()]
    
                
    
                # Fallback: return default store access
    
                logger.warning(f"No store mapping found for user {user_id}, using default")
    
                return ['seattle']  # Default store access
    
                
    
            except Exception as e:
    
                logger.error(f"Failed to get store access for user {user_id}: {e}")
    
                return []  # No access if we can't determine stores
    
    
    
    # Global authenticator instance
    
    azure_authenticator = AzureAuthenticator()
    
    

    Authorization Middleware

    
    # mcp_server/security/authorization.py
    
    """
    
    Authorization middleware and decorators for MCP server.
    
    """
    
    import functools
    
    from typing import Dict, List, Optional, Callable, Any
    
    from fastapi import HTTPException, status, Request
    
    from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    security = HTTPBearer()
    
    
    
    class AuthorizationError(Exception):
    
        """Custom authorization error."""
    
        pass
    
    
    
    class RoleBasedAuth:
    
        """Role-based access control implementation."""
    
        
    
        # Define role hierarchy
    
        ROLE_HIERARCHY = {
    
            'store_admin': ['store_manager', 'store_user', 'store_readonly'],
    
            'store_manager': ['store_user', 'store_readonly'],
    
            'store_user': ['store_readonly'],
    
            'store_readonly': []
    
        }
    
        
    
        # Define permissions for each role
    
        ROLE_PERMISSIONS = {
    
            'store_admin': [
    
                'read_all', 'write_all', 'delete_all', 'manage_users'
    
            ],
    
            'store_manager': [
    
                'read_all', 'write_transactions', 'write_inventory', 'read_reports'
    
            ],
    
            'store_user': [
    
                'read_products', 'read_customers', 'write_transactions'
    
            ],
    
            'store_readonly': [
    
                'read_products', 'read_basic_reports'
    
            ]
    
        }
    
        
    
        @classmethod
    
        def has_permission(cls, user_roles: List[str], required_permission: str) -> bool:
    
            """Check if user has required permission."""
    
            
    
            user_permissions = set()
    
            
    
            for role in user_roles:
    
                # Add direct permissions
    
                user_permissions.update(cls.ROLE_PERMISSIONS.get(role, []))
    
                
    
                # Add inherited permissions
    
                inherited_roles = cls.ROLE_HIERARCHY.get(role, [])
    
                for inherited_role in inherited_roles:
    
                    user_permissions.update(cls.ROLE_PERMISSIONS.get(inherited_role, []))
    
            
    
            return required_permission in user_permissions
    
        
    
        @classmethod
    
        def get_user_stores(cls, user_info: Dict) -> List[str]:
    
            """Extract stores user has access to from user info."""
    
            
    
            # This would typically come from your user management system
    
            # For demo, we'll extract from custom claims or groups
    
            
    
            stores = []
    
            
    
            # Check for direct store assignments in groups
    
            for group in user_info.get('groups', []):
    
                if group.startswith('store_'):
    
                    store_id = group.replace('store_', '')
    
                    stores.append(store_id)
    
            
    
            # Check for app-specific roles
    
            for role in user_info.get('app_roles', []):
    
                if 'store:' in role:
    
                    _, store_id = role.split('store:', 1)
    
                    stores.append(store_id)
    
            
    
            return list(set(stores))  # Remove duplicates
    
    
    
    def require_auth(required_permission: str = None, require_store_access: bool = True):
    
        """Decorator to require authentication and authorization."""
    
        
    
        def decorator(func: Callable) -> Callable:
    
            @functools.wraps(func)
    
            async def wrapper(*args, **kwargs):
    
                # Extract request from args (FastAPI dependency injection)
    
                request = None
    
                for arg in args:
    
                    if isinstance(arg, Request):
    
                        request = arg
    
                        break
    
                
    
                if not request:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    
                        detail="Request object not found"
    
                    )
    
                
    
                # Get authorization header
    
                auth_header = request.headers.get('Authorization')
    
                if not auth_header or not auth_header.startswith('Bearer '):
    
                    raise HTTPException(
    
                        status_code=status.HTTP_401_UNAUTHORIZED,
    
                        detail="Missing or invalid authorization header",
    
                        headers={"WWW-Authenticate": "Bearer"}
    
                    )
    
                
    
                token = auth_header.split(' ')[1]
    
                
    
                try:
    
                    # Validate token
    
                    user_info = await azure_authenticator.validate_token(token)
    
                    
    
                    # Check required permission
    
                    if required_permission:
    
                        user_roles = user_info.get('roles', [])
    
                        if not RoleBasedAuth.has_permission(user_roles, required_permission):
    
                            raise HTTPException(
    
                                status_code=status.HTTP_403_FORBIDDEN,
    
                                detail=f"Insufficient permissions. Required: {required_permission}"
    
                            )
    
                    
    
                    # Check store access
    
                    if require_store_access:
    
                        user_stores = RoleBasedAuth.get_user_stores(user_info)
    
                        if not user_stores:
    
                            raise HTTPException(
    
                                status_code=status.HTTP_403_FORBIDDEN,
    
                                detail="No store access configured for user"
    
                            )
    
                        
    
                        # Set default store context (first accessible store)
    
                        request.state.current_store = user_stores[0]
    
                        request.state.accessible_stores = user_stores
    
                    
    
                    # Add user info to request state
    
                    request.state.user_info = user_info
    
                    request.state.user_id = user_info['user_id']
    
                    
    
                    # Call the original function
    
                    return await func(*args, **kwargs)
    
                    
    
                except ValueError as e:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_401_UNAUTHORIZED,
    
                        detail=str(e),
    
                        headers={"WWW-Authenticate": "Bearer"}
    
                    )
    
                except AuthorizationError as e:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_403_FORBIDDEN,
    
                        detail=str(e)
    
                    )
    
            
    
            return wrapper
    
        return decorator
    
    
    
    def require_store_context(store_param: str = 'store_id'):
    
        """Decorator to validate and set store context."""
    
        
    
        def decorator(func: Callable) -> Callable:
    
            @functools.wraps(func)
    
            async def wrapper(*args, **kwargs):
    
                # Get store_id from kwargs
    
                store_id = kwargs.get(store_param)
    
                
    
                if not store_id:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_400_BAD_REQUEST,
    
                        detail=f"Missing required parameter: {store_param}"
    
                    )
    
                
    
                # Extract request from args
    
                request = None
    
                for arg in args:
    
                    if isinstance(arg, Request):
    
                        request = arg
    
                        break
    
                
    
                if not request or not hasattr(request.state, 'accessible_stores'):
    
                    raise HTTPException(
    
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    
                        detail="Authentication required before store context validation"
    
                    )
    
                
    
                # Validate user has access to requested store
    
                if store_id not in request.state.accessible_stores:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_403_FORBIDDEN,
    
                        detail=f"Access denied to store: {store_id}"
    
                    )
    
                
    
                # Set store context in request state
    
                request.state.current_store = store_id
    
                
    
                return await func(*args, **kwargs)
    
            
    
            return wrapper
    
        return decorator
    
    

    ๐Ÿ” Security Audit and Compliance

    Comprehensive Audit Logging

    
    -- Security audit log table
    
    CREATE TABLE retail.security_audit_log (
    
        log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        event_type VARCHAR(100) NOT NULL,
    
        user_name VARCHAR(100) NOT NULL,
    
        user_id VARCHAR(100),
    
        store_id VARCHAR(50),
    
        ip_address INET,
    
        user_agent TEXT,
    
        request_id VARCHAR(100),
    
        session_id VARCHAR(100),
    
        resource_type VARCHAR(100),
    
        resource_id VARCHAR(100),
    
        action VARCHAR(50) NOT NULL,
    
        success BOOLEAN NOT NULL DEFAULT TRUE,
    
        failure_reason TEXT,
    
        details JSONB,
    
        severity VARCHAR(20) DEFAULT 'INFO',
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure proper indexing for security queries
    
        CONSTRAINT valid_severity CHECK (severity IN ('DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'))
    
    );
    
    
    
    -- Indexes for security audit queries
    
    CREATE INDEX idx_security_audit_event_type ON retail.security_audit_log(event_type);
    
    CREATE INDEX idx_security_audit_user_name ON retail.security_audit_log(user_name);
    
    CREATE INDEX idx_security_audit_store_id ON retail.security_audit_log(store_id);
    
    CREATE INDEX idx_security_audit_created_at ON retail.security_audit_log(created_at);
    
    CREATE INDEX idx_security_audit_success ON retail.security_audit_log(success);
    
    CREATE INDEX idx_security_audit_severity ON retail.security_audit_log(severity);
    
    CREATE INDEX idx_security_audit_details ON retail.security_audit_log USING GIN(details);
    
    
    
    -- Function to log security events
    
    CREATE OR REPLACE FUNCTION retail.log_security_event(
    
        p_event_type VARCHAR(100),
    
        p_user_name VARCHAR(100),
    
        p_user_id VARCHAR(100) DEFAULT NULL,
    
        p_store_id VARCHAR(50) DEFAULT NULL,
    
        p_ip_address TEXT DEFAULT NULL,
    
        p_action VARCHAR(50) DEFAULT 'unknown',
    
        p_success BOOLEAN DEFAULT TRUE,
    
        p_failure_reason TEXT DEFAULT NULL,
    
        p_details JSONB DEFAULT NULL,
    
        p_severity VARCHAR(20) DEFAULT 'INFO'
    
    )
    
    RETURNS UUID
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    DECLARE
    
        log_id UUID;
    
    BEGIN
    
        INSERT INTO retail.security_audit_log (
    
            event_type,
    
            user_name,
    
            user_id,
    
            store_id,
    
            ip_address,
    
            action,
    
            success,
    
            failure_reason,
    
            details,
    
            severity
    
        ) VALUES (
    
            p_event_type,
    
            p_user_name,
    
            p_user_id,
    
            p_store_id,
    
            p_ip_address::INET,
    
            p_action,
    
            p_success,
    
            p_failure_reason,
    
            p_details,
    
            p_severity
    
        ) RETURNING log_id INTO log_id;
    
        
    
        RETURN log_id;
    
    END;
    
    $$;
    
    
    
    -- Grant execute to MCP user
    
    GRANT EXECUTE ON FUNCTION retail.log_security_event TO mcp_user;
    
    

    Security Monitoring Views

    
    -- Failed authentication attempts
    
    CREATE VIEW retail.security_failed_auth AS
    
    SELECT 
    
        event_type,
    
        user_name,
    
        ip_address,
    
        COUNT(*) as attempt_count,
    
        MIN(created_at) as first_attempt,
    
        MAX(created_at) as last_attempt,
    
        ARRAY_AGG(DISTINCT failure_reason) as failure_reasons
    
    FROM retail.security_audit_log
    
    WHERE success = FALSE 
    
      AND event_type IN ('authentication_failed', 'token_validation_failed')
    
      AND created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
    
    GROUP BY event_type, user_name, ip_address
    
    HAVING COUNT(*) >= 3  -- 3 or more failures
    
    ORDER BY attempt_count DESC, last_attempt DESC;
    
    
    
    -- Suspicious access patterns
    
    CREATE VIEW retail.security_suspicious_access AS
    
    SELECT 
    
        user_name,
    
        user_id,
    
        COUNT(DISTINCT ip_address) as ip_count,
    
        COUNT(DISTINCT store_id) as store_count,
    
        ARRAY_AGG(DISTINCT ip_address::TEXT) as ip_addresses,
    
        ARRAY_AGG(DISTINCT store_id) as stores_accessed,
    
        MIN(created_at) as first_access,
    
        MAX(created_at) as last_access
    
    FROM retail.security_audit_log
    
    WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
      AND success = TRUE
    
    GROUP BY user_name, user_id
    
    HAVING COUNT(DISTINCT ip_address) > 3  -- Access from multiple IPs
    
       OR COUNT(DISTINCT store_id) > 2     -- Access to multiple stores
    
    ORDER BY ip_count DESC, store_count DESC;
    
    
    
    -- Data access patterns
    
    CREATE VIEW retail.security_data_access_summary AS
    
    SELECT 
    
        DATE_TRUNC('hour', created_at) as access_hour,
    
        store_id,
    
        resource_type,
    
        action,
    
        COUNT(*) as access_count,
    
        COUNT(DISTINCT user_id) as unique_users
    
    FROM retail.security_audit_log
    
    WHERE resource_type IS NOT NULL
    
      AND created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
    
    GROUP BY DATE_TRUNC('hour', created_at), store_id, resource_type, action
    
    ORDER BY access_hour DESC, access_count DESC;
    
    

    Security Event Monitoring

    
    # mcp_server/security/monitoring.py
    
    """
    
    Security monitoring and alerting for MCP server.
    
    """
    
    import asyncio
    
    import asyncpg
    
    from typing import Dict, List, Any
    
    from datetime import datetime, timedelta
    
    from dataclasses import dataclass
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    @dataclass
    
    class SecurityAlert:
    
        """Security alert data structure."""
    
        alert_type: str
    
        severity: str
    
        message: str
    
        details: Dict[str, Any]
    
        timestamp: datetime
    
    
    
    class SecurityMonitor:
    
        """Monitor security events and generate alerts."""
    
        
    
        def __init__(self, db_connection_string: str):
    
            self.db_connection_string = db_connection_string
    
            self.alert_handlers = []
    
            
    
            # Alert thresholds
    
            self.thresholds = {
    
                'failed_auth_attempts': 5,      # per user per hour
    
                'multiple_ip_access': 3,        # different IPs per user per hour
    
                'excessive_data_access': 1000,  # queries per user per hour
    
                'privilege_escalation': 1,      # any attempt
    
                'unauthorized_store_access': 1  # any attempt
    
            }
    
        
    
        async def start_monitoring(self):
    
            """Start security monitoring loop."""
    
            logger.info("Starting security monitoring")
    
            
    
            while True:
    
                try:
    
                    await self._check_security_events()
    
                    await asyncio.sleep(300)  # Check every 5 minutes
    
                except Exception as e:
    
                    logger.error(f"Security monitoring error: {e}")
    
                    await asyncio.sleep(60)  # Short retry on error
    
        
    
        async def _check_security_events(self):
    
            """Check for security events and generate alerts."""
    
            
    
            conn = await asyncpg.connect(self.db_connection_string)
    
            
    
            try:
    
                # Check failed authentication attempts
    
                await self._check_failed_auth(conn)
    
                
    
                # Check suspicious access patterns
    
                await self._check_suspicious_access(conn)
    
                
    
                # Check data access anomalies
    
                await self._check_data_access_anomalies(conn)
    
                
    
                # Check unauthorized access attempts
    
                await self._check_unauthorized_access(conn)
    
                
    
            finally:
    
                await conn.close()
    
        
    
        async def _check_failed_auth(self, conn):
    
            """Check for excessive failed authentication attempts."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                ip_address,
    
                COUNT(*) as attempt_count,
    
                MAX(created_at) as last_attempt
    
            FROM retail.security_audit_log
    
            WHERE success = FALSE 
    
              AND event_type IN ('authentication_failed', 'token_validation_failed')
    
              AND created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
            GROUP BY user_name, ip_address
    
            HAVING COUNT(*) >= $1
    
            """
    
            
    
            results = await conn.fetch(query, self.thresholds['failed_auth_attempts'])
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='failed_authentication',
    
                    severity='HIGH',
    
                    message=f"Excessive failed login attempts for user {record['user_name']}",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'ip_address': str(record['ip_address']),
    
                        'attempt_count': record['attempt_count'],
    
                        'last_attempt': record['last_attempt'].isoformat()
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _check_suspicious_access(self, conn):
    
            """Check for suspicious access patterns."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                user_id,
    
                COUNT(DISTINCT ip_address) as ip_count,
    
                ARRAY_AGG(DISTINCT ip_address::TEXT) as ip_addresses
    
            FROM retail.security_audit_log
    
            WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
              AND success = TRUE
    
            GROUP BY user_name, user_id
    
            HAVING COUNT(DISTINCT ip_address) >= $1
    
            """
    
            
    
            results = await conn.fetch(query, self.thresholds['multiple_ip_access'])
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='suspicious_access',
    
                    severity='MEDIUM',
    
                    message=f"User {record['user_name']} accessed from multiple IP addresses",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'user_id': record['user_id'],
    
                        'ip_count': record['ip_count'],
    
                        'ip_addresses': record['ip_addresses']
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _check_unauthorized_access(self, conn):
    
            """Check for unauthorized store access attempts."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                user_id,
    
                store_id,
    
                failure_reason,
    
                created_at
    
            FROM retail.security_audit_log
    
            WHERE success = FALSE 
    
              AND event_type = 'unauthorized_store_access'
    
              AND created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
            """
    
            
    
            results = await conn.fetch(query)
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='unauthorized_access',
    
                    severity='HIGH',
    
                    message=f"Unauthorized store access attempt by {record['user_name']}",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'user_id': record['user_id'],
    
                        'store_id': record['store_id'],
    
                        'failure_reason': record['failure_reason'],
    
                        'timestamp': record['created_at'].isoformat()
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _send_alert(self, alert: SecurityAlert):
    
            """Send security alert to all configured handlers."""
    
            
    
            logger.warning(
    
                f"Security Alert: {alert.alert_type} - {alert.message}",
    
                extra={'alert_details': alert.details}
    
            )
    
            
    
            # Send to configured alert handlers
    
            for handler in self.alert_handlers:
    
                try:
    
                    await handler.send_alert(alert)
    
                except Exception as e:
    
                    logger.error(f"Failed to send alert via {handler.__class__.__name__}: {e}")
    
        
    
        def add_alert_handler(self, handler):
    
            """Add alert handler."""
    
            self.alert_handlers.append(handler)
    
    

    ๐Ÿงช Security Testing and Validation

    Automated Security Tests

    
    # tests/security/test_security.py
    
    """
    
    Comprehensive security tests for MCP server.
    
    """
    
    import pytest
    
    import asyncio
    
    import asyncpg
    
    from datetime import datetime, timezone
    
    import jwt
    
    from unittest.mock import Mock, patch
    
    
    
    class TestRowLevelSecurity:
    
        """Test Row Level Security implementation."""
    
        
    
        @pytest.fixture
    
        async def db_connection(self):
    
            """Database connection for testing."""
    
            conn = await asyncpg.connect(
    
                "postgresql://mcp_user:password@localhost:5432/retail_test"
    
            )
    
            yield conn
    
            await conn.close()
    
        
    
        async def test_store_context_isolation(self, db_connection):
    
            """Test that RLS properly isolates data by store."""
    
            
    
            # Set Seattle store context
    
            await db_connection.execute("SELECT retail.set_store_context('seattle')")
    
            
    
            # Get customer count
    
            seattle_customers = await db_connection.fetchval(
    
                "SELECT COUNT(*) FROM retail.customers"
    
            )
    
            
    
            # Set Redmond store context
    
            await db_connection.execute("SELECT retail.set_store_context('redmond')")
    
            
    
            # Get customer count
    
            redmond_customers = await db_connection.fetchval(
    
                "SELECT COUNT(*) FROM retail.customers"
    
            )
    
            
    
            # Verify isolation (counts should be different)
    
            assert seattle_customers != redmond_customers or (
    
                seattle_customers == 0 and redmond_customers == 0
    
            )
    
        
    
        async def test_unauthorized_store_access(self, db_connection):
    
            """Test that invalid store access is blocked."""
    
            
    
            with pytest.raises(Exception) as exc_info:
    
                await db_connection.execute("SELECT retail.set_store_context('invalid_store')")
    
            
    
            assert "Store not found" in str(exc_info.value)
    
        
    
        async def test_cross_store_data_leakage(self, db_connection):
    
            """Test that users cannot access data from other stores."""
    
            
    
            # Set context to one store
    
            await db_connection.execute("SELECT retail.set_store_context('seattle')")
    
            
    
            # Try to insert data with different store_id
    
            with pytest.raises(Exception):
    
                await db_connection.execute("""
    
                    INSERT INTO retail.customers (store_id, first_name, last_name, email)
    
                    VALUES ('redmond', 'Test', 'User', 'test@example.com')
    
                """)
    
    
    
    class TestAuthentication:
    
        """Test authentication and authorization."""
    
        
    
        def test_valid_jwt_token(self):
    
            """Test valid JWT token validation."""
    
            
    
            # Mock valid token
    
            token_payload = {
    
                'oid': 'user-123',
    
                'email': 'test@example.com',
    
                'name': 'Test User',
    
                'tid': 'tenant-123',
    
                'aud': 'app-client-id',
    
                'iss': 'https://login.microsoftonline.com/tenant-123/v2.0',
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) + 3600,
    
                'iat': int((datetime.now(timezone.utc)).timestamp()),
    
                'roles': ['store_user']
    
            }
    
            
    
            # This would require mocking the JWKS endpoint
    
            # In real implementation, use proper test JWT tokens
    
            
    
        def test_expired_token_rejection(self):
    
            """Test that expired tokens are rejected."""
    
            
    
            token_payload = {
    
                'oid': 'user-123',
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) - 3600,  # Expired
    
                'iat': int((datetime.now(timezone.utc)).timestamp()) - 7200
    
            }
    
            
    
            # Test would verify that expired tokens are rejected
    
            
    
        def test_invalid_audience_rejection(self):
    
            """Test that tokens with wrong audience are rejected."""
    
            
    
            token_payload = {
    
                'oid': 'user-123',
    
                'aud': 'wrong-audience',  # Invalid audience
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) + 3600,
    
                'iat': int((datetime.now(timezone.utc)).timestamp())
    
            }
    
            
    
            # Test would verify that wrong audience tokens are rejected
    
    
    
    class TestAuthorization:
    
        """Test role-based authorization."""
    
        
    
        def test_role_hierarchy(self):
    
            """Test that role hierarchy works correctly."""
    
            
    
            from mcp_server.security.authorization import RoleBasedAuth
    
            
    
            # Store admin should have all permissions
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'read_all')
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'write_all')
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'delete_all')
    
            
    
            # Store user should have limited permissions
    
            assert RoleBasedAuth.has_permission(['store_user'], 'read_products')
    
            assert not RoleBasedAuth.has_permission(['store_user'], 'delete_all')
    
            
    
            # Store readonly should have minimal permissions
    
            assert RoleBasedAuth.has_permission(['store_readonly'], 'read_products')
    
            assert not RoleBasedAuth.has_permission(['store_readonly'], 'write_transactions')
    
        
    
        def test_permission_inheritance(self):
    
            """Test that permissions are properly inherited."""
    
            
    
            from mcp_server.security.authorization import RoleBasedAuth
    
            
    
            # Manager should inherit user permissions
    
            assert RoleBasedAuth.has_permission(['store_manager'], 'read_products')
    
            assert RoleBasedAuth.has_permission(['store_manager'], 'write_transactions')
    
    
    
    # Security test runner
    
    if __name__ == "__main__":
    
        pytest.main([__file__, "-v"])
    
    

    Penetration Testing Checklist

    
    # security-test-checklist.yml
    
    penetration_testing:
    
      
    
      authentication_bypass:
    
        - name: "Test authentication bypass attempts"
    
          tests:
    
            - "Missing Authorization header"
    
            - "Malformed JWT tokens"
    
            - "Replay attack with expired tokens"
    
            - "Token signature manipulation"
    
            - "Audience/issuer manipulation"
    
        
    
      authorization_escalation:
    
        - name: "Test privilege escalation attempts"
    
          tests:
    
            - "Role manipulation in token"
    
            - "Store access boundary testing"
    
            - "Cross-tenant data access attempts"
    
            - "Administrative function access"
    
        
    
      sql_injection:
    
        - name: "Test SQL injection vulnerabilities"
    
          tests:
    
            - "Parameter injection in search queries"
    
            - "Store ID manipulation"
    
            - "JSON parameter injection"
    
            - "Union-based injection attempts"
    
        
    
      data_exposure:
    
        - name: "Test for data exposure vulnerabilities"
    
          tests:
    
            - "Error message information disclosure"
    
            - "Timing attack possibilities"
    
            - "Cross-store data leakage"
    
            - "Audit log exposure"
    
        
    
      rate_limiting:
    
        - name: "Test rate limiting and DoS protection"
    
          tests:
    
            - "Authentication endpoint flooding"
    
            - "API endpoint rate limits"
    
            - "Resource exhaustion attempts"
    
            - "Connection pool exhaustion"
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Multi-Tenant Security: Implemented Row Level Security for complete data isolation

    โœ… Azure Authentication: Integrated Azure Entra ID with JWT validation

    โœ… Role-Based Authorization: Configured hierarchical role and permission system

    โœ… Comprehensive Audit Logging: Established security event tracking and monitoring

    โœ… Security Testing: Implemented automated security validation tests

    โœ… Threat Monitoring: Created real-time security event detection and alerting

    ๐Ÿš€ What's Next

    Continue with Lab 03: Environment Setup to:

  • Configure development environments with security best practices
  • Set up Azure services for authentication and monitoring
  • Implement secure database connections and secrets management
  • Validate security configurations in development environments
  • ๐Ÿ“š Additional Resources

    Azure Security

  • Azure Entra ID Documentation - Complete identity platform guide
  • Azure Key Vault - Secrets management service
  • Azure Security Best Practices - Security guidance
  • Database Security

  • PostgreSQL Row Level Security - Official RLS documentation
  • Database Security Checklist - PostgreSQL security guide
  • Multi-Tenant Database Patterns - Architecture patterns
  • Security Testing

  • OWASP Testing Guide - Comprehensive security testing
  • JWT Security Best Practices - JWT security considerations
  • API Security Testing - API-specific security testing
  • ---

    Previous: Lab 01: Core Architecture Concepts

    Next: Lab 03: Environment Setup

    Row Level Security, authentication, and multi-tenant data access Learn

    Security and Multi-Tenancy

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on implementing enterprise-grade security and multi-tenancy for MCP servers.

    You'll learn to design secure, compliant systems that protect sensitive retail data while enabling flexible access patterns across multiple tenants.

    Overview

    Security is paramount in retail applications that handle customer data, payment information, and business intelligence.

    This lab covers the complete security architecture from authentication and authorization to data isolation and compliance monitoring.

    We implement a defense-in-depth strategy combining Azure identity services, PostgreSQL Row Level Security, application-level controls, and comprehensive audit logging to create a robust, compliant platform.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Implement enterprise-grade Row Level Security for multi-tenant data isolation
  • Design secure authentication and authorization patterns with Azure
  • Configure comprehensive audit logging for compliance requirements
  • Apply defense-in-depth security strategies across all application layers
  • Validate security implementations through systematic testing
  • Monitor security events and respond to potential threats
  • ๐Ÿ” Multi-Tenant Security Architecture

    Security Layers Overview

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚               Azure Front Door                  โ”‚ โ† WAF, DDoS Protection
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚              Application Gateway                โ”‚ โ† SSL Termination, Rate Limiting
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚                MCP Server                       โ”‚ โ† Authentication, Authorization
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚           Connection Layer                  โ”‚ โ† Connection Pooling, Circuit Breakers
    
    โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚         Business Logic Layer               โ”‚ โ† Input Validation, Business Rules
    
    โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚           Data Access Layer                โ”‚ โ† Query Sanitization, RLS Context
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚              PostgreSQL RLS                    โ”‚ โ† Row Level Security, Audit Triggers
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Multi-Tenancy Models

    Our implementation uses the Shared Database, Shared Schema model with Row Level Security:

    Benefits:

  • Cost-effective resource utilization
  • Simplified maintenance and updates
  • Strong data isolation through RLS
  • Compliance-friendly audit trails
  • Trade-offs:

  • Requires careful RLS policy design
  • Schema changes affect all tenants
  • Need robust backup/restore procedures
  • ๐Ÿ›ก๏ธ Row Level Security Implementation

    RLS Foundation

    
    -- Enable RLS on all multi-tenant tables
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.products ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.sales_transactions ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.sales_transaction_items ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.product_embeddings ENABLE ROW LEVEL SECURITY;
    
    
    
    -- Create application role for MCP server
    
    CREATE ROLE mcp_user LOGIN;
    
    GRANT USAGE ON SCHEMA retail TO mcp_user;
    
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA retail TO mcp_user;
    
    

    Store Context Management

    
    -- Function to securely set store context
    
    CREATE OR REPLACE FUNCTION retail.set_store_context(store_id_param VARCHAR(50))
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    SET search_path = retail, pg_temp
    
    AS $$
    
    DECLARE
    
        user_info RECORD;
    
    BEGIN
    
        -- Validate store exists and is active
    
        SELECT store_id, store_name, is_active 
    
        INTO user_info
    
        FROM retail.stores 
    
        WHERE store_id = store_id_param;
    
        
    
        IF NOT FOUND THEN
    
            RAISE EXCEPTION 'Store not found: %', store_id_param
    
                USING ERRCODE = 'invalid_parameter_value',
    
                      HINT = 'Verify store ID and ensure it exists in the system';
    
        END IF;
    
        
    
        IF NOT user_info.is_active THEN
    
            RAISE EXCEPTION 'Store is inactive: %', store_id_param
    
                USING ERRCODE = 'insufficient_privilege',
    
                      HINT = 'Contact administrator to activate store';
    
        END IF;
    
        
    
        -- Set the secure context
    
        PERFORM set_config('app.current_store_id', store_id_param, false);
    
        PERFORM set_config('app.store_name', user_info.store_name, false);
    
        PERFORM set_config('app.context_set_at', extract(epoch from current_timestamp)::text, false);
    
        
    
        -- Log context change for audit
    
        INSERT INTO retail.security_audit_log (
    
            event_type,
    
            user_name,
    
            store_id,
    
            ip_address,
    
            user_agent,
    
            details,
    
            severity
    
        ) VALUES (
    
            'store_context_set',
    
            current_user,
    
            store_id_param,
    
            inet_client_addr()::text,
    
            current_setting('application_name', true),
    
            jsonb_build_object(
    
                'store_name', user_info.store_name,
    
                'timestamp', current_timestamp,
    
                'session_id', pg_backend_pid()
    
            ),
    
            'INFO'
    
        );
    
    END;
    
    $$;
    
    
    
    -- Grant execute to MCP user
    
    GRANT EXECUTE ON FUNCTION retail.set_store_context TO mcp_user;
    
    

    RLS Policies

    
    -- Customers RLS Policy
    
    CREATE POLICY customers_store_isolation ON retail.customers
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Products RLS Policy with additional business rules
    
    CREATE POLICY products_store_isolation ON retail.products
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
            AND is_active = TRUE  -- Additional business rule
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Sales Transactions RLS Policy
    
    CREATE POLICY sales_transactions_store_isolation ON retail.sales_transactions
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Transaction Items RLS Policy (via join)
    
    CREATE POLICY sales_transaction_items_store_isolation ON retail.sales_transaction_items
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        )
    
        WITH CHECK (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        );
    
    
    
    -- Product Embeddings RLS Policy
    
    CREATE POLICY product_embeddings_store_isolation ON retail.product_embeddings
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    

    RLS Testing and Validation

    
    -- Test RLS policies with different store contexts
    
    DO $$
    
    DECLARE
    
        test_result RECORD;
    
        customer_count INTEGER;
    
        product_count INTEGER;
    
    BEGIN
    
        -- Test Seattle store context
    
        PERFORM retail.set_store_context('seattle');
    
        
    
        SELECT COUNT(*) INTO customer_count FROM retail.customers;
    
        SELECT COUNT(*) INTO product_count FROM retail.products;
    
        
    
        RAISE NOTICE 'Seattle store - Customers: %, Products: %', customer_count, product_count;
    
        
    
        -- Test Redmond store context
    
        PERFORM retail.set_store_context('redmond');
    
        
    
        SELECT COUNT(*) INTO customer_count FROM retail.customers;
    
        SELECT COUNT(*) INTO product_count FROM retail.products;
    
        
    
        RAISE NOTICE 'Redmond store - Customers: %, Products: %', customer_count, product_count;
    
        
    
        -- Verify data isolation
    
        IF customer_count > 0 AND product_count > 0 THEN
    
            RAISE NOTICE 'RLS policies are working correctly';
    
        ELSE
    
            RAISE WARNING 'RLS policies may not be configured correctly';
    
        END IF;
    
    END;
    
    $$;
    
    

    ๐Ÿ”‘ Authentication and Authorization

    Azure Entra ID Integration

    
    # mcp_server/security/authentication.py
    
    """
    
    Azure Entra ID authentication for MCP server.
    
    """
    
    import os
    
    import jwt
    
    import aiohttp
    
    import asyncio
    
    from typing import Dict, Optional, List
    
    from datetime import datetime, timezone
    
    from azure.identity.aio import DefaultAzureCredential
    
    from azure.keyvault.secrets.aio import SecretClient
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class AzureAuthenticator:
    
        """Handle Azure Entra ID authentication and token validation."""
    
        
    
        def __init__(self):
    
            self.tenant_id = os.getenv('AZURE_TENANT_ID')
    
            self.client_id = os.getenv('AZURE_CLIENT_ID')
    
            self.audience = os.getenv('AZURE_AUDIENCE', self.client_id)
    
            self.issuer = f"https://login.microsoftonline.com/{self.tenant_id}/v2.0"
    
            
    
            # Cache for JWKS (JSON Web Key Set)
    
            self._jwks_cache = None
    
            self._jwks_cache_expiry = None
    
            
    
            # Key Vault for secrets
    
            self.key_vault_url = os.getenv('AZURE_KEY_VAULT_URL')
    
            self.credential = DefaultAzureCredential()
    
            
    
            if self.key_vault_url:
    
                self.secret_client = SecretClient(
    
                    vault_url=self.key_vault_url,
    
                    credential=self.credential
    
                )
    
        
    
        async def validate_token(self, token: str) -> Dict:
    
            """Validate JWT token from Azure Entra ID."""
    
            
    
            try:
    
                # Get signing keys
    
                signing_keys = await self._get_signing_keys()
    
                
    
                # Decode token header to get key ID
    
                unverified_header = jwt.get_unverified_header(token)
    
                key_id = unverified_header.get('kid')
    
                
    
                if not key_id:
    
                    raise ValueError("Token missing key ID")
    
                
    
                # Find the corresponding key
    
                signing_key = None
    
                for key in signing_keys:
    
                    if key['kid'] == key_id:
    
                        signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
    
                        break
    
                
    
                if not signing_key:
    
                    raise ValueError(f"Unable to find signing key for kid: {key_id}")
    
                
    
                # Validate and decode token
    
                payload = jwt.decode(
    
                    token,
    
                    signing_key,
    
                    algorithms=['RS256'],
    
                    audience=self.audience,
    
                    issuer=self.issuer,
    
                    options={
    
                        'verify_exp': True,
    
                        'verify_aud': True,
    
                        'verify_iss': True
    
                    }
    
                )
    
                
    
                # Extract user information
    
                user_info = self._extract_user_info(payload)
    
                
    
                # Log successful authentication
    
                logger.info(
    
                    "User authenticated successfully",
    
                    extra={
    
                        'user_id': user_info['user_id'],
    
                        'email': user_info.get('email'),
    
                        'tenant_id': payload.get('tid')
    
                    }
    
                )
    
                
    
                return user_info
    
                
    
            except jwt.ExpiredSignatureError:
    
                logger.warning("Token has expired")
    
                raise ValueError("Token has expired")
    
            except jwt.InvalidAudienceError:
    
                logger.warning(f"Invalid audience in token. Expected: {self.audience}")
    
                raise ValueError("Invalid token audience")
    
            except jwt.InvalidIssuerError:
    
                logger.warning(f"Invalid issuer in token. Expected: {self.issuer}")
    
                raise ValueError("Invalid token issuer")
    
            except Exception as e:
    
                logger.error(f"Token validation failed: {str(e)}")
    
                raise ValueError(f"Token validation failed: {str(e)}")
    
        
    
        async def _get_signing_keys(self) -> List[Dict]:
    
            """Get JWKS from Azure Entra ID with caching."""
    
            
    
            current_time = datetime.now(timezone.utc)
    
            
    
            # Check if cache is valid
    
            if (self._jwks_cache and self._jwks_cache_expiry and 
    
                current_time < self._jwks_cache_expiry):
    
                return self._jwks_cache
    
            
    
            # Fetch new JWKS
    
            jwks_url = f"{self.issuer}/keys"
    
            
    
            async with aiohttp.ClientSession() as session:
    
                async with session.get(jwks_url) as response:
    
                    if response.status != 200:
    
                        raise Exception(f"Failed to fetch JWKS: {response.status}")
    
                    
    
                    jwks_data = await response.json()
    
                    
    
            # Cache for 1 hour
    
            self._jwks_cache = jwks_data['keys']
    
            self._jwks_cache_expiry = current_time.replace(
    
                hour=current_time.hour + 1
    
            )
    
            
    
            return self._jwks_cache
    
        
    
        def _extract_user_info(self, payload: Dict) -> Dict:
    
            """Extract user information from JWT payload."""
    
            
    
            return {
    
                'user_id': payload.get('oid') or payload.get('sub'),
    
                'email': payload.get('email') or payload.get('preferred_username'),
    
                'name': payload.get('name'),
    
                'tenant_id': payload.get('tid'),
    
                'roles': payload.get('roles', []),
    
                'groups': payload.get('groups', []),
    
                'app_roles': payload.get('app_roles', []),
    
                'scope': payload.get('scp', '').split() if payload.get('scp') else [],
    
                'expires_at': datetime.fromtimestamp(payload['exp'], timezone.utc),
    
                'issued_at': datetime.fromtimestamp(payload['iat'], timezone.utc)
    
            }
    
        
    
        async def get_user_store_access(self, user_id: str) -> List[str]:
    
            """Get list of stores the user has access to."""
    
            
    
            try:
    
                # This would typically query your user/store mapping
    
                # For demo, we'll use a simple Key Vault secret
    
                secret_name = f"user-{user_id}-stores"
    
                
    
                if self.secret_client:
    
                    secret = await self.secret_client.get_secret(secret_name)
    
                    store_list = secret.value.split(',')
    
                    return [store.strip() for store in store_list if store.strip()]
    
                
    
                # Fallback: return default store access
    
                logger.warning(f"No store mapping found for user {user_id}, using default")
    
                return ['seattle']  # Default store access
    
                
    
            except Exception as e:
    
                logger.error(f"Failed to get store access for user {user_id}: {e}")
    
                return []  # No access if we can't determine stores
    
    
    
    # Global authenticator instance
    
    azure_authenticator = AzureAuthenticator()
    
    

    Authorization Middleware

    
    # mcp_server/security/authorization.py
    
    """
    
    Authorization middleware and decorators for MCP server.
    
    """
    
    import functools
    
    from typing import Dict, List, Optional, Callable, Any
    
    from fastapi import HTTPException, status, Request
    
    from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    security = HTTPBearer()
    
    
    
    class AuthorizationError(Exception):
    
        """Custom authorization error."""
    
        pass
    
    
    
    class RoleBasedAuth:
    
        """Role-based access control implementation."""
    
        
    
        # Define role hierarchy
    
        ROLE_HIERARCHY = {
    
            'store_admin': ['store_manager', 'store_user', 'store_readonly'],
    
            'store_manager': ['store_user', 'store_readonly'],
    
            'store_user': ['store_readonly'],
    
            'store_readonly': []
    
        }
    
        
    
        # Define permissions for each role
    
        ROLE_PERMISSIONS = {
    
            'store_admin': [
    
                'read_all', 'write_all', 'delete_all', 'manage_users'
    
            ],
    
            'store_manager': [
    
                'read_all', 'write_transactions', 'write_inventory', 'read_reports'
    
            ],
    
            'store_user': [
    
                'read_products', 'read_customers', 'write_transactions'
    
            ],
    
            'store_readonly': [
    
                'read_products', 'read_basic_reports'
    
            ]
    
        }
    
        
    
        @classmethod
    
        def has_permission(cls, user_roles: List[str], required_permission: str) -> bool:
    
            """Check if user has required permission."""
    
            
    
            user_permissions = set()
    
            
    
            for role in user_roles:
    
                # Add direct permissions
    
                user_permissions.update(cls.ROLE_PERMISSIONS.get(role, []))
    
                
    
                # Add inherited permissions
    
                inherited_roles = cls.ROLE_HIERARCHY.get(role, [])
    
                for inherited_role in inherited_roles:
    
                    user_permissions.update(cls.ROLE_PERMISSIONS.get(inherited_role, []))
    
            
    
            return required_permission in user_permissions
    
        
    
        @classmethod
    
        def get_user_stores(cls, user_info: Dict) -> List[str]:
    
            """Extract stores user has access to from user info."""
    
            
    
            # This would typically come from your user management system
    
            # For demo, we'll extract from custom claims or groups
    
            
    
            stores = []
    
            
    
            # Check for direct store assignments in groups
    
            for group in user_info.get('groups', []):
    
                if group.startswith('store_'):
    
                    store_id = group.replace('store_', '')
    
                    stores.append(store_id)
    
            
    
            # Check for app-specific roles
    
            for role in user_info.get('app_roles', []):
    
                if 'store:' in role:
    
                    _, store_id = role.split('store:', 1)
    
                    stores.append(store_id)
    
            
    
            return list(set(stores))  # Remove duplicates
    
    
    
    def require_auth(required_permission: str = None, require_store_access: bool = True):
    
        """Decorator to require authentication and authorization."""
    
        
    
        def decorator(func: Callable) -> Callable:
    
            @functools.wraps(func)
    
            async def wrapper(*args, **kwargs):
    
                # Extract request from args (FastAPI dependency injection)
    
                request = None
    
                for arg in args:
    
                    if isinstance(arg, Request):
    
                        request = arg
    
                        break
    
                
    
                if not request:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    
                        detail="Request object not found"
    
                    )
    
                
    
                # Get authorization header
    
                auth_header = request.headers.get('Authorization')
    
                if not auth_header or not auth_header.startswith('Bearer '):
    
                    raise HTTPException(
    
                        status_code=status.HTTP_401_UNAUTHORIZED,
    
                        detail="Missing or invalid authorization header",
    
                        headers={"WWW-Authenticate": "Bearer"}
    
                    )
    
                
    
                token = auth_header.split(' ')[1]
    
                
    
                try:
    
                    # Validate token
    
                    user_info = await azure_authenticator.validate_token(token)
    
                    
    
                    # Check required permission
    
                    if required_permission:
    
                        user_roles = user_info.get('roles', [])
    
                        if not RoleBasedAuth.has_permission(user_roles, required_permission):
    
                            raise HTTPException(
    
                                status_code=status.HTTP_403_FORBIDDEN,
    
                                detail=f"Insufficient permissions. Required: {required_permission}"
    
                            )
    
                    
    
                    # Check store access
    
                    if require_store_access:
    
                        user_stores = RoleBasedAuth.get_user_stores(user_info)
    
                        if not user_stores:
    
                            raise HTTPException(
    
                                status_code=status.HTTP_403_FORBIDDEN,
    
                                detail="No store access configured for user"
    
                            )
    
                        
    
                        # Set default store context (first accessible store)
    
                        request.state.current_store = user_stores[0]
    
                        request.state.accessible_stores = user_stores
    
                    
    
                    # Add user info to request state
    
                    request.state.user_info = user_info
    
                    request.state.user_id = user_info['user_id']
    
                    
    
                    # Call the original function
    
                    return await func(*args, **kwargs)
    
                    
    
                except ValueError as e:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_401_UNAUTHORIZED,
    
                        detail=str(e),
    
                        headers={"WWW-Authenticate": "Bearer"}
    
                    )
    
                except AuthorizationError as e:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_403_FORBIDDEN,
    
                        detail=str(e)
    
                    )
    
            
    
            return wrapper
    
        return decorator
    
    
    
    def require_store_context(store_param: str = 'store_id'):
    
        """Decorator to validate and set store context."""
    
        
    
        def decorator(func: Callable) -> Callable:
    
            @functools.wraps(func)
    
            async def wrapper(*args, **kwargs):
    
                # Get store_id from kwargs
    
                store_id = kwargs.get(store_param)
    
                
    
                if not store_id:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_400_BAD_REQUEST,
    
                        detail=f"Missing required parameter: {store_param}"
    
                    )
    
                
    
                # Extract request from args
    
                request = None
    
                for arg in args:
    
                    if isinstance(arg, Request):
    
                        request = arg
    
                        break
    
                
    
                if not request or not hasattr(request.state, 'accessible_stores'):
    
                    raise HTTPException(
    
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    
                        detail="Authentication required before store context validation"
    
                    )
    
                
    
                # Validate user has access to requested store
    
                if store_id not in request.state.accessible_stores:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_403_FORBIDDEN,
    
                        detail=f"Access denied to store: {store_id}"
    
                    )
    
                
    
                # Set store context in request state
    
                request.state.current_store = store_id
    
                
    
                return await func(*args, **kwargs)
    
            
    
            return wrapper
    
        return decorator
    
    

    ๐Ÿ” Security Audit and Compliance

    Comprehensive Audit Logging

    
    -- Security audit log table
    
    CREATE TABLE retail.security_audit_log (
    
        log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        event_type VARCHAR(100) NOT NULL,
    
        user_name VARCHAR(100) NOT NULL,
    
        user_id VARCHAR(100),
    
        store_id VARCHAR(50),
    
        ip_address INET,
    
        user_agent TEXT,
    
        request_id VARCHAR(100),
    
        session_id VARCHAR(100),
    
        resource_type VARCHAR(100),
    
        resource_id VARCHAR(100),
    
        action VARCHAR(50) NOT NULL,
    
        success BOOLEAN NOT NULL DEFAULT TRUE,
    
        failure_reason TEXT,
    
        details JSONB,
    
        severity VARCHAR(20) DEFAULT 'INFO',
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure proper indexing for security queries
    
        CONSTRAINT valid_severity CHECK (severity IN ('DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'))
    
    );
    
    
    
    -- Indexes for security audit queries
    
    CREATE INDEX idx_security_audit_event_type ON retail.security_audit_log(event_type);
    
    CREATE INDEX idx_security_audit_user_name ON retail.security_audit_log(user_name);
    
    CREATE INDEX idx_security_audit_store_id ON retail.security_audit_log(store_id);
    
    CREATE INDEX idx_security_audit_created_at ON retail.security_audit_log(created_at);
    
    CREATE INDEX idx_security_audit_success ON retail.security_audit_log(success);
    
    CREATE INDEX idx_security_audit_severity ON retail.security_audit_log(severity);
    
    CREATE INDEX idx_security_audit_details ON retail.security_audit_log USING GIN(details);
    
    
    
    -- Function to log security events
    
    CREATE OR REPLACE FUNCTION retail.log_security_event(
    
        p_event_type VARCHAR(100),
    
        p_user_name VARCHAR(100),
    
        p_user_id VARCHAR(100) DEFAULT NULL,
    
        p_store_id VARCHAR(50) DEFAULT NULL,
    
        p_ip_address TEXT DEFAULT NULL,
    
        p_action VARCHAR(50) DEFAULT 'unknown',
    
        p_success BOOLEAN DEFAULT TRUE,
    
        p_failure_reason TEXT DEFAULT NULL,
    
        p_details JSONB DEFAULT NULL,
    
        p_severity VARCHAR(20) DEFAULT 'INFO'
    
    )
    
    RETURNS UUID
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    DECLARE
    
        log_id UUID;
    
    BEGIN
    
        INSERT INTO retail.security_audit_log (
    
            event_type,
    
            user_name,
    
            user_id,
    
            store_id,
    
            ip_address,
    
            action,
    
            success,
    
            failure_reason,
    
            details,
    
            severity
    
        ) VALUES (
    
            p_event_type,
    
            p_user_name,
    
            p_user_id,
    
            p_store_id,
    
            p_ip_address::INET,
    
            p_action,
    
            p_success,
    
            p_failure_reason,
    
            p_details,
    
            p_severity
    
        ) RETURNING log_id INTO log_id;
    
        
    
        RETURN log_id;
    
    END;
    
    $$;
    
    
    
    -- Grant execute to MCP user
    
    GRANT EXECUTE ON FUNCTION retail.log_security_event TO mcp_user;
    
    

    Security Monitoring Views

    
    -- Failed authentication attempts
    
    CREATE VIEW retail.security_failed_auth AS
    
    SELECT 
    
        event_type,
    
        user_name,
    
        ip_address,
    
        COUNT(*) as attempt_count,
    
        MIN(created_at) as first_attempt,
    
        MAX(created_at) as last_attempt,
    
        ARRAY_AGG(DISTINCT failure_reason) as failure_reasons
    
    FROM retail.security_audit_log
    
    WHERE success = FALSE 
    
      AND event_type IN ('authentication_failed', 'token_validation_failed')
    
      AND created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
    
    GROUP BY event_type, user_name, ip_address
    
    HAVING COUNT(*) >= 3  -- 3 or more failures
    
    ORDER BY attempt_count DESC, last_attempt DESC;
    
    
    
    -- Suspicious access patterns
    
    CREATE VIEW retail.security_suspicious_access AS
    
    SELECT 
    
        user_name,
    
        user_id,
    
        COUNT(DISTINCT ip_address) as ip_count,
    
        COUNT(DISTINCT store_id) as store_count,
    
        ARRAY_AGG(DISTINCT ip_address::TEXT) as ip_addresses,
    
        ARRAY_AGG(DISTINCT store_id) as stores_accessed,
    
        MIN(created_at) as first_access,
    
        MAX(created_at) as last_access
    
    FROM retail.security_audit_log
    
    WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
      AND success = TRUE
    
    GROUP BY user_name, user_id
    
    HAVING COUNT(DISTINCT ip_address) > 3  -- Access from multiple IPs
    
       OR COUNT(DISTINCT store_id) > 2     -- Access to multiple stores
    
    ORDER BY ip_count DESC, store_count DESC;
    
    
    
    -- Data access patterns
    
    CREATE VIEW retail.security_data_access_summary AS
    
    SELECT 
    
        DATE_TRUNC('hour', created_at) as access_hour,
    
        store_id,
    
        resource_type,
    
        action,
    
        COUNT(*) as access_count,
    
        COUNT(DISTINCT user_id) as unique_users
    
    FROM retail.security_audit_log
    
    WHERE resource_type IS NOT NULL
    
      AND created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
    
    GROUP BY DATE_TRUNC('hour', created_at), store_id, resource_type, action
    
    ORDER BY access_hour DESC, access_count DESC;
    
    

    Security Event Monitoring

    
    # mcp_server/security/monitoring.py
    
    """
    
    Security monitoring and alerting for MCP server.
    
    """
    
    import asyncio
    
    import asyncpg
    
    from typing import Dict, List, Any
    
    from datetime import datetime, timedelta
    
    from dataclasses import dataclass
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    @dataclass
    
    class SecurityAlert:
    
        """Security alert data structure."""
    
        alert_type: str
    
        severity: str
    
        message: str
    
        details: Dict[str, Any]
    
        timestamp: datetime
    
    
    
    class SecurityMonitor:
    
        """Monitor security events and generate alerts."""
    
        
    
        def __init__(self, db_connection_string: str):
    
            self.db_connection_string = db_connection_string
    
            self.alert_handlers = []
    
            
    
            # Alert thresholds
    
            self.thresholds = {
    
                'failed_auth_attempts': 5,      # per user per hour
    
                'multiple_ip_access': 3,        # different IPs per user per hour
    
                'excessive_data_access': 1000,  # queries per user per hour
    
                'privilege_escalation': 1,      # any attempt
    
                'unauthorized_store_access': 1  # any attempt
    
            }
    
        
    
        async def start_monitoring(self):
    
            """Start security monitoring loop."""
    
            logger.info("Starting security monitoring")
    
            
    
            while True:
    
                try:
    
                    await self._check_security_events()
    
                    await asyncio.sleep(300)  # Check every 5 minutes
    
                except Exception as e:
    
                    logger.error(f"Security monitoring error: {e}")
    
                    await asyncio.sleep(60)  # Short retry on error
    
        
    
        async def _check_security_events(self):
    
            """Check for security events and generate alerts."""
    
            
    
            conn = await asyncpg.connect(self.db_connection_string)
    
            
    
            try:
    
                # Check failed authentication attempts
    
                await self._check_failed_auth(conn)
    
                
    
                # Check suspicious access patterns
    
                await self._check_suspicious_access(conn)
    
                
    
                # Check data access anomalies
    
                await self._check_data_access_anomalies(conn)
    
                
    
                # Check unauthorized access attempts
    
                await self._check_unauthorized_access(conn)
    
                
    
            finally:
    
                await conn.close()
    
        
    
        async def _check_failed_auth(self, conn):
    
            """Check for excessive failed authentication attempts."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                ip_address,
    
                COUNT(*) as attempt_count,
    
                MAX(created_at) as last_attempt
    
            FROM retail.security_audit_log
    
            WHERE success = FALSE 
    
              AND event_type IN ('authentication_failed', 'token_validation_failed')
    
              AND created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
            GROUP BY user_name, ip_address
    
            HAVING COUNT(*) >= $1
    
            """
    
            
    
            results = await conn.fetch(query, self.thresholds['failed_auth_attempts'])
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='failed_authentication',
    
                    severity='HIGH',
    
                    message=f"Excessive failed login attempts for user {record['user_name']}",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'ip_address': str(record['ip_address']),
    
                        'attempt_count': record['attempt_count'],
    
                        'last_attempt': record['last_attempt'].isoformat()
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _check_suspicious_access(self, conn):
    
            """Check for suspicious access patterns."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                user_id,
    
                COUNT(DISTINCT ip_address) as ip_count,
    
                ARRAY_AGG(DISTINCT ip_address::TEXT) as ip_addresses
    
            FROM retail.security_audit_log
    
            WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
              AND success = TRUE
    
            GROUP BY user_name, user_id
    
            HAVING COUNT(DISTINCT ip_address) >= $1
    
            """
    
            
    
            results = await conn.fetch(query, self.thresholds['multiple_ip_access'])
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='suspicious_access',
    
                    severity='MEDIUM',
    
                    message=f"User {record['user_name']} accessed from multiple IP addresses",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'user_id': record['user_id'],
    
                        'ip_count': record['ip_count'],
    
                        'ip_addresses': record['ip_addresses']
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _check_unauthorized_access(self, conn):
    
            """Check for unauthorized store access attempts."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                user_id,
    
                store_id,
    
                failure_reason,
    
                created_at
    
            FROM retail.security_audit_log
    
            WHERE success = FALSE 
    
              AND event_type = 'unauthorized_store_access'
    
              AND created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
            """
    
            
    
            results = await conn.fetch(query)
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='unauthorized_access',
    
                    severity='HIGH',
    
                    message=f"Unauthorized store access attempt by {record['user_name']}",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'user_id': record['user_id'],
    
                        'store_id': record['store_id'],
    
                        'failure_reason': record['failure_reason'],
    
                        'timestamp': record['created_at'].isoformat()
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _send_alert(self, alert: SecurityAlert):
    
            """Send security alert to all configured handlers."""
    
            
    
            logger.warning(
    
                f"Security Alert: {alert.alert_type} - {alert.message}",
    
                extra={'alert_details': alert.details}
    
            )
    
            
    
            # Send to configured alert handlers
    
            for handler in self.alert_handlers:
    
                try:
    
                    await handler.send_alert(alert)
    
                except Exception as e:
    
                    logger.error(f"Failed to send alert via {handler.__class__.__name__}: {e}")
    
        
    
        def add_alert_handler(self, handler):
    
            """Add alert handler."""
    
            self.alert_handlers.append(handler)
    
    

    ๐Ÿงช Security Testing and Validation

    Automated Security Tests

    
    # tests/security/test_security.py
    
    """
    
    Comprehensive security tests for MCP server.
    
    """
    
    import pytest
    
    import asyncio
    
    import asyncpg
    
    from datetime import datetime, timezone
    
    import jwt
    
    from unittest.mock import Mock, patch
    
    
    
    class TestRowLevelSecurity:
    
        """Test Row Level Security implementation."""
    
        
    
        @pytest.fixture
    
        async def db_connection(self):
    
            """Database connection for testing."""
    
            conn = await asyncpg.connect(
    
                "postgresql://mcp_user:password@localhost:5432/retail_test"
    
            )
    
            yield conn
    
            await conn.close()
    
        
    
        async def test_store_context_isolation(self, db_connection):
    
            """Test that RLS properly isolates data by store."""
    
            
    
            # Set Seattle store context
    
            await db_connection.execute("SELECT retail.set_store_context('seattle')")
    
            
    
            # Get customer count
    
            seattle_customers = await db_connection.fetchval(
    
                "SELECT COUNT(*) FROM retail.customers"
    
            )
    
            
    
            # Set Redmond store context
    
            await db_connection.execute("SELECT retail.set_store_context('redmond')")
    
            
    
            # Get customer count
    
            redmond_customers = await db_connection.fetchval(
    
                "SELECT COUNT(*) FROM retail.customers"
    
            )
    
            
    
            # Verify isolation (counts should be different)
    
            assert seattle_customers != redmond_customers or (
    
                seattle_customers == 0 and redmond_customers == 0
    
            )
    
        
    
        async def test_unauthorized_store_access(self, db_connection):
    
            """Test that invalid store access is blocked."""
    
            
    
            with pytest.raises(Exception) as exc_info:
    
                await db_connection.execute("SELECT retail.set_store_context('invalid_store')")
    
            
    
            assert "Store not found" in str(exc_info.value)
    
        
    
        async def test_cross_store_data_leakage(self, db_connection):
    
            """Test that users cannot access data from other stores."""
    
            
    
            # Set context to one store
    
            await db_connection.execute("SELECT retail.set_store_context('seattle')")
    
            
    
            # Try to insert data with different store_id
    
            with pytest.raises(Exception):
    
                await db_connection.execute("""
    
                    INSERT INTO retail.customers (store_id, first_name, last_name, email)
    
                    VALUES ('redmond', 'Test', 'User', 'test@example.com')
    
                """)
    
    
    
    class TestAuthentication:
    
        """Test authentication and authorization."""
    
        
    
        def test_valid_jwt_token(self):
    
            """Test valid JWT token validation."""
    
            
    
            # Mock valid token
    
            token_payload = {
    
                'oid': 'user-123',
    
                'email': 'test@example.com',
    
                'name': 'Test User',
    
                'tid': 'tenant-123',
    
                'aud': 'app-client-id',
    
                'iss': 'https://login.microsoftonline.com/tenant-123/v2.0',
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) + 3600,
    
                'iat': int((datetime.now(timezone.utc)).timestamp()),
    
                'roles': ['store_user']
    
            }
    
            
    
            # This would require mocking the JWKS endpoint
    
            # In real implementation, use proper test JWT tokens
    
            
    
        def test_expired_token_rejection(self):
    
            """Test that expired tokens are rejected."""
    
            
    
            token_payload = {
    
                'oid': 'user-123',
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) - 3600,  # Expired
    
                'iat': int((datetime.now(timezone.utc)).timestamp()) - 7200
    
            }
    
            
    
            # Test would verify that expired tokens are rejected
    
            
    
        def test_invalid_audience_rejection(self):
    
            """Test that tokens with wrong audience are rejected."""
    
            
    
            token_payload = {
    
                'oid': 'user-123',
    
                'aud': 'wrong-audience',  # Invalid audience
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) + 3600,
    
                'iat': int((datetime.now(timezone.utc)).timestamp())
    
            }
    
            
    
            # Test would verify that wrong audience tokens are rejected
    
    
    
    class TestAuthorization:
    
        """Test role-based authorization."""
    
        
    
        def test_role_hierarchy(self):
    
            """Test that role hierarchy works correctly."""
    
            
    
            from mcp_server.security.authorization import RoleBasedAuth
    
            
    
            # Store admin should have all permissions
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'read_all')
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'write_all')
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'delete_all')
    
            
    
            # Store user should have limited permissions
    
            assert RoleBasedAuth.has_permission(['store_user'], 'read_products')
    
            assert not RoleBasedAuth.has_permission(['store_user'], 'delete_all')
    
            
    
            # Store readonly should have minimal permissions
    
            assert RoleBasedAuth.has_permission(['store_readonly'], 'read_products')
    
            assert not RoleBasedAuth.has_permission(['store_readonly'], 'write_transactions')
    
        
    
        def test_permission_inheritance(self):
    
            """Test that permissions are properly inherited."""
    
            
    
            from mcp_server.security.authorization import RoleBasedAuth
    
            
    
            # Manager should inherit user permissions
    
            assert RoleBasedAuth.has_permission(['store_manager'], 'read_products')
    
            assert RoleBasedAuth.has_permission(['store_manager'], 'write_transactions')
    
    
    
    # Security test runner
    
    if __name__ == "__main__":
    
        pytest.main([__file__, "-v"])
    
    

    Penetration Testing Checklist

    
    # security-test-checklist.yml
    
    penetration_testing:
    
      
    
      authentication_bypass:
    
        - name: "Test authentication bypass attempts"
    
          tests:
    
            - "Missing Authorization header"
    
            - "Malformed JWT tokens"
    
            - "Replay attack with expired tokens"
    
            - "Token signature manipulation"
    
            - "Audience/issuer manipulation"
    
        
    
      authorization_escalation:
    
        - name: "Test privilege escalation attempts"
    
          tests:
    
            - "Role manipulation in token"
    
            - "Store access boundary testing"
    
            - "Cross-tenant data access attempts"
    
            - "Administrative function access"
    
        
    
      sql_injection:
    
        - name: "Test SQL injection vulnerabilities"
    
          tests:
    
            - "Parameter injection in search queries"
    
            - "Store ID manipulation"
    
            - "JSON parameter injection"
    
            - "Union-based injection attempts"
    
        
    
      data_exposure:
    
        - name: "Test for data exposure vulnerabilities"
    
          tests:
    
            - "Error message information disclosure"
    
            - "Timing attack possibilities"
    
            - "Cross-store data leakage"
    
            - "Audit log exposure"
    
        
    
      rate_limiting:
    
        - name: "Test rate limiting and DoS protection"
    
          tests:
    
            - "Authentication endpoint flooding"
    
            - "API endpoint rate limits"
    
            - "Resource exhaustion attempts"
    
            - "Connection pool exhaustion"
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Multi-Tenant Security: Implemented Row Level Security for complete data isolation

    โœ… Azure Authentication: Integrated Azure Entra ID with JWT validation

    โœ… Role-Based Authorization: Configured hierarchical role and permission system

    โœ… Comprehensive Audit Logging: Established security event tracking and monitoring

    โœ… Security Testing: Implemented automated security validation tests

    โœ… Threat Monitoring: Created real-time security event detection and alerting

    ๐Ÿš€ What's Next

    Continue with Lab 03: Environment Setup to:

  • Configure development environments with security best practices
  • Set up Azure services for authentication and monitoring
  • Implement secure database connections and secrets management
  • Validate security configurations in development environments
  • ๐Ÿ“š Additional Resources

    Azure Security

  • Azure Entra ID Documentation - Complete identity platform guide
  • Azure Key Vault - Secrets management service
  • Azure Security Best Practices - Security guidance
  • Database Security

  • PostgreSQL Row Level Security - Official RLS documentation
  • Database Security Checklist - PostgreSQL security guide
  • Multi-Tenant Database Patterns - Architecture patterns
  • Security Testing

  • OWASP Testing Guide - Comprehensive security testing
  • JWT Security Best Practices - JWT security considerations
  • API Security Testing - API-specific security testing
  • ---

    Previous: Lab 01: Core Architecture Concepts

    Next: Lab 03: Environment Setup

    03 Environment Setup

    Environment Setup

    ๐ŸŽฏ What This Lab Covers

    This hands-on lab guides you through setting up a complete development environment for building MCP servers with PostgreSQL integration.

    You'll configure all necessary tools, deploy Azure resources, and validate your setup before proceeding with implementation.

    Overview

    A proper development environment is crucial for successful MCP server development. This lab provides step-by-step instructions for setting up Docker, Azure services, development tools, and validating that everything works correctly together.

    By the end of this lab, you'll have a fully functional development environment ready for building the Zava Retail MCP server.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Install and configure all required development tools
  • Deploy Azure resources needed for the MCP server
  • Set up Docker containers for PostgreSQL and the MCP server
  • Validate your environment setup with test connections
  • Troubleshoot common setup issues and configuration problems
  • Understand the development workflow and file structure
  • ๐Ÿ“‹ Prerequisites Check

    Before starting, ensure you have:

    Required Knowledge

  • Basic command line usage (Windows Command Prompt/PowerShell)
  • Understanding of environment variables
  • Familiarity with Git version control
  • Basic Docker concepts (containers, images, volumes)
  • System Requirements

  • Operating System: Windows 10/11, macOS, or Linux
  • RAM: Minimum 8GB (16GB recommended)
  • Storage: At least 10GB free space
  • Network: Internet connection for downloads and Azure deployment
  • Account Requirements

  • Azure Subscription: Free tier is sufficient
  • GitHub Account: For repository access
  • Docker Hub Account: (Optional) For custom image publishing
  • ๐Ÿ› ๏ธ Tool Installation

    1. Install Docker Desktop

    Docker provides the containerized environment for our development setup.

    Windows Installation

    1. Download Docker Desktop:

    ```cmd

    # Visit https://desktop.docker.com/win/stable/Docker%20Desktop%20Installer.exe

    # Or use Windows Package Manager

    winget install Docker.DockerDesktop

    ```

    2. Install and Configure:

    - Run the installer as Administrator

    - Enable WSL 2 integration when prompted

    - Restart your computer when installation completes

    3. Verify Installation:

    ```cmd

    docker --version

    docker-compose --version

    ```

    macOS Installation

    1. Download and Install:

    ```bash

    # Download from https://desktop.docker.com/mac/stable/Docker.dmg

    # Or use Homebrew

    brew install --cask docker

    ```

    2. Start Docker Desktop:

    - Launch Docker Desktop from Applications

    - Complete the initial setup wizard

    3. Verify Installation:

    ```bash

    docker --version

    docker-compose --version

    ```

    Linux Installation

    1. Install Docker Engine:

    ```bash

    # Ubuntu/Debian

    curl -fsSL https://get.docker.com -o get-docker.sh

    sudo sh get-docker.sh

    sudo usermod -aG docker $USER

    # Log out and back in for group changes to take effect

    ```

    2. Install Docker Compose:

    ```bash

    sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

    sudo chmod +x /usr/local/bin/docker-compose

    ```

    2. Install Azure CLI

    The Azure CLI enables Azure resource deployment and management.

    Windows Installation
    
    # Using Windows Package Manager
    
    winget install Microsoft.AzureCLI
    
    
    
    # Or download MSI from: https://aka.ms/installazurecliwindows
    
    
    macOS Installation
    
    # Using Homebrew
    
    brew install azure-cli
    
    
    
    # Or using installer
    
    curl -L https://aka.ms/InstallAzureCli | bash
    
    
    Linux Installation
    
    # Ubuntu/Debian
    
    curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
    
    
    
    # RHEL/CentOS
    
    sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
    
    sudo dnf install azure-cli
    
    
    Verify and Authenticate
    
    # Check installation
    
    az version
    
    
    
    # Login to Azure
    
    az login
    
    
    
    # Set default subscription (if you have multiple)
    
    az account list --output table
    
    az account set --subscription "Your-Subscription-Name"
    
    

    3. Install Git

    Git is required for cloning the repository and version control.

    Windows
    
    # Using Windows Package Manager
    
    winget install Git.Git
    
    
    
    # Or download from: https://git-scm.com/download/win
    
    
    macOS
    
    # Git is usually pre-installed, but you can update via Homebrew
    
    brew install git
    
    
    Linux
    
    # Ubuntu/Debian
    
    sudo apt update && sudo apt install git
    
    
    
    # RHEL/CentOS
    
    sudo dnf install git
    
    

    4. Install VS Code

    Visual Studio Code provides the integrated development environment with MCP support.

    Installation
    
    # Windows
    
    winget install Microsoft.VisualStudioCode
    
    
    
    # macOS
    
    brew install --cask visual-studio-code
    
    
    
    # Linux (Ubuntu/Debian)
    
    sudo snap install code --classic
    
    
    Required Extensions

    Install these VS Code extensions:

    
    # Install via command line
    
    code --install-extension ms-python.python
    
    code --install-extension ms-vscode.vscode-json
    
    code --install-extension ms-azuretools.vscode-docker
    
    code --install-extension ms-vscode.azure-account
    
    

    Or install through VS Code:

    1. Open VS Code

    2. Go to Extensions (Ctrl+Shift+X)

    3. Install:

    - Python (Microsoft)

    - Docker (Microsoft)

    - Azure Account (Microsoft)

    - JSON (Microsoft)

    5. Install Python

    Python 3.8+ is required for MCP server development.

    Windows
    
    # Using Windows Package Manager
    
    winget install Python.Python.3.11
    
    
    
    # Or download from: https://www.python.org/downloads/
    
    
    macOS
    
    # Using Homebrew
    
    brew install python@3.11
    
    
    Linux
    
    # Ubuntu/Debian
    
    sudo apt update && sudo apt install python3.11 python3.11-pip python3.11-venv
    
    
    
    # RHEL/CentOS
    
    sudo dnf install python3.11 python3.11-pip
    
    
    Verify Installation
    
    python --version  # Should show Python 3.11.x
    
    pip --version      # Should show pip version
    
    

    ๐Ÿš€ Project Setup

    1. Clone the Repository

    
    # Clone the main repository
    
    git clone https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail.git
    
    
    
    # Navigate to the project directory
    
    cd MCP-Server-and-PostgreSQL-Sample-Retail
    
    
    
    # Verify repository structure
    
    ls -la
    
    

    2. Create Python Virtual Environment

    
    # Create virtual environment
    
    python -m venv mcp-env
    
    
    
    # Activate virtual environment
    
    # Windows
    
    mcp-env\Scripts\activate
    
    
    
    # macOS/Linux
    
    source mcp-env/bin/activate
    
    
    
    # Upgrade pip
    
    python -m pip install --upgrade pip
    
    

    3. Install Python Dependencies

    
    # Install development dependencies
    
    pip install -r requirements.lock.txt
    
    
    
    # Verify key packages
    
    pip list | grep fastmcp
    
    pip list | grep asyncpg
    
    pip list | grep azure
    
    

    โ˜๏ธ Azure Resource Deployment

    1. Understand Resource Requirements

    Our MCP server requires these Azure resources:

    Resource Purpose Estimated Cost -------------- ------------- ------------------- Azure AI Foundry AI model hosting and management $10-50/month OpenAI Deployment Text embedding model (text-embedding-3-small) $5-20/month Application Insights Monitoring and telemetry $5-15/month Resource Group Resource organization Free

    2. Deploy Azure Resources

    Option A: Automated Deployment (Recommended)
    
    # Navigate to infrastructure directory
    
    cd infra
    
    
    
    # Windows - PowerShell
    
    ./deploy.ps1
    
    
    
    # macOS/Linux - Bash
    
    ./deploy.sh
    
    

    The deployment script will:

    1. Create a unique resource group

    2. Deploy Azure AI Foundry resources

    3. Deploy the text-embedding-3-small model

    4. Configure Application Insights

    5. Create a service principal for authentication

    6. Generate .env file with configuration

    Option B: Manual Deployment

    If you prefer manual control or the automated script fails:

    
    # Set variables
    
    RESOURCE_GROUP="rg-zava-mcp-$(date +%s)"
    
    LOCATION="westus2"
    
    AI_PROJECT_NAME="zava-ai-project"
    
    
    
    # Create resource group
    
    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    
    
    # Deploy main template
    
    az deployment group create \
    
      --resource-group $RESOURCE_GROUP \
    
      --template-file main.bicep \
    
      --parameters location=$LOCATION \
    
      --parameters resourcePrefix="zava-mcp"
    
    

    3. Verify Azure Deployment

    
    # Check resource group
    
    az group show --name $RESOURCE_GROUP --output table
    
    
    
    # List deployed resources
    
    az resource list --resource-group $RESOURCE_GROUP --output table
    
    
    
    # Test AI service
    
    az cognitiveservices account show \
    
      --name "your-ai-service-name" \
    
      --resource-group $RESOURCE_GROUP
    
    

    4. Configure Environment Variables

    After deployment, you should have a .env file. Verify it contains:

    
    # .env file contents
    
    PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com/
    
    AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/
    
    EMBEDDING_MODEL_DEPLOYMENT_NAME=text-embedding-3-small
    
    AZURE_CLIENT_ID=your-client-id
    
    AZURE_CLIENT_SECRET=your-client-secret
    
    AZURE_TENANT_ID=your-tenant-id
    
    APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=your-key;...
    
    
    
    # Database configuration (for development)
    
    POSTGRES_HOST=localhost
    
    POSTGRES_PORT=5432
    
    POSTGRES_DB=zava
    
    POSTGRES_USER=postgres
    
    POSTGRES_PASSWORD=your-secure-password
    
    

    ๐Ÿณ Docker Environment Setup

    1. Understand Docker Composition

    Our development environment uses Docker Compose:

    
    # docker-compose.yml overview
    
    version: '3.8'
    
    services:
    
      postgres:
    
        image: pgvector/pgvector:pg17
    
        environment:
    
          POSTGRES_DB: zava
    
          POSTGRES_USER: postgres
    
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - ./data:/backup_data:ro
    
          - ./docker-init:/docker-entrypoint-initdb.d:ro
    
        
    
      mcp_server:
    
        build: .
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        ports:
    
          - "8000:8000"
    
        env_file:
    
          - .env
    
    

    2. Start the Development Environment

    
    # Ensure you're in the project root directory
    
    cd /path/to/MCP-Server-and-PostgreSQL-Sample-Retail
    
    
    
    # Start the services
    
    docker-compose up -d
    
    
    
    # Check service status
    
    docker-compose ps
    
    
    
    # View logs
    
    docker-compose logs -f
    
    

    3. Verify Database Setup

    
    # Connect to PostgreSQL container
    
    docker-compose exec postgres psql -U postgres -d zava
    
    
    
    # Check database structure
    
    \dt retail.*
    
    
    
    # Verify sample data
    
    SELECT COUNT(*) FROM retail.stores;
    
    SELECT COUNT(*) FROM retail.products;
    
    SELECT COUNT(*) FROM retail.orders;
    
    
    
    # Exit PostgreSQL
    
    \q
    
    

    4. Test MCP Server

    
    # Check MCP server health
    
    curl http://localhost:8000/health
    
    
    
    # Test basic MCP endpoint
    
    curl -X POST http://localhost:8000/mcp \
    
      -H "Content-Type: application/json" \
    
      -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \
    
      -d '{"method": "tools/list", "params": {}}'
    
    

    ๐Ÿ”ง VS Code Configuration

    1. Configure MCP Integration

    Create VS Code MCP configuration:

    
    // .vscode/mcp.json
    
    {
    
        "servers": {
    
            "zava-sales-analysis-headoffice": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "00000000-0000-0000-0000-000000000000"}
    
            },
    
            "zava-sales-analysis-seattle": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"}
    
            },
    
            "zava-sales-analysis-redmond": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "e7f8a9b0-c1d2-3e4f-5678-90abcdef1234"}
    
            }
    
        },
    
        "inputs": []
    
    }
    
    

    2. Configure Python Environment

    
    // .vscode/settings.json
    
    {
    
        "python.defaultInterpreterPath": "./mcp-env/bin/python",
    
        "python.linting.enabled": true,
    
        "python.linting.pylintEnabled": true,
    
        "python.formatting.provider": "black",
    
        "python.testing.pytestEnabled": true,
    
        "python.testing.pytestArgs": ["tests"],
    
        "files.exclude": {
    
            "**/__pycache__": true,
    
            "**/.pytest_cache": true,
    
            "**/mcp-env": true
    
        }
    
    }
    
    

    3. Test VS Code Integration

    1. Open the project in VS Code:

    ```bash

    code .

    ```

    2. Open AI Chat:

    - Press Ctrl+Shift+P (Windows/Linux) or Cmd+Shift+P (macOS)

    - Type "AI Chat" and select "AI Chat: Open Chat"

    3. Test MCP Server Connection:

    - In AI Chat, type #zava and select one of the configured servers

    - Ask: "What tables are available in the database?"

    - You should receive a response listing the retail database tables

    โœ… Environment Validation

    1. Comprehensive System Check

    Run this validation script to verify your setup:

    
    # Create validation script
    
    cat > validate_setup.py << 'EOF'
    
    #!/usr/bin/env python3
    
    """
    
    Environment validation script for MCP Server setup.
    
    """
    
    import asyncio
    
    import os
    
    import sys
    
    import subprocess
    
    import requests
    
    import asyncpg
    
    from azure.identity import DefaultAzureCredential
    
    from azure.ai.projects import AIProjectClient
    
    
    
    async def validate_environment():
    
        """Comprehensive environment validation."""
    
        results = {}
    
        
    
        # Check Python version
    
        python_version = sys.version_info
    
        results['python'] = {
    
            'status': 'pass' if python_version >= (3, 8) else 'fail',
    
            'version': f"{python_version.major}.{python_version.minor}.{python_version.micro}",
    
            'required': '3.8+'
    
        }
    
        
    
        # Check required packages
    
        required_packages = ['fastmcp', 'asyncpg', 'azure-ai-projects']
    
        for package in required_packages:
    
            try:
    
                __import__(package)
    
                results[f'package_{package}'] = {'status': 'pass'}
    
            except ImportError:
    
                results[f'package_{package}'] = {'status': 'fail', 'error': 'Not installed'}
    
        
    
        # Check Docker
    
        try:
    
            result = subprocess.run(['docker', '--version'], capture_output=True, text=True)
    
            results['docker'] = {
    
                'status': 'pass' if result.returncode == 0 else 'fail',
    
                'version': result.stdout.strip() if result.returncode == 0 else 'Not available'
    
            }
    
        except FileNotFoundError:
    
            results['docker'] = {'status': 'fail', 'error': 'Docker not found'}
    
        
    
        # Check Azure CLI
    
        try:
    
            result = subprocess.run(['az', '--version'], capture_output=True, text=True)
    
            results['azure_cli'] = {
    
                'status': 'pass' if result.returncode == 0 else 'fail',
    
                'version': result.stdout.split('\n')[0] if result.returncode == 0 else 'Not available'
    
            }
    
        except FileNotFoundError:
    
            results['azure_cli'] = {'status': 'fail', 'error': 'Azure CLI not found'}
    
        
    
        # Check environment variables
    
        required_env_vars = [
    
            'PROJECT_ENDPOINT',
    
            'AZURE_OPENAI_ENDPOINT',
    
            'EMBEDDING_MODEL_DEPLOYMENT_NAME',
    
            'AZURE_CLIENT_ID',
    
            'AZURE_CLIENT_SECRET',
    
            'AZURE_TENANT_ID'
    
        ]
    
        
    
        for var in required_env_vars:
    
            value = os.getenv(var)
    
            results[f'env_{var}'] = {
    
                'status': 'pass' if value else 'fail',
    
                'value': '***' if value and 'SECRET' in var else value
    
            }
    
        
    
        # Check database connection
    
        try:
    
            conn = await asyncpg.connect(
    
                host=os.getenv('POSTGRES_HOST', 'localhost'),
    
                port=int(os.getenv('POSTGRES_PORT', 5432)),
    
                database=os.getenv('POSTGRES_DB', 'zava'),
    
                user=os.getenv('POSTGRES_USER', 'postgres'),
    
                password=os.getenv('POSTGRES_PASSWORD', 'secure_password')
    
            )
    
            
    
            # Test query
    
            result = await conn.fetchval('SELECT COUNT(*) FROM retail.stores')
    
            await conn.close()
    
            
    
            results['database'] = {
    
                'status': 'pass',
    
                'store_count': result
    
            }
    
        except Exception as e:
    
            results['database'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        # Check MCP server
    
        try:
    
            response = requests.get('http://localhost:8000/health', timeout=5)
    
            results['mcp_server'] = {
    
                'status': 'pass' if response.status_code == 200 else 'fail',
    
                'response': response.json() if response.status_code == 200 else response.text
    
            }
    
        except Exception as e:
    
            results['mcp_server'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        # Check Azure AI service
    
        try:
    
            credential = DefaultAzureCredential()
    
            project_client = AIProjectClient(
    
                endpoint=os.getenv('PROJECT_ENDPOINT'),
    
                credential=credential
    
            )
    
            
    
            # This will fail if credentials are invalid
    
            results['azure_ai'] = {'status': 'pass'}
    
            
    
        except Exception as e:
    
            results['azure_ai'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        return results
    
    
    
    def print_results(results):
    
        """Print formatted validation results."""
    
        print("๐Ÿ” Environment Validation Results\n")
    
        print("=" * 50)
    
        
    
        passed = 0
    
        failed = 0
    
        
    
        for component, result in results.items():
    
            status = result.get('status', 'unknown')
    
            if status == 'pass':
    
                print(f"โœ… {component}: PASS")
    
                passed += 1
    
            else:
    
                print(f"โŒ {component}: FAIL")
    
                if 'error' in result:
    
                    print(f"   Error: {result['error']}")
    
                failed += 1
    
        
    
        print("\n" + "=" * 50)
    
        print(f"Summary: {passed} passed, {failed} failed")
    
        
    
        if failed > 0:
    
            print("\nโ— Please fix the failed components before proceeding.")
    
            return False
    
        else:
    
            print("\n๐ŸŽ‰ All validations passed! Your environment is ready.")
    
            return True
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
    
    async def main():
    
        results = await validate_environment()
    
        success = print_results(results)
    
        sys.exit(0 if success else 1)
    
    
    
    EOF
    
    
    
    # Run validation
    
    python validate_setup.py
    
    

    2. Manual Validation Checklist

    โœ… Basic Tools

  • [ ] Docker version 20.10+ installed and running
  • [ ] Azure CLI 2.40+ installed and authenticated
  • [ ] Python 3.8+ with pip installed
  • [ ] Git 2.30+ installed
  • [ ] VS Code with required extensions
  • โœ… Azure Resources

  • [ ] Resource group created successfully
  • [ ] AI Foundry project deployed
  • [ ] OpenAI text-embedding-3-small model deployed
  • [ ] Application Insights configured
  • [ ] Service principal created with proper permissions
  • โœ… Environment Configuration

  • [ ] .env file created with all required variables
  • [ ] Azure credentials working (test with az account show)
  • [ ] PostgreSQL container running and accessible
  • [ ] Sample data loaded in database
  • โœ… VS Code Integration

  • [ ] .vscode/mcp.json configured
  • [ ] Python interpreter set to virtual environment
  • [ ] MCP servers appear in AI Chat
  • [ ] Can execute test queries through AI Chat
  • ๐Ÿ› ๏ธ Troubleshooting Common Issues

    Docker Issues

    Problem: Docker containers won't start

    
    # Check Docker service status
    
    docker info
    
    
    
    # Check available resources
    
    docker system df
    
    
    
    # Clean up if needed
    
    docker system prune -f
    
    
    
    # Restart Docker Desktop (Windows/macOS)
    
    # Or restart Docker service (Linux)
    
    sudo systemctl restart docker
    
    

    Problem: PostgreSQL connection fails

    
    # Check container logs
    
    docker-compose logs postgres
    
    
    
    # Verify container is healthy
    
    docker-compose ps
    
    
    
    # Test direct connection
    
    docker-compose exec postgres psql -U postgres -d zava -c "SELECT 1;"
    
    

    Azure Deployment Issues

    Problem: Azure deployment fails

    
    # Check Azure CLI authentication
    
    az account show
    
    
    
    # Verify subscription permissions
    
    az role assignment list --assignee $(az account show --query user.name -o tsv)
    
    
    
    # Check resource provider registration
    
    az provider register --namespace Microsoft.CognitiveServices
    
    az provider register --namespace Microsoft.Insights
    
    

    Problem: AI service authentication fails

    
    # Test service principal
    
    az login --service-principal \
    
      --username $AZURE_CLIENT_ID \
    
      --password $AZURE_CLIENT_SECRET \
    
      --tenant $AZURE_TENANT_ID
    
    
    
    # Verify AI service deployment
    
    az cognitiveservices account list --query "[].{Name:name,Kind:kind,Location:location}"
    
    

    Python Environment Issues

    Problem: Package installation fails

    
    # Upgrade pip and setuptools
    
    python -m pip install --upgrade pip setuptools wheel
    
    
    
    # Clear pip cache
    
    pip cache purge
    
    
    
    # Install packages one by one to identify issues
    
    pip install fastmcp
    
    pip install asyncpg
    
    pip install azure-ai-projects
    
    

    Problem: VS Code can't find Python interpreter

    
    # Show Python interpreter paths
    
    which python  # macOS/Linux
    
    where python  # Windows
    
    
    
    # Activate virtual environment first
    
    source mcp-env/bin/activate  # macOS/Linux
    
    mcp-env\Scripts\activate     # Windows
    
    
    
    # Then open VS Code
    
    code .
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Complete Development Environment: All tools installed and configured

    โœ… Azure Resources Deployed: AI services and supporting infrastructure

    โœ… Docker Environment Running: PostgreSQL and MCP server containers

    โœ… VS Code Integration: MCP servers configured and accessible

    โœ… Validated Setup: All components tested and working together

    โœ… Troubleshooting Knowledge: Common issues and solutions

    ๐Ÿš€ What's Next

    With your environment ready, continue to Lab 04: Database Design and Schema to:

  • Explore the retail database schema in detail
  • Understand multi-tenant data modeling
  • Learn about Row Level Security implementation
  • Work with sample retail data
  • ๐Ÿ“š Additional Resources

    Development Tools

  • Docker Documentation - Complete Docker reference
  • Azure CLI Reference - Azure CLI commands
  • VS Code Documentation - Editor configuration and extensions
  • Azure Services

  • Azure AI Foundry Documentation - AI service configuration
  • Azure OpenAI Service - AI model deployment
  • Application Insights - Monitoring setup
  • Python Development

  • Python Virtual Environments - Environment management
  • AsyncIO Documentation - Async programming patterns
  • FastAPI Documentation - Web framework patterns
  • ---

    Next: Environment ready? Continue with Lab 04: Database Design and Schema

    Setting up development environment, Docker, Azure resources Setup

    Environment Setup

    ๐ŸŽฏ What This Lab Covers

    This hands-on lab guides you through setting up a complete development environment for building MCP servers with PostgreSQL integration.

    You'll configure all necessary tools, deploy Azure resources, and validate your setup before proceeding with implementation.

    Overview

    A proper development environment is crucial for successful MCP server development. This lab provides step-by-step instructions for setting up Docker, Azure services, development tools, and validating that everything works correctly together.

    By the end of this lab, you'll have a fully functional development environment ready for building the Zava Retail MCP server.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Install and configure all required development tools
  • Deploy Azure resources needed for the MCP server
  • Set up Docker containers for PostgreSQL and the MCP server
  • Validate your environment setup with test connections
  • Troubleshoot common setup issues and configuration problems
  • Understand the development workflow and file structure
  • ๐Ÿ“‹ Prerequisites Check

    Before starting, ensure you have:

    Required Knowledge

  • Basic command line usage (Windows Command Prompt/PowerShell)
  • Understanding of environment variables
  • Familiarity with Git version control
  • Basic Docker concepts (containers, images, volumes)
  • System Requirements

  • Operating System: Windows 10/11, macOS, or Linux
  • RAM: Minimum 8GB (16GB recommended)
  • Storage: At least 10GB free space
  • Network: Internet connection for downloads and Azure deployment
  • Account Requirements

  • Azure Subscription: Free tier is sufficient
  • GitHub Account: For repository access
  • Docker Hub Account: (Optional) For custom image publishing
  • ๐Ÿ› ๏ธ Tool Installation

    1. Install Docker Desktop

    Docker provides the containerized environment for our development setup.

    Windows Installation

    1. Download Docker Desktop:

    ```cmd

    # Visit https://desktop.docker.com/win/stable/Docker%20Desktop%20Installer.exe

    # Or use Windows Package Manager

    winget install Docker.DockerDesktop

    ```

    2. Install and Configure:

    - Run the installer as Administrator

    - Enable WSL 2 integration when prompted

    - Restart your computer when installation completes

    3. Verify Installation:

    ```cmd

    docker --version

    docker-compose --version

    ```

    macOS Installation

    1. Download and Install:

    ```bash

    # Download from https://desktop.docker.com/mac/stable/Docker.dmg

    # Or use Homebrew

    brew install --cask docker

    ```

    2. Start Docker Desktop:

    - Launch Docker Desktop from Applications

    - Complete the initial setup wizard

    3. Verify Installation:

    ```bash

    docker --version

    docker-compose --version

    ```

    Linux Installation

    1. Install Docker Engine:

    ```bash

    # Ubuntu/Debian

    curl -fsSL https://get.docker.com -o get-docker.sh

    sudo sh get-docker.sh

    sudo usermod -aG docker $USER

    # Log out and back in for group changes to take effect

    ```

    2. Install Docker Compose:

    ```bash

    sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

    sudo chmod +x /usr/local/bin/docker-compose

    ```

    2. Install Azure CLI

    The Azure CLI enables Azure resource deployment and management.

    Windows Installation
    
    # Using Windows Package Manager
    
    winget install Microsoft.AzureCLI
    
    
    
    # Or download MSI from: https://aka.ms/installazurecliwindows
    
    
    macOS Installation
    
    # Using Homebrew
    
    brew install azure-cli
    
    
    
    # Or using installer
    
    curl -L https://aka.ms/InstallAzureCli | bash
    
    
    Linux Installation
    
    # Ubuntu/Debian
    
    curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
    
    
    
    # RHEL/CentOS
    
    sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
    
    sudo dnf install azure-cli
    
    
    Verify and Authenticate
    
    # Check installation
    
    az version
    
    
    
    # Login to Azure
    
    az login
    
    
    
    # Set default subscription (if you have multiple)
    
    az account list --output table
    
    az account set --subscription "Your-Subscription-Name"
    
    

    3. Install Git

    Git is required for cloning the repository and version control.

    Windows
    
    # Using Windows Package Manager
    
    winget install Git.Git
    
    
    
    # Or download from: https://git-scm.com/download/win
    
    
    macOS
    
    # Git is usually pre-installed, but you can update via Homebrew
    
    brew install git
    
    
    Linux
    
    # Ubuntu/Debian
    
    sudo apt update && sudo apt install git
    
    
    
    # RHEL/CentOS
    
    sudo dnf install git
    
    

    4. Install VS Code

    Visual Studio Code provides the integrated development environment with MCP support.

    Installation
    
    # Windows
    
    winget install Microsoft.VisualStudioCode
    
    
    
    # macOS
    
    brew install --cask visual-studio-code
    
    
    
    # Linux (Ubuntu/Debian)
    
    sudo snap install code --classic
    
    
    Required Extensions

    Install these VS Code extensions:

    
    # Install via command line
    
    code --install-extension ms-python.python
    
    code --install-extension ms-vscode.vscode-json
    
    code --install-extension ms-azuretools.vscode-docker
    
    code --install-extension ms-vscode.azure-account
    
    

    Or install through VS Code:

    1. Open VS Code

    2. Go to Extensions (Ctrl+Shift+X)

    3. Install:

    - Python (Microsoft)

    - Docker (Microsoft)

    - Azure Account (Microsoft)

    - JSON (Microsoft)

    5. Install Python

    Python 3.8+ is required for MCP server development.

    Windows
    
    # Using Windows Package Manager
    
    winget install Python.Python.3.11
    
    
    
    # Or download from: https://www.python.org/downloads/
    
    
    macOS
    
    # Using Homebrew
    
    brew install python@3.11
    
    
    Linux
    
    # Ubuntu/Debian
    
    sudo apt update && sudo apt install python3.11 python3.11-pip python3.11-venv
    
    
    
    # RHEL/CentOS
    
    sudo dnf install python3.11 python3.11-pip
    
    
    Verify Installation
    
    python --version  # Should show Python 3.11.x
    
    pip --version      # Should show pip version
    
    

    ๐Ÿš€ Project Setup

    1. Clone the Repository

    
    # Clone the main repository
    
    git clone https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail.git
    
    
    
    # Navigate to the project directory
    
    cd MCP-Server-and-PostgreSQL-Sample-Retail
    
    
    
    # Verify repository structure
    
    ls -la
    
    

    2. Create Python Virtual Environment

    
    # Create virtual environment
    
    python -m venv mcp-env
    
    
    
    # Activate virtual environment
    
    # Windows
    
    mcp-env\Scripts\activate
    
    
    
    # macOS/Linux
    
    source mcp-env/bin/activate
    
    
    
    # Upgrade pip
    
    python -m pip install --upgrade pip
    
    

    3. Install Python Dependencies

    
    # Install development dependencies
    
    pip install -r requirements.lock.txt
    
    
    
    # Verify key packages
    
    pip list | grep fastmcp
    
    pip list | grep asyncpg
    
    pip list | grep azure
    
    

    โ˜๏ธ Azure Resource Deployment

    1. Understand Resource Requirements

    Our MCP server requires these Azure resources:

    Resource Purpose Estimated Cost -------------- ------------- ------------------- Azure AI Foundry AI model hosting and management $10-50/month OpenAI Deployment Text embedding model (text-embedding-3-small) $5-20/month Application Insights Monitoring and telemetry $5-15/month Resource Group Resource organization Free

    2. Deploy Azure Resources

    Option A: Automated Deployment (Recommended)
    
    # Navigate to infrastructure directory
    
    cd infra
    
    
    
    # Windows - PowerShell
    
    ./deploy.ps1
    
    
    
    # macOS/Linux - Bash
    
    ./deploy.sh
    
    

    The deployment script will:

    1. Create a unique resource group

    2. Deploy Azure AI Foundry resources

    3. Deploy the text-embedding-3-small model

    4. Configure Application Insights

    5. Create a service principal for authentication

    6. Generate .env file with configuration

    Option B: Manual Deployment

    If you prefer manual control or the automated script fails:

    
    # Set variables
    
    RESOURCE_GROUP="rg-zava-mcp-$(date +%s)"
    
    LOCATION="westus2"
    
    AI_PROJECT_NAME="zava-ai-project"
    
    
    
    # Create resource group
    
    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    
    
    # Deploy main template
    
    az deployment group create \
    
      --resource-group $RESOURCE_GROUP \
    
      --template-file main.bicep \
    
      --parameters location=$LOCATION \
    
      --parameters resourcePrefix="zava-mcp"
    
    

    3. Verify Azure Deployment

    
    # Check resource group
    
    az group show --name $RESOURCE_GROUP --output table
    
    
    
    # List deployed resources
    
    az resource list --resource-group $RESOURCE_GROUP --output table
    
    
    
    # Test AI service
    
    az cognitiveservices account show \
    
      --name "your-ai-service-name" \
    
      --resource-group $RESOURCE_GROUP
    
    

    4. Configure Environment Variables

    After deployment, you should have a .env file. Verify it contains:

    
    # .env file contents
    
    PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com/
    
    AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/
    
    EMBEDDING_MODEL_DEPLOYMENT_NAME=text-embedding-3-small
    
    AZURE_CLIENT_ID=your-client-id
    
    AZURE_CLIENT_SECRET=your-client-secret
    
    AZURE_TENANT_ID=your-tenant-id
    
    APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=your-key;...
    
    
    
    # Database configuration (for development)
    
    POSTGRES_HOST=localhost
    
    POSTGRES_PORT=5432
    
    POSTGRES_DB=zava
    
    POSTGRES_USER=postgres
    
    POSTGRES_PASSWORD=your-secure-password
    
    

    ๐Ÿณ Docker Environment Setup

    1. Understand Docker Composition

    Our development environment uses Docker Compose:

    
    # docker-compose.yml overview
    
    version: '3.8'
    
    services:
    
      postgres:
    
        image: pgvector/pgvector:pg17
    
        environment:
    
          POSTGRES_DB: zava
    
          POSTGRES_USER: postgres
    
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - ./data:/backup_data:ro
    
          - ./docker-init:/docker-entrypoint-initdb.d:ro
    
        
    
      mcp_server:
    
        build: .
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        ports:
    
          - "8000:8000"
    
        env_file:
    
          - .env
    
    

    2. Start the Development Environment

    
    # Ensure you're in the project root directory
    
    cd /path/to/MCP-Server-and-PostgreSQL-Sample-Retail
    
    
    
    # Start the services
    
    docker-compose up -d
    
    
    
    # Check service status
    
    docker-compose ps
    
    
    
    # View logs
    
    docker-compose logs -f
    
    

    3. Verify Database Setup

    
    # Connect to PostgreSQL container
    
    docker-compose exec postgres psql -U postgres -d zava
    
    
    
    # Check database structure
    
    \dt retail.*
    
    
    
    # Verify sample data
    
    SELECT COUNT(*) FROM retail.stores;
    
    SELECT COUNT(*) FROM retail.products;
    
    SELECT COUNT(*) FROM retail.orders;
    
    
    
    # Exit PostgreSQL
    
    \q
    
    

    4. Test MCP Server

    
    # Check MCP server health
    
    curl http://localhost:8000/health
    
    
    
    # Test basic MCP endpoint
    
    curl -X POST http://localhost:8000/mcp \
    
      -H "Content-Type: application/json" \
    
      -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \
    
      -d '{"method": "tools/list", "params": {}}'
    
    

    ๐Ÿ”ง VS Code Configuration

    1. Configure MCP Integration

    Create VS Code MCP configuration:

    
    // .vscode/mcp.json
    
    {
    
        "servers": {
    
            "zava-sales-analysis-headoffice": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "00000000-0000-0000-0000-000000000000"}
    
            },
    
            "zava-sales-analysis-seattle": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"}
    
            },
    
            "zava-sales-analysis-redmond": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "e7f8a9b0-c1d2-3e4f-5678-90abcdef1234"}
    
            }
    
        },
    
        "inputs": []
    
    }
    
    

    2. Configure Python Environment

    
    // .vscode/settings.json
    
    {
    
        "python.defaultInterpreterPath": "./mcp-env/bin/python",
    
        "python.linting.enabled": true,
    
        "python.linting.pylintEnabled": true,
    
        "python.formatting.provider": "black",
    
        "python.testing.pytestEnabled": true,
    
        "python.testing.pytestArgs": ["tests"],
    
        "files.exclude": {
    
            "**/__pycache__": true,
    
            "**/.pytest_cache": true,
    
            "**/mcp-env": true
    
        }
    
    }
    
    

    3. Test VS Code Integration

    1. Open the project in VS Code:

    ```bash

    code .

    ```

    2. Open AI Chat:

    - Press Ctrl+Shift+P (Windows/Linux) or Cmd+Shift+P (macOS)

    - Type "AI Chat" and select "AI Chat: Open Chat"

    3. Test MCP Server Connection:

    - In AI Chat, type #zava and select one of the configured servers

    - Ask: "What tables are available in the database?"

    - You should receive a response listing the retail database tables

    โœ… Environment Validation

    1. Comprehensive System Check

    Run this validation script to verify your setup:

    
    # Create validation script
    
    cat > validate_setup.py << 'EOF'
    
    #!/usr/bin/env python3
    
    """
    
    Environment validation script for MCP Server setup.
    
    """
    
    import asyncio
    
    import os
    
    import sys
    
    import subprocess
    
    import requests
    
    import asyncpg
    
    from azure.identity import DefaultAzureCredential
    
    from azure.ai.projects import AIProjectClient
    
    
    
    async def validate_environment():
    
        """Comprehensive environment validation."""
    
        results = {}
    
        
    
        # Check Python version
    
        python_version = sys.version_info
    
        results['python'] = {
    
            'status': 'pass' if python_version >= (3, 8) else 'fail',
    
            'version': f"{python_version.major}.{python_version.minor}.{python_version.micro}",
    
            'required': '3.8+'
    
        }
    
        
    
        # Check required packages
    
        required_packages = ['fastmcp', 'asyncpg', 'azure-ai-projects']
    
        for package in required_packages:
    
            try:
    
                __import__(package)
    
                results[f'package_{package}'] = {'status': 'pass'}
    
            except ImportError:
    
                results[f'package_{package}'] = {'status': 'fail', 'error': 'Not installed'}
    
        
    
        # Check Docker
    
        try:
    
            result = subprocess.run(['docker', '--version'], capture_output=True, text=True)
    
            results['docker'] = {
    
                'status': 'pass' if result.returncode == 0 else 'fail',
    
                'version': result.stdout.strip() if result.returncode == 0 else 'Not available'
    
            }
    
        except FileNotFoundError:
    
            results['docker'] = {'status': 'fail', 'error': 'Docker not found'}
    
        
    
        # Check Azure CLI
    
        try:
    
            result = subprocess.run(['az', '--version'], capture_output=True, text=True)
    
            results['azure_cli'] = {
    
                'status': 'pass' if result.returncode == 0 else 'fail',
    
                'version': result.stdout.split('\n')[0] if result.returncode == 0 else 'Not available'
    
            }
    
        except FileNotFoundError:
    
            results['azure_cli'] = {'status': 'fail', 'error': 'Azure CLI not found'}
    
        
    
        # Check environment variables
    
        required_env_vars = [
    
            'PROJECT_ENDPOINT',
    
            'AZURE_OPENAI_ENDPOINT',
    
            'EMBEDDING_MODEL_DEPLOYMENT_NAME',
    
            'AZURE_CLIENT_ID',
    
            'AZURE_CLIENT_SECRET',
    
            'AZURE_TENANT_ID'
    
        ]
    
        
    
        for var in required_env_vars:
    
            value = os.getenv(var)
    
            results[f'env_{var}'] = {
    
                'status': 'pass' if value else 'fail',
    
                'value': '***' if value and 'SECRET' in var else value
    
            }
    
        
    
        # Check database connection
    
        try:
    
            conn = await asyncpg.connect(
    
                host=os.getenv('POSTGRES_HOST', 'localhost'),
    
                port=int(os.getenv('POSTGRES_PORT', 5432)),
    
                database=os.getenv('POSTGRES_DB', 'zava'),
    
                user=os.getenv('POSTGRES_USER', 'postgres'),
    
                password=os.getenv('POSTGRES_PASSWORD', 'secure_password')
    
            )
    
            
    
            # Test query
    
            result = await conn.fetchval('SELECT COUNT(*) FROM retail.stores')
    
            await conn.close()
    
            
    
            results['database'] = {
    
                'status': 'pass',
    
                'store_count': result
    
            }
    
        except Exception as e:
    
            results['database'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        # Check MCP server
    
        try:
    
            response = requests.get('http://localhost:8000/health', timeout=5)
    
            results['mcp_server'] = {
    
                'status': 'pass' if response.status_code == 200 else 'fail',
    
                'response': response.json() if response.status_code == 200 else response.text
    
            }
    
        except Exception as e:
    
            results['mcp_server'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        # Check Azure AI service
    
        try:
    
            credential = DefaultAzureCredential()
    
            project_client = AIProjectClient(
    
                endpoint=os.getenv('PROJECT_ENDPOINT'),
    
                credential=credential
    
            )
    
            
    
            # This will fail if credentials are invalid
    
            results['azure_ai'] = {'status': 'pass'}
    
            
    
        except Exception as e:
    
            results['azure_ai'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        return results
    
    
    
    def print_results(results):
    
        """Print formatted validation results."""
    
        print("๐Ÿ” Environment Validation Results\n")
    
        print("=" * 50)
    
        
    
        passed = 0
    
        failed = 0
    
        
    
        for component, result in results.items():
    
            status = result.get('status', 'unknown')
    
            if status == 'pass':
    
                print(f"โœ… {component}: PASS")
    
                passed += 1
    
            else:
    
                print(f"โŒ {component}: FAIL")
    
                if 'error' in result:
    
                    print(f"   Error: {result['error']}")
    
                failed += 1
    
        
    
        print("\n" + "=" * 50)
    
        print(f"Summary: {passed} passed, {failed} failed")
    
        
    
        if failed > 0:
    
            print("\nโ— Please fix the failed components before proceeding.")
    
            return False
    
        else:
    
            print("\n๐ŸŽ‰ All validations passed! Your environment is ready.")
    
            return True
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
    
    async def main():
    
        results = await validate_environment()
    
        success = print_results(results)
    
        sys.exit(0 if success else 1)
    
    
    
    EOF
    
    
    
    # Run validation
    
    python validate_setup.py
    
    

    2. Manual Validation Checklist

    โœ… Basic Tools

  • [ ] Docker version 20.10+ installed and running
  • [ ] Azure CLI 2.40+ installed and authenticated
  • [ ] Python 3.8+ with pip installed
  • [ ] Git 2.30+ installed
  • [ ] VS Code with required extensions
  • โœ… Azure Resources

  • [ ] Resource group created successfully
  • [ ] AI Foundry project deployed
  • [ ] OpenAI text-embedding-3-small model deployed
  • [ ] Application Insights configured
  • [ ] Service principal created with proper permissions
  • โœ… Environment Configuration

  • [ ] .env file created with all required variables
  • [ ] Azure credentials working (test with az account show)
  • [ ] PostgreSQL container running and accessible
  • [ ] Sample data loaded in database
  • โœ… VS Code Integration

  • [ ] .vscode/mcp.json configured
  • [ ] Python interpreter set to virtual environment
  • [ ] MCP servers appear in AI Chat
  • [ ] Can execute test queries through AI Chat
  • ๐Ÿ› ๏ธ Troubleshooting Common Issues

    Docker Issues

    Problem: Docker containers won't start

    
    # Check Docker service status
    
    docker info
    
    
    
    # Check available resources
    
    docker system df
    
    
    
    # Clean up if needed
    
    docker system prune -f
    
    
    
    # Restart Docker Desktop (Windows/macOS)
    
    # Or restart Docker service (Linux)
    
    sudo systemctl restart docker
    
    

    Problem: PostgreSQL connection fails

    
    # Check container logs
    
    docker-compose logs postgres
    
    
    
    # Verify container is healthy
    
    docker-compose ps
    
    
    
    # Test direct connection
    
    docker-compose exec postgres psql -U postgres -d zava -c "SELECT 1;"
    
    

    Azure Deployment Issues

    Problem: Azure deployment fails

    
    # Check Azure CLI authentication
    
    az account show
    
    
    
    # Verify subscription permissions
    
    az role assignment list --assignee $(az account show --query user.name -o tsv)
    
    
    
    # Check resource provider registration
    
    az provider register --namespace Microsoft.CognitiveServices
    
    az provider register --namespace Microsoft.Insights
    
    

    Problem: AI service authentication fails

    
    # Test service principal
    
    az login --service-principal \
    
      --username $AZURE_CLIENT_ID \
    
      --password $AZURE_CLIENT_SECRET \
    
      --tenant $AZURE_TENANT_ID
    
    
    
    # Verify AI service deployment
    
    az cognitiveservices account list --query "[].{Name:name,Kind:kind,Location:location}"
    
    

    Python Environment Issues

    Problem: Package installation fails

    
    # Upgrade pip and setuptools
    
    python -m pip install --upgrade pip setuptools wheel
    
    
    
    # Clear pip cache
    
    pip cache purge
    
    
    
    # Install packages one by one to identify issues
    
    pip install fastmcp
    
    pip install asyncpg
    
    pip install azure-ai-projects
    
    

    Problem: VS Code can't find Python interpreter

    
    # Show Python interpreter paths
    
    which python  # macOS/Linux
    
    where python  # Windows
    
    
    
    # Activate virtual environment first
    
    source mcp-env/bin/activate  # macOS/Linux
    
    mcp-env\Scripts\activate     # Windows
    
    
    
    # Then open VS Code
    
    code .
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Complete Development Environment: All tools installed and configured

    โœ… Azure Resources Deployed: AI services and supporting infrastructure

    โœ… Docker Environment Running: PostgreSQL and MCP server containers

    โœ… VS Code Integration: MCP servers configured and accessible

    โœ… Validated Setup: All components tested and working together

    โœ… Troubleshooting Knowledge: Common issues and solutions

    ๐Ÿš€ What's Next

    With your environment ready, continue to Lab 04: Database Design and Schema to:

  • Explore the retail database schema in detail
  • Understand multi-tenant data modeling
  • Learn about Row Level Security implementation
  • Work with sample retail data
  • ๐Ÿ“š Additional Resources

    Development Tools

  • Docker Documentation - Complete Docker reference
  • Azure CLI Reference - Azure CLI commands
  • VS Code Documentation - Editor configuration and extensions
  • Azure Services

  • Azure AI Foundry Documentation - AI service configuration
  • Azure OpenAI Service - AI model deployment
  • Application Insights - Monitoring setup
  • Python Development

  • Python Virtual Environments - Environment management
  • AsyncIO Documentation - Async programming patterns
  • FastAPI Documentation - Web framework patterns
  • ---

    Next: Environment ready? Continue with Lab 04: Database Design and Schema

    Lab 4-6: Building the MCP Server 04 Database Design and Schema

    Database Design and Schema

    ๐ŸŽฏ What This Lab Covers

    This lab dives deep into the PostgreSQL database design for the Zava Retail system. You'll learn to implement a comprehensive retail schema with vector search capabilities, multi-tenant data modeling, and Row Level Security (RLS) for data isolation.

    Overview

    The database is the foundation of our MCP server, storing retail data across multiple stores while maintaining strict data isolation.

    We use PostgreSQL with the pgvector extension to enable semantic search capabilities, allowing customers to find products using natural language queries.

    Our schema follows modern multi-tenant patterns with Row Level Security ensuring users can only access data from their authorized stores. This approach provides enterprise-grade security while maintaining optimal performance.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Design scalable multi-tenant retail database schemas
  • Implement PostgreSQL with pgvector for vector search
  • Configure Row Level Security for data isolation
  • Generate realistic sample data for testing
  • Optimize database performance for retail workloads
  • Implement backup and recovery strategies
  • ๐Ÿ—ƒ๏ธ Database Architecture

    PostgreSQL with pgvector

    Our database leverages PostgreSQL's enterprise features combined with the pgvector extension for AI-powered search:

    
    -- Enable required extensions
    
    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
    
    CREATE EXTENSION IF NOT EXISTS "pgcrypto";
    
    CREATE EXTENSION IF NOT EXISTS "vector";
    
    
    
    -- Verify vector extension installation
    
    SELECT * FROM pg_extension WHERE extname = 'vector';
    
    

    Multi-Tenant Architecture

    The database uses a shared database, shared schema multi-tenancy model with Row Level Security:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                 PostgreSQL                      โ”‚
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  retail Schema (Shared)                        โ”‚
    
    โ”‚  โ”œโ”€โ”€ stores (Master tenant data)               โ”‚
    
    โ”‚  โ”œโ”€โ”€ customers (RLS by store_id)               โ”‚
    
    โ”‚  โ”œโ”€โ”€ products (RLS by store_id)                โ”‚
    
    โ”‚  โ”œโ”€โ”€ sales_transactions (RLS by store_id)      โ”‚
    
    โ”‚  โ”œโ”€โ”€ sales_transaction_items (RLS via join)    โ”‚
    
    โ”‚  โ””โ”€โ”€ product_embeddings (RLS by store_id)      โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ๐Ÿ“Š Core Schema Design

    Stores Table (Tenant Master)

    
    -- Stores table: Master tenant registry
    
    CREATE TABLE retail.stores (
    
        store_id VARCHAR(50) PRIMARY KEY,
    
        store_name VARCHAR(100) NOT NULL,
    
        store_location VARCHAR(100),
    
        store_type VARCHAR(50),
    
        region VARCHAR(50),
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        is_active BOOLEAN DEFAULT TRUE
    
    );
    
    
    
    -- Sample stores data
    
    INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region) VALUES
    
    ('seattle', 'Zava Retail Seattle', 'Seattle, WA', 'flagship', 'west'),
    
    ('redmond', 'Zava Retail Redmond', 'Redmond, WA', 'standard', 'west'),
    
    ('bellevue', 'Zava Retail Bellevue', 'Bellevue, WA', 'standard', 'west'),
    
    ('online', 'Zava Retail Online', 'Digital', 'ecommerce', 'global');
    
    
    
    -- Create index for performance
    
    CREATE INDEX idx_stores_region ON retail.stores(region);
    
    CREATE INDEX idx_stores_active ON retail.stores(is_active) WHERE is_active = TRUE;
    
    

    Customers Table

    
    -- Customers table with RLS
    
    CREATE TABLE retail.customers (
    
        customer_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        first_name VARCHAR(50) NOT NULL,
    
        last_name VARCHAR(50) NOT NULL,
    
        email VARCHAR(100) UNIQUE NOT NULL,
    
        phone VARCHAR(20),
    
        date_of_birth DATE,
    
        gender VARCHAR(20),
    
        customer_since DATE DEFAULT CURRENT_DATE,
    
        loyalty_tier VARCHAR(20) DEFAULT 'bronze',
    
        total_lifetime_value DECIMAL(10,2) DEFAULT 0.00,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Enable RLS
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy: Users can only see customers from their store
    
    CREATE POLICY customers_store_isolation ON retail.customers
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Indexes for performance
    
    CREATE INDEX idx_customers_store_id ON retail.customers(store_id);
    
    CREATE INDEX idx_customers_email ON retail.customers(email);
    
    CREATE INDEX idx_customers_loyalty_tier ON retail.customers(loyalty_tier);
    
    CREATE INDEX idx_customers_created_at ON retail.customers(created_at);
    
    

    Products Table with Categories

    
    -- Product categories
    
    CREATE TABLE retail.product_categories (
    
        category_id SERIAL PRIMARY KEY,
    
        category_name VARCHAR(100) NOT NULL UNIQUE,
    
        parent_category_id INTEGER REFERENCES retail.product_categories(category_id),
    
        description TEXT,
    
        is_active BOOLEAN DEFAULT TRUE
    
    );
    
    
    
    -- Insert sample categories
    
    INSERT INTO retail.product_categories (category_name, description) VALUES
    
    ('Electronics', 'Electronic devices and accessories'),
    
    ('Clothing', 'Apparel and fashion items'),
    
    ('Home & Garden', 'Home improvement and garden supplies'),
    
    ('Sports & Outdoors', 'Sports equipment and outdoor gear'),
    
    ('Books & Media', 'Books, movies, and digital media'),
    
    ('Health & Beauty', 'Health and beauty products'),
    
    ('Automotive', 'Car parts and automotive accessories');
    
    
    
    -- Products table with rich metadata
    
    CREATE TABLE retail.products (
    
        product_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        sku VARCHAR(50) NOT NULL,
    
        product_name VARCHAR(200) NOT NULL,
    
        product_description TEXT,
    
        category_id INTEGER REFERENCES retail.product_categories(category_id),
    
        brand VARCHAR(100),
    
        model VARCHAR(100),
    
        color VARCHAR(50),
    
        size VARCHAR(50),
    
        weight_kg DECIMAL(8,3),
    
        dimensions_cm VARCHAR(50), -- e.g., "30x20x15"
    
        price DECIMAL(10,2) NOT NULL,
    
        cost DECIMAL(10,2),
    
        current_stock INTEGER DEFAULT 0,
    
        minimum_stock INTEGER DEFAULT 0,
    
        maximum_stock INTEGER DEFAULT 1000,
    
        reorder_point INTEGER DEFAULT 10,
    
        supplier_name VARCHAR(100),
    
        supplier_sku VARCHAR(50),
    
        is_active BOOLEAN DEFAULT TRUE,
    
        is_featured BOOLEAN DEFAULT FALSE,
    
        rating_average DECIMAL(3,2) DEFAULT 0.00,
    
        rating_count INTEGER DEFAULT 0,
    
        tags TEXT[], -- Array of tags for flexible categorization
    
        metadata JSONB, -- Flexible metadata storage
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure SKU uniqueness within store
    
        CONSTRAINT unique_sku_per_store UNIQUE (store_id, sku)
    
    );
    
    
    
    -- Enable RLS for products
    
    ALTER TABLE retail.products ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for products
    
    CREATE POLICY products_store_isolation ON retail.products
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Comprehensive indexes
    
    CREATE INDEX idx_products_store_id ON retail.products(store_id);
    
    CREATE INDEX idx_products_sku ON retail.products(sku);
    
    CREATE INDEX idx_products_category ON retail.products(category_id);
    
    CREATE INDEX idx_products_brand ON retail.products(brand);
    
    CREATE INDEX idx_products_price ON retail.products(price);
    
    CREATE INDEX idx_products_stock ON retail.products(current_stock);
    
    CREATE INDEX idx_products_active ON retail.products(is_active) WHERE is_active = TRUE;
    
    CREATE INDEX idx_products_featured ON retail.products(is_featured) WHERE is_featured = TRUE;
    
    CREATE INDEX idx_products_tags ON retail.products USING GIN(tags);
    
    CREATE INDEX idx_products_metadata ON retail.products USING GIN(metadata);
    
    CREATE INDEX idx_products_text_search ON retail.products USING GIN(
    
        to_tsvector('english', product_name || ' ' || COALESCE(product_description, '') || ' ' || COALESCE(brand, ''))
    
    );
    
    

    Sales Transactions

    
    -- Sales transactions table
    
    CREATE TABLE retail.sales_transactions (
    
        transaction_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        customer_id UUID REFERENCES retail.customers(customer_id),
    
        transaction_date TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        transaction_type VARCHAR(20) DEFAULT 'sale', -- 'sale', 'return', 'exchange'
    
        payment_method VARCHAR(50), -- 'cash', 'credit_card', 'debit_card', 'digital_wallet'
    
        subtotal DECIMAL(10,2) NOT NULL,
    
        tax_amount DECIMAL(10,2) DEFAULT 0.00,
    
        discount_amount DECIMAL(10,2) DEFAULT 0.00,
    
        total_amount DECIMAL(10,2) NOT NULL,
    
        cashier_id VARCHAR(50),
    
        register_id VARCHAR(50),
    
        receipt_number VARCHAR(50),
    
        notes TEXT,
    
        metadata JSONB,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Sales transaction items (line items)
    
    CREATE TABLE retail.sales_transaction_items (
    
        item_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        transaction_id UUID NOT NULL REFERENCES retail.sales_transactions(transaction_id) ON DELETE CASCADE,
    
        product_id UUID NOT NULL REFERENCES retail.products(product_id),
    
        quantity INTEGER NOT NULL DEFAULT 1,
    
        unit_price DECIMAL(10,2) NOT NULL,
    
        total_price DECIMAL(10,2) NOT NULL,
    
        discount_amount DECIMAL(10,2) DEFAULT 0.00,
    
        tax_amount DECIMAL(10,2) DEFAULT 0.00,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure positive quantities and prices
    
        CONSTRAINT positive_quantity CHECK (quantity > 0),
    
        CONSTRAINT positive_unit_price CHECK (unit_price >= 0),
    
        CONSTRAINT positive_total_price CHECK (total_price >= 0)
    
    );
    
    
    
    -- Enable RLS for transactions
    
    ALTER TABLE retail.sales_transactions ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for sales transactions
    
    CREATE POLICY sales_transactions_store_isolation ON retail.sales_transactions
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- RLS for transaction items (via join with transactions)
    
    ALTER TABLE retail.sales_transaction_items ENABLE ROW LEVEL SECURITY;
    
    
    
    CREATE POLICY sales_transaction_items_store_isolation ON retail.sales_transaction_items
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        );
    
    
    
    -- Performance indexes
    
    CREATE INDEX idx_sales_transactions_store_id ON retail.sales_transactions(store_id);
    
    CREATE INDEX idx_sales_transactions_customer_id ON retail.sales_transactions(customer_id);
    
    CREATE INDEX idx_sales_transactions_date ON retail.sales_transactions(transaction_date);
    
    CREATE INDEX idx_sales_transactions_type ON retail.sales_transactions(transaction_type);
    
    CREATE INDEX idx_sales_transactions_payment ON retail.sales_transactions(payment_method);
    
    
    
    CREATE INDEX idx_sales_transaction_items_transaction_id ON retail.sales_transaction_items(transaction_id);
    
    CREATE INDEX idx_sales_transaction_items_product_id ON retail.sales_transaction_items(product_id);
    
    

    ๐Ÿ” Vector Search Implementation

    Product Embeddings Table

    
    -- Product embeddings for semantic search
    
    CREATE TABLE retail.product_embeddings (
    
        embedding_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        product_id UUID NOT NULL REFERENCES retail.products(product_id) ON DELETE CASCADE,
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        embedding_text TEXT NOT NULL, -- The text that was embedded
    
        embedding vector(1536), -- OpenAI text-embedding-3-small dimension
    
        embedding_model VARCHAR(100) NOT NULL DEFAULT 'text-embedding-3-small',
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure one embedding per product per model
    
        CONSTRAINT unique_product_embedding UNIQUE (product_id, embedding_model)
    
    );
    
    
    
    -- Enable RLS for embeddings
    
    ALTER TABLE retail.product_embeddings ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for embeddings
    
    CREATE POLICY product_embeddings_store_isolation ON retail.product_embeddings
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Vector similarity index (HNSW for fast approximate search)
    
    CREATE INDEX idx_product_embeddings_vector ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops);
    
    
    
    -- Additional indexes
    
    CREATE INDEX idx_product_embeddings_product_id ON retail.product_embeddings(product_id);
    
    CREATE INDEX idx_product_embeddings_store_id ON retail.product_embeddings(store_id);
    
    CREATE INDEX idx_product_embeddings_model ON retail.product_embeddings(embedding_model);
    
    

    Vector Search Functions

    
    -- Function to search products by similarity
    
    CREATE OR REPLACE FUNCTION retail.search_products_by_similarity(
    
        search_embedding vector(1536),
    
        similarity_threshold float DEFAULT 0.7,
    
        max_results integer DEFAULT 20
    
    )
    
    RETURNS TABLE (
    
        product_id UUID,
    
        product_name VARCHAR(200),
    
        product_description TEXT,
    
        brand VARCHAR(100),
    
        price DECIMAL(10,2),
    
        similarity_score float
    
    ) 
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            p.product_id,
    
            p.product_name,
    
            p.product_description,
    
            p.brand,
    
            p.price,
    
            1 - (pe.embedding <=> search_embedding) as similarity_score
    
        FROM retail.product_embeddings pe
    
        JOIN retail.products p ON pe.product_id = p.product_id
    
        WHERE 
    
            pe.store_id = current_setting('app.current_store_id', true)
    
            AND p.is_active = TRUE
    
            AND 1 - (pe.embedding <=> search_embedding) >= similarity_threshold
    
        ORDER BY pe.embedding <=> search_embedding
    
        LIMIT max_results;
    
    END;
    
    $$;
    
    
    
    -- Grant execute permission
    
    GRANT EXECUTE ON FUNCTION retail.search_products_by_similarity TO mcp_user;
    
    

    ๐Ÿ” Row Level Security Setup

    Database Roles and Permissions

    
    -- Create MCP application role
    
    CREATE ROLE mcp_user LOGIN;
    
    
    
    -- Grant schema usage
    
    GRANT USAGE ON SCHEMA retail TO mcp_user;
    
    
    
    -- Grant table permissions
    
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA retail TO mcp_user;
    
    GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA retail TO mcp_user;
    
    
    
    -- Grant permissions on future tables
    
    ALTER DEFAULT PRIVILEGES IN SCHEMA retail GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO mcp_user;
    
    ALTER DEFAULT PRIVILEGES IN SCHEMA retail GRANT USAGE, SELECT ON SEQUENCES TO mcp_user;
    
    
    
    -- Function to set store context
    
    CREATE OR REPLACE FUNCTION retail.set_store_context(store_id_param VARCHAR(50))
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    BEGIN
    
        -- Verify store exists and user has access
    
        IF NOT EXISTS (SELECT 1 FROM retail.stores WHERE store_id = store_id_param AND is_active = TRUE) THEN
    
            RAISE EXCEPTION 'Invalid or inactive store: %', store_id_param;
    
        END IF;
    
        
    
        -- Set the store context
    
        PERFORM set_config('app.current_store_id', store_id_param, false);
    
        
    
        -- Log the context change
    
        INSERT INTO retail.audit_log (
    
            table_name,
    
            action,
    
            user_name,
    
            store_id,
    
            metadata
    
        ) VALUES (
    
            'security_context',
    
            'store_context_set',
    
            current_user,
    
            store_id_param,
    
            jsonb_build_object('timestamp', current_timestamp)
    
        );
    
    END;
    
    $$;
    
    
    
    -- Grant execute permission
    
    GRANT EXECUTE ON FUNCTION retail.set_store_context TO mcp_user;
    
    

    Audit Logging

    
    -- Audit log table for security and compliance
    
    CREATE TABLE retail.audit_log (
    
        log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        table_name VARCHAR(100) NOT NULL,
    
        action VARCHAR(50) NOT NULL, -- INSERT, UPDATE, DELETE, SELECT
    
        user_name VARCHAR(100) NOT NULL DEFAULT current_user,
    
        store_id VARCHAR(50),
    
        record_id UUID,
    
        old_values JSONB,
    
        new_values JSONB,
    
        metadata JSONB,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Index for audit queries
    
    CREATE INDEX idx_audit_log_table_name ON retail.audit_log(table_name);
    
    CREATE INDEX idx_audit_log_action ON retail.audit_log(action);
    
    CREATE INDEX idx_audit_log_user_name ON retail.audit_log(user_name);
    
    CREATE INDEX idx_audit_log_store_id ON retail.audit_log(store_id);
    
    CREATE INDEX idx_audit_log_created_at ON retail.audit_log(created_at);
    
    
    
    -- Audit trigger function
    
    CREATE OR REPLACE FUNCTION retail.audit_trigger()
    
    RETURNS trigger AS $$
    
    BEGIN
    
        IF TG_OP = 'DELETE' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                old_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(OLD.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(OLD.customer_id, OLD.product_id, OLD.transaction_id),
    
                row_to_json(OLD)
    
            );
    
            RETURN OLD;
    
        ELSIF TG_OP = 'UPDATE' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                old_values,
    
                new_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(NEW.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(NEW.customer_id, NEW.product_id, NEW.transaction_id),
    
                row_to_json(OLD),
    
                row_to_json(NEW)
    
            );
    
            RETURN NEW;
    
        ELSIF TG_OP = 'INSERT' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                new_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(NEW.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(NEW.customer_id, NEW.product_id, NEW.transaction_id),
    
                row_to_json(NEW)
    
            );
    
            RETURN NEW;
    
        END IF;
    
        RETURN NULL;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    
    
    -- Create audit triggers
    
    CREATE TRIGGER customers_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.customers
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    
    
    CREATE TRIGGER products_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.products
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    
    
    CREATE TRIGGER sales_transactions_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.sales_transactions
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    

    ๐Ÿ“Š Sample Data Generation

    Realistic Test Data Script

    
    # scripts/generate_sample_data.py
    
    """
    
    Generate realistic sample data for the Zava Retail database.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import random
    
    import json
    
    from datetime import datetime, timedelta
    
    from faker import Faker
    
    from typing import List, Dict, Any
    
    import numpy as np
    
    
    
    fake = Faker()
    
    
    
    class SampleDataGenerator:
    
        """Generate realistic retail sample data."""
    
        
    
        def __init__(self, connection_string: str):
    
            self.connection_string = connection_string
    
            self.stores = ['seattle', 'redmond', 'bellevue', 'online']
    
            
    
            # Product categories with realistic items
    
            self.product_data = {
    
                'Electronics': {
    
                    'brands': ['Apple', 'Samsung', 'Sony', 'LG', 'HP', 'Dell'],
    
                    'items': [
    
                        'Smartphone', 'Laptop', 'Tablet', 'Headphones', 'Smart TV',
    
                        'Gaming Console', 'Smartwatch', 'Bluetooth Speaker'
    
                    ]
    
                },
    
                'Clothing': {
    
                    'brands': ['Nike', 'Adidas', 'Zara', 'H&M', 'Levi\'s', 'Gap'],
    
                    'items': [
    
                        'T-Shirt', 'Jeans', 'Dress', 'Jacket', 'Sneakers',
    
                        'Sweater', 'Shorts', 'Blouse'
    
                    ]
    
                },
    
                'Home & Garden': {
    
                    'brands': ['IKEA', 'Home Depot', 'Wayfair', 'Target', 'Walmart'],
    
                    'items': [
    
                        'Sofa', 'Dining Table', 'Lamp', 'Garden Tool', 'Plant Pot',
    
                        'Curtains', 'Rug', 'Kitchen Appliance'
    
                    ]
    
                }
    
            }
    
        
    
        async def generate_all_data(self):
    
            """Generate complete sample dataset."""
    
            
    
            conn = await asyncpg.connect(self.connection_string)
    
            
    
            try:
    
                print("๐Ÿช Generating stores data...")
    
                await self._ensure_stores_exist(conn)
    
                
    
                print("๐Ÿ‘ฅ Generating customers...")
    
                customers = await self._generate_customers(conn, 2000)
    
                
    
                print("๐Ÿ“ฆ Generating products...")
    
                products = await self._generate_products(conn, 500)
    
                
    
                print("๐Ÿ›’ Generating sales transactions...")
    
                await self._generate_sales_transactions(conn, customers, products, 5000)
    
                
    
                print("โœ… Sample data generation complete!")
    
                
    
            finally:
    
                await conn.close()
    
        
    
        async def _ensure_stores_exist(self, conn):
    
            """Ensure all stores exist in the database."""
    
            
    
            stores_data = [
    
                ('seattle', 'Zava Retail Seattle', 'Seattle, WA', 'flagship', 'west'),
    
                ('redmond', 'Zava Retail Redmond', 'Redmond, WA', 'standard', 'west'),
    
                ('bellevue', 'Zava Retail Bellevue', 'Bellevue, WA', 'standard', 'west'),
    
                ('online', 'Zava Retail Online', 'Digital', 'ecommerce', 'global')
    
            ]
    
            
    
            for store_data in stores_data:
    
                await conn.execute("""
    
                    INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region)
    
                    VALUES ($1, $2, $3, $4, $5)
    
                    ON CONFLICT (store_id) DO NOTHING
    
                """, *store_data)
    
        
    
        async def _generate_customers(self, conn, count: int) -> List[Dict]:
    
            """Generate realistic customer data."""
    
            
    
            customers = []
    
            
    
            for _ in range(count):
    
                store_id = random.choice(self.stores)
    
                customer_data = {
    
                    'store_id': store_id,
    
                    'first_name': fake.first_name(),
    
                    'last_name': fake.last_name(),
    
                    'email': fake.unique.email(),
    
                    'phone': fake.phone_number()[:20],
    
                    'date_of_birth': fake.date_of_birth(minimum_age=18, maximum_age=80),
    
                    'gender': random.choice(['Male', 'Female', 'Other', 'Prefer not to say']),
    
                    'customer_since': fake.date_between(start_date='-5y', end_date='today'),
    
                    'loyalty_tier': random.choices(
    
                        ['bronze', 'silver', 'gold', 'platinum'],
    
                        weights=[50, 30, 15, 5]
    
                    )[0]
    
                }
    
                
    
                customer_id = await conn.fetchval("""
    
                    INSERT INTO retail.customers (
    
                        store_id, first_name, last_name, email, phone,
    
                        date_of_birth, gender, customer_since, loyalty_tier
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
    
                    RETURNING customer_id
    
                """, *customer_data.values())
    
                
    
                customer_data['customer_id'] = customer_id
    
                customers.append(customer_data)
    
            
    
            return customers
    
        
    
        async def _generate_products(self, conn, count: int) -> List[Dict]:
    
            """Generate realistic product data."""
    
            
    
            # Get category IDs
    
            categories = await conn.fetch("SELECT category_id, category_name FROM retail.product_categories")
    
            category_map = {cat['category_name']: cat['category_id'] for cat in categories}
    
            
    
            products = []
    
            
    
            for _ in range(count):
    
                store_id = random.choice(self.stores)
    
                category_name = random.choice(list(self.product_data.keys()))
    
                category_id = category_map.get(category_name)
    
                
    
                if not category_id:
    
                    continue
    
                
    
                brand = random.choice(self.product_data[category_name]['brands'])
    
                item_type = random.choice(self.product_data[category_name]['items'])
    
                
    
                # Generate realistic pricing
    
                base_price = random.uniform(10, 1000)
    
                cost = base_price * random.uniform(0.4, 0.7)  # 40-70% cost margin
    
                
    
                product_data = {
    
                    'store_id': store_id,
    
                    'sku': f"{brand[:3].upper()}-{fake.unique.random_number(digits=6)}",
    
                    'product_name': f"{brand} {item_type}",
    
                    'product_description': fake.text(max_nb_chars=500),
    
                    'category_id': category_id,
    
                    'brand': brand,
    
                    'model': f"Model {fake.random_number(digits=4)}",
    
                    'color': fake.color_name(),
    
                    'size': random.choice(['XS', 'S', 'M', 'L', 'XL', 'XXL', 'One Size']),
    
                    'weight_kg': round(random.uniform(0.1, 10.0), 2),
    
                    'price': round(base_price, 2),
    
                    'cost': round(cost, 2),
    
                    'current_stock': random.randint(0, 100),
    
                    'minimum_stock': random.randint(5, 20),
    
                    'reorder_point': random.randint(10, 30),
    
                    'supplier_name': fake.company(),
    
                    'is_featured': random.choice([True, False]),
    
                    'rating_average': round(random.uniform(3.0, 5.0), 2),
    
                    'rating_count': random.randint(0, 500),
    
                    'tags': random.sample([
    
                        'popular', 'new', 'sale', 'limited', 'bestseller', 
    
                        'eco-friendly', 'premium', 'budget'
    
                    ], k=random.randint(1, 3))
    
                }
    
                
    
                product_id = await conn.fetchval("""
    
                    INSERT INTO retail.products (
    
                        store_id, sku, product_name, product_description, category_id,
    
                        brand, model, color, size, weight_kg, price, cost,
    
                        current_stock, minimum_stock, reorder_point, supplier_name,
    
                        is_featured, rating_average, rating_count, tags
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
    
                    RETURNING product_id
    
                """, *product_data.values())
    
                
    
                product_data['product_id'] = product_id
    
                products.append(product_data)
    
            
    
            return products
    
        
    
        async def _generate_sales_transactions(self, conn, customers: List[Dict], products: List[Dict], count: int):
    
            """Generate realistic sales transaction data."""
    
            
    
            for _ in range(count):
    
                # Select customer and matching store products
    
                customer = random.choice(customers)
    
                store_products = [p for p in products if p['store_id'] == customer['store_id']]
    
                
    
                if not store_products:
    
                    continue
    
                
    
                # Generate transaction basics
    
                transaction_date = fake.date_time_between(start_date='-1y', end_date='now')
    
                transaction_type = random.choices(
    
                    ['sale', 'return', 'exchange'],
    
                    weights=[90, 7, 3]
    
                )[0]
    
                
    
                payment_method = random.choices(
    
                    ['credit_card', 'debit_card', 'cash', 'digital_wallet'],
    
                    weights=[45, 25, 20, 10]
    
                )[0]
    
                
    
                # Generate transaction items (1-5 items per transaction)
    
                num_items = random.choices([1, 2, 3, 4, 5], weights=[40, 30, 20, 7, 3])[0]
    
                selected_products = random.sample(store_products, min(num_items, len(store_products)))
    
                
    
                subtotal = 0
    
                transaction_items = []
    
                
    
                for product in selected_products:
    
                    quantity = random.randint(1, 3)
    
                    unit_price = product['price']
    
                    
    
                    # Apply random discounts occasionally
    
                    discount_amount = 0
    
                    if random.random() < 0.2:  # 20% chance of discount
    
                        discount_amount = unit_price * quantity * random.uniform(0.05, 0.25)
    
                    
    
                    total_price = (unit_price * quantity) - discount_amount
    
                    subtotal += total_price
    
                    
    
                    transaction_items.append({
    
                        'product_id': product['product_id'],
    
                        'quantity': quantity,
    
                        'unit_price': unit_price,
    
                        'total_price': total_price,
    
                        'discount_amount': discount_amount
    
                    })
    
                
    
                # Calculate totals
    
                discount_amount = sum(item['discount_amount'] for item in transaction_items)
    
                tax_amount = subtotal * 0.08  # 8% tax rate
    
                total_amount = subtotal + tax_amount
    
                
    
                # Insert transaction
    
                transaction_id = await conn.fetchval("""
    
                    INSERT INTO retail.sales_transactions (
    
                        store_id, customer_id, transaction_date, transaction_type,
    
                        payment_method, subtotal, tax_amount, discount_amount, total_amount,
    
                        cashier_id, register_id, receipt_number
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
    
                    RETURNING transaction_id
    
                """, 
    
                    customer['store_id'], customer['customer_id'], transaction_date,
    
                    transaction_type, payment_method, subtotal, tax_amount,
    
                    discount_amount, total_amount, f"CASHIER{random.randint(1, 10)}",
    
                    f"REG{random.randint(1, 5)}", f"RCP{fake.random_number(digits=8)}"
    
                )
    
                
    
                # Insert transaction items
    
                for item in transaction_items:
    
                    await conn.execute("""
    
                        INSERT INTO retail.sales_transaction_items (
    
                            transaction_id, product_id, quantity, unit_price,
    
                            total_price, discount_amount
    
                        ) VALUES ($1, $2, $3, $4, $5, $6)
    
                    """, 
    
                        transaction_id, item['product_id'], item['quantity'],
    
                        item['unit_price'], item['total_price'], item['discount_amount']
    
                    )
    
    
    
    # Usage example
    
    if __name__ == "__main__":
    
        import os
    
        from config import Config
    
        
    
        config = Config()
    
        generator = SampleDataGenerator(config.database.connection_string)
    
        
    
        asyncio.run(generator.generate_all_data())
    
    

    ๐Ÿš€ Performance Optimization

    Database Configuration

    
    -- Performance-oriented PostgreSQL settings
    
    -- Add to postgresql.conf
    
    
    
    # Memory settings
    
    shared_buffers = '256MB'                # 25% of RAM for dedicated DB server
    
    effective_cache_size = '1GB'           # Estimate of OS cache size
    
    work_mem = '4MB'                       # Memory for sorts and hash joins
    
    maintenance_work_mem = '64MB'          # Memory for VACUUM, CREATE INDEX
    
    
    
    # Connection settings
    
    max_connections = 100                  # Adjust based on application needs
    
    
    
    # Write-ahead logging
    
    wal_buffers = '16MB'
    
    checkpoint_segments = 32               # PostgreSQL < 9.5
    
    max_wal_size = '1GB'                   # PostgreSQL >= 9.5
    
    
    
    # Query planner
    
    random_page_cost = 1.1                 # SSD-optimized
    
    effective_io_concurrency = 200         # SSD concurrent I/O capability
    
    
    
    # Logging for performance monitoring
    
    log_min_duration_statement = 1000      # Log queries > 1 second
    
    log_checkpoints = on
    
    log_connections = on
    
    log_disconnections = on
    
    log_line_prefix = '%t [%p-%l] %q%u@%d '
    
    

    Query Optimization Views

    
    -- Create monitoring views for query performance
    
    CREATE VIEW retail.slow_queries AS
    
    SELECT 
    
        query,
    
        calls,
    
        total_exec_time,
    
        mean_exec_time,
    
        max_exec_time,
    
        stddev_exec_time,
    
        rows,
    
        100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
    
    FROM pg_stat_statements
    
    WHERE mean_exec_time > 100  -- Queries taking more than 100ms on average
    
    ORDER BY mean_exec_time DESC;
    
    
    
    -- Table sizes and index usage
    
    CREATE VIEW retail.table_stats AS
    
    SELECT
    
        schemaname,
    
        tablename,
    
        pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
    
        pg_stat_get_tuples_inserted(c.oid) as inserts,
    
        pg_stat_get_tuples_updated(c.oid) as updates,
    
        pg_stat_get_tuples_deleted(c.oid) as deletes,
    
        pg_stat_get_live_tuples(c.oid) as live_tuples,
    
        pg_stat_get_dead_tuples(c.oid) as dead_tuples
    
    FROM pg_tables pt
    
    JOIN pg_class c ON c.relname = pt.tablename
    
    WHERE schemaname = 'retail'
    
    ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
    
    
    
    -- Index usage statistics
    
    CREATE VIEW retail.index_usage AS
    
    SELECT
    
        schemaname,
    
        tablename,
    
        indexname,
    
        idx_tup_read,
    
        idx_tup_fetch,
    
        pg_size_pretty(pg_relation_size(indexrelname)) as size
    
    FROM pg_stat_user_indexes
    
    WHERE schemaname = 'retail'
    
    ORDER BY idx_tup_read DESC;
    
    

    Automated Maintenance

    
    -- Create function for automated maintenance
    
    CREATE OR REPLACE FUNCTION retail.perform_maintenance()
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    AS $$
    
    BEGIN
    
        -- Update table statistics
    
        ANALYZE retail.customers;
    
        ANALYZE retail.products;
    
        ANALYZE retail.sales_transactions;
    
        ANALYZE retail.sales_transaction_items;
    
        ANALYZE retail.product_embeddings;
    
        
    
        -- Vacuum tables with high update/delete activity
    
        VACUUM (ANALYZE, VERBOSE) retail.customers;
    
        VACUUM (ANALYZE, VERBOSE) retail.products;
    
        
    
        -- Reindex if needed (check for index bloat)
    
        REINDEX INDEX CONCURRENTLY idx_products_text_search;
    
        REINDEX INDEX CONCURRENTLY idx_product_embeddings_vector;
    
        
    
        -- Log maintenance completion
    
        INSERT INTO retail.audit_log (
    
            table_name,
    
            action,
    
            metadata
    
        ) VALUES (
    
            'maintenance',
    
            'automated_maintenance_completed',
    
            jsonb_build_object(
    
                'timestamp', current_timestamp,
    
                'database_size', pg_database_size(current_database())
    
            )
    
        );
    
    END;
    
    $$;
    
    
    
    -- Schedule maintenance (would typically be done via cron or scheduled job)
    
    -- Example cron entry: 0 2 * * 0 psql -d retail_db -c "SELECT retail.perform_maintenance();"
    
    

    ๐Ÿ’พ Backup and Recovery

    Backup Strategy

    
    #!/bin/bash
    
    # scripts/backup_database.sh
    
    
    
    # Comprehensive backup script for production environments
    
    
    
    set -e
    
    
    
    # Configuration
    
    DB_HOST="${POSTGRES_HOST:-localhost}"
    
    DB_PORT="${POSTGRES_PORT:-5432}"
    
    DB_NAME="${POSTGRES_DB:-retail_db}"
    
    DB_USER="${POSTGRES_USER:-postgres}"
    
    BACKUP_DIR="/backups/postgresql"
    
    RETENTION_DAYS=30
    
    
    
    # Create backup directory
    
    mkdir -p "$BACKUP_DIR"
    
    
    
    # Generate backup filename with timestamp
    
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    
    BACKUP_FILE="$BACKUP_DIR/retail_backup_$TIMESTAMP.sql"
    
    COMPRESSED_BACKUP="$BACKUP_FILE.gz"
    
    
    
    echo "Starting database backup: $TIMESTAMP"
    
    
    
    # Create comprehensive backup
    
    pg_dump \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --dbname="$DB_NAME" \
    
        --verbose \
    
        --clean \
    
        --create \
    
        --if-exists \
    
        --format=custom \
    
        --file="$BACKUP_FILE"
    
    
    
    # Compress backup
    
    gzip "$BACKUP_FILE"
    
    
    
    # Verify backup integrity
    
    echo "Verifying backup integrity..."
    
    pg_restore --list "$COMPRESSED_BACKUP" > /dev/null
    
    
    
    # Clean up old backups
    
    find "$BACKUP_DIR" -name "retail_backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
    
    
    
    # Calculate backup size
    
    BACKUP_SIZE=$(du -h "$COMPRESSED_BACKUP" | cut -f1)
    
    
    
    echo "Backup completed successfully:"
    
    echo "  File: $COMPRESSED_BACKUP"
    
    echo "  Size: $BACKUP_SIZE"
    
    echo "  Timestamp: $TIMESTAMP"
    
    
    
    # Optional: Upload to cloud storage
    
    if [ -n "$AZURE_STORAGE_ACCOUNT" ] && [ -n "$AZURE_STORAGE_KEY" ]; then
    
        echo "Uploading backup to Azure Storage..."
    
        az storage blob upload \
    
            --account-name "$AZURE_STORAGE_ACCOUNT" \
    
            --account-key "$AZURE_STORAGE_KEY" \
    
            --container-name "database-backups" \
    
            --name "retail_backup_$TIMESTAMP.sql.gz" \
    
            --file "$COMPRESSED_BACKUP"
    
    fi
    
    

    Recovery Procedures

    
    #!/bin/bash
    
    # scripts/restore_database.sh
    
    
    
    # Database restoration script
    
    
    
    set -e
    
    
    
    if [ $# -lt 1 ]; then
    
        echo "Usage: $0 <backup_file> [target_database]"
    
        echo "Example: $0 /backups/retail_backup_20241001_120000.sql.gz retail_db_restored"
    
        exit 1
    
    fi
    
    
    
    BACKUP_FILE="$1"
    
    TARGET_DB="${2:-retail_db_restored}"
    
    
    
    # Configuration
    
    DB_HOST="${POSTGRES_HOST:-localhost}"
    
    DB_PORT="${POSTGRES_PORT:-5432}"
    
    DB_USER="${POSTGRES_USER:-postgres}"
    
    
    
    echo "Starting database restoration..."
    
    echo "  Source: $BACKUP_FILE"
    
    echo "  Target: $TARGET_DB"
    
    
    
    # Verify backup file exists
    
    if [ ! -f "$BACKUP_FILE" ]; then
    
        echo "Error: Backup file not found: $BACKUP_FILE"
    
        exit 1
    
    fi
    
    
    
    # Create target database
    
    createdb \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --owner="$DB_USER" \
    
        "$TARGET_DB"
    
    
    
    # Restore from backup
    
    if [[ "$BACKUP_FILE" == *.gz ]]; then
    
        # Compressed backup
    
        gunzip -c "$BACKUP_FILE" | pg_restore \
    
            --host="$DB_HOST" \
    
            --port="$DB_PORT" \
    
            --username="$DB_USER" \
    
            --dbname="$TARGET_DB" \
    
            --verbose \
    
            --clean \
    
            --if-exists
    
    else
    
        # Uncompressed backup
    
        pg_restore \
    
            --host="$DB_HOST" \
    
            --port="$DB_PORT" \
    
            --username="$DB_USER" \
    
            --dbname="$TARGET_DB" \
    
            --verbose \
    
            --clean \
    
            --if-exists \
    
            "$BACKUP_FILE"
    
    fi
    
    
    
    echo "Database restoration completed successfully!"
    
    echo "Restored database: $TARGET_DB"
    
    
    
    # Verify restoration
    
    echo "Verifying restoration..."
    
    TABLES_COUNT=$(psql \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --dbname="$TARGET_DB" \
    
        --tuples-only \
    
        --command="SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'retail';"
    
    )
    
    
    
    echo "Verified $TABLES_COUNT tables in retail schema"
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Multi-Tenant Database Design: Implemented Row Level Security for secure data isolation

    โœ… Vector Search Capabilities: Configured pgvector for semantic product search

    โœ… Comprehensive Schema: Created production-ready retail database schema

    โœ… Sample Data Generation: Built realistic test data for development and testing

    โœ… Performance Optimization: Configured indexes and query optimization

    โœ… Backup and Recovery: Established robust data protection strategies

    ๐Ÿš€ What's Next

    Continue with Lab 05: MCP Server Implementation to:

  • Build the FastMCP server that connects to this database
  • Implement database query tools for the MCP protocol
  • Add semantic search capabilities using the embeddings
  • Configure connection pooling and error handling
  • ๐Ÿ“š Additional Resources

    PostgreSQL & pgvector

  • PostgreSQL Documentation - Complete PostgreSQL reference
  • pgvector Extension - Vector similarity search for PostgreSQL
  • PostgreSQL Performance Tuning - Optimization best practices
  • Multi-Tenant Architecture

  • Row Level Security - PostgreSQL RLS documentation
  • Multi-Tenant Data Architecture - Azure architecture patterns
  • Database Security Best Practices - PostgreSQL security guide
  • Vector Databases

  • Vector Search Fundamentals - Understanding vector databases
  • Embedding Models - OpenAI embeddings documentation
  • HNSW Algorithm - Hierarchical Navigable Small World graphs
  • ---

    Previous: Lab 03: Environment Setup

    Next: Lab 05: MCP Server Implementation

    PostgreSQL setup, retail schema design, and sample data Build

    Database Design and Schema

    ๐ŸŽฏ What This Lab Covers

    This lab dives deep into the PostgreSQL database design for the Zava Retail system. You'll learn to implement a comprehensive retail schema with vector search capabilities, multi-tenant data modeling, and Row Level Security (RLS) for data isolation.

    Overview

    The database is the foundation of our MCP server, storing retail data across multiple stores while maintaining strict data isolation.

    We use PostgreSQL with the pgvector extension to enable semantic search capabilities, allowing customers to find products using natural language queries.

    Our schema follows modern multi-tenant patterns with Row Level Security ensuring users can only access data from their authorized stores. This approach provides enterprise-grade security while maintaining optimal performance.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Design scalable multi-tenant retail database schemas
  • Implement PostgreSQL with pgvector for vector search
  • Configure Row Level Security for data isolation
  • Generate realistic sample data for testing
  • Optimize database performance for retail workloads
  • Implement backup and recovery strategies
  • ๐Ÿ—ƒ๏ธ Database Architecture

    PostgreSQL with pgvector

    Our database leverages PostgreSQL's enterprise features combined with the pgvector extension for AI-powered search:

    
    -- Enable required extensions
    
    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
    
    CREATE EXTENSION IF NOT EXISTS "pgcrypto";
    
    CREATE EXTENSION IF NOT EXISTS "vector";
    
    
    
    -- Verify vector extension installation
    
    SELECT * FROM pg_extension WHERE extname = 'vector';
    
    

    Multi-Tenant Architecture

    The database uses a shared database, shared schema multi-tenancy model with Row Level Security:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                 PostgreSQL                      โ”‚
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  retail Schema (Shared)                        โ”‚
    
    โ”‚  โ”œโ”€โ”€ stores (Master tenant data)               โ”‚
    
    โ”‚  โ”œโ”€โ”€ customers (RLS by store_id)               โ”‚
    
    โ”‚  โ”œโ”€โ”€ products (RLS by store_id)                โ”‚
    
    โ”‚  โ”œโ”€โ”€ sales_transactions (RLS by store_id)      โ”‚
    
    โ”‚  โ”œโ”€โ”€ sales_transaction_items (RLS via join)    โ”‚
    
    โ”‚  โ””โ”€โ”€ product_embeddings (RLS by store_id)      โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ๐Ÿ“Š Core Schema Design

    Stores Table (Tenant Master)

    
    -- Stores table: Master tenant registry
    
    CREATE TABLE retail.stores (
    
        store_id VARCHAR(50) PRIMARY KEY,
    
        store_name VARCHAR(100) NOT NULL,
    
        store_location VARCHAR(100),
    
        store_type VARCHAR(50),
    
        region VARCHAR(50),
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        is_active BOOLEAN DEFAULT TRUE
    
    );
    
    
    
    -- Sample stores data
    
    INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region) VALUES
    
    ('seattle', 'Zava Retail Seattle', 'Seattle, WA', 'flagship', 'west'),
    
    ('redmond', 'Zava Retail Redmond', 'Redmond, WA', 'standard', 'west'),
    
    ('bellevue', 'Zava Retail Bellevue', 'Bellevue, WA', 'standard', 'west'),
    
    ('online', 'Zava Retail Online', 'Digital', 'ecommerce', 'global');
    
    
    
    -- Create index for performance
    
    CREATE INDEX idx_stores_region ON retail.stores(region);
    
    CREATE INDEX idx_stores_active ON retail.stores(is_active) WHERE is_active = TRUE;
    
    

    Customers Table

    
    -- Customers table with RLS
    
    CREATE TABLE retail.customers (
    
        customer_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        first_name VARCHAR(50) NOT NULL,
    
        last_name VARCHAR(50) NOT NULL,
    
        email VARCHAR(100) UNIQUE NOT NULL,
    
        phone VARCHAR(20),
    
        date_of_birth DATE,
    
        gender VARCHAR(20),
    
        customer_since DATE DEFAULT CURRENT_DATE,
    
        loyalty_tier VARCHAR(20) DEFAULT 'bronze',
    
        total_lifetime_value DECIMAL(10,2) DEFAULT 0.00,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Enable RLS
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy: Users can only see customers from their store
    
    CREATE POLICY customers_store_isolation ON retail.customers
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Indexes for performance
    
    CREATE INDEX idx_customers_store_id ON retail.customers(store_id);
    
    CREATE INDEX idx_customers_email ON retail.customers(email);
    
    CREATE INDEX idx_customers_loyalty_tier ON retail.customers(loyalty_tier);
    
    CREATE INDEX idx_customers_created_at ON retail.customers(created_at);
    
    

    Products Table with Categories

    
    -- Product categories
    
    CREATE TABLE retail.product_categories (
    
        category_id SERIAL PRIMARY KEY,
    
        category_name VARCHAR(100) NOT NULL UNIQUE,
    
        parent_category_id INTEGER REFERENCES retail.product_categories(category_id),
    
        description TEXT,
    
        is_active BOOLEAN DEFAULT TRUE
    
    );
    
    
    
    -- Insert sample categories
    
    INSERT INTO retail.product_categories (category_name, description) VALUES
    
    ('Electronics', 'Electronic devices and accessories'),
    
    ('Clothing', 'Apparel and fashion items'),
    
    ('Home & Garden', 'Home improvement and garden supplies'),
    
    ('Sports & Outdoors', 'Sports equipment and outdoor gear'),
    
    ('Books & Media', 'Books, movies, and digital media'),
    
    ('Health & Beauty', 'Health and beauty products'),
    
    ('Automotive', 'Car parts and automotive accessories');
    
    
    
    -- Products table with rich metadata
    
    CREATE TABLE retail.products (
    
        product_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        sku VARCHAR(50) NOT NULL,
    
        product_name VARCHAR(200) NOT NULL,
    
        product_description TEXT,
    
        category_id INTEGER REFERENCES retail.product_categories(category_id),
    
        brand VARCHAR(100),
    
        model VARCHAR(100),
    
        color VARCHAR(50),
    
        size VARCHAR(50),
    
        weight_kg DECIMAL(8,3),
    
        dimensions_cm VARCHAR(50), -- e.g., "30x20x15"
    
        price DECIMAL(10,2) NOT NULL,
    
        cost DECIMAL(10,2),
    
        current_stock INTEGER DEFAULT 0,
    
        minimum_stock INTEGER DEFAULT 0,
    
        maximum_stock INTEGER DEFAULT 1000,
    
        reorder_point INTEGER DEFAULT 10,
    
        supplier_name VARCHAR(100),
    
        supplier_sku VARCHAR(50),
    
        is_active BOOLEAN DEFAULT TRUE,
    
        is_featured BOOLEAN DEFAULT FALSE,
    
        rating_average DECIMAL(3,2) DEFAULT 0.00,
    
        rating_count INTEGER DEFAULT 0,
    
        tags TEXT[], -- Array of tags for flexible categorization
    
        metadata JSONB, -- Flexible metadata storage
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure SKU uniqueness within store
    
        CONSTRAINT unique_sku_per_store UNIQUE (store_id, sku)
    
    );
    
    
    
    -- Enable RLS for products
    
    ALTER TABLE retail.products ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for products
    
    CREATE POLICY products_store_isolation ON retail.products
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Comprehensive indexes
    
    CREATE INDEX idx_products_store_id ON retail.products(store_id);
    
    CREATE INDEX idx_products_sku ON retail.products(sku);
    
    CREATE INDEX idx_products_category ON retail.products(category_id);
    
    CREATE INDEX idx_products_brand ON retail.products(brand);
    
    CREATE INDEX idx_products_price ON retail.products(price);
    
    CREATE INDEX idx_products_stock ON retail.products(current_stock);
    
    CREATE INDEX idx_products_active ON retail.products(is_active) WHERE is_active = TRUE;
    
    CREATE INDEX idx_products_featured ON retail.products(is_featured) WHERE is_featured = TRUE;
    
    CREATE INDEX idx_products_tags ON retail.products USING GIN(tags);
    
    CREATE INDEX idx_products_metadata ON retail.products USING GIN(metadata);
    
    CREATE INDEX idx_products_text_search ON retail.products USING GIN(
    
        to_tsvector('english', product_name || ' ' || COALESCE(product_description, '') || ' ' || COALESCE(brand, ''))
    
    );
    
    

    Sales Transactions

    
    -- Sales transactions table
    
    CREATE TABLE retail.sales_transactions (
    
        transaction_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        customer_id UUID REFERENCES retail.customers(customer_id),
    
        transaction_date TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        transaction_type VARCHAR(20) DEFAULT 'sale', -- 'sale', 'return', 'exchange'
    
        payment_method VARCHAR(50), -- 'cash', 'credit_card', 'debit_card', 'digital_wallet'
    
        subtotal DECIMAL(10,2) NOT NULL,
    
        tax_amount DECIMAL(10,2) DEFAULT 0.00,
    
        discount_amount DECIMAL(10,2) DEFAULT 0.00,
    
        total_amount DECIMAL(10,2) NOT NULL,
    
        cashier_id VARCHAR(50),
    
        register_id VARCHAR(50),
    
        receipt_number VARCHAR(50),
    
        notes TEXT,
    
        metadata JSONB,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Sales transaction items (line items)
    
    CREATE TABLE retail.sales_transaction_items (
    
        item_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        transaction_id UUID NOT NULL REFERENCES retail.sales_transactions(transaction_id) ON DELETE CASCADE,
    
        product_id UUID NOT NULL REFERENCES retail.products(product_id),
    
        quantity INTEGER NOT NULL DEFAULT 1,
    
        unit_price DECIMAL(10,2) NOT NULL,
    
        total_price DECIMAL(10,2) NOT NULL,
    
        discount_amount DECIMAL(10,2) DEFAULT 0.00,
    
        tax_amount DECIMAL(10,2) DEFAULT 0.00,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure positive quantities and prices
    
        CONSTRAINT positive_quantity CHECK (quantity > 0),
    
        CONSTRAINT positive_unit_price CHECK (unit_price >= 0),
    
        CONSTRAINT positive_total_price CHECK (total_price >= 0)
    
    );
    
    
    
    -- Enable RLS for transactions
    
    ALTER TABLE retail.sales_transactions ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for sales transactions
    
    CREATE POLICY sales_transactions_store_isolation ON retail.sales_transactions
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- RLS for transaction items (via join with transactions)
    
    ALTER TABLE retail.sales_transaction_items ENABLE ROW LEVEL SECURITY;
    
    
    
    CREATE POLICY sales_transaction_items_store_isolation ON retail.sales_transaction_items
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        );
    
    
    
    -- Performance indexes
    
    CREATE INDEX idx_sales_transactions_store_id ON retail.sales_transactions(store_id);
    
    CREATE INDEX idx_sales_transactions_customer_id ON retail.sales_transactions(customer_id);
    
    CREATE INDEX idx_sales_transactions_date ON retail.sales_transactions(transaction_date);
    
    CREATE INDEX idx_sales_transactions_type ON retail.sales_transactions(transaction_type);
    
    CREATE INDEX idx_sales_transactions_payment ON retail.sales_transactions(payment_method);
    
    
    
    CREATE INDEX idx_sales_transaction_items_transaction_id ON retail.sales_transaction_items(transaction_id);
    
    CREATE INDEX idx_sales_transaction_items_product_id ON retail.sales_transaction_items(product_id);
    
    

    ๐Ÿ” Vector Search Implementation

    Product Embeddings Table

    
    -- Product embeddings for semantic search
    
    CREATE TABLE retail.product_embeddings (
    
        embedding_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        product_id UUID NOT NULL REFERENCES retail.products(product_id) ON DELETE CASCADE,
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        embedding_text TEXT NOT NULL, -- The text that was embedded
    
        embedding vector(1536), -- OpenAI text-embedding-3-small dimension
    
        embedding_model VARCHAR(100) NOT NULL DEFAULT 'text-embedding-3-small',
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure one embedding per product per model
    
        CONSTRAINT unique_product_embedding UNIQUE (product_id, embedding_model)
    
    );
    
    
    
    -- Enable RLS for embeddings
    
    ALTER TABLE retail.product_embeddings ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for embeddings
    
    CREATE POLICY product_embeddings_store_isolation ON retail.product_embeddings
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Vector similarity index (HNSW for fast approximate search)
    
    CREATE INDEX idx_product_embeddings_vector ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops);
    
    
    
    -- Additional indexes
    
    CREATE INDEX idx_product_embeddings_product_id ON retail.product_embeddings(product_id);
    
    CREATE INDEX idx_product_embeddings_store_id ON retail.product_embeddings(store_id);
    
    CREATE INDEX idx_product_embeddings_model ON retail.product_embeddings(embedding_model);
    
    

    Vector Search Functions

    
    -- Function to search products by similarity
    
    CREATE OR REPLACE FUNCTION retail.search_products_by_similarity(
    
        search_embedding vector(1536),
    
        similarity_threshold float DEFAULT 0.7,
    
        max_results integer DEFAULT 20
    
    )
    
    RETURNS TABLE (
    
        product_id UUID,
    
        product_name VARCHAR(200),
    
        product_description TEXT,
    
        brand VARCHAR(100),
    
        price DECIMAL(10,2),
    
        similarity_score float
    
    ) 
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            p.product_id,
    
            p.product_name,
    
            p.product_description,
    
            p.brand,
    
            p.price,
    
            1 - (pe.embedding <=> search_embedding) as similarity_score
    
        FROM retail.product_embeddings pe
    
        JOIN retail.products p ON pe.product_id = p.product_id
    
        WHERE 
    
            pe.store_id = current_setting('app.current_store_id', true)
    
            AND p.is_active = TRUE
    
            AND 1 - (pe.embedding <=> search_embedding) >= similarity_threshold
    
        ORDER BY pe.embedding <=> search_embedding
    
        LIMIT max_results;
    
    END;
    
    $$;
    
    
    
    -- Grant execute permission
    
    GRANT EXECUTE ON FUNCTION retail.search_products_by_similarity TO mcp_user;
    
    

    ๐Ÿ” Row Level Security Setup

    Database Roles and Permissions

    
    -- Create MCP application role
    
    CREATE ROLE mcp_user LOGIN;
    
    
    
    -- Grant schema usage
    
    GRANT USAGE ON SCHEMA retail TO mcp_user;
    
    
    
    -- Grant table permissions
    
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA retail TO mcp_user;
    
    GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA retail TO mcp_user;
    
    
    
    -- Grant permissions on future tables
    
    ALTER DEFAULT PRIVILEGES IN SCHEMA retail GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO mcp_user;
    
    ALTER DEFAULT PRIVILEGES IN SCHEMA retail GRANT USAGE, SELECT ON SEQUENCES TO mcp_user;
    
    
    
    -- Function to set store context
    
    CREATE OR REPLACE FUNCTION retail.set_store_context(store_id_param VARCHAR(50))
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    BEGIN
    
        -- Verify store exists and user has access
    
        IF NOT EXISTS (SELECT 1 FROM retail.stores WHERE store_id = store_id_param AND is_active = TRUE) THEN
    
            RAISE EXCEPTION 'Invalid or inactive store: %', store_id_param;
    
        END IF;
    
        
    
        -- Set the store context
    
        PERFORM set_config('app.current_store_id', store_id_param, false);
    
        
    
        -- Log the context change
    
        INSERT INTO retail.audit_log (
    
            table_name,
    
            action,
    
            user_name,
    
            store_id,
    
            metadata
    
        ) VALUES (
    
            'security_context',
    
            'store_context_set',
    
            current_user,
    
            store_id_param,
    
            jsonb_build_object('timestamp', current_timestamp)
    
        );
    
    END;
    
    $$;
    
    
    
    -- Grant execute permission
    
    GRANT EXECUTE ON FUNCTION retail.set_store_context TO mcp_user;
    
    

    Audit Logging

    
    -- Audit log table for security and compliance
    
    CREATE TABLE retail.audit_log (
    
        log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        table_name VARCHAR(100) NOT NULL,
    
        action VARCHAR(50) NOT NULL, -- INSERT, UPDATE, DELETE, SELECT
    
        user_name VARCHAR(100) NOT NULL DEFAULT current_user,
    
        store_id VARCHAR(50),
    
        record_id UUID,
    
        old_values JSONB,
    
        new_values JSONB,
    
        metadata JSONB,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Index for audit queries
    
    CREATE INDEX idx_audit_log_table_name ON retail.audit_log(table_name);
    
    CREATE INDEX idx_audit_log_action ON retail.audit_log(action);
    
    CREATE INDEX idx_audit_log_user_name ON retail.audit_log(user_name);
    
    CREATE INDEX idx_audit_log_store_id ON retail.audit_log(store_id);
    
    CREATE INDEX idx_audit_log_created_at ON retail.audit_log(created_at);
    
    
    
    -- Audit trigger function
    
    CREATE OR REPLACE FUNCTION retail.audit_trigger()
    
    RETURNS trigger AS $$
    
    BEGIN
    
        IF TG_OP = 'DELETE' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                old_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(OLD.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(OLD.customer_id, OLD.product_id, OLD.transaction_id),
    
                row_to_json(OLD)
    
            );
    
            RETURN OLD;
    
        ELSIF TG_OP = 'UPDATE' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                old_values,
    
                new_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(NEW.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(NEW.customer_id, NEW.product_id, NEW.transaction_id),
    
                row_to_json(OLD),
    
                row_to_json(NEW)
    
            );
    
            RETURN NEW;
    
        ELSIF TG_OP = 'INSERT' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                new_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(NEW.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(NEW.customer_id, NEW.product_id, NEW.transaction_id),
    
                row_to_json(NEW)
    
            );
    
            RETURN NEW;
    
        END IF;
    
        RETURN NULL;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    
    
    -- Create audit triggers
    
    CREATE TRIGGER customers_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.customers
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    
    
    CREATE TRIGGER products_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.products
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    
    
    CREATE TRIGGER sales_transactions_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.sales_transactions
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    

    ๐Ÿ“Š Sample Data Generation

    Realistic Test Data Script

    
    # scripts/generate_sample_data.py
    
    """
    
    Generate realistic sample data for the Zava Retail database.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import random
    
    import json
    
    from datetime import datetime, timedelta
    
    from faker import Faker
    
    from typing import List, Dict, Any
    
    import numpy as np
    
    
    
    fake = Faker()
    
    
    
    class SampleDataGenerator:
    
        """Generate realistic retail sample data."""
    
        
    
        def __init__(self, connection_string: str):
    
            self.connection_string = connection_string
    
            self.stores = ['seattle', 'redmond', 'bellevue', 'online']
    
            
    
            # Product categories with realistic items
    
            self.product_data = {
    
                'Electronics': {
    
                    'brands': ['Apple', 'Samsung', 'Sony', 'LG', 'HP', 'Dell'],
    
                    'items': [
    
                        'Smartphone', 'Laptop', 'Tablet', 'Headphones', 'Smart TV',
    
                        'Gaming Console', 'Smartwatch', 'Bluetooth Speaker'
    
                    ]
    
                },
    
                'Clothing': {
    
                    'brands': ['Nike', 'Adidas', 'Zara', 'H&M', 'Levi\'s', 'Gap'],
    
                    'items': [
    
                        'T-Shirt', 'Jeans', 'Dress', 'Jacket', 'Sneakers',
    
                        'Sweater', 'Shorts', 'Blouse'
    
                    ]
    
                },
    
                'Home & Garden': {
    
                    'brands': ['IKEA', 'Home Depot', 'Wayfair', 'Target', 'Walmart'],
    
                    'items': [
    
                        'Sofa', 'Dining Table', 'Lamp', 'Garden Tool', 'Plant Pot',
    
                        'Curtains', 'Rug', 'Kitchen Appliance'
    
                    ]
    
                }
    
            }
    
        
    
        async def generate_all_data(self):
    
            """Generate complete sample dataset."""
    
            
    
            conn = await asyncpg.connect(self.connection_string)
    
            
    
            try:
    
                print("๐Ÿช Generating stores data...")
    
                await self._ensure_stores_exist(conn)
    
                
    
                print("๐Ÿ‘ฅ Generating customers...")
    
                customers = await self._generate_customers(conn, 2000)
    
                
    
                print("๐Ÿ“ฆ Generating products...")
    
                products = await self._generate_products(conn, 500)
    
                
    
                print("๐Ÿ›’ Generating sales transactions...")
    
                await self._generate_sales_transactions(conn, customers, products, 5000)
    
                
    
                print("โœ… Sample data generation complete!")
    
                
    
            finally:
    
                await conn.close()
    
        
    
        async def _ensure_stores_exist(self, conn):
    
            """Ensure all stores exist in the database."""
    
            
    
            stores_data = [
    
                ('seattle', 'Zava Retail Seattle', 'Seattle, WA', 'flagship', 'west'),
    
                ('redmond', 'Zava Retail Redmond', 'Redmond, WA', 'standard', 'west'),
    
                ('bellevue', 'Zava Retail Bellevue', 'Bellevue, WA', 'standard', 'west'),
    
                ('online', 'Zava Retail Online', 'Digital', 'ecommerce', 'global')
    
            ]
    
            
    
            for store_data in stores_data:
    
                await conn.execute("""
    
                    INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region)
    
                    VALUES ($1, $2, $3, $4, $5)
    
                    ON CONFLICT (store_id) DO NOTHING
    
                """, *store_data)
    
        
    
        async def _generate_customers(self, conn, count: int) -> List[Dict]:
    
            """Generate realistic customer data."""
    
            
    
            customers = []
    
            
    
            for _ in range(count):
    
                store_id = random.choice(self.stores)
    
                customer_data = {
    
                    'store_id': store_id,
    
                    'first_name': fake.first_name(),
    
                    'last_name': fake.last_name(),
    
                    'email': fake.unique.email(),
    
                    'phone': fake.phone_number()[:20],
    
                    'date_of_birth': fake.date_of_birth(minimum_age=18, maximum_age=80),
    
                    'gender': random.choice(['Male', 'Female', 'Other', 'Prefer not to say']),
    
                    'customer_since': fake.date_between(start_date='-5y', end_date='today'),
    
                    'loyalty_tier': random.choices(
    
                        ['bronze', 'silver', 'gold', 'platinum'],
    
                        weights=[50, 30, 15, 5]
    
                    )[0]
    
                }
    
                
    
                customer_id = await conn.fetchval("""
    
                    INSERT INTO retail.customers (
    
                        store_id, first_name, last_name, email, phone,
    
                        date_of_birth, gender, customer_since, loyalty_tier
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
    
                    RETURNING customer_id
    
                """, *customer_data.values())
    
                
    
                customer_data['customer_id'] = customer_id
    
                customers.append(customer_data)
    
            
    
            return customers
    
        
    
        async def _generate_products(self, conn, count: int) -> List[Dict]:
    
            """Generate realistic product data."""
    
            
    
            # Get category IDs
    
            categories = await conn.fetch("SELECT category_id, category_name FROM retail.product_categories")
    
            category_map = {cat['category_name']: cat['category_id'] for cat in categories}
    
            
    
            products = []
    
            
    
            for _ in range(count):
    
                store_id = random.choice(self.stores)
    
                category_name = random.choice(list(self.product_data.keys()))
    
                category_id = category_map.get(category_name)
    
                
    
                if not category_id:
    
                    continue
    
                
    
                brand = random.choice(self.product_data[category_name]['brands'])
    
                item_type = random.choice(self.product_data[category_name]['items'])
    
                
    
                # Generate realistic pricing
    
                base_price = random.uniform(10, 1000)
    
                cost = base_price * random.uniform(0.4, 0.7)  # 40-70% cost margin
    
                
    
                product_data = {
    
                    'store_id': store_id,
    
                    'sku': f"{brand[:3].upper()}-{fake.unique.random_number(digits=6)}",
    
                    'product_name': f"{brand} {item_type}",
    
                    'product_description': fake.text(max_nb_chars=500),
    
                    'category_id': category_id,
    
                    'brand': brand,
    
                    'model': f"Model {fake.random_number(digits=4)}",
    
                    'color': fake.color_name(),
    
                    'size': random.choice(['XS', 'S', 'M', 'L', 'XL', 'XXL', 'One Size']),
    
                    'weight_kg': round(random.uniform(0.1, 10.0), 2),
    
                    'price': round(base_price, 2),
    
                    'cost': round(cost, 2),
    
                    'current_stock': random.randint(0, 100),
    
                    'minimum_stock': random.randint(5, 20),
    
                    'reorder_point': random.randint(10, 30),
    
                    'supplier_name': fake.company(),
    
                    'is_featured': random.choice([True, False]),
    
                    'rating_average': round(random.uniform(3.0, 5.0), 2),
    
                    'rating_count': random.randint(0, 500),
    
                    'tags': random.sample([
    
                        'popular', 'new', 'sale', 'limited', 'bestseller', 
    
                        'eco-friendly', 'premium', 'budget'
    
                    ], k=random.randint(1, 3))
    
                }
    
                
    
                product_id = await conn.fetchval("""
    
                    INSERT INTO retail.products (
    
                        store_id, sku, product_name, product_description, category_id,
    
                        brand, model, color, size, weight_kg, price, cost,
    
                        current_stock, minimum_stock, reorder_point, supplier_name,
    
                        is_featured, rating_average, rating_count, tags
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
    
                    RETURNING product_id
    
                """, *product_data.values())
    
                
    
                product_data['product_id'] = product_id
    
                products.append(product_data)
    
            
    
            return products
    
        
    
        async def _generate_sales_transactions(self, conn, customers: List[Dict], products: List[Dict], count: int):
    
            """Generate realistic sales transaction data."""
    
            
    
            for _ in range(count):
    
                # Select customer and matching store products
    
                customer = random.choice(customers)
    
                store_products = [p for p in products if p['store_id'] == customer['store_id']]
    
                
    
                if not store_products:
    
                    continue
    
                
    
                # Generate transaction basics
    
                transaction_date = fake.date_time_between(start_date='-1y', end_date='now')
    
                transaction_type = random.choices(
    
                    ['sale', 'return', 'exchange'],
    
                    weights=[90, 7, 3]
    
                )[0]
    
                
    
                payment_method = random.choices(
    
                    ['credit_card', 'debit_card', 'cash', 'digital_wallet'],
    
                    weights=[45, 25, 20, 10]
    
                )[0]
    
                
    
                # Generate transaction items (1-5 items per transaction)
    
                num_items = random.choices([1, 2, 3, 4, 5], weights=[40, 30, 20, 7, 3])[0]
    
                selected_products = random.sample(store_products, min(num_items, len(store_products)))
    
                
    
                subtotal = 0
    
                transaction_items = []
    
                
    
                for product in selected_products:
    
                    quantity = random.randint(1, 3)
    
                    unit_price = product['price']
    
                    
    
                    # Apply random discounts occasionally
    
                    discount_amount = 0
    
                    if random.random() < 0.2:  # 20% chance of discount
    
                        discount_amount = unit_price * quantity * random.uniform(0.05, 0.25)
    
                    
    
                    total_price = (unit_price * quantity) - discount_amount
    
                    subtotal += total_price
    
                    
    
                    transaction_items.append({
    
                        'product_id': product['product_id'],
    
                        'quantity': quantity,
    
                        'unit_price': unit_price,
    
                        'total_price': total_price,
    
                        'discount_amount': discount_amount
    
                    })
    
                
    
                # Calculate totals
    
                discount_amount = sum(item['discount_amount'] for item in transaction_items)
    
                tax_amount = subtotal * 0.08  # 8% tax rate
    
                total_amount = subtotal + tax_amount
    
                
    
                # Insert transaction
    
                transaction_id = await conn.fetchval("""
    
                    INSERT INTO retail.sales_transactions (
    
                        store_id, customer_id, transaction_date, transaction_type,
    
                        payment_method, subtotal, tax_amount, discount_amount, total_amount,
    
                        cashier_id, register_id, receipt_number
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
    
                    RETURNING transaction_id
    
                """, 
    
                    customer['store_id'], customer['customer_id'], transaction_date,
    
                    transaction_type, payment_method, subtotal, tax_amount,
    
                    discount_amount, total_amount, f"CASHIER{random.randint(1, 10)}",
    
                    f"REG{random.randint(1, 5)}", f"RCP{fake.random_number(digits=8)}"
    
                )
    
                
    
                # Insert transaction items
    
                for item in transaction_items:
    
                    await conn.execute("""
    
                        INSERT INTO retail.sales_transaction_items (
    
                            transaction_id, product_id, quantity, unit_price,
    
                            total_price, discount_amount
    
                        ) VALUES ($1, $2, $3, $4, $5, $6)
    
                    """, 
    
                        transaction_id, item['product_id'], item['quantity'],
    
                        item['unit_price'], item['total_price'], item['discount_amount']
    
                    )
    
    
    
    # Usage example
    
    if __name__ == "__main__":
    
        import os
    
        from config import Config
    
        
    
        config = Config()
    
        generator = SampleDataGenerator(config.database.connection_string)
    
        
    
        asyncio.run(generator.generate_all_data())
    
    

    ๐Ÿš€ Performance Optimization

    Database Configuration

    
    -- Performance-oriented PostgreSQL settings
    
    -- Add to postgresql.conf
    
    
    
    # Memory settings
    
    shared_buffers = '256MB'                # 25% of RAM for dedicated DB server
    
    effective_cache_size = '1GB'           # Estimate of OS cache size
    
    work_mem = '4MB'                       # Memory for sorts and hash joins
    
    maintenance_work_mem = '64MB'          # Memory for VACUUM, CREATE INDEX
    
    
    
    # Connection settings
    
    max_connections = 100                  # Adjust based on application needs
    
    
    
    # Write-ahead logging
    
    wal_buffers = '16MB'
    
    checkpoint_segments = 32               # PostgreSQL < 9.5
    
    max_wal_size = '1GB'                   # PostgreSQL >= 9.5
    
    
    
    # Query planner
    
    random_page_cost = 1.1                 # SSD-optimized
    
    effective_io_concurrency = 200         # SSD concurrent I/O capability
    
    
    
    # Logging for performance monitoring
    
    log_min_duration_statement = 1000      # Log queries > 1 second
    
    log_checkpoints = on
    
    log_connections = on
    
    log_disconnections = on
    
    log_line_prefix = '%t [%p-%l] %q%u@%d '
    
    

    Query Optimization Views

    
    -- Create monitoring views for query performance
    
    CREATE VIEW retail.slow_queries AS
    
    SELECT 
    
        query,
    
        calls,
    
        total_exec_time,
    
        mean_exec_time,
    
        max_exec_time,
    
        stddev_exec_time,
    
        rows,
    
        100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
    
    FROM pg_stat_statements
    
    WHERE mean_exec_time > 100  -- Queries taking more than 100ms on average
    
    ORDER BY mean_exec_time DESC;
    
    
    
    -- Table sizes and index usage
    
    CREATE VIEW retail.table_stats AS
    
    SELECT
    
        schemaname,
    
        tablename,
    
        pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
    
        pg_stat_get_tuples_inserted(c.oid) as inserts,
    
        pg_stat_get_tuples_updated(c.oid) as updates,
    
        pg_stat_get_tuples_deleted(c.oid) as deletes,
    
        pg_stat_get_live_tuples(c.oid) as live_tuples,
    
        pg_stat_get_dead_tuples(c.oid) as dead_tuples
    
    FROM pg_tables pt
    
    JOIN pg_class c ON c.relname = pt.tablename
    
    WHERE schemaname = 'retail'
    
    ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
    
    
    
    -- Index usage statistics
    
    CREATE VIEW retail.index_usage AS
    
    SELECT
    
        schemaname,
    
        tablename,
    
        indexname,
    
        idx_tup_read,
    
        idx_tup_fetch,
    
        pg_size_pretty(pg_relation_size(indexrelname)) as size
    
    FROM pg_stat_user_indexes
    
    WHERE schemaname = 'retail'
    
    ORDER BY idx_tup_read DESC;
    
    

    Automated Maintenance

    
    -- Create function for automated maintenance
    
    CREATE OR REPLACE FUNCTION retail.perform_maintenance()
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    AS $$
    
    BEGIN
    
        -- Update table statistics
    
        ANALYZE retail.customers;
    
        ANALYZE retail.products;
    
        ANALYZE retail.sales_transactions;
    
        ANALYZE retail.sales_transaction_items;
    
        ANALYZE retail.product_embeddings;
    
        
    
        -- Vacuum tables with high update/delete activity
    
        VACUUM (ANALYZE, VERBOSE) retail.customers;
    
        VACUUM (ANALYZE, VERBOSE) retail.products;
    
        
    
        -- Reindex if needed (check for index bloat)
    
        REINDEX INDEX CONCURRENTLY idx_products_text_search;
    
        REINDEX INDEX CONCURRENTLY idx_product_embeddings_vector;
    
        
    
        -- Log maintenance completion
    
        INSERT INTO retail.audit_log (
    
            table_name,
    
            action,
    
            metadata
    
        ) VALUES (
    
            'maintenance',
    
            'automated_maintenance_completed',
    
            jsonb_build_object(
    
                'timestamp', current_timestamp,
    
                'database_size', pg_database_size(current_database())
    
            )
    
        );
    
    END;
    
    $$;
    
    
    
    -- Schedule maintenance (would typically be done via cron or scheduled job)
    
    -- Example cron entry: 0 2 * * 0 psql -d retail_db -c "SELECT retail.perform_maintenance();"
    
    

    ๐Ÿ’พ Backup and Recovery

    Backup Strategy

    
    #!/bin/bash
    
    # scripts/backup_database.sh
    
    
    
    # Comprehensive backup script for production environments
    
    
    
    set -e
    
    
    
    # Configuration
    
    DB_HOST="${POSTGRES_HOST:-localhost}"
    
    DB_PORT="${POSTGRES_PORT:-5432}"
    
    DB_NAME="${POSTGRES_DB:-retail_db}"
    
    DB_USER="${POSTGRES_USER:-postgres}"
    
    BACKUP_DIR="/backups/postgresql"
    
    RETENTION_DAYS=30
    
    
    
    # Create backup directory
    
    mkdir -p "$BACKUP_DIR"
    
    
    
    # Generate backup filename with timestamp
    
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    
    BACKUP_FILE="$BACKUP_DIR/retail_backup_$TIMESTAMP.sql"
    
    COMPRESSED_BACKUP="$BACKUP_FILE.gz"
    
    
    
    echo "Starting database backup: $TIMESTAMP"
    
    
    
    # Create comprehensive backup
    
    pg_dump \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --dbname="$DB_NAME" \
    
        --verbose \
    
        --clean \
    
        --create \
    
        --if-exists \
    
        --format=custom \
    
        --file="$BACKUP_FILE"
    
    
    
    # Compress backup
    
    gzip "$BACKUP_FILE"
    
    
    
    # Verify backup integrity
    
    echo "Verifying backup integrity..."
    
    pg_restore --list "$COMPRESSED_BACKUP" > /dev/null
    
    
    
    # Clean up old backups
    
    find "$BACKUP_DIR" -name "retail_backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
    
    
    
    # Calculate backup size
    
    BACKUP_SIZE=$(du -h "$COMPRESSED_BACKUP" | cut -f1)
    
    
    
    echo "Backup completed successfully:"
    
    echo "  File: $COMPRESSED_BACKUP"
    
    echo "  Size: $BACKUP_SIZE"
    
    echo "  Timestamp: $TIMESTAMP"
    
    
    
    # Optional: Upload to cloud storage
    
    if [ -n "$AZURE_STORAGE_ACCOUNT" ] && [ -n "$AZURE_STORAGE_KEY" ]; then
    
        echo "Uploading backup to Azure Storage..."
    
        az storage blob upload \
    
            --account-name "$AZURE_STORAGE_ACCOUNT" \
    
            --account-key "$AZURE_STORAGE_KEY" \
    
            --container-name "database-backups" \
    
            --name "retail_backup_$TIMESTAMP.sql.gz" \
    
            --file "$COMPRESSED_BACKUP"
    
    fi
    
    

    Recovery Procedures

    
    #!/bin/bash
    
    # scripts/restore_database.sh
    
    
    
    # Database restoration script
    
    
    
    set -e
    
    
    
    if [ $# -lt 1 ]; then
    
        echo "Usage: $0 <backup_file> [target_database]"
    
        echo "Example: $0 /backups/retail_backup_20241001_120000.sql.gz retail_db_restored"
    
        exit 1
    
    fi
    
    
    
    BACKUP_FILE="$1"
    
    TARGET_DB="${2:-retail_db_restored}"
    
    
    
    # Configuration
    
    DB_HOST="${POSTGRES_HOST:-localhost}"
    
    DB_PORT="${POSTGRES_PORT:-5432}"
    
    DB_USER="${POSTGRES_USER:-postgres}"
    
    
    
    echo "Starting database restoration..."
    
    echo "  Source: $BACKUP_FILE"
    
    echo "  Target: $TARGET_DB"
    
    
    
    # Verify backup file exists
    
    if [ ! -f "$BACKUP_FILE" ]; then
    
        echo "Error: Backup file not found: $BACKUP_FILE"
    
        exit 1
    
    fi
    
    
    
    # Create target database
    
    createdb \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --owner="$DB_USER" \
    
        "$TARGET_DB"
    
    
    
    # Restore from backup
    
    if [[ "$BACKUP_FILE" == *.gz ]]; then
    
        # Compressed backup
    
        gunzip -c "$BACKUP_FILE" | pg_restore \
    
            --host="$DB_HOST" \
    
            --port="$DB_PORT" \
    
            --username="$DB_USER" \
    
            --dbname="$TARGET_DB" \
    
            --verbose \
    
            --clean \
    
            --if-exists
    
    else
    
        # Uncompressed backup
    
        pg_restore \
    
            --host="$DB_HOST" \
    
            --port="$DB_PORT" \
    
            --username="$DB_USER" \
    
            --dbname="$TARGET_DB" \
    
            --verbose \
    
            --clean \
    
            --if-exists \
    
            "$BACKUP_FILE"
    
    fi
    
    
    
    echo "Database restoration completed successfully!"
    
    echo "Restored database: $TARGET_DB"
    
    
    
    # Verify restoration
    
    echo "Verifying restoration..."
    
    TABLES_COUNT=$(psql \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --dbname="$TARGET_DB" \
    
        --tuples-only \
    
        --command="SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'retail';"
    
    )
    
    
    
    echo "Verified $TABLES_COUNT tables in retail schema"
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Multi-Tenant Database Design: Implemented Row Level Security for secure data isolation

    โœ… Vector Search Capabilities: Configured pgvector for semantic product search

    โœ… Comprehensive Schema: Created production-ready retail database schema

    โœ… Sample Data Generation: Built realistic test data for development and testing

    โœ… Performance Optimization: Configured indexes and query optimization

    โœ… Backup and Recovery: Established robust data protection strategies

    ๐Ÿš€ What's Next

    Continue with Lab 05: MCP Server Implementation to:

  • Build the FastMCP server that connects to this database
  • Implement database query tools for the MCP protocol
  • Add semantic search capabilities using the embeddings
  • Configure connection pooling and error handling
  • ๐Ÿ“š Additional Resources

    PostgreSQL & pgvector

  • PostgreSQL Documentation - Complete PostgreSQL reference
  • pgvector Extension - Vector similarity search for PostgreSQL
  • PostgreSQL Performance Tuning - Optimization best practices
  • Multi-Tenant Architecture

  • Row Level Security - PostgreSQL RLS documentation
  • Multi-Tenant Data Architecture - Azure architecture patterns
  • Database Security Best Practices - PostgreSQL security guide
  • Vector Databases

  • Vector Search Fundamentals - Understanding vector databases
  • Embedding Models - OpenAI embeddings documentation
  • HNSW Algorithm - Hierarchical Navigable Small World graphs
  • ---

    Previous: Lab 03: Environment Setup

    Next: Lab 05: MCP Server Implementation

    05 MCP Server Implementation

    MCP Server Implementation

    ๐ŸŽฏ What This Lab Covers

    This hands-on lab guides you through implementing a production-ready MCP server using FastMCP framework.

    You'll build the core server structure, implement database integration, create tools for data access, and establish the foundation for AI-powered retail analytics.

    Overview

    The MCP server is the heart of our retail analytics solution. It acts as a bridge between AI assistants and the PostgreSQL database, providing secure, intelligent access to business data through a standardized protocol.

    This lab teaches you to build a robust, scalable MCP server following enterprise patterns and best practices.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Build a FastMCP server with proper architecture and organization
  • Implement database integration with connection pooling and error handling
  • Create MCP tools for database schema introspection and query execution
  • Configure Row Level Security context management
  • Add health monitoring and observability features
  • Test your MCP server implementation locally and with VS Code
  • ๐Ÿ“ Project Structure

    Let's examine the MCP server organization:

    
    mcp_server/
    
    โ”œโ”€โ”€ __init__.py                 # Package initialization
    
    โ”œโ”€โ”€ config.py                   # Configuration management
    
    โ”œโ”€โ”€ health_check.py             # Health monitoring endpoints
    
    โ”œโ”€โ”€ sales_analysis.py           # Main MCP server implementation
    
    โ”œโ”€โ”€ sales_analysis_postgres.py  # Database integration layer
    
    โ””โ”€โ”€ sales_analysis_text_embeddings.py  # AI/semantic search integration
    
    

    ๐Ÿ”ง Configuration Management

    Environment Configuration (config.py)

    First, let's create a robust configuration system:

    
    # mcp_server/config.py
    
    """
    
    Configuration management for the MCP server.
    
    Handles environment variables, validation, and defaults.
    
    """
    
    import os
    
    import logging
    
    from typing import Optional, Dict, Any
    
    from dataclasses import dataclass
    
    from dotenv import load_dotenv
    
    
    
    # Load environment variables from .env file
    
    load_dotenv()
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    @dataclass
    
    class DatabaseConfig:
    
        """Database connection configuration."""
    
        host: str
    
        port: int
    
        database: str
    
        user: str
    
        password: str
    
        min_connections: int = 2
    
        max_connections: int = 10
    
        command_timeout: int = 30
    
        
    
        @classmethod
    
        def from_env(cls) -> 'DatabaseConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                host=os.getenv('POSTGRES_HOST', 'localhost'),
    
                port=int(os.getenv('POSTGRES_PORT', '5432')),
    
                database=os.getenv('POSTGRES_DB', 'zava'),
    
                user=os.getenv('POSTGRES_USER', 'postgres'),
    
                password=os.getenv('POSTGRES_PASSWORD', ''),
    
                min_connections=int(os.getenv('POSTGRES_MIN_CONNECTIONS', '2')),
    
                max_connections=int(os.getenv('POSTGRES_MAX_CONNECTIONS', '10')),
    
                command_timeout=int(os.getenv('POSTGRES_COMMAND_TIMEOUT', '30'))
    
            )
    
        
    
        def to_asyncpg_params(self) -> Dict[str, Any]:
    
            """Convert to asyncpg connection parameters."""
    
            return {
    
                'host': self.host,
    
                'port': self.port,
    
                'database': self.database,
    
                'user': self.user,
    
                'password': self.password,
    
                'command_timeout': self.command_timeout,
    
                'server_settings': {
    
                    'application_name': 'zava-mcp-server',
    
                    'jit': 'off',  # Disable JIT for stability
    
                    'work_mem': '4MB',
    
                    'statement_timeout': f'{self.command_timeout}s'
    
                }
    
            }
    
    
    
    @dataclass
    
    class AzureConfig:
    
        """Azure AI services configuration."""
    
        project_endpoint: str
    
        openai_endpoint: str
    
        embedding_model_deployment: str
    
        client_id: str
    
        client_secret: str
    
        tenant_id: str
    
        
    
        @classmethod
    
        def from_env(cls) -> 'AzureConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                project_endpoint=os.getenv('PROJECT_ENDPOINT', ''),
    
                openai_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
    
                embedding_model_deployment=os.getenv('EMBEDDING_MODEL_DEPLOYMENT_NAME', 'text-embedding-3-small'),
    
                client_id=os.getenv('AZURE_CLIENT_ID', ''),
    
                client_secret=os.getenv('AZURE_CLIENT_SECRET', ''),
    
                tenant_id=os.getenv('AZURE_TENANT_ID', '')
    
            )
    
        
    
        def is_configured(self) -> bool:
    
            """Check if all required Azure configuration is present."""
    
            return all([
    
                self.project_endpoint,
    
                self.openai_endpoint,
    
                self.client_id,
    
                self.client_secret,
    
                self.tenant_id
    
            ])
    
    
    
    @dataclass
    
    class ServerConfig:
    
        """MCP server configuration."""
    
        host: str = '0.0.0.0'
    
        port: int = 8000
    
        log_level: str = 'INFO'
    
        enable_cors: bool = True
    
        enable_health_check: bool = True
    
        applicationinsights_connection_string: Optional[str] = None
    
        
    
        @classmethod
    
        def from_env(cls) -> 'ServerConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                host=os.getenv('MCP_SERVER_HOST', '0.0.0.0'),
    
                port=int(os.getenv('MCP_SERVER_PORT', '8000')),
    
                log_level=os.getenv('LOG_LEVEL', 'INFO').upper(),
    
                enable_cors=os.getenv('ENABLE_CORS', 'true').lower() == 'true',
    
                enable_health_check=os.getenv('ENABLE_HEALTH_CHECK', 'true').lower() == 'true',
    
                applicationinsights_connection_string=os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING')
    
            )
    
    
    
    class MCPServerConfig:
    
        """Main configuration class for the MCP server."""
    
        
    
        def __init__(self):
    
            self.database = DatabaseConfig.from_env()
    
            self.azure = AzureConfig.from_env()
    
            self.server = ServerConfig.from_env()
    
            
    
            # Validate configuration
    
            self._validate_config()
    
        
    
        def _validate_config(self):
    
            """Validate configuration and log warnings for missing values."""
    
            if not self.database.password:
    
                logger.warning("Database password is empty. This may cause connection issues.")
    
            
    
            if not self.azure.is_configured():
    
                logger.warning("Azure configuration is incomplete. AI features may not work.")
    
            
    
            logger.info(f"Configuration loaded - Database: {self.database.host}:{self.database.port}")
    
            logger.info(f"Server will run on {self.server.host}:{self.server.port}")
    
    
    
    # Global configuration instance
    
    config = MCPServerConfig()
    
    

    Key Configuration Features

  • Environment Variable Loading: Automatic .env file support
  • Type Safety: Dataclass validation and type hints
  • Flexible Defaults: Sensible defaults for development
  • Validation: Configuration validation with helpful error messages
  • Security: Sensitive values only from environment variables
  • ๐Ÿ—„๏ธ Database Integration Layer

    PostgreSQL Provider (sales_analysis_postgres.py)

    Let's implement the database integration layer:

    
    # mcp_server/sales_analysis_postgres.py
    
    """
    
    PostgreSQL database integration for MCP server.
    
    Handles connections, queries, and schema introspection.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import logging
    
    from typing import Dict, Any, List, Optional, Tuple
    
    from contextlib import asynccontextmanager
    
    from datetime import datetime
    
    import json
    
    
    
    from .config import config
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class PostgreSQLSchemaProvider:
    
        """Provides PostgreSQL database access and schema information."""
    
        
    
        def __init__(self):
    
            self.connection_pool: Optional[asyncpg.Pool] = None
    
            self.postgres_config = config.database.to_asyncpg_params()
    
            
    
        async def create_pool(self) -> None:
    
            """Create connection pool for database operations."""
    
            if self.connection_pool is None:
    
                try:
    
                    self.connection_pool = await asyncpg.create_pool(
    
                        **self.postgres_config,
    
                        min_size=config.database.min_connections,
    
                        max_size=config.database.max_connections,
    
                        max_inactive_connection_lifetime=300  # 5 minutes
    
                    )
    
                    logger.info("Database connection pool created successfully")
    
                except Exception as e:
    
                    logger.error(f"Failed to create database connection pool: {e}")
    
                    raise
    
        
    
        async def close_pool(self) -> None:
    
            """Close the connection pool."""
    
            if self.connection_pool:
    
                await self.connection_pool.close()
    
                self.connection_pool = None
    
                logger.info("Database connection pool closed")
    
        
    
        @asynccontextmanager
    
        async def get_connection(self):
    
            """Get a database connection from the pool."""
    
            if not self.connection_pool:
    
                await self.create_pool()
    
            
    
            async with self.connection_pool.acquire() as connection:
    
                yield connection
    
        
    
        async def set_rls_context(self, connection: asyncpg.Connection, rls_user_id: str) -> None:
    
            """Set Row Level Security context for the connection."""
    
            try:
    
                await connection.execute(
    
                    "SELECT set_config('app.current_rls_user_id', $1, false)",
    
                    rls_user_id
    
                )
    
                logger.debug(f"RLS context set for user: {rls_user_id}")
    
            except Exception as e:
    
                logger.error(f"Failed to set RLS context: {e}")
    
                raise
    
        
    
        async def get_table_schema(self, table_name: str, rls_user_id: str) -> Dict[str, Any]:
    
            """Get detailed schema information for a specific table."""
    
            async with self.get_connection() as conn:
    
                await self.set_rls_context(conn, rls_user_id)
    
                
    
                # Parse schema and table name
    
                if '.' in table_name:
    
                    schema_name, table_name = table_name.split('.', 1)
    
                else:
    
                    schema_name = 'retail'  # Default schema
    
                
    
                # Get column information
    
                columns_query = """
    
                    SELECT 
    
                        column_name,
    
                        data_type,
    
                        is_nullable,
    
                        column_default,
    
                        character_maximum_length,
    
                        numeric_precision,
    
                        numeric_scale,
    
                        ordinal_position
    
                    FROM information_schema.columns 
    
                    WHERE table_schema = $1 AND table_name = $2
    
                    ORDER BY ordinal_position
    
                """
    
                
    
                columns = await conn.fetch(columns_query, schema_name, table_name)
    
                
    
                if not columns:
    
                    raise ValueError(f"Table {schema_name}.{table_name} not found or not accessible")
    
                
    
                # Get foreign key relationships
    
                fk_query = """
    
                    SELECT 
    
                        kcu.column_name,
    
                        ccu.table_schema AS foreign_table_schema,
    
                        ccu.table_name AS foreign_table_name,
    
                        ccu.column_name AS foreign_column_name
    
                    FROM information_schema.table_constraints tc
    
                    JOIN information_schema.key_column_usage kcu 
    
                        ON tc.constraint_name = kcu.constraint_name
    
                    JOIN information_schema.constraint_column_usage ccu 
    
                        ON ccu.constraint_name = tc.constraint_name
    
                    WHERE tc.constraint_type = 'FOREIGN KEY' 
    
                        AND tc.table_schema = $1 
    
                        AND tc.table_name = $2
    
                """
    
                
    
                foreign_keys = await conn.fetch(fk_query, schema_name, table_name)
    
                
    
                # Get indexes
    
                index_query = """
    
                    SELECT 
    
                        indexname,
    
                        indexdef
    
                    FROM pg_indexes 
    
                    WHERE schemaname = $1 AND tablename = $2
    
                """
    
                
    
                indexes = await conn.fetch(index_query, schema_name, table_name)
    
                
    
                # Format schema information
    
                schema_info = {
    
                    "table_name": f"{schema_name}.{table_name}",
    
                    "columns": [
    
                        {
    
                            "name": col["column_name"],
    
                            "type": col["data_type"],
    
                            "nullable": col["is_nullable"] == "YES",
    
                            "default": col["column_default"],
    
                            "max_length": col["character_maximum_length"],
    
                            "precision": col["numeric_precision"],
    
                            "scale": col["numeric_scale"],
    
                            "position": col["ordinal_position"]
    
                        }
    
                        for col in columns
    
                    ],
    
                    "foreign_keys": [
    
                        {
    
                            "column": fk["column_name"],
    
                            "references": f"{fk['foreign_table_schema']}.{fk['foreign_table_name']}.{fk['foreign_column_name']}"
    
                        }
    
                        for fk in foreign_keys
    
                    ],
    
                    "indexes": [
    
                        {
    
                            "name": idx["indexname"],
    
                            "definition": idx["indexdef"]
    
                        }
    
                        for idx in indexes
    
                    ]
    
                }
    
                
    
                return schema_info
    
        
    
        async def get_multiple_table_schemas(
    
            self, 
    
            table_names: List[str], 
    
            rls_user_id: str
    
        ) -> str:
    
            """Get schema information for multiple tables."""
    
            schemas = []
    
            
    
            for table_name in table_names:
    
                try:
    
                    schema = await self.get_table_schema(table_name, rls_user_id)
    
                    schemas.append(self._format_schema_for_ai(schema))
    
                except Exception as e:
    
                    logger.warning(f"Failed to get schema for {table_name}: {e}")
    
                    schemas.append(f"Error retrieving schema for {table_name}: {str(e)}")
    
            
    
            return "\n\n".join(schemas)
    
        
    
        def _format_schema_for_ai(self, schema: Dict[str, Any]) -> str:
    
            """Format schema information for AI consumption."""
    
            table_name = schema["table_name"]
    
            columns = schema["columns"]
    
            foreign_keys = schema["foreign_keys"]
    
            
    
            # Create column definitions
    
            column_lines = []
    
            for col in columns:
    
                nullable = "NULL" if col["nullable"] else "NOT NULL"
    
                type_info = col["type"]
    
                
    
                if col["max_length"]:
    
                    type_info += f"({col['max_length']})"
    
                elif col["precision"] and col["scale"]:
    
                    type_info += f"({col['precision']},{col['scale']})"
    
                
    
                default_info = f" DEFAULT {col['default']}" if col["default"] else ""
    
                
    
                column_lines.append(f"  {col['name']} {type_info} {nullable}{default_info}")
    
            
    
            # Create foreign key information
    
            fk_lines = []
    
            for fk in foreign_keys:
    
                fk_lines.append(f"  {fk['column']} -> {fk['references']}")
    
            
    
            # Combine into readable format
    
            schema_text = f"Table: {table_name}\n"
    
            schema_text += "Columns:\n" + "\n".join(column_lines)
    
            
    
            if fk_lines:
    
                schema_text += "\n\nForeign Keys:\n" + "\n".join(fk_lines)
    
            
    
            return schema_text
    
        
    
        async def execute_query(
    
            self, 
    
            sql_query: str, 
    
            rls_user_id: str,
    
            max_rows: int = 20
    
        ) -> str:
    
            """Execute a SQL query with Row Level Security context."""
    
            async with self.get_connection() as conn:
    
                await self.set_rls_context(conn, rls_user_id)
    
                
    
                try:
    
                    # Set a query timeout
    
                    rows = await asyncio.wait_for(
    
                        conn.fetch(sql_query),
    
                        timeout=config.database.command_timeout
    
                    )
    
                    
    
                    if not rows:
    
                        return "Query executed successfully. No rows returned."
    
                    
    
                    # Limit result set size
    
                    limited_rows = rows[:max_rows]
    
                    
    
                    # Format results
    
                    result = self._format_query_results(limited_rows, len(rows), max_rows)
    
                    
    
                    logger.info(f"Query executed successfully. Returned {len(limited_rows)} rows.")
    
                    return result
    
                    
    
                except asyncio.TimeoutError:
    
                    error_msg = f"Query timeout after {config.database.command_timeout} seconds"
    
                    logger.error(error_msg)
    
                    raise Exception(error_msg)
    
                except Exception as e:
    
                    logger.error(f"Query execution failed: {e}")
    
                    raise
    
        
    
        def _format_query_results(
    
            self, 
    
            rows: List[asyncpg.Record], 
    
            total_rows: int,
    
            max_rows: int
    
        ) -> str:
    
            """Format query results for AI consumption."""
    
            if not rows:
    
                return "No results found."
    
            
    
            # Get column names
    
            columns = list(rows[0].keys())
    
            
    
            # Create header
    
            result_lines = [f"Results ({len(rows)} of {total_rows} rows):"]
    
            result_lines.append("=" * 50)
    
            
    
            # Add column headers
    
            header = " | ".join(columns)
    
            result_lines.append(header)
    
            result_lines.append("-" * len(header))
    
            
    
            # Add data rows
    
            for row in rows:
    
                formatted_values = []
    
                for col in columns:
    
                    value = row[col]
    
                    if value is None:
    
                        formatted_values.append("NULL")
    
                    elif isinstance(value, datetime):
    
                        formatted_values.append(value.strftime("%Y-%m-%d %H:%M:%S"))
    
                    elif isinstance(value, (dict, list)):
    
                        formatted_values.append(json.dumps(value))
    
                    else:
    
                        formatted_values.append(str(value))
    
                
    
                result_lines.append(" | ".join(formatted_values))
    
            
    
            # Add truncation notice if needed
    
            if total_rows > max_rows:
    
                result_lines.append(f"\n... and {total_rows - max_rows} more rows (truncated for display)")
    
            
    
            return "\n".join(result_lines)
    
        
    
        async def get_current_utc_date(self) -> str:
    
            """Get current UTC date/time."""
    
            async with self.get_connection() as conn:
    
                result = await conn.fetchval("SELECT NOW() AT TIME ZONE 'UTC'")
    
                return result.isoformat() + "Z"
    
        
    
        async def health_check(self) -> Dict[str, Any]:
    
            """Perform database health check."""
    
            try:
    
                async with self.get_connection() as conn:
    
                    # Simple connectivity test
    
                    result = await conn.fetchval("SELECT 1")
    
                    
    
                    # Check pool status
    
                    pool_info = {
    
                        "min_size": self.connection_pool._minsize if self.connection_pool else 0,
    
                        "max_size": self.connection_pool._maxsize if self.connection_pool else 0,
    
                        "current_size": self.connection_pool.get_size() if self.connection_pool else 0,
    
                        "idle_size": self.connection_pool.get_idle_size() if self.connection_pool else 0
    
                    }
    
                    
    
                    return {
    
                        "status": "healthy",
    
                        "database_responsive": result == 1,
    
                        "pool_info": pool_info
    
                    }
    
                    
    
            except Exception as e:
    
                return {
    
                    "status": "unhealthy",
    
                    "error": str(e)
    
                }
    
    
    
    # Global database provider instance
    
    db_provider = PostgreSQLSchemaProvider()
    
    

    Key Database Layer Features

  • Connection Pooling: Efficient resource management with asyncpg
  • RLS Integration: Automatic Row Level Security context setting
  • Schema Introspection: Dynamic table schema discovery
  • Error Handling: Comprehensive error management and logging
  • Query Formatting: AI-friendly result formatting
  • Health Monitoring: Database connectivity and pool status checks
  • ๐Ÿ”ง Main MCP Server Implementation

    FastMCP Server (sales_analysis.py)

    Now let's implement the main MCP server:

    
    # mcp_server/sales_analysis.py
    
    """
    
    Main MCP server implementation for Zava Retail Sales Analysis.
    
    Provides AI assistants with secure access to retail database.
    
    """
    
    import logging
    
    import asyncio
    
    from typing import Dict, Any, List, Annotated
    
    from contextlib import asynccontextmanager
    
    
    
    from fastmcp import FastMCP, Context
    
    from pydantic import Field
    
    
    
    from .config import config
    
    from .sales_analysis_postgres import db_provider
    
    from .health_check import setup_health_endpoints
    
    
    
    # Configure logging
    
    logging.basicConfig(
    
        level=getattr(logging, config.server.log_level),
    
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    
    )
    
    logger = logging.getLogger(__name__)
    
    
    
    # Create FastMCP server instance
    
    mcp = FastMCP("Zava Retail Sales Analysis")
    
    
    
    # List of valid tables for schema access
    
    VALID_TABLES = [
    
        "retail.stores",
    
        "retail.customers", 
    
        "retail.categories",
    
        "retail.product_types",
    
        "retail.products",
    
        "retail.orders",
    
        "retail.order_items",
    
        "retail.inventory"
    
    ]
    
    
    
    def get_rls_user_id(ctx: Context) -> str:
    
        """Extract Row Level Security User ID from request context."""
    
        # In HTTP mode, get from headers
    
        if hasattr(ctx, 'headers') and ctx.headers:
    
            rls_user_id = ctx.headers.get("x-rls-user-id")
    
            if rls_user_id:
    
                logger.debug(f"RLS User ID from headers: {rls_user_id}")
    
                return rls_user_id
    
        
    
        # Default fallback for development/testing
    
        default_id = "00000000-0000-0000-0000-000000000000"
    
        logger.warning(f"No RLS User ID found, using default: {default_id}")
    
        return default_id
    
    
    
    @mcp.tool()
    
    async def get_multiple_table_schemas(
    
        ctx: Context,
    
        table_names: Annotated[List[str], Field(description="List of table names to retrieve schemas for. Valid tables: " + ", ".join(VALID_TABLES))]
    
    ) -> str:
    
        """
    
        Retrieve database schemas for multiple tables in a single request.
    
        
    
        This tool provides comprehensive schema information including:
    
        - Column names, types, and constraints
    
        - Foreign key relationships
    
        - Index information
    
        - Table structure for AI query planning
    
        
    
        Args:
    
            table_names: List of valid table names from the retail schema
    
            
    
        Returns:
    
            Formatted schema information for all requested tables
    
        """
    
        rls_user_id = get_rls_user_id(ctx)
    
        
    
        # Validate table names
    
        invalid_tables = [table for table in table_names if table not in VALID_TABLES]
    
        if invalid_tables:
    
            logger.warning(f"Invalid table names requested: {invalid_tables}")
    
            return f"Error: Invalid table names: {', '.join(invalid_tables)}. Valid tables are: {', '.join(VALID_TABLES)}"
    
        
    
        try:
    
            logger.info(f"Retrieving schemas for tables: {table_names} (User: {rls_user_id})")
    
            result = await db_provider.get_multiple_table_schemas(table_names, rls_user_id)
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error retrieving table schemas: {e}")
    
            return f"Error retrieving table schemas: {e!s}"
    
    
    
    @mcp.tool()
    
    async def execute_sales_query(
    
        ctx: Context,
    
        postgresql_query: Annotated[str, Field(description="A well-formed PostgreSQL query to execute against the retail database. Always get table schemas first before writing queries.")]
    
    ) -> str:
    
        """
    
        Execute PostgreSQL queries against the retail sales database with Row Level Security.
    
        
    
        This tool allows AI assistants to run analytical queries on retail data including:
    
        - Sales performance analysis
    
        - Customer behavior insights  
    
        - Inventory management queries
    
        - Product performance metrics
    
        - Store-specific reporting
    
        
    
        Important: Row Level Security ensures users only see data they're authorized to access.
    
        
    
        Args:
    
            postgresql_query: SQL query to execute (automatically filtered by RLS)
    
            
    
        Returns:
    
            Query results formatted for AI analysis (limited to 20 rows for readability)
    
        """
    
        rls_user_id = get_rls_user_id(ctx)
    
        
    
        try:
    
            logger.info(f"Executing query for user: {rls_user_id}")
    
            logger.debug(f"Query: {postgresql_query[:100]}...")
    
            
    
            result = await db_provider.execute_query(postgresql_query, rls_user_id)
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error executing database query: {e}")
    
            return f"Error executing database query: {e!s}"
    
    
    
    @mcp.tool()
    
    async def get_current_utc_date(ctx: Context) -> str:
    
        """
    
        Get the current UTC date and time in ISO format.
    
        
    
        Useful for time-sensitive queries and date-based analysis.
    
        
    
        Returns:
    
            Current UTC date/time in ISO format (YYYY-MM-DDTHH:MM:SS.fffffZ)
    
        """
    
        try:
    
            result = await db_provider.get_current_utc_date()
    
            logger.debug(f"Current UTC date retrieved: {result}")
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error getting current UTC date: {e}")
    
            return f"Error getting current UTC date: {e!s}"
    
    
    
    # Application lifecycle management
    
    @asynccontextmanager
    
    async def lifespan(app):
    
        """Manage application startup and shutdown."""
    
        logger.info("Starting Zava Retail MCP Server...")
    
        
    
        try:
    
            # Initialize database connection pool
    
            await db_provider.create_pool()
    
            logger.info("Database connection pool initialized")
    
            
    
            # Test database connectivity
    
            health_status = await db_provider.health_check()
    
            if health_status["status"] != "healthy":
    
                logger.error(f"Database health check failed: {health_status}")
    
                raise Exception("Database not healthy")
    
            
    
            logger.info("MCP Server startup complete")
    
            yield
    
            
    
        except Exception as e:
    
            logger.error(f"Startup failed: {e}")
    
            raise
    
        finally:
    
            # Cleanup
    
            logger.info("Shutting down MCP Server...")
    
            await db_provider.close_pool()
    
            logger.info("MCP Server shutdown complete")
    
    
    
    # Configure server application
    
    def create_app():
    
        """Create and configure the MCP server application."""
    
        
    
        # Get the FastMCP app instance
    
        app = mcp.sse_app()
    
        
    
        # Set up lifecycle management
    
        app.router.lifespan_context = lifespan
    
        
    
        # Add health check endpoints if enabled
    
        if config.server.enable_health_check:
    
            setup_health_endpoints(app, db_provider)
    
        
    
        # Configure CORS if enabled
    
        if config.server.enable_cors:
    
            from fastapi.middleware.cors import CORSMiddleware
    
            app.add_middleware(
    
                CORSMiddleware,
    
                allow_origins=["*"],  # Configure appropriately for production
    
                allow_credentials=True,
    
                allow_methods=["*"],
    
                allow_headers=["*"],
    
            )
    
        
    
        logger.info(f"MCP Server configured - CORS: {config.server.enable_cors}, Health: {config.server.enable_health_check}")
    
        
    
        return app
    
    
    
    # Create the application instance
    
    app = create_app()
    
    
    
    # Main entry point for development
    
    if __name__ == "__main__":
    
        import uvicorn
    
        
    
        logger.info(f"Starting development server on {config.server.host}:{config.server.port}")
    
        
    
        uvicorn.run(
    
            "sales_analysis:app",
    
            host=config.server.host,
    
            port=config.server.port,
    
            reload=True,
    
            log_level=config.server.log_level.lower()
    
        )
    
    

    Key MCP Server Features

  • Tool Registration: Declarative tool definitions with type safety
  • RLS Context Management: Automatic user identity extraction and context setting
  • Error Handling: Comprehensive error management with user-friendly messages
  • Lifecycle Management: Proper startup/shutdown with resource cleanup
  • Health Monitoring: Built-in health check endpoints
  • Development Support: Hot reload and debugging capabilities
  • ๐Ÿฅ Health Monitoring

    Health Check Implementation (health_check.py)

    
    # mcp_server/health_check.py
    
    """
    
    Health check endpoints for monitoring MCP server status.
    
    """
    
    import logging
    
    from typing import Dict, Any
    
    from fastapi import FastAPI, HTTPException
    
    from fastapi.responses import JSONResponse
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    def setup_health_endpoints(app: FastAPI, db_provider) -> None:
    
        """Add health check endpoints to the FastAPI application."""
    
        
    
        @app.get("/health")
    
        async def health_check() -> JSONResponse:
    
            """Basic health check endpoint."""
    
            return JSONResponse(
    
                status_code=200,
    
                content={
    
                    "status": "healthy",
    
                    "service": "zava-retail-mcp-server",
    
                    "timestamp": await db_provider.get_current_utc_date()
    
                }
    
            )
    
        
    
        @app.get("/health/detailed")
    
        async def detailed_health_check() -> JSONResponse:
    
            """Detailed health check including database connectivity."""
    
            health_status = {
    
                "service": "zava-retail-mcp-server",
    
                "status": "healthy",
    
                "components": {}
    
            }
    
            
    
            overall_healthy = True
    
            
    
            # Check database
    
            try:
    
                db_health = await db_provider.health_check()
    
                health_status["components"]["database"] = db_health
    
                
    
                if db_health["status"] != "healthy":
    
                    overall_healthy = False
    
                    
    
            except Exception as e:
    
                health_status["components"]["database"] = {
    
                    "status": "unhealthy",
    
                    "error": str(e)
    
                }
    
                overall_healthy = False
    
            
    
            # Update overall status
    
            if not overall_healthy:
    
                health_status["status"] = "unhealthy"
    
            
    
            status_code = 200 if overall_healthy else 503
    
            
    
            return JSONResponse(
    
                status_code=status_code,
    
                content=health_status
    
            )
    
        
    
        @app.get("/health/ready")
    
        async def readiness_check() -> JSONResponse:
    
            """Kubernetes readiness probe endpoint."""
    
            try:
    
                # Test critical functionality
    
                db_health = await db_provider.health_check()
    
                
    
                if db_health["status"] != "healthy":
    
                    raise HTTPException(status_code=503, detail="Database not ready")
    
                
    
                return JSONResponse(
    
                    status_code=200,
    
                    content={"status": "ready"}
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Readiness check failed: {e}")
    
                raise HTTPException(status_code=503, detail="Service not ready")
    
        
    
        @app.get("/health/live")
    
        async def liveness_check() -> JSONResponse:
    
            """Kubernetes liveness probe endpoint."""
    
            return JSONResponse(
    
                status_code=200,
    
                content={"status": "alive"}
    
            )
    
        
    
        logger.info("Health check endpoints configured")
    
    

    ๐Ÿงช Testing Your MCP Server

    Local Testing

    1. Start the MCP Server:

    ```bash

    # Activate virtual environment

    source mcp-env/bin/activate # macOS/Linux

    # mcp-env\Scripts\activate # Windows

    # Start server

    cd mcp_server

    python sales_analysis.py

    ```

    2. Test Health Endpoints:

    ```bash

    # Basic health check

    curl http://localhost:8000/health

    # Detailed health check

    curl http://localhost:8000/health/detailed

    ```

    3. Test MCP Tools:

    ```bash

    # List available tools

    curl -X POST http://localhost:8000/mcp \

    -H "Content-Type: application/json" \

    -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \

    -d '{"method": "tools/list", "params": {}}'

    # Get table schemas

    curl -X POST http://localhost:8000/mcp \

    -H "Content-Type: application/json" \

    -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \

    -d '{

    "method": "tools/call",

    "params": {

    "name": "get_multiple_table_schemas",

    "arguments": {

    "table_names": ["retail.stores", "retail.products"]

    }

    }

    }'

    ```

    VS Code Integration Testing

    1. Configure VS Code MCP:

    ```json

    // .vscode/mcp.json

    {

    "servers": {

    "zava-retail-test": {

    "url": "http://127.0.0.1:8000/mcp",

    "type": "http",

    "headers": {"x-rls-user-id": "00000000-0000-0000-0000-000000000000"}

    }

    }

    }

    ```

    2. Test in AI Chat:

    - Open VS Code AI Chat

    - Type #zava and select your server

    - Ask: "What tables are available?"

    - Ask: "Show me the top 5 stores by number of orders"

    Unit Testing

    Create comprehensive unit tests:

    
    # tests/test_mcp_server.py
    
    import pytest
    
    import asyncio
    
    from mcp_server.sales_analysis_postgres import PostgreSQLSchemaProvider
    
    from mcp_server.config import config
    
    
    
    @pytest.mark.asyncio
    
    async def test_database_connection():
    
        """Test database connectivity."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            health = await db.health_check()
    
            assert health["status"] == "healthy"
    
        finally:
    
            await db.close_pool()
    
    
    
    @pytest.mark.asyncio
    
    async def test_table_schema_retrieval():
    
        """Test table schema retrieval."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            schema = await db.get_table_schema("retail.stores", "00000000-0000-0000-0000-000000000000")
    
            
    
            assert schema["table_name"] == "retail.stores"
    
            assert len(schema["columns"]) > 0
    
            
    
        finally:
    
            await db.close_pool()
    
    
    
    @pytest.mark.asyncio
    
    async def test_query_execution():
    
        """Test query execution with RLS."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            result = await db.execute_query(
    
                "SELECT COUNT(*) as store_count FROM retail.stores",
    
                "00000000-0000-0000-0000-000000000000"
    
            )
    
            
    
            assert "store_count" in result
    
            
    
        finally:
    
            await db.close_pool()
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Working MCP Server: FastMCP server with database integration

    โœ… Configuration Management: Robust environment-based configuration

    โœ… Database Layer: PostgreSQL integration with connection pooling

    โœ… MCP Tools: Schema introspection and query execution tools

    โœ… RLS Integration: Row Level Security context management

    โœ… Health Monitoring: Comprehensive health check endpoints

    โœ… Testing Strategy: Local testing and VS Code integration

    ๐Ÿš€ What's Next

    Continue with Lab 06: Tool Development to:

  • Expand your MCP tool collection
  • Implement advanced query patterns
  • Add data validation and transformation
  • Create specialized analytics tools
  • ๐Ÿ“š Additional Resources

    FastMCP Framework

  • FastMCP Documentation - Official FastMCP guide
  • MCP Specification - Protocol specification
  • Tool Development Guide - Creating MCP tools
  • Database Integration

  • asyncpg Documentation - PostgreSQL async driver
  • Connection Pooling Best Practices - PostgreSQL tuning
  • Row Level Security Guide - RLS implementation
  • FastAPI Patterns

  • FastAPI Documentation - Web framework reference
  • Dependency Injection - FastAPI patterns
  • Background Tasks - Async task management
  • ---

    Next: Ready to expand your tools? Continue with Lab 06: Tool Development

    Building the FastMCP server with database integration Build

    MCP Server Implementation

    ๐ŸŽฏ What This Lab Covers

    This hands-on lab guides you through implementing a production-ready MCP server using FastMCP framework.

    You'll build the core server structure, implement database integration, create tools for data access, and establish the foundation for AI-powered retail analytics.

    Overview

    The MCP server is the heart of our retail analytics solution. It acts as a bridge between AI assistants and the PostgreSQL database, providing secure, intelligent access to business data through a standardized protocol.

    This lab teaches you to build a robust, scalable MCP server following enterprise patterns and best practices.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Build a FastMCP server with proper architecture and organization
  • Implement database integration with connection pooling and error handling
  • Create MCP tools for database schema introspection and query execution
  • Configure Row Level Security context management
  • Add health monitoring and observability features
  • Test your MCP server implementation locally and with VS Code
  • ๐Ÿ“ Project Structure

    Let's examine the MCP server organization:

    
    mcp_server/
    
    โ”œโ”€โ”€ __init__.py                 # Package initialization
    
    โ”œโ”€โ”€ config.py                   # Configuration management
    
    โ”œโ”€โ”€ health_check.py             # Health monitoring endpoints
    
    โ”œโ”€โ”€ sales_analysis.py           # Main MCP server implementation
    
    โ”œโ”€โ”€ sales_analysis_postgres.py  # Database integration layer
    
    โ””โ”€โ”€ sales_analysis_text_embeddings.py  # AI/semantic search integration
    
    

    ๐Ÿ”ง Configuration Management

    Environment Configuration (config.py)

    First, let's create a robust configuration system:

    
    # mcp_server/config.py
    
    """
    
    Configuration management for the MCP server.
    
    Handles environment variables, validation, and defaults.
    
    """
    
    import os
    
    import logging
    
    from typing import Optional, Dict, Any
    
    from dataclasses import dataclass
    
    from dotenv import load_dotenv
    
    
    
    # Load environment variables from .env file
    
    load_dotenv()
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    @dataclass
    
    class DatabaseConfig:
    
        """Database connection configuration."""
    
        host: str
    
        port: int
    
        database: str
    
        user: str
    
        password: str
    
        min_connections: int = 2
    
        max_connections: int = 10
    
        command_timeout: int = 30
    
        
    
        @classmethod
    
        def from_env(cls) -> 'DatabaseConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                host=os.getenv('POSTGRES_HOST', 'localhost'),
    
                port=int(os.getenv('POSTGRES_PORT', '5432')),
    
                database=os.getenv('POSTGRES_DB', 'zava'),
    
                user=os.getenv('POSTGRES_USER', 'postgres'),
    
                password=os.getenv('POSTGRES_PASSWORD', ''),
    
                min_connections=int(os.getenv('POSTGRES_MIN_CONNECTIONS', '2')),
    
                max_connections=int(os.getenv('POSTGRES_MAX_CONNECTIONS', '10')),
    
                command_timeout=int(os.getenv('POSTGRES_COMMAND_TIMEOUT', '30'))
    
            )
    
        
    
        def to_asyncpg_params(self) -> Dict[str, Any]:
    
            """Convert to asyncpg connection parameters."""
    
            return {
    
                'host': self.host,
    
                'port': self.port,
    
                'database': self.database,
    
                'user': self.user,
    
                'password': self.password,
    
                'command_timeout': self.command_timeout,
    
                'server_settings': {
    
                    'application_name': 'zava-mcp-server',
    
                    'jit': 'off',  # Disable JIT for stability
    
                    'work_mem': '4MB',
    
                    'statement_timeout': f'{self.command_timeout}s'
    
                }
    
            }
    
    
    
    @dataclass
    
    class AzureConfig:
    
        """Azure AI services configuration."""
    
        project_endpoint: str
    
        openai_endpoint: str
    
        embedding_model_deployment: str
    
        client_id: str
    
        client_secret: str
    
        tenant_id: str
    
        
    
        @classmethod
    
        def from_env(cls) -> 'AzureConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                project_endpoint=os.getenv('PROJECT_ENDPOINT', ''),
    
                openai_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
    
                embedding_model_deployment=os.getenv('EMBEDDING_MODEL_DEPLOYMENT_NAME', 'text-embedding-3-small'),
    
                client_id=os.getenv('AZURE_CLIENT_ID', ''),
    
                client_secret=os.getenv('AZURE_CLIENT_SECRET', ''),
    
                tenant_id=os.getenv('AZURE_TENANT_ID', '')
    
            )
    
        
    
        def is_configured(self) -> bool:
    
            """Check if all required Azure configuration is present."""
    
            return all([
    
                self.project_endpoint,
    
                self.openai_endpoint,
    
                self.client_id,
    
                self.client_secret,
    
                self.tenant_id
    
            ])
    
    
    
    @dataclass
    
    class ServerConfig:
    
        """MCP server configuration."""
    
        host: str = '0.0.0.0'
    
        port: int = 8000
    
        log_level: str = 'INFO'
    
        enable_cors: bool = True
    
        enable_health_check: bool = True
    
        applicationinsights_connection_string: Optional[str] = None
    
        
    
        @classmethod
    
        def from_env(cls) -> 'ServerConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                host=os.getenv('MCP_SERVER_HOST', '0.0.0.0'),
    
                port=int(os.getenv('MCP_SERVER_PORT', '8000')),
    
                log_level=os.getenv('LOG_LEVEL', 'INFO').upper(),
    
                enable_cors=os.getenv('ENABLE_CORS', 'true').lower() == 'true',
    
                enable_health_check=os.getenv('ENABLE_HEALTH_CHECK', 'true').lower() == 'true',
    
                applicationinsights_connection_string=os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING')
    
            )
    
    
    
    class MCPServerConfig:
    
        """Main configuration class for the MCP server."""
    
        
    
        def __init__(self):
    
            self.database = DatabaseConfig.from_env()
    
            self.azure = AzureConfig.from_env()
    
            self.server = ServerConfig.from_env()
    
            
    
            # Validate configuration
    
            self._validate_config()
    
        
    
        def _validate_config(self):
    
            """Validate configuration and log warnings for missing values."""
    
            if not self.database.password:
    
                logger.warning("Database password is empty. This may cause connection issues.")
    
            
    
            if not self.azure.is_configured():
    
                logger.warning("Azure configuration is incomplete. AI features may not work.")
    
            
    
            logger.info(f"Configuration loaded - Database: {self.database.host}:{self.database.port}")
    
            logger.info(f"Server will run on {self.server.host}:{self.server.port}")
    
    
    
    # Global configuration instance
    
    config = MCPServerConfig()
    
    

    Key Configuration Features

  • Environment Variable Loading: Automatic .env file support
  • Type Safety: Dataclass validation and type hints
  • Flexible Defaults: Sensible defaults for development
  • Validation: Configuration validation with helpful error messages
  • Security: Sensitive values only from environment variables
  • ๐Ÿ—„๏ธ Database Integration Layer

    PostgreSQL Provider (sales_analysis_postgres.py)

    Let's implement the database integration layer:

    
    # mcp_server/sales_analysis_postgres.py
    
    """
    
    PostgreSQL database integration for MCP server.
    
    Handles connections, queries, and schema introspection.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import logging
    
    from typing import Dict, Any, List, Optional, Tuple
    
    from contextlib import asynccontextmanager
    
    from datetime import datetime
    
    import json
    
    
    
    from .config import config
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class PostgreSQLSchemaProvider:
    
        """Provides PostgreSQL database access and schema information."""
    
        
    
        def __init__(self):
    
            self.connection_pool: Optional[asyncpg.Pool] = None
    
            self.postgres_config = config.database.to_asyncpg_params()
    
            
    
        async def create_pool(self) -> None:
    
            """Create connection pool for database operations."""
    
            if self.connection_pool is None:
    
                try:
    
                    self.connection_pool = await asyncpg.create_pool(
    
                        **self.postgres_config,
    
                        min_size=config.database.min_connections,
    
                        max_size=config.database.max_connections,
    
                        max_inactive_connection_lifetime=300  # 5 minutes
    
                    )
    
                    logger.info("Database connection pool created successfully")
    
                except Exception as e:
    
                    logger.error(f"Failed to create database connection pool: {e}")
    
                    raise
    
        
    
        async def close_pool(self) -> None:
    
            """Close the connection pool."""
    
            if self.connection_pool:
    
                await self.connection_pool.close()
    
                self.connection_pool = None
    
                logger.info("Database connection pool closed")
    
        
    
        @asynccontextmanager
    
        async def get_connection(self):
    
            """Get a database connection from the pool."""
    
            if not self.connection_pool:
    
                await self.create_pool()
    
            
    
            async with self.connection_pool.acquire() as connection:
    
                yield connection
    
        
    
        async def set_rls_context(self, connection: asyncpg.Connection, rls_user_id: str) -> None:
    
            """Set Row Level Security context for the connection."""
    
            try:
    
                await connection.execute(
    
                    "SELECT set_config('app.current_rls_user_id', $1, false)",
    
                    rls_user_id
    
                )
    
                logger.debug(f"RLS context set for user: {rls_user_id}")
    
            except Exception as e:
    
                logger.error(f"Failed to set RLS context: {e}")
    
                raise
    
        
    
        async def get_table_schema(self, table_name: str, rls_user_id: str) -> Dict[str, Any]:
    
            """Get detailed schema information for a specific table."""
    
            async with self.get_connection() as conn:
    
                await self.set_rls_context(conn, rls_user_id)
    
                
    
                # Parse schema and table name
    
                if '.' in table_name:
    
                    schema_name, table_name = table_name.split('.', 1)
    
                else:
    
                    schema_name = 'retail'  # Default schema
    
                
    
                # Get column information
    
                columns_query = """
    
                    SELECT 
    
                        column_name,
    
                        data_type,
    
                        is_nullable,
    
                        column_default,
    
                        character_maximum_length,
    
                        numeric_precision,
    
                        numeric_scale,
    
                        ordinal_position
    
                    FROM information_schema.columns 
    
                    WHERE table_schema = $1 AND table_name = $2
    
                    ORDER BY ordinal_position
    
                """
    
                
    
                columns = await conn.fetch(columns_query, schema_name, table_name)
    
                
    
                if not columns:
    
                    raise ValueError(f"Table {schema_name}.{table_name} not found or not accessible")
    
                
    
                # Get foreign key relationships
    
                fk_query = """
    
                    SELECT 
    
                        kcu.column_name,
    
                        ccu.table_schema AS foreign_table_schema,
    
                        ccu.table_name AS foreign_table_name,
    
                        ccu.column_name AS foreign_column_name
    
                    FROM information_schema.table_constraints tc
    
                    JOIN information_schema.key_column_usage kcu 
    
                        ON tc.constraint_name = kcu.constraint_name
    
                    JOIN information_schema.constraint_column_usage ccu 
    
                        ON ccu.constraint_name = tc.constraint_name
    
                    WHERE tc.constraint_type = 'FOREIGN KEY' 
    
                        AND tc.table_schema = $1 
    
                        AND tc.table_name = $2
    
                """
    
                
    
                foreign_keys = await conn.fetch(fk_query, schema_name, table_name)
    
                
    
                # Get indexes
    
                index_query = """
    
                    SELECT 
    
                        indexname,
    
                        indexdef
    
                    FROM pg_indexes 
    
                    WHERE schemaname = $1 AND tablename = $2
    
                """
    
                
    
                indexes = await conn.fetch(index_query, schema_name, table_name)
    
                
    
                # Format schema information
    
                schema_info = {
    
                    "table_name": f"{schema_name}.{table_name}",
    
                    "columns": [
    
                        {
    
                            "name": col["column_name"],
    
                            "type": col["data_type"],
    
                            "nullable": col["is_nullable"] == "YES",
    
                            "default": col["column_default"],
    
                            "max_length": col["character_maximum_length"],
    
                            "precision": col["numeric_precision"],
    
                            "scale": col["numeric_scale"],
    
                            "position": col["ordinal_position"]
    
                        }
    
                        for col in columns
    
                    ],
    
                    "foreign_keys": [
    
                        {
    
                            "column": fk["column_name"],
    
                            "references": f"{fk['foreign_table_schema']}.{fk['foreign_table_name']}.{fk['foreign_column_name']}"
    
                        }
    
                        for fk in foreign_keys
    
                    ],
    
                    "indexes": [
    
                        {
    
                            "name": idx["indexname"],
    
                            "definition": idx["indexdef"]
    
                        }
    
                        for idx in indexes
    
                    ]
    
                }
    
                
    
                return schema_info
    
        
    
        async def get_multiple_table_schemas(
    
            self, 
    
            table_names: List[str], 
    
            rls_user_id: str
    
        ) -> str:
    
            """Get schema information for multiple tables."""
    
            schemas = []
    
            
    
            for table_name in table_names:
    
                try:
    
                    schema = await self.get_table_schema(table_name, rls_user_id)
    
                    schemas.append(self._format_schema_for_ai(schema))
    
                except Exception as e:
    
                    logger.warning(f"Failed to get schema for {table_name}: {e}")
    
                    schemas.append(f"Error retrieving schema for {table_name}: {str(e)}")
    
            
    
            return "\n\n".join(schemas)
    
        
    
        def _format_schema_for_ai(self, schema: Dict[str, Any]) -> str:
    
            """Format schema information for AI consumption."""
    
            table_name = schema["table_name"]
    
            columns = schema["columns"]
    
            foreign_keys = schema["foreign_keys"]
    
            
    
            # Create column definitions
    
            column_lines = []
    
            for col in columns:
    
                nullable = "NULL" if col["nullable"] else "NOT NULL"
    
                type_info = col["type"]
    
                
    
                if col["max_length"]:
    
                    type_info += f"({col['max_length']})"
    
                elif col["precision"] and col["scale"]:
    
                    type_info += f"({col['precision']},{col['scale']})"
    
                
    
                default_info = f" DEFAULT {col['default']}" if col["default"] else ""
    
                
    
                column_lines.append(f"  {col['name']} {type_info} {nullable}{default_info}")
    
            
    
            # Create foreign key information
    
            fk_lines = []
    
            for fk in foreign_keys:
    
                fk_lines.append(f"  {fk['column']} -> {fk['references']}")
    
            
    
            # Combine into readable format
    
            schema_text = f"Table: {table_name}\n"
    
            schema_text += "Columns:\n" + "\n".join(column_lines)
    
            
    
            if fk_lines:
    
                schema_text += "\n\nForeign Keys:\n" + "\n".join(fk_lines)
    
            
    
            return schema_text
    
        
    
        async def execute_query(
    
            self, 
    
            sql_query: str, 
    
            rls_user_id: str,
    
            max_rows: int = 20
    
        ) -> str:
    
            """Execute a SQL query with Row Level Security context."""
    
            async with self.get_connection() as conn:
    
                await self.set_rls_context(conn, rls_user_id)
    
                
    
                try:
    
                    # Set a query timeout
    
                    rows = await asyncio.wait_for(
    
                        conn.fetch(sql_query),
    
                        timeout=config.database.command_timeout
    
                    )
    
                    
    
                    if not rows:
    
                        return "Query executed successfully. No rows returned."
    
                    
    
                    # Limit result set size
    
                    limited_rows = rows[:max_rows]
    
                    
    
                    # Format results
    
                    result = self._format_query_results(limited_rows, len(rows), max_rows)
    
                    
    
                    logger.info(f"Query executed successfully. Returned {len(limited_rows)} rows.")
    
                    return result
    
                    
    
                except asyncio.TimeoutError:
    
                    error_msg = f"Query timeout after {config.database.command_timeout} seconds"
    
                    logger.error(error_msg)
    
                    raise Exception(error_msg)
    
                except Exception as e:
    
                    logger.error(f"Query execution failed: {e}")
    
                    raise
    
        
    
        def _format_query_results(
    
            self, 
    
            rows: List[asyncpg.Record], 
    
            total_rows: int,
    
            max_rows: int
    
        ) -> str:
    
            """Format query results for AI consumption."""
    
            if not rows:
    
                return "No results found."
    
            
    
            # Get column names
    
            columns = list(rows[0].keys())
    
            
    
            # Create header
    
            result_lines = [f"Results ({len(rows)} of {total_rows} rows):"]
    
            result_lines.append("=" * 50)
    
            
    
            # Add column headers
    
            header = " | ".join(columns)
    
            result_lines.append(header)
    
            result_lines.append("-" * len(header))
    
            
    
            # Add data rows
    
            for row in rows:
    
                formatted_values = []
    
                for col in columns:
    
                    value = row[col]
    
                    if value is None:
    
                        formatted_values.append("NULL")
    
                    elif isinstance(value, datetime):
    
                        formatted_values.append(value.strftime("%Y-%m-%d %H:%M:%S"))
    
                    elif isinstance(value, (dict, list)):
    
                        formatted_values.append(json.dumps(value))
    
                    else:
    
                        formatted_values.append(str(value))
    
                
    
                result_lines.append(" | ".join(formatted_values))
    
            
    
            # Add truncation notice if needed
    
            if total_rows > max_rows:
    
                result_lines.append(f"\n... and {total_rows - max_rows} more rows (truncated for display)")
    
            
    
            return "\n".join(result_lines)
    
        
    
        async def get_current_utc_date(self) -> str:
    
            """Get current UTC date/time."""
    
            async with self.get_connection() as conn:
    
                result = await conn.fetchval("SELECT NOW() AT TIME ZONE 'UTC'")
    
                return result.isoformat() + "Z"
    
        
    
        async def health_check(self) -> Dict[str, Any]:
    
            """Perform database health check."""
    
            try:
    
                async with self.get_connection() as conn:
    
                    # Simple connectivity test
    
                    result = await conn.fetchval("SELECT 1")
    
                    
    
                    # Check pool status
    
                    pool_info = {
    
                        "min_size": self.connection_pool._minsize if self.connection_pool else 0,
    
                        "max_size": self.connection_pool._maxsize if self.connection_pool else 0,
    
                        "current_size": self.connection_pool.get_size() if self.connection_pool else 0,
    
                        "idle_size": self.connection_pool.get_idle_size() if self.connection_pool else 0
    
                    }
    
                    
    
                    return {
    
                        "status": "healthy",
    
                        "database_responsive": result == 1,
    
                        "pool_info": pool_info
    
                    }
    
                    
    
            except Exception as e:
    
                return {
    
                    "status": "unhealthy",
    
                    "error": str(e)
    
                }
    
    
    
    # Global database provider instance
    
    db_provider = PostgreSQLSchemaProvider()
    
    

    Key Database Layer Features

  • Connection Pooling: Efficient resource management with asyncpg
  • RLS Integration: Automatic Row Level Security context setting
  • Schema Introspection: Dynamic table schema discovery
  • Error Handling: Comprehensive error management and logging
  • Query Formatting: AI-friendly result formatting
  • Health Monitoring: Database connectivity and pool status checks
  • ๐Ÿ”ง Main MCP Server Implementation

    FastMCP Server (sales_analysis.py)

    Now let's implement the main MCP server:

    
    # mcp_server/sales_analysis.py
    
    """
    
    Main MCP server implementation for Zava Retail Sales Analysis.
    
    Provides AI assistants with secure access to retail database.
    
    """
    
    import logging
    
    import asyncio
    
    from typing import Dict, Any, List, Annotated
    
    from contextlib import asynccontextmanager
    
    
    
    from fastmcp import FastMCP, Context
    
    from pydantic import Field
    
    
    
    from .config import config
    
    from .sales_analysis_postgres import db_provider
    
    from .health_check import setup_health_endpoints
    
    
    
    # Configure logging
    
    logging.basicConfig(
    
        level=getattr(logging, config.server.log_level),
    
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    
    )
    
    logger = logging.getLogger(__name__)
    
    
    
    # Create FastMCP server instance
    
    mcp = FastMCP("Zava Retail Sales Analysis")
    
    
    
    # List of valid tables for schema access
    
    VALID_TABLES = [
    
        "retail.stores",
    
        "retail.customers", 
    
        "retail.categories",
    
        "retail.product_types",
    
        "retail.products",
    
        "retail.orders",
    
        "retail.order_items",
    
        "retail.inventory"
    
    ]
    
    
    
    def get_rls_user_id(ctx: Context) -> str:
    
        """Extract Row Level Security User ID from request context."""
    
        # In HTTP mode, get from headers
    
        if hasattr(ctx, 'headers') and ctx.headers:
    
            rls_user_id = ctx.headers.get("x-rls-user-id")
    
            if rls_user_id:
    
                logger.debug(f"RLS User ID from headers: {rls_user_id}")
    
                return rls_user_id
    
        
    
        # Default fallback for development/testing
    
        default_id = "00000000-0000-0000-0000-000000000000"
    
        logger.warning(f"No RLS User ID found, using default: {default_id}")
    
        return default_id
    
    
    
    @mcp.tool()
    
    async def get_multiple_table_schemas(
    
        ctx: Context,
    
        table_names: Annotated[List[str], Field(description="List of table names to retrieve schemas for. Valid tables: " + ", ".join(VALID_TABLES))]
    
    ) -> str:
    
        """
    
        Retrieve database schemas for multiple tables in a single request.
    
        
    
        This tool provides comprehensive schema information including:
    
        - Column names, types, and constraints
    
        - Foreign key relationships
    
        - Index information
    
        - Table structure for AI query planning
    
        
    
        Args:
    
            table_names: List of valid table names from the retail schema
    
            
    
        Returns:
    
            Formatted schema information for all requested tables
    
        """
    
        rls_user_id = get_rls_user_id(ctx)
    
        
    
        # Validate table names
    
        invalid_tables = [table for table in table_names if table not in VALID_TABLES]
    
        if invalid_tables:
    
            logger.warning(f"Invalid table names requested: {invalid_tables}")
    
            return f"Error: Invalid table names: {', '.join(invalid_tables)}. Valid tables are: {', '.join(VALID_TABLES)}"
    
        
    
        try:
    
            logger.info(f"Retrieving schemas for tables: {table_names} (User: {rls_user_id})")
    
            result = await db_provider.get_multiple_table_schemas(table_names, rls_user_id)
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error retrieving table schemas: {e}")
    
            return f"Error retrieving table schemas: {e!s}"
    
    
    
    @mcp.tool()
    
    async def execute_sales_query(
    
        ctx: Context,
    
        postgresql_query: Annotated[str, Field(description="A well-formed PostgreSQL query to execute against the retail database. Always get table schemas first before writing queries.")]
    
    ) -> str:
    
        """
    
        Execute PostgreSQL queries against the retail sales database with Row Level Security.
    
        
    
        This tool allows AI assistants to run analytical queries on retail data including:
    
        - Sales performance analysis
    
        - Customer behavior insights  
    
        - Inventory management queries
    
        - Product performance metrics
    
        - Store-specific reporting
    
        
    
        Important: Row Level Security ensures users only see data they're authorized to access.
    
        
    
        Args:
    
            postgresql_query: SQL query to execute (automatically filtered by RLS)
    
            
    
        Returns:
    
            Query results formatted for AI analysis (limited to 20 rows for readability)
    
        """
    
        rls_user_id = get_rls_user_id(ctx)
    
        
    
        try:
    
            logger.info(f"Executing query for user: {rls_user_id}")
    
            logger.debug(f"Query: {postgresql_query[:100]}...")
    
            
    
            result = await db_provider.execute_query(postgresql_query, rls_user_id)
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error executing database query: {e}")
    
            return f"Error executing database query: {e!s}"
    
    
    
    @mcp.tool()
    
    async def get_current_utc_date(ctx: Context) -> str:
    
        """
    
        Get the current UTC date and time in ISO format.
    
        
    
        Useful for time-sensitive queries and date-based analysis.
    
        
    
        Returns:
    
            Current UTC date/time in ISO format (YYYY-MM-DDTHH:MM:SS.fffffZ)
    
        """
    
        try:
    
            result = await db_provider.get_current_utc_date()
    
            logger.debug(f"Current UTC date retrieved: {result}")
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error getting current UTC date: {e}")
    
            return f"Error getting current UTC date: {e!s}"
    
    
    
    # Application lifecycle management
    
    @asynccontextmanager
    
    async def lifespan(app):
    
        """Manage application startup and shutdown."""
    
        logger.info("Starting Zava Retail MCP Server...")
    
        
    
        try:
    
            # Initialize database connection pool
    
            await db_provider.create_pool()
    
            logger.info("Database connection pool initialized")
    
            
    
            # Test database connectivity
    
            health_status = await db_provider.health_check()
    
            if health_status["status"] != "healthy":
    
                logger.error(f"Database health check failed: {health_status}")
    
                raise Exception("Database not healthy")
    
            
    
            logger.info("MCP Server startup complete")
    
            yield
    
            
    
        except Exception as e:
    
            logger.error(f"Startup failed: {e}")
    
            raise
    
        finally:
    
            # Cleanup
    
            logger.info("Shutting down MCP Server...")
    
            await db_provider.close_pool()
    
            logger.info("MCP Server shutdown complete")
    
    
    
    # Configure server application
    
    def create_app():
    
        """Create and configure the MCP server application."""
    
        
    
        # Get the FastMCP app instance
    
        app = mcp.sse_app()
    
        
    
        # Set up lifecycle management
    
        app.router.lifespan_context = lifespan
    
        
    
        # Add health check endpoints if enabled
    
        if config.server.enable_health_check:
    
            setup_health_endpoints(app, db_provider)
    
        
    
        # Configure CORS if enabled
    
        if config.server.enable_cors:
    
            from fastapi.middleware.cors import CORSMiddleware
    
            app.add_middleware(
    
                CORSMiddleware,
    
                allow_origins=["*"],  # Configure appropriately for production
    
                allow_credentials=True,
    
                allow_methods=["*"],
    
                allow_headers=["*"],
    
            )
    
        
    
        logger.info(f"MCP Server configured - CORS: {config.server.enable_cors}, Health: {config.server.enable_health_check}")
    
        
    
        return app
    
    
    
    # Create the application instance
    
    app = create_app()
    
    
    
    # Main entry point for development
    
    if __name__ == "__main__":
    
        import uvicorn
    
        
    
        logger.info(f"Starting development server on {config.server.host}:{config.server.port}")
    
        
    
        uvicorn.run(
    
            "sales_analysis:app",
    
            host=config.server.host,
    
            port=config.server.port,
    
            reload=True,
    
            log_level=config.server.log_level.lower()
    
        )
    
    

    Key MCP Server Features

  • Tool Registration: Declarative tool definitions with type safety
  • RLS Context Management: Automatic user identity extraction and context setting
  • Error Handling: Comprehensive error management with user-friendly messages
  • Lifecycle Management: Proper startup/shutdown with resource cleanup
  • Health Monitoring: Built-in health check endpoints
  • Development Support: Hot reload and debugging capabilities
  • ๐Ÿฅ Health Monitoring

    Health Check Implementation (health_check.py)

    
    # mcp_server/health_check.py
    
    """
    
    Health check endpoints for monitoring MCP server status.
    
    """
    
    import logging
    
    from typing import Dict, Any
    
    from fastapi import FastAPI, HTTPException
    
    from fastapi.responses import JSONResponse
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    def setup_health_endpoints(app: FastAPI, db_provider) -> None:
    
        """Add health check endpoints to the FastAPI application."""
    
        
    
        @app.get("/health")
    
        async def health_check() -> JSONResponse:
    
            """Basic health check endpoint."""
    
            return JSONResponse(
    
                status_code=200,
    
                content={
    
                    "status": "healthy",
    
                    "service": "zava-retail-mcp-server",
    
                    "timestamp": await db_provider.get_current_utc_date()
    
                }
    
            )
    
        
    
        @app.get("/health/detailed")
    
        async def detailed_health_check() -> JSONResponse:
    
            """Detailed health check including database connectivity."""
    
            health_status = {
    
                "service": "zava-retail-mcp-server",
    
                "status": "healthy",
    
                "components": {}
    
            }
    
            
    
            overall_healthy = True
    
            
    
            # Check database
    
            try:
    
                db_health = await db_provider.health_check()
    
                health_status["components"]["database"] = db_health
    
                
    
                if db_health["status"] != "healthy":
    
                    overall_healthy = False
    
                    
    
            except Exception as e:
    
                health_status["components"]["database"] = {
    
                    "status": "unhealthy",
    
                    "error": str(e)
    
                }
    
                overall_healthy = False
    
            
    
            # Update overall status
    
            if not overall_healthy:
    
                health_status["status"] = "unhealthy"
    
            
    
            status_code = 200 if overall_healthy else 503
    
            
    
            return JSONResponse(
    
                status_code=status_code,
    
                content=health_status
    
            )
    
        
    
        @app.get("/health/ready")
    
        async def readiness_check() -> JSONResponse:
    
            """Kubernetes readiness probe endpoint."""
    
            try:
    
                # Test critical functionality
    
                db_health = await db_provider.health_check()
    
                
    
                if db_health["status"] != "healthy":
    
                    raise HTTPException(status_code=503, detail="Database not ready")
    
                
    
                return JSONResponse(
    
                    status_code=200,
    
                    content={"status": "ready"}
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Readiness check failed: {e}")
    
                raise HTTPException(status_code=503, detail="Service not ready")
    
        
    
        @app.get("/health/live")
    
        async def liveness_check() -> JSONResponse:
    
            """Kubernetes liveness probe endpoint."""
    
            return JSONResponse(
    
                status_code=200,
    
                content={"status": "alive"}
    
            )
    
        
    
        logger.info("Health check endpoints configured")
    
    

    ๐Ÿงช Testing Your MCP Server

    Local Testing

    1. Start the MCP Server:

    ```bash

    # Activate virtual environment

    source mcp-env/bin/activate # macOS/Linux

    # mcp-env\Scripts\activate # Windows

    # Start server

    cd mcp_server

    python sales_analysis.py

    ```

    2. Test Health Endpoints:

    ```bash

    # Basic health check

    curl http://localhost:8000/health

    # Detailed health check

    curl http://localhost:8000/health/detailed

    ```

    3. Test MCP Tools:

    ```bash

    # List available tools

    curl -X POST http://localhost:8000/mcp \

    -H "Content-Type: application/json" \

    -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \

    -d '{"method": "tools/list", "params": {}}'

    # Get table schemas

    curl -X POST http://localhost:8000/mcp \

    -H "Content-Type: application/json" \

    -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \

    -d '{

    "method": "tools/call",

    "params": {

    "name": "get_multiple_table_schemas",

    "arguments": {

    "table_names": ["retail.stores", "retail.products"]

    }

    }

    }'

    ```

    VS Code Integration Testing

    1. Configure VS Code MCP:

    ```json

    // .vscode/mcp.json

    {

    "servers": {

    "zava-retail-test": {

    "url": "http://127.0.0.1:8000/mcp",

    "type": "http",

    "headers": {"x-rls-user-id": "00000000-0000-0000-0000-000000000000"}

    }

    }

    }

    ```

    2. Test in AI Chat:

    - Open VS Code AI Chat

    - Type #zava and select your server

    - Ask: "What tables are available?"

    - Ask: "Show me the top 5 stores by number of orders"

    Unit Testing

    Create comprehensive unit tests:

    
    # tests/test_mcp_server.py
    
    import pytest
    
    import asyncio
    
    from mcp_server.sales_analysis_postgres import PostgreSQLSchemaProvider
    
    from mcp_server.config import config
    
    
    
    @pytest.mark.asyncio
    
    async def test_database_connection():
    
        """Test database connectivity."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            health = await db.health_check()
    
            assert health["status"] == "healthy"
    
        finally:
    
            await db.close_pool()
    
    
    
    @pytest.mark.asyncio
    
    async def test_table_schema_retrieval():
    
        """Test table schema retrieval."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            schema = await db.get_table_schema("retail.stores", "00000000-0000-0000-0000-000000000000")
    
            
    
            assert schema["table_name"] == "retail.stores"
    
            assert len(schema["columns"]) > 0
    
            
    
        finally:
    
            await db.close_pool()
    
    
    
    @pytest.mark.asyncio
    
    async def test_query_execution():
    
        """Test query execution with RLS."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            result = await db.execute_query(
    
                "SELECT COUNT(*) as store_count FROM retail.stores",
    
                "00000000-0000-0000-0000-000000000000"
    
            )
    
            
    
            assert "store_count" in result
    
            
    
        finally:
    
            await db.close_pool()
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Working MCP Server: FastMCP server with database integration

    โœ… Configuration Management: Robust environment-based configuration

    โœ… Database Layer: PostgreSQL integration with connection pooling

    โœ… MCP Tools: Schema introspection and query execution tools

    โœ… RLS Integration: Row Level Security context management

    โœ… Health Monitoring: Comprehensive health check endpoints

    โœ… Testing Strategy: Local testing and VS Code integration

    ๐Ÿš€ What's Next

    Continue with Lab 06: Tool Development to:

  • Expand your MCP tool collection
  • Implement advanced query patterns
  • Add data validation and transformation
  • Create specialized analytics tools
  • ๐Ÿ“š Additional Resources

    FastMCP Framework

  • FastMCP Documentation - Official FastMCP guide
  • MCP Specification - Protocol specification
  • Tool Development Guide - Creating MCP tools
  • Database Integration

  • asyncpg Documentation - PostgreSQL async driver
  • Connection Pooling Best Practices - PostgreSQL tuning
  • Row Level Security Guide - RLS implementation
  • FastAPI Patterns

  • FastAPI Documentation - Web framework reference
  • Dependency Injection - FastAPI patterns
  • Background Tasks - Async task management
  • ---

    Next: Ready to expand your tools? Continue with Lab 06: Tool Development

    06 Tool Development

    Tool Development

    ๐ŸŽฏ What This Lab Covers

    This lab dives deep into creating sophisticated MCP tools that provide AI assistants with powerful database query capabilities, schema introspection, and analytics functions.

    You'll learn to build tools that are both powerful and safe, with comprehensive error handling and performance optimization.

    Overview

    MCP tools are the interface between AI assistants and your data systems.

    Well-designed tools provide structured, validated access to complex operations while maintaining security and performance.

    This lab covers the complete lifecycle of tool development from design to deployment.

    Our retail MCP server implements a comprehensive suite of tools that enable natural language querying of sales data, product catalogs, and business analytics while maintaining strict security boundaries and optimal performance.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Design advanced MCP tools with complex parameter validation
  • Implement secure database query tools with SQL injection protection
  • Create schema introspection capabilities for dynamic queries
  • Build custom analytics tools for business intelligence
  • Apply comprehensive error handling and graceful degradation
  • Optimize tool performance for production workloads
  • ๐Ÿ› ๏ธ Core Tool Architecture

    Tool Design Principles

    
    # mcp_server/tools/base.py
    
    """
    
    Base classes and patterns for MCP tool development.
    
    """
    
    from abc import ABC, abstractmethod
    
    from typing import Any, Dict, List, Optional, Union
    
    from dataclasses import dataclass
    
    from enum import Enum
    
    import asyncio
    
    import time
    
    import logging
    
    from contextlib import asynccontextmanager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ToolCategory(Enum):
    
        """Tool categorization for organization and discovery."""
    
        DATABASE_QUERY = "database_query"
    
        SCHEMA_INTROSPECTION = "schema_introspection"
    
        ANALYTICS = "analytics"
    
        UTILITY = "utility"
    
        ADMINISTRATIVE = "administrative"
    
    
    
    @dataclass
    
    class ToolResult:
    
        """Standardized tool result structure."""
    
        success: bool
    
        data: Any = None
    
        error: Optional[str] = None
    
        metadata: Optional[Dict[str, Any]] = None
    
        execution_time_ms: Optional[float] = None
    
        row_count: Optional[int] = None
    
    
    
    class BaseTool(ABC):
    
        """Abstract base class for all MCP tools."""
    
        
    
        def __init__(self, name: str, description: str, category: ToolCategory):
    
            self.name = name
    
            self.description = description
    
            self.category = category
    
            self.call_count = 0
    
            self.total_execution_time = 0.0
    
            
    
        @abstractmethod
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute the tool with given parameters."""
    
            pass
    
        
    
        @abstractmethod
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get JSON schema for tool input validation."""
    
            pass
    
        
    
        async def call(self, **kwargs) -> ToolResult:
    
            """Wrapper for tool execution with metrics and error handling."""
    
            
    
            start_time = time.time()
    
            self.call_count += 1
    
            
    
            try:
    
                # Validate input parameters
    
                self._validate_input(kwargs)
    
                
    
                # Log tool execution
    
                logger.info(
    
                    f"Executing tool: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'tool_category': self.category.value,
    
                        'parameters': self._sanitize_parameters(kwargs)
    
                    }
    
                )
    
                
    
                # Execute the tool
    
                result = await self.execute(**kwargs)
    
                
    
                # Record execution time
    
                execution_time = (time.time() - start_time) * 1000
    
                result.execution_time_ms = execution_time
    
                self.total_execution_time += execution_time
    
                
    
                # Log success
    
                logger.info(
    
                    f"Tool execution completed: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'execution_time_ms': execution_time,
    
                        'success': result.success,
    
                        'row_count': result.row_count
    
                    }
    
                )
    
                
    
                return result
    
                
    
            except Exception as e:
    
                execution_time = (time.time() - start_time) * 1000
    
                
    
                logger.error(
    
                    f"Tool execution failed: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'execution_time_ms': execution_time,
    
                        'error': str(e)
    
                    },
    
                    exc_info=True
    
                )
    
                
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Tool execution failed: {str(e)}",
    
                    execution_time_ms=execution_time
    
                )
    
        
    
        def _validate_input(self, kwargs: Dict[str, Any]):
    
            """Validate input parameters against schema."""
    
            
    
            schema = self.get_input_schema()
    
            required_props = schema.get('required', [])
    
            properties = schema.get('properties', {})
    
            
    
            # Check required parameters
    
            missing_required = [prop for prop in required_props if prop not in kwargs]
    
            if missing_required:
    
                raise ValueError(f"Missing required parameters: {missing_required}")
    
            
    
            # Type validation would go here
    
            # For production, use jsonschema library for comprehensive validation
    
        
    
        def _sanitize_parameters(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
    
            """Sanitize parameters for logging (remove sensitive data)."""
    
            
    
            # Remove or mask sensitive parameters
    
            sanitized = kwargs.copy()
    
            sensitive_keys = ['password', 'token', 'secret', 'key']
    
            
    
            for key in sanitized:
    
                if any(sensitive in key.lower() for sensitive in sensitive_keys):
    
                    sanitized[key] = "***MASKED***"
    
            
    
            return sanitized
    
        
    
        def get_statistics(self) -> Dict[str, Any]:
    
            """Get tool usage statistics."""
    
            
    
            return {
    
                'name': self.name,
    
                'category': self.category.value,
    
                'call_count': self.call_count,
    
                'total_execution_time_ms': self.total_execution_time,
    
                'average_execution_time_ms': (
    
                    self.total_execution_time / self.call_count 
    
                    if self.call_count > 0 else 0
    
                )
    
            }
    
    
    
    class DatabaseTool(BaseTool):
    
        """Base class for database-related tools."""
    
        
    
        def __init__(self, name: str, description: str, db_provider):
    
            super().__init__(name, description, ToolCategory.DATABASE_QUERY)
    
            self.db_provider = db_provider
    
        
    
        @asynccontextmanager
    
        async def get_connection(self):
    
            """Get database connection with proper context management."""
    
            
    
            conn = None
    
            try:
    
                conn = await self.db_provider.get_connection()
    
                yield conn
    
            finally:
    
                if conn:
    
                    await self.db_provider.release_connection(conn)
    
        
    
        async def execute_query(
    
            self, 
    
            query: str, 
    
            params: tuple = None,
    
            store_id: str = None
    
        ) -> ToolResult:
    
            """Execute database query with security and performance monitoring."""
    
            
    
            async with self.get_connection() as conn:
    
                try:
    
                    # Set store context if provided
    
                    if store_id:
    
                        await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Execute query
    
                    start_time = time.time()
    
                    
    
                    if params:
    
                        rows = await conn.fetch(query, *params)
    
                    else:
    
                        rows = await conn.fetch(query)
    
                    
    
                    execution_time = (time.time() - start_time) * 1000
    
                    
    
                    # Convert rows to dictionaries
    
                    data = [dict(row) for row in rows]
    
                    
    
                    return ToolResult(
    
                        success=True,
    
                        data=data,
    
                        row_count=len(data),
    
                        execution_time_ms=execution_time
    
                    )
    
                    
    
                except Exception as e:
    
                    logger.error(f"Database query failed: {str(e)}")
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Query execution failed: {str(e)}"
    
                    )
    
    

    Query Validation and Security

    
    # mcp_server/tools/query_validator.py
    
    """
    
    SQL query validation and security for MCP tools.
    
    """
    
    import re
    
    import sqlparse
    
    from typing import List, Dict, Any, Set
    
    from enum import Enum
    
    
    
    class QueryRisk(Enum):
    
        """Query risk levels."""
    
        LOW = "low"
    
        MEDIUM = "medium"
    
        HIGH = "high"
    
        CRITICAL = "critical"
    
    
    
    class QueryValidator:
    
        """Validate and analyze SQL queries for security risks."""
    
        
    
        # Dangerous SQL keywords and patterns
    
        DANGEROUS_KEYWORDS = {
    
            'DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE', 'INSERT',
    
            'UPDATE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE', 'sp_',
    
            'xp_', 'BULK', 'OPENROWSET', 'OPENDATASOURCE'
    
        }
    
        
    
        # Allowed read-only operations
    
        SAFE_KEYWORDS = {
    
            'SELECT', 'WITH', 'UNION', 'ORDER', 'GROUP', 'HAVING',
    
            'WHERE', 'FROM', 'JOIN', 'AS', 'ON', 'IN', 'EXISTS',
    
            'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'AND', 'OR', 'NOT'
    
        }
    
        
    
        # Allowed schemas and tables
    
        ALLOWED_SCHEMAS = {'retail', 'information_schema', 'pg_catalog'}
    
        ALLOWED_TABLES = {
    
            'customers', 'products', 'sales_transactions', 
    
            'sales_transaction_items', 'product_categories',
    
            'product_embeddings', 'stores'
    
        }
    
        
    
        def __init__(self):
    
            self.injection_patterns = [
    
                # SQL injection patterns
    
                r"(\b(UNION|union)\s+(ALL\s+)?(SELECT|select))",
    
                r"(\b(DROP|drop)\s+(TABLE|table|DATABASE|database))",
    
                r"(\b(DELETE|delete)\s+(FROM|from))",
    
                r"(\b(INSERT|insert)\s+(INTO|into))",
    
                r"(\b(UPDATE|update)\s+\w+\s+(SET|set))",
    
                r"(\b(EXEC|exec|EXECUTE|execute)\s*\()",
    
                r"(\b(sp_|xp_)\w+)",
    
                r"(--\s*$)",  # SQL comments
    
                r"(/\*.*?\*/)",  # Block comments
    
                r"(;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER))",
    
                r"(\bOR\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)",  # OR injection
    
                r"(\bAND\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)",  # AND injection
    
            ]
    
            
    
            self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.injection_patterns]
    
        
    
        def validate_query(self, query: str) -> Dict[str, Any]:
    
            """Comprehensive query validation."""
    
            
    
            validation_result = {
    
                'is_safe': True,
    
                'risk_level': QueryRisk.LOW,
    
                'issues': [],
    
                'warnings': [],
    
                'allowed_operations': [],
    
                'metadata': {}
    
            }
    
            
    
            try:
    
                # Parse the query
    
                parsed = sqlparse.parse(query)
    
                
    
                if not parsed:
    
                    validation_result['is_safe'] = False
    
                    validation_result['issues'].append("Unable to parse query")
    
                    validation_result['risk_level'] = QueryRisk.HIGH
    
                    return validation_result
    
                
    
                # Analyze each statement
    
                for statement in parsed:
    
                    self._analyze_statement(statement, validation_result)
    
                
    
                # Check for injection patterns
    
                self._check_injection_patterns(query, validation_result)
    
                
    
                # Validate table/schema access
    
                self._validate_table_access(query, validation_result)
    
                
    
                # Determine final risk level
    
                self._determine_risk_level(validation_result)
    
                
    
            except Exception as e:
    
                validation_result['is_safe'] = False
    
                validation_result['issues'].append(f"Query analysis failed: {str(e)}")
    
                validation_result['risk_level'] = QueryRisk.CRITICAL
    
            
    
            return validation_result
    
        
    
        def _analyze_statement(self, statement, validation_result):
    
            """Analyze individual SQL statement."""
    
            
    
            # Get statement type
    
            stmt_type = statement.get_type()
    
            
    
            # Check if statement type is allowed
    
            if stmt_type and stmt_type.upper() not in ['SELECT', 'WITH']:
    
                validation_result['issues'].append(f"Disallowed statement type: {stmt_type}")
    
                validation_result['is_safe'] = False
    
                return
    
            
    
            # Extract tokens and analyze
    
            for token in statement.flatten():
    
                if token.ttype is sqlparse.tokens.Keyword:
    
                    keyword = token.value.upper()
    
                    
    
                    if keyword in self.DANGEROUS_KEYWORDS:
    
                        validation_result['issues'].append(f"Dangerous keyword detected: {keyword}")
    
                        validation_result['is_safe'] = False
    
                    elif keyword in self.SAFE_KEYWORDS:
    
                        if keyword not in validation_result['allowed_operations']:
    
                            validation_result['allowed_operations'].append(keyword)
    
        
    
        def _check_injection_patterns(self, query: str, validation_result):
    
            """Check for SQL injection patterns."""
    
            
    
            for pattern in self.compiled_patterns:
    
                matches = pattern.findall(query)
    
                if matches:
    
                    validation_result['issues'].append(f"Potential injection pattern detected")
    
                    validation_result['is_safe'] = False
    
        
    
        def _validate_table_access(self, query: str, validation_result):
    
            """Validate that only allowed tables/schemas are accessed."""
    
            
    
            # Extract table names (simplified approach)
    
            # In production, use proper SQL parsing
    
            from_match = re.findall(r'FROM\s+(\w+\.?\w*)', query, re.IGNORECASE)
    
            join_match = re.findall(r'JOIN\s+(\w+\.?\w*)', query, re.IGNORECASE)
    
            
    
            all_tables = from_match + join_match
    
            
    
            for table_ref in all_tables:
    
                if '.' in table_ref:
    
                    schema, table = table_ref.split('.', 1)
    
                    if schema.lower() not in self.ALLOWED_SCHEMAS:
    
                        validation_result['issues'].append(f"Access to unauthorized schema: {schema}")
    
                        validation_result['is_safe'] = False
    
                    if table.lower() not in self.ALLOWED_TABLES:
    
                        validation_result['warnings'].append(f"Access to table: {table}")
    
                else:
    
                    # Assume retail schema if not specified
    
                    if table_ref.lower() not in self.ALLOWED_TABLES:
    
                        validation_result['warnings'].append(f"Access to table: {table_ref}")
    
        
    
        def _determine_risk_level(self, validation_result):
    
            """Determine overall risk level."""
    
            
    
            if not validation_result['is_safe']:
    
                if any('injection' in issue.lower() for issue in validation_result['issues']):
    
                    validation_result['risk_level'] = QueryRisk.CRITICAL
    
                elif any('DROP' in issue or 'DELETE' in issue for issue in validation_result['issues']):
    
                    validation_result['risk_level'] = QueryRisk.HIGH
    
                else:
    
                    validation_result['risk_level'] = QueryRisk.MEDIUM
    
            elif validation_result['warnings']:
    
                validation_result['risk_level'] = QueryRisk.LOW
    
            else:
    
                validation_result['risk_level'] = QueryRisk.LOW
    
    
    
    # Global validator instance
    
    query_validator = QueryValidator()
    
    

    ๐Ÿ—ƒ๏ธ Database Query Tools

    Sales Analysis Tool

    
    # mcp_server/tools/sales_analysis.py
    
    """
    
    Comprehensive sales analysis tool for retail data querying.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from datetime import datetime, timedelta
    
    from .base import DatabaseTool, ToolResult
    
    from .query_validator import query_validator
    
    
    
    class SalesAnalysisTool(DatabaseTool):
    
        """Advanced sales analysis and reporting tool."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="execute_sales_query",
    
                description="Execute sophisticated sales analysis queries with natural language support",
    
                db_provider=db_provider
    
            )
    
            
    
            # Pre-built query templates for common analysis
    
            self.query_templates = {
    
                'daily_sales': """
    
                    SELECT 
    
                        DATE(transaction_date) as sales_date,
    
                        COUNT(*) as transaction_count,
    
                        SUM(total_amount) as total_revenue,
    
                        AVG(total_amount) as avg_transaction_value,
    
                        COUNT(DISTINCT customer_id) as unique_customers
    
                    FROM retail.sales_transactions 
    
                    WHERE transaction_date >= $1 AND transaction_date <= $2
    
                      AND transaction_type = 'sale'
    
                    GROUP BY DATE(transaction_date)
    
                    ORDER BY sales_date DESC
    
                """,
    
                
    
                'top_products': """
    
                    SELECT 
    
                        p.product_name,
    
                        p.brand,
    
                        SUM(sti.quantity) as total_quantity_sold,
    
                        SUM(sti.total_price) as total_revenue,
    
                        COUNT(DISTINCT st.transaction_id) as transaction_count,
    
                        AVG(sti.unit_price) as avg_price
    
                    FROM retail.sales_transaction_items sti
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY p.product_id, p.product_name, p.brand
    
                    ORDER BY total_revenue DESC
    
                    LIMIT $3
    
                """,
    
                
    
                'customer_analysis': """
    
                    SELECT 
    
                        c.customer_id,
    
                        c.first_name || ' ' || c.last_name as customer_name,
    
                        c.loyalty_tier,
    
                        COUNT(st.transaction_id) as transaction_count,
    
                        SUM(st.total_amount) as total_spent,
    
                        AVG(st.total_amount) as avg_transaction_value,
    
                        MAX(st.transaction_date) as last_purchase_date,
    
                        DATE_PART('day', CURRENT_DATE - MAX(st.transaction_date)) as days_since_last_purchase
    
                    FROM retail.customers c
    
                    LEFT JOIN retail.sales_transactions st ON c.customer_id = st.customer_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY c.customer_id, c.first_name, c.last_name, c.loyalty_tier
    
                    HAVING COUNT(st.transaction_id) > 0
    
                    ORDER BY total_spent DESC
    
                    LIMIT $3
    
                """,
    
                
    
                'category_performance': """
    
                    SELECT 
    
                        pc.category_name,
    
                        COUNT(DISTINCT p.product_id) as unique_products,
    
                        SUM(sti.quantity) as total_quantity_sold,
    
                        SUM(sti.total_price) as total_revenue,
    
                        AVG(sti.unit_price) as avg_price,
    
                        COUNT(DISTINCT st.transaction_id) as transaction_count
    
                    FROM retail.product_categories pc
    
                    JOIN retail.products p ON pc.category_id = p.category_id
    
                    JOIN retail.sales_transaction_items sti ON p.product_id = sti.product_id
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY pc.category_id, pc.category_name
    
                    ORDER BY total_revenue DESC
    
                """,
    
                
    
                'sales_trends': """
    
                    WITH daily_sales AS (
    
                        SELECT 
    
                            DATE(transaction_date) as sales_date,
    
                            SUM(total_amount) as daily_revenue,
    
                            COUNT(*) as daily_transactions
    
                        FROM retail.sales_transactions 
    
                        WHERE transaction_date >= $1 AND transaction_date <= $2
    
                          AND transaction_type = 'sale'
    
                        GROUP BY DATE(transaction_date)
    
                    ),
    
                    trend_analysis AS (
    
                        SELECT 
    
                            sales_date,
    
                            daily_revenue,
    
                            daily_transactions,
    
                            LAG(daily_revenue, 1) OVER (ORDER BY sales_date) as prev_day_revenue,
    
                            LAG(daily_revenue, 7) OVER (ORDER BY sales_date) as prev_week_revenue,
    
                            AVG(daily_revenue) OVER (
    
                                ORDER BY sales_date 
    
                                ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    
                            ) as rolling_7day_avg
    
                        FROM daily_sales
    
                    )
    
                    SELECT 
    
                        sales_date,
    
                        daily_revenue,
    
                        daily_transactions,
    
                        rolling_7day_avg,
    
                        CASE 
    
                            WHEN prev_day_revenue IS NOT NULL THEN
    
                                ROUND(((daily_revenue - prev_day_revenue) / prev_day_revenue * 100)::numeric, 2)
    
                            ELSE NULL
    
                        END as day_over_day_growth_pct,
    
                        CASE 
    
                            WHEN prev_week_revenue IS NOT NULL THEN
    
                                ROUND(((daily_revenue - prev_week_revenue) / prev_week_revenue * 100)::numeric, 2)
    
                            ELSE NULL
    
                        END as week_over_week_growth_pct
    
                    FROM trend_analysis
    
                    ORDER BY sales_date DESC
    
                """
    
            }
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute sales analysis query."""
    
            
    
            query_type = kwargs.get('query_type', 'custom')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for sales analysis"
    
                )
    
            
    
            try:
    
                if query_type in self.query_templates:
    
                    return await self._execute_template_query(query_type, kwargs)
    
                elif query_type == 'custom':
    
                    return await self._execute_custom_query(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown query type: {query_type}"
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Sales analysis failed: {str(e)}"
    
                )
    
        
    
        async def _execute_template_query(self, query_type: str, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Execute pre-built template query."""
    
            
    
            query = self.query_templates[query_type]
    
            store_id = kwargs['store_id']
    
            
    
            # Default parameters for template queries
    
            start_date = kwargs.get('start_date', (datetime.now() - timedelta(days=30)).date())
    
            end_date = kwargs.get('end_date', datetime.now().date())
    
            limit = kwargs.get('limit', 20)
    
            
    
            # Convert string dates if needed
    
            if isinstance(start_date, str):
    
                start_date = datetime.fromisoformat(start_date).date()
    
            if isinstance(end_date, str):
    
                end_date = datetime.fromisoformat(end_date).date()
    
            
    
            # Execute query with parameters
    
            params = (start_date, end_date, limit) if '$3' in query else (start_date, end_date)
    
            
    
            result = await self.execute_query(query, params, store_id)
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'query_type': query_type,
    
                    'date_range': f"{start_date} to {end_date}",
    
                    'store_id': store_id,
    
                    'analysis_type': 'template'
    
                }
    
            
    
            return result
    
        
    
        async def _execute_custom_query(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Execute custom SQL query with validation."""
    
            
    
            custom_query = kwargs.get('query')
    
            store_id = kwargs['store_id']
    
            
    
            if not custom_query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Custom query is required when query_type is 'custom'"
    
                )
    
            
    
            # Validate the query for security
    
            validation = query_validator.validate_query(custom_query)
    
            
    
            if not validation['is_safe']:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Query validation failed: {', '.join(validation['issues'])}",
    
                    metadata={
    
                        'validation_result': validation,
    
                        'risk_level': validation['risk_level'].value
    
                    }
    
                )
    
            
    
            # Execute validated query
    
            result = await self.execute_query(custom_query, None, store_id)
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'validation_warnings': validation.get('warnings', []),
    
                    'analysis_type': 'custom'
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for the sales analysis tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query_type": {
    
                        "type": "string",
    
                        "enum": list(self.query_templates.keys()) + ["custom"],
    
                        "description": "Type of sales analysis to perform",
    
                        "default": "daily_sales"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for data isolation",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "start_date": {
    
                        "type": "string",
    
                        "format": "date",
    
                        "description": "Start date for analysis (YYYY-MM-DD)"
    
                    },
    
                    "end_date": {
    
                        "type": "string",
    
                        "format": "date",
    
                        "description": "End date for analysis (YYYY-MM-DD)"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "minimum": 1,
    
                        "maximum": 1000,
    
                        "description": "Maximum number of results to return",
    
                        "default": 20
    
                    },
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Custom SQL query (required when query_type is 'custom')"
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    

    Schema Introspection Tool

    
    # mcp_server/tools/schema_introspection.py
    
    """
    
    Database schema introspection and metadata tools.
    
    """
    
    from typing import Dict, Any, List
    
    from .base import DatabaseTool, ToolResult, ToolCategory
    
    
    
    class SchemaIntrospectionTool(DatabaseTool):
    
        """Tool for exploring database schema and metadata."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_table_schema",
    
                description="Get detailed schema information for database tables",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.SCHEMA_INTROSPECTION
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute schema introspection."""
    
            
    
            table_name = kwargs.get('table_name')
    
            include_constraints = kwargs.get('include_constraints', True)
    
            include_indexes = kwargs.get('include_indexes', True)
    
            include_statistics = kwargs.get('include_statistics', False)
    
            
    
            try:
    
                if table_name:
    
                    return await self._get_single_table_schema(
    
                        table_name, include_constraints, include_indexes, include_statistics
    
                    )
    
                else:
    
                    return await self._get_all_tables_schema(include_constraints, include_indexes)
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Schema introspection failed: {str(e)}"
    
                )
    
        
    
        async def _get_single_table_schema(
    
            self, 
    
            table_name: str, 
    
            include_constraints: bool,
    
            include_indexes: bool,
    
            include_statistics: bool
    
        ) -> ToolResult:
    
            """Get detailed schema for a single table."""
    
            
    
            schema_info = {
    
                'table_name': table_name,
    
                'columns': [],
    
                'constraints': [],
    
                'indexes': [],
    
                'statistics': {}
    
            }
    
            
    
            async with self.get_connection() as conn:
    
                # Get column information
    
                columns_query = """
    
                    SELECT 
    
                        column_name,
    
                        data_type,
    
                        is_nullable,
    
                        column_default,
    
                        character_maximum_length,
    
                        numeric_precision,
    
                        numeric_scale,
    
                        ordinal_position,
    
                        udt_name
    
                    FROM information_schema.columns
    
                    WHERE table_schema = 'retail' AND table_name = $1
    
                    ORDER BY ordinal_position
    
                """
    
                
    
                columns = await conn.fetch(columns_query, table_name)
    
                schema_info['columns'] = [dict(col) for col in columns]
    
                
    
                # Get constraints if requested
    
                if include_constraints:
    
                    constraints_query = """
    
                        SELECT 
    
                            constraint_name,
    
                            constraint_type,
    
                            column_name,
    
                            foreign_table_name,
    
                            foreign_column_name
    
                        FROM information_schema.table_constraints tc
    
                        LEFT JOIN information_schema.key_column_usage kcu 
    
                            ON tc.constraint_name = kcu.constraint_name
    
                        LEFT JOIN information_schema.referential_constraints rc
    
                            ON tc.constraint_name = rc.constraint_name
    
                        LEFT JOIN information_schema.key_column_usage fkcu
    
                            ON rc.unique_constraint_name = fkcu.constraint_name
    
                        WHERE tc.table_schema = 'retail' AND tc.table_name = $1
    
                    """
    
                    
    
                    constraints = await conn.fetch(constraints_query, table_name)
    
                    schema_info['constraints'] = [dict(const) for const in constraints]
    
                
    
                # Get indexes if requested
    
                if include_indexes:
    
                    indexes_query = """
    
                        SELECT 
    
                            indexname as index_name,
    
                            indexdef as index_definition,
    
                            tablespace
    
                        FROM pg_indexes
    
                        WHERE schemaname = 'retail' AND tablename = $1
    
                    """
    
                    
    
                    indexes = await conn.fetch(indexes_query, table_name)
    
                    schema_info['indexes'] = [dict(idx) for idx in indexes]
    
                
    
                # Get table statistics if requested
    
                if include_statistics:
    
                    stats_query = """
    
                        SELECT 
    
                            n_tup_ins as inserts,
    
                            n_tup_upd as updates,
    
                            n_tup_del as deletes,
    
                            n_live_tup as live_tuples,
    
                            n_dead_tup as dead_tuples,
    
                            last_vacuum,
    
                            last_autovacuum,
    
                            last_analyze,
    
                            last_autoanalyze
    
                        FROM pg_stat_user_tables
    
                        WHERE schemaname = 'retail' AND relname = $1
    
                    """
    
                    
    
                    stats = await conn.fetchrow(stats_query, table_name)
    
                    if stats:
    
                        schema_info['statistics'] = dict(stats)
    
            
    
            return ToolResult(
    
                success=True,
    
                data=schema_info,
    
                metadata={
    
                    'table_name': table_name,
    
                    'schema': 'retail',
    
                    'introspection_type': 'single_table'
    
                }
    
            )
    
        
    
        async def _get_all_tables_schema(
    
            self, 
    
            include_constraints: bool,
    
            include_indexes: bool
    
        ) -> ToolResult:
    
            """Get schema information for all tables."""
    
            
    
            async with self.get_connection() as conn:
    
                # Get all tables in retail schema
    
                tables_query = """
    
                    SELECT 
    
                        table_name,
    
                        table_type
    
                    FROM information_schema.tables
    
                    WHERE table_schema = 'retail'
    
                    ORDER BY table_name
    
                """
    
                
    
                tables = await conn.fetch(tables_query)
    
                schema_info = {
    
                    'schema_name': 'retail',
    
                    'tables': []
    
                }
    
                
    
                for table in tables:
    
                    table_info = {
    
                        'table_name': table['table_name'],
    
                        'table_type': table['table_type'],
    
                        'columns': []
    
                    }
    
                    
    
                    # Get columns for each table
    
                    columns_query = """
    
                        SELECT 
    
                            column_name,
    
                            data_type,
    
                            is_nullable,
    
                            column_default
    
                        FROM information_schema.columns
    
                        WHERE table_schema = 'retail' AND table_name = $1
    
                        ORDER BY ordinal_position
    
                    """
    
                    
    
                    columns = await conn.fetch(columns_query, table['table_name'])
    
                    table_info['columns'] = [dict(col) for col in columns]
    
                    
    
                    schema_info['tables'].append(table_info)
    
            
    
            return ToolResult(
    
                success=True,
    
                data=schema_info,
    
                metadata={
    
                    'schema': 'retail',
    
                    'table_count': len(schema_info['tables']),
    
                    'introspection_type': 'all_tables'
    
                }
    
            )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for schema introspection tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "table_name": {
    
                        "type": "string",
    
                        "description": "Specific table name to introspect (optional - if not provided, all tables are returned)",
    
                        "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
    
                    },
    
                    "include_constraints": {
    
                        "type": "boolean",
    
                        "description": "Include constraint information",
    
                        "default": True
    
                    },
    
                    "include_indexes": {
    
                        "type": "boolean",
    
                        "description": "Include index information",
    
                        "default": True
    
                    },
    
                    "include_statistics": {
    
                        "type": "boolean",
    
                        "description": "Include table statistics",
    
                        "default": False
    
                    }
    
                },
    
                "additionalProperties": False
    
            }
    
    
    
    class MultiTableSchemaTool(DatabaseTool):
    
        """Tool for getting schema information for multiple tables at once."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_multiple_table_schemas",
    
                description="Get schema information for multiple tables efficiently",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.SCHEMA_INTROSPECTION
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute multi-table schema introspection."""
    
            
    
            table_names = kwargs.get('table_names', [])
    
            
    
            if not table_names:
    
                return ToolResult(
    
                    success=False,
    
                    error="At least one table name is required"
    
                )
    
            
    
            try:
    
                schemas = {}
    
                
    
                async with self.get_connection() as conn:
    
                    for table_name in table_names:
    
                        # Get table schema
    
                        schema_query = """
    
                            SELECT 
    
                                c.column_name,
    
                                c.data_type,
    
                                c.is_nullable,
    
                                c.column_default,
    
                                c.character_maximum_length,
    
                                tc.constraint_type,
    
                                kcu.constraint_name
    
                            FROM information_schema.columns c
    
                            LEFT JOIN information_schema.key_column_usage kcu
    
                                ON c.table_name = kcu.table_name 
    
                                AND c.column_name = kcu.column_name
    
                                AND c.table_schema = kcu.table_schema
    
                            LEFT JOIN information_schema.table_constraints tc
    
                                ON kcu.constraint_name = tc.constraint_name
    
                                AND kcu.table_schema = tc.table_schema
    
                            WHERE c.table_schema = 'retail' AND c.table_name = $1
    
                            ORDER BY c.ordinal_position
    
                        """
    
                        
    
                        columns = await conn.fetch(schema_query, table_name)
    
                        
    
                        if columns:
    
                            schemas[table_name] = {
    
                                'table_name': table_name,
    
                                'columns': [dict(col) for col in columns]
    
                            }
    
                        else:
    
                            schemas[table_name] = {
    
                                'table_name': table_name,
    
                                'error': 'Table not found or not accessible'
    
                            }
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=schemas,
    
                    metadata={
    
                        'requested_tables': table_names,
    
                        'found_tables': [name for name, info in schemas.items() if 'error' not in info],
    
                        'missing_tables': [name for name, info in schemas.items() if 'error' in info]
    
                    }
    
                )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Multi-table schema introspection failed: {str(e)}"
    
                )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for multi-table schema tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "table_names": {
    
                        "type": "array",
    
                        "items": {
    
                            "type": "string",
    
                            "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
    
                        },
    
                        "description": "List of table names to get schema information for",
    
                        "minItems": 1,
    
                        "maxItems": 20
    
                    }
    
                },
    
                "required": ["table_names"],
    
                "additionalProperties": False
    
            }
    
    

    ๐Ÿ“Š Analytics and Utility Tools

    Business Intelligence Tool

    
    # mcp_server/tools/business_intelligence.py
    
    """
    
    Advanced business intelligence and analytics tools.
    
    """
    
    from typing import Dict, Any, List
    
    from datetime import datetime, timedelta
    
    from .base import DatabaseTool, ToolResult, ToolCategory
    
    
    
    class BusinessIntelligenceTool(DatabaseTool):
    
        """Advanced analytics tool for business intelligence queries."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="generate_business_insights",
    
                description="Generate comprehensive business intelligence reports and insights",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.ANALYTICS
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute business intelligence analysis."""
    
            
    
            analysis_type = kwargs.get('analysis_type', 'summary')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for business intelligence analysis"
    
                )
    
            
    
            try:
    
                if analysis_type == 'summary':
    
                    return await self._generate_business_summary(kwargs)
    
                elif analysis_type == 'customer_segmentation':
    
                    return await self._analyze_customer_segmentation(kwargs)
    
                elif analysis_type == 'product_performance':
    
                    return await self._analyze_product_performance(kwargs)
    
                elif analysis_type == 'seasonal_trends':
    
                    return await self._analyze_seasonal_trends(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown analysis type: {analysis_type}"
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Business intelligence analysis failed: {str(e)}"
    
                )
    
        
    
        async def _generate_business_summary(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Generate comprehensive business summary."""
    
            
    
            store_id = kwargs['store_id']
    
            days = kwargs.get('days', 30)
    
            
    
            summary_query = """
    
                WITH date_range AS (
    
                    SELECT CURRENT_DATE - INTERVAL '%s days' as start_date,
    
                           CURRENT_DATE as end_date
    
                ),
    
                sales_summary AS (
    
                    SELECT 
    
                        COUNT(*) as total_transactions,
    
                        COUNT(DISTINCT customer_id) as unique_customers,
    
                        SUM(total_amount) as total_revenue,
    
                        AVG(total_amount) as avg_transaction_value,
    
                        COUNT(DISTINCT DATE(transaction_date)) as active_days
    
                    FROM retail.sales_transactions st, date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                product_summary AS (
    
                    SELECT 
    
                        COUNT(DISTINCT p.product_id) as products_sold,
    
                        SUM(sti.quantity) as total_items_sold
    
                    FROM retail.sales_transaction_items sti
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    CROSS JOIN date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                top_category AS (
    
                    SELECT 
    
                        pc.category_name,
    
                        SUM(sti.total_price) as category_revenue
    
                    FROM retail.product_categories pc
    
                    JOIN retail.products p ON pc.category_id = p.category_id
    
                    JOIN retail.sales_transaction_items sti ON p.product_id = sti.product_id
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    CROSS JOIN date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY pc.category_name
    
                    ORDER BY category_revenue DESC
    
                    LIMIT 1
    
                )
    
                SELECT 
    
                    ss.*,
    
                    ps.products_sold,
    
                    ps.total_items_sold,
    
                    tc.category_name as top_category,
    
                    tc.category_revenue as top_category_revenue,
    
                    CASE 
    
                        WHEN ss.active_days > 0 THEN ss.total_revenue / ss.active_days
    
                        ELSE 0
    
                    END as avg_daily_revenue
    
                FROM sales_summary ss
    
                CROSS JOIN product_summary ps
    
                CROSS JOIN top_category tc
    
            """ % days
    
            
    
            result = await self.execute_query(summary_query, None, store_id)
    
            
    
            if result.success and result.data:
    
                summary = result.data[0]
    
                
    
                # Add derived insights
    
                insights = {
    
                    'revenue_trend': 'stable',  # Would calculate based on historical data
    
                    'customer_retention': f"{summary.get('unique_customers', 0)} active customers",
    
                    'performance_indicators': {
    
                        'transactions_per_day': round(summary.get('total_transactions', 0) / max(summary.get('active_days', 1), 1), 2),
    
                        'revenue_per_customer': round(summary.get('total_revenue', 0) / max(summary.get('unique_customers', 1), 1), 2),
    
                        'items_per_transaction': round(summary.get('total_items_sold', 0) / max(summary.get('total_transactions', 1), 1), 2)
    
                    }
    
                }
    
                
    
                summary['insights'] = insights
    
                
    
                result.data = [summary]
    
                result.metadata = {
    
                    'analysis_type': 'business_summary',
    
                    'period_days': days,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for business intelligence tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "analysis_type": {
    
                        "type": "string",
    
                        "enum": ["summary", "customer_segmentation", "product_performance", "seasonal_trends"],
    
                        "description": "Type of business intelligence analysis to perform",
    
                        "default": "summary"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for analysis",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "days": {
    
                        "type": "integer",
    
                        "minimum": 1,
    
                        "maximum": 365,
    
                        "description": "Number of days to analyze",
    
                        "default": 30
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    
    
    class UtilityTool(DatabaseTool):
    
        """Utility tool for common operations."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_current_utc_date",
    
                description="Get current UTC date and time for reference",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.UTILITY
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute utility operation."""
    
            
    
            format_type = kwargs.get('format', 'iso')
    
            
    
            try:
    
                async with self.get_connection() as conn:
    
                    if format_type == 'iso':
    
                        query = "SELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC' as current_utc_datetime"
    
                    elif format_type == 'epoch':
    
                        query = "SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC') as current_utc_epoch"
    
                    elif format_type == 'date_only':
    
                        query = "SELECT CURRENT_DATE as current_date"
    
                    else:
    
                        return ToolResult(
    
                            success=False,
    
                            error=f"Unknown format type: {format_type}"
    
                        )
    
                    
    
                    result = await conn.fetchrow(query)
    
                    
    
                    return ToolResult(
    
                        success=True,
    
                        data=dict(result),
    
                        metadata={
    
                            'format_type': format_type,
    
                            'timezone': 'UTC'
    
                        }
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Utility operation failed: {str(e)}"
    
                )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for utility tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "format": {
    
                        "type": "string",
    
                        "enum": ["iso", "epoch", "date_only"],
    
                        "description": "Format for the returned date/time",
    
                        "default": "iso"
    
                    }
    
                },
    
                "additionalProperties": False
    
            }
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Advanced Tool Architecture: Implemented sophisticated MCP tools with comprehensive error handling

    โœ… Query Validation: Built secure SQL validation to prevent injection attacks

    โœ… Database Tools: Created powerful sales analysis and schema introspection capabilities

    โœ… Business Intelligence: Developed analytics tools for comprehensive business insights

    โœ… Performance Optimization: Applied caching, connection pooling, and query optimization

    โœ… Security Integration: Implemented role-based access control and audit logging

    ๐Ÿš€ What's Next

    Continue with Lab 07: Semantic Search Integration to:

  • Integrate vector search capabilities with MCP tools
  • Build semantic product search functionality
  • Implement AI-powered query understanding
  • Create hybrid search combining traditional and vector queries
  • ๐Ÿ“š Additional Resources

    MCP Tool Development

  • Model Context Protocol Documentation - Official MCP specification
  • FastMCP Framework - Python MCP implementation
  • MCP Tool Patterns - Example tool implementations
  • Database Security

  • SQL Injection Prevention - OWASP security guide
  • PostgreSQL Security - Database security best practices
  • Query Validation Techniques - Secure query patterns
  • Performance Optimization

  • Database Query Optimization - PostgreSQL performance guide
  • Connection Pooling Best Practices - Connection management
  • Async Python Patterns - Asynchronous programming guide
  • ---

    Previous: Lab 05: MCP Server Implementation

    Next: Lab 07: Semantic Search Integration

    Creating database query tools and schema introspection Build

    Tool Development

    ๐ŸŽฏ What This Lab Covers

    This lab dives deep into creating sophisticated MCP tools that provide AI assistants with powerful database query capabilities, schema introspection, and analytics functions.

    You'll learn to build tools that are both powerful and safe, with comprehensive error handling and performance optimization.

    Overview

    MCP tools are the interface between AI assistants and your data systems.

    Well-designed tools provide structured, validated access to complex operations while maintaining security and performance.

    This lab covers the complete lifecycle of tool development from design to deployment.

    Our retail MCP server implements a comprehensive suite of tools that enable natural language querying of sales data, product catalogs, and business analytics while maintaining strict security boundaries and optimal performance.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Design advanced MCP tools with complex parameter validation
  • Implement secure database query tools with SQL injection protection
  • Create schema introspection capabilities for dynamic queries
  • Build custom analytics tools for business intelligence
  • Apply comprehensive error handling and graceful degradation
  • Optimize tool performance for production workloads
  • ๐Ÿ› ๏ธ Core Tool Architecture

    Tool Design Principles

    
    # mcp_server/tools/base.py
    
    """
    
    Base classes and patterns for MCP tool development.
    
    """
    
    from abc import ABC, abstractmethod
    
    from typing import Any, Dict, List, Optional, Union
    
    from dataclasses import dataclass
    
    from enum import Enum
    
    import asyncio
    
    import time
    
    import logging
    
    from contextlib import asynccontextmanager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ToolCategory(Enum):
    
        """Tool categorization for organization and discovery."""
    
        DATABASE_QUERY = "database_query"
    
        SCHEMA_INTROSPECTION = "schema_introspection"
    
        ANALYTICS = "analytics"
    
        UTILITY = "utility"
    
        ADMINISTRATIVE = "administrative"
    
    
    
    @dataclass
    
    class ToolResult:
    
        """Standardized tool result structure."""
    
        success: bool
    
        data: Any = None
    
        error: Optional[str] = None
    
        metadata: Optional[Dict[str, Any]] = None
    
        execution_time_ms: Optional[float] = None
    
        row_count: Optional[int] = None
    
    
    
    class BaseTool(ABC):
    
        """Abstract base class for all MCP tools."""
    
        
    
        def __init__(self, name: str, description: str, category: ToolCategory):
    
            self.name = name
    
            self.description = description
    
            self.category = category
    
            self.call_count = 0
    
            self.total_execution_time = 0.0
    
            
    
        @abstractmethod
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute the tool with given parameters."""
    
            pass
    
        
    
        @abstractmethod
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get JSON schema for tool input validation."""
    
            pass
    
        
    
        async def call(self, **kwargs) -> ToolResult:
    
            """Wrapper for tool execution with metrics and error handling."""
    
            
    
            start_time = time.time()
    
            self.call_count += 1
    
            
    
            try:
    
                # Validate input parameters
    
                self._validate_input(kwargs)
    
                
    
                # Log tool execution
    
                logger.info(
    
                    f"Executing tool: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'tool_category': self.category.value,
    
                        'parameters': self._sanitize_parameters(kwargs)
    
                    }
    
                )
    
                
    
                # Execute the tool
    
                result = await self.execute(**kwargs)
    
                
    
                # Record execution time
    
                execution_time = (time.time() - start_time) * 1000
    
                result.execution_time_ms = execution_time
    
                self.total_execution_time += execution_time
    
                
    
                # Log success
    
                logger.info(
    
                    f"Tool execution completed: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'execution_time_ms': execution_time,
    
                        'success': result.success,
    
                        'row_count': result.row_count
    
                    }
    
                )
    
                
    
                return result
    
                
    
            except Exception as e:
    
                execution_time = (time.time() - start_time) * 1000
    
                
    
                logger.error(
    
                    f"Tool execution failed: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'execution_time_ms': execution_time,
    
                        'error': str(e)
    
                    },
    
                    exc_info=True
    
                )
    
                
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Tool execution failed: {str(e)}",
    
                    execution_time_ms=execution_time
    
                )
    
        
    
        def _validate_input(self, kwargs: Dict[str, Any]):
    
            """Validate input parameters against schema."""
    
            
    
            schema = self.get_input_schema()
    
            required_props = schema.get('required', [])
    
            properties = schema.get('properties', {})
    
            
    
            # Check required parameters
    
            missing_required = [prop for prop in required_props if prop not in kwargs]
    
            if missing_required:
    
                raise ValueError(f"Missing required parameters: {missing_required}")
    
            
    
            # Type validation would go here
    
            # For production, use jsonschema library for comprehensive validation
    
        
    
        def _sanitize_parameters(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
    
            """Sanitize parameters for logging (remove sensitive data)."""
    
            
    
            # Remove or mask sensitive parameters
    
            sanitized = kwargs.copy()
    
            sensitive_keys = ['password', 'token', 'secret', 'key']
    
            
    
            for key in sanitized:
    
                if any(sensitive in key.lower() for sensitive in sensitive_keys):
    
                    sanitized[key] = "***MASKED***"
    
            
    
            return sanitized
    
        
    
        def get_statistics(self) -> Dict[str, Any]:
    
            """Get tool usage statistics."""
    
            
    
            return {
    
                'name': self.name,
    
                'category': self.category.value,
    
                'call_count': self.call_count,
    
                'total_execution_time_ms': self.total_execution_time,
    
                'average_execution_time_ms': (
    
                    self.total_execution_time / self.call_count 
    
                    if self.call_count > 0 else 0
    
                )
    
            }
    
    
    
    class DatabaseTool(BaseTool):
    
        """Base class for database-related tools."""
    
        
    
        def __init__(self, name: str, description: str, db_provider):
    
            super().__init__(name, description, ToolCategory.DATABASE_QUERY)
    
            self.db_provider = db_provider
    
        
    
        @asynccontextmanager
    
        async def get_connection(self):
    
            """Get database connection with proper context management."""
    
            
    
            conn = None
    
            try:
    
                conn = await self.db_provider.get_connection()
    
                yield conn
    
            finally:
    
                if conn:
    
                    await self.db_provider.release_connection(conn)
    
        
    
        async def execute_query(
    
            self, 
    
            query: str, 
    
            params: tuple = None,
    
            store_id: str = None
    
        ) -> ToolResult:
    
            """Execute database query with security and performance monitoring."""
    
            
    
            async with self.get_connection() as conn:
    
                try:
    
                    # Set store context if provided
    
                    if store_id:
    
                        await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Execute query
    
                    start_time = time.time()
    
                    
    
                    if params:
    
                        rows = await conn.fetch(query, *params)
    
                    else:
    
                        rows = await conn.fetch(query)
    
                    
    
                    execution_time = (time.time() - start_time) * 1000
    
                    
    
                    # Convert rows to dictionaries
    
                    data = [dict(row) for row in rows]
    
                    
    
                    return ToolResult(
    
                        success=True,
    
                        data=data,
    
                        row_count=len(data),
    
                        execution_time_ms=execution_time
    
                    )
    
                    
    
                except Exception as e:
    
                    logger.error(f"Database query failed: {str(e)}")
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Query execution failed: {str(e)}"
    
                    )
    
    

    Query Validation and Security

    
    # mcp_server/tools/query_validator.py
    
    """
    
    SQL query validation and security for MCP tools.
    
    """
    
    import re
    
    import sqlparse
    
    from typing import List, Dict, Any, Set
    
    from enum import Enum
    
    
    
    class QueryRisk(Enum):
    
        """Query risk levels."""
    
        LOW = "low"
    
        MEDIUM = "medium"
    
        HIGH = "high"
    
        CRITICAL = "critical"
    
    
    
    class QueryValidator:
    
        """Validate and analyze SQL queries for security risks."""
    
        
    
        # Dangerous SQL keywords and patterns
    
        DANGEROUS_KEYWORDS = {
    
            'DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE', 'INSERT',
    
            'UPDATE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE', 'sp_',
    
            'xp_', 'BULK', 'OPENROWSET', 'OPENDATASOURCE'
    
        }
    
        
    
        # Allowed read-only operations
    
        SAFE_KEYWORDS = {
    
            'SELECT', 'WITH', 'UNION', 'ORDER', 'GROUP', 'HAVING',
    
            'WHERE', 'FROM', 'JOIN', 'AS', 'ON', 'IN', 'EXISTS',
    
            'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'AND', 'OR', 'NOT'
    
        }
    
        
    
        # Allowed schemas and tables
    
        ALLOWED_SCHEMAS = {'retail', 'information_schema', 'pg_catalog'}
    
        ALLOWED_TABLES = {
    
            'customers', 'products', 'sales_transactions', 
    
            'sales_transaction_items', 'product_categories',
    
            'product_embeddings', 'stores'
    
        }
    
        
    
        def __init__(self):
    
            self.injection_patterns = [
    
                # SQL injection patterns
    
                r"(\b(UNION|union)\s+(ALL\s+)?(SELECT|select))",
    
                r"(\b(DROP|drop)\s+(TABLE|table|DATABASE|database))",
    
                r"(\b(DELETE|delete)\s+(FROM|from))",
    
                r"(\b(INSERT|insert)\s+(INTO|into))",
    
                r"(\b(UPDATE|update)\s+\w+\s+(SET|set))",
    
                r"(\b(EXEC|exec|EXECUTE|execute)\s*\()",
    
                r"(\b(sp_|xp_)\w+)",
    
                r"(--\s*$)",  # SQL comments
    
                r"(/\*.*?\*/)",  # Block comments
    
                r"(;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER))",
    
                r"(\bOR\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)",  # OR injection
    
                r"(\bAND\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)",  # AND injection
    
            ]
    
            
    
            self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.injection_patterns]
    
        
    
        def validate_query(self, query: str) -> Dict[str, Any]:
    
            """Comprehensive query validation."""
    
            
    
            validation_result = {
    
                'is_safe': True,
    
                'risk_level': QueryRisk.LOW,
    
                'issues': [],
    
                'warnings': [],
    
                'allowed_operations': [],
    
                'metadata': {}
    
            }
    
            
    
            try:
    
                # Parse the query
    
                parsed = sqlparse.parse(query)
    
                
    
                if not parsed:
    
                    validation_result['is_safe'] = False
    
                    validation_result['issues'].append("Unable to parse query")
    
                    validation_result['risk_level'] = QueryRisk.HIGH
    
                    return validation_result
    
                
    
                # Analyze each statement
    
                for statement in parsed:
    
                    self._analyze_statement(statement, validation_result)
    
                
    
                # Check for injection patterns
    
                self._check_injection_patterns(query, validation_result)
    
                
    
                # Validate table/schema access
    
                self._validate_table_access(query, validation_result)
    
                
    
                # Determine final risk level
    
                self._determine_risk_level(validation_result)
    
                
    
            except Exception as e:
    
                validation_result['is_safe'] = False
    
                validation_result['issues'].append(f"Query analysis failed: {str(e)}")
    
                validation_result['risk_level'] = QueryRisk.CRITICAL
    
            
    
            return validation_result
    
        
    
        def _analyze_statement(self, statement, validation_result):
    
            """Analyze individual SQL statement."""
    
            
    
            # Get statement type
    
            stmt_type = statement.get_type()
    
            
    
            # Check if statement type is allowed
    
            if stmt_type and stmt_type.upper() not in ['SELECT', 'WITH']:
    
                validation_result['issues'].append(f"Disallowed statement type: {stmt_type}")
    
                validation_result['is_safe'] = False
    
                return
    
            
    
            # Extract tokens and analyze
    
            for token in statement.flatten():
    
                if token.ttype is sqlparse.tokens.Keyword:
    
                    keyword = token.value.upper()
    
                    
    
                    if keyword in self.DANGEROUS_KEYWORDS:
    
                        validation_result['issues'].append(f"Dangerous keyword detected: {keyword}")
    
                        validation_result['is_safe'] = False
    
                    elif keyword in self.SAFE_KEYWORDS:
    
                        if keyword not in validation_result['allowed_operations']:
    
                            validation_result['allowed_operations'].append(keyword)
    
        
    
        def _check_injection_patterns(self, query: str, validation_result):
    
            """Check for SQL injection patterns."""
    
            
    
            for pattern in self.compiled_patterns:
    
                matches = pattern.findall(query)
    
                if matches:
    
                    validation_result['issues'].append(f"Potential injection pattern detected")
    
                    validation_result['is_safe'] = False
    
        
    
        def _validate_table_access(self, query: str, validation_result):
    
            """Validate that only allowed tables/schemas are accessed."""
    
            
    
            # Extract table names (simplified approach)
    
            # In production, use proper SQL parsing
    
            from_match = re.findall(r'FROM\s+(\w+\.?\w*)', query, re.IGNORECASE)
    
            join_match = re.findall(r'JOIN\s+(\w+\.?\w*)', query, re.IGNORECASE)
    
            
    
            all_tables = from_match + join_match
    
            
    
            for table_ref in all_tables:
    
                if '.' in table_ref:
    
                    schema, table = table_ref.split('.', 1)
    
                    if schema.lower() not in self.ALLOWED_SCHEMAS:
    
                        validation_result['issues'].append(f"Access to unauthorized schema: {schema}")
    
                        validation_result['is_safe'] = False
    
                    if table.lower() not in self.ALLOWED_TABLES:
    
                        validation_result['warnings'].append(f"Access to table: {table}")
    
                else:
    
                    # Assume retail schema if not specified
    
                    if table_ref.lower() not in self.ALLOWED_TABLES:
    
                        validation_result['warnings'].append(f"Access to table: {table_ref}")
    
        
    
        def _determine_risk_level(self, validation_result):
    
            """Determine overall risk level."""
    
            
    
            if not validation_result['is_safe']:
    
                if any('injection' in issue.lower() for issue in validation_result['issues']):
    
                    validation_result['risk_level'] = QueryRisk.CRITICAL
    
                elif any('DROP' in issue or 'DELETE' in issue for issue in validation_result['issues']):
    
                    validation_result['risk_level'] = QueryRisk.HIGH
    
                else:
    
                    validation_result['risk_level'] = QueryRisk.MEDIUM
    
            elif validation_result['warnings']:
    
                validation_result['risk_level'] = QueryRisk.LOW
    
            else:
    
                validation_result['risk_level'] = QueryRisk.LOW
    
    
    
    # Global validator instance
    
    query_validator = QueryValidator()
    
    

    ๐Ÿ—ƒ๏ธ Database Query Tools

    Sales Analysis Tool

    
    # mcp_server/tools/sales_analysis.py
    
    """
    
    Comprehensive sales analysis tool for retail data querying.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from datetime import datetime, timedelta
    
    from .base import DatabaseTool, ToolResult
    
    from .query_validator import query_validator
    
    
    
    class SalesAnalysisTool(DatabaseTool):
    
        """Advanced sales analysis and reporting tool."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="execute_sales_query",
    
                description="Execute sophisticated sales analysis queries with natural language support",
    
                db_provider=db_provider
    
            )
    
            
    
            # Pre-built query templates for common analysis
    
            self.query_templates = {
    
                'daily_sales': """
    
                    SELECT 
    
                        DATE(transaction_date) as sales_date,
    
                        COUNT(*) as transaction_count,
    
                        SUM(total_amount) as total_revenue,
    
                        AVG(total_amount) as avg_transaction_value,
    
                        COUNT(DISTINCT customer_id) as unique_customers
    
                    FROM retail.sales_transactions 
    
                    WHERE transaction_date >= $1 AND transaction_date <= $2
    
                      AND transaction_type = 'sale'
    
                    GROUP BY DATE(transaction_date)
    
                    ORDER BY sales_date DESC
    
                """,
    
                
    
                'top_products': """
    
                    SELECT 
    
                        p.product_name,
    
                        p.brand,
    
                        SUM(sti.quantity) as total_quantity_sold,
    
                        SUM(sti.total_price) as total_revenue,
    
                        COUNT(DISTINCT st.transaction_id) as transaction_count,
    
                        AVG(sti.unit_price) as avg_price
    
                    FROM retail.sales_transaction_items sti
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY p.product_id, p.product_name, p.brand
    
                    ORDER BY total_revenue DESC
    
                    LIMIT $3
    
                """,
    
                
    
                'customer_analysis': """
    
                    SELECT 
    
                        c.customer_id,
    
                        c.first_name || ' ' || c.last_name as customer_name,
    
                        c.loyalty_tier,
    
                        COUNT(st.transaction_id) as transaction_count,
    
                        SUM(st.total_amount) as total_spent,
    
                        AVG(st.total_amount) as avg_transaction_value,
    
                        MAX(st.transaction_date) as last_purchase_date,
    
                        DATE_PART('day', CURRENT_DATE - MAX(st.transaction_date)) as days_since_last_purchase
    
                    FROM retail.customers c
    
                    LEFT JOIN retail.sales_transactions st ON c.customer_id = st.customer_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY c.customer_id, c.first_name, c.last_name, c.loyalty_tier
    
                    HAVING COUNT(st.transaction_id) > 0
    
                    ORDER BY total_spent DESC
    
                    LIMIT $3
    
                """,
    
                
    
                'category_performance': """
    
                    SELECT 
    
                        pc.category_name,
    
                        COUNT(DISTINCT p.product_id) as unique_products,
    
                        SUM(sti.quantity) as total_quantity_sold,
    
                        SUM(sti.total_price) as total_revenue,
    
                        AVG(sti.unit_price) as avg_price,
    
                        COUNT(DISTINCT st.transaction_id) as transaction_count
    
                    FROM retail.product_categories pc
    
                    JOIN retail.products p ON pc.category_id = p.category_id
    
                    JOIN retail.sales_transaction_items sti ON p.product_id = sti.product_id
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY pc.category_id, pc.category_name
    
                    ORDER BY total_revenue DESC
    
                """,
    
                
    
                'sales_trends': """
    
                    WITH daily_sales AS (
    
                        SELECT 
    
                            DATE(transaction_date) as sales_date,
    
                            SUM(total_amount) as daily_revenue,
    
                            COUNT(*) as daily_transactions
    
                        FROM retail.sales_transactions 
    
                        WHERE transaction_date >= $1 AND transaction_date <= $2
    
                          AND transaction_type = 'sale'
    
                        GROUP BY DATE(transaction_date)
    
                    ),
    
                    trend_analysis AS (
    
                        SELECT 
    
                            sales_date,
    
                            daily_revenue,
    
                            daily_transactions,
    
                            LAG(daily_revenue, 1) OVER (ORDER BY sales_date) as prev_day_revenue,
    
                            LAG(daily_revenue, 7) OVER (ORDER BY sales_date) as prev_week_revenue,
    
                            AVG(daily_revenue) OVER (
    
                                ORDER BY sales_date 
    
                                ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    
                            ) as rolling_7day_avg
    
                        FROM daily_sales
    
                    )
    
                    SELECT 
    
                        sales_date,
    
                        daily_revenue,
    
                        daily_transactions,
    
                        rolling_7day_avg,
    
                        CASE 
    
                            WHEN prev_day_revenue IS NOT NULL THEN
    
                                ROUND(((daily_revenue - prev_day_revenue) / prev_day_revenue * 100)::numeric, 2)
    
                            ELSE NULL
    
                        END as day_over_day_growth_pct,
    
                        CASE 
    
                            WHEN prev_week_revenue IS NOT NULL THEN
    
                                ROUND(((daily_revenue - prev_week_revenue) / prev_week_revenue * 100)::numeric, 2)
    
                            ELSE NULL
    
                        END as week_over_week_growth_pct
    
                    FROM trend_analysis
    
                    ORDER BY sales_date DESC
    
                """
    
            }
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute sales analysis query."""
    
            
    
            query_type = kwargs.get('query_type', 'custom')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for sales analysis"
    
                )
    
            
    
            try:
    
                if query_type in self.query_templates:
    
                    return await self._execute_template_query(query_type, kwargs)
    
                elif query_type == 'custom':
    
                    return await self._execute_custom_query(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown query type: {query_type}"
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Sales analysis failed: {str(e)}"
    
                )
    
        
    
        async def _execute_template_query(self, query_type: str, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Execute pre-built template query."""
    
            
    
            query = self.query_templates[query_type]
    
            store_id = kwargs['store_id']
    
            
    
            # Default parameters for template queries
    
            start_date = kwargs.get('start_date', (datetime.now() - timedelta(days=30)).date())
    
            end_date = kwargs.get('end_date', datetime.now().date())
    
            limit = kwargs.get('limit', 20)
    
            
    
            # Convert string dates if needed
    
            if isinstance(start_date, str):
    
                start_date = datetime.fromisoformat(start_date).date()
    
            if isinstance(end_date, str):
    
                end_date = datetime.fromisoformat(end_date).date()
    
            
    
            # Execute query with parameters
    
            params = (start_date, end_date, limit) if '$3' in query else (start_date, end_date)
    
            
    
            result = await self.execute_query(query, params, store_id)
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'query_type': query_type,
    
                    'date_range': f"{start_date} to {end_date}",
    
                    'store_id': store_id,
    
                    'analysis_type': 'template'
    
                }
    
            
    
            return result
    
        
    
        async def _execute_custom_query(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Execute custom SQL query with validation."""
    
            
    
            custom_query = kwargs.get('query')
    
            store_id = kwargs['store_id']
    
            
    
            if not custom_query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Custom query is required when query_type is 'custom'"
    
                )
    
            
    
            # Validate the query for security
    
            validation = query_validator.validate_query(custom_query)
    
            
    
            if not validation['is_safe']:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Query validation failed: {', '.join(validation['issues'])}",
    
                    metadata={
    
                        'validation_result': validation,
    
                        'risk_level': validation['risk_level'].value
    
                    }
    
                )
    
            
    
            # Execute validated query
    
            result = await self.execute_query(custom_query, None, store_id)
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'validation_warnings': validation.get('warnings', []),
    
                    'analysis_type': 'custom'
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for the sales analysis tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query_type": {
    
                        "type": "string",
    
                        "enum": list(self.query_templates.keys()) + ["custom"],
    
                        "description": "Type of sales analysis to perform",
    
                        "default": "daily_sales"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for data isolation",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "start_date": {
    
                        "type": "string",
    
                        "format": "date",
    
                        "description": "Start date for analysis (YYYY-MM-DD)"
    
                    },
    
                    "end_date": {
    
                        "type": "string",
    
                        "format": "date",
    
                        "description": "End date for analysis (YYYY-MM-DD)"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "minimum": 1,
    
                        "maximum": 1000,
    
                        "description": "Maximum number of results to return",
    
                        "default": 20
    
                    },
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Custom SQL query (required when query_type is 'custom')"
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    

    Schema Introspection Tool

    
    # mcp_server/tools/schema_introspection.py
    
    """
    
    Database schema introspection and metadata tools.
    
    """
    
    from typing import Dict, Any, List
    
    from .base import DatabaseTool, ToolResult, ToolCategory
    
    
    
    class SchemaIntrospectionTool(DatabaseTool):
    
        """Tool for exploring database schema and metadata."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_table_schema",
    
                description="Get detailed schema information for database tables",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.SCHEMA_INTROSPECTION
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute schema introspection."""
    
            
    
            table_name = kwargs.get('table_name')
    
            include_constraints = kwargs.get('include_constraints', True)
    
            include_indexes = kwargs.get('include_indexes', True)
    
            include_statistics = kwargs.get('include_statistics', False)
    
            
    
            try:
    
                if table_name:
    
                    return await self._get_single_table_schema(
    
                        table_name, include_constraints, include_indexes, include_statistics
    
                    )
    
                else:
    
                    return await self._get_all_tables_schema(include_constraints, include_indexes)
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Schema introspection failed: {str(e)}"
    
                )
    
        
    
        async def _get_single_table_schema(
    
            self, 
    
            table_name: str, 
    
            include_constraints: bool,
    
            include_indexes: bool,
    
            include_statistics: bool
    
        ) -> ToolResult:
    
            """Get detailed schema for a single table."""
    
            
    
            schema_info = {
    
                'table_name': table_name,
    
                'columns': [],
    
                'constraints': [],
    
                'indexes': [],
    
                'statistics': {}
    
            }
    
            
    
            async with self.get_connection() as conn:
    
                # Get column information
    
                columns_query = """
    
                    SELECT 
    
                        column_name,
    
                        data_type,
    
                        is_nullable,
    
                        column_default,
    
                        character_maximum_length,
    
                        numeric_precision,
    
                        numeric_scale,
    
                        ordinal_position,
    
                        udt_name
    
                    FROM information_schema.columns
    
                    WHERE table_schema = 'retail' AND table_name = $1
    
                    ORDER BY ordinal_position
    
                """
    
                
    
                columns = await conn.fetch(columns_query, table_name)
    
                schema_info['columns'] = [dict(col) for col in columns]
    
                
    
                # Get constraints if requested
    
                if include_constraints:
    
                    constraints_query = """
    
                        SELECT 
    
                            constraint_name,
    
                            constraint_type,
    
                            column_name,
    
                            foreign_table_name,
    
                            foreign_column_name
    
                        FROM information_schema.table_constraints tc
    
                        LEFT JOIN information_schema.key_column_usage kcu 
    
                            ON tc.constraint_name = kcu.constraint_name
    
                        LEFT JOIN information_schema.referential_constraints rc
    
                            ON tc.constraint_name = rc.constraint_name
    
                        LEFT JOIN information_schema.key_column_usage fkcu
    
                            ON rc.unique_constraint_name = fkcu.constraint_name
    
                        WHERE tc.table_schema = 'retail' AND tc.table_name = $1
    
                    """
    
                    
    
                    constraints = await conn.fetch(constraints_query, table_name)
    
                    schema_info['constraints'] = [dict(const) for const in constraints]
    
                
    
                # Get indexes if requested
    
                if include_indexes:
    
                    indexes_query = """
    
                        SELECT 
    
                            indexname as index_name,
    
                            indexdef as index_definition,
    
                            tablespace
    
                        FROM pg_indexes
    
                        WHERE schemaname = 'retail' AND tablename = $1
    
                    """
    
                    
    
                    indexes = await conn.fetch(indexes_query, table_name)
    
                    schema_info['indexes'] = [dict(idx) for idx in indexes]
    
                
    
                # Get table statistics if requested
    
                if include_statistics:
    
                    stats_query = """
    
                        SELECT 
    
                            n_tup_ins as inserts,
    
                            n_tup_upd as updates,
    
                            n_tup_del as deletes,
    
                            n_live_tup as live_tuples,
    
                            n_dead_tup as dead_tuples,
    
                            last_vacuum,
    
                            last_autovacuum,
    
                            last_analyze,
    
                            last_autoanalyze
    
                        FROM pg_stat_user_tables
    
                        WHERE schemaname = 'retail' AND relname = $1
    
                    """
    
                    
    
                    stats = await conn.fetchrow(stats_query, table_name)
    
                    if stats:
    
                        schema_info['statistics'] = dict(stats)
    
            
    
            return ToolResult(
    
                success=True,
    
                data=schema_info,
    
                metadata={
    
                    'table_name': table_name,
    
                    'schema': 'retail',
    
                    'introspection_type': 'single_table'
    
                }
    
            )
    
        
    
        async def _get_all_tables_schema(
    
            self, 
    
            include_constraints: bool,
    
            include_indexes: bool
    
        ) -> ToolResult:
    
            """Get schema information for all tables."""
    
            
    
            async with self.get_connection() as conn:
    
                # Get all tables in retail schema
    
                tables_query = """
    
                    SELECT 
    
                        table_name,
    
                        table_type
    
                    FROM information_schema.tables
    
                    WHERE table_schema = 'retail'
    
                    ORDER BY table_name
    
                """
    
                
    
                tables = await conn.fetch(tables_query)
    
                schema_info = {
    
                    'schema_name': 'retail',
    
                    'tables': []
    
                }
    
                
    
                for table in tables:
    
                    table_info = {
    
                        'table_name': table['table_name'],
    
                        'table_type': table['table_type'],
    
                        'columns': []
    
                    }
    
                    
    
                    # Get columns for each table
    
                    columns_query = """
    
                        SELECT 
    
                            column_name,
    
                            data_type,
    
                            is_nullable,
    
                            column_default
    
                        FROM information_schema.columns
    
                        WHERE table_schema = 'retail' AND table_name = $1
    
                        ORDER BY ordinal_position
    
                    """
    
                    
    
                    columns = await conn.fetch(columns_query, table['table_name'])
    
                    table_info['columns'] = [dict(col) for col in columns]
    
                    
    
                    schema_info['tables'].append(table_info)
    
            
    
            return ToolResult(
    
                success=True,
    
                data=schema_info,
    
                metadata={
    
                    'schema': 'retail',
    
                    'table_count': len(schema_info['tables']),
    
                    'introspection_type': 'all_tables'
    
                }
    
            )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for schema introspection tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "table_name": {
    
                        "type": "string",
    
                        "description": "Specific table name to introspect (optional - if not provided, all tables are returned)",
    
                        "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
    
                    },
    
                    "include_constraints": {
    
                        "type": "boolean",
    
                        "description": "Include constraint information",
    
                        "default": True
    
                    },
    
                    "include_indexes": {
    
                        "type": "boolean",
    
                        "description": "Include index information",
    
                        "default": True
    
                    },
    
                    "include_statistics": {
    
                        "type": "boolean",
    
                        "description": "Include table statistics",
    
                        "default": False
    
                    }
    
                },
    
                "additionalProperties": False
    
            }
    
    
    
    class MultiTableSchemaTool(DatabaseTool):
    
        """Tool for getting schema information for multiple tables at once."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_multiple_table_schemas",
    
                description="Get schema information for multiple tables efficiently",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.SCHEMA_INTROSPECTION
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute multi-table schema introspection."""
    
            
    
            table_names = kwargs.get('table_names', [])
    
            
    
            if not table_names:
    
                return ToolResult(
    
                    success=False,
    
                    error="At least one table name is required"
    
                )
    
            
    
            try:
    
                schemas = {}
    
                
    
                async with self.get_connection() as conn:
    
                    for table_name in table_names:
    
                        # Get table schema
    
                        schema_query = """
    
                            SELECT 
    
                                c.column_name,
    
                                c.data_type,
    
                                c.is_nullable,
    
                                c.column_default,
    
                                c.character_maximum_length,
    
                                tc.constraint_type,
    
                                kcu.constraint_name
    
                            FROM information_schema.columns c
    
                            LEFT JOIN information_schema.key_column_usage kcu
    
                                ON c.table_name = kcu.table_name 
    
                                AND c.column_name = kcu.column_name
    
                                AND c.table_schema = kcu.table_schema
    
                            LEFT JOIN information_schema.table_constraints tc
    
                                ON kcu.constraint_name = tc.constraint_name
    
                                AND kcu.table_schema = tc.table_schema
    
                            WHERE c.table_schema = 'retail' AND c.table_name = $1
    
                            ORDER BY c.ordinal_position
    
                        """
    
                        
    
                        columns = await conn.fetch(schema_query, table_name)
    
                        
    
                        if columns:
    
                            schemas[table_name] = {
    
                                'table_name': table_name,
    
                                'columns': [dict(col) for col in columns]
    
                            }
    
                        else:
    
                            schemas[table_name] = {
    
                                'table_name': table_name,
    
                                'error': 'Table not found or not accessible'
    
                            }
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=schemas,
    
                    metadata={
    
                        'requested_tables': table_names,
    
                        'found_tables': [name for name, info in schemas.items() if 'error' not in info],
    
                        'missing_tables': [name for name, info in schemas.items() if 'error' in info]
    
                    }
    
                )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Multi-table schema introspection failed: {str(e)}"
    
                )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for multi-table schema tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "table_names": {
    
                        "type": "array",
    
                        "items": {
    
                            "type": "string",
    
                            "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
    
                        },
    
                        "description": "List of table names to get schema information for",
    
                        "minItems": 1,
    
                        "maxItems": 20
    
                    }
    
                },
    
                "required": ["table_names"],
    
                "additionalProperties": False
    
            }
    
    

    ๐Ÿ“Š Analytics and Utility Tools

    Business Intelligence Tool

    
    # mcp_server/tools/business_intelligence.py
    
    """
    
    Advanced business intelligence and analytics tools.
    
    """
    
    from typing import Dict, Any, List
    
    from datetime import datetime, timedelta
    
    from .base import DatabaseTool, ToolResult, ToolCategory
    
    
    
    class BusinessIntelligenceTool(DatabaseTool):
    
        """Advanced analytics tool for business intelligence queries."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="generate_business_insights",
    
                description="Generate comprehensive business intelligence reports and insights",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.ANALYTICS
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute business intelligence analysis."""
    
            
    
            analysis_type = kwargs.get('analysis_type', 'summary')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for business intelligence analysis"
    
                )
    
            
    
            try:
    
                if analysis_type == 'summary':
    
                    return await self._generate_business_summary(kwargs)
    
                elif analysis_type == 'customer_segmentation':
    
                    return await self._analyze_customer_segmentation(kwargs)
    
                elif analysis_type == 'product_performance':
    
                    return await self._analyze_product_performance(kwargs)
    
                elif analysis_type == 'seasonal_trends':
    
                    return await self._analyze_seasonal_trends(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown analysis type: {analysis_type}"
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Business intelligence analysis failed: {str(e)}"
    
                )
    
        
    
        async def _generate_business_summary(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Generate comprehensive business summary."""
    
            
    
            store_id = kwargs['store_id']
    
            days = kwargs.get('days', 30)
    
            
    
            summary_query = """
    
                WITH date_range AS (
    
                    SELECT CURRENT_DATE - INTERVAL '%s days' as start_date,
    
                           CURRENT_DATE as end_date
    
                ),
    
                sales_summary AS (
    
                    SELECT 
    
                        COUNT(*) as total_transactions,
    
                        COUNT(DISTINCT customer_id) as unique_customers,
    
                        SUM(total_amount) as total_revenue,
    
                        AVG(total_amount) as avg_transaction_value,
    
                        COUNT(DISTINCT DATE(transaction_date)) as active_days
    
                    FROM retail.sales_transactions st, date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                product_summary AS (
    
                    SELECT 
    
                        COUNT(DISTINCT p.product_id) as products_sold,
    
                        SUM(sti.quantity) as total_items_sold
    
                    FROM retail.sales_transaction_items sti
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    CROSS JOIN date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                top_category AS (
    
                    SELECT 
    
                        pc.category_name,
    
                        SUM(sti.total_price) as category_revenue
    
                    FROM retail.product_categories pc
    
                    JOIN retail.products p ON pc.category_id = p.category_id
    
                    JOIN retail.sales_transaction_items sti ON p.product_id = sti.product_id
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    CROSS JOIN date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY pc.category_name
    
                    ORDER BY category_revenue DESC
    
                    LIMIT 1
    
                )
    
                SELECT 
    
                    ss.*,
    
                    ps.products_sold,
    
                    ps.total_items_sold,
    
                    tc.category_name as top_category,
    
                    tc.category_revenue as top_category_revenue,
    
                    CASE 
    
                        WHEN ss.active_days > 0 THEN ss.total_revenue / ss.active_days
    
                        ELSE 0
    
                    END as avg_daily_revenue
    
                FROM sales_summary ss
    
                CROSS JOIN product_summary ps
    
                CROSS JOIN top_category tc
    
            """ % days
    
            
    
            result = await self.execute_query(summary_query, None, store_id)
    
            
    
            if result.success and result.data:
    
                summary = result.data[0]
    
                
    
                # Add derived insights
    
                insights = {
    
                    'revenue_trend': 'stable',  # Would calculate based on historical data
    
                    'customer_retention': f"{summary.get('unique_customers', 0)} active customers",
    
                    'performance_indicators': {
    
                        'transactions_per_day': round(summary.get('total_transactions', 0) / max(summary.get('active_days', 1), 1), 2),
    
                        'revenue_per_customer': round(summary.get('total_revenue', 0) / max(summary.get('unique_customers', 1), 1), 2),
    
                        'items_per_transaction': round(summary.get('total_items_sold', 0) / max(summary.get('total_transactions', 1), 1), 2)
    
                    }
    
                }
    
                
    
                summary['insights'] = insights
    
                
    
                result.data = [summary]
    
                result.metadata = {
    
                    'analysis_type': 'business_summary',
    
                    'period_days': days,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for business intelligence tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "analysis_type": {
    
                        "type": "string",
    
                        "enum": ["summary", "customer_segmentation", "product_performance", "seasonal_trends"],
    
                        "description": "Type of business intelligence analysis to perform",
    
                        "default": "summary"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for analysis",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "days": {
    
                        "type": "integer",
    
                        "minimum": 1,
    
                        "maximum": 365,
    
                        "description": "Number of days to analyze",
    
                        "default": 30
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    
    
    class UtilityTool(DatabaseTool):
    
        """Utility tool for common operations."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_current_utc_date",
    
                description="Get current UTC date and time for reference",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.UTILITY
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute utility operation."""
    
            
    
            format_type = kwargs.get('format', 'iso')
    
            
    
            try:
    
                async with self.get_connection() as conn:
    
                    if format_type == 'iso':
    
                        query = "SELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC' as current_utc_datetime"
    
                    elif format_type == 'epoch':
    
                        query = "SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC') as current_utc_epoch"
    
                    elif format_type == 'date_only':
    
                        query = "SELECT CURRENT_DATE as current_date"
    
                    else:
    
                        return ToolResult(
    
                            success=False,
    
                            error=f"Unknown format type: {format_type}"
    
                        )
    
                    
    
                    result = await conn.fetchrow(query)
    
                    
    
                    return ToolResult(
    
                        success=True,
    
                        data=dict(result),
    
                        metadata={
    
                            'format_type': format_type,
    
                            'timezone': 'UTC'
    
                        }
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Utility operation failed: {str(e)}"
    
                )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for utility tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "format": {
    
                        "type": "string",
    
                        "enum": ["iso", "epoch", "date_only"],
    
                        "description": "Format for the returned date/time",
    
                        "default": "iso"
    
                    }
    
                },
    
                "additionalProperties": False
    
            }
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Advanced Tool Architecture: Implemented sophisticated MCP tools with comprehensive error handling

    โœ… Query Validation: Built secure SQL validation to prevent injection attacks

    โœ… Database Tools: Created powerful sales analysis and schema introspection capabilities

    โœ… Business Intelligence: Developed analytics tools for comprehensive business insights

    โœ… Performance Optimization: Applied caching, connection pooling, and query optimization

    โœ… Security Integration: Implemented role-based access control and audit logging

    ๐Ÿš€ What's Next

    Continue with Lab 07: Semantic Search Integration to:

  • Integrate vector search capabilities with MCP tools
  • Build semantic product search functionality
  • Implement AI-powered query understanding
  • Create hybrid search combining traditional and vector queries
  • ๐Ÿ“š Additional Resources

    MCP Tool Development

  • Model Context Protocol Documentation - Official MCP specification
  • FastMCP Framework - Python MCP implementation
  • MCP Tool Patterns - Example tool implementations
  • Database Security

  • SQL Injection Prevention - OWASP security guide
  • PostgreSQL Security - Database security best practices
  • Query Validation Techniques - Secure query patterns
  • Performance Optimization

  • Database Query Optimization - PostgreSQL performance guide
  • Connection Pooling Best Practices - Connection management
  • Async Python Patterns - Asynchronous programming guide
  • ---

    Previous: Lab 05: MCP Server Implementation

    Next: Lab 07: Semantic Search Integration

    Lab 7-9: Advanced Features 07 Semantic Search Integration

    Semantic Search Integration

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on implementing semantic search capabilities using Azure OpenAI embeddings and PostgreSQL's pgvector extension.

    You'll learn to build AI-powered product search that understands natural language queries and delivers relevant results based on semantic similarity.

    Overview

    Traditional keyword-based search often fails to capture user intent and semantic meaning.

    Semantic search using vector embeddings enables natural language queries like "comfortable running shoes for rainy weather" to find relevant products even if those exact words don't appear in product descriptions.

    Our implementation combines Azure OpenAI's powerful embedding models with PostgreSQL's pgvector extension to create a high-performance, scalable semantic search system that enhances the retail experience with intelligent product discovery.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Integrate Azure OpenAI embedding models for text vectorization
  • Implement pgvector for efficient similarity search operations
  • Build semantic search tools for natural language product queries
  • Create hybrid search combining traditional and vector search
  • Optimize vector queries for production performance
  • Design recommendation systems using embedding similarity
  • ๐Ÿง  Semantic Search Architecture

    Vector Search Pipeline

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                User Query                       โ”‚
    
    โ”‚         "comfortable running shoes"            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚           Azure OpenAI API                     โ”‚
    
    โ”‚        text-embedding-3-small                  โ”‚
    
    โ”‚        Input: Query Text                       โ”‚
    
    โ”‚        Output: 1536-dimensional vector         โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚              pgvector Search                   โ”‚
    
    โ”‚      Cosine Similarity: embedding <=> vector   โ”‚
    
    โ”‚      WHERE similarity > threshold              โ”‚
    
    โ”‚      ORDER BY similarity DESC                  โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚            Ranked Results                      โ”‚
    
    โ”‚    1. Nike Air Zoom (0.89 similarity)         โ”‚
    
    โ”‚    2. Adidas Ultraboost (0.85 similarity)     โ”‚
    
    โ”‚    3. New Balance Fresh Foam (0.82 similarity) โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Embedding Generation Strategy

    
    # mcp_server/embeddings/embedding_manager.py
    
    """
    
    Comprehensive embedding management for semantic search.
    
    """
    
    import asyncio
    
    import hashlib
    
    import json
    
    from typing import List, Dict, Any, Optional, Tuple
    
    from datetime import datetime, timedelta
    
    import numpy as np
    
    from azure.ai.projects.aio import AIProjectClient
    
    from azure.identity.aio import DefaultAzureCredential
    
    from azure.core.exceptions import HttpResponseError
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class EmbeddingManager:
    
        """Manage text embeddings for semantic search."""
    
        
    
        def __init__(self, project_endpoint: str, deployment_name: str = "text-embedding-3-small"):
    
            self.project_endpoint = project_endpoint
    
            self.deployment_name = deployment_name
    
            self.credential = DefaultAzureCredential()
    
            self.client = None
    
            
    
            # Embedding configuration
    
            self.embedding_dimension = 1536  # text-embedding-3-small dimension
    
            self.max_tokens = 8000  # Maximum tokens per request
    
            self.batch_size = 100  # Batch processing size
    
            
    
            # Caching configuration
    
            self.embedding_cache = {}
    
            self.cache_ttl = timedelta(hours=24)
    
            
    
            # Rate limiting
    
            self.rate_limit_requests = 1000  # Per minute
    
            self.rate_limit_tokens = 150000  # Per minute
    
            
    
        async def initialize(self):
    
            """Initialize the Azure AI client."""
    
            
    
            try:
    
                self.client = AIProjectClient(
    
                    endpoint=self.project_endpoint,
    
                    credential=self.credential
    
                )
    
                
    
                # Test connection
    
                await self._test_connection()
    
                
    
                logger.info("Embedding manager initialized successfully")
    
                
    
            except Exception as e:
    
                logger.error(f"Failed to initialize embedding manager: {e}")
    
                raise
    
        
    
        async def _test_connection(self):
    
            """Test Azure OpenAI connection."""
    
            
    
            try:
    
                test_embedding = await self.generate_embedding("test connection")
    
                if len(test_embedding) != self.embedding_dimension:
    
                    raise ValueError(f"Unexpected embedding dimension: {len(test_embedding)}")
    
                
    
                logger.info("Azure OpenAI connection test successful")
    
                
    
            except Exception as e:
    
                logger.error(f"Azure OpenAI connection test failed: {e}")
    
                raise
    
        
    
        async def generate_embedding(self, text: str, use_cache: bool = True) -> List[float]:
    
            """Generate embedding for a single text."""
    
            
    
            if not text or not text.strip():
    
                raise ValueError("Text cannot be empty")
    
            
    
            # Check cache first
    
            if use_cache:
    
                cache_key = self._get_cache_key(text)
    
                cached_embedding = self._get_cached_embedding(cache_key)
    
                if cached_embedding:
    
                    return cached_embedding
    
            
    
            try:
    
                # Ensure client is initialized
    
                if not self.client:
    
                    await self.initialize()
    
                
    
                # Generate embedding
    
                response = await self.client.embeddings.create(
    
                    model=self.deployment_name,
    
                    input=text.strip()
    
                )
    
                
    
                embedding = response.data[0].embedding
    
                
    
                # Cache the result
    
                if use_cache:
    
                    self._cache_embedding(cache_key, embedding)
    
                
    
                logger.debug(f"Generated embedding for text (length: {len(text)})")
    
                
    
                return embedding
    
                
    
            except HttpResponseError as e:
    
                logger.error(f"Azure OpenAI API error: {e}")
    
                raise Exception(f"Embedding generation failed: {e}")
    
            except Exception as e:
    
                logger.error(f"Embedding generation error: {e}")
    
                raise
    
        
    
        async def generate_embeddings_batch(
    
            self, 
    
            texts: List[str], 
    
            use_cache: bool = True
    
        ) -> List[List[float]]:
    
            """Generate embeddings for multiple texts efficiently."""
    
            
    
            if not texts:
    
                return []
    
            
    
            embeddings = []
    
            cache_misses = []
    
            cache_miss_indices = []
    
            
    
            # Check cache for each text
    
            for i, text in enumerate(texts):
    
                if not text or not text.strip():
    
                    embeddings.append([])
    
                    continue
    
                    
    
                if use_cache:
    
                    cache_key = self._get_cache_key(text)
    
                    cached_embedding = self._get_cached_embedding(cache_key)
    
                    if cached_embedding:
    
                        embeddings.append(cached_embedding)
    
                        continue
    
                
    
                # Track cache misses
    
                embeddings.append(None)  # Placeholder
    
                cache_misses.append(text.strip())
    
                cache_miss_indices.append(i)
    
            
    
            # Generate embeddings for cache misses
    
            if cache_misses:
    
                try:
    
                    # Process in batches to respect API limits
    
                    for batch_start in range(0, len(cache_misses), self.batch_size):
    
                        batch_end = min(batch_start + self.batch_size, len(cache_misses))
    
                        batch_texts = cache_misses[batch_start:batch_end]
    
                        
    
                        # Generate batch embeddings
    
                        response = await self.client.embeddings.create(
    
                            model=self.deployment_name,
    
                            input=batch_texts
    
                        )
    
                        
    
                        # Process batch results
    
                        for j, embedding_data in enumerate(response.data):
    
                            actual_index = cache_miss_indices[batch_start + j]
    
                            embedding = embedding_data.embedding
    
                            embeddings[actual_index] = embedding
    
                            
    
                            # Cache the result
    
                            if use_cache:
    
                                text = batch_texts[j]
    
                                cache_key = self._get_cache_key(text)
    
                                self._cache_embedding(cache_key, embedding)
    
                        
    
                        # Rate limiting - small delay between batches
    
                        if batch_end < len(cache_misses):
    
                            await asyncio.sleep(0.1)
    
                    
    
                    logger.info(f"Generated {len(cache_misses)} embeddings in batch")
    
                    
    
                except Exception as e:
    
                    logger.error(f"Batch embedding generation failed: {e}")
    
                    raise
    
            
    
            return embeddings
    
        
    
        def _get_cache_key(self, text: str) -> str:
    
            """Generate cache key for text."""
    
            
    
            # Use SHA-256 hash of text + model for cache key
    
            content = f"{self.deployment_name}:{text.strip()}"
    
            return hashlib.sha256(content.encode()).hexdigest()
    
        
    
        def _get_cached_embedding(self, cache_key: str) -> Optional[List[float]]:
    
            """Get embedding from cache if not expired."""
    
            
    
            if cache_key in self.embedding_cache:
    
                embedding_data = self.embedding_cache[cache_key]
    
                
    
                # Check if cache entry is still valid
    
                if datetime.now() - embedding_data['timestamp'] < self.cache_ttl:
    
                    return embedding_data['embedding']
    
                else:
    
                    # Remove expired entry
    
                    del self.embedding_cache[cache_key]
    
            
    
            return None
    
        
    
        def _cache_embedding(self, cache_key: str, embedding: List[float]):
    
            """Cache embedding with timestamp."""
    
            
    
            self.embedding_cache[cache_key] = {
    
                'embedding': embedding,
    
                'timestamp': datetime.now()
    
            }
    
            
    
            # Limit cache size
    
            if len(self.embedding_cache) > 10000:
    
                # Remove oldest entries
    
                oldest_keys = sorted(
    
                    self.embedding_cache.keys(),
    
                    key=lambda k: self.embedding_cache[k]['timestamp']
    
                )[:1000]
    
                
    
                for key in oldest_keys:
    
                    del self.embedding_cache[key]
    
        
    
        async def cleanup(self):
    
            """Cleanup resources."""
    
            
    
            if self.client:
    
                await self.client.close()
    
            
    
            logger.info("Embedding manager cleanup completed")
    
    
    
    # Global embedding manager instance
    
    embedding_manager = EmbeddingManager(
    
        project_endpoint=os.getenv('PROJECT_ENDPOINT'),
    
        deployment_name=os.getenv('EMBEDDING_DEPLOYMENT_NAME', 'text-embedding-3-small')
    
    )
    
    

    ๐Ÿ” Product Embedding Generation

    Automated Embedding Pipeline

    
    # mcp_server/embeddings/product_embedder.py
    
    """
    
    Product embedding generation and management.
    
    """
    
    import asyncio
    
    import asyncpg
    
    from typing import List, Dict, Any, Optional
    
    from datetime import datetime
    
    import logging
    
    from .embedding_manager import embedding_manager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ProductEmbedder:
    
        """Generate and manage product embeddings for semantic search."""
    
        
    
        def __init__(self, db_provider):
    
            self.db_provider = db_provider
    
            self.embedding_manager = embedding_manager
    
            
    
            # Text combination strategy for products
    
            self.text_template = "{product_name} {brand} {description} {category} {tags}"
    
            
    
        async def generate_product_embeddings(
    
            self, 
    
            store_id: str,
    
            batch_size: int = 50,
    
            force_regenerate: bool = False
    
        ) -> Dict[str, Any]:
    
            """Generate embeddings for all products in a store."""
    
            
    
            async with self.db_provider.get_connection() as conn:
    
                try:
    
                    # Set store context
    
                    await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Get products that need embeddings
    
                    if force_regenerate:
    
                        products_query = """
    
                            SELECT 
    
                                p.product_id,
    
                                p.product_name,
    
                                p.product_description,
    
                                p.brand,
    
                                pc.category_name,
    
                                array_to_string(p.tags, ' ') as tags_text
    
                            FROM retail.products p
    
                            LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                            WHERE p.is_active = TRUE
    
                            ORDER BY p.created_at DESC
    
                        """
    
                    else:
    
                        products_query = """
    
                            SELECT 
    
                                p.product_id,
    
                                p.product_name,
    
                                p.product_description,
    
                                p.brand,
    
                                pc.category_name,
    
                                array_to_string(p.tags, ' ') as tags_text
    
                            FROM retail.products p
    
                            LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                            LEFT JOIN retail.product_embeddings pe ON p.product_id = pe.product_id
    
                            WHERE p.is_active = TRUE
    
                              AND (pe.product_id IS NULL OR pe.updated_at < p.updated_at)
    
                            ORDER BY p.created_at DESC
    
                        """
    
                    
    
                    products = await conn.fetch(products_query)
    
                    
    
                    if not products:
    
                        return {
    
                            'success': True,
    
                            'message': 'No products need embedding generation',
    
                            'processed_count': 0,
    
                            'store_id': store_id
    
                        }
    
                    
    
                    logger.info(f"Generating embeddings for {len(products)} products in store {store_id}")
    
                    
    
                    # Process products in batches
    
                    processed_count = 0
    
                    
    
                    for i in range(0, len(products), batch_size):
    
                        batch = products[i:i + batch_size]
    
                        await self._process_product_batch(conn, batch, store_id)
    
                        processed_count += len(batch)
    
                        
    
                        logger.info(f"Processed {processed_count}/{len(products)} products")
    
                    
    
                    return {
    
                        'success': True,
    
                        'message': f'Successfully generated embeddings for {processed_count} products',
    
                        'processed_count': processed_count,
    
                        'store_id': store_id,
    
                        'total_products': len(products)
    
                    }
    
                    
    
                except Exception as e:
    
                    logger.error(f"Product embedding generation failed: {e}")
    
                    return {
    
                        'success': False,
    
                        'error': str(e),
    
                        'store_id': store_id
    
                    }
    
        
    
        async def _process_product_batch(
    
            self, 
    
            conn: asyncpg.Connection, 
    
            products: List[Dict], 
    
            store_id: str
    
        ):
    
            """Process a batch of products for embedding generation."""
    
            
    
            # Prepare texts for embedding
    
            texts = []
    
            product_ids = []
    
            
    
            for product in products:
    
                # Combine product information into searchable text
    
                combined_text = self._create_product_text(product)
    
                texts.append(combined_text)
    
                product_ids.append(product['product_id'])
    
            
    
            # Generate embeddings
    
            embeddings = await self.embedding_manager.generate_embeddings_batch(texts)
    
            
    
            # Store embeddings in database
    
            for i, (product_id, embedding) in enumerate(zip(product_ids, embeddings)):
    
                if embedding:  # Skip failed embeddings
    
                    await self._store_product_embedding(
    
                        conn, 
    
                        product_id, 
    
                        store_id, 
    
                        texts[i], 
    
                        embedding
    
                    )
    
        
    
        def _create_product_text(self, product: Dict[str, Any]) -> str:
    
            """Create combined text for product embedding."""
    
            
    
            # Handle None values
    
            product_name = product.get('product_name') or ''
    
            brand = product.get('brand') or ''
    
            description = product.get('product_description') or ''
    
            category = product.get('category_name') or ''
    
            tags = product.get('tags_text') or ''
    
            
    
            # Combine into searchable text
    
            combined_text = self.text_template.format(
    
                product_name=product_name,
    
                brand=brand,
    
                description=description,
    
                category=category,
    
                tags=tags
    
            )
    
            
    
            # Clean up extra whitespace
    
            return ' '.join(combined_text.split())
    
        
    
        async def _store_product_embedding(
    
            self,
    
            conn: asyncpg.Connection,
    
            product_id: str,
    
            store_id: str,
    
            embedding_text: str,
    
            embedding: List[float]
    
        ):
    
            """Store product embedding in database."""
    
            
    
            # Convert embedding to pgvector format
    
            embedding_vector = f"[{','.join(map(str, embedding))}]"
    
            
    
            # Upsert embedding
    
            upsert_query = """
    
                INSERT INTO retail.product_embeddings (
    
                    product_id, store_id, embedding_text, embedding, embedding_model
    
                ) VALUES ($1, $2, $3, $4, $5)
    
                ON CONFLICT (product_id, embedding_model) 
    
                DO UPDATE SET
    
                    store_id = EXCLUDED.store_id,
    
                    embedding_text = EXCLUDED.embedding_text,
    
                    embedding = EXCLUDED.embedding,
    
                    updated_at = CURRENT_TIMESTAMP
    
            """
    
            
    
            await conn.execute(
    
                upsert_query,
    
                product_id,
    
                store_id,
    
                embedding_text,
    
                embedding_vector,
    
                self.embedding_manager.deployment_name
    
            )
    
        
    
        async def update_product_embedding(
    
            self, 
    
            product_id: str, 
    
            store_id: str
    
        ) -> Dict[str, Any]:
    
            """Update embedding for a single product."""
    
            
    
            async with self.db_provider.get_connection() as conn:
    
                try:
    
                    # Set store context
    
                    await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Get product information
    
                    product_query = """
    
                        SELECT 
    
                            p.product_id,
    
                            p.product_name,
    
                            p.product_description,
    
                            p.brand,
    
                            pc.category_name,
    
                            array_to_string(p.tags, ' ') as tags_text
    
                        FROM retail.products p
    
                        LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                        WHERE p.product_id = $1 AND p.is_active = TRUE
    
                    """
    
                    
    
                    product = await conn.fetchrow(product_query, product_id)
    
                    
    
                    if not product:
    
                        return {
    
                            'success': False,
    
                            'error': f'Product {product_id} not found or inactive'
    
                        }
    
                    
    
                    # Generate embedding
    
                    combined_text = self._create_product_text(dict(product))
    
                    embedding = await self.embedding_manager.generate_embedding(combined_text)
    
                    
    
                    # Store embedding
    
                    await self._store_product_embedding(
    
                        conn, product_id, store_id, combined_text, embedding
    
                    )
    
                    
    
                    return {
    
                        'success': True,
    
                        'message': f'Successfully updated embedding for product {product_id}',
    
                        'product_id': product_id,
    
                        'store_id': store_id
    
                    }
    
                    
    
                except Exception as e:
    
                    logger.error(f"Single product embedding update failed: {e}")
    
                    return {
    
                        'success': False,
    
                        'error': str(e),
    
                        'product_id': product_id
    
                    }
    
    
    
    # Global product embedder instance
    
    product_embedder = ProductEmbedder(db_provider)
    
    

    ๐Ÿ”Ž Semantic Search Tools

    Semantic Product Search Tool

    
    # mcp_server/tools/semantic_search.py
    
    """
    
    Semantic search tools for natural language product queries.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from ..tools.base import DatabaseTool, ToolResult, ToolCategory
    
    from ..embeddings.embedding_manager import embedding_manager
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class SemanticProductSearchTool(DatabaseTool):
    
        """Advanced semantic search tool for products using vector similarity."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="semantic_search_products",
    
                description="Search products using natural language queries with semantic understanding",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.DATABASE_QUERY
    
            self.embedding_manager = embedding_manager
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute semantic product search."""
    
            
    
            query = kwargs.get('query')
    
            store_id = kwargs.get('store_id')
    
            limit = kwargs.get('limit', 20)
    
            similarity_threshold = kwargs.get('similarity_threshold', 0.7)
    
            include_metadata = kwargs.get('include_metadata', True)
    
            
    
            if not query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Search query is required"
    
                )
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for semantic search"
    
                )
    
            
    
            try:
    
                # Generate query embedding
    
                query_embedding = await self.embedding_manager.generate_embedding(query)
    
                
    
                # Perform semantic search
    
                search_results = await self._perform_semantic_search(
    
                    query_embedding,
    
                    store_id,
    
                    limit,
    
                    similarity_threshold,
    
                    include_metadata
    
                )
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=search_results,
    
                    row_count=len(search_results),
    
                    metadata={
    
                        'query': query,
    
                        'store_id': store_id,
    
                        'similarity_threshold': similarity_threshold,
    
                        'search_type': 'semantic'
    
                    }
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Semantic search failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Semantic search failed: {str(e)}"
    
                )
    
        
    
        async def _perform_semantic_search(
    
            self,
    
            query_embedding: List[float],
    
            store_id: str,
    
            limit: int,
    
            similarity_threshold: float,
    
            include_metadata: bool
    
        ) -> List[Dict[str, Any]]:
    
            """Perform vector similarity search."""
    
            
    
            # Convert embedding to PostgreSQL vector format
    
            embedding_vector = f"[{','.join(map(str, query_embedding))}]"
    
            
    
            # Build search query
    
            if include_metadata:
    
                search_query = """
    
                    SELECT 
    
                        p.product_id,
    
                        p.product_name,
    
                        p.brand,
    
                        p.price,
    
                        p.product_description,
    
                        p.current_stock,
    
                        p.rating_average,
    
                        p.rating_count,
    
                        p.tags,
    
                        pc.category_name,
    
                        pe.embedding_text,
    
                        1 - (pe.embedding <=> $1::vector) as similarity_score
    
                    FROM retail.product_embeddings pe
    
                    JOIN retail.products p ON pe.product_id = p.product_id
    
                    LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                    WHERE pe.store_id = $2
    
                      AND p.is_active = TRUE
    
                      AND 1 - (pe.embedding <=> $1::vector) >= $3
    
                    ORDER BY pe.embedding <=> $1::vector
    
                    LIMIT $4
    
                """
    
            else:
    
                search_query = """
    
                    SELECT 
    
                        p.product_id,
    
                        p.product_name,
    
                        p.brand,
    
                        p.price,
    
                        1 - (pe.embedding <=> $1::vector) as similarity_score
    
                    FROM retail.product_embeddings pe
    
                    JOIN retail.products p ON pe.product_id = p.product_id
    
                    WHERE pe.store_id = $2
    
                      AND p.is_active = TRUE
    
                      AND 1 - (pe.embedding <=> $1::vector) >= $3
    
                    ORDER BY pe.embedding <=> $1::vector
    
                    LIMIT $4
    
                """
    
            
    
            async with self.get_connection() as conn:
    
                # Set store context
    
                await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                
    
                # Execute search
    
                results = await conn.fetch(
    
                    search_query,
    
                    embedding_vector,
    
                    store_id,
    
                    similarity_threshold,
    
                    limit
    
                )
    
                
    
                return [dict(result) for result in results]
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for semantic search tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Natural language search query",
    
                        "minLength": 1,
    
                        "maxLength": 500
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for search scope",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of results to return",
    
                        "minimum": 1,
    
                        "maximum": 100,
    
                        "default": 20
    
                    },
    
                    "similarity_threshold": {
    
                        "type": "number",
    
                        "description": "Minimum similarity score (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "include_metadata": {
    
                        "type": "boolean",
    
                        "description": "Include detailed product metadata in results",
    
                        "default": True
    
                    }
    
                },
    
                "required": ["query", "store_id"],
    
                "additionalProperties": False
    
            }
    
    
    
    class HybridSearchTool(DatabaseTool):
    
        """Hybrid search combining traditional keyword and semantic search."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="hybrid_product_search",
    
                description="Hybrid search combining keyword matching and semantic similarity for optimal results",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.DATABASE_QUERY
    
            self.embedding_manager = embedding_manager
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute hybrid product search."""
    
            
    
            query = kwargs.get('query')
    
            store_id = kwargs.get('store_id')
    
            limit = kwargs.get('limit', 20)
    
            semantic_weight = kwargs.get('semantic_weight', 0.7)
    
            keyword_weight = kwargs.get('keyword_weight', 0.3)
    
            
    
            if not query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Search query is required"
    
                )
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for hybrid search"
    
                )
    
            
    
            try:
    
                # Generate query embedding for semantic search
    
                query_embedding = await self.embedding_manager.generate_embedding(query)
    
                
    
                # Perform hybrid search
    
                search_results = await self._perform_hybrid_search(
    
                    query,
    
                    query_embedding,
    
                    store_id,
    
                    limit,
    
                    semantic_weight,
    
                    keyword_weight
    
                )
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=search_results,
    
                    row_count=len(search_results),
    
                    metadata={
    
                        'query': query,
    
                        'store_id': store_id,
    
                        'semantic_weight': semantic_weight,
    
                        'keyword_weight': keyword_weight,
    
                        'search_type': 'hybrid'
    
                    }
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Hybrid search failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Hybrid search failed: {str(e)}"
    
                )
    
        
    
        async def _perform_hybrid_search(
    
            self,
    
            query: str,
    
            query_embedding: List[float],
    
            store_id: str,
    
            limit: int,
    
            semantic_weight: float,
    
            keyword_weight: float
    
        ) -> List[Dict[str, Any]]:
    
            """Perform hybrid search combining keyword and semantic similarity."""
    
            
    
            # Convert embedding to PostgreSQL vector format
    
            embedding_vector = f"[{','.join(map(str, query_embedding))}]"
    
            
    
            # Create search terms for keyword matching
    
            search_terms = ' & '.join(query.lower().split())
    
            
    
            hybrid_query = """
    
                WITH keyword_scores AS (
    
                    SELECT 
    
                        p.product_id,
    
                        ts_rank(
    
                            to_tsvector('english', 
    
                                p.product_name || ' ' || 
    
                                COALESCE(p.product_description, '') || ' ' || 
    
                                COALESCE(p.brand, '') || ' ' ||
    
                                COALESCE(array_to_string(p.tags, ' '), '')
    
                            ),
    
                            plainto_tsquery('english', $2)
    
                        ) as keyword_score
    
                    FROM retail.products p
    
                    WHERE p.is_active = TRUE
    
                      AND p.store_id = $3
    
                      AND (
    
                        to_tsvector('english', 
    
                            p.product_name || ' ' || 
    
                            COALESCE(p.product_description, '') || ' ' || 
    
                            COALESCE(p.brand, '') || ' ' ||
    
                            COALESCE(array_to_string(p.tags, ' '), '')
    
                        ) @@ plainto_tsquery('english', $2)
    
                        OR p.product_name ILIKE '%' || $2 || '%'
    
                        OR p.brand ILIKE '%' || $2 || '%'
    
                      )
    
                ),
    
                semantic_scores AS (
    
                    SELECT 
    
                        pe.product_id,
    
                        1 - (pe.embedding <=> $1::vector) as semantic_score
    
                    FROM retail.product_embeddings pe
    
                    WHERE pe.store_id = $3
    
                      AND 1 - (pe.embedding <=> $1::vector) >= 0.5
    
                ),
    
                combined_scores AS (
    
                    SELECT 
    
                        COALESCE(ks.product_id, ss.product_id) as product_id,
    
                        COALESCE(ks.keyword_score, 0) * $4 as weighted_keyword_score,
    
                        COALESCE(ss.semantic_score, 0) * $5 as weighted_semantic_score,
    
                        COALESCE(ks.keyword_score, 0) * $4 + COALESCE(ss.semantic_score, 0) * $5 as combined_score
    
                    FROM keyword_scores ks
    
                    FULL OUTER JOIN semantic_scores ss ON ks.product_id = ss.product_id
    
                    WHERE COALESCE(ks.keyword_score, 0) * $4 + COALESCE(ss.semantic_score, 0) * $5 > 0
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.current_stock,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    p.tags,
    
                    pc.category_name,
    
                    cs.weighted_keyword_score,
    
                    cs.weighted_semantic_score,
    
                    cs.combined_score
    
                FROM combined_scores cs
    
                JOIN retail.products p ON cs.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE p.is_active = TRUE
    
                ORDER BY cs.combined_score DESC
    
                LIMIT $6
    
            """
    
            
    
            async with self.get_connection() as conn:
    
                # Set store context
    
                await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                
    
                # Execute hybrid search
    
                results = await conn.fetch(
    
                    hybrid_query,
    
                    embedding_vector,  # $1
    
                    query,            # $2
    
                    store_id,         # $3
    
                    keyword_weight,   # $4
    
                    semantic_weight,  # $5
    
                    limit            # $6
    
                )
    
                
    
                return [dict(result) for result in results]
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for hybrid search tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Search query (supports both keywords and natural language)",
    
                        "minLength": 1,
    
                        "maxLength": 500
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for search scope",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of results to return",
    
                        "minimum": 1,
    
                        "maximum": 100,
    
                        "default": 20
    
                    },
    
                    "semantic_weight": {
    
                        "type": "number",
    
                        "description": "Weight for semantic similarity (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "keyword_weight": {
    
                        "type": "number",
    
                        "description": "Weight for keyword matching (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.3
    
                    }
    
                },
    
                "required": ["query", "store_id"],
    
                "additionalProperties": False
    
            }
    
    

    ๐ŸŽฏ Recommendation Systems

    Product Recommendation Engine

    
    # mcp_server/tools/recommendations.py
    
    """
    
    Product recommendation system using embedding similarity.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from ..tools.base import DatabaseTool, ToolResult, ToolCategory
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ProductRecommendationTool(DatabaseTool):
    
        """Generate product recommendations based on similarity and user behavior."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_product_recommendations",
    
                description="Generate personalized product recommendations using similarity analysis",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.ANALYTICS
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute product recommendation generation."""
    
            
    
            recommendation_type = kwargs.get('type', 'similar_products')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for recommendations"
    
                )
    
            
    
            try:
    
                if recommendation_type == 'similar_products':
    
                    return await self._get_similar_products(kwargs)
    
                elif recommendation_type == 'customer_based':
    
                    return await self._get_customer_recommendations(kwargs)
    
                elif recommendation_type == 'trending':
    
                    return await self._get_trending_products(kwargs)
    
                elif recommendation_type == 'cross_sell':
    
                    return await self._get_cross_sell_recommendations(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown recommendation type: {recommendation_type}"
    
                    )
    
            
    
            except Exception as e:
    
                logger.error(f"Product recommendation failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Recommendation generation failed: {str(e)}"
    
                )
    
        
    
        async def _get_similar_products(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Get products similar to a given product using embedding similarity."""
    
            
    
            product_id = kwargs.get('product_id')
    
            store_id = kwargs['store_id']
    
            limit = kwargs.get('limit', 10)
    
            similarity_threshold = kwargs.get('similarity_threshold', 0.7)
    
            
    
            if not product_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="product_id is required for similar product recommendations"
    
                )
    
            
    
            similar_products_query = """
    
                WITH target_product AS (
    
                    SELECT embedding
    
                    FROM retail.product_embeddings
    
                    WHERE product_id = $1 AND store_id = $2
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    pc.category_name,
    
                    1 - (pe.embedding <=> tp.embedding) as similarity_score
    
                FROM retail.product_embeddings pe
    
                CROSS JOIN target_product tp
    
                JOIN retail.products p ON pe.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE pe.store_id = $2
    
                  AND pe.product_id != $1  -- Exclude the target product itself
    
                  AND p.is_active = TRUE
    
                  AND 1 - (pe.embedding <=> tp.embedding) >= $3
    
                ORDER BY pe.embedding <=> tp.embedding
    
                LIMIT $4
    
            """
    
            
    
            result = await self.execute_query(
    
                similar_products_query,
    
                (product_id, store_id, similarity_threshold, limit),
    
                store_id
    
            )
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'recommendation_type': 'similar_products',
    
                    'target_product_id': product_id,
    
                    'similarity_threshold': similarity_threshold,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        async def _get_customer_recommendations(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Get personalized recommendations based on customer purchase history."""
    
            
    
            customer_id = kwargs.get('customer_id')
    
            store_id = kwargs['store_id']
    
            limit = kwargs.get('limit', 10)
    
            days_back = kwargs.get('days_back', 90)
    
            
    
            if not customer_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="customer_id is required for customer-based recommendations"
    
                )
    
            
    
            customer_recommendations_query = """
    
                WITH customer_purchases AS (
    
                    -- Get products purchased by the customer
    
                    SELECT DISTINCT p.product_id, pe.embedding
    
                    FROM retail.sales_transactions st
    
                    JOIN retail.sales_transaction_items sti ON st.transaction_id = sti.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    JOIN retail.product_embeddings pe ON p.product_id = pe.product_id
    
                    WHERE st.customer_id = $1
    
                      AND st.transaction_date >= CURRENT_DATE - INTERVAL '%s days'
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                avg_customer_embedding AS (
    
                    -- Calculate average embedding vector for customer preferences
    
                    SELECT 
    
                        (
    
                            SELECT ARRAY(
    
                                SELECT AVG(embedding_element)
    
                                FROM customer_purchases cp,
    
                                     LATERAL unnest(cp.embedding) WITH ORDINALITY AS t(embedding_element, ordinality)
    
                                GROUP BY ordinality
    
                                ORDER BY ordinality
    
                            )
    
                        )::vector as avg_embedding
    
                    FROM (SELECT 1) dummy
    
                    WHERE EXISTS (SELECT 1 FROM customer_purchases)
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    pc.category_name,
    
                    1 - (pe.embedding <=> ace.avg_embedding) as preference_score
    
                FROM retail.product_embeddings pe
    
                CROSS JOIN avg_customer_embedding ace
    
                JOIN retail.products p ON pe.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE pe.store_id = $2
    
                  AND p.is_active = TRUE
    
                  AND pe.product_id NOT IN (SELECT product_id FROM customer_purchases)
    
                  AND 1 - (pe.embedding <=> ace.avg_embedding) >= 0.6
    
                ORDER BY pe.embedding <=> ace.avg_embedding
    
                LIMIT $3
    
            """ % days_back
    
            
    
            result = await self.execute_query(
    
                customer_recommendations_query,
    
                (customer_id, store_id, limit),
    
                store_id
    
            )
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'recommendation_type': 'customer_based',
    
                    'customer_id': customer_id,
    
                    'days_back': days_back,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for recommendation tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "type": {
    
                        "type": "string",
    
                        "enum": ["similar_products", "customer_based", "trending", "cross_sell"],
    
                        "description": "Type of recommendation to generate",
    
                        "default": "similar_products"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for recommendations",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "product_id": {
    
                        "type": "string",
    
                        "description": "Product ID for similar product recommendations"
    
                    },
    
                    "customer_id": {
    
                        "type": "string",
    
                        "description": "Customer ID for personalized recommendations"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of recommendations",
    
                        "minimum": 1,
    
                        "maximum": 50,
    
                        "default": 10
    
                    },
    
                    "similarity_threshold": {
    
                        "type": "number",
    
                        "description": "Minimum similarity score",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "days_back": {
    
                        "type": "integer",
    
                        "description": "Days of purchase history to consider",
    
                        "minimum": 1,
    
                        "maximum": 365,
    
                        "default": 90
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    

    โšก Performance Optimization

    Vector Query Optimization

    
    -- Optimize pgvector performance
    
    -- Add to postgresql.conf
    
    
    
    # Increase work_mem for vector operations
    
    work_mem = '256MB'
    
    
    
    # Optimize shared_buffers for vector data
    
    shared_buffers = '512MB'
    
    
    
    # Enable parallel query execution
    
    max_parallel_workers_per_gather = 4
    
    max_parallel_workers = 8
    
    
    
    # Vector-specific optimizations
    
    SET maintenance_work_mem = '1GB';
    
    SET max_parallel_maintenance_workers = 4;
    
    
    
    -- Optimize HNSW index parameters
    
    CREATE INDEX CONCURRENTLY idx_product_embeddings_vector_optimized 
    
    ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops)
    
    WITH (m = 16, ef_construction = 200);
    
    
    
    -- Create partial indexes for active products only
    
    CREATE INDEX CONCURRENTLY idx_product_embeddings_active
    
    ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops)
    
    WHERE store_id IN (SELECT store_id FROM retail.stores WHERE is_active = TRUE);
    
    
    
    -- Analyze vector distribution for optimization
    
    ANALYZE retail.product_embeddings;
    
    
    
    -- Vector search performance monitoring
    
    CREATE OR REPLACE FUNCTION retail.analyze_vector_performance()
    
    RETURNS TABLE (
    
        avg_search_time_ms NUMERIC,
    
        index_size TEXT,
    
        total_vectors BIGINT,
    
        cache_hit_ratio NUMERIC
    
    ) AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            (SELECT AVG(EXTRACT(MILLISECONDS FROM clock_timestamp() - query_start))
    
             FROM pg_stat_activity 
    
             WHERE query LIKE '%embedding <=> %'
    
             AND state = 'active') as avg_search_time_ms,
    
            pg_size_pretty(pg_relation_size('idx_product_embeddings_vector')) as index_size,
    
            COUNT(*)::BIGINT as total_vectors,
    
            (SELECT 100.0 * blks_hit / (blks_hit + blks_read) 
    
             FROM pg_stat_user_indexes 
    
             WHERE indexrelname = 'idx_product_embeddings_vector') as cache_hit_ratio
    
        FROM retail.product_embeddings;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    

    Embedding Cache Strategy

    
    # mcp_server/embeddings/cache_manager.py
    
    """
    
    Advanced caching strategy for embeddings and search results.
    
    """
    
    import redis.asyncio as redis
    
    import json
    
    import hashlib
    
    from typing import Dict, Any, List, Optional
    
    from datetime import timedelta
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class EmbeddingCacheManager:
    
        """Advanced caching for embeddings and search results."""
    
        
    
        def __init__(self, redis_url: str = "redis://localhost:6379"):
    
            self.redis_client = None
    
            self.redis_url = redis_url
    
            
    
            # Cache TTL settings
    
            self.embedding_ttl = timedelta(days=7)  # Embeddings cached for 1 week
    
            self.search_ttl = timedelta(hours=1)    # Search results cached for 1 hour
    
            self.recommendation_ttl = timedelta(hours=4)  # Recommendations cached for 4 hours
    
            
    
            # Cache key prefixes
    
            self.EMBEDDING_PREFIX = "emb:"
    
            self.SEARCH_PREFIX = "search:"
    
            self.RECOMMENDATION_PREFIX = "rec:"
    
        
    
        async def initialize(self):
    
            """Initialize Redis connection."""
    
            
    
            try:
    
                self.redis_client = redis.from_url(self.redis_url)
    
                # Test connection
    
                await self.redis_client.ping()
    
                logger.info("Embedding cache manager initialized")
    
            
    
            except Exception as e:
    
                logger.warning(f"Redis cache not available: {e}")
    
                self.redis_client = None
    
        
    
        async def cache_embedding(self, text: str, embedding: List[float], model: str):
    
            """Cache text embedding."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                cache_key = self._get_embedding_key(text, model)
    
                cache_data = {
    
                    'embedding': embedding,
    
                    'model': model,
    
                    'cached_at': str(datetime.utcnow())
    
                }
    
                
    
                await self.redis_client.setex(
    
                    cache_key,
    
                    self.embedding_ttl,
    
                    json.dumps(cache_data)
    
                )
    
                
    
            except Exception as e:
    
                logger.warning(f"Failed to cache embedding: {e}")
    
        
    
        async def get_cached_embedding(self, text: str, model: str) -> Optional[List[float]]:
    
            """Get cached embedding."""
    
            
    
            if not self.redis_client:
    
                return None
    
            
    
            try:
    
                cache_key = self._get_embedding_key(text, model)
    
                cached_data = await self.redis_client.get(cache_key)
    
                
    
                if cached_data:
    
                    data = json.loads(cached_data)
    
                    return data['embedding']
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to retrieve cached embedding: {e}")
    
            
    
            return None
    
        
    
        async def cache_search_results(
    
            self, 
    
            query: str, 
    
            store_id: str, 
    
            results: List[Dict],
    
            search_params: Dict[str, Any]
    
        ):
    
            """Cache search results."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                cache_key = self._get_search_key(query, store_id, search_params)
    
                cache_data = {
    
                    'results': results,
    
                    'query': query,
    
                    'store_id': store_id,
    
                    'params': search_params,
    
                    'cached_at': str(datetime.utcnow())
    
                }
    
                
    
                await self.redis_client.setex(
    
                    cache_key,
    
                    self.search_ttl,
    
                    json.dumps(cache_data, default=str)
    
                )
    
                
    
            except Exception as e:
    
                logger.warning(f"Failed to cache search results: {e}")
    
        
    
        async def get_cached_search_results(
    
            self, 
    
            query: str, 
    
            store_id: str, 
    
            search_params: Dict[str, Any]
    
        ) -> Optional[List[Dict]]:
    
            """Get cached search results."""
    
            
    
            if not self.redis_client:
    
                return None
    
            
    
            try:
    
                cache_key = self._get_search_key(query, store_id, search_params)
    
                cached_data = await self.redis_client.get(cache_key)
    
                
    
                if cached_data:
    
                    data = json.loads(cached_data)
    
                    return data['results']
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to retrieve cached search results: {e}")
    
            
    
            return None
    
        
    
        def _get_embedding_key(self, text: str, model: str) -> str:
    
            """Generate cache key for embedding."""
    
            
    
            content = f"{model}:{text.strip()}"
    
            hash_key = hashlib.sha256(content.encode()).hexdigest()
    
            return f"{self.EMBEDDING_PREFIX}{hash_key}"
    
        
    
        def _get_search_key(self, query: str, store_id: str, params: Dict[str, Any]) -> str:
    
            """Generate cache key for search results."""
    
            
    
            # Create stable hash from query and parameters
    
            content = f"{query}:{store_id}:{json.dumps(params, sort_keys=True)}"
    
            hash_key = hashlib.sha256(content.encode()).hexdigest()
    
            return f"{self.SEARCH_PREFIX}{hash_key}"
    
        
    
        async def invalidate_store_cache(self, store_id: str):
    
            """Invalidate all cached data for a store."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                # Find all keys related to the store
    
                pattern = f"*:{store_id}:*"
    
                keys = await self.redis_client.keys(pattern)
    
                
    
                if keys:
    
                    await self.redis_client.delete(*keys)
    
                    logger.info(f"Invalidated {len(keys)} cache entries for store {store_id}")
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to invalidate store cache: {e}")
    
        
    
        async def cleanup(self):
    
            """Cleanup cache resources."""
    
            
    
            if self.redis_client:
    
                await self.redis_client.close()
    
    
    
    # Global cache manager
    
    cache_manager = EmbeddingCacheManager()
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Azure OpenAI Integration: Complete embedding generation with caching and optimization

    โœ… Vector Search Implementation: Production-ready semantic search with pgvector

    โœ… Hybrid Search Capabilities: Combined keyword and semantic search for optimal results

    โœ… Recommendation Systems: AI-powered product recommendations using similarity

    โœ… Performance Optimization: Vector index optimization and intelligent caching

    โœ… Scalable Architecture: Enterprise-ready semantic search infrastructure

    ๐Ÿš€ What's Next

    Continue with Lab 08: Testing and Debugging to:

  • Implement comprehensive testing strategies for semantic search
  • Debug vector search performance issues
  • Validate embedding quality and relevance
  • Test recommendation system accuracy
  • ๐Ÿ“š Additional Resources

    Azure OpenAI

  • Azure OpenAI Service Documentation - Complete service guide
  • Embeddings API Reference - API documentation
  • Best Practices for Embeddings - Implementation guidance
  • Vector Databases

  • pgvector Documentation - PostgreSQL vector extension
  • Vector Search Optimization - Performance tuning
  • HNSW Algorithm - Hierarchical navigable small world graphs
  • Semantic Search

  • Information Retrieval Fundamentals - Stanford IR textbook
  • Vector Search Best Practices - Implementation patterns
  • Hybrid Search Strategies - Combining different search approaches
  • ---

    Previous: Lab 06: Tool Development

    Next: Lab 08: Testing and Debugging

    Implementing vector embeddings with Azure OpenAI and pgvector Advance

    Semantic Search Integration

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on implementing semantic search capabilities using Azure OpenAI embeddings and PostgreSQL's pgvector extension.

    You'll learn to build AI-powered product search that understands natural language queries and delivers relevant results based on semantic similarity.

    Overview

    Traditional keyword-based search often fails to capture user intent and semantic meaning.

    Semantic search using vector embeddings enables natural language queries like "comfortable running shoes for rainy weather" to find relevant products even if those exact words don't appear in product descriptions.

    Our implementation combines Azure OpenAI's powerful embedding models with PostgreSQL's pgvector extension to create a high-performance, scalable semantic search system that enhances the retail experience with intelligent product discovery.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Integrate Azure OpenAI embedding models for text vectorization
  • Implement pgvector for efficient similarity search operations
  • Build semantic search tools for natural language product queries
  • Create hybrid search combining traditional and vector search
  • Optimize vector queries for production performance
  • Design recommendation systems using embedding similarity
  • ๐Ÿง  Semantic Search Architecture

    Vector Search Pipeline

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                User Query                       โ”‚
    
    โ”‚         "comfortable running shoes"            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚           Azure OpenAI API                     โ”‚
    
    โ”‚        text-embedding-3-small                  โ”‚
    
    โ”‚        Input: Query Text                       โ”‚
    
    โ”‚        Output: 1536-dimensional vector         โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚              pgvector Search                   โ”‚
    
    โ”‚      Cosine Similarity: embedding <=> vector   โ”‚
    
    โ”‚      WHERE similarity > threshold              โ”‚
    
    โ”‚      ORDER BY similarity DESC                  โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚            Ranked Results                      โ”‚
    
    โ”‚    1. Nike Air Zoom (0.89 similarity)         โ”‚
    
    โ”‚    2. Adidas Ultraboost (0.85 similarity)     โ”‚
    
    โ”‚    3. New Balance Fresh Foam (0.82 similarity) โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Embedding Generation Strategy

    
    # mcp_server/embeddings/embedding_manager.py
    
    """
    
    Comprehensive embedding management for semantic search.
    
    """
    
    import asyncio
    
    import hashlib
    
    import json
    
    from typing import List, Dict, Any, Optional, Tuple
    
    from datetime import datetime, timedelta
    
    import numpy as np
    
    from azure.ai.projects.aio import AIProjectClient
    
    from azure.identity.aio import DefaultAzureCredential
    
    from azure.core.exceptions import HttpResponseError
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class EmbeddingManager:
    
        """Manage text embeddings for semantic search."""
    
        
    
        def __init__(self, project_endpoint: str, deployment_name: str = "text-embedding-3-small"):
    
            self.project_endpoint = project_endpoint
    
            self.deployment_name = deployment_name
    
            self.credential = DefaultAzureCredential()
    
            self.client = None
    
            
    
            # Embedding configuration
    
            self.embedding_dimension = 1536  # text-embedding-3-small dimension
    
            self.max_tokens = 8000  # Maximum tokens per request
    
            self.batch_size = 100  # Batch processing size
    
            
    
            # Caching configuration
    
            self.embedding_cache = {}
    
            self.cache_ttl = timedelta(hours=24)
    
            
    
            # Rate limiting
    
            self.rate_limit_requests = 1000  # Per minute
    
            self.rate_limit_tokens = 150000  # Per minute
    
            
    
        async def initialize(self):
    
            """Initialize the Azure AI client."""
    
            
    
            try:
    
                self.client = AIProjectClient(
    
                    endpoint=self.project_endpoint,
    
                    credential=self.credential
    
                )
    
                
    
                # Test connection
    
                await self._test_connection()
    
                
    
                logger.info("Embedding manager initialized successfully")
    
                
    
            except Exception as e:
    
                logger.error(f"Failed to initialize embedding manager: {e}")
    
                raise
    
        
    
        async def _test_connection(self):
    
            """Test Azure OpenAI connection."""
    
            
    
            try:
    
                test_embedding = await self.generate_embedding("test connection")
    
                if len(test_embedding) != self.embedding_dimension:
    
                    raise ValueError(f"Unexpected embedding dimension: {len(test_embedding)}")
    
                
    
                logger.info("Azure OpenAI connection test successful")
    
                
    
            except Exception as e:
    
                logger.error(f"Azure OpenAI connection test failed: {e}")
    
                raise
    
        
    
        async def generate_embedding(self, text: str, use_cache: bool = True) -> List[float]:
    
            """Generate embedding for a single text."""
    
            
    
            if not text or not text.strip():
    
                raise ValueError("Text cannot be empty")
    
            
    
            # Check cache first
    
            if use_cache:
    
                cache_key = self._get_cache_key(text)
    
                cached_embedding = self._get_cached_embedding(cache_key)
    
                if cached_embedding:
    
                    return cached_embedding
    
            
    
            try:
    
                # Ensure client is initialized
    
                if not self.client:
    
                    await self.initialize()
    
                
    
                # Generate embedding
    
                response = await self.client.embeddings.create(
    
                    model=self.deployment_name,
    
                    input=text.strip()
    
                )
    
                
    
                embedding = response.data[0].embedding
    
                
    
                # Cache the result
    
                if use_cache:
    
                    self._cache_embedding(cache_key, embedding)
    
                
    
                logger.debug(f"Generated embedding for text (length: {len(text)})")
    
                
    
                return embedding
    
                
    
            except HttpResponseError as e:
    
                logger.error(f"Azure OpenAI API error: {e}")
    
                raise Exception(f"Embedding generation failed: {e}")
    
            except Exception as e:
    
                logger.error(f"Embedding generation error: {e}")
    
                raise
    
        
    
        async def generate_embeddings_batch(
    
            self, 
    
            texts: List[str], 
    
            use_cache: bool = True
    
        ) -> List[List[float]]:
    
            """Generate embeddings for multiple texts efficiently."""
    
            
    
            if not texts:
    
                return []
    
            
    
            embeddings = []
    
            cache_misses = []
    
            cache_miss_indices = []
    
            
    
            # Check cache for each text
    
            for i, text in enumerate(texts):
    
                if not text or not text.strip():
    
                    embeddings.append([])
    
                    continue
    
                    
    
                if use_cache:
    
                    cache_key = self._get_cache_key(text)
    
                    cached_embedding = self._get_cached_embedding(cache_key)
    
                    if cached_embedding:
    
                        embeddings.append(cached_embedding)
    
                        continue
    
                
    
                # Track cache misses
    
                embeddings.append(None)  # Placeholder
    
                cache_misses.append(text.strip())
    
                cache_miss_indices.append(i)
    
            
    
            # Generate embeddings for cache misses
    
            if cache_misses:
    
                try:
    
                    # Process in batches to respect API limits
    
                    for batch_start in range(0, len(cache_misses), self.batch_size):
    
                        batch_end = min(batch_start + self.batch_size, len(cache_misses))
    
                        batch_texts = cache_misses[batch_start:batch_end]
    
                        
    
                        # Generate batch embeddings
    
                        response = await self.client.embeddings.create(
    
                            model=self.deployment_name,
    
                            input=batch_texts
    
                        )
    
                        
    
                        # Process batch results
    
                        for j, embedding_data in enumerate(response.data):
    
                            actual_index = cache_miss_indices[batch_start + j]
    
                            embedding = embedding_data.embedding
    
                            embeddings[actual_index] = embedding
    
                            
    
                            # Cache the result
    
                            if use_cache:
    
                                text = batch_texts[j]
    
                                cache_key = self._get_cache_key(text)
    
                                self._cache_embedding(cache_key, embedding)
    
                        
    
                        # Rate limiting - small delay between batches
    
                        if batch_end < len(cache_misses):
    
                            await asyncio.sleep(0.1)
    
                    
    
                    logger.info(f"Generated {len(cache_misses)} embeddings in batch")
    
                    
    
                except Exception as e:
    
                    logger.error(f"Batch embedding generation failed: {e}")
    
                    raise
    
            
    
            return embeddings
    
        
    
        def _get_cache_key(self, text: str) -> str:
    
            """Generate cache key for text."""
    
            
    
            # Use SHA-256 hash of text + model for cache key
    
            content = f"{self.deployment_name}:{text.strip()}"
    
            return hashlib.sha256(content.encode()).hexdigest()
    
        
    
        def _get_cached_embedding(self, cache_key: str) -> Optional[List[float]]:
    
            """Get embedding from cache if not expired."""
    
            
    
            if cache_key in self.embedding_cache:
    
                embedding_data = self.embedding_cache[cache_key]
    
                
    
                # Check if cache entry is still valid
    
                if datetime.now() - embedding_data['timestamp'] < self.cache_ttl:
    
                    return embedding_data['embedding']
    
                else:
    
                    # Remove expired entry
    
                    del self.embedding_cache[cache_key]
    
            
    
            return None
    
        
    
        def _cache_embedding(self, cache_key: str, embedding: List[float]):
    
            """Cache embedding with timestamp."""
    
            
    
            self.embedding_cache[cache_key] = {
    
                'embedding': embedding,
    
                'timestamp': datetime.now()
    
            }
    
            
    
            # Limit cache size
    
            if len(self.embedding_cache) > 10000:
    
                # Remove oldest entries
    
                oldest_keys = sorted(
    
                    self.embedding_cache.keys(),
    
                    key=lambda k: self.embedding_cache[k]['timestamp']
    
                )[:1000]
    
                
    
                for key in oldest_keys:
    
                    del self.embedding_cache[key]
    
        
    
        async def cleanup(self):
    
            """Cleanup resources."""
    
            
    
            if self.client:
    
                await self.client.close()
    
            
    
            logger.info("Embedding manager cleanup completed")
    
    
    
    # Global embedding manager instance
    
    embedding_manager = EmbeddingManager(
    
        project_endpoint=os.getenv('PROJECT_ENDPOINT'),
    
        deployment_name=os.getenv('EMBEDDING_DEPLOYMENT_NAME', 'text-embedding-3-small')
    
    )
    
    

    ๐Ÿ” Product Embedding Generation

    Automated Embedding Pipeline

    
    # mcp_server/embeddings/product_embedder.py
    
    """
    
    Product embedding generation and management.
    
    """
    
    import asyncio
    
    import asyncpg
    
    from typing import List, Dict, Any, Optional
    
    from datetime import datetime
    
    import logging
    
    from .embedding_manager import embedding_manager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ProductEmbedder:
    
        """Generate and manage product embeddings for semantic search."""
    
        
    
        def __init__(self, db_provider):
    
            self.db_provider = db_provider
    
            self.embedding_manager = embedding_manager
    
            
    
            # Text combination strategy for products
    
            self.text_template = "{product_name} {brand} {description} {category} {tags}"
    
            
    
        async def generate_product_embeddings(
    
            self, 
    
            store_id: str,
    
            batch_size: int = 50,
    
            force_regenerate: bool = False
    
        ) -> Dict[str, Any]:
    
            """Generate embeddings for all products in a store."""
    
            
    
            async with self.db_provider.get_connection() as conn:
    
                try:
    
                    # Set store context
    
                    await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Get products that need embeddings
    
                    if force_regenerate:
    
                        products_query = """
    
                            SELECT 
    
                                p.product_id,
    
                                p.product_name,
    
                                p.product_description,
    
                                p.brand,
    
                                pc.category_name,
    
                                array_to_string(p.tags, ' ') as tags_text
    
                            FROM retail.products p
    
                            LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                            WHERE p.is_active = TRUE
    
                            ORDER BY p.created_at DESC
    
                        """
    
                    else:
    
                        products_query = """
    
                            SELECT 
    
                                p.product_id,
    
                                p.product_name,
    
                                p.product_description,
    
                                p.brand,
    
                                pc.category_name,
    
                                array_to_string(p.tags, ' ') as tags_text
    
                            FROM retail.products p
    
                            LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                            LEFT JOIN retail.product_embeddings pe ON p.product_id = pe.product_id
    
                            WHERE p.is_active = TRUE
    
                              AND (pe.product_id IS NULL OR pe.updated_at < p.updated_at)
    
                            ORDER BY p.created_at DESC
    
                        """
    
                    
    
                    products = await conn.fetch(products_query)
    
                    
    
                    if not products:
    
                        return {
    
                            'success': True,
    
                            'message': 'No products need embedding generation',
    
                            'processed_count': 0,
    
                            'store_id': store_id
    
                        }
    
                    
    
                    logger.info(f"Generating embeddings for {len(products)} products in store {store_id}")
    
                    
    
                    # Process products in batches
    
                    processed_count = 0
    
                    
    
                    for i in range(0, len(products), batch_size):
    
                        batch = products[i:i + batch_size]
    
                        await self._process_product_batch(conn, batch, store_id)
    
                        processed_count += len(batch)
    
                        
    
                        logger.info(f"Processed {processed_count}/{len(products)} products")
    
                    
    
                    return {
    
                        'success': True,
    
                        'message': f'Successfully generated embeddings for {processed_count} products',
    
                        'processed_count': processed_count,
    
                        'store_id': store_id,
    
                        'total_products': len(products)
    
                    }
    
                    
    
                except Exception as e:
    
                    logger.error(f"Product embedding generation failed: {e}")
    
                    return {
    
                        'success': False,
    
                        'error': str(e),
    
                        'store_id': store_id
    
                    }
    
        
    
        async def _process_product_batch(
    
            self, 
    
            conn: asyncpg.Connection, 
    
            products: List[Dict], 
    
            store_id: str
    
        ):
    
            """Process a batch of products for embedding generation."""
    
            
    
            # Prepare texts for embedding
    
            texts = []
    
            product_ids = []
    
            
    
            for product in products:
    
                # Combine product information into searchable text
    
                combined_text = self._create_product_text(product)
    
                texts.append(combined_text)
    
                product_ids.append(product['product_id'])
    
            
    
            # Generate embeddings
    
            embeddings = await self.embedding_manager.generate_embeddings_batch(texts)
    
            
    
            # Store embeddings in database
    
            for i, (product_id, embedding) in enumerate(zip(product_ids, embeddings)):
    
                if embedding:  # Skip failed embeddings
    
                    await self._store_product_embedding(
    
                        conn, 
    
                        product_id, 
    
                        store_id, 
    
                        texts[i], 
    
                        embedding
    
                    )
    
        
    
        def _create_product_text(self, product: Dict[str, Any]) -> str:
    
            """Create combined text for product embedding."""
    
            
    
            # Handle None values
    
            product_name = product.get('product_name') or ''
    
            brand = product.get('brand') or ''
    
            description = product.get('product_description') or ''
    
            category = product.get('category_name') or ''
    
            tags = product.get('tags_text') or ''
    
            
    
            # Combine into searchable text
    
            combined_text = self.text_template.format(
    
                product_name=product_name,
    
                brand=brand,
    
                description=description,
    
                category=category,
    
                tags=tags
    
            )
    
            
    
            # Clean up extra whitespace
    
            return ' '.join(combined_text.split())
    
        
    
        async def _store_product_embedding(
    
            self,
    
            conn: asyncpg.Connection,
    
            product_id: str,
    
            store_id: str,
    
            embedding_text: str,
    
            embedding: List[float]
    
        ):
    
            """Store product embedding in database."""
    
            
    
            # Convert embedding to pgvector format
    
            embedding_vector = f"[{','.join(map(str, embedding))}]"
    
            
    
            # Upsert embedding
    
            upsert_query = """
    
                INSERT INTO retail.product_embeddings (
    
                    product_id, store_id, embedding_text, embedding, embedding_model
    
                ) VALUES ($1, $2, $3, $4, $5)
    
                ON CONFLICT (product_id, embedding_model) 
    
                DO UPDATE SET
    
                    store_id = EXCLUDED.store_id,
    
                    embedding_text = EXCLUDED.embedding_text,
    
                    embedding = EXCLUDED.embedding,
    
                    updated_at = CURRENT_TIMESTAMP
    
            """
    
            
    
            await conn.execute(
    
                upsert_query,
    
                product_id,
    
                store_id,
    
                embedding_text,
    
                embedding_vector,
    
                self.embedding_manager.deployment_name
    
            )
    
        
    
        async def update_product_embedding(
    
            self, 
    
            product_id: str, 
    
            store_id: str
    
        ) -> Dict[str, Any]:
    
            """Update embedding for a single product."""
    
            
    
            async with self.db_provider.get_connection() as conn:
    
                try:
    
                    # Set store context
    
                    await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Get product information
    
                    product_query = """
    
                        SELECT 
    
                            p.product_id,
    
                            p.product_name,
    
                            p.product_description,
    
                            p.brand,
    
                            pc.category_name,
    
                            array_to_string(p.tags, ' ') as tags_text
    
                        FROM retail.products p
    
                        LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                        WHERE p.product_id = $1 AND p.is_active = TRUE
    
                    """
    
                    
    
                    product = await conn.fetchrow(product_query, product_id)
    
                    
    
                    if not product:
    
                        return {
    
                            'success': False,
    
                            'error': f'Product {product_id} not found or inactive'
    
                        }
    
                    
    
                    # Generate embedding
    
                    combined_text = self._create_product_text(dict(product))
    
                    embedding = await self.embedding_manager.generate_embedding(combined_text)
    
                    
    
                    # Store embedding
    
                    await self._store_product_embedding(
    
                        conn, product_id, store_id, combined_text, embedding
    
                    )
    
                    
    
                    return {
    
                        'success': True,
    
                        'message': f'Successfully updated embedding for product {product_id}',
    
                        'product_id': product_id,
    
                        'store_id': store_id
    
                    }
    
                    
    
                except Exception as e:
    
                    logger.error(f"Single product embedding update failed: {e}")
    
                    return {
    
                        'success': False,
    
                        'error': str(e),
    
                        'product_id': product_id
    
                    }
    
    
    
    # Global product embedder instance
    
    product_embedder = ProductEmbedder(db_provider)
    
    

    ๐Ÿ”Ž Semantic Search Tools

    Semantic Product Search Tool

    
    # mcp_server/tools/semantic_search.py
    
    """
    
    Semantic search tools for natural language product queries.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from ..tools.base import DatabaseTool, ToolResult, ToolCategory
    
    from ..embeddings.embedding_manager import embedding_manager
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class SemanticProductSearchTool(DatabaseTool):
    
        """Advanced semantic search tool for products using vector similarity."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="semantic_search_products",
    
                description="Search products using natural language queries with semantic understanding",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.DATABASE_QUERY
    
            self.embedding_manager = embedding_manager
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute semantic product search."""
    
            
    
            query = kwargs.get('query')
    
            store_id = kwargs.get('store_id')
    
            limit = kwargs.get('limit', 20)
    
            similarity_threshold = kwargs.get('similarity_threshold', 0.7)
    
            include_metadata = kwargs.get('include_metadata', True)
    
            
    
            if not query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Search query is required"
    
                )
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for semantic search"
    
                )
    
            
    
            try:
    
                # Generate query embedding
    
                query_embedding = await self.embedding_manager.generate_embedding(query)
    
                
    
                # Perform semantic search
    
                search_results = await self._perform_semantic_search(
    
                    query_embedding,
    
                    store_id,
    
                    limit,
    
                    similarity_threshold,
    
                    include_metadata
    
                )
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=search_results,
    
                    row_count=len(search_results),
    
                    metadata={
    
                        'query': query,
    
                        'store_id': store_id,
    
                        'similarity_threshold': similarity_threshold,
    
                        'search_type': 'semantic'
    
                    }
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Semantic search failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Semantic search failed: {str(e)}"
    
                )
    
        
    
        async def _perform_semantic_search(
    
            self,
    
            query_embedding: List[float],
    
            store_id: str,
    
            limit: int,
    
            similarity_threshold: float,
    
            include_metadata: bool
    
        ) -> List[Dict[str, Any]]:
    
            """Perform vector similarity search."""
    
            
    
            # Convert embedding to PostgreSQL vector format
    
            embedding_vector = f"[{','.join(map(str, query_embedding))}]"
    
            
    
            # Build search query
    
            if include_metadata:
    
                search_query = """
    
                    SELECT 
    
                        p.product_id,
    
                        p.product_name,
    
                        p.brand,
    
                        p.price,
    
                        p.product_description,
    
                        p.current_stock,
    
                        p.rating_average,
    
                        p.rating_count,
    
                        p.tags,
    
                        pc.category_name,
    
                        pe.embedding_text,
    
                        1 - (pe.embedding <=> $1::vector) as similarity_score
    
                    FROM retail.product_embeddings pe
    
                    JOIN retail.products p ON pe.product_id = p.product_id
    
                    LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                    WHERE pe.store_id = $2
    
                      AND p.is_active = TRUE
    
                      AND 1 - (pe.embedding <=> $1::vector) >= $3
    
                    ORDER BY pe.embedding <=> $1::vector
    
                    LIMIT $4
    
                """
    
            else:
    
                search_query = """
    
                    SELECT 
    
                        p.product_id,
    
                        p.product_name,
    
                        p.brand,
    
                        p.price,
    
                        1 - (pe.embedding <=> $1::vector) as similarity_score
    
                    FROM retail.product_embeddings pe
    
                    JOIN retail.products p ON pe.product_id = p.product_id
    
                    WHERE pe.store_id = $2
    
                      AND p.is_active = TRUE
    
                      AND 1 - (pe.embedding <=> $1::vector) >= $3
    
                    ORDER BY pe.embedding <=> $1::vector
    
                    LIMIT $4
    
                """
    
            
    
            async with self.get_connection() as conn:
    
                # Set store context
    
                await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                
    
                # Execute search
    
                results = await conn.fetch(
    
                    search_query,
    
                    embedding_vector,
    
                    store_id,
    
                    similarity_threshold,
    
                    limit
    
                )
    
                
    
                return [dict(result) for result in results]
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for semantic search tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Natural language search query",
    
                        "minLength": 1,
    
                        "maxLength": 500
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for search scope",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of results to return",
    
                        "minimum": 1,
    
                        "maximum": 100,
    
                        "default": 20
    
                    },
    
                    "similarity_threshold": {
    
                        "type": "number",
    
                        "description": "Minimum similarity score (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "include_metadata": {
    
                        "type": "boolean",
    
                        "description": "Include detailed product metadata in results",
    
                        "default": True
    
                    }
    
                },
    
                "required": ["query", "store_id"],
    
                "additionalProperties": False
    
            }
    
    
    
    class HybridSearchTool(DatabaseTool):
    
        """Hybrid search combining traditional keyword and semantic search."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="hybrid_product_search",
    
                description="Hybrid search combining keyword matching and semantic similarity for optimal results",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.DATABASE_QUERY
    
            self.embedding_manager = embedding_manager
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute hybrid product search."""
    
            
    
            query = kwargs.get('query')
    
            store_id = kwargs.get('store_id')
    
            limit = kwargs.get('limit', 20)
    
            semantic_weight = kwargs.get('semantic_weight', 0.7)
    
            keyword_weight = kwargs.get('keyword_weight', 0.3)
    
            
    
            if not query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Search query is required"
    
                )
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for hybrid search"
    
                )
    
            
    
            try:
    
                # Generate query embedding for semantic search
    
                query_embedding = await self.embedding_manager.generate_embedding(query)
    
                
    
                # Perform hybrid search
    
                search_results = await self._perform_hybrid_search(
    
                    query,
    
                    query_embedding,
    
                    store_id,
    
                    limit,
    
                    semantic_weight,
    
                    keyword_weight
    
                )
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=search_results,
    
                    row_count=len(search_results),
    
                    metadata={
    
                        'query': query,
    
                        'store_id': store_id,
    
                        'semantic_weight': semantic_weight,
    
                        'keyword_weight': keyword_weight,
    
                        'search_type': 'hybrid'
    
                    }
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Hybrid search failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Hybrid search failed: {str(e)}"
    
                )
    
        
    
        async def _perform_hybrid_search(
    
            self,
    
            query: str,
    
            query_embedding: List[float],
    
            store_id: str,
    
            limit: int,
    
            semantic_weight: float,
    
            keyword_weight: float
    
        ) -> List[Dict[str, Any]]:
    
            """Perform hybrid search combining keyword and semantic similarity."""
    
            
    
            # Convert embedding to PostgreSQL vector format
    
            embedding_vector = f"[{','.join(map(str, query_embedding))}]"
    
            
    
            # Create search terms for keyword matching
    
            search_terms = ' & '.join(query.lower().split())
    
            
    
            hybrid_query = """
    
                WITH keyword_scores AS (
    
                    SELECT 
    
                        p.product_id,
    
                        ts_rank(
    
                            to_tsvector('english', 
    
                                p.product_name || ' ' || 
    
                                COALESCE(p.product_description, '') || ' ' || 
    
                                COALESCE(p.brand, '') || ' ' ||
    
                                COALESCE(array_to_string(p.tags, ' '), '')
    
                            ),
    
                            plainto_tsquery('english', $2)
    
                        ) as keyword_score
    
                    FROM retail.products p
    
                    WHERE p.is_active = TRUE
    
                      AND p.store_id = $3
    
                      AND (
    
                        to_tsvector('english', 
    
                            p.product_name || ' ' || 
    
                            COALESCE(p.product_description, '') || ' ' || 
    
                            COALESCE(p.brand, '') || ' ' ||
    
                            COALESCE(array_to_string(p.tags, ' '), '')
    
                        ) @@ plainto_tsquery('english', $2)
    
                        OR p.product_name ILIKE '%' || $2 || '%'
    
                        OR p.brand ILIKE '%' || $2 || '%'
    
                      )
    
                ),
    
                semantic_scores AS (
    
                    SELECT 
    
                        pe.product_id,
    
                        1 - (pe.embedding <=> $1::vector) as semantic_score
    
                    FROM retail.product_embeddings pe
    
                    WHERE pe.store_id = $3
    
                      AND 1 - (pe.embedding <=> $1::vector) >= 0.5
    
                ),
    
                combined_scores AS (
    
                    SELECT 
    
                        COALESCE(ks.product_id, ss.product_id) as product_id,
    
                        COALESCE(ks.keyword_score, 0) * $4 as weighted_keyword_score,
    
                        COALESCE(ss.semantic_score, 0) * $5 as weighted_semantic_score,
    
                        COALESCE(ks.keyword_score, 0) * $4 + COALESCE(ss.semantic_score, 0) * $5 as combined_score
    
                    FROM keyword_scores ks
    
                    FULL OUTER JOIN semantic_scores ss ON ks.product_id = ss.product_id
    
                    WHERE COALESCE(ks.keyword_score, 0) * $4 + COALESCE(ss.semantic_score, 0) * $5 > 0
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.current_stock,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    p.tags,
    
                    pc.category_name,
    
                    cs.weighted_keyword_score,
    
                    cs.weighted_semantic_score,
    
                    cs.combined_score
    
                FROM combined_scores cs
    
                JOIN retail.products p ON cs.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE p.is_active = TRUE
    
                ORDER BY cs.combined_score DESC
    
                LIMIT $6
    
            """
    
            
    
            async with self.get_connection() as conn:
    
                # Set store context
    
                await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                
    
                # Execute hybrid search
    
                results = await conn.fetch(
    
                    hybrid_query,
    
                    embedding_vector,  # $1
    
                    query,            # $2
    
                    store_id,         # $3
    
                    keyword_weight,   # $4
    
                    semantic_weight,  # $5
    
                    limit            # $6
    
                )
    
                
    
                return [dict(result) for result in results]
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for hybrid search tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Search query (supports both keywords and natural language)",
    
                        "minLength": 1,
    
                        "maxLength": 500
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for search scope",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of results to return",
    
                        "minimum": 1,
    
                        "maximum": 100,
    
                        "default": 20
    
                    },
    
                    "semantic_weight": {
    
                        "type": "number",
    
                        "description": "Weight for semantic similarity (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "keyword_weight": {
    
                        "type": "number",
    
                        "description": "Weight for keyword matching (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.3
    
                    }
    
                },
    
                "required": ["query", "store_id"],
    
                "additionalProperties": False
    
            }
    
    

    ๐ŸŽฏ Recommendation Systems

    Product Recommendation Engine

    
    # mcp_server/tools/recommendations.py
    
    """
    
    Product recommendation system using embedding similarity.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from ..tools.base import DatabaseTool, ToolResult, ToolCategory
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ProductRecommendationTool(DatabaseTool):
    
        """Generate product recommendations based on similarity and user behavior."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_product_recommendations",
    
                description="Generate personalized product recommendations using similarity analysis",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.ANALYTICS
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute product recommendation generation."""
    
            
    
            recommendation_type = kwargs.get('type', 'similar_products')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for recommendations"
    
                )
    
            
    
            try:
    
                if recommendation_type == 'similar_products':
    
                    return await self._get_similar_products(kwargs)
    
                elif recommendation_type == 'customer_based':
    
                    return await self._get_customer_recommendations(kwargs)
    
                elif recommendation_type == 'trending':
    
                    return await self._get_trending_products(kwargs)
    
                elif recommendation_type == 'cross_sell':
    
                    return await self._get_cross_sell_recommendations(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown recommendation type: {recommendation_type}"
    
                    )
    
            
    
            except Exception as e:
    
                logger.error(f"Product recommendation failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Recommendation generation failed: {str(e)}"
    
                )
    
        
    
        async def _get_similar_products(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Get products similar to a given product using embedding similarity."""
    
            
    
            product_id = kwargs.get('product_id')
    
            store_id = kwargs['store_id']
    
            limit = kwargs.get('limit', 10)
    
            similarity_threshold = kwargs.get('similarity_threshold', 0.7)
    
            
    
            if not product_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="product_id is required for similar product recommendations"
    
                )
    
            
    
            similar_products_query = """
    
                WITH target_product AS (
    
                    SELECT embedding
    
                    FROM retail.product_embeddings
    
                    WHERE product_id = $1 AND store_id = $2
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    pc.category_name,
    
                    1 - (pe.embedding <=> tp.embedding) as similarity_score
    
                FROM retail.product_embeddings pe
    
                CROSS JOIN target_product tp
    
                JOIN retail.products p ON pe.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE pe.store_id = $2
    
                  AND pe.product_id != $1  -- Exclude the target product itself
    
                  AND p.is_active = TRUE
    
                  AND 1 - (pe.embedding <=> tp.embedding) >= $3
    
                ORDER BY pe.embedding <=> tp.embedding
    
                LIMIT $4
    
            """
    
            
    
            result = await self.execute_query(
    
                similar_products_query,
    
                (product_id, store_id, similarity_threshold, limit),
    
                store_id
    
            )
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'recommendation_type': 'similar_products',
    
                    'target_product_id': product_id,
    
                    'similarity_threshold': similarity_threshold,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        async def _get_customer_recommendations(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Get personalized recommendations based on customer purchase history."""
    
            
    
            customer_id = kwargs.get('customer_id')
    
            store_id = kwargs['store_id']
    
            limit = kwargs.get('limit', 10)
    
            days_back = kwargs.get('days_back', 90)
    
            
    
            if not customer_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="customer_id is required for customer-based recommendations"
    
                )
    
            
    
            customer_recommendations_query = """
    
                WITH customer_purchases AS (
    
                    -- Get products purchased by the customer
    
                    SELECT DISTINCT p.product_id, pe.embedding
    
                    FROM retail.sales_transactions st
    
                    JOIN retail.sales_transaction_items sti ON st.transaction_id = sti.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    JOIN retail.product_embeddings pe ON p.product_id = pe.product_id
    
                    WHERE st.customer_id = $1
    
                      AND st.transaction_date >= CURRENT_DATE - INTERVAL '%s days'
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                avg_customer_embedding AS (
    
                    -- Calculate average embedding vector for customer preferences
    
                    SELECT 
    
                        (
    
                            SELECT ARRAY(
    
                                SELECT AVG(embedding_element)
    
                                FROM customer_purchases cp,
    
                                     LATERAL unnest(cp.embedding) WITH ORDINALITY AS t(embedding_element, ordinality)
    
                                GROUP BY ordinality
    
                                ORDER BY ordinality
    
                            )
    
                        )::vector as avg_embedding
    
                    FROM (SELECT 1) dummy
    
                    WHERE EXISTS (SELECT 1 FROM customer_purchases)
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    pc.category_name,
    
                    1 - (pe.embedding <=> ace.avg_embedding) as preference_score
    
                FROM retail.product_embeddings pe
    
                CROSS JOIN avg_customer_embedding ace
    
                JOIN retail.products p ON pe.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE pe.store_id = $2
    
                  AND p.is_active = TRUE
    
                  AND pe.product_id NOT IN (SELECT product_id FROM customer_purchases)
    
                  AND 1 - (pe.embedding <=> ace.avg_embedding) >= 0.6
    
                ORDER BY pe.embedding <=> ace.avg_embedding
    
                LIMIT $3
    
            """ % days_back
    
            
    
            result = await self.execute_query(
    
                customer_recommendations_query,
    
                (customer_id, store_id, limit),
    
                store_id
    
            )
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'recommendation_type': 'customer_based',
    
                    'customer_id': customer_id,
    
                    'days_back': days_back,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for recommendation tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "type": {
    
                        "type": "string",
    
                        "enum": ["similar_products", "customer_based", "trending", "cross_sell"],
    
                        "description": "Type of recommendation to generate",
    
                        "default": "similar_products"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for recommendations",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "product_id": {
    
                        "type": "string",
    
                        "description": "Product ID for similar product recommendations"
    
                    },
    
                    "customer_id": {
    
                        "type": "string",
    
                        "description": "Customer ID for personalized recommendations"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of recommendations",
    
                        "minimum": 1,
    
                        "maximum": 50,
    
                        "default": 10
    
                    },
    
                    "similarity_threshold": {
    
                        "type": "number",
    
                        "description": "Minimum similarity score",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "days_back": {
    
                        "type": "integer",
    
                        "description": "Days of purchase history to consider",
    
                        "minimum": 1,
    
                        "maximum": 365,
    
                        "default": 90
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    

    โšก Performance Optimization

    Vector Query Optimization

    
    -- Optimize pgvector performance
    
    -- Add to postgresql.conf
    
    
    
    # Increase work_mem for vector operations
    
    work_mem = '256MB'
    
    
    
    # Optimize shared_buffers for vector data
    
    shared_buffers = '512MB'
    
    
    
    # Enable parallel query execution
    
    max_parallel_workers_per_gather = 4
    
    max_parallel_workers = 8
    
    
    
    # Vector-specific optimizations
    
    SET maintenance_work_mem = '1GB';
    
    SET max_parallel_maintenance_workers = 4;
    
    
    
    -- Optimize HNSW index parameters
    
    CREATE INDEX CONCURRENTLY idx_product_embeddings_vector_optimized 
    
    ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops)
    
    WITH (m = 16, ef_construction = 200);
    
    
    
    -- Create partial indexes for active products only
    
    CREATE INDEX CONCURRENTLY idx_product_embeddings_active
    
    ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops)
    
    WHERE store_id IN (SELECT store_id FROM retail.stores WHERE is_active = TRUE);
    
    
    
    -- Analyze vector distribution for optimization
    
    ANALYZE retail.product_embeddings;
    
    
    
    -- Vector search performance monitoring
    
    CREATE OR REPLACE FUNCTION retail.analyze_vector_performance()
    
    RETURNS TABLE (
    
        avg_search_time_ms NUMERIC,
    
        index_size TEXT,
    
        total_vectors BIGINT,
    
        cache_hit_ratio NUMERIC
    
    ) AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            (SELECT AVG(EXTRACT(MILLISECONDS FROM clock_timestamp() - query_start))
    
             FROM pg_stat_activity 
    
             WHERE query LIKE '%embedding <=> %'
    
             AND state = 'active') as avg_search_time_ms,
    
            pg_size_pretty(pg_relation_size('idx_product_embeddings_vector')) as index_size,
    
            COUNT(*)::BIGINT as total_vectors,
    
            (SELECT 100.0 * blks_hit / (blks_hit + blks_read) 
    
             FROM pg_stat_user_indexes 
    
             WHERE indexrelname = 'idx_product_embeddings_vector') as cache_hit_ratio
    
        FROM retail.product_embeddings;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    

    Embedding Cache Strategy

    
    # mcp_server/embeddings/cache_manager.py
    
    """
    
    Advanced caching strategy for embeddings and search results.
    
    """
    
    import redis.asyncio as redis
    
    import json
    
    import hashlib
    
    from typing import Dict, Any, List, Optional
    
    from datetime import timedelta
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class EmbeddingCacheManager:
    
        """Advanced caching for embeddings and search results."""
    
        
    
        def __init__(self, redis_url: str = "redis://localhost:6379"):
    
            self.redis_client = None
    
            self.redis_url = redis_url
    
            
    
            # Cache TTL settings
    
            self.embedding_ttl = timedelta(days=7)  # Embeddings cached for 1 week
    
            self.search_ttl = timedelta(hours=1)    # Search results cached for 1 hour
    
            self.recommendation_ttl = timedelta(hours=4)  # Recommendations cached for 4 hours
    
            
    
            # Cache key prefixes
    
            self.EMBEDDING_PREFIX = "emb:"
    
            self.SEARCH_PREFIX = "search:"
    
            self.RECOMMENDATION_PREFIX = "rec:"
    
        
    
        async def initialize(self):
    
            """Initialize Redis connection."""
    
            
    
            try:
    
                self.redis_client = redis.from_url(self.redis_url)
    
                # Test connection
    
                await self.redis_client.ping()
    
                logger.info("Embedding cache manager initialized")
    
            
    
            except Exception as e:
    
                logger.warning(f"Redis cache not available: {e}")
    
                self.redis_client = None
    
        
    
        async def cache_embedding(self, text: str, embedding: List[float], model: str):
    
            """Cache text embedding."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                cache_key = self._get_embedding_key(text, model)
    
                cache_data = {
    
                    'embedding': embedding,
    
                    'model': model,
    
                    'cached_at': str(datetime.utcnow())
    
                }
    
                
    
                await self.redis_client.setex(
    
                    cache_key,
    
                    self.embedding_ttl,
    
                    json.dumps(cache_data)
    
                )
    
                
    
            except Exception as e:
    
                logger.warning(f"Failed to cache embedding: {e}")
    
        
    
        async def get_cached_embedding(self, text: str, model: str) -> Optional[List[float]]:
    
            """Get cached embedding."""
    
            
    
            if not self.redis_client:
    
                return None
    
            
    
            try:
    
                cache_key = self._get_embedding_key(text, model)
    
                cached_data = await self.redis_client.get(cache_key)
    
                
    
                if cached_data:
    
                    data = json.loads(cached_data)
    
                    return data['embedding']
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to retrieve cached embedding: {e}")
    
            
    
            return None
    
        
    
        async def cache_search_results(
    
            self, 
    
            query: str, 
    
            store_id: str, 
    
            results: List[Dict],
    
            search_params: Dict[str, Any]
    
        ):
    
            """Cache search results."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                cache_key = self._get_search_key(query, store_id, search_params)
    
                cache_data = {
    
                    'results': results,
    
                    'query': query,
    
                    'store_id': store_id,
    
                    'params': search_params,
    
                    'cached_at': str(datetime.utcnow())
    
                }
    
                
    
                await self.redis_client.setex(
    
                    cache_key,
    
                    self.search_ttl,
    
                    json.dumps(cache_data, default=str)
    
                )
    
                
    
            except Exception as e:
    
                logger.warning(f"Failed to cache search results: {e}")
    
        
    
        async def get_cached_search_results(
    
            self, 
    
            query: str, 
    
            store_id: str, 
    
            search_params: Dict[str, Any]
    
        ) -> Optional[List[Dict]]:
    
            """Get cached search results."""
    
            
    
            if not self.redis_client:
    
                return None
    
            
    
            try:
    
                cache_key = self._get_search_key(query, store_id, search_params)
    
                cached_data = await self.redis_client.get(cache_key)
    
                
    
                if cached_data:
    
                    data = json.loads(cached_data)
    
                    return data['results']
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to retrieve cached search results: {e}")
    
            
    
            return None
    
        
    
        def _get_embedding_key(self, text: str, model: str) -> str:
    
            """Generate cache key for embedding."""
    
            
    
            content = f"{model}:{text.strip()}"
    
            hash_key = hashlib.sha256(content.encode()).hexdigest()
    
            return f"{self.EMBEDDING_PREFIX}{hash_key}"
    
        
    
        def _get_search_key(self, query: str, store_id: str, params: Dict[str, Any]) -> str:
    
            """Generate cache key for search results."""
    
            
    
            # Create stable hash from query and parameters
    
            content = f"{query}:{store_id}:{json.dumps(params, sort_keys=True)}"
    
            hash_key = hashlib.sha256(content.encode()).hexdigest()
    
            return f"{self.SEARCH_PREFIX}{hash_key}"
    
        
    
        async def invalidate_store_cache(self, store_id: str):
    
            """Invalidate all cached data for a store."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                # Find all keys related to the store
    
                pattern = f"*:{store_id}:*"
    
                keys = await self.redis_client.keys(pattern)
    
                
    
                if keys:
    
                    await self.redis_client.delete(*keys)
    
                    logger.info(f"Invalidated {len(keys)} cache entries for store {store_id}")
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to invalidate store cache: {e}")
    
        
    
        async def cleanup(self):
    
            """Cleanup cache resources."""
    
            
    
            if self.redis_client:
    
                await self.redis_client.close()
    
    
    
    # Global cache manager
    
    cache_manager = EmbeddingCacheManager()
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Azure OpenAI Integration: Complete embedding generation with caching and optimization

    โœ… Vector Search Implementation: Production-ready semantic search with pgvector

    โœ… Hybrid Search Capabilities: Combined keyword and semantic search for optimal results

    โœ… Recommendation Systems: AI-powered product recommendations using similarity

    โœ… Performance Optimization: Vector index optimization and intelligent caching

    โœ… Scalable Architecture: Enterprise-ready semantic search infrastructure

    ๐Ÿš€ What's Next

    Continue with Lab 08: Testing and Debugging to:

  • Implement comprehensive testing strategies for semantic search
  • Debug vector search performance issues
  • Validate embedding quality and relevance
  • Test recommendation system accuracy
  • ๐Ÿ“š Additional Resources

    Azure OpenAI

  • Azure OpenAI Service Documentation - Complete service guide
  • Embeddings API Reference - API documentation
  • Best Practices for Embeddings - Implementation guidance
  • Vector Databases

  • pgvector Documentation - PostgreSQL vector extension
  • Vector Search Optimization - Performance tuning
  • HNSW Algorithm - Hierarchical navigable small world graphs
  • Semantic Search

  • Information Retrieval Fundamentals - Stanford IR textbook
  • Vector Search Best Practices - Implementation patterns
  • Hybrid Search Strategies - Combining different search approaches
  • ---

    Previous: Lab 06: Tool Development

    Next: Lab 08: Testing and Debugging

    08 Testing and Debugging

    Testing and Debugging

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on testing and debugging MCP servers in production environments.

    You'll learn to implement robust testing strategies, debug complex issues, and ensure your MCP server performs reliably under various conditions.

    Overview

    Testing MCP servers requires a multi-layered approach covering unit tests, integration tests, performance validation, and real-world scenario testing. This lab covers the complete testing lifecycle from development to production monitoring.

    Our testing strategy emphasizes reliability, security, and performance, ensuring your MCP server can handle production workloads while maintaining data integrity and user experience quality.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Implement comprehensive unit and integration test suites
  • Design effective testing strategies for MCP tools and database operations
  • Debug complex issues using advanced debugging techniques
  • Validate performance under load with realistic testing scenarios
  • Monitor production systems with effective alerting and observability
  • Automate testing workflows for continuous integration
  • ๐Ÿงช Testing Architecture

    Testing Strategy Overview

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                Unit Tests                       โ”‚
    
    โ”‚   โ€ข Tool execution logic                       โ”‚
    
    โ”‚   โ€ข Database query validation                  โ”‚
    
    โ”‚   โ€ข Authentication/authorization               โ”‚
    
    โ”‚   โ€ข Embedding generation                       โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚             Integration Tests                   โ”‚
    
    โ”‚   โ€ข End-to-end MCP workflows                  โ”‚
    
    โ”‚   โ€ข Database schema validation                 โ”‚
    
    โ”‚   โ€ข API endpoint testing                       โ”‚
    
    โ”‚   โ€ข Multi-tool interactions                    โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚            Performance Tests                    โ”‚
    
    โ”‚   โ€ข Load testing under realistic conditions    โ”‚
    
    โ”‚   โ€ข Database performance validation            โ”‚
    
    โ”‚   โ€ข Memory and resource usage                  โ”‚
    
    โ”‚   โ€ข Embedding generation performance           โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚              E2E Tests                         โ”‚
    
    โ”‚   โ€ข Complete user workflows                    โ”‚
    
    โ”‚   โ€ข VS Code integration testing               โ”‚
    
    โ”‚   โ€ข Real-world scenario validation            โ”‚
    
    โ”‚   โ€ข Cross-browser compatibility               โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Test Environment Setup

    
    # tests/conftest.py
    
    """
    
    Pytest configuration and shared fixtures for MCP server testing.
    
    """
    
    import pytest
    
    import asyncio
    
    import asyncpg
    
    import os
    
    from typing import AsyncGenerator, Dict, Any
    
    from unittest.mock import AsyncMock, Mock
    
    import tempfile
    
    import shutil
    
    from datetime import datetime
    
    
    
    # Test configuration
    
    TEST_DATABASE_URL = "postgresql://test_user:test_pass@localhost:5432/test_retail_db"
    
    TEST_STORE_IDS = ['test_seattle', 'test_redmond', 'test_bellevue']
    
    
    
    @pytest.fixture(scope="session")
    
    def event_loop():
    
        """Create an instance of the default event loop for the test session."""
    
        loop = asyncio.get_event_loop_policy().new_event_loop()
    
        yield loop
    
        loop.close()
    
    
    
    @pytest.fixture(scope="session")
    
    async def test_database():
    
        """Set up test database with schema and sample data."""
    
        
    
        # Create test database connection
    
        sys_conn = await asyncpg.connect(
    
            "postgresql://postgres:password@localhost:5432/postgres"
    
        )
    
        
    
        try:
    
            # Create test database
    
            await sys_conn.execute("DROP DATABASE IF EXISTS test_retail_db")
    
            await sys_conn.execute("CREATE DATABASE test_retail_db")
    
        finally:
    
            await sys_conn.close()
    
        
    
        # Connect to test database and set up schema
    
        test_conn = await asyncpg.connect(TEST_DATABASE_URL)
    
        
    
        try:
    
            # Load schema
    
            schema_sql = await load_sql_file("../scripts/create_schema.sql")
    
            await test_conn.execute(schema_sql)
    
            
    
            # Load sample data
    
            sample_data_sql = await load_sql_file("../scripts/sample_data.sql")
    
            await test_conn.execute(sample_data_sql)
    
            
    
            yield test_conn
    
            
    
        finally:
    
            await test_conn.close()
    
            
    
            # Cleanup test database
    
            sys_conn = await asyncpg.connect(
    
                "postgresql://postgres:password@localhost:5432/postgres"
    
            )
    
            try:
    
                await sys_conn.execute("DROP DATABASE IF EXISTS test_retail_db")
    
            finally:
    
                await sys_conn.close()
    
    
    
    @pytest.fixture
    
    async def db_connection(test_database):
    
        """Provide a clean database connection for each test."""
    
        
    
        conn = await asyncpg.connect(TEST_DATABASE_URL)
    
        
    
        # Start transaction for test isolation
    
        tx = conn.transaction()
    
        await tx.start()
    
        
    
        try:
    
            yield conn
    
        finally:
    
            # Rollback transaction to maintain test isolation
    
            await tx.rollback()
    
            await conn.close()
    
    
    
    @pytest.fixture
    
    async def mock_embedding_manager():
    
        """Mock embedding manager for testing without Azure OpenAI calls."""
    
        
    
        mock_manager = AsyncMock()
    
        
    
        # Mock embedding generation
    
        mock_manager.generate_embedding.return_value = [0.1] * 1536  # Mock embedding
    
        mock_manager.generate_embeddings_batch.return_value = [[0.1] * 1536] * 10
    
        
    
        # Mock initialization
    
        mock_manager.initialize.return_value = None
    
        mock_manager.cleanup.return_value = None
    
        
    
        return mock_manager
    
    
    
    @pytest.fixture
    
    async def test_mcp_server(db_connection, mock_embedding_manager):
    
        """Set up test MCP server instance."""
    
        
    
        from mcp_server.server import MCPServer
    
        from mcp_server.database import DatabaseProvider
    
        from mcp_server.config import Config
    
        
    
        # Create test configuration
    
        config = Config()
    
        config.database.connection_string = TEST_DATABASE_URL
    
        config.server.enable_debug = True
    
        
    
        # Create database provider
    
        db_provider = DatabaseProvider(config.database.connection_string)
    
        await db_provider.initialize()
    
        
    
        # Create MCP server
    
        server = MCPServer(config, db_provider)
    
        server.embedding_manager = mock_embedding_manager
    
        
    
        await server.initialize()
    
        
    
        yield server
    
        
    
        await server.cleanup()
    
    
    
    @pytest.fixture
    
    def sample_products():
    
        """Sample product data for testing."""
    
        
    
        return [
    
            {
    
                'product_id': 'test-product-1',
    
                'product_name': 'Test Running Shoes',
    
                'brand': 'TestBrand',
    
                'price': 99.99,
    
                'product_description': 'Comfortable running shoes for daily training',
    
                'category_name': 'Electronics',
    
                'current_stock': 50
    
            },
    
            {
    
                'product_id': 'test-product-2',
    
                'product_name': 'Test Laptop',
    
                'brand': 'TestTech',
    
                'price': 1299.99,
    
                'product_description': 'High-performance laptop for professional use',
    
                'category_name': 'Electronics',
    
                'current_stock': 25
    
            }
    
        ]
    
    
    
    async def load_sql_file(file_path: str) -> str:
    
        """Load SQL file content."""
    
        
    
        with open(file_path, 'r') as file:
    
            return file.read()
    
    
    
    # Test data helpers
    
    class TestDataHelper:
    
        """Helper class for managing test data."""
    
        
    
        @staticmethod
    
        async def create_test_store(conn: asyncpg.Connection, store_id: str) -> Dict[str, Any]:
    
            """Create a test store."""
    
            
    
            store_data = {
    
                'store_id': store_id,
    
                'store_name': f'Test Store {store_id}',
    
                'store_location': 'Test Location',
    
                'store_type': 'test',
    
                'region': 'test'
    
            }
    
            
    
            await conn.execute("""
    
                INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region)
    
                VALUES ($1, $2, $3, $4, $5)
    
                ON CONFLICT (store_id) DO NOTHING
    
            """, *store_data.values())
    
            
    
            return store_data
    
        
    
        @staticmethod
    
        async def create_test_customer(conn: asyncpg.Connection, store_id: str) -> str:
    
            """Create a test customer."""
    
            
    
            customer_id = await conn.fetchval("""
    
                INSERT INTO retail.customers (
    
                    store_id, first_name, last_name, email, loyalty_tier
    
                ) VALUES ($1, $2, $3, $4, $5)
    
                RETURNING customer_id
    
            """, store_id, 'Test', 'Customer', 'test@example.com', 'bronze')
    
            
    
            return customer_id
    
        
    
        @staticmethod
    
        async def create_test_product(
    
            conn: asyncpg.Connection, 
    
            store_id: str, 
    
            product_data: Dict[str, Any]
    
        ) -> str:
    
            """Create a test product."""
    
            
    
            product_id = await conn.fetchval("""
    
                INSERT INTO retail.products (
    
                    store_id, sku, product_name, brand, price, product_description, current_stock
    
                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    
                RETURNING product_id
    
            """, 
    
                store_id, 
    
                f"TEST-{product_data['product_name'][:10]}",
    
                product_data['product_name'],
    
                product_data['brand'],
    
                product_data['price'],
    
                product_data['product_description'],
    
                product_data['current_stock']
    
            )
    
            
    
            return product_id
    
    

    ๐Ÿ”ง Unit Testing

    Tool Testing Framework

    
    # tests/test_tools.py
    
    """
    
    Comprehensive unit tests for MCP tools.
    
    """
    
    import pytest
    
    import asyncio
    
    from unittest.mock import AsyncMock, patch
    
    from datetime import datetime, timedelta
    
    
    
    from mcp_server.tools.sales_analysis import SalesAnalysisTool
    
    from mcp_server.tools.semantic_search import SemanticProductSearchTool
    
    from mcp_server.tools.schema_introspection import SchemaIntrospectionTool
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestSalesAnalysisTool:
    
        """Test sales analysis tool functionality."""
    
        
    
        @pytest.fixture
    
        async def sales_tool(self, test_mcp_server):
    
            """Create sales analysis tool for testing."""
    
            return SalesAnalysisTool(test_mcp_server.db_provider)
    
        
    
        async def test_daily_sales_query(self, sales_tool, db_connection):
    
            """Test daily sales analysis query."""
    
            
    
            # Set up test data
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            customer_id = await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Create test transaction
    
            await db_connection.execute("""
    
                INSERT INTO retail.sales_transactions (
    
                    store_id, customer_id, transaction_date, total_amount, transaction_type
    
                ) VALUES ($1, $2, $3, $4, $5)
    
            """, store_id, customer_id, datetime.now(), 150.00, 'sale')
    
            
    
            # Execute tool
    
            result = await sales_tool.execute(
    
                query_type='daily_sales',
    
                store_id=store_id,
    
                start_date=(datetime.now() - timedelta(days=7)).date(),
    
                end_date=datetime.now().date()
    
            )
    
            
    
            # Validate results
    
            assert result.success is True
    
            assert len(result.data) > 0
    
            assert 'total_revenue' in result.data[0]
    
            assert result.metadata['query_type'] == 'daily_sales'
    
        
    
        async def test_custom_query_validation(self, sales_tool, db_connection):
    
            """Test custom query validation."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Test valid query
    
            valid_query = "SELECT COUNT(*) as customer_count FROM retail.customers"
    
            result = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store_id,
    
                query=valid_query
    
            )
    
            
    
            assert result.success is True
    
            
    
            # Test invalid query (should be blocked)
    
            invalid_query = "DROP TABLE retail.customers"
    
            result = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store_id,
    
                query=invalid_query
    
            )
    
            
    
            assert result.success is False
    
            assert 'validation failed' in result.error.lower()
    
        
    
        async def test_store_isolation(self, sales_tool, db_connection):
    
            """Test that store isolation works correctly."""
    
            
    
            # Create two different stores
    
            store1 = 'test_store1'
    
            store2 = 'test_store2'
    
            
    
            await TestDataHelper.create_test_store(db_connection, store1)
    
            await TestDataHelper.create_test_store(db_connection, store2)
    
            
    
            # Create customers in different stores
    
            customer1 = await TestDataHelper.create_test_customer(db_connection, store1)
    
            customer2 = await TestDataHelper.create_test_customer(db_connection, store2)
    
            
    
            # Query from store1 should only see store1 data
    
            result1 = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store1,
    
                query="SELECT COUNT(*) as count FROM retail.customers"
    
            )
    
            
    
            # Query from store2 should only see store2 data
    
            result2 = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store2,
    
                query="SELECT COUNT(*) as count FROM retail.customers"
    
            )
    
            
    
            assert result1.success is True
    
            assert result2.success is True
    
            assert result1.data[0]['count'] == 1
    
            assert result2.data[0]['count'] == 1
    
    
    
    class TestSemanticSearchTool:
    
        """Test semantic search tool functionality."""
    
        
    
        @pytest.fixture
    
        async def search_tool(self, test_mcp_server):
    
            """Create semantic search tool for testing."""
    
            return SemanticProductSearchTool(test_mcp_server.db_provider)
    
        
    
        async def test_semantic_search_execution(self, search_tool, db_connection, sample_products):
    
            """Test semantic search with mock embeddings."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test products
    
            for product_data in sample_products:
    
                product_id = await TestDataHelper.create_test_product(
    
                    db_connection, store_id, product_data
    
                )
    
                
    
                # Create mock embedding
    
                await db_connection.execute("""
    
                    INSERT INTO retail.product_embeddings (
    
                        product_id, store_id, embedding_text, embedding
    
                    ) VALUES ($1, $2, $3, $4)
    
                """, 
    
                    product_id, store_id, 
    
                    f"{product_data['product_name']} {product_data['brand']}", 
    
                    '[0.1,0.2,0.3]'  # Mock embedding
    
                )
    
            
    
            # Execute search
    
            result = await search_tool.execute(
    
                query='running shoes',
    
                store_id=store_id,
    
                limit=10,
    
                similarity_threshold=0.0
    
            )
    
            
    
            # Validate results
    
            assert result.success is True
    
            assert len(result.data) > 0
    
            assert 'similarity_score' in result.data[0]
    
            assert result.metadata['search_type'] == 'semantic'
    
        
    
        async def test_search_parameter_validation(self, search_tool):
    
            """Test search parameter validation."""
    
            
    
            # Test missing query
    
            result = await search_tool.execute(store_id='test_store')
    
            assert result.success is False
    
            assert 'query is required' in result.error.lower()
    
            
    
            # Test missing store_id
    
            result = await search_tool.execute(query='test query')
    
            assert result.success is False
    
            assert 'store_id is required' in result.error.lower()
    
    
    
    class TestSchemaIntrospectionTool:
    
        """Test schema introspection tool."""
    
        
    
        @pytest.fixture
    
        async def schema_tool(self, test_mcp_server):
    
            """Create schema introspection tool for testing."""
    
            return SchemaIntrospectionTool(test_mcp_server.db_provider)
    
        
    
        async def test_single_table_schema(self, schema_tool, db_connection):
    
            """Test getting schema for a single table."""
    
            
    
            result = await schema_tool.execute(
    
                table_name='customers',
    
                include_constraints=True,
    
                include_indexes=True
    
            )
    
            
    
            assert result.success is True
    
            assert result.data['table_name'] == 'customers'
    
            assert len(result.data['columns']) > 0
    
            assert 'customer_id' in [col['column_name'] for col in result.data['columns']]
    
        
    
        async def test_all_tables_schema(self, schema_tool, db_connection):
    
            """Test getting schema for all tables."""
    
            
    
            result = await schema_tool.execute()
    
            
    
            assert result.success is True
    
            assert result.data['schema_name'] == 'retail'
    
            assert len(result.data['tables']) > 0
    
            
    
            table_names = [table['table_name'] for table in result.data['tables']]
    
            expected_tables = ['customers', 'products', 'sales_transactions']
    
            
    
            for expected_table in expected_tables:
    
                assert expected_table in table_names
    
    

    Database Testing

    
    # tests/test_database.py
    
    """
    
    Database layer testing including RLS and security.
    
    """
    
    import pytest
    
    import asyncpg
    
    from datetime import datetime
    
    
    
    from mcp_server.database import DatabaseProvider
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestRowLevelSecurity:
    
        """Test Row Level Security implementation."""
    
        
    
        async def test_store_context_setting(self, db_connection):
    
            """Test that store context is set correctly."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Set store context
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store_id)
    
            
    
            # Verify context is set
    
            current_store = await db_connection.fetchval(
    
                "SELECT current_setting('app.current_store_id', true)"
    
            )
    
            
    
            assert current_store == store_id
    
        
    
        async def test_customer_isolation(self, db_connection):
    
            """Test that customers are properly isolated by store."""
    
            
    
            # Create two stores
    
            store1 = 'test_store1'
    
            store2 = 'test_store2'
    
            
    
            await TestDataHelper.create_test_store(db_connection, store1)
    
            await TestDataHelper.create_test_store(db_connection, store2)
    
            
    
            # Create customers in different stores
    
            await TestDataHelper.create_test_customer(db_connection, store1)
    
            await TestDataHelper.create_test_customer(db_connection, store2)
    
            
    
            # Set context to store1 and count customers
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store1)
    
            store1_count = await db_connection.fetchval("SELECT COUNT(*) FROM retail.customers")
    
            
    
            # Set context to store2 and count customers
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store2)
    
            store2_count = await db_connection.fetchval("SELECT COUNT(*) FROM retail.customers")
    
            
    
            # Each store should only see its own customers
    
            assert store1_count == 1
    
            assert store2_count == 1
    
        
    
        async def test_invalid_store_context(self, db_connection):
    
            """Test that invalid store context raises error."""
    
            
    
            with pytest.raises(Exception) as exc_info:
    
                await db_connection.execute("SELECT retail.set_store_context($1)", 'invalid_store')
    
            
    
            assert "Store not found" in str(exc_info.value)
    
        
    
        async def test_cross_store_data_insertion_blocked(self, db_connection):
    
            """Test that users cannot insert data for other stores."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Set store context
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store_id)
    
            
    
            # Try to insert customer for different store (should fail)
    
            with pytest.raises(Exception):
    
                await db_connection.execute("""
    
                    INSERT INTO retail.customers (store_id, first_name, last_name, email)
    
                    VALUES ($1, $2, $3, $4)
    
                """, 'different_store', 'Test', 'Customer', 'test@example.com')
    
    
    
    class TestDatabaseProvider:
    
        """Test database provider functionality."""
    
        
    
        @pytest.fixture
    
        async def db_provider(self):
    
            """Create database provider for testing."""
    
            
    
            provider = DatabaseProvider(TEST_DATABASE_URL)
    
            await provider.initialize()
    
            yield provider
    
            await provider.cleanup()
    
        
    
        async def test_connection_pooling(self, db_provider):
    
            """Test connection pool functionality."""
    
            
    
            # Get multiple connections
    
            conn1 = await db_provider.get_connection()
    
            conn2 = await db_provider.get_connection()
    
            
    
            assert conn1 is not None
    
            assert conn2 is not None
    
            assert conn1 != conn2  # Should be different connection objects
    
            
    
            # Release connections
    
            await db_provider.release_connection(conn1)
    
            await db_provider.release_connection(conn2)
    
        
    
        async def test_health_check(self, db_provider):
    
            """Test database health check."""
    
            
    
            health_status = await db_provider.health_check()
    
            
    
            assert health_status['status'] == 'healthy'
    
            assert 'connection_pool_size' in health_status
    
            assert 'database_version' in health_status
    
        
    
        async def test_connection_recovery(self, db_provider):
    
            """Test connection recovery after database issues."""
    
            
    
            # This would test connection recovery scenarios
    
            # In a real test, you might temporarily break the connection
    
            # and verify that the pool recovers
    
            
    
            # For now, just verify health check works
    
            health_status = await db_provider.health_check()
    
            assert health_status['status'] == 'healthy'
    
    

    ๐Ÿš€ Integration Testing

    End-to-End Workflow Testing

    
    # tests/test_integration.py
    
    """
    
    Integration tests for complete MCP workflows.
    
    """
    
    import pytest
    
    import json
    
    from datetime import datetime, timedelta
    
    
    
    from mcp_server.server import MCPServer
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestMCPWorkflows:
    
        """Test complete MCP server workflows."""
    
        
    
        async def test_product_search_workflow(self, test_mcp_server, db_connection, sample_products):
    
            """Test complete product search workflow."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test products with embeddings
    
            for product_data in sample_products:
    
                product_id = await TestDataHelper.create_test_product(
    
                    db_connection, store_id, product_data
    
                )
    
                
    
                # Create embedding for product
    
                await db_connection.execute("""
    
                    INSERT INTO retail.product_embeddings (
    
                        product_id, store_id, embedding_text, embedding
    
                    ) VALUES ($1, $2, $3, $4)
    
                """, 
    
                    product_id, store_id, 
    
                    f"{product_data['product_name']} {product_data['brand']}", 
    
                    '[' + ','.join(['0.1'] * 1536) + ']'  # Mock embedding
    
                )
    
            
    
            # Test semantic search
    
            search_result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {
    
                    'query': 'running shoes',
    
                    'store_id': store_id,
    
                    'limit': 10
    
                }
    
            )
    
            
    
            assert search_result['success'] is True
    
            assert len(search_result['data']) > 0
    
            
    
            # Test schema introspection
    
            schema_result = await test_mcp_server.execute_tool(
    
                'get_table_schema',
    
                {'table_name': 'products'}
    
            )
    
            
    
            assert schema_result['success'] is True
    
            assert schema_result['data']['table_name'] == 'products'
    
        
    
        async def test_sales_analysis_workflow(self, test_mcp_server, db_connection):
    
            """Test sales analysis workflow."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test customer and product
    
            customer_id = await TestDataHelper.create_test_customer(db_connection, store_id)
    
            product_id = await TestDataHelper.create_test_product(
    
                db_connection, store_id, {
    
                    'product_name': 'Test Product',
    
                    'brand': 'TestBrand',
    
                    'price': 99.99,
    
                    'product_description': 'Test product description',
    
                    'current_stock': 50
    
                }
    
            )
    
            
    
            # Create test transaction
    
            transaction_id = await db_connection.fetchval("""
    
                INSERT INTO retail.sales_transactions (
    
                    store_id, customer_id, transaction_date, total_amount, 
    
                    subtotal, tax_amount, transaction_type
    
                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    
                RETURNING transaction_id
    
            """, store_id, customer_id, datetime.now(), 107.99, 99.99, 8.00, 'sale')
    
            
    
            # Create transaction item
    
            await db_connection.execute("""
    
                INSERT INTO retail.sales_transaction_items (
    
                    transaction_id, product_id, quantity, unit_price, total_price
    
                ) VALUES ($1, $2, $3, $4, $5)
    
            """, transaction_id, product_id, 1, 99.99, 99.99)
    
            
    
            # Test daily sales analysis
    
            sales_result = await test_mcp_server.execute_tool(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'daily_sales',
    
                    'store_id': store_id,
    
                    'start_date': (datetime.now() - timedelta(days=1)).date().isoformat(),
    
                    'end_date': datetime.now().date().isoformat()
    
                }
    
            )
    
            
    
            assert sales_result['success'] is True
    
            assert len(sales_result['data']) > 0
    
            assert sales_result['data'][0]['total_revenue'] == 107.99
    
        
    
        async def test_multi_store_workflow(self, test_mcp_server, db_connection):
    
            """Test workflows across multiple stores."""
    
            
    
            # Create multiple stores
    
            stores = ['test_seattle', 'test_redmond', 'test_bellevue']
    
            
    
            for store_id in stores:
    
                await TestDataHelper.create_test_store(db_connection, store_id)
    
                
    
                # Create customer in each store
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Test that each store sees only its own data
    
            for store_id in stores:
    
                schema_result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) as customer_count FROM retail.customers'
    
                    }
    
                )
    
                
    
                assert schema_result['success'] is True
    
                assert schema_result['data'][0]['customer_count'] == 1
    
    
    
    class TestErrorHandling:
    
        """Test error handling and edge cases."""
    
        
    
        async def test_database_connection_failure(self, test_mcp_server):
    
            """Test behavior when database connection fails."""
    
            
    
            # Simulate database failure by using invalid connection
    
            with patch.object(test_mcp_server.db_provider, 'get_connection') as mock_conn:
    
                mock_conn.side_effect = Exception("Database connection failed")
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'get_table_schema',
    
                    {'table_name': 'customers'}
    
                )
    
                
    
                assert result['success'] is False
    
                assert 'connection failed' in result['error'].lower()
    
        
    
        async def test_invalid_tool_parameters(self, test_mcp_server):
    
            """Test handling of invalid tool parameters."""
    
            
    
            # Missing required parameter
    
            result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {'query': 'test query'}  # Missing store_id
    
            )
    
            
    
            assert result['success'] is False
    
            assert 'store_id is required' in result['error'].lower()
    
            
    
            # Invalid parameter type
    
            result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {
    
                    'query': 'test query',
    
                    'store_id': 'test_store',
    
                    'limit': 'invalid'  # Should be integer
    
                }
    
            )
    
            
    
            assert result['success'] is False
    
        
    
        async def test_sql_injection_prevention(self, test_mcp_server, db_connection):
    
            """Test that SQL injection attempts are blocked."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Attempt SQL injection
    
            malicious_query = "SELECT * FROM retail.customers; DROP TABLE retail.customers; --"
    
            
    
            result = await test_mcp_server.execute_tool(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'query': malicious_query
    
                }
    
            )
    
            
    
            assert result['success'] is False
    
            assert 'validation failed' in result['error'].lower()
    
    

    ๐Ÿ“Š Performance Testing

    Load Testing Framework

    
    # tests/test_performance.py
    
    """
    
    Performance and load testing for MCP server.
    
    """
    
    import pytest
    
    import asyncio
    
    import time
    
    import statistics
    
    from concurrent.futures import ThreadPoolExecutor
    
    from typing import List, Dict, Any
    
    
    
    class TestPerformance:
    
        """Performance testing for MCP server operations."""
    
        
    
        async def test_concurrent_tool_execution(self, test_mcp_server, db_connection):
    
            """Test performance under concurrent tool execution."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test data
    
            for i in range(100):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Define test scenarios
    
            async def execute_tool_scenario():
    
                """Execute a tool and measure performance."""
    
                start_time = time.time()
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) as count FROM retail.customers'
    
                    }
    
                )
    
                
    
                execution_time = time.time() - start_time
    
                return {
    
                    'success': result['success'],
    
                    'execution_time': execution_time
    
                }
    
            
    
            # Run concurrent executions
    
            concurrent_tasks = 20
    
            tasks = [execute_tool_scenario() for _ in range(concurrent_tasks)]
    
            
    
            start_time = time.time()
    
            results = await asyncio.gather(*tasks)
    
            total_time = time.time() - start_time
    
            
    
            # Analyze results
    
            successful_executions = [r for r in results if r['success']]
    
            execution_times = [r['execution_time'] for r in successful_executions]
    
            
    
            assert len(successful_executions) == concurrent_tasks
    
            assert statistics.mean(execution_times) < 1.0  # Average under 1 second
    
            assert max(execution_times) < 5.0  # No execution over 5 seconds
    
            assert total_time < 10.0  # All executions under 10 seconds
    
            
    
            print(f"Concurrent execution stats:")
    
            print(f"  Total time: {total_time:.2f}s")
    
            print(f"  Average execution time: {statistics.mean(execution_times):.3f}s")
    
            print(f"  Max execution time: {max(execution_times):.3f}s")
    
            print(f"  Min execution time: {min(execution_times):.3f}s")
    
        
    
        async def test_database_query_performance(self, test_mcp_server, db_connection):
    
            """Test database query performance with large datasets."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create large dataset
    
            print("Creating test dataset...")
    
            for i in range(1000):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Test various query patterns
    
            query_tests = [
    
                {
    
                    'name': 'Simple COUNT',
    
                    'query': 'SELECT COUNT(*) FROM retail.customers',
    
                    'expected_max_time': 0.1
    
                },
    
                {
    
                    'name': 'Filtered SELECT',
    
                    'query': "SELECT * FROM retail.customers WHERE loyalty_tier = 'bronze' LIMIT 100",
    
                    'expected_max_time': 0.5
    
                },
    
                {
    
                    'name': 'Aggregation',
    
                    'query': 'SELECT loyalty_tier, COUNT(*) FROM retail.customers GROUP BY loyalty_tier',
    
                    'expected_max_time': 0.5
    
                }
    
            ]
    
            
    
            for test_case in query_tests:
    
                start_time = time.time()
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': test_case['query']
    
                    }
    
                )
    
                
    
                execution_time = time.time() - start_time
    
                
    
                assert result['success'] is True
    
                assert execution_time < test_case['expected_max_time']
    
                
    
                print(f"Query '{test_case['name']}': {execution_time:.3f}s")
    
        
    
        async def test_embedding_generation_performance(self, test_mcp_server):
    
            """Test embedding generation performance."""
    
            
    
            from mcp_server.embeddings.product_embedder import ProductEmbedder
    
            
    
            # Test with mock embedding manager (no actual API calls)
    
            embedder = ProductEmbedder(test_mcp_server.db_provider)
    
            embedder.embedding_manager = test_mcp_server.embedding_manager
    
            
    
            # Test batch embedding generation
    
            test_texts = [f"Test product {i} description" for i in range(100)]
    
            
    
            start_time = time.time()
    
            embeddings = await embedder.embedding_manager.generate_embeddings_batch(test_texts)
    
            batch_time = time.time() - start_time
    
            
    
            assert len(embeddings) == 100
    
            assert batch_time < 5.0  # Should complete in under 5 seconds with mocks
    
            
    
            print(f"Batch embedding generation (100 items): {batch_time:.3f}s")
    
            print(f"Average per embedding: {batch_time/100:.4f}s")
    
        
    
        @pytest.mark.slow
    
        async def test_memory_usage(self, test_mcp_server, db_connection):
    
            """Test memory usage under load."""
    
            
    
            import psutil
    
            import os
    
            
    
            process = psutil.Process(os.getpid())
    
            initial_memory = process.memory_info().rss / 1024 / 1024  # MB
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create substantial dataset
    
            for i in range(500):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Execute multiple operations
    
            for i in range(50):
    
                await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT * FROM retail.customers LIMIT 100'
    
                    }
    
                )
    
            
    
            final_memory = process.memory_info().rss / 1024 / 1024  # MB
    
            memory_increase = final_memory - initial_memory
    
            
    
            # Memory increase should be reasonable (under 100MB for this test)
    
            assert memory_increase < 100
    
            
    
            print(f"Memory usage:")
    
            print(f"  Initial: {initial_memory:.1f} MB")
    
            print(f"  Final: {final_memory:.1f} MB")
    
            print(f"  Increase: {memory_increase:.1f} MB")
    
    
    
    class TestScalability:
    
        """Test scalability characteristics."""
    
        
    
        async def test_response_time_scaling(self, test_mcp_server, db_connection):
    
            """Test how response time scales with data size."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Test with different data sizes
    
            data_sizes = [100, 500, 1000, 2000]
    
            response_times = []
    
            
    
            for size in data_sizes:
    
                # Clear existing data
    
                await db_connection.execute("DELETE FROM retail.customers WHERE store_id = $1", store_id)
    
                
    
                # Create dataset of specified size
    
                for i in range(size):
    
                    await TestDataHelper.create_test_customer(db_connection, store_id)
    
                
    
                # Measure query time
    
                start_time = time.time()
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) FROM retail.customers'
    
                    }
    
                )
    
                execution_time = time.time() - start_time
    
                
    
                assert result['success'] is True
    
                response_times.append(execution_time)
    
                
    
                print(f"Data size {size}: {execution_time:.3f}s")
    
            
    
            # Response time should scale reasonably (not exponentially)
    
            # Simple count queries should remain fast even with larger datasets
    
            for time_val in response_times:
    
                assert time_val < 1.0  # All queries under 1 second
    
    

    ๐Ÿ” Debugging Tools

    Advanced Debugging Framework

    
    # mcp_server/debugging/debug_tools.py
    
    """
    
    Advanced debugging tools for MCP server troubleshooting.
    
    """
    
    import asyncio
    
    import json
    
    import time
    
    import traceback
    
    from typing import Dict, Any, List, Optional
    
    from datetime import datetime
    
    import logging
    
    from contextlib import asynccontextmanager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class MCPDebugger:
    
        """Comprehensive debugging utilities for MCP server."""
    
        
    
        def __init__(self, server_instance):
    
            self.server = server_instance
    
            self.debug_logs = []
    
            self.performance_metrics = {}
    
            self.active_traces = {}
    
            
    
        @asynccontextmanager
    
        async def trace_execution(self, operation_name: str, context: Dict[str, Any] = None):
    
            """Trace operation execution with detailed logging."""
    
            
    
            trace_id = f"{operation_name}_{int(time.time() * 1000)}"
    
            start_time = time.time()
    
            
    
            trace_info = {
    
                'trace_id': trace_id,
    
                'operation': operation_name,
    
                'start_time': start_time,
    
                'context': context or {},
    
                'status': 'running'
    
            }
    
            
    
            self.active_traces[trace_id] = trace_info
    
            
    
            logger.debug(f"Starting trace: {trace_id} - {operation_name}")
    
            
    
            try:
    
                yield trace_info
    
                
    
                # Success
    
                execution_time = time.time() - start_time
    
                trace_info.update({
    
                    'status': 'completed',
    
                    'execution_time': execution_time
    
                })
    
                
    
                logger.debug(f"Completed trace: {trace_id} in {execution_time:.3f}s")
    
                
    
            except Exception as e:
    
                # Error
    
                execution_time = time.time() - start_time
    
                trace_info.update({
    
                    'status': 'error',
    
                    'execution_time': execution_time,
    
                    'error': str(e),
    
                    'traceback': traceback.format_exc()
    
                })
    
                
    
                logger.error(f"Error in trace: {trace_id} - {str(e)}")
    
                raise
    
                
    
            finally:
    
                # Store completed trace
    
                self.debug_logs.append(trace_info.copy())
    
                del self.active_traces[trace_id]
    
                
    
                # Limit debug log size
    
                if len(self.debug_logs) > 1000:
    
                    self.debug_logs = self.debug_logs[-500:]
    
        
    
        async def debug_tool_execution(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
    
            """Debug tool execution with comprehensive logging."""
    
            
    
            async with self.trace_execution(f"tool_execution_{tool_name}", {'parameters': parameters}) as trace:
    
                
    
                # Pre-execution validation
    
                validation_result = await self._validate_tool_parameters(tool_name, parameters)
    
                trace['validation'] = validation_result
    
                
    
                if not validation_result['valid']:
    
                    return {
    
                        'success': False,
    
                        'error': f"Parameter validation failed: {validation_result['errors']}",
    
                        'debug_info': trace
    
                    }
    
                
    
                # Database connection check
    
                db_health = await self._check_database_health()
    
                trace['database_health'] = db_health
    
                
    
                # Execute tool with monitoring
    
                try:
    
                    tool_instance = self.server.get_tool(tool_name)
    
                    if not tool_instance:
    
                        return {
    
                            'success': False,
    
                            'error': f"Tool '{tool_name}' not found",
    
                            'debug_info': trace
    
                        }
    
                    
    
                    # Monitor resource usage during execution
    
                    start_memory = await self._get_memory_usage()
    
                    
    
                    result = await tool_instance.call(**parameters)
    
                    
    
                    end_memory = await self._get_memory_usage()
    
                    
    
                    trace.update({
    
                        'memory_start_mb': start_memory,
    
                        'memory_end_mb': end_memory,
    
                        'memory_used_mb': end_memory - start_memory,
    
                        'result_success': result.success,
    
                        'result_row_count': result.row_count
    
                    })
    
                    
    
                    return {
    
                        'success': result.success,
    
                        'data': result.data,
    
                        'error': result.error,
    
                        'metadata': result.metadata,
    
                        'debug_info': trace
    
                    }
    
                    
    
                except Exception as e:
    
                    trace['exception'] = {
    
                        'type': type(e).__name__,
    
                        'message': str(e),
    
                        'traceback': traceback.format_exc()
    
                    }
    
                    
    
                    return {
    
                        'success': False,
    
                        'error': f"Tool execution failed: {str(e)}",
    
                        'debug_info': trace
    
                    }
    
        
    
        async def analyze_performance_bottlenecks(self) -> Dict[str, Any]:
    
            """Analyze performance bottlenecks from debug logs."""
    
            
    
            if not self.debug_logs:
    
                return {'message': 'No debug data available'}
    
            
    
            # Analyze execution times
    
            execution_times = {}
    
            error_rates = {}
    
            memory_usage = {}
    
            
    
            for log_entry in self.debug_logs[-100:]:  # Last 100 entries
    
                operation = log_entry['operation']
    
                
    
                # Execution time analysis
    
                if 'execution_time' in log_entry:
    
                    if operation not in execution_times:
    
                        execution_times[operation] = []
    
                    execution_times[operation].append(log_entry['execution_time'])
    
                
    
                # Error rate analysis
    
                if operation not in error_rates:
    
                    error_rates[operation] = {'total': 0, 'errors': 0}
    
                
    
                error_rates[operation]['total'] += 1
    
                if log_entry['status'] == 'error':
    
                    error_rates[operation]['errors'] += 1
    
                
    
                # Memory usage analysis
    
                if 'memory_used_mb' in log_entry:
    
                    if operation not in memory_usage:
    
                        memory_usage[operation] = []
    
                    memory_usage[operation].append(log_entry['memory_used_mb'])
    
            
    
            # Calculate statistics
    
            performance_stats = {}
    
            
    
            for operation, times in execution_times.items():
    
                if times:
    
                    performance_stats[operation] = {
    
                        'avg_execution_time': sum(times) / len(times),
    
                        'max_execution_time': max(times),
    
                        'min_execution_time': min(times),
    
                        'execution_count': len(times),
    
                        'error_rate': (error_rates[operation]['errors'] / 
    
                                     error_rates[operation]['total'] * 100),
    
                        'avg_memory_usage': (sum(memory_usage.get(operation, [0])) / 
    
                                           len(memory_usage.get(operation, [1])))
    
                    }
    
            
    
            # Identify bottlenecks
    
            bottlenecks = []
    
            
    
            for operation, stats in performance_stats.items():
    
                if stats['avg_execution_time'] > 2.0:  # Slow operations
    
                    bottlenecks.append({
    
                        'type': 'slow_execution',
    
                        'operation': operation,
    
                        'avg_time': stats['avg_execution_time']
    
                    })
    
                
    
                if stats['error_rate'] > 5.0:  # High error rate
    
                    bottlenecks.append({
    
                        'type': 'high_error_rate',
    
                        'operation': operation,
    
                        'error_rate': stats['error_rate']
    
                    })
    
                
    
                if stats['avg_memory_usage'] > 100:  # High memory usage
    
                    bottlenecks.append({
    
                        'type': 'high_memory_usage',
    
                        'operation': operation,
    
                        'memory_mb': stats['avg_memory_usage']
    
                    })
    
            
    
            return {
    
                'performance_stats': performance_stats,
    
                'bottlenecks': bottlenecks,
    
                'total_operations': len(self.debug_logs),
    
                'analysis_timestamp': datetime.now().isoformat()
    
            }
    
        
    
        async def _validate_tool_parameters(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
    
            """Validate tool parameters against schema."""
    
            
    
            try:
    
                tool_instance = self.server.get_tool(tool_name)
    
                if not tool_instance:
    
                    return {
    
                        'valid': False,
    
                        'errors': [f"Tool '{tool_name}' not found"]
    
                    }
    
                
    
                schema = tool_instance.get_input_schema()
    
                
    
                # Basic validation (in production, use jsonschema library)
    
                errors = []
    
                required_props = schema.get('required', [])
    
                
    
                for prop in required_props:
    
                    if prop not in parameters:
    
                        errors.append(f"Missing required parameter: {prop}")
    
                
    
                return {
    
                    'valid': len(errors) == 0,
    
                    'errors': errors,
    
                    'schema': schema
    
                }
    
                
    
            except Exception as e:
    
                return {
    
                    'valid': False,
    
                    'errors': [f"Validation error: {str(e)}"]
    
                }
    
        
    
        async def _check_database_health(self) -> Dict[str, Any]:
    
            """Check database health and connectivity."""
    
            
    
            try:
    
                health_status = await self.server.db_provider.health_check()
    
                return {
    
                    'healthy': health_status.get('status') == 'healthy',
    
                    'details': health_status
    
                }
    
            except Exception as e:
    
                return {
    
                    'healthy': False,
    
                    'error': str(e)
    
                }
    
        
    
        async def _get_memory_usage(self) -> float:
    
            """Get current memory usage in MB."""
    
            
    
            try:
    
                import psutil
    
                import os
    
                process = psutil.Process(os.getpid())
    
                return process.memory_info().rss / 1024 / 1024
    
            except:
    
                return 0.0
    
        
    
        def get_debug_summary(self) -> Dict[str, Any]:
    
            """Get summary of debug information."""
    
            
    
            recent_logs = self.debug_logs[-50:] if self.debug_logs else []
    
            
    
            return {
    
                'total_operations': len(self.debug_logs),
    
                'active_traces': len(self.active_traces),
    
                'recent_operations': [
    
                    {
    
                        'operation': log['operation'],
    
                        'status': log['status'],
    
                        'execution_time': log.get('execution_time', 0),
    
                        'timestamp': log.get('start_time', 0)
    
                    }
    
                    for log in recent_logs
    
                ],
    
                'current_traces': list(self.active_traces.keys())
    
            }
    
    
    
    # Debug tool for direct use
    
    class DebugTool:
    
        """Interactive debugging tool for MCP server."""
    
        
    
        def __init__(self, server_instance):
    
            self.debugger = MCPDebugger(server_instance)
    
        
    
        async def debug_query(self, query: str, store_id: str) -> Dict[str, Any]:
    
            """Debug a specific database query."""
    
            
    
            return await self.debugger.debug_tool_execution(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'query': query
    
                }
    
            )
    
        
    
        async def debug_search(self, query: str, store_id: str) -> Dict[str, Any]:
    
            """Debug a semantic search query."""
    
            
    
            return await self.debugger.debug_tool_execution(
    
                'semantic_search_products',
    
                {
    
                    'query': query,
    
                    'store_id': store_id,
    
                    'limit': 10
    
                }
    
            )
    
        
    
        async def get_performance_report(self) -> Dict[str, Any]:
    
            """Get comprehensive performance report."""
    
            
    
            return await self.debugger.analyze_performance_bottlenecks()
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Comprehensive Testing Framework: Unit, integration, and performance tests for all components

    โœ… Advanced Debugging Tools: Sophisticated debugging utilities with execution tracing

    โœ… Performance Validation: Load testing and scalability analysis capabilities

    โœ… Security Testing: SQL injection prevention and RLS validation

    โœ… Monitoring Integration: Performance metrics and bottleneck analysis

    โœ… CI/CD Ready: Automated testing workflows for continuous integration

    ๐Ÿš€ What's Next

    Continue with Lab 09: VS Code Integration to:

  • Configure VS Code for MCP server development
  • Set up debugging environments in VS Code
  • Integrate MCP server with VS Code Chat
  • Test the complete VS Code workflow
  • ๐Ÿ“š Additional Resources

    Testing Frameworks

  • pytest Documentation - Python testing framework
  • AsyncPG Testing - Async PostgreSQL testing
  • FastAPI Testing - API testing patterns
  • Performance Testing

  • Load Testing Best Practices - Async performance testing
  • Database Performance Testing - PostgreSQL optimization
  • Memory Profiling - Python memory analysis
  • Debugging Tools

  • Python Debugging - Python debugger
  • Async Debugging - Asyncio debugging
  • SQL Debugging - PostgreSQL logging
  • ---

    Previous: Lab 07: Semantic Search Integration

    Next: Lab 09: VS Code Integration

    Testing strategies, debugging tools, and validation approaches Test

    Testing and Debugging

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on testing and debugging MCP servers in production environments.

    You'll learn to implement robust testing strategies, debug complex issues, and ensure your MCP server performs reliably under various conditions.

    Overview

    Testing MCP servers requires a multi-layered approach covering unit tests, integration tests, performance validation, and real-world scenario testing. This lab covers the complete testing lifecycle from development to production monitoring.

    Our testing strategy emphasizes reliability, security, and performance, ensuring your MCP server can handle production workloads while maintaining data integrity and user experience quality.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Implement comprehensive unit and integration test suites
  • Design effective testing strategies for MCP tools and database operations
  • Debug complex issues using advanced debugging techniques
  • Validate performance under load with realistic testing scenarios
  • Monitor production systems with effective alerting and observability
  • Automate testing workflows for continuous integration
  • ๐Ÿงช Testing Architecture

    Testing Strategy Overview

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                Unit Tests                       โ”‚
    
    โ”‚   โ€ข Tool execution logic                       โ”‚
    
    โ”‚   โ€ข Database query validation                  โ”‚
    
    โ”‚   โ€ข Authentication/authorization               โ”‚
    
    โ”‚   โ€ข Embedding generation                       โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚             Integration Tests                   โ”‚
    
    โ”‚   โ€ข End-to-end MCP workflows                  โ”‚
    
    โ”‚   โ€ข Database schema validation                 โ”‚
    
    โ”‚   โ€ข API endpoint testing                       โ”‚
    
    โ”‚   โ€ข Multi-tool interactions                    โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚            Performance Tests                    โ”‚
    
    โ”‚   โ€ข Load testing under realistic conditions    โ”‚
    
    โ”‚   โ€ข Database performance validation            โ”‚
    
    โ”‚   โ€ข Memory and resource usage                  โ”‚
    
    โ”‚   โ€ข Embedding generation performance           โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚              E2E Tests                         โ”‚
    
    โ”‚   โ€ข Complete user workflows                    โ”‚
    
    โ”‚   โ€ข VS Code integration testing               โ”‚
    
    โ”‚   โ€ข Real-world scenario validation            โ”‚
    
    โ”‚   โ€ข Cross-browser compatibility               โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Test Environment Setup

    
    # tests/conftest.py
    
    """
    
    Pytest configuration and shared fixtures for MCP server testing.
    
    """
    
    import pytest
    
    import asyncio
    
    import asyncpg
    
    import os
    
    from typing import AsyncGenerator, Dict, Any
    
    from unittest.mock import AsyncMock, Mock
    
    import tempfile
    
    import shutil
    
    from datetime import datetime
    
    
    
    # Test configuration
    
    TEST_DATABASE_URL = "postgresql://test_user:test_pass@localhost:5432/test_retail_db"
    
    TEST_STORE_IDS = ['test_seattle', 'test_redmond', 'test_bellevue']
    
    
    
    @pytest.fixture(scope="session")
    
    def event_loop():
    
        """Create an instance of the default event loop for the test session."""
    
        loop = asyncio.get_event_loop_policy().new_event_loop()
    
        yield loop
    
        loop.close()
    
    
    
    @pytest.fixture(scope="session")
    
    async def test_database():
    
        """Set up test database with schema and sample data."""
    
        
    
        # Create test database connection
    
        sys_conn = await asyncpg.connect(
    
            "postgresql://postgres:password@localhost:5432/postgres"
    
        )
    
        
    
        try:
    
            # Create test database
    
            await sys_conn.execute("DROP DATABASE IF EXISTS test_retail_db")
    
            await sys_conn.execute("CREATE DATABASE test_retail_db")
    
        finally:
    
            await sys_conn.close()
    
        
    
        # Connect to test database and set up schema
    
        test_conn = await asyncpg.connect(TEST_DATABASE_URL)
    
        
    
        try:
    
            # Load schema
    
            schema_sql = await load_sql_file("../scripts/create_schema.sql")
    
            await test_conn.execute(schema_sql)
    
            
    
            # Load sample data
    
            sample_data_sql = await load_sql_file("../scripts/sample_data.sql")
    
            await test_conn.execute(sample_data_sql)
    
            
    
            yield test_conn
    
            
    
        finally:
    
            await test_conn.close()
    
            
    
            # Cleanup test database
    
            sys_conn = await asyncpg.connect(
    
                "postgresql://postgres:password@localhost:5432/postgres"
    
            )
    
            try:
    
                await sys_conn.execute("DROP DATABASE IF EXISTS test_retail_db")
    
            finally:
    
                await sys_conn.close()
    
    
    
    @pytest.fixture
    
    async def db_connection(test_database):
    
        """Provide a clean database connection for each test."""
    
        
    
        conn = await asyncpg.connect(TEST_DATABASE_URL)
    
        
    
        # Start transaction for test isolation
    
        tx = conn.transaction()
    
        await tx.start()
    
        
    
        try:
    
            yield conn
    
        finally:
    
            # Rollback transaction to maintain test isolation
    
            await tx.rollback()
    
            await conn.close()
    
    
    
    @pytest.fixture
    
    async def mock_embedding_manager():
    
        """Mock embedding manager for testing without Azure OpenAI calls."""
    
        
    
        mock_manager = AsyncMock()
    
        
    
        # Mock embedding generation
    
        mock_manager.generate_embedding.return_value = [0.1] * 1536  # Mock embedding
    
        mock_manager.generate_embeddings_batch.return_value = [[0.1] * 1536] * 10
    
        
    
        # Mock initialization
    
        mock_manager.initialize.return_value = None
    
        mock_manager.cleanup.return_value = None
    
        
    
        return mock_manager
    
    
    
    @pytest.fixture
    
    async def test_mcp_server(db_connection, mock_embedding_manager):
    
        """Set up test MCP server instance."""
    
        
    
        from mcp_server.server import MCPServer
    
        from mcp_server.database import DatabaseProvider
    
        from mcp_server.config import Config
    
        
    
        # Create test configuration
    
        config = Config()
    
        config.database.connection_string = TEST_DATABASE_URL
    
        config.server.enable_debug = True
    
        
    
        # Create database provider
    
        db_provider = DatabaseProvider(config.database.connection_string)
    
        await db_provider.initialize()
    
        
    
        # Create MCP server
    
        server = MCPServer(config, db_provider)
    
        server.embedding_manager = mock_embedding_manager
    
        
    
        await server.initialize()
    
        
    
        yield server
    
        
    
        await server.cleanup()
    
    
    
    @pytest.fixture
    
    def sample_products():
    
        """Sample product data for testing."""
    
        
    
        return [
    
            {
    
                'product_id': 'test-product-1',
    
                'product_name': 'Test Running Shoes',
    
                'brand': 'TestBrand',
    
                'price': 99.99,
    
                'product_description': 'Comfortable running shoes for daily training',
    
                'category_name': 'Electronics',
    
                'current_stock': 50
    
            },
    
            {
    
                'product_id': 'test-product-2',
    
                'product_name': 'Test Laptop',
    
                'brand': 'TestTech',
    
                'price': 1299.99,
    
                'product_description': 'High-performance laptop for professional use',
    
                'category_name': 'Electronics',
    
                'current_stock': 25
    
            }
    
        ]
    
    
    
    async def load_sql_file(file_path: str) -> str:
    
        """Load SQL file content."""
    
        
    
        with open(file_path, 'r') as file:
    
            return file.read()
    
    
    
    # Test data helpers
    
    class TestDataHelper:
    
        """Helper class for managing test data."""
    
        
    
        @staticmethod
    
        async def create_test_store(conn: asyncpg.Connection, store_id: str) -> Dict[str, Any]:
    
            """Create a test store."""
    
            
    
            store_data = {
    
                'store_id': store_id,
    
                'store_name': f'Test Store {store_id}',
    
                'store_location': 'Test Location',
    
                'store_type': 'test',
    
                'region': 'test'
    
            }
    
            
    
            await conn.execute("""
    
                INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region)
    
                VALUES ($1, $2, $3, $4, $5)
    
                ON CONFLICT (store_id) DO NOTHING
    
            """, *store_data.values())
    
            
    
            return store_data
    
        
    
        @staticmethod
    
        async def create_test_customer(conn: asyncpg.Connection, store_id: str) -> str:
    
            """Create a test customer."""
    
            
    
            customer_id = await conn.fetchval("""
    
                INSERT INTO retail.customers (
    
                    store_id, first_name, last_name, email, loyalty_tier
    
                ) VALUES ($1, $2, $3, $4, $5)
    
                RETURNING customer_id
    
            """, store_id, 'Test', 'Customer', 'test@example.com', 'bronze')
    
            
    
            return customer_id
    
        
    
        @staticmethod
    
        async def create_test_product(
    
            conn: asyncpg.Connection, 
    
            store_id: str, 
    
            product_data: Dict[str, Any]
    
        ) -> str:
    
            """Create a test product."""
    
            
    
            product_id = await conn.fetchval("""
    
                INSERT INTO retail.products (
    
                    store_id, sku, product_name, brand, price, product_description, current_stock
    
                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    
                RETURNING product_id
    
            """, 
    
                store_id, 
    
                f"TEST-{product_data['product_name'][:10]}",
    
                product_data['product_name'],
    
                product_data['brand'],
    
                product_data['price'],
    
                product_data['product_description'],
    
                product_data['current_stock']
    
            )
    
            
    
            return product_id
    
    

    ๐Ÿ”ง Unit Testing

    Tool Testing Framework

    
    # tests/test_tools.py
    
    """
    
    Comprehensive unit tests for MCP tools.
    
    """
    
    import pytest
    
    import asyncio
    
    from unittest.mock import AsyncMock, patch
    
    from datetime import datetime, timedelta
    
    
    
    from mcp_server.tools.sales_analysis import SalesAnalysisTool
    
    from mcp_server.tools.semantic_search import SemanticProductSearchTool
    
    from mcp_server.tools.schema_introspection import SchemaIntrospectionTool
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestSalesAnalysisTool:
    
        """Test sales analysis tool functionality."""
    
        
    
        @pytest.fixture
    
        async def sales_tool(self, test_mcp_server):
    
            """Create sales analysis tool for testing."""
    
            return SalesAnalysisTool(test_mcp_server.db_provider)
    
        
    
        async def test_daily_sales_query(self, sales_tool, db_connection):
    
            """Test daily sales analysis query."""
    
            
    
            # Set up test data
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            customer_id = await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Create test transaction
    
            await db_connection.execute("""
    
                INSERT INTO retail.sales_transactions (
    
                    store_id, customer_id, transaction_date, total_amount, transaction_type
    
                ) VALUES ($1, $2, $3, $4, $5)
    
            """, store_id, customer_id, datetime.now(), 150.00, 'sale')
    
            
    
            # Execute tool
    
            result = await sales_tool.execute(
    
                query_type='daily_sales',
    
                store_id=store_id,
    
                start_date=(datetime.now() - timedelta(days=7)).date(),
    
                end_date=datetime.now().date()
    
            )
    
            
    
            # Validate results
    
            assert result.success is True
    
            assert len(result.data) > 0
    
            assert 'total_revenue' in result.data[0]
    
            assert result.metadata['query_type'] == 'daily_sales'
    
        
    
        async def test_custom_query_validation(self, sales_tool, db_connection):
    
            """Test custom query validation."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Test valid query
    
            valid_query = "SELECT COUNT(*) as customer_count FROM retail.customers"
    
            result = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store_id,
    
                query=valid_query
    
            )
    
            
    
            assert result.success is True
    
            
    
            # Test invalid query (should be blocked)
    
            invalid_query = "DROP TABLE retail.customers"
    
            result = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store_id,
    
                query=invalid_query
    
            )
    
            
    
            assert result.success is False
    
            assert 'validation failed' in result.error.lower()
    
        
    
        async def test_store_isolation(self, sales_tool, db_connection):
    
            """Test that store isolation works correctly."""
    
            
    
            # Create two different stores
    
            store1 = 'test_store1'
    
            store2 = 'test_store2'
    
            
    
            await TestDataHelper.create_test_store(db_connection, store1)
    
            await TestDataHelper.create_test_store(db_connection, store2)
    
            
    
            # Create customers in different stores
    
            customer1 = await TestDataHelper.create_test_customer(db_connection, store1)
    
            customer2 = await TestDataHelper.create_test_customer(db_connection, store2)
    
            
    
            # Query from store1 should only see store1 data
    
            result1 = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store1,
    
                query="SELECT COUNT(*) as count FROM retail.customers"
    
            )
    
            
    
            # Query from store2 should only see store2 data
    
            result2 = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store2,
    
                query="SELECT COUNT(*) as count FROM retail.customers"
    
            )
    
            
    
            assert result1.success is True
    
            assert result2.success is True
    
            assert result1.data[0]['count'] == 1
    
            assert result2.data[0]['count'] == 1
    
    
    
    class TestSemanticSearchTool:
    
        """Test semantic search tool functionality."""
    
        
    
        @pytest.fixture
    
        async def search_tool(self, test_mcp_server):
    
            """Create semantic search tool for testing."""
    
            return SemanticProductSearchTool(test_mcp_server.db_provider)
    
        
    
        async def test_semantic_search_execution(self, search_tool, db_connection, sample_products):
    
            """Test semantic search with mock embeddings."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test products
    
            for product_data in sample_products:
    
                product_id = await TestDataHelper.create_test_product(
    
                    db_connection, store_id, product_data
    
                )
    
                
    
                # Create mock embedding
    
                await db_connection.execute("""
    
                    INSERT INTO retail.product_embeddings (
    
                        product_id, store_id, embedding_text, embedding
    
                    ) VALUES ($1, $2, $3, $4)
    
                """, 
    
                    product_id, store_id, 
    
                    f"{product_data['product_name']} {product_data['brand']}", 
    
                    '[0.1,0.2,0.3]'  # Mock embedding
    
                )
    
            
    
            # Execute search
    
            result = await search_tool.execute(
    
                query='running shoes',
    
                store_id=store_id,
    
                limit=10,
    
                similarity_threshold=0.0
    
            )
    
            
    
            # Validate results
    
            assert result.success is True
    
            assert len(result.data) > 0
    
            assert 'similarity_score' in result.data[0]
    
            assert result.metadata['search_type'] == 'semantic'
    
        
    
        async def test_search_parameter_validation(self, search_tool):
    
            """Test search parameter validation."""
    
            
    
            # Test missing query
    
            result = await search_tool.execute(store_id='test_store')
    
            assert result.success is False
    
            assert 'query is required' in result.error.lower()
    
            
    
            # Test missing store_id
    
            result = await search_tool.execute(query='test query')
    
            assert result.success is False
    
            assert 'store_id is required' in result.error.lower()
    
    
    
    class TestSchemaIntrospectionTool:
    
        """Test schema introspection tool."""
    
        
    
        @pytest.fixture
    
        async def schema_tool(self, test_mcp_server):
    
            """Create schema introspection tool for testing."""
    
            return SchemaIntrospectionTool(test_mcp_server.db_provider)
    
        
    
        async def test_single_table_schema(self, schema_tool, db_connection):
    
            """Test getting schema for a single table."""
    
            
    
            result = await schema_tool.execute(
    
                table_name='customers',
    
                include_constraints=True,
    
                include_indexes=True
    
            )
    
            
    
            assert result.success is True
    
            assert result.data['table_name'] == 'customers'
    
            assert len(result.data['columns']) > 0
    
            assert 'customer_id' in [col['column_name'] for col in result.data['columns']]
    
        
    
        async def test_all_tables_schema(self, schema_tool, db_connection):
    
            """Test getting schema for all tables."""
    
            
    
            result = await schema_tool.execute()
    
            
    
            assert result.success is True
    
            assert result.data['schema_name'] == 'retail'
    
            assert len(result.data['tables']) > 0
    
            
    
            table_names = [table['table_name'] for table in result.data['tables']]
    
            expected_tables = ['customers', 'products', 'sales_transactions']
    
            
    
            for expected_table in expected_tables:
    
                assert expected_table in table_names
    
    

    Database Testing

    
    # tests/test_database.py
    
    """
    
    Database layer testing including RLS and security.
    
    """
    
    import pytest
    
    import asyncpg
    
    from datetime import datetime
    
    
    
    from mcp_server.database import DatabaseProvider
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestRowLevelSecurity:
    
        """Test Row Level Security implementation."""
    
        
    
        async def test_store_context_setting(self, db_connection):
    
            """Test that store context is set correctly."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Set store context
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store_id)
    
            
    
            # Verify context is set
    
            current_store = await db_connection.fetchval(
    
                "SELECT current_setting('app.current_store_id', true)"
    
            )
    
            
    
            assert current_store == store_id
    
        
    
        async def test_customer_isolation(self, db_connection):
    
            """Test that customers are properly isolated by store."""
    
            
    
            # Create two stores
    
            store1 = 'test_store1'
    
            store2 = 'test_store2'
    
            
    
            await TestDataHelper.create_test_store(db_connection, store1)
    
            await TestDataHelper.create_test_store(db_connection, store2)
    
            
    
            # Create customers in different stores
    
            await TestDataHelper.create_test_customer(db_connection, store1)
    
            await TestDataHelper.create_test_customer(db_connection, store2)
    
            
    
            # Set context to store1 and count customers
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store1)
    
            store1_count = await db_connection.fetchval("SELECT COUNT(*) FROM retail.customers")
    
            
    
            # Set context to store2 and count customers
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store2)
    
            store2_count = await db_connection.fetchval("SELECT COUNT(*) FROM retail.customers")
    
            
    
            # Each store should only see its own customers
    
            assert store1_count == 1
    
            assert store2_count == 1
    
        
    
        async def test_invalid_store_context(self, db_connection):
    
            """Test that invalid store context raises error."""
    
            
    
            with pytest.raises(Exception) as exc_info:
    
                await db_connection.execute("SELECT retail.set_store_context($1)", 'invalid_store')
    
            
    
            assert "Store not found" in str(exc_info.value)
    
        
    
        async def test_cross_store_data_insertion_blocked(self, db_connection):
    
            """Test that users cannot insert data for other stores."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Set store context
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store_id)
    
            
    
            # Try to insert customer for different store (should fail)
    
            with pytest.raises(Exception):
    
                await db_connection.execute("""
    
                    INSERT INTO retail.customers (store_id, first_name, last_name, email)
    
                    VALUES ($1, $2, $3, $4)
    
                """, 'different_store', 'Test', 'Customer', 'test@example.com')
    
    
    
    class TestDatabaseProvider:
    
        """Test database provider functionality."""
    
        
    
        @pytest.fixture
    
        async def db_provider(self):
    
            """Create database provider for testing."""
    
            
    
            provider = DatabaseProvider(TEST_DATABASE_URL)
    
            await provider.initialize()
    
            yield provider
    
            await provider.cleanup()
    
        
    
        async def test_connection_pooling(self, db_provider):
    
            """Test connection pool functionality."""
    
            
    
            # Get multiple connections
    
            conn1 = await db_provider.get_connection()
    
            conn2 = await db_provider.get_connection()
    
            
    
            assert conn1 is not None
    
            assert conn2 is not None
    
            assert conn1 != conn2  # Should be different connection objects
    
            
    
            # Release connections
    
            await db_provider.release_connection(conn1)
    
            await db_provider.release_connection(conn2)
    
        
    
        async def test_health_check(self, db_provider):
    
            """Test database health check."""
    
            
    
            health_status = await db_provider.health_check()
    
            
    
            assert health_status['status'] == 'healthy'
    
            assert 'connection_pool_size' in health_status
    
            assert 'database_version' in health_status
    
        
    
        async def test_connection_recovery(self, db_provider):
    
            """Test connection recovery after database issues."""
    
            
    
            # This would test connection recovery scenarios
    
            # In a real test, you might temporarily break the connection
    
            # and verify that the pool recovers
    
            
    
            # For now, just verify health check works
    
            health_status = await db_provider.health_check()
    
            assert health_status['status'] == 'healthy'
    
    

    ๐Ÿš€ Integration Testing

    End-to-End Workflow Testing

    
    # tests/test_integration.py
    
    """
    
    Integration tests for complete MCP workflows.
    
    """
    
    import pytest
    
    import json
    
    from datetime import datetime, timedelta
    
    
    
    from mcp_server.server import MCPServer
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestMCPWorkflows:
    
        """Test complete MCP server workflows."""
    
        
    
        async def test_product_search_workflow(self, test_mcp_server, db_connection, sample_products):
    
            """Test complete product search workflow."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test products with embeddings
    
            for product_data in sample_products:
    
                product_id = await TestDataHelper.create_test_product(
    
                    db_connection, store_id, product_data
    
                )
    
                
    
                # Create embedding for product
    
                await db_connection.execute("""
    
                    INSERT INTO retail.product_embeddings (
    
                        product_id, store_id, embedding_text, embedding
    
                    ) VALUES ($1, $2, $3, $4)
    
                """, 
    
                    product_id, store_id, 
    
                    f"{product_data['product_name']} {product_data['brand']}", 
    
                    '[' + ','.join(['0.1'] * 1536) + ']'  # Mock embedding
    
                )
    
            
    
            # Test semantic search
    
            search_result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {
    
                    'query': 'running shoes',
    
                    'store_id': store_id,
    
                    'limit': 10
    
                }
    
            )
    
            
    
            assert search_result['success'] is True
    
            assert len(search_result['data']) > 0
    
            
    
            # Test schema introspection
    
            schema_result = await test_mcp_server.execute_tool(
    
                'get_table_schema',
    
                {'table_name': 'products'}
    
            )
    
            
    
            assert schema_result['success'] is True
    
            assert schema_result['data']['table_name'] == 'products'
    
        
    
        async def test_sales_analysis_workflow(self, test_mcp_server, db_connection):
    
            """Test sales analysis workflow."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test customer and product
    
            customer_id = await TestDataHelper.create_test_customer(db_connection, store_id)
    
            product_id = await TestDataHelper.create_test_product(
    
                db_connection, store_id, {
    
                    'product_name': 'Test Product',
    
                    'brand': 'TestBrand',
    
                    'price': 99.99,
    
                    'product_description': 'Test product description',
    
                    'current_stock': 50
    
                }
    
            )
    
            
    
            # Create test transaction
    
            transaction_id = await db_connection.fetchval("""
    
                INSERT INTO retail.sales_transactions (
    
                    store_id, customer_id, transaction_date, total_amount, 
    
                    subtotal, tax_amount, transaction_type
    
                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    
                RETURNING transaction_id
    
            """, store_id, customer_id, datetime.now(), 107.99, 99.99, 8.00, 'sale')
    
            
    
            # Create transaction item
    
            await db_connection.execute("""
    
                INSERT INTO retail.sales_transaction_items (
    
                    transaction_id, product_id, quantity, unit_price, total_price
    
                ) VALUES ($1, $2, $3, $4, $5)
    
            """, transaction_id, product_id, 1, 99.99, 99.99)
    
            
    
            # Test daily sales analysis
    
            sales_result = await test_mcp_server.execute_tool(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'daily_sales',
    
                    'store_id': store_id,
    
                    'start_date': (datetime.now() - timedelta(days=1)).date().isoformat(),
    
                    'end_date': datetime.now().date().isoformat()
    
                }
    
            )
    
            
    
            assert sales_result['success'] is True
    
            assert len(sales_result['data']) > 0
    
            assert sales_result['data'][0]['total_revenue'] == 107.99
    
        
    
        async def test_multi_store_workflow(self, test_mcp_server, db_connection):
    
            """Test workflows across multiple stores."""
    
            
    
            # Create multiple stores
    
            stores = ['test_seattle', 'test_redmond', 'test_bellevue']
    
            
    
            for store_id in stores:
    
                await TestDataHelper.create_test_store(db_connection, store_id)
    
                
    
                # Create customer in each store
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Test that each store sees only its own data
    
            for store_id in stores:
    
                schema_result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) as customer_count FROM retail.customers'
    
                    }
    
                )
    
                
    
                assert schema_result['success'] is True
    
                assert schema_result['data'][0]['customer_count'] == 1
    
    
    
    class TestErrorHandling:
    
        """Test error handling and edge cases."""
    
        
    
        async def test_database_connection_failure(self, test_mcp_server):
    
            """Test behavior when database connection fails."""
    
            
    
            # Simulate database failure by using invalid connection
    
            with patch.object(test_mcp_server.db_provider, 'get_connection') as mock_conn:
    
                mock_conn.side_effect = Exception("Database connection failed")
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'get_table_schema',
    
                    {'table_name': 'customers'}
    
                )
    
                
    
                assert result['success'] is False
    
                assert 'connection failed' in result['error'].lower()
    
        
    
        async def test_invalid_tool_parameters(self, test_mcp_server):
    
            """Test handling of invalid tool parameters."""
    
            
    
            # Missing required parameter
    
            result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {'query': 'test query'}  # Missing store_id
    
            )
    
            
    
            assert result['success'] is False
    
            assert 'store_id is required' in result['error'].lower()
    
            
    
            # Invalid parameter type
    
            result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {
    
                    'query': 'test query',
    
                    'store_id': 'test_store',
    
                    'limit': 'invalid'  # Should be integer
    
                }
    
            )
    
            
    
            assert result['success'] is False
    
        
    
        async def test_sql_injection_prevention(self, test_mcp_server, db_connection):
    
            """Test that SQL injection attempts are blocked."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Attempt SQL injection
    
            malicious_query = "SELECT * FROM retail.customers; DROP TABLE retail.customers; --"
    
            
    
            result = await test_mcp_server.execute_tool(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'query': malicious_query
    
                }
    
            )
    
            
    
            assert result['success'] is False
    
            assert 'validation failed' in result['error'].lower()
    
    

    ๐Ÿ“Š Performance Testing

    Load Testing Framework

    
    # tests/test_performance.py
    
    """
    
    Performance and load testing for MCP server.
    
    """
    
    import pytest
    
    import asyncio
    
    import time
    
    import statistics
    
    from concurrent.futures import ThreadPoolExecutor
    
    from typing import List, Dict, Any
    
    
    
    class TestPerformance:
    
        """Performance testing for MCP server operations."""
    
        
    
        async def test_concurrent_tool_execution(self, test_mcp_server, db_connection):
    
            """Test performance under concurrent tool execution."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test data
    
            for i in range(100):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Define test scenarios
    
            async def execute_tool_scenario():
    
                """Execute a tool and measure performance."""
    
                start_time = time.time()
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) as count FROM retail.customers'
    
                    }
    
                )
    
                
    
                execution_time = time.time() - start_time
    
                return {
    
                    'success': result['success'],
    
                    'execution_time': execution_time
    
                }
    
            
    
            # Run concurrent executions
    
            concurrent_tasks = 20
    
            tasks = [execute_tool_scenario() for _ in range(concurrent_tasks)]
    
            
    
            start_time = time.time()
    
            results = await asyncio.gather(*tasks)
    
            total_time = time.time() - start_time
    
            
    
            # Analyze results
    
            successful_executions = [r for r in results if r['success']]
    
            execution_times = [r['execution_time'] for r in successful_executions]
    
            
    
            assert len(successful_executions) == concurrent_tasks
    
            assert statistics.mean(execution_times) < 1.0  # Average under 1 second
    
            assert max(execution_times) < 5.0  # No execution over 5 seconds
    
            assert total_time < 10.0  # All executions under 10 seconds
    
            
    
            print(f"Concurrent execution stats:")
    
            print(f"  Total time: {total_time:.2f}s")
    
            print(f"  Average execution time: {statistics.mean(execution_times):.3f}s")
    
            print(f"  Max execution time: {max(execution_times):.3f}s")
    
            print(f"  Min execution time: {min(execution_times):.3f}s")
    
        
    
        async def test_database_query_performance(self, test_mcp_server, db_connection):
    
            """Test database query performance with large datasets."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create large dataset
    
            print("Creating test dataset...")
    
            for i in range(1000):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Test various query patterns
    
            query_tests = [
    
                {
    
                    'name': 'Simple COUNT',
    
                    'query': 'SELECT COUNT(*) FROM retail.customers',
    
                    'expected_max_time': 0.1
    
                },
    
                {
    
                    'name': 'Filtered SELECT',
    
                    'query': "SELECT * FROM retail.customers WHERE loyalty_tier = 'bronze' LIMIT 100",
    
                    'expected_max_time': 0.5
    
                },
    
                {
    
                    'name': 'Aggregation',
    
                    'query': 'SELECT loyalty_tier, COUNT(*) FROM retail.customers GROUP BY loyalty_tier',
    
                    'expected_max_time': 0.5
    
                }
    
            ]
    
            
    
            for test_case in query_tests:
    
                start_time = time.time()
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': test_case['query']
    
                    }
    
                )
    
                
    
                execution_time = time.time() - start_time
    
                
    
                assert result['success'] is True
    
                assert execution_time < test_case['expected_max_time']
    
                
    
                print(f"Query '{test_case['name']}': {execution_time:.3f}s")
    
        
    
        async def test_embedding_generation_performance(self, test_mcp_server):
    
            """Test embedding generation performance."""
    
            
    
            from mcp_server.embeddings.product_embedder import ProductEmbedder
    
            
    
            # Test with mock embedding manager (no actual API calls)
    
            embedder = ProductEmbedder(test_mcp_server.db_provider)
    
            embedder.embedding_manager = test_mcp_server.embedding_manager
    
            
    
            # Test batch embedding generation
    
            test_texts = [f"Test product {i} description" for i in range(100)]
    
            
    
            start_time = time.time()
    
            embeddings = await embedder.embedding_manager.generate_embeddings_batch(test_texts)
    
            batch_time = time.time() - start_time
    
            
    
            assert len(embeddings) == 100
    
            assert batch_time < 5.0  # Should complete in under 5 seconds with mocks
    
            
    
            print(f"Batch embedding generation (100 items): {batch_time:.3f}s")
    
            print(f"Average per embedding: {batch_time/100:.4f}s")
    
        
    
        @pytest.mark.slow
    
        async def test_memory_usage(self, test_mcp_server, db_connection):
    
            """Test memory usage under load."""
    
            
    
            import psutil
    
            import os
    
            
    
            process = psutil.Process(os.getpid())
    
            initial_memory = process.memory_info().rss / 1024 / 1024  # MB
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create substantial dataset
    
            for i in range(500):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Execute multiple operations
    
            for i in range(50):
    
                await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT * FROM retail.customers LIMIT 100'
    
                    }
    
                )
    
            
    
            final_memory = process.memory_info().rss / 1024 / 1024  # MB
    
            memory_increase = final_memory - initial_memory
    
            
    
            # Memory increase should be reasonable (under 100MB for this test)
    
            assert memory_increase < 100
    
            
    
            print(f"Memory usage:")
    
            print(f"  Initial: {initial_memory:.1f} MB")
    
            print(f"  Final: {final_memory:.1f} MB")
    
            print(f"  Increase: {memory_increase:.1f} MB")
    
    
    
    class TestScalability:
    
        """Test scalability characteristics."""
    
        
    
        async def test_response_time_scaling(self, test_mcp_server, db_connection):
    
            """Test how response time scales with data size."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Test with different data sizes
    
            data_sizes = [100, 500, 1000, 2000]
    
            response_times = []
    
            
    
            for size in data_sizes:
    
                # Clear existing data
    
                await db_connection.execute("DELETE FROM retail.customers WHERE store_id = $1", store_id)
    
                
    
                # Create dataset of specified size
    
                for i in range(size):
    
                    await TestDataHelper.create_test_customer(db_connection, store_id)
    
                
    
                # Measure query time
    
                start_time = time.time()
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) FROM retail.customers'
    
                    }
    
                )
    
                execution_time = time.time() - start_time
    
                
    
                assert result['success'] is True
    
                response_times.append(execution_time)
    
                
    
                print(f"Data size {size}: {execution_time:.3f}s")
    
            
    
            # Response time should scale reasonably (not exponentially)
    
            # Simple count queries should remain fast even with larger datasets
    
            for time_val in response_times:
    
                assert time_val < 1.0  # All queries under 1 second
    
    

    ๐Ÿ” Debugging Tools

    Advanced Debugging Framework

    
    # mcp_server/debugging/debug_tools.py
    
    """
    
    Advanced debugging tools for MCP server troubleshooting.
    
    """
    
    import asyncio
    
    import json
    
    import time
    
    import traceback
    
    from typing import Dict, Any, List, Optional
    
    from datetime import datetime
    
    import logging
    
    from contextlib import asynccontextmanager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class MCPDebugger:
    
        """Comprehensive debugging utilities for MCP server."""
    
        
    
        def __init__(self, server_instance):
    
            self.server = server_instance
    
            self.debug_logs = []
    
            self.performance_metrics = {}
    
            self.active_traces = {}
    
            
    
        @asynccontextmanager
    
        async def trace_execution(self, operation_name: str, context: Dict[str, Any] = None):
    
            """Trace operation execution with detailed logging."""
    
            
    
            trace_id = f"{operation_name}_{int(time.time() * 1000)}"
    
            start_time = time.time()
    
            
    
            trace_info = {
    
                'trace_id': trace_id,
    
                'operation': operation_name,
    
                'start_time': start_time,
    
                'context': context or {},
    
                'status': 'running'
    
            }
    
            
    
            self.active_traces[trace_id] = trace_info
    
            
    
            logger.debug(f"Starting trace: {trace_id} - {operation_name}")
    
            
    
            try:
    
                yield trace_info
    
                
    
                # Success
    
                execution_time = time.time() - start_time
    
                trace_info.update({
    
                    'status': 'completed',
    
                    'execution_time': execution_time
    
                })
    
                
    
                logger.debug(f"Completed trace: {trace_id} in {execution_time:.3f}s")
    
                
    
            except Exception as e:
    
                # Error
    
                execution_time = time.time() - start_time
    
                trace_info.update({
    
                    'status': 'error',
    
                    'execution_time': execution_time,
    
                    'error': str(e),
    
                    'traceback': traceback.format_exc()
    
                })
    
                
    
                logger.error(f"Error in trace: {trace_id} - {str(e)}")
    
                raise
    
                
    
            finally:
    
                # Store completed trace
    
                self.debug_logs.append(trace_info.copy())
    
                del self.active_traces[trace_id]
    
                
    
                # Limit debug log size
    
                if len(self.debug_logs) > 1000:
    
                    self.debug_logs = self.debug_logs[-500:]
    
        
    
        async def debug_tool_execution(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
    
            """Debug tool execution with comprehensive logging."""
    
            
    
            async with self.trace_execution(f"tool_execution_{tool_name}", {'parameters': parameters}) as trace:
    
                
    
                # Pre-execution validation
    
                validation_result = await self._validate_tool_parameters(tool_name, parameters)
    
                trace['validation'] = validation_result
    
                
    
                if not validation_result['valid']:
    
                    return {
    
                        'success': False,
    
                        'error': f"Parameter validation failed: {validation_result['errors']}",
    
                        'debug_info': trace
    
                    }
    
                
    
                # Database connection check
    
                db_health = await self._check_database_health()
    
                trace['database_health'] = db_health
    
                
    
                # Execute tool with monitoring
    
                try:
    
                    tool_instance = self.server.get_tool(tool_name)
    
                    if not tool_instance:
    
                        return {
    
                            'success': False,
    
                            'error': f"Tool '{tool_name}' not found",
    
                            'debug_info': trace
    
                        }
    
                    
    
                    # Monitor resource usage during execution
    
                    start_memory = await self._get_memory_usage()
    
                    
    
                    result = await tool_instance.call(**parameters)
    
                    
    
                    end_memory = await self._get_memory_usage()
    
                    
    
                    trace.update({
    
                        'memory_start_mb': start_memory,
    
                        'memory_end_mb': end_memory,
    
                        'memory_used_mb': end_memory - start_memory,
    
                        'result_success': result.success,
    
                        'result_row_count': result.row_count
    
                    })
    
                    
    
                    return {
    
                        'success': result.success,
    
                        'data': result.data,
    
                        'error': result.error,
    
                        'metadata': result.metadata,
    
                        'debug_info': trace
    
                    }
    
                    
    
                except Exception as e:
    
                    trace['exception'] = {
    
                        'type': type(e).__name__,
    
                        'message': str(e),
    
                        'traceback': traceback.format_exc()
    
                    }
    
                    
    
                    return {
    
                        'success': False,
    
                        'error': f"Tool execution failed: {str(e)}",
    
                        'debug_info': trace
    
                    }
    
        
    
        async def analyze_performance_bottlenecks(self) -> Dict[str, Any]:
    
            """Analyze performance bottlenecks from debug logs."""
    
            
    
            if not self.debug_logs:
    
                return {'message': 'No debug data available'}
    
            
    
            # Analyze execution times
    
            execution_times = {}
    
            error_rates = {}
    
            memory_usage = {}
    
            
    
            for log_entry in self.debug_logs[-100:]:  # Last 100 entries
    
                operation = log_entry['operation']
    
                
    
                # Execution time analysis
    
                if 'execution_time' in log_entry:
    
                    if operation not in execution_times:
    
                        execution_times[operation] = []
    
                    execution_times[operation].append(log_entry['execution_time'])
    
                
    
                # Error rate analysis
    
                if operation not in error_rates:
    
                    error_rates[operation] = {'total': 0, 'errors': 0}
    
                
    
                error_rates[operation]['total'] += 1
    
                if log_entry['status'] == 'error':
    
                    error_rates[operation]['errors'] += 1
    
                
    
                # Memory usage analysis
    
                if 'memory_used_mb' in log_entry:
    
                    if operation not in memory_usage:
    
                        memory_usage[operation] = []
    
                    memory_usage[operation].append(log_entry['memory_used_mb'])
    
            
    
            # Calculate statistics
    
            performance_stats = {}
    
            
    
            for operation, times in execution_times.items():
    
                if times:
    
                    performance_stats[operation] = {
    
                        'avg_execution_time': sum(times) / len(times),
    
                        'max_execution_time': max(times),
    
                        'min_execution_time': min(times),
    
                        'execution_count': len(times),
    
                        'error_rate': (error_rates[operation]['errors'] / 
    
                                     error_rates[operation]['total'] * 100),
    
                        'avg_memory_usage': (sum(memory_usage.get(operation, [0])) / 
    
                                           len(memory_usage.get(operation, [1])))
    
                    }
    
            
    
            # Identify bottlenecks
    
            bottlenecks = []
    
            
    
            for operation, stats in performance_stats.items():
    
                if stats['avg_execution_time'] > 2.0:  # Slow operations
    
                    bottlenecks.append({
    
                        'type': 'slow_execution',
    
                        'operation': operation,
    
                        'avg_time': stats['avg_execution_time']
    
                    })
    
                
    
                if stats['error_rate'] > 5.0:  # High error rate
    
                    bottlenecks.append({
    
                        'type': 'high_error_rate',
    
                        'operation': operation,
    
                        'error_rate': stats['error_rate']
    
                    })
    
                
    
                if stats['avg_memory_usage'] > 100:  # High memory usage
    
                    bottlenecks.append({
    
                        'type': 'high_memory_usage',
    
                        'operation': operation,
    
                        'memory_mb': stats['avg_memory_usage']
    
                    })
    
            
    
            return {
    
                'performance_stats': performance_stats,
    
                'bottlenecks': bottlenecks,
    
                'total_operations': len(self.debug_logs),
    
                'analysis_timestamp': datetime.now().isoformat()
    
            }
    
        
    
        async def _validate_tool_parameters(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
    
            """Validate tool parameters against schema."""
    
            
    
            try:
    
                tool_instance = self.server.get_tool(tool_name)
    
                if not tool_instance:
    
                    return {
    
                        'valid': False,
    
                        'errors': [f"Tool '{tool_name}' not found"]
    
                    }
    
                
    
                schema = tool_instance.get_input_schema()
    
                
    
                # Basic validation (in production, use jsonschema library)
    
                errors = []
    
                required_props = schema.get('required', [])
    
                
    
                for prop in required_props:
    
                    if prop not in parameters:
    
                        errors.append(f"Missing required parameter: {prop}")
    
                
    
                return {
    
                    'valid': len(errors) == 0,
    
                    'errors': errors,
    
                    'schema': schema
    
                }
    
                
    
            except Exception as e:
    
                return {
    
                    'valid': False,
    
                    'errors': [f"Validation error: {str(e)}"]
    
                }
    
        
    
        async def _check_database_health(self) -> Dict[str, Any]:
    
            """Check database health and connectivity."""
    
            
    
            try:
    
                health_status = await self.server.db_provider.health_check()
    
                return {
    
                    'healthy': health_status.get('status') == 'healthy',
    
                    'details': health_status
    
                }
    
            except Exception as e:
    
                return {
    
                    'healthy': False,
    
                    'error': str(e)
    
                }
    
        
    
        async def _get_memory_usage(self) -> float:
    
            """Get current memory usage in MB."""
    
            
    
            try:
    
                import psutil
    
                import os
    
                process = psutil.Process(os.getpid())
    
                return process.memory_info().rss / 1024 / 1024
    
            except:
    
                return 0.0
    
        
    
        def get_debug_summary(self) -> Dict[str, Any]:
    
            """Get summary of debug information."""
    
            
    
            recent_logs = self.debug_logs[-50:] if self.debug_logs else []
    
            
    
            return {
    
                'total_operations': len(self.debug_logs),
    
                'active_traces': len(self.active_traces),
    
                'recent_operations': [
    
                    {
    
                        'operation': log['operation'],
    
                        'status': log['status'],
    
                        'execution_time': log.get('execution_time', 0),
    
                        'timestamp': log.get('start_time', 0)
    
                    }
    
                    for log in recent_logs
    
                ],
    
                'current_traces': list(self.active_traces.keys())
    
            }
    
    
    
    # Debug tool for direct use
    
    class DebugTool:
    
        """Interactive debugging tool for MCP server."""
    
        
    
        def __init__(self, server_instance):
    
            self.debugger = MCPDebugger(server_instance)
    
        
    
        async def debug_query(self, query: str, store_id: str) -> Dict[str, Any]:
    
            """Debug a specific database query."""
    
            
    
            return await self.debugger.debug_tool_execution(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'query': query
    
                }
    
            )
    
        
    
        async def debug_search(self, query: str, store_id: str) -> Dict[str, Any]:
    
            """Debug a semantic search query."""
    
            
    
            return await self.debugger.debug_tool_execution(
    
                'semantic_search_products',
    
                {
    
                    'query': query,
    
                    'store_id': store_id,
    
                    'limit': 10
    
                }
    
            )
    
        
    
        async def get_performance_report(self) -> Dict[str, Any]:
    
            """Get comprehensive performance report."""
    
            
    
            return await self.debugger.analyze_performance_bottlenecks()
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Comprehensive Testing Framework: Unit, integration, and performance tests for all components

    โœ… Advanced Debugging Tools: Sophisticated debugging utilities with execution tracing

    โœ… Performance Validation: Load testing and scalability analysis capabilities

    โœ… Security Testing: SQL injection prevention and RLS validation

    โœ… Monitoring Integration: Performance metrics and bottleneck analysis

    โœ… CI/CD Ready: Automated testing workflows for continuous integration

    ๐Ÿš€ What's Next

    Continue with Lab 09: VS Code Integration to:

  • Configure VS Code for MCP server development
  • Set up debugging environments in VS Code
  • Integrate MCP server with VS Code Chat
  • Test the complete VS Code workflow
  • ๐Ÿ“š Additional Resources

    Testing Frameworks

  • pytest Documentation - Python testing framework
  • AsyncPG Testing - Async PostgreSQL testing
  • FastAPI Testing - API testing patterns
  • Performance Testing

  • Load Testing Best Practices - Async performance testing
  • Database Performance Testing - PostgreSQL optimization
  • Memory Profiling - Python memory analysis
  • Debugging Tools

  • Python Debugging - Python debugger
  • Async Debugging - Asyncio debugging
  • SQL Debugging - PostgreSQL logging
  • ---

    Previous: Lab 07: Semantic Search Integration

    Next: Lab 09: VS Code Integration

    09 VS Code Integration

    VS Code Integration

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on integrating your MCP server with VS Code to enable natural language queries through AI Chat.

    You'll learn to configure VS Code for optimal MCP usage, debug server connections, and leverage the full power of AI-assisted database interactions.

    Overview

    VS Code's MCP integration transforms how developers interact with databases and APIs through natural language.

    By connecting your retail MCP server to VS Code Chat, you enable intelligent querying of sales data, product catalogs, and business analytics using conversational AI.

    This integration allows developers to ask questions like "Show me top selling products this month" or "Find customers who haven't purchased in 90 days" and get structured data responses without writing SQL queries.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Configure VS Code MCP settings for your retail server
  • Integrate MCP servers with VS Code AI Chat functionality
  • Debug MCP server connections and troubleshoot issues
  • Optimize natural language query patterns for better results
  • Customize VS Code workspace for MCP development
  • Deploy multi-server configurations for complex scenarios
  • ๐Ÿ”ง VS Code MCP Configuration

    Initial Setup and Installation

    
    // .vscode/settings.json
    
    {
    
        "mcp.servers": {
    
            "retail-mcp-server": {
    
                "command": "python",
    
                "args": [
    
                    "-m", "mcp_server.main"
    
                ],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_PORT": "5432",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "AZURE_CLIENT_ID": "${env:AZURE_CLIENT_ID}",
    
                    "AZURE_CLIENT_SECRET": "${env:AZURE_CLIENT_SECRET}",
    
                    "AZURE_TENANT_ID": "${env:AZURE_TENANT_ID}",
    
                    "LOG_LEVEL": "INFO",
    
                    "MCP_SERVER_DEBUG": "false"
    
                },
    
                "cwd": "${workspaceFolder}",
    
                "initializationOptions": {
    
                    "store_id": "seattle",
    
                    "enable_semantic_search": true,
    
                    "enable_analytics": true,
    
                    "cache_embeddings": true
    
                }
    
            }
    
        },
    
        "mcp.serverTimeout": 30000,
    
        "mcp.enableLogging": true,
    
        "mcp.logLevel": "info"
    
    }
    
    

    Environment Configuration

    
    # .env file for development
    
    POSTGRES_HOST=localhost
    
    POSTGRES_PORT=5432
    
    POSTGRES_DB=retail_db
    
    POSTGRES_USER=mcp_user
    
    POSTGRES_PASSWORD=your_secure_password
    
    
    
    # Azure Configuration
    
    PROJECT_ENDPOINT=https://your-project.openai.azure.com
    
    AZURE_CLIENT_ID=your-client-id
    
    AZURE_CLIENT_SECRET=your-client-secret
    
    AZURE_TENANT_ID=your-tenant-id
    
    
    
    # Optional: Azure Key Vault
    
    AZURE_KEY_VAULT_URL=https://your-keyvault.vault.azure.net/
    
    
    
    # Server Configuration
    
    MCP_SERVER_PORT=8000
    
    MCP_SERVER_HOST=127.0.0.1
    
    LOG_LEVEL=INFO
    
    

    Workspace Configuration

    
    // .vscode/launch.json
    
    {
    
        "version": "0.2.0",
    
        "configurations": [
    
            {
    
                "name": "Debug MCP Server",
    
                "type": "python",
    
                "request": "launch",
    
                "module": "mcp_server.main",
    
                "console": "integratedTerminal",
    
                "envFile": "${workspaceFolder}/.env",
    
                "env": {
    
                    "MCP_SERVER_DEBUG": "true",
    
                    "LOG_LEVEL": "DEBUG"
    
                },
    
                "args": [],
    
                "justMyCode": false,
    
                "stopOnEntry": false
    
            },
    
            {
    
                "name": "Test MCP Server",
    
                "type": "python",
    
                "request": "launch",
    
                "module": "pytest",
    
                "console": "integratedTerminal",
    
                "envFile": "${workspaceFolder}/.env.test",
    
                "args": [
    
                    "tests/",
    
                    "-v",
    
                    "--tb=short"
    
                ]
    
            }
    
        ]
    
    }
    
    

    Task Configuration

    
    // .vscode/tasks.json
    
    {
    
        "version": "2.0.0",
    
        "tasks": [
    
            {
    
                "label": "Start MCP Server",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "-m", "mcp_server.main"
    
                ],
    
                "group": "build",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "new"
    
                },
    
                "options": {
    
                    "env": {
    
                        "PYTHONPATH": "${workspaceFolder}"
    
                    }
    
                },
    
                "isBackground": true,
    
                "problemMatcher": {
    
                    "pattern": {
    
                        "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
    
                        "file": 1,
    
                        "line": 2,
    
                        "column": 3,
    
                        "severity": 4,
    
                        "message": 5
    
                    },
    
                    "background": {
    
                        "activeOnStart": true,
    
                        "beginsPattern": "^.*Starting MCP server.*$",
    
                        "endsPattern": "^.*MCP server ready.*$"
    
                    }
    
                }
    
            },
    
            {
    
                "label": "Run Tests",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "-m", "pytest",
    
                    "tests/",
    
                    "-v"
    
                ],
    
                "group": "test",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "shared"
    
                }
    
            },
    
            {
    
                "label": "Generate Sample Data",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "scripts/generate_sample_data.py"
    
                ],
    
                "group": "build",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "shared"
    
                }
    
            },
    
            {
    
                "label": "Create Database Schema",
    
                "type": "shell",
    
                "command": "psql",
    
                "args": [
    
                    "-h", "${env:POSTGRES_HOST}",
    
                    "-p", "${env:POSTGRES_PORT}",
    
                    "-U", "${env:POSTGRES_USER}",
    
                    "-d", "${env:POSTGRES_DB}",
    
                    "-f", "scripts/create_schema.sql"
    
                ],
    
                "group": "build"
    
            }
    
        ]
    
    }
    
    

    ๐Ÿ’ฌ AI Chat Integration

    Natural Language Query Patterns

    
    // Example query patterns for VS Code Chat
    
    interface QueryPattern {
    
        intent: string;
    
        examples: string[];
    
        expectedTools: string[];
    
    }
    
    
    
    const retailQueryPatterns: QueryPattern[] = [
    
        {
    
            intent: "sales_analysis",
    
            examples: [
    
                "Show me daily sales for the last 30 days",
    
                "What are our top selling products this month?",
    
                "Which customers have spent the most this quarter?",
    
                "Compare sales performance between stores"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "product_search",
    
            examples: [
    
                "Find running shoes for women",
    
                "Show me electronics under $500",
    
                "What laptops do we have in stock?",
    
                "Search for wireless headphones"
    
            ],
    
            expectedTools: ["semantic_search_products", "hybrid_product_search"]
    
        },
    
        {
    
            intent: "inventory_management",
    
            examples: [
    
                "Which products are low on stock?",
    
                "Show me products that need reordering",
    
                "What's our current inventory value?",
    
                "Find products with zero stock"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "customer_analysis",
    
            examples: [
    
                "Show me customers who haven't purchased in 90 days",
    
                "What's the average customer lifetime value?",
    
                "Which customers are in the gold tier?",
    
                "Find customers with returns"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "business_intelligence",
    
            examples: [
    
                "Generate a business summary for this month",
    
                "Show me seasonal trends",
    
                "What are our best performing categories?",
    
                "Create a sales forecast"
    
            ],
    
            expectedTools: ["generate_business_insights"]
    
        },
    
        {
    
            intent: "recommendations",
    
            examples: [
    
                "Recommend products similar to product X",
    
                "What should we recommend to customer Y?",
    
                "Show me trending products",
    
                "Find cross-sell opportunities"
    
            ],
    
            expectedTools: ["get_product_recommendations"]
    
        }
    
    ];
    
    

    Chat Integration Examples

    
    <!-- Examples of VS Code Chat interactions -->
    
    
    
    ## Sales Analysis Queries
    
    
    
    **User**: Show me the top 10 selling products in the Seattle store for the last month
    
    
    
    **Expected Response**: 
    
    - Tool: execute_sales_query
    
    - Parameters: query_type="top_products", store_id="seattle", start_date="2025-08-29", end_date="2025-09-29", limit=10
    
    - Result: Formatted table with product names, quantities sold, revenue, and performance metrics
    
    
    
    **User**: What was our daily revenue trend last week?
    
    
    
    **Expected Response**:
    
    - Tool: execute_sales_query  
    
    - Parameters: query_type="daily_sales", store_id="seattle", start_date="2025-09-22", end_date="2025-09-29"
    
    - Result: Chart-ready data with daily revenue figures and growth percentages
    
    
    
    ## Product Search Queries
    
    
    
    **User**: Find comfortable running shoes for outdoor activities
    
    
    
    **Expected Response**:
    
    - Tool: semantic_search_products
    
    - Parameters: query="comfortable running shoes outdoor activities", store_id="seattle", similarity_threshold=0.7
    
    - Result: Ranked list of relevant products with similarity scores and detailed information
    
    
    
    **User**: Search for laptops under $1500 with good reviews
    
    
    
    **Expected Response**:
    
    - Tool: hybrid_product_search
    
    - Parameters: query="laptops under $1500 good reviews", store_id="seattle", semantic_weight=0.6, keyword_weight=0.4
    
    - Result: Combined keyword and semantic search results with price and rating filters
    
    
    
    ## Business Intelligence Queries
    
    
    
    **User**: Generate a comprehensive business summary for September
    
    
    
    **Expected Response**:
    
    - Tool: generate_business_insights
    
    - Parameters: analysis_type="summary", store_id="seattle", days=30
    
    - Result: KPI dashboard with revenue, customer metrics, top categories, and growth trends
    
    

    Chat Response Formatting

    
    # mcp_server/chat/response_formatter.py
    
    """
    
    Format MCP tool responses for optimal VS Code Chat display.
    
    """
    
    from typing import Dict, Any, List
    
    import json
    
    from datetime import datetime
    
    
    
    class ChatResponseFormatter:
    
        """Format tool responses for VS Code Chat consumption."""
    
        
    
        @staticmethod
    
        def format_sales_data(data: List[Dict[str, Any]], query_type: str) -> str:
    
            """Format sales data for chat display."""
    
            
    
            if not data:
    
                return "No sales data found for the specified criteria."
    
            
    
            if query_type == "daily_sales":
    
                return ChatResponseFormatter._format_daily_sales(data)
    
            elif query_type == "top_products":
    
                return ChatResponseFormatter._format_top_products(data)
    
            elif query_type == "customer_analysis":
    
                return ChatResponseFormatter._format_customer_analysis(data)
    
            else:
    
                return ChatResponseFormatter._format_generic_table(data)
    
        
    
        @staticmethod
    
        def _format_daily_sales(data: List[Dict[str, Any]]) -> str:
    
            """Format daily sales data."""
    
            
    
            response = "## Daily Sales Summary\n\n"
    
            response += "| Date | Revenue | Transactions | Avg Order Value | Customers |\n"
    
            response += "|------|---------|-------------|----------------|----------|\n"
    
            
    
            total_revenue = 0
    
            total_transactions = 0
    
            
    
            for day in data:
    
                revenue = float(day.get('total_revenue', 0))
    
                transactions = int(day.get('transaction_count', 0))
    
                avg_value = float(day.get('avg_transaction_value', 0))
    
                customers = int(day.get('unique_customers', 0))
    
                
    
                total_revenue += revenue
    
                total_transactions += transactions
    
                
    
                response += f"| {day.get('sales_date', 'N/A')} | "
    
                response += f"${revenue:,.2f} | "
    
                response += f"{transactions:,} | "
    
                response += f"${avg_value:.2f} | "
    
                response += f"{customers:,} |\n"
    
            
    
            response += f"\n**Totals**: ${total_revenue:,.2f} revenue, {total_transactions:,} transactions"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def _format_top_products(data: List[Dict[str, Any]]) -> str:
    
            """Format top products data."""
    
            
    
            response = "## Top Selling Products\n\n"
    
            response += "| Rank | Product | Brand | Revenue | Qty Sold | Avg Price |\n"
    
            response += "|------|---------|-------|---------|----------|----------|\n"
    
            
    
            for i, product in enumerate(data, 1):
    
                response += f"| {i} | "
    
                response += f"{product.get('product_name', 'N/A')} | "
    
                response += f"{product.get('brand', 'N/A')} | "
    
                response += f"${float(product.get('total_revenue', 0)):,.2f} | "
    
                response += f"{int(product.get('total_quantity_sold', 0)):,} | "
    
                response += f"${float(product.get('avg_price', 0)):.2f} |\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_search_results(data: List[Dict[str, Any]], search_type: str) -> str:
    
            """Format product search results."""
    
            
    
            if not data:
    
                return "No products found matching your search criteria."
    
            
    
            response = f"## Product Search Results ({search_type})\n\n"
    
            
    
            for i, product in enumerate(data, 1):
    
                response += f"### {i}. {product.get('product_name', 'Unknown Product')}\n"
    
                response += f"**Brand**: {product.get('brand', 'N/A')}\n"
    
                response += f"**Price**: ${float(product.get('price', 0)):.2f}\n"
    
                response += f"**Stock**: {int(product.get('current_stock', 0))} units\n"
    
                
    
                if 'similarity_score' in product:
    
                    score = float(product['similarity_score'])
    
                    response += f"**Relevance**: {score:.1%}\n"
    
                
    
                if 'rating_average' in product and product['rating_average']:
    
                    rating = float(product['rating_average'])
    
                    count = int(product.get('rating_count', 0))
    
                    response += f"**Rating**: {rating:.1f}/5.0 ({count:,} reviews)\n"
    
                
    
                if product.get('product_description'):
    
                    desc = product['product_description']
    
                    if len(desc) > 150:
    
                        desc = desc[:150] + "..."
    
                    response += f"**Description**: {desc}\n"
    
                
    
                response += "\n---\n\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_business_insights(data: Dict[str, Any]) -> str:
    
            """Format business intelligence data."""
    
            
    
            response = "## Business Intelligence Summary\n\n"
    
            
    
            # Key metrics
    
            response += "### Key Performance Indicators\n\n"
    
            response += f"- **Total Revenue**: ${float(data.get('total_revenue', 0)):,.2f}\n"
    
            response += f"- **Total Transactions**: {int(data.get('total_transactions', 0)):,}\n"
    
            response += f"- **Unique Customers**: {int(data.get('unique_customers', 0)):,}\n"
    
            response += f"- **Average Order Value**: ${float(data.get('avg_transaction_value', 0)):.2f}\n"
    
            response += f"- **Products Sold**: {int(data.get('products_sold', 0)):,} items\n\n"
    
            
    
            # Performance indicators
    
            if 'insights' in data and 'performance_indicators' in data['insights']:
    
                pi = data['insights']['performance_indicators']
    
                response += "### Performance Indicators\n\n"
    
                response += f"- **Transactions per Day**: {float(pi.get('transactions_per_day', 0)):.1f}\n"
    
                response += f"- **Revenue per Customer**: ${float(pi.get('revenue_per_customer', 0)):,.2f}\n"
    
                response += f"- **Items per Transaction**: {float(pi.get('items_per_transaction', 0)):.1f}\n\n"
    
            
    
            # Top category
    
            if data.get('top_category'):
    
                response += f"### Top Performing Category\n\n"
    
                response += f"**{data['top_category']}** - ${float(data.get('top_category_revenue', 0)):,.2f} revenue\n\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_error_response(error: str, tool_name: str) -> str:
    
            """Format error responses for chat."""
    
            
    
            response = f"## โŒ Error in {tool_name}\n\n"
    
            response += f"I encountered an issue while processing your request:\n\n"
    
            response += f"**Error**: {error}\n\n"
    
            response += "Please try:\n"
    
            response += "- Checking your query parameters\n"
    
            response += "- Verifying store access permissions\n"
    
            response += "- Simplifying your request\n"
    
            response += "- Contacting support if the issue persists\n"
    
            
    
            return response
    
    

    ๐Ÿ” Debugging and Troubleshooting

    VS Code Debug Configuration

    
    # mcp_server/debug/vscode_debug.py
    
    """
    
    VS Code specific debugging utilities for MCP server.
    
    """
    
    import logging
    
    import json
    
    from typing import Dict, Any
    
    from datetime import datetime
    
    
    
    class VSCodeDebugLogger:
    
        """Enhanced logging for VS Code debugging."""
    
        
    
        def __init__(self):
    
            self.logger = logging.getLogger("mcp_vscode_debug")
    
            self.setup_vscode_logging()
    
        
    
        def setup_vscode_logging(self):
    
            """Configure logging for VS Code debugging."""
    
            
    
            # Create VS Code specific formatter
    
            formatter = logging.Formatter(
    
                '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s'
    
            )
    
            
    
            # Console handler for VS Code terminal
    
            console_handler = logging.StreamHandler()
    
            console_handler.setFormatter(formatter)
    
            console_handler.setLevel(logging.DEBUG)
    
            
    
            self.logger.addHandler(console_handler)
    
            self.logger.setLevel(logging.DEBUG)
    
        
    
        def log_mcp_request(self, method: str, params: Dict[str, Any]):
    
            """Log MCP requests for debugging."""
    
            
    
            self.logger.info(f"MCP Request: {method}")
    
            self.logger.debug(f"Parameters: {json.dumps(params, indent=2)}")
    
        
    
        def log_tool_execution(self, tool_name: str, result: Dict[str, Any]):
    
            """Log tool execution results."""
    
            
    
            success = result.get('success', False)
    
            level = logging.INFO if success else logging.ERROR
    
            
    
            self.logger.log(level, f"Tool '{tool_name}' - {'Success' if success else 'Failed'}")
    
            
    
            if not success and result.get('error'):
    
                self.logger.error(f"Error: {result['error']}")
    
            
    
            if result.get('data'):
    
                data_summary = self._summarize_data(result['data'])
    
                self.logger.debug(f"Result summary: {data_summary}")
    
        
    
        def _summarize_data(self, data: Any) -> str:
    
            """Create a summary of result data."""
    
            
    
            if isinstance(data, list):
    
                return f"List with {len(data)} items"
    
            elif isinstance(data, dict):
    
                return f"Dict with keys: {list(data.keys())}"
    
            else:
    
                return f"Data type: {type(data).__name__}"
    
    
    
    # Global debug logger
    
    vscode_debug_logger = VSCodeDebugLogger()
    
    

    Connection Troubleshooting

    
    # scripts/debug_mcp_connection.py
    
    """
    
    Debug script for troubleshooting MCP server connections in VS Code.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import os
    
    import sys
    
    from typing import Dict, Any
    
    
    
    async def test_database_connection() -> Dict[str, Any]:
    
        """Test database connectivity."""
    
        
    
        try:
    
            # Get connection parameters from environment
    
            connection_params = {
    
                'host': os.getenv('POSTGRES_HOST', 'localhost'),
    
                'port': int(os.getenv('POSTGRES_PORT', '5432')),
    
                'database': os.getenv('POSTGRES_DB', 'retail_db'),
    
                'user': os.getenv('POSTGRES_USER', 'mcp_user'),
    
                'password': os.getenv('POSTGRES_PASSWORD', '')
    
            }
    
            
    
            print(f"Testing connection to {connection_params['host']}:{connection_params['port']}")
    
            
    
            # Test connection
    
            conn = await asyncpg.connect(**connection_params)
    
            
    
            # Test basic query
    
            result = await conn.fetchval("SELECT version()")
    
            
    
            # Test schema access
    
            tables = await conn.fetch("""
    
                SELECT table_name FROM information_schema.tables 
    
                WHERE table_schema = 'retail'
    
            """)
    
            
    
            await conn.close()
    
            
    
            return {
    
                'success': True,
    
                'database_version': result,
    
                'retail_tables': len(tables),
    
                'table_names': [table['table_name'] for table in tables]
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e),
    
                'connection_params': {k: v for k, v in connection_params.items() if k != 'password'}
    
            }
    
    
    
    async def test_azure_openai_connection() -> Dict[str, Any]:
    
        """Test Azure OpenAI connectivity."""
    
        
    
        try:
    
            from azure.identity import DefaultAzureCredential
    
            from azure.ai.projects import AIProjectClient
    
            
    
            project_endpoint = os.getenv('PROJECT_ENDPOINT')
    
            if not project_endpoint:
    
                return {
    
                    'success': False,
    
                    'error': 'PROJECT_ENDPOINT not configured'
    
                }
    
            
    
            print(f"Testing Azure OpenAI connection to {project_endpoint}")
    
            
    
            credential = DefaultAzureCredential()
    
            client = AIProjectClient(
    
                endpoint=project_endpoint,
    
                credential=credential
    
            )
    
            
    
            # Test embedding generation
    
            response = await client.embeddings.create(
    
                model="text-embedding-3-small",
    
                input="test connection"
    
            )
    
            
    
            embedding = response.data[0].embedding
    
            
    
            return {
    
                'success': True,
    
                'project_endpoint': project_endpoint,
    
                'embedding_dimension': len(embedding),
    
                'model': 'text-embedding-3-small'
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e),
    
                'project_endpoint': os.getenv('PROJECT_ENDPOINT', 'Not configured')
    
            }
    
    
    
    async def test_mcp_tools() -> Dict[str, Any]:
    
        """Test MCP tool availability."""
    
        
    
        try:
    
            # Import MCP server components
    
            sys.path.append(os.path.dirname(os.path.dirname(__file__)))
    
            
    
            from mcp_server.server import MCPServer
    
            from mcp_server.database import DatabaseProvider
    
            from mcp_server.config import Config
    
            
    
            # Create test configuration
    
            config = Config()
    
            db_provider = DatabaseProvider(config.database.connection_string)
    
            
    
            # Initialize server
    
            server = MCPServer(config, db_provider)
    
            await server.initialize()
    
            
    
            # Get available tools
    
            tools = server.get_available_tools()
    
            
    
            # Test a simple tool
    
            test_result = await server.execute_tool(
    
                'get_current_utc_date',
    
                {'format': 'iso'}
    
            )
    
            
    
            await server.cleanup()
    
            
    
            return {
    
                'success': True,
    
                'available_tools': [tool.name for tool in tools],
    
                'tool_count': len(tools),
    
                'test_tool_result': test_result.get('success', False)
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e)
    
            }
    
    
    
    async def main():
    
        """Run comprehensive connection tests."""
    
        
    
        print("๐Ÿ” MCP Server Connection Diagnostics")
    
        print("=" * 50)
    
        
    
        # Test database connection
    
        print("\n๐Ÿ“Š Testing Database Connection...")
    
        db_result = await test_database_connection()
    
        
    
        if db_result['success']:
    
            print("โœ… Database connection successful")
    
            print(f"   Database version: {db_result['database_version']}")
    
            print(f"   Retail tables found: {db_result['retail_tables']}")
    
            print(f"   Table names: {', '.join(db_result['table_names'])}")
    
        else:
    
            print("โŒ Database connection failed")
    
            print(f"   Error: {db_result['error']}")
    
        
    
        # Test Azure OpenAI connection
    
        print("\n๐Ÿค– Testing Azure OpenAI Connection...")
    
        azure_result = await test_azure_openai_connection()
    
        
    
        if azure_result['success']:
    
            print("โœ… Azure OpenAI connection successful")
    
            print(f"   Endpoint: {azure_result['project_endpoint']}")
    
            print(f"   Embedding dimension: {azure_result['embedding_dimension']}")
    
        else:
    
            print("โŒ Azure OpenAI connection failed")
    
            print(f"   Error: {azure_result['error']}")
    
        
    
        # Test MCP tools
    
        print("\n๐Ÿ› ๏ธ  Testing MCP Tools...")
    
        tools_result = await test_mcp_tools()
    
        
    
        if tools_result['success']:
    
            print("โœ… MCP tools loaded successfully")
    
            print(f"   Available tools: {tools_result['tool_count']}")
    
            print(f"   Tool names: {', '.join(tools_result['available_tools'])}")
    
            print(f"   Test execution: {'โœ…' if tools_result['test_tool_result'] else 'โŒ'}")
    
        else:
    
            print("โŒ MCP tools loading failed")
    
            print(f"   Error: {tools_result['error']}")
    
        
    
        # Overall status
    
        print("\n๐Ÿ“‹ Overall Status")
    
        print("=" * 50)
    
        
    
        all_success = all([
    
            db_result['success'],
    
            azure_result['success'],
    
            tools_result['success']
    
        ])
    
        
    
        if all_success:
    
            print("๐ŸŽ‰ All systems ready! MCP server should work correctly in VS Code.")
    
        else:
    
            print("โš ๏ธ  Some issues detected. Please resolve the errors above.")
    
            print("\n๐Ÿ’ก Troubleshooting tips:")
    
            print("   - Check environment variables in .env file")
    
            print("   - Verify database is running and accessible")
    
            print("   - Confirm Azure credentials are configured")
    
            print("   - Review VS Code MCP server configuration")
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    

    ๐Ÿš€ Advanced Configuration

    Multi-Server Setup

    
    // .vscode/settings.json - Multiple MCP servers
    
    {
    
        "mcp.servers": {
    
            "retail-seattle": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "DEFAULT_STORE_ID": "seattle"
    
                },
    
                "initializationOptions": {
    
                    "store_id": "seattle",
    
                    "server_name": "Seattle Store"
    
                }
    
            },
    
            "retail-redmond": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "DEFAULT_STORE_ID": "redmond"
    
                },
    
                "initializationOptions": {
    
                    "store_id": "redmond",
    
                    "server_name": "Redmond Store"
    
                }
    
            },
    
            "retail-analytics": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.analytics_main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "analytics_user",
    
                    "POSTGRES_PASSWORD": "${env:ANALYTICS_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}"
    
                },
    
                "initializationOptions": {
    
                    "mode": "analytics",
    
                    "cross_store_access": true
    
                }
    
            }
    
        }
    
    }
    
    

    Custom VS Code Extension

    
    // src/extension.ts - Custom MCP retail extension
    
    import * as vscode from 'vscode';
    
    
    
    export function activate(context: vscode.ExtensionContext) {
    
        
    
        // Register MCP retail commands
    
        const disposable = vscode.commands.registerCommand(
    
            'mcp-retail.quickQuery', 
    
            async () => {
    
                const quickPick = vscode.window.createQuickPick();
    
                quickPick.items = [
    
                    {
    
                        label: '๐Ÿ“Š Daily Sales',
    
                        description: 'Show daily sales for the last 30 days'
    
                    },
    
                    {
    
                        label: '๐Ÿ† Top Products',
    
                        description: 'Show top selling products this month'
    
                    },
    
                    {
    
                        label: '๐Ÿ‘ฅ Customer Analysis',
    
                        description: 'Analyze customer behavior and trends'
    
                    },
    
                    {
    
                        label: '๐Ÿ” Product Search',
    
                        description: 'Search for products using natural language'
    
                    },
    
                    {
    
                        label: '๐Ÿ“ˆ Business Insights',
    
                        description: 'Generate comprehensive business summary'
    
                    }
    
                ];
    
                
    
                quickPick.onDidChangeSelection(selection => {
    
                    if (selection[0]) {
    
                        executeQuickQuery(selection[0].label);
    
                    }
    
                });
    
                
    
                quickPick.onDidHide(() => quickPick.dispose());
    
                quickPick.show();
    
            }
    
        );
    
        
    
        context.subscriptions.push(disposable);
    
        
    
        // Register store switcher
    
        const storeSwitcher = vscode.commands.registerCommand(
    
            'mcp-retail.switchStore',
    
            async () => {
    
                const stores = ['seattle', 'redmond', 'bellevue', 'online'];
    
                const selected = await vscode.window.showQuickPick(stores, {
    
                    placeHolder: 'Select store for queries'
    
                });
    
                
    
                if (selected) {
    
                    // Update configuration
    
                    const config = vscode.workspace.getConfiguration('mcp');
    
                    await config.update('defaultStore', selected, true);
    
                    
    
                    vscode.window.showInformationMessage(
    
                        `Switched to ${selected.charAt(0).toUpperCase() + selected.slice(1)} store`
    
                    );
    
                }
    
            }
    
        );
    
        
    
        context.subscriptions.push(storeSwitcher);
    
    }
    
    
    
    async function executeQuickQuery(queryType: string) {
    
        // Execute predefined queries in VS Code Chat
    
        const chatCommands = {
    
            '๐Ÿ“Š Daily Sales': '@retail Show me daily sales for the last 30 days',
    
            '๐Ÿ† Top Products': '@retail What are the top 10 selling products this month?',
    
            '๐Ÿ‘ฅ Customer Analysis': '@retail Show me customer analysis for active customers',
    
            '๐Ÿ” Product Search': '@retail Find products matching "laptop computer"',
    
            '๐Ÿ“ˆ Business Insights': '@retail Generate a business summary for this month'
    
        };
    
        
    
        const command = chatCommands[queryType];
    
        if (command) {
    
            await vscode.commands.executeCommand('workbench.action.chat.open');
    
            await vscode.commands.executeCommand('workbench.action.chat.insert', command);
    
        }
    
    }
    
    
    
    export function deactivate() {}
    
    

    Extension Package Configuration

    
    // package.json for VS Code extension
    
    {
    
        "name": "mcp-retail-assistant",
    
        "displayName": "MCP Retail Assistant",
    
        "description": "AI-powered retail data analysis through MCP",
    
        "version": "1.0.0",
    
        "engines": {
    
            "vscode": "^1.74.0"
    
        },
    
        "categories": [
    
            "Other",
    
            "Data Science",
    
            "Machine Learning"
    
        ],
    
        "activationEvents": [
    
            "onCommand:mcp-retail.quickQuery",
    
            "onCommand:mcp-retail.switchStore"
    
        ],
    
        "main": "./out/extension.js",
    
        "contributes": {
    
            "commands": [
    
                {
    
                    "command": "mcp-retail.quickQuery",
    
                    "title": "Quick Retail Query",
    
                    "category": "MCP Retail"
    
                },
    
                {
    
                    "command": "mcp-retail.switchStore",
    
                    "title": "Switch Store",
    
                    "category": "MCP Retail"
    
                }
    
            ],
    
            "keybindings": [
    
                {
    
                    "command": "mcp-retail.quickQuery",
    
                    "key": "ctrl+shift+r",
    
                    "mac": "cmd+shift+r"
    
                }
    
            ],
    
            "configuration": {
    
                "title": "MCP Retail",
    
                "properties": {
    
                    "mcp-retail.defaultStore": {
    
                        "type": "string",
    
                        "default": "seattle",
    
                        "enum": ["seattle", "redmond", "bellevue", "online"],
    
                        "description": "Default store for retail queries"
    
                    },
    
                    "mcp-retail.enableAnalytics": {
    
                        "type": "boolean",
    
                        "default": true,
    
                        "description": "Enable advanced analytics features"
    
                    }
    
                }
    
            }
    
        },
    
        "scripts": {
    
            "vscode:prepublish": "npm run compile",
    
            "compile": "tsc -p ./",
    
            "watch": "tsc -watch -p ./"
    
        },
    
        "devDependencies": {
    
            "@types/vscode": "^1.74.0",
    
            "@types/node": "16.x",
    
            "typescript": "^4.9.4"
    
        }
    
    }
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… VS Code MCP Configuration: Complete setup for optimal MCP integration

    โœ… AI Chat Integration: Natural language querying capabilities in VS Code

    โœ… Debugging Tools: Comprehensive troubleshooting and connection diagnostics

    โœ… Multi-Server Setup: Configuration for multiple MCP server instances

    โœ… Custom Extensions: Enhanced VS Code experience with retail-specific features

    โœ… Production Readiness: Enterprise-ready VS Code development environment

    ๐Ÿš€ What's Next

    Continue with Lab 10: Deployment Strategies to:

  • Deploy MCP servers to production environments
  • Configure cloud infrastructure for scalability
  • Implement CI/CD pipelines for automated deployment
  • Monitor production MCP server performance
  • ๐Ÿ“š Additional Resources

    VS Code Development

  • VS Code Extension API - Official extension development guide
  • VS Code MCP Documentation - MCP integration documentation
  • TypeScript for VS Code - TypeScript development in VS Code
  • MCP Protocol

  • Model Context Protocol Specification - Official MCP specification
  • MCP Best Practices - Implementation best practices
  • FastMCP Framework - Python MCP implementation
  • Development Tools

  • Python in VS Code - Python development setup
  • Debugging in VS Code - Advanced debugging techniques
  • VS Code Tasks - Task automation and configuration
  • ---

    Previous: Lab 08: Testing and Debugging

    Next: Lab 10: Deployment Strategies

    Configuring VS Code MCP integration and AI Chat usage Integrate

    VS Code Integration

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on integrating your MCP server with VS Code to enable natural language queries through AI Chat.

    You'll learn to configure VS Code for optimal MCP usage, debug server connections, and leverage the full power of AI-assisted database interactions.

    Overview

    VS Code's MCP integration transforms how developers interact with databases and APIs through natural language.

    By connecting your retail MCP server to VS Code Chat, you enable intelligent querying of sales data, product catalogs, and business analytics using conversational AI.

    This integration allows developers to ask questions like "Show me top selling products this month" or "Find customers who haven't purchased in 90 days" and get structured data responses without writing SQL queries.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Configure VS Code MCP settings for your retail server
  • Integrate MCP servers with VS Code AI Chat functionality
  • Debug MCP server connections and troubleshoot issues
  • Optimize natural language query patterns for better results
  • Customize VS Code workspace for MCP development
  • Deploy multi-server configurations for complex scenarios
  • ๐Ÿ”ง VS Code MCP Configuration

    Initial Setup and Installation

    
    // .vscode/settings.json
    
    {
    
        "mcp.servers": {
    
            "retail-mcp-server": {
    
                "command": "python",
    
                "args": [
    
                    "-m", "mcp_server.main"
    
                ],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_PORT": "5432",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "AZURE_CLIENT_ID": "${env:AZURE_CLIENT_ID}",
    
                    "AZURE_CLIENT_SECRET": "${env:AZURE_CLIENT_SECRET}",
    
                    "AZURE_TENANT_ID": "${env:AZURE_TENANT_ID}",
    
                    "LOG_LEVEL": "INFO",
    
                    "MCP_SERVER_DEBUG": "false"
    
                },
    
                "cwd": "${workspaceFolder}",
    
                "initializationOptions": {
    
                    "store_id": "seattle",
    
                    "enable_semantic_search": true,
    
                    "enable_analytics": true,
    
                    "cache_embeddings": true
    
                }
    
            }
    
        },
    
        "mcp.serverTimeout": 30000,
    
        "mcp.enableLogging": true,
    
        "mcp.logLevel": "info"
    
    }
    
    

    Environment Configuration

    
    # .env file for development
    
    POSTGRES_HOST=localhost
    
    POSTGRES_PORT=5432
    
    POSTGRES_DB=retail_db
    
    POSTGRES_USER=mcp_user
    
    POSTGRES_PASSWORD=your_secure_password
    
    
    
    # Azure Configuration
    
    PROJECT_ENDPOINT=https://your-project.openai.azure.com
    
    AZURE_CLIENT_ID=your-client-id
    
    AZURE_CLIENT_SECRET=your-client-secret
    
    AZURE_TENANT_ID=your-tenant-id
    
    
    
    # Optional: Azure Key Vault
    
    AZURE_KEY_VAULT_URL=https://your-keyvault.vault.azure.net/
    
    
    
    # Server Configuration
    
    MCP_SERVER_PORT=8000
    
    MCP_SERVER_HOST=127.0.0.1
    
    LOG_LEVEL=INFO
    
    

    Workspace Configuration

    
    // .vscode/launch.json
    
    {
    
        "version": "0.2.0",
    
        "configurations": [
    
            {
    
                "name": "Debug MCP Server",
    
                "type": "python",
    
                "request": "launch",
    
                "module": "mcp_server.main",
    
                "console": "integratedTerminal",
    
                "envFile": "${workspaceFolder}/.env",
    
                "env": {
    
                    "MCP_SERVER_DEBUG": "true",
    
                    "LOG_LEVEL": "DEBUG"
    
                },
    
                "args": [],
    
                "justMyCode": false,
    
                "stopOnEntry": false
    
            },
    
            {
    
                "name": "Test MCP Server",
    
                "type": "python",
    
                "request": "launch",
    
                "module": "pytest",
    
                "console": "integratedTerminal",
    
                "envFile": "${workspaceFolder}/.env.test",
    
                "args": [
    
                    "tests/",
    
                    "-v",
    
                    "--tb=short"
    
                ]
    
            }
    
        ]
    
    }
    
    

    Task Configuration

    
    // .vscode/tasks.json
    
    {
    
        "version": "2.0.0",
    
        "tasks": [
    
            {
    
                "label": "Start MCP Server",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "-m", "mcp_server.main"
    
                ],
    
                "group": "build",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "new"
    
                },
    
                "options": {
    
                    "env": {
    
                        "PYTHONPATH": "${workspaceFolder}"
    
                    }
    
                },
    
                "isBackground": true,
    
                "problemMatcher": {
    
                    "pattern": {
    
                        "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
    
                        "file": 1,
    
                        "line": 2,
    
                        "column": 3,
    
                        "severity": 4,
    
                        "message": 5
    
                    },
    
                    "background": {
    
                        "activeOnStart": true,
    
                        "beginsPattern": "^.*Starting MCP server.*$",
    
                        "endsPattern": "^.*MCP server ready.*$"
    
                    }
    
                }
    
            },
    
            {
    
                "label": "Run Tests",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "-m", "pytest",
    
                    "tests/",
    
                    "-v"
    
                ],
    
                "group": "test",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "shared"
    
                }
    
            },
    
            {
    
                "label": "Generate Sample Data",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "scripts/generate_sample_data.py"
    
                ],
    
                "group": "build",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "shared"
    
                }
    
            },
    
            {
    
                "label": "Create Database Schema",
    
                "type": "shell",
    
                "command": "psql",
    
                "args": [
    
                    "-h", "${env:POSTGRES_HOST}",
    
                    "-p", "${env:POSTGRES_PORT}",
    
                    "-U", "${env:POSTGRES_USER}",
    
                    "-d", "${env:POSTGRES_DB}",
    
                    "-f", "scripts/create_schema.sql"
    
                ],
    
                "group": "build"
    
            }
    
        ]
    
    }
    
    

    ๐Ÿ’ฌ AI Chat Integration

    Natural Language Query Patterns

    
    // Example query patterns for VS Code Chat
    
    interface QueryPattern {
    
        intent: string;
    
        examples: string[];
    
        expectedTools: string[];
    
    }
    
    
    
    const retailQueryPatterns: QueryPattern[] = [
    
        {
    
            intent: "sales_analysis",
    
            examples: [
    
                "Show me daily sales for the last 30 days",
    
                "What are our top selling products this month?",
    
                "Which customers have spent the most this quarter?",
    
                "Compare sales performance between stores"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "product_search",
    
            examples: [
    
                "Find running shoes for women",
    
                "Show me electronics under $500",
    
                "What laptops do we have in stock?",
    
                "Search for wireless headphones"
    
            ],
    
            expectedTools: ["semantic_search_products", "hybrid_product_search"]
    
        },
    
        {
    
            intent: "inventory_management",
    
            examples: [
    
                "Which products are low on stock?",
    
                "Show me products that need reordering",
    
                "What's our current inventory value?",
    
                "Find products with zero stock"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "customer_analysis",
    
            examples: [
    
                "Show me customers who haven't purchased in 90 days",
    
                "What's the average customer lifetime value?",
    
                "Which customers are in the gold tier?",
    
                "Find customers with returns"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "business_intelligence",
    
            examples: [
    
                "Generate a business summary for this month",
    
                "Show me seasonal trends",
    
                "What are our best performing categories?",
    
                "Create a sales forecast"
    
            ],
    
            expectedTools: ["generate_business_insights"]
    
        },
    
        {
    
            intent: "recommendations",
    
            examples: [
    
                "Recommend products similar to product X",
    
                "What should we recommend to customer Y?",
    
                "Show me trending products",
    
                "Find cross-sell opportunities"
    
            ],
    
            expectedTools: ["get_product_recommendations"]
    
        }
    
    ];
    
    

    Chat Integration Examples

    
    <!-- Examples of VS Code Chat interactions -->
    
    
    
    ## Sales Analysis Queries
    
    
    
    **User**: Show me the top 10 selling products in the Seattle store for the last month
    
    
    
    **Expected Response**: 
    
    - Tool: execute_sales_query
    
    - Parameters: query_type="top_products", store_id="seattle", start_date="2025-08-29", end_date="2025-09-29", limit=10
    
    - Result: Formatted table with product names, quantities sold, revenue, and performance metrics
    
    
    
    **User**: What was our daily revenue trend last week?
    
    
    
    **Expected Response**:
    
    - Tool: execute_sales_query  
    
    - Parameters: query_type="daily_sales", store_id="seattle", start_date="2025-09-22", end_date="2025-09-29"
    
    - Result: Chart-ready data with daily revenue figures and growth percentages
    
    
    
    ## Product Search Queries
    
    
    
    **User**: Find comfortable running shoes for outdoor activities
    
    
    
    **Expected Response**:
    
    - Tool: semantic_search_products
    
    - Parameters: query="comfortable running shoes outdoor activities", store_id="seattle", similarity_threshold=0.7
    
    - Result: Ranked list of relevant products with similarity scores and detailed information
    
    
    
    **User**: Search for laptops under $1500 with good reviews
    
    
    
    **Expected Response**:
    
    - Tool: hybrid_product_search
    
    - Parameters: query="laptops under $1500 good reviews", store_id="seattle", semantic_weight=0.6, keyword_weight=0.4
    
    - Result: Combined keyword and semantic search results with price and rating filters
    
    
    
    ## Business Intelligence Queries
    
    
    
    **User**: Generate a comprehensive business summary for September
    
    
    
    **Expected Response**:
    
    - Tool: generate_business_insights
    
    - Parameters: analysis_type="summary", store_id="seattle", days=30
    
    - Result: KPI dashboard with revenue, customer metrics, top categories, and growth trends
    
    

    Chat Response Formatting

    
    # mcp_server/chat/response_formatter.py
    
    """
    
    Format MCP tool responses for optimal VS Code Chat display.
    
    """
    
    from typing import Dict, Any, List
    
    import json
    
    from datetime import datetime
    
    
    
    class ChatResponseFormatter:
    
        """Format tool responses for VS Code Chat consumption."""
    
        
    
        @staticmethod
    
        def format_sales_data(data: List[Dict[str, Any]], query_type: str) -> str:
    
            """Format sales data for chat display."""
    
            
    
            if not data:
    
                return "No sales data found for the specified criteria."
    
            
    
            if query_type == "daily_sales":
    
                return ChatResponseFormatter._format_daily_sales(data)
    
            elif query_type == "top_products":
    
                return ChatResponseFormatter._format_top_products(data)
    
            elif query_type == "customer_analysis":
    
                return ChatResponseFormatter._format_customer_analysis(data)
    
            else:
    
                return ChatResponseFormatter._format_generic_table(data)
    
        
    
        @staticmethod
    
        def _format_daily_sales(data: List[Dict[str, Any]]) -> str:
    
            """Format daily sales data."""
    
            
    
            response = "## Daily Sales Summary\n\n"
    
            response += "| Date | Revenue | Transactions | Avg Order Value | Customers |\n"
    
            response += "|------|---------|-------------|----------------|----------|\n"
    
            
    
            total_revenue = 0
    
            total_transactions = 0
    
            
    
            for day in data:
    
                revenue = float(day.get('total_revenue', 0))
    
                transactions = int(day.get('transaction_count', 0))
    
                avg_value = float(day.get('avg_transaction_value', 0))
    
                customers = int(day.get('unique_customers', 0))
    
                
    
                total_revenue += revenue
    
                total_transactions += transactions
    
                
    
                response += f"| {day.get('sales_date', 'N/A')} | "
    
                response += f"${revenue:,.2f} | "
    
                response += f"{transactions:,} | "
    
                response += f"${avg_value:.2f} | "
    
                response += f"{customers:,} |\n"
    
            
    
            response += f"\n**Totals**: ${total_revenue:,.2f} revenue, {total_transactions:,} transactions"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def _format_top_products(data: List[Dict[str, Any]]) -> str:
    
            """Format top products data."""
    
            
    
            response = "## Top Selling Products\n\n"
    
            response += "| Rank | Product | Brand | Revenue | Qty Sold | Avg Price |\n"
    
            response += "|------|---------|-------|---------|----------|----------|\n"
    
            
    
            for i, product in enumerate(data, 1):
    
                response += f"| {i} | "
    
                response += f"{product.get('product_name', 'N/A')} | "
    
                response += f"{product.get('brand', 'N/A')} | "
    
                response += f"${float(product.get('total_revenue', 0)):,.2f} | "
    
                response += f"{int(product.get('total_quantity_sold', 0)):,} | "
    
                response += f"${float(product.get('avg_price', 0)):.2f} |\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_search_results(data: List[Dict[str, Any]], search_type: str) -> str:
    
            """Format product search results."""
    
            
    
            if not data:
    
                return "No products found matching your search criteria."
    
            
    
            response = f"## Product Search Results ({search_type})\n\n"
    
            
    
            for i, product in enumerate(data, 1):
    
                response += f"### {i}. {product.get('product_name', 'Unknown Product')}\n"
    
                response += f"**Brand**: {product.get('brand', 'N/A')}\n"
    
                response += f"**Price**: ${float(product.get('price', 0)):.2f}\n"
    
                response += f"**Stock**: {int(product.get('current_stock', 0))} units\n"
    
                
    
                if 'similarity_score' in product:
    
                    score = float(product['similarity_score'])
    
                    response += f"**Relevance**: {score:.1%}\n"
    
                
    
                if 'rating_average' in product and product['rating_average']:
    
                    rating = float(product['rating_average'])
    
                    count = int(product.get('rating_count', 0))
    
                    response += f"**Rating**: {rating:.1f}/5.0 ({count:,} reviews)\n"
    
                
    
                if product.get('product_description'):
    
                    desc = product['product_description']
    
                    if len(desc) > 150:
    
                        desc = desc[:150] + "..."
    
                    response += f"**Description**: {desc}\n"
    
                
    
                response += "\n---\n\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_business_insights(data: Dict[str, Any]) -> str:
    
            """Format business intelligence data."""
    
            
    
            response = "## Business Intelligence Summary\n\n"
    
            
    
            # Key metrics
    
            response += "### Key Performance Indicators\n\n"
    
            response += f"- **Total Revenue**: ${float(data.get('total_revenue', 0)):,.2f}\n"
    
            response += f"- **Total Transactions**: {int(data.get('total_transactions', 0)):,}\n"
    
            response += f"- **Unique Customers**: {int(data.get('unique_customers', 0)):,}\n"
    
            response += f"- **Average Order Value**: ${float(data.get('avg_transaction_value', 0)):.2f}\n"
    
            response += f"- **Products Sold**: {int(data.get('products_sold', 0)):,} items\n\n"
    
            
    
            # Performance indicators
    
            if 'insights' in data and 'performance_indicators' in data['insights']:
    
                pi = data['insights']['performance_indicators']
    
                response += "### Performance Indicators\n\n"
    
                response += f"- **Transactions per Day**: {float(pi.get('transactions_per_day', 0)):.1f}\n"
    
                response += f"- **Revenue per Customer**: ${float(pi.get('revenue_per_customer', 0)):,.2f}\n"
    
                response += f"- **Items per Transaction**: {float(pi.get('items_per_transaction', 0)):.1f}\n\n"
    
            
    
            # Top category
    
            if data.get('top_category'):
    
                response += f"### Top Performing Category\n\n"
    
                response += f"**{data['top_category']}** - ${float(data.get('top_category_revenue', 0)):,.2f} revenue\n\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_error_response(error: str, tool_name: str) -> str:
    
            """Format error responses for chat."""
    
            
    
            response = f"## โŒ Error in {tool_name}\n\n"
    
            response += f"I encountered an issue while processing your request:\n\n"
    
            response += f"**Error**: {error}\n\n"
    
            response += "Please try:\n"
    
            response += "- Checking your query parameters\n"
    
            response += "- Verifying store access permissions\n"
    
            response += "- Simplifying your request\n"
    
            response += "- Contacting support if the issue persists\n"
    
            
    
            return response
    
    

    ๐Ÿ” Debugging and Troubleshooting

    VS Code Debug Configuration

    
    # mcp_server/debug/vscode_debug.py
    
    """
    
    VS Code specific debugging utilities for MCP server.
    
    """
    
    import logging
    
    import json
    
    from typing import Dict, Any
    
    from datetime import datetime
    
    
    
    class VSCodeDebugLogger:
    
        """Enhanced logging for VS Code debugging."""
    
        
    
        def __init__(self):
    
            self.logger = logging.getLogger("mcp_vscode_debug")
    
            self.setup_vscode_logging()
    
        
    
        def setup_vscode_logging(self):
    
            """Configure logging for VS Code debugging."""
    
            
    
            # Create VS Code specific formatter
    
            formatter = logging.Formatter(
    
                '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s'
    
            )
    
            
    
            # Console handler for VS Code terminal
    
            console_handler = logging.StreamHandler()
    
            console_handler.setFormatter(formatter)
    
            console_handler.setLevel(logging.DEBUG)
    
            
    
            self.logger.addHandler(console_handler)
    
            self.logger.setLevel(logging.DEBUG)
    
        
    
        def log_mcp_request(self, method: str, params: Dict[str, Any]):
    
            """Log MCP requests for debugging."""
    
            
    
            self.logger.info(f"MCP Request: {method}")
    
            self.logger.debug(f"Parameters: {json.dumps(params, indent=2)}")
    
        
    
        def log_tool_execution(self, tool_name: str, result: Dict[str, Any]):
    
            """Log tool execution results."""
    
            
    
            success = result.get('success', False)
    
            level = logging.INFO if success else logging.ERROR
    
            
    
            self.logger.log(level, f"Tool '{tool_name}' - {'Success' if success else 'Failed'}")
    
            
    
            if not success and result.get('error'):
    
                self.logger.error(f"Error: {result['error']}")
    
            
    
            if result.get('data'):
    
                data_summary = self._summarize_data(result['data'])
    
                self.logger.debug(f"Result summary: {data_summary}")
    
        
    
        def _summarize_data(self, data: Any) -> str:
    
            """Create a summary of result data."""
    
            
    
            if isinstance(data, list):
    
                return f"List with {len(data)} items"
    
            elif isinstance(data, dict):
    
                return f"Dict with keys: {list(data.keys())}"
    
            else:
    
                return f"Data type: {type(data).__name__}"
    
    
    
    # Global debug logger
    
    vscode_debug_logger = VSCodeDebugLogger()
    
    

    Connection Troubleshooting

    
    # scripts/debug_mcp_connection.py
    
    """
    
    Debug script for troubleshooting MCP server connections in VS Code.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import os
    
    import sys
    
    from typing import Dict, Any
    
    
    
    async def test_database_connection() -> Dict[str, Any]:
    
        """Test database connectivity."""
    
        
    
        try:
    
            # Get connection parameters from environment
    
            connection_params = {
    
                'host': os.getenv('POSTGRES_HOST', 'localhost'),
    
                'port': int(os.getenv('POSTGRES_PORT', '5432')),
    
                'database': os.getenv('POSTGRES_DB', 'retail_db'),
    
                'user': os.getenv('POSTGRES_USER', 'mcp_user'),
    
                'password': os.getenv('POSTGRES_PASSWORD', '')
    
            }
    
            
    
            print(f"Testing connection to {connection_params['host']}:{connection_params['port']}")
    
            
    
            # Test connection
    
            conn = await asyncpg.connect(**connection_params)
    
            
    
            # Test basic query
    
            result = await conn.fetchval("SELECT version()")
    
            
    
            # Test schema access
    
            tables = await conn.fetch("""
    
                SELECT table_name FROM information_schema.tables 
    
                WHERE table_schema = 'retail'
    
            """)
    
            
    
            await conn.close()
    
            
    
            return {
    
                'success': True,
    
                'database_version': result,
    
                'retail_tables': len(tables),
    
                'table_names': [table['table_name'] for table in tables]
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e),
    
                'connection_params': {k: v for k, v in connection_params.items() if k != 'password'}
    
            }
    
    
    
    async def test_azure_openai_connection() -> Dict[str, Any]:
    
        """Test Azure OpenAI connectivity."""
    
        
    
        try:
    
            from azure.identity import DefaultAzureCredential
    
            from azure.ai.projects import AIProjectClient
    
            
    
            project_endpoint = os.getenv('PROJECT_ENDPOINT')
    
            if not project_endpoint:
    
                return {
    
                    'success': False,
    
                    'error': 'PROJECT_ENDPOINT not configured'
    
                }
    
            
    
            print(f"Testing Azure OpenAI connection to {project_endpoint}")
    
            
    
            credential = DefaultAzureCredential()
    
            client = AIProjectClient(
    
                endpoint=project_endpoint,
    
                credential=credential
    
            )
    
            
    
            # Test embedding generation
    
            response = await client.embeddings.create(
    
                model="text-embedding-3-small",
    
                input="test connection"
    
            )
    
            
    
            embedding = response.data[0].embedding
    
            
    
            return {
    
                'success': True,
    
                'project_endpoint': project_endpoint,
    
                'embedding_dimension': len(embedding),
    
                'model': 'text-embedding-3-small'
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e),
    
                'project_endpoint': os.getenv('PROJECT_ENDPOINT', 'Not configured')
    
            }
    
    
    
    async def test_mcp_tools() -> Dict[str, Any]:
    
        """Test MCP tool availability."""
    
        
    
        try:
    
            # Import MCP server components
    
            sys.path.append(os.path.dirname(os.path.dirname(__file__)))
    
            
    
            from mcp_server.server import MCPServer
    
            from mcp_server.database import DatabaseProvider
    
            from mcp_server.config import Config
    
            
    
            # Create test configuration
    
            config = Config()
    
            db_provider = DatabaseProvider(config.database.connection_string)
    
            
    
            # Initialize server
    
            server = MCPServer(config, db_provider)
    
            await server.initialize()
    
            
    
            # Get available tools
    
            tools = server.get_available_tools()
    
            
    
            # Test a simple tool
    
            test_result = await server.execute_tool(
    
                'get_current_utc_date',
    
                {'format': 'iso'}
    
            )
    
            
    
            await server.cleanup()
    
            
    
            return {
    
                'success': True,
    
                'available_tools': [tool.name for tool in tools],
    
                'tool_count': len(tools),
    
                'test_tool_result': test_result.get('success', False)
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e)
    
            }
    
    
    
    async def main():
    
        """Run comprehensive connection tests."""
    
        
    
        print("๐Ÿ” MCP Server Connection Diagnostics")
    
        print("=" * 50)
    
        
    
        # Test database connection
    
        print("\n๐Ÿ“Š Testing Database Connection...")
    
        db_result = await test_database_connection()
    
        
    
        if db_result['success']:
    
            print("โœ… Database connection successful")
    
            print(f"   Database version: {db_result['database_version']}")
    
            print(f"   Retail tables found: {db_result['retail_tables']}")
    
            print(f"   Table names: {', '.join(db_result['table_names'])}")
    
        else:
    
            print("โŒ Database connection failed")
    
            print(f"   Error: {db_result['error']}")
    
        
    
        # Test Azure OpenAI connection
    
        print("\n๐Ÿค– Testing Azure OpenAI Connection...")
    
        azure_result = await test_azure_openai_connection()
    
        
    
        if azure_result['success']:
    
            print("โœ… Azure OpenAI connection successful")
    
            print(f"   Endpoint: {azure_result['project_endpoint']}")
    
            print(f"   Embedding dimension: {azure_result['embedding_dimension']}")
    
        else:
    
            print("โŒ Azure OpenAI connection failed")
    
            print(f"   Error: {azure_result['error']}")
    
        
    
        # Test MCP tools
    
        print("\n๐Ÿ› ๏ธ  Testing MCP Tools...")
    
        tools_result = await test_mcp_tools()
    
        
    
        if tools_result['success']:
    
            print("โœ… MCP tools loaded successfully")
    
            print(f"   Available tools: {tools_result['tool_count']}")
    
            print(f"   Tool names: {', '.join(tools_result['available_tools'])}")
    
            print(f"   Test execution: {'โœ…' if tools_result['test_tool_result'] else 'โŒ'}")
    
        else:
    
            print("โŒ MCP tools loading failed")
    
            print(f"   Error: {tools_result['error']}")
    
        
    
        # Overall status
    
        print("\n๐Ÿ“‹ Overall Status")
    
        print("=" * 50)
    
        
    
        all_success = all([
    
            db_result['success'],
    
            azure_result['success'],
    
            tools_result['success']
    
        ])
    
        
    
        if all_success:
    
            print("๐ŸŽ‰ All systems ready! MCP server should work correctly in VS Code.")
    
        else:
    
            print("โš ๏ธ  Some issues detected. Please resolve the errors above.")
    
            print("\n๐Ÿ’ก Troubleshooting tips:")
    
            print("   - Check environment variables in .env file")
    
            print("   - Verify database is running and accessible")
    
            print("   - Confirm Azure credentials are configured")
    
            print("   - Review VS Code MCP server configuration")
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    

    ๐Ÿš€ Advanced Configuration

    Multi-Server Setup

    
    // .vscode/settings.json - Multiple MCP servers
    
    {
    
        "mcp.servers": {
    
            "retail-seattle": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "DEFAULT_STORE_ID": "seattle"
    
                },
    
                "initializationOptions": {
    
                    "store_id": "seattle",
    
                    "server_name": "Seattle Store"
    
                }
    
            },
    
            "retail-redmond": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "DEFAULT_STORE_ID": "redmond"
    
                },
    
                "initializationOptions": {
    
                    "store_id": "redmond",
    
                    "server_name": "Redmond Store"
    
                }
    
            },
    
            "retail-analytics": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.analytics_main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "analytics_user",
    
                    "POSTGRES_PASSWORD": "${env:ANALYTICS_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}"
    
                },
    
                "initializationOptions": {
    
                    "mode": "analytics",
    
                    "cross_store_access": true
    
                }
    
            }
    
        }
    
    }
    
    

    Custom VS Code Extension

    
    // src/extension.ts - Custom MCP retail extension
    
    import * as vscode from 'vscode';
    
    
    
    export function activate(context: vscode.ExtensionContext) {
    
        
    
        // Register MCP retail commands
    
        const disposable = vscode.commands.registerCommand(
    
            'mcp-retail.quickQuery', 
    
            async () => {
    
                const quickPick = vscode.window.createQuickPick();
    
                quickPick.items = [
    
                    {
    
                        label: '๐Ÿ“Š Daily Sales',
    
                        description: 'Show daily sales for the last 30 days'
    
                    },
    
                    {
    
                        label: '๐Ÿ† Top Products',
    
                        description: 'Show top selling products this month'
    
                    },
    
                    {
    
                        label: '๐Ÿ‘ฅ Customer Analysis',
    
                        description: 'Analyze customer behavior and trends'
    
                    },
    
                    {
    
                        label: '๐Ÿ” Product Search',
    
                        description: 'Search for products using natural language'
    
                    },
    
                    {
    
                        label: '๐Ÿ“ˆ Business Insights',
    
                        description: 'Generate comprehensive business summary'
    
                    }
    
                ];
    
                
    
                quickPick.onDidChangeSelection(selection => {
    
                    if (selection[0]) {
    
                        executeQuickQuery(selection[0].label);
    
                    }
    
                });
    
                
    
                quickPick.onDidHide(() => quickPick.dispose());
    
                quickPick.show();
    
            }
    
        );
    
        
    
        context.subscriptions.push(disposable);
    
        
    
        // Register store switcher
    
        const storeSwitcher = vscode.commands.registerCommand(
    
            'mcp-retail.switchStore',
    
            async () => {
    
                const stores = ['seattle', 'redmond', 'bellevue', 'online'];
    
                const selected = await vscode.window.showQuickPick(stores, {
    
                    placeHolder: 'Select store for queries'
    
                });
    
                
    
                if (selected) {
    
                    // Update configuration
    
                    const config = vscode.workspace.getConfiguration('mcp');
    
                    await config.update('defaultStore', selected, true);
    
                    
    
                    vscode.window.showInformationMessage(
    
                        `Switched to ${selected.charAt(0).toUpperCase() + selected.slice(1)} store`
    
                    );
    
                }
    
            }
    
        );
    
        
    
        context.subscriptions.push(storeSwitcher);
    
    }
    
    
    
    async function executeQuickQuery(queryType: string) {
    
        // Execute predefined queries in VS Code Chat
    
        const chatCommands = {
    
            '๐Ÿ“Š Daily Sales': '@retail Show me daily sales for the last 30 days',
    
            '๐Ÿ† Top Products': '@retail What are the top 10 selling products this month?',
    
            '๐Ÿ‘ฅ Customer Analysis': '@retail Show me customer analysis for active customers',
    
            '๐Ÿ” Product Search': '@retail Find products matching "laptop computer"',
    
            '๐Ÿ“ˆ Business Insights': '@retail Generate a business summary for this month'
    
        };
    
        
    
        const command = chatCommands[queryType];
    
        if (command) {
    
            await vscode.commands.executeCommand('workbench.action.chat.open');
    
            await vscode.commands.executeCommand('workbench.action.chat.insert', command);
    
        }
    
    }
    
    
    
    export function deactivate() {}
    
    

    Extension Package Configuration

    
    // package.json for VS Code extension
    
    {
    
        "name": "mcp-retail-assistant",
    
        "displayName": "MCP Retail Assistant",
    
        "description": "AI-powered retail data analysis through MCP",
    
        "version": "1.0.0",
    
        "engines": {
    
            "vscode": "^1.74.0"
    
        },
    
        "categories": [
    
            "Other",
    
            "Data Science",
    
            "Machine Learning"
    
        ],
    
        "activationEvents": [
    
            "onCommand:mcp-retail.quickQuery",
    
            "onCommand:mcp-retail.switchStore"
    
        ],
    
        "main": "./out/extension.js",
    
        "contributes": {
    
            "commands": [
    
                {
    
                    "command": "mcp-retail.quickQuery",
    
                    "title": "Quick Retail Query",
    
                    "category": "MCP Retail"
    
                },
    
                {
    
                    "command": "mcp-retail.switchStore",
    
                    "title": "Switch Store",
    
                    "category": "MCP Retail"
    
                }
    
            ],
    
            "keybindings": [
    
                {
    
                    "command": "mcp-retail.quickQuery",
    
                    "key": "ctrl+shift+r",
    
                    "mac": "cmd+shift+r"
    
                }
    
            ],
    
            "configuration": {
    
                "title": "MCP Retail",
    
                "properties": {
    
                    "mcp-retail.defaultStore": {
    
                        "type": "string",
    
                        "default": "seattle",
    
                        "enum": ["seattle", "redmond", "bellevue", "online"],
    
                        "description": "Default store for retail queries"
    
                    },
    
                    "mcp-retail.enableAnalytics": {
    
                        "type": "boolean",
    
                        "default": true,
    
                        "description": "Enable advanced analytics features"
    
                    }
    
                }
    
            }
    
        },
    
        "scripts": {
    
            "vscode:prepublish": "npm run compile",
    
            "compile": "tsc -p ./",
    
            "watch": "tsc -watch -p ./"
    
        },
    
        "devDependencies": {
    
            "@types/vscode": "^1.74.0",
    
            "@types/node": "16.x",
    
            "typescript": "^4.9.4"
    
        }
    
    }
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… VS Code MCP Configuration: Complete setup for optimal MCP integration

    โœ… AI Chat Integration: Natural language querying capabilities in VS Code

    โœ… Debugging Tools: Comprehensive troubleshooting and connection diagnostics

    โœ… Multi-Server Setup: Configuration for multiple MCP server instances

    โœ… Custom Extensions: Enhanced VS Code experience with retail-specific features

    โœ… Production Readiness: Enterprise-ready VS Code development environment

    ๐Ÿš€ What's Next

    Continue with Lab 10: Deployment Strategies to:

  • Deploy MCP servers to production environments
  • Configure cloud infrastructure for scalability
  • Implement CI/CD pipelines for automated deployment
  • Monitor production MCP server performance
  • ๐Ÿ“š Additional Resources

    VS Code Development

  • VS Code Extension API - Official extension development guide
  • VS Code MCP Documentation - MCP integration documentation
  • TypeScript for VS Code - TypeScript development in VS Code
  • MCP Protocol

  • Model Context Protocol Specification - Official MCP specification
  • MCP Best Practices - Implementation best practices
  • FastMCP Framework - Python MCP implementation
  • Development Tools

  • Python in VS Code - Python development setup
  • Debugging in VS Code - Advanced debugging techniques
  • VS Code Tasks - Task automation and configuration
  • ---

    Previous: Lab 08: Testing and Debugging

    Next: Lab 10: Deployment Strategies

    Lab 10-12: Production and Best Practices 10 Deployment Strategies

    Deployment Strategies

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on deploying your MCP retail server to production environments using modern containerization and cloud-native approaches.

    You'll learn to deploy scalable, secure, and monitored MCP servers that can handle enterprise workloads.

    Overview

    Production deployment of MCP servers requires careful consideration of containerization, orchestration, security, scalability, and monitoring.

    This lab covers deploying to Azure Container Apps with PostgreSQL Flexible Server, implementing CI/CD pipelines, and configuring auto-scaling for variable workloads.

    The deployment strategies range from simple single-container deployments for development to sophisticated multi-region, auto-scaling production environments with comprehensive monitoring and security features.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Containerize MCP servers using Docker with multi-stage builds
  • Deploy to Azure Container Apps with secure networking
  • Configure production-grade PostgreSQL with high availability
  • Implement CI/CD pipelines for automated deployment
  • Scale applications automatically based on demand
  • Monitor production deployments with comprehensive observability
  • ๐Ÿณ Docker Containerization

    Multi-Stage Dockerfile

    
    # Dockerfile - Production-ready multi-stage build
    
    FROM python:3.11-slim AS builder
    
    
    
    # Set build environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PIP_NO_CACHE_DIR=1 \
    
        PIP_DISABLE_PIP_VERSION_CHECK=1
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        build-essential \
    
        libpq-dev \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install dependencies
    
    COPY requirements.lock.txt /tmp/
    
    RUN pip install --no-cache-dir -r /tmp/requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim AS production
    
    
    
    # Set production environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PATH="/opt/venv/bin:$PATH" \
    
        PYTHONPATH="/app"
    
    
    
    # Install runtime dependencies
    
    RUN apt-get update && apt-get install -y \
    
        libpq5 \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/* \
    
        && groupadd -r mcp \
    
        && useradd -r -g mcp -d /app -s /bin/bash mcp
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    
    
    # Set working directory and copy application
    
    WORKDIR /app
    
    COPY --chown=mcp:mcp . .
    
    
    
    # Create necessary directories with proper permissions
    
    RUN mkdir -p /app/logs /app/data /tmp/mcp \
    
        && chown -R mcp:mcp /app /tmp/mcp \
    
        && chmod -R 755 /app
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    
        CMD python -m mcp_server.health_check || exit 1
    
    
    
    # Switch to non-root user
    
    USER mcp
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Default command
    
    CMD ["python", "-m", "mcp_server.main"]
    
    

    Docker Compose for Development

    
    # docker-compose.yml - Development environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        build:
    
          context: .
    
          dockerfile: Dockerfile
    
          target: production
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=postgres
    
          - POSTGRES_PORT=5432
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=mcp_user
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=development
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        volumes:
    
          - ./logs:/app/logs
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      postgres:
    
        image: pgvector/pgvector:pg16
    
        environment:
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=postgres
    
          - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - postgres_data:/var/lib/postgresql/data
    
          - ./docker-init:/docker-entrypoint-initdb.d
    
          - ./data:/backup
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD-SHELL", "pg_isready -U postgres -d retail_db"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      redis:
    
        image: redis:7-alpine
    
        ports:
    
          - "6379:6379"
    
        command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    
        volumes:
    
          - redis_data:/data
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
    
    
    volumes:
    
      postgres_data:
    
        driver: local
    
      redis_data:
    
        driver: local
    
    
    
    networks:
    
      mcp-network:
    
        driver: bridge
    
    

    Production Docker Compose

    
    # docker-compose.prod.yml - Production environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        image: ${CONTAINER_REGISTRY}/mcp-retail-server:${IMAGE_TAG}
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=${POSTGRES_HOST}
    
          - POSTGRES_PORT=${POSTGRES_PORT}
    
          - POSTGRES_DB=${POSTGRES_DB}
    
          - POSTGRES_USER=${POSTGRES_USER}
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=production
    
          - REDIS_URL=${REDIS_URL}
    
        deploy:
    
          replicas: 3
    
          resources:
    
            limits:
    
              cpus: '2.0'
    
              memory: 2G
    
            reservations:
    
              cpus: '0.5'
    
              memory: 512M
    
          restart_policy:
    
            condition: on-failure
    
            delay: 5s
    
            max_attempts: 3
    
          update_config:
    
            parallelism: 1
    
            delay: 10s
    
            failure_action: rollback
    
        networks:
    
          - mcp-network
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
    networks:
    
      mcp-network:
    
        external: true
    
    

    โ˜๏ธ Azure Container Apps Deployment

    Infrastructure as Code with Bicep

    
    // infra/container-apps.bicep - Azure Container Apps deployment
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('Environment name')
    
    param environmentName string
    
    
    
    @description('Container App name')
    
    param containerAppName string
    
    
    
    @description('Container registry details')
    
    param containerRegistry object
    
    
    
    @description('Database connection details')
    
    @secure()
    
    param databaseConnectionString string
    
    
    
    @description('Azure OpenAI configuration')
    
    param azureOpenAI object
    
    
    
    @description('Application Insights workspace ID')
    
    param workspaceId string
    
    
    
    // Container Apps Environment
    
    resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
    
      name: '${environmentName}-env'
    
      location: location
    
      properties: {
    
        appLogsConfiguration: {
    
          destination: 'log-analytics'
    
          logAnalyticsConfiguration: {
    
            customerId: workspaceId
    
          }
    
        }
    
        infrastructureResourceGroup: '${environmentName}-infra-rg'
    
      }
    
    }
    
    
    
    // Container App
    
    resource mcp_retail_server 'Microsoft.App/containerApps@2023-05-01' = {
    
      name: containerAppName
    
      location: location
    
      properties: {
    
        managedEnvironmentId: containerAppsEnvironment.id
    
        configuration: {
    
          activeRevisionsMode: 'Single'
    
          ingress: {
    
            external: false
    
            targetPort: 8000
    
            allowInsecure: false
    
            traffic: [
    
              {
    
                weight: 100
    
                latestRevision: true
    
              }
    
            ]
    
          }
    
          registries: [
    
            {
    
              server: containerRegistry.server
    
              identity: containerRegistry.identity
    
            }
    
          ]
    
          secrets: [
    
            {
    
              name: 'database-connection-string'
    
              value: databaseConnectionString
    
            }
    
            {
    
              name: 'azure-openai-key'
    
              value: azureOpenAI.apiKey
    
            }
    
          ]
    
        }
    
        template: {
    
          containers: [
    
            {
    
              name: 'mcp-retail-server'
    
              image: '${containerRegistry.server}/mcp-retail-server:latest'
    
              resources: {
    
                cpu: json('1.0')
    
                memory: '2Gi'
    
              }
    
              env: [
    
                {
    
                  name: 'POSTGRES_CONNECTION_STRING'
    
                  secretRef: 'database-connection-string'
    
                }
    
                {
    
                  name: 'PROJECT_ENDPOINT'
    
                  value: azureOpenAI.endpoint
    
                }
    
                {
    
                  name: 'AZURE_OPENAI_API_KEY'
    
                  secretRef: 'azure-openai-key'
    
                }
    
                {
    
                  name: 'LOG_LEVEL'
    
                  value: 'INFO'
    
                }
    
                {
    
                  name: 'ENVIRONMENT'
    
                  value: 'production'
    
                }
    
              ]
    
              probes: [
    
                {
    
                  type: 'Liveness'
    
                  httpGet: {
    
                    path: '/health'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 60
    
                  periodSeconds: 30
    
                  timeoutSeconds: 10
    
                  failureThreshold: 3
    
                }
    
                {
    
                  type: 'Readiness'
    
                  httpGet: {
    
                    path: '/ready'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 30
    
                  periodSeconds: 10
    
                  timeoutSeconds: 5
    
                  failureThreshold: 3
    
                }
    
              ]
    
            }
    
          ]
    
          scale: {
    
            minReplicas: 2
    
            maxReplicas: 20
    
            rules: [
    
              {
    
                name: 'http-scaling'
    
                http: {
    
                  metadata: {
    
                    concurrentRequests: '10'
    
                  }
    
                }
    
              }
    
              {
    
                name: 'cpu-scaling'
    
                custom: {
    
                  type: 'cpu'
    
                  metadata: {
    
                    type: 'Utilization'
    
                    value: '70'
    
                  }
    
                }
    
              }
    
            ]
    
          }
    
        }
    
      }
    
    }
    
    
    
    // Output the FQDN
    
    output containerAppFQDN string = mcp_retail_server.properties.configuration.ingress.fqdn
    
    output containerAppId string = mcp_retail_server.id
    
    

    PostgreSQL Flexible Server

    
    // infra/database.bicep - PostgreSQL Flexible Server
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('PostgreSQL server name')
    
    param serverName string
    
    
    
    @description('Database administrator login')
    
    param administratorLogin string
    
    
    
    @description('Database administrator password')
    
    @secure()
    
    param administratorPassword string
    
    
    
    @description('Virtual network subnet ID')
    
    param subnetId string
    
    
    
    @description('Private DNS zone ID')
    
    param privateDnsZoneId string
    
    
    
    // PostgreSQL Flexible Server
    
    resource postgresqlServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
    
      name: serverName
    
      location: location
    
      sku: {
    
        name: 'Standard_D4s_v3'
    
        tier: 'GeneralPurpose'
    
      }
    
      properties: {
    
        administratorLogin: administratorLogin
    
        administratorLoginPassword: administratorPassword
    
        version: '16'
    
        storage: {
    
          storageSizeGB: 128
    
          autoGrow: 'Enabled'
    
          type: 'PremiumSSD'
    
        }
    
        backup: {
    
          backupRetentionDays: 35
    
          geoRedundantBackup: 'Enabled'
    
        }
    
        highAvailability: {
    
          mode: 'ZoneRedundant'
    
        }
    
        network: {
    
          delegatedSubnetResourceId: subnetId
    
          privateDnsZoneArmResourceId: privateDnsZoneId
    
        }
    
        maintenanceWindow: {
    
          dayOfWeek: 0
    
          startHour: 2
    
          startMinute: 0
    
        }
    
      }
    
    }
    
    
    
    // Database
    
    resource retailDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'retail_db'
    
      properties: {
    
        charset: 'UTF8'
    
        collation: 'en_US.utf8'
    
      }
    
    }
    
    
    
    // PostgreSQL extensions
    
    resource pgvectorExtension 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'shared_preload_libraries'
    
      properties: {
    
        value: 'pg_stat_statements,pgaudit,vector'
    
        source: 'user-override'
    
      }
    
    }
    
    
    
    // Output connection details
    
    output serverFQDN string = postgresqlServer.properties.fullyQualifiedDomainName
    
    output serverId string = postgresqlServer.id
    
    output databaseName string = retailDatabase.name
    
    

    ๐Ÿš€ CI/CD Pipeline Configuration

    GitHub Actions Workflow

    
    # .github/workflows/deploy.yml - CI/CD pipeline
    
    name: Deploy MCP Retail Server
    
    
    
    on:
    
      push:
    
        branches: [main]
    
      pull_request:
    
        branches: [main]
    
      workflow_dispatch:
    
        inputs:
    
          environment:
    
            description: 'Deployment environment'
    
            required: true
    
            default: 'development'
    
            type: choice
    
            options:
    
              - development
    
              - staging
    
              - production
    
    
    
    env:
    
      CONTAINER_REGISTRY: mcpretailregistry.azurecr.io
    
      IMAGE_NAME: mcp-retail-server
    
      AZURE_RESOURCE_GROUP: mcp-retail-rg
    
    
    
    jobs:
    
      test:
    
        runs-on: ubuntu-latest
    
        services:
    
          postgres:
    
            image: pgvector/pgvector:pg16
    
            env:
    
              POSTGRES_PASSWORD: postgres
    
              POSTGRES_DB: retail_test
    
            options: >-
    
              --health-cmd pg_isready
    
              --health-interval 10s
    
              --health-timeout 5s
    
              --health-retries 5
    
            ports:
    
              - 5432:5432
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Set up Python
    
            uses: actions/setup-python@v4
    
            with:
    
              python-version: '3.11'
    
              cache: 'pip'
    
    
    
          - name: Install dependencies
    
            run: |
    
              python -m pip install --upgrade pip
    
              pip install -r requirements.lock.txt
    
              pip install pytest pytest-cov pytest-asyncio
    
    
    
          - name: Set up test database
    
            run: |
    
              PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
              python scripts/generate_sample_data.py --test
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
    
    
          - name: Run tests
    
            run: |
    
              pytest tests/ -v --cov=mcp_server --cov-report=xml --cov-report=html
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
              PROJECT_ENDPOINT: ${{ secrets.TEST_PROJECT_ENDPOINT }}
    
              AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }}
    
              AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }}
    
              AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    
    
    
          - name: Upload coverage reports
    
            uses: codecov/codecov-action@v3
    
            with:
    
              file: ./coverage.xml
    
              flags: unittests
    
    
    
      security-scan:
    
        runs-on: ubuntu-latest
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Run Trivy vulnerability scanner
    
            uses: aquasecurity/trivy-action@master
    
            with:
    
              scan-type: 'fs'
    
              scan-ref: '.'
    
              format: 'sarif'
    
              output: 'trivy-results.sarif'
    
    
    
          - name: Upload Trivy scan results
    
            uses: github/codeql-action/upload-sarif@v2
    
            with:
    
              sarif_file: 'trivy-results.sarif'
    
    
    
          - name: Run Bandit security linter
    
            run: |
    
              pip install bandit[toml]
    
              bandit -r mcp_server/ -f json -o bandit-report.json
    
    
    
      build:
    
        runs-on: ubuntu-latest
    
        needs: [test, security-scan]
    
        if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
    
        
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Build and push Docker image
    
            uses: azure/docker-login@v1
    
            with:
    
              login-server: ${{ env.CONTAINER_REGISTRY }}
    
              username: ${{ secrets.REGISTRY_USERNAME }}
    
              password: ${{ secrets.REGISTRY_PASSWORD }}
    
    
    
          - name: Build, tag, and push image
    
            run: |
    
              # Generate unique tag
    
              IMAGE_TAG="${GITHUB_SHA::8}-$(date +%s)"
    
              
    
              # Build image
    
              docker build \
    
                --target production \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:latest \
    
                .
    
              
    
              # Push images
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:latest
    
              
    
              # Save tag for deployment
    
              echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
    
    
    
          - name: Output image details
    
            run: |
    
              echo "Built and pushed image: $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
    
    
    
      deploy-staging:
    
        runs-on: ubuntu-latest
    
        needs: build
    
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
        environment: staging
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to staging
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy infrastructure
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.staging.json \
    
                  --parameters containerImageTag=$IMAGE_TAG
    
    
    
                # Update container app
    
                az containerapp update \
    
                  --name mcp-retail-server-staging \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --image $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
    
    
          - name: Run integration tests
    
            run: |
    
              # Wait for deployment to be ready
    
              sleep 60
    
              
    
              # Run integration tests against staging
    
              pytest tests/integration/ \
    
                --endpoint https://mcp-retail-server-staging.azurecontainerapps.io \
    
                --timeout 300
    
    
    
      deploy-production:
    
        runs-on: ubuntu-latest
    
        needs: [build, deploy-staging]
    
        if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
    
        environment: production
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to production
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy with blue-green strategy
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.prod.json \
    
                  --parameters containerImageTag=$IMAGE_TAG \
    
                  --parameters deploymentSlot=green
    
    
    
                # Health check
    
                az containerapp show \
    
                  --name mcp-retail-server-prod-green \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod
    
    
    
                # Switch traffic (blue-green deployment)
    
                az containerapp ingress traffic set \
    
                  --name mcp-retail-server-prod \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --revision-weight latest=100
    
    

    Azure DevOps Pipeline

    
    # azure-pipelines.yml - Azure DevOps pipeline
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - develop
    
      paths:
    
        exclude:
    
          - docs/*
    
          - README.md
    
    
    
    variables:
    
      containerRegistry: 'mcpretailregistry.azurecr.io'
    
      imageName: 'mcp-retail-server'
    
      imageTag: '$(Build.BuildId)'
    
      azureServiceConnection: 'azure-service-connection'
    
    
    
    stages:
    
      - stage: Build
    
        displayName: 'Build and Test'
    
        jobs:
    
          - job: Test
    
            displayName: 'Run Tests'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            
    
            services:
    
              postgres:
    
                image: pgvector/pgvector:pg16
    
                env:
    
                  POSTGRES_PASSWORD: postgres
    
                  POSTGRES_DB: retail_test
    
                ports:
    
                  5432:5432
    
    
    
            steps:
    
              - task: UsePythonVersion@0
    
                inputs:
    
                  versionSpec: '3.11'
    
                  displayName: 'Use Python 3.11'
    
    
    
              - script: |
    
                  python -m pip install --upgrade pip
    
                  pip install -r requirements.lock.txt
    
                  pip install pytest pytest-cov pytest-asyncio
    
                displayName: 'Install dependencies'
    
    
    
              - script: |
    
                  PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
                  python scripts/generate_sample_data.py --test
    
                displayName: 'Set up test database'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - script: |
    
                  pytest tests/ -v --cov=mcp_server --cov-report=xml --junitxml=test-results.xml
    
                displayName: 'Run tests'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - task: PublishTestResults@2
    
                condition: succeededOrFailed()
    
                inputs:
    
                  testResultsFiles: 'test-results.xml'
    
                  testRunTitle: 'Python Tests'
    
    
    
              - task: PublishCodeCoverageResults@1
    
                inputs:
    
                  codeCoverageTool: 'Cobertura'
    
                  summaryFileLocation: 'coverage.xml'
    
    
    
          - job: Build
    
            displayName: 'Build Docker Image'
    
            dependsOn: Test
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
    
    
            steps:
    
              - task: AzureCLI@2
    
                displayName: 'Build and push Docker image'
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  scriptType: 'bash'
    
                  scriptLocation: 'inlineScript'
    
                  inlineScript: |
    
                    # Login to container registry
    
                    az acr login --name $(containerRegistry)
    
                    
    
                    # Build and push image
    
                    docker build \
    
                      --target production \
    
                      --tag $(containerRegistry)/$(imageName):$(imageTag) \
    
                      --tag $(containerRegistry)/$(imageName):latest \
    
                      .
    
                    
    
                    docker push $(containerRegistry)/$(imageName):$(imageTag)
    
                    docker push $(containerRegistry)/$(imageName):latest
    
    
    
      - stage: Deploy_Staging
    
        displayName: 'Deploy to Staging'
    
        dependsOn: Build
    
        condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
        
    
        jobs:
    
          - deployment: DeployStaging
    
            displayName: 'Deploy to Staging Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'staging'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy infrastructure'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-staging-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.staging.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    
    
      - stage: Deploy_Production
    
        displayName: 'Deploy to Production'
    
        dependsOn: Deploy_Staging
    
        condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
    
        
    
        jobs:
    
          - deployment: DeployProduction
    
            displayName: 'Deploy to Production Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'production'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy to production'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-prod-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.prod.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    

    ๐Ÿ“Š Scaling and Performance

    Auto-scaling Configuration

    
    # k8s/hpa.yaml - Horizontal Pod Autoscaler for Kubernetes
    
    apiVersion: autoscaling/v2
    
    kind: HorizontalPodAutoscaler
    
    metadata:
    
      name: mcp-retail-server-hpa
    
      namespace: mcp-retail
    
    spec:
    
      scaleTargetRef:
    
        apiVersion: apps/v1
    
        kind: Deployment
    
        name: mcp-retail-server
    
      minReplicas: 3
    
      maxReplicas: 50
    
      metrics:
    
        - type: Resource
    
          resource:
    
            name: cpu
    
            target:
    
              type: Utilization
    
              averageUtilization: 70
    
        - type: Resource
    
          resource:
    
            name: memory
    
            target:
    
              type: Utilization
    
              averageUtilization: 80
    
        - type: Pods
    
          pods:
    
            metric:
    
              name: http_requests_per_second
    
            target:
    
              type: AverageValue
    
              averageValue: 100
    
      behavior:
    
        scaleDown:
    
          stabilizationWindowSeconds: 300
    
          policies:
    
            - type: Percent
    
              value: 50
    
              periodSeconds: 60
    
        scaleUp:
    
          stabilizationWindowSeconds: 60
    
          policies:
    
            - type: Percent
    
              value: 100
    
              periodSeconds: 30
    
            - type: Pods
    
              value: 5
    
              periodSeconds: 30
    
          selectPolicy: Max
    
    

    Performance Monitoring

    
    # mcp_server/monitoring/performance.py
    
    """
    
    Performance monitoring and metrics collection for production deployment.
    
    """
    
    import asyncio
    
    import time
    
    import psutil
    
    from typing import Dict, Any
    
    from dataclasses import dataclass
    
    from datetime import datetime, timedelta
    
    import logging
    
    
    
    @dataclass
    
    class PerformanceMetrics:
    
        """Performance metrics data structure."""
    
        timestamp: datetime
    
        cpu_percent: float
    
        memory_percent: float
    
        memory_used_mb: float
    
        active_connections: int
    
        request_rate: float
    
        avg_response_time: float
    
        error_rate: float
    
        database_connections: int
    
    
    
    class PerformanceMonitor:
    
        """Monitor and collect performance metrics."""
    
        
    
        def __init__(self, config):
    
            self.config = config
    
            self.logger = logging.getLogger(__name__)
    
            
    
            # Metrics collection
    
            self.metrics_history = []
    
            self.request_times = []
    
            self.error_count = 0
    
            self.request_count = 0
    
            
    
            # Database monitoring
    
            self.db_pool = None
    
            
    
        async def start_monitoring(self):
    
            """Start continuous performance monitoring."""
    
            
    
            self.logger.info("Starting performance monitoring")
    
            
    
            # Start metrics collection task
    
            asyncio.create_task(self._collect_metrics_loop())
    
            asyncio.create_task(self._cleanup_old_metrics())
    
            
    
        async def _collect_metrics_loop(self):
    
            """Continuously collect performance metrics."""
    
            
    
            while True:
    
                try:
    
                    metrics = await self._collect_current_metrics()
    
                    self.metrics_history.append(metrics)
    
                    
    
                    # Log critical metrics
    
                    if metrics.cpu_percent > 90:
    
                        self.logger.warning(f"High CPU usage: {metrics.cpu_percent:.1f}%")
    
                    
    
                    if metrics.memory_percent > 90:
    
                        self.logger.warning(f"High memory usage: {metrics.memory_percent:.1f}%")
    
                    
    
                    if metrics.error_rate > 0.05:  # 5% error rate
    
                        self.logger.warning(f"High error rate: {metrics.error_rate:.2%}")
    
                    
    
                    await asyncio.sleep(30)  # Collect every 30 seconds
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error collecting metrics: {e}")
    
                    await asyncio.sleep(60)
    
        
    
        async def _collect_current_metrics(self) -> PerformanceMetrics:
    
            """Collect current system metrics."""
    
            
    
            # System metrics
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            memory = psutil.virtual_memory()
    
            
    
            # Application metrics
    
            current_time = datetime.utcnow()
    
            recent_requests = [
    
                req_time for req_time in self.request_times
    
                if current_time - req_time < timedelta(minutes=1)
    
            ]
    
            
    
            request_rate = len(recent_requests) / 60.0  # requests per second
    
            
    
            # Calculate average response time
    
            avg_response_time = 0.0
    
            if hasattr(self, '_recent_response_times'):
    
                recent_response_times = [
    
                    rt for rt in self._recent_response_times
    
                    if current_time - rt['timestamp'] < timedelta(minutes=5)
    
                ]
    
                if recent_response_times:
    
                    avg_response_time = sum(rt['time'] for rt in recent_response_times) / len(recent_response_times)
    
            
    
            # Error rate calculation
    
            error_rate = 0.0
    
            if self.request_count > 0:
    
                error_rate = self.error_count / self.request_count
    
            
    
            # Database connections
    
            db_connections = 0
    
            if self.db_pool:
    
                db_connections = len(self.db_pool._holders)
    
            
    
            return PerformanceMetrics(
    
                timestamp=current_time,
    
                cpu_percent=cpu_percent,
    
                memory_percent=memory.percent,
    
                memory_used_mb=memory.used / (1024 * 1024),
    
                active_connections=0,  # To be implemented with connection tracking
    
                request_rate=request_rate,
    
                avg_response_time=avg_response_time,
    
                error_rate=error_rate,
    
                database_connections=db_connections
    
            )
    
        
    
        async def _cleanup_old_metrics(self):
    
            """Clean up old metrics to prevent memory leaks."""
    
            
    
            while True:
    
                try:
    
                    cutoff_time = datetime.utcnow() - timedelta(hours=24)
    
                    
    
                    # Clean up metrics history
    
                    self.metrics_history = [
    
                        m for m in self.metrics_history
    
                        if m.timestamp > cutoff_time
    
                    ]
    
                    
    
                    # Clean up request times
    
                    self.request_times = [
    
                        rt for rt in self.request_times
    
                        if rt > cutoff_time
    
                    ]
    
                    
    
                    # Reset counters periodically
    
                    if datetime.utcnow().minute == 0:  # Every hour
    
                        self.error_count = 0
    
                        self.request_count = 0
    
                    
    
                    await asyncio.sleep(3600)  # Run every hour
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error cleaning up metrics: {e}")
    
                    await asyncio.sleep(3600)
    
        
    
        def record_request(self, response_time: float, success: bool = True):
    
            """Record a request for metrics."""
    
            
    
            current_time = datetime.utcnow()
    
            self.request_times.append(current_time)
    
            self.request_count += 1
    
            
    
            if not success:
    
                self.error_count += 1
    
            
    
            # Record response time
    
            if not hasattr(self, '_recent_response_times'):
    
                self._recent_response_times = []
    
            
    
            self._recent_response_times.append({
    
                'timestamp': current_time,
    
                'time': response_time
    
            })
    
        
    
        def get_current_metrics(self) -> Dict[str, Any]:
    
            """Get current performance metrics."""
    
            
    
            if not self.metrics_history:
    
                return {}
    
            
    
            latest_metrics = self.metrics_history[-1]
    
            
    
            return {
    
                'timestamp': latest_metrics.timestamp.isoformat(),
    
                'system': {
    
                    'cpu_percent': latest_metrics.cpu_percent,
    
                    'memory_percent': latest_metrics.memory_percent,
    
                    'memory_used_mb': latest_metrics.memory_used_mb
    
                },
    
                'application': {
    
                    'active_connections': latest_metrics.active_connections,
    
                    'request_rate': latest_metrics.request_rate,
    
                    'avg_response_time': latest_metrics.avg_response_time,
    
                    'error_rate': latest_metrics.error_rate
    
                },
    
                'database': {
    
                    'connections': latest_metrics.database_connections
    
                }
    
            }
    
        
    
        def get_metrics_summary(self, hours: int = 24) -> Dict[str, Any]:
    
            """Get performance metrics summary for the specified hours."""
    
            
    
            cutoff_time = datetime.utcnow() - timedelta(hours=hours)
    
            recent_metrics = [
    
                m for m in self.metrics_history
    
                if m.timestamp > cutoff_time
    
            ]
    
            
    
            if not recent_metrics:
    
                return {}
    
            
    
            # Calculate averages
    
            avg_cpu = sum(m.cpu_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_memory = sum(m.memory_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_response_time = sum(m.avg_response_time for m in recent_metrics) / len(recent_metrics)
    
            
    
            # Calculate peaks
    
            max_cpu = max(m.cpu_percent for m in recent_metrics)
    
            max_memory = max(m.memory_percent for m in recent_metrics)
    
            max_response_time = max(m.avg_response_time for m in recent_metrics)
    
            
    
            return {
    
                'period_hours': hours,
    
                'averages': {
    
                    'cpu_percent': round(avg_cpu, 2),
    
                    'memory_percent': round(avg_memory, 2),
    
                    'response_time': round(avg_response_time, 3)
    
                },
    
                'peaks': {
    
                    'cpu_percent': round(max_cpu, 2),
    
                    'memory_percent': round(max_memory, 2),
    
                    'response_time': round(max_response_time, 3)
    
                },
    
                'data_points': len(recent_metrics)
    
            }
    
    

    ๐Ÿ” Production Security Configuration

    Security Hardening

    
    # k8s/security-policy.yaml - Kubernetes security policies
    
    apiVersion: v1
    
    kind: SecurityContext
    
    metadata:
    
      name: mcp-retail-security-context
    
    spec:
    
      runAsNonRoot: true
    
      runAsUser: 1000
    
      runAsGroup: 1000
    
      fsGroup: 1000
    
      seccompProfile:
    
        type: RuntimeDefault
    
      capabilities:
    
        drop:
    
          - ALL
    
      readOnlyRootFilesystem: true
    
      allowPrivilegeEscalation: false
    
    
    
    ---
    
    apiVersion: networking.k8s.io/v1
    
    kind: NetworkPolicy
    
    metadata:
    
      name: mcp-retail-network-policy
    
      namespace: mcp-retail
    
    spec:
    
      podSelector:
    
        matchLabels:
    
          app: mcp-retail-server
    
      policyTypes:
    
        - Ingress
    
        - Egress
    
      ingress:
    
        - from:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: ingress-nginx
    
          ports:
    
            - protocol: TCP
    
              port: 8000
    
      egress:
    
        - to:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: database
    
          ports:
    
            - protocol: TCP
    
              port: 5432
    
        - to: []
    
          ports:
    
            - protocol: TCP
    
              port: 443  # HTTPS for Azure OpenAI
    
            - protocol: TCP
    
              port: 53   # DNS
    
            - protocol: UDP
    
              port: 53   # DNS
    
    

    Environment Configuration

    
    # scripts/setup-production-env.sh
    
    #!/bin/bash
    
    
    
    # Production environment setup script
    
    set -euo pipefail
    
    
    
    echo "๐Ÿ”ง Setting up production environment..."
    
    
    
    # Create resource groups
    
    az group create --name "mcp-retail-prod-rg" --location "East US"
    
    az group create --name "mcp-retail-shared-rg" --location "East US"
    
    
    
    # Create Key Vault
    
    echo "๐Ÿ” Creating Azure Key Vault..."
    
    az keyvault create \
    
      --name "mcp-retail-kv-prod" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --location "East US" \
    
      --enable-rbac-authorization true
    
    
    
    # Set secrets
    
    echo "๐Ÿ”‘ Setting up secrets..."
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "postgres-password" \
    
      --value "${POSTGRES_PASSWORD}"
    
    
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "azure-openai-key" \
    
      --value "${AZURE_OPENAI_KEY}"
    
    
    
    # Create container registry
    
    echo "๐Ÿ“ฆ Creating container registry..."
    
    az acr create \
    
      --name "mcpretailregistry" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --sku Premium \
    
      --admin-enabled false
    
    
    
    # Create virtual network
    
    echo "๐ŸŒ Creating virtual network..."
    
    az network vnet create \
    
      --name "mcp-retail-vnet" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --address-prefix "10.0.0.0/16" \
    
      --subnet-name "container-apps" \
    
      --subnet-prefix "10.0.1.0/24"
    
    
    
    az network vnet subnet create \
    
      --name "database" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --vnet-name "mcp-retail-vnet" \
    
      --address-prefix "10.0.2.0/24" \
    
      --delegations Microsoft.DBforPostgreSQL/flexibleServers
    
    
    
    # Deploy infrastructure
    
    echo "๐Ÿ—๏ธ  Deploying infrastructure..."
    
    az deployment group create \
    
      --resource-group "mcp-retail-prod-rg" \
    
      --template-file "infra/main.bicep" \
    
      --parameters "infra/main.parameters.prod.json"
    
    
    
    echo "โœ… Production environment setup complete!"
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Container Strategy: Production-ready Docker containers with security hardening

    โœ… Cloud Deployment: Azure Container Apps with auto-scaling and monitoring

    โœ… Database Deployment: PostgreSQL Flexible Server with high availability

    โœ… CI/CD Pipelines: Automated testing, building, and deployment workflows

    โœ… Performance Monitoring: Comprehensive metrics collection and alerting

    โœ… Security Configuration: Production-grade security policies and network isolation

    ๐Ÿš€ What's Next

    Continue with Lab 11: Monitoring and Observability to:

  • Set up comprehensive monitoring with Application Insights
  • Configure structured logging and distributed tracing
  • Implement alerting and automated response systems
  • Monitor business metrics and performance KPIs
  • ๐Ÿ“š Additional Resources

    Container Technologies

  • Docker Best Practices - Official Docker best practices
  • Azure Container Apps - Azure Container Apps documentation
  • Kubernetes Documentation - Kubernetes official documentation
  • CI/CD and DevOps

  • GitHub Actions - GitHub Actions documentation
  • Azure DevOps - Azure DevOps services
  • Infrastructure as Code - Azure Bicep documentation
  • Security and Monitoring

  • Azure Security Center - Azure security recommendations
  • Container Security - Kubernetes security concepts
  • Application Insights - Azure Application Insights
  • ---

    Previous: Lab 09: VS Code Integration

    Next: Lab 11: Monitoring and Observability

    Docker deployment, Azure Container Apps, and scaling considerations Deploy

    Deployment Strategies

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance on deploying your MCP retail server to production environments using modern containerization and cloud-native approaches.

    You'll learn to deploy scalable, secure, and monitored MCP servers that can handle enterprise workloads.

    Overview

    Production deployment of MCP servers requires careful consideration of containerization, orchestration, security, scalability, and monitoring.

    This lab covers deploying to Azure Container Apps with PostgreSQL Flexible Server, implementing CI/CD pipelines, and configuring auto-scaling for variable workloads.

    The deployment strategies range from simple single-container deployments for development to sophisticated multi-region, auto-scaling production environments with comprehensive monitoring and security features.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Containerize MCP servers using Docker with multi-stage builds
  • Deploy to Azure Container Apps with secure networking
  • Configure production-grade PostgreSQL with high availability
  • Implement CI/CD pipelines for automated deployment
  • Scale applications automatically based on demand
  • Monitor production deployments with comprehensive observability
  • ๐Ÿณ Docker Containerization

    Multi-Stage Dockerfile

    
    # Dockerfile - Production-ready multi-stage build
    
    FROM python:3.11-slim AS builder
    
    
    
    # Set build environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PIP_NO_CACHE_DIR=1 \
    
        PIP_DISABLE_PIP_VERSION_CHECK=1
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        build-essential \
    
        libpq-dev \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install dependencies
    
    COPY requirements.lock.txt /tmp/
    
    RUN pip install --no-cache-dir -r /tmp/requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim AS production
    
    
    
    # Set production environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PATH="/opt/venv/bin:$PATH" \
    
        PYTHONPATH="/app"
    
    
    
    # Install runtime dependencies
    
    RUN apt-get update && apt-get install -y \
    
        libpq5 \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/* \
    
        && groupadd -r mcp \
    
        && useradd -r -g mcp -d /app -s /bin/bash mcp
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    
    
    # Set working directory and copy application
    
    WORKDIR /app
    
    COPY --chown=mcp:mcp . .
    
    
    
    # Create necessary directories with proper permissions
    
    RUN mkdir -p /app/logs /app/data /tmp/mcp \
    
        && chown -R mcp:mcp /app /tmp/mcp \
    
        && chmod -R 755 /app
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    
        CMD python -m mcp_server.health_check || exit 1
    
    
    
    # Switch to non-root user
    
    USER mcp
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Default command
    
    CMD ["python", "-m", "mcp_server.main"]
    
    

    Docker Compose for Development

    
    # docker-compose.yml - Development environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        build:
    
          context: .
    
          dockerfile: Dockerfile
    
          target: production
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=postgres
    
          - POSTGRES_PORT=5432
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=mcp_user
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=development
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        volumes:
    
          - ./logs:/app/logs
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      postgres:
    
        image: pgvector/pgvector:pg16
    
        environment:
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=postgres
    
          - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - postgres_data:/var/lib/postgresql/data
    
          - ./docker-init:/docker-entrypoint-initdb.d
    
          - ./data:/backup
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD-SHELL", "pg_isready -U postgres -d retail_db"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      redis:
    
        image: redis:7-alpine
    
        ports:
    
          - "6379:6379"
    
        command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    
        volumes:
    
          - redis_data:/data
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
    
    
    volumes:
    
      postgres_data:
    
        driver: local
    
      redis_data:
    
        driver: local
    
    
    
    networks:
    
      mcp-network:
    
        driver: bridge
    
    

    Production Docker Compose

    
    # docker-compose.prod.yml - Production environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        image: ${CONTAINER_REGISTRY}/mcp-retail-server:${IMAGE_TAG}
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=${POSTGRES_HOST}
    
          - POSTGRES_PORT=${POSTGRES_PORT}
    
          - POSTGRES_DB=${POSTGRES_DB}
    
          - POSTGRES_USER=${POSTGRES_USER}
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=production
    
          - REDIS_URL=${REDIS_URL}
    
        deploy:
    
          replicas: 3
    
          resources:
    
            limits:
    
              cpus: '2.0'
    
              memory: 2G
    
            reservations:
    
              cpus: '0.5'
    
              memory: 512M
    
          restart_policy:
    
            condition: on-failure
    
            delay: 5s
    
            max_attempts: 3
    
          update_config:
    
            parallelism: 1
    
            delay: 10s
    
            failure_action: rollback
    
        networks:
    
          - mcp-network
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
    networks:
    
      mcp-network:
    
        external: true
    
    

    โ˜๏ธ Azure Container Apps Deployment

    Infrastructure as Code with Bicep

    
    // infra/container-apps.bicep - Azure Container Apps deployment
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('Environment name')
    
    param environmentName string
    
    
    
    @description('Container App name')
    
    param containerAppName string
    
    
    
    @description('Container registry details')
    
    param containerRegistry object
    
    
    
    @description('Database connection details')
    
    @secure()
    
    param databaseConnectionString string
    
    
    
    @description('Azure OpenAI configuration')
    
    param azureOpenAI object
    
    
    
    @description('Application Insights workspace ID')
    
    param workspaceId string
    
    
    
    // Container Apps Environment
    
    resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
    
      name: '${environmentName}-env'
    
      location: location
    
      properties: {
    
        appLogsConfiguration: {
    
          destination: 'log-analytics'
    
          logAnalyticsConfiguration: {
    
            customerId: workspaceId
    
          }
    
        }
    
        infrastructureResourceGroup: '${environmentName}-infra-rg'
    
      }
    
    }
    
    
    
    // Container App
    
    resource mcp_retail_server 'Microsoft.App/containerApps@2023-05-01' = {
    
      name: containerAppName
    
      location: location
    
      properties: {
    
        managedEnvironmentId: containerAppsEnvironment.id
    
        configuration: {
    
          activeRevisionsMode: 'Single'
    
          ingress: {
    
            external: false
    
            targetPort: 8000
    
            allowInsecure: false
    
            traffic: [
    
              {
    
                weight: 100
    
                latestRevision: true
    
              }
    
            ]
    
          }
    
          registries: [
    
            {
    
              server: containerRegistry.server
    
              identity: containerRegistry.identity
    
            }
    
          ]
    
          secrets: [
    
            {
    
              name: 'database-connection-string'
    
              value: databaseConnectionString
    
            }
    
            {
    
              name: 'azure-openai-key'
    
              value: azureOpenAI.apiKey
    
            }
    
          ]
    
        }
    
        template: {
    
          containers: [
    
            {
    
              name: 'mcp-retail-server'
    
              image: '${containerRegistry.server}/mcp-retail-server:latest'
    
              resources: {
    
                cpu: json('1.0')
    
                memory: '2Gi'
    
              }
    
              env: [
    
                {
    
                  name: 'POSTGRES_CONNECTION_STRING'
    
                  secretRef: 'database-connection-string'
    
                }
    
                {
    
                  name: 'PROJECT_ENDPOINT'
    
                  value: azureOpenAI.endpoint
    
                }
    
                {
    
                  name: 'AZURE_OPENAI_API_KEY'
    
                  secretRef: 'azure-openai-key'
    
                }
    
                {
    
                  name: 'LOG_LEVEL'
    
                  value: 'INFO'
    
                }
    
                {
    
                  name: 'ENVIRONMENT'
    
                  value: 'production'
    
                }
    
              ]
    
              probes: [
    
                {
    
                  type: 'Liveness'
    
                  httpGet: {
    
                    path: '/health'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 60
    
                  periodSeconds: 30
    
                  timeoutSeconds: 10
    
                  failureThreshold: 3
    
                }
    
                {
    
                  type: 'Readiness'
    
                  httpGet: {
    
                    path: '/ready'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 30
    
                  periodSeconds: 10
    
                  timeoutSeconds: 5
    
                  failureThreshold: 3
    
                }
    
              ]
    
            }
    
          ]
    
          scale: {
    
            minReplicas: 2
    
            maxReplicas: 20
    
            rules: [
    
              {
    
                name: 'http-scaling'
    
                http: {
    
                  metadata: {
    
                    concurrentRequests: '10'
    
                  }
    
                }
    
              }
    
              {
    
                name: 'cpu-scaling'
    
                custom: {
    
                  type: 'cpu'
    
                  metadata: {
    
                    type: 'Utilization'
    
                    value: '70'
    
                  }
    
                }
    
              }
    
            ]
    
          }
    
        }
    
      }
    
    }
    
    
    
    // Output the FQDN
    
    output containerAppFQDN string = mcp_retail_server.properties.configuration.ingress.fqdn
    
    output containerAppId string = mcp_retail_server.id
    
    

    PostgreSQL Flexible Server

    
    // infra/database.bicep - PostgreSQL Flexible Server
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('PostgreSQL server name')
    
    param serverName string
    
    
    
    @description('Database administrator login')
    
    param administratorLogin string
    
    
    
    @description('Database administrator password')
    
    @secure()
    
    param administratorPassword string
    
    
    
    @description('Virtual network subnet ID')
    
    param subnetId string
    
    
    
    @description('Private DNS zone ID')
    
    param privateDnsZoneId string
    
    
    
    // PostgreSQL Flexible Server
    
    resource postgresqlServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
    
      name: serverName
    
      location: location
    
      sku: {
    
        name: 'Standard_D4s_v3'
    
        tier: 'GeneralPurpose'
    
      }
    
      properties: {
    
        administratorLogin: administratorLogin
    
        administratorLoginPassword: administratorPassword
    
        version: '16'
    
        storage: {
    
          storageSizeGB: 128
    
          autoGrow: 'Enabled'
    
          type: 'PremiumSSD'
    
        }
    
        backup: {
    
          backupRetentionDays: 35
    
          geoRedundantBackup: 'Enabled'
    
        }
    
        highAvailability: {
    
          mode: 'ZoneRedundant'
    
        }
    
        network: {
    
          delegatedSubnetResourceId: subnetId
    
          privateDnsZoneArmResourceId: privateDnsZoneId
    
        }
    
        maintenanceWindow: {
    
          dayOfWeek: 0
    
          startHour: 2
    
          startMinute: 0
    
        }
    
      }
    
    }
    
    
    
    // Database
    
    resource retailDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'retail_db'
    
      properties: {
    
        charset: 'UTF8'
    
        collation: 'en_US.utf8'
    
      }
    
    }
    
    
    
    // PostgreSQL extensions
    
    resource pgvectorExtension 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'shared_preload_libraries'
    
      properties: {
    
        value: 'pg_stat_statements,pgaudit,vector'
    
        source: 'user-override'
    
      }
    
    }
    
    
    
    // Output connection details
    
    output serverFQDN string = postgresqlServer.properties.fullyQualifiedDomainName
    
    output serverId string = postgresqlServer.id
    
    output databaseName string = retailDatabase.name
    
    

    ๐Ÿš€ CI/CD Pipeline Configuration

    GitHub Actions Workflow

    
    # .github/workflows/deploy.yml - CI/CD pipeline
    
    name: Deploy MCP Retail Server
    
    
    
    on:
    
      push:
    
        branches: [main]
    
      pull_request:
    
        branches: [main]
    
      workflow_dispatch:
    
        inputs:
    
          environment:
    
            description: 'Deployment environment'
    
            required: true
    
            default: 'development'
    
            type: choice
    
            options:
    
              - development
    
              - staging
    
              - production
    
    
    
    env:
    
      CONTAINER_REGISTRY: mcpretailregistry.azurecr.io
    
      IMAGE_NAME: mcp-retail-server
    
      AZURE_RESOURCE_GROUP: mcp-retail-rg
    
    
    
    jobs:
    
      test:
    
        runs-on: ubuntu-latest
    
        services:
    
          postgres:
    
            image: pgvector/pgvector:pg16
    
            env:
    
              POSTGRES_PASSWORD: postgres
    
              POSTGRES_DB: retail_test
    
            options: >-
    
              --health-cmd pg_isready
    
              --health-interval 10s
    
              --health-timeout 5s
    
              --health-retries 5
    
            ports:
    
              - 5432:5432
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Set up Python
    
            uses: actions/setup-python@v4
    
            with:
    
              python-version: '3.11'
    
              cache: 'pip'
    
    
    
          - name: Install dependencies
    
            run: |
    
              python -m pip install --upgrade pip
    
              pip install -r requirements.lock.txt
    
              pip install pytest pytest-cov pytest-asyncio
    
    
    
          - name: Set up test database
    
            run: |
    
              PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
              python scripts/generate_sample_data.py --test
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
    
    
          - name: Run tests
    
            run: |
    
              pytest tests/ -v --cov=mcp_server --cov-report=xml --cov-report=html
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
              PROJECT_ENDPOINT: ${{ secrets.TEST_PROJECT_ENDPOINT }}
    
              AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }}
    
              AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }}
    
              AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    
    
    
          - name: Upload coverage reports
    
            uses: codecov/codecov-action@v3
    
            with:
    
              file: ./coverage.xml
    
              flags: unittests
    
    
    
      security-scan:
    
        runs-on: ubuntu-latest
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Run Trivy vulnerability scanner
    
            uses: aquasecurity/trivy-action@master
    
            with:
    
              scan-type: 'fs'
    
              scan-ref: '.'
    
              format: 'sarif'
    
              output: 'trivy-results.sarif'
    
    
    
          - name: Upload Trivy scan results
    
            uses: github/codeql-action/upload-sarif@v2
    
            with:
    
              sarif_file: 'trivy-results.sarif'
    
    
    
          - name: Run Bandit security linter
    
            run: |
    
              pip install bandit[toml]
    
              bandit -r mcp_server/ -f json -o bandit-report.json
    
    
    
      build:
    
        runs-on: ubuntu-latest
    
        needs: [test, security-scan]
    
        if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
    
        
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Build and push Docker image
    
            uses: azure/docker-login@v1
    
            with:
    
              login-server: ${{ env.CONTAINER_REGISTRY }}
    
              username: ${{ secrets.REGISTRY_USERNAME }}
    
              password: ${{ secrets.REGISTRY_PASSWORD }}
    
    
    
          - name: Build, tag, and push image
    
            run: |
    
              # Generate unique tag
    
              IMAGE_TAG="${GITHUB_SHA::8}-$(date +%s)"
    
              
    
              # Build image
    
              docker build \
    
                --target production \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:latest \
    
                .
    
              
    
              # Push images
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:latest
    
              
    
              # Save tag for deployment
    
              echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
    
    
    
          - name: Output image details
    
            run: |
    
              echo "Built and pushed image: $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
    
    
    
      deploy-staging:
    
        runs-on: ubuntu-latest
    
        needs: build
    
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
        environment: staging
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to staging
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy infrastructure
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.staging.json \
    
                  --parameters containerImageTag=$IMAGE_TAG
    
    
    
                # Update container app
    
                az containerapp update \
    
                  --name mcp-retail-server-staging \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --image $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
    
    
          - name: Run integration tests
    
            run: |
    
              # Wait for deployment to be ready
    
              sleep 60
    
              
    
              # Run integration tests against staging
    
              pytest tests/integration/ \
    
                --endpoint https://mcp-retail-server-staging.azurecontainerapps.io \
    
                --timeout 300
    
    
    
      deploy-production:
    
        runs-on: ubuntu-latest
    
        needs: [build, deploy-staging]
    
        if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
    
        environment: production
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to production
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy with blue-green strategy
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.prod.json \
    
                  --parameters containerImageTag=$IMAGE_TAG \
    
                  --parameters deploymentSlot=green
    
    
    
                # Health check
    
                az containerapp show \
    
                  --name mcp-retail-server-prod-green \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod
    
    
    
                # Switch traffic (blue-green deployment)
    
                az containerapp ingress traffic set \
    
                  --name mcp-retail-server-prod \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --revision-weight latest=100
    
    

    Azure DevOps Pipeline

    
    # azure-pipelines.yml - Azure DevOps pipeline
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - develop
    
      paths:
    
        exclude:
    
          - docs/*
    
          - README.md
    
    
    
    variables:
    
      containerRegistry: 'mcpretailregistry.azurecr.io'
    
      imageName: 'mcp-retail-server'
    
      imageTag: '$(Build.BuildId)'
    
      azureServiceConnection: 'azure-service-connection'
    
    
    
    stages:
    
      - stage: Build
    
        displayName: 'Build and Test'
    
        jobs:
    
          - job: Test
    
            displayName: 'Run Tests'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            
    
            services:
    
              postgres:
    
                image: pgvector/pgvector:pg16
    
                env:
    
                  POSTGRES_PASSWORD: postgres
    
                  POSTGRES_DB: retail_test
    
                ports:
    
                  5432:5432
    
    
    
            steps:
    
              - task: UsePythonVersion@0
    
                inputs:
    
                  versionSpec: '3.11'
    
                  displayName: 'Use Python 3.11'
    
    
    
              - script: |
    
                  python -m pip install --upgrade pip
    
                  pip install -r requirements.lock.txt
    
                  pip install pytest pytest-cov pytest-asyncio
    
                displayName: 'Install dependencies'
    
    
    
              - script: |
    
                  PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
                  python scripts/generate_sample_data.py --test
    
                displayName: 'Set up test database'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - script: |
    
                  pytest tests/ -v --cov=mcp_server --cov-report=xml --junitxml=test-results.xml
    
                displayName: 'Run tests'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - task: PublishTestResults@2
    
                condition: succeededOrFailed()
    
                inputs:
    
                  testResultsFiles: 'test-results.xml'
    
                  testRunTitle: 'Python Tests'
    
    
    
              - task: PublishCodeCoverageResults@1
    
                inputs:
    
                  codeCoverageTool: 'Cobertura'
    
                  summaryFileLocation: 'coverage.xml'
    
    
    
          - job: Build
    
            displayName: 'Build Docker Image'
    
            dependsOn: Test
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
    
    
            steps:
    
              - task: AzureCLI@2
    
                displayName: 'Build and push Docker image'
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  scriptType: 'bash'
    
                  scriptLocation: 'inlineScript'
    
                  inlineScript: |
    
                    # Login to container registry
    
                    az acr login --name $(containerRegistry)
    
                    
    
                    # Build and push image
    
                    docker build \
    
                      --target production \
    
                      --tag $(containerRegistry)/$(imageName):$(imageTag) \
    
                      --tag $(containerRegistry)/$(imageName):latest \
    
                      .
    
                    
    
                    docker push $(containerRegistry)/$(imageName):$(imageTag)
    
                    docker push $(containerRegistry)/$(imageName):latest
    
    
    
      - stage: Deploy_Staging
    
        displayName: 'Deploy to Staging'
    
        dependsOn: Build
    
        condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
        
    
        jobs:
    
          - deployment: DeployStaging
    
            displayName: 'Deploy to Staging Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'staging'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy infrastructure'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-staging-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.staging.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    
    
      - stage: Deploy_Production
    
        displayName: 'Deploy to Production'
    
        dependsOn: Deploy_Staging
    
        condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
    
        
    
        jobs:
    
          - deployment: DeployProduction
    
            displayName: 'Deploy to Production Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'production'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy to production'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-prod-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.prod.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    

    ๐Ÿ“Š Scaling and Performance

    Auto-scaling Configuration

    
    # k8s/hpa.yaml - Horizontal Pod Autoscaler for Kubernetes
    
    apiVersion: autoscaling/v2
    
    kind: HorizontalPodAutoscaler
    
    metadata:
    
      name: mcp-retail-server-hpa
    
      namespace: mcp-retail
    
    spec:
    
      scaleTargetRef:
    
        apiVersion: apps/v1
    
        kind: Deployment
    
        name: mcp-retail-server
    
      minReplicas: 3
    
      maxReplicas: 50
    
      metrics:
    
        - type: Resource
    
          resource:
    
            name: cpu
    
            target:
    
              type: Utilization
    
              averageUtilization: 70
    
        - type: Resource
    
          resource:
    
            name: memory
    
            target:
    
              type: Utilization
    
              averageUtilization: 80
    
        - type: Pods
    
          pods:
    
            metric:
    
              name: http_requests_per_second
    
            target:
    
              type: AverageValue
    
              averageValue: 100
    
      behavior:
    
        scaleDown:
    
          stabilizationWindowSeconds: 300
    
          policies:
    
            - type: Percent
    
              value: 50
    
              periodSeconds: 60
    
        scaleUp:
    
          stabilizationWindowSeconds: 60
    
          policies:
    
            - type: Percent
    
              value: 100
    
              periodSeconds: 30
    
            - type: Pods
    
              value: 5
    
              periodSeconds: 30
    
          selectPolicy: Max
    
    

    Performance Monitoring

    
    # mcp_server/monitoring/performance.py
    
    """
    
    Performance monitoring and metrics collection for production deployment.
    
    """
    
    import asyncio
    
    import time
    
    import psutil
    
    from typing import Dict, Any
    
    from dataclasses import dataclass
    
    from datetime import datetime, timedelta
    
    import logging
    
    
    
    @dataclass
    
    class PerformanceMetrics:
    
        """Performance metrics data structure."""
    
        timestamp: datetime
    
        cpu_percent: float
    
        memory_percent: float
    
        memory_used_mb: float
    
        active_connections: int
    
        request_rate: float
    
        avg_response_time: float
    
        error_rate: float
    
        database_connections: int
    
    
    
    class PerformanceMonitor:
    
        """Monitor and collect performance metrics."""
    
        
    
        def __init__(self, config):
    
            self.config = config
    
            self.logger = logging.getLogger(__name__)
    
            
    
            # Metrics collection
    
            self.metrics_history = []
    
            self.request_times = []
    
            self.error_count = 0
    
            self.request_count = 0
    
            
    
            # Database monitoring
    
            self.db_pool = None
    
            
    
        async def start_monitoring(self):
    
            """Start continuous performance monitoring."""
    
            
    
            self.logger.info("Starting performance monitoring")
    
            
    
            # Start metrics collection task
    
            asyncio.create_task(self._collect_metrics_loop())
    
            asyncio.create_task(self._cleanup_old_metrics())
    
            
    
        async def _collect_metrics_loop(self):
    
            """Continuously collect performance metrics."""
    
            
    
            while True:
    
                try:
    
                    metrics = await self._collect_current_metrics()
    
                    self.metrics_history.append(metrics)
    
                    
    
                    # Log critical metrics
    
                    if metrics.cpu_percent > 90:
    
                        self.logger.warning(f"High CPU usage: {metrics.cpu_percent:.1f}%")
    
                    
    
                    if metrics.memory_percent > 90:
    
                        self.logger.warning(f"High memory usage: {metrics.memory_percent:.1f}%")
    
                    
    
                    if metrics.error_rate > 0.05:  # 5% error rate
    
                        self.logger.warning(f"High error rate: {metrics.error_rate:.2%}")
    
                    
    
                    await asyncio.sleep(30)  # Collect every 30 seconds
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error collecting metrics: {e}")
    
                    await asyncio.sleep(60)
    
        
    
        async def _collect_current_metrics(self) -> PerformanceMetrics:
    
            """Collect current system metrics."""
    
            
    
            # System metrics
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            memory = psutil.virtual_memory()
    
            
    
            # Application metrics
    
            current_time = datetime.utcnow()
    
            recent_requests = [
    
                req_time for req_time in self.request_times
    
                if current_time - req_time < timedelta(minutes=1)
    
            ]
    
            
    
            request_rate = len(recent_requests) / 60.0  # requests per second
    
            
    
            # Calculate average response time
    
            avg_response_time = 0.0
    
            if hasattr(self, '_recent_response_times'):
    
                recent_response_times = [
    
                    rt for rt in self._recent_response_times
    
                    if current_time - rt['timestamp'] < timedelta(minutes=5)
    
                ]
    
                if recent_response_times:
    
                    avg_response_time = sum(rt['time'] for rt in recent_response_times) / len(recent_response_times)
    
            
    
            # Error rate calculation
    
            error_rate = 0.0
    
            if self.request_count > 0:
    
                error_rate = self.error_count / self.request_count
    
            
    
            # Database connections
    
            db_connections = 0
    
            if self.db_pool:
    
                db_connections = len(self.db_pool._holders)
    
            
    
            return PerformanceMetrics(
    
                timestamp=current_time,
    
                cpu_percent=cpu_percent,
    
                memory_percent=memory.percent,
    
                memory_used_mb=memory.used / (1024 * 1024),
    
                active_connections=0,  # To be implemented with connection tracking
    
                request_rate=request_rate,
    
                avg_response_time=avg_response_time,
    
                error_rate=error_rate,
    
                database_connections=db_connections
    
            )
    
        
    
        async def _cleanup_old_metrics(self):
    
            """Clean up old metrics to prevent memory leaks."""
    
            
    
            while True:
    
                try:
    
                    cutoff_time = datetime.utcnow() - timedelta(hours=24)
    
                    
    
                    # Clean up metrics history
    
                    self.metrics_history = [
    
                        m for m in self.metrics_history
    
                        if m.timestamp > cutoff_time
    
                    ]
    
                    
    
                    # Clean up request times
    
                    self.request_times = [
    
                        rt for rt in self.request_times
    
                        if rt > cutoff_time
    
                    ]
    
                    
    
                    # Reset counters periodically
    
                    if datetime.utcnow().minute == 0:  # Every hour
    
                        self.error_count = 0
    
                        self.request_count = 0
    
                    
    
                    await asyncio.sleep(3600)  # Run every hour
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error cleaning up metrics: {e}")
    
                    await asyncio.sleep(3600)
    
        
    
        def record_request(self, response_time: float, success: bool = True):
    
            """Record a request for metrics."""
    
            
    
            current_time = datetime.utcnow()
    
            self.request_times.append(current_time)
    
            self.request_count += 1
    
            
    
            if not success:
    
                self.error_count += 1
    
            
    
            # Record response time
    
            if not hasattr(self, '_recent_response_times'):
    
                self._recent_response_times = []
    
            
    
            self._recent_response_times.append({
    
                'timestamp': current_time,
    
                'time': response_time
    
            })
    
        
    
        def get_current_metrics(self) -> Dict[str, Any]:
    
            """Get current performance metrics."""
    
            
    
            if not self.metrics_history:
    
                return {}
    
            
    
            latest_metrics = self.metrics_history[-1]
    
            
    
            return {
    
                'timestamp': latest_metrics.timestamp.isoformat(),
    
                'system': {
    
                    'cpu_percent': latest_metrics.cpu_percent,
    
                    'memory_percent': latest_metrics.memory_percent,
    
                    'memory_used_mb': latest_metrics.memory_used_mb
    
                },
    
                'application': {
    
                    'active_connections': latest_metrics.active_connections,
    
                    'request_rate': latest_metrics.request_rate,
    
                    'avg_response_time': latest_metrics.avg_response_time,
    
                    'error_rate': latest_metrics.error_rate
    
                },
    
                'database': {
    
                    'connections': latest_metrics.database_connections
    
                }
    
            }
    
        
    
        def get_metrics_summary(self, hours: int = 24) -> Dict[str, Any]:
    
            """Get performance metrics summary for the specified hours."""
    
            
    
            cutoff_time = datetime.utcnow() - timedelta(hours=hours)
    
            recent_metrics = [
    
                m for m in self.metrics_history
    
                if m.timestamp > cutoff_time
    
            ]
    
            
    
            if not recent_metrics:
    
                return {}
    
            
    
            # Calculate averages
    
            avg_cpu = sum(m.cpu_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_memory = sum(m.memory_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_response_time = sum(m.avg_response_time for m in recent_metrics) / len(recent_metrics)
    
            
    
            # Calculate peaks
    
            max_cpu = max(m.cpu_percent for m in recent_metrics)
    
            max_memory = max(m.memory_percent for m in recent_metrics)
    
            max_response_time = max(m.avg_response_time for m in recent_metrics)
    
            
    
            return {
    
                'period_hours': hours,
    
                'averages': {
    
                    'cpu_percent': round(avg_cpu, 2),
    
                    'memory_percent': round(avg_memory, 2),
    
                    'response_time': round(avg_response_time, 3)
    
                },
    
                'peaks': {
    
                    'cpu_percent': round(max_cpu, 2),
    
                    'memory_percent': round(max_memory, 2),
    
                    'response_time': round(max_response_time, 3)
    
                },
    
                'data_points': len(recent_metrics)
    
            }
    
    

    ๐Ÿ” Production Security Configuration

    Security Hardening

    
    # k8s/security-policy.yaml - Kubernetes security policies
    
    apiVersion: v1
    
    kind: SecurityContext
    
    metadata:
    
      name: mcp-retail-security-context
    
    spec:
    
      runAsNonRoot: true
    
      runAsUser: 1000
    
      runAsGroup: 1000
    
      fsGroup: 1000
    
      seccompProfile:
    
        type: RuntimeDefault
    
      capabilities:
    
        drop:
    
          - ALL
    
      readOnlyRootFilesystem: true
    
      allowPrivilegeEscalation: false
    
    
    
    ---
    
    apiVersion: networking.k8s.io/v1
    
    kind: NetworkPolicy
    
    metadata:
    
      name: mcp-retail-network-policy
    
      namespace: mcp-retail
    
    spec:
    
      podSelector:
    
        matchLabels:
    
          app: mcp-retail-server
    
      policyTypes:
    
        - Ingress
    
        - Egress
    
      ingress:
    
        - from:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: ingress-nginx
    
          ports:
    
            - protocol: TCP
    
              port: 8000
    
      egress:
    
        - to:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: database
    
          ports:
    
            - protocol: TCP
    
              port: 5432
    
        - to: []
    
          ports:
    
            - protocol: TCP
    
              port: 443  # HTTPS for Azure OpenAI
    
            - protocol: TCP
    
              port: 53   # DNS
    
            - protocol: UDP
    
              port: 53   # DNS
    
    

    Environment Configuration

    
    # scripts/setup-production-env.sh
    
    #!/bin/bash
    
    
    
    # Production environment setup script
    
    set -euo pipefail
    
    
    
    echo "๐Ÿ”ง Setting up production environment..."
    
    
    
    # Create resource groups
    
    az group create --name "mcp-retail-prod-rg" --location "East US"
    
    az group create --name "mcp-retail-shared-rg" --location "East US"
    
    
    
    # Create Key Vault
    
    echo "๐Ÿ” Creating Azure Key Vault..."
    
    az keyvault create \
    
      --name "mcp-retail-kv-prod" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --location "East US" \
    
      --enable-rbac-authorization true
    
    
    
    # Set secrets
    
    echo "๐Ÿ”‘ Setting up secrets..."
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "postgres-password" \
    
      --value "${POSTGRES_PASSWORD}"
    
    
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "azure-openai-key" \
    
      --value "${AZURE_OPENAI_KEY}"
    
    
    
    # Create container registry
    
    echo "๐Ÿ“ฆ Creating container registry..."
    
    az acr create \
    
      --name "mcpretailregistry" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --sku Premium \
    
      --admin-enabled false
    
    
    
    # Create virtual network
    
    echo "๐ŸŒ Creating virtual network..."
    
    az network vnet create \
    
      --name "mcp-retail-vnet" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --address-prefix "10.0.0.0/16" \
    
      --subnet-name "container-apps" \
    
      --subnet-prefix "10.0.1.0/24"
    
    
    
    az network vnet subnet create \
    
      --name "database" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --vnet-name "mcp-retail-vnet" \
    
      --address-prefix "10.0.2.0/24" \
    
      --delegations Microsoft.DBforPostgreSQL/flexibleServers
    
    
    
    # Deploy infrastructure
    
    echo "๐Ÿ—๏ธ  Deploying infrastructure..."
    
    az deployment group create \
    
      --resource-group "mcp-retail-prod-rg" \
    
      --template-file "infra/main.bicep" \
    
      --parameters "infra/main.parameters.prod.json"
    
    
    
    echo "โœ… Production environment setup complete!"
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Container Strategy: Production-ready Docker containers with security hardening

    โœ… Cloud Deployment: Azure Container Apps with auto-scaling and monitoring

    โœ… Database Deployment: PostgreSQL Flexible Server with high availability

    โœ… CI/CD Pipelines: Automated testing, building, and deployment workflows

    โœ… Performance Monitoring: Comprehensive metrics collection and alerting

    โœ… Security Configuration: Production-grade security policies and network isolation

    ๐Ÿš€ What's Next

    Continue with Lab 11: Monitoring and Observability to:

  • Set up comprehensive monitoring with Application Insights
  • Configure structured logging and distributed tracing
  • Implement alerting and automated response systems
  • Monitor business metrics and performance KPIs
  • ๐Ÿ“š Additional Resources

    Container Technologies

  • Docker Best Practices - Official Docker best practices
  • Azure Container Apps - Azure Container Apps documentation
  • Kubernetes Documentation - Kubernetes official documentation
  • CI/CD and DevOps

  • GitHub Actions - GitHub Actions documentation
  • Azure DevOps - Azure DevOps services
  • Infrastructure as Code - Azure Bicep documentation
  • Security and Monitoring

  • Azure Security Center - Azure security recommendations
  • Container Security - Kubernetes security concepts
  • Application Insights - Azure Application Insights
  • ---

    Previous: Lab 09: VS Code Integration

    Next: Lab 11: Monitoring and Observability

    11 Monitoring and Observability

    Monitoring and Observability

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance for implementing monitoring, observability, and alerting for your MCP server in production environments.

    You'll learn to set up Application Insights, create meaningful dashboards, implement effective alerting, and establish troubleshooting workflows for operational excellence.

    Overview

    Effective monitoring and observability are crucial for maintaining reliable MCP servers in production.

    This lab covers the three pillars of observabilityโ€”metrics, logs, and tracesโ€”and shows you how to implement comprehensive monitoring that enables proactive issue detection and rapid problem resolution.

    You'll learn to transform raw telemetry data into actionable insights that help you understand system behavior, optimize performance, and ensure high availability.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Implement comprehensive Application Insights integration for MCP servers
  • Design structured logging patterns for effective troubleshooting
  • Create performance metrics collection and analysis systems
  • Configure intelligent alerting with actionable notifications
  • Build operational dashboards for real-time monitoring
  • Establish effective troubleshooting workflows and runbooks
  • ๐Ÿ“Š Application Insights Integration

    Setting Up Application Insights

    
    # mcp_server/monitoring.py
    
    """
    
    Comprehensive monitoring and telemetry for MCP server.
    
    """
    
    import logging
    
    import time
    
    import psutil
    
    from typing import Dict, Any, Optional
    
    from contextlib import contextmanager
    
    from azure.monitor.opentelemetry import configure_azure_monitor
    
    from opentelemetry import trace, metrics
    
    from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
    
    from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
    
    from opentelemetry.instrumentation.requests import RequestsInstrumentor
    
    
    
    class MCPTelemetryManager:
    
        """Comprehensive telemetry management for MCP server."""
    
        
    
        def __init__(self, connection_string: str):
    
            self.connection_string = connection_string
    
            self.tracer = None
    
            self.meter = None
    
            self.custom_metrics = {}
    
            
    
        def initialize_telemetry(self, app):
    
            """Initialize Application Insights and OpenTelemetry."""
    
            
    
            # Configure Azure Monitor
    
            configure_azure_monitor(
    
                connection_string=self.connection_string,
    
                logger_name="mcp_server",
    
                disable_offline_storage=False
    
            )
    
            
    
            # Get tracer and meter
    
            self.tracer = trace.get_tracer(__name__)
    
            self.meter = metrics.get_meter(__name__)
    
            
    
            # Initialize custom metrics
    
            self._setup_custom_metrics()
    
            
    
            # Instrument FastAPI
    
            FastAPIInstrumentor.instrument_app(app)
    
            
    
            # Instrument database
    
            AsyncPGInstrumentor().instrument()
    
            
    
            # Instrument HTTP requests
    
            RequestsInstrumentor().instrument()
    
            
    
            logging.info("Telemetry initialization complete")
    
        
    
        def _setup_custom_metrics(self):
    
            """Set up custom metrics for MCP server operations."""
    
            
    
            self.custom_metrics = {
    
                # Request metrics
    
                "mcp_requests_total": self.meter.create_counter(
    
                    name="mcp_requests_total",
    
                    description="Total number of MCP requests",
    
                    unit="1"
    
                ),
    
                
    
                "mcp_request_duration": self.meter.create_histogram(
    
                    name="mcp_request_duration_seconds",
    
                    description="MCP request duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                # Database metrics
    
                "database_queries_total": self.meter.create_counter(
    
                    name="database_queries_total",
    
                    description="Total database queries executed",
    
                    unit="1"
    
                ),
    
                
    
                "database_query_duration": self.meter.create_histogram(
    
                    name="database_query_duration_seconds",
    
                    description="Database query duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                "database_connections_active": self.meter.create_up_down_counter(
    
                    name="database_connections_active",
    
                    description="Number of active database connections",
    
                    unit="1"
    
                ),
    
                
    
                # Tool metrics
    
                "tool_executions_total": self.meter.create_counter(
    
                    name="tool_executions_total",
    
                    description="Total tool executions",
    
                    unit="1"
    
                ),
    
                
    
                "tool_execution_duration": self.meter.create_histogram(
    
                    name="tool_execution_duration_seconds",
    
                    description="Tool execution duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                # System metrics
    
                "system_cpu_usage": self.meter.create_gauge(
    
                    name="system_cpu_usage_percent",
    
                    description="System CPU usage percentage",
    
                    unit="%"
    
                ),
    
                
    
                "system_memory_usage": self.meter.create_gauge(
    
                    name="system_memory_usage_bytes",
    
                    description="System memory usage in bytes",
    
                    unit="byte"
    
                ),
    
                
    
                # Error metrics
    
                "errors_total": self.meter.create_counter(
    
                    name="errors_total",
    
                    description="Total number of errors",
    
                    unit="1"
    
                )
    
            }
    
        
    
        @contextmanager
    
        def trace_operation(self, operation_name: str, attributes: Dict[str, Any] = None):
    
            """Create a traced operation with automatic metrics collection."""
    
            
    
            with self.tracer.start_as_current_span(operation_name) as span:
    
                start_time = time.time()
    
                
    
                # Add attributes to span
    
                if attributes:
    
                    for key, value in attributes.items():
    
                        span.set_attribute(key, value)
    
                
    
                try:
    
                    yield span
    
                    
    
                    # Record success metrics
    
                    duration = time.time() - start_time
    
                    
    
                    if "request" in operation_name.lower():
    
                        self.custom_metrics["mcp_requests_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["mcp_request_duration"].record(duration)
    
                    
    
                    elif "query" in operation_name.lower():
    
                        self.custom_metrics["database_queries_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["database_query_duration"].record(duration)
    
                    
    
                    elif "tool" in operation_name.lower():
    
                        self.custom_metrics["tool_executions_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["tool_execution_duration"].record(duration)
    
                    
    
                except Exception as e:
    
                    # Record error
    
                    span.record_exception(e)
    
                    span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
    
                    
    
                    # Record error metrics
    
                    self.custom_metrics["errors_total"].add(1, {
    
                        "operation": operation_name,
    
                        "error_type": type(e).__name__
    
                    })
    
                    
    
                    raise
    
        
    
        def record_system_metrics(self):
    
            """Record system-level metrics."""
    
            
    
            # CPU usage
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            self.custom_metrics["system_cpu_usage"].set(cpu_percent)
    
            
    
            # Memory usage
    
            memory = psutil.virtual_memory()
    
            self.custom_metrics["system_memory_usage"].set(memory.used)
    
            
    
            # Database connections (if available)
    
            if hasattr(db_provider, 'connection_pool') and db_provider.connection_pool:
    
                active_connections = db_provider.connection_pool.get_size()
    
                self.custom_metrics["database_connections_active"].add(active_connections)
    
    
    
    # Global telemetry manager
    
    telemetry_manager = MCPTelemetryManager(
    
        connection_string=config.server.applicationinsights_connection_string
    
    )
    
    

    Enhanced Logging with Structured Data

    
    # mcp_server/logging_config.py
    
    """
    
    Structured logging configuration for MCP server.
    
    """
    
    import logging
    
    import json
    
    import sys
    
    from datetime import datetime
    
    from typing import Dict, Any
    
    import traceback
    
    
    
    class StructuredFormatter(logging.Formatter):
    
        """Custom formatter for structured JSON logging."""
    
        
    
        def format(self, record: logging.LogRecord) -> str:
    
            """Format log record as structured JSON."""
    
            
    
            # Base log structure
    
            log_entry = {
    
                "timestamp": datetime.utcnow().isoformat() + "Z",
    
                "level": record.levelname,
    
                "logger": record.name,
    
                "message": record.getMessage(),
    
                "module": record.module,
    
                "function": record.funcName,
    
                "line": record.lineno
    
            }
    
            
    
            # Add exception information if present
    
            if record.exc_info:
    
                log_entry["exception"] = {
    
                    "type": record.exc_info[0].__name__,
    
                    "message": str(record.exc_info[1]),
    
                    "traceback": traceback.format_exception(*record.exc_info)
    
                }
    
            
    
            # Add custom attributes from extra
    
            if hasattr(record, 'extra_data'):
    
                log_entry.update(record.extra_data)
    
            
    
            # Add correlation ID if available
    
            if hasattr(record, 'correlation_id'):
    
                log_entry["correlation_id"] = record.correlation_id
    
            
    
            # Add user context if available
    
            if hasattr(record, 'user_id'):
    
                log_entry["user_id"] = record.user_id
    
            
    
            if hasattr(record, 'rls_user_id'):
    
                log_entry["rls_user_id"] = record.rls_user_id
    
            
    
            return json.dumps(log_entry, ensure_ascii=False)
    
    
    
    class MCPLogger:
    
        """Enhanced logging utilities for MCP server."""
    
        
    
        def __init__(self, name: str):
    
            self.logger = logging.getLogger(name)
    
            self._setup_structured_logging()
    
        
    
        def _setup_structured_logging(self):
    
            """Configure structured logging."""
    
            
    
            # Remove existing handlers
    
            for handler in self.logger.handlers[:]:
    
                self.logger.removeHandler(handler)
    
            
    
            # Create structured handler
    
            handler = logging.StreamHandler(sys.stdout)
    
            handler.setFormatter(StructuredFormatter())
    
            
    
            self.logger.addHandler(handler)
    
            self.logger.setLevel(logging.INFO)
    
        
    
        def log_mcp_request(
    
            self, 
    
            method: str, 
    
            user_id: str, 
    
            rls_user_id: str,
    
            duration: float = None,
    
            status: str = "success",
    
            **kwargs
    
        ):
    
            """Log MCP request with structured data."""
    
            
    
            extra_data = {
    
                "event_type": "mcp_request",
    
                "method": method,
    
                "user_id": user_id,
    
                "rls_user_id": rls_user_id,
    
                "status": status
    
            }
    
            
    
            if duration is not None:
    
                extra_data["duration_ms"] = duration * 1000
    
            
    
            extra_data.update(kwargs)
    
            
    
            self.logger.info(
    
                f"MCP request: {method} - {status}",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_database_query(
    
            self,
    
            query: str,
    
            duration: float,
    
            row_count: int = None,
    
            user_id: str = None,
    
            **kwargs
    
        ):
    
            """Log database query with performance data."""
    
            
    
            extra_data = {
    
                "event_type": "database_query",
    
                "query_hash": hash(query.strip()),
    
                "duration_ms": duration * 1000,
    
                "query_preview": query[:100] + "..." if len(query) > 100 else query
    
            }
    
            
    
            if row_count is not None:
    
                extra_data["row_count"] = row_count
    
            
    
            if user_id:
    
                extra_data["user_id"] = user_id
    
            
    
            extra_data.update(kwargs)
    
            
    
            level = logging.WARNING if duration > 1.0 else logging.INFO
    
            
    
            self.logger.log(
    
                level,
    
                f"Database query executed ({duration*1000:.2f}ms)",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_security_event(
    
            self,
    
            event_type: str,
    
            user_id: str = None,
    
            ip_address: str = None,
    
            success: bool = True,
    
            details: Dict[str, Any] = None
    
        ):
    
            """Log security-related events."""
    
            
    
            extra_data = {
    
                "event_type": "security_event",
    
                "security_event_type": event_type,
    
                "success": success
    
            }
    
            
    
            if user_id:
    
                extra_data["user_id"] = user_id
    
            
    
            if ip_address:
    
                extra_data["ip_address"] = ip_address
    
            
    
            if details:
    
                extra_data["details"] = details
    
            
    
            level = logging.INFO if success else logging.WARNING
    
            
    
            self.logger.log(
    
                level,
    
                f"Security event: {event_type} - {'success' if success else 'failure'}",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_performance_metric(
    
            self,
    
            metric_name: str,
    
            value: float,
    
            unit: str = "count",
    
            dimensions: Dict[str, str] = None
    
        ):
    
            """Log custom performance metrics."""
    
            
    
            extra_data = {
    
                "event_type": "performance_metric",
    
                "metric_name": metric_name,
    
                "value": value,
    
                "unit": unit
    
            }
    
            
    
            if dimensions:
    
                extra_data["dimensions"] = dimensions
    
            
    
            self.logger.info(
    
                f"Performance metric: {metric_name} = {value} {unit}",
    
                extra={"extra_data": extra_data}
    
            )
    
    
    
    # Global logger instance
    
    mcp_logger = MCPLogger("mcp_server")
    
    

    Custom Metrics Collection

    
    # mcp_server/metrics_collector.py
    
    """
    
    Custom metrics collection for business and operational insights.
    
    """
    
    import asyncio
    
    import time
    
    from typing import Dict, Any, List
    
    from dataclasses import dataclass
    
    from collections import defaultdict, deque
    
    import statistics
    
    
    
    @dataclass
    
    class MetricPoint:
    
        """Individual metric data point."""
    
        timestamp: float
    
        value: float
    
        dimensions: Dict[str, str]
    
    
    
    class MetricsCollector:
    
        """Advanced metrics collection and analysis."""
    
        
    
        def __init__(self, retention_minutes: int = 60):
    
            self.retention_seconds = retention_minutes * 60
    
            self.metrics_buffer = defaultdict(lambda: deque(maxlen=1000))
    
            self.aggregated_metrics = {}
    
            
    
        def record_metric(
    
            self,
    
            name: str,
    
            value: float,
    
            dimensions: Dict[str, str] = None
    
        ):
    
            """Record a metric point."""
    
            
    
            metric_point = MetricPoint(
    
                timestamp=time.time(),
    
                value=value,
    
                dimensions=dimensions or {}
    
            )
    
            
    
            self.metrics_buffer[name].append(metric_point)
    
            self._cleanup_old_metrics(name)
    
        
    
        def _cleanup_old_metrics(self, metric_name: str):
    
            """Remove metrics older than retention period."""
    
            
    
            cutoff_time = time.time() - self.retention_seconds
    
            buffer = self.metrics_buffer[metric_name]
    
            
    
            while buffer and buffer[0].timestamp < cutoff_time:
    
                buffer.popleft()
    
        
    
        def get_metric_summary(
    
            self,
    
            name: str,
    
            time_window_minutes: int = 5
    
        ) -> Dict[str, Any]:
    
            """Get statistical summary of a metric."""
    
            
    
            time_window_seconds = time_window_minutes * 60
    
            cutoff_time = time.time() - time_window_seconds
    
            
    
            relevant_points = [
    
                point for point in self.metrics_buffer[name]
    
                if point.timestamp >= cutoff_time
    
            ]
    
            
    
            if not relevant_points:
    
                return {"error": "No data available"}
    
            
    
            values = [point.value for point in relevant_points]
    
            
    
            return {
    
                "count": len(values),
    
                "min": min(values),
    
                "max": max(values),
    
                "mean": statistics.mean(values),
    
                "median": statistics.median(values),
    
                "p95": self._percentile(values, 95),
    
                "p99": self._percentile(values, 99),
    
                "time_window_minutes": time_window_minutes
    
            }
    
        
    
        def _percentile(self, values: List[float], percentile: float) -> float:
    
            """Calculate percentile value."""
    
            if not values:
    
                return 0
    
            
    
            sorted_values = sorted(values)
    
            index = int((percentile / 100) * len(sorted_values))
    
            index = min(index, len(sorted_values) - 1)
    
            
    
            return sorted_values[index]
    
        
    
        async def collect_business_metrics(self):
    
            """Collect business-specific metrics."""
    
            
    
            try:
    
                # Query execution patterns
    
                query_types = await self._analyze_query_patterns()
    
                for query_type, count in query_types.items():
    
                    self.record_metric(
    
                        "business_queries_by_type",
    
                        count,
    
                        {"query_type": query_type}
    
                    )
    
                
    
                # User activity patterns
    
                user_activity = await self._analyze_user_activity()
    
                for store_id, activity_count in user_activity.items():
    
                    self.record_metric(
    
                        "user_activity_by_store",
    
                        activity_count,
    
                        {"store_id": store_id}
    
                    )
    
                
    
                # Tool usage patterns
    
                tool_usage = await self._analyze_tool_usage()
    
                for tool_name, usage_count in tool_usage.items():
    
                    self.record_metric(
    
                        "tool_usage",
    
                        usage_count,
    
                        {"tool_name": tool_name}
    
                    )
    
                    
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Business metrics collection failed: {e}")
    
        
    
        async def _analyze_query_patterns(self) -> Dict[str, int]:
    
            """Analyze database query patterns."""
    
            
    
            # This would analyze actual query logs
    
            # For demo purposes, returning sample data
    
            return {
    
                "sales_analysis": 45,
    
                "inventory_check": 23,
    
                "customer_lookup": 18,
    
                "product_search": 31
    
            }
    
        
    
        async def _analyze_user_activity(self) -> Dict[str, int]:
    
            """Analyze user activity by store."""
    
            
    
            # This would analyze actual user activity logs
    
            return {
    
                "seattle": 67,
    
                "redmond": 34,
    
                "bellevue": 23,
    
                "online": 89
    
            }
    
        
    
        async def _analyze_tool_usage(self) -> Dict[str, int]:
    
            """Analyze MCP tool usage patterns."""
    
            
    
            return {
    
                "execute_sales_query": 156,
    
                "get_multiple_table_schemas": 45,
    
                "semantic_search_products": 78,
    
                "get_current_utc_date": 23
    
            }
    
    
    
    # Global metrics collector
    
    metrics_collector = MetricsCollector()
    
    

    ๐Ÿ”” Alert Configuration

    Intelligent Alerting System

    
    # mcp_server/alerting.py
    
    """
    
    Intelligent alerting system for MCP server operations.
    
    """
    
    import asyncio
    
    import json
    
    from typing import Dict, List, Any, Callable
    
    from enum import Enum
    
    from dataclasses import dataclass
    
    from azure.communication.email import EmailClient
    
    import smtplib
    
    from email.mime.text import MIMEText
    
    from email.mime.multipart import MIMEMultipart
    
    
    
    class AlertSeverity(Enum):
    
        LOW = "low"
    
        MEDIUM = "medium"
    
        HIGH = "high"
    
        CRITICAL = "critical"
    
    
    
    @dataclass
    
    class AlertRule:
    
        """Alert rule configuration."""
    
        name: str
    
        condition: Callable[[Dict[str, Any]], bool]
    
        severity: AlertSeverity
    
        cooldown_minutes: int
    
        message_template: str
    
        enabled: bool = True
    
    
    
    @dataclass
    
    class Alert:
    
        """Alert instance."""
    
        rule_name: str
    
        severity: AlertSeverity
    
        message: str
    
        timestamp: float
    
        details: Dict[str, Any]
    
        acknowledged: bool = False
    
    
    
    class AlertManager:
    
        """Comprehensive alerting management."""
    
        
    
        def __init__(self):
    
            self.alert_rules = {}
    
            self.active_alerts = {}
    
            self.alert_history = deque(maxlen=1000)
    
            self.notification_channels = {}
    
            self._setup_default_rules()
    
            self._setup_notification_channels()
    
        
    
        def _setup_default_rules(self):
    
            """Set up default alert rules."""
    
            
    
            # Database connection issues
    
            self.add_alert_rule(AlertRule(
    
                name="database_connection_failure",
    
                condition=lambda metrics: metrics.get("database_status") != "healthy",
    
                severity=AlertSeverity.CRITICAL,
    
                cooldown_minutes=5,
    
                message_template="Database connection failure detected. Service may be unavailable."
    
            ))
    
            
    
            # High error rate
    
            self.add_alert_rule(AlertRule(
    
                name="high_error_rate",
    
                condition=lambda metrics: metrics.get("error_rate", 0) > 0.05,  # 5% error rate
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=10,
    
                message_template="High error rate detected: {error_rate:.2%}. Investigate immediately."
    
            ))
    
            
    
            # Slow query performance
    
            self.add_alert_rule(AlertRule(
    
                name="slow_query_performance",
    
                condition=lambda metrics: metrics.get("avg_query_duration", 0) > 2.0,  # 2 seconds
    
                severity=AlertSeverity.MEDIUM,
    
                cooldown_minutes=15,
    
                message_template="Slow query performance detected. Average duration: {avg_query_duration:.2f}s"
    
            ))
    
            
    
            # High CPU usage
    
            self.add_alert_rule(AlertRule(
    
                name="high_cpu_usage",
    
                condition=lambda metrics: metrics.get("cpu_usage", 0) > 85,  # 85% CPU
    
                severity=AlertSeverity.MEDIUM,
    
                cooldown_minutes=10,
    
                message_template="High CPU usage detected: {cpu_usage:.1f}%"
    
            ))
    
            
    
            # Memory usage
    
            self.add_alert_rule(AlertRule(
    
                name="high_memory_usage",
    
                condition=lambda metrics: metrics.get("memory_usage_percent", 0) > 90,  # 90% memory
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=5,
    
                message_template="High memory usage detected: {memory_usage_percent:.1f}%"
    
            ))
    
            
    
            # Authentication failures
    
            self.add_alert_rule(AlertRule(
    
                name="authentication_failures",
    
                condition=lambda metrics: metrics.get("auth_failure_rate", 0) > 0.1,  # 10% failure rate
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=5,
    
                message_template="High authentication failure rate: {auth_failure_rate:.2%}. Possible security incident."
    
            ))
    
        
    
        def _setup_notification_channels(self):
    
            """Set up notification channels."""
    
            
    
            # Email notifications
    
            email_config = {
    
                "smtp_server": os.getenv("SMTP_SERVER", "smtp.office365.com"),
    
                "smtp_port": int(os.getenv("SMTP_PORT", "587")),
    
                "username": os.getenv("SMTP_USERNAME"),
    
                "password": os.getenv("SMTP_PASSWORD"),
    
                "from_address": os.getenv("ALERT_FROM_EMAIL"),
    
                "to_addresses": os.getenv("ALERT_TO_EMAILS", "").split(",")
    
            }
    
            
    
            if email_config["username"] and email_config["password"]:
    
                self.notification_channels["email"] = EmailNotifier(email_config)
    
            
    
            # Microsoft Teams webhook
    
            teams_webhook = os.getenv("TEAMS_WEBHOOK_URL")
    
            if teams_webhook:
    
                self.notification_channels["teams"] = TeamsNotifier(teams_webhook)
    
            
    
            # Slack webhook
    
            slack_webhook = os.getenv("SLACK_WEBHOOK_URL")
    
            if slack_webhook:
    
                self.notification_channels["slack"] = SlackNotifier(slack_webhook)
    
        
    
        def add_alert_rule(self, rule: AlertRule):
    
            """Add or update an alert rule."""
    
            self.alert_rules[rule.name] = rule
    
        
    
        async def evaluate_metrics(self, metrics: Dict[str, Any]):
    
            """Evaluate metrics against alert rules."""
    
            
    
            for rule_name, rule in self.alert_rules.items():
    
                if not rule.enabled:
    
                    continue
    
                
    
                try:
    
                    # Check if rule condition is met
    
                    if rule.condition(metrics):
    
                        await self._trigger_alert(rule, metrics)
    
                    else:
    
                        # Clear alert if condition no longer met
    
                        await self._clear_alert(rule_name)
    
                        
    
                except Exception as e:
    
                    mcp_logger.logger.error(f"Error evaluating alert rule {rule_name}: {e}")
    
        
    
        async def _trigger_alert(self, rule: AlertRule, metrics: Dict[str, Any]):
    
            """Trigger an alert."""
    
            
    
            current_time = time.time()
    
            
    
            # Check cooldown period
    
            if rule.name in self.active_alerts:
    
                last_alert_time = self.active_alerts[rule.name].timestamp
    
                if current_time - last_alert_time < rule.cooldown_minutes * 60:
    
                    return  # Still in cooldown
    
            
    
            # Format alert message
    
            message = rule.message_template.format(**metrics)
    
            
    
            # Create alert
    
            alert = Alert(
    
                rule_name=rule.name,
    
                severity=rule.severity,
    
                message=message,
    
                timestamp=current_time,
    
                details=metrics.copy()
    
            )
    
            
    
            # Store alert
    
            self.active_alerts[rule.name] = alert
    
            self.alert_history.append(alert)
    
            
    
            # Send notifications
    
            await self._send_notifications(alert)
    
            
    
            mcp_logger.log_security_event(
    
                "alert_triggered",
    
                details={
    
                    "rule_name": rule.name,
    
                    "severity": rule.severity.value,
    
                    "message": message
    
                }
    
            )
    
        
    
        async def _clear_alert(self, rule_name: str):
    
            """Clear an active alert."""
    
            
    
            if rule_name in self.active_alerts:
    
                alert = self.active_alerts[rule_name]
    
                del self.active_alerts[rule_name]
    
                
    
                # Send resolution notification for high/critical alerts
    
                if alert.severity in [AlertSeverity.HIGH, AlertSeverity.CRITICAL]:
    
                    resolution_alert = Alert(
    
                        rule_name=rule_name,
    
                        severity=AlertSeverity.LOW,
    
                        message=f"RESOLVED: {alert.message}",
    
                        timestamp=time.time(),
    
                        details={"resolution": True}
    
                    )
    
                    
    
                    await self._send_notifications(resolution_alert)
    
        
    
        async def _send_notifications(self, alert: Alert):
    
            """Send alert notifications through all configured channels."""
    
            
    
            tasks = []
    
            
    
            for channel_name, notifier in self.notification_channels.items():
    
                task = asyncio.create_task(
    
                    notifier.send_notification(alert),
    
                    name=f"notify_{channel_name}"
    
                )
    
                tasks.append(task)
    
            
    
            if tasks:
    
                # Wait for all notifications with timeout
    
                try:
    
                    await asyncio.wait_for(
    
                        asyncio.gather(*tasks, return_exceptions=True),
    
                        timeout=30.0
    
                    )
    
                except asyncio.TimeoutError:
    
                    mcp_logger.logger.warning("Some alert notifications timed out")
    
    
    
    # Notification implementations
    
    class EmailNotifier:
    
        """Email notification handler."""
    
        
    
        def __init__(self, config: Dict[str, Any]):
    
            self.config = config
    
        
    
        async def send_notification(self, alert: Alert):
    
            """Send email notification."""
    
            
    
            try:
    
                msg = MIMEMultipart()
    
                msg['From'] = self.config['from_address']
    
                msg['To'] = ', '.join(self.config['to_addresses'])
    
                msg['Subject'] = f"[{alert.severity.value.upper()}] MCP Server Alert: {alert.rule_name}"
    
                
    
                body = f"""
    
    Alert Details:
    
    - Rule: {alert.rule_name}
    
    - Severity: {alert.severity.value.upper()}
    
    - Time: {datetime.fromtimestamp(alert.timestamp).isoformat()}
    
    - Message: {alert.message}
    
    
    
    Additional Details:
    
    {json.dumps(alert.details, indent=2)}
    
    
    
    This is an automated alert from the MCP Server monitoring system.
    
                """
    
                
    
                msg.attach(MIMEText(body, 'plain'))
    
                
    
                # Send email
    
                with smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port']) as server:
    
                    server.starttls()
    
                    server.login(self.config['username'], self.config['password'])
    
                    server.send_message(msg)
    
                    
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Failed to send email notification: {e}")
    
    
    
    class TeamsNotifier:
    
        """Microsoft Teams notification handler."""
    
        
    
        def __init__(self, webhook_url: str):
    
            self.webhook_url = webhook_url
    
        
    
        async def send_notification(self, alert: Alert):
    
            """Send Teams notification."""
    
            
    
            color_map = {
    
                AlertSeverity.LOW: "28a745",     # Green
    
                AlertSeverity.MEDIUM: "ffc107",   # Yellow
    
                AlertSeverity.HIGH: "fd7e14",     # Orange
    
                AlertSeverity.CRITICAL: "dc3545"  # Red
    
            }
    
            
    
            payload = {
    
                "@type": "MessageCard",
    
                "@context": "http://schema.org/extensions",
    
                "themeColor": color_map.get(alert.severity, "0076D7"),
    
                "summary": f"MCP Server Alert: {alert.rule_name}",
    
                "sections": [{
    
                    "activityTitle": f"๐Ÿšจ {alert.severity.value.upper()} Alert",
    
                    "activitySubtitle": alert.rule_name,
    
                    "text": alert.message,
    
                    "facts": [
    
                        {"name": "Timestamp", "value": datetime.fromtimestamp(alert.timestamp).isoformat()},
    
                        {"name": "Severity", "value": alert.severity.value.upper()}
    
                    ]
    
                }]
    
            }
    
            
    
            try:
    
                async with aiohttp.ClientSession() as session:
    
                    async with session.post(self.webhook_url, json=payload) as response:
    
                        if response.status != 200:
    
                            raise Exception(f"Teams webhook returned {response.status}")
    
                            
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Failed to send Teams notification: {e}")
    
    
    
    # Global alert manager
    
    alert_manager = AlertManager()
    
    

    ๐Ÿ“ˆ Dashboard Creation

    Azure Monitor Workbooks

    
    {
    
      "version": "Notebook/1.0",
    
      "items": [
    
        {
    
          "type": 1,
    
          "content": {
    
            "json": "# MCP Server Operations Dashboard\n\nComprehensive monitoring dashboard for Zava Retail MCP Server operations, performance, and health metrics."
    
          },
    
          "name": "title"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart",
    
            "version": "KqlItem/1.0",
    
            "query": "requests\n| where timestamp >= ago(1h)\n| where name contains \"mcp\"\n| summarize RequestCount = count(), AvgDuration = avg(duration) by bin(timestamp, 5m)\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "MCP Request Volume and Performance",
    
            "timeContext": {
    
              "durationMs": 3600000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "timechart"
    
          },
    
          "name": "request-metrics"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-2",
    
            "version": "KqlItem/1.0",
    
            "query": "customMetrics\n| where name == \"database_query_duration_seconds\"\n| where timestamp >= ago(1h)\n| summarize \n    AvgDuration = avg(value),\n    P95Duration = percentile(value, 95),\n    P99Duration = percentile(value, 99)\n    by bin(timestamp, 5m)\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "Database Query Performance",
    
            "timeContext": {
    
              "durationMs": 3600000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "timechart"
    
          },
    
          "name": "database-performance"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-3",
    
            "version": "KqlItem/1.0",
    
            "query": "exceptions\n| where timestamp >= ago(24h)\n| where method contains \"mcp\"\n| summarize ErrorCount = count() by bin(timestamp, 1h), type\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "Error Rate Analysis",
    
            "timeContext": {
    
              "durationMs": 86400000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "barchart"
    
          },
    
          "name": "error-analysis"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-4",
    
            "version": "KqlItem/1.0",
    
            "query": "customMetrics\n| where name in (\"system_cpu_usage_percent\", \"system_memory_usage_bytes\")\n| where timestamp >= ago(2h)\n| extend MetricType = case(\n    name == \"system_cpu_usage_percent\", \"CPU %\",\n    name == \"system_memory_usage_bytes\", \"Memory GB\",\n    \"Unknown\"\n)\n| extend NormalizedValue = case(\n    name == \"system_memory_usage_bytes\", value / (1024*1024*1024),\n    value\n)\n| summarize AvgValue = avg(NormalizedValue) by bin(timestamp, 5m), MetricType\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "System Resource Usage",
    
            "timeContext": {
    
              "durationMs": 7200000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "linechart"
    
          },
    
          "name": "system-resources"
    
        }
    
      ],
    
      "isLocked": false,
    
      "fallbackResourceIds": [
    
        "/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/microsoft.insights/components/{app-insights-name}"
    
      ]
    
    }
    
    

    Custom Dashboard Implementation

    
    # mcp_server/dashboard.py
    
    """
    
    Custom dashboard data provider for MCP server metrics.
    
    """
    
    from typing import Dict, List, Any
    
    from fastapi import APIRouter, Depends
    
    from datetime import datetime, timedelta
    
    
    
    dashboard_router = APIRouter(prefix="/dashboard", tags=["dashboard"])
    
    
    
    class DashboardDataProvider:
    
        """Provide dashboard data from various sources."""
    
        
    
        def __init__(self):
    
            self.metrics_collector = metrics_collector
    
            self.alert_manager = alert_manager
    
        
    
        async def get_overview_metrics(self) -> Dict[str, Any]:
    
            """Get high-level overview metrics."""
    
            
    
            current_time = time.time()
    
            one_hour_ago = current_time - 3600
    
            
    
            return {
    
                "timestamp": current_time,
    
                "active_alerts": len(self.alert_manager.active_alerts),
    
                "critical_alerts": len([
    
                    alert for alert in self.alert_manager.active_alerts.values()
    
                    if alert.severity == AlertSeverity.CRITICAL
    
                ]),
    
                "requests_last_hour": await self._get_request_count(one_hour_ago),
    
                "avg_response_time": await self._get_avg_response_time(one_hour_ago),
    
                "error_rate": await self._get_error_rate(one_hour_ago),
    
                "database_status": await self._get_database_status(),
    
                "system_health": await self._get_system_health()
    
            }
    
        
    
        async def get_performance_trends(self, hours: int = 24) -> Dict[str, List[Dict]]:
    
            """Get performance trends over time."""
    
            
    
            end_time = time.time()
    
            start_time = end_time - (hours * 3600)
    
            
    
            # Generate hourly data points
    
            data_points = []
    
            current = start_time
    
            
    
            while current < end_time:
    
                hour_start = current
    
                hour_end = current + 3600
    
                
    
                data_points.append({
    
                    "timestamp": current,
    
                    "requests": await self._get_request_count_range(hour_start, hour_end),
    
                    "avg_duration": await self._get_avg_duration_range(hour_start, hour_end),
    
                    "error_count": await self._get_error_count_range(hour_start, hour_end),
    
                    "cpu_usage": await self._get_cpu_usage_range(hour_start, hour_end),
    
                    "memory_usage": await self._get_memory_usage_range(hour_start, hour_end)
    
                })
    
                
    
                current = hour_end
    
            
    
            return {
    
                "time_series": data_points,
    
                "period_hours": hours,
    
                "data_points": len(data_points)
    
            }
    
        
    
        async def get_business_insights(self) -> Dict[str, Any]:
    
            """Get business-specific insights."""
    
            
    
            return {
    
                "top_queries": await self._get_top_queries(),
    
                "store_activity": await self._get_store_activity(),
    
                "tool_usage": await self._get_tool_usage_stats(),
    
                "user_patterns": await self._get_user_patterns(),
    
                "peak_hours": await self._get_peak_hours()
    
            }
    
        
    
        async def _get_request_count(self, since_time: float) -> int:
    
            """Get request count since specified time."""
    
            summary = self.metrics_collector.get_metric_summary(
    
                "mcp_requests_total",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            return summary.get("count", 0)
    
        
    
        async def _get_avg_response_time(self, since_time: float) -> float:
    
            """Get average response time since specified time."""
    
            summary = self.metrics_collector.get_metric_summary(
    
                "mcp_request_duration_seconds",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            return summary.get("mean", 0.0) * 1000  # Convert to milliseconds
    
        
    
        async def _get_error_rate(self, since_time: float) -> float:
    
            """Calculate error rate since specified time."""
    
            
    
            total_requests = await self._get_request_count(since_time)
    
            error_summary = self.metrics_collector.get_metric_summary(
    
                "errors_total",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            error_count = error_summary.get("count", 0)
    
            
    
            if total_requests == 0:
    
                return 0.0
    
            
    
            return error_count / total_requests
    
        
    
        async def _get_database_status(self) -> str:
    
            """Get current database status."""
    
            try:
    
                health = await db_provider.health_check()
    
                return health.get("status", "unknown")
    
            except Exception:
    
                return "unhealthy"
    
        
    
        async def _get_system_health(self) -> Dict[str, Any]:
    
            """Get current system health metrics."""
    
            
    
            cpu_summary = self.metrics_collector.get_metric_summary("system_cpu_usage_percent", 5)
    
            memory_summary = self.metrics_collector.get_metric_summary("system_memory_usage_bytes", 5)
    
            
    
            return {
    
                "cpu_usage": cpu_summary.get("mean", 0),
    
                "memory_usage_gb": memory_summary.get("mean", 0) / (1024**3),
    
                "status": "healthy"  # Would implement actual health logic
    
            }
    
    
    
    # Dashboard API endpoints
    
    dashboard_provider = DashboardDataProvider()
    
    
    
    @dashboard_router.get("/overview")
    
    async def get_dashboard_overview():
    
        """Get dashboard overview data."""
    
        return await dashboard_provider.get_overview_metrics()
    
    
    
    @dashboard_router.get("/performance")
    
    async def get_performance_data(hours: int = 24):
    
        """Get performance trend data."""
    
        return await dashboard_provider.get_performance_trends(hours)
    
    
    
    @dashboard_router.get("/business")
    
    async def get_business_insights():
    
        """Get business insights data."""
    
        return await dashboard_provider.get_business_insights()
    
    
    
    @dashboard_router.get("/alerts")
    
    async def get_active_alerts():
    
        """Get active alerts."""
    
        return {
    
            "active_alerts": [
    
                {
    
                    "rule_name": alert.rule_name,
    
                    "severity": alert.severity.value,
    
                    "message": alert.message,
    
                    "timestamp": alert.timestamp,
    
                    "acknowledged": alert.acknowledged
    
                }
    
                for alert in alert_manager.active_alerts.values()
    
            ],
    
            "alert_count": len(alert_manager.active_alerts)
    
        }
    
    

    ๐Ÿ” Troubleshooting Workflows

    Automated Diagnostics

    
    # mcp_server/diagnostics.py
    
    """
    
    Automated diagnostics and troubleshooting for MCP server.
    
    """
    
    import asyncio
    
    import subprocess
    
    from typing import Dict, List, Any, Optional
    
    from dataclasses import dataclass
    
    
    
    @dataclass
    
    class DiagnosticResult:
    
        """Result of a diagnostic check."""
    
        check_name: str
    
        status: str  # "pass", "fail", "warning"
    
        message: str
    
        details: Dict[str, Any]
    
        remediation: Optional[str] = None
    
    
    
    class DiagnosticsEngine:
    
        """Comprehensive diagnostics engine."""
    
        
    
        def __init__(self):
    
            self.diagnostic_checks = []
    
            self._register_default_checks()
    
        
    
        def _register_default_checks(self):
    
            """Register default diagnostic checks."""
    
            
    
            self.diagnostic_checks = [
    
                self._check_database_connectivity,
    
                self._check_azure_services,
    
                self._check_system_resources,
    
                self._check_configuration,
    
                self._check_network_connectivity,
    
                self._check_disk_space,
    
                self._check_log_files,
    
                self._check_security_status
    
            ]
    
        
    
        async def run_full_diagnostics(self) -> List[DiagnosticResult]:
    
            """Run all diagnostic checks."""
    
            
    
            results = []
    
            
    
            for check_func in self.diagnostic_checks:
    
                try:
    
                    result = await check_func()
    
                    results.append(result)
    
                except Exception as e:
    
                    results.append(DiagnosticResult(
    
                        check_name=check_func.__name__,
    
                        status="fail",
    
                        message=f"Diagnostic check failed: {str(e)}",
    
                        details={"exception": str(e)}
    
                    ))
    
            
    
            return results
    
        
    
        async def _check_database_connectivity(self) -> DiagnosticResult:
    
            """Check database connectivity and performance."""
    
            
    
            try:
    
                start_time = time.time()
    
                health = await db_provider.health_check()
    
                duration = time.time() - start_time
    
                
    
                if health["status"] == "healthy":
    
                    if duration > 1.0:
    
                        return DiagnosticResult(
    
                            check_name="database_connectivity",
    
                            status="warning",
    
                            message=f"Database responsive but slow ({duration:.2f}s)",
    
                            details=health,
    
                            remediation="Check database server load and network latency"
    
                        )
    
                    else:
    
                        return DiagnosticResult(
    
                            check_name="database_connectivity",
    
                            status="pass",
    
                            message=f"Database healthy ({duration:.2f}s response time)",
    
                            details=health
    
                        )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="database_connectivity",
    
                        status="fail",
    
                        message="Database not healthy",
    
                        details=health,
    
                        remediation="Check database server status and connection parameters"
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="database_connectivity",
    
                    status="fail",
    
                    message=f"Database connectivity failed: {str(e)}",
    
                    details={"error": str(e)},
    
                    remediation="Verify database server is running and connection parameters are correct"
    
                )
    
        
    
        async def _check_azure_services(self) -> DiagnosticResult:
    
            """Check Azure AI services connectivity."""
    
            
    
            try:
    
                # Test Azure OpenAI connectivity
    
                from azure.identity import DefaultAzureCredential
    
                from azure.ai.projects import AIProjectClient
    
                
    
                credential = DefaultAzureCredential()
    
                project_client = AIProjectClient(
    
                    endpoint=config.azure.project_endpoint,
    
                    credential=credential
    
                )
    
                
    
                # This would perform actual connectivity test
    
                # For now, just check configuration
    
                
    
                if config.azure.is_configured():
    
                    return DiagnosticResult(
    
                        check_name="azure_services",
    
                        status="pass",
    
                        message="Azure services configuration valid",
    
                        details={
    
                            "project_endpoint": config.azure.project_endpoint,
    
                            "openai_endpoint": config.azure.openai_endpoint
    
                        }
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="azure_services",
    
                        status="fail",
    
                        message="Azure services not properly configured",
    
                        details={"missing_config": "Check environment variables"},
    
                        remediation="Ensure all Azure configuration environment variables are set"
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="azure_services",
    
                    status="fail",
    
                    message=f"Azure services check failed: {str(e)}",
    
                    details={"error": str(e)},
    
                    remediation="Check Azure credentials and network connectivity"
    
                )
    
        
    
        async def _check_system_resources(self) -> DiagnosticResult:
    
            """Check system resource usage."""
    
            
    
            try:
    
                import psutil
    
                
    
                cpu_percent = psutil.cpu_percent(interval=1)
    
                memory = psutil.virtual_memory()
    
                disk = psutil.disk_usage('/')
    
                
    
                warnings = []
    
                
    
                if cpu_percent > 85:
    
                    warnings.append(f"High CPU usage: {cpu_percent:.1f}%")
    
                
    
                if memory.percent > 85:
    
                    warnings.append(f"High memory usage: {memory.percent:.1f}%")
    
                
    
                if disk.percent > 85:
    
                    warnings.append(f"High disk usage: {disk.percent:.1f}%")
    
                
    
                details = {
    
                    "cpu_percent": cpu_percent,
    
                    "memory_percent": memory.percent,
    
                    "memory_available_gb": memory.available / (1024**3),
    
                    "disk_percent": disk.percent,
    
                    "disk_free_gb": disk.free / (1024**3)
    
                }
    
                
    
                if warnings:
    
                    return DiagnosticResult(
    
                        check_name="system_resources",
    
                        status="warning",
    
                        message=f"Resource warnings: {'; '.join(warnings)}",
    
                        details=details,
    
                        remediation="Monitor resource usage and consider scaling"
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="system_resources",
    
                        status="pass",
    
                        message="System resources normal",
    
                        details=details
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="system_resources",
    
                    status="fail",
    
                    message=f"Resource check failed: {str(e)}",
    
                    details={"error": str(e)}
    
                )
    
        
    
        async def _check_configuration(self) -> DiagnosticResult:
    
            """Check configuration validity."""
    
            
    
            try:
    
                issues = []
    
                
    
                # Check required environment variables
    
                required_vars = [
    
                    "POSTGRES_HOST", "POSTGRES_PASSWORD",
    
                    "PROJECT_ENDPOINT", "AZURE_CLIENT_ID"
    
                ]
    
                
    
                for var in required_vars:
    
                    if not os.getenv(var):
    
                        issues.append(f"Missing environment variable: {var}")
    
                
    
                # Check configuration consistency
    
                if config.server.enable_health_check and not config.server.applicationinsights_connection_string:
    
                    issues.append("Health check enabled but Application Insights not configured")
    
                
    
                if issues:
    
                    return DiagnosticResult(
    
                        check_name="configuration",
    
                        status="fail",
    
                        message=f"Configuration issues: {'; '.join(issues)}",
    
                        details={"issues": issues},
    
                        remediation="Fix configuration issues and restart service"
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="configuration",
    
                        status="pass",
    
                        message="Configuration valid",
    
                        details={"status": "all_checks_passed"}
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="configuration",
    
                    status="fail",
    
                    message=f"Configuration check failed: {str(e)}",
    
                    details={"error": str(e)}
    
                )
    
    
    
    # Diagnostic API endpoint
    
    @dashboard_router.get("/diagnostics")
    
    async def run_diagnostics():
    
        """Run comprehensive diagnostics."""
    
        
    
        diagnostics_engine = DiagnosticsEngine()
    
        results = await diagnostics_engine.run_full_diagnostics()
    
        
    
        # Summarize results
    
        summary = {
    
            "total_checks": len(results),
    
            "passed": len([r for r in results if r.status == "pass"]),
    
            "warnings": len([r for r in results if r.status == "warning"]),
    
            "failed": len([r for r in results if r.status == "fail"]),
    
            "overall_status": "healthy" if all(r.status in ["pass", "warning"] for r in results) else "unhealthy"
    
        }
    
        
    
        return {
    
            "summary": summary,
    
            "results": [
    
                {
    
                    "check_name": r.check_name,
    
                    "status": r.status,
    
                    "message": r.message,
    
                    "details": r.details,
    
                    "remediation": r.remediation
    
                }
    
                for r in results
    
            ],
    
            "timestamp": time.time()
    
        }
    
    

    Operational Runbooks

    
    # operational-runbooks.yml
    
    runbooks:
    
      
    
      database_connection_failure:
    
        title: "Database Connection Failure"
    
        description: "Steps to resolve database connectivity issues"
    
        severity: "critical"
    
        steps:
    
          - name: "Check database server status"
    
            action: "Verify PostgreSQL service is running"
    
            commands:
    
              - "docker-compose ps postgres"
    
              - "docker-compose logs postgres"
    
          
    
          - name: "Test network connectivity"
    
            action: "Verify network connection to database"
    
            commands:
    
              - "telnet postgres-host 5432"
    
              - "nslookup postgres-host"
    
          
    
          - name: "Check connection pool"
    
            action: "Verify connection pool status"
    
            commands:
    
              - "curl http://localhost:8000/health/detailed"
    
          
    
          - name: "Restart services"
    
            action: "Restart MCP server and database if needed"
    
            commands:
    
              - "docker-compose restart"
    
        
    
        escalation:
    
          - "If issue persists, contact database administrator"
    
          - "Check for infrastructure issues in Azure portal"
    
    
    
      high_error_rate:
    
        title: "High Error Rate Detected"
    
        description: "Steps to investigate and resolve high error rates"
    
        severity: "high"
    
        steps:
    
          - name: "Check recent logs"
    
            action: "Review error logs for patterns"
    
            commands:
    
              - "docker-compose logs mcp_server | grep ERROR | tail -50"
    
          
    
          - name: "Analyze error types"
    
            action: "Categorize errors by type and frequency"
    
            api_endpoint: "/dashboard/diagnostics"
    
          
    
          - name: "Check system resources"
    
            action: "Verify system is not under resource pressure"
    
            commands:
    
              - "curl http://localhost:8000/health/detailed"
    
          
    
          - name: "Review recent deployments"
    
            action: "Check if errors started after recent deployment"
    
            
    
          - name: "Enable debug logging"
    
            action: "Temporarily increase log level for detailed diagnostics"
    
            environment_variable: "LOG_LEVEL=DEBUG"
    
    
    
      slow_performance:
    
        title: "Slow Query Performance"
    
        description: "Steps to diagnose and improve query performance"
    
        severity: "medium"
    
        steps:
    
          - name: "Identify slow queries"
    
            action: "Find queries taking longer than normal"
    
            sql_query: "SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10"
    
          
    
          - name: "Check database indexes"
    
            action: "Verify proper indexes exist"
    
            sql_query: "SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname = 'retail'"
    
          
    
          - name: "Analyze query plans"
    
            action: "Review execution plans for slow queries"
    
            sql_command: "EXPLAIN ANALYZE"
    
          
    
          - name: "Check connection pool"
    
            action: "Verify connection pool is not exhausted"
    
            api_endpoint: "/health/detailed"
    
          
    
          - name: "Monitor resource usage"
    
            action: "Check CPU and memory during queries"
    
            commands:
    
              - "top -p $(pgrep postgres)"
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Application Insights Integration: Complete telemetry and monitoring setup

    โœ… Structured Logging: Production-ready logging with correlation and context

    โœ… Custom Metrics: Business and technical metrics collection and analysis

    โœ… Intelligent Alerting: Proactive alerting with multiple notification channels

    โœ… Operational Dashboards: Real-time monitoring and business insights

    โœ… Troubleshooting Workflows: Automated diagnostics and operational runbooks

    ๐Ÿš€ What's Next

    Continue with Lab 12: Best Practices and Optimization to:

  • Apply performance optimization techniques
  • Implement comprehensive security hardening
  • Learn production deployment best practices
  • Establish cost optimization strategies
  • ๐Ÿ“š Additional Resources

    Azure Monitor

  • Application Insights Documentation - Complete monitoring guide
  • KQL Query Reference - Query language for Application Insights
  • Azure Monitor Workbooks - Custom dashboard creation
  • OpenTelemetry

  • OpenTelemetry Python - Instrumentation guide
  • Distributed Tracing - Tracing concepts
  • Metrics Collection - Metrics best practices
  • Operational Excellence

  • SRE Handbook - Site Reliability Engineering principles
  • Monitoring Best Practices - Industry best practices
  • Incident Response - Incident management guide
  • ---

    Previous: Lab 10: Deployment Strategies

    Next: Lab 12: Best Practices and Optimization

    Application Insights, logging, performance monitoring Monitor

    Monitoring and Observability

    ๐ŸŽฏ What This Lab Covers

    This lab provides comprehensive guidance for implementing monitoring, observability, and alerting for your MCP server in production environments.

    You'll learn to set up Application Insights, create meaningful dashboards, implement effective alerting, and establish troubleshooting workflows for operational excellence.

    Overview

    Effective monitoring and observability are crucial for maintaining reliable MCP servers in production.

    This lab covers the three pillars of observabilityโ€”metrics, logs, and tracesโ€”and shows you how to implement comprehensive monitoring that enables proactive issue detection and rapid problem resolution.

    You'll learn to transform raw telemetry data into actionable insights that help you understand system behavior, optimize performance, and ensure high availability.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Implement comprehensive Application Insights integration for MCP servers
  • Design structured logging patterns for effective troubleshooting
  • Create performance metrics collection and analysis systems
  • Configure intelligent alerting with actionable notifications
  • Build operational dashboards for real-time monitoring
  • Establish effective troubleshooting workflows and runbooks
  • ๐Ÿ“Š Application Insights Integration

    Setting Up Application Insights

    
    # mcp_server/monitoring.py
    
    """
    
    Comprehensive monitoring and telemetry for MCP server.
    
    """
    
    import logging
    
    import time
    
    import psutil
    
    from typing import Dict, Any, Optional
    
    from contextlib import contextmanager
    
    from azure.monitor.opentelemetry import configure_azure_monitor
    
    from opentelemetry import trace, metrics
    
    from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
    
    from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
    
    from opentelemetry.instrumentation.requests import RequestsInstrumentor
    
    
    
    class MCPTelemetryManager:
    
        """Comprehensive telemetry management for MCP server."""
    
        
    
        def __init__(self, connection_string: str):
    
            self.connection_string = connection_string
    
            self.tracer = None
    
            self.meter = None
    
            self.custom_metrics = {}
    
            
    
        def initialize_telemetry(self, app):
    
            """Initialize Application Insights and OpenTelemetry."""
    
            
    
            # Configure Azure Monitor
    
            configure_azure_monitor(
    
                connection_string=self.connection_string,
    
                logger_name="mcp_server",
    
                disable_offline_storage=False
    
            )
    
            
    
            # Get tracer and meter
    
            self.tracer = trace.get_tracer(__name__)
    
            self.meter = metrics.get_meter(__name__)
    
            
    
            # Initialize custom metrics
    
            self._setup_custom_metrics()
    
            
    
            # Instrument FastAPI
    
            FastAPIInstrumentor.instrument_app(app)
    
            
    
            # Instrument database
    
            AsyncPGInstrumentor().instrument()
    
            
    
            # Instrument HTTP requests
    
            RequestsInstrumentor().instrument()
    
            
    
            logging.info("Telemetry initialization complete")
    
        
    
        def _setup_custom_metrics(self):
    
            """Set up custom metrics for MCP server operations."""
    
            
    
            self.custom_metrics = {
    
                # Request metrics
    
                "mcp_requests_total": self.meter.create_counter(
    
                    name="mcp_requests_total",
    
                    description="Total number of MCP requests",
    
                    unit="1"
    
                ),
    
                
    
                "mcp_request_duration": self.meter.create_histogram(
    
                    name="mcp_request_duration_seconds",
    
                    description="MCP request duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                # Database metrics
    
                "database_queries_total": self.meter.create_counter(
    
                    name="database_queries_total",
    
                    description="Total database queries executed",
    
                    unit="1"
    
                ),
    
                
    
                "database_query_duration": self.meter.create_histogram(
    
                    name="database_query_duration_seconds",
    
                    description="Database query duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                "database_connections_active": self.meter.create_up_down_counter(
    
                    name="database_connections_active",
    
                    description="Number of active database connections",
    
                    unit="1"
    
                ),
    
                
    
                # Tool metrics
    
                "tool_executions_total": self.meter.create_counter(
    
                    name="tool_executions_total",
    
                    description="Total tool executions",
    
                    unit="1"
    
                ),
    
                
    
                "tool_execution_duration": self.meter.create_histogram(
    
                    name="tool_execution_duration_seconds",
    
                    description="Tool execution duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                # System metrics
    
                "system_cpu_usage": self.meter.create_gauge(
    
                    name="system_cpu_usage_percent",
    
                    description="System CPU usage percentage",
    
                    unit="%"
    
                ),
    
                
    
                "system_memory_usage": self.meter.create_gauge(
    
                    name="system_memory_usage_bytes",
    
                    description="System memory usage in bytes",
    
                    unit="byte"
    
                ),
    
                
    
                # Error metrics
    
                "errors_total": self.meter.create_counter(
    
                    name="errors_total",
    
                    description="Total number of errors",
    
                    unit="1"
    
                )
    
            }
    
        
    
        @contextmanager
    
        def trace_operation(self, operation_name: str, attributes: Dict[str, Any] = None):
    
            """Create a traced operation with automatic metrics collection."""
    
            
    
            with self.tracer.start_as_current_span(operation_name) as span:
    
                start_time = time.time()
    
                
    
                # Add attributes to span
    
                if attributes:
    
                    for key, value in attributes.items():
    
                        span.set_attribute(key, value)
    
                
    
                try:
    
                    yield span
    
                    
    
                    # Record success metrics
    
                    duration = time.time() - start_time
    
                    
    
                    if "request" in operation_name.lower():
    
                        self.custom_metrics["mcp_requests_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["mcp_request_duration"].record(duration)
    
                    
    
                    elif "query" in operation_name.lower():
    
                        self.custom_metrics["database_queries_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["database_query_duration"].record(duration)
    
                    
    
                    elif "tool" in operation_name.lower():
    
                        self.custom_metrics["tool_executions_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["tool_execution_duration"].record(duration)
    
                    
    
                except Exception as e:
    
                    # Record error
    
                    span.record_exception(e)
    
                    span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
    
                    
    
                    # Record error metrics
    
                    self.custom_metrics["errors_total"].add(1, {
    
                        "operation": operation_name,
    
                        "error_type": type(e).__name__
    
                    })
    
                    
    
                    raise
    
        
    
        def record_system_metrics(self):
    
            """Record system-level metrics."""
    
            
    
            # CPU usage
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            self.custom_metrics["system_cpu_usage"].set(cpu_percent)
    
            
    
            # Memory usage
    
            memory = psutil.virtual_memory()
    
            self.custom_metrics["system_memory_usage"].set(memory.used)
    
            
    
            # Database connections (if available)
    
            if hasattr(db_provider, 'connection_pool') and db_provider.connection_pool:
    
                active_connections = db_provider.connection_pool.get_size()
    
                self.custom_metrics["database_connections_active"].add(active_connections)
    
    
    
    # Global telemetry manager
    
    telemetry_manager = MCPTelemetryManager(
    
        connection_string=config.server.applicationinsights_connection_string
    
    )
    
    

    Enhanced Logging with Structured Data

    
    # mcp_server/logging_config.py
    
    """
    
    Structured logging configuration for MCP server.
    
    """
    
    import logging
    
    import json
    
    import sys
    
    from datetime import datetime
    
    from typing import Dict, Any
    
    import traceback
    
    
    
    class StructuredFormatter(logging.Formatter):
    
        """Custom formatter for structured JSON logging."""
    
        
    
        def format(self, record: logging.LogRecord) -> str:
    
            """Format log record as structured JSON."""
    
            
    
            # Base log structure
    
            log_entry = {
    
                "timestamp": datetime.utcnow().isoformat() + "Z",
    
                "level": record.levelname,
    
                "logger": record.name,
    
                "message": record.getMessage(),
    
                "module": record.module,
    
                "function": record.funcName,
    
                "line": record.lineno
    
            }
    
            
    
            # Add exception information if present
    
            if record.exc_info:
    
                log_entry["exception"] = {
    
                    "type": record.exc_info[0].__name__,
    
                    "message": str(record.exc_info[1]),
    
                    "traceback": traceback.format_exception(*record.exc_info)
    
                }
    
            
    
            # Add custom attributes from extra
    
            if hasattr(record, 'extra_data'):
    
                log_entry.update(record.extra_data)
    
            
    
            # Add correlation ID if available
    
            if hasattr(record, 'correlation_id'):
    
                log_entry["correlation_id"] = record.correlation_id
    
            
    
            # Add user context if available
    
            if hasattr(record, 'user_id'):
    
                log_entry["user_id"] = record.user_id
    
            
    
            if hasattr(record, 'rls_user_id'):
    
                log_entry["rls_user_id"] = record.rls_user_id
    
            
    
            return json.dumps(log_entry, ensure_ascii=False)
    
    
    
    class MCPLogger:
    
        """Enhanced logging utilities for MCP server."""
    
        
    
        def __init__(self, name: str):
    
            self.logger = logging.getLogger(name)
    
            self._setup_structured_logging()
    
        
    
        def _setup_structured_logging(self):
    
            """Configure structured logging."""
    
            
    
            # Remove existing handlers
    
            for handler in self.logger.handlers[:]:
    
                self.logger.removeHandler(handler)
    
            
    
            # Create structured handler
    
            handler = logging.StreamHandler(sys.stdout)
    
            handler.setFormatter(StructuredFormatter())
    
            
    
            self.logger.addHandler(handler)
    
            self.logger.setLevel(logging.INFO)
    
        
    
        def log_mcp_request(
    
            self, 
    
            method: str, 
    
            user_id: str, 
    
            rls_user_id: str,
    
            duration: float = None,
    
            status: str = "success",
    
            **kwargs
    
        ):
    
            """Log MCP request with structured data."""
    
            
    
            extra_data = {
    
                "event_type": "mcp_request",
    
                "method": method,
    
                "user_id": user_id,
    
                "rls_user_id": rls_user_id,
    
                "status": status
    
            }
    
            
    
            if duration is not None:
    
                extra_data["duration_ms"] = duration * 1000
    
            
    
            extra_data.update(kwargs)
    
            
    
            self.logger.info(
    
                f"MCP request: {method} - {status}",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_database_query(
    
            self,
    
            query: str,
    
            duration: float,
    
            row_count: int = None,
    
            user_id: str = None,
    
            **kwargs
    
        ):
    
            """Log database query with performance data."""
    
            
    
            extra_data = {
    
                "event_type": "database_query",
    
                "query_hash": hash(query.strip()),
    
                "duration_ms": duration * 1000,
    
                "query_preview": query[:100] + "..." if len(query) > 100 else query
    
            }
    
            
    
            if row_count is not None:
    
                extra_data["row_count"] = row_count
    
            
    
            if user_id:
    
                extra_data["user_id"] = user_id
    
            
    
            extra_data.update(kwargs)
    
            
    
            level = logging.WARNING if duration > 1.0 else logging.INFO
    
            
    
            self.logger.log(
    
                level,
    
                f"Database query executed ({duration*1000:.2f}ms)",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_security_event(
    
            self,
    
            event_type: str,
    
            user_id: str = None,
    
            ip_address: str = None,
    
            success: bool = True,
    
            details: Dict[str, Any] = None
    
        ):
    
            """Log security-related events."""
    
            
    
            extra_data = {
    
                "event_type": "security_event",
    
                "security_event_type": event_type,
    
                "success": success
    
            }
    
            
    
            if user_id:
    
                extra_data["user_id"] = user_id
    
            
    
            if ip_address:
    
                extra_data["ip_address"] = ip_address
    
            
    
            if details:
    
                extra_data["details"] = details
    
            
    
            level = logging.INFO if success else logging.WARNING
    
            
    
            self.logger.log(
    
                level,
    
                f"Security event: {event_type} - {'success' if success else 'failure'}",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_performance_metric(
    
            self,
    
            metric_name: str,
    
            value: float,
    
            unit: str = "count",
    
            dimensions: Dict[str, str] = None
    
        ):
    
            """Log custom performance metrics."""
    
            
    
            extra_data = {
    
                "event_type": "performance_metric",
    
                "metric_name": metric_name,
    
                "value": value,
    
                "unit": unit
    
            }
    
            
    
            if dimensions:
    
                extra_data["dimensions"] = dimensions
    
            
    
            self.logger.info(
    
                f"Performance metric: {metric_name} = {value} {unit}",
    
                extra={"extra_data": extra_data}
    
            )
    
    
    
    # Global logger instance
    
    mcp_logger = MCPLogger("mcp_server")
    
    

    Custom Metrics Collection

    
    # mcp_server/metrics_collector.py
    
    """
    
    Custom metrics collection for business and operational insights.
    
    """
    
    import asyncio
    
    import time
    
    from typing import Dict, Any, List
    
    from dataclasses import dataclass
    
    from collections import defaultdict, deque
    
    import statistics
    
    
    
    @dataclass
    
    class MetricPoint:
    
        """Individual metric data point."""
    
        timestamp: float
    
        value: float
    
        dimensions: Dict[str, str]
    
    
    
    class MetricsCollector:
    
        """Advanced metrics collection and analysis."""
    
        
    
        def __init__(self, retention_minutes: int = 60):
    
            self.retention_seconds = retention_minutes * 60
    
            self.metrics_buffer = defaultdict(lambda: deque(maxlen=1000))
    
            self.aggregated_metrics = {}
    
            
    
        def record_metric(
    
            self,
    
            name: str,
    
            value: float,
    
            dimensions: Dict[str, str] = None
    
        ):
    
            """Record a metric point."""
    
            
    
            metric_point = MetricPoint(
    
                timestamp=time.time(),
    
                value=value,
    
                dimensions=dimensions or {}
    
            )
    
            
    
            self.metrics_buffer[name].append(metric_point)
    
            self._cleanup_old_metrics(name)
    
        
    
        def _cleanup_old_metrics(self, metric_name: str):
    
            """Remove metrics older than retention period."""
    
            
    
            cutoff_time = time.time() - self.retention_seconds
    
            buffer = self.metrics_buffer[metric_name]
    
            
    
            while buffer and buffer[0].timestamp < cutoff_time:
    
                buffer.popleft()
    
        
    
        def get_metric_summary(
    
            self,
    
            name: str,
    
            time_window_minutes: int = 5
    
        ) -> Dict[str, Any]:
    
            """Get statistical summary of a metric."""
    
            
    
            time_window_seconds = time_window_minutes * 60
    
            cutoff_time = time.time() - time_window_seconds
    
            
    
            relevant_points = [
    
                point for point in self.metrics_buffer[name]
    
                if point.timestamp >= cutoff_time
    
            ]
    
            
    
            if not relevant_points:
    
                return {"error": "No data available"}
    
            
    
            values = [point.value for point in relevant_points]
    
            
    
            return {
    
                "count": len(values),
    
                "min": min(values),
    
                "max": max(values),
    
                "mean": statistics.mean(values),
    
                "median": statistics.median(values),
    
                "p95": self._percentile(values, 95),
    
                "p99": self._percentile(values, 99),
    
                "time_window_minutes": time_window_minutes
    
            }
    
        
    
        def _percentile(self, values: List[float], percentile: float) -> float:
    
            """Calculate percentile value."""
    
            if not values:
    
                return 0
    
            
    
            sorted_values = sorted(values)
    
            index = int((percentile / 100) * len(sorted_values))
    
            index = min(index, len(sorted_values) - 1)
    
            
    
            return sorted_values[index]
    
        
    
        async def collect_business_metrics(self):
    
            """Collect business-specific metrics."""
    
            
    
            try:
    
                # Query execution patterns
    
                query_types = await self._analyze_query_patterns()
    
                for query_type, count in query_types.items():
    
                    self.record_metric(
    
                        "business_queries_by_type",
    
                        count,
    
                        {"query_type": query_type}
    
                    )
    
                
    
                # User activity patterns
    
                user_activity = await self._analyze_user_activity()
    
                for store_id, activity_count in user_activity.items():
    
                    self.record_metric(
    
                        "user_activity_by_store",
    
                        activity_count,
    
                        {"store_id": store_id}
    
                    )
    
                
    
                # Tool usage patterns
    
                tool_usage = await self._analyze_tool_usage()
    
                for tool_name, usage_count in tool_usage.items():
    
                    self.record_metric(
    
                        "tool_usage",
    
                        usage_count,
    
                        {"tool_name": tool_name}
    
                    )
    
                    
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Business metrics collection failed: {e}")
    
        
    
        async def _analyze_query_patterns(self) -> Dict[str, int]:
    
            """Analyze database query patterns."""
    
            
    
            # This would analyze actual query logs
    
            # For demo purposes, returning sample data
    
            return {
    
                "sales_analysis": 45,
    
                "inventory_check": 23,
    
                "customer_lookup": 18,
    
                "product_search": 31
    
            }
    
        
    
        async def _analyze_user_activity(self) -> Dict[str, int]:
    
            """Analyze user activity by store."""
    
            
    
            # This would analyze actual user activity logs
    
            return {
    
                "seattle": 67,
    
                "redmond": 34,
    
                "bellevue": 23,
    
                "online": 89
    
            }
    
        
    
        async def _analyze_tool_usage(self) -> Dict[str, int]:
    
            """Analyze MCP tool usage patterns."""
    
            
    
            return {
    
                "execute_sales_query": 156,
    
                "get_multiple_table_schemas": 45,
    
                "semantic_search_products": 78,
    
                "get_current_utc_date": 23
    
            }
    
    
    
    # Global metrics collector
    
    metrics_collector = MetricsCollector()
    
    

    ๐Ÿ”” Alert Configuration

    Intelligent Alerting System

    
    # mcp_server/alerting.py
    
    """
    
    Intelligent alerting system for MCP server operations.
    
    """
    
    import asyncio
    
    import json
    
    from typing import Dict, List, Any, Callable
    
    from enum import Enum
    
    from dataclasses import dataclass
    
    from azure.communication.email import EmailClient
    
    import smtplib
    
    from email.mime.text import MIMEText
    
    from email.mime.multipart import MIMEMultipart
    
    
    
    class AlertSeverity(Enum):
    
        LOW = "low"
    
        MEDIUM = "medium"
    
        HIGH = "high"
    
        CRITICAL = "critical"
    
    
    
    @dataclass
    
    class AlertRule:
    
        """Alert rule configuration."""
    
        name: str
    
        condition: Callable[[Dict[str, Any]], bool]
    
        severity: AlertSeverity
    
        cooldown_minutes: int
    
        message_template: str
    
        enabled: bool = True
    
    
    
    @dataclass
    
    class Alert:
    
        """Alert instance."""
    
        rule_name: str
    
        severity: AlertSeverity
    
        message: str
    
        timestamp: float
    
        details: Dict[str, Any]
    
        acknowledged: bool = False
    
    
    
    class AlertManager:
    
        """Comprehensive alerting management."""
    
        
    
        def __init__(self):
    
            self.alert_rules = {}
    
            self.active_alerts = {}
    
            self.alert_history = deque(maxlen=1000)
    
            self.notification_channels = {}
    
            self._setup_default_rules()
    
            self._setup_notification_channels()
    
        
    
        def _setup_default_rules(self):
    
            """Set up default alert rules."""
    
            
    
            # Database connection issues
    
            self.add_alert_rule(AlertRule(
    
                name="database_connection_failure",
    
                condition=lambda metrics: metrics.get("database_status") != "healthy",
    
                severity=AlertSeverity.CRITICAL,
    
                cooldown_minutes=5,
    
                message_template="Database connection failure detected. Service may be unavailable."
    
            ))
    
            
    
            # High error rate
    
            self.add_alert_rule(AlertRule(
    
                name="high_error_rate",
    
                condition=lambda metrics: metrics.get("error_rate", 0) > 0.05,  # 5% error rate
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=10,
    
                message_template="High error rate detected: {error_rate:.2%}. Investigate immediately."
    
            ))
    
            
    
            # Slow query performance
    
            self.add_alert_rule(AlertRule(
    
                name="slow_query_performance",
    
                condition=lambda metrics: metrics.get("avg_query_duration", 0) > 2.0,  # 2 seconds
    
                severity=AlertSeverity.MEDIUM,
    
                cooldown_minutes=15,
    
                message_template="Slow query performance detected. Average duration: {avg_query_duration:.2f}s"
    
            ))
    
            
    
            # High CPU usage
    
            self.add_alert_rule(AlertRule(
    
                name="high_cpu_usage",
    
                condition=lambda metrics: metrics.get("cpu_usage", 0) > 85,  # 85% CPU
    
                severity=AlertSeverity.MEDIUM,
    
                cooldown_minutes=10,
    
                message_template="High CPU usage detected: {cpu_usage:.1f}%"
    
            ))
    
            
    
            # Memory usage
    
            self.add_alert_rule(AlertRule(
    
                name="high_memory_usage",
    
                condition=lambda metrics: metrics.get("memory_usage_percent", 0) > 90,  # 90% memory
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=5,
    
                message_template="High memory usage detected: {memory_usage_percent:.1f}%"
    
            ))
    
            
    
            # Authentication failures
    
            self.add_alert_rule(AlertRule(
    
                name="authentication_failures",
    
                condition=lambda metrics: metrics.get("auth_failure_rate", 0) > 0.1,  # 10% failure rate
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=5,
    
                message_template="High authentication failure rate: {auth_failure_rate:.2%}. Possible security incident."
    
            ))
    
        
    
        def _setup_notification_channels(self):
    
            """Set up notification channels."""
    
            
    
            # Email notifications
    
            email_config = {
    
                "smtp_server": os.getenv("SMTP_SERVER", "smtp.office365.com"),
    
                "smtp_port": int(os.getenv("SMTP_PORT", "587")),
    
                "username": os.getenv("SMTP_USERNAME"),
    
                "password": os.getenv("SMTP_PASSWORD"),
    
                "from_address": os.getenv("ALERT_FROM_EMAIL"),
    
                "to_addresses": os.getenv("ALERT_TO_EMAILS", "").split(",")
    
            }
    
            
    
            if email_config["username"] and email_config["password"]:
    
                self.notification_channels["email"] = EmailNotifier(email_config)
    
            
    
            # Microsoft Teams webhook
    
            teams_webhook = os.getenv("TEAMS_WEBHOOK_URL")
    
            if teams_webhook:
    
                self.notification_channels["teams"] = TeamsNotifier(teams_webhook)
    
            
    
            # Slack webhook
    
            slack_webhook = os.getenv("SLACK_WEBHOOK_URL")
    
            if slack_webhook:
    
                self.notification_channels["slack"] = SlackNotifier(slack_webhook)
    
        
    
        def add_alert_rule(self, rule: AlertRule):
    
            """Add or update an alert rule."""
    
            self.alert_rules[rule.name] = rule
    
        
    
        async def evaluate_metrics(self, metrics: Dict[str, Any]):
    
            """Evaluate metrics against alert rules."""
    
            
    
            for rule_name, rule in self.alert_rules.items():
    
                if not rule.enabled:
    
                    continue
    
                
    
                try:
    
                    # Check if rule condition is met
    
                    if rule.condition(metrics):
    
                        await self._trigger_alert(rule, metrics)
    
                    else:
    
                        # Clear alert if condition no longer met
    
                        await self._clear_alert(rule_name)
    
                        
    
                except Exception as e:
    
                    mcp_logger.logger.error(f"Error evaluating alert rule {rule_name}: {e}")
    
        
    
        async def _trigger_alert(self, rule: AlertRule, metrics: Dict[str, Any]):
    
            """Trigger an alert."""
    
            
    
            current_time = time.time()
    
            
    
            # Check cooldown period
    
            if rule.name in self.active_alerts:
    
                last_alert_time = self.active_alerts[rule.name].timestamp
    
                if current_time - last_alert_time < rule.cooldown_minutes * 60:
    
                    return  # Still in cooldown
    
            
    
            # Format alert message
    
            message = rule.message_template.format(**metrics)
    
            
    
            # Create alert
    
            alert = Alert(
    
                rule_name=rule.name,
    
                severity=rule.severity,
    
                message=message,
    
                timestamp=current_time,
    
                details=metrics.copy()
    
            )
    
            
    
            # Store alert
    
            self.active_alerts[rule.name] = alert
    
            self.alert_history.append(alert)
    
            
    
            # Send notifications
    
            await self._send_notifications(alert)
    
            
    
            mcp_logger.log_security_event(
    
                "alert_triggered",
    
                details={
    
                    "rule_name": rule.name,
    
                    "severity": rule.severity.value,
    
                    "message": message
    
                }
    
            )
    
        
    
        async def _clear_alert(self, rule_name: str):
    
            """Clear an active alert."""
    
            
    
            if rule_name in self.active_alerts:
    
                alert = self.active_alerts[rule_name]
    
                del self.active_alerts[rule_name]
    
                
    
                # Send resolution notification for high/critical alerts
    
                if alert.severity in [AlertSeverity.HIGH, AlertSeverity.CRITICAL]:
    
                    resolution_alert = Alert(
    
                        rule_name=rule_name,
    
                        severity=AlertSeverity.LOW,
    
                        message=f"RESOLVED: {alert.message}",
    
                        timestamp=time.time(),
    
                        details={"resolution": True}
    
                    )
    
                    
    
                    await self._send_notifications(resolution_alert)
    
        
    
        async def _send_notifications(self, alert: Alert):
    
            """Send alert notifications through all configured channels."""
    
            
    
            tasks = []
    
            
    
            for channel_name, notifier in self.notification_channels.items():
    
                task = asyncio.create_task(
    
                    notifier.send_notification(alert),
    
                    name=f"notify_{channel_name}"
    
                )
    
                tasks.append(task)
    
            
    
            if tasks:
    
                # Wait for all notifications with timeout
    
                try:
    
                    await asyncio.wait_for(
    
                        asyncio.gather(*tasks, return_exceptions=True),
    
                        timeout=30.0
    
                    )
    
                except asyncio.TimeoutError:
    
                    mcp_logger.logger.warning("Some alert notifications timed out")
    
    
    
    # Notification implementations
    
    class EmailNotifier:
    
        """Email notification handler."""
    
        
    
        def __init__(self, config: Dict[str, Any]):
    
            self.config = config
    
        
    
        async def send_notification(self, alert: Alert):
    
            """Send email notification."""
    
            
    
            try:
    
                msg = MIMEMultipart()
    
                msg['From'] = self.config['from_address']
    
                msg['To'] = ', '.join(self.config['to_addresses'])
    
                msg['Subject'] = f"[{alert.severity.value.upper()}] MCP Server Alert: {alert.rule_name}"
    
                
    
                body = f"""
    
    Alert Details:
    
    - Rule: {alert.rule_name}
    
    - Severity: {alert.severity.value.upper()}
    
    - Time: {datetime.fromtimestamp(alert.timestamp).isoformat()}
    
    - Message: {alert.message}
    
    
    
    Additional Details:
    
    {json.dumps(alert.details, indent=2)}
    
    
    
    This is an automated alert from the MCP Server monitoring system.
    
                """
    
                
    
                msg.attach(MIMEText(body, 'plain'))
    
                
    
                # Send email
    
                with smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port']) as server:
    
                    server.starttls()
    
                    server.login(self.config['username'], self.config['password'])
    
                    server.send_message(msg)
    
                    
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Failed to send email notification: {e}")
    
    
    
    class TeamsNotifier:
    
        """Microsoft Teams notification handler."""
    
        
    
        def __init__(self, webhook_url: str):
    
            self.webhook_url = webhook_url
    
        
    
        async def send_notification(self, alert: Alert):
    
            """Send Teams notification."""
    
            
    
            color_map = {
    
                AlertSeverity.LOW: "28a745",     # Green
    
                AlertSeverity.MEDIUM: "ffc107",   # Yellow
    
                AlertSeverity.HIGH: "fd7e14",     # Orange
    
                AlertSeverity.CRITICAL: "dc3545"  # Red
    
            }
    
            
    
            payload = {
    
                "@type": "MessageCard",
    
                "@context": "http://schema.org/extensions",
    
                "themeColor": color_map.get(alert.severity, "0076D7"),
    
                "summary": f"MCP Server Alert: {alert.rule_name}",
    
                "sections": [{
    
                    "activityTitle": f"๐Ÿšจ {alert.severity.value.upper()} Alert",
    
                    "activitySubtitle": alert.rule_name,
    
                    "text": alert.message,
    
                    "facts": [
    
                        {"name": "Timestamp", "value": datetime.fromtimestamp(alert.timestamp).isoformat()},
    
                        {"name": "Severity", "value": alert.severity.value.upper()}
    
                    ]
    
                }]
    
            }
    
            
    
            try:
    
                async with aiohttp.ClientSession() as session:
    
                    async with session.post(self.webhook_url, json=payload) as response:
    
                        if response.status != 200:
    
                            raise Exception(f"Teams webhook returned {response.status}")
    
                            
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Failed to send Teams notification: {e}")
    
    
    
    # Global alert manager
    
    alert_manager = AlertManager()
    
    

    ๐Ÿ“ˆ Dashboard Creation

    Azure Monitor Workbooks

    
    {
    
      "version": "Notebook/1.0",
    
      "items": [
    
        {
    
          "type": 1,
    
          "content": {
    
            "json": "# MCP Server Operations Dashboard\n\nComprehensive monitoring dashboard for Zava Retail MCP Server operations, performance, and health metrics."
    
          },
    
          "name": "title"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart",
    
            "version": "KqlItem/1.0",
    
            "query": "requests\n| where timestamp >= ago(1h)\n| where name contains \"mcp\"\n| summarize RequestCount = count(), AvgDuration = avg(duration) by bin(timestamp, 5m)\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "MCP Request Volume and Performance",
    
            "timeContext": {
    
              "durationMs": 3600000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "timechart"
    
          },
    
          "name": "request-metrics"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-2",
    
            "version": "KqlItem/1.0",
    
            "query": "customMetrics\n| where name == \"database_query_duration_seconds\"\n| where timestamp >= ago(1h)\n| summarize \n    AvgDuration = avg(value),\n    P95Duration = percentile(value, 95),\n    P99Duration = percentile(value, 99)\n    by bin(timestamp, 5m)\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "Database Query Performance",
    
            "timeContext": {
    
              "durationMs": 3600000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "timechart"
    
          },
    
          "name": "database-performance"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-3",
    
            "version": "KqlItem/1.0",
    
            "query": "exceptions\n| where timestamp >= ago(24h)\n| where method contains \"mcp\"\n| summarize ErrorCount = count() by bin(timestamp, 1h), type\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "Error Rate Analysis",
    
            "timeContext": {
    
              "durationMs": 86400000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "barchart"
    
          },
    
          "name": "error-analysis"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-4",
    
            "version": "KqlItem/1.0",
    
            "query": "customMetrics\n| where name in (\"system_cpu_usage_percent\", \"system_memory_usage_bytes\")\n| where timestamp >= ago(2h)\n| extend MetricType = case(\n    name == \"system_cpu_usage_percent\", \"CPU %\",\n    name == \"system_memory_usage_bytes\", \"Memory GB\",\n    \"Unknown\"\n)\n| extend NormalizedValue = case(\n    name == \"system_memory_usage_bytes\", value / (1024*1024*1024),\n    value\n)\n| summarize AvgValue = avg(NormalizedValue) by bin(timestamp, 5m), MetricType\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "System Resource Usage",
    
            "timeContext": {
    
              "durationMs": 7200000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "linechart"
    
          },
    
          "name": "system-resources"
    
        }
    
      ],
    
      "isLocked": false,
    
      "fallbackResourceIds": [
    
        "/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/microsoft.insights/components/{app-insights-name}"
    
      ]
    
    }
    
    

    Custom Dashboard Implementation

    
    # mcp_server/dashboard.py
    
    """
    
    Custom dashboard data provider for MCP server metrics.
    
    """
    
    from typing import Dict, List, Any
    
    from fastapi import APIRouter, Depends
    
    from datetime import datetime, timedelta
    
    
    
    dashboard_router = APIRouter(prefix="/dashboard", tags=["dashboard"])
    
    
    
    class DashboardDataProvider:
    
        """Provide dashboard data from various sources."""
    
        
    
        def __init__(self):
    
            self.metrics_collector = metrics_collector
    
            self.alert_manager = alert_manager
    
        
    
        async def get_overview_metrics(self) -> Dict[str, Any]:
    
            """Get high-level overview metrics."""
    
            
    
            current_time = time.time()
    
            one_hour_ago = current_time - 3600
    
            
    
            return {
    
                "timestamp": current_time,
    
                "active_alerts": len(self.alert_manager.active_alerts),
    
                "critical_alerts": len([
    
                    alert for alert in self.alert_manager.active_alerts.values()
    
                    if alert.severity == AlertSeverity.CRITICAL
    
                ]),
    
                "requests_last_hour": await self._get_request_count(one_hour_ago),
    
                "avg_response_time": await self._get_avg_response_time(one_hour_ago),
    
                "error_rate": await self._get_error_rate(one_hour_ago),
    
                "database_status": await self._get_database_status(),
    
                "system_health": await self._get_system_health()
    
            }
    
        
    
        async def get_performance_trends(self, hours: int = 24) -> Dict[str, List[Dict]]:
    
            """Get performance trends over time."""
    
            
    
            end_time = time.time()
    
            start_time = end_time - (hours * 3600)
    
            
    
            # Generate hourly data points
    
            data_points = []
    
            current = start_time
    
            
    
            while current < end_time:
    
                hour_start = current
    
                hour_end = current + 3600
    
                
    
                data_points.append({
    
                    "timestamp": current,
    
                    "requests": await self._get_request_count_range(hour_start, hour_end),
    
                    "avg_duration": await self._get_avg_duration_range(hour_start, hour_end),
    
                    "error_count": await self._get_error_count_range(hour_start, hour_end),
    
                    "cpu_usage": await self._get_cpu_usage_range(hour_start, hour_end),
    
                    "memory_usage": await self._get_memory_usage_range(hour_start, hour_end)
    
                })
    
                
    
                current = hour_end
    
            
    
            return {
    
                "time_series": data_points,
    
                "period_hours": hours,
    
                "data_points": len(data_points)
    
            }
    
        
    
        async def get_business_insights(self) -> Dict[str, Any]:
    
            """Get business-specific insights."""
    
            
    
            return {
    
                "top_queries": await self._get_top_queries(),
    
                "store_activity": await self._get_store_activity(),
    
                "tool_usage": await self._get_tool_usage_stats(),
    
                "user_patterns": await self._get_user_patterns(),
    
                "peak_hours": await self._get_peak_hours()
    
            }
    
        
    
        async def _get_request_count(self, since_time: float) -> int:
    
            """Get request count since specified time."""
    
            summary = self.metrics_collector.get_metric_summary(
    
                "mcp_requests_total",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            return summary.get("count", 0)
    
        
    
        async def _get_avg_response_time(self, since_time: float) -> float:
    
            """Get average response time since specified time."""
    
            summary = self.metrics_collector.get_metric_summary(
    
                "mcp_request_duration_seconds",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            return summary.get("mean", 0.0) * 1000  # Convert to milliseconds
    
        
    
        async def _get_error_rate(self, since_time: float) -> float:
    
            """Calculate error rate since specified time."""
    
            
    
            total_requests = await self._get_request_count(since_time)
    
            error_summary = self.metrics_collector.get_metric_summary(
    
                "errors_total",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            error_count = error_summary.get("count", 0)
    
            
    
            if total_requests == 0:
    
                return 0.0
    
            
    
            return error_count / total_requests
    
        
    
        async def _get_database_status(self) -> str:
    
            """Get current database status."""
    
            try:
    
                health = await db_provider.health_check()
    
                return health.get("status", "unknown")
    
            except Exception:
    
                return "unhealthy"
    
        
    
        async def _get_system_health(self) -> Dict[str, Any]:
    
            """Get current system health metrics."""
    
            
    
            cpu_summary = self.metrics_collector.get_metric_summary("system_cpu_usage_percent", 5)
    
            memory_summary = self.metrics_collector.get_metric_summary("system_memory_usage_bytes", 5)
    
            
    
            return {
    
                "cpu_usage": cpu_summary.get("mean", 0),
    
                "memory_usage_gb": memory_summary.get("mean", 0) / (1024**3),
    
                "status": "healthy"  # Would implement actual health logic
    
            }
    
    
    
    # Dashboard API endpoints
    
    dashboard_provider = DashboardDataProvider()
    
    
    
    @dashboard_router.get("/overview")
    
    async def get_dashboard_overview():
    
        """Get dashboard overview data."""
    
        return await dashboard_provider.get_overview_metrics()
    
    
    
    @dashboard_router.get("/performance")
    
    async def get_performance_data(hours: int = 24):
    
        """Get performance trend data."""
    
        return await dashboard_provider.get_performance_trends(hours)
    
    
    
    @dashboard_router.get("/business")
    
    async def get_business_insights():
    
        """Get business insights data."""
    
        return await dashboard_provider.get_business_insights()
    
    
    
    @dashboard_router.get("/alerts")
    
    async def get_active_alerts():
    
        """Get active alerts."""
    
        return {
    
            "active_alerts": [
    
                {
    
                    "rule_name": alert.rule_name,
    
                    "severity": alert.severity.value,
    
                    "message": alert.message,
    
                    "timestamp": alert.timestamp,
    
                    "acknowledged": alert.acknowledged
    
                }
    
                for alert in alert_manager.active_alerts.values()
    
            ],
    
            "alert_count": len(alert_manager.active_alerts)
    
        }
    
    

    ๐Ÿ” Troubleshooting Workflows

    Automated Diagnostics

    
    # mcp_server/diagnostics.py
    
    """
    
    Automated diagnostics and troubleshooting for MCP server.
    
    """
    
    import asyncio
    
    import subprocess
    
    from typing import Dict, List, Any, Optional
    
    from dataclasses import dataclass
    
    
    
    @dataclass
    
    class DiagnosticResult:
    
        """Result of a diagnostic check."""
    
        check_name: str
    
        status: str  # "pass", "fail", "warning"
    
        message: str
    
        details: Dict[str, Any]
    
        remediation: Optional[str] = None
    
    
    
    class DiagnosticsEngine:
    
        """Comprehensive diagnostics engine."""
    
        
    
        def __init__(self):
    
            self.diagnostic_checks = []
    
            self._register_default_checks()
    
        
    
        def _register_default_checks(self):
    
            """Register default diagnostic checks."""
    
            
    
            self.diagnostic_checks = [
    
                self._check_database_connectivity,
    
                self._check_azure_services,
    
                self._check_system_resources,
    
                self._check_configuration,
    
                self._check_network_connectivity,
    
                self._check_disk_space,
    
                self._check_log_files,
    
                self._check_security_status
    
            ]
    
        
    
        async def run_full_diagnostics(self) -> List[DiagnosticResult]:
    
            """Run all diagnostic checks."""
    
            
    
            results = []
    
            
    
            for check_func in self.diagnostic_checks:
    
                try:
    
                    result = await check_func()
    
                    results.append(result)
    
                except Exception as e:
    
                    results.append(DiagnosticResult(
    
                        check_name=check_func.__name__,
    
                        status="fail",
    
                        message=f"Diagnostic check failed: {str(e)}",
    
                        details={"exception": str(e)}
    
                    ))
    
            
    
            return results
    
        
    
        async def _check_database_connectivity(self) -> DiagnosticResult:
    
            """Check database connectivity and performance."""
    
            
    
            try:
    
                start_time = time.time()
    
                health = await db_provider.health_check()
    
                duration = time.time() - start_time
    
                
    
                if health["status"] == "healthy":
    
                    if duration > 1.0:
    
                        return DiagnosticResult(
    
                            check_name="database_connectivity",
    
                            status="warning",
    
                            message=f"Database responsive but slow ({duration:.2f}s)",
    
                            details=health,
    
                            remediation="Check database server load and network latency"
    
                        )
    
                    else:
    
                        return DiagnosticResult(
    
                            check_name="database_connectivity",
    
                            status="pass",
    
                            message=f"Database healthy ({duration:.2f}s response time)",
    
                            details=health
    
                        )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="database_connectivity",
    
                        status="fail",
    
                        message="Database not healthy",
    
                        details=health,
    
                        remediation="Check database server status and connection parameters"
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="database_connectivity",
    
                    status="fail",
    
                    message=f"Database connectivity failed: {str(e)}",
    
                    details={"error": str(e)},
    
                    remediation="Verify database server is running and connection parameters are correct"
    
                )
    
        
    
        async def _check_azure_services(self) -> DiagnosticResult:
    
            """Check Azure AI services connectivity."""
    
            
    
            try:
    
                # Test Azure OpenAI connectivity
    
                from azure.identity import DefaultAzureCredential
    
                from azure.ai.projects import AIProjectClient
    
                
    
                credential = DefaultAzureCredential()
    
                project_client = AIProjectClient(
    
                    endpoint=config.azure.project_endpoint,
    
                    credential=credential
    
                )
    
                
    
                # This would perform actual connectivity test
    
                # For now, just check configuration
    
                
    
                if config.azure.is_configured():
    
                    return DiagnosticResult(
    
                        check_name="azure_services",
    
                        status="pass",
    
                        message="Azure services configuration valid",
    
                        details={
    
                            "project_endpoint": config.azure.project_endpoint,
    
                            "openai_endpoint": config.azure.openai_endpoint
    
                        }
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="azure_services",
    
                        status="fail",
    
                        message="Azure services not properly configured",
    
                        details={"missing_config": "Check environment variables"},
    
                        remediation="Ensure all Azure configuration environment variables are set"
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="azure_services",
    
                    status="fail",
    
                    message=f"Azure services check failed: {str(e)}",
    
                    details={"error": str(e)},
    
                    remediation="Check Azure credentials and network connectivity"
    
                )
    
        
    
        async def _check_system_resources(self) -> DiagnosticResult:
    
            """Check system resource usage."""
    
            
    
            try:
    
                import psutil
    
                
    
                cpu_percent = psutil.cpu_percent(interval=1)
    
                memory = psutil.virtual_memory()
    
                disk = psutil.disk_usage('/')
    
                
    
                warnings = []
    
                
    
                if cpu_percent > 85:
    
                    warnings.append(f"High CPU usage: {cpu_percent:.1f}%")
    
                
    
                if memory.percent > 85:
    
                    warnings.append(f"High memory usage: {memory.percent:.1f}%")
    
                
    
                if disk.percent > 85:
    
                    warnings.append(f"High disk usage: {disk.percent:.1f}%")
    
                
    
                details = {
    
                    "cpu_percent": cpu_percent,
    
                    "memory_percent": memory.percent,
    
                    "memory_available_gb": memory.available / (1024**3),
    
                    "disk_percent": disk.percent,
    
                    "disk_free_gb": disk.free / (1024**3)
    
                }
    
                
    
                if warnings:
    
                    return DiagnosticResult(
    
                        check_name="system_resources",
    
                        status="warning",
    
                        message=f"Resource warnings: {'; '.join(warnings)}",
    
                        details=details,
    
                        remediation="Monitor resource usage and consider scaling"
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="system_resources",
    
                        status="pass",
    
                        message="System resources normal",
    
                        details=details
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="system_resources",
    
                    status="fail",
    
                    message=f"Resource check failed: {str(e)}",
    
                    details={"error": str(e)}
    
                )
    
        
    
        async def _check_configuration(self) -> DiagnosticResult:
    
            """Check configuration validity."""
    
            
    
            try:
    
                issues = []
    
                
    
                # Check required environment variables
    
                required_vars = [
    
                    "POSTGRES_HOST", "POSTGRES_PASSWORD",
    
                    "PROJECT_ENDPOINT", "AZURE_CLIENT_ID"
    
                ]
    
                
    
                for var in required_vars:
    
                    if not os.getenv(var):
    
                        issues.append(f"Missing environment variable: {var}")
    
                
    
                # Check configuration consistency
    
                if config.server.enable_health_check and not config.server.applicationinsights_connection_string:
    
                    issues.append("Health check enabled but Application Insights not configured")
    
                
    
                if issues:
    
                    return DiagnosticResult(
    
                        check_name="configuration",
    
                        status="fail",
    
                        message=f"Configuration issues: {'; '.join(issues)}",
    
                        details={"issues": issues},
    
                        remediation="Fix configuration issues and restart service"
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="configuration",
    
                        status="pass",
    
                        message="Configuration valid",
    
                        details={"status": "all_checks_passed"}
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="configuration",
    
                    status="fail",
    
                    message=f"Configuration check failed: {str(e)}",
    
                    details={"error": str(e)}
    
                )
    
    
    
    # Diagnostic API endpoint
    
    @dashboard_router.get("/diagnostics")
    
    async def run_diagnostics():
    
        """Run comprehensive diagnostics."""
    
        
    
        diagnostics_engine = DiagnosticsEngine()
    
        results = await diagnostics_engine.run_full_diagnostics()
    
        
    
        # Summarize results
    
        summary = {
    
            "total_checks": len(results),
    
            "passed": len([r for r in results if r.status == "pass"]),
    
            "warnings": len([r for r in results if r.status == "warning"]),
    
            "failed": len([r for r in results if r.status == "fail"]),
    
            "overall_status": "healthy" if all(r.status in ["pass", "warning"] for r in results) else "unhealthy"
    
        }
    
        
    
        return {
    
            "summary": summary,
    
            "results": [
    
                {
    
                    "check_name": r.check_name,
    
                    "status": r.status,
    
                    "message": r.message,
    
                    "details": r.details,
    
                    "remediation": r.remediation
    
                }
    
                for r in results
    
            ],
    
            "timestamp": time.time()
    
        }
    
    

    Operational Runbooks

    
    # operational-runbooks.yml
    
    runbooks:
    
      
    
      database_connection_failure:
    
        title: "Database Connection Failure"
    
        description: "Steps to resolve database connectivity issues"
    
        severity: "critical"
    
        steps:
    
          - name: "Check database server status"
    
            action: "Verify PostgreSQL service is running"
    
            commands:
    
              - "docker-compose ps postgres"
    
              - "docker-compose logs postgres"
    
          
    
          - name: "Test network connectivity"
    
            action: "Verify network connection to database"
    
            commands:
    
              - "telnet postgres-host 5432"
    
              - "nslookup postgres-host"
    
          
    
          - name: "Check connection pool"
    
            action: "Verify connection pool status"
    
            commands:
    
              - "curl http://localhost:8000/health/detailed"
    
          
    
          - name: "Restart services"
    
            action: "Restart MCP server and database if needed"
    
            commands:
    
              - "docker-compose restart"
    
        
    
        escalation:
    
          - "If issue persists, contact database administrator"
    
          - "Check for infrastructure issues in Azure portal"
    
    
    
      high_error_rate:
    
        title: "High Error Rate Detected"
    
        description: "Steps to investigate and resolve high error rates"
    
        severity: "high"
    
        steps:
    
          - name: "Check recent logs"
    
            action: "Review error logs for patterns"
    
            commands:
    
              - "docker-compose logs mcp_server | grep ERROR | tail -50"
    
          
    
          - name: "Analyze error types"
    
            action: "Categorize errors by type and frequency"
    
            api_endpoint: "/dashboard/diagnostics"
    
          
    
          - name: "Check system resources"
    
            action: "Verify system is not under resource pressure"
    
            commands:
    
              - "curl http://localhost:8000/health/detailed"
    
          
    
          - name: "Review recent deployments"
    
            action: "Check if errors started after recent deployment"
    
            
    
          - name: "Enable debug logging"
    
            action: "Temporarily increase log level for detailed diagnostics"
    
            environment_variable: "LOG_LEVEL=DEBUG"
    
    
    
      slow_performance:
    
        title: "Slow Query Performance"
    
        description: "Steps to diagnose and improve query performance"
    
        severity: "medium"
    
        steps:
    
          - name: "Identify slow queries"
    
            action: "Find queries taking longer than normal"
    
            sql_query: "SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10"
    
          
    
          - name: "Check database indexes"
    
            action: "Verify proper indexes exist"
    
            sql_query: "SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname = 'retail'"
    
          
    
          - name: "Analyze query plans"
    
            action: "Review execution plans for slow queries"
    
            sql_command: "EXPLAIN ANALYZE"
    
          
    
          - name: "Check connection pool"
    
            action: "Verify connection pool is not exhausted"
    
            api_endpoint: "/health/detailed"
    
          
    
          - name: "Monitor resource usage"
    
            action: "Check CPU and memory during queries"
    
            commands:
    
              - "top -p $(pgrep postgres)"
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this lab, you should have:

    โœ… Application Insights Integration: Complete telemetry and monitoring setup

    โœ… Structured Logging: Production-ready logging with correlation and context

    โœ… Custom Metrics: Business and technical metrics collection and analysis

    โœ… Intelligent Alerting: Proactive alerting with multiple notification channels

    โœ… Operational Dashboards: Real-time monitoring and business insights

    โœ… Troubleshooting Workflows: Automated diagnostics and operational runbooks

    ๐Ÿš€ What's Next

    Continue with Lab 12: Best Practices and Optimization to:

  • Apply performance optimization techniques
  • Implement comprehensive security hardening
  • Learn production deployment best practices
  • Establish cost optimization strategies
  • ๐Ÿ“š Additional Resources

    Azure Monitor

  • Application Insights Documentation - Complete monitoring guide
  • KQL Query Reference - Query language for Application Insights
  • Azure Monitor Workbooks - Custom dashboard creation
  • OpenTelemetry

  • OpenTelemetry Python - Instrumentation guide
  • Distributed Tracing - Tracing concepts
  • Metrics Collection - Metrics best practices
  • Operational Excellence

  • SRE Handbook - Site Reliability Engineering principles
  • Monitoring Best Practices - Industry best practices
  • Incident Response - Incident management guide
  • ---

    Previous: Lab 10: Deployment Strategies

    Next: Lab 12: Best Practices and Optimization

    12 Best Practices and Optimization

    Best Practices and Optimization

    ๐ŸŽฏ What This Lab Covers

    This capstone lab consolidates best practices, optimization techniques, and production guidelines for building robust, scalable, and secure MCP servers with database integration.

    You'll learn from real-world experience and industry standards to ensure your implementation is production-ready.

    Overview

    Building a successful MCP server is more than just getting the code to work.

    This lab covers the essential practices that separate proof-of-concept implementations from production-ready systems that can scale, perform reliably, and maintain security standards.

    These best practices are derived from real-world deployments, community feedback, and lessons learned from enterprise implementations.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Apply performance optimization techniques for MCP servers and databases
  • Implement comprehensive security hardening measures
  • Design scalable architecture patterns for production environments
  • Establish monitoring, maintenance, and operational procedures
  • Optimize costs while maintaining performance and reliability
  • Contribute to the MCP community and ecosystem
  • ๐Ÿš€ Performance Optimization

    Database Performance

    Connection Pool Optimization
    
    # Optimized connection pool configuration
    
    POOL_CONFIG = {
    
        # Size configuration
    
        "min_size": max(2, cpu_count()),           # At least 2, scale with CPU
    
        "max_size": min(20, cpu_count() * 4),     # Cap at reasonable maximum
    
        
    
        # Timing configuration
    
        "max_inactive_connection_lifetime": 300,   # 5 minutes
    
        "command_timeout": 30,                     # 30 seconds
    
        "max_queries": 50000,                      # Rotate connections
    
        
    
        # PostgreSQL settings
    
        "server_settings": {
    
            "application_name": "mcp-server-prod",
    
            "jit": "off",                          # Disable for consistency
    
            "work_mem": "8MB",                     # Optimize for queries
    
            "shared_preload_libraries": "pg_stat_statements",
    
            "log_statement": "mod",                # Log modifications only
    
            "log_min_duration_statement": "1s",   # Log slow queries
    
        }
    
    }
    
    
    Query Optimization Patterns
    
    class QueryOptimizer:
    
        """Database query optimization utilities."""
    
        
    
        def __init__(self):
    
            self.query_cache = {}
    
            self.slow_query_threshold = 1.0  # seconds
    
            
    
        async def execute_optimized_query(
    
            self, 
    
            query: str, 
    
            params: tuple = None,
    
            cache_key: str = None,
    
            cache_ttl: int = 300
    
        ):
    
            """Execute query with optimization and caching."""
    
            
    
            # Check cache first
    
            if cache_key and cache_key in self.query_cache:
    
                cache_entry = self.query_cache[cache_key]
    
                if time.time() - cache_entry['timestamp'] < cache_ttl:
    
                    return cache_entry['result']
    
            
    
            # Execute with monitoring
    
            start_time = time.time()
    
            
    
            try:
    
                async with db_provider.get_connection() as conn:
    
                    # Optimize query execution
    
                    await conn.execute("SET enable_seqscan = off")  # Prefer indexes
    
                    await conn.execute("SET work_mem = '16MB'")     # More memory for this query
    
                    
    
                    result = await conn.fetch(query, *params if params else ())
    
                    
    
                    duration = time.time() - start_time
    
                    
    
                    # Log slow queries
    
                    if duration > self.slow_query_threshold:
    
                        logger.warning(f"Slow query detected: {duration:.2f}s", extra={
    
                            "query": query[:200],
    
                            "duration": duration,
    
                            "params_count": len(params) if params else 0
    
                        })
    
                    
    
                    # Cache successful results
    
                    if cache_key and len(result) < 1000:  # Don't cache large results
    
                        self.query_cache[cache_key] = {
    
                            'result': result,
    
                            'timestamp': time.time()
    
                        }
    
                    
    
                    return result
    
                    
    
            except Exception as e:
    
                logger.error(f"Query optimization failed: {e}")
    
                raise
    
    
    
    # Index recommendations
    
    RECOMMENDED_INDEXES = [
    
        # Core business indexes
    
        "CREATE INDEX CONCURRENTLY idx_orders_store_date ON retail.orders (store_id, order_date DESC);",
    
        "CREATE INDEX CONCURRENTLY idx_order_items_product ON retail.order_items (product_id);",
    
        "CREATE INDEX CONCURRENTLY idx_customers_store_email ON retail.customers (store_id, email);",
    
        
    
        # Analytics indexes
    
        "CREATE INDEX CONCURRENTLY idx_orders_date_amount ON retail.orders (order_date, total_amount);",
    
        "CREATE INDEX CONCURRENTLY idx_products_category_price ON retail.products (category_id, unit_price);",
    
        
    
        # Vector search optimization
    
        "CREATE INDEX CONCURRENTLY idx_embeddings_vector ON retail.product_description_embeddings USING ivfflat (description_embedding vector_cosine_ops) WITH (lists = 100);",
    
    ]
    
    

    Application Performance

    Async Programming Best Practices
    
    import asyncio
    
    from asyncio import Semaphore
    
    from typing import List, Any
    
    
    
    class AsyncOptimizer:
    
        """Async operation optimization patterns."""
    
        
    
        def __init__(self, max_concurrent: int = 10):
    
            self.semaphore = Semaphore(max_concurrent)
    
            self.circuit_breaker = CircuitBreaker()
    
        
    
        async def batch_process(
    
            self, 
    
            items: List[Any], 
    
            process_func: callable,
    
            batch_size: int = 100
    
        ):
    
            """Process items in optimized batches."""
    
            
    
            async def process_batch(batch):
    
                async with self.semaphore:
    
                    return await asyncio.gather(
    
                        *[process_func(item) for item in batch],
    
                        return_exceptions=True
    
                    )
    
            
    
            # Process in batches to avoid overwhelming the system
    
            results = []
    
            for i in range(0, len(items), batch_size):
    
                batch = items[i:i + batch_size]
    
                batch_results = await process_batch(batch)
    
                results.extend(batch_results)
    
                
    
                # Small delay between batches to prevent resource exhaustion
    
                if i + batch_size < len(items):
    
                    await asyncio.sleep(0.1)
    
            
    
            return results
    
        
    
        @circuit_breaker_decorator
    
        async def resilient_operation(self, operation: callable, *args, **kwargs):
    
            """Execute operation with circuit breaker protection."""
    
            return await operation(*args, **kwargs)
    
    
    
    # Circuit breaker implementation
    
    class CircuitBreaker:
    
        """Circuit breaker for external service calls."""
    
        
    
        def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
    
            self.failure_threshold = failure_threshold
    
            self.recovery_timeout = recovery_timeout
    
            self.failure_count = 0
    
            self.last_failure_time = None
    
            self.state = "CLOSED"  # CLOSED, OPEN, HALF_OPEN
    
        
    
        async def call(self, func, *args, **kwargs):
    
            """Execute function with circuit breaker protection."""
    
            
    
            if self.state == "OPEN":
    
                if time.time() - self.last_failure_time > self.recovery_timeout:
    
                    self.state = "HALF_OPEN"
    
                else:
    
                    raise Exception("Circuit breaker is OPEN")
    
            
    
            try:
    
                result = await func(*args, **kwargs)
    
                
    
                # Reset on success
    
                if self.state == "HALF_OPEN":
    
                    self.state = "CLOSED"
    
                    self.failure_count = 0
    
                
    
                return result
    
                
    
            except Exception as e:
    
                self.failure_count += 1
    
                self.last_failure_time = time.time()
    
                
    
                if self.failure_count >= self.failure_threshold:
    
                    self.state = "OPEN"
    
                
    
                raise
    
    

    Caching Strategies

    
    import redis
    
    import pickle
    
    from typing import Union, Optional
    
    
    
    class SmartCache:
    
        """Multi-level caching system."""
    
        
    
        def __init__(self, redis_url: Optional[str] = None):
    
            self.memory_cache = {}
    
            self.redis_client = redis.Redis.from_url(redis_url) if redis_url else None
    
            self.max_memory_items = 1000
    
        
    
        async def get(self, key: str) -> Optional[Any]:
    
            """Get from cache with fallback levels."""
    
            
    
            # Level 1: Memory cache
    
            if key in self.memory_cache:
    
                return self.memory_cache[key]['value']
    
            
    
            # Level 2: Redis cache
    
            if self.redis_client:
    
                try:
    
                    cached_data = self.redis_client.get(key)
    
                    if cached_data:
    
                        value = pickle.loads(cached_data)
    
                        
    
                        # Promote to memory cache
    
                        self._set_memory_cache(key, value)
    
                        return value
    
                except Exception as e:
    
                    logger.warning(f"Redis cache error: {e}")
    
            
    
            return None
    
        
    
        async def set(
    
            self, 
    
            key: str, 
    
            value: Any, 
    
            ttl: int = 300,
    
            cache_level: str = "both"
    
        ):
    
            """Set cache value at specified levels."""
    
            
    
            if cache_level in ["memory", "both"]:
    
                self._set_memory_cache(key, value, ttl)
    
            
    
            if cache_level in ["redis", "both"] and self.redis_client:
    
                try:
    
                    self.redis_client.setex(
    
                        key, 
    
                        ttl, 
    
                        pickle.dumps(value)
    
                    )
    
                except Exception as e:
    
                    logger.warning(f"Redis set error: {e}")
    
        
    
        def _set_memory_cache(self, key: str, value: Any, ttl: int = 300):
    
            """Set value in memory cache with LRU eviction."""
    
            
    
            # Implement LRU eviction
    
            if len(self.memory_cache) >= self.max_memory_items:
    
                oldest_key = min(
    
                    self.memory_cache.keys(),
    
                    key=lambda k: self.memory_cache[k]['timestamp']
    
                )
    
                del self.memory_cache[oldest_key]
    
            
    
            self.memory_cache[key] = {
    
                'value': value,
    
                'timestamp': time.time(),
    
                'ttl': ttl
    
            }
    
    
    
    # Cache key generation
    
    def generate_cache_key(query: str, user_context: str, params: dict = None) -> str:
    
        """Generate consistent cache keys."""
    
        key_components = [
    
            query.strip().lower(),
    
            user_context,
    
            json.dumps(params, sort_keys=True) if params else ""
    
        ]
    
        
    
        key_string = "|".join(key_components)
    
        return hashlib.sha256(key_string.encode()).hexdigest()
    
    

    ๐Ÿ”’ Security Hardening

    Authentication and Authorization

    
    from azure.identity import DefaultAzureCredential, ClientSecretCredential
    
    from azure.keyvault.secrets import SecretClient
    
    import jwt
    
    from typing import Dict, List
    
    
    
    class SecurityManager:
    
        """Comprehensive security management."""
    
        
    
        def __init__(self):
    
            self.key_vault_client = self._setup_key_vault()
    
            self.token_blacklist = set()
    
            
    
        def _setup_key_vault(self) -> SecretClient:
    
            """Initialize Azure Key Vault client."""
    
            credential = DefaultAzureCredential()
    
            vault_url = os.getenv("AZURE_KEY_VAULT_URL")
    
            
    
            if vault_url:
    
                return SecretClient(vault_url=vault_url, credential=credential)
    
            return None
    
        
    
        async def validate_request(self, request_headers: Dict[str, str]) -> Dict[str, Any]:
    
            """Comprehensive request validation."""
    
            
    
            # Extract and validate authentication
    
            auth_token = request_headers.get("authorization", "").replace("Bearer ", "")
    
            if not auth_token:
    
                raise AuthenticationError("Missing authentication token")
    
            
    
            # Validate token
    
            user_context = await self._validate_token(auth_token)
    
            
    
            # Check rate limiting
    
            await self._check_rate_limit(user_context["user_id"])
    
            
    
            # Validate RLS context
    
            rls_user_id = request_headers.get("x-rls-user-id")
    
            if not self._validate_rls_access(user_context, rls_user_id):
    
                raise AuthorizationError("Invalid RLS context for user")
    
            
    
            return {
    
                "user_id": user_context["user_id"],
    
                "roles": user_context["roles"],
    
                "rls_user_id": rls_user_id,
    
                "permissions": user_context["permissions"]
    
            }
    
        
    
        async def _validate_token(self, token: str) -> Dict[str, Any]:
    
            """Validate JWT token."""
    
            
    
            if token in self.token_blacklist:
    
                raise AuthenticationError("Token has been revoked")
    
            
    
            try:
    
                # Get public key from Key Vault or cache
    
                public_key = await self._get_public_key()
    
                
    
                # Decode and validate token
    
                payload = jwt.decode(
    
                    token, 
    
                    public_key, 
    
                    algorithms=["RS256"],
    
                    audience="mcp-server",
    
                    issuer="zava-auth"
    
                )
    
                
    
                return {
    
                    "user_id": payload["sub"],
    
                    "roles": payload.get("roles", []),
    
                    "permissions": payload.get("permissions", []),
    
                    "expires_at": payload["exp"]
    
                }
    
                
    
            except jwt.InvalidTokenError as e:
    
                raise AuthenticationError(f"Invalid token: {e}")
    
        
    
        def _validate_rls_access(self, user_context: Dict, rls_user_id: str) -> bool:
    
            """Validate RLS context access."""
    
            
    
            # Super admins can access any context
    
            if "super_admin" in user_context["roles"]:
    
                return True
    
            
    
            # Store managers can only access their own store
    
            if "store_manager" in user_context["roles"]:
    
                allowed_stores = user_context.get("allowed_stores", [])
    
                return rls_user_id in allowed_stores
    
            
    
            # Regional managers can access multiple stores
    
            if "regional_manager" in user_context["roles"]:
    
                allowed_regions = user_context.get("allowed_regions", [])
    
                return self._check_store_in_regions(rls_user_id, allowed_regions)
    
            
    
            return False
    
    
    
    # Input validation and sanitization
    
    class InputValidator:
    
        """SQL injection prevention and input validation."""
    
        
    
        @staticmethod
    
        def validate_sql_query(query: str) -> bool:
    
            """Validate SQL query for safety."""
    
            
    
            # Forbidden patterns
    
            forbidden_patterns = [
    
                r";\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+",
    
                r"--.*",
    
                r"/\*.*\*/",
    
                r"xp_cmdshell",
    
                r"sp_executesql",
    
                r"EXEC\s*\(",
    
            ]
    
            
    
            query_upper = query.upper()
    
            
    
            for pattern in forbidden_patterns:
    
                if re.search(pattern, query_upper, re.IGNORECASE):
    
                    logger.warning(f"Blocked potentially dangerous query: {pattern}")
    
                    return False
    
            
    
            # Only allow SELECT statements
    
            if not query_upper.strip().startswith("SELECT"):
    
                return False
    
            
    
            return True
    
        
    
        @staticmethod
    
        def sanitize_table_name(table_name: str) -> str:
    
            """Sanitize table name input."""
    
            
    
            # Only allow alphanumeric, underscore, and dot
    
            if not re.match(r"^[a-zA-Z0-9_.]+$", table_name):
    
                raise ValueError("Invalid table name format")
    
            
    
            # Validate against allowed tables
    
            if table_name not in VALID_TABLES:
    
                raise ValueError(f"Table {table_name} not allowed")
    
            
    
            return table_name
    
    

    Data Protection

    
    from cryptography.fernet import Fernet
    
    import hashlib
    
    
    
    class DataProtection:
    
        """Data encryption and protection utilities."""
    
        
    
        def __init__(self):
    
            self.encryption_key = self._get_encryption_key()
    
            self.cipher_suite = Fernet(self.encryption_key)
    
        
    
        def _get_encryption_key(self) -> bytes:
    
            """Get encryption key from secure storage."""
    
            
    
            # In production, get from Azure Key Vault
    
            key_vault_secret = os.getenv("ENCRYPTION_KEY_SECRET_NAME")
    
            if key_vault_secret and self.key_vault_client:
    
                secret = self.key_vault_client.get_secret(key_vault_secret)
    
                return secret.value.encode()
    
            
    
            # Fallback for development (not for production!)
    
            dev_key = os.getenv("DEV_ENCRYPTION_KEY")
    
            if dev_key:
    
                return dev_key.encode()
    
            
    
            raise ValueError("No encryption key available")
    
        
    
        def encrypt_sensitive_data(self, data: str) -> str:
    
            """Encrypt sensitive data."""
    
            return self.cipher_suite.encrypt(data.encode()).decode()
    
        
    
        def decrypt_sensitive_data(self, encrypted_data: str) -> str:
    
            """Decrypt sensitive data."""
    
            return self.cipher_suite.decrypt(encrypted_data.encode()).decode()
    
        
    
        @staticmethod
    
        def hash_password(password: str, salt: str = None) -> tuple:
    
            """Hash password with salt."""
    
            if not salt:
    
                salt = os.urandom(32).hex()
    
            
    
            password_hash = hashlib.pbkdf2_hmac(
    
                'sha256',
    
                password.encode(),
    
                salt.encode(),
    
                100000  # iterations
    
            ).hex()
    
            
    
            return password_hash, salt
    
        
    
        @staticmethod
    
        def mask_sensitive_logs(log_data: dict) -> dict:
    
            """Mask sensitive information in logs."""
    
            
    
            sensitive_fields = [
    
                'password', 'token', 'secret', 'key', 'authorization',
    
                'x-api-key', 'client_secret', 'connection_string'
    
            ]
    
            
    
            masked_data = log_data.copy()
    
            
    
            for field in sensitive_fields:
    
                if field in masked_data:
    
                    value = str(masked_data[field])
    
                    if len(value) > 4:
    
                        masked_data[field] = value[:2] + "*" * (len(value) - 4) + value[-2:]
    
                    else:
    
                        masked_data[field] = "***"
    
            
    
            return masked_data
    
    

    ๐Ÿ“Š Production Deployment Guidelines

    Infrastructure as Code

    
    # azure-pipelines.yml
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - release/*
    
    
    
    variables:
    
      - group: mcp-server-secrets
    
      - name: imageRepository
    
        value: 'zava-mcp-server'
    
      - name: containerRegistry
    
        value: 'zavamcpregistry.azurecr.io'
    
    
    
    stages:
    
    - stage: Build
    
      displayName: Build and Test
    
      jobs:
    
      - job: Build
    
        displayName: Build
    
        pool:
    
          vmImage: ubuntu-latest
    
        
    
        steps:
    
        - task: UsePythonVersion@0
    
          inputs:
    
            versionSpec: '3.11'
    
            displayName: 'Use Python 3.11'
    
        
    
        - script: |
    
            python -m pip install --upgrade pip
    
            pip install -r requirements.lock.txt
    
            pip install pytest pytest-cov
    
          displayName: 'Install dependencies'
    
        
    
        - script: |
    
            pytest tests/ --cov=mcp_server --cov-report=xml
    
          displayName: 'Run tests with coverage'
    
        
    
        - task: PublishCodeCoverageResults@1
    
          inputs:
    
            codeCoverageTool: Cobertura
    
            summaryFileLocation: 'coverage.xml'
    
        
    
        - task: Docker@2
    
          displayName: Build Docker image
    
          inputs:
    
            command: build
    
            repository: $(imageRepository)
    
            dockerfile: Dockerfile
    
            tags: |
    
              $(Build.BuildId)
    
              latest
    
    
    
    - stage: Deploy
    
      displayName: Deploy to Production
    
      dependsOn: Build
    
      condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
      
    
      jobs:
    
      - deployment: DeployProduction
    
        displayName: Deploy to Production
    
        environment: 'production'
    
        pool:
    
          vmImage: ubuntu-latest
    
        
    
        strategy:
    
          runOnce:
    
            deploy:
    
              steps:
    
              - task: AzureContainerApps@1
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  containerAppName: 'zava-mcp-server'
    
                  resourceGroup: '$(resourceGroupName)'
    
                  imageToDeploy: '$(containerRegistry)/$(imageRepository):$(Build.BuildId)'
    
    

    Container Optimization

    
    # Multi-stage Dockerfile for production
    
    FROM python:3.11-slim as builder
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        gcc \
    
        g++ \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install Python dependencies
    
    COPY requirements.lock.txt .
    
    RUN pip install --no-cache-dir --upgrade pip && \
    
        pip install --no-cache-dir -r requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim as production
    
    
    
    # Create non-root user
    
    RUN groupadd -r mcpserver && useradd -r -g mcpserver mcpserver
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Set working directory
    
    WORKDIR /app
    
    
    
    # Copy application code
    
    COPY mcp_server/ ./mcp_server/
    
    COPY --chown=mcpserver:mcpserver . .
    
    
    
    # Set security configurations
    
    RUN chmod -R 755 /app && \
    
        chown -R mcpserver:mcpserver /app
    
    
    
    # Switch to non-root user
    
    USER mcpserver
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    
        CMD curl -f http://localhost:8000/health || exit 1
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Start application
    
    CMD ["python", "-m", "mcp_server.sales_analysis"]
    
    

    Environment Configuration

    
    # Production configuration management
    
    class ProductionConfig:
    
        """Production-specific configuration."""
    
        
    
        def __init__(self):
    
            self.validate_production_requirements()
    
            self.setup_logging()
    
            self.configure_security()
    
        
    
        def validate_production_requirements(self):
    
            """Validate all required production settings."""
    
            
    
            required_settings = [
    
                "AZURE_CLIENT_ID",
    
                "AZURE_CLIENT_SECRET", 
    
                "AZURE_TENANT_ID",
    
                "PROJECT_ENDPOINT",
    
                "AZURE_OPENAI_ENDPOINT",
    
                "POSTGRES_HOST",
    
                "POSTGRES_PASSWORD",
    
                "APPLICATIONINSIGHTS_CONNECTION_STRING"
    
            ]
    
            
    
            missing_settings = [
    
                setting for setting in required_settings 
    
                if not os.getenv(setting)
    
            ]
    
            
    
            if missing_settings:
    
                raise EnvironmentError(
    
                    f"Missing required production settings: {missing_settings}"
    
                )
    
        
    
        def setup_logging(self):
    
            """Configure production logging."""
    
            
    
            logging.basicConfig(
    
                level=logging.INFO,
    
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    
                handlers=[
    
                    logging.StreamHandler(sys.stdout),
    
                    logging.handlers.RotatingFileHandler(
    
                        '/var/log/mcp-server.log',
    
                        maxBytes=50*1024*1024,  # 50MB
    
                        backupCount=5
    
                    )
    
                ]
    
            )
    
            
    
            # Set third-party loggers to WARNING
    
            logging.getLogger('azure').setLevel(logging.WARNING)
    
            logging.getLogger('urllib3').setLevel(logging.WARNING)
    
        
    
        def configure_security(self):
    
            """Configure production security settings."""
    
            
    
            # Disable debug mode
    
            os.environ['DEBUG'] = 'False'
    
            
    
            # Set secure headers
    
            os.environ['SECURE_SSL_REDIRECT'] = 'True'
    
            os.environ['SECURE_HSTS_SECONDS'] = '31536000'
    
            os.environ['SECURE_CONTENT_TYPE_NOSNIFF'] = 'True'
    
            os.environ['SECURE_BROWSER_XSS_FILTER'] = 'True'
    
    

    ๐Ÿ’ฐ Cost Optimization

    Resource Management

    
    class CostOptimizer:
    
        """Cost optimization strategies."""
    
        
    
        def __init__(self):
    
            self.metrics_collector = MetricsCollector()
    
            self.auto_scaler = AutoScaler()
    
        
    
        async def optimize_database_connections(self):
    
            """Dynamically adjust connection pool based on load."""
    
            
    
            current_load = await self.metrics_collector.get_current_load()
    
            
    
            if current_load < 0.3:  # Low load
    
                target_pool_size = max(2, int(current_load * 10))
    
            elif current_load < 0.7:  # Medium load
    
                target_pool_size = max(5, int(current_load * 15))
    
            else:  # High load
    
                target_pool_size = min(20, int(current_load * 25))
    
            
    
            await db_provider.adjust_pool_size(target_pool_size)
    
            
    
            logger.info(f"Adjusted pool size to {target_pool_size} for load {current_load}")
    
        
    
        async def implement_smart_caching(self):
    
            """Implement intelligent caching to reduce compute costs."""
    
            
    
            # Cache expensive operations
    
            expensive_queries = await self.identify_expensive_queries()
    
            
    
            for query in expensive_queries:
    
                cache_key = self.generate_cache_key(query)
    
                ttl = self.calculate_optimal_ttl(query)
    
                
    
                await smart_cache.set(cache_key, None, ttl=ttl)
    
        
    
        def calculate_azure_costs(self) -> Dict[str, float]:
    
            """Calculate estimated Azure resource costs."""
    
            
    
            return {
    
                "container_apps": self.estimate_container_costs(),
    
                "postgresql": self.estimate_database_costs(),
    
                "openai": self.estimate_ai_costs(),
    
                "application_insights": self.estimate_monitoring_costs(),
    
                "storage": self.estimate_storage_costs()
    
            }
    
    
    
    # Auto-scaling configuration
    
    class AutoScaler:
    
        """Automatic scaling based on metrics."""
    
        
    
        async def scale_decision(self) -> str:
    
            """Determine scaling action based on metrics."""
    
            
    
            metrics = await self.collect_scaling_metrics()
    
            
    
            # CPU-based scaling
    
            if metrics['cpu_usage'] > 80:
    
                return "scale_up"
    
            elif metrics['cpu_usage'] < 20 and metrics['instance_count'] > 1:
    
                return "scale_down"
    
            
    
            # Memory-based scaling
    
            if metrics['memory_usage'] > 85:
    
                return "scale_up"
    
            
    
            # Request queue scaling
    
            if metrics['queue_length'] > 100:
    
                return "scale_up"
    
            elif metrics['queue_length'] < 10 and metrics['instance_count'] > 1:
    
                return "scale_down"
    
            
    
            return "no_action"
    
    

    ๐Ÿ”ง Maintenance and Operations

    Health Monitoring

    
    class OperationalHealth:
    
        """Comprehensive operational health monitoring."""
    
        
    
        def __init__(self):
    
            self.alert_manager = AlertManager()
    
            self.health_checks = {}
    
            
    
        async def comprehensive_health_check(self) -> Dict[str, Any]:
    
            """Perform comprehensive system health check."""
    
            
    
            health_report = {
    
                "timestamp": datetime.utcnow().isoformat(),
    
                "overall_status": "healthy",
    
                "components": {}
    
            }
    
            
    
            # Database health
    
            db_health = await self.check_database_health()
    
            health_report["components"]["database"] = db_health
    
            
    
            # External services health
    
            ai_health = await self.check_ai_service_health()
    
            health_report["components"]["ai_service"] = ai_health
    
            
    
            # System resources
    
            system_health = await self.check_system_resources()
    
            health_report["components"]["system"] = system_health
    
            
    
            # Application metrics
    
            app_health = await self.check_application_health()
    
            health_report["components"]["application"] = app_health
    
            
    
            # Determine overall status
    
            failed_components = [
    
                name for name, status in health_report["components"].items()
    
                if status.get("status") != "healthy"
    
            ]
    
            
    
            if failed_components:
    
                health_report["overall_status"] = "unhealthy"
    
                health_report["failed_components"] = failed_components
    
                
    
                # Trigger alerts
    
                await self.alert_manager.send_alert(
    
                    severity="high",
    
                    message=f"Health check failed for: {failed_components}",
    
                    details=health_report
    
                )
    
            
    
            return health_report
    
        
    
        async def check_database_health(self) -> Dict[str, Any]:
    
            """Check database connectivity and performance."""
    
            
    
            try:
    
                start_time = time.time()
    
                
    
                async with db_provider.get_connection() as conn:
    
                    # Basic connectivity
    
                    await conn.fetchval("SELECT 1")
    
                    
    
                    # Check slow queries
    
                    slow_queries = await conn.fetch("""
    
                        SELECT query, mean_exec_time, calls 
    
                        FROM pg_stat_statements 
    
                        WHERE mean_exec_time > 1000 
    
                        ORDER BY mean_exec_time DESC 
    
                        LIMIT 5
    
                    """)
    
                    
    
                    # Check connection count
    
                    connection_count = await conn.fetchval("""
    
                        SELECT count(*) FROM pg_stat_activity 
    
                        WHERE state = 'active'
    
                    """)
    
                    
    
                    response_time = time.time() - start_time
    
                    
    
                    return {
    
                        "status": "healthy",
    
                        "response_time_ms": response_time * 1000,
    
                        "active_connections": connection_count,
    
                        "slow_queries_count": len(slow_queries),
    
                        "pool_size": db_provider.connection_pool.get_size()
    
                    }
    
                    
    
            except Exception as e:
    
                return {
    
                    "status": "unhealthy",
    
                    "error": str(e),
    
                    "last_check": datetime.utcnow().isoformat()
    
                }
    
    
    
    # Automated backup and recovery
    
    class BackupManager:
    
        """Database backup and recovery management."""
    
        
    
        async def create_backup(self, backup_type: str = "full") -> str:
    
            """Create database backup."""
    
            
    
            timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
    
            backup_name = f"zava_backup_{backup_type}_{timestamp}"
    
            
    
            if backup_type == "full":
    
                await self.create_full_backup(backup_name)
    
            elif backup_type == "incremental":
    
                await self.create_incremental_backup(backup_name)
    
            
    
            # Upload to Azure Blob Storage
    
            await self.upload_backup_to_azure(backup_name)
    
            
    
            return backup_name
    
        
    
        async def schedule_automated_backups(self):
    
            """Schedule regular automated backups."""
    
            
    
            # Daily full backup at 2 AM UTC
    
            schedule.every().day.at("02:00").do(
    
                lambda: asyncio.create_task(self.create_backup("full"))
    
            )
    
            
    
            # Hourly incremental backups
    
            schedule.every().hour.do(
    
                lambda: asyncio.create_task(self.create_backup("incremental"))
    
            )
    
    

    ๐ŸŒ Community Contributions

    Open Source Best Practices

    
    # Contributing to MCP Database Integration
    
    
    
    ## Development Guidelines
    
    
    
    ### Code Quality Standards
    
    - Follow PEP 8 for Python code style
    
    - Maintain test coverage above 90%
    
    - Use type hints throughout the codebase
    
    - Write comprehensive docstrings
    
    
    
    ### Testing Requirements
    
    - Unit tests for all new functionality
    
    - Integration tests for database operations
    
    - Performance benchmarks for critical paths
    
    - Security tests for authentication/authorization
    
    
    
    ### Documentation Standards
    
    - Update README.md for any new features
    
    - Add inline code documentation
    
    - Create examples for new tools or patterns
    
    - Maintain API documentation
    
    
    
    ## Security Considerations
    
    
    
    ### Reporting Security Issues
    
    - Report security vulnerabilities privately
    
    - Use encrypted communication channels
    
    - Provide detailed reproduction steps
    
    - Include potential impact assessment
    
    
    
    ### Security Review Process
    
    - All PRs undergo security review
    
    - Static analysis tools required to pass
    
    - Dependency vulnerability scanning
    
    - Manual security testing for critical changes
    
    

    Community Engagement

    
    class CommunityContributor:
    
        """Tools for community engagement and contribution."""
    
        
    
        @staticmethod
    
        def generate_contribution_guide():
    
            """Generate personalized contribution guide."""
    
            
    
            return {
    
                "getting_started": {
    
                    "setup": "Follow setup guide in Lab 03",
    
                    "first_contribution": "Start with documentation improvements",
    
                    "testing": "Run full test suite before submitting PR"
    
                },
    
                
    
                "contribution_areas": {
    
                    "documentation": "Improve learning labs and examples",
    
                    "testing": "Add test cases and improve coverage",
    
                    "features": "Implement new MCP tools and capabilities",
    
                    "performance": "Optimize queries and caching",
    
                    "security": "Enhance security measures and validation"
    
                },
    
                
    
                "community_resources": {
    
                    "discord": "https://discord.com/invite/ByRwuEEgH4",
    
                    "discussions": "GitHub Discussions for Q&A",
    
                    "issues": "GitHub Issues for bug reports",
    
                    "examples": "Share your implementation examples"
    
                }
    
            }
    
        
    
        @staticmethod
    
        def validate_contribution(pr_data: Dict) -> Dict[str, bool]:
    
            """Validate contribution meets standards."""
    
            
    
            return {
    
                "has_tests": "test" in pr_data.get("files_changed", []),
    
                "has_documentation": "README" in str(pr_data.get("files_changed", [])),
    
                "follows_conventions": True,  # Would implement actual checks
    
                "security_reviewed": pr_data.get("security_review", False),
    
                "performance_tested": pr_data.get("benchmark_results", False)
    
            }
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this comprehensive learning path, you should have mastered:

    โœ… Performance Optimization: Database tuning, async patterns, and caching strategies

    โœ… Security Hardening: Authentication, authorization, and data protection

    โœ… Production Deployment: Infrastructure as code and container optimization

    โœ… Cost Management: Resource optimization and intelligent scaling

    โœ… Operational Excellence: Monitoring, maintenance, and automation

    โœ… Community Engagement: Contributing to the MCP ecosystem

    ๐Ÿ† Certification and Next Steps

    Practical Assessment

    Complete this final project to demonstrate your mastery:

    Build a Production-Ready MCP Server that includes:

  • [ ] Multi-tenant retail analytics with RLS
  • [ ] Semantic search with Azure OpenAI
  • [ ] Comprehensive security implementation
  • [ ] Production deployment on Azure
  • [ ] Monitoring and alerting setup
  • [ ] Documentation and testing
  • Advanced Learning Paths

    Continue your MCP journey with:

  • MCP Architecture Patterns: Advanced server architectures
  • Multi-Model Integration: Combining different AI models
  • Enterprise Scale: Large-scale MCP deployments
  • Custom Tool Development: Building specialized MCP tools
  • MCP Ecosystem: Contributing to the broader community
  • Community Recognition

    Share your achievement:

  • GitHub Portfolio: Showcase your implementation
  • Community Contributions: Submit improvements or examples
  • Speaking Opportunities: Present at meetups or conferences
  • Mentoring: Help other developers learn MCP
  • ๐Ÿ“š Additional Resources

    Advanced Topics

  • PostgreSQL Performance Tuning - Database optimization
  • Azure Container Apps Best Practices - Production deployment
  • Python Async Best Practices - Async programming
  • Security Resources

  • OWASP Top 10 - Security vulnerabilities
  • Azure Security Best Practices - Cloud security
  • Python Security Guidelines - Secure coding
  • Community

  • MCP Community Discord - Live discussions
  • GitHub Discussions - Q&A and sharing
  • Stack Overflow - Technical questions
  • ---

    ๐ŸŽ‰ Congratulations! You've completed the comprehensive MCP Database Integration learning path. You now have the knowledge and skills to build production-ready MCP servers that bridge AI assistants with real-world data systems.

    Ready to contribute? Join our community and help others learn MCP by sharing your experiences, contributing code improvements, or creating additional learning resources.

    Performance optimization, security hardening, and production tips Optimize

    Best Practices and Optimization

    ๐ŸŽฏ What This Lab Covers

    This capstone lab consolidates best practices, optimization techniques, and production guidelines for building robust, scalable, and secure MCP servers with database integration.

    You'll learn from real-world experience and industry standards to ensure your implementation is production-ready.

    Overview

    Building a successful MCP server is more than just getting the code to work.

    This lab covers the essential practices that separate proof-of-concept implementations from production-ready systems that can scale, perform reliably, and maintain security standards.

    These best practices are derived from real-world deployments, community feedback, and lessons learned from enterprise implementations.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Apply performance optimization techniques for MCP servers and databases
  • Implement comprehensive security hardening measures
  • Design scalable architecture patterns for production environments
  • Establish monitoring, maintenance, and operational procedures
  • Optimize costs while maintaining performance and reliability
  • Contribute to the MCP community and ecosystem
  • ๐Ÿš€ Performance Optimization

    Database Performance

    Connection Pool Optimization
    
    # Optimized connection pool configuration
    
    POOL_CONFIG = {
    
        # Size configuration
    
        "min_size": max(2, cpu_count()),           # At least 2, scale with CPU
    
        "max_size": min(20, cpu_count() * 4),     # Cap at reasonable maximum
    
        
    
        # Timing configuration
    
        "max_inactive_connection_lifetime": 300,   # 5 minutes
    
        "command_timeout": 30,                     # 30 seconds
    
        "max_queries": 50000,                      # Rotate connections
    
        
    
        # PostgreSQL settings
    
        "server_settings": {
    
            "application_name": "mcp-server-prod",
    
            "jit": "off",                          # Disable for consistency
    
            "work_mem": "8MB",                     # Optimize for queries
    
            "shared_preload_libraries": "pg_stat_statements",
    
            "log_statement": "mod",                # Log modifications only
    
            "log_min_duration_statement": "1s",   # Log slow queries
    
        }
    
    }
    
    
    Query Optimization Patterns
    
    class QueryOptimizer:
    
        """Database query optimization utilities."""
    
        
    
        def __init__(self):
    
            self.query_cache = {}
    
            self.slow_query_threshold = 1.0  # seconds
    
            
    
        async def execute_optimized_query(
    
            self, 
    
            query: str, 
    
            params: tuple = None,
    
            cache_key: str = None,
    
            cache_ttl: int = 300
    
        ):
    
            """Execute query with optimization and caching."""
    
            
    
            # Check cache first
    
            if cache_key and cache_key in self.query_cache:
    
                cache_entry = self.query_cache[cache_key]
    
                if time.time() - cache_entry['timestamp'] < cache_ttl:
    
                    return cache_entry['result']
    
            
    
            # Execute with monitoring
    
            start_time = time.time()
    
            
    
            try:
    
                async with db_provider.get_connection() as conn:
    
                    # Optimize query execution
    
                    await conn.execute("SET enable_seqscan = off")  # Prefer indexes
    
                    await conn.execute("SET work_mem = '16MB'")     # More memory for this query
    
                    
    
                    result = await conn.fetch(query, *params if params else ())
    
                    
    
                    duration = time.time() - start_time
    
                    
    
                    # Log slow queries
    
                    if duration > self.slow_query_threshold:
    
                        logger.warning(f"Slow query detected: {duration:.2f}s", extra={
    
                            "query": query[:200],
    
                            "duration": duration,
    
                            "params_count": len(params) if params else 0
    
                        })
    
                    
    
                    # Cache successful results
    
                    if cache_key and len(result) < 1000:  # Don't cache large results
    
                        self.query_cache[cache_key] = {
    
                            'result': result,
    
                            'timestamp': time.time()
    
                        }
    
                    
    
                    return result
    
                    
    
            except Exception as e:
    
                logger.error(f"Query optimization failed: {e}")
    
                raise
    
    
    
    # Index recommendations
    
    RECOMMENDED_INDEXES = [
    
        # Core business indexes
    
        "CREATE INDEX CONCURRENTLY idx_orders_store_date ON retail.orders (store_id, order_date DESC);",
    
        "CREATE INDEX CONCURRENTLY idx_order_items_product ON retail.order_items (product_id);",
    
        "CREATE INDEX CONCURRENTLY idx_customers_store_email ON retail.customers (store_id, email);",
    
        
    
        # Analytics indexes
    
        "CREATE INDEX CONCURRENTLY idx_orders_date_amount ON retail.orders (order_date, total_amount);",
    
        "CREATE INDEX CONCURRENTLY idx_products_category_price ON retail.products (category_id, unit_price);",
    
        
    
        # Vector search optimization
    
        "CREATE INDEX CONCURRENTLY idx_embeddings_vector ON retail.product_description_embeddings USING ivfflat (description_embedding vector_cosine_ops) WITH (lists = 100);",
    
    ]
    
    

    Application Performance

    Async Programming Best Practices
    
    import asyncio
    
    from asyncio import Semaphore
    
    from typing import List, Any
    
    
    
    class AsyncOptimizer:
    
        """Async operation optimization patterns."""
    
        
    
        def __init__(self, max_concurrent: int = 10):
    
            self.semaphore = Semaphore(max_concurrent)
    
            self.circuit_breaker = CircuitBreaker()
    
        
    
        async def batch_process(
    
            self, 
    
            items: List[Any], 
    
            process_func: callable,
    
            batch_size: int = 100
    
        ):
    
            """Process items in optimized batches."""
    
            
    
            async def process_batch(batch):
    
                async with self.semaphore:
    
                    return await asyncio.gather(
    
                        *[process_func(item) for item in batch],
    
                        return_exceptions=True
    
                    )
    
            
    
            # Process in batches to avoid overwhelming the system
    
            results = []
    
            for i in range(0, len(items), batch_size):
    
                batch = items[i:i + batch_size]
    
                batch_results = await process_batch(batch)
    
                results.extend(batch_results)
    
                
    
                # Small delay between batches to prevent resource exhaustion
    
                if i + batch_size < len(items):
    
                    await asyncio.sleep(0.1)
    
            
    
            return results
    
        
    
        @circuit_breaker_decorator
    
        async def resilient_operation(self, operation: callable, *args, **kwargs):
    
            """Execute operation with circuit breaker protection."""
    
            return await operation(*args, **kwargs)
    
    
    
    # Circuit breaker implementation
    
    class CircuitBreaker:
    
        """Circuit breaker for external service calls."""
    
        
    
        def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
    
            self.failure_threshold = failure_threshold
    
            self.recovery_timeout = recovery_timeout
    
            self.failure_count = 0
    
            self.last_failure_time = None
    
            self.state = "CLOSED"  # CLOSED, OPEN, HALF_OPEN
    
        
    
        async def call(self, func, *args, **kwargs):
    
            """Execute function with circuit breaker protection."""
    
            
    
            if self.state == "OPEN":
    
                if time.time() - self.last_failure_time > self.recovery_timeout:
    
                    self.state = "HALF_OPEN"
    
                else:
    
                    raise Exception("Circuit breaker is OPEN")
    
            
    
            try:
    
                result = await func(*args, **kwargs)
    
                
    
                # Reset on success
    
                if self.state == "HALF_OPEN":
    
                    self.state = "CLOSED"
    
                    self.failure_count = 0
    
                
    
                return result
    
                
    
            except Exception as e:
    
                self.failure_count += 1
    
                self.last_failure_time = time.time()
    
                
    
                if self.failure_count >= self.failure_threshold:
    
                    self.state = "OPEN"
    
                
    
                raise
    
    

    Caching Strategies

    
    import redis
    
    import pickle
    
    from typing import Union, Optional
    
    
    
    class SmartCache:
    
        """Multi-level caching system."""
    
        
    
        def __init__(self, redis_url: Optional[str] = None):
    
            self.memory_cache = {}
    
            self.redis_client = redis.Redis.from_url(redis_url) if redis_url else None
    
            self.max_memory_items = 1000
    
        
    
        async def get(self, key: str) -> Optional[Any]:
    
            """Get from cache with fallback levels."""
    
            
    
            # Level 1: Memory cache
    
            if key in self.memory_cache:
    
                return self.memory_cache[key]['value']
    
            
    
            # Level 2: Redis cache
    
            if self.redis_client:
    
                try:
    
                    cached_data = self.redis_client.get(key)
    
                    if cached_data:
    
                        value = pickle.loads(cached_data)
    
                        
    
                        # Promote to memory cache
    
                        self._set_memory_cache(key, value)
    
                        return value
    
                except Exception as e:
    
                    logger.warning(f"Redis cache error: {e}")
    
            
    
            return None
    
        
    
        async def set(
    
            self, 
    
            key: str, 
    
            value: Any, 
    
            ttl: int = 300,
    
            cache_level: str = "both"
    
        ):
    
            """Set cache value at specified levels."""
    
            
    
            if cache_level in ["memory", "both"]:
    
                self._set_memory_cache(key, value, ttl)
    
            
    
            if cache_level in ["redis", "both"] and self.redis_client:
    
                try:
    
                    self.redis_client.setex(
    
                        key, 
    
                        ttl, 
    
                        pickle.dumps(value)
    
                    )
    
                except Exception as e:
    
                    logger.warning(f"Redis set error: {e}")
    
        
    
        def _set_memory_cache(self, key: str, value: Any, ttl: int = 300):
    
            """Set value in memory cache with LRU eviction."""
    
            
    
            # Implement LRU eviction
    
            if len(self.memory_cache) >= self.max_memory_items:
    
                oldest_key = min(
    
                    self.memory_cache.keys(),
    
                    key=lambda k: self.memory_cache[k]['timestamp']
    
                )
    
                del self.memory_cache[oldest_key]
    
            
    
            self.memory_cache[key] = {
    
                'value': value,
    
                'timestamp': time.time(),
    
                'ttl': ttl
    
            }
    
    
    
    # Cache key generation
    
    def generate_cache_key(query: str, user_context: str, params: dict = None) -> str:
    
        """Generate consistent cache keys."""
    
        key_components = [
    
            query.strip().lower(),
    
            user_context,
    
            json.dumps(params, sort_keys=True) if params else ""
    
        ]
    
        
    
        key_string = "|".join(key_components)
    
        return hashlib.sha256(key_string.encode()).hexdigest()
    
    

    ๐Ÿ”’ Security Hardening

    Authentication and Authorization

    
    from azure.identity import DefaultAzureCredential, ClientSecretCredential
    
    from azure.keyvault.secrets import SecretClient
    
    import jwt
    
    from typing import Dict, List
    
    
    
    class SecurityManager:
    
        """Comprehensive security management."""
    
        
    
        def __init__(self):
    
            self.key_vault_client = self._setup_key_vault()
    
            self.token_blacklist = set()
    
            
    
        def _setup_key_vault(self) -> SecretClient:
    
            """Initialize Azure Key Vault client."""
    
            credential = DefaultAzureCredential()
    
            vault_url = os.getenv("AZURE_KEY_VAULT_URL")
    
            
    
            if vault_url:
    
                return SecretClient(vault_url=vault_url, credential=credential)
    
            return None
    
        
    
        async def validate_request(self, request_headers: Dict[str, str]) -> Dict[str, Any]:
    
            """Comprehensive request validation."""
    
            
    
            # Extract and validate authentication
    
            auth_token = request_headers.get("authorization", "").replace("Bearer ", "")
    
            if not auth_token:
    
                raise AuthenticationError("Missing authentication token")
    
            
    
            # Validate token
    
            user_context = await self._validate_token(auth_token)
    
            
    
            # Check rate limiting
    
            await self._check_rate_limit(user_context["user_id"])
    
            
    
            # Validate RLS context
    
            rls_user_id = request_headers.get("x-rls-user-id")
    
            if not self._validate_rls_access(user_context, rls_user_id):
    
                raise AuthorizationError("Invalid RLS context for user")
    
            
    
            return {
    
                "user_id": user_context["user_id"],
    
                "roles": user_context["roles"],
    
                "rls_user_id": rls_user_id,
    
                "permissions": user_context["permissions"]
    
            }
    
        
    
        async def _validate_token(self, token: str) -> Dict[str, Any]:
    
            """Validate JWT token."""
    
            
    
            if token in self.token_blacklist:
    
                raise AuthenticationError("Token has been revoked")
    
            
    
            try:
    
                # Get public key from Key Vault or cache
    
                public_key = await self._get_public_key()
    
                
    
                # Decode and validate token
    
                payload = jwt.decode(
    
                    token, 
    
                    public_key, 
    
                    algorithms=["RS256"],
    
                    audience="mcp-server",
    
                    issuer="zava-auth"
    
                )
    
                
    
                return {
    
                    "user_id": payload["sub"],
    
                    "roles": payload.get("roles", []),
    
                    "permissions": payload.get("permissions", []),
    
                    "expires_at": payload["exp"]
    
                }
    
                
    
            except jwt.InvalidTokenError as e:
    
                raise AuthenticationError(f"Invalid token: {e}")
    
        
    
        def _validate_rls_access(self, user_context: Dict, rls_user_id: str) -> bool:
    
            """Validate RLS context access."""
    
            
    
            # Super admins can access any context
    
            if "super_admin" in user_context["roles"]:
    
                return True
    
            
    
            # Store managers can only access their own store
    
            if "store_manager" in user_context["roles"]:
    
                allowed_stores = user_context.get("allowed_stores", [])
    
                return rls_user_id in allowed_stores
    
            
    
            # Regional managers can access multiple stores
    
            if "regional_manager" in user_context["roles"]:
    
                allowed_regions = user_context.get("allowed_regions", [])
    
                return self._check_store_in_regions(rls_user_id, allowed_regions)
    
            
    
            return False
    
    
    
    # Input validation and sanitization
    
    class InputValidator:
    
        """SQL injection prevention and input validation."""
    
        
    
        @staticmethod
    
        def validate_sql_query(query: str) -> bool:
    
            """Validate SQL query for safety."""
    
            
    
            # Forbidden patterns
    
            forbidden_patterns = [
    
                r";\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+",
    
                r"--.*",
    
                r"/\*.*\*/",
    
                r"xp_cmdshell",
    
                r"sp_executesql",
    
                r"EXEC\s*\(",
    
            ]
    
            
    
            query_upper = query.upper()
    
            
    
            for pattern in forbidden_patterns:
    
                if re.search(pattern, query_upper, re.IGNORECASE):
    
                    logger.warning(f"Blocked potentially dangerous query: {pattern}")
    
                    return False
    
            
    
            # Only allow SELECT statements
    
            if not query_upper.strip().startswith("SELECT"):
    
                return False
    
            
    
            return True
    
        
    
        @staticmethod
    
        def sanitize_table_name(table_name: str) -> str:
    
            """Sanitize table name input."""
    
            
    
            # Only allow alphanumeric, underscore, and dot
    
            if not re.match(r"^[a-zA-Z0-9_.]+$", table_name):
    
                raise ValueError("Invalid table name format")
    
            
    
            # Validate against allowed tables
    
            if table_name not in VALID_TABLES:
    
                raise ValueError(f"Table {table_name} not allowed")
    
            
    
            return table_name
    
    

    Data Protection

    
    from cryptography.fernet import Fernet
    
    import hashlib
    
    
    
    class DataProtection:
    
        """Data encryption and protection utilities."""
    
        
    
        def __init__(self):
    
            self.encryption_key = self._get_encryption_key()
    
            self.cipher_suite = Fernet(self.encryption_key)
    
        
    
        def _get_encryption_key(self) -> bytes:
    
            """Get encryption key from secure storage."""
    
            
    
            # In production, get from Azure Key Vault
    
            key_vault_secret = os.getenv("ENCRYPTION_KEY_SECRET_NAME")
    
            if key_vault_secret and self.key_vault_client:
    
                secret = self.key_vault_client.get_secret(key_vault_secret)
    
                return secret.value.encode()
    
            
    
            # Fallback for development (not for production!)
    
            dev_key = os.getenv("DEV_ENCRYPTION_KEY")
    
            if dev_key:
    
                return dev_key.encode()
    
            
    
            raise ValueError("No encryption key available")
    
        
    
        def encrypt_sensitive_data(self, data: str) -> str:
    
            """Encrypt sensitive data."""
    
            return self.cipher_suite.encrypt(data.encode()).decode()
    
        
    
        def decrypt_sensitive_data(self, encrypted_data: str) -> str:
    
            """Decrypt sensitive data."""
    
            return self.cipher_suite.decrypt(encrypted_data.encode()).decode()
    
        
    
        @staticmethod
    
        def hash_password(password: str, salt: str = None) -> tuple:
    
            """Hash password with salt."""
    
            if not salt:
    
                salt = os.urandom(32).hex()
    
            
    
            password_hash = hashlib.pbkdf2_hmac(
    
                'sha256',
    
                password.encode(),
    
                salt.encode(),
    
                100000  # iterations
    
            ).hex()
    
            
    
            return password_hash, salt
    
        
    
        @staticmethod
    
        def mask_sensitive_logs(log_data: dict) -> dict:
    
            """Mask sensitive information in logs."""
    
            
    
            sensitive_fields = [
    
                'password', 'token', 'secret', 'key', 'authorization',
    
                'x-api-key', 'client_secret', 'connection_string'
    
            ]
    
            
    
            masked_data = log_data.copy()
    
            
    
            for field in sensitive_fields:
    
                if field in masked_data:
    
                    value = str(masked_data[field])
    
                    if len(value) > 4:
    
                        masked_data[field] = value[:2] + "*" * (len(value) - 4) + value[-2:]
    
                    else:
    
                        masked_data[field] = "***"
    
            
    
            return masked_data
    
    

    ๐Ÿ“Š Production Deployment Guidelines

    Infrastructure as Code

    
    # azure-pipelines.yml
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - release/*
    
    
    
    variables:
    
      - group: mcp-server-secrets
    
      - name: imageRepository
    
        value: 'zava-mcp-server'
    
      - name: containerRegistry
    
        value: 'zavamcpregistry.azurecr.io'
    
    
    
    stages:
    
    - stage: Build
    
      displayName: Build and Test
    
      jobs:
    
      - job: Build
    
        displayName: Build
    
        pool:
    
          vmImage: ubuntu-latest
    
        
    
        steps:
    
        - task: UsePythonVersion@0
    
          inputs:
    
            versionSpec: '3.11'
    
            displayName: 'Use Python 3.11'
    
        
    
        - script: |
    
            python -m pip install --upgrade pip
    
            pip install -r requirements.lock.txt
    
            pip install pytest pytest-cov
    
          displayName: 'Install dependencies'
    
        
    
        - script: |
    
            pytest tests/ --cov=mcp_server --cov-report=xml
    
          displayName: 'Run tests with coverage'
    
        
    
        - task: PublishCodeCoverageResults@1
    
          inputs:
    
            codeCoverageTool: Cobertura
    
            summaryFileLocation: 'coverage.xml'
    
        
    
        - task: Docker@2
    
          displayName: Build Docker image
    
          inputs:
    
            command: build
    
            repository: $(imageRepository)
    
            dockerfile: Dockerfile
    
            tags: |
    
              $(Build.BuildId)
    
              latest
    
    
    
    - stage: Deploy
    
      displayName: Deploy to Production
    
      dependsOn: Build
    
      condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
      
    
      jobs:
    
      - deployment: DeployProduction
    
        displayName: Deploy to Production
    
        environment: 'production'
    
        pool:
    
          vmImage: ubuntu-latest
    
        
    
        strategy:
    
          runOnce:
    
            deploy:
    
              steps:
    
              - task: AzureContainerApps@1
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  containerAppName: 'zava-mcp-server'
    
                  resourceGroup: '$(resourceGroupName)'
    
                  imageToDeploy: '$(containerRegistry)/$(imageRepository):$(Build.BuildId)'
    
    

    Container Optimization

    
    # Multi-stage Dockerfile for production
    
    FROM python:3.11-slim as builder
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        gcc \
    
        g++ \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install Python dependencies
    
    COPY requirements.lock.txt .
    
    RUN pip install --no-cache-dir --upgrade pip && \
    
        pip install --no-cache-dir -r requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim as production
    
    
    
    # Create non-root user
    
    RUN groupadd -r mcpserver && useradd -r -g mcpserver mcpserver
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Set working directory
    
    WORKDIR /app
    
    
    
    # Copy application code
    
    COPY mcp_server/ ./mcp_server/
    
    COPY --chown=mcpserver:mcpserver . .
    
    
    
    # Set security configurations
    
    RUN chmod -R 755 /app && \
    
        chown -R mcpserver:mcpserver /app
    
    
    
    # Switch to non-root user
    
    USER mcpserver
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    
        CMD curl -f http://localhost:8000/health || exit 1
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Start application
    
    CMD ["python", "-m", "mcp_server.sales_analysis"]
    
    

    Environment Configuration

    
    # Production configuration management
    
    class ProductionConfig:
    
        """Production-specific configuration."""
    
        
    
        def __init__(self):
    
            self.validate_production_requirements()
    
            self.setup_logging()
    
            self.configure_security()
    
        
    
        def validate_production_requirements(self):
    
            """Validate all required production settings."""
    
            
    
            required_settings = [
    
                "AZURE_CLIENT_ID",
    
                "AZURE_CLIENT_SECRET", 
    
                "AZURE_TENANT_ID",
    
                "PROJECT_ENDPOINT",
    
                "AZURE_OPENAI_ENDPOINT",
    
                "POSTGRES_HOST",
    
                "POSTGRES_PASSWORD",
    
                "APPLICATIONINSIGHTS_CONNECTION_STRING"
    
            ]
    
            
    
            missing_settings = [
    
                setting for setting in required_settings 
    
                if not os.getenv(setting)
    
            ]
    
            
    
            if missing_settings:
    
                raise EnvironmentError(
    
                    f"Missing required production settings: {missing_settings}"
    
                )
    
        
    
        def setup_logging(self):
    
            """Configure production logging."""
    
            
    
            logging.basicConfig(
    
                level=logging.INFO,
    
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    
                handlers=[
    
                    logging.StreamHandler(sys.stdout),
    
                    logging.handlers.RotatingFileHandler(
    
                        '/var/log/mcp-server.log',
    
                        maxBytes=50*1024*1024,  # 50MB
    
                        backupCount=5
    
                    )
    
                ]
    
            )
    
            
    
            # Set third-party loggers to WARNING
    
            logging.getLogger('azure').setLevel(logging.WARNING)
    
            logging.getLogger('urllib3').setLevel(logging.WARNING)
    
        
    
        def configure_security(self):
    
            """Configure production security settings."""
    
            
    
            # Disable debug mode
    
            os.environ['DEBUG'] = 'False'
    
            
    
            # Set secure headers
    
            os.environ['SECURE_SSL_REDIRECT'] = 'True'
    
            os.environ['SECURE_HSTS_SECONDS'] = '31536000'
    
            os.environ['SECURE_CONTENT_TYPE_NOSNIFF'] = 'True'
    
            os.environ['SECURE_BROWSER_XSS_FILTER'] = 'True'
    
    

    ๐Ÿ’ฐ Cost Optimization

    Resource Management

    
    class CostOptimizer:
    
        """Cost optimization strategies."""
    
        
    
        def __init__(self):
    
            self.metrics_collector = MetricsCollector()
    
            self.auto_scaler = AutoScaler()
    
        
    
        async def optimize_database_connections(self):
    
            """Dynamically adjust connection pool based on load."""
    
            
    
            current_load = await self.metrics_collector.get_current_load()
    
            
    
            if current_load < 0.3:  # Low load
    
                target_pool_size = max(2, int(current_load * 10))
    
            elif current_load < 0.7:  # Medium load
    
                target_pool_size = max(5, int(current_load * 15))
    
            else:  # High load
    
                target_pool_size = min(20, int(current_load * 25))
    
            
    
            await db_provider.adjust_pool_size(target_pool_size)
    
            
    
            logger.info(f"Adjusted pool size to {target_pool_size} for load {current_load}")
    
        
    
        async def implement_smart_caching(self):
    
            """Implement intelligent caching to reduce compute costs."""
    
            
    
            # Cache expensive operations
    
            expensive_queries = await self.identify_expensive_queries()
    
            
    
            for query in expensive_queries:
    
                cache_key = self.generate_cache_key(query)
    
                ttl = self.calculate_optimal_ttl(query)
    
                
    
                await smart_cache.set(cache_key, None, ttl=ttl)
    
        
    
        def calculate_azure_costs(self) -> Dict[str, float]:
    
            """Calculate estimated Azure resource costs."""
    
            
    
            return {
    
                "container_apps": self.estimate_container_costs(),
    
                "postgresql": self.estimate_database_costs(),
    
                "openai": self.estimate_ai_costs(),
    
                "application_insights": self.estimate_monitoring_costs(),
    
                "storage": self.estimate_storage_costs()
    
            }
    
    
    
    # Auto-scaling configuration
    
    class AutoScaler:
    
        """Automatic scaling based on metrics."""
    
        
    
        async def scale_decision(self) -> str:
    
            """Determine scaling action based on metrics."""
    
            
    
            metrics = await self.collect_scaling_metrics()
    
            
    
            # CPU-based scaling
    
            if metrics['cpu_usage'] > 80:
    
                return "scale_up"
    
            elif metrics['cpu_usage'] < 20 and metrics['instance_count'] > 1:
    
                return "scale_down"
    
            
    
            # Memory-based scaling
    
            if metrics['memory_usage'] > 85:
    
                return "scale_up"
    
            
    
            # Request queue scaling
    
            if metrics['queue_length'] > 100:
    
                return "scale_up"
    
            elif metrics['queue_length'] < 10 and metrics['instance_count'] > 1:
    
                return "scale_down"
    
            
    
            return "no_action"
    
    

    ๐Ÿ”ง Maintenance and Operations

    Health Monitoring

    
    class OperationalHealth:
    
        """Comprehensive operational health monitoring."""
    
        
    
        def __init__(self):
    
            self.alert_manager = AlertManager()
    
            self.health_checks = {}
    
            
    
        async def comprehensive_health_check(self) -> Dict[str, Any]:
    
            """Perform comprehensive system health check."""
    
            
    
            health_report = {
    
                "timestamp": datetime.utcnow().isoformat(),
    
                "overall_status": "healthy",
    
                "components": {}
    
            }
    
            
    
            # Database health
    
            db_health = await self.check_database_health()
    
            health_report["components"]["database"] = db_health
    
            
    
            # External services health
    
            ai_health = await self.check_ai_service_health()
    
            health_report["components"]["ai_service"] = ai_health
    
            
    
            # System resources
    
            system_health = await self.check_system_resources()
    
            health_report["components"]["system"] = system_health
    
            
    
            # Application metrics
    
            app_health = await self.check_application_health()
    
            health_report["components"]["application"] = app_health
    
            
    
            # Determine overall status
    
            failed_components = [
    
                name for name, status in health_report["components"].items()
    
                if status.get("status") != "healthy"
    
            ]
    
            
    
            if failed_components:
    
                health_report["overall_status"] = "unhealthy"
    
                health_report["failed_components"] = failed_components
    
                
    
                # Trigger alerts
    
                await self.alert_manager.send_alert(
    
                    severity="high",
    
                    message=f"Health check failed for: {failed_components}",
    
                    details=health_report
    
                )
    
            
    
            return health_report
    
        
    
        async def check_database_health(self) -> Dict[str, Any]:
    
            """Check database connectivity and performance."""
    
            
    
            try:
    
                start_time = time.time()
    
                
    
                async with db_provider.get_connection() as conn:
    
                    # Basic connectivity
    
                    await conn.fetchval("SELECT 1")
    
                    
    
                    # Check slow queries
    
                    slow_queries = await conn.fetch("""
    
                        SELECT query, mean_exec_time, calls 
    
                        FROM pg_stat_statements 
    
                        WHERE mean_exec_time > 1000 
    
                        ORDER BY mean_exec_time DESC 
    
                        LIMIT 5
    
                    """)
    
                    
    
                    # Check connection count
    
                    connection_count = await conn.fetchval("""
    
                        SELECT count(*) FROM pg_stat_activity 
    
                        WHERE state = 'active'
    
                    """)
    
                    
    
                    response_time = time.time() - start_time
    
                    
    
                    return {
    
                        "status": "healthy",
    
                        "response_time_ms": response_time * 1000,
    
                        "active_connections": connection_count,
    
                        "slow_queries_count": len(slow_queries),
    
                        "pool_size": db_provider.connection_pool.get_size()
    
                    }
    
                    
    
            except Exception as e:
    
                return {
    
                    "status": "unhealthy",
    
                    "error": str(e),
    
                    "last_check": datetime.utcnow().isoformat()
    
                }
    
    
    
    # Automated backup and recovery
    
    class BackupManager:
    
        """Database backup and recovery management."""
    
        
    
        async def create_backup(self, backup_type: str = "full") -> str:
    
            """Create database backup."""
    
            
    
            timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
    
            backup_name = f"zava_backup_{backup_type}_{timestamp}"
    
            
    
            if backup_type == "full":
    
                await self.create_full_backup(backup_name)
    
            elif backup_type == "incremental":
    
                await self.create_incremental_backup(backup_name)
    
            
    
            # Upload to Azure Blob Storage
    
            await self.upload_backup_to_azure(backup_name)
    
            
    
            return backup_name
    
        
    
        async def schedule_automated_backups(self):
    
            """Schedule regular automated backups."""
    
            
    
            # Daily full backup at 2 AM UTC
    
            schedule.every().day.at("02:00").do(
    
                lambda: asyncio.create_task(self.create_backup("full"))
    
            )
    
            
    
            # Hourly incremental backups
    
            schedule.every().hour.do(
    
                lambda: asyncio.create_task(self.create_backup("incremental"))
    
            )
    
    

    ๐ŸŒ Community Contributions

    Open Source Best Practices

    
    # Contributing to MCP Database Integration
    
    
    
    ## Development Guidelines
    
    
    
    ### Code Quality Standards
    
    - Follow PEP 8 for Python code style
    
    - Maintain test coverage above 90%
    
    - Use type hints throughout the codebase
    
    - Write comprehensive docstrings
    
    
    
    ### Testing Requirements
    
    - Unit tests for all new functionality
    
    - Integration tests for database operations
    
    - Performance benchmarks for critical paths
    
    - Security tests for authentication/authorization
    
    
    
    ### Documentation Standards
    
    - Update README.md for any new features
    
    - Add inline code documentation
    
    - Create examples for new tools or patterns
    
    - Maintain API documentation
    
    
    
    ## Security Considerations
    
    
    
    ### Reporting Security Issues
    
    - Report security vulnerabilities privately
    
    - Use encrypted communication channels
    
    - Provide detailed reproduction steps
    
    - Include potential impact assessment
    
    
    
    ### Security Review Process
    
    - All PRs undergo security review
    
    - Static analysis tools required to pass
    
    - Dependency vulnerability scanning
    
    - Manual security testing for critical changes
    
    

    Community Engagement

    
    class CommunityContributor:
    
        """Tools for community engagement and contribution."""
    
        
    
        @staticmethod
    
        def generate_contribution_guide():
    
            """Generate personalized contribution guide."""
    
            
    
            return {
    
                "getting_started": {
    
                    "setup": "Follow setup guide in Lab 03",
    
                    "first_contribution": "Start with documentation improvements",
    
                    "testing": "Run full test suite before submitting PR"
    
                },
    
                
    
                "contribution_areas": {
    
                    "documentation": "Improve learning labs and examples",
    
                    "testing": "Add test cases and improve coverage",
    
                    "features": "Implement new MCP tools and capabilities",
    
                    "performance": "Optimize queries and caching",
    
                    "security": "Enhance security measures and validation"
    
                },
    
                
    
                "community_resources": {
    
                    "discord": "https://discord.com/invite/ByRwuEEgH4",
    
                    "discussions": "GitHub Discussions for Q&A",
    
                    "issues": "GitHub Issues for bug reports",
    
                    "examples": "Share your implementation examples"
    
                }
    
            }
    
        
    
        @staticmethod
    
        def validate_contribution(pr_data: Dict) -> Dict[str, bool]:
    
            """Validate contribution meets standards."""
    
            
    
            return {
    
                "has_tests": "test" in pr_data.get("files_changed", []),
    
                "has_documentation": "README" in str(pr_data.get("files_changed", [])),
    
                "follows_conventions": True,  # Would implement actual checks
    
                "security_reviewed": pr_data.get("security_review", False),
    
                "performance_tested": pr_data.get("benchmark_results", False)
    
            }
    
    

    ๐ŸŽฏ Key Takeaways

    After completing this comprehensive learning path, you should have mastered:

    โœ… Performance Optimization: Database tuning, async patterns, and caching strategies

    โœ… Security Hardening: Authentication, authorization, and data protection

    โœ… Production Deployment: Infrastructure as code and container optimization

    โœ… Cost Management: Resource optimization and intelligent scaling

    โœ… Operational Excellence: Monitoring, maintenance, and automation

    โœ… Community Engagement: Contributing to the MCP ecosystem

    ๐Ÿ† Certification and Next Steps

    Practical Assessment

    Complete this final project to demonstrate your mastery:

    Build a Production-Ready MCP Server that includes:

  • [ ] Multi-tenant retail analytics with RLS
  • [ ] Semantic search with Azure OpenAI
  • [ ] Comprehensive security implementation
  • [ ] Production deployment on Azure
  • [ ] Monitoring and alerting setup
  • [ ] Documentation and testing
  • Advanced Learning Paths

    Continue your MCP journey with:

  • MCP Architecture Patterns: Advanced server architectures
  • Multi-Model Integration: Combining different AI models
  • Enterprise Scale: Large-scale MCP deployments
  • Custom Tool Development: Building specialized MCP tools
  • MCP Ecosystem: Contributing to the broader community
  • Community Recognition

    Share your achievement:

  • GitHub Portfolio: Showcase your implementation
  • Community Contributions: Submit improvements or examples
  • Speaking Opportunities: Present at meetups or conferences
  • Mentoring: Help other developers learn MCP
  • ๐Ÿ“š Additional Resources

    Advanced Topics

  • PostgreSQL Performance Tuning - Database optimization
  • Azure Container Apps Best Practices - Production deployment
  • Python Async Best Practices - Async programming
  • Security Resources

  • OWASP Top 10 - Security vulnerabilities
  • Azure Security Best Practices - Cloud security
  • Python Security Guidelines - Secure coding
  • Community

  • MCP Community Discord - Live discussions
  • GitHub Discussions - Q&A and sharing
  • Stack Overflow - Technical questions
  • ---

    ๐ŸŽ‰ Congratulations! You've completed the comprehensive MCP Database Integration learning path. You now have the knowledge and skills to build production-ready MCP servers that bridge AI assistants with real-world data systems.

    Ready to contribute? Join our community and help others learn MCP by sharing your experiences, contributing code improvements, or creating additional learning resources.

    ๐Ÿ’ป What You'll Build

    By the end of this learning path, you'll have built a complete Zava Retail Analytics MCP Server featuring:

  • Multi-table retail database with customer orders, products, and inventory
  • Row Level Security for store-based data isolation
  • Semantic product search using Azure OpenAI embeddings
  • VS Code AI Chat integration for natural language queries
  • Production-ready deployment with Docker and Azure
  • Comprehensive monitoring with Application Insights
  • ๐ŸŽฏ Prerequisites for Learning

    To get the most out of this learning path, you should have:

  • Programming Experience: Familiarity with Python (preferred) or similar languages
  • Database Knowledge: Basic understanding of SQL and relational databases
  • API Concepts: Understanding of REST APIs and HTTP concepts
  • Development Tools: Experience with command line, Git, and code editors
  • Cloud Basics: (Optional) Basic knowledge of Azure or similar cloud platforms
  • Docker Familiarity: (Optional) Understanding of containerization concepts
  • Required Tools

  • Docker Desktop - For running PostgreSQL and the MCP server
  • Azure CLI - For cloud resource deployment
  • VS Code - For development and MCP integration
  • Git - For version control
  • Python 3.8+ - For MCP server development
  • ๐Ÿ“š Study Guide & Resources

    This learning path includes comprehensive resources to help you navigate effectively:

    Study Guide

    Each lab includes:

  • Clear learning objectives - What you'll achieve
  • Step-by-step instructions - Detailed implementation guides
  • Code examples - Working samples with explanations
  • Exercises - Hands-on practice opportunities
  • Troubleshooting guides - Common issues and solutions
  • Additional resources - Further reading and exploration
  • Prerequisites Check

    Before starting each lab, you'll find:

  • Required knowledge - What you should know beforehand
  • Setup validation - How to verify your environment
  • Time estimates - Expected completion time
  • Learning outcomes - What you'll know after completion
  • Recommended Learning Paths

    Choose your path based on your experience level:

    ๐ŸŸข Beginner Path (New to MCP)

    1. Ensure you have completed 0-10 of MCP for Beginners first

    2. Complete labs 00-03 to reforce your understand foundations

    3. Follow labs 04-06 for hands-on building

    4. Try labs 07-09 for practical usage

    ๐ŸŸก Intermediate Path (Some MCP Experience)

    1. Review labs 00-01 for database-specific concepts

    2. Focus on labs 02-06 for implementation

    3. Dive deep into labs 07-12 for advanced features

    ๐Ÿ”ด Advanced Path (Experienced with MCP)

    1. Skim labs 00-03 for context

    2. Focus on labs 04-09 for database integration

    3. Concentrate on labs 10-12 for production deployment

    ๐Ÿ› ๏ธ How to Use This Learning Path Effectively

    Sequential Learning (Recommended)

    Work through labs in order for a comprehensive understanding:

    1. Read the overview - Understand what you'll learn

    2. Check prerequisites - Ensure you have required knowledge

    3. Follow step-by-step guides - Implement as you learn

    4. Complete exercises - Reinforce your understanding

    5. Review key takeaways - Solidify learning outcomes

    Targeted Learning

    If you need specific skills:

  • Database Integration: Focus on labs 04-06
  • Security Implementation: Concentrate on labs 02, 08, 12
  • AI/Semantic Search: Deep dive into lab 07
  • Production Deployment: Study labs 10-12
  • Hands-on Practice

    Each lab includes:

  • Working code examples - Copy, modify, and experiment
  • Real-world scenarios - Practical retail analytics use cases
  • Progressive complexity - Building from simple to advanced
  • Validation steps - Verify your implementation works
  • ๐ŸŒŸ Community and Support

    Get Help

  • Azure AI Discord: Join for expert support
  • GitHub Repo and Implementation Sample: Deployment Sample and Resources
  • MCP Community: Join broader MCP discussions
  • ๐Ÿš€ Ready to Start?

    Begin your journey with Lab 00: Introduction to MCP Database Integration

    Introduction to MCP Database Integration

    ๐ŸŽฏ What This Lab Covers

    This introduction lab provides a comprehensive overview of building Model Context Protocol (MCP) servers with database integration.

    You'll understand the business case, technical architecture, and real-world applications through the Zava Retail analytics use case at https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail.

    Overview

    Model Context Protocol (MCP) enables AI assistants to securely access and interact with external data sources in real-time. When combined with database integration, MCP unlocks powerful capabilities for data-driven AI applications.

    This learning path teaches you to build production-ready MCP servers that connect AI assistants to retail sales data through PostgreSQL, implementing enterprise patterns like Row Level Security, semantic search, and multi-tenant data access.

    Learning Objectives

    By the end of this lab, you will be able to:

  • Define Model Context Protocol and its core benefits for database integration
  • Identify key components of an MCP server architecture with databases
  • Understand the Zava Retail use case and its business requirements
  • Recognize enterprise patterns for secure, scalable database access
  • List the tools and technologies used throughout this learning path
  • ๐Ÿงญ The Challenge: AI Meets Real-World Data

    Traditional AI Limitations

    Modern AI assistants are incredibly powerful but face significant limitations when working with real-world business data:

    Challenge Description Business Impact --------------- ----------------- ------------------- Static Knowledge AI models trained on fixed datasets can't access current business data Outdated insights, missed opportunities Data Silos Information locked in databases, APIs, and systems AI can't reach Incomplete analysis, fragmented workflows Security Constraints Direct database access raises security and compliance concerns Limited deployment, manual data preparation Complex Queries Business users need technical knowledge to extract data insights Reduced adoption, inefficient processes

    The MCP Solution

    Model Context Protocol addresses these challenges by providing:

  • Real-time Data Access: AI assistants query live databases and APIs
  • Secure Integration: Controlled access with authentication and permissions
  • Natural Language Interface: Business users ask questions in plain English
  • Standardized Protocol: Works across different AI platforms and tools
  • ๐Ÿช Meet Zava Retail: Our Learning Case Study https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail

    Throughout this learning path, we'll build an MCP server for Zava Retail, a fictional DIY retail chain with multiple store locations. This realistic scenario demonstrates enterprise-grade MCP implementation.

    Business Context

    Zava Retail operates:

  • 8 physical stores across Washington state (Seattle, Bellevue, Tacoma, Spokane, Everett, Redmond, Kirkland)
  • 1 online store for e-commerce sales
  • Diverse product catalog including tools, hardware, garden supplies, and building materials
  • Multi-level management with store managers, regional managers, and executives
  • Business Requirements

    Store managers and executives need AI-powered analytics to:

    1. Analyze sales performance across stores and time periods

    2. Track inventory levels and identify restocking needs

    3. Understand customer behavior and purchasing patterns

    4. Discover product insights through semantic search

    5. Generate reports with natural language queries

    6. Maintain data security with role-based access control

    Technical Requirements

    The MCP server must provide:

  • Multi-tenant data access where store managers see only their store's data
  • Flexible querying supporting complex SQL operations
  • Semantic search for product discovery and recommendations
  • Real-time data reflecting current business state
  • Secure authentication with row-level security
  • Scalable architecture supporting multiple concurrent users
  • ๐Ÿ—๏ธ MCP Server Architecture Overview

    Our MCP server implements a layered architecture optimized for database integration:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                    VS Code AI Client                       โ”‚
    
    โ”‚                  (Natural Language Queries)                โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ HTTP/SSE
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                     MCP Server                             โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚   Tool Layer    โ”‚ โ”‚  Security Layer โ”‚ โ”‚  Config Layer โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Query Tools   โ”‚ โ”‚ โ€ข RLS Context   โ”‚ โ”‚ โ€ข Environment โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Schema Tools  โ”‚ โ”‚ โ€ข User Identity โ”‚ โ”‚ โ€ข Connections โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Search Tools  โ”‚ โ”‚ โ€ข Access Controlโ”‚ โ”‚ โ€ข Validation  โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ asyncpg
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                PostgreSQL Database                         โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚  Retail Schema  โ”‚ โ”‚   RLS Policies  โ”‚ โ”‚   pgvector    โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Stores        โ”‚ โ”‚ โ€ข Store-based   โ”‚ โ”‚ โ€ข Embeddings  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Customers     โ”‚ โ”‚   Isolation     โ”‚ โ”‚ โ€ข Similarity  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Products      โ”‚ โ”‚ โ€ข Role Control  โ”‚ โ”‚   Search      โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Orders        โ”‚ โ”‚ โ€ข Audit Logs    โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ REST API
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                  Azure OpenAI                              โ”‚
    
    โ”‚               (Text Embeddings)                            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    Key Components

    1. MCP Server Layer
  • FastMCP Framework: Modern Python MCP server implementation
  • Tool Registration: Declarative tool definitions with type safety
  • Request Context: User identity and session management
  • Error Handling: Robust error management and logging
  • 2. Database Integration Layer
  • Connection Pooling: Efficient asyncpg connection management
  • Schema Provider: Dynamic table schema discovery
  • Query Executor: Secure SQL execution with RLS context
  • Transaction Management: ACID compliance and rollback handling
  • 3. Security Layer
  • Row Level Security: PostgreSQL RLS for multi-tenant data isolation
  • User Identity: Store manager authentication and authorization
  • Access Control: Fine-grained permissions and audit trails
  • Input Validation: SQL injection prevention and query validation
  • 4. AI Enhancement Layer
  • Semantic Search: Vector embeddings for product discovery
  • Azure OpenAI Integration: Text embedding generation
  • Similarity Algorithms: pgvector cosine similarity search
  • Search Optimization: Indexing and performance tuning
  • ๐Ÿ”ง Technology Stack

    Core Technologies

    Component Technology Purpose --------------- ---------------- ------------- MCP Framework FastMCP (Python) Modern MCP server implementation Database PostgreSQL 17 + pgvector Relational data with vector search AI Services Azure OpenAI Text embeddings and language models Containerization Docker + Docker Compose Development environment Cloud Platform Microsoft Azure Production deployment IDE Integration VS Code AI Chat and development workflow

    Development Tools

    Tool Purpose ---------- ------------- asyncpg High-performance PostgreSQL driver Pydantic Data validation and serialization Azure SDK Cloud service integration pytest Testing framework Docker Containerization and deployment

    Production Stack

    Service Azure Resource Purpose ------------- ------------------- ------------- Database Azure Database for PostgreSQL Managed database service Container Azure Container Apps Serverless container hosting AI Services Azure AI Foundry OpenAI models and endpoints Monitoring Application Insights Observability and diagnostics Security Azure Key Vault Secrets and configuration management

    ๐ŸŽฌ Real-World Usage Scenarios

    Let's explore how different users interact with our MCP server:

    Scenario 1: Store Manager Performance Review

    User: Sarah, Seattle Store Manager

    Goal: Analyze last quarter's sales performance

    Natural Language Query:

    > "Show me the top 10 products by revenue for my store in Q4 2024"

    What Happens:

    1. VS Code AI Chat sends query to MCP server

    2. MCP server identifies Sarah's store context (Seattle)

    3. RLS policies filter data to Seattle store only

    4. SQL query generated and executed

    5. Results formatted and returned to AI Chat

    6. AI provides analysis and insights

    Scenario 2: Product Discovery with Semantic Search

    User: Mike, Inventory Manager

    Goal: Find products similar to a customer request

    Natural Language Query:

    > "What products do we sell that are similar to 'waterproof electrical connectors for outdoor use'?"

    What Happens:

    1. Query processed by semantic search tool

    2. Azure OpenAI generates embedding vector

    3. pgvector performs similarity search

    4. Related products ranked by relevance

    5. Results include product details and availability

    6. AI suggests alternatives and bundling opportunities

    Scenario 3: Cross-Store Analytics

    User: Jennifer, Regional Manager

    Goal: Compare performance across all stores

    Natural Language Query:

    > "Compare sales by category for all stores in the last 6 months"

    What Happens:

    1. RLS context set for regional manager access

    2. Complex multi-store query generated

    3. Data aggregated across store locations

    4. Results include trends and comparisons

    5. AI identifies insights and recommendations

    ๐Ÿ”’ Security and Multi-Tenancy Deep Dive

    Our implementation prioritizes enterprise-grade security:

    Row Level Security (RLS)

    PostgreSQL RLS ensures data isolation:

    
    -- Store managers see only their store's data
    
    CREATE POLICY store_manager_policy ON retail.orders
    
      FOR ALL TO store_managers
    
      USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_policy ON retail.orders
    
      FOR ALL TO regional_managers
    
      USING (store_id = ANY(get_user_store_list()));
    
    

    User Identity Management

    Each MCP connection includes:

  • Store Manager ID: Unique identifier for RLS context
  • Role Assignment: Permissions and access levels
  • Session Management: Secure authentication tokens
  • Audit Logging: Complete access history
  • Data Protection

    Multiple layers of security:

  • Connection Encryption: TLS for all database connections
  • SQL Injection Prevention: Parameterized queries only
  • Input Validation: Comprehensive request validation
  • Error Handling: No sensitive data in error messages
  • ๐ŸŽฏ Key Takeaways

    After completing this introduction, you should understand:

    โœ… MCP Value Proposition: How MCP bridges AI assistants and real-world data

    โœ… Business Context: Zava Retail's requirements and challenges

    โœ… Architecture Overview: Key components and their interactions

    โœ… Technology Stack: Tools and frameworks used throughout

    โœ… Security Model: Multi-tenant data access and protection

    โœ… Usage Patterns: Real-world query scenarios and workflows

    ๐Ÿš€ What's Next

    Ready to dive deeper? Continue with:

    Lab 01: Core Architecture Concepts

    Learn about MCP server architecture patterns, database design principles, and the detailed technical implementation that powers our retail analytics solution.

    ๐Ÿ“š Additional Resources

    MCP Documentation

  • MCP Specification - Official protocol documentation
  • MCP for Beginners - Comprehensive MCP learning guide
  • FastMCP Documentation - Python SDK documentation
  • Database Integration

  • PostgreSQL Documentation - Complete PostgreSQL reference
  • pgvector Guide - Vector extension documentation
  • Row Level Security - PostgreSQL RLS guide
  • Azure Services

  • Azure OpenAI Documentation - AI service integration
  • Azure Database for PostgreSQL - Managed database service
  • Azure Container Apps - Serverless containers
  • ---

    Disclaimer: This is a learning exercise using fictional retail data. Always follow your organization's data governance and security policies when implementing similar solutions in production environments.

    ---

    *Master building production-ready MCP servers with database integration through this comprehensive, hands-on learning experience.*

    school Study Guide

    Study Guide

    Model Context Protocol (MCP) for Beginners - Study Guide

    This study guide provides an overview of the repository structure and content for the "Model Context Protocol (MCP) for Beginners" curriculum. Use this guide to navigate the repository efficiently and make the most of the available resources.

    Repository Overview

    The Model Context Protocol (MCP) is a standardized framework for interactions between AI models and client applications.

    Initially created by Anthropic, MCP is now maintained by the broader MCP community through the official GitHub organization.

    This repository provides a comprehensive curriculum with hands-on code examples in C#, Java, JavaScript, Python, and TypeScript, designed for AI developers, system architects, and software engineers.

    Visual Curriculum Map

    
    mindmap
    
      root((MCP for Beginners))
    
        00. Introduction
    
          ::icon(fa fa-book)
    
          (Protocol Overview)
    
          (Standardization Benefits)
    
          (Real-world Use Cases)
    
          (AI Integration Fundamentals)
    
        01. Core Concepts
    
          ::icon(fa fa-puzzle-piece)
    
          (Client-Server Architecture)
    
          (Protocol Components)
    
          (Messaging Patterns)
    
          (Transport Mechanisms)
    
          (Tasks - Experimental)
    
          (Tool Annotations)
    
        02. Security
    
          ::icon(fa fa-shield)
    
          (AI-Specific Threats)
    
          (Best Practices 2025)
    
          (Azure Content Safety)
    
          (Auth & Authorization)
    
          (Microsoft Prompt Shields)
    
          (OWASP MCP Top 10)
    
          (Sherpa Security Workshop)
    
        03. Getting Started
    
          ::icon(fa fa-rocket)
    
          (First Server Implementation)
    
          (Client Development)
    
          (LLM Client Integration)
    
          (VS Code Extensions)
    
          (SSE Server Setup)
    
          (HTTP Streaming)
    
          (AI Toolkit Integration)
    
          (Testing Frameworks)
    
          (Advanced Server Usage)
    
          (Simple Auth)
    
          (Deployment Strategies)
    
          (MCP Hosts Setup)
    
          (MCP Inspector)
    
        04. Practical Implementation
    
          ::icon(fa fa-code)
    
          (Multi-Language SDKs)
    
          (Testing & Debugging)
    
          (Prompt Templates)
    
          (Sample Projects)
    
          (Production Patterns)
    
          (Pagination Strategies)
    
        05. Advanced Topics
    
          ::icon(fa fa-graduation-cap)
    
          (Context Engineering)
    
          (Foundry Agent Integration)
    
          (Multi-modal AI Workflows)
    
          (OAuth2 Authentication)
    
          (Real-time Search)
    
          (Streaming Protocols)
    
          (Root Contexts)
    
          (Routing Strategies)
    
          (Sampling Techniques)
    
          (Scaling Solutions)
    
          (Security Hardening)
    
          (Entra ID Integration)
    
          (Web Search MCP)
    
          (Protocol Features Deep Dive)
    
          (Adversarial Multi-Agent Reasoning)
    
          
    
        06. Community
    
          ::icon(fa fa-users)
    
          (Code Contributions)
    
          (Documentation)
    
          (MCP Client Ecosystem)
    
          (MCP Server Registry)
    
          (Image Generation Tools)
    
          (GitHub Collaboration)
    
        07. Early Adoption
    
          ::icon(fa fa-lightbulb)
    
          (Production Deployments)
    
          (Microsoft MCP Servers)
    
          (Azure MCP Service)
    
          (Enterprise Case Studies)
    
          (Future Roadmap)
    
        08. Best Practices
    
          ::icon(fa fa-check)
    
          (Performance Optimization)
    
          (Fault Tolerance)
    
          (System Resilience)
    
          (Monitoring & Observability)
    
        09. Case Studies
    
          ::icon(fa fa-file-text)
    
          (Azure API Management)
    
          (AI Travel Agent)
    
          (Azure DevOps Integration)
    
          (Documentation MCP)
    
          (GitHub MCP Registry)
    
          (VS Code Integration)
    
          (Real-world Implementations)
    
        10. Hands-on Workshop
    
          ::icon(fa fa-laptop)
    
          (MCP Server Fundamentals)
    
          (Advanced Development)
    
          (AI Toolkit Integration)
    
          (Production Deployment)
    
          (4-Lab Structure)
    
        11. Database Integration Labs
    
          ::icon(fa fa-database)
    
          (PostgreSQL Integration)
    
          (Retail Analytics Use Case)
    
          (Row Level Security)
    
          (Semantic Search)
    
          (Production Deployment)
    
          (13-Lab Structure)
    
          (Hands-on Learning)
    
    

    Repository Structure

    The repository is organized into eleven main sections, each focusing on different aspects of MCP:

    1. Introduction (00-Introduction/)

    - Overview of the Model Context Protocol

    - Why standardization matters in AI pipelines

    - Practical use cases and benefits

    2. Core Concepts (01-CoreConcepts/)

    - Client-server architecture

    - Key protocol components

    - Messaging patterns in MCP

    3. Security (02-Security/)

    - Security threats in MCP-based systems

    - Best practices for securing implementations

    - Authentication and authorization strategies

    - Comprehensive Security Documentation:

    - MCP Security Best Practices 2025

    - Azure Content Safety Implementation Guide

    - MCP Security Controls and Techniques

    - MCP Best Practices Quick Reference

    - Key Security Topics:

    - Prompt injection and tool poisoning attacks

    - Session hijacking and confused deputy problems

    - Token passthrough vulnerabilities

    - Excessive permissions and access control

    - Supply chain security for AI components

    - Microsoft Prompt Shields integration

    4. Getting Started (03-GettingStarted/)

    - Environment setup and configuration

    - Creating basic MCP servers and clients

    - Integration with existing applications

    - Includes sections for:

    - First server implementation

    - Client development

    - LLM client integration

    - VS Code integration

    - Server-Sent Events (SSE) server

    - Advanced server usage

    - HTTP streaming

    - AI Toolkit integration

    - Testing strategies

    - Deployment guidelines

    5. Practical Implementation (04-PracticalImplementation/)

    - Using SDKs across different programming languages

    - Debugging, testing, and validation techniques

    - Crafting reusable prompt templates and workflows

    - Sample projects with implementation examples

    6. Advanced Topics (05-AdvancedTopics/)

    - Context engineering techniques

    - Foundry agent integration

    - Multi-modal AI workflows

    - OAuth2 authentication demos

    - Real-time search capabilities

    - Real-time streaming

    - Root contexts implementation

    - Routing strategies

    - Sampling techniques

    - Scaling approaches

    - Security considerations

    - Entra ID security integration

    - Web search integration

    - Adversarial multi-agent reasoning (debate patterns)

    7. Community Contributions (06-CommunityContributions/)

    - How to contribute code and documentation

    - Collaborating via GitHub

    - Community-driven enhancements and feedback

    - Using various MCP clients (Claude Desktop, Cline, VSCode)

    - Working with popular MCP servers including image generation

    8. Lessons from Early Adoption (07-LessonsfromEarlyAdoption/)

    - Real-world implementations and success stories

    - Building and deploying MCP-based solutions

    - Trends and future roadmap

    - Microsoft MCP Servers Guide: Comprehensive guide to 10 production-ready Microsoft MCP servers including:

    - Microsoft Learn Docs MCP Server

    - Azure MCP Server (15+ specialized connectors)

    - GitHub MCP Server

    - Azure DevOps MCP Server

    - MarkItDown MCP Server

    - SQL Server MCP Server

    - Playwright MCP Server

    - Dev Box MCP Server

    - Azure AI Foundry MCP Server

    - Microsoft 365 Agents Toolkit MCP Server

    9. Best Practices (08-BestPractices/)

    - Performance tuning and optimization

    - Designing fault-tolerant MCP systems

    - Testing and resilience strategies

    10. Case Studies (09-CaseStudy/)

    - Seven comprehensive case studies demonstrating MCP versatility across diverse scenarios:

    - Azure AI Travel Agents: Multi-agent orchestration with Azure OpenAI and AI Search

    - Azure DevOps Integration: Automating workflow processes with YouTube data updates

    - Real-Time Documentation Retrieval: Python console client with streaming HTTP

    - Interactive Study Plan Generator: Chainlit web app with conversational AI

    - In-Editor Documentation: VS Code integration with GitHub Copilot workflows

    - Azure API Management: Enterprise API integration with MCP server creation

    - GitHub MCP Registry: Ecosystem development and agentic integration platform

    - Implementation examples spanning enterprise integration, developer productivity, and ecosystem development

    11. Hands-on Workshop (10-StreamliningAIWorkflowsBuildingAnMCPServerWithAIToolkit/)

    - Comprehensive hands-on workshop combining MCP with AI Toolkit

    - Building intelligent applications bridging AI models with real-world tools

    - Practical modules covering fundamentals, custom server development, and production deployment strategies

    - Lab Structure:

    - Lab 1: MCP Server Fundamentals

    - Lab 2: Advanced MCP Server Development

    - Lab 3: AI Toolkit Integration

    - Lab 4: Production Deployment and Scaling

    - Lab-based learning approach with step-by-step instructions

    12. MCP Server Database Integration Labs (11-MCPServerHandsOnLabs/)

    - Comprehensive 13-lab learning path for building production-ready MCP servers with PostgreSQL integration

    - Real-world retail analytics implementation using the Zava Retail use case

    - Enterprise-grade patterns including Row Level Security (RLS), semantic search, and multi-tenant data access

    - Complete Lab Structure:

    - Labs 00-03: Foundations - Introduction, Architecture, Security, Environment Setup

    - Labs 04-06: Building the MCP Server - Database Design, MCP Server Implementation, Tool Development

    - Labs 07-09: Advanced Features - Semantic Search, Testing & Debugging, VS Code Integration

    - Labs 10-12: Production & Best Practices - Deployment, Monitoring, Optimization

    - Technologies Covered: FastMCP framework, PostgreSQL, Azure OpenAI, Azure Container Apps, Application Insights

    - Learning Outcomes: Production-ready MCP servers, database integration patterns, AI-powered analytics, enterprise security

    Additional Resources

    The repository includes supporting resources:

  • Images folder: Contains diagrams and illustrations used throughout the curriculum
  • Translations: Multi-language support with automated translations of documentation
  • Official MCP Resources:
  • - MCP Documentation

    - MCP Specification

    - MCP GitHub Repository

    How to Use This Repository

    1. Sequential Learning: Follow the chapters in order (00 through 11) for a structured learning experience.

    2. Language-Specific Focus: If you're interested in a particular programming language, explore the samples directories for implementations in your preferred language.

    3. Practical Implementation: Start with the "Getting Started" section to set up your environment and create your first MCP server and client.

    4. Advanced Exploration: Once comfortable with the basics, dive into the advanced topics to expand your knowledge.

    5. Community Engagement: Join the MCP community through GitHub discussions and Discord channels to connect with experts and fellow developers.

    MCP Clients and Tools

    The curriculum covers various MCP clients and tools:

    1. Official Clients:

    - Visual Studio Code

    - MCP in Visual Studio Code

    - Claude Desktop

    - Claude in VSCode

    - Claude API

    2. Community Clients:

    - Cline (terminal-based)

    - Cursor (code editor)

    - ChatMCP

    - Windsurf

    3. MCP Management Tools:

    - MCP CLI

    - MCP Manager

    - MCP Linker

    - MCP Router

    Popular MCP Servers

    The repository introduces various MCP servers, including:

    1. Official Microsoft MCP Servers:

    - Microsoft Learn Docs MCP Server

    - Azure MCP Server (15+ specialized connectors)

    - GitHub MCP Server

    - Azure DevOps MCP Server

    - MarkItDown MCP Server

    - SQL Server MCP Server

    - Playwright MCP Server

    - Dev Box MCP Server

    - Azure AI Foundry MCP Server

    - Microsoft 365 Agents Toolkit MCP Server

    2. Official Reference Servers:

    - Filesystem

    - Fetch

    - Memory

    - Sequential Thinking

    3. Image Generation:

    - Azure OpenAI DALL-E 3

    - Stable Diffusion WebUI

    - Replicate

    4. Development Tools:

    - Git MCP

    - Terminal Control

    - Code Assistant

    5. Specialized Servers:

    - Salesforce

    - Microsoft Teams

    - Jira & Confluence

    Contributing

    This repository welcomes contributions from the community. See the Community Contributions section for guidance on how to contribute effectively to the MCP ecosystem.

    ----

    *This study guide was last updated on February 5, 2026, reflecting the latest MCP Specification 2025-11-25 and provides an overview of the repository as of that date. Repository content may be updated after this date.*

    code Module 08

    Module 08 — ๋ชจ๋ฒ” ์‚ฌ๋ก€

    MCP ๊ฐœ๋ฐœ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    _(์œ„ ์ด๋ฏธ์ง€ ํด๋ฆญ ์‹œ ๋ณธ ์ˆ˜์—…์˜ ์˜์ƒ ์‹œ์ฒญ)_

    ๊ฐœ์š”

    ์ด ์ˆ˜์—…์€ MCP ์„œ๋ฒ„ ๋ฐ ๊ธฐ๋Šฅ์„ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๊ฐœ๋ฐœ, ํ…Œ์ŠคํŠธ ๋ฐ ๋ฐฐํฌํ•  ๋•Œ์˜ ๊ณ ๊ธ‰ ๋ชจ๋ฒ” ์‚ฌ๋ก€์— ์ค‘์ ์„ ๋‘ก๋‹ˆ๋‹ค. MCP ์ƒํƒœ๊ณ„๊ฐ€ ๋ณต์žก์„ฑ๊ณผ ์ค‘์š”์„ฑ์ด ์ปค์ง์— ๋”ฐ๋ผ, ํ™•๋ฆฝ๋œ ํŒจํ„ด์„ ๋”ฐ๋ฅด๋Š” ๊ฒƒ์€ ์‹ ๋ขฐ์„ฑ, ์œ ์ง€๋ณด์ˆ˜์„ฑ ๋ฐ ์ƒํ˜ธ ์šด์šฉ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ๋ณธ ์ˆ˜์—…์€ ์‹ค์ œ MCP ๊ตฌํ˜„์—์„œ ์–ป์€ ์‹ค์šฉ์  ์ง€ํ˜œ๋ฅผ ํ†ตํ•ฉํ•˜์—ฌ ๊ฒฌ๊ณ ํ•˜๊ณ  ํšจ์œจ์ ์ธ ์„œ๋ฒ„๋ฅผ ํšจ๊ณผ์ ์ธ ๋ฆฌ์†Œ์Šค, ํ”„๋กฌํ”„ํŠธ ๋ฐ ๋„๊ตฌ์™€ ํ•จ๊ป˜ ๋งŒ๋“œ๋Š” ๋ฐ ๋„์›€์„ ์ค๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์ˆ˜์—…์ด ๋๋‚˜๋ฉด ๋‹ค์Œ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • MCP ์„œ๋ฒ„ ๋ฐ ๊ธฐ๋Šฅ ์„ค๊ณ„์—์„œ ์—…๊ณ„ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ์ ์šฉ
  • MCP ์„œ๋ฒ„์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์ „๋žต ์ˆ˜๋ฆฝ
  • ๋ณต์žกํ•œ MCP ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ํšจ์œจ์ ์ด๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ํŒจํ„ด ์„ค๊ณ„
  • MCP ์„œ๋ฒ„์—์„œ ์ ์ ˆํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ, ๋กœ๊น… ๋ฐ ๊ด€์ธก์„ฑ ๊ตฌํ˜„
  • MCP ๊ตฌํ˜„์˜ ์„ฑ๋Šฅ, ๋ณด์•ˆ, ์œ ์ง€๋ณด์ˆ˜์„ฑ ์ตœ์ ํ™”
  • MCP ํ•ต์‹ฌ ์›์น™

    ๊ตฌ์ฒด์ ์ธ ๊ตฌํ˜„ ๊ด€ํ–‰์— ๋“ค์–ด๊ฐ€๊ธฐ ์ „์—, ํšจ๊ณผ์ ์ธ MCP ๊ฐœ๋ฐœ์„ ์•ˆ๋‚ดํ•˜๋Š” ํ•ต์‹ฌ ์›์น™์„ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค:

    1. ํ‘œ์ค€ํ™”๋œ ํ†ต์‹ : MCP๋Š” JSON-RPC 2.0์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•˜์—ฌ ๋ชจ๋“  ๊ตฌํ˜„ ์‚ฌ์ด์— ์š”์ฒญ, ์‘๋‹ต ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ ์ผ๊ด€๋œ ํ˜•์‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    2. ์‚ฌ์šฉ์ž ์ค‘์‹ฌ ์„ค๊ณ„: ํ•ญ์ƒ MCP ๊ตฌํ˜„์—์„œ ์‚ฌ์šฉ์ž ๋™์˜, ์ œ์–ด ๋ฐ ํˆฌ๋ช…์„ฑ์„ ์ตœ์šฐ์„ ์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

    3. ๋ณด์•ˆ ์šฐ์„ : ์ธ์ฆ, ๊ถŒํ•œ ๋ถ€์—ฌ, ๊ฒ€์ฆ, ์†๋„ ์ œํ•œ ๋“ฑ ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ ์กฐ์น˜๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

    4. ๋ชจ๋“ˆ์‹ ์•„ํ‚คํ…์ฒ˜: ๊ฐ ๋„๊ตฌ์™€ ๋ฆฌ์†Œ์Šค๊ฐ€ ๋ช…ํ™•ํ•˜๊ณ  ์ง‘์ค‘๋œ ๋ชฉ์ ์„ ๊ฐ€์ง€๋Š” ๋ชจ๋“ˆ์‹ ์ ‘๊ทผ๋ฐฉ์‹์œผ๋กœ MCP ์„œ๋ฒ„๋ฅผ ์„ค๊ณ„ํ•ฉ๋‹ˆ๋‹ค.

    5. ์ƒํƒœ ์œ ์ง€ ์—ฐ๊ฒฐ: ์—ฌ๋Ÿฌ ์š”์ฒญ์— ๊ฑธ์ณ ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๋Š” MCP์˜ ๋Šฅ๋ ฅ์„ ํ™œ์šฉํ•˜์—ฌ ๋” ์ผ๊ด€๋˜๊ณ  ๋ฌธ๋งฅ ์ธ์ง€์ ์ธ ์ƒํ˜ธ์ž‘์šฉ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    ๊ณต์‹ MCP ๋ชจ๋ฒ” ์‚ฌ๋ก€

    ๋‹ค์Œ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋Š” ๊ณต์‹ ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ํ”„๋กœํ† ์ฝœ ๋ฌธ์„œ์—์„œ ์œ ๋ž˜ํ–ˆ์Šต๋‹ˆ๋‹ค:

    ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    1. ์‚ฌ์šฉ์ž ๋™์˜ ๋ฐ ์ œ์–ด: ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๋˜๋Š” ์ž‘์—… ์ˆ˜ํ–‰ ์ „์— ๋ช…์‹œ์ ์ธ ์‚ฌ์šฉ์ž ๋™์˜๋ฅผ ํ•ญ์ƒ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. ๊ณต์œ ๋˜๋Š” ๋ฐ์ดํ„ฐ์™€ ์Šน์ธ๋œ ์ž‘์—…์— ๋Œ€ํ•ด ๋ช…ํ™•ํ•œ ์ œ์–ด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    2. ๋ฐ์ดํ„ฐ ํ”„๋ผ์ด๋ฒ„์‹œ: ๋ช…์‹œ์  ๋™์˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ๋…ธ์ถœํ•˜๋ฉฐ ์ ์ ˆํ•œ ์ ‘๊ทผ ์ œ์–ด๋กœ ๋ณดํ˜ธํ•ฉ๋‹ˆ๋‹ค. ๋ฌด๋‹จ ๋ฐ์ดํ„ฐ ์ „์†ก์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.

    3. ๋„๊ตฌ ์•ˆ์ „์„ฑ: ๋„๊ตฌ ํ˜ธ์ถœ ์ „์— ๋ช…ํ™•ํ•œ ์‚ฌ์šฉ์ž ๋™์˜๋ฅผ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ๊ฐ ๋„๊ตฌ์˜ ๊ธฐ๋Šฅ์„ ์ดํ•ดํ•˜๋„๋ก ํ•˜๊ณ  ๊ฐ•๋ ฅํ•œ ๋ณด์•ˆ ๊ฒฝ๊ณ„๋ฅผ ์‹œํ–‰ํ•ฉ๋‹ˆ๋‹ค.

    4. ๋„๊ตฌ ๊ถŒํ•œ ์ œ์–ด: ์„ธ์…˜ ์ค‘ ๋ชจ๋ธ์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๋ฅผ ๊ตฌ์„ฑํ•˜์—ฌ ๋ช…์‹œ์ ์œผ๋กœ ์Šน์ธ๋œ ๋„๊ตฌ๋งŒ ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

    5. ์ธ์ฆ: API ํ‚ค, OAuth ํ† ํฐ ๋˜๋Š” ๊ธฐํƒ€ ์•ˆ์ „ํ•œ ์ธ์ฆ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ ๋„๊ตฌ, ๋ฆฌ์†Œ์Šค ๋˜๋Š” ๋ฏผ๊ฐ ์ž‘์—…์— ์ ‘๊ทผํ•˜๊ธฐ ์ „์— ์ ์ ˆํ•œ ์ธ์ฆ์„ ์š”๊ตฌํ•ฉ๋‹ˆ๋‹ค.

    6. ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ: ๋ชจ๋“  ๋„๊ตฌ ํ˜ธ์ถœ์— ๋Œ€ํ•ด ๊ฒ€์ฆ์„ ์‹œํ–‰ํ•˜์—ฌ ์ž˜๋ชป๋˜๊ฑฐ๋‚˜ ์•…์˜์ ์ธ ์ž…๋ ฅ์ด ๋„๊ตฌ ๊ตฌํ˜„์— ๋„๋‹ฌํ•˜์ง€ ์•Š๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

    7. ์†๋„ ์ œํ•œ: ์„œ๋ฒ„ ์ž์›์˜ ๋‚จ์šฉ์„ ๋ฐฉ์ง€ํ•˜๊ณ  ๊ณต์ •ํ•œ ์‚ฌ์šฉ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด ์†๋„ ์ œํ•œ์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

    ๊ตฌํ˜„ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    1. ๊ธฐ๋Šฅ ํ˜‘์ƒ: ์—ฐ๊ฒฐ ์„ค์ • ์ค‘ ์ง€์› ๊ธฐ๋Šฅ, ํ”„๋กœํ† ์ฝœ ๋ฒ„์ „, ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ ๋ฐ ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ๊ตํ™˜ํ•ฉ๋‹ˆ๋‹ค.

    2. ๋„๊ตฌ ์„ค๊ณ„: ์—ฌ๋Ÿฌ ๊ด€์‹ฌ์‚ฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฑฐ๋Œ€ ๋„๊ตฌ ๋Œ€์‹  ํ•˜๋‚˜์˜ ์ž‘์—…์— ์ง‘์ค‘ํ•˜๋Š” ๋„๊ตฌ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    3. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ๋ฌธ์ œ ์ง„๋‹จ, ์‹คํŒจ ์šฐ์•„ํ•œ ์ฒ˜๋ฆฌ ๋ฐ ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ”ผ๋“œ๋ฐฑ ์ œ๊ณต์„ ์œ„ํ•œ ํ‘œ์ค€ํ™”๋œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์™€ ์ฝ”๋“œ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค.

    4. ๋กœ๊น…: ๊ฐ์‚ฌ, ๋””๋ฒ„๊ทธ ๋ฐ ํ”„๋กœํ† ์ฝœ ์ƒํ˜ธ์ž‘์šฉ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ ๊ตฌ์กฐํ™”๋œ ๋กœ๊ทธ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.

    5. ์ง„ํ–‰ ์ถ”์ : ์žฅ์‹œ๊ฐ„ ์‹คํ–‰ ์ž‘์—…์— ๋Œ€ํ•ด ์ง„ํ–‰ ์ƒํ™ฉ ์—…๋ฐ์ดํŠธ๋ฅผ ๋ณด๊ณ ํ•˜์—ฌ ๋ฐ˜์‘ํ˜• ์‚ฌ์šฉ์ž ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

    6. ์š”์ฒญ ์ทจ์†Œ: ํ•„์š”์—†๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ์ง„ํ–‰ ์ค‘์ธ ์š”์ฒญ์„ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ทจ์†Œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

    ์ถ”๊ฐ€ ์ฐธ๊ณ  ์ž๋ฃŒ

    ์ตœ์‹  MCP ๋ชจ๋ฒ” ์‚ฌ๋ก€ ์ •๋ณด๋Š” ๋‹ค์Œ์„ ์ฐธ์กฐํ•˜์„ธ์š”:

  • MCP ๋ฌธ์„œ
  • MCP ๋ช…์„ธ (2025-11-25)
  • GitHub ์ €์žฅ์†Œ
  • ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • OWASP MCP Top 10 - ๋ณด์•ˆ ์œ„ํ—˜ ๋ฐ ์™„ํ™”์ฑ…
  • MCP ๋ณด์•ˆ ์ •์ƒํšŒ๋‹ด ์›Œํฌ์ˆ (Sherpa) - ์‹ค์Šต ๋ณด์•ˆ ๊ต์œก
  • ์‹ค์šฉ์  ๊ตฌํ˜„ ์˜ˆ์‹œ

    ๋„๊ตฌ ์„ค๊ณ„ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    1. ๋‹จ์ผ ์ฑ…์ž„ ์›์น™

    ๊ฐ MCP ๋„๊ตฌ๋Š” ๋ช…ํ™•ํ•˜๊ณ  ์ง‘์ค‘๋œ ๋ชฉ์ ์„ ๊ฐ€์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ๊ด€์‹ฌ์‚ฌ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ ค๋Š” ๊ฑฐ๋Œ€ ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค๊ธฐ๋ณด๋‹ค ํŠน์ • ์ž‘์—…์— ๋›ฐ์–ด๋‚œ ์ „๋ฌธ ๋„๊ตฌ๋ฅผ ๊ฐœ๋ฐœํ•˜์„ธ์š”.

    
    // A focused tool that does one thing well
    
    public class WeatherForecastTool : ITool
    
    {
    
        private readonly IWeatherService _weatherService;
    
        
    
        public WeatherForecastTool(IWeatherService weatherService)
    
        {
    
            _weatherService = weatherService;
    
        }
    
        
    
        public string Name => "weatherForecast";
    
        public string Description => "Gets weather forecast for a specific location";
    
        
    
        public ToolDefinition GetDefinition()
    
        {
    
            return new ToolDefinition
    
            {
    
                Name = Name,
    
                Description = Description,
    
                Parameters = new Dictionary<string, ParameterDefinition>
    
                {
    
                    ["location"] = new ParameterDefinition
    
                    {
    
                        Type = ParameterType.String,
    
                        Description = "City or location name"
    
                    },
    
                    ["days"] = new ParameterDefinition
    
                    {
    
                        Type = ParameterType.Integer,
    
                        Description = "Number of forecast days",
    
                        Default = 3
    
                    }
    
                },
    
                Required = new[] { "location" }
    
            };
    
        }
    
        
    
        public async Task<ToolResponse> ExecuteAsync(IDictionary<string, object> parameters)
    
        {
    
            var location = parameters["location"].ToString();
    
            var days = parameters.ContainsKey("days") 
    
                ? Convert.ToInt32(parameters["days"]) 
    
                : 3;
    
                
    
            var forecast = await _weatherService.GetForecastAsync(location, days);
    
            
    
            return new ToolResponse
    
            {
    
                Content = new List<ContentItem>
    
                {
    
                    new TextContent(JsonSerializer.Serialize(forecast))
    
                }
    
            };
    
        }
    
    }
    
    
    2. ์ผ๊ด€๋œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

    ์ •๋ณด๊ฐ€ ํ’๋ถ€ํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์™€ ์ ์ ˆํ•œ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์„ ๊ฐ–์ถ˜ ๊ฒฌ๊ณ ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”.

    
    # ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ํฌํ•จํ•œ ํŒŒ์ด์ฌ ์˜ˆ์ œ
    
    class DataQueryTool:
    
        def get_name(self):
    
            return "dataQuery"
    
            
    
        def get_description(self):
    
            return "Queries data from specified database tables"
    
        
    
        async def execute(self, parameters):
    
            try:
    
                # ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ
    
                if "query" not in parameters:
    
                    raise ToolParameterError("Missing required parameter: query")
    
                    
    
                query = parameters["query"]
    
                
    
                # ๋ณด์•ˆ ๊ฒ€์ฆ
    
                if self._contains_unsafe_sql(query):
    
                    raise ToolSecurityError("Query contains potentially unsafe SQL")
    
                
    
                try:
    
                    # ํƒ€์ž„์•„์›ƒ์ด ์žˆ๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…
    
                    async with timeout(10):  # 10์ดˆ ํƒ€์ž„์•„์›ƒ
    
                        result = await self._database.execute_query(query)
    
                        
    
                    return ToolResponse(
    
                        content=[TextContent(json.dumps(result))]
    
                    )
    
                except asyncio.TimeoutError:
    
                    raise ToolExecutionError("Database query timed out after 10 seconds")
    
                except DatabaseConnectionError as e:
    
                    # ์—ฐ๊ฒฐ ์˜ค๋ฅ˜๋Š” ์ผ์‹œ์ ์ผ ์ˆ˜ ์žˆ์Œ
    
                    self._log_error("Database connection error", e)
    
                    raise ToolExecutionError(f"Database connection error: {str(e)}")
    
                except DatabaseQueryError as e:
    
                    # ์ฟผ๋ฆฌ ์˜ค๋ฅ˜๋Š” ํด๋ผ์ด์–ธํŠธ ์˜ค๋ฅ˜์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Œ
    
                    self._log_error("Database query error", e)
    
                    raise ToolExecutionError(f"Invalid query: {str(e)}")
    
                    
    
            except ToolError:
    
                # ๋„๊ตฌ๋ณ„ ์˜ค๋ฅ˜๋Š” ํ†ต๊ณผ์‹œํ‚ด
    
                raise
    
            except Exception as e:
    
                # ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜์— ๋Œ€ํ•œ ํฌ๊ด„์  ์ฒ˜๋ฆฌ
    
                self._log_error("Unexpected error in DataQueryTool", e)
    
                raise ToolExecutionError(f"An unexpected error occurred: {str(e)}")
    
        
    
        def _contains_unsafe_sql(self, query):
    
            # SQL ์ธ์ ์…˜ ํƒ์ง€ ๊ตฌํ˜„
    
            pass
    
            
    
        def _log_error(self, message, error):
    
            # ์˜ค๋ฅ˜ ๋กœ๊น… ๊ตฌํ˜„
    
            pass
    
    
    3. ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ

    ํ•ญ์ƒ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ฒ ์ €ํžˆ ๊ฒ€์ฆํ•˜์—ฌ ์ž˜๋ชป๋˜๊ฑฐ๋‚˜ ์•…์˜์ ์ธ ์ž…๋ ฅ์„ ๋ฐฉ์ง€ํ•˜์„ธ์š”.

    
    // ์ž์„ธํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ์ด ํฌํ•จ๋œ JavaScript/TypeScript ์˜ˆ์ œ
    
    class FileOperationTool {
    
      getName() {
    
        return "fileOperation";
    
      }
    
      
    
      getDescription() {
    
        return "Performs file operations like read, write, and delete";
    
      }
    
      
    
      getDefinition() {
    
        return {
    
          name: this.getName(),
    
          description: this.getDescription(),
    
          parameters: {
    
            operation: {
    
              type: "string",
    
              description: "Operation to perform",
    
              enum: ["read", "write", "delete"]
    
            },
    
            path: {
    
              type: "string",
    
              description: "File path (must be within allowed directories)"
    
            },
    
            content: {
    
              type: "string",
    
              description: "Content to write (only for write operation)",
    
              optional: true
    
            }
    
          },
    
          required: ["operation", "path"]
    
        };
    
      }
    
      
    
      async execute(parameters) {
    
        // 1. ๋งค๊ฐœ๋ณ€์ˆ˜ ์กด์žฌ ์—ฌ๋ถ€ ๊ฒ€์ฆ
    
        if (!parameters.operation) {
    
          throw new ToolError("Missing required parameter: operation");
    
        }
    
        
    
        if (!parameters.path) {
    
          throw new ToolError("Missing required parameter: path");
    
        }
    
        
    
        // 2. ๋งค๊ฐœ๋ณ€์ˆ˜ ํƒ€์ž… ๊ฒ€์ฆ
    
        if (typeof parameters.operation !== "string") {
    
          throw new ToolError("Parameter 'operation' must be a string");
    
        }
    
        
    
        if (typeof parameters.path !== "string") {
    
          throw new ToolError("Parameter 'path' must be a string");
    
        }
    
        
    
        // 3. ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฐ’ ๊ฒ€์ฆ
    
        const validOperations = ["read", "write", "delete"];
    
        if (!validOperations.includes(parameters.operation)) {
    
          throw new ToolError(`Invalid operation. Must be one of: ${validOperations.join(", ")}`);
    
        }
    
        
    
        // 4. ์“ฐ๊ธฐ ์ž‘์—…์„ ์œ„ํ•œ ๋‚ด์šฉ ์กด์žฌ ์—ฌ๋ถ€ ๊ฒ€์ฆ
    
        if (parameters.operation === "write" && !parameters.content) {
    
          throw new ToolError("Content parameter is required for write operation");
    
        }
    
        
    
        // 5. ๊ฒฝ๋กœ ์•ˆ์ „์„ฑ ๊ฒ€์ฆ
    
        if (!this.isPathWithinAllowedDirectories(parameters.path)) {
    
          throw new ToolError("Access denied: path is outside of allowed directories");
    
        }
    
        
    
        // ๊ฒ€์ฆ๋œ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ๊ตฌํ˜„
    
        // ...
    
      }
    
      
    
      isPathWithinAllowedDirectories(path) {
    
        // ๊ฒฝ๋กœ ์•ˆ์ „์„ฑ ๊ฒ€์‚ฌ ๊ตฌํ˜„
    
        // ...
    
      }
    
    }
    
    

    ๋ณด์•ˆ ๊ตฌํ˜„ ์˜ˆ์‹œ

    1. ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ
    
    // ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ๊ฐ€ ํฌํ•จ๋œ Java ์˜ˆ์ œ
    
    public class SecureDataAccessTool implements Tool {
    
        private final AuthenticationService authService;
    
        private final AuthorizationService authzService;
    
        private final DataService dataService;
    
        
    
        // ์˜์กด์„ฑ ์ฃผ์ž…
    
        public SecureDataAccessTool(
    
                AuthenticationService authService,
    
                AuthorizationService authzService,
    
                DataService dataService) {
    
            this.authService = authService;
    
            this.authzService = authzService;
    
            this.dataService = dataService;
    
        }
    
        
    
        @Override
    
        public String getName() {
    
            return "secureDataAccess";
    
        }
    
        
    
        @Override
    
        public ToolResponse execute(ToolRequest request) {
    
            // 1. ์ธ์ฆ ์ปจํ…์ŠคํŠธ ์ถ”์ถœ
    
            String authToken = request.getContext().getAuthToken();
    
            
    
            // 2. ์‚ฌ์šฉ์ž ์ธ์ฆ
    
            UserIdentity user;
    
            try {
    
                user = authService.validateToken(authToken);
    
            } catch (AuthenticationException e) {
    
                return ToolResponse.error("Authentication failed: " + e.getMessage());
    
            }
    
            
    
            // 3. ํŠน์ • ์ž‘์—…์— ๋Œ€ํ•œ ๊ถŒํ•œ ํ™•์ธ
    
            String dataId = request.getParameters().get("dataId").getAsString();
    
            String operation = request.getParameters().get("operation").getAsString();
    
            
    
            boolean isAuthorized = authzService.isAuthorized(user, "data:" + dataId, operation);
    
            if (!isAuthorized) {
    
                return ToolResponse.error("Access denied: Insufficient permissions for this operation");
    
            }
    
            
    
            // 4. ๊ถŒํ•œ์ด ๋ถ€์—ฌ๋œ ์ž‘์—… ์ง„ํ–‰
    
            try {
    
                switch (operation) {
    
                    case "read":
    
                        Object data = dataService.getData(dataId, user.getId());
    
                        return ToolResponse.success(data);
    
                    case "update":
    
                        JsonNode newData = request.getParameters().get("newData");
    
                        dataService.updateData(dataId, newData, user.getId());
    
                        return ToolResponse.success("Data updated successfully");
    
                    default:
    
                        return ToolResponse.error("Unsupported operation: " + operation);
    
                }
    
            } catch (Exception e) {
    
                return ToolResponse.error("Operation failed: " + e.getMessage());
    
            }
    
        }
    
    }
    
    
    2. ์†๋„ ์ œํ•œ
    
    // C# rate limiting implementation
    
    public class RateLimitingMiddleware
    
    {
    
        private readonly RequestDelegate _next;
    
        private readonly IMemoryCache _cache;
    
        private readonly ILogger<RateLimitingMiddleware> _logger;
    
        
    
        // Configuration options
    
        private readonly int _maxRequestsPerMinute;
    
        
    
        public RateLimitingMiddleware(
    
            RequestDelegate next,
    
            IMemoryCache cache,
    
            ILogger<RateLimitingMiddleware> logger,
    
            IConfiguration config)
    
        {
    
            _next = next;
    
            _cache = cache;
    
            _logger = logger;
    
            _maxRequestsPerMinute = config.GetValue<int>("RateLimit:MaxRequestsPerMinute", 60);
    
        }
    
        
    
        public async Task InvokeAsync(HttpContext context)
    
        {
    
            // 1. Get client identifier (API key or user ID)
    
            string clientId = GetClientIdentifier(context);
    
            
    
            // 2. Get rate limiting key for this minute
    
            string cacheKey = $"rate_limit:{clientId}:{DateTime.UtcNow:yyyyMMddHHmm}";
    
            
    
            // 3. Check current request count
    
            if (!_cache.TryGetValue(cacheKey, out int requestCount))
    
            {
    
                requestCount = 0;
    
            }
    
            
    
            // 4. Enforce rate limit
    
            if (requestCount >= _maxRequestsPerMinute)
    
            {
    
                _logger.LogWarning("Rate limit exceeded for client {ClientId}", clientId);
    
                
    
                context.Response.StatusCode = StatusCodes.Status429TooManyRequests;
    
                context.Response.Headers.Add("Retry-After", "60");
    
                
    
                await context.Response.WriteAsJsonAsync(new
    
                {
    
                    error = "Rate limit exceeded",
    
                    message = "Too many requests. Please try again later.",
    
                    retryAfterSeconds = 60
    
                });
    
                
    
                return;
    
            }
    
            
    
            // 5. Increment request count
    
            _cache.Set(cacheKey, requestCount + 1, TimeSpan.FromMinutes(2));
    
            
    
            // 6. Add rate limit headers
    
            context.Response.Headers.Add("X-RateLimit-Limit", _maxRequestsPerMinute.ToString());
    
            context.Response.Headers.Add("X-RateLimit-Remaining", (_maxRequestsPerMinute - requestCount - 1).ToString());
    
            
    
            // 7. Continue with the request
    
            await _next(context);
    
        }
    
        
    
        private string GetClientIdentifier(HttpContext context)
    
        {
    
            // Implementation to extract API key or user ID
    
            // ...
    
        }
    
    }
    
    

    ํ…Œ์ŠคํŠธ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    1. MCP ๋„๊ตฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

    ๋„๊ตฌ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธํ•˜๊ณ  ์™ธ๋ถ€ ์ข…์†์„ฑ์„ ๋ชจํ‚นํ•˜์„ธ์š”:

    
    // ๋„๊ตฌ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ์˜ TypeScript ์˜ˆ์ œ
    
    describe('WeatherForecastTool', () => {
    
      let tool: WeatherForecastTool;
    
      let mockWeatherService: jest.Mocked<IWeatherService>;
    
      
    
      beforeEach(() => {
    
        // ๋ชฉ ๋‚ ์”จ ์„œ๋น„์Šค ์ƒ์„ฑ
    
        mockWeatherService = {
    
          getForecasts: jest.fn()
    
        } as any;
    
        
    
        // ๋ชฉ ์˜์กด์„ฑ์„ ๊ฐ€์ง„ ๋„๊ตฌ ์ƒ์„ฑ
    
        tool = new WeatherForecastTool(mockWeatherService);
    
      });
    
      
    
      it('should return weather forecast for a location', async () => {
    
        // ์ค€๋น„
    
        const mockForecast = {
    
          location: 'Seattle',
    
          forecasts: [
    
            { date: '2025-07-16', temperature: 72, conditions: 'Sunny' },
    
            { date: '2025-07-17', temperature: 68, conditions: 'Partly Cloudy' },
    
            { date: '2025-07-18', temperature: 65, conditions: 'Rain' }
    
          ]
    
        };
    
        
    
        mockWeatherService.getForecasts.mockResolvedValue(mockForecast);
    
        
    
        // ์‹คํ–‰
    
        const response = await tool.execute({
    
          location: 'Seattle',
    
          days: 3
    
        });
    
        
    
        // ๊ฒ€์ฆ
    
        expect(mockWeatherService.getForecasts).toHaveBeenCalledWith('Seattle', 3);
    
        expect(response.content[0].text).toContain('Seattle');
    
        expect(response.content[0].text).toContain('Sunny');
    
      });
    
      
    
      it('should handle errors from the weather service', async () => {
    
        // ์ค€๋น„
    
        mockWeatherService.getForecasts.mockRejectedValue(new Error('Service unavailable'));
    
        
    
        // ์‹คํ–‰ ๋ฐ ๊ฒ€์ฆ
    
        await expect(tool.execute({
    
          location: 'Seattle',
    
          days: 3
    
        })).rejects.toThrow('Weather service error: Service unavailable');
    
      });
    
    });
    
    

    2. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    ํด๋ผ์ด์–ธํŠธ ์š”์ฒญ๋ถ€ํ„ฐ ์„œ๋ฒ„ ์‘๋‹ต๊นŒ์ง€์˜ ์ „์ฒด ํ๋ฆ„์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”:

    
    # ํŒŒ์ด์ฌ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์˜ˆ์ œ
    
    @pytest.mark.asyncio
    
    async def test_mcp_server_integration():
    
        # ํ…Œ์ŠคํŠธ ์„œ๋ฒ„ ์‹œ์ž‘
    
        server = McpServer()
    
        server.register_tool(WeatherForecastTool(MockWeatherService()))
    
        await server.start(port=5000)
    
        
    
        try:
    
            # ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ
    
            client = McpClient("http://localhost:5000")
    
            
    
            # ๋„๊ตฌ ๊ฒ€์ƒ‰ ํ…Œ์ŠคํŠธ
    
            tools = await client.discover_tools()
    
            assert "weatherForecast" in [t.name for t in tools]
    
            
    
            # ๋„๊ตฌ ์‹คํ–‰ ํ…Œ์ŠคํŠธ
    
            response = await client.execute_tool("weatherForecast", {
    
                "location": "Seattle",
    
                "days": 3
    
            })
    
            
    
            # ์‘๋‹ต ํ™•์ธ
    
            assert response.status_code == 200
    
            assert "Seattle" in response.content[0].text
    
            assert len(json.loads(response.content[0].text)["forecasts"]) == 3
    
            
    
        finally:
    
            # ์ •๋ฆฌ ์ž‘์—…
    
            await server.stop()
    
    

    ์„ฑ๋Šฅ ์ตœ์ ํ™”

    1. ์บ์‹ฑ ์ „๋žต

    ์ง€์—ฐ ์‹œ๊ฐ„๊ณผ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰์„ ์ค„์ด๊ธฐ ์œ„ํ•ด ์ ์ ˆํ•œ ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”:

    
    // C# example with caching
    
    public class CachedWeatherTool : ITool
    
    {
    
        private readonly IWeatherService _weatherService;
    
        private readonly IDistributedCache _cache;
    
        private readonly ILogger<CachedWeatherTool> _logger;
    
        
    
        public CachedWeatherTool(
    
            IWeatherService weatherService,
    
            IDistributedCache cache,
    
            ILogger<CachedWeatherTool> logger)
    
        {
    
            _weatherService = weatherService;
    
            _cache = cache;
    
            _logger = logger;
    
        }
    
        
    
        public string Name => "weatherForecast";
    
        
    
        public async Task<ToolResponse> ExecuteAsync(IDictionary<string, object> parameters)
    
        {
    
            var location = parameters["location"].ToString();
    
            var days = Convert.ToInt32(parameters.GetValueOrDefault("days", 3));
    
            
    
            // Create cache key
    
            string cacheKey = $"weather:{location}:{days}";
    
            
    
            // Try to get from cache
    
            string cachedForecast = await _cache.GetStringAsync(cacheKey);
    
            if (!string.IsNullOrEmpty(cachedForecast))
    
            {
    
                _logger.LogInformation("Cache hit for weather forecast: {Location}", location);
    
                return new ToolResponse
    
                {
    
                    Content = new List<ContentItem>
    
                    {
    
                        new TextContent(cachedForecast)
    
                    }
    
                };
    
            }
    
            
    
            // Cache miss - get from service
    
            _logger.LogInformation("Cache miss for weather forecast: {Location}", location);
    
            var forecast = await _weatherService.GetForecastAsync(location, days);
    
            string forecastJson = JsonSerializer.Serialize(forecast);
    
            
    
            // Store in cache (weather forecasts valid for 1 hour)
    
            await _cache.SetStringAsync(
    
                cacheKey,
    
                forecastJson,
    
                new DistributedCacheEntryOptions
    
                {
    
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1)
    
                });
    
            
    
            return new ToolResponse
    
            {
    
                Content = new List<ContentItem>
    
                {
    
                    new TextContent(forecastJson)
    
                }
    
            };
    
        }
    
    }
    
    
    2. ์˜์กด์„ฑ ์ฃผ์ž…๊ณผ ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ

    ์˜์กด์„ฑ์„ ์ƒ์„ฑ์ž ์ฃผ์ž… ํ†ตํ•ด ๋ฐ›์•„๋“ค์ด๋„๋ก ๋„๊ตฌ๋ฅผ ์„ค๊ณ„ํ•˜์—ฌ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•˜๊ณ  ๊ตฌ์„ฑ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“œ์„ธ์š”:

    
    // ์˜์กด์„ฑ ์ฃผ์ž…์ด ํฌํ•จ๋œ ์ž๋ฐ” ์˜ˆ์ œ
    
    public class CurrencyConversionTool implements Tool {
    
        private final ExchangeRateService exchangeService;
    
        private final CacheService cacheService;
    
        private final Logger logger;
    
        
    
        // ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•œ ์˜์กด์„ฑ ์ฃผ์ž…
    
        public CurrencyConversionTool(
    
                ExchangeRateService exchangeService,
    
                CacheService cacheService,
    
                Logger logger) {
    
            this.exchangeService = exchangeService;
    
            this.cacheService = cacheService;
    
            this.logger = logger;
    
        }
    
        
    
        // ๋„๊ตฌ ๊ตฌํ˜„
    
        // ...
    
    }
    
    
    3. ์กฐํ•ฉ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ

    ๋” ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๋งŒ๋“ค๊ธฐ ์œ„ํ•ด ๋„๊ตฌ๋ฅผ ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•˜์„ธ์š”:

    
    # ์กฐํ•ฉ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ๋ฅผ ๋ณด์—ฌ์ฃผ๋Š” ํŒŒ์ด์ฌ ์˜ˆ์ œ
    
    class DataFetchTool(Tool):
    
        def get_name(self):
    
            return "dataFetch"
    
        
    
        # ๊ตฌํ˜„...
    
    
    
    class DataAnalysisTool(Tool):
    
        def get_name(self):
    
            return "dataAnalysis"
    
        
    
        # ์ด ๋„๊ตฌ๋Š” dataFetch ๋„๊ตฌ์˜ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
    
        async def execute_async(self, request):
    
            # ๊ตฌํ˜„...
    
            pass
    
    
    
    class DataVisualizationTool(Tool):
    
        def get_name(self):
    
            return "dataVisualize"
    
        
    
        # ์ด ๋„๊ตฌ๋Š” dataAnalysis ๋„๊ตฌ์˜ ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
    
        async def execute_async(self, request):
    
            # ๊ตฌํ˜„...
    
            pass
    
    
    
    # ์ด ๋„๊ตฌ๋“ค์€ ๋…๋ฆฝ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ ์›Œํฌํ”Œ๋กœ์šฐ์˜ ์ผ๋ถ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
    
    

    ์Šคํ‚ค๋งˆ ์„ค๊ณ„ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    ์Šคํ‚ค๋งˆ๋Š” ๋ชจ๋ธ๊ณผ ๋„๊ตฌ ๊ฐ„์˜ ๊ณ„์•ฝ์ž…๋‹ˆ๋‹ค. ์ž˜ ์„ค๊ณ„๋œ ์Šคํ‚ค๋งˆ๋Š” ๋„๊ตฌ ์‚ฌ์šฉ์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค.

    1. ๋ช…ํ™•ํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜ ์„ค๋ช…

    ๊ฐ ๋งค๊ฐœ๋ณ€์ˆ˜์— ์„ค๋ช… ์ •๋ณด๋ฅผ ํ•ญ์ƒ ํฌํ•จํ•˜์„ธ์š”:

    
    public object GetSchema()
    
    {
    
        return new {
    
            type = "object",
    
            properties = new {
    
                query = new { 
    
                    type = "string", 
    
                    description = "Search query text. Use precise keywords for better results." 
    
                },
    
                filters = new {
    
                    type = "object",
    
                    description = "Optional filters to narrow down search results",
    
                    properties = new {
    
                        dateRange = new { 
    
                            type = "string", 
    
                            description = "Date range in format YYYY-MM-DD:YYYY-MM-DD" 
    
                        },
    
                        category = new { 
    
                            type = "string", 
    
                            description = "Category name to filter by" 
    
                        }
    
                    }
    
                },
    
                limit = new { 
    
                    type = "integer", 
    
                    description = "Maximum number of results to return (1-50)",
    
                    default = 10
    
                }
    
            },
    
            required = new[] { "query" }
    
        };
    
    }
    
    
    2. ๊ฒ€์ฆ ์ œ์•ฝ์กฐ๊ฑด

    ์ž˜๋ชป๋œ ์ž…๋ ฅ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๊ฒ€์ฆ ์ œ์•ฝ์กฐ๊ฑด์„ ํฌํ•จํ•˜์„ธ์š”:

    
    Map<String, Object> getSchema() {
    
        Map<String, Object> schema = new HashMap<>();
    
        schema.put("type", "object");
    
        
    
        Map<String, Object> properties = new HashMap<>();
    
        
    
        // ํ˜•์‹ ๊ฒ€์ฆ์ด ํฌํ•จ๋œ ์ด๋ฉ”์ผ ์†์„ฑ
    
        Map<String, Object> email = new HashMap<>();
    
        email.put("type", "string");
    
        email.put("format", "email");
    
        email.put("description", "User email address");
    
        
    
        // ์ˆซ์ž ์ œ์•ฝ ์กฐ๊ฑด์ด ์žˆ๋Š” ๋‚˜์ด ์†์„ฑ
    
        Map<String, Object> age = new HashMap<>();
    
        age.put("type", "integer");
    
        age.put("minimum", 13);
    
        age.put("maximum", 120);
    
        age.put("description", "User age in years");
    
        
    
        // ์—ด๊ฑฐํ˜• ์†์„ฑ
    
        Map<String, Object> subscription = new HashMap<>();
    
        subscription.put("type", "string");
    
        subscription.put("enum", Arrays.asList("free", "basic", "premium"));
    
        subscription.put("default", "free");
    
        subscription.put("description", "Subscription tier");
    
        
    
        properties.put("email", email);
    
        properties.put("age", age);
    
        properties.put("subscription", subscription);
    
        
    
        schema.put("properties", properties);
    
        schema.put("required", Arrays.asList("email"));
    
        
    
        return schema;
    
    }
    
    
    3. ์ผ๊ด€๋œ ๋ฐ˜ํ™˜ ๊ตฌ์กฐ

    ๋ชจ๋ธ์ด ๊ฒฐ๊ณผ๋ฅผ ํ•ด์„ํ•˜๊ธฐ ์‰ฝ๋„๋ก ์‘๋‹ต ๊ตฌ์กฐ๋ฅผ ์ผ๊ด€๋˜๊ฒŒ ์œ ์ง€ํ•˜์„ธ์š”:

    
    async def execute_async(self, request):
    
        try:
    
            # ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค
    
            results = await self._search_database(request.parameters["query"])
    
            
    
            # ํ•ญ์ƒ ์ผ๊ด€๋œ ๊ตฌ์กฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค
    
            return ToolResponse(
    
                result={
    
                    "matches": [self._format_item(item) for item in results],
    
                    "totalCount": len(results),
    
                    "queryTime": calculation_time_ms,
    
                    "status": "success"
    
                }
    
            )
    
        except Exception as e:
    
            return ToolResponse(
    
                result={
    
                    "matches": [],
    
                    "totalCount": 0,
    
                    "queryTime": 0,
    
                    "status": "error",
    
                    "error": str(e)
    
                }
    
            )
    
        
    
    def _format_item(self, item):
    
        """Ensures each item has a consistent structure"""
    
        return {
    
            "id": item.id,
    
            "title": item.title,
    
            "summary": item.summary[:100] + "..." if len(item.summary) > 100 else item.summary,
    
            "url": item.url,
    
            "relevance": item.score
    
        }
    
    

    ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

    ์‹ ๋ขฐ์„ฑ์„ ์œ ์ง€ํ•˜๋ ค๋ฉด ๊ฒฌ๊ณ ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.

    1. ์šฐ์•„ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ

    ์ ์ ˆํ•œ ์ˆ˜์ค€์—์„œ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์ •๋ณด์„ฑ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ๊ณตํ•˜์„ธ์š”:

    
    public async Task<ToolResponse> ExecuteAsync(ToolRequest request)
    
    {
    
        try
    
        {
    
            string fileId = request.Parameters.GetProperty("fileId").GetString();
    
            
    
            try
    
            {
    
                var fileData = await _fileService.GetFileAsync(fileId);
    
                return new ToolResponse { 
    
                    Result = JsonSerializer.SerializeToElement(fileData) 
    
                };
    
            }
    
            catch (FileNotFoundException)
    
            {
    
                throw new ToolExecutionException($"File not found: {fileId}");
    
            }
    
            catch (UnauthorizedAccessException)
    
            {
    
                throw new ToolExecutionException("You don't have permission to access this file");
    
            }
    
            catch (Exception ex) when (ex is IOException || ex is TimeoutException)
    
            {
    
                _logger.LogError(ex, "Error accessing file {FileId}", fileId);
    
                throw new ToolExecutionException("Error accessing file: The service is temporarily unavailable");
    
            }
    
        }
    
        catch (JsonException)
    
        {
    
            throw new ToolExecutionException("Invalid file ID format");
    
        }
    
        catch (Exception ex)
    
        {
    
            _logger.LogError(ex, "Unexpected error in FileAccessTool");
    
            throw new ToolExecutionException("An unexpected error occurred");
    
        }
    
    }
    
    
    2. ๊ตฌ์กฐํ™”๋œ ์˜ค๋ฅ˜ ์‘๋‹ต

    ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ ๊ตฌ์กฐํ™”๋œ ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•˜์„ธ์š”:

    
    @Override
    
    public ToolResponse execute(ToolRequest request) {
    
        try {
    
            // ๊ตฌํ˜„
    
        } catch (Exception ex) {
    
            Map<String, Object> errorResult = new HashMap<>();
    
            
    
            errorResult.put("success", false);
    
            
    
            if (ex instanceof ValidationException) {
    
                ValidationException validationEx = (ValidationException) ex;
    
                
    
                errorResult.put("errorType", "validation");
    
                errorResult.put("errorMessage", validationEx.getMessage());
    
                errorResult.put("validationErrors", validationEx.getErrors());
    
                
    
                return new ToolResponse.Builder()
    
                    .setResult(errorResult)
    
                    .build();
    
            }
    
            
    
            // ๋‹ค๋ฅธ ์˜ˆ์™ธ๋ฅผ ToolExecutionException์œผ๋กœ ๋‹ค์‹œ ๋˜์ง
    
            throw new ToolExecutionException("Tool execution failed: " + ex.getMessage(), ex);
    
        }
    
    }
    
    
    3. ์žฌ์‹œ๋„ ๋กœ์ง

    ์ผ์‹œ์  ์‹คํŒจ์— ๋Œ€ํ•ด ์ ์ ˆํ•œ ์žฌ์‹œ๋„ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜์„ธ์š”:

    
    async def execute_async(self, request):
    
        max_retries = 3
    
        retry_count = 0
    
        base_delay = 1  # ์ดˆ
    
        
    
        while retry_count < max_retries:
    
            try:
    
                # ์™ธ๋ถ€ API ํ˜ธ์ถœ
    
                return await self._call_api(request.parameters)
    
            except TransientError as e:
    
                retry_count += 1
    
                if retry_count >= max_retries:
    
                    raise ToolExecutionException(f"Operation failed after {max_retries} attempts: {str(e)}")
    
                    
    
                # ์ง€์ˆ˜ ๋ฐฑ์˜คํ”„
    
                delay = base_delay * (2 ** (retry_count - 1))
    
                logging.warning(f"Transient error, retrying in {delay}s: {str(e)}")
    
                await asyncio.sleep(delay)
    
            except Exception as e:
    
                # ์ผ์‹œ์ ์ด์ง€ ์•Š์€ ์˜ค๋ฅ˜, ์žฌ์‹œ๋„ํ•˜์ง€ ์•Š์Œ
    
                raise ToolExecutionException(f"Operation failed: {str(e)}")
    
    

    ์„ฑ๋Šฅ ์ตœ์ ํ™”

    1. ์บ์‹ฑ

    ๋น„์šฉ์ด ํฐ ์ž‘์—…์— ๋Œ€ํ•ด ์บ์‹ฑ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”:

    
    public class CachedDataTool : IMcpTool
    
    {
    
        private readonly IDatabase _database;
    
        private readonly IMemoryCache _cache;
    
        
    
        public CachedDataTool(IDatabase database, IMemoryCache cache)
    
        {
    
            _database = database;
    
            _cache = cache;
    
        }
    
        
    
        public async Task<ToolResponse> ExecuteAsync(ToolRequest request)
    
        {
    
            var query = request.Parameters.GetProperty("query").GetString();
    
            
    
            // Create cache key based on parameters
    
            var cacheKey = $"data_query_{ComputeHash(query)}";
    
            
    
            // Try to get from cache first
    
            if (_cache.TryGetValue(cacheKey, out var cachedResult))
    
            {
    
                return new ToolResponse { Result = cachedResult };
    
            }
    
            
    
            // Cache miss - perform actual query
    
            var result = await _database.QueryAsync(query);
    
            
    
            // Store in cache with expiration
    
            var cacheOptions = new MemoryCacheEntryOptions()
    
                .SetAbsoluteExpiration(TimeSpan.FromMinutes(15));
    
                
    
            _cache.Set(cacheKey, JsonSerializer.SerializeToElement(result), cacheOptions);
    
            
    
            return new ToolResponse { Result = JsonSerializer.SerializeToElement(result) };
    
        }
    
        
    
        private string ComputeHash(string input)
    
        {
    
            // Implementation to generate stable hash for cache key
    
        }
    
    }
    
    
    2. ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ

    I/O ๋ฐ”์šด๋“œ ์ž‘์—…์— ๋Œ€ํ•ด ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์„ธ์š”:

    
    public class AsyncDocumentProcessingTool implements Tool {
    
        private final DocumentService documentService;
    
        private final ExecutorService executorService;
    
        
    
        @Override
    
        public ToolResponse execute(ToolRequest request) {
    
            String documentId = request.getParameters().get("documentId").asText();
    
            
    
            // ์žฅ์‹œ๊ฐ„ ์‹คํ–‰๋˜๋Š” ์ž‘์—…์˜ ๊ฒฝ์šฐ ์ฆ‰์‹œ ์ฒ˜๋ฆฌ ID๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค
    
            String processId = UUID.randomUUID().toString();
    
            
    
            // ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค
    
            CompletableFuture.runAsync(() -> {
    
                try {
    
                    // ์žฅ์‹œ๊ฐ„ ์‹คํ–‰๋˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค
    
                    documentService.processDocument(documentId);
    
                    
    
                    // ์ƒํƒœ๋ฅผ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค (์ผ๋ฐ˜์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค)
    
                    processStatusRepository.updateStatus(processId, "completed");
    
                } catch (Exception ex) {
    
                    processStatusRepository.updateStatus(processId, "failed", ex.getMessage());
    
                }
    
            }, executorService);
    
            
    
            // ํ”„๋กœ์„ธ์Šค ID์™€ ํ•จ๊ป˜ ์ฆ‰์‹œ ์‘๋‹ต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค
    
            Map<String, Object> result = new HashMap<>();
    
            result.put("processId", processId);
    
            result.put("status", "processing");
    
            result.put("estimatedCompletionTime", ZonedDateTime.now().plusMinutes(5));
    
            
    
            return new ToolResponse.Builder().setResult(result).build();
    
        }
    
        
    
        // ๋™๋ฐ˜ ์ƒํƒœ ํ™•์ธ ๋„๊ตฌ
    
        public class ProcessStatusTool implements Tool {
    
            @Override
    
            public ToolResponse execute(ToolRequest request) {
    
                String processId = request.getParameters().get("processId").asText();
    
                ProcessStatus status = processStatusRepository.getStatus(processId);
    
                
    
                return new ToolResponse.Builder().setResult(status).build();
    
            }
    
        }
    
    }
    
    
    3. ๋ฆฌ์†Œ์Šค ์ œํ•œ

    ๊ณผ๋ถ€ํ•˜๋ฅผ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ๋ฆฌ์†Œ์Šค ์ œํ•œ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”:

    
    class ThrottledApiTool(Tool):
    
        def __init__(self):
    
            self.rate_limiter = TokenBucketRateLimiter(
    
                tokens_per_second=5,  # ์ดˆ๋‹น 5๊ฐœ์˜ ์š”์ฒญ ํ—ˆ์šฉ
    
                bucket_size=10        # ์ตœ๋Œ€ 10๊ฐœ์˜ ์š”์ฒญ ๋ฒ„์ŠคํŠธ ํ—ˆ์šฉ
    
            )
    
        
    
        async def execute_async(self, request):
    
            # ์ง„ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋˜๋Š” ๋Œ€๊ธฐํ•ด์•ผ ํ•˜๋Š”์ง€ ํ™•์ธ
    
            delay = self.rate_limiter.get_delay_time()
    
            
    
            if delay > 0:
    
                if delay > 2.0:  # ๋Œ€๊ธฐ ์‹œ๊ฐ„์ด ๋„ˆ๋ฌด ๊ธด ๊ฒฝ์šฐ
    
                    raise ToolExecutionException(
    
                        f"Rate limit exceeded. Please try again in {delay:.1f} seconds."
    
                    )
    
                else:
    
                    # ์ ์ ˆํ•œ ์ง€์—ฐ ์‹œ๊ฐ„ ๋™์•ˆ ๋Œ€๊ธฐ
    
                    await asyncio.sleep(delay)
    
            
    
            # ํ† ํฐ์„ ์†Œ๋ชจํ•˜๊ณ  ์š”์ฒญ ์ง„ํ–‰
    
            self.rate_limiter.consume()
    
            
    
            # API ํ˜ธ์ถœ
    
            result = await self._call_api(request.parameters)
    
            return ToolResponse(result=result)
    
    
    
    class TokenBucketRateLimiter:
    
        def __init__(self, tokens_per_second, bucket_size):
    
            self.tokens_per_second = tokens_per_second
    
            self.bucket_size = bucket_size
    
            self.tokens = bucket_size
    
            self.last_refill = time.time()
    
            self.lock = asyncio.Lock()
    
        
    
        async def get_delay_time(self):
    
            async with self.lock:
    
                self._refill()
    
                if self.tokens >= 1:
    
                    return 0
    
                
    
                # ๋‹ค์Œ ํ† ํฐ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์‹œ๊ฐ„ ๊ณ„์‚ฐ
    
                return (1 - self.tokens) / self.tokens_per_second
    
        
    
        async def consume(self):
    
            async with self.lock:
    
                self._refill()
    
                self.tokens -= 1
    
        
    
        def _refill(self):
    
            now = time.time()
    
            elapsed = now - self.last_refill
    
            
    
            # ๊ฒฝ๊ณผ ์‹œ๊ฐ„์— ๋”ฐ๋ผ ์ƒˆ๋กœ์šด ํ† ํฐ ์ถ”๊ฐ€
    
            new_tokens = elapsed * self.tokens_per_second
    
            self.tokens = min(self.bucket_size, self.tokens + new_tokens)
    
            self.last_refill = now
    
    

    ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    1. ์ž…๋ ฅ ๊ฒ€์ฆ

    ํ•ญ์ƒ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ฒ ์ €ํžˆ ๊ฒ€์ฆํ•˜์„ธ์š”:

    
    public async Task<ToolResponse> ExecuteAsync(ToolRequest request)
    
    {
    
        // Validate parameters exist
    
        if (!request.Parameters.TryGetProperty("query", out var queryProp))
    
        {
    
            throw new ToolExecutionException("Missing required parameter: query");
    
        }
    
        
    
        // Validate correct type
    
        if (queryProp.ValueKind != JsonValueKind.String)
    
        {
    
            throw new ToolExecutionException("Query parameter must be a string");
    
        }
    
        
    
        var query = queryProp.GetString();
    
        
    
        // Validate string content
    
        if (string.IsNullOrWhiteSpace(query))
    
        {
    
            throw new ToolExecutionException("Query parameter cannot be empty");
    
        }
    
        
    
        if (query.Length > 500)
    
        {
    
            throw new ToolExecutionException("Query parameter exceeds maximum length of 500 characters");
    
        }
    
        
    
        // Check for SQL injection attacks if applicable
    
        if (ContainsSqlInjection(query))
    
        {
    
            throw new ToolExecutionException("Invalid query: contains potentially unsafe SQL");
    
        }
    
        
    
        // Proceed with execution
    
        // ...
    
    }
    
    
    2. ๊ถŒํ•œ ๊ฒ€์‚ฌ

    ์ ์ ˆํ•œ ๊ถŒํ•œ ๊ฒ€์‚ฌ๋ฅผ ๊ตฌํ˜„ํ•˜์„ธ์š”:

    
    @Override
    
    public ToolResponse execute(ToolRequest request) {
    
        // ์š”์ฒญ์—์„œ ์‚ฌ์šฉ์ž ์ปจํ…์ŠคํŠธ ๊ฐ€์ ธ์˜ค๊ธฐ
    
        UserContext user = request.getContext().getUserContext();
    
        
    
        // ์‚ฌ์šฉ์ž๊ฐ€ ํ•„์š”ํ•œ ๊ถŒํ•œ์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š”์ง€ ํ™•์ธ
    
        if (!authorizationService.hasPermission(user, "documents:read")) {
    
            throw new ToolExecutionException("User does not have permission to access documents");
    
        }
    
        
    
        // ํŠน์ • ๋ฆฌ์†Œ์Šค์˜ ๊ฒฝ์šฐ ํ•ด๋‹น ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ ํ™•์ธ
    
        String documentId = request.getParameters().get("documentId").asText();
    
        if (!documentService.canUserAccess(user.getId(), documentId)) {
    
            throw new ToolExecutionException("Access denied to the requested document");
    
        }
    
        
    
        // ๋„๊ตฌ ์‹คํ–‰ ์ง„ํ–‰
    
        // ...
    
    }
    
    
    3. ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ

    ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐ์‹ฌ์Šค๋Ÿฝ๊ฒŒ ์ฒ˜๋ฆฌํ•˜์„ธ์š”:

    
    class SecureDataTool(Tool):
    
        def get_schema(self):
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "userId": {"type": "string"},
    
                    "includeSensitiveData": {"type": "boolean", "default": False}
    
                },
    
                "required": ["userId"]
    
            }
    
        
    
        async def execute_async(self, request):
    
            user_id = request.parameters["userId"]
    
            include_sensitive = request.parameters.get("includeSensitiveData", False)
    
            
    
            # ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
    
            user_data = await self.user_service.get_user_data(user_id)
    
            
    
            # ๋ช…์‹œ์ ์œผ๋กœ ์š”์ฒญ๋˜๊ณ  ๊ถŒํ•œ์ด ๋ถ€์—ฌ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋ฏผ๊ฐํ•œ ํ•„๋“œ ํ•„ํ„ฐ๋ง
    
            if not include_sensitive or not self._is_authorized_for_sensitive_data(request):
    
                user_data = self._redact_sensitive_fields(user_data)
    
            
    
            return ToolResponse(result=user_data)
    
        
    
        def _is_authorized_for_sensitive_data(self, request):
    
            # ์š”์ฒญ ์ปจํ…์ŠคํŠธ์—์„œ ๊ถŒํ•œ ์ˆ˜์ค€ ํ™•์ธ
    
            auth_level = request.context.get("authorizationLevel")
    
            return auth_level == "admin"
    
        
    
        def _redact_sensitive_fields(self, user_data):
    
            # ์›๋ณธ ๋ณ€๊ฒฝ์„ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ๋ณต์‚ฌ๋ณธ ์ƒ์„ฑ
    
            redacted = user_data.copy()
    
            
    
            # ํŠน์ • ๋ฏผ๊ฐํ•œ ํ•„๋“œ ๊ฐ€๋ฆฌ๊ธฐ
    
            sensitive_fields = ["ssn", "creditCardNumber", "password"]
    
            for field in sensitive_fields:
    
                if field in redacted:
    
                    redacted[field] = "REDACTED"
    
            
    
            # ์ค‘์ฒฉ๋œ ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ๊ฐ€๋ฆฌ๊ธฐ
    
            if "financialInfo" in redacted:
    
                redacted["financialInfo"] = {"available": True, "accessRestricted": True}
    
            
    
            return redacted
    
    

    MCP ๋„๊ตฌ ํ…Œ์ŠคํŠธ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ๋Š” MCP ๋„๊ตฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๊ณ  ๊ทน๋‹จ์  ์‚ฌ๋ก€๋ฅผ ์ฒ˜๋ฆฌํ•˜๋ฉฐ ์‹œ์Šคํ…œ๊ณผ ์ ์ ˆํžˆ ํ†ตํ•ฉ๋˜๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

    1. ๊ฐ ๋„๊ตฌ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ํ…Œ์ŠคํŠธ

    ๊ฐ ๋„๊ตฌ ๊ธฐ๋Šฅ์— ์ง‘์ค‘ํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ๋งŒ๋“œ์„ธ์š”:

    
    [Fact]
    
    public async Task WeatherTool_ValidLocation_ReturnsCorrectForecast()
    
    {
    
        // Arrange
    
        var mockWeatherService = new Mock<IWeatherService>();
    
        mockWeatherService
    
            .Setup(s => s.GetForecastAsync("Seattle", 3))
    
            .ReturnsAsync(new WeatherForecast(/* test data */));
    
        
    
        var tool = new WeatherForecastTool(mockWeatherService.Object);
    
        
    
        var request = new ToolRequest(
    
            toolName: "weatherForecast",
    
            parameters: JsonSerializer.SerializeToElement(new { 
    
                location = "Seattle", 
    
                days = 3 
    
            })
    
        );
    
        
    
        // Act
    
        var response = await tool.ExecuteAsync(request);
    
        
    
        // Assert
    
        Assert.NotNull(response);
    
        var result = JsonSerializer.Deserialize<WeatherForecast>(response.Result);
    
        Assert.Equal("Seattle", result.Location);
    
        Assert.Equal(3, result.DailyForecasts.Count);
    
    }
    
    
    
    [Fact]
    
    public async Task WeatherTool_InvalidLocation_ThrowsToolExecutionException()
    
    {
    
        // Arrange
    
        var mockWeatherService = new Mock<IWeatherService>();
    
        mockWeatherService
    
            .Setup(s => s.GetForecastAsync("InvalidLocation", It.IsAny<int>()))
    
            .ThrowsAsync(new LocationNotFoundException("Location not found"));
    
        
    
        var tool = new WeatherForecastTool(mockWeatherService.Object);
    
        
    
        var request = new ToolRequest(
    
            toolName: "weatherForecast",
    
            parameters: JsonSerializer.SerializeToElement(new { 
    
                location = "InvalidLocation", 
    
                days = 3 
    
            })
    
        );
    
        
    
        // Act & Assert
    
        var exception = await Assert.ThrowsAsync<ToolExecutionException>(
    
            () => tool.ExecuteAsync(request)
    
        );
    
        
    
        Assert.Contains("Location not found", exception.Message);
    
    }
    
    
    2. ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ

    ์Šคํ‚ค๋งˆ๊ฐ€ ์œ ํšจํ•˜๋ฉฐ ์ œ์•ฝ์„ ์ œ๋Œ€๋กœ ์‹œํ–‰ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”:

    
    @Test
    
    public void testSchemaValidation() {
    
        // ๋„๊ตฌ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ
    
        SearchTool searchTool = new SearchTool();
    
        
    
        // ์Šคํ‚ค๋งˆ ๊ฐ€์ ธ์˜ค๊ธฐ
    
        Object schema = searchTool.getSchema();
    
        
    
        // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์šฉ์œผ๋กœ ์Šคํ‚ค๋งˆ๋ฅผ JSON์œผ๋กœ ๋ณ€ํ™˜
    
        String schemaJson = objectMapper.writeValueAsString(schema);
    
        
    
        // ์Šคํ‚ค๋งˆ๊ฐ€ ์œ ํšจํ•œ JSONSchema์ธ์ง€ ๊ฒ€์ฆ
    
        JsonSchemaFactory factory = JsonSchemaFactory.byDefault();
    
        JsonSchema jsonSchema = factory.getJsonSchema(schemaJson);
    
        
    
        // ์œ ํšจํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜ ํ…Œ์ŠคํŠธ
    
        JsonNode validParams = objectMapper.createObjectNode()
    
            .put("query", "test query")
    
            .put("limit", 5);
    
            
    
        ProcessingReport validReport = jsonSchema.validate(validParams);
    
        assertTrue(validReport.isSuccess());
    
        
    
        // ํ•„์ˆ˜ ๋งค๊ฐœ๋ณ€์ˆ˜๊ฐ€ ๋ˆ„๋ฝ๋œ ๊ฒฝ์šฐ ํ…Œ์ŠคํŠธ
    
        JsonNode missingRequired = objectMapper.createObjectNode()
    
            .put("limit", 5);
    
            
    
        ProcessingReport missingReport = jsonSchema.validate(missingRequired);
    
        assertFalse(missingReport.isSuccess());
    
        
    
        // ์ž˜๋ชป๋œ ๋งค๊ฐœ๋ณ€์ˆ˜ ์œ ํ˜• ํ…Œ์ŠคํŠธ
    
        JsonNode invalidType = objectMapper.createObjectNode()
    
            .put("query", "test")
    
            .put("limit", "not-a-number");
    
            
    
        ProcessingReport invalidReport = jsonSchema.validate(invalidType);
    
        assertFalse(invalidReport.isSuccess());
    
    }
    
    
    3. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ

    ์˜ค๋ฅ˜ ์กฐ๊ฑด์— ๋Œ€ํ•œ ํŠน์ • ํ…Œ์ŠคํŠธ๋ฅผ ๋งŒ๋“œ์„ธ์š”:

    
    @pytest.mark.asyncio
    
    async def test_api_tool_handles_timeout():
    
        # ์ •๋ ฌ
    
        tool = ApiTool(timeout=0.1)  # ๋งค์šฐ ์งง์€ ํƒ€์ž„์•„์›ƒ
    
        
    
        # ํƒ€์ž„์•„์›ƒ ๋  ์š”์ฒญ์„ ๋ชจํ‚น
    
        with aioresponses() as mocked:
    
            mocked.get(
    
                "https://api.example.com/data",
    
                callback=lambda *args, **kwargs: asyncio.sleep(0.5)  # ํƒ€์ž„์•„์›ƒ๋ณด๋‹ค ๊ธด
    
            )
    
            
    
            request = ToolRequest(
    
                tool_name="apiTool",
    
                parameters={"url": "https://api.example.com/data"}
    
            )
    
            
    
            # ์‹คํ–‰ ๋ฐ ๊ฒ€์ฆ
    
            with pytest.raises(ToolExecutionException) as exc_info:
    
                await tool.execute_async(request)
    
            
    
            # ์˜ˆ์™ธ ๋ฉ”์‹œ์ง€ ํ™•์ธ
    
            assert "timed out" in str(exc_info.value).lower()
    
    
    
    @pytest.mark.asyncio
    
    async def test_api_tool_handles_rate_limiting():
    
        # ์ •๋ ฌ
    
        tool = ApiTool()
    
        
    
        # ์†๋„ ์ œํ•œ ์‘๋‹ต ๋ชจํ‚น
    
        with aioresponses() as mocked:
    
            mocked.get(
    
                "https://api.example.com/data",
    
                status=429,
    
                headers={"Retry-After": "2"},
    
                body=json.dumps({"error": "Rate limit exceeded"})
    
            )
    
            
    
            request = ToolRequest(
    
                tool_name="apiTool",
    
                parameters={"url": "https://api.example.com/data"}
    
            )
    
            
    
            # ์‹คํ–‰ ๋ฐ ๊ฒ€์ฆ
    
            with pytest.raises(ToolExecutionException) as exc_info:
    
                await tool.execute_async(request)
    
            
    
            # ์˜ˆ์™ธ์— ์†๋„ ์ œํ•œ ์ •๋ณด ํฌํ•จ ํ™•์ธ
    
            error_msg = str(exc_info.value).lower()
    
            assert "rate limit" in error_msg
    
            assert "try again" in error_msg
    
    

    ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    1. ๋„๊ตฌ ์ฒด์ธ ํ…Œ์ŠคํŠธ

    ๊ธฐ๋Œ€ํ•˜๋Š” ์กฐํ•ฉ์—์„œ ๋„๊ตฌ๋“ค์ด ํ•จ๊ป˜ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”:

    
    [Fact]
    
    public async Task DataProcessingWorkflow_CompletesSuccessfully()
    
    {
    
        // Arrange
    
        var dataFetchTool = new DataFetchTool(mockDataService.Object);
    
        var analysisTools = new DataAnalysisTool(mockAnalysisService.Object);
    
        var visualizationTool = new DataVisualizationTool(mockVisualizationService.Object);
    
        
    
        var toolRegistry = new ToolRegistry();
    
        toolRegistry.RegisterTool(dataFetchTool);
    
        toolRegistry.RegisterTool(analysisTools);
    
        toolRegistry.RegisterTool(visualizationTool);
    
        
    
        var workflowExecutor = new WorkflowExecutor(toolRegistry);
    
        
    
        // Act
    
        var result = await workflowExecutor.ExecuteWorkflowAsync(new[] {
    
            new ToolCall("dataFetch", new { source = "sales2023" }),
    
            new ToolCall("dataAnalysis", ctx => new { 
    
                data = ctx.GetResult("dataFetch"),
    
                analysis = "trend" 
    
            }),
    
            new ToolCall("dataVisualize", ctx => new {
    
                analysisResult = ctx.GetResult("dataAnalysis"),
    
                type = "line-chart"
    
            })
    
        });
    
        
    
        // Assert
    
        Assert.NotNull(result);
    
        Assert.True(result.Success);
    
        Assert.NotNull(result.GetResult("dataVisualize"));
    
        Assert.Contains("chartUrl", result.GetResult("dataVisualize").ToString());
    
    }
    
    
    2. MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ

    ์ „์ฒด ๋„๊ตฌ ๋“ฑ๋ก๊ณผ ์‹คํ–‰์œผ๋กœ MCP ์„œ๋ฒ„๋ฅผ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”:

    
    @SpringBootTest
    
    @AutoConfigureMockMvc
    
    public class McpServerIntegrationTest {
    
        
    
        @Autowired
    
        private MockMvc mockMvc;
    
        
    
        @Autowired
    
        private ObjectMapper objectMapper;
    
        
    
        @Test
    
        public void testToolDiscovery() throws Exception {
    
            // ๋ฐœ๊ฒฌ ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ
    
            mockMvc.perform(get("/mcp/tools"))
    
                .andExpect(status().isOk())
    
                .andExpect(jsonPath("$.tools").isArray())
    
                .andExpect(jsonPath("$.tools[*].name").value(hasItems(
    
                    "weatherForecast", "calculator", "documentSearch"
    
                )));
    
        }
    
        
    
        @Test
    
        public void testToolExecution() throws Exception {
    
            // ๋„๊ตฌ ์š”์ฒญ ์ƒ์„ฑ
    
            Map<String, Object> request = new HashMap<>();
    
            request.put("toolName", "calculator");
    
            
    
            Map<String, Object> parameters = new HashMap<>();
    
            parameters.put("operation", "add");
    
            parameters.put("a", 5);
    
            parameters.put("b", 7);
    
            request.put("parameters", parameters);
    
            
    
            // ์š”์ฒญ ์ „์†ก ๋ฐ ์‘๋‹ต ํ™•์ธ
    
            mockMvc.perform(post("/mcp/execute")
    
                .contentType(MediaType.APPLICATION_JSON)
    
                .content(objectMapper.writeValueAsString(request)))
    
                .andExpect(status().isOk())
    
                .andExpect(jsonPath("$.result.value").value(12));
    
        }
    
        
    
        @Test
    
        public void testToolValidation() throws Exception {
    
            // ์ž˜๋ชป๋œ ๋„๊ตฌ ์š”์ฒญ ์ƒ์„ฑ
    
            Map<String, Object> request = new HashMap<>();
    
            request.put("toolName", "calculator");
    
            
    
            Map<String, Object> parameters = new HashMap<>();
    
            parameters.put("operation", "divide");
    
            parameters.put("a", 10);
    
            // ๋ˆ„๋ฝ๋œ ๋งค๊ฐœ๋ณ€์ˆ˜ "b"
    
            request.put("parameters", parameters);
    
            
    
            // ์š”์ฒญ ์ „์†ก ๋ฐ ์˜ค๋ฅ˜ ์‘๋‹ต ํ™•์ธ
    
            mockMvc.perform(post("/mcp/execute")
    
                .contentType(MediaType.APPLICATION_JSON)
    
                .content(objectMapper.writeValueAsString(request)))
    
                .andExpect(status().isBadRequest())
    
                .andExpect(jsonPath("$.error").exists());
    
        }
    
    }
    
    
    3. ์—”๋“œ ํˆฌ ์—”๋“œ ํ…Œ์ŠคํŠธ

    ๋ชจ๋ธ ํ”„๋กฌํ”„ํŠธ๋ถ€ํ„ฐ ๋„๊ตฌ ์‹คํ–‰๊นŒ์ง€์˜ ์™„์ „ํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”:

    
    @pytest.mark.asyncio
    
    async def test_model_interaction_with_tool():
    
        # ์ •๋ ฌ - MCP ํด๋ผ์ด์–ธํŠธ ๋ฐ ๋ชจํ˜• ์„ค์ •
    
        mcp_client = McpClient(server_url="http://localhost:5000")
    
        
    
        # ๋ชจํ˜• ์‘๋‹ต ๋ชจ์˜
    
        mock_model = MockLanguageModel([
    
            MockResponse(
    
                "What's the weather in Seattle?",
    
                tool_calls=[{
    
                    "tool_name": "weatherForecast",
    
                    "parameters": {"location": "Seattle", "days": 3}
    
                }]
    
            ),
    
            MockResponse(
    
                "Here's the weather forecast for Seattle:\n- Today: 65ยฐF, Partly Cloudy\n- Tomorrow: 68ยฐF, Sunny\n- Day after: 62ยฐF, Rain",
    
                tool_calls=[]
    
            )
    
        ])
    
        
    
        # ๋‚ ์”จ ๋„๊ตฌ ์‘๋‹ต ๋ชจ์˜
    
        with aioresponses() as mocked:
    
            mocked.post(
    
                "http://localhost:5000/mcp/execute",
    
                payload={
    
                    "result": {
    
                        "location": "Seattle",
    
                        "forecast": [
    
                            {"date": "2023-06-01", "temperature": 65, "conditions": "Partly Cloudy"},
    
                            {"date": "2023-06-02", "temperature": 68, "conditions": "Sunny"},
    
                            {"date": "2023-06-03", "temperature": 62, "conditions": "Rain"}
    
                        ]
    
                    }
    
                }
    
            )
    
            
    
            # ์‹คํ–‰
    
            response = await mcp_client.send_prompt(
    
                "What's the weather in Seattle?",
    
                model=mock_model,
    
                allowed_tools=["weatherForecast"]
    
            )
    
            
    
            # ๋‹จ์–ธ
    
            assert "Seattle" in response.generated_text
    
            assert "65" in response.generated_text
    
            assert "Sunny" in response.generated_text
    
            assert "Rain" in response.generated_text
    
            assert len(response.tool_calls) == 1
    
            assert response.tool_calls[0].tool_name == "weatherForecast"
    
    

    ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

    1. ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ

    MCP ์„œ๋ฒ„๊ฐ€ ์–ผ๋งˆ๋‚˜ ๋งŽ์€ ๋™์‹œ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”:

    
    [Fact]
    
    public async Task McpServer_HandlesHighConcurrency()
    
    {
    
        // Arrange
    
        var server = new McpServer(
    
            name: "TestServer",
    
            version: "1.0",
    
            maxConcurrentRequests: 100
    
        );
    
        
    
        server.RegisterTool(new FastExecutingTool());
    
        await server.StartAsync();
    
        
    
        var client = new McpClient("http://localhost:5000");
    
        
    
        // Act
    
        var tasks = new List<Task<McpResponse>>();
    
        for (int i = 0; i < 1000; i++)
    
        {
    
            tasks.Add(client.ExecuteToolAsync("fastTool", new { iteration = i }));
    
        }
    
        
    
        var results = await Task.WhenAll(tasks);
    
        
    
        // Assert
    
        Assert.Equal(1000, results.Length);
    
        Assert.All(results, r => Assert.NotNull(r));
    
    }
    
    
    2. ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ

    ๊ทนํ•œ ๋ถ€ํ•˜ ํ•˜์—์„œ ์‹œ์Šคํ…œ์„ ํ…Œ์ŠคํŠธํ•˜์„ธ์š”:

    
    @Test
    
    public void testServerUnderStress() {
    
        int maxUsers = 1000;
    
        int rampUpTimeSeconds = 60;
    
        int testDurationSeconds = 300;
    
        
    
        // ์ŠคํŠธ๋ ˆ์Šค ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด JMeter ์„ค์น˜
    
        StandardJMeterEngine jmeter = new StandardJMeterEngine();
    
        
    
        // JMeter ํ…Œ์ŠคํŠธ ๊ณ„ํš ๊ตฌ์„ฑ
    
        HashTree testPlanTree = new HashTree();
    
        
    
        // ํ…Œ์ŠคํŠธ ๊ณ„ํš, ์Šค๋ ˆ๋“œ ๊ทธ๋ฃน, ์ƒ˜ํ”Œ๋Ÿฌ ๋“ฑ์„ ์ƒ์„ฑ
    
        TestPlan testPlan = new TestPlan("MCP Server Stress Test");
    
        testPlanTree.add(testPlan);
    
        
    
        ThreadGroup threadGroup = new ThreadGroup();
    
        threadGroup.setNumThreads(maxUsers);
    
        threadGroup.setRampUp(rampUpTimeSeconds);
    
        threadGroup.setScheduler(true);
    
        threadGroup.setDuration(testDurationSeconds);
    
        
    
        testPlanTree.add(threadGroup);
    
        
    
        // ๋„๊ตฌ ์‹คํ–‰์„ ์œ„ํ•œ HTTP ์ƒ˜ํ”Œ๋Ÿฌ ์ถ”๊ฐ€
    
        HTTPSampler toolExecutionSampler = new HTTPSampler();
    
        toolExecutionSampler.setDomain("localhost");
    
        toolExecutionSampler.setPort(5000);
    
        toolExecutionSampler.setPath("/mcp/execute");
    
        toolExecutionSampler.setMethod("POST");
    
        toolExecutionSampler.addArgument("toolName", "calculator");
    
        toolExecutionSampler.addArgument("parameters", "{\"operation\":\"add\",\"a\":5,\"b\":7}");
    
        
    
        threadGroup.add(toolExecutionSampler);
    
        
    
        // ๋ฆฌ์Šค๋„ˆ ์ถ”๊ฐ€
    
        SummaryReport summaryReport = new SummaryReport();
    
        threadGroup.add(summaryReport);
    
        
    
        // ํ…Œ์ŠคํŠธ ์‹คํ–‰
    
        jmeter.configure(testPlanTree);
    
        jmeter.run();
    
        
    
        // ๊ฒฐ๊ณผ ๊ฒ€์ฆ
    
        assertEquals(0, summaryReport.getErrorCount());
    
        assertTrue(summaryReport.getAverage() < 200); // ํ‰๊ท  ์‘๋‹ต ์‹œ๊ฐ„ < 200ms
    
        assertTrue(summaryReport.getPercentile(90.0) < 500); // 90๋ฒˆ์งธ ๋ฐฑ๋ถ„์œ„์ˆ˜ < 500ms
    
    }
    
    
    3. ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ํ”„๋กœํŒŒ์ผ๋ง

    ์žฅ๊ธฐ์  ์„ฑ๋Šฅ ๋ถ„์„์„ ์œ„ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์„ค์ •ํ•˜์„ธ์š”:

    
    # MCP ์„œ๋ฒ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ตฌ์„ฑ
    
    def configure_monitoring(server):
    
        # Prometheus ๋ฉ”ํŠธ๋ฆญ ์„ค์ •
    
        prometheus_metrics = {
    
            "request_count": Counter("mcp_requests_total", "Total MCP requests"),
    
            "request_latency": Histogram(
    
                "mcp_request_duration_seconds", 
    
                "Request duration in seconds",
    
                buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 10.0]
    
            ),
    
            "tool_execution_count": Counter(
    
                "mcp_tool_executions_total", 
    
                "Tool execution count",
    
                labelnames=["tool_name"]
    
            ),
    
            "tool_execution_latency": Histogram(
    
                "mcp_tool_duration_seconds", 
    
                "Tool execution duration in seconds",
    
                labelnames=["tool_name"],
    
                buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.5, 5.0, 10.0]
    
            ),
    
            "tool_errors": Counter(
    
                "mcp_tool_errors_total",
    
                "Tool execution errors",
    
                labelnames=["tool_name", "error_type"]
    
            )
    
        }
    
        
    
        # ํƒ€์ด๋ฐ ๋ฐ ๋ฉ”ํŠธ๋ฆญ ๊ธฐ๋ก์„ ์œ„ํ•œ ๋ฏธ๋“ค์›จ์–ด ์ถ”๊ฐ€
    
        server.add_middleware(PrometheusMiddleware(prometheus_metrics))
    
        
    
        # ๋ฉ”ํŠธ๋ฆญ ์—”๋“œํฌ์ธํŠธ ๋…ธ์ถœ
    
        @server.router.get("/metrics")
    
        async def metrics():
    
            return generate_latest()
    
        
    
        return server
    
    

    MCP ์›Œํฌํ”Œ๋กœ์šฐ ์„ค๊ณ„ ํŒจํ„ด

    ์ž˜ ์„ค๊ณ„๋œ MCP ์›Œํฌํ”Œ๋กœ์šฐ๋Š” ํšจ์œจ์„ฑ, ์‹ ๋ขฐ์„ฑ, ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. ์ฃผ์š” ํŒจํ„ด์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    1. ๋„๊ตฌ ์ฒด์ธ ํŒจํ„ด

    ์—ฌ๋Ÿฌ ๋„๊ตฌ๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ์—ฐ๊ฒฐํ•˜์—ฌ ๊ฐ ๋„๊ตฌ์˜ ์ถœ๋ ฅ์ด ๋‹ค์Œ ๋„๊ตฌ ์ž…๋ ฅ์ด ๋˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค:

    
    # ํŒŒ์ด์ฌ ์ฒด์ธ ์˜ค๋ธŒ ํˆด ๊ตฌํ˜„
    
    class ChainWorkflow:
    
        def __init__(self, tools_chain):
    
            self.tools_chain = tools_chain  # ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•  ๋„๊ตฌ ์ด๋ฆ„ ๋ชฉ๋ก
    
        
    
        async def execute(self, mcp_client, initial_input):
    
            current_result = initial_input
    
            all_results = {"input": initial_input}
    
            
    
            for tool_name in self.tools_chain:
    
                # ์ฒด์ธ์— ์žˆ๋Š” ๊ฐ ๋„๊ตฌ๋ฅผ ์‹คํ–‰ํ•˜๊ณ  ์ด์ „ ๊ฒฐ๊ณผ๋ฅผ ์ „๋‹ฌ
    
                response = await mcp_client.execute_tool(tool_name, current_result)
    
                
    
                # ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•˜๊ณ  ๋‹ค์Œ ๋„๊ตฌ์˜ ์ž…๋ ฅ์œผ๋กœ ์‚ฌ์šฉ
    
                all_results[tool_name] = response.result
    
                current_result = response.result
    
            
    
            return {
    
                "final_result": current_result,
    
                "all_results": all_results
    
            }
    
    
    
    # ์‚ฌ์šฉ ์˜ˆ์‹œ
    
    data_processing_chain = ChainWorkflow([
    
        "dataFetch",
    
        "dataCleaner",
    
        "dataAnalyzer",
    
        "dataVisualizer"
    
    ])
    
    
    
    result = await data_processing_chain.execute(
    
        mcp_client,
    
        {"source": "sales_database", "table": "transactions"}
    
    )
    
    

    2. ๋””์ŠคํŒจ์ฒ˜ ํŒจํ„ด

    ์ž…๋ ฅ์— ๋”ฐ๋ผ ์ „๋ฌธ ๋„๊ตฌ๋กœ ๋ถ„๋ฐฐํ•˜๋Š” ์ค‘์•™ ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜์„ธ์š”:

    
    public class ContentDispatcherTool : IMcpTool
    
    {
    
        private readonly IMcpClient _mcpClient;
    
        
    
        public ContentDispatcherTool(IMcpClient mcpClient)
    
        {
    
            _mcpClient = mcpClient;
    
        }
    
        
    
        public string Name => "contentProcessor";
    
        public string Description => "Processes content of various types";
    
        
    
        public object GetSchema()
    
        {
    
            return new {
    
                type = "object",
    
                properties = new {
    
                    content = new { type = "string" },
    
                    contentType = new { 
    
                        type = "string",
    
                        enum = new[] { "text", "html", "markdown", "csv", "code" }
    
                    },
    
                    operation = new { 
    
                        type = "string",
    
                        enum = new[] { "summarize", "analyze", "extract", "convert" }
    
                    }
    
                },
    
                required = new[] { "content", "contentType", "operation" }
    
            };
    
        }
    
        
    
        public async Task<ToolResponse> ExecuteAsync(ToolRequest request)
    
        {
    
            var content = request.Parameters.GetProperty("content").GetString();
    
            var contentType = request.Parameters.GetProperty("contentType").GetString();
    
            var operation = request.Parameters.GetProperty("operation").GetString();
    
            
    
            // Determine which specialized tool to use
    
            string targetTool = DetermineTargetTool(contentType, operation);
    
            
    
            // Forward to the specialized tool
    
            var specializedResponse = await _mcpClient.ExecuteToolAsync(
    
                targetTool,
    
                new { content, options = GetOptionsForTool(targetTool, operation) }
    
            );
    
            
    
            return new ToolResponse { Result = specializedResponse.Result };
    
        }
    
        
    
        private string DetermineTargetTool(string contentType, string operation)
    
        {
    
            return (contentType, operation) switch
    
            {
    
                ("text", "summarize") => "textSummarizer",
    
                ("text", "analyze") => "textAnalyzer",
    
                ("html", _) => "htmlProcessor",
    
                ("markdown", _) => "markdownProcessor",
    
                ("csv", _) => "csvProcessor",
    
                ("code", _) => "codeAnalyzer",
    
                _ => throw new ToolExecutionException($"No tool available for {contentType}/{operation}")
    
            };
    
        }
    
        
    
        private object GetOptionsForTool(string toolName, string operation)
    
        {
    
            // Return appropriate options for each specialized tool
    
            return toolName switch
    
            {
    
                "textSummarizer" => new { length = "medium" },
    
                "htmlProcessor" => new { cleanUp = true, operation },
    
                // Options for other tools...
    
                _ => new { }
    
            };
    
        }
    
    }
    
    

    3. ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ ํŒจํ„ด

    ํšจ์œจ์„ฑ์„ ์œ„ํ•ด ์—ฌ๋Ÿฌ ๋„๊ตฌ๋ฅผ ๋™์‹œ์— ์‹คํ–‰ํ•˜์„ธ์š”:

    
    public class ParallelDataProcessingWorkflow {
    
        private final McpClient mcpClient;
    
        
    
        public ParallelDataProcessingWorkflow(McpClient mcpClient) {
    
            this.mcpClient = mcpClient;
    
        }
    
        
    
        public WorkflowResult execute(String datasetId) {
    
            // 1๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ์…‹ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ(๋™๊ธฐ์‹)
    
            ToolResponse metadataResponse = mcpClient.executeTool("datasetMetadata", 
    
                Map.of("datasetId", datasetId));
    
            
    
            // 2๋‹จ๊ณ„: ์—ฌ๋Ÿฌ ๋ถ„์„์„ ๋ณ‘๋ ฌ๋กœ ์‹œ์ž‘
    
            CompletableFuture<ToolResponse> statisticalAnalysis = CompletableFuture.supplyAsync(() ->
    
                mcpClient.executeTool("statisticalAnalysis", Map.of(
    
                    "datasetId", datasetId,
    
                    "type", "comprehensive"
    
                ))
    
            );
    
            
    
            CompletableFuture<ToolResponse> correlationAnalysis = CompletableFuture.supplyAsync(() ->
    
                mcpClient.executeTool("correlationAnalysis", Map.of(
    
                    "datasetId", datasetId,
    
                    "method", "pearson"
    
                ))
    
            );
    
            
    
            CompletableFuture<ToolResponse> outlierDetection = CompletableFuture.supplyAsync(() ->
    
                mcpClient.executeTool("outlierDetection", Map.of(
    
                    "datasetId", datasetId,
    
                    "sensitivity", "medium"
    
                ))
    
            );
    
            
    
            // ๋ชจ๋“  ๋ณ‘๋ ฌ ์ž‘์—…์ด ์™„๋ฃŒ๋  ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ
    
            CompletableFuture<Void> allAnalyses = CompletableFuture.allOf(
    
                statisticalAnalysis, correlationAnalysis, outlierDetection
    
            );
    
            
    
            allAnalyses.join();  // ์™„๋ฃŒ ๋Œ€๊ธฐ
    
            
    
            // 3๋‹จ๊ณ„: ๊ฒฐ๊ณผ ๋ณ‘ํ•ฉ
    
            Map<String, Object> combinedResults = new HashMap<>();
    
            combinedResults.put("metadata", metadataResponse.getResult());
    
            combinedResults.put("statistics", statisticalAnalysis.join().getResult());
    
            combinedResults.put("correlations", correlationAnalysis.join().getResult());
    
            combinedResults.put("outliers", outlierDetection.join().getResult());
    
            
    
            // 4๋‹จ๊ณ„: ์š”์•ฝ ๋ณด๊ณ ์„œ ์ƒ์„ฑ
    
            ToolResponse summaryResponse = mcpClient.executeTool("reportGenerator", 
    
                Map.of("analysisResults", combinedResults));
    
            
    
            // ์ „์ฒด ์›Œํฌํ”Œ๋กœ์šฐ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
    
            WorkflowResult result = new WorkflowResult();
    
            result.setDatasetId(datasetId);
    
            result.setAnalysisResults(combinedResults);
    
            result.setSummaryReport(summaryResponse.getResult());
    
            
    
            return result;
    
        }
    
    }
    
    

    4. ์˜ค๋ฅ˜ ๋ณต๊ตฌ ํŒจํ„ด

    ๋„๊ตฌ ์‹คํŒจ์— ๋Œ€ํ•ด ์šฐ์•„ํ•œ ๋Œ€์ฒด ์ˆ˜๋‹จ์„ ๊ตฌํ˜„ํ•˜์„ธ์š”:

    
    class ResilientWorkflow:
    
        def __init__(self, mcp_client):
    
            self.client = mcp_client
    
        
    
        async def execute_with_fallback(self, primary_tool, fallback_tool, parameters):
    
            try:
    
                # ๋จผ์ € ๊ธฐ๋ณธ ๋„๊ตฌ๋ฅผ ์‹œ๋„ํ•˜์‹ญ์‹œ์˜ค
    
                response = await self.client.execute_tool(primary_tool, parameters)
    
                return {
    
                    "result": response.result,
    
                    "source": "primary",
    
                    "tool": primary_tool
    
                }
    
            except ToolExecutionException as e:
    
                # ์‹คํŒจ๋ฅผ ๊ธฐ๋กํ•˜์‹ญ์‹œ์˜ค
    
                logging.warning(f"Primary tool '{primary_tool}' failed: {str(e)}")
    
                
    
                # ๋ณด์กฐ ๋„๊ตฌ๋กœ ๋Œ€์ฒดํ•˜์‹ญ์‹œ์˜ค
    
                try:
    
                    # ๋ณด์กฐ ๋„๊ตฌ์— ๋งž๊ฒŒ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ณ€ํ™˜ํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค
    
                    fallback_params = self._adapt_parameters(parameters, primary_tool, fallback_tool)
    
                    
    
                    response = await self.client.execute_tool(fallback_tool, fallback_params)
    
                    return {
    
                        "result": response.result,
    
                        "source": "fallback",
    
                        "tool": fallback_tool,
    
                        "primaryError": str(e)
    
                    }
    
                except ToolExecutionException as fallback_error:
    
                    # ๋‘ ๋„๊ตฌ ๋ชจ๋‘ ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค
    
                    logging.error(f"Both primary and fallback tools failed. Fallback error: {str(fallback_error)}")
    
                    raise WorkflowExecutionException(
    
                        f"Workflow failed: primary error: {str(e)}; fallback error: {str(fallback_error)}"
    
                    )
    
        
    
        def _adapt_parameters(self, params, from_tool, to_tool):
    
            """Adapt parameters between different tools if needed"""
    
            # ์ด ๊ตฌํ˜„์€ ํŠน์ • ๋„๊ตฌ์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค
    
            # ์ด ์˜ˆ์ œ์—์„œ๋Š” ์›๋ž˜ ๋งค๊ฐœ๋ณ€์ˆ˜๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค
    
            return params
    
    
    
    # ์‚ฌ์šฉ ์˜ˆ
    
    async def get_weather(workflow, location):
    
        return await workflow.execute_with_fallback(
    
            "premiumWeatherService",  # ๊ธฐ๋ณธ(์œ ๋ฃŒ) ๋‚ ์”จ API
    
            "basicWeatherService",    # ๋ณด์กฐ(๋ฌด๋ฃŒ) ๋‚ ์”จ API
    
            {"location": location}
    
        )
    
    

    5. ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌ์„ฑ ํŒจํ„ด

    ๊ฐ„๋‹จํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ๋ณต์žกํ•œ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌ์ถ•ํ•˜์„ธ์š”:

    
    public class CompositeWorkflow : IWorkflow
    
    {
    
        private readonly List<IWorkflow> _workflows;
    
        
    
        public CompositeWorkflow(IEnumerable<IWorkflow> workflows)
    
        {
    
            _workflows = new List<IWorkflow>(workflows);
    
        }
    
        
    
        public async Task<WorkflowResult> ExecuteAsync(WorkflowContext context)
    
        {
    
            var results = new Dictionary<string, object>();
    
            
    
            foreach (var workflow in _workflows)
    
            {
    
                var workflowResult = await workflow.ExecuteAsync(context);
    
                
    
                // Store each workflow's result
    
                results[workflow.Name] = workflowResult;
    
                
    
                // Update context with the result for the next workflow
    
                context = context.WithResult(workflow.Name, workflowResult);
    
            }
    
            
    
            return new WorkflowResult(results);
    
        }
    
        
    
        public string Name => "CompositeWorkflow";
    
        public string Description => "Executes multiple workflows in sequence";
    
    }
    
    
    
    // Example usage
    
    var documentWorkflow = new CompositeWorkflow(new IWorkflow[] {
    
        new DocumentFetchWorkflow(),
    
        new DocumentProcessingWorkflow(),
    
        new InsightGenerationWorkflow(),
    
        new ReportGenerationWorkflow()
    
    });
    
    
    
    var result = await documentWorkflow.ExecuteAsync(new WorkflowContext {
    
        Parameters = new { documentId = "12345" }
    
    });
    
    

    MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ: ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ฃผ์š” ํŒ

    ๊ฐœ์š”

    ํ…Œ์ŠคํŠธ๋Š” ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๊ณ  ๊ณ ํ’ˆ์งˆ์˜ MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์˜ ํ•ต์‹ฌ ์š”์†Œ์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ถ€ํ„ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, ์—”๋“œ ํˆฌ ์—”๋“œ ๊ฒ€์ฆ์— ์ด๋ฅด๋Š” ๊ฐœ๋ฐœ ์ˆ˜๋ช…์ฃผ๊ธฐ ๋™์•ˆ MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ํฌ๊ด„์ ์ธ ๋ชจ๋ฒ” ์‚ฌ๋ก€์™€ ํŒ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ๊ฐ€ ์ค‘์š”ํ•œ ์ด์œ 

    MCP ์„œ๋ฒ„๋Š” AI ๋ชจ๋ธ๊ณผ ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐ„์˜ ์ค‘์š”ํ•œ ๋ฏธ๋“ค์›จ์–ด ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ์ฒ ์ €ํ•œ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค์Œ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค:

  • ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ์˜ ์‹ ๋ขฐ์„ฑ
  • ์š”์ฒญ ๋ฐ ์‘๋‹ต์˜ ์ •ํ™•ํ•œ ์ฒ˜๋ฆฌ
  • MCP ๋ช…์„ธ์˜ ์ ์ ˆํ•œ ๊ตฌํ˜„
  • ์‹คํŒจ ๋ฐ ๊ทนํ•œ ์‚ฌ๋ก€์— ๋Œ€ํ•œ ๋ณต์›๋ ฅ
  • ๋‹ค์–‘ํ•œ ๋ถ€ํ•˜์—์„œ ์ผ๊ด€๋œ ์„ฑ๋Šฅ
  • MCP ์„œ๋ฒ„ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

    ๋‹จ์œ„ ํ…Œ์ŠคํŠธ (๊ธฐ์ดˆ ๋‹จ๊ณ„)

    ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋Š” MCP ์„œ๋ฒ„์˜ ๊ฐœ๋ณ„ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๋…๋ฆฝ์ ์œผ๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

    ํ…Œ์ŠคํŠธ ๋Œ€์ƒ

    1. ๋ฆฌ์†Œ์Šค ํ•ธ๋“ค๋Ÿฌ: ๊ฐ ๋ฆฌ์†Œ์Šค ํ•ธ๋“ค๋Ÿฌ์˜ ๋กœ์ง ๋…๋ฆฝ์  ํ…Œ์ŠคํŠธ

    2. ๋„๊ตฌ ๊ตฌํ˜„: ๋‹ค์–‘ํ•œ ์ž…๋ ฅ์— ๋Œ€ํ•œ ๋„๊ตฌ ๋™์ž‘ ๊ฒ€์ฆ

    3. ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ: ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ Œ๋”๋˜๋Š”์ง€ ํ™•์ธ

    4. ์Šคํ‚ค๋งˆ ๊ฒ€์ฆ: ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ ๋กœ์ง ํ…Œ์ŠคํŠธ

    5. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ์ž˜๋ชป๋œ ์ž…๋ ฅ์— ๋Œ€ํ•œ ์˜ค๋ฅ˜ ์‘๋‹ต ๊ฒ€์ฆ

    ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ๋ชจ๋ฒ” ์‚ฌ๋ก€
    
    // Example unit test for a calculator tool in C#
    
    [Fact]
    
    public async Task CalculatorTool_Add_ReturnsCorrectSum()
    
    {
    
        // Arrange
    
        var calculator = new CalculatorTool();
    
        var parameters = new Dictionary<string, object>
    
        {
    
            ["operation"] = "add",
    
            ["a"] = 5,
    
            ["b"] = 7
    
        };
    
        
    
        // Act
    
        var response = await calculator.ExecuteAsync(parameters);
    
        var result = JsonSerializer.Deserialize<CalculationResult>(response.Content[0].ToString());
    
        
    
        // Assert
    
        Assert.Equal(12, result.Value);
    
    }
    
    
    
    # Python์—์„œ ๊ณ„์‚ฐ๊ธฐ ๋„๊ตฌ์— ๋Œ€ํ•œ ์˜ˆ์ œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ
    
    def test_calculator_tool_add():
    
        # ์ค€๋น„
    
        calculator = CalculatorTool()
    
        parameters = {
    
            "operation": "add",
    
            "a": 5,
    
            "b": 7
    
        }
    
        
    
        # ์‹คํ–‰
    
        response = calculator.execute(parameters)
    
        result = json.loads(response.content[0].text)
    
        
    
        # ๊ฒ€์ฆ
    
        assert result["value"] == 12
    
    

    ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ (์ค‘๊ฐ„ ๊ณ„์ธต)

    ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ๋Š” MCP ์„œ๋ฒ„ ๊ตฌ์„ฑ ์š”์†Œ ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

    ํ…Œ์ŠคํŠธ ๋Œ€์ƒ

    1. ์„œ๋ฒ„ ์ดˆ๊ธฐํ™”: ๋‹ค์–‘ํ•œ ๊ตฌ์„ฑ์œผ๋กœ ์„œ๋ฒ„ ์‹œ์ž‘ ํ…Œ์ŠคํŠธ

    2. ๋ผ์šฐํŠธ ๋“ฑ๋ก: ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ๊ฐ€ ์ œ๋Œ€๋กœ ๋“ฑ๋ก๋˜์—ˆ๋Š”์ง€ ํ™•์ธ

    3. ์š”์ฒญ ์ฒ˜๋ฆฌ: ์ „์ฒด ์š”์ฒญ-์‘๋‹ต ์‚ฌ์ดํด ํ…Œ์ŠคํŠธ

    4. ์˜ค๋ฅ˜ ์ „ํŒŒ: ๊ตฌ์„ฑ ์š”์†Œ ๊ฐ„ ์˜ค๋ฅ˜๊ฐ€ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€ ํ™•์ธ

    5. ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ: ๋ณด์•ˆ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ํ…Œ์ŠคํŠธ

    ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ๋ชจ๋ฒ” ์‚ฌ๋ก€
    
    // Example integration test for MCP server in C#
    
    [Fact]
    
    public async Task Server_ProcessToolRequest_ReturnsValidResponse()
    
    {
    
        // Arrange
    
        var server = new McpServer();
    
        server.RegisterTool(new CalculatorTool());
    
        await server.StartAsync();
    
        
    
        var request = new McpRequest
    
        {
    
            Tool = "calculator",
    
            Parameters = new Dictionary<string, object>
    
            {
    
                ["operation"] = "multiply",
    
                ["a"] = 6,
    
                ["b"] = 7
    
            }
    
        };
    
        
    
        // Act
    
        var response = await server.ProcessRequestAsync(request);
    
        
    
        // Assert
    
        Assert.NotNull(response);
    
        Assert.Equal(McpStatusCodes.Success, response.StatusCode);
    
        // Additional assertions for response content
    
        
    
        // Cleanup
    
        await server.StopAsync();
    
    }
    
    

    ์—”๋“œ ํˆฌ ์—”๋“œ ํ…Œ์ŠคํŠธ (์ตœ์ƒ์œ„ ๊ณ„์ธต)

    ์—”๋“œ ํˆฌ ์—”๋“œ ํ…Œ์ŠคํŠธ๋Š” ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„๊นŒ์ง€ ์ „์ฒด ์‹œ์Šคํ…œ ๋™์ž‘์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

    ํ…Œ์ŠคํŠธ ๋Œ€์ƒ

    1. ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ํ†ต์‹ : ์™„์ „ํ•œ ์š”์ฒญ-์‘๋‹ต ์‚ฌ์ดํด ํ…Œ์ŠคํŠธ

    2. ์‹ค์ œ ํด๋ผ์ด์–ธํŠธ SDK: ์‹ค์ œ ํด๋ผ์ด์–ธํŠธ ๊ตฌํ˜„์ฒด์™€ ํ…Œ์ŠคํŠธ

    3. ๋ถ€ํ•˜ ํ•˜ ์„ฑ๋Šฅ: ๋‹ค์ˆ˜ ๋™์‹œ ์š”์ฒญ ์‹œ ๋™์ž‘ ๊ฒ€์ฆ

    4. ์˜ค๋ฅ˜ ๋ณต๊ตฌ: ์‹คํŒจ ์‹œ ์‹œ์Šคํ…œ ๋ณต๊ตฌ ํ…Œ์ŠคํŠธ

    5. ์žฅ๊ธฐ๊ฐ„ ์ž‘์—…: ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐ ์žฅ๊ธฐ ์ž‘์—… ์ฒ˜๋ฆฌ ๊ฒ€์ฆ

    ์—”๋“œ ํˆฌ ์—”๋“œ ํ…Œ์ŠคํŠธ ๋ชจ๋ฒ” ์‚ฌ๋ก€
    
    // TypeScript๋กœ ์ž‘์„ฑ๋œ ํด๋ผ์ด์–ธํŠธ๋ฅผ ์‚ฌ์šฉํ•œ E2E ํ…Œ์ŠคํŠธ ์˜ˆ์ œ
    
    describe('MCP Server E2E Tests', () => {
    
      let client: McpClient;
    
      
    
      beforeAll(async () => {
    
        // ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ์—์„œ ์„œ๋ฒ„ ์‹œ์ž‘
    
        await startTestServer();
    
        client = new McpClient('http://localhost:5000');
    
      });
    
      
    
      afterAll(async () => {
    
        await stopTestServer();
    
      });
    
      
    
      test('Client can invoke calculator tool and get correct result', async () => {
    
        // ์‹คํ–‰
    
        const response = await client.invokeToolAsync('calculator', {
    
          operation: 'divide',
    
          a: 20,
    
          b: 4
    
        });
    
        
    
        // ๊ฒ€์ฆ
    
        expect(response.statusCode).toBe(200);
    
        expect(response.content[0].text).toContain('5');
    
      });
    
    });
    
    

    MCP ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๋ชจํ‚น ์ „๋žต

    ๋ชจํ‚น์€ ํ…Œ์ŠคํŠธํ•˜๋Š” ๋™์•ˆ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ฒฉ๋ฆฌํ•  ๋•Œ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.

    ๋ชจํ‚นํ•  ๊ตฌ์„ฑ ์š”์†Œ

    1. ์™ธ๋ถ€ AI ๋ชจ๋ธ: ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ๋ชจ๋ธ ์‘๋‹ต ๋ชจํ‚น

    2. ์™ธ๋ถ€ ์„œ๋น„์Šค: API ์ข…์†์„ฑ(๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ์„œ๋“œํŒŒํ‹ฐ ์„œ๋น„์Šค) ๋ชจํ‚น

    3. ์ธ์ฆ ์„œ๋น„์Šค: ์‹ ์› ์ œ๊ณต์ž ๋ชจํ‚น

    4. ๋ฆฌ์†Œ์Šค ์ œ๊ณต์ž: ๋น„์šฉ์ด ํฐ ๋ฆฌ์†Œ์Šค ํ•ธ๋“ค๋Ÿฌ ๋ชจํ‚น

    ์˜ˆ์‹œ: AI ๋ชจ๋ธ ์‘๋‹ต ๋ชจํ‚น

    
    // C# example with Moq
    
    var mockModel = new Mock<ILanguageModel>();
    
    mockModel
    
        .Setup(m => m.GenerateResponseAsync(
    
            It.IsAny<string>(),
    
            It.IsAny<McpRequestContext>()))
    
        .ReturnsAsync(new ModelResponse { 
    
            Text = "Mocked model response",
    
            FinishReason = FinishReason.Completed
    
        });
    
    
    
    var server = new McpServer(modelClient: mockModel.Object);
    
    
    
    # unittest.mock๋ฅผ ์‚ฌ์šฉํ•œ Python ์˜ˆ์ œ
    
    @patch('mcp_server.models.OpenAIModel')
    
    def test_with_mock_model(mock_model):
    
        # ๋ชฉ ์„ค์ •
    
        mock_model.return_value.generate_response.return_value = {
    
            "text": "Mocked model response",
    
            "finish_reason": "completed"
    
        }
    
        
    
        # ํ…Œ์ŠคํŠธ์—์„œ ๋ชฉ ์‚ฌ์šฉ
    
        server = McpServer(model_client=mock_model)
    
        # ํ…Œ์ŠคํŠธ ๊ณ„์† ์ง„ํ–‰
    
    

    ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

    ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ๋Š” ํ”„๋กœ๋•์…˜ MCP ์„œ๋ฒ„์— ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค.

    ์ธก์ • ๋Œ€์ƒ

    1. ์ง€์—ฐ ์‹œ๊ฐ„: ์š”์ฒญ์— ๋Œ€ํ•œ ์‘๋‹ต ์‹œ๊ฐ„

    2. ์ฒ˜๋ฆฌ๋Ÿ‰: ์ดˆ๋‹น ์ฒ˜๋ฆฌ ์š”์ฒญ ์ˆ˜

    3. ์ž์› ํ™œ์šฉ๋„: CPU, ๋ฉ”๋ชจ๋ฆฌ, ๋„คํŠธ์›Œํฌ ์‚ฌ์šฉ๋Ÿ‰

    4. ๋™์‹œ์„ฑ ์ฒ˜๋ฆฌ: ๋ณ‘๋ ฌ ์š”์ฒญ ์‹œ ๋™์ž‘

    5. ํ™•์žฅ ํŠน์„ฑ: ๋ถ€ํ•˜ ์ฆ๊ฐ€ ์‹œ ์„ฑ๋Šฅ

    ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ๋„๊ตฌ

  • k6: ์˜คํ”ˆ์†Œ์Šค ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๋„๊ตฌ
  • JMeter: ์ข…ํ•ฉ ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ
  • Locust: ํŒŒ์ด์ฌ ๊ธฐ๋ฐ˜ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ
  • Azure Load Testing: ํด๋ผ์šฐ๋“œ ๊ธฐ๋ฐ˜ ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ
  • ์˜ˆ์‹œ: k6๋ฅผ ์ด์šฉํ•œ ๊ธฐ๋ณธ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ

    
    // MCP ์„œ๋ฒ„ ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ k6 ์Šคํฌ๋ฆฝํŠธ
    
    import http from 'k6/http';
    
    import { check, sleep } from 'k6';
    
    
    
    export const options = {
    
      vus: 10,  // 10๋ช…์˜ ๊ฐ€์ƒ ์‚ฌ์šฉ์ž
    
      duration: '30s',
    
    };
    
    
    
    export default function () {
    
      const payload = JSON.stringify({
    
        tool: 'calculator',
    
        parameters: {
    
          operation: 'add',
    
          a: Math.floor(Math.random() * 100),
    
          b: Math.floor(Math.random() * 100)
    
        }
    
      });
    
    
    
      const params = {
    
        headers: {
    
          'Content-Type': 'application/json',
    
          'Authorization': 'Bearer test-token'
    
        },
    
      };
    
    
    
      const res = http.post('http://localhost:5000/api/tools/invoke', payload, params);
    
      
    
      check(res, {
    
        'status is 200': (r) => r.status === 200,
    
        'response time < 500ms': (r) => r.timings.duration < 500,
    
      });
    
      
    
      sleep(1);
    
    }
    
    

    MCP ์„œ๋ฒ„๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์ž๋™ํ™”

    ํ…Œ์ŠคํŠธ ์ž๋™ํ™”๋Š” ์ผ๊ด€๋œ ํ’ˆ์งˆ ์œ ์ง€์™€ ๋น ๋ฅธ ํ”ผ๋“œ๋ฐฑ ๋ฃจํ”„๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

    CI/CD ํ†ตํ•ฉ

    1. ํ’€ ๋ฆฌํ€˜์ŠคํŠธ์—์„œ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ ์‹คํ–‰: ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ๊ธฐ์กด ๊ธฐ๋Šฅ์„ ๊นจ๋œจ๋ฆฌ์ง€ ์•Š๋Š”์ง€ ํ™•์ธ

    2. ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ์—์„œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ: ์‚ฌ์ „ ์šด์˜ ํ™˜๊ฒฝ์—์„œ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์‹คํ–‰

    3. ์„ฑ๋Šฅ ๊ธฐ์ค€์„  ์œ ์ง€: ํšŒ๊ท€๋ฅผ ๊ฐ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์„ฑ๋Šฅ ๋ฒค์น˜๋งˆํฌ ์œ ์ง€

    4. ๋ณด์•ˆ ์Šค์บ”: ํŒŒ์ดํ”„๋ผ์ธ์˜ ์ผ๋ถ€๋กœ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ์ž๋™ํ™”

    ์˜ˆ์ œ CI ํŒŒ์ดํ”„๋ผ์ธ (GitHub Actions)

    
    name: MCP Server Tests
    
    
    
    on:
    
      push:
    
        branches: [ main ]
    
      pull_request:
    
        branches: [ main ]
    
    
    
    jobs:
    
      test:
    
        runs-on: ubuntu-latest
    
        
    
        steps:
    
        - uses: actions/checkout@v2
    
        
    
        - name: Set up Runtime
    
          uses: actions/setup-dotnet@v1
    
          with:
    
            dotnet-version: '8.0.x'
    
        
    
        - name: Restore dependencies
    
          run: dotnet restore
    
        
    
        - name: Build
    
          run: dotnet build --no-restore
    
        
    
        - name: Unit Tests
    
          run: dotnet test --no-build --filter Category=Unit
    
        
    
        - name: Integration Tests
    
          run: dotnet test --no-build --filter Category=Integration
    
          
    
        - name: Performance Tests
    
          run: dotnet run --project tests/PerformanceTests/PerformanceTests.csproj
    
    

    MCP ์‚ฌ์–‘ ์ค€์ˆ˜๋ฅผ ์œ„ํ•œ ํ…Œ์ŠคํŠธ

    ์„œ๋ฒ„๊ฐ€ MCP ์‚ฌ์–‘์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„ํ–ˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.

    ์ฃผ์š” ์ค€์ˆ˜ ์˜์—ญ

    1. API ์—”๋“œํฌ์ธํŠธ: ํ•„์ˆ˜ ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ(/resources, /tools ๋“ฑ)

    2. ์š”์ฒญ/์‘๋‹ต ํฌ๋งท: ์Šคํ‚ค๋งˆ ์ค€์ˆ˜ ๊ฒ€์ฆ

    3. ์˜ค๋ฅ˜ ์ฝ”๋“œ: ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋Œ€ํ•œ ์˜ฌ๋ฐ”๋ฅธ ์ƒํƒœ ์ฝ”๋“œ ํ™•์ธ

    4. ์ฝ˜ํ…์ธ  ์œ ํ˜•: ๋‹ค์–‘ํ•œ ์ฝ˜ํ…์ธ  ์œ ํ˜• ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ

    5. ์ธ์ฆ ํ๋ฆ„: ์‚ฌ์–‘์— ๋งž๋Š” ์ธ์ฆ ๋ฉ”์ปค๋‹ˆ์ฆ˜ ๊ฒ€์ฆ

    ์ค€์ˆ˜ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ

    
    [Fact]
    
    public async Task Server_ResourceEndpoint_ReturnsCorrectSchema()
    
    {
    
        // Arrange
    
        var client = new HttpClient();
    
        client.DefaultRequestHeaders.Add("Authorization", "Bearer test-token");
    
        
    
        // Act
    
        var response = await client.GetAsync("http://localhost:5000/api/resources");
    
        var content = await response.Content.ReadAsStringAsync();
    
        var resources = JsonSerializer.Deserialize<ResourceList>(content);
    
        
    
        // Assert
    
        Assert.Equal(HttpStatusCode.OK, response.StatusCode);
    
        Assert.NotNull(resources);
    
        Assert.All(resources.Resources, resource => 
    
        {
    
            Assert.NotNull(resource.Id);
    
            Assert.NotNull(resource.Type);
    
            // Additional schema validation
    
        });
    
    }
    
    

    ํšจ๊ณผ์ ์ธ MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ƒ์œ„ 10๊ฐ€์ง€ ํŒ

    1. ๋„๊ตฌ ์ •์˜๋ฅผ ๋ณ„๋„๋กœ ํ…Œ์ŠคํŠธ: ๋„๊ตฌ ๋กœ์ง๊ณผ ๋…๋ฆฝ์ ์œผ๋กœ ์Šคํ‚ค๋งˆ ์ •์˜ ๊ฒ€์ฆ

    2. ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ํ…Œ์ŠคํŠธ ์‚ฌ์šฉ: ๋‹ค์–‘ํ•œ ์ž…๋ ฅ๊ฐ’๊ณผ ๊ฒฝ๊ณ„๊ฐ’์„ ํฌํ•จํ•œ ๋„๊ตฌ ํ…Œ์ŠคํŠธ

    3. ์˜ค๋ฅ˜ ์‘๋‹ต ํ™•์ธ: ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์˜ค๋ฅ˜ ์กฐ๊ฑด์— ๋Œ€ํ•œ ์ ์ ˆํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ

    4. ๊ถŒํ•œ ๋ถ€์—ฌ ๋กœ์ง ํ…Œ์ŠคํŠธ: ๋‹ค์–‘ํ•œ ์‚ฌ์šฉ์ž ์—ญํ• ์— ๋Œ€ํ•œ ์ ์ ˆํ•œ ์ ‘๊ทผ ์ œ์–ด ๋ณด์žฅ

    5. ํ…Œ์ŠคํŠธ ์ปค๋ฒ„๋ฆฌ์ง€ ๋ชจ๋‹ˆํ„ฐ๋ง: ํ•ต์‹ฌ ๊ฒฝ๋กœ ์ฝ”๋“œ๋ฅผ ๋†’์€ ์ปค๋ฒ„๋ฆฌ์ง€๋กœ ๋ชฉํ‘œ ์„ค์ •

    6. ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต ํ…Œ์ŠคํŠธ: ์ŠคํŠธ๋ฆฌ๋ฐ ์ฝ˜ํ…์ธ ์˜ ์˜ฌ๋ฐ”๋ฅธ ์ฒ˜๋ฆฌ ๊ฒ€์ฆ

    7. ๋„คํŠธ์›Œํฌ ๋ฌธ์ œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜: ์—ด์•…ํ•œ ๋„คํŠธ์›Œํฌ ์กฐ๊ฑด์—์„œ ๋™์ž‘ ํ…Œ์ŠคํŠธ

    8. ๋ฆฌ์†Œ์Šค ํ•œ๋„ ํ…Œ์ŠคํŠธ: ํ• ๋‹น๋Ÿ‰ ๋˜๋Š” ์†๋„ ์ œํ•œ ๋„๋‹ฌ ์‹œ ๋™์ž‘ ๊ฒ€์ฆ

    9. ํšŒ๊ท€ ํ…Œ์ŠคํŠธ ์ž๋™ํ™”: ๋ชจ๋“  ์ฝ”๋“œ ๋ณ€๊ฒฝ ์‹œ ์‹คํ–‰๋˜๋Š” ์Šค์œ„ํŠธ ๊ตฌ์ถ•

    10. ํ…Œ์ŠคํŠธ ์ผ€์ด์Šค ๋ฌธ์„œํ™”: ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ๋ฌธ์„œํ™” ์œ ์ง€

    ์ผ๋ฐ˜์ ์ธ ํ…Œ์ŠคํŠธ ํ•จ์ •

  • ์ •์ƒ ๊ฒฝ๋กœ ํ…Œ์ŠคํŠธ์— ๊ณผ๋„ํ•œ ์˜์กด: ์˜ค๋ฅ˜ ์ผ€์ด์Šค๋ฅผ ์ฒ ์ €ํžˆ ํ…Œ์ŠคํŠธํ•  ๊ฒƒ
  • ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ ๋ฌด์‹œ: ํ”„๋กœ๋•์…˜์— ์˜ํ–ฅ์„ ๋ฏธ์น˜๊ธฐ ์ „์— ๋ณ‘๋ชฉ ์ง€์  ํŒŒ์•…
  • ๋ถ„๋ฆฌ๋œ ํ…Œ์ŠคํŠธ๋งŒ ์ˆ˜ํ–‰: ๋‹จ์œ„, ํ†ตํ•ฉ, E2E ํ…Œ์ŠคํŠธ ๊ฒฐํ•ฉ
  • ๋ถˆ์™„์ „ํ•œ API ์ปค๋ฒ„๋ฆฌ์ง€: ๋ชจ๋“  ์—”๋“œํฌ์ธํŠธ ๋ฐ ๊ธฐ๋Šฅ ํ…Œ์ŠคํŠธ ๋ณด์žฅ
  • ์ผ๊ด€๋˜์ง€ ์•Š์€ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ: ์ปจํ…Œ์ด๋„ˆ๋ฅผ ์‚ฌ์šฉํ•ด ์ผ๊ด€๋œ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์œ ์ง€
  • ๊ฒฐ๋ก 

    ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๊ณ  ๊ณ ํ’ˆ์งˆ์˜ MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์„ ์œ„ํ•ด ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์ „๋žต์ด ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ์— ์ œ์‹œ๋œ ๋ชจ๋ฒ” ์‚ฌ๋ก€์™€ ํŒ์„ ๊ตฌํ˜„ํ•˜๋ฉด MCP ๊ตฌํ˜„์ด ์ตœ๊ณ  ์ˆ˜์ค€์˜ ํ’ˆ์งˆ, ์‹ ๋ขฐ์„ฑ ๋ฐ ์„ฑ๋Šฅ์„ ์ถฉ์กฑํ•จ์„ ๋ณด์žฅํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ฃผ์š” ์š”์ 

    1. ๋„๊ตฌ ์„ค๊ณ„: ๋‹จ์ผ ์ฑ…์ž„ ์›์น™ ์ค€์ˆ˜, ์˜์กด์„ฑ ์ฃผ์ž… ์‚ฌ์šฉ, ์กฐํ•ฉ ๊ฐ€๋Šฅ์„ฑ์„ ๊ณ ๋ คํ•œ ์„ค๊ณ„

    2. ์Šคํ‚ค๋งˆ ์„ค๊ณ„: ๋ช…ํ™•ํ•˜๊ณ  ์ž˜ ๋ฌธ์„œํ™”๋œ ์Šคํ‚ค๋งˆ ์ƒ์„ฑ, ์ ์ ˆํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์ œ์•ฝ ์กฐ๊ฑด ํฌํ•จ

    3. ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ์šฐ์•„ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ, ๊ตฌ์กฐํ™”๋œ ์˜ค๋ฅ˜ ์‘๋‹ต ๋ฐ ์žฌ์‹œ๋„ ๋กœ์ง ๊ตฌํ˜„

    4. ์„ฑ๋Šฅ: ์บ์‹ฑ, ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ, ๋ฆฌ์†Œ์Šค ์ œํ•œ ์‚ฌ์šฉ

    5. ๋ณด์•ˆ: ์ฒ ์ €ํ•œ ์ž…๋ ฅ ๊ฒ€์ฆ, ๊ถŒํ•œ ๊ฒ€์‚ฌ, ๋ฏผ๊ฐ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์ ์šฉ

    6. ํ…Œ์ŠคํŠธ: ํฌ๊ด„์ ์ธ ๋‹จ์œ„, ํ†ตํ•ฉ, ์—”๋“œํˆฌ์—”๋“œ ํ…Œ์ŠคํŠธ ์ƒ์„ฑ

    7. ์›Œํฌํ”Œ๋กœ ํŒจํ„ด: ์ฒด์ธ, ๋””์ŠคํŒจ์ฒ˜, ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ์™€ ๊ฐ™์€ ํ™•๋ฆฝ๋œ ํŒจํ„ด ์ ์šฉ

    ์‹ค์Šต

    ๋‹ค์Œ ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฌธ์„œ ์ฒ˜๋ฆฌ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ MCP ๋„๊ตฌ ๋ฐ ์›Œํฌํ”Œ๋กœ ์„ค๊ณ„:

    1. ์—ฌ๋Ÿฌ ํ˜•์‹(PDF, DOCX, TXT)์˜ ๋ฌธ์„œ ์ˆ˜๋ฝ

    2. ๋ฌธ์„œ์—์„œ ํ…์ŠคํŠธ ๋ฐ ์ฃผ์š” ์ •๋ณด ์ถ”์ถœ

    3. ๋ฌธ์„œ ์œ ํ˜• ๋ฐ ๋‚ด์šฉ์— ๋”ฐ๋ผ ๋ถ„๋ฅ˜

    4. ๊ฐ ๋ฌธ์„œ ์š”์•ฝ ์ƒ์„ฑ

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์— ๊ฐ€์žฅ ์ ํ•ฉํ•œ ๋„๊ตฌ ์Šคํ‚ค๋งˆ, ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ, ์›Œํฌํ”Œ๋กœ ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜์„ธ์š”. ๋˜ํ•œ ์ด ๊ตฌํ˜„์„ ์–ด๋–ป๊ฒŒ ํ…Œ์ŠคํŠธํ• ์ง€ ๊ณ ๋ คํ•ด ๋ณด์„ธ์š”.

    ์ž๋ฃŒ

    1. ์ตœ์‹  ๊ฐœ๋ฐœ ์†Œ์‹์„ ์ ‘ํ•˜๋ ค๋ฉด Azure AI Foundry Discord Community์—์„œ MCP ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ฐธ์—ฌํ•˜์„ธ์š”.

    2. ์˜คํ”ˆ ์†Œ์Šค MCP ํ”„๋กœ์ ํŠธ์— ๊ธฐ์—ฌํ•˜์„ธ์š”.

    3. ๊ท€ํ•˜ ์กฐ์ง์˜ AI ์ด๋‹ˆ์…”ํ‹ฐ๋ธŒ์— MCP ์›์น™์„ ์ ์šฉํ•˜์„ธ์š”.

    4. ๊ท€ํ•˜ ์‚ฐ์—…์— ํŠนํ™”๋œ MCP ๊ตฌํ˜„๋„ ํƒ์ƒ‰ํ•ด ๋ณด์„ธ์š”.

    5. ๋‹ค์ค‘ ๋ชจ๋‹ฌ ํ†ตํ•ฉ ๋˜๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํ†ตํ•ฉ ๊ฐ™์€ ํŠน์ • MCP ์ฃผ์ œ์— ๋Œ€ํ•œ ๊ณ ๊ธ‰ ๊ณผ์ •์„ ์ˆ˜๊ฐ•ํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.

    6. Hands on Lab์„ ํ†ตํ•ด ๋ฐฐ์šด ์›์น™์œผ๋กœ ์ž์‹ ๋งŒ์˜ MCP ๋„๊ตฌ์™€ ์›Œํฌํ”Œ๋กœ๋ฅผ ์‹คํ—˜ํ•ด ๋ณด์„ธ์š”.

    ๋‹ค์Œ ๋‹จ๊ณ„

    ๋‹ค์Œ: ์‚ฌ๋ก€ ์—ฐ๊ตฌ

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ์„ ์•Œ๋ ค๋“œ๋ฆฝ๋‹ˆ๋‹ค.

    ์›๋ฌธ ๋ฌธ์„œ๋Š” ํ•ด๋‹น ์–ธ์–ด์˜ ๊ณต์‹ ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ ์ „๋ฌธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•œ ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    code Module 09

    Module 09 — ์‚ฌ๋ก€ ์—ฐ๊ตฌ

    MCP ์‹คํ–‰ ์‚ฌ๋ก€: ์‹ค์ œ ์‚ฌ๋ก€ ์—ฐ๊ตฌ

    _(์œ„ ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆญํ•˜๋ฉด ์ด ์ˆ˜์—…์˜ ๋™์˜์ƒ์„ ๋ณผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค)_

    Model Context Protocol(MCP)์€ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋ฐ์ดํ„ฐ, ๋„๊ตฌ ๋ฐ ์„œ๋น„์Šค์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ํ˜์‹ ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ์„น์…˜์—์„œ๋Š” ๋‹ค์–‘ํ•œ ๊ธฐ์—… ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ MCP์˜ ์‹ค์ œ ์ ์šฉ ์‚ฌ๋ก€๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ์ด ์„น์…˜์—์„œ๋Š” MCP ๊ตฌํ˜„์˜ ๊ตฌ์ฒด์ ์ธ ์˜ˆ๋ฅผ ํ†ตํ•ด ์กฐ์ง๋“ค์ด ์ด ํ”„๋กœํ† ์ฝœ์„ ํ™œ์šฉํ•ด ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•˜๊ณ  ์žˆ๋Š”์ง€ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค. ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋ฅผ ์‚ดํŽด๋ณด๋ฉด ์‹ค์ œ ์ƒํ™ฉ์—์„œ MCP์˜ ๋‹ค์žฌ๋‹ค๋Šฅํ•จ, ํ™•์žฅ์„ฑ, ์‹ค์งˆ์  ์ด์ ์— ๋Œ€ํ•œ ํ†ต์ฐฐ์„ ์–ป์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ฃผ์š” ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋“ค์„ ํƒ๊ตฌํ•จ์œผ๋กœ์จ ์—ฌ๋Ÿฌ๋ถ„์€:

  • MCP๊ฐ€ ํŠน์ • ๋น„์ฆˆ๋‹ˆ์Šค ๋ฌธ์ œ ํ•ด๊ฒฐ์— ์–ด๋–ป๊ฒŒ ์ ์šฉ๋  ์ˆ˜ ์žˆ๋Š”์ง€ ์ดํ•ดํ•˜๊ณ 
  • ๋‹ค์–‘ํ•œ ํ†ตํ•ฉ ํŒจํ„ด๊ณผ ์•„ํ‚คํ…์ฒ˜ ์ ‘๊ทผ ๋ฐฉ์‹์„ ๋ฐฐ์šฐ๋ฉฐ
  • ๊ธฐ์—… ํ™˜๊ฒฝ์—์„œ MCP ๊ตฌํ˜„์„ ์œ„ํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ์ธ์‹ํ•˜๊ณ 
  • ์‹ค์ œ ๊ตฌํ˜„ ๊ณผ์ •์—์„œ ๋งˆ์ฃผํ•œ ๋„์ „๊ณผ ํ•ด๊ฒฐ์ฑ…์— ๋Œ€ํ•œ ํ†ต์ฐฐ์„ ์–ป๊ณ 
  • ์œ ์‚ฌํ•œ ํŒจํ„ด์„ ์ž์‹ ์˜ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•  ๊ธฐํšŒ๋ฅผ ๋ฐœ๊ฒฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค
  • ์ฃผ์š” ์‚ฌ๋ก€ ์—ฐ๊ตฌ

    1. Azure AI ์—ฌํ–‰์‚ฌ โ€“ ์ฐธ์กฐ ๊ตฌํ˜„

    ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” MCP, Azure OpenAI, Azure AI Search๋ฅผ ์‚ฌ์šฉํ•ด ๋‹ค์ค‘ ์—์ด์ „ํŠธ ๊ธฐ๋ฐ˜ AI ์—ฌํ–‰ ๊ณ„ํš ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ์˜ ํฌ๊ด„์  ์ฐธ์กฐ ์†”๋ฃจ์…˜์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ํ”„๋กœ์ ํŠธ๋Š” ๋‹ค์Œ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:

  • MCP๋ฅผ ํ†ตํ•œ ๋‹ค์ค‘ ์—์ด์ „ํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜
  • Azure AI Search์™€์˜ ๊ธฐ์—… ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ
  • Azure ์„œ๋น„์Šค๋กœ ์•ˆ์ „ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜ ๊ตฌํ˜„
  • ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ MCP ๊ตฌ์„ฑ์š”์†Œ๋ฅผ ํ†ตํ•œ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ๋ง
  • Azure OpenAI ๊ธฐ๋ฐ˜ ๋Œ€ํ™”ํ˜• ์‚ฌ์šฉ์ž ๊ฒฝํ—˜
  • ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ์€ MCP๋ฅผ ์กฐ์œจ ๊ณ„์ธต์œผ๋กœ ํ™œ์šฉํ•œ ๋ณต์žกํ•œ ๋‹ค์ค‘ ์—์ด์ „ํŠธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•์— ๋Œ€ํ•œ ์œ ์šฉํ•œ ํ†ต์ฐฐ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    2. YouTube ๋ฐ์ดํ„ฐ๋กœ Azure DevOps ํ•ญ๋ชฉ ์—…๋ฐ์ดํŠธ

    ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” MCP๋ฅผ ์ด์šฉํ•ด ์›Œํฌํ”Œ๋กœ์šฐ ์ž๋™ํ™”๋ฅผ ์‹ค์šฉ์ ์œผ๋กœ ์ ์šฉํ•œ ์˜ˆ๋ฅผ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. MCP ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ:

  • ์˜จ๋ผ์ธ ํ”Œ๋žซํผ(YouTube)์—์„œ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
  • Azure DevOps ์‹œ์Šคํ…œ์˜ ์ž‘์—… ํ•ญ๋ชฉ ์—…๋ฐ์ดํŠธ
  • ๋ฐ˜๋ณต ๊ฐ€๋Šฅํ•œ ์ž๋™ํ™” ์›Œํฌํ”Œ๋กœ์šฐ ์ƒ์„ฑ
  • ๋ถ„์‚ฐ๋œ ์‹œ์Šคํ…œ ๊ฐ„ ๋ฐ์ดํ„ฐ ํ†ตํ•ฉ
  • ์ด ์˜ˆ์ œ๋Š” ๋น„๊ต์  ๋‹จ์ˆœํ•œ MCP ๊ตฌํ˜„์ด๋ผ๋„ ์ผ์ƒ ์—…๋ฌด ์ž๋™ํ™”์™€ ์‹œ์Šคํ…œ ๊ฐ„ ๋ฐ์ดํ„ฐ ์ผ๊ด€์„ฑ ํ–ฅ์ƒ์„ ํ†ตํ•ด ์ƒ๋‹นํ•œ ํšจ์œจ์„ฑ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Œ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    3. MCP๋ฅผ ํ™œ์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๊ฒ€์ƒ‰

    ์‚ฌ๋ก€ ์—ฐ๊ตฌ: ํด๋ผ์ด์–ธํŠธ์—์„œ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๊ธฐ

    ์ฝ”๋“œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ํ•  ๋•Œ ๋ฌธ์„œ ์‚ฌ์ดํŠธ, Stack Overflow, ์ˆ˜๋งŽ์€ ๊ฒ€์ƒ‰ ์—”์ง„ ํƒญ ์‚ฌ์ด๋ฅผ ๋ฐ”์˜๊ฒŒ ์˜ค๊ฐ€ ๋ณธ ์ ์ด ์žˆ๋‚˜์š”?

    ํ˜น์‹œ ๋ฌธ์„œ ์ „์šฉ์œผ๋กœ ๋‘ ๋ฒˆ์งธ ๋ชจ๋‹ˆํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ IDE์™€ ๋ธŒ๋ผ์šฐ์ € ์‚ฌ์ด๋ฅผ ๊ณ„์†ํ•ด์„œ Alt+Tab ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค.

    ๋ฌธ์„œ๋ฅผ ์›Œํฌํ”Œ๋กœ์šฐ ์•ˆ์—์„œโ€”์•ฑ, IDE ๋˜๋Š” ๋งž์ถค ๋„๊ตฌ์— ํ†ตํ•ฉํ•ด์„œโ€”๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ํ›จ์”ฌ ๋‚ซ์ง€ ์•Š์„๊นŒ์š”?

    ์ด๋ฒˆ ์‚ฌ๋ก€ ์—ฐ๊ตฌ์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ง์ ‘ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ํ˜„๋Œ€ ๊ฐœ๋ฐœ์€ ๋‹จ์ง€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ ์‹œ์— ์ ์ ˆํ•œ ์ •๋ณด๋ฅผ ์ฐพ๋Š” ์ผ์ž…๋‹ˆ๋‹ค.

    ๋ฌธ์„œ๋Š” ์–ด๋””์—๋‚˜ ์กด์žฌํ•˜์ง€๋งŒ, ๊ฐ€์žฅ ํ•„์š”ํ•  ๋•Œ์ธ ๋„๊ตฌ์™€ ์›Œํฌํ”Œ๋กœ์šฐ ๋‚ด๋ถ€์— ์žˆ๋Š” ๊ฒฝ์šฐ๋Š” ๋“œ๋ญ…๋‹ˆ๋‹ค.

    ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ง์ ‘ ํ†ตํ•ฉํ•˜๋ฉด ์‹œ๊ฐ„์„ ์ ˆ์•ฝํ•˜๊ณ , ์ปจํ…์ŠคํŠธ ์ „ํ™˜์„ ์ค„์ด๋ฉฐ ์ƒ์‚ฐ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ด ์„น์…˜์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์—ฌ ์•ฑ์„ ๋– ๋‚˜์ง€ ์•Š๊ณ ๋„ ์‹ค์‹œ๊ฐ„ ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ์— ์ ‘๊ทผํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.

    ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜๊ณ , ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉฐ, ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์„ ๋‹จ๊ณ„๋ณ„๋กœ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ ‘๊ทผ๋ฒ•์€ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ฐ„์†Œํ™”ํ•  ๋ฟ ์•„๋‹ˆ๋ผ, ๋” ๋˜‘๋˜‘ํ•˜๊ณ  ๋„์›€์ด ๋˜๋Š” ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€๋Šฅ์„ฑ์„ ์—ด์–ด์ค๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์™œ ์ด ์ž‘์—…์„ ํ• ๊นŒ์š”? ์ตœ๊ณ ์˜ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์€ ๋งˆ์ฐฐ์„ ์ œ๊ฑฐํ•˜๋Š” ๋ฐ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ, ์ฑ—๋ด‡ ๋˜๋Š” ์›น ์•ฑ์ด Microsoft Learn์˜ ์ตœ์‹  ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•ด ๋ฌธ์„œ ์งˆ๋ฌธ์— ์ฆ‰์‹œ ๋‹ตํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ์ƒํ•ด ๋ณด์„ธ์š”. ์ด ์žฅ์„ ๋งˆ์น˜๋ฉด ๋‹ค์Œ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋ฌธ์„œ์šฉ MCP ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ ํ†ต์‹ ์˜ ๊ธฐ๋ณธ ์ดํ•ด
  • Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ˜์†” ๋˜๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌํ˜„
  • ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ์ŠคํŠธ๋ฆฌ๋ฐ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ๋ฒ•
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ ๋ฌธ์„œ ์‘๋‹ต์„ ๋กœ๊น…ํ•˜๊ณ  ํ•ด์„ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์ด ๊ธฐ์ˆ ๋“ค์„ ํ†ตํ•ด ๋ฐ˜์‘ํ˜• ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ง„์ •์œผ๋กœ ๋Œ€ํ™”ํ˜•์ด๋ฉฐ ๋งฅ๋ฝ ์ธ์‹์ด ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฒ•์„ ๋ฐฐ์šฐ์‹ค ๊ฒ๋‹ˆ๋‹ค.

    ์‹œ๋‚˜๋ฆฌ์˜ค 1 - MCP๋ฅผ ์ด์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๊ฒ€์ƒ‰

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ๋ฅผ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์•ฑ์„ ๋– ๋‚˜์ง€ ์•Š๊ณ ๋„ ์‹ค์‹œ๊ฐ„ ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์‹ค์Šต์„ ์‹œ์ž‘ํ•ด ๋ด…์‹œ๋‹ค. Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์—ฌ microsoft_docs_search ๋„๊ตฌ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์„ ์ฝ˜์†”์— ๊ธฐ๋กํ•˜๋Š” ์•ฑ์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ๊ณผ์ œ์ž…๋‹ˆ๋‹ค.

    ์™œ ์ด ๋ฐฉ๋ฒ•์ธ๊ฐ€์š”?

    ์ด๊ฒƒ์ด ์ฑ—๋ด‡, IDE ํ™•์žฅ, ์›น ๋Œ€์‹œ๋ณด๋“œ์™€ ๊ฐ™์€ ๊ณ ๊ธ‰ ํ†ตํ•ฉ ๊ธฐ๋Šฅ์„ ๊ตฌ์ถ•ํ•˜๋Š” ํ† ๋Œ€๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ์ฝ”๋“œ์™€ ์ง€์นจ์€ ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ ๋‚ด solution ํด๋”์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„์— ๋”ฐ๋ผ ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜์„ธ์š”:

  • ๊ณต์‹ MCP SDK์™€ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ฐ€๋Šฅํ•œ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ
  • microsoft_docs_search ๋„๊ตฌ๋ฅผ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ํ˜ธ์ถœํ•˜์—ฌ ๋ฌธ์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
  • ์ ์ ˆํ•œ ๋กœ๊น… ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
  • ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€ํ™”ํ˜• ์ฝ˜์†” ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
  • ์ด ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ๋‹ค์Œ์„ ์‹œ์—ฐํ•ฉ๋‹ˆ๋‹ค:

  • Docs MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ
  • ์ฟผ๋ฆฌ ์ „์†ก
  • ๊ฒฐ๊ณผ ํŒŒ์‹ฑ ๋ฐ ์ถœ๋ ฅ
  • ์‹คํ–‰ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    Prompt> What is Azure Key Vault?
    
    Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
    
    

    ์•„๋ž˜๋Š” ์ตœ์†Œ ์ƒ˜ํ”Œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ”๋“œ์™€ ์ž์„ธํ•œ ๋‚ด์šฉ์€ solution ํด๋”์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    Python

    
    import asyncio
    
    from mcp.client.streamable_http import streamablehttp_client
    
    from mcp import ClientSession
    
    
    
    async def main():
    
        async with streamablehttp_client("https://learn.microsoft.com/api/mcp") as (read_stream, write_stream, _):
    
            async with ClientSession(read_stream, write_stream) as session:
    
                await session.initialize()
    
                result = await session.call_tool("microsoft_docs_search", {"query": "Azure Functions best practices"})
    
                print(result.content)
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
  • ์™„์ „ํ•œ ๊ตฌํ˜„ ๋ฐ ๋กœ๊น…์€ scenario1.py๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
  • ์„ค์น˜ ๋ฐ ์‚ฌ์šฉ๋ฒ• ์•ˆ๋‚ด๋Š” ๋™์ผ ํด๋”์˜ README.md๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.
  • ์‹œ๋‚˜๋ฆฌ์˜ค 2 - MCP๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋Œ€ํ™”ํ˜• ํ•™์Šต ํ”Œ๋žœ ์ƒ์„ฑ ์›น ์•ฑ

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” Docs MCP๋ฅผ ์›น ๊ฐœ๋ฐœ ํ”„๋กœ์ ํŠธ์— ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค. ๋ชฉํ‘œ๋Š” ์‚ฌ์šฉ์ž๋“ค์ด ์›น ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์ง์ ‘ Microsoft Learn ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์—ฌ, ์•ฑ์ด๋‚˜ ์‚ฌ์ดํŠธ ๋‚ด์—์„œ ์ฆ‰์‹œ ๋ฌธ์„œ ์ ‘๊ทผ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ๋‹ค์Œ ๋‚ด์šฉ์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์›น ์•ฑ ์„ค์ • ๋ฐฉ๋ฒ•
  • Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐ ๋ฐฉ๋ฒ•
  • ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฒ˜๋ฆฌ ๋ฐ ๊ฒฐ๊ณผ ํ‘œ์‹œ ๋ฐฉ๋ฒ•
  • ์‹คํ–‰ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    User> I want to learn about AI102 - so suggest the roadmap to get it started from learn for 6 weeks
    
    
    
    Assistant> Hereโ€™s a detailed 6-week roadmap to start your preparation for the AI-102: Designing and Implementing a Microsoft Azure AI Solution certification, using official Microsoft resources and focusing on exam skills areas:
    
    
    
    ---
    
    ## Week 1: Introduction & Fundamentals
    
    - **Understand the Exam**: Review the [AI-102 exam skills outline](https://learn.microsoft.com/en-us/credentials/certifications/exams/ai-102/).
    
    - **Set up Azure**: Sign up for a free Azure account if you don't have one.
    
    - **Learning Path**: [Introduction to Azure AI services](https://learn.microsoft.com/en-us/training/modules/intro-to-azure-ai/)
    
    - **Focus**: Get familiar with Azure portal, AI capabilities, and necessary tools.
    
    
    
    ....more weeks of the roadmap...
    
    
    
    Let me know if you want module-specific recommendations or need more customized weekly tasks!
    
    

    ์•„๋ž˜๋Š” ์ตœ์†Œ ์ƒ˜ํ”Œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ”๋“œ์™€ ์ž์„ธํ•œ ๋‚ด์šฉ์€ solution ํด๋”์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    Python (Chainlit)

    Chainlit์€ ๋Œ€ํ™”ํ˜• AI ์›น ์•ฑ์„ ๊ตฌ์ถ•ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. MCP ๋„๊ตฌ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋Œ€ํ™”ํ˜• ์ฑ—๋ด‡๊ณผ ์–ด์‹œ์Šคํ„ดํŠธ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ดํ•‘๊ณผ ์‚ฌ์šฉ์ž ์นœํ™”์  ์ธํ„ฐํŽ˜์ด์Šค์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

    
    import chainlit as cl
    
    import requests
    
    
    
    MCP_URL = "https://learn.microsoft.com/api/mcp"
    
    
    
    @cl.on_message
    
    def handle_message(message):
    
        query = {"question": message}
    
        response = requests.post(MCP_URL, json=query)
    
        if response.ok:
    
            result = response.json()
    
            cl.Message(content=result.get("answer", "No answer found.")).send()
    
        else:
    
            cl.Message(content="Error: " + response.text).send()
    
    
  • ์™„์ „ํ•œ ๊ตฌํ˜„์€ scenario2.py๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.
  • ์„ค์ • ๋ฐ ์‹คํ–‰ ์•ˆ๋‚ด๋Š” README.md๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
  • ์‹œ๋‚˜๋ฆฌ์˜ค 3: VS Code ๋‚ด MCP ์„œ๋ฒ„๋ฅผ ์ด์šฉํ•œ ์—๋””ํ„ฐ ๋‚ด ๋ฌธ์„œ ์กฐํšŒ

    VS Code ๋‚ด์—์„œ ๋ณ„๋„ ๋ธŒ๋ผ์šฐ์ € ํƒญ์„ ์ „ํ™˜ํ•˜์ง€ ์•Š๊ณ  Microsoft Learn Docs๋ฅผ ์ง์ ‘ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด MCP ์„œ๋ฒ„๋ฅผ ์—๋””ํ„ฐ ๋‚ด์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์Œ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค:

  • VS Code ๋‚ด์—์„œ ์ฝ”๋”ฉ ํ™˜๊ฒฝ์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๊ณ  ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋ฐ ์ฝ๊ธฐ
  • README ๋˜๋Š” ๊ฐ•์˜ ํŒŒ์ผ์— ๋ฌธ์„œ ์ฐธ์กฐ ๋ฐ ๋งํฌ ์‚ฝ์ž…
  • GitHub Copilot๊ณผ MCP๋ฅผ ํ•จ๊ป˜ ํ™œ์šฉํ•˜์—ฌ ์›ํ™œํ•œ AI ๊ธฐ๋ฐ˜ ๋ฌธ์„œ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌํ˜„
  • ๋‹ค์Œ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

  • ์ž‘์—…๊ณต๊ฐ„ ๋ฃจํŠธ์— ์œ ํšจํ•œ .vscode/mcp.json ํŒŒ์ผ ์ถ”๊ฐ€(์˜ˆ์‹œ๋Š” ์•„๋ž˜ ์ฐธ์กฐ)
  • VS Code ๋‚ด MCP ํŒจ๋„ ์—ด๊ธฐ ๋˜๋Š” ๋ช…๋ น ํŒ”๋ ˆํŠธ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋ฐ ์‚ฝ์ž…
  • ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ ์ž‘์—… ์ค‘์— ๋ฌธ์„œ ์ง์ ‘ ์ฐธ์กฐ
  • GitHub Copilot๊ณผ ๊ฒฐํ•ฉํ•˜์—ฌ ์ƒ์‚ฐ์„ฑ ๊ทน๋Œ€ํ™”
  • VS Code์—์„œ MCP ์„œ๋ฒ„ ์„ค์ • ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    {
    
      "servers": {
    
        "LearnDocsMCP": {
    
          "url": "https://learn.microsoft.com/api/mcp"
    
        }
    
      }
    
    }
    
    

    > ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ์™€ ์Šคํฌ๋ฆฐ์ƒท์ด ํฌํ•จ๋œ ์ž์„ธํ•œ ์„ค๋ช…์€ README.md๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ์ด ๋ฐฉ๋ฒ•์€ ๊ธฐ์ˆ  ๊ฐ•์ขŒ๋ฅผ ๋งŒ๋“ค๊ฑฐ๋‚˜ ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ฐธ์กฐ๊ฐ€ ํ•„์š”ํ•œ ์ฝ”๋“œ๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๋ชจ๋“  ์‚ฌ๋žŒ์—๊ฒŒ ์ด์ƒ์ ์ž…๋‹ˆ๋‹ค.

    ์ฃผ์š” ์š”์ 

    ๋ฌธ์„œ๋ฅผ ๋„๊ตฌ์— ์ง์ ‘ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์€ ๋‹จ์ˆœํ•œ ํŽธ์˜๊ฐ€ ์•„๋‹ˆ๋ผ ์ƒ์‚ฐ์„ฑ์˜ ํ˜๋ช…์ž…๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋ฉด:

  • ์ฝ”๋“œ์™€ ๋ฌธ์„œ ์‚ฌ์ด์˜ ์ปจํ…์ŠคํŠธ ์ „ํ™˜์„ ์—†์•จ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ตœ์‹  ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋” ๋˜‘๋˜‘ํ•˜๊ณ  ๋Œ€ํ™”ํ˜•์ ์ธ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด ๊ธฐ์ˆ ๋“ค์€ ํšจ์œจ์ ์ผ ๋ฟ ์•„๋‹ˆ๋ผ ์‚ฌ์šฉํ•˜๊ธฐ ์ฆ๊ฑฐ์šด ์†”๋ฃจ์…˜์„ ๋งŒ๋“œ๋Š” ๋ฐ ๋„์›€์„ ์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ์ถ”๊ฐ€ ์ž๋ฃŒ

    ์ดํ•ด๋ฅผ ๊นŠ๊ฒŒ ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๊ณต์‹ ์ž์›์„ ํƒ์ƒ‰ํ•ด ๋ณด์„ธ์š”:

  • Microsoft Learn Docs MCP ์„œ๋ฒ„ (GitHub)
  • Azure MCP ์„œ๋ฒ„ ์‹œ์ž‘ํ•˜๊ธฐ (mcp-python)
  • Azure MCP ์„œ๋ฒ„๋ž€?
  • Model Context Protocol (MCP) ์†Œ๊ฐœ
  • MCP ์„œ๋ฒ„์—์„œ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€ํ•˜๊ธฐ (Python)
  • ๋‹ค์Œ ๋‹จ๊ณ„

  • ์ด์ „์œผ๋กœ: ์‚ฌ๋ก€ ์—ฐ๊ตฌ ๊ฐœ์š”
  • ๊ณ„์†: ๋ชจ๋“ˆ 10: AI Toolkit์œผ๋กœ AI ์›Œํฌํ”Œ๋กœ์šฐ ๊ฐ„์†Œํ™”
  • ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋‚ด์šฉ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•ด ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ ๋ฌธ์„œ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•œ ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ์—์„œ๋Š” Python ์ฝ˜์†” ํด๋ผ์ด์–ธํŠธ๋ฅผ MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์—ฌ ์‹ค์‹œ๊ฐ„์œผ๋กœ ์ปจํ…์ŠคํŠธ ์ธ์‹ Microsoft ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๊ณ  ๊ธฐ๋กํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. ๋ฐฐ์šธ ๋‚ด์šฉ์€:

  • Python ํด๋ผ์ด์–ธํŠธ์™€ ๊ณต์‹ MCP SDK๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ํšจ์œจ์ ์ด๊ณ  ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ์ŠคํŠธ๋ฆฌ๋ฐ HTTP ํด๋ผ์ด์–ธํŠธ ํ™œ์šฉ
  • ์„œ๋ฒ„์˜ ๋ฌธ์„œํ™” ๋„๊ตฌ ํ˜ธ์ถœ ๋ฐ ์‘๋‹ต์„ ์ฝ˜์†”์— ์ง์ ‘ ๊ธฐ๋กํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ํ„ฐ๋ฏธ๋„์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๊ณ  ์ตœ์‹  Microsoft ๋ฌธ์„œ๋ฅผ ์›Œํฌํ”Œ๋กœ์šฐ์— ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์ด ์ฑ•ํ„ฐ์—๋Š” ์‹ค์Šต ๊ณผ์ œ, ์ตœ์†Œํ•œ์˜ ์ž‘๋™ ์ฝ”๋“œ ์ƒ˜ํ”Œ, ์‹ฌํ™” ํ•™์Šต์„ ์œ„ํ•œ ๋ฆฌ์†Œ์Šค ๋งํฌ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. MCP๊ฐ€ ์ฝ˜์†” ๊ธฐ๋ฐ˜ ํ™˜๊ฒฝ์—์„œ ๋ฌธ์„œ ์ ‘๊ทผ์„ฑ๊ณผ ๊ฐœ๋ฐœ์ž ์ƒ์‚ฐ์„ฑ์„ ์–ด๋–ป๊ฒŒ ํ˜์‹ ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์ „์ฒด ์›Œํฌ์Šค๋ฃจ์™€ ์ฝ”๋“œ๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    4. MCP ๊ธฐ๋ฐ˜ ๋Œ€ํ™”ํ˜• ํ•™์Šต ๊ณ„ํš ์ƒ์„ฑ ์›น์•ฑ

    ์‚ฌ๋ก€ ์—ฐ๊ตฌ: ํด๋ผ์ด์–ธํŠธ์—์„œ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๊ธฐ

    ์ฝ”๋“œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ํ•  ๋•Œ ๋ฌธ์„œ ์‚ฌ์ดํŠธ, Stack Overflow, ์ˆ˜๋งŽ์€ ๊ฒ€์ƒ‰ ์—”์ง„ ํƒญ ์‚ฌ์ด๋ฅผ ๋ฐ”์˜๊ฒŒ ์˜ค๊ฐ€ ๋ณธ ์ ์ด ์žˆ๋‚˜์š”?

    ํ˜น์‹œ ๋ฌธ์„œ ์ „์šฉ์œผ๋กœ ๋‘ ๋ฒˆ์งธ ๋ชจ๋‹ˆํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ IDE์™€ ๋ธŒ๋ผ์šฐ์ € ์‚ฌ์ด๋ฅผ ๊ณ„์†ํ•ด์„œ Alt+Tab ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค.

    ๋ฌธ์„œ๋ฅผ ์›Œํฌํ”Œ๋กœ์šฐ ์•ˆ์—์„œโ€”์•ฑ, IDE ๋˜๋Š” ๋งž์ถค ๋„๊ตฌ์— ํ†ตํ•ฉํ•ด์„œโ€”๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ํ›จ์”ฌ ๋‚ซ์ง€ ์•Š์„๊นŒ์š”?

    ์ด๋ฒˆ ์‚ฌ๋ก€ ์—ฐ๊ตฌ์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ง์ ‘ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ํ˜„๋Œ€ ๊ฐœ๋ฐœ์€ ๋‹จ์ง€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ ์‹œ์— ์ ์ ˆํ•œ ์ •๋ณด๋ฅผ ์ฐพ๋Š” ์ผ์ž…๋‹ˆ๋‹ค.

    ๋ฌธ์„œ๋Š” ์–ด๋””์—๋‚˜ ์กด์žฌํ•˜์ง€๋งŒ, ๊ฐ€์žฅ ํ•„์š”ํ•  ๋•Œ์ธ ๋„๊ตฌ์™€ ์›Œํฌํ”Œ๋กœ์šฐ ๋‚ด๋ถ€์— ์žˆ๋Š” ๊ฒฝ์šฐ๋Š” ๋“œ๋ญ…๋‹ˆ๋‹ค.

    ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ง์ ‘ ํ†ตํ•ฉํ•˜๋ฉด ์‹œ๊ฐ„์„ ์ ˆ์•ฝํ•˜๊ณ , ์ปจํ…์ŠคํŠธ ์ „ํ™˜์„ ์ค„์ด๋ฉฐ ์ƒ์‚ฐ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ด ์„น์…˜์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์—ฌ ์•ฑ์„ ๋– ๋‚˜์ง€ ์•Š๊ณ ๋„ ์‹ค์‹œ๊ฐ„ ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ์— ์ ‘๊ทผํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.

    ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜๊ณ , ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉฐ, ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์„ ๋‹จ๊ณ„๋ณ„๋กœ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ ‘๊ทผ๋ฒ•์€ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ฐ„์†Œํ™”ํ•  ๋ฟ ์•„๋‹ˆ๋ผ, ๋” ๋˜‘๋˜‘ํ•˜๊ณ  ๋„์›€์ด ๋˜๋Š” ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€๋Šฅ์„ฑ์„ ์—ด์–ด์ค๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์™œ ์ด ์ž‘์—…์„ ํ• ๊นŒ์š”? ์ตœ๊ณ ์˜ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์€ ๋งˆ์ฐฐ์„ ์ œ๊ฑฐํ•˜๋Š” ๋ฐ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ, ์ฑ—๋ด‡ ๋˜๋Š” ์›น ์•ฑ์ด Microsoft Learn์˜ ์ตœ์‹  ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•ด ๋ฌธ์„œ ์งˆ๋ฌธ์— ์ฆ‰์‹œ ๋‹ตํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ์ƒํ•ด ๋ณด์„ธ์š”. ์ด ์žฅ์„ ๋งˆ์น˜๋ฉด ๋‹ค์Œ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋ฌธ์„œ์šฉ MCP ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ ํ†ต์‹ ์˜ ๊ธฐ๋ณธ ์ดํ•ด
  • Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ˜์†” ๋˜๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌํ˜„
  • ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ์ŠคํŠธ๋ฆฌ๋ฐ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ๋ฒ•
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ ๋ฌธ์„œ ์‘๋‹ต์„ ๋กœ๊น…ํ•˜๊ณ  ํ•ด์„ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์ด ๊ธฐ์ˆ ๋“ค์„ ํ†ตํ•ด ๋ฐ˜์‘ํ˜• ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ง„์ •์œผ๋กœ ๋Œ€ํ™”ํ˜•์ด๋ฉฐ ๋งฅ๋ฝ ์ธ์‹์ด ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฒ•์„ ๋ฐฐ์šฐ์‹ค ๊ฒ๋‹ˆ๋‹ค.

    ์‹œ๋‚˜๋ฆฌ์˜ค 1 - MCP๋ฅผ ์ด์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๊ฒ€์ƒ‰

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ๋ฅผ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์•ฑ์„ ๋– ๋‚˜์ง€ ์•Š๊ณ ๋„ ์‹ค์‹œ๊ฐ„ ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์‹ค์Šต์„ ์‹œ์ž‘ํ•ด ๋ด…์‹œ๋‹ค. Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์—ฌ microsoft_docs_search ๋„๊ตฌ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์„ ์ฝ˜์†”์— ๊ธฐ๋กํ•˜๋Š” ์•ฑ์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ๊ณผ์ œ์ž…๋‹ˆ๋‹ค.

    ์™œ ์ด ๋ฐฉ๋ฒ•์ธ๊ฐ€์š”?

    ์ด๊ฒƒ์ด ์ฑ—๋ด‡, IDE ํ™•์žฅ, ์›น ๋Œ€์‹œ๋ณด๋“œ์™€ ๊ฐ™์€ ๊ณ ๊ธ‰ ํ†ตํ•ฉ ๊ธฐ๋Šฅ์„ ๊ตฌ์ถ•ํ•˜๋Š” ํ† ๋Œ€๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ์ฝ”๋“œ์™€ ์ง€์นจ์€ ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ ๋‚ด solution ํด๋”์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„์— ๋”ฐ๋ผ ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜์„ธ์š”:

  • ๊ณต์‹ MCP SDK์™€ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ฐ€๋Šฅํ•œ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ
  • microsoft_docs_search ๋„๊ตฌ๋ฅผ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ํ˜ธ์ถœํ•˜์—ฌ ๋ฌธ์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
  • ์ ์ ˆํ•œ ๋กœ๊น… ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
  • ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€ํ™”ํ˜• ์ฝ˜์†” ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
  • ์ด ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ๋‹ค์Œ์„ ์‹œ์—ฐํ•ฉ๋‹ˆ๋‹ค:

  • Docs MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ
  • ์ฟผ๋ฆฌ ์ „์†ก
  • ๊ฒฐ๊ณผ ํŒŒ์‹ฑ ๋ฐ ์ถœ๋ ฅ
  • ์‹คํ–‰ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    Prompt> What is Azure Key Vault?
    
    Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
    
    

    ์•„๋ž˜๋Š” ์ตœ์†Œ ์ƒ˜ํ”Œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ”๋“œ์™€ ์ž์„ธํ•œ ๋‚ด์šฉ์€ solution ํด๋”์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    Python

    
    import asyncio
    
    from mcp.client.streamable_http import streamablehttp_client
    
    from mcp import ClientSession
    
    
    
    async def main():
    
        async with streamablehttp_client("https://learn.microsoft.com/api/mcp") as (read_stream, write_stream, _):
    
            async with ClientSession(read_stream, write_stream) as session:
    
                await session.initialize()
    
                result = await session.call_tool("microsoft_docs_search", {"query": "Azure Functions best practices"})
    
                print(result.content)
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
  • ์™„์ „ํ•œ ๊ตฌํ˜„ ๋ฐ ๋กœ๊น…์€ scenario1.py๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
  • ์„ค์น˜ ๋ฐ ์‚ฌ์šฉ๋ฒ• ์•ˆ๋‚ด๋Š” ๋™์ผ ํด๋”์˜ README.md๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.
  • ์‹œ๋‚˜๋ฆฌ์˜ค 2 - MCP๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋Œ€ํ™”ํ˜• ํ•™์Šต ํ”Œ๋žœ ์ƒ์„ฑ ์›น ์•ฑ

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” Docs MCP๋ฅผ ์›น ๊ฐœ๋ฐœ ํ”„๋กœ์ ํŠธ์— ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค. ๋ชฉํ‘œ๋Š” ์‚ฌ์šฉ์ž๋“ค์ด ์›น ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์ง์ ‘ Microsoft Learn ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์—ฌ, ์•ฑ์ด๋‚˜ ์‚ฌ์ดํŠธ ๋‚ด์—์„œ ์ฆ‰์‹œ ๋ฌธ์„œ ์ ‘๊ทผ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ๋‹ค์Œ ๋‚ด์šฉ์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์›น ์•ฑ ์„ค์ • ๋ฐฉ๋ฒ•
  • Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐ ๋ฐฉ๋ฒ•
  • ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฒ˜๋ฆฌ ๋ฐ ๊ฒฐ๊ณผ ํ‘œ์‹œ ๋ฐฉ๋ฒ•
  • ์‹คํ–‰ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    User> I want to learn about AI102 - so suggest the roadmap to get it started from learn for 6 weeks
    
    
    
    Assistant> Hereโ€™s a detailed 6-week roadmap to start your preparation for the AI-102: Designing and Implementing a Microsoft Azure AI Solution certification, using official Microsoft resources and focusing on exam skills areas:
    
    
    
    ---
    
    ## Week 1: Introduction & Fundamentals
    
    - **Understand the Exam**: Review the [AI-102 exam skills outline](https://learn.microsoft.com/en-us/credentials/certifications/exams/ai-102/).
    
    - **Set up Azure**: Sign up for a free Azure account if you don't have one.
    
    - **Learning Path**: [Introduction to Azure AI services](https://learn.microsoft.com/en-us/training/modules/intro-to-azure-ai/)
    
    - **Focus**: Get familiar with Azure portal, AI capabilities, and necessary tools.
    
    
    
    ....more weeks of the roadmap...
    
    
    
    Let me know if you want module-specific recommendations or need more customized weekly tasks!
    
    

    ์•„๋ž˜๋Š” ์ตœ์†Œ ์ƒ˜ํ”Œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ”๋“œ์™€ ์ž์„ธํ•œ ๋‚ด์šฉ์€ solution ํด๋”์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    Python (Chainlit)

    Chainlit์€ ๋Œ€ํ™”ํ˜• AI ์›น ์•ฑ์„ ๊ตฌ์ถ•ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. MCP ๋„๊ตฌ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋Œ€ํ™”ํ˜• ์ฑ—๋ด‡๊ณผ ์–ด์‹œ์Šคํ„ดํŠธ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ดํ•‘๊ณผ ์‚ฌ์šฉ์ž ์นœํ™”์  ์ธํ„ฐํŽ˜์ด์Šค์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

    
    import chainlit as cl
    
    import requests
    
    
    
    MCP_URL = "https://learn.microsoft.com/api/mcp"
    
    
    
    @cl.on_message
    
    def handle_message(message):
    
        query = {"question": message}
    
        response = requests.post(MCP_URL, json=query)
    
        if response.ok:
    
            result = response.json()
    
            cl.Message(content=result.get("answer", "No answer found.")).send()
    
        else:
    
            cl.Message(content="Error: " + response.text).send()
    
    
  • ์™„์ „ํ•œ ๊ตฌํ˜„์€ scenario2.py๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.
  • ์„ค์ • ๋ฐ ์‹คํ–‰ ์•ˆ๋‚ด๋Š” README.md๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
  • ์‹œ๋‚˜๋ฆฌ์˜ค 3: VS Code ๋‚ด MCP ์„œ๋ฒ„๋ฅผ ์ด์šฉํ•œ ์—๋””ํ„ฐ ๋‚ด ๋ฌธ์„œ ์กฐํšŒ

    VS Code ๋‚ด์—์„œ ๋ณ„๋„ ๋ธŒ๋ผ์šฐ์ € ํƒญ์„ ์ „ํ™˜ํ•˜์ง€ ์•Š๊ณ  Microsoft Learn Docs๋ฅผ ์ง์ ‘ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด MCP ์„œ๋ฒ„๋ฅผ ์—๋””ํ„ฐ ๋‚ด์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์Œ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค:

  • VS Code ๋‚ด์—์„œ ์ฝ”๋”ฉ ํ™˜๊ฒฝ์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๊ณ  ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋ฐ ์ฝ๊ธฐ
  • README ๋˜๋Š” ๊ฐ•์˜ ํŒŒ์ผ์— ๋ฌธ์„œ ์ฐธ์กฐ ๋ฐ ๋งํฌ ์‚ฝ์ž…
  • GitHub Copilot๊ณผ MCP๋ฅผ ํ•จ๊ป˜ ํ™œ์šฉํ•˜์—ฌ ์›ํ™œํ•œ AI ๊ธฐ๋ฐ˜ ๋ฌธ์„œ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌํ˜„
  • ๋‹ค์Œ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

  • ์ž‘์—…๊ณต๊ฐ„ ๋ฃจํŠธ์— ์œ ํšจํ•œ .vscode/mcp.json ํŒŒ์ผ ์ถ”๊ฐ€(์˜ˆ์‹œ๋Š” ์•„๋ž˜ ์ฐธ์กฐ)
  • VS Code ๋‚ด MCP ํŒจ๋„ ์—ด๊ธฐ ๋˜๋Š” ๋ช…๋ น ํŒ”๋ ˆํŠธ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋ฐ ์‚ฝ์ž…
  • ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ ์ž‘์—… ์ค‘์— ๋ฌธ์„œ ์ง์ ‘ ์ฐธ์กฐ
  • GitHub Copilot๊ณผ ๊ฒฐํ•ฉํ•˜์—ฌ ์ƒ์‚ฐ์„ฑ ๊ทน๋Œ€ํ™”
  • VS Code์—์„œ MCP ์„œ๋ฒ„ ์„ค์ • ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    {
    
      "servers": {
    
        "LearnDocsMCP": {
    
          "url": "https://learn.microsoft.com/api/mcp"
    
        }
    
      }
    
    }
    
    

    > ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ์™€ ์Šคํฌ๋ฆฐ์ƒท์ด ํฌํ•จ๋œ ์ž์„ธํ•œ ์„ค๋ช…์€ README.md๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ์ด ๋ฐฉ๋ฒ•์€ ๊ธฐ์ˆ  ๊ฐ•์ขŒ๋ฅผ ๋งŒ๋“ค๊ฑฐ๋‚˜ ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ฐธ์กฐ๊ฐ€ ํ•„์š”ํ•œ ์ฝ”๋“œ๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๋ชจ๋“  ์‚ฌ๋žŒ์—๊ฒŒ ์ด์ƒ์ ์ž…๋‹ˆ๋‹ค.

    ์ฃผ์š” ์š”์ 

    ๋ฌธ์„œ๋ฅผ ๋„๊ตฌ์— ์ง์ ‘ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์€ ๋‹จ์ˆœํ•œ ํŽธ์˜๊ฐ€ ์•„๋‹ˆ๋ผ ์ƒ์‚ฐ์„ฑ์˜ ํ˜๋ช…์ž…๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋ฉด:

  • ์ฝ”๋“œ์™€ ๋ฌธ์„œ ์‚ฌ์ด์˜ ์ปจํ…์ŠคํŠธ ์ „ํ™˜์„ ์—†์•จ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ตœ์‹  ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋” ๋˜‘๋˜‘ํ•˜๊ณ  ๋Œ€ํ™”ํ˜•์ ์ธ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด ๊ธฐ์ˆ ๋“ค์€ ํšจ์œจ์ ์ผ ๋ฟ ์•„๋‹ˆ๋ผ ์‚ฌ์šฉํ•˜๊ธฐ ์ฆ๊ฑฐ์šด ์†”๋ฃจ์…˜์„ ๋งŒ๋“œ๋Š” ๋ฐ ๋„์›€์„ ์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ์ถ”๊ฐ€ ์ž๋ฃŒ

    ์ดํ•ด๋ฅผ ๊นŠ๊ฒŒ ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๊ณต์‹ ์ž์›์„ ํƒ์ƒ‰ํ•ด ๋ณด์„ธ์š”:

  • Microsoft Learn Docs MCP ์„œ๋ฒ„ (GitHub)
  • Azure MCP ์„œ๋ฒ„ ์‹œ์ž‘ํ•˜๊ธฐ (mcp-python)
  • Azure MCP ์„œ๋ฒ„๋ž€?
  • Model Context Protocol (MCP) ์†Œ๊ฐœ
  • MCP ์„œ๋ฒ„์—์„œ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€ํ•˜๊ธฐ (Python)
  • ๋‹ค์Œ ๋‹จ๊ณ„

  • ์ด์ „์œผ๋กœ: ์‚ฌ๋ก€ ์—ฐ๊ตฌ ๊ฐœ์š”
  • ๊ณ„์†: ๋ชจ๋“ˆ 10: AI Toolkit์œผ๋กœ AI ์›Œํฌํ”Œ๋กœ์šฐ ๊ฐ„์†Œํ™”
  • ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋‚ด์šฉ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•ด ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ ๋ฌธ์„œ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•œ ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” Chainlit์™€ Model Context Protocol(MCP)์„ ์‚ฌ์šฉํ•ด ์ฃผ์ œ๋ณ„ ๋งž์ถคํ˜• ํ•™์Šต ๊ณ„ํš์„ ์ƒ์„ฑํ•˜๋Š” ๋Œ€ํ™”ํ˜• ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์‚ฌ์šฉ์ž๋Š” ์˜ˆ๋ฅผ ๋“ค์–ด "AI-900 ์ธ์ฆ" ๊ฐ™์€ ์ฃผ์ œ์™€ 8์ฃผ ๊ฐ™์€ ํ•™์Šต ๊ธฐ๊ฐ„์„ ์ง€์ •ํ•˜๋ฉด ์•ฑ์€ ์ฃผ๋ณ„ ์ถ”์ฒœ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Chainlit์€ ๋Œ€ํ™”ํ˜• ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์—ฌ ๊ฒฝํ—˜์„ ํฅ๋ฏธ๋กญ๊ณ  ์ ์‘์ ์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

  • Chainlit๊ฐ€ ์ง€์›ํ•˜๋Š” ๋Œ€ํ™”ํ˜• ์›น ์•ฑ
  • ์‚ฌ์šฉ์ž๊ฐ€ ์ฃผ์ œ์™€ ๊ธฐ๊ฐ„์„ ์ง์ ‘ ์ง€์ •ํ•˜๋Š” ํ”„๋กฌํ”„ํŠธ
  • MCP๋ฅผ ์‚ฌ์šฉํ•œ ์ฃผ๋ณ„ ์ฝ˜ํ…์ธ  ์ถ”์ฒœ
  • ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค ๋‚ด ์‹ค์‹œ๊ฐ„ ์ ์‘ํ˜• ์‘๋‹ต
  • ์ด ํ”„๋กœ์ ํŠธ๋Š” ๋Œ€ํ™”ํ˜• AI์™€ MCP๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ํ˜„๋Œ€์  ์›น ํ™˜๊ฒฝ์—์„œ ๋™์ ์ด๊ณ  ์‚ฌ์šฉ์ž ์ค‘์‹ฌ์˜ ๊ต์œก ๋„๊ตฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ์‹์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    5. VS Code ๋‚ด MCP ์„œ๋ฒ„๋กœ ์—๋””ํ„ฐ ๋‚ด ๋ฌธ์„œ ํ™•์ธ

    ์‚ฌ๋ก€ ์—ฐ๊ตฌ: ํด๋ผ์ด์–ธํŠธ์—์„œ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๊ธฐ

    ์ฝ”๋“œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๊ณ  ํ•  ๋•Œ ๋ฌธ์„œ ์‚ฌ์ดํŠธ, Stack Overflow, ์ˆ˜๋งŽ์€ ๊ฒ€์ƒ‰ ์—”์ง„ ํƒญ ์‚ฌ์ด๋ฅผ ๋ฐ”์˜๊ฒŒ ์˜ค๊ฐ€ ๋ณธ ์ ์ด ์žˆ๋‚˜์š”?

    ํ˜น์‹œ ๋ฌธ์„œ ์ „์šฉ์œผ๋กœ ๋‘ ๋ฒˆ์งธ ๋ชจ๋‹ˆํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ IDE์™€ ๋ธŒ๋ผ์šฐ์ € ์‚ฌ์ด๋ฅผ ๊ณ„์†ํ•ด์„œ Alt+Tab ํ•˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์„ ๊ฒ๋‹ˆ๋‹ค.

    ๋ฌธ์„œ๋ฅผ ์›Œํฌํ”Œ๋กœ์šฐ ์•ˆ์—์„œโ€”์•ฑ, IDE ๋˜๋Š” ๋งž์ถค ๋„๊ตฌ์— ํ†ตํ•ฉํ•ด์„œโ€”๋ฐ”๋กœ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ํ›จ์”ฌ ๋‚ซ์ง€ ์•Š์„๊นŒ์š”?

    ์ด๋ฒˆ ์‚ฌ๋ก€ ์—ฐ๊ตฌ์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ง์ ‘ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ํ˜„๋Œ€ ๊ฐœ๋ฐœ์€ ๋‹จ์ง€ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ ์‹œ์— ์ ์ ˆํ•œ ์ •๋ณด๋ฅผ ์ฐพ๋Š” ์ผ์ž…๋‹ˆ๋‹ค.

    ๋ฌธ์„œ๋Š” ์–ด๋””์—๋‚˜ ์กด์žฌํ•˜์ง€๋งŒ, ๊ฐ€์žฅ ํ•„์š”ํ•  ๋•Œ์ธ ๋„๊ตฌ์™€ ์›Œํฌํ”Œ๋กœ์šฐ ๋‚ด๋ถ€์— ์žˆ๋Š” ๊ฒฝ์šฐ๋Š” ๋“œ๋ญ…๋‹ˆ๋‹ค.

    ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์ง์ ‘ ํ†ตํ•ฉํ•˜๋ฉด ์‹œ๊ฐ„์„ ์ ˆ์•ฝํ•˜๊ณ , ์ปจํ…์ŠคํŠธ ์ „ํ™˜์„ ์ค„์ด๋ฉฐ ์ƒ์‚ฐ์„ฑ์„ ๋†’์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ด ์„น์…˜์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ๊ฐ€ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์—ฌ ์•ฑ์„ ๋– ๋‚˜์ง€ ์•Š๊ณ ๋„ ์‹ค์‹œ๊ฐ„ ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ์— ์ ‘๊ทผํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค.

    ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜๊ณ , ์š”์ฒญ์„ ๋ณด๋‚ด๋ฉฐ, ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์„ ํšจ์œจ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ณผ์ •์„ ๋‹จ๊ณ„๋ณ„๋กœ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค. ์ด ์ ‘๊ทผ๋ฒ•์€ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ฐ„์†Œํ™”ํ•  ๋ฟ ์•„๋‹ˆ๋ผ, ๋” ๋˜‘๋˜‘ํ•˜๊ณ  ๋„์›€์ด ๋˜๋Š” ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ€๋Šฅ์„ฑ์„ ์—ด์–ด์ค๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์™œ ์ด ์ž‘์—…์„ ํ• ๊นŒ์š”? ์ตœ๊ณ ์˜ ๊ฐœ๋ฐœ์ž ๊ฒฝํ—˜์€ ๋งˆ์ฐฐ์„ ์ œ๊ฑฐํ•˜๋Š” ๋ฐ ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ, ์ฑ—๋ด‡ ๋˜๋Š” ์›น ์•ฑ์ด Microsoft Learn์˜ ์ตœ์‹  ์ฝ˜ํ…์ธ ๋ฅผ ์‚ฌ์šฉํ•ด ๋ฌธ์„œ ์งˆ๋ฌธ์— ์ฆ‰์‹œ ๋‹ตํ•  ์ˆ˜ ์žˆ๋‹ค๊ณ  ์ƒ์ƒํ•ด ๋ณด์„ธ์š”. ์ด ์žฅ์„ ๋งˆ์น˜๋ฉด ๋‹ค์Œ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋ฌธ์„œ์šฉ MCP ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ ํ†ต์‹ ์˜ ๊ธฐ๋ณธ ์ดํ•ด
  • Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ์ฝ˜์†” ๋˜๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌํ˜„
  • ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ์ŠคํŠธ๋ฆฌ๋ฐ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ๋ฒ•
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์—์„œ ๋ฌธ์„œ ์‘๋‹ต์„ ๋กœ๊น…ํ•˜๊ณ  ํ•ด์„ํ•˜๋Š” ๋ฐฉ๋ฒ•
  • ์ด ๊ธฐ์ˆ ๋“ค์„ ํ†ตํ•ด ๋ฐ˜์‘ํ˜• ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ์ง„์ •์œผ๋กœ ๋Œ€ํ™”ํ˜•์ด๋ฉฐ ๋งฅ๋ฝ ์ธ์‹์ด ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฒ•์„ ๋ฐฐ์šฐ์‹ค ๊ฒ๋‹ˆ๋‹ค.

    ์‹œ๋‚˜๋ฆฌ์˜ค 1 - MCP๋ฅผ ์ด์šฉํ•œ ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๊ฒ€์ƒ‰

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” ํด๋ผ์ด์–ธํŠธ๋ฅผ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ์•ฑ์„ ๋– ๋‚˜์ง€ ์•Š๊ณ ๋„ ์‹ค์‹œ๊ฐ„ ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์‹ค์Šต์„ ์‹œ์ž‘ํ•ด ๋ด…์‹œ๋‹ค. Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜์—ฌ microsoft_docs_search ๋„๊ตฌ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ , ์ŠคํŠธ๋ฆฌ๋ฐ ์‘๋‹ต์„ ์ฝ˜์†”์— ๊ธฐ๋กํ•˜๋Š” ์•ฑ์„ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ๊ณผ์ œ์ž…๋‹ˆ๋‹ค.

    ์™œ ์ด ๋ฐฉ๋ฒ•์ธ๊ฐ€์š”?

    ์ด๊ฒƒ์ด ์ฑ—๋ด‡, IDE ํ™•์žฅ, ์›น ๋Œ€์‹œ๋ณด๋“œ์™€ ๊ฐ™์€ ๊ณ ๊ธ‰ ํ†ตํ•ฉ ๊ธฐ๋Šฅ์„ ๊ตฌ์ถ•ํ•˜๋Š” ํ† ๋Œ€๊ฐ€ ๋˜๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์˜ ์ฝ”๋“œ์™€ ์ง€์นจ์€ ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ ๋‚ด solution ํด๋”์—์„œ ์ฐพ์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ ๋‹จ๊ณ„์— ๋”ฐ๋ผ ์—ฐ๊ฒฐ์„ ์„ค์ •ํ•˜์„ธ์š”:

  • ๊ณต์‹ MCP SDK์™€ ์ŠคํŠธ๋ฆฌ๋ฐ ๊ฐ€๋Šฅํ•œ HTTP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ
  • microsoft_docs_search ๋„๊ตฌ๋ฅผ ์ฟผ๋ฆฌ ๋งค๊ฐœ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ํ˜ธ์ถœํ•˜์—ฌ ๋ฌธ์„œ ๊ฐ€์ ธ์˜ค๊ธฐ
  • ์ ์ ˆํ•œ ๋กœ๊น… ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ตฌํ˜„
  • ์‚ฌ์šฉ์ž๊ฐ€ ์—ฌ๋Ÿฌ ๊ฒ€์ƒ‰ ์ฟผ๋ฆฌ๋ฅผ ์ž…๋ ฅํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€ํ™”ํ˜• ์ฝ˜์†” ์ธํ„ฐํŽ˜์ด์Šค ์ƒ์„ฑ
  • ์ด ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ๋‹ค์Œ์„ ์‹œ์—ฐํ•ฉ๋‹ˆ๋‹ค:

  • Docs MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ
  • ์ฟผ๋ฆฌ ์ „์†ก
  • ๊ฒฐ๊ณผ ํŒŒ์‹ฑ ๋ฐ ์ถœ๋ ฅ
  • ์‹คํ–‰ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    Prompt> What is Azure Key Vault?
    
    Answer> Azure Key Vault is a cloud service for securely storing and accessing secrets. ...
    
    

    ์•„๋ž˜๋Š” ์ตœ์†Œ ์ƒ˜ํ”Œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ”๋“œ์™€ ์ž์„ธํ•œ ๋‚ด์šฉ์€ solution ํด๋”์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    Python

    
    import asyncio
    
    from mcp.client.streamable_http import streamablehttp_client
    
    from mcp import ClientSession
    
    
    
    async def main():
    
        async with streamablehttp_client("https://learn.microsoft.com/api/mcp") as (read_stream, write_stream, _):
    
            async with ClientSession(read_stream, write_stream) as session:
    
                await session.initialize()
    
                result = await session.call_tool("microsoft_docs_search", {"query": "Azure Functions best practices"})
    
                print(result.content)
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
  • ์™„์ „ํ•œ ๊ตฌํ˜„ ๋ฐ ๋กœ๊น…์€ scenario1.py๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
  • ์„ค์น˜ ๋ฐ ์‚ฌ์šฉ๋ฒ• ์•ˆ๋‚ด๋Š” ๋™์ผ ํด๋”์˜ README.md๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.
  • ์‹œ๋‚˜๋ฆฌ์˜ค 2 - MCP๋กœ ๊ตฌํ˜„ํ•˜๋Š” ๋Œ€ํ™”ํ˜• ํ•™์Šต ํ”Œ๋žœ ์ƒ์„ฑ ์›น ์•ฑ

    ์ด ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ๋Š” Docs MCP๋ฅผ ์›น ๊ฐœ๋ฐœ ํ”„๋กœ์ ํŠธ์— ํ†ตํ•ฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค. ๋ชฉํ‘œ๋Š” ์‚ฌ์šฉ์ž๋“ค์ด ์›น ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์ง์ ‘ Microsoft Learn ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•˜์—ฌ, ์•ฑ์ด๋‚˜ ์‚ฌ์ดํŠธ ๋‚ด์—์„œ ์ฆ‰์‹œ ๋ฌธ์„œ ์ ‘๊ทผ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ๋‹ค์Œ ๋‚ด์šฉ์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์›น ์•ฑ ์„ค์ • ๋ฐฉ๋ฒ•
  • Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐ ๋ฐฉ๋ฒ•
  • ์‚ฌ์šฉ์ž ์ž…๋ ฅ ์ฒ˜๋ฆฌ ๋ฐ ๊ฒฐ๊ณผ ํ‘œ์‹œ ๋ฐฉ๋ฒ•
  • ์‹คํ–‰ ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    User> I want to learn about AI102 - so suggest the roadmap to get it started from learn for 6 weeks
    
    
    
    Assistant> Hereโ€™s a detailed 6-week roadmap to start your preparation for the AI-102: Designing and Implementing a Microsoft Azure AI Solution certification, using official Microsoft resources and focusing on exam skills areas:
    
    
    
    ---
    
    ## Week 1: Introduction & Fundamentals
    
    - **Understand the Exam**: Review the [AI-102 exam skills outline](https://learn.microsoft.com/en-us/credentials/certifications/exams/ai-102/).
    
    - **Set up Azure**: Sign up for a free Azure account if you don't have one.
    
    - **Learning Path**: [Introduction to Azure AI services](https://learn.microsoft.com/en-us/training/modules/intro-to-azure-ai/)
    
    - **Focus**: Get familiar with Azure portal, AI capabilities, and necessary tools.
    
    
    
    ....more weeks of the roadmap...
    
    
    
    Let me know if you want module-specific recommendations or need more customized weekly tasks!
    
    

    ์•„๋ž˜๋Š” ์ตœ์†Œ ์ƒ˜ํ”Œ ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค. ์ „์ฒด ์ฝ”๋“œ์™€ ์ž์„ธํ•œ ๋‚ด์šฉ์€ solution ํด๋”์—์„œ ํ™•์ธ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.

    Python (Chainlit)

    Chainlit์€ ๋Œ€ํ™”ํ˜• AI ์›น ์•ฑ์„ ๊ตฌ์ถ•ํ•˜๋Š” ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค. MCP ๋„๊ตฌ๋ฅผ ํ˜ธ์ถœํ•˜๊ณ  ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ํ‘œ์‹œํ•˜๋Š” ๋Œ€ํ™”ํ˜• ์ฑ—๋ด‡๊ณผ ์–ด์‹œ์Šคํ„ดํŠธ๋ฅผ ์‰ฝ๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ดํ•‘๊ณผ ์‚ฌ์šฉ์ž ์นœํ™”์  ์ธํ„ฐํŽ˜์ด์Šค์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

    
    import chainlit as cl
    
    import requests
    
    
    
    MCP_URL = "https://learn.microsoft.com/api/mcp"
    
    
    
    @cl.on_message
    
    def handle_message(message):
    
        query = {"question": message}
    
        response = requests.post(MCP_URL, json=query)
    
        if response.ok:
    
            result = response.json()
    
            cl.Message(content=result.get("answer", "No answer found.")).send()
    
        else:
    
            cl.Message(content="Error: " + response.text).send()
    
    
  • ์™„์ „ํ•œ ๊ตฌํ˜„์€ scenario2.py๋ฅผ ์ฐธ๊ณ ํ•˜์„ธ์š”.
  • ์„ค์ • ๋ฐ ์‹คํ–‰ ์•ˆ๋‚ด๋Š” README.md๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.
  • ์‹œ๋‚˜๋ฆฌ์˜ค 3: VS Code ๋‚ด MCP ์„œ๋ฒ„๋ฅผ ์ด์šฉํ•œ ์—๋””ํ„ฐ ๋‚ด ๋ฌธ์„œ ์กฐํšŒ

    VS Code ๋‚ด์—์„œ ๋ณ„๋„ ๋ธŒ๋ผ์šฐ์ € ํƒญ์„ ์ „ํ™˜ํ•˜์ง€ ์•Š๊ณ  Microsoft Learn Docs๋ฅผ ์ง์ ‘ ๋ณด๊ณ  ์‹ถ๋‹ค๋ฉด MCP ์„œ๋ฒ„๋ฅผ ์—๋””ํ„ฐ ๋‚ด์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋‹ค์Œ์ด ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค:

  • VS Code ๋‚ด์—์„œ ์ฝ”๋”ฉ ํ™˜๊ฒฝ์„ ๋ฒ—์–ด๋‚˜์ง€ ์•Š๊ณ  ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋ฐ ์ฝ๊ธฐ
  • README ๋˜๋Š” ๊ฐ•์˜ ํŒŒ์ผ์— ๋ฌธ์„œ ์ฐธ์กฐ ๋ฐ ๋งํฌ ์‚ฝ์ž…
  • GitHub Copilot๊ณผ MCP๋ฅผ ํ•จ๊ป˜ ํ™œ์šฉํ•˜์—ฌ ์›ํ™œํ•œ AI ๊ธฐ๋ฐ˜ ๋ฌธ์„œ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌํ˜„
  • ๋‹ค์Œ ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

  • ์ž‘์—…๊ณต๊ฐ„ ๋ฃจํŠธ์— ์œ ํšจํ•œ .vscode/mcp.json ํŒŒ์ผ ์ถ”๊ฐ€(์˜ˆ์‹œ๋Š” ์•„๋ž˜ ์ฐธ์กฐ)
  • VS Code ๋‚ด MCP ํŒจ๋„ ์—ด๊ธฐ ๋˜๋Š” ๋ช…๋ น ํŒ”๋ ˆํŠธ ์‚ฌ์šฉํ•˜์—ฌ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋ฐ ์‚ฝ์ž…
  • ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ ์ž‘์—… ์ค‘์— ๋ฌธ์„œ ์ง์ ‘ ์ฐธ์กฐ
  • GitHub Copilot๊ณผ ๊ฒฐํ•ฉํ•˜์—ฌ ์ƒ์‚ฐ์„ฑ ๊ทน๋Œ€ํ™”
  • VS Code์—์„œ MCP ์„œ๋ฒ„ ์„ค์ • ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    
    {
    
      "servers": {
    
        "LearnDocsMCP": {
    
          "url": "https://learn.microsoft.com/api/mcp"
    
        }
    
      }
    
    }
    
    

    > ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ์™€ ์Šคํฌ๋ฆฐ์ƒท์ด ํฌํ•จ๋œ ์ž์„ธํ•œ ์„ค๋ช…์€ README.md๋ฅผ ์ฐธ์กฐํ•˜์„ธ์š”.

    ์ด ๋ฐฉ๋ฒ•์€ ๊ธฐ์ˆ  ๊ฐ•์ขŒ๋ฅผ ๋งŒ๋“ค๊ฑฐ๋‚˜ ๋ฌธ์„œ๋ฅผ ์ž‘์„ฑํ•˜๊ฑฐ๋‚˜ ๋นˆ๋ฒˆํ•˜๊ฒŒ ์ฐธ์กฐ๊ฐ€ ํ•„์š”ํ•œ ์ฝ”๋“œ๋ฅผ ๊ฐœ๋ฐœํ•˜๋Š” ๋ชจ๋“  ์‚ฌ๋žŒ์—๊ฒŒ ์ด์ƒ์ ์ž…๋‹ˆ๋‹ค.

    ์ฃผ์š” ์š”์ 

    ๋ฌธ์„œ๋ฅผ ๋„๊ตฌ์— ์ง์ ‘ ํ†ตํ•ฉํ•˜๋Š” ๊ฒƒ์€ ๋‹จ์ˆœํ•œ ํŽธ์˜๊ฐ€ ์•„๋‹ˆ๋ผ ์ƒ์‚ฐ์„ฑ์˜ ํ˜๋ช…์ž…๋‹ˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ์—์„œ Microsoft Learn Docs MCP ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•˜๋ฉด:

  • ์ฝ”๋“œ์™€ ๋ฌธ์„œ ์‚ฌ์ด์˜ ์ปจํ…์ŠคํŠธ ์ „ํ™˜์„ ์—†์•จ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ตœ์‹  ๋งฅ๋ฝ ์ธ์‹ ๋ฌธ์„œ๋ฅผ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ๋” ๋˜‘๋˜‘ํ•˜๊ณ  ๋Œ€ํ™”ํ˜•์ ์ธ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
  • ์ด ๊ธฐ์ˆ ๋“ค์€ ํšจ์œจ์ ์ผ ๋ฟ ์•„๋‹ˆ๋ผ ์‚ฌ์šฉํ•˜๊ธฐ ์ฆ๊ฑฐ์šด ์†”๋ฃจ์…˜์„ ๋งŒ๋“œ๋Š” ๋ฐ ๋„์›€์„ ์ค„ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ์ถ”๊ฐ€ ์ž๋ฃŒ

    ์ดํ•ด๋ฅผ ๊นŠ๊ฒŒ ํ•˜๋ ค๋ฉด ๋‹ค์Œ ๊ณต์‹ ์ž์›์„ ํƒ์ƒ‰ํ•ด ๋ณด์„ธ์š”:

  • Microsoft Learn Docs MCP ์„œ๋ฒ„ (GitHub)
  • Azure MCP ์„œ๋ฒ„ ์‹œ์ž‘ํ•˜๊ธฐ (mcp-python)
  • Azure MCP ์„œ๋ฒ„๋ž€?
  • Model Context Protocol (MCP) ์†Œ๊ฐœ
  • MCP ์„œ๋ฒ„์—์„œ ํ”Œ๋Ÿฌ๊ทธ์ธ ์ถ”๊ฐ€ํ•˜๊ธฐ (Python)
  • ๋‹ค์Œ ๋‹จ๊ณ„

  • ์ด์ „์œผ๋กœ: ์‚ฌ๋ก€ ์—ฐ๊ตฌ ๊ฐœ์š”
  • ๊ณ„์†: ๋ชจ๋“ˆ 10: AI Toolkit์œผ๋กœ AI ์›Œํฌํ”Œ๋กœ์šฐ ๊ฐ„์†Œํ™”
  • ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋‚ด์šฉ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•ด ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ ๋ฌธ์„œ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•œ ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” MCP ์„œ๋ฒ„๋ฅผ ์ด์šฉํ•ด Microsoft Learn Docs๋ฅผ VS Code ํ™˜๊ฒฝ ์•ˆ์œผ๋กœ ์ง์ ‘ ๋ถˆ๋Ÿฌ์˜ค๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹คโ€”๋” ์ด์ƒ ๋ธŒ๋ผ์šฐ์ € ํƒญ์„ ์ „ํ™˜ํ•˜์ง€ ์•Š์•„๋„ ๋ฉ๋‹ˆ๋‹ค! ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

  • MCP ํŒจ๋„์ด๋‚˜ ๋ช…๋ น ํŒ”๋ ˆํŠธ๋ฅผ ์‚ฌ์šฉํ•ด VS Code ๋‚ด๋ถ€์—์„œ ์ฆ‰์‹œ ๋ฌธ์„œ ๊ฒ€์ƒ‰ ๋ฐ ์ฝ๊ธฐ
  • ๋ฌธ์„œ ์ฐธ์กฐ ๋ฐ README ํ˜น์€ ๊ฐ•์˜์šฉ ๋งˆํฌ๋‹ค์šด ํŒŒ์ผ์— ๋งํฌ ์‚ฝ์ž…
  • GitHub Copilot๊ณผ MCP๋ฅผ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด ์›ํ™œํ•˜๊ณ  AI ์ง€์› ๋ฌธ์„œ ๋ฐ ์ฝ”๋“œ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌํ˜„
  • ์‹ค์‹œ๊ฐ„ ํ”ผ๋“œ๋ฐฑ๊ณผ Microsoft์—์„œ ์ œ๊ณตํ•˜๋Š” ์ •ํ™•์„ฑ์œผ๋กœ ๋ฌธ์„œ ๊ฒ€์ฆ ๋ฐ ๊ฐœ์„ 
  • ์—ฐ์†์ ์ธ ๋ฌธ์„œ ๊ฒ€์ฆ์„ ์œ„ํ•œ GitHub ์›Œํฌํ”Œ๋กœ์šฐ์™€ MCP ํ†ตํ•ฉ
  • ๊ตฌํ˜„์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:

  • ์†์‰ฌ์šด ์„ค์ •์„ ์œ„ํ•œ .vscode/mcp.json ์˜ˆ์ œ ๊ตฌ์„ฑ
  • ์—๋””ํ„ฐ ๋‚ด ๊ฒฝํ—˜์„ ๋ณด์—ฌ์ฃผ๋Š” ์Šคํฌ๋ฆฐ์ƒท ์„ค์น˜ ์ ˆ์ฐจ
  • Copilot๊ณผ MCP์˜ ๊ฒฐํ•ฉ ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ํŒ
  • ์ด ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์ฝ”์Šค ์ €์ž, ๋ฌธ์„œ ์ž‘์„ฑ์ž, ๊ฐœ๋ฐœ์ž๊ฐ€ ๋ฌธ์„œ, Copilot, ๊ฒ€์ฆ ๋„๊ตฌ๋ฅผ ์—๋””ํ„ฐ ๋‚ด์—์„œ ์ž‘์—…ํ•˜๋ฉฐ ์ง‘์ค‘๋ ฅ์„ ์œ ์ง€ํ•˜๋Š” ๋ฐ ์ด์ƒ์ ์ž…๋‹ˆ๋‹ค.

    6. APIM MCP ์„œ๋ฒ„ ์ƒ์„ฑ

    ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” Azure API Management(APIM)๋ฅผ ์‚ฌ์šฉํ•ด MCP ์„œ๋ฒ„๋ฅผ ๋งŒ๋“œ๋Š” ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋‚ด์šฉ์€ ๋‹ค์Œ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค:

  • Azure API Management์—์„œ MCP ์„œ๋ฒ„ ์„ค์ •
  • MCP ๋„๊ตฌ๋กœ API ์ž‘์—… ๊ณต๊ฐœ
  • ์†๋„ ์ œํ•œ ๋ฐ ๋ณด์•ˆ์„ ์œ„ํ•œ ์ •์ฑ… ๊ตฌ์„ฑ
  • Visual Studio Code ๋ฐ GitHub Copilot์„ ์‚ฌ์šฉํ•œ MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ
  • ์ด ์˜ˆ์‹œ๋Š” Azure ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•ด ๊ฒฌ๊ณ ํ•œ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๊ณ , AI ์‹œ์Šคํ…œ๊ณผ ๊ธฐ์—… API ๊ฐ„ ํ†ตํ•ฉ์„ ๊ฐ•ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ ์ค๋‹ˆ๋‹ค.

    7. GitHub MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ โ€” ์—์ด์ „ํ‹ฑ ํ†ตํ•ฉ ๊ฐ€์†ํ™”

    ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” 2025๋…„ 9์›”์— ์ถœ์‹œ๋œ GitHub MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๊ฐ€ AI ์ƒํƒœ๊ณ„์˜ ์ค‘์š”ํ•œ ๋ฌธ์ œ์ธ ๋ถ„์‚ฐ๋œ MCP ์„œ๋ฒ„ ๋ฐœ๊ฒฌ ๋ฐ ๋ฐฐํฌ ๋ฌธ์ œ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ•˜๋Š”์ง€ ์‚ดํŽด๋ด…๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์™€ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ํฉ์–ด์ง„ MCP ์„œ๋ฒ„์˜ ์ฆ๊ฐ€ํ•˜๋Š” ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. ์ด์ „์—๋Š” ํ†ตํ•ฉ์ด ๋А๋ฆฌ๊ณ  ์˜ค๋ฅ˜๊ฐ€ ์žฆ์•˜์Šต๋‹ˆ๋‹ค. ์ด๋Ÿฌํ•œ ์„œ๋ฒ„๋“ค์€ AI ์—์ด์ „ํŠธ๊ฐ€ API, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๋ฌธ์„œ ์†Œ์Šค ๋“ฑ ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

    ๋ฌธ์ œ ์ •์˜

    ์—์ด์ „ํ‹ฑ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋“ค์ด ๋งˆ์ฃผํ•œ ๋ฌธ์ œ:

  • ์„œ๋กœ ๋‹ค๋ฅธ ํ”Œ๋žซํผ์— ๋ถ„์‚ฐ๋œ MCP ์„œ๋ฒ„์˜ ์—ด์•…ํ•œ ๋ฐœ๊ฒฌ์„ฑ
  • ํฌ๋Ÿผ๊ณผ ๋ฌธ์„œ ์ „๋ฐ˜์— ํฉ์–ด์ง„ ์ค‘๋ณต ์„ค์ • ์งˆ๋ฌธ
  • ๊ฒ€์ฆ๋˜์ง€ ์•Š์€ ์ถœ์ฒ˜๋กœ๋ถ€ํ„ฐ ๋ฐœ์ƒํ•˜๋Š” ๋ณด์•ˆ ์œ„ํ—˜
  • ์„œ๋ฒ„ ํ’ˆ์งˆ๊ณผ ํ˜ธํ™˜์„ฑ์— ๋Œ€ํ•œ ํ‘œ์ค€ํ™” ๋ถ€์กฑ
  • ์†”๋ฃจ์…˜ ์•„ํ‚คํ…์ฒ˜

    GitHub MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋Š” ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” MCP ์„œ๋ฒ„๋ฅผ ์ค‘์•™ ์ง‘์ค‘ํ™”ํ•˜๋ฉฐ ์ฃผ์š” ๊ธฐ๋Šฅ์€:

  • VS Code๋ฅผ ํ†ตํ•œ ์›ํด๋ฆญ ์„ค์น˜ ํ†ตํ•ฉ์œผ๋กœ ๊ฐ„์†Œํ™”๋œ ์„ค์ •
  • ๋ณ„, ํ™œ๋™, ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ฒ€์ฆ์„ ํ†ตํ•œ ์‹œ๊ทธ๋„ ๋Œ€ ์žก์Œ ์ •๋ ฌ
  • GitHub Copilot๊ณผ ๋‹ค๋ฅธ MCP ํ˜ธํ™˜ ๋„๊ตฌ์™€์˜ ์ง์ ‘ ํ†ตํ•ฉ
  • ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ๊ธฐ์—… ํŒŒํŠธ๋„ˆ๊ฐ€ ๋ชจ๋‘ ๊ธฐ์—ฌํ•  ์ˆ˜ ์žˆ๋Š” ์˜คํ”ˆ ๊ธฐ์—ฌ ๋ชจ๋ธ
  • ๋น„์ฆˆ๋‹ˆ์Šค ์˜ํ–ฅ

    ์ด ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ธก์ • ๊ฐ€๋Šฅํ•œ ๊ฐœ์„ ์„ ๊ฐ€์ ธ์™”์Šต๋‹ˆ๋‹ค:

  • Microsoft Learn MCP ์„œ๋ฒ„์ฒ˜๋Ÿผ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์—์ด์ „ํŠธ์— ์ง์ ‘ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋Š” ๋„๊ตฌ๋กœ ๊ฐœ๋ฐœ์ž ์˜จ๋ณด๋”ฉ ์†๋„ ํ–ฅ์ƒ
  • ์ž์—ฐ์–ด GitHub ์ž๋™ํ™”(PR ์ƒ์„ฑ, CI ์žฌ์‹คํ–‰, ์ฝ”๋“œ ์Šค์บ”)๋ฅผ ๊ฐ€๋Šฅ์ผ€ ํ•˜๋Š” ์ „๋ฌธํ™” ์„œ๋ฒ„(github-mcp-server)๋ฅผ ํ†ตํ•œ ์ƒ์‚ฐ์„ฑ ํ–ฅ์ƒ
  • ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๋ชฉ๋ก ๋ฐ ํˆฌ๋ช…ํ•œ ๊ตฌ์„ฑ ํ‘œ์ค€์„ ํ†ตํ•œ ์ƒํƒœ๊ณ„ ์‹ ๋ขฐ ๊ฐ•ํ™”
  • ์ „๋žต์  ๊ฐ€์น˜

    ์—์ด์ „ํŠธ ์ˆ˜๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ ๋ฐ ์žฌํ˜„ ๊ฐ€๋Šฅํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ์ „๋ฌธ๊ฐ€๋Š” MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ํ†ตํ•ด:

  • ํ‘œ์ค€ํ™”๋œ ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ํ™œ์šฉํ•œ ๋ชจ๋“ˆ์„ฑ ์—์ด์ „ํŠธ ๋ฐฐํฌ ๊ธฐ๋Šฅ
  • ์ผ๊ด€๋œ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ์„ ์ง€์›ํ•˜๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ธฐ๋ฐ˜ ํ‰๊ฐ€ ํŒŒ์ดํ”„๋ผ์ธ
  • ๋‹ค์–‘ํ•œ AI ํ”Œ๋žซํผ ๊ฐ„ ๋งค๋„๋Ÿฌ์šด ํ†ตํ•ฉ์„ ๊ฐ€๋Šฅ์ผ€ ํ•˜๋Š” ๋„๊ตฌ ๊ฐ„ ์ƒํ˜ธ์šด์šฉ์„ฑ
  • ์ด ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๊ฐ€ ๋‹จ์ˆœํ•œ ๋””๋ ‰ํ„ฐ๋ฆฌ๋ฅผ ๋„˜์–ด ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ณ  ์‹ค์ œ ๋ชจ๋ธ ํ†ตํ•ฉ๊ณผ ์—์ด์ „ํŠธ ์‹œ์Šคํ…œ ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ๊ธฐ๋ฐ˜ ํ”Œ๋žซํผ์ž„์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ๊ฒฐ๋ก 

    ์ด ์ผ๊ณฑ ๊ฐ€์ง€ ์ข…ํ•ฉ ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” Model Context Protocol์˜ ๋›ฐ์–ด๋‚œ ๋‹ค์žฌ๋‹ค๋Šฅํ•จ๊ณผ ๋‹ค์–‘ํ•œ ์‹ค์ œ ์‹œ๋‚˜๋ฆฌ์˜ค์—์„œ์˜ ํ™œ์šฉ์„ ์ž…์ฆํ•ฉ๋‹ˆ๋‹ค. ๋ณต์žกํ•œ ๋‹ค์ค‘ ์—์ด์ „ํŠธ ์—ฌํ–‰ ๊ณ„ํš ์‹œ์Šคํ…œ, ๊ธฐ์—… API ๊ด€๋ฆฌ, ๊ฐ„์†Œํ™”๋œ ๋ฌธ์„œ ์›Œํฌํ”Œ๋กœ์šฐ์—์„œ ํ˜์‹ ์ ์ธ GitHub MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ์ด๋ฅด๊ธฐ๊นŒ์ง€, ์ด ์‚ฌ๋ก€๋“ค์€ MCP๊ฐ€ AI ์‹œ์Šคํ…œ์„ ๋„๊ตฌ, ๋ฐ์ดํ„ฐ, ์„œ๋น„์Šค์™€ ์—ฐ๊ฒฐํ•˜๋Š” ํ‘œ์ค€ํ™”๋˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฐฉ์‹์„ ์ œ๊ณตํ•˜์—ฌ ํƒ์›”ํ•œ ๊ฐ€์น˜๋ฅผ ๊ตฌํ˜„ํ•จ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋Š” MCP ๊ตฌํ˜„์˜ ์—ฌ๋Ÿฌ ์ฐจ์›์„ ํฌ๊ด„ํ•ฉ๋‹ˆ๋‹ค:

  • ๊ธฐ์—… ํ†ตํ•ฉ: Azure API ๊ด€๋ฆฌ์™€ Azure DevOps ์ž๋™ํ™”
  • ๋‹ค์ค‘ ์—์ด์ „ํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜: ์กฐ์œจ๋œ AI ์—์ด์ „ํŠธ์™€ ํ•จ๊ป˜ํ•˜๋Š” ์—ฌํ–‰ ๊ณ„ํš
  • ๊ฐœ๋ฐœ์ž ์ƒ์‚ฐ์„ฑ: VS Code ํ†ตํ•ฉ ๋ฐ ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ์ ‘๊ทผ
  • ์ƒํƒœ๊ณ„ ๊ฐœ๋ฐœ: GitHub MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ๊ธฐ๋ฐ˜ ํ”Œ๋žซํผ
  • ๊ต์œก ์‘์šฉ: ๋Œ€ํ™”ํ˜• ํ•™์Šต ๊ณ„ํš ์ƒ์„ฑ๊ธฐ ๋ฐ ๋Œ€ํ™” ์ธํ„ฐํŽ˜์ด์Šค
  • ์ด ๊ตฌํ˜„์„ ํ•™์Šตํ•จ์œผ๋กœ์จ ์–ป๋Š” ํ•ต์‹ฌ ํ†ต์ฐฐ์€:

  • ๋‹ค์–‘ํ•œ ๊ทœ๋ชจ์™€ ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋งž๋Š” ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ๊ธฐ๋Šฅ์„ฑ๊ณผ ์œ ์ง€ ๋ณด์ˆ˜์„ฑ์„ ๊ท ํ˜• ์žˆ๊ฒŒ ํ•œ ๊ตฌํ˜„ ์ „๋žต
  • ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์‹œ ๊ณ ๋ คํ•ด์•ผ ํ•  ๋ณด์•ˆ ๋ฐ ํ™•์žฅ์„ฑ
  • MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ ๋ฐ ํด๋ผ์ด์–ธํŠธ ํ†ตํ•ฉ์„ ์œ„ํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ์ƒํ˜ธ ์—ฐ๊ฒฐ๋œ AI ๊ธฐ๋ฐ˜ ์†”๋ฃจ์…˜ ๊ตฌ์ถ•์„ ์œ„ํ•œ ์ƒํƒœ๊ณ„ ์‚ฌ๊ณ ๋ฐฉ์‹
  • ์ด ์‚ฌ๋ก€๋“ค์€ MCP๊ฐ€ ๋‹จ์ˆœ ์ด๋ก ์  ํ”„๋ ˆ์ž„์›Œํฌ๊ฐ€ ์•„๋‹Œ, ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋ฌธ์ œ์— ์‹ค์งˆ์  ์†”๋ฃจ์…˜์„ ์ œ๊ณตํ•˜๋Š” ์„ฑ์ˆ™ํ•˜๊ณ  ํ”„๋กœ๋•์…˜ ์ค€๋น„๋œ ํ”„๋กœํ† ์ฝœ์ž„์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค. ๋‹จ์ˆœ ์ž๋™ํ™” ๋„๊ตฌ๋“  ์ •๊ตํ•œ ๋‹ค์ค‘ ์—์ด์ „ํŠธ ์‹œ์Šคํ…œ์ด๋“ , ์—ฌ๊ธฐ ์ œ์‹œ๋œ ํŒจํ„ด๊ณผ ์ ‘๊ทผ๋ฒ•์€ ์—ฌ๋Ÿฌ๋ถ„ ์ž์‹ ์˜ MCP ํ”„๋กœ์ ํŠธ์— ๊ฒฌ๊ณ ํ•œ ๊ธฐ๋ฐ˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์ถ”๊ฐ€ ์ž๋ฃŒ

  • Azure AI Travel Agents GitHub ์ €์žฅ์†Œ
  • Azure DevOps MCP ๋„๊ตฌ
  • Playwright MCP ๋„๊ตฌ
  • Microsoft Docs MCP ์„œ๋ฒ„
  • GitHub MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ โ€” ์—์ด์ „ํ‹ฑ ํ†ตํ•ฉ ๊ฐ€์†ํ™”
  • MCP ์ปค๋ฎค๋‹ˆํ‹ฐ ์˜ˆ์ œ
  • ๋‹ค์Œ ๋‹จ๊ณ„

  • ์ด์ „: ๋ชจ๋“ˆ 8: ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ๋‹ค์Œ: ๋ชจ๋“ˆ 10: AI ์›Œํฌํ”Œ๋กœ์šฐ ๊ฐ„์†Œํ™”: AI ํˆดํ‚ท์œผ๋กœ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•
  • ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋‚ด์šฉ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•ด ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ์€ ํ•ด๋‹น ๋ฌธ์„œ์˜ ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ ์ „๋ฌธ๊ฐ€์˜ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•œ ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    code Module 10

    Module 10 — AI ํˆดํ‚ท

    AI ์›Œํฌํ”Œ๋กœ์šฐ ๊ฐ„์†Œํ™”: AI Toolkit์œผ๋กœ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•

    ๐ŸŽฏ ๊ฐœ์š”

    _(์œ„ ์ด๋ฏธ์ง€๋ฅผ ํด๋ฆญํ•˜์—ฌ ์ด ๊ฐ•์˜์˜ ๋น„๋””์˜ค๋ฅผ ์‹œ์ฒญํ•˜์„ธ์š”)_

    Model Context Protocol (MCP) ์›Œํฌ์ˆ์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค! ์ด ์ข…ํ•ฉ ์‹ค์Šต ์›Œํฌ์ˆ์€ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ์„ ํ˜์‹ ํ•˜๋Š” ๋‘ ๊ฐ€์ง€ ์ตœ์ฒจ๋‹จ ๊ธฐ์ˆ ์„ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค:

  • ๐Ÿ”— Model Context Protocol (MCP): ์›ํ™œํ•œ AI-๋„๊ตฌ ํ†ตํ•ฉ์„ ์œ„ํ•œ ์˜คํ”ˆ ํ‘œ์ค€
  • ๐Ÿ› ๏ธ Visual Studio Code์šฉ AI Toolkit (AITK): Microsoft์˜ ๊ฐ•๋ ฅํ•œ AI ๊ฐœ๋ฐœ ํ™•์žฅ ๊ธฐ๋Šฅ
  • ๐ŸŽ“ ๋ฐฐ์šธ ๋‚ด์šฉ

    ์ด ์›Œํฌ์ˆ์ด ๋๋‚˜๋ฉด AI ๋ชจ๋ธ๊ณผ ์‹ค์ œ ๋„๊ตฌ ๋ฐ ์„œ๋น„์Šค๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์ง€๋Šฅํ˜• ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์ถ• ๊ธฐ์ˆ ์„ ์Šต๋“ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค. ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ๋ถ€ํ„ฐ ๋งž์ถคํ˜• API ํ†ตํ•ฉ๊นŒ์ง€ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ณผ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ์‹ค๋ฌด ๋Šฅ๋ ฅ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๐Ÿ—๏ธ ๊ธฐ์ˆ  ์Šคํƒ

    ๐Ÿ”Œ Model Context Protocol (MCP)

    MCP๋Š” AI ๋ชจ๋ธ์„ ์™ธ๋ถ€ ๋„๊ตฌ ๋ฐ ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ์—ฐ๊ฒฐํ•˜๋Š” "AI์šฉ USB-C"์™€ ๊ฐ™์€ ๋ฒ”์šฉ ํ‘œ์ค€์ž…๋‹ˆ๋‹ค.

    โœจ ์ฃผ์š” ํŠน์ง•:

  • ๐Ÿ”„ ํ‘œ์ค€ํ™”๋œ ํ†ตํ•ฉ: AI-๋„๊ตฌ ์—ฐ๊ฒฐ์„ ์œ„ํ•œ ๋ณดํŽธ์  ์ธํ„ฐํŽ˜์ด์Šค
  • ๐Ÿ›๏ธ ์œ ์—ฐํ•œ ์•„ํ‚คํ…์ฒ˜: stdio/SSE ์ „์†ก์„ ํ†ตํ•œ ๋กœ์ปฌ ๋ฐ ์›๊ฒฉ ์„œ๋ฒ„ ์ง€์›
  • ๐Ÿงฐ ํ’๋ถ€ํ•œ ์ƒํƒœ๊ณ„: ๋„๊ตฌ, ํ”„๋กฌํ”„ํŠธ ๋ฐ ๋ฆฌ์†Œ์Šค๋ฅผ ํ•˜๋‚˜์˜ ํ”„๋กœํ† ์ฝœ๋กœ ์ œ๊ณต
  • ๐Ÿ”’ ๊ธฐ์—…์šฉ ์ค€๋น„: ๋‚ด์žฅ๋œ ๋ณด์•ˆ ๋ฐ ์•ˆ์ •์„ฑ
  • ๐ŸŽฏ MCP์˜ ์ค‘์š”์„ฑ:

    USB-C๊ฐ€ ์ผ€์ด๋ธ” ํ˜ผ๋ž€์„ ์—†์•ค ๊ฒƒ์ฒ˜๋Ÿผ, MCP๋Š” AI ํ†ตํ•ฉ์˜ ๋ณต์žก์„ฑ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ ํ”„๋กœํ† ์ฝœ, ๋ฌดํ•œํ•œ ๊ฐ€๋Šฅ์„ฑ.

    ๐Ÿค– Visual Studio Code์šฉ AI Toolkit (AITK)

    Microsoft์˜ ์ฃผ๋ ฅ AI ๊ฐœ๋ฐœ ํ™•์žฅ์œผ๋กœ, VS Code๋ฅผ AI ํ˜์‹  ํ”Œ๋žซํผ์œผ๋กœ ํƒˆ๋ฐ”๊ฟˆ์‹œํ‚ต๋‹ˆ๋‹ค.

    ๐Ÿš€ ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ๐Ÿ“ฆ ๋ชจ๋ธ ์นดํƒˆ๋กœ๊ทธ: Azure AI, GitHub, Hugging Face, Ollama ๋ชจ๋ธ ์ ‘๊ทผ
  • โšก ๋กœ์ปฌ ์ถ”๋ก : ONNX ์ตœ์ ํ™” CPU/GPU/NPU ์‹คํ–‰
  • ๐Ÿ—๏ธ ์—์ด์ „ํŠธ ๋นŒ๋”: MCP ํ†ตํ•ฉ์„ ํ†ตํ•œ ์‹œ๊ฐ์  AI ์—์ด์ „ํŠธ ๊ฐœ๋ฐœ
  • ๐ŸŽญ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ง€์›: ํ…์ŠคํŠธ, ๋น„์ „, ๊ตฌ์กฐํ™”๋œ ์ถœ๋ ฅ ์ง€์›
  • ๐Ÿ’ก ๊ฐœ๋ฐœ ์ด์ :

  • ๋ฌด์„ค์ • ๋ชจ๋ธ ๋ฐฐํฌ
  • ์‹œ๊ฐ์  ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง
  • ์‹ค์‹œ๊ฐ„ ํ…Œ์ŠคํŠธ ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ
  • ์™„๋ฒฝํ•œ MCP ์„œ๋ฒ„ ํ†ตํ•ฉ
  • ๐Ÿ“š ํ•™์Šต ์—ฌ์ •

    ๐Ÿš€ ๋ชจ๋“ˆ 1: AI Toolkit ๊ธฐ์ดˆ

    ๐Ÿš€ ๋ชจ๋“ˆ 1: AI Toolkit ๊ธฐ์ดˆ

    ๐Ÿ“‹ ํ•™์Šต ๋ชฉํ‘œ

    ์ด ๋ชจ๋“ˆ์„ ๋งˆ์น˜๋ฉด ๋‹ค์Œ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • โœ… Visual Studio Code์šฉ AI Toolkit ์„ค์น˜ ๋ฐ ์„ค์ •
  • โœ… ๋ชจ๋ธ ์นดํƒˆ๋กœ๊ทธ ํƒ์ƒ‰ ๋ฐ ๋‹ค์–‘ํ•œ ๋ชจ๋ธ ์†Œ์Šค ์ดํ•ด
  • โœ… Playground๋ฅผ ์‚ฌ์šฉํ•œ ๋ชจ๋ธ ํ…Œ์ŠคํŠธ ๋ฐ ์‹คํ—˜
  • โœ… Agent Builder๋ฅผ ์ด์šฉํ•œ ๋งž์ถคํ˜• AI ์—์ด์ „ํŠธ ์ƒ์„ฑ
  • โœ… ๋‹ค์–‘ํ•œ ์ œ๊ณต์—…์ฒด์˜ ๋ชจ๋ธ ์„ฑ๋Šฅ ๋น„๊ต
  • โœ… ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง ๋ชจ๋ฒ” ์‚ฌ๋ก€ ์ ์šฉ
  • ๐Ÿง  AI Toolkit (AITK) ์†Œ๊ฐœ

    Visual Studio Code์šฉ AI Toolkit์€ ๋งˆ์ดํฌ๋กœ์†Œํ”„ํŠธ์˜ ๋Œ€ํ‘œ ํ™•์žฅ ๊ธฐ๋Šฅ์œผ๋กœ, VS Code๋ฅผ ์ข…ํ•ฉ์ ์ธ AI ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์œผ๋กœ ๋ฐ”๊ฟ”์ค๋‹ˆ๋‹ค. AI ์—ฐ๊ตฌ์™€ ์‹ค์ œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐœ๋ฐœ ๊ฐ„์˜ ๊ฐ„๊ทน์„ ๋ฉ”์šฐ๋ฉฐ, ๋ชจ๋“  ์ˆ˜์ค€์˜ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ƒ์„ฑํ˜• AI๋ฅผ ์‰ฝ๊ฒŒ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•์Šต๋‹ˆ๋‹ค.

    ๐ŸŒŸ ์ฃผ์š” ๊ธฐ๋Šฅ

    ๊ธฐ๋Šฅ ์„ค๋ช… ํ™œ์šฉ ์‚ฌ๋ก€ --------- ------------- ---------- ๐Ÿ—‚๏ธ ๋ชจ๋ธ ์นดํƒˆ๋กœ๊ทธ GitHub, ONNX, OpenAI, Anthropic, Google ๋“ฑ 100๊ฐœ ์ด์ƒ์˜ ๋ชจ๋ธ ์ ‘๊ทผ ๋ชจ๋ธ ํƒ์ƒ‰ ๋ฐ ์„ ํƒ ๐Ÿ”Œ BYOM ์ง€์› ์ž์ฒด ๋ชจ๋ธ(๋กœ์ปฌ/์›๊ฒฉ) ํ†ตํ•ฉ ๋งž์ถคํ˜• ๋ชจ๋ธ ๋ฐฐํฌ ๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ ์ฑ„ํŒ… ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ๋ชจ๋ธ ํ…Œ์ŠคํŠธ ๋น ๋ฅธ ํ”„๋กœํ† ํƒ€์ดํ•‘ ๋ฐ ํ…Œ์ŠคํŠธ ๐Ÿ“Ž ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ง€์› ํ…์ŠคํŠธ, ์ด๋ฏธ์ง€, ์ฒจ๋ถ€ํŒŒ์ผ ์ฒ˜๋ฆฌ ๋ณตํ•ฉ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ โšก ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์—ฌ๋Ÿฌ ํ”„๋กฌํ”„ํŠธ ๋™์‹œ ์‹คํ–‰ ํšจ์œจ์ ์ธ ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ์šฐ ๐Ÿ“Š ๋ชจ๋ธ ํ‰๊ฐ€ ๋‚ด์žฅ ์ง€ํ‘œ(F1, ๊ด€๋ จ์„ฑ, ์œ ์‚ฌ์„ฑ, ์ผ๊ด€์„ฑ) ์„ฑ๋Šฅ ํ‰๊ฐ€

    ๐ŸŽฏ AI Toolkit์ด ์ค‘์š”ํ•œ ์ด์œ 

  • ๐Ÿš€ ๊ฐœ๋ฐœ ๊ฐ€์†ํ™”: ์•„์ด๋””์–ด์—์„œ ํ”„๋กœํ† ํƒ€์ž…๊นŒ์ง€ ๋ช‡ ๋ถ„ ๋งŒ์—
  • ๐Ÿ”„ ํ†ตํ•ฉ ์›Œํฌํ”Œ๋กœ์šฐ: ์—ฌ๋Ÿฌ AI ์ œ๊ณต์—…์ฒด๋ฅผ ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์—์„œ
  • ๐Ÿงช ๊ฐ„ํŽธํ•œ ์‹คํ—˜: ๋ณต์žกํ•œ ์„ค์ • ์—†์ด ๋ชจ๋ธ ๋น„๊ต ๊ฐ€๋Šฅ
  • ๐Ÿ“ˆ ํ”„๋กœ๋•์…˜ ์ค€๋น„ ์™„๋ฃŒ: ํ”„๋กœํ† ํƒ€์ž…์—์„œ ๋ฐฐํฌ๊นŒ์ง€ ์›ํ™œํ•œ ์ „ํ™˜
  • ๐Ÿ› ๏ธ ์‚ฌ์ „ ์ค€๋น„ ๋ฐ ์„ค์ •

    ๐Ÿ“ฆ AI Toolkit ํ™•์žฅ ์„ค์น˜

    1๋‹จ๊ณ„: ํ™•์žฅ ๋งˆ์ผ“ํ”Œ๋ ˆ์ด์Šค ์ ‘์†

    1. Visual Studio Code ์‹คํ–‰

    2. ํ™•์žฅ ๋ทฐ ์—ด๊ธฐ (Ctrl+Shift+X ๋˜๋Š” Cmd+Shift+X)

    3. "AI Toolkit" ๊ฒ€์ƒ‰

    2๋‹จ๊ณ„: ๋ฒ„์ „ ์„ ํƒ

  • ๐ŸŸข ์ •์‹ ๋ฒ„์ „: ํ”„๋กœ๋•์…˜ ์‚ฌ์šฉ ๊ถŒ์žฅ
  • ๐Ÿ”ถ ํ”„๋ฆฌ๋ฆด๋ฆฌ์Šค: ์ตœ์‹  ๊ธฐ๋Šฅ ์กฐ๊ธฐ ์ฒดํ—˜ ๊ฐ€๋Šฅ
  • 3๋‹จ๊ณ„: ์„ค์น˜ ๋ฐ ํ™œ์„ฑํ™”

    โœ… ํ™•์ธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • [ ] VS Code ์‚ฌ์ด๋“œ๋ฐ”์— AI Toolkit ์•„์ด์ฝ˜ ํ‘œ์‹œ
  • [ ] ํ™•์žฅ ๊ธฐ๋Šฅ์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ์Œ
  • [ ] ์ถœ๋ ฅ ํŒจ๋„์— ์„ค์น˜ ์˜ค๋ฅ˜ ์—†์Œ
  • ๐Ÿงช ์‹ค์Šต 1: GitHub ๋ชจ๋ธ ํƒ์ƒ‰

    ๐ŸŽฏ ๋ชฉํ‘œ: ๋ชจ๋ธ ์นดํƒˆ๋กœ๊ทธ๋ฅผ ์ตํžˆ๊ณ  ์ฒซ AI ๋ชจ๋ธ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

    ๐Ÿ“Š 1๋‹จ๊ณ„: ๋ชจ๋ธ ์นดํƒˆ๋กœ๊ทธ ํƒ์ƒ‰

    ๋ชจ๋ธ ์นดํƒˆ๋กœ๊ทธ๋Š” AI ์ƒํƒœ๊ณ„๋กœ ๊ฐ€๋Š” ๊ด€๋ฌธ์ž…๋‹ˆ๋‹ค. ์—ฌ๋Ÿฌ ์ œ๊ณต์—…์ฒด์˜ ๋ชจ๋ธ์„ ํ•œ๋ฐ ๋ชจ์•„ ์‰ฝ๊ฒŒ ํƒ์ƒ‰ํ•˜๊ณ  ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๐Ÿ” ํƒ์ƒ‰ ๊ฐ€์ด๋“œ:

    AI Toolkit ์‚ฌ์ด๋“œ๋ฐ”์—์„œ MODELS - Catalog ํด๋ฆญ

    ๐Ÿ’ก ํŒ: ์ฝ”๋“œ ์ƒ์„ฑ, ์ฐฝ์˜์  ๊ธ€์“ฐ๊ธฐ, ๋ถ„์„ ๋“ฑ ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋งž๋Š” ํŠน์ • ๊ธฐ๋Šฅ์„ ๊ฐ€์ง„ ๋ชจ๋ธ์„ ์ฐพ์•„๋ณด์„ธ์š”.

    โš ๏ธ ์ฃผ์˜: GitHub์— ํ˜ธ์ŠคํŒ…๋œ ๋ชจ๋ธ(GitHub Models)์€ ๋ฌด๋ฃŒ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ์š”์ฒญ ๋ฐ ํ† ํฐ์— ์ œํ•œ์ด ์žˆ์Šต๋‹ˆ๋‹ค. Azure AI๋‚˜ ๋‹ค๋ฅธ ์—”๋“œํฌ์ธํŠธ๋ฅผ ํ†ตํ•ด ํ˜ธ์ŠคํŒ…๋œ ๋น„-GitHub ๋ชจ๋ธ์— ์ ‘๊ทผํ•˜๋ ค๋ฉด ์ ์ ˆํ•œ API ํ‚ค๋‚˜ ์ธ์ฆ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

    ๐Ÿš€ 2๋‹จ๊ณ„: ์ฒซ ๋ชจ๋ธ ์ถ”๊ฐ€ ๋ฐ ์„ค์ •

    ๋ชจ๋ธ ์„ ํƒ ์ „๋žต:

  • GPT-4.1: ๋ณต์žกํ•œ ์ถ”๋ก ๊ณผ ๋ถ„์„์— ์ตœ์ 
  • Phi-4-mini: ๊ฐ€๋ฒผ์šฐ๋ฉฐ ๊ฐ„๋‹จํ•œ ์ž‘์—…์— ๋น ๋ฅธ ์‘๋‹ต ์ œ๊ณต
  • ๐Ÿ”ง ์„ค์ • ์ ˆ์ฐจ:

    1. ์นดํƒˆ๋กœ๊ทธ์—์„œ OpenAI GPT-4.1 ์„ ํƒ

    2. Add to My Models ํด๋ฆญํ•˜์—ฌ ๋ชจ๋ธ ๋“ฑ๋ก

    3. Try in Playground ์„ ํƒํ•ด ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์‹คํ–‰

    4. ๋ชจ๋ธ ์ดˆ๊ธฐํ™” ๋Œ€๊ธฐ (์ฒซ ์‹คํ–‰ ์‹œ ์‹œ๊ฐ„์ด ๊ฑธ๋ฆด ์ˆ˜ ์žˆ์Œ)

    โš™๏ธ ๋ชจ๋ธ ํŒŒ๋ผ๋ฏธํ„ฐ ์ดํ•ดํ•˜๊ธฐ:

  • Temperature: ์ฐฝ์˜์„ฑ ์กฐ์ ˆ (0 = ๊ฒฐ์ •์ , 1 = ์ฐฝ์˜์ )
  • Max Tokens: ์ตœ๋Œ€ ์‘๋‹ต ๊ธธ์ด
  • Top-p: ์‘๋‹ต ๋‹ค์–‘์„ฑ์„ ์œ„ํ•œ ํ•ต์‹ฌ ์ƒ˜ํ”Œ๋ง
  • ๐ŸŽฏ 3๋‹จ๊ณ„: ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ ์ธํ„ฐํŽ˜์ด์Šค ๋งˆ์Šคํ„ฐํ•˜๊ธฐ

    ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ๋Š” AI ์‹คํ—˜์‹ค์ž…๋‹ˆ๋‹ค. ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค:

    ๐ŸŽจ ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง ๋ชจ๋ฒ” ์‚ฌ๋ก€:

    1. ๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑ: ๋ช…ํ™•ํ•˜๊ณ  ์ž์„ธํ•œ ์ง€์‹œ๊ฐ€ ๋” ์ข‹์€ ๊ฒฐ๊ณผ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค

    2. ๋งฅ๋ฝ ์ œ๊ณต: ๊ด€๋ จ ๋ฐฐ๊ฒฝ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์„ธ์š”

    3. ์˜ˆ์‹œ ์‚ฌ์šฉ: ์›ํ•˜๋Š” ๋ฐ”๋ฅผ ์˜ˆ์‹œ๋กœ ๋ณด์—ฌ์ฃผ์„ธ์š”

    4. ๋ฐ˜๋ณต ๊ฐœ์„ : ์ดˆ๊ธฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ํ”„๋กฌํ”„ํŠธ๋ฅผ ๋‹ค๋“ฌ์œผ์„ธ์š”

    ๐Ÿงช ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค:

    
    # Example 1: Code Generation
    
    "Write a Python function that calculates the factorial of a number using recursion. Include error handling and docstrings."
    
    
    
    # Example 2: Creative Writing
    
    "Write a professional email to a client explaining a project delay, maintaining a positive tone while being transparent about challenges."
    
    
    
    # Example 3: Data Analysis
    
    "Analyze this sales data and provide insights: [paste your data]. Focus on trends, anomalies, and actionable recommendations."
    
    

    ๐Ÿ† ๋„์ „ ๊ณผ์ œ: ๋ชจ๋ธ ์„ฑ๋Šฅ ๋น„๊ต

    ๐ŸŽฏ ๋ชฉํ‘œ: ๋™์ผํ•œ ํ”„๋กฌํ”„ํŠธ๋กœ ์—ฌ๋Ÿฌ ๋ชจ๋ธ์„ ๋น„๊ตํ•ด ๊ฐ•์ ์„ ํŒŒ์•…ํ•˜๊ธฐ

    ๐Ÿ“‹ ์ง€์นจ:

    1. ์ž‘์—… ๊ณต๊ฐ„์— Phi-4-mini ์ถ”๊ฐ€

    2. GPT-4.1๊ณผ Phi-4-mini์— ๋™์ผํ•œ ํ”„๋กฌํ”„ํŠธ ์‚ฌ์šฉ

    3. ์‘๋‹ต ํ’ˆ์งˆ, ์†๋„, ์ •ํ™•๋„ ๋น„๊ต

    4. ๊ฒฐ๊ณผ ์„น์…˜์— ๋ฐœ๊ฒฌ ๋‚ด์šฉ ๊ธฐ๋ก

    ๐Ÿ’ก ์•Œ์•„์•ผ ํ•  ํ•ต์‹ฌ ์ธ์‚ฌ์ดํŠธ:

  • LLM๊ณผ SLM ์‚ฌ์šฉ ์‹œ๊ธฐ
  • ๋น„์šฉ๊ณผ ์„ฑ๋Šฅ ๊ฐ„ ๊ท ํ˜•
  • ๋ชจ๋ธ๋ณ„ ํŠนํ™” ๊ธฐ๋Šฅ
  • ๐Ÿค– ์‹ค์Šต 2: Agent Builder๋กœ ๋งž์ถคํ˜• ์—์ด์ „ํŠธ ๋งŒ๋“ค๊ธฐ

    ๐ŸŽฏ ๋ชฉํ‘œ: ํŠน์ • ์ž‘์—…๊ณผ ์›Œํฌํ”Œ๋กœ์šฐ์— ๋งž์ถ˜ ์ „๋ฌธ AI ์—์ด์ „ํŠธ ์ƒ์„ฑ

    ๐Ÿ—๏ธ 1๋‹จ๊ณ„: Agent Builder ์ดํ•ดํ•˜๊ธฐ

    Agent Builder๋Š” AI Toolkit์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์ž…๋‹ˆ๋‹ค. ๋Œ€ํ˜• ์–ธ์–ด ๋ชจ๋ธ์˜ ํž˜์„ ๋งž์ถคํ˜• ์ง€์‹œ, ํŠน์ • ํŒŒ๋ผ๋ฏธํ„ฐ, ์ „๋ฌธ ์ง€์‹๊ณผ ๊ฒฐํ•ฉํ•ด ๋ชฉ์ ์— ๋งž๋Š” AI ๋น„์„œ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๐Ÿง  ์—์ด์ „ํŠธ ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์„ฑ์š”์†Œ:

  • Core Model: ๊ธฐ๋ณธ LLM (GPT-4, Groks, Phi ๋“ฑ)
  • System Prompt: ์—์ด์ „ํŠธ ์„ฑ๊ฒฉ๊ณผ ํ–‰๋™ ์ •์˜
  • Parameters: ์ตœ์  ์„ฑ๋Šฅ์„ ์œ„ํ•œ ์„ธ๋ถ€ ์„ค์ •
  • Tools Integration: ์™ธ๋ถ€ API ๋ฐ MCP ์„œ๋น„์Šค ์—ฐ๊ฒฐ
  • Memory: ๋Œ€ํ™” ๋งฅ๋ฝ๊ณผ ์„ธ์…˜ ์œ ์ง€
  • โš™๏ธ 2๋‹จ๊ณ„: ์—์ด์ „ํŠธ ์„ค์ • ์‹ฌํ™”

    ๐ŸŽจ ํšจ๊ณผ์ ์ธ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์ž‘์„ฑ:

    
    # Template Structure:
    
    ## Role Definition
    
    You are a [specific role] with expertise in [domain].
    
    
    
    ## Capabilities
    
    - List specific abilities
    
    - Define scope of knowledge
    
    - Clarify limitations
    
    
    
    ## Behavior Guidelines
    
    - Response style (formal, casual, technical)
    
    - Output format preferences
    
    - Error handling approach
    
    
    
    ## Examples
    
    Provide 2-3 examples of ideal interactions
    
    

    *๋ฌผ๋ก  Generate System Prompt ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•ด AI๊ฐ€ ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ๊ณผ ์ตœ์ ํ™”๋ฅผ ๋„์™€์ค„ ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค*

    ๐Ÿ”ง ํŒŒ๋ผ๋ฏธํ„ฐ ์ตœ์ ํ™”:

    ํŒŒ๋ผ๋ฏธํ„ฐ ๊ถŒ์žฅ ๋ฒ”์œ„ ํ™œ์šฉ ์‚ฌ๋ก€ ----------- ------------------ ---------- Temperature 0.1-0.3 ๊ธฐ์ˆ ์ /์‚ฌ์‹ค์  ์‘๋‹ต Temperature 0.7-0.9 ์ฐฝ์˜์ /๋ธŒ๋ ˆ์ธ์Šคํ† ๋ฐ ์ž‘์—… Max Tokens 500-1000 ๊ฐ„๊ฒฐํ•œ ์‘๋‹ต Max Tokens 2000-4000 ์ƒ์„ธํ•œ ์„ค๋ช…

    ๐Ÿ 3๋‹จ๊ณ„: ์‹ค์Šต - ํŒŒ์ด์ฌ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์—์ด์ „ํŠธ

    ๐ŸŽฏ ๋ฏธ์…˜: ์ „๋ฌธ์ ์ธ ํŒŒ์ด์ฌ ์ฝ”๋”ฉ ์–ด์‹œ์Šคํ„ดํŠธ ๋งŒ๋“ค๊ธฐ

    ๐Ÿ“‹ ์„ค์ • ๋‹จ๊ณ„:

    1. ๋ชจ๋ธ ์„ ํƒ: Claude 3.5 Sonnet ์„ ํƒ (์ฝ”๋“œ ์ž‘์—…์— ํƒ์›”)

    2. ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์„ค๊ณ„:

    
    # Python Programming Expert Agent
    
    
    
    ## Role
    
    You are a senior Python developer with 10+ years of experience. You excel at writing clean, efficient, and well-documented Python code.
    
    
    
    ## Capabilities
    
    - Write production-ready Python code
    
    - Debug complex issues
    
    - Explain code concepts clearly
    
    - Suggest best practices and optimizations
    
    - Provide complete working examples
    
    
    
    ## Response Format
    
    - Always include docstrings
    
    - Add inline comments for complex logic
    
    - Suggest testing approaches
    
    - Mention relevant libraries when applicable
    
    
    
    ## Code Quality Standards
    
    - Follow PEP 8 style guidelines
    
    - Use type hints where appropriate
    
    - Handle exceptions gracefully
    
    - Write readable, maintainable code
    
    

    3. ํŒŒ๋ผ๋ฏธํ„ฐ ์„ค์ •:

    - Temperature: 0.2 (์ผ๊ด€๋˜๊ณ  ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ์ฝ”๋“œ)

    - Max Tokens: 2000 (์ƒ์„ธํ•œ ์„ค๋ช…)

    - Top-p: 0.9 (๊ท ํ˜• ์žกํžŒ ์ฐฝ์˜์„ฑ)

    ๐Ÿงช 4๋‹จ๊ณ„: ํŒŒ์ด์ฌ ์—์ด์ „ํŠธ ํ…Œ์ŠคํŠธ

    ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค:

    1. ๊ธฐ๋ณธ ๊ธฐ๋Šฅ: "์†Œ์ˆ˜ ์ฐพ๊ธฐ ํ•จ์ˆ˜ ์ž‘์„ฑ"

    2. ๋ณต์žกํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜: "์‚ฝ์ž…, ์‚ญ์ œ, ๊ฒ€์ƒ‰ ๋ฉ”์„œ๋“œ๋ฅผ ํฌํ•จํ•œ ์ด์ง„ ํƒ์ƒ‰ ํŠธ๋ฆฌ ๊ตฌํ˜„"

    3. ์‹ค์ œ ๋ฌธ์ œ: "์š”์ฒญ ์ œํ•œ๊ณผ ์žฌ์‹œ๋„๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์›น ์Šคํฌ๋ž˜ํผ ๋งŒ๋“ค๊ธฐ"

    4. ๋””๋ฒ„๊น…: "์ด ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด ์ฃผ์„ธ์š” [๋ฒ„๊ทธ ์žˆ๋Š” ์ฝ”๋“œ ๋ถ™์—ฌ๋„ฃ๊ธฐ]"

    ๐Ÿ† ์„ฑ๊ณต ๊ธฐ์ค€:

  • โœ… ์˜ค๋ฅ˜ ์—†์ด ์ฝ”๋“œ ์‹คํ–‰
  • โœ… ์ ์ ˆํ•œ ๋ฌธ์„œํ™” ํฌํ•จ
  • โœ… ํŒŒ์ด์ฌ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ์ค€์ˆ˜
  • โœ… ๋ช…ํ™•ํ•œ ์„ค๋ช… ์ œ๊ณต
  • โœ… ๊ฐœ์„  ์‚ฌํ•ญ ์ œ์•ˆ
  • ๐ŸŽ“ ๋ชจ๋“ˆ 1 ์ •๋ฆฌ ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

    ๐Ÿ“Š ์ง€์‹ ์ ๊ฒ€

    ์ดํ•ด๋„๋ฅผ ํ™•์ธํ•ด ๋ณด์„ธ์š”:

  • [ ] ์นดํƒˆ๋กœ๊ทธ ๋‚ด ๋ชจ๋ธ ๊ฐ„ ์ฐจ์ด๋ฅผ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‚˜์š”?
  • [ ] ๋งž์ถคํ˜• ์—์ด์ „ํŠธ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ํ…Œ์ŠคํŠธํ–ˆ๋‚˜์š”?
  • [ ] ๋‹ค์–‘ํ•œ ์‚ฌ์šฉ ์‚ฌ๋ก€์— ๋งž๊ฒŒ ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ตœ์ ํ™”ํ•  ์ˆ˜ ์žˆ๋‚˜์š”?
  • [ ] ํšจ๊ณผ์ ์ธ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋ฅผ ์„ค๊ณ„ํ•  ์ˆ˜ ์žˆ๋‚˜์š”?
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

  • AI Toolkit ๋ฌธ์„œ: ๊ณต์‹ Microsoft Docs
  • ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง ๊ฐ€์ด๋“œ: ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • AI Toolkit ๋‚ด ๋ชจ๋ธ: ๊ฐœ๋ฐœ ์ค‘์ธ ๋ชจ๋ธ
  • ๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! AI Toolkit์˜ ๊ธฐ๋ณธ๊ธฐ๋ฅผ ๋งˆ์Šคํ„ฐํ–ˆ์œผ๋ฉฐ, ๋” ๊ณ ๊ธ‰ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค ์ค€๋น„๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

    ๐Ÿ”œ ๋‹ค์Œ ๋ชจ๋“ˆ๋กœ ๊ณ„์†ํ•˜๊ธฐ

    ๋” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ๋ฐฐ์šฐ๊ณ  ์‹ถ๋‹ค๋ฉด ๋ชจ๋“ˆ 2: MCP with AI Toolkit Fundamentals ๋กœ ์ด๋™ํ•˜์„ธ์š”. ์—ฌ๊ธฐ์„œ ๋‹ค์Œ์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

  • Model Context Protocol (MCP)์„ ์‚ฌ์šฉํ•ด ์—์ด์ „ํŠธ๋ฅผ ์™ธ๋ถ€ ๋„๊ตฌ์— ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•
  • Playwright๋กœ ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ์—์ด์ „ํŠธ ๊ตฌ์ถ•
  • AI Toolkit ์—์ด์ „ํŠธ์™€ MCP ์„œ๋ฒ„ ํ†ตํ•ฉ
  • ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ์™€ ๊ธฐ๋Šฅ์œผ๋กœ ์—์ด์ „ํŠธ ๊ฐ•ํ™”ํ•˜๊ธฐ
  • ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋ถ€๋ถ„์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ์€ ํ•ด๋‹น ์–ธ์–ด์˜ ์›๋ณธ ๋ฌธ์„œ๊ฐ€ ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์†Œ์š” ์‹œ๊ฐ„: 15๋ถ„

  • ๐Ÿ› ๏ธ Visual Studio Code์šฉ AI Toolkit ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ
  • ๐Ÿ—‚๏ธ ๋ชจ๋ธ ์นดํƒˆ๋กœ๊ทธ ํƒ์ƒ‰ (GitHub, ONNX, OpenAI, Anthropic, Google ๋“ฑ 100๊ฐœ ์ด์ƒ์˜ ๋ชจ๋ธ)
  • ๐ŸŽฎ ์‹ค์‹œ๊ฐ„ ๋ชจ๋ธ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํ”Œ๋ ˆ์ด๊ทธ๋ผ์šด๋“œ ๋งˆ์Šคํ„ฐ
  • ๐Ÿค– Agent Builder๋กœ ์ฒซ AI ์—์ด์ „ํŠธ ๊ตฌ์ถ•
  • ๐Ÿ“Š ๋‚ด์žฅ๋œ ๋ฉ”ํŠธ๋ฆญ(F1, ๊ด€๋ จ์„ฑ, ์œ ์‚ฌ์„ฑ, ์ผ๊ด€์„ฑ)์œผ๋กœ ๋ชจ๋ธ ์„ฑ๋Šฅ ํ‰๊ฐ€
  • โšก ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ๋ฐ ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ ์ง€์› ๊ธฐ๋Šฅ ํ•™์Šต
  • ๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ: AITK ๊ธฐ๋Šฅ์„ ํฌ๊ด„์ ์œผ๋กœ ์ดํ•ดํ•˜๊ณ  ์‹ค์šฉ์ ์ธ AI ์—์ด์ „ํŠธ ์ƒ์„ฑ

    ๐ŸŒ ๋ชจ๋“ˆ 2: MCP์™€ AI Toolkit ๊ธฐ์ดˆ

    ๐ŸŒ ๋ชจ๋“ˆ 2: AI Toolkit๊ณผ ํ•จ๊ป˜ํ•˜๋Š” MCP ๊ธฐ๋ณธ ๊ฐœ๋…

    ๐Ÿ“‹ ํ•™์Šต ๋ชฉํ‘œ

    ์ด ๋ชจ๋“ˆ์„ ๋งˆ์น˜๋ฉด ๋‹ค์Œ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • โœ… Model Context Protocol (MCP) ์•„ํ‚คํ…์ฒ˜์™€ ์žฅ์  ์ดํ•ดํ•˜๊ธฐ
  • โœ… Microsoft์˜ MCP ์„œ๋ฒ„ ์ƒํƒœ๊ณ„ ํƒ์ƒ‰ํ•˜๊ธฐ
  • โœ… MCP ์„œ๋ฒ„๋ฅผ AI Toolkit Agent Builder์™€ ํ†ตํ•ฉํ•˜๊ธฐ
  • โœ… Playwright MCP๋ฅผ ํ™œ์šฉํ•œ ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ์—์ด์ „ํŠธ ๊ตฌ์ถ•ํ•˜๊ธฐ
  • โœ… ์—์ด์ „ํŠธ ๋‚ด์—์„œ MCP ๋„๊ตฌ ๊ตฌ์„ฑ ๋ฐ ํ…Œ์ŠคํŠธํ•˜๊ธฐ
  • โœ… MCP ๊ธฐ๋ฐ˜ ์—์ด์ „ํŠธ๋ฅผ ๋‚ด๋ณด๋‚ด๊ณ  ํ”„๋กœ๋•์…˜์— ๋ฐฐํฌํ•˜๊ธฐ
  • ๐ŸŽฏ ๋ชจ๋“ˆ 1์—์„œ ์ด์–ด์„œ

    ๋ชจ๋“ˆ 1์—์„œ๋Š” AI Toolkit ๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํžˆ๊ณ  ์ฒซ Python ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ ํ˜์‹ ์ ์ธ Model Context Protocol (MCP)์„ ํ†ตํ•ด ์™ธ๋ถ€ ๋„๊ตฌ์™€ ์„œ๋น„์Šค์— ์—ฐ๊ฒฐํ•˜์—ฌ ์—์ด์ „ํŠธ๋ฅผ ๊ฐ•๋ ฅํ•˜๊ฒŒ ์—…๊ทธ๋ ˆ์ด๋“œํ•  ์ฐจ๋ก€์ž…๋‹ˆ๋‹ค.

    ๊ธฐ๋ณธ ๊ณ„์‚ฐ๊ธฐ์—์„œ ์™„์ „ํ•œ ์ปดํ“จํ„ฐ๋กœ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋Š” ๊ฒƒ๊ณผ ๊ฐ™๋‹ค๊ณ  ์ƒ๊ฐํ•˜์„ธ์š” โ€” AI ์—์ด์ „ํŠธ๊ฐ€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋Šฅ๋ ฅ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

  • ๐ŸŒ ์›น์‚ฌ์ดํŠธ ํƒ์ƒ‰ ๋ฐ ์ƒํ˜ธ์ž‘์šฉ
  • ๐Ÿ“ ํŒŒ์ผ ์ ‘๊ทผ ๋ฐ ์กฐ์ž‘
  • ๐Ÿ”ง ๊ธฐ์—… ์‹œ์Šคํ…œ๊ณผ ํ†ตํ•ฉ
  • ๐Ÿ“Š API๋ฅผ ํ†ตํ•œ ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ
  • ๐Ÿง  Model Context Protocol (MCP) ์ดํ•ดํ•˜๊ธฐ

    ๐Ÿ” MCP๋ž€ ๋ฌด์—‡์ธ๊ฐ€?

    Model Context Protocol (MCP)์€ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ "USB-C"์™€ ๊ฐ™์€ ํ˜์‹ ์ ์ธ ์˜คํ”ˆ ํ‘œ์ค€์ž…๋‹ˆ๋‹ค. ๋Œ€ํ˜• ์–ธ์–ด ๋ชจ๋ธ(LLM)์„ ์™ธ๋ถ€ ๋„๊ตฌ, ๋ฐ์ดํ„ฐ ์†Œ์Šค, ์„œ๋น„์Šค์™€ ์—ฐ๊ฒฐํ•ด ์ค๋‹ˆ๋‹ค. USB-C๊ฐ€ ๋ณต์žกํ•œ ์ผ€์ด๋ธ” ๋ฌธ์ œ๋ฅผ ํ•˜๋‚˜์˜ ํ‘œ์ค€ ์ปค๋„ฅํ„ฐ๋กœ ํ•ด๊ฒฐํ–ˆ๋“ฏ, MCP๋Š” AI ํ†ตํ•ฉ์˜ ๋ณต์žกํ•จ์„ ํ•˜๋‚˜์˜ ํ‘œ์ค€ ํ”„๋กœํ† ์ฝœ๋กœ ๊ฐ„์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค.

    ๐ŸŽฏ MCP๊ฐ€ ํ•ด๊ฒฐํ•˜๋Š” ๋ฌธ์ œ

    MCP ์ด์ „:

  • ๐Ÿ”ง ๋„๊ตฌ๋ณ„ ๋งž์ถค ํ†ตํ•ฉ ํ•„์š”
  • ๐Ÿ”„ ๋…์  ์†”๋ฃจ์…˜์— ์˜ํ•œ ๊ณต๊ธ‰์—…์ฒด ์ข…์†
  • ๐Ÿ”’ ์ž„์‹œ ์—ฐ๊ฒฐ๋กœ ์ธํ•œ ๋ณด์•ˆ ์ทจ์•ฝ์ 
  • โฑ๏ธ ๊ธฐ๋ณธ ํ†ตํ•ฉ์—๋„ ์ˆ˜๊ฐœ์›” ๊ฐœ๋ฐœ ์†Œ์š”
  • MCP ๋„์ž… ํ›„:

  • โšก ํ”Œ๋Ÿฌ๊ทธ ์•ค ํ”Œ๋ ˆ์ด ๋„๊ตฌ ํ†ตํ•ฉ
  • ๐Ÿ”„ ๊ณต๊ธ‰์—…์ฒด์— ๊ตฌ์• ๋ฐ›์ง€ ์•Š๋Š” ์•„ํ‚คํ…์ฒ˜
  • ๐Ÿ›ก๏ธ ๋‚ด์žฅ๋œ ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ๐Ÿš€ ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ ์ถ”๊ฐ€์— ๋ช‡ ๋ถ„ ์†Œ์š”
  • ๐Ÿ—๏ธ MCP ์•„ํ‚คํ…์ฒ˜ ์‹ฌ์ธต ๋ถ„์„

    MCP๋Š” ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋”ฐ๋ฅด๋ฉฐ, ์•ˆ์ „ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๊ณ„๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค:

    
    graph TB
    
        A[AI Application/Agent] --> B[MCP Client]
    
        B --> C[MCP Server 1: Files]
    
        B --> D[MCP Server 2: Web APIs]
    
        B --> E[MCP Server 3: Database]
    
        B --> F[MCP Server N: Custom Tools]
    
        
    
        C --> G[Local File System]
    
        D --> H[External APIs]
    
        E --> I[Database Systems]
    
        F --> J[Enterprise Systems]
    
    

    ๐Ÿ”ง ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ:

    ๊ตฌ์„ฑ ์š”์†Œ ์—ญํ•  ์˜ˆ์‹œ ----------- ------ ---------- MCP Hosts MCP ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ Claude Desktop, VS Code, AI Toolkit MCP Clients ํ”„๋กœํ† ์ฝœ ํ•ธ๋“ค๋Ÿฌ (์„œ๋ฒ„์™€ 1:1 ๋งค์นญ) ํ˜ธ์ŠคํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด์žฅ MCP Servers ํ‘œ์ค€ ํ”„๋กœํ† ์ฝœ๋กœ ๊ธฐ๋Šฅ ์ œ๊ณต Playwright, Files, Azure, GitHub ์ „์†ก ๊ณ„์ธต ํ†ต์‹  ๋ฐฉ์‹ stdio, HTTP, WebSockets

    ๐Ÿข Microsoft์˜ MCP ์„œ๋ฒ„ ์ƒํƒœ๊ณ„

    Microsoft๋Š” ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ๋ฅผ ์ถฉ์กฑํ•˜๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ์„œ๋ฒ„ ์ œํ’ˆ๊ตฐ์œผ๋กœ MCP ์ƒํƒœ๊ณ„๋ฅผ ์„ ๋„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

    ๐ŸŒŸ ์ฃผ์š” Microsoft MCP ์„œ๋ฒ„

    1. โ˜๏ธ Azure MCP ์„œ๋ฒ„

    ๐Ÿ”— ์ €์žฅ์†Œ: azure/azure-mcp

    ๐ŸŽฏ ๋ชฉ์ : AI ํ†ตํ•ฉ์„ ํ†ตํ•œ ์ข…ํ•ฉ Azure ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ

    โœจ ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ์„ ์–ธ์  ์ธํ”„๋ผ ํ”„๋กœ๋น„์ €๋‹
  • ์‹ค์‹œ๊ฐ„ ๋ฆฌ์†Œ์Šค ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๋น„์šฉ ์ตœ์ ํ™” ๊ถŒ๊ณ 
  • ๋ณด์•ˆ ๊ทœ์ • ์ค€์ˆ˜ ๊ฒ€์‚ฌ
  • ๐Ÿš€ ํ™œ์šฉ ์‚ฌ๋ก€:

  • AI ์ง€์› ์ธํ”„๋ผ ์ฝ”๋“œ ๊ด€๋ฆฌ
  • ์ž๋™ ๋ฆฌ์†Œ์Šค ์Šค์ผ€์ผ๋ง
  • ํด๋ผ์šฐ๋“œ ๋น„์šฉ ์ตœ์ ํ™”
  • DevOps ์›Œํฌํ”Œ๋กœ์šฐ ์ž๋™ํ™”
  • 2. ๐Ÿ“Š Microsoft Dataverse MCP

    ๐Ÿ“š ๋ฌธ์„œ: Microsoft Dataverse Integration

    ๐ŸŽฏ ๋ชฉ์ : ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์œ„ํ•œ ์ž์—ฐ์–ด ์ธํ„ฐํŽ˜์ด์Šค

    โœจ ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ์ž์—ฐ์–ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ
  • ๋น„์ฆˆ๋‹ˆ์Šค ์ปจํ…์ŠคํŠธ ์ดํ•ด
  • ๋งž์ถคํ˜• ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ
  • ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๋ฐ์ดํ„ฐ ๊ฑฐ๋ฒ„๋„Œ์Šค
  • ๐Ÿš€ ํ™œ์šฉ ์‚ฌ๋ก€:

  • ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค ๋ณด๊ณ 
  • ๊ณ ๊ฐ ๋ฐ์ดํ„ฐ ๋ถ„์„
  • ์˜์—… ํŒŒ์ดํ”„๋ผ์ธ ์ธ์‚ฌ์ดํŠธ
  • ๊ทœ์ • ์ค€์ˆ˜ ๋ฐ์ดํ„ฐ ์ฟผ๋ฆฌ
  • 3. ๐ŸŒ Playwright MCP ์„œ๋ฒ„

    ๐Ÿ”— ์ €์žฅ์†Œ: microsoft/playwright-mcp

    ๐ŸŽฏ ๋ชฉ์ : ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ๋ฐ ์›น ์ƒํ˜ธ์ž‘์šฉ ๊ธฐ๋Šฅ ์ œ๊ณต

    โœจ ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ํฌ๋กœ์Šค ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” (Chrome, Firefox, Safari)
  • ์ง€๋Šฅํ˜• ์š”์†Œ ๊ฐ์ง€
  • ์Šคํฌ๋ฆฐ์ƒท ๋ฐ PDF ์ƒ์„ฑ
  • ๋„คํŠธ์›Œํฌ ํŠธ๋ž˜ํ”ฝ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๐Ÿš€ ํ™œ์šฉ ์‚ฌ๋ก€:

  • ์ž๋™ํ™” ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ์šฐ
  • ์›น ์Šคํฌ๋ž˜ํ•‘ ๋ฐ ๋ฐ์ดํ„ฐ ์ถ”์ถœ
  • UI/UX ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๊ฒฝ์Ÿ์‚ฌ ๋ถ„์„ ์ž๋™ํ™”
  • 4. ๐Ÿ“ Files MCP ์„œ๋ฒ„

    ๐Ÿ”— ์ €์žฅ์†Œ: microsoft/files-mcp-server

    ๐ŸŽฏ ๋ชฉ์ : ์ง€๋Šฅํ˜• ํŒŒ์ผ ์‹œ์Šคํ…œ ์ž‘์—…

    โœจ ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ์„ ์–ธ์  ํŒŒ์ผ ๊ด€๋ฆฌ
  • ์ฝ˜ํ…์ธ  ๋™๊ธฐํ™”
  • ๋ฒ„์ „ ๊ด€๋ฆฌ ํ†ตํ•ฉ
  • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ถ”์ถœ
  • ๐Ÿš€ ํ™œ์šฉ ์‚ฌ๋ก€:

  • ๋ฌธ์„œ ๊ด€๋ฆฌ
  • ์ฝ”๋“œ ์ €์žฅ์†Œ ์ •๋ฆฌ
  • ์ฝ˜ํ…์ธ  ํผ๋ธ”๋ฆฌ์‹ฑ ์›Œํฌํ”Œ๋กœ์šฐ
  • ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ ํŒŒ์ผ ์ฒ˜๋ฆฌ
  • 5. ๐Ÿ“ MarkItDown MCP ์„œ๋ฒ„

    ๐Ÿ”— ์ €์žฅ์†Œ: microsoft/markitdown

    ๐ŸŽฏ ๋ชฉ์ : ๊ณ ๊ธ‰ Markdown ์ฒ˜๋ฆฌ ๋ฐ ์กฐ์ž‘

    โœจ ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ํ’๋ถ€ํ•œ Markdown ํŒŒ์‹ฑ
  • ํฌ๋งท ๋ณ€ํ™˜ (MD โ†” HTML โ†” PDF)
  • ์ฝ˜ํ…์ธ  ๊ตฌ์กฐ ๋ถ„์„
  • ํ…œํ”Œ๋ฆฟ ์ฒ˜๋ฆฌ
  • ๐Ÿš€ ํ™œ์šฉ ์‚ฌ๋ก€:

  • ๊ธฐ์ˆ  ๋ฌธ์„œ ์›Œํฌํ”Œ๋กœ์šฐ
  • ์ฝ˜ํ…์ธ  ๊ด€๋ฆฌ ์‹œ์Šคํ…œ
  • ๋ณด๊ณ ์„œ ์ƒ์„ฑ
  • ์ง€์‹ ๋ฒ ์ด์Šค ์ž๋™ํ™”
  • 6. ๐Ÿ“ˆ Clarity MCP ์„œ๋ฒ„

    ๐Ÿ“ฆ ํŒจํ‚ค์ง€: @microsoft/clarity-mcp-server

    ๐ŸŽฏ ๋ชฉ์ : ์›น ๋ถ„์„ ๋ฐ ์‚ฌ์šฉ์ž ํ–‰๋™ ์ธ์‚ฌ์ดํŠธ ์ œ๊ณต

    โœจ ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ํžˆํŠธ๋งต ๋ฐ์ดํ„ฐ ๋ถ„์„
  • ์‚ฌ์šฉ์ž ์„ธ์…˜ ๋…นํ™”
  • ์„ฑ๋Šฅ ์ง€ํ‘œ
  • ์ „ํ™˜ ํผ๋„ ๋ถ„์„
  • ๐Ÿš€ ํ™œ์šฉ ์‚ฌ๋ก€:

  • ์›น์‚ฌ์ดํŠธ ์ตœ์ ํ™”
  • ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ์—ฐ๊ตฌ
  • A/B ํ…Œ์ŠคํŠธ ๋ถ„์„
  • ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค ๋Œ€์‹œ๋ณด๋“œ
  • ๐ŸŒ ์ปค๋ฎค๋‹ˆํ‹ฐ ์ƒํƒœ๊ณ„

    Microsoft ์„œ๋ฒ„ ์™ธ์—๋„ MCP ์ƒํƒœ๊ณ„์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:

  • ๐Ÿ™ GitHub MCP: ์ €์žฅ์†Œ ๊ด€๋ฆฌ ๋ฐ ์ฝ”๋“œ ๋ถ„์„
  • ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค MCP: PostgreSQL, MySQL, MongoDB ํ†ตํ•ฉ
  • โ˜๏ธ ํด๋ผ์šฐ๋“œ ์ œ๊ณต์ž MCP: AWS, GCP, Digital Ocean ๋„๊ตฌ
  • ๐Ÿ“ง ์ปค๋ฎค๋‹ˆ์ผ€์ด์…˜ MCP: Slack, Teams, ์ด๋ฉ”์ผ ํ†ตํ•ฉ
  • ๐Ÿ› ๏ธ ์‹ค์Šต: ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ์—์ด์ „ํŠธ ๋งŒ๋“ค๊ธฐ

    ๐ŸŽฏ ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ: Playwright MCP ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•ด ์›น์‚ฌ์ดํŠธ๋ฅผ ํƒ์ƒ‰ํ•˜๊ณ  ์ •๋ณด๋ฅผ ์ถ”์ถœํ•˜๋ฉฐ ๋ณต์žกํ•œ ์›น ์ƒํ˜ธ์ž‘์šฉ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ์ง€๋Šฅํ˜• ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    ๐Ÿš€ 1๋‹จ๊ณ„: ์—์ด์ „ํŠธ ๊ธฐ๋ณธ ์„ค์ •

    1๋‹จ๊ณ„: ์—์ด์ „ํŠธ ์ดˆ๊ธฐํ™”

    1. AI Toolkit Agent Builder ์—ด๊ธฐ

    2. ์ƒˆ ์—์ด์ „ํŠธ ์ƒ์„ฑ ๋ฐ ๋‹ค์Œ ์„ค์ • ์ ์šฉ:

    - ์ด๋ฆ„: BrowserAgent

    - ๋ชจ๋ธ: GPT-4o ์„ ํƒ

    ๐Ÿ”ง 2๋‹จ๊ณ„: MCP ํ†ตํ•ฉ ์›Œํฌํ”Œ๋กœ์šฐ

    3๋‹จ๊ณ„: MCP ์„œ๋ฒ„ ํ†ตํ•ฉ ์ถ”๊ฐ€

    1. Agent Builder์—์„œ ๋„๊ตฌ ์„น์…˜์œผ๋กœ ์ด๋™

    2. "๋„๊ตฌ ์ถ”๊ฐ€" ํด๋ฆญํ•˜์—ฌ ํ†ตํ•ฉ ๋ฉ”๋‰ด ์—ด๊ธฐ

    3. "MCP ์„œ๋ฒ„" ์„ ํƒ

    ๐Ÿ” ๋„๊ตฌ ์œ ํ˜• ์ดํ•ดํ•˜๊ธฐ:

  • ๋‚ด์žฅ ๋„๊ตฌ: ์‚ฌ์ „ ๊ตฌ์„ฑ๋œ AI Toolkit ๊ธฐ๋Šฅ
  • MCP ์„œ๋ฒ„: ์™ธ๋ถ€ ์„œ๋น„์Šค ํ†ตํ•ฉ
  • ์‚ฌ์šฉ์ž ์ •์˜ API: ์ง์ ‘ ๋งŒ๋“  ์„œ๋น„์Šค ์—”๋“œํฌ์ธํŠธ
  • ํ•จ์ˆ˜ ํ˜ธ์ถœ: ๋ชจ๋ธ ํ•จ์ˆ˜ ์ง์ ‘ ์ ‘๊ทผ
  • 4๋‹จ๊ณ„: MCP ์„œ๋ฒ„ ์„ ํƒ

    1. "MCP ์„œ๋ฒ„" ์˜ต์…˜ ์„ ํƒํ•˜์—ฌ ์ง„ํ–‰

    2. MCP ์นดํƒˆ๋กœ๊ทธ ํƒ์ƒ‰ํ•˜์—ฌ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ†ตํ•ฉ ํ™•์ธ

    ๐ŸŽฎ 3๋‹จ๊ณ„: Playwright MCP ๊ตฌ์„ฑ

    5๋‹จ๊ณ„: Playwright ์„ ํƒ ๋ฐ ์„ค์ •

    1. "์ถ”์ฒœ MCP ์„œ๋ฒ„ ์‚ฌ์šฉ" ํด๋ฆญํ•˜์—ฌ Microsoft ๊ฒ€์ฆ ์„œ๋ฒ„ ์ ‘๊ทผ

    2. ์ถ”์ฒœ ๋ชฉ๋ก์—์„œ "Playwright" ์„ ํƒ

    3. ๊ธฐ๋ณธ MCP ID ์ˆ˜๋ฝ ๋˜๋Š” ํ™˜๊ฒฝ์— ๋งž๊ฒŒ ์ˆ˜์ •

    6๋‹จ๊ณ„: Playwright ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”

    ๐Ÿ”‘ ์ค‘์š” ๋‹จ๊ณ„: ์ตœ๋Œ€ ๊ธฐ๋Šฅ์„ ์œ„ํ•ด Playwright์˜ ๋ชจ๋“  ๋ฉ”์„œ๋“œ ์„ ํƒ

    ๐Ÿ› ๏ธ ํ•„์ˆ˜ Playwright ๋„๊ตฌ:

  • ํƒ์ƒ‰: goto, goBack, goForward, reload
  • ์ƒํ˜ธ์ž‘์šฉ: click, fill, press, hover, drag
  • ์ถ”์ถœ: textContent, innerHTML, getAttribute
  • ๊ฒ€์ฆ: isVisible, isEnabled, waitForSelector
  • ์บก์ฒ˜: screenshot, pdf, video
  • ๋„คํŠธ์›Œํฌ: setExtraHTTPHeaders, route, waitForResponse
  • 7๋‹จ๊ณ„: ํ†ตํ•ฉ ์„ฑ๊ณต ํ™•์ธ

    โœ… ์„ฑ๊ณต ์ง€ํ‘œ:

  • ๋ชจ๋“  ๋„๊ตฌ๊ฐ€ Agent Builder ์ธํ„ฐํŽ˜์ด์Šค์— ํ‘œ์‹œ๋จ
  • ํ†ตํ•ฉ ํŒจ๋„์— ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์—†์Œ
  • Playwright ์„œ๋ฒ„ ์ƒํƒœ๊ฐ€ "Connected"๋กœ ํ‘œ์‹œ๋จ
  • ๐Ÿ”ง ์ผ๋ฐ˜ ๋ฌธ์ œ ํ•ด๊ฒฐ:

  • ์—ฐ๊ฒฐ ์‹คํŒจ: ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ ๋ฐ ๋ฐฉํ™”๋ฒฝ ์„ค์ • ํ™•์ธ
  • ๋„๊ตฌ ๋ˆ„๋ฝ: ์„ค์ • ์‹œ ๋ชจ๋“  ๊ธฐ๋Šฅ ์„ ํƒ ์—ฌ๋ถ€ ํ™•์ธ
  • ๊ถŒํ•œ ์˜ค๋ฅ˜: VS Code์— ํ•„์š”ํ•œ ์‹œ์Šคํ…œ ๊ถŒํ•œ ๋ถ€์—ฌ ํ™•์ธ
  • ๐ŸŽฏ 4๋‹จ๊ณ„: ๊ณ ๊ธ‰ ํ”„๋กฌํ”„ํŠธ ์„ค๊ณ„

    8๋‹จ๊ณ„: ์ง€๋Šฅํ˜• ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋””์ž์ธ

    Playwright์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋Š” ์ •๊ตํ•œ ํ”„๋กฌํ”„ํŠธ ์ž‘์„ฑ:

    
    # Web Automation Expert System Prompt
    
    
    
    ## Core Identity
    
    You are an advanced web automation specialist with deep expertise in browser automation, web scraping, and user experience analysis. You have access to Playwright tools for comprehensive browser control.
    
    
    
    ## Capabilities & Approach
    
    ### Navigation Strategy
    
    - Always start with screenshots to understand page layout
    
    - Use semantic selectors (text content, labels) when possible
    
    - Implement wait strategies for dynamic content
    
    - Handle single-page applications (SPAs) effectively
    
    
    
    ### Error Handling
    
    - Retry failed operations with exponential backoff
    
    - Provide clear error descriptions and solutions
    
    - Suggest alternative approaches when primary methods fail
    
    - Always capture diagnostic screenshots on errors
    
    
    
    ### Data Extraction
    
    - Extract structured data in JSON format when possible
    
    - Provide confidence scores for extracted information
    
    - Validate data completeness and accuracy
    
    - Handle pagination and infinite scroll scenarios
    
    
    
    ### Reporting
    
    - Include step-by-step execution logs
    
    - Provide before/after screenshots for verification
    
    - Suggest optimizations and alternative approaches
    
    - Document any limitations or edge cases encountered
    
    
    
    ## Ethical Guidelines
    
    - Respect robots.txt and rate limiting
    
    - Avoid overloading target servers
    
    - Only extract publicly available information
    
    - Follow website terms of service
    
    
    9๋‹จ๊ณ„: ๋™์  ์‚ฌ์šฉ์ž ํ”„๋กฌํ”„ํŠธ ์ƒ์„ฑ

    ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ๋ณด์—ฌ์ฃผ๋Š” ํ”„๋กฌํ”„ํŠธ ์„ค๊ณ„:

    ๐ŸŒ ์›น ๋ถ„์„ ์˜ˆ์‹œ:

    
    Navigate to github.com/kinfey and provide a comprehensive analysis including:
    
    1. Repository structure and organization
    
    2. Recent activity and contribution patterns  
    
    3. Documentation quality assessment
    
    4. Technology stack identification
    
    5. Community engagement metrics
    
    6. Notable projects and their purposes
    
    
    
    Include screenshots at key steps and provide actionable insights.
    
    

    ๐Ÿš€ 5๋‹จ๊ณ„: ์‹คํ–‰ ๋ฐ ํ…Œ์ŠคํŠธ

    10๋‹จ๊ณ„: ์ฒซ ์ž๋™ํ™” ์‹คํ–‰

    1. "์‹คํ–‰" ํด๋ฆญํ•˜์—ฌ ์ž๋™ํ™” ์‹œํ€€์Šค ์‹œ์ž‘

    2. ์‹ค์‹œ๊ฐ„ ์‹คํ–‰ ๋ชจ๋‹ˆํ„ฐ๋ง:

    - Chrome ๋ธŒ๋ผ์šฐ์ € ์ž๋™ ์‹คํ–‰

    - ์—์ด์ „ํŠธ๊ฐ€ ๋Œ€์ƒ ์›น์‚ฌ์ดํŠธ ํƒ์ƒ‰

    - ์ฃผ์š” ๋‹จ๊ณ„๋งˆ๋‹ค ์Šคํฌ๋ฆฐ์ƒท ์บก์ฒ˜

    - ๋ถ„์„ ๊ฒฐ๊ณผ ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ

    11๋‹จ๊ณ„: ๊ฒฐ๊ณผ ๋ฐ ์ธ์‚ฌ์ดํŠธ ๋ถ„์„

    Agent Builder ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์ข…ํ•ฉ ๋ถ„์„ ๊ฒ€ํ† :

    ๐ŸŒŸ 6๋‹จ๊ณ„: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ๋ฐ ๋ฐฐํฌ

    12๋‹จ๊ณ„: ๋‚ด๋ณด๋‚ด๊ธฐ ๋ฐ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ

    Agent Builder๋Š” ๋‹ค์–‘ํ•œ ๋ฐฐํฌ ์˜ต์…˜์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค:

    ๐ŸŽ“ ๋ชจ๋“ˆ 2 ์š”์•ฝ ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

    ๐Ÿ† ๋‹ฌ์„ฑํ•œ ๋ชฉํ‘œ: MCP ํ†ตํ•ฉ ๋งˆ์Šคํ„ฐ

    โœ… ์Šต๋“ํ•œ ๊ธฐ์ˆ :

  • [ ] MCP ์•„ํ‚คํ…์ฒ˜์™€ ์žฅ์  ์ดํ•ด
  • [ ] Microsoft MCP ์„œ๋ฒ„ ์ƒํƒœ๊ณ„ ํƒ์ƒ‰
  • [ ] Playwright MCP์™€ AI Toolkit ํ†ตํ•ฉ
  • [ ] ์ •๊ตํ•œ ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ์—์ด์ „ํŠธ ๊ตฌ์ถ•
  • [ ] ์›น ์ž๋™ํ™”๋ฅผ ์œ„ํ•œ ๊ณ ๊ธ‰ ํ”„๋กฌํ”„ํŠธ ์—”์ง€๋‹ˆ์–ด๋ง
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

  • ๐Ÿ”— MCP ์‚ฌ์–‘: ๊ณต์‹ ํ”„๋กœํ† ์ฝœ ๋ฌธ์„œ
  • ๐Ÿ› ๏ธ Playwright API: ์ „์ฒด ๋ฉ”์„œ๋“œ ์ฐธ์กฐ
  • ๐Ÿข Microsoft MCP ์„œ๋ฒ„: ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํ†ตํ•ฉ ๊ฐ€์ด๋“œ
  • ๐ŸŒ ์ปค๋ฎค๋‹ˆํ‹ฐ ์˜ˆ์ œ: MCP ์„œ๋ฒ„ ๊ฐค๋Ÿฌ๋ฆฌ
  • ๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! MCP ํ†ตํ•ฉ์„ ์„ฑ๊ณต์ ์œผ๋กœ ๋งˆ์Šคํ„ฐํ•˜์—ฌ ์™ธ๋ถ€ ๋„๊ตฌ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ํ”„๋กœ๋•์…˜ ์ค€๋น„ AI ์—์ด์ „ํŠธ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค!

    ๐Ÿ”œ ๋‹ค์Œ ๋ชจ๋“ˆ๋กœ ์ง„ํ–‰

    MCP ๊ธฐ์ˆ ์„ ํ•œ ๋‹จ๊ณ„ ๋” ๋ฐœ์ „์‹œํ‚ค๊ณ  ์‹ถ๋‹ค๋ฉด, ๋ชจ๋“ˆ 3: AI Toolkit๊ณผ ํ•จ๊ป˜ํ•˜๋Š” ๊ณ ๊ธ‰ MCP ๊ฐœ๋ฐœ์œผ๋กœ ์ด๋™ํ•˜์„ธ์š”. ์—ฌ๊ธฐ์„œ ๋‹ค์Œ์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

  • ์ž์‹ ๋งŒ์˜ ๋งž์ถค MCP ์„œ๋ฒ„ ๋งŒ๋“ค๊ธฐ
  • ์ตœ์‹  MCP Python SDK ๊ตฌ์„ฑ ๋ฐ ์‚ฌ์šฉ๋ฒ•
  • MCP Inspector๋ฅผ ํ†ตํ•œ ๋””๋ฒ„๊น… ์„ค์ •
  • ๊ณ ๊ธ‰ MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ ๋งˆ์Šคํ„ฐํ•˜๊ธฐ
  • ์ฒ˜์Œ๋ถ€ํ„ฐ Weather MCP ์„œ๋ฒ„ ๊ตฌ์ถ•ํ•˜๊ธฐ
  • ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋ถ€๋ถ„์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•ด ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ์€ ํ•ด๋‹น ์–ธ์–ด์˜ ์›๋ณธ ๋ฌธ์„œ๊ฐ€ ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ์˜ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์†Œ์š” ์‹œ๊ฐ„: 20๋ถ„

  • ๐Ÿง  Model Context Protocol (MCP) ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๊ฐœ๋… ์ˆ™๋‹ฌ
  • ๐ŸŒ Microsoft MCP ์„œ๋ฒ„ ์ƒํƒœ๊ณ„ ํƒ์ƒ‰
  • ๐Ÿค– Playwright MCP ์„œ๋ฒ„๋ฅผ ์‚ฌ์šฉํ•œ ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ์—์ด์ „ํŠธ ๊ตฌ์ถ•
  • ๐Ÿ”ง MCP ์„œ๋ฒ„๋ฅผ AI Toolkit Agent Builder์™€ ํ†ตํ•ฉ
  • ๐Ÿ“Š ์—์ด์ „ํŠธ ๋‚ด MCP ๋„๊ตฌ ๊ตฌ์„ฑ ๋ฐ ํ…Œ์ŠคํŠธ
  • ๐Ÿš€ MCP ๊ธฐ๋ฐ˜ ์—์ด์ „ํŠธ ์ œ์ž‘ ๋ฐ ๋ฐฐํฌ
  • ๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ: ์™ธ๋ถ€ ๋„๊ตฌ๊ฐ€ ๊ฐ•ํ™”๋œ AI ์—์ด์ „ํŠธ ๋ฐฐํฌ ๋Šฅ๋ ฅ ์Šต๋“

    ๐Ÿ”ง ๋ชจ๋“ˆ 3: AI Toolkit์œผ๋กœ ๊ณ ๊ธ‰ MCP ๊ฐœ๋ฐœ

    ๐Ÿ”ง ๋ชจ๋“ˆ 3: AI Toolkit์„ ํ™œ์šฉํ•œ ๊ณ ๊ธ‰ MCP ๊ฐœ๋ฐœ

    ๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ๋งˆ์น˜๋ฉด ๋‹ค์Œ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • โœ… AI Toolkit์„ ์‚ฌ์šฉํ•ด ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ์ƒ์„ฑ
  • โœ… ์ตœ์‹  MCP Python SDK(v1.9.3) ์„ค์ • ๋ฐ ํ™œ์šฉ
  • โœ… ๋””๋ฒ„๊น…์„ ์œ„ํ•œ MCP Inspector ์„ค์ • ๋ฐ ์‚ฌ์šฉ
  • โœ… Agent Builder์™€ Inspector ํ™˜๊ฒฝ์—์„œ MCP ์„œ๋ฒ„ ๋””๋ฒ„๊น…
  • โœ… ๊ณ ๊ธ‰ MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ ์ดํ•ด
  • ๐Ÿ“‹ ์‚ฌ์ „ ์ค€๋น„ ์‚ฌํ•ญ

  • Lab 2 (MCP ๊ธฐ์ดˆ) ์™„๋ฃŒ
  • AI Toolkit ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์ด ์„ค์น˜๋œ VS Code
  • Python 3.10 ์ด์ƒ ํ™˜๊ฒฝ
  • Inspector ์„ค์ •์„ ์œ„ํ•œ Node.js ๋ฐ npm
  • ๐Ÿ—๏ธ ๋งŒ๋“ค๊ฒŒ ๋  ๊ฒƒ

    ์ด๋ฒˆ ์‹ค์Šต์—์„œ๋Š” Weather MCP Server๋ฅผ ๋งŒ๋“ค์–ด ๋‹ค์Œ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค:

  • ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ๊ตฌํ˜„
  • AI Toolkit Agent Builder์™€์˜ ํ†ตํ•ฉ
  • ์ „๋ฌธ์ ์ธ ๋””๋ฒ„๊น… ์›Œํฌํ”Œ๋กœ์šฐ
  • ์ตœ์‹  MCP SDK ์‚ฌ์šฉ ํŒจํ„ด
  • ---

    ๐Ÿ”ง ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ ๊ฐœ์š”

    ๐Ÿ MCP Python SDK

    Model Context Protocol Python SDK๋Š” ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ๊ตฌ์ถ•์˜ ๊ธฐ๋ฐ˜์ž…๋‹ˆ๋‹ค. ๋””๋ฒ„๊น… ๊ธฐ๋Šฅ์ด ๊ฐ•ํ™”๋œ 1.9.3 ๋ฒ„์ „์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

    ๐Ÿ” MCP Inspector

    ๊ฐ•๋ ฅํ•œ ๋””๋ฒ„๊น… ๋„๊ตฌ๋กœ ๋‹ค์Œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

  • ์‹ค์‹œ๊ฐ„ ์„œ๋ฒ„ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๋„๊ตฌ ์‹คํ–‰ ์‹œ๊ฐํ™”
  • ๋„คํŠธ์›Œํฌ ์š”์ฒญ/์‘๋‹ต ๊ฒ€์‚ฌ
  • ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ
  • ---

    ๐Ÿ“– ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„

    1๋‹จ๊ณ„: Agent Builder์—์„œ WeatherAgent ์ƒ์„ฑ

    1. AI Toolkit ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์„ ํ†ตํ•ด VS Code์—์„œ Agent Builder ์‹คํ–‰

    2. ๋‹ค์Œ ์„ค์ •์œผ๋กœ ์ƒˆ ์—์ด์ „ํŠธ ์ƒ์„ฑ:

    - ์—์ด์ „ํŠธ ์ด๋ฆ„: WeatherAgent

    2๋‹จ๊ณ„: MCP ์„œ๋ฒ„ ํ”„๋กœ์ ํŠธ ์ดˆ๊ธฐํ™”

    1. Agent Builder์—์„œ Tools โ†’ Add Tool๋กœ ์ด๋™

    2. "MCP Server" ์„ ํƒ

    3. "Create A new MCP Server" ์„ ํƒ

    4. python-weather ํ…œํ”Œ๋ฆฟ ์„ ํƒ

    5. ์„œ๋ฒ„ ์ด๋ฆ„ ์ง€์ •: weather_mcp

    3๋‹จ๊ณ„: ํ”„๋กœ์ ํŠธ ์—ด๊ณ  ๊ตฌ์กฐ ํ™•์ธ

    1. ์ƒ์„ฑ๋œ ํ”„๋กœ์ ํŠธ๋ฅผ VS Code์—์„œ ์—ด๊ธฐ

    2. ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ ๊ฒ€ํ† :

    ```

    weather_mcp/

    โ”œโ”€โ”€ src/

    โ”‚ โ”œโ”€โ”€ __init__.py

    โ”‚ โ””โ”€โ”€ server.py

    โ”œโ”€โ”€ inspector/

    โ”‚ โ”œโ”€โ”€ package.json

    โ”‚ โ””โ”€โ”€ package-lock.json

    โ”œโ”€โ”€ .vscode/

    โ”‚ โ”œโ”€โ”€ launch.json

    โ”‚ โ””โ”€โ”€ tasks.json

    โ”œโ”€โ”€ pyproject.toml

    โ””โ”€โ”€ README.md

    ```

    4๋‹จ๊ณ„: ์ตœ์‹  MCP SDK๋กœ ์—…๊ทธ๋ ˆ์ด๋“œ

    > ๐Ÿ” ์™œ ์—…๊ทธ๋ ˆ์ด๋“œํ•˜๋‚˜์š”? ์ตœ์‹  MCP SDK(v1.9.3)์™€ Inspector ์„œ๋น„์Šค(0.14.0)๋ฅผ ์‚ฌ์šฉํ•ด ํ–ฅ์ƒ๋œ ๊ธฐ๋Šฅ๊ณผ ๋””๋ฒ„๊น… ์„ฑ๋Šฅ์„ ์–ป๊ธฐ ์œ„ํ•จ์ž…๋‹ˆ๋‹ค.

    4a. Python ์˜์กด์„ฑ ์—…๋ฐ์ดํŠธ

    pyproject.toml ํŽธ์ง‘: ./code/weather_mcp/pyproject.toml ์—…๋ฐ์ดํŠธ

    4b. Inspector ์„ค์ • ์—…๋ฐ์ดํŠธ

    inspector/package.json ํŽธ์ง‘: ./code/weather_mcp/inspector/package.json ์—…๋ฐ์ดํŠธ

    4c. Inspector ์˜์กด์„ฑ ์—…๋ฐ์ดํŠธ

    inspector/package-lock.json ํŽธ์ง‘: ./code/weather_mcp/inspector/package-lock.json ์—…๋ฐ์ดํŠธ

    > ๐Ÿ“ ์ฐธ๊ณ : ์ด ํŒŒ์ผ์€ ๋ฐฉ๋Œ€ํ•œ ์˜์กด์„ฑ ์ •์˜๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜๋Š” ํ•ต์‹ฌ ๊ตฌ์กฐ์ด๋ฉฐ, ์ „์ฒด ๋‚ด์šฉ์€ ์˜ฌ๋ฐ”๋ฅธ ์˜์กด์„ฑ ํ•ด๊ฒฐ์„ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

    > โšก ์ „์ฒด ํŒจํ‚ค์ง€ ๋ฝ: package-lock.json ์ „์ฒด ํŒŒ์ผ์€ ์•ฝ 3000์ค„์— ๋‹ฌํ•˜๋Š” ์˜์กด์„ฑ ์ •์˜๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์œ„๋Š” ์ฃผ์š” ๊ตฌ์กฐ๋งŒ ๋ณด์—ฌ์ฃผ๋ฉฐ, ์™„์ „ํ•œ ์˜์กด์„ฑ ํ•ด๊ฒฐ์„ ์œ„ํ•ด ์ œ๊ณต๋œ ํŒŒ์ผ์„ ์‚ฌ์šฉํ•˜์„ธ์š”.

    5๋‹จ๊ณ„: VS Code ๋””๋ฒ„๊น… ์„ค์ •

    *์ฐธ๊ณ : ์ง€์ •๋œ ๊ฒฝ๋กœ์˜ ํŒŒ์ผ์„ ๋ณต์‚ฌํ•˜์—ฌ ๋กœ์ปฌ ํŒŒ์ผ์„ ๊ต์ฒดํ•˜์„ธ์š”*

    5a. ์‹คํ–‰ ๊ตฌ์„ฑ ์—…๋ฐ์ดํŠธ

    .vscode/launch.json ํŽธ์ง‘:

    
    {
    
      "version": "0.2.0",
    
      "configurations": [
    
        {
    
          "name": "Attach to Local MCP",
    
          "type": "debugpy",
    
          "request": "attach",
    
          "connect": {
    
            "host": "localhost",
    
            "port": 5678
    
          },
    
          "presentation": {
    
            "hidden": true
    
          },
    
          "internalConsoleOptions": "neverOpen",
    
          "postDebugTask": "Terminate All Tasks"
    
        },
    
        {
    
          "name": "Launch Inspector (Edge)",
    
          "type": "msedge",
    
          "request": "launch",
    
          "url": "http://localhost:6274?timeout=60000&serverUrl=http://localhost:3001/sse#tools",
    
          "cascadeTerminateToConfigurations": [
    
            "Attach to Local MCP"
    
          ],
    
          "presentation": {
    
            "hidden": true
    
          },
    
          "internalConsoleOptions": "neverOpen"
    
        },
    
        {
    
          "name": "Launch Inspector (Chrome)",
    
          "type": "chrome",
    
          "request": "launch",
    
          "url": "http://localhost:6274?timeout=60000&serverUrl=http://localhost:3001/sse#tools",
    
          "cascadeTerminateToConfigurations": [
    
            "Attach to Local MCP"
    
          ],
    
          "presentation": {
    
            "hidden": true
    
          },
    
          "internalConsoleOptions": "neverOpen"
    
        }
    
      ],
    
      "compounds": [
    
        {
    
          "name": "Debug in Agent Builder",
    
          "configurations": [
    
            "Attach to Local MCP"
    
          ],
    
          "preLaunchTask": "Open Agent Builder",
    
        },
    
        {
    
          "name": "Debug in Inspector (Edge)",
    
          "configurations": [
    
            "Launch Inspector (Edge)",
    
            "Attach to Local MCP"
    
          ],
    
          "preLaunchTask": "Start MCP Inspector",
    
          "stopAll": true
    
        },
    
        {
    
          "name": "Debug in Inspector (Chrome)",
    
          "configurations": [
    
            "Launch Inspector (Chrome)",
    
            "Attach to Local MCP"
    
          ],
    
          "preLaunchTask": "Start MCP Inspector",
    
          "stopAll": true
    
        }
    
      ]
    
    }
    
    

    .vscode/tasks.json ํŽธ์ง‘:

    
    {
    
      "version": "2.0.0",
    
      "tasks": [
    
        {
    
          "label": "Start MCP Server",
    
          "type": "shell",
    
          "command": "python -m debugpy --listen 127.0.0.1:5678 src/__init__.py sse",
    
          "isBackground": true,
    
          "options": {
    
            "cwd": "${workspaceFolder}",
    
            "env": {
    
              "PORT": "3001"
    
            }
    
          },
    
          "problemMatcher": {
    
            "pattern": [
    
              {
    
                "regexp": "^.*$",
    
                "file": 0,
    
                "location": 1,
    
                "message": 2
    
              }
    
            ],
    
            "background": {
    
              "activeOnStart": true,
    
              "beginsPattern": ".*",
    
              "endsPattern": "Application startup complete|running"
    
            }
    
          }
    
        },
    
        {
    
          "label": "Start MCP Inspector",
    
          "type": "shell",
    
          "command": "npm run dev:inspector",
    
          "isBackground": true,
    
          "options": {
    
            "cwd": "${workspaceFolder}/inspector",
    
            "env": {
    
              "CLIENT_PORT": "6274",
    
              "SERVER_PORT": "6277",
    
            }
    
          },
    
          "problemMatcher": {
    
            "pattern": [
    
              {
    
                "regexp": "^.*$",
    
                "file": 0,
    
                "location": 1,
    
                "message": 2
    
              }
    
            ],
    
            "background": {
    
              "activeOnStart": true,
    
              "beginsPattern": "Starting MCP inspector",
    
              "endsPattern": "Proxy server listening on port"
    
            }
    
          },
    
          "dependsOn": [
    
            "Start MCP Server"
    
          ]
    
        },
    
        {
    
          "label": "Open Agent Builder",
    
          "type": "shell",
    
          "command": "echo ${input:openAgentBuilder}",
    
          "presentation": {
    
            "reveal": "never"
    
          },
    
          "dependsOn": [
    
            "Start MCP Server"
    
          ],
    
        },
    
        {
    
          "label": "Terminate All Tasks",
    
          "command": "echo ${input:terminate}",
    
          "type": "shell",
    
          "problemMatcher": []
    
        }
    
      ],
    
      "inputs": [
    
        {
    
          "id": "openAgentBuilder",
    
          "type": "command",
    
          "command": "ai-mlstudio.agentBuilder",
    
          "args": {
    
            "initialMCPs": [ "local-server-weather_mcp" ],
    
            "triggeredFrom": "vsc-tasks"
    
          }
    
        },
    
        {
    
          "id": "terminate",
    
          "type": "command",
    
          "command": "workbench.action.tasks.terminate",
    
          "args": "terminateAll"
    
        }
    
      ]
    
    }
    
    

    ---

    ๐Ÿš€ MCP ์„œ๋ฒ„ ์‹คํ–‰ ๋ฐ ํ…Œ์ŠคํŠธ

    6๋‹จ๊ณ„: ์˜์กด์„ฑ ์„ค์น˜

    ์„ค์ • ๋ณ€๊ฒฝ ํ›„ ๋‹ค์Œ ๋ช…๋ น์–ด ์‹คํ–‰:

    Python ์˜์กด์„ฑ ์„ค์น˜:

    
    uv sync
    
    

    Inspector ์˜์กด์„ฑ ์„ค์น˜:

    
    cd inspector
    
    npm install
    
    

    7๋‹จ๊ณ„: Agent Builder์—์„œ ๋””๋ฒ„๊น…

    1. F5 ํ‚ค๋ฅผ ๋ˆ„๋ฅด๊ฑฐ๋‚˜ "Debug in Agent Builder" ๊ตฌ์„ฑ ์‚ฌ์šฉ

    2. ๋””๋ฒ„๊ทธ ํŒจ๋„์—์„œ ๋ณตํ•ฉ ๊ตฌ์„ฑ ์„ ํƒ

    3. ์„œ๋ฒ„๊ฐ€ ์‹œ์ž‘๋˜๊ณ  Agent Builder๊ฐ€ ์—ด๋ฆด ๋•Œ๊นŒ์ง€ ๋Œ€๊ธฐ

    4. ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋กœ ๋‚ ์”จ MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ

    ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ž…๋ ฅ ํ”„๋กฌํ”„ํŠธ ์˜ˆ์‹œ

    SYSTEM_PROMPT

    
    You are my weather assistant
    
    

    USER_PROMPT

    
    How's the weather like in Seattle
    
    

    8๋‹จ๊ณ„: MCP Inspector์—์„œ ๋””๋ฒ„๊น…

    1. "Debug in Inspector" ๊ตฌ์„ฑ ์‚ฌ์šฉ (Edge ๋˜๋Š” Chrome)

    2. http://localhost:6274์—์„œ Inspector ์ธํ„ฐํŽ˜์ด์Šค ์—ด๊ธฐ

    3. ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ํƒ์ƒ‰:

    - ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋„๊ตฌ ํ™•์ธ

    - ๋„๊ตฌ ์‹คํ–‰ ํ…Œ์ŠคํŠธ

    - ๋„คํŠธ์›Œํฌ ์š”์ฒญ ๋ชจ๋‹ˆํ„ฐ๋ง

    - ์„œ๋ฒ„ ์‘๋‹ต ๋””๋ฒ„๊น…

    ---

    ๐ŸŽฏ ์ฃผ์š” ํ•™์Šต ์„ฑ๊ณผ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜์—ฌ ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค:

  • [x] AI Toolkit ํ…œํ”Œ๋ฆฟ์„ ํ™œ์šฉํ•œ ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ์ƒ์„ฑ
  • [x] ํ–ฅ์ƒ๋œ ๊ธฐ๋Šฅ์„ ์œ„ํ•œ ์ตœ์‹  MCP SDK(v1.9.3) ์—…๊ทธ๋ ˆ์ด๋“œ
  • [x] Agent Builder์™€ Inspector ๋ชจ๋‘์— ๋Œ€ํ•œ ์ „๋ฌธ์ ์ธ ๋””๋ฒ„๊น… ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌ์„ฑ
  • [x] ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ MCP Inspector ์„ค์ •
  • [x] MCP ๊ฐœ๋ฐœ์„ ์œ„ํ•œ VS Code ๋””๋ฒ„๊น… ๊ตฌ์„ฑ ๋งˆ์Šคํ„ฐ
  • ๐Ÿ”ง ํƒ๊ตฌํ•œ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ

    ๊ธฐ๋Šฅ ์„ค๋ช… ํ™œ์šฉ ์‚ฌ๋ก€ --------- ------------- ---------- MCP Python SDK v1.9.3 ์ตœ์‹  ํ”„๋กœํ† ์ฝœ ๊ตฌํ˜„ ํ˜„๋Œ€์ ์ธ ์„œ๋ฒ„ ๊ฐœ๋ฐœ MCP Inspector 0.14.0 ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ๋””๋ฒ„๊น… ๋„๊ตฌ ์‹ค์‹œ๊ฐ„ ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ VS Code ๋””๋ฒ„๊น… ํ†ตํ•ฉ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์ „๋ฌธ์ ์ธ ๋””๋ฒ„๊น… ์›Œํฌํ”Œ๋กœ์šฐ Agent Builder ํ†ตํ•ฉ AI Toolkit๊ณผ ์ง์ ‘ ์—ฐ๊ฒฐ ์—”๋“œํˆฌ์—”๋“œ ์—์ด์ „ํŠธ ํ…Œ์ŠคํŠธ

    ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

  • MCP Python SDK ๋ฌธ์„œ
  • AI Toolkit ํ™•์žฅ ๊ฐ€์ด๋“œ
  • VS Code ๋””๋ฒ„๊น… ๋ฌธ์„œ
  • Model Context Protocol ์‚ฌ์–‘
  • ---

    ๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! Lab 3์„ ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒํ•˜์—ฌ ์ „๋ฌธ์ ์ธ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ๋กœ ๋งž์ถคํ˜• MCP ์„œ๋ฒ„๋ฅผ ์ƒ์„ฑ, ๋””๋ฒ„๊น…, ๋ฐฐํฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ๐Ÿ”œ ๋‹ค์Œ ๋ชจ๋“ˆ๋กœ ๊ณ„์† ์ง„ํ–‰

    ์‹ค์ œ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ์— MCP ๊ธฐ์ˆ ์„ ์ ์šฉํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ๋ชจ๋“ˆ 4: ์‹ค์ „ MCP ๊ฐœ๋ฐœ - ๋งž์ถคํ˜• GitHub ํด๋ก  ์„œ๋ฒ„๋กœ ์ด๋™ํ•˜์—ฌ:

  • GitHub ์ €์žฅ์†Œ ์ž‘์—…์„ ์ž๋™ํ™”ํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•
  • MCP๋ฅผ ํ†ตํ•œ GitHub ์ €์žฅ์†Œ ํด๋ก  ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • VS Code ๋ฐ GitHub Copilot Agent ๋ชจ๋“œ์™€ ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ํ†ตํ•ฉ
  • ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ ๋ฐ ๋ฐฐํฌ
  • ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ์‹ค์šฉ์ ์ธ ์›Œํฌํ”Œ๋กœ์šฐ ์ž๋™ํ™” ํ•™์Šต
  • ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋ถ€๋ถ„์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ์€ ํ•ด๋‹น ์–ธ์–ด์˜ ์›๋ณธ ๋ฌธ์„œ๊ฐ€ ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์†Œ์š” ์‹œ๊ฐ„: 20๋ถ„

  • ๐Ÿ’ป AI Toolkit์œผ๋กœ ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ๊ตฌ์ถ•
  • ๐Ÿ ์ตœ์‹  MCP Python SDK(v1.9.3) ๊ตฌ์„ฑ ๋ฐ ํ™œ์šฉ
  • ๐Ÿ” MCP Inspector๋ฅผ ์ด์šฉํ•œ ๋””๋ฒ„๊น… ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ์‚ฌ์šฉ
  • ๐Ÿ› ๏ธ ์ „๋ฌธ ๋””๋ฒ„๊น… ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ฐ–์ถ˜ ๋‚ ์”จ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•
  • ๐Ÿงช Agent Builder ๋ฐ Inspector ํ™˜๊ฒฝ์—์„œ MCP ์„œ๋ฒ„ ๋””๋ฒ„๊น…
  • ๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ: ์ตœ์‹  ๋„๊ตฌ๋กœ ์ปค์Šคํ…€ MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ ๋ฐ ๋””๋ฒ„๊น…

    ๐Ÿ™ ๋ชจ๋“ˆ 4: ์‹ค์ „ MCP ๊ฐœ๋ฐœ - ๋งž์ถคํ˜• GitHub Clone ์„œ๋ฒ„

    ๐Ÿ™ ๋ชจ๋“ˆ 4: ์‹ค์šฉ์ ์ธ MCP ๊ฐœ๋ฐœ - ๋งž์ถคํ˜• GitHub ํด๋ก  ์„œ๋ฒ„

    > โšก ๋น ๋ฅธ ์‹œ์ž‘: 30๋ถ„ ๋งŒ์— GitHub ์ €์žฅ์†Œ ํด๋กœ๋‹๊ณผ VS Code ํ†ตํ•ฉ์„ ์ž๋™ํ™”ํ•˜๋Š” ์ƒ์‚ฐ ์ค€๋น„ ์™„๋ฃŒ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜์„ธ์š”!

    ๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • โœ… ์‹ค์ œ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ์— ๋งž๋Š” ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ์ƒ์„ฑ
  • โœ… MCP๋ฅผ ํ†ตํ•œ GitHub ์ €์žฅ์†Œ ํด๋กœ๋‹ ๊ธฐ๋Šฅ ๊ตฌํ˜„
  • โœ… ๋งž์ถคํ˜• MCP ์„œ๋ฒ„๋ฅผ VS Code ๋ฐ Agent Builder์™€ ํ†ตํ•ฉ
  • โœ… GitHub Copilot ์—์ด์ „ํŠธ ๋ชจ๋“œ๋ฅผ ๋งž์ถค MCP ๋„๊ตฌ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ
  • โœ… ์ƒ์‚ฐ ํ™˜๊ฒฝ์—์„œ ๋งž์ถค MCP ์„œ๋ฒ„๋ฅผ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋ฐฐํฌ
  • ๐Ÿ“‹ ์‚ฌ์ „ ์ค€๋น„ ์‚ฌํ•ญ

  • ์‹ค์Šต 1~3 ์™„๋ฃŒ (MCP ๊ธฐ์ดˆ ๋ฐ ๊ณ ๊ธ‰ ๊ฐœ๋ฐœ)
  • GitHub Copilot ๊ตฌ๋… (๋ฌด๋ฃŒ ๊ฐ€์ž… ๊ฐ€๋Šฅ)
  • AI Toolkit ๋ฐ GitHub Copilot ํ™•์žฅ์ด ์„ค์น˜๋œ VS Code
  • ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ๋œ Git CLI
  • ๐Ÿ—๏ธ ํ”„๋กœ์ ํŠธ ๊ฐœ์š”

    ์‹ค์ œ ๊ฐœ๋ฐœ ๊ณผ์ œ

    ๊ฐœ๋ฐœ์ž๋กœ์„œ ์šฐ๋ฆฌ๋Š” ์ž์ฃผ GitHub ์ €์žฅ์†Œ๋ฅผ ํด๋ก ํ•˜๊ณ  VS Code ๋˜๋Š” VS Code Insiders์—์„œ ์—ฝ๋‹ˆ๋‹ค. ์ด ์ˆ˜๋™ ๊ณผ์ •์€ ๋‹ค์Œ ๋‹จ๊ณ„๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค:

    1. ํ„ฐ๋ฏธ๋„/๋ช…๋ น ํ”„๋กฌํ”„ํŠธ ์—ด๊ธฐ

    2. ์›ํ•˜๋Š” ๋””๋ ‰ํ„ฐ๋ฆฌ๋กœ ์ด๋™

    3. git clone ๋ช…๋ น ์‹คํ–‰

    4. ํด๋ก ํ•œ ๋””๋ ‰ํ„ฐ๋ฆฌ์—์„œ VS Code ์—ด๊ธฐ

    ์šฐ๋ฆฌ์˜ MCP ์†”๋ฃจ์…˜์€ ์ด ๊ณผ์ •์„ ํ•˜๋‚˜์˜ ์Šค๋งˆํŠธํ•œ ๋ช…๋ น์œผ๋กœ ๊ฐ„์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค!

    ๋งŒ๋“ค๊ฒŒ ๋  ๊ฒƒ

    GitHub Clone MCP ์„œ๋ฒ„ (git_mcp_server)๋Š” ๋‹ค์Œ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค:

    ๊ธฐ๋Šฅ ์„ค๋ช… ์ด์  --------- ------------- --------- ๐Ÿ”„ ์Šค๋งˆํŠธ ์ €์žฅ์†Œ ํด๋กœ๋‹ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์™€ ํ•จ๊ป˜ GitHub ์ €์žฅ์†Œ ํด๋ก  ์ž๋™ํ™”๋œ ์˜ค๋ฅ˜ ๊ฒ€์ฆ ๐Ÿ“ ์ง€๋Šฅํ˜• ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ด€๋ฆฌ ์•ˆ์ „ํ•œ ๋””๋ ‰ํ„ฐ๋ฆฌ ํ™•์ธ ๋ฐ ์ƒ์„ฑ ๋ฎ์–ด์“ฐ๊ธฐ ๋ฐฉ์ง€ ๐Ÿš€ ํฌ๋กœ์Šค ํ”Œ๋žซํผ VS Code ํ†ตํ•ฉ ํ”„๋กœ์ ํŠธ๋ฅผ VS Code/Insiders์—์„œ ์—ด๊ธฐ ์›ํ™œํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ์ „ํ™˜ ๐Ÿ›ก๏ธ ๊ฒฌ๊ณ ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋„คํŠธ์›Œํฌ, ๊ถŒํ•œ, ๊ฒฝ๋กœ ๋ฌธ์ œ ์ฒ˜๋ฆฌ ์ƒ์‚ฐ ํ™˜๊ฒฝ์šฉ ์‹ ๋ขฐ์„ฑ

    ---

    ๐Ÿ“– ๋‹จ๊ณ„๋ณ„ ๊ตฌํ˜„

    1๋‹จ๊ณ„: Agent Builder์—์„œ GitHub ์—์ด์ „ํŠธ ์ƒ์„ฑ

    1. AI Toolkit ํ™•์žฅ์—์„œ Agent Builder ์‹คํ–‰

    2. ๋‹ค์Œ ์„ค์ •์œผ๋กœ ์ƒˆ ์—์ด์ „ํŠธ ์ƒ์„ฑ:

    ```

    Agent Name: GitHubAgent

    ```

    3. ๋งž์ถค MCP ์„œ๋ฒ„ ์ดˆ๊ธฐํ™”:

    - ๋„๊ตฌ โ†’ ๋„๊ตฌ ์ถ”๊ฐ€ โ†’ MCP ์„œ๋ฒ„ ๋กœ ์ด๋™

    - "์ƒˆ MCP ์„œ๋ฒ„ ์ƒ์„ฑ" ์„ ํƒ

    - ์ตœ๋Œ€ ์œ ์—ฐ์„ฑ์„ ์œ„ํ•œ Python ํ…œํ”Œ๋ฆฟ ์„ ํƒ

    - ์„œ๋ฒ„ ์ด๋ฆ„: git_mcp_server

    2๋‹จ๊ณ„: GitHub Copilot ์—์ด์ „ํŠธ ๋ชจ๋“œ ๊ตฌ์„ฑ

    1. VS Code์—์„œ GitHub Copilot ์‹คํ–‰ (Ctrl/Cmd + Shift + P โ†’ "GitHub Copilot: Open")

    2. Copilot ์ธํ„ฐํŽ˜์ด์Šค์—์„œ ์—์ด์ „ํŠธ ๋ชจ๋ธ ์„ ํƒ

    3. ํ–ฅ์ƒ๋œ ์ถ”๋ก  ๋Šฅ๋ ฅ์„ ์œ„ํ•ด Claude 3.7 ๋ชจ๋ธ ์„ ํƒ

    4. ๋„๊ตฌ ์ ‘๊ทผ์šฉ MCP ํ†ตํ•ฉ ํ™œ์„ฑํ™”

    > ๐Ÿ’ก ์ „๋ฌธ๊ฐ€ ํŒ: Claude 3.7์€ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ์™€ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ํŒจํ„ด์— ๋Œ€ํ•œ ์ดํ•ด๋„๊ฐ€ ๋›ฐ์–ด๋‚ฉ๋‹ˆ๋‹ค.

    3๋‹จ๊ณ„: ํ•ต์‹ฌ MCP ์„œ๋ฒ„ ๊ธฐ๋Šฅ ๊ตฌํ˜„

    GitHub Copilot ์—์ด์ „ํŠธ ๋ชจ๋“œ์™€ ํ•จ๊ป˜ ๋‹ค์Œ ์ƒ์„ธ ํ”„๋กฌํ”„ํŠธ ์‚ฌ์šฉ:

    
    Create two MCP tools with the following comprehensive requirements:
    
    
    
    ๐Ÿ”ง TOOL A: clone_repository
    
    Requirements:
    
    - Clone any GitHub repository to a specified local folder
    
    - Return the absolute path of the successfully cloned project
    
    - Implement comprehensive validation:
    
      โœ“ Check if target directory already exists (return error if exists)
    
      โœ“ Validate GitHub URL format (https://github.com/user/repo)
    
      โœ“ Verify git command availability (prompt installation if missing)
    
      โœ“ Handle network connectivity issues
    
      โœ“ Provide clear error messages for all failure scenarios
    
    
    
    ๐Ÿš€ TOOL B: open_in_vscode
    
    Requirements:
    
    - Open specified folder in VS Code or VS Code Insiders
    
    - Cross-platform compatibility (Windows/Linux/macOS)
    
    - Use direct application launch (not terminal commands)
    
    - Auto-detect available VS Code installations
    
    - Handle cases where VS Code is not installed
    
    - Provide user-friendly error messages
    
    
    
    Additional Requirements:
    
    - Follow MCP 1.9.3 best practices
    
    - Include proper type hints and documentation
    
    - Implement logging for debugging purposes
    
    - Add input validation for all parameters
    
    - Include comprehensive error handling
    
    

    4๋‹จ๊ณ„: MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ

    4a. Agent Builder์—์„œ ํ…Œ์ŠคํŠธ

    1. Agent Builder์˜ ๋””๋ฒ„๊ทธ ๊ตฌ์„ฑ ์‹คํ–‰

    2. ๋‹ค์Œ ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋กœ ์—์ด์ „ํŠธ ๊ตฌ์„ฑ:

    
    SYSTEM_PROMPT:
    
    You are my intelligent coding repository assistant. You help developers efficiently clone GitHub repositories and set up their development environment. Always provide clear feedback about operations and handle errors gracefully.
    
    

    3. ํ˜„์‹ค์ ์ธ ์‚ฌ์šฉ์ž ์‹œ๋‚˜๋ฆฌ์˜ค๋กœ ํ…Œ์ŠคํŠธ:

    
    USER_PROMPT EXAMPLES:
    
    
    
    Scenario : Basic Clone and Open
    
    "Clone {Your GitHub Repo link such as https://github.com/kinfey/GHCAgentWorkshop
    
     } and save to {The global path you specify}, then open it with VS Code Insiders"
    
    

    ์˜ˆ์ƒ ๊ฒฐ๊ณผ:

  • โœ… ๊ฒฝ๋กœ ํ™•์ธ๊ณผ ํ•จ๊ป˜ ์„ฑ๊ณต์ ์ธ ํด๋ก 
  • โœ… ์ž๋™์œผ๋กœ VS Code ์‹คํ–‰
  • โœ… ์ž˜๋ชป๋œ ์‹œ๋‚˜๋ฆฌ์˜ค์— ๋Œ€ํ•œ ๋ช…ํ™•ํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€
  • โœ… ๊ฒฝ๊ณ„ ์‚ฌ๋ก€์— ๋Œ€ํ•œ ์ ์ ˆํ•œ ์ฒ˜๋ฆฌ
  • 4b. MCP Inspector์—์„œ ํ…Œ์ŠคํŠธ

    ---

    ๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! ์‹ค์ œ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ์‹ค์šฉ์ ์ด๊ณ  ์ƒ์‚ฐ ์ค€๋น„ ์™„๋ฃŒ๋œ MCP ์„œ๋ฒ„๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋งŒ๋“ค์—ˆ์Šต๋‹ˆ๋‹ค. ๋งž์ถค GitHub ํด๋ก  ์„œ๋ฒ„๋Š” ๊ฐœ๋ฐœ์ž ์ƒ์‚ฐ์„ฑ์„ ์ž๋™ํ™”ํ•˜๊ณ  ํ–ฅ์ƒ์‹œํ‚ค๊ธฐ ์œ„ํ•œ MCP์˜ ๊ฐ•๋ ฅํ•จ์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ๐Ÿ† ๋‹ฌ์„ฑํ•œ ์„ฑ๊ณผ:

  • โœ… MCP ๊ฐœ๋ฐœ์ž - ๋งž์ถค MCP ์„œ๋ฒ„ ์ƒ์„ฑ
  • โœ… ์›Œํฌํ”Œ๋กœ์šฐ ์ž๋™ํ™” ์ „๋ฌธ๊ฐ€ - ๊ฐœ๋ฐœ ํ”„๋กœ์„ธ์Šค ๊ฐ„์†Œํ™”
  • โœ… ํ†ตํ•ฉ ์ „๋ฌธ๊ฐ€ - ๋‹ค์–‘ํ•œ ๊ฐœ๋ฐœ ๋„๊ตฌ ์—ฐ๊ฒฐ
  • โœ… ์ƒ์‚ฐ ์ค€๋น„ ์™„๋ฃŒ - ๋ฐฐํฌ ๊ฐ€๋Šฅํ•œ ์†”๋ฃจ์…˜ ๊ตฌ์ถ•
  • ---

    ๐ŸŽ“ ์›Œํฌ์ˆ ์™„๋ฃŒ: Model Context Protocol ์—ฌ์ •

    ์›Œํฌ์ˆ ์ฐธ๊ฐ€์ž ์—ฌ๋Ÿฌ๋ถ„,

    Model Context Protocol ์›Œํฌ์ˆ์˜ ๋„ค ๊ฐœ ๋ชจ๋“ˆ ๋ชจ๋‘๋ฅผ ์™„๋ฃŒํ•˜์‹  ๊ฒƒ์„ ์ถ•ํ•˜๋“œ๋ฆฝ๋‹ˆ๋‹ค! ๊ธฐ๋ณธ AI Toolkit ๊ฐœ๋… ์ดํ•ด๋ถ€ํ„ฐ ์‹ค์ œ ๊ฐœ๋ฐœ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋Š” ์ƒ์‚ฐ ์ค€๋น„ ์™„๋ฃŒ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•๊นŒ์ง€ ๊ธด ์—ฌ์ •์„ ๊ฑธ์–ด์˜ค์…จ์Šต๋‹ˆ๋‹ค.

    ๐Ÿš€ ํ•™์Šต ๊ฒฝ๋กœ ์š”์•ฝ:

    ๋ชจ๋“ˆ 1: AI Toolkit ๊ธฐ์ดˆ, ๋ชจ๋ธ ํ…Œ์ŠคํŠธ, ์ฒซ ๋ฒˆ์งธ AI ์—์ด์ „ํŠธ ์ƒ์„ฑ ํƒ์ƒ‰

    ๋ชจ๋“ˆ 2: MCP ์•„ํ‚คํ…์ฒ˜ ํ•™์Šต, Playwright MCP ํ†ตํ•ฉ, ์ฒซ ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™” ์—์ด์ „ํŠธ ๊ตฌ์ถ•

    ๋ชจ๋“ˆ 3: Weather MCP ์„œ๋ฒ„์™€ ๋””๋ฒ„๊น… ๋„๊ตฌ๋ฅผ ํ†ตํ•œ ๋งž์ถค MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ ๊ณ ๊ธ‰ ๊ณผ์ •

    ๋ชจ๋“ˆ 4: ์‹ค์šฉ์ ์ธ GitHub ์ €์žฅ์†Œ ์›Œํฌํ”Œ๋กœ์šฐ ์ž๋™ํ™” ๋„๊ตฌ๋ฅผ ์ œ์ž‘ ์ ์šฉ

    ๐ŸŒŸ ๋งˆ์Šคํ„ฐํ•œ ๋‚ด์šฉ:

  • โœ… AI Toolkit ์ƒํƒœ๊ณ„: ๋ชจ๋ธ, ์—์ด์ „ํŠธ, ํ†ตํ•ฉ ํŒจํ„ด
  • โœ… MCP ์•„ํ‚คํ…์ฒ˜: ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ๋””์ž์ธ, ์ „์†ก ํ”„๋กœํ† ์ฝœ, ๋ณด์•ˆ
  • โœ… ๊ฐœ๋ฐœ์ž ๋„๊ตฌ: Playground, Inspector, ์ƒ์‚ฐ ๋ฐฐํฌ๊นŒ์ง€
  • โœ… ๋งž์ถค ๊ฐœ๋ฐœ: ์ง์ ‘ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•, ํ…Œ์ŠคํŠธ ๋ฐ ๋ฐฐํฌ
  • โœ… ์‹ค๋ฌด ์ ์šฉ: AI๋กœ ์‹ค์ œ ์›Œํฌํ”Œ๋กœ์šฐ ๋ฌธ์ œ ํ•ด๊ฒฐ
  • ๐Ÿ”ฎ ์•ž์œผ๋กœ์˜ ๋‹จ๊ณ„:

    1. ์ž์‹ ๋งŒ์˜ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•: ๊ณ ์œ ํ•œ ์›Œํฌํ”Œ๋กœ์šฐ ์ž๋™ํ™”์— ์ด ๊ธฐ์ˆ  ํ™œ์šฉ

    2. MCP ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ: ์ฐฝ์ž‘๋ฌผ ๊ณต์œ  ๋ฐ ๋ฐฐ์šฐ๊ธฐ

    3. ๊ณ ๊ธ‰ ํ†ตํ•ฉ ํƒํ—˜: MCP ์„œ๋ฒ„๋ฅผ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์‹œ์Šคํ…œ๊ณผ ์—ฐ๊ฒฐ

    4. ์˜คํ”ˆ ์†Œ์Šค ๊ธฐ์—ฌ: MCP ๋„๊ตฌ์™€ ๋ฌธ์„œ ๊ฐœ์„ ์— ๊ธฐ์—ฌ

    ์ด ์›Œํฌ์ˆ์€ ์‹œ์ž‘์— ๋ถˆ๊ณผํ•ฉ๋‹ˆ๋‹ค. Model Context Protocol ์ƒํƒœ๊ณ„๋Š” ๋น ๋ฅด๊ฒŒ ์ง„ํ™” ์ค‘์ด๋ฉฐ, ์—ฌ๋Ÿฌ๋ถ„์€ AI ๊ธฐ๋ฐ˜ ๊ฐœ๋ฐœ ๋„๊ตฌ ์„ ๋‘์— ์„ค ์ค€๋น„๊ฐ€ ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ฐธ์—ฌ์™€ ํ•™์Šต์— ๊ฐ์‚ฌ๋“œ๋ฆฝ๋‹ˆ๋‹ค!

    ์ด ์›Œํฌ์ˆ์ด ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ฐœ๋ฐœ ์—ฌ์ •์—์„œ AI ๋„๊ตฌ๋ฅผ ๊ตฌ์ถ•ํ•˜๊ณ  ํ™œ์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ํ˜์‹ ํ•˜๋Š” ์•„์ด๋””์–ด์˜ ๋ถˆ์”จ๊ฐ€ ๋˜๊ธธ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์ฆ๊ฑฐ์šด ์ฝ”๋”ฉ ๋˜์„ธ์š”!

    ---

    ๋‹ค์Œ ๋‹จ๊ณ„

    ๋ชจ๋“ˆ 10 ๊ด€๋ จ ๋ชจ๋“  ์‹ค์Šต์„ ์™„๋ฃŒํ•˜์‹  ๊ฒƒ์„ ์ถ•ํ•˜๋“œ๋ฆฝ๋‹ˆ๋‹ค!

  • ๋’ค๋กœ ๊ฐ€๊ธฐ: ๋ชจ๋“ˆ 10 ๊ฐœ์š”
  • ๊ณ„์† ์ง„ํ–‰: ๋ชจ๋“ˆ 11: MCP ์„œ๋ฒ„ ์‹ค์Šต
  • ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ €ํฌ๋Š” ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ ๊ฒฐ๊ณผ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋ถ€๋ถ„์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ์„ ์–‘ํ•ดํ•ด ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ์€ ํ•ด๋‹น ์–ธ์–ด๋กœ ์ž‘์„ฑ๋œ ์›๋ณธ ๋ฌธ์„œ๊ฐ€ ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์— ๋Œ€ํ•ด์„œ๋Š” ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ์ €ํฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์†Œ์š” ์‹œ๊ฐ„: 30๋ถ„

  • ๐Ÿ—๏ธ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์œ„ํ•œ ์‹ค์ œ GitHub Clone MCP ์„œ๋ฒ„ ๊ตฌ์ถ•
  • ๐Ÿ”„ ๊ฒ€์ฆ ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ํฌํ•จํ•œ ์Šค๋งˆํŠธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํด๋ก  ๊ตฌํ˜„
  • ๐Ÿ“ ์ง€๋Šฅํ˜• ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ด€๋ฆฌ ๋ฐ VS Code ํ†ตํ•ฉ
  • ๐Ÿค– GitHub Copilot Agent ๋ชจ๋“œ๋ฅผ ์ปค์Šคํ…€ MCP ๋„๊ตฌ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ
  • ๐Ÿ›ก๏ธ ์ƒ์‚ฐ ํ™˜๊ฒฝ์— ์ ํ•ฉํ•œ ์‹ ๋ขฐ์„ฑ ๋ฐ ํฌ๋กœ์Šค ํ”Œ๋žซํผ ํ˜ธํ™˜์„ฑ ์ ์šฉ
  • ๐ŸŽฏ ํ•™์Šต ๋ชฉํ‘œ: ์‹ค์ œ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ฐ„์†Œํ™”ํ•˜๋Š” ์ƒ์‚ฐ ์ค€๋น„๋œ MCP ์„œ๋ฒ„ ๋ฐฐํฌ

    ๐Ÿ’ก ์‹ค์ œ ์ ์šฉ ์‚ฌ๋ก€ ๋ฐ ์˜ํ–ฅ

    ๐Ÿข ๊ธฐ์—…์šฉ ํ™œ์šฉ ์‚ฌ๋ก€

    ๐Ÿ”„ DevOps ์ž๋™ํ™”

    ์ง€๋Šฅํ˜• ์ž๋™ํ™”๋ฅผ ํ†ตํ•ด ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ ํ˜์‹ :

  • ์Šค๋งˆํŠธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ด€๋ฆฌ: AI ๊ธฐ๋ฐ˜ ์ฝ”๋“œ ๋ฆฌ๋ทฐ ๋ฐ ๋ณ‘ํ•ฉ ๊ฒฐ์ •
  • ์ง€๋Šฅํ˜• CI/CD: ์ฝ”๋“œ ๋ณ€๊ฒฝ์— ๋”ฐ๋ฅธ ์ž๋™ ํŒŒ์ดํ”„๋ผ์ธ ์ตœ์ ํ™”
  • ์ด์Šˆ ๋ถ„๋ฅ˜: ์ž๋™ ๋ฒ„๊ทธ ๋ถ„๋ฅ˜ ๋ฐ ํ• ๋‹น
  • ๐Ÿงช ํ’ˆ์งˆ ๋ณด์ฆ ํ˜์‹ 

    AI ๊ธฐ๋ฐ˜ ์ž๋™ํ™”๋กœ ํ…Œ์ŠคํŠธ ์ˆ˜์ค€ ํ–ฅ์ƒ:

  • ์ง€๋Šฅํ˜• ํ…Œ์ŠคํŠธ ์ƒ์„ฑ: ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ ์ž๋™ ์ƒ์„ฑ
  • ๋น„์ฃผ์–ผ ํšŒ๊ท€ ํ…Œ์ŠคํŠธ: AI ๊ธฐ๋ฐ˜ UI ๋ณ€๊ฒฝ ๊ฐ์ง€
  • ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง: ์‚ฌ์ „ ๋ฌธ์ œ ์‹๋ณ„ ๋ฐ ํ•ด๊ฒฐ
  • ๐Ÿ“Š ๋ฐ์ดํ„ฐ ํŒŒ์ดํ”„๋ผ์ธ ์ธํ…”๋ฆฌ์ „์Šค

    ๋” ์Šค๋งˆํŠธํ•œ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌ์ถ•:

  • ์ ์‘ํ˜• ETL ํ”„๋กœ์„ธ์Šค: ์ž๊ธฐ ์ตœ์ ํ™” ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜
  • ์ด์ƒ ํƒ์ง€: ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ํ’ˆ์งˆ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ์ง€๋Šฅํ˜• ๋ผ์šฐํŒ…: ์Šค๋งˆํŠธ ๋ฐ์ดํ„ฐ ํ๋ฆ„ ๊ด€๋ฆฌ
  • ๐ŸŽง ๊ณ ๊ฐ ๊ฒฝํ—˜ ํ–ฅ์ƒ

    ํƒ์›”ํ•œ ๊ณ ๊ฐ ์ƒํ˜ธ์ž‘์šฉ ์ƒ์„ฑ:

  • ์ƒํ™ฉ ์ธ์ง€ ์ง€์›: ๊ณ ๊ฐ ์ด๋ ฅ์— ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ AI ์—์ด์ „ํŠธ
  • ์‚ฌ์ „ ๋ฌธ์ œ ํ•ด๊ฒฐ: ์˜ˆ์ธก์ ์ธ ๊ณ ๊ฐ ์„œ๋น„์Šค
  • ๋ฉ€ํ‹ฐ ์ฑ„๋„ ํ†ตํ•ฉ: ํ”Œ๋žซํผ ์ „๋ฐ˜์˜ ํ†ตํ•ฉ AI ๊ฒฝํ—˜
  • ๐Ÿ› ๏ธ ์‚ฌ์ „ ์ค€๋น„ ๋ฐ ์„ค์ •

    ๐Ÿ’ป ์‹œ์Šคํ…œ ์š”๊ตฌ์‚ฌํ•ญ

    ๊ตฌ์„ฑ์š”์†Œ ์š”๊ตฌ์‚ฌํ•ญ ๋น„๊ณ  ----------- ------------- ------- ์šด์˜ ์ฒด์ œ Windows 10+, macOS 10.15+, Linux ์ตœ์‹  ์šด์˜์ฒด์ œ ๋ชจ๋‘ ํฌํ•จ Visual Studio Code ์ตœ์‹  ์•ˆ์ • ๋ฒ„์ „ AITK ํ•„์ˆ˜ Node.js v18.0+ ๋ฐ npm MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์šฉ Python 3.10+ Python MCP ์„œ๋ฒ„ ์„ ํƒ ์‚ฌํ•ญ ๋ฉ”๋ชจ๋ฆฌ ์ตœ์†Œ 8GB RAM ๋กœ์ปฌ ๋ชจ๋ธ์šฉ 16GB ๊ถŒ์žฅ

    ๐Ÿ”ง ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

    ๊ถŒ์žฅ VS Code ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ
  • AI Toolkit (ms-windows-ai-studio.windows-ai-studio)
  • Python (ms-python.python)
  • Python ๋””๋ฒ„๊ฑฐ (ms-python.debugpy)
  • GitHub Copilot (GitHub.copilot) - ์„ ํƒ ์‚ฌํ•ญ์ด์ง€๋งŒ ์œ ์šฉ
  • ์„ ํƒ ๋„๊ตฌ
  • uv: ์ตœ์‹  Python ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ์ž
  • MCP Inspector: MCP ์„œ๋ฒ„์šฉ ์‹œ๊ฐ์  ๋””๋ฒ„๊น… ๋„๊ตฌ
  • Playwright: ์›น ์ž๋™ํ™” ์˜ˆ์ œ์šฉ
  • ๐ŸŽ–๏ธ ํ•™์Šต ๊ฒฐ๊ณผ ๋ฐ ์ธ์ฆ ๊ฒฝ๋กœ

    ๐Ÿ† ์—ญ๋Ÿ‰ ๋งˆ์Šคํ„ฐ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

    ์ด ์›Œํฌ์ˆ์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์˜ ๋งˆ์Šคํ„ฐ๋ฆฌ๋ฅผ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    ๐ŸŽฏ ํ•ต์‹ฌ ์—ญ๋Ÿ‰
  • [ ] MCP ํ”„๋กœํ† ์ฝœ ๋งˆ์Šคํ„ฐ๋ฆฌ: ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๊ตฌํ˜„ ํŒจํ„ด์— ๋Œ€ํ•œ ์‹ฌ์ธต ์ดํ•ด
  • [ ] AITK ์ˆ™๋ จ๋„: AI Toolkit์„ ํ†ตํ•œ ์‹ ์†ํ•œ ๊ฐœ๋ฐœ ์ „๋ฌธ์„ฑ ํ™•๋ณด
  • [ ] ์ปค์Šคํ…€ ์„œ๋ฒ„ ๊ฐœ๋ฐœ: MCP ์„œ๋ฒ„ ๊ตฌ์ถ•, ๋ฐฐํฌ, ์œ ์ง€ ๊ด€๋ฆฌ
  • [ ] ๋„๊ตฌ ํ†ตํ•ฉ ์šฐ์ˆ˜์„ฑ: ๊ธฐ์กด ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ์™€ AI ์™„๋ฒฝ ์—ฐ๋™
  • [ ] ๋ฌธ์ œ ํ•ด๊ฒฐ ์ ์šฉ: ์‹ค๋ฌด ๋น„์ฆˆ๋‹ˆ์Šค ๋ฌธ์ œ์— ํ•™์Šต ๋‚ด์šฉ ์ ์šฉ
  • ๐Ÿ”ง ๊ธฐ์ˆ ์  ์—ญ๋Ÿ‰
  • [ ] VS Code์—์„œ AI Toolkit ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ
  • [ ] ๋งž์ถคํ˜• MCP ์„œ๋ฒ„ ์„ค๊ณ„ ๋ฐ ๊ตฌํ˜„
  • [ ] GitHub ๋ชจ๋ธ์„ MCP ์•„ํ‚คํ…์ฒ˜์™€ ํ†ตํ•ฉ
  • [ ] Playwright ์ž๋™ํ™” ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ์šฐ ๊ตฌ์ถ•
  • [ ] ํ”„๋กœ๋•์…˜์šฉ AI ์—์ด์ „ํŠธ ๋ฐฐํฌ
  • [ ] MCP ์„œ๋ฒ„ ์„ฑ๋Šฅ ๋””๋ฒ„๊น… ๋ฐ ์ตœ์ ํ™”
  • ๐Ÿš€ ๊ณ ๊ธ‰ ์—ญ๋Ÿ‰
  • [ ] ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๊ทœ๋ชจ AI ํ†ตํ•ฉ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„
  • [ ] AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๊ตฌํ˜„
  • [ ] ํ™•์žฅ ๊ฐ€๋Šฅํ•œ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ์„ค๊ณ„
  • [ ] ํŠน์ • ๋„๋ฉ”์ธ ๋งž์ถค ๋„๊ตฌ ์ฒด์ธ ์ œ์ž‘
  • [ ] AI ๋„ค์ดํ‹ฐ๋ธŒ ๊ฐœ๋ฐœ ๋ฉ˜ํ† ๋ง
  • ๐Ÿ“– ์ถ”๊ฐ€ ์ž๋ฃŒ

  • MCP ์‚ฌ์–‘ (2025-11-25)
  • AI Toolkit GitHub ์ €์žฅ์†Œ
  • ์ƒ˜ํ”Œ MCP ์„œ๋ฒ„ ๋ชจ์Œ
  • ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๊ฐ€์ด๋“œ
  • OWASP MCP Top 10 - ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ---

    ๐Ÿš€ AI ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ์šฐ ํ˜์‹ ํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”?

    MCP์™€ AI Toolkit์œผ๋กœ ํ•จ๊ป˜ ์ง€๋Šฅํ˜• ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ๋ฏธ๋ž˜๋ฅผ ๊ตฌ์ถ•ํ•ด ๋ด…์‹œ๋‹ค!

    ๋‹ค์Œ ๋‹จ๊ณ„

    ๊ณ„์† ์ง„ํ–‰: ๋ชจ๋“ˆ 11: MCP ์„œ๋ฒ„ ์‹ค์Šต

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋‚ด์šฉ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ฌธ์€ ํ•ด๋‹น ์–ธ์–ด์˜ ์›๋ณธ ๋ฌธ์„œ๊ฐ€ ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ์˜ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    code Module 11

    Module 11 — PostgreSQL

    ๐Ÿš€ PostgreSQL๊ณผ ํ•จ๊ป˜ํ•˜๋Š” MCP ์„œ๋ฒ„ - ์™„๋ฒฝ ํ•™์Šต ๊ฐ€์ด๋“œ

    ๐Ÿง  MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ํ•™์Šต ๊ฒฝ๋กœ ๊ฐœ์š”

    ์ด ํฌ๊ด„์ ์ธ ํ•™์Šต ๊ฐ€์ด๋“œ๋Š” ์‹ค๋ฌด์šฉ ์†Œ๋งค ๋ถ„์„ ๊ตฌํ˜„์„ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ ํ†ตํ•ฉ๋˜๋Š” ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ํ”„๋กœํ† ์ฝœ(Model Context Protocol, MCP) ์„œ๋ฒ„๋ฅผ ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€์œผ๋กœ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ฐ€๋ฅด์นฉ๋‹ˆ๋‹ค.

    ์—ฌ๊ธฐ์„œ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ(Row Level Security, RLS), ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰, Azure AI ํ†ตํ•ฉ, ๋‹ค์ค‘ ํ…Œ๋„Œ์‹œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ๊ณผ ๊ฐ™์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ํŒจํ„ด์„ ๋ฐฐ์šธ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๋ฐฑ์—”๋“œ ๊ฐœ๋ฐœ์ž, AI ์—”์ง€๋‹ˆ์–ด, ๋ฐ์ดํ„ฐ ์•„ํ‚คํ…ํŠธ ์ค‘ ๋ˆ„๊ตฌ๋“ , ์ด ๊ฐ€์ด๋“œ๋Š” ๋‹ค์Œ MCP ์„œ๋ฒ„ https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail์— ๋‹ด๊ธด ์‹ค์ œ ์˜ˆ์ œ์™€ ์‹ค์Šต์œผ๋กœ ๊ตฌ์กฐํ™”๋œ ํ•™์Šต์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ๐Ÿ”— ๊ณต์‹ MCP ๋ฆฌ์†Œ์Šค

  • ๐Ÿ“˜ MCP ๋ฌธ์„œ โ€“ ์ƒ์„ธ ํŠœํ† ๋ฆฌ์–ผ ๋ฐ ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ
  • ๐Ÿ“œ MCP ์‚ฌ์–‘์„œ (2025-11-25) โ€“ ํ”„๋กœํ† ์ฝœ ์•„ํ‚คํ…์ฒ˜ ๋ฐ ๊ธฐ์ˆ  ์ฐธ์กฐ
  • ๐Ÿง‘โ€๐Ÿ’ป MCP GitHub ์ €์žฅ์†Œ โ€“ ์˜คํ”ˆ์†Œ์Šค SDK, ๋„๊ตฌ ๋ฐ ์ฝ”๋“œ ์ƒ˜ํ”Œ
  • ๐ŸŒ MCP ์ปค๋ฎค๋‹ˆํ‹ฐ โ€“ ํ† ๋ก  ์ฐธ์—ฌ ๋ฐ ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ์—ฌ
  • ๐Ÿ”’ OWASP MCP Top 10 โ€“ ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์œ„ํ—˜ ์™„ํ™”
  • ๐Ÿงญ MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ํ•™์Šต ๊ฒฝ๋กœ

    ๐Ÿ“š https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail ์™„์ „ ํ•™์Šต ๊ตฌ์กฐ

    ์‹ค์Šต ์ฃผ์ œ ์„ค๋ช… ๋งํฌ -------- ------- ------------- ------ ์‹ค์Šต 1-3: ๊ธฐ์ดˆ 00 MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ์†Œ๊ฐœ

    MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ์†Œ๊ฐœ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์ž…๋ฌธ ์‹ค์Šต์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ํ†ตํ•ด Model Context Protocol (MCP) ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ๊ฐœ์š”๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail์˜ Zava Retail ๋ถ„์„ ์‚ฌ๋ก€๋ฅผ ํ†ตํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ๋ก€, ๊ธฐ์ˆ  ์•„ํ‚คํ…์ฒ˜, ์‹ค์ œ ์‘์šฉ ์‚ฌ๋ก€๋ฅผ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    Model Context Protocol (MCP)์€ AI ์–ด์‹œ์Šคํ„ดํŠธ๊ฐ€ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์•ก์„ธ์Šคํ•˜๊ณ  ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด MCP๋Š” ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์ด ํ•™์Šต ๊ฒฝ๋กœ๋Š” PostgreSQL์„ ํ†ตํ•ด AI ์–ด์‹œ์Šคํ„ดํŠธ๋ฅผ ์†Œ๋งค ํŒ๋งค ๋ฐ์ดํ„ฐ์— ์—ฐ๊ฒฐํ•˜๊ณ , Row Level Security, ์˜๋ฏธ ๊ฒ€์ƒ‰, ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค์™€ ๊ฐ™์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ฐ€๋ฅด์นฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ •์˜: Model Context Protocol๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์˜ ํ•ต์‹ฌ ์ด์ 
  • ์‹๋ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํฌํ•จํ•œ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜์˜ ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ
  • ์ดํ•ด: Zava Retail ์‚ฌ๋ก€์™€ ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ ์‚ฌํ•ญ
  • ์ธ์‹: ์•ˆ์ „ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ก์„ธ์Šค๋ฅผ ์œ„ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด
  • ๋ชฉ๋ก ์ž‘์„ฑ: ์ด ํ•™์Šต ๊ฒฝ๋กœ์—์„œ ์‚ฌ์šฉ๋œ ๋„๊ตฌ์™€ ๊ธฐ์ˆ 
  • ๐Ÿงญ ๋„์ „ ๊ณผ์ œ: AI์™€ ์‹ค์ œ ๋ฐ์ดํ„ฐ์˜ ๋งŒ๋‚จ

    ๊ธฐ์กด AI์˜ ํ•œ๊ณ„

    ํ˜„๋Œ€์˜ AI ์–ด์‹œ์Šคํ„ดํŠธ๋Š” ๋งค์šฐ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ์™€ ์ž‘์—…ํ•  ๋•Œ ์ค‘์š”ํ•œ ํ•œ๊ณ„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

    ๋„์ „ ๊ณผ์ œ ์„ค๋ช… ๋น„์ฆˆ๋‹ˆ์Šค ์˜ํ–ฅ --------------- ----------------- ------------------- ์ •์  ์ง€์‹ ๊ณ ์ •๋œ ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ํ›ˆ๋ จ๋œ AI ๋ชจ๋ธ์€ ํ˜„์žฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Œ ์˜ค๋ž˜๋œ ํ†ต์ฐฐ๋ ฅ, ๊ธฐํšŒ ์ƒ์‹ค ๋ฐ์ดํ„ฐ ์‚ฌ์ผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, API, ์‹œ์Šคํ…œ์— ์ž ๊ธด ์ •๋ณด๋กœ ์ธํ•ด AI๊ฐ€ ์ ‘๊ทผ ๋ถˆ๊ฐ€ ๋ถˆ์™„์ „ํ•œ ๋ถ„์„, ๋‹จํŽธํ™”๋œ ์›Œํฌํ”Œ๋กœ ๋ณด์•ˆ ์ œ์•ฝ ์ง์ ‘์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ก์„ธ์Šค๋Š” ๋ณด์•ˆ ๋ฐ ๊ทœ์ • ์ค€์ˆ˜ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐ ์ œํ•œ๋œ ๋ฐฐํฌ, ์ˆ˜๋™ ๋ฐ์ดํ„ฐ ์ค€๋น„ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ์ดํ„ฐ ํ†ต์ฐฐ๋ ฅ์„ ์ถ”์ถœํ•˜๋ ค๋ฉด ๊ธฐ์ˆ ์  ์ง€์‹์ด ํ•„์š” ๋‚ฎ์€ ์ฑ„ํƒ๋ฅ , ๋น„ํšจ์œจ์ ์ธ ํ”„๋กœ์„ธ์Šค

    MCP ์†”๋ฃจ์…˜

    Model Context Protocol์€ ๋‹ค์Œ์„ ํ†ตํ•ด ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค:

  • ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค: AI ์–ด์‹œ์Šคํ„ดํŠธ๊ฐ€ ๋ผ์ด๋ธŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ API๋ฅผ ์ฟผ๋ฆฌ
  • ์•ˆ์ „ํ•œ ํ†ตํ•ฉ: ์ธ์ฆ ๋ฐ ๊ถŒํ•œ์„ ํ†ตํ•œ ์ œ์–ด๋œ ์•ก์„ธ์Šค
  • ์ž์—ฐ์–ด ์ธํ„ฐํŽ˜์ด์Šค: ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ์šฉ์ž๊ฐ€ ํ‰๋ฒ”ํ•œ ์˜์–ด๋กœ ์งˆ๋ฌธ
  • ํ‘œ์ค€ํ™”๋œ ํ”„๋กœํ† ์ฝœ: ๋‹ค์–‘ํ•œ AI ํ”Œ๋žซํผ ๋ฐ ๋„๊ตฌ์—์„œ ์ž‘๋™
  • ๐Ÿช Zava Retail ์†Œ๊ฐœ: ํ•™์Šต ์‚ฌ๋ก€ ์—ฐ๊ตฌ https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail

    ์ด ํ•™์Šต ๊ฒฝ๋กœ์—์„œ๋Š” Zava Retail์ด๋ผ๋Š” ๊ฐ€์ƒ์˜ DIY ์†Œ๋งค ์ฒด์ธ์„ ์œ„ํ•œ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ˜„์‹ค์ ์ธ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ MCP ๊ตฌํ˜„์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐฐ๊ฒฝ

    Zava Retail์€ ๋‹ค์Œ์„ ์šด์˜ํ•ฉ๋‹ˆ๋‹ค:

  • ์›Œ์‹ฑํ„ด ์ฃผ ์ „์—ญ์— ๊ฑธ์นœ 8๊ฐœ์˜ ์˜คํ”„๋ผ์ธ ๋งค์žฅ (์‹œ์• ํ‹€, ๋ฒจ๋ทฐ, ํƒ€์ฝ”๋งˆ, ์Šคํฌ์บ”, ์—๋ฒ„๋ ›, ๋ ˆ๋“œ๋จผ๋“œ, ์ปคํด๋žœ๋“œ)
  • 1๊ฐœ์˜ ์˜จ๋ผ์ธ ๋งค์žฅ์„ ํ†ตํ•œ ์ „์ž์ƒ๊ฑฐ๋ž˜ ํŒ๋งค
  • ๋„๊ตฌ, ํ•˜๋“œ์›จ์–ด, ์ •์› ์šฉํ’ˆ, ๊ฑด์ถ• ์ž์žฌ๋ฅผ ํฌํ•จํ•œ ๋‹ค์–‘ํ•œ ์ œํ’ˆ ์นดํƒˆ๋กœ๊ทธ
  • ๋งค์žฅ ๊ด€๋ฆฌ์ž, ์ง€์—ญ ๊ด€๋ฆฌ์ž, ์ž„์›์„ ํฌํ•จํ•œ ๋‹ค๋‹จ๊ณ„ ๊ด€๋ฆฌ
  • ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ ์‚ฌํ•ญ

    ๋งค์žฅ ๊ด€๋ฆฌ์ž์™€ ์ž„์›์€ AI ๊ธฐ๋ฐ˜ ๋ถ„์„์„ ํ†ตํ•ด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

    1. ๋งค์žฅ ๋ฐ ๊ธฐ๊ฐ„๋ณ„ ํŒ๋งค ์„ฑ๊ณผ ๋ถ„์„

    2. ์žฌ๊ณ  ์ˆ˜์ค€ ์ถ”์  ๋ฐ ์žฌ์ž…๊ณ  ํ•„์š”์„ฑ ์‹๋ณ„

    3. ๊ณ ๊ฐ ํ–‰๋™ ๋ฐ ๊ตฌ๋งค ํŒจํ„ด ์ดํ•ด

    4. ์˜๋ฏธ ๊ฒ€์ƒ‰์„ ํ†ตํ•œ ์ œํ’ˆ ํ†ต์ฐฐ๋ ฅ ๋ฐœ๊ฒฌ

    5. ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ ๋ณด๊ณ ์„œ ์ƒ์„ฑ

    6. ์—ญํ•  ๊ธฐ๋ฐ˜ ์•ก์„ธ์Šค ์ œ์–ด๋ฅผ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ๋ณด์•ˆ ์œ ์ง€

    ๊ธฐ์ˆ  ์š”๊ตฌ ์‚ฌํ•ญ

    MCP ์„œ๋ฒ„๋Š” ๋‹ค์Œ์„ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค: ๋งค์žฅ ๊ด€๋ฆฌ์ž๊ฐ€ ์ž์‹ ์˜ ๋งค์žฅ ๋ฐ์ดํ„ฐ๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก
  • ์œ ์—ฐํ•œ ์ฟผ๋ฆฌ: ๋ณต์žกํ•œ SQL ์ž‘์—… ์ง€์›
  • ์˜๋ฏธ ๊ฒ€์ƒ‰: ์ œํ’ˆ ๊ฒ€์ƒ‰ ๋ฐ ์ถ”์ฒœ
  • ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ: ํ˜„์žฌ ๋น„์ฆˆ๋‹ˆ์Šค ์ƒํƒœ ๋ฐ˜์˜
  • ์•ˆ์ „ํ•œ ์ธ์ฆ: Row Level Security ํฌํ•จ
  • ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜: ์—ฌ๋Ÿฌ ๋™์‹œ ์‚ฌ์šฉ์ž๋ฅผ ์ง€์›
  • ๐Ÿ—๏ธ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”

    ์šฐ๋ฆฌ์˜ MCP ์„œ๋ฒ„๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์— ์ตœ์ ํ™”๋œ ๊ณ„์ธตํ˜• ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                    VS Code AI Client                       โ”‚
    
    โ”‚                  (Natural Language Queries)                โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ HTTP/SSE
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                     MCP Server                             โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚   Tool Layer    โ”‚ โ”‚  Security Layer โ”‚ โ”‚  Config Layer โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Query Tools   โ”‚ โ”‚ โ€ข RLS Context   โ”‚ โ”‚ โ€ข Environment โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Schema Tools  โ”‚ โ”‚ โ€ข User Identity โ”‚ โ”‚ โ€ข Connections โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Search Tools  โ”‚ โ”‚ โ€ข Access Controlโ”‚ โ”‚ โ€ข Validation  โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ asyncpg
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                PostgreSQL Database                         โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚  Retail Schema  โ”‚ โ”‚   RLS Policies  โ”‚ โ”‚   pgvector    โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Stores        โ”‚ โ”‚ โ€ข Store-based   โ”‚ โ”‚ โ€ข Embeddings  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Customers     โ”‚ โ”‚   Isolation     โ”‚ โ”‚ โ€ข Similarity  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Products      โ”‚ โ”‚ โ€ข Role Control  โ”‚ โ”‚   Search      โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Orders        โ”‚ โ”‚ โ€ข Audit Logs    โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ REST API
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                  Azure OpenAI                              โ”‚
    
    โ”‚               (Text Embeddings)                            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ

    1. MCP ์„œ๋ฒ„ ๊ณ„์ธต
  • FastMCP Framework: ํ˜„๋Œ€์ ์ธ Python MCP ์„œ๋ฒ„ ๊ตฌํ˜„
  • ๋„๊ตฌ ๋“ฑ๋ก: ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ฐ–์ถ˜ ์„ ์–ธ์  ๋„๊ตฌ ์ •์˜
  • ์š”์ฒญ ์ปจํ…์ŠคํŠธ: ์‚ฌ์šฉ์ž ์‹ ์› ๋ฐ ์„ธ์…˜ ๊ด€๋ฆฌ
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ๊ฐ•๋ ฅํ•œ ์˜ค๋ฅ˜ ๊ด€๋ฆฌ ๋ฐ ๋กœ๊น…
  • 2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ๊ณ„์ธต
  • ์—ฐ๊ฒฐ ํ’€๋ง: ํšจ์œจ์ ์ธ asyncpg ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • ์Šคํ‚ค๋งˆ ์ œ๊ณต์ž: ๋™์  ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ๊ฒ€์ƒ‰
  • ์ฟผ๋ฆฌ ์‹คํ–‰๊ธฐ: RLS ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ SQL ์‹คํ–‰
  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ: ACID ์ค€์ˆ˜ ๋ฐ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ
  • 3. ๋ณด์•ˆ ๊ณ„์ธต
  • Row Level Security: ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ PostgreSQL RLS
  • ์‚ฌ์šฉ์ž ์‹ ์›: ๋งค์žฅ ๊ด€๋ฆฌ์ž ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ
  • ์•ก์„ธ์Šค ์ œ์–ด: ์„ธ๋ถ„ํ™”๋œ ๊ถŒํ•œ ๋ฐ ๊ฐ์‚ฌ ๊ธฐ๋ก
  • ์ž…๋ ฅ ๊ฒ€์ฆ: SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ๋ฐ ์ฟผ๋ฆฌ ๊ฒ€์ฆ
  • 4. AI ๊ฐ•ํ™” ๊ณ„์ธต
  • ์˜๋ฏธ ๊ฒ€์ƒ‰: ์ œํ’ˆ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ
  • Azure OpenAI ํ†ตํ•ฉ: ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ
  • ์œ ์‚ฌ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜: pgvector ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰
  • ๊ฒ€์ƒ‰ ์ตœ์ ํ™”: ์ธ๋ฑ์‹ฑ ๋ฐ ์„ฑ๋Šฅ ํŠœ๋‹
  • ๐Ÿ”ง ๊ธฐ์ˆ  ์Šคํƒ

    ํ•ต์‹ฌ ๊ธฐ์ˆ 

    ๊ตฌ์„ฑ ์š”์†Œ ๊ธฐ์ˆ  ๋ชฉ์  --------------- ---------------- ------------- MCP Framework FastMCP (Python) ํ˜„๋Œ€์ ์ธ MCP ์„œ๋ฒ„ ๊ตฌํ˜„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค PostgreSQL 17 + pgvector ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ์™€ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ AI ์„œ๋น„์Šค Azure OpenAI ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ๋ฐ ์–ธ์–ด ๋ชจ๋ธ ์ปจํ…Œ์ด๋„ˆํ™” Docker + Docker Compose ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ Microsoft Azure ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ IDE ํ†ตํ•ฉ VS Code AI ์ฑ„ํŒ… ๋ฐ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ

    ๊ฐœ๋ฐœ ๋„๊ตฌ

    ๋„๊ตฌ ๋ชฉ์  ---------- ------------- asyncpg ๊ณ ์„ฑ๋Šฅ PostgreSQL ๋“œ๋ผ์ด๋ฒ„ Pydantic ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ์ง๋ ฌํ™” Azure SDK ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค ํ†ตํ•ฉ pytest ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ Docker ์ปจํ…Œ์ด๋„ˆํ™” ๋ฐ ๋ฐฐํฌ

    ํ”„๋กœ๋•์…˜ ์Šคํƒ

    ์„œ๋น„์Šค Azure ๋ฆฌ์†Œ์Šค ๋ชฉ์  ------------- ------------------- ------------- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค Azure Database for PostgreSQL ๊ด€๋ฆฌํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋น„์Šค ์ปจํ…Œ์ด๋„ˆ Azure Container Apps ์„œ๋ฒ„๋ฆฌ์Šค ์ปจํ…Œ์ด๋„ˆ ํ˜ธ์ŠคํŒ… AI ์„œ๋น„์Šค Azure AI Foundry OpenAI ๋ชจ๋ธ ๋ฐ ์—”๋“œํฌ์ธํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง Application Insights ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ ๋ฐ ์ง„๋‹จ ๋ณด์•ˆ Azure Key Vault ๋น„๋ฐ€ ๋ฐ ๊ตฌ์„ฑ ๊ด€๋ฆฌ

    ๐ŸŽฌ ์‹ค์ œ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

    ๋‹ค์–‘ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ MCP ์„œ๋ฒ„์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

    ์‹œ๋‚˜๋ฆฌ์˜ค 1: ๋งค์žฅ ๊ด€๋ฆฌ์ž ์„ฑ๊ณผ ๊ฒ€ํ† 

    ์‚ฌ์šฉ์ž: Sarah, ์‹œ์• ํ‹€ ๋งค์žฅ ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ์ง€๋‚œ ๋ถ„๊ธฐ์˜ ํŒ๋งค ์„ฑ๊ณผ ๋ถ„์„

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "2024๋…„ 4๋ถ„๊ธฐ ๋™์•ˆ ๋‚ด ๋งค์žฅ์—์„œ ๋งค์ถœ ๊ธฐ์ค€ ์ƒ์œ„ 10๊ฐœ ์ œํ’ˆ์„ ๋ณด์—ฌ์ค˜"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. VS Code AI ์ฑ„ํŒ…์ด ์ฟผ๋ฆฌ๋ฅผ MCP ์„œ๋ฒ„๋กœ ์ „์†ก

    2. MCP ์„œ๋ฒ„๊ฐ€ Sarah์˜ ๋งค์žฅ ์ปจํ…์ŠคํŠธ(์‹œ์• ํ‹€)๋ฅผ ์‹๋ณ„

    3. RLS ์ •์ฑ…์ด ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ์• ํ‹€ ๋งค์žฅ์œผ๋กœ ํ•„ํ„ฐ๋ง

    4. SQL ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์‹คํ–‰๋จ

    5. ๊ฒฐ๊ณผ๊ฐ€ ํฌ๋งท๋˜์–ด AI ์ฑ„ํŒ…์œผ๋กœ ๋ฐ˜ํ™˜

    6. AI๊ฐ€ ๋ถ„์„ ๋ฐ ํ†ต์ฐฐ๋ ฅ์„ ์ œ๊ณต

    ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์˜๋ฏธ ๊ฒ€์ƒ‰์„ ํ†ตํ•œ ์ œํ’ˆ ๋ฐœ๊ฒฌ

    ์‚ฌ์šฉ์ž: Mike, ์žฌ๊ณ  ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ๊ณ ๊ฐ ์š”์ฒญ๊ณผ ์œ ์‚ฌํ•œ ์ œํ’ˆ ์ฐพ๊ธฐ

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "์•ผ์™ธ์šฉ ๋ฐฉ์ˆ˜ ์ „๊ธฐ ์ปค๋„ฅํ„ฐ์™€ ์œ ์‚ฌํ•œ ์ œํ’ˆ์„ ์šฐ๋ฆฌ๊ฐ€ ํŒ๋งคํ•˜๋‚˜์š”?"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. ์ฟผ๋ฆฌ๊ฐ€ ์˜๋ฏธ ๊ฒ€์ƒ‰ ๋„๊ตฌ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋จ

    2. Azure OpenAI๊ฐ€ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑ

    3. pgvector๊ฐ€ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰

    4. ๊ด€๋ จ ์ œํ’ˆ์ด ๊ด€๋ จ์„ฑ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ

    5. ๊ฒฐ๊ณผ์— ์ œํ’ˆ ์„ธ๋ถ€ ์ •๋ณด์™€ ๊ฐ€์šฉ์„ฑ์ด ํฌํ•จ๋จ

    6. AI๊ฐ€ ๋Œ€์•ˆ ๋ฐ ๋ฒˆ๋“ค๋ง ๊ธฐํšŒ๋ฅผ ์ œ์•ˆ

    ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๋งค์žฅ ๊ฐ„ ๋ถ„์„

    ์‚ฌ์šฉ์ž: Jennifer, ์ง€์—ญ ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ๋ชจ๋“  ๋งค์žฅ์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํŒ๋งค ๋น„๊ต

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "์ง€๋‚œ 6๊ฐœ์›” ๋™์•ˆ ๋ชจ๋“  ๋งค์žฅ์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํŒ๋งค๋ฅผ ๋น„๊ตํ•ด์ค˜"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. RLS ์ปจํ…์ŠคํŠธ๊ฐ€ ์ง€์—ญ ๊ด€๋ฆฌ์ž ์•ก์„ธ์Šค๋กœ ์„ค์ •๋จ

    2. ๋ณต์žกํ•œ ๋‹ค์ค‘ ๋งค์žฅ ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋จ

    3. ๋ฐ์ดํ„ฐ๊ฐ€ ๋งค์žฅ ์œ„์น˜๋ณ„๋กœ ์ง‘๊ณ„๋จ

    4. ๊ฒฐ๊ณผ์— ํŠธ๋ Œ๋“œ์™€ ๋น„๊ต๊ฐ€ ํฌํ•จ๋จ

    5. AI๊ฐ€ ํ†ต์ฐฐ๋ ฅ๊ณผ ์ถ”์ฒœ์„ ์‹๋ณ„

    ๐Ÿ”’ ๋ณด์•ˆ ๋ฐ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ์‹ฌ์ธต ๋ถ„์„

    ์šฐ๋ฆฌ์˜ ๊ตฌํ˜„์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ์„ ์šฐ์„ ์‹œํ•ฉ๋‹ˆ๋‹ค:

    Row Level Security (RLS)

    PostgreSQL RLS๋Š” ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค:

    
    -- Store managers see only their store's data
    
    CREATE POLICY store_manager_policy ON retail.orders
    
      FOR ALL TO store_managers
    
      USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_policy ON retail.orders
    
      FOR ALL TO regional_managers
    
      USING (store_id = ANY(get_user_store_list()));
    
    

    ์‚ฌ์šฉ์ž ์‹ ์› ๊ด€๋ฆฌ

    ๊ฐ MCP ์—ฐ๊ฒฐ์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:

  • ๋งค์žฅ ๊ด€๋ฆฌ์ž ID: RLS ์ปจํ…์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๊ณ ์œ  ์‹๋ณ„์ž
  • ์—ญํ•  ํ• ๋‹น: ๊ถŒํ•œ ๋ฐ ์•ก์„ธ์Šค ์ˆ˜์ค€
  • ์„ธ์…˜ ๊ด€๋ฆฌ: ์•ˆ์ „ํ•œ ์ธ์ฆ ํ† ํฐ
  • ๊ฐ์‚ฌ ๋กœ๊น…: ์™„์ „ํ•œ ์•ก์„ธ์Šค ๊ธฐ๋ก
  • ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ

    ๋‹ค์ค‘ ๋ณด์•ˆ ๊ณ„์ธต:

  • ์—ฐ๊ฒฐ ์•”ํ˜ธํ™”: ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์— TLS ์‚ฌ์šฉ
  • SQL ์ธ์ ์…˜ ๋ฐฉ์ง€: ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ฟผ๋ฆฌ๋งŒ ํ—ˆ์šฉ
  • ์ž…๋ ฅ ๊ฒ€์ฆ: ํฌ๊ด„์ ์ธ ์š”์ฒญ ๊ฒ€์ฆ
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ํฌํ•จ ๊ธˆ์ง€
  • ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์†Œ๊ฐœ๋ฅผ ์™„๋ฃŒํ•œ ํ›„ ๋‹ค์Œ์„ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

    โœ… MCP ๊ฐ€์น˜ ์ œ์•ˆ: MCP๊ฐ€ AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•

    โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐฐ๊ฒฝ: Zava Retail์˜ ์š”๊ตฌ ์‚ฌํ•ญ๊ณผ ๊ณผ์ œ

    โœ… ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”: ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ์™€ ์ƒํ˜ธ์ž‘์šฉ

    โœ… ๊ธฐ์ˆ  ์Šคํƒ: ์‚ฌ์šฉ๋œ ๋„๊ตฌ์™€ ํ”„๋ ˆ์ž„์›Œํฌ

    โœ… ๋ณด์•ˆ ๋ชจ๋ธ: ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค ๋ฐ ๋ณดํ˜ธ

    โœ… ์‚ฌ์šฉ ํŒจํ„ด: ์‹ค์ œ ์ฟผ๋ฆฌ ์‹œ๋‚˜๋ฆฌ์˜ค์™€ ์›Œํฌํ”Œ๋กœ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ๋” ๊นŠ์ด ํƒ๊ตฌํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ๋‹ค์Œ์„ ์ง„ํ–‰ํ•˜์„ธ์š”:

    Lab 01: ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ๋…

    MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ์›์น™, ์†Œ๋งค ๋ถ„์„ ์†”๋ฃจ์…˜์„ ์ง€์›ํ•˜๋Š” ์ƒ์„ธ ๊ธฐ์ˆ  ๊ตฌํ˜„์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์„ธ์š”.

    ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    MCP ๋ฌธ์„œ

  • MCP ์‚ฌ์–‘ - ๊ณต์‹ ํ”„๋กœํ† ์ฝœ ๋ฌธ์„œ
  • MCP ์ดˆ๋ณด์ž์šฉ - ํฌ๊ด„์ ์ธ MCP ํ•™์Šต ๊ฐ€์ด๋“œ
  • FastMCP ๋ฌธ์„œ - Python SDK ๋ฌธ์„œ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ

  • PostgreSQL ๋ฌธ์„œ - PostgreSQL ์ฐธ์กฐ ์ž๋ฃŒ
  • pgvector ๊ฐ€์ด๋“œ - ๋ฒกํ„ฐ ํ™•์žฅ ๋ฌธ์„œ
  • Row Level Security - PostgreSQL RLS ๊ฐ€์ด๋“œ
  • Azure ์„œ๋น„์Šค

  • Azure OpenAI ๋ฌธ์„œ - AI ์„œ๋น„์Šค ํ†ตํ•ฉ
  • Azure Database for PostgreSQL - ๊ด€๋ฆฌํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋น„์Šค
  • Azure Container Apps - ์„œ๋ฒ„๋ฆฌ์Šค ์ปจํ…Œ์ด๋„ˆ
  • ---

    ๋ฉด์ฑ… ์กฐํ•ญ: ์ด๋Š” ๊ฐ€์ƒ์˜ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•™์Šต ์—ฐ์Šต์ž…๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์œ ์‚ฌํ•œ ์†”๋ฃจ์…˜์„ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ํ•ญ์ƒ ์กฐ์ง์˜ ๋ฐ์ดํ„ฐ ๊ฑฐ๋ฒ„๋„Œ์Šค ๋ฐ ๋ณด์•ˆ ์ •์ฑ…์„ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    MCP์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ, ์†Œ๋งค ๋ถ„์„ ์‚ฌ๋ก€ ๊ฐœ์š” ์‹œ์ž‘ํ•˜๊ธฐ

    MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ์†Œ๊ฐœ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์ž…๋ฌธ ์‹ค์Šต์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ํ†ตํ•ด Model Context Protocol (MCP) ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ๊ฐœ์š”๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail์˜ Zava Retail ๋ถ„์„ ์‚ฌ๋ก€๋ฅผ ํ†ตํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ๋ก€, ๊ธฐ์ˆ  ์•„ํ‚คํ…์ฒ˜, ์‹ค์ œ ์‘์šฉ ์‚ฌ๋ก€๋ฅผ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    Model Context Protocol (MCP)์€ AI ์–ด์‹œ์Šคํ„ดํŠธ๊ฐ€ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์•ก์„ธ์Šคํ•˜๊ณ  ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด MCP๋Š” ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์ด ํ•™์Šต ๊ฒฝ๋กœ๋Š” PostgreSQL์„ ํ†ตํ•ด AI ์–ด์‹œ์Šคํ„ดํŠธ๋ฅผ ์†Œ๋งค ํŒ๋งค ๋ฐ์ดํ„ฐ์— ์—ฐ๊ฒฐํ•˜๊ณ , Row Level Security, ์˜๋ฏธ ๊ฒ€์ƒ‰, ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค์™€ ๊ฐ™์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ฐ€๋ฅด์นฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ •์˜: Model Context Protocol๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์˜ ํ•ต์‹ฌ ์ด์ 
  • ์‹๋ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํฌํ•จํ•œ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜์˜ ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ
  • ์ดํ•ด: Zava Retail ์‚ฌ๋ก€์™€ ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ ์‚ฌํ•ญ
  • ์ธ์‹: ์•ˆ์ „ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ก์„ธ์Šค๋ฅผ ์œ„ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด
  • ๋ชฉ๋ก ์ž‘์„ฑ: ์ด ํ•™์Šต ๊ฒฝ๋กœ์—์„œ ์‚ฌ์šฉ๋œ ๋„๊ตฌ์™€ ๊ธฐ์ˆ 
  • ๐Ÿงญ ๋„์ „ ๊ณผ์ œ: AI์™€ ์‹ค์ œ ๋ฐ์ดํ„ฐ์˜ ๋งŒ๋‚จ

    ๊ธฐ์กด AI์˜ ํ•œ๊ณ„

    ํ˜„๋Œ€์˜ AI ์–ด์‹œ์Šคํ„ดํŠธ๋Š” ๋งค์šฐ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ์™€ ์ž‘์—…ํ•  ๋•Œ ์ค‘์š”ํ•œ ํ•œ๊ณ„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

    ๋„์ „ ๊ณผ์ œ ์„ค๋ช… ๋น„์ฆˆ๋‹ˆ์Šค ์˜ํ–ฅ --------------- ----------------- ------------------- ์ •์  ์ง€์‹ ๊ณ ์ •๋œ ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ํ›ˆ๋ จ๋œ AI ๋ชจ๋ธ์€ ํ˜„์žฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Œ ์˜ค๋ž˜๋œ ํ†ต์ฐฐ๋ ฅ, ๊ธฐํšŒ ์ƒ์‹ค ๋ฐ์ดํ„ฐ ์‚ฌ์ผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, API, ์‹œ์Šคํ…œ์— ์ž ๊ธด ์ •๋ณด๋กœ ์ธํ•ด AI๊ฐ€ ์ ‘๊ทผ ๋ถˆ๊ฐ€ ๋ถˆ์™„์ „ํ•œ ๋ถ„์„, ๋‹จํŽธํ™”๋œ ์›Œํฌํ”Œ๋กœ ๋ณด์•ˆ ์ œ์•ฝ ์ง์ ‘์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ก์„ธ์Šค๋Š” ๋ณด์•ˆ ๋ฐ ๊ทœ์ • ์ค€์ˆ˜ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐ ์ œํ•œ๋œ ๋ฐฐํฌ, ์ˆ˜๋™ ๋ฐ์ดํ„ฐ ์ค€๋น„ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ์ดํ„ฐ ํ†ต์ฐฐ๋ ฅ์„ ์ถ”์ถœํ•˜๋ ค๋ฉด ๊ธฐ์ˆ ์  ์ง€์‹์ด ํ•„์š” ๋‚ฎ์€ ์ฑ„ํƒ๋ฅ , ๋น„ํšจ์œจ์ ์ธ ํ”„๋กœ์„ธ์Šค

    MCP ์†”๋ฃจ์…˜

    Model Context Protocol์€ ๋‹ค์Œ์„ ํ†ตํ•ด ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค:

  • ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค: AI ์–ด์‹œ์Šคํ„ดํŠธ๊ฐ€ ๋ผ์ด๋ธŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ API๋ฅผ ์ฟผ๋ฆฌ
  • ์•ˆ์ „ํ•œ ํ†ตํ•ฉ: ์ธ์ฆ ๋ฐ ๊ถŒํ•œ์„ ํ†ตํ•œ ์ œ์–ด๋œ ์•ก์„ธ์Šค
  • ์ž์—ฐ์–ด ์ธํ„ฐํŽ˜์ด์Šค: ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ์šฉ์ž๊ฐ€ ํ‰๋ฒ”ํ•œ ์˜์–ด๋กœ ์งˆ๋ฌธ
  • ํ‘œ์ค€ํ™”๋œ ํ”„๋กœํ† ์ฝœ: ๋‹ค์–‘ํ•œ AI ํ”Œ๋žซํผ ๋ฐ ๋„๊ตฌ์—์„œ ์ž‘๋™
  • ๐Ÿช Zava Retail ์†Œ๊ฐœ: ํ•™์Šต ์‚ฌ๋ก€ ์—ฐ๊ตฌ https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail

    ์ด ํ•™์Šต ๊ฒฝ๋กœ์—์„œ๋Š” Zava Retail์ด๋ผ๋Š” ๊ฐ€์ƒ์˜ DIY ์†Œ๋งค ์ฒด์ธ์„ ์œ„ํ•œ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ˜„์‹ค์ ์ธ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ MCP ๊ตฌํ˜„์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐฐ๊ฒฝ

    Zava Retail์€ ๋‹ค์Œ์„ ์šด์˜ํ•ฉ๋‹ˆ๋‹ค:

  • ์›Œ์‹ฑํ„ด ์ฃผ ์ „์—ญ์— ๊ฑธ์นœ 8๊ฐœ์˜ ์˜คํ”„๋ผ์ธ ๋งค์žฅ (์‹œ์• ํ‹€, ๋ฒจ๋ทฐ, ํƒ€์ฝ”๋งˆ, ์Šคํฌ์บ”, ์—๋ฒ„๋ ›, ๋ ˆ๋“œ๋จผ๋“œ, ์ปคํด๋žœ๋“œ)
  • 1๊ฐœ์˜ ์˜จ๋ผ์ธ ๋งค์žฅ์„ ํ†ตํ•œ ์ „์ž์ƒ๊ฑฐ๋ž˜ ํŒ๋งค
  • ๋„๊ตฌ, ํ•˜๋“œ์›จ์–ด, ์ •์› ์šฉํ’ˆ, ๊ฑด์ถ• ์ž์žฌ๋ฅผ ํฌํ•จํ•œ ๋‹ค์–‘ํ•œ ์ œํ’ˆ ์นดํƒˆ๋กœ๊ทธ
  • ๋งค์žฅ ๊ด€๋ฆฌ์ž, ์ง€์—ญ ๊ด€๋ฆฌ์ž, ์ž„์›์„ ํฌํ•จํ•œ ๋‹ค๋‹จ๊ณ„ ๊ด€๋ฆฌ
  • ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ ์‚ฌํ•ญ

    ๋งค์žฅ ๊ด€๋ฆฌ์ž์™€ ์ž„์›์€ AI ๊ธฐ๋ฐ˜ ๋ถ„์„์„ ํ†ตํ•ด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

    1. ๋งค์žฅ ๋ฐ ๊ธฐ๊ฐ„๋ณ„ ํŒ๋งค ์„ฑ๊ณผ ๋ถ„์„

    2. ์žฌ๊ณ  ์ˆ˜์ค€ ์ถ”์  ๋ฐ ์žฌ์ž…๊ณ  ํ•„์š”์„ฑ ์‹๋ณ„

    3. ๊ณ ๊ฐ ํ–‰๋™ ๋ฐ ๊ตฌ๋งค ํŒจํ„ด ์ดํ•ด

    4. ์˜๋ฏธ ๊ฒ€์ƒ‰์„ ํ†ตํ•œ ์ œํ’ˆ ํ†ต์ฐฐ๋ ฅ ๋ฐœ๊ฒฌ

    5. ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ ๋ณด๊ณ ์„œ ์ƒ์„ฑ

    6. ์—ญํ•  ๊ธฐ๋ฐ˜ ์•ก์„ธ์Šค ์ œ์–ด๋ฅผ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ๋ณด์•ˆ ์œ ์ง€

    ๊ธฐ์ˆ  ์š”๊ตฌ ์‚ฌํ•ญ

    MCP ์„œ๋ฒ„๋Š” ๋‹ค์Œ์„ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค: ๋งค์žฅ ๊ด€๋ฆฌ์ž๊ฐ€ ์ž์‹ ์˜ ๋งค์žฅ ๋ฐ์ดํ„ฐ๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก
  • ์œ ์—ฐํ•œ ์ฟผ๋ฆฌ: ๋ณต์žกํ•œ SQL ์ž‘์—… ์ง€์›
  • ์˜๋ฏธ ๊ฒ€์ƒ‰: ์ œํ’ˆ ๊ฒ€์ƒ‰ ๋ฐ ์ถ”์ฒœ
  • ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ: ํ˜„์žฌ ๋น„์ฆˆ๋‹ˆ์Šค ์ƒํƒœ ๋ฐ˜์˜
  • ์•ˆ์ „ํ•œ ์ธ์ฆ: Row Level Security ํฌํ•จ
  • ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜: ์—ฌ๋Ÿฌ ๋™์‹œ ์‚ฌ์šฉ์ž๋ฅผ ์ง€์›
  • ๐Ÿ—๏ธ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”

    ์šฐ๋ฆฌ์˜ MCP ์„œ๋ฒ„๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์— ์ตœ์ ํ™”๋œ ๊ณ„์ธตํ˜• ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                    VS Code AI Client                       โ”‚
    
    โ”‚                  (Natural Language Queries)                โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ HTTP/SSE
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                     MCP Server                             โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚   Tool Layer    โ”‚ โ”‚  Security Layer โ”‚ โ”‚  Config Layer โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Query Tools   โ”‚ โ”‚ โ€ข RLS Context   โ”‚ โ”‚ โ€ข Environment โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Schema Tools  โ”‚ โ”‚ โ€ข User Identity โ”‚ โ”‚ โ€ข Connections โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Search Tools  โ”‚ โ”‚ โ€ข Access Controlโ”‚ โ”‚ โ€ข Validation  โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ asyncpg
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                PostgreSQL Database                         โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚  Retail Schema  โ”‚ โ”‚   RLS Policies  โ”‚ โ”‚   pgvector    โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Stores        โ”‚ โ”‚ โ€ข Store-based   โ”‚ โ”‚ โ€ข Embeddings  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Customers     โ”‚ โ”‚   Isolation     โ”‚ โ”‚ โ€ข Similarity  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Products      โ”‚ โ”‚ โ€ข Role Control  โ”‚ โ”‚   Search      โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Orders        โ”‚ โ”‚ โ€ข Audit Logs    โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ REST API
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                  Azure OpenAI                              โ”‚
    
    โ”‚               (Text Embeddings)                            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ

    1. MCP ์„œ๋ฒ„ ๊ณ„์ธต
  • FastMCP Framework: ํ˜„๋Œ€์ ์ธ Python MCP ์„œ๋ฒ„ ๊ตฌํ˜„
  • ๋„๊ตฌ ๋“ฑ๋ก: ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ฐ–์ถ˜ ์„ ์–ธ์  ๋„๊ตฌ ์ •์˜
  • ์š”์ฒญ ์ปจํ…์ŠคํŠธ: ์‚ฌ์šฉ์ž ์‹ ์› ๋ฐ ์„ธ์…˜ ๊ด€๋ฆฌ
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ๊ฐ•๋ ฅํ•œ ์˜ค๋ฅ˜ ๊ด€๋ฆฌ ๋ฐ ๋กœ๊น…
  • 2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ๊ณ„์ธต
  • ์—ฐ๊ฒฐ ํ’€๋ง: ํšจ์œจ์ ์ธ asyncpg ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • ์Šคํ‚ค๋งˆ ์ œ๊ณต์ž: ๋™์  ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ๊ฒ€์ƒ‰
  • ์ฟผ๋ฆฌ ์‹คํ–‰๊ธฐ: RLS ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ SQL ์‹คํ–‰
  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ: ACID ์ค€์ˆ˜ ๋ฐ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ
  • 3. ๋ณด์•ˆ ๊ณ„์ธต
  • Row Level Security: ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ PostgreSQL RLS
  • ์‚ฌ์šฉ์ž ์‹ ์›: ๋งค์žฅ ๊ด€๋ฆฌ์ž ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ
  • ์•ก์„ธ์Šค ์ œ์–ด: ์„ธ๋ถ„ํ™”๋œ ๊ถŒํ•œ ๋ฐ ๊ฐ์‚ฌ ๊ธฐ๋ก
  • ์ž…๋ ฅ ๊ฒ€์ฆ: SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ๋ฐ ์ฟผ๋ฆฌ ๊ฒ€์ฆ
  • 4. AI ๊ฐ•ํ™” ๊ณ„์ธต
  • ์˜๋ฏธ ๊ฒ€์ƒ‰: ์ œํ’ˆ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ
  • Azure OpenAI ํ†ตํ•ฉ: ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ
  • ์œ ์‚ฌ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜: pgvector ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰
  • ๊ฒ€์ƒ‰ ์ตœ์ ํ™”: ์ธ๋ฑ์‹ฑ ๋ฐ ์„ฑ๋Šฅ ํŠœ๋‹
  • ๐Ÿ”ง ๊ธฐ์ˆ  ์Šคํƒ

    ํ•ต์‹ฌ ๊ธฐ์ˆ 

    ๊ตฌ์„ฑ ์š”์†Œ ๊ธฐ์ˆ  ๋ชฉ์  --------------- ---------------- ------------- MCP Framework FastMCP (Python) ํ˜„๋Œ€์ ์ธ MCP ์„œ๋ฒ„ ๊ตฌํ˜„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค PostgreSQL 17 + pgvector ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ์™€ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ AI ์„œ๋น„์Šค Azure OpenAI ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ๋ฐ ์–ธ์–ด ๋ชจ๋ธ ์ปจํ…Œ์ด๋„ˆํ™” Docker + Docker Compose ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ Microsoft Azure ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ IDE ํ†ตํ•ฉ VS Code AI ์ฑ„ํŒ… ๋ฐ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ

    ๊ฐœ๋ฐœ ๋„๊ตฌ

    ๋„๊ตฌ ๋ชฉ์  ---------- ------------- asyncpg ๊ณ ์„ฑ๋Šฅ PostgreSQL ๋“œ๋ผ์ด๋ฒ„ Pydantic ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ์ง๋ ฌํ™” Azure SDK ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค ํ†ตํ•ฉ pytest ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ Docker ์ปจํ…Œ์ด๋„ˆํ™” ๋ฐ ๋ฐฐํฌ

    ํ”„๋กœ๋•์…˜ ์Šคํƒ

    ์„œ๋น„์Šค Azure ๋ฆฌ์†Œ์Šค ๋ชฉ์  ------------- ------------------- ------------- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค Azure Database for PostgreSQL ๊ด€๋ฆฌํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋น„์Šค ์ปจํ…Œ์ด๋„ˆ Azure Container Apps ์„œ๋ฒ„๋ฆฌ์Šค ์ปจํ…Œ์ด๋„ˆ ํ˜ธ์ŠคํŒ… AI ์„œ๋น„์Šค Azure AI Foundry OpenAI ๋ชจ๋ธ ๋ฐ ์—”๋“œํฌ์ธํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง Application Insights ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ ๋ฐ ์ง„๋‹จ ๋ณด์•ˆ Azure Key Vault ๋น„๋ฐ€ ๋ฐ ๊ตฌ์„ฑ ๊ด€๋ฆฌ

    ๐ŸŽฌ ์‹ค์ œ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

    ๋‹ค์–‘ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ MCP ์„œ๋ฒ„์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

    ์‹œ๋‚˜๋ฆฌ์˜ค 1: ๋งค์žฅ ๊ด€๋ฆฌ์ž ์„ฑ๊ณผ ๊ฒ€ํ† 

    ์‚ฌ์šฉ์ž: Sarah, ์‹œ์• ํ‹€ ๋งค์žฅ ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ์ง€๋‚œ ๋ถ„๊ธฐ์˜ ํŒ๋งค ์„ฑ๊ณผ ๋ถ„์„

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "2024๋…„ 4๋ถ„๊ธฐ ๋™์•ˆ ๋‚ด ๋งค์žฅ์—์„œ ๋งค์ถœ ๊ธฐ์ค€ ์ƒ์œ„ 10๊ฐœ ์ œํ’ˆ์„ ๋ณด์—ฌ์ค˜"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. VS Code AI ์ฑ„ํŒ…์ด ์ฟผ๋ฆฌ๋ฅผ MCP ์„œ๋ฒ„๋กœ ์ „์†ก

    2. MCP ์„œ๋ฒ„๊ฐ€ Sarah์˜ ๋งค์žฅ ์ปจํ…์ŠคํŠธ(์‹œ์• ํ‹€)๋ฅผ ์‹๋ณ„

    3. RLS ์ •์ฑ…์ด ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ์• ํ‹€ ๋งค์žฅ์œผ๋กœ ํ•„ํ„ฐ๋ง

    4. SQL ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์‹คํ–‰๋จ

    5. ๊ฒฐ๊ณผ๊ฐ€ ํฌ๋งท๋˜์–ด AI ์ฑ„ํŒ…์œผ๋กœ ๋ฐ˜ํ™˜

    6. AI๊ฐ€ ๋ถ„์„ ๋ฐ ํ†ต์ฐฐ๋ ฅ์„ ์ œ๊ณต

    ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์˜๋ฏธ ๊ฒ€์ƒ‰์„ ํ†ตํ•œ ์ œํ’ˆ ๋ฐœ๊ฒฌ

    ์‚ฌ์šฉ์ž: Mike, ์žฌ๊ณ  ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ๊ณ ๊ฐ ์š”์ฒญ๊ณผ ์œ ์‚ฌํ•œ ์ œํ’ˆ ์ฐพ๊ธฐ

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "์•ผ์™ธ์šฉ ๋ฐฉ์ˆ˜ ์ „๊ธฐ ์ปค๋„ฅํ„ฐ์™€ ์œ ์‚ฌํ•œ ์ œํ’ˆ์„ ์šฐ๋ฆฌ๊ฐ€ ํŒ๋งคํ•˜๋‚˜์š”?"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. ์ฟผ๋ฆฌ๊ฐ€ ์˜๋ฏธ ๊ฒ€์ƒ‰ ๋„๊ตฌ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋จ

    2. Azure OpenAI๊ฐ€ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑ

    3. pgvector๊ฐ€ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰

    4. ๊ด€๋ จ ์ œํ’ˆ์ด ๊ด€๋ จ์„ฑ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ

    5. ๊ฒฐ๊ณผ์— ์ œํ’ˆ ์„ธ๋ถ€ ์ •๋ณด์™€ ๊ฐ€์šฉ์„ฑ์ด ํฌํ•จ๋จ

    6. AI๊ฐ€ ๋Œ€์•ˆ ๋ฐ ๋ฒˆ๋“ค๋ง ๊ธฐํšŒ๋ฅผ ์ œ์•ˆ

    ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๋งค์žฅ ๊ฐ„ ๋ถ„์„

    ์‚ฌ์šฉ์ž: Jennifer, ์ง€์—ญ ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ๋ชจ๋“  ๋งค์žฅ์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํŒ๋งค ๋น„๊ต

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "์ง€๋‚œ 6๊ฐœ์›” ๋™์•ˆ ๋ชจ๋“  ๋งค์žฅ์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํŒ๋งค๋ฅผ ๋น„๊ตํ•ด์ค˜"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. RLS ์ปจํ…์ŠคํŠธ๊ฐ€ ์ง€์—ญ ๊ด€๋ฆฌ์ž ์•ก์„ธ์Šค๋กœ ์„ค์ •๋จ

    2. ๋ณต์žกํ•œ ๋‹ค์ค‘ ๋งค์žฅ ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋จ

    3. ๋ฐ์ดํ„ฐ๊ฐ€ ๋งค์žฅ ์œ„์น˜๋ณ„๋กœ ์ง‘๊ณ„๋จ

    4. ๊ฒฐ๊ณผ์— ํŠธ๋ Œ๋“œ์™€ ๋น„๊ต๊ฐ€ ํฌํ•จ๋จ

    5. AI๊ฐ€ ํ†ต์ฐฐ๋ ฅ๊ณผ ์ถ”์ฒœ์„ ์‹๋ณ„

    ๐Ÿ”’ ๋ณด์•ˆ ๋ฐ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ์‹ฌ์ธต ๋ถ„์„

    ์šฐ๋ฆฌ์˜ ๊ตฌํ˜„์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ์„ ์šฐ์„ ์‹œํ•ฉ๋‹ˆ๋‹ค:

    Row Level Security (RLS)

    PostgreSQL RLS๋Š” ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค:

    
    -- Store managers see only their store's data
    
    CREATE POLICY store_manager_policy ON retail.orders
    
      FOR ALL TO store_managers
    
      USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_policy ON retail.orders
    
      FOR ALL TO regional_managers
    
      USING (store_id = ANY(get_user_store_list()));
    
    

    ์‚ฌ์šฉ์ž ์‹ ์› ๊ด€๋ฆฌ

    ๊ฐ MCP ์—ฐ๊ฒฐ์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:

  • ๋งค์žฅ ๊ด€๋ฆฌ์ž ID: RLS ์ปจํ…์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๊ณ ์œ  ์‹๋ณ„์ž
  • ์—ญํ•  ํ• ๋‹น: ๊ถŒํ•œ ๋ฐ ์•ก์„ธ์Šค ์ˆ˜์ค€
  • ์„ธ์…˜ ๊ด€๋ฆฌ: ์•ˆ์ „ํ•œ ์ธ์ฆ ํ† ํฐ
  • ๊ฐ์‚ฌ ๋กœ๊น…: ์™„์ „ํ•œ ์•ก์„ธ์Šค ๊ธฐ๋ก
  • ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ

    ๋‹ค์ค‘ ๋ณด์•ˆ ๊ณ„์ธต:

  • ์—ฐ๊ฒฐ ์•”ํ˜ธํ™”: ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์— TLS ์‚ฌ์šฉ
  • SQL ์ธ์ ์…˜ ๋ฐฉ์ง€: ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ฟผ๋ฆฌ๋งŒ ํ—ˆ์šฉ
  • ์ž…๋ ฅ ๊ฒ€์ฆ: ํฌ๊ด„์ ์ธ ์š”์ฒญ ๊ฒ€์ฆ
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ํฌํ•จ ๊ธˆ์ง€
  • ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์†Œ๊ฐœ๋ฅผ ์™„๋ฃŒํ•œ ํ›„ ๋‹ค์Œ์„ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

    โœ… MCP ๊ฐ€์น˜ ์ œ์•ˆ: MCP๊ฐ€ AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•

    โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐฐ๊ฒฝ: Zava Retail์˜ ์š”๊ตฌ ์‚ฌํ•ญ๊ณผ ๊ณผ์ œ

    โœ… ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”: ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ์™€ ์ƒํ˜ธ์ž‘์šฉ

    โœ… ๊ธฐ์ˆ  ์Šคํƒ: ์‚ฌ์šฉ๋œ ๋„๊ตฌ์™€ ํ”„๋ ˆ์ž„์›Œํฌ

    โœ… ๋ณด์•ˆ ๋ชจ๋ธ: ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค ๋ฐ ๋ณดํ˜ธ

    โœ… ์‚ฌ์šฉ ํŒจํ„ด: ์‹ค์ œ ์ฟผ๋ฆฌ ์‹œ๋‚˜๋ฆฌ์˜ค์™€ ์›Œํฌํ”Œ๋กœ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ๋” ๊นŠ์ด ํƒ๊ตฌํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ๋‹ค์Œ์„ ์ง„ํ–‰ํ•˜์„ธ์š”:

    Lab 01: ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ๋…

    MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ์›์น™, ์†Œ๋งค ๋ถ„์„ ์†”๋ฃจ์…˜์„ ์ง€์›ํ•˜๋Š” ์ƒ์„ธ ๊ธฐ์ˆ  ๊ตฌํ˜„์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์„ธ์š”.

    ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    MCP ๋ฌธ์„œ

  • MCP ์‚ฌ์–‘ - ๊ณต์‹ ํ”„๋กœํ† ์ฝœ ๋ฌธ์„œ
  • MCP ์ดˆ๋ณด์ž์šฉ - ํฌ๊ด„์ ์ธ MCP ํ•™์Šต ๊ฐ€์ด๋“œ
  • FastMCP ๋ฌธ์„œ - Python SDK ๋ฌธ์„œ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ

  • PostgreSQL ๋ฌธ์„œ - PostgreSQL ์ฐธ์กฐ ์ž๋ฃŒ
  • pgvector ๊ฐ€์ด๋“œ - ๋ฒกํ„ฐ ํ™•์žฅ ๋ฌธ์„œ
  • Row Level Security - PostgreSQL RLS ๊ฐ€์ด๋“œ
  • Azure ์„œ๋น„์Šค

  • Azure OpenAI ๋ฌธ์„œ - AI ์„œ๋น„์Šค ํ†ตํ•ฉ
  • Azure Database for PostgreSQL - ๊ด€๋ฆฌํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋น„์Šค
  • Azure Container Apps - ์„œ๋ฒ„๋ฆฌ์Šค ์ปจํ…Œ์ด๋„ˆ
  • ---

    ๋ฉด์ฑ… ์กฐํ•ญ: ์ด๋Š” ๊ฐ€์ƒ์˜ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•™์Šต ์—ฐ์Šต์ž…๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์œ ์‚ฌํ•œ ์†”๋ฃจ์…˜์„ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ํ•ญ์ƒ ์กฐ์ง์˜ ๋ฐ์ดํ„ฐ ๊ฑฐ๋ฒ„๋„Œ์Šค ๋ฐ ๋ณด์•ˆ ์ •์ฑ…์„ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    01 ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ๋…

    ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ๋…

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ์›์น™, ๊ทธ๋ฆฌ๊ณ  ๊ฒฌ๊ณ ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ธฐ์ˆ ์  ์ „๋žต์— ๋Œ€ํ•œ ์‹ฌ์ธต์ ์ธ ํƒ๊ตฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ํฌํ•จํ•œ ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋ ค๋ฉด ์‹ ์ค‘ํ•œ ์•„ํ‚คํ…์ฒ˜ ๊ฒฐ์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” Zava Retail ๋ถ„์„ ์†”๋ฃจ์…˜์„ ๊ฒฌ๊ณ ํ•˜๊ณ  ์•ˆ์ „ํ•˜๋ฉฐ ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ, ์„ค๊ณ„ ํŒจํ„ด, ๊ธฐ์ˆ ์  ๊ณ ๋ ค ์‚ฌํ•ญ์„ ๋ถ„ํ•ดํ•˜์—ฌ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

    ๊ฐ ๊ณ„์ธต์ด ์–ด๋–ป๊ฒŒ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š”์ง€, ํŠน์ • ๊ธฐ์ˆ ์ด ์™œ ์„ ํƒ๋˜์—ˆ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์ด๋Ÿฌํ•œ ํŒจํ„ด์„ ์ž์‹ ์˜ MCP ๊ตฌํ˜„์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ ์ดํ•ดํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋ถ„์„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ MCP ์„œ๋ฒ„์˜ ๊ณ„์ธตํ™”๋œ ์•„ํ‚คํ…์ฒ˜ ๋ถ„์„
  • ์ดํ•ด: ๊ฐ ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์„ฑ ์š”์†Œ์˜ ์—ญํ• ๊ณผ ์ฑ…์ž„
  • ์„ค๊ณ„: ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ MCP ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ง€์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„
  • ๊ตฌํ˜„: ์—ฐ๊ฒฐ ํ’€๋ง ๋ฐ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ ์ „๋žต
  • ์ ์šฉ: ํ”„๋กœ๋•์…˜ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ๋กœ๊น… ํŒจํ„ด
  • ํ‰๊ฐ€: ๋‹ค์–‘ํ•œ ์•„ํ‚คํ…์ฒ˜ ์ ‘๊ทผ ๋ฐฉ์‹ ๊ฐ„์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ ํ‰๊ฐ€
  • ๐Ÿ—๏ธ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ๊ณ„์ธต

    ์šฐ๋ฆฌ์˜ MCP ์„œ๋ฒ„๋Š” ๊ณ„์ธตํ™”๋œ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์ด‰์ง„ํ•ฉ๋‹ˆ๋‹ค:

    ๊ณ„์ธต 1: ํ”„๋กœํ† ์ฝœ ๊ณ„์ธต (FastMCP)

    ์ฑ…์ž„: MCP ํ”„๋กœํ† ์ฝœ ํ†ต์‹  ๋ฐ ๋ฉ”์‹œ์ง€ ๋ผ์šฐํŒ… ์ฒ˜๋ฆฌ

    
    # FastMCP server setup
    
    from fastmcp import FastMCP
    
    
    
    mcp = FastMCP("Zava Retail Analytics")
    
    
    
    # Tool registration with type safety
    
    @mcp.tool()
    
    async def execute_sales_query(
    
        ctx: Context,
    
        postgresql_query: Annotated[str, Field(description="Well-formed PostgreSQL query")]
    
    ) -> str:
    
        """Execute PostgreSQL queries with Row Level Security."""
    
        return await query_executor.execute(postgresql_query, ctx)
    
    

    ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ํ”„๋กœํ† ์ฝœ ์ค€์ˆ˜: MCP ์‚ฌ์–‘ ์™„๋ฒฝ ์ง€์›
  • ํƒ€์ž… ์•ˆ์ „์„ฑ: ์š”์ฒญ/์‘๋‹ต ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•œ Pydantic ๋ชจ๋ธ
  • ๋น„๋™๊ธฐ ์ง€์›: ๋†’์€ ๋™์‹œ์„ฑ์„ ์œ„ํ•œ ๋น„์ฐจ๋‹จ I/O
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ํ‘œ์ค€ํ™”๋œ ์˜ค๋ฅ˜ ์‘๋‹ต
  • ๊ณ„์ธต 2: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ณ„์ธต

    ์ฑ…์ž„: ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ๊ตฌํ˜„ ๋ฐ ํ”„๋กœํ† ์ฝœ๊ณผ ๋ฐ์ดํ„ฐ ๊ณ„์ธต ๊ฐ„ ์กฐ์ •

    
    class SalesAnalyticsService:
    
        """Business logic for retail analytics operations."""
    
        
    
        async def get_store_performance(
    
            self, 
    
            store_id: str, 
    
            time_period: str
    
        ) -> Dict[str, Any]:
    
            """Calculate store performance metrics."""
    
            
    
            # Validate business rules
    
            if not self._validate_store_access(store_id):
    
                raise UnauthorizedError("Access denied for store")
    
            
    
            # Coordinate data retrieval
    
            sales_data = await self.db_provider.get_sales_data(store_id, time_period)
    
            metrics = self._calculate_metrics(sales_data)
    
            
    
            return {
    
                "store_id": store_id,
    
                "period": time_period,
    
                "metrics": metrics,
    
                "insights": self._generate_insights(metrics)
    
            }
    
    

    ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ์ค€์ˆ˜: ์ €์žฅ์†Œ ์ ‘๊ทผ ๊ฒ€์ฆ ๋ฐ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ
  • ์„œ๋น„์Šค ์กฐ์ •: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ AI ์„œ๋น„์Šค ๊ฐ„ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜
  • ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜: ์›์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋น„์ฆˆ๋‹ˆ์Šค ์ธ์‚ฌ์ดํŠธ๋กœ ๋ณ€ํ™˜
  • ์บ์‹ฑ ์ „๋žต: ๋นˆ๋ฒˆํ•œ ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๊ณ„์ธต 3: ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ณ„์ธต

    ์ฑ…์ž„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๊ด€๋ฆฌ, ์ฟผ๋ฆฌ ์‹คํ–‰ ๋ฐ ๋ฐ์ดํ„ฐ ๋งคํ•‘

    
    class PostgreSQLProvider:
    
        """Data access layer for PostgreSQL operations."""
    
        
    
        def __init__(self, connection_config: Dict[str, Any]):
    
            self.connection_pool: Optional[Pool] = None
    
            self.config = connection_config
    
        
    
        async def execute_query(
    
            self, 
    
            query: str, 
    
            rls_user_id: str
    
        ) -> List[Dict[str, Any]]:
    
            """Execute query with RLS context."""
    
            
    
            async with self.connection_pool.acquire() as conn:
    
                # Set RLS context
    
                await conn.execute(
    
                    "SELECT set_config('app.current_rls_user_id', $1, false)",
    
                    rls_user_id
    
                )
    
                
    
                # Execute query with timeout
    
                try:
    
                    rows = await asyncio.wait_for(
    
                        conn.fetch(query),
    
                        timeout=30.0
    
                    )
    
                    return [dict(row) for row in rows]
    
                except asyncio.TimeoutError:
    
                    raise QueryTimeoutError("Query execution exceeded timeout")
    
    

    ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ์—ฐ๊ฒฐ ํ’€๋ง: ํšจ์œจ์ ์ธ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ
  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ: ACID ์ค€์ˆ˜ ๋ฐ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ
  • ์ฟผ๋ฆฌ ์ตœ์ ํ™”: ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ตœ์ ํ™”
  • RLS ํ†ตํ•ฉ: ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ
  • ๊ณ„์ธต 4: ์ธํ”„๋ผ ๊ณ„์ธต

    ์ฑ…์ž„: ๋กœ๊น…, ๋ชจ๋‹ˆํ„ฐ๋ง, ๊ตฌ์„ฑ๊ณผ ๊ฐ™์€ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ ์ฒ˜๋ฆฌ

    
    class InfrastructureManager:
    
        """Infrastructure concerns management."""
    
        
    
        def __init__(self):
    
            self.logger = self._setup_logging()
    
            self.metrics = self._setup_metrics()
    
            self.config = self._load_configuration()
    
        
    
        def _setup_logging(self) -> Logger:
    
            """Configure structured logging."""
    
            logging.basicConfig(
    
                level=logging.INFO,
    
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    
                handlers=[
    
                    logging.StreamHandler(),
    
                    logging.FileHandler('mcp_server.log')
    
                ]
    
            )
    
            return logging.getLogger(__name__)
    
        
    
        async def track_query_execution(
    
            self, 
    
            query_type: str, 
    
            duration: float, 
    
            success: bool
    
        ):
    
            """Track query performance metrics."""
    
            self.metrics.counter('query_total').labels(
    
                type=query_type,
    
                status='success' if success else 'error'
    
            ).inc()
    
            
    
            self.metrics.histogram('query_duration').labels(
    
                type=query_type
    
            ).observe(duration)
    
    

    ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ํŒจํ„ด

    ์šฐ๋ฆฌ์˜ PostgreSQL ์Šคํ‚ค๋งˆ๋Š” ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ MCP ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ์ฃผ์š” ํŒจํ„ด์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:

    1. ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ ์Šคํ‚ค๋งˆ ์„ค๊ณ„

    
    -- Core retail entities with store-based partitioning
    
    CREATE TABLE retail.stores (
    
        store_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        name VARCHAR(100) NOT NULL,
    
        location VARCHAR(200) NOT NULL,
    
        manager_id UUID NOT NULL,
    
        created_at TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    CREATE TABLE retail.customers (
    
        customer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        store_id UUID REFERENCES retail.stores(store_id),
    
        first_name VARCHAR(50) NOT NULL,
    
        last_name VARCHAR(50) NOT NULL,
    
        email VARCHAR(100) UNIQUE,
    
        created_at TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    CREATE TABLE retail.orders (
    
        order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        customer_id UUID REFERENCES retail.customers(customer_id),
    
        store_id UUID REFERENCES retail.stores(store_id),
    
        order_date TIMESTAMP DEFAULT NOW(),
    
        total_amount DECIMAL(10,2) NOT NULL,
    
        status VARCHAR(20) DEFAULT 'pending'
    
    );
    
    

    ์„ค๊ณ„ ์›์น™:

  • ์™ธ๋ž˜ ํ‚ค ์ผ๊ด€์„ฑ: ํ…Œ์ด๋ธ” ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ
  • ์ €์žฅ์†Œ ID ์ „ํŒŒ: ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜ ํ…Œ์ด๋ธ”์— store_id ํฌํ•จ
  • UUID ๊ธฐ๋ณธ ํ‚ค: ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ์ „์—ญ์ ์œผ๋กœ ๊ณ ์œ ํ•œ ์‹๋ณ„์ž
  • ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”์ : ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ๊ฐ์‚ฌ ๊ธฐ๋ก
  • 2. ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„

    
    -- Enable RLS on multi-tenant tables
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.orders ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.order_items ENABLE ROW LEVEL SECURITY;
    
    
    
    -- Store manager can only see their store's data
    
    CREATE POLICY store_manager_customers ON retail.customers
    
        FOR ALL TO store_managers
    
        USING (store_id = get_current_user_store());
    
    
    
    CREATE POLICY store_manager_orders ON retail.orders
    
        FOR ALL TO store_managers
    
        USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_orders ON retail.orders
    
        FOR ALL TO regional_managers
    
        USING (store_id = ANY(get_user_store_list()));
    
    
    
    -- Support function for RLS context
    
    CREATE OR REPLACE FUNCTION get_current_user_store()
    
    RETURNS UUID AS $$
    
    BEGIN
    
        RETURN current_setting('app.current_rls_user_id')::UUID;
    
    EXCEPTION WHEN OTHERS THEN
    
        RETURN '00000000-0000-0000-0000-000000000000'::UUID;
    
    END;
    
    $$ LANGUAGE plpgsql SECURITY DEFINER;
    
    

    RLS์˜ ์ด์ :

  • ์ž๋™ ํ•„ํ„ฐ๋ง: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ๊ฐ•์ œ
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์ˆœํ™”: ๋ณต์žกํ•œ WHERE ์ ˆ ๋ถˆํ•„์š”
  • ๊ธฐ๋ณธ ๋ณด์•ˆ: ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์ด ๋ถˆ๊ฐ€๋Šฅ
  • ๊ฐ์‚ฌ ์ค€์ˆ˜: ๋ช…ํ™•ํ•œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฒฝ๊ณ„
  • 3. ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ์Šคํ‚ค๋งˆ

    
    -- Product embeddings for semantic search
    
    CREATE TABLE retail.product_description_embeddings (
    
        product_id UUID PRIMARY KEY REFERENCES retail.products(product_id),
    
        description_embedding vector(1536),
    
        last_updated TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    -- Optimize vector similarity search
    
    CREATE INDEX idx_product_embeddings_vector 
    
    ON retail.product_description_embeddings 
    
    USING ivfflat (description_embedding vector_cosine_ops);
    
    
    
    -- Semantic search function
    
    CREATE OR REPLACE FUNCTION search_products_by_description(
    
        query_embedding vector(1536),
    
        similarity_threshold FLOAT DEFAULT 0.7,
    
        max_results INTEGER DEFAULT 20
    
    )
    
    RETURNS TABLE(
    
        product_id UUID,
    
        name VARCHAR,
    
        description TEXT,
    
        similarity_score FLOAT
    
    ) AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            p.product_id,
    
            p.name,
    
            p.description,
    
            (1 - (pde.description_embedding <=> query_embedding)) AS similarity_score
    
        FROM retail.products p
    
        JOIN retail.product_description_embeddings pde ON p.product_id = pde.product_id
    
        WHERE (pde.description_embedding <=> query_embedding) <= (1 - similarity_threshold)
    
        ORDER BY similarity_score DESC
    
        LIMIT max_results;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    

    ๐Ÿ”Œ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ ํŒจํ„ด

    ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๊ด€๋ฆฌ๋Š” MCP ์„œ๋ฒ„ ์„ฑ๋Šฅ์— ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค:

    ์—ฐ๊ฒฐ ํ’€ ๊ตฌ์„ฑ

    
    class ConnectionPoolManager:
    
        """Manages PostgreSQL connection pools."""
    
        
    
        async def create_pool(self) -> Pool:
    
            """Create optimized connection pool."""
    
            return await asyncpg.create_pool(
    
                host=self.config.db_host,
    
                port=self.config.db_port,
    
                database=self.config.db_name,
    
                user=self.config.db_user,
    
                password=self.config.db_password,
    
                
    
                # Pool configuration
    
                min_size=2,          # Minimum connections
    
                max_size=10,         # Maximum connections
    
                max_inactive_connection_lifetime=300,  # 5 minutes
    
                
    
                # Query configuration
    
                command_timeout=30,   # Query timeout
    
                server_settings={
    
                    "application_name": "zava-mcp-server",
    
                    "jit": "off",          # Disable JIT for stability
    
                    "work_mem": "4MB",     # Limit work memory
    
                    "statement_timeout": "30s"
    
                }
    
            )
    
        
    
        async def execute_with_retry(
    
            self, 
    
            query: str, 
    
            params: Tuple = None,
    
            max_retries: int = 3
    
        ) -> List[Dict[str, Any]]:
    
            """Execute query with automatic retry logic."""
    
            
    
            for attempt in range(max_retries):
    
                try:
    
                    async with self.pool.acquire() as conn:
    
                        if params:
    
                            rows = await conn.fetch(query, *params)
    
                        else:
    
                            rows = await conn.fetch(query)
    
                        return [dict(row) for row in rows]
    
                        
    
                except (ConnectionError, InterfaceError) as e:
    
                    if attempt == max_retries - 1:
    
                        raise
    
                    
    
                    # Exponential backoff
    
                    await asyncio.sleep(2 ** attempt)
    
                    logger.warning(f"Database connection failed, retrying ({attempt + 1}/{max_retries})")
    
    

    ๋ฆฌ์†Œ์Šค ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ

    
    class MCPServerManager:
    
        """Manages MCP server lifecycle and resources."""
    
        
    
        async def startup(self):
    
            """Initialize server resources."""
    
            # Create database connection pool
    
            self.db_pool = await self.pool_manager.create_pool()
    
            
    
            # Initialize AI services
    
            self.ai_client = await self.create_ai_client()
    
            
    
            # Setup monitoring
    
            self.metrics_collector = MetricsCollector()
    
            
    
            logger.info("MCP server startup complete")
    
        
    
        async def shutdown(self):
    
            """Cleanup server resources."""
    
            try:
    
                # Close database connections
    
                if self.db_pool:
    
                    await self.db_pool.close()
    
                
    
                # Cleanup AI client
    
                if self.ai_client:
    
                    await self.ai_client.close()
    
                
    
                # Flush metrics
    
                await self.metrics_collector.flush()
    
                
    
                logger.info("MCP server shutdown complete")
    
                
    
            except Exception as e:
    
                logger.error(f"Error during shutdown: {e}")
    
        
    
        async def health_check(self) -> Dict[str, str]:
    
            """Verify server health status."""
    
            status = {}
    
            
    
            # Check database connection
    
            try:
    
                async with self.db_pool.acquire() as conn:
    
                    await conn.fetchval("SELECT 1")
    
                status["database"] = "healthy"
    
            except Exception as e:
    
                status["database"] = f"unhealthy: {e}"
    
            
    
            # Check AI service
    
            try:
    
                await self.ai_client.health_check()
    
                status["ai_service"] = "healthy"
    
            except Exception as e:
    
                status["ai_service"] = f"unhealthy: {e}"
    
            
    
            return status
    
    

    ๐Ÿ›ก๏ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ๋ณต์›๋ ฅ ํŒจํ„ด

    ๊ฒฌ๊ณ ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋Š” MCP ์„œ๋ฒ„์˜ ์•ˆ์ •์ ์ธ ์šด์˜์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค:

    ๊ณ„์ธต์  ์˜ค๋ฅ˜ ์œ ํ˜•

    
    class MCPError(Exception):
    
        """Base MCP server error."""
    
        def __init__(self, message: str, error_code: str = "MCP_ERROR"):
    
            self.message = message
    
            self.error_code = error_code
    
            super().__init__(message)
    
    
    
    class DatabaseError(MCPError):
    
        """Database operation errors."""
    
        def __init__(self, message: str, query: str = None):
    
            super().__init__(message, "DATABASE_ERROR")
    
            self.query = query
    
    
    
    class AuthorizationError(MCPError):
    
        """Access control errors."""
    
        def __init__(self, message: str, user_id: str = None):
    
            super().__init__(message, "AUTHORIZATION_ERROR")
    
            self.user_id = user_id
    
    
    
    class QueryTimeoutError(DatabaseError):
    
        """Query execution timeout."""
    
        def __init__(self, query: str):
    
            super().__init__(f"Query timeout: {query[:100]}...", query)
    
            self.error_code = "QUERY_TIMEOUT"
    
    
    
    class ValidationError(MCPError):
    
        """Input validation errors."""
    
        def __init__(self, field: str, value: Any, constraint: str):
    
            message = f"Validation failed for {field}: {constraint}"
    
            super().__init__(message, "VALIDATION_ERROR")
    
            self.field = field
    
            self.value = value
    
    

    ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฏธ๋“ค์›จ์–ด

    
    @contextmanager
    
    async def error_handling_context(operation_name: str, user_id: str = None):
    
        """Centralized error handling for operations."""
    
        start_time = time.time()
    
        
    
        try:
    
            yield
    
            
    
            # Success metrics
    
            duration = time.time() - start_time
    
            metrics.operation_success.labels(operation=operation_name).inc()
    
            metrics.operation_duration.labels(operation=operation_name).observe(duration)
    
            
    
        except ValidationError as e:
    
            logger.warning(f"Validation error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "validation",
    
                "field": e.field
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="validation").inc()
    
            raise
    
            
    
        except AuthorizationError as e:
    
            logger.warning(f"Authorization error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "authorization"
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="authorization").inc()
    
            raise
    
            
    
        except DatabaseError as e:
    
            logger.error(f"Database error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "database",
    
                "query": e.query[:100] if e.query else None
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="database").inc()
    
            raise
    
            
    
        except Exception as e:
    
            logger.error(f"Unexpected error in {operation_name}: {str(e)}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "unexpected"
    
            }, exc_info=True)
    
            metrics.operation_error.labels(operation=operation_name, type="unexpected").inc()
    
            raise MCPError(f"Internal server error in {operation_name}")
    
    

    ๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

    ์ฟผ๋ฆฌ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

    
    class QueryPerformanceMonitor:
    
        """Monitor and optimize query performance."""
    
        
    
        def __init__(self):
    
            self.slow_query_threshold = 1.0  # seconds
    
            self.query_stats = defaultdict(list)
    
        
    
        @contextmanager
    
        async def monitor_query(self, query: str, operation_type: str = "unknown"):
    
            """Monitor query execution time and performance."""
    
            start_time = time.time()
    
            query_hash = hashlib.md5(query.encode()).hexdigest()[:8]
    
            
    
            try:
    
                yield
    
                
    
                duration = time.time() - start_time
    
                
    
                # Record performance metrics
    
                self.query_stats[operation_type].append(duration)
    
                
    
                # Log slow queries
    
                if duration > self.slow_query_threshold:
    
                    logger.warning(f"Slow query detected", extra={
    
                        "query_hash": query_hash,
    
                        "duration": duration,
    
                        "operation_type": operation_type,
    
                        "query": query[:200]
    
                    })
    
                
    
                # Update metrics
    
                metrics.query_duration.labels(type=operation_type).observe(duration)
    
                
    
            except Exception as e:
    
                duration = time.time() - start_time
    
                logger.error(f"Query failed", extra={
    
                    "query_hash": query_hash,
    
                    "duration": duration,
    
                    "operation_type": operation_type,
    
                    "error": str(e)
    
                })
    
                raise
    
        
    
        def get_performance_summary(self) -> Dict[str, Any]:
    
            """Generate performance summary report."""
    
            summary = {}
    
            
    
            for operation_type, durations in self.query_stats.items():
    
                if durations:
    
                    summary[operation_type] = {
    
                        "count": len(durations),
    
                        "avg_duration": sum(durations) / len(durations),
    
                        "max_duration": max(durations),
    
                        "min_duration": min(durations),
    
                        "slow_queries": len([d for d in durations if d > self.slow_query_threshold])
    
                    }
    
            
    
            return summary
    
    

    ์บ์‹ฑ ์ „๋žต

    
    class QueryCache:
    
        """Intelligent query result caching."""
    
        
    
        def __init__(self, redis_url: str = None):
    
            self.cache = {}  # In-memory fallback
    
            self.redis_client = redis.Redis.from_url(redis_url) if redis_url else None
    
            self.cache_ttl = 300  # 5 minutes default
    
        
    
        async def get_cached_result(
    
            self, 
    
            cache_key: str, 
    
            query_func: Callable,
    
            ttl: int = None
    
        ) -> Any:
    
            """Get result from cache or execute query."""
    
            ttl = ttl or self.cache_ttl
    
            
    
            # Try cache first
    
            cached_result = await self._get_from_cache(cache_key)
    
            if cached_result is not None:
    
                metrics.cache_hit.labels(type="query").inc()
    
                return cached_result
    
            
    
            # Execute query
    
            metrics.cache_miss.labels(type="query").inc()
    
            result = await query_func()
    
            
    
            # Cache result
    
            await self._set_in_cache(cache_key, result, ttl)
    
            
    
            return result
    
        
    
        def _generate_cache_key(self, query: str, user_context: str) -> str:
    
            """Generate consistent cache key."""
    
            key_data = f"{query}:{user_context}"
    
            return hashlib.sha256(key_data.encode()).hexdigest()
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

    โœ… ๊ณ„์ธตํ™”๋œ ์•„ํ‚คํ…์ฒ˜: MCP ์„œ๋ฒ„ ์„ค๊ณ„์—์„œ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•

    โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒจํ„ด: ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ ์Šคํ‚ค๋งˆ ์„ค๊ณ„ ๋ฐ RLS ๊ตฌํ˜„

    โœ… ์—ฐ๊ฒฐ ๊ด€๋ฆฌ: ํšจ์œจ์ ์ธ ํ’€๋ง ๋ฐ ๋ฆฌ์†Œ์Šค ๋ผ์ดํ”„์‚ฌ์ดํด

    โœ… ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ๊ณ„์ธต์  ์˜ค๋ฅ˜ ์œ ํ˜• ๋ฐ ๋ณต์›๋ ฅ ํŒจํ„ด

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ชจ๋‹ˆํ„ฐ๋ง, ์บ์‹ฑ ๋ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™”

    โœ… ํ”„๋กœ๋•์…˜ ์ค€๋น„: ์ธํ”„๋ผ ๊ด€์‹ฌ์‚ฌ ๋ฐ ์šด์˜ ํŒจํ„ด

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 02: ๋ณด์•ˆ ๋ฐ ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ๋กœ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ ๋‹ค์Œ์„ ์‹ฌ์ธต์ ์œผ๋กœ ํƒ๊ตฌํ•˜์„ธ์š”:

  • ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ
  • ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ ํŒจํ„ด
  • ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ์ „๋žต
  • ๋ณด์•ˆ ๊ฐ์‚ฌ ๋ฐ ์ค€์ˆ˜ ๊ณ ๋ ค ์‚ฌํ•ญ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด

  • Python์—์„œ์˜ ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜ - Python ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ํŒจํ„ด - ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ์›์น™
  • ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํŒจํ„ด - ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • PostgreSQL ๊ณ ๊ธ‰ ์ฃผ์ œ

  • PostgreSQL ์„ฑ๋Šฅ ํŠœ๋‹ - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™” ๊ฐ€์ด๋“œ
  • ์—ฐ๊ฒฐ ํ’€๋ง ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • ์ฟผ๋ฆฌ ๊ณ„ํš ๋ฐ ์ตœ์ ํ™” - ์ฟผ๋ฆฌ ์„ฑ๋Šฅ
  • Python ๋น„๋™๊ธฐ ํŒจํ„ด

  • AsyncIO ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจํ„ด
  • FastAPI ์•„ํ‚คํ…์ฒ˜ - ํ˜„๋Œ€์ ์ธ Python ์›น ์•„ํ‚คํ…์ฒ˜
  • Pydantic ๋ชจ๋ธ - ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ์ง๋ ฌํ™”
  • ---

    ๋‹ค์Œ: ๋ณด์•ˆ ํŒจํ„ด์„ ํƒ๊ตฌํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? Lab 02: ๋ณด์•ˆ ๋ฐ ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ๋กœ ๊ณ„์† ์ง„ํ–‰ํ•˜์„ธ์š”.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณ„์ธต, ๋ณด์•ˆ ํŒจํ„ด ์ดํ•ด ํ•™์Šตํ•˜๊ธฐ

    ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ๋…

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ์›์น™, ๊ทธ๋ฆฌ๊ณ  ๊ฒฌ๊ณ ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌํ˜„ํ•˜๋Š” ๊ธฐ์ˆ ์  ์ „๋žต์— ๋Œ€ํ•œ ์‹ฌ์ธต์ ์ธ ํƒ๊ตฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ํฌํ•จํ•œ ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋ ค๋ฉด ์‹ ์ค‘ํ•œ ์•„ํ‚คํ…์ฒ˜ ๊ฒฐ์ •์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” Zava Retail ๋ถ„์„ ์†”๋ฃจ์…˜์„ ๊ฒฌ๊ณ ํ•˜๊ณ  ์•ˆ์ „ํ•˜๋ฉฐ ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ํ•ต์‹ฌ ๊ตฌ์„ฑ ์š”์†Œ, ์„ค๊ณ„ ํŒจํ„ด, ๊ธฐ์ˆ ์  ๊ณ ๋ ค ์‚ฌํ•ญ์„ ๋ถ„ํ•ดํ•˜์—ฌ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

    ๊ฐ ๊ณ„์ธต์ด ์–ด๋–ป๊ฒŒ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š”์ง€, ํŠน์ • ๊ธฐ์ˆ ์ด ์™œ ์„ ํƒ๋˜์—ˆ๋Š”์ง€, ๊ทธ๋ฆฌ๊ณ  ์ด๋Ÿฌํ•œ ํŒจํ„ด์„ ์ž์‹ ์˜ MCP ๊ตฌํ˜„์— ์–ด๋–ป๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€๋ฅผ ์ดํ•ดํ•˜๊ฒŒ ๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๋ถ„์„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ MCP ์„œ๋ฒ„์˜ ๊ณ„์ธตํ™”๋œ ์•„ํ‚คํ…์ฒ˜ ๋ถ„์„
  • ์ดํ•ด: ๊ฐ ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์„ฑ ์š”์†Œ์˜ ์—ญํ• ๊ณผ ์ฑ…์ž„
  • ์„ค๊ณ„: ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ MCP ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์ง€์›ํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„
  • ๊ตฌํ˜„: ์—ฐ๊ฒฐ ํ’€๋ง ๋ฐ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ ์ „๋žต
  • ์ ์šฉ: ํ”„๋กœ๋•์…˜ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ๋กœ๊น… ํŒจํ„ด
  • ํ‰๊ฐ€: ๋‹ค์–‘ํ•œ ์•„ํ‚คํ…์ฒ˜ ์ ‘๊ทผ ๋ฐฉ์‹ ๊ฐ„์˜ ํŠธ๋ ˆ์ด๋“œ์˜คํ”„ ํ‰๊ฐ€
  • ๐Ÿ—๏ธ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ๊ณ„์ธต

    ์šฐ๋ฆฌ์˜ MCP ์„œ๋ฒ„๋Š” ๊ณ„์ธตํ™”๋œ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•˜์—ฌ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜๋ฅผ ์ด‰์ง„ํ•ฉ๋‹ˆ๋‹ค:

    ๊ณ„์ธต 1: ํ”„๋กœํ† ์ฝœ ๊ณ„์ธต (FastMCP)

    ์ฑ…์ž„: MCP ํ”„๋กœํ† ์ฝœ ํ†ต์‹  ๋ฐ ๋ฉ”์‹œ์ง€ ๋ผ์šฐํŒ… ์ฒ˜๋ฆฌ

    
    # FastMCP server setup
    
    from fastmcp import FastMCP
    
    
    
    mcp = FastMCP("Zava Retail Analytics")
    
    
    
    # Tool registration with type safety
    
    @mcp.tool()
    
    async def execute_sales_query(
    
        ctx: Context,
    
        postgresql_query: Annotated[str, Field(description="Well-formed PostgreSQL query")]
    
    ) -> str:
    
        """Execute PostgreSQL queries with Row Level Security."""
    
        return await query_executor.execute(postgresql_query, ctx)
    
    

    ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ํ”„๋กœํ† ์ฝœ ์ค€์ˆ˜: MCP ์‚ฌ์–‘ ์™„๋ฒฝ ์ง€์›
  • ํƒ€์ž… ์•ˆ์ „์„ฑ: ์š”์ฒญ/์‘๋‹ต ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•œ Pydantic ๋ชจ๋ธ
  • ๋น„๋™๊ธฐ ์ง€์›: ๋†’์€ ๋™์‹œ์„ฑ์„ ์œ„ํ•œ ๋น„์ฐจ๋‹จ I/O
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ํ‘œ์ค€ํ™”๋œ ์˜ค๋ฅ˜ ์‘๋‹ต
  • ๊ณ„์ธต 2: ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ๊ณ„์ธต

    ์ฑ…์ž„: ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ๊ตฌํ˜„ ๋ฐ ํ”„๋กœํ† ์ฝœ๊ณผ ๋ฐ์ดํ„ฐ ๊ณ„์ธต ๊ฐ„ ์กฐ์ •

    
    class SalesAnalyticsService:
    
        """Business logic for retail analytics operations."""
    
        
    
        async def get_store_performance(
    
            self, 
    
            store_id: str, 
    
            time_period: str
    
        ) -> Dict[str, Any]:
    
            """Calculate store performance metrics."""
    
            
    
            # Validate business rules
    
            if not self._validate_store_access(store_id):
    
                raise UnauthorizedError("Access denied for store")
    
            
    
            # Coordinate data retrieval
    
            sales_data = await self.db_provider.get_sales_data(store_id, time_period)
    
            metrics = self._calculate_metrics(sales_data)
    
            
    
            return {
    
                "store_id": store_id,
    
                "period": time_period,
    
                "metrics": metrics,
    
                "insights": self._generate_insights(metrics)
    
            }
    
    

    ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ์ค€์ˆ˜: ์ €์žฅ์†Œ ์ ‘๊ทผ ๊ฒ€์ฆ ๋ฐ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ
  • ์„œ๋น„์Šค ์กฐ์ •: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ AI ์„œ๋น„์Šค ๊ฐ„ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜
  • ๋ฐ์ดํ„ฐ ๋ณ€ํ™˜: ์›์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋น„์ฆˆ๋‹ˆ์Šค ์ธ์‚ฌ์ดํŠธ๋กœ ๋ณ€ํ™˜
  • ์บ์‹ฑ ์ „๋žต: ๋นˆ๋ฒˆํ•œ ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๊ณ„์ธต 3: ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ณ„์ธต

    ์ฑ…์ž„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๊ด€๋ฆฌ, ์ฟผ๋ฆฌ ์‹คํ–‰ ๋ฐ ๋ฐ์ดํ„ฐ ๋งคํ•‘

    
    class PostgreSQLProvider:
    
        """Data access layer for PostgreSQL operations."""
    
        
    
        def __init__(self, connection_config: Dict[str, Any]):
    
            self.connection_pool: Optional[Pool] = None
    
            self.config = connection_config
    
        
    
        async def execute_query(
    
            self, 
    
            query: str, 
    
            rls_user_id: str
    
        ) -> List[Dict[str, Any]]:
    
            """Execute query with RLS context."""
    
            
    
            async with self.connection_pool.acquire() as conn:
    
                # Set RLS context
    
                await conn.execute(
    
                    "SELECT set_config('app.current_rls_user_id', $1, false)",
    
                    rls_user_id
    
                )
    
                
    
                # Execute query with timeout
    
                try:
    
                    rows = await asyncio.wait_for(
    
                        conn.fetch(query),
    
                        timeout=30.0
    
                    )
    
                    return [dict(row) for row in rows]
    
                except asyncio.TimeoutError:
    
                    raise QueryTimeoutError("Query execution exceeded timeout")
    
    

    ์ฃผ์š” ๊ธฐ๋Šฅ:

  • ์—ฐ๊ฒฐ ํ’€๋ง: ํšจ์œจ์ ์ธ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ
  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ: ACID ์ค€์ˆ˜ ๋ฐ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ
  • ์ฟผ๋ฆฌ ์ตœ์ ํ™”: ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ตœ์ ํ™”
  • RLS ํ†ตํ•ฉ: ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ
  • ๊ณ„์ธต 4: ์ธํ”„๋ผ ๊ณ„์ธต

    ์ฑ…์ž„: ๋กœ๊น…, ๋ชจ๋‹ˆํ„ฐ๋ง, ๊ตฌ์„ฑ๊ณผ ๊ฐ™์€ ํšก๋‹จ ๊ด€์‹ฌ์‚ฌ ์ฒ˜๋ฆฌ

    
    class InfrastructureManager:
    
        """Infrastructure concerns management."""
    
        
    
        def __init__(self):
    
            self.logger = self._setup_logging()
    
            self.metrics = self._setup_metrics()
    
            self.config = self._load_configuration()
    
        
    
        def _setup_logging(self) -> Logger:
    
            """Configure structured logging."""
    
            logging.basicConfig(
    
                level=logging.INFO,
    
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    
                handlers=[
    
                    logging.StreamHandler(),
    
                    logging.FileHandler('mcp_server.log')
    
                ]
    
            )
    
            return logging.getLogger(__name__)
    
        
    
        async def track_query_execution(
    
            self, 
    
            query_type: str, 
    
            duration: float, 
    
            success: bool
    
        ):
    
            """Track query performance metrics."""
    
            self.metrics.counter('query_total').labels(
    
                type=query_type,
    
                status='success' if success else 'error'
    
            ).inc()
    
            
    
            self.metrics.histogram('query_duration').labels(
    
                type=query_type
    
            ).observe(duration)
    
    

    ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ํŒจํ„ด

    ์šฐ๋ฆฌ์˜ PostgreSQL ์Šคํ‚ค๋งˆ๋Š” ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ MCP ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๋ช‡ ๊ฐ€์ง€ ์ฃผ์š” ํŒจํ„ด์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:

    1. ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ ์Šคํ‚ค๋งˆ ์„ค๊ณ„

    
    -- Core retail entities with store-based partitioning
    
    CREATE TABLE retail.stores (
    
        store_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        name VARCHAR(100) NOT NULL,
    
        location VARCHAR(200) NOT NULL,
    
        manager_id UUID NOT NULL,
    
        created_at TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    CREATE TABLE retail.customers (
    
        customer_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        store_id UUID REFERENCES retail.stores(store_id),
    
        first_name VARCHAR(50) NOT NULL,
    
        last_name VARCHAR(50) NOT NULL,
    
        email VARCHAR(100) UNIQUE,
    
        created_at TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    CREATE TABLE retail.orders (
    
        order_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    
        customer_id UUID REFERENCES retail.customers(customer_id),
    
        store_id UUID REFERENCES retail.stores(store_id),
    
        order_date TIMESTAMP DEFAULT NOW(),
    
        total_amount DECIMAL(10,2) NOT NULL,
    
        status VARCHAR(20) DEFAULT 'pending'
    
    );
    
    

    ์„ค๊ณ„ ์›์น™:

  • ์™ธ๋ž˜ ํ‚ค ์ผ๊ด€์„ฑ: ํ…Œ์ด๋ธ” ๊ฐ„ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ๋ณด์žฅ
  • ์ €์žฅ์†Œ ID ์ „ํŒŒ: ๋ชจ๋“  ํŠธ๋žœ์žญ์…˜ ํ…Œ์ด๋ธ”์— store_id ํฌํ•จ
  • UUID ๊ธฐ๋ณธ ํ‚ค: ๋ถ„์‚ฐ ์‹œ์Šคํ…œ์„ ์œ„ํ•œ ์ „์—ญ์ ์œผ๋กœ ๊ณ ์œ ํ•œ ์‹๋ณ„์ž
  • ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”์ : ๋ชจ๋“  ๋ฐ์ดํ„ฐ ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ๊ฐ์‚ฌ ๊ธฐ๋ก
  • 2. ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„

    
    -- Enable RLS on multi-tenant tables
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.orders ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.order_items ENABLE ROW LEVEL SECURITY;
    
    
    
    -- Store manager can only see their store's data
    
    CREATE POLICY store_manager_customers ON retail.customers
    
        FOR ALL TO store_managers
    
        USING (store_id = get_current_user_store());
    
    
    
    CREATE POLICY store_manager_orders ON retail.orders
    
        FOR ALL TO store_managers
    
        USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_orders ON retail.orders
    
        FOR ALL TO regional_managers
    
        USING (store_id = ANY(get_user_store_list()));
    
    
    
    -- Support function for RLS context
    
    CREATE OR REPLACE FUNCTION get_current_user_store()
    
    RETURNS UUID AS $$
    
    BEGIN
    
        RETURN current_setting('app.current_rls_user_id')::UUID;
    
    EXCEPTION WHEN OTHERS THEN
    
        RETURN '00000000-0000-0000-0000-000000000000'::UUID;
    
    END;
    
    $$ LANGUAGE plpgsql SECURITY DEFINER;
    
    

    RLS์˜ ์ด์ :

  • ์ž๋™ ํ•„ํ„ฐ๋ง: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๊ฐ€ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ๊ฐ•์ œ
  • ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‹จ์ˆœํ™”: ๋ณต์žกํ•œ WHERE ์ ˆ ๋ถˆํ•„์š”
  • ๊ธฐ๋ณธ ๋ณด์•ˆ: ์ž˜๋ชป๋œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ์ด ๋ถˆ๊ฐ€๋Šฅ
  • ๊ฐ์‚ฌ ์ค€์ˆ˜: ๋ช…ํ™•ํ•œ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๊ฒฝ๊ณ„
  • 3. ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ์Šคํ‚ค๋งˆ

    
    -- Product embeddings for semantic search
    
    CREATE TABLE retail.product_description_embeddings (
    
        product_id UUID PRIMARY KEY REFERENCES retail.products(product_id),
    
        description_embedding vector(1536),
    
        last_updated TIMESTAMP DEFAULT NOW()
    
    );
    
    
    
    -- Optimize vector similarity search
    
    CREATE INDEX idx_product_embeddings_vector 
    
    ON retail.product_description_embeddings 
    
    USING ivfflat (description_embedding vector_cosine_ops);
    
    
    
    -- Semantic search function
    
    CREATE OR REPLACE FUNCTION search_products_by_description(
    
        query_embedding vector(1536),
    
        similarity_threshold FLOAT DEFAULT 0.7,
    
        max_results INTEGER DEFAULT 20
    
    )
    
    RETURNS TABLE(
    
        product_id UUID,
    
        name VARCHAR,
    
        description TEXT,
    
        similarity_score FLOAT
    
    ) AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            p.product_id,
    
            p.name,
    
            p.description,
    
            (1 - (pde.description_embedding <=> query_embedding)) AS similarity_score
    
        FROM retail.products p
    
        JOIN retail.product_description_embeddings pde ON p.product_id = pde.product_id
    
        WHERE (pde.description_embedding <=> query_embedding) <= (1 - similarity_threshold)
    
        ORDER BY similarity_score DESC
    
        LIMIT max_results;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    

    ๐Ÿ”Œ ์—ฐ๊ฒฐ ๊ด€๋ฆฌ ํŒจํ„ด

    ํšจ์œจ์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๊ด€๋ฆฌ๋Š” MCP ์„œ๋ฒ„ ์„ฑ๋Šฅ์— ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค:

    ์—ฐ๊ฒฐ ํ’€ ๊ตฌ์„ฑ

    
    class ConnectionPoolManager:
    
        """Manages PostgreSQL connection pools."""
    
        
    
        async def create_pool(self) -> Pool:
    
            """Create optimized connection pool."""
    
            return await asyncpg.create_pool(
    
                host=self.config.db_host,
    
                port=self.config.db_port,
    
                database=self.config.db_name,
    
                user=self.config.db_user,
    
                password=self.config.db_password,
    
                
    
                # Pool configuration
    
                min_size=2,          # Minimum connections
    
                max_size=10,         # Maximum connections
    
                max_inactive_connection_lifetime=300,  # 5 minutes
    
                
    
                # Query configuration
    
                command_timeout=30,   # Query timeout
    
                server_settings={
    
                    "application_name": "zava-mcp-server",
    
                    "jit": "off",          # Disable JIT for stability
    
                    "work_mem": "4MB",     # Limit work memory
    
                    "statement_timeout": "30s"
    
                }
    
            )
    
        
    
        async def execute_with_retry(
    
            self, 
    
            query: str, 
    
            params: Tuple = None,
    
            max_retries: int = 3
    
        ) -> List[Dict[str, Any]]:
    
            """Execute query with automatic retry logic."""
    
            
    
            for attempt in range(max_retries):
    
                try:
    
                    async with self.pool.acquire() as conn:
    
                        if params:
    
                            rows = await conn.fetch(query, *params)
    
                        else:
    
                            rows = await conn.fetch(query)
    
                        return [dict(row) for row in rows]
    
                        
    
                except (ConnectionError, InterfaceError) as e:
    
                    if attempt == max_retries - 1:
    
                        raise
    
                    
    
                    # Exponential backoff
    
                    await asyncio.sleep(2 ** attempt)
    
                    logger.warning(f"Database connection failed, retrying ({attempt + 1}/{max_retries})")
    
    

    ๋ฆฌ์†Œ์Šค ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ

    
    class MCPServerManager:
    
        """Manages MCP server lifecycle and resources."""
    
        
    
        async def startup(self):
    
            """Initialize server resources."""
    
            # Create database connection pool
    
            self.db_pool = await self.pool_manager.create_pool()
    
            
    
            # Initialize AI services
    
            self.ai_client = await self.create_ai_client()
    
            
    
            # Setup monitoring
    
            self.metrics_collector = MetricsCollector()
    
            
    
            logger.info("MCP server startup complete")
    
        
    
        async def shutdown(self):
    
            """Cleanup server resources."""
    
            try:
    
                # Close database connections
    
                if self.db_pool:
    
                    await self.db_pool.close()
    
                
    
                # Cleanup AI client
    
                if self.ai_client:
    
                    await self.ai_client.close()
    
                
    
                # Flush metrics
    
                await self.metrics_collector.flush()
    
                
    
                logger.info("MCP server shutdown complete")
    
                
    
            except Exception as e:
    
                logger.error(f"Error during shutdown: {e}")
    
        
    
        async def health_check(self) -> Dict[str, str]:
    
            """Verify server health status."""
    
            status = {}
    
            
    
            # Check database connection
    
            try:
    
                async with self.db_pool.acquire() as conn:
    
                    await conn.fetchval("SELECT 1")
    
                status["database"] = "healthy"
    
            except Exception as e:
    
                status["database"] = f"unhealthy: {e}"
    
            
    
            # Check AI service
    
            try:
    
                await self.ai_client.health_check()
    
                status["ai_service"] = "healthy"
    
            except Exception as e:
    
                status["ai_service"] = f"unhealthy: {e}"
    
            
    
            return status
    
    

    ๐Ÿ›ก๏ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ๋ณต์›๋ ฅ ํŒจํ„ด

    ๊ฒฌ๊ณ ํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋Š” MCP ์„œ๋ฒ„์˜ ์•ˆ์ •์ ์ธ ์šด์˜์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค:

    ๊ณ„์ธต์  ์˜ค๋ฅ˜ ์œ ํ˜•

    
    class MCPError(Exception):
    
        """Base MCP server error."""
    
        def __init__(self, message: str, error_code: str = "MCP_ERROR"):
    
            self.message = message
    
            self.error_code = error_code
    
            super().__init__(message)
    
    
    
    class DatabaseError(MCPError):
    
        """Database operation errors."""
    
        def __init__(self, message: str, query: str = None):
    
            super().__init__(message, "DATABASE_ERROR")
    
            self.query = query
    
    
    
    class AuthorizationError(MCPError):
    
        """Access control errors."""
    
        def __init__(self, message: str, user_id: str = None):
    
            super().__init__(message, "AUTHORIZATION_ERROR")
    
            self.user_id = user_id
    
    
    
    class QueryTimeoutError(DatabaseError):
    
        """Query execution timeout."""
    
        def __init__(self, query: str):
    
            super().__init__(f"Query timeout: {query[:100]}...", query)
    
            self.error_code = "QUERY_TIMEOUT"
    
    
    
    class ValidationError(MCPError):
    
        """Input validation errors."""
    
        def __init__(self, field: str, value: Any, constraint: str):
    
            message = f"Validation failed for {field}: {constraint}"
    
            super().__init__(message, "VALIDATION_ERROR")
    
            self.field = field
    
            self.value = value
    
    

    ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฏธ๋“ค์›จ์–ด

    
    @contextmanager
    
    async def error_handling_context(operation_name: str, user_id: str = None):
    
        """Centralized error handling for operations."""
    
        start_time = time.time()
    
        
    
        try:
    
            yield
    
            
    
            # Success metrics
    
            duration = time.time() - start_time
    
            metrics.operation_success.labels(operation=operation_name).inc()
    
            metrics.operation_duration.labels(operation=operation_name).observe(duration)
    
            
    
        except ValidationError as e:
    
            logger.warning(f"Validation error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "validation",
    
                "field": e.field
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="validation").inc()
    
            raise
    
            
    
        except AuthorizationError as e:
    
            logger.warning(f"Authorization error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "authorization"
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="authorization").inc()
    
            raise
    
            
    
        except DatabaseError as e:
    
            logger.error(f"Database error in {operation_name}: {e.message}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "database",
    
                "query": e.query[:100] if e.query else None
    
            })
    
            metrics.operation_error.labels(operation=operation_name, type="database").inc()
    
            raise
    
            
    
        except Exception as e:
    
            logger.error(f"Unexpected error in {operation_name}: {str(e)}", extra={
    
                "operation": operation_name,
    
                "user_id": user_id,
    
                "error_type": "unexpected"
    
            }, exc_info=True)
    
            metrics.operation_error.labels(operation=operation_name, type="unexpected").inc()
    
            raise MCPError(f"Internal server error in {operation_name}")
    
    

    ๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™” ์ „๋žต

    ์ฟผ๋ฆฌ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

    
    class QueryPerformanceMonitor:
    
        """Monitor and optimize query performance."""
    
        
    
        def __init__(self):
    
            self.slow_query_threshold = 1.0  # seconds
    
            self.query_stats = defaultdict(list)
    
        
    
        @contextmanager
    
        async def monitor_query(self, query: str, operation_type: str = "unknown"):
    
            """Monitor query execution time and performance."""
    
            start_time = time.time()
    
            query_hash = hashlib.md5(query.encode()).hexdigest()[:8]
    
            
    
            try:
    
                yield
    
                
    
                duration = time.time() - start_time
    
                
    
                # Record performance metrics
    
                self.query_stats[operation_type].append(duration)
    
                
    
                # Log slow queries
    
                if duration > self.slow_query_threshold:
    
                    logger.warning(f"Slow query detected", extra={
    
                        "query_hash": query_hash,
    
                        "duration": duration,
    
                        "operation_type": operation_type,
    
                        "query": query[:200]
    
                    })
    
                
    
                # Update metrics
    
                metrics.query_duration.labels(type=operation_type).observe(duration)
    
                
    
            except Exception as e:
    
                duration = time.time() - start_time
    
                logger.error(f"Query failed", extra={
    
                    "query_hash": query_hash,
    
                    "duration": duration,
    
                    "operation_type": operation_type,
    
                    "error": str(e)
    
                })
    
                raise
    
        
    
        def get_performance_summary(self) -> Dict[str, Any]:
    
            """Generate performance summary report."""
    
            summary = {}
    
            
    
            for operation_type, durations in self.query_stats.items():
    
                if durations:
    
                    summary[operation_type] = {
    
                        "count": len(durations),
    
                        "avg_duration": sum(durations) / len(durations),
    
                        "max_duration": max(durations),
    
                        "min_duration": min(durations),
    
                        "slow_queries": len([d for d in durations if d > self.slow_query_threshold])
    
                    }
    
            
    
            return summary
    
    

    ์บ์‹ฑ ์ „๋žต

    
    class QueryCache:
    
        """Intelligent query result caching."""
    
        
    
        def __init__(self, redis_url: str = None):
    
            self.cache = {}  # In-memory fallback
    
            self.redis_client = redis.Redis.from_url(redis_url) if redis_url else None
    
            self.cache_ttl = 300  # 5 minutes default
    
        
    
        async def get_cached_result(
    
            self, 
    
            cache_key: str, 
    
            query_func: Callable,
    
            ttl: int = None
    
        ) -> Any:
    
            """Get result from cache or execute query."""
    
            ttl = ttl or self.cache_ttl
    
            
    
            # Try cache first
    
            cached_result = await self._get_from_cache(cache_key)
    
            if cached_result is not None:
    
                metrics.cache_hit.labels(type="query").inc()
    
                return cached_result
    
            
    
            # Execute query
    
            metrics.cache_miss.labels(type="query").inc()
    
            result = await query_func()
    
            
    
            # Cache result
    
            await self._set_in_cache(cache_key, result, ttl)
    
            
    
            return result
    
        
    
        def _generate_cache_key(self, query: str, user_context: str) -> str:
    
            """Generate consistent cache key."""
    
            key_data = f"{query}:{user_context}"
    
            return hashlib.sha256(key_data.encode()).hexdigest()
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

    โœ… ๊ณ„์ธตํ™”๋œ ์•„ํ‚คํ…์ฒ˜: MCP ์„œ๋ฒ„ ์„ค๊ณ„์—์„œ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ถ„๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•

    โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒจํ„ด: ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ ์Šคํ‚ค๋งˆ ์„ค๊ณ„ ๋ฐ RLS ๊ตฌํ˜„

    โœ… ์—ฐ๊ฒฐ ๊ด€๋ฆฌ: ํšจ์œจ์ ์ธ ํ’€๋ง ๋ฐ ๋ฆฌ์†Œ์Šค ๋ผ์ดํ”„์‚ฌ์ดํด

    โœ… ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ๊ณ„์ธต์  ์˜ค๋ฅ˜ ์œ ํ˜• ๋ฐ ๋ณต์›๋ ฅ ํŒจํ„ด

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ชจ๋‹ˆํ„ฐ๋ง, ์บ์‹ฑ ๋ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™”

    โœ… ํ”„๋กœ๋•์…˜ ์ค€๋น„: ์ธํ”„๋ผ ๊ด€์‹ฌ์‚ฌ ๋ฐ ์šด์˜ ํŒจํ„ด

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 02: ๋ณด์•ˆ ๋ฐ ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ๋กœ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ ๋‹ค์Œ์„ ์‹ฌ์ธต์ ์œผ๋กœ ํƒ๊ตฌํ•˜์„ธ์š”:

  • ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„ ์„ธ๋ถ€์‚ฌํ•ญ
  • ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ ํŒจํ„ด
  • ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ์ „๋žต
  • ๋ณด์•ˆ ๊ฐ์‚ฌ ๋ฐ ์ค€์ˆ˜ ๊ณ ๋ ค ์‚ฌํ•ญ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด

  • Python์—์„œ์˜ ํด๋ฆฐ ์•„ํ‚คํ…์ฒ˜ - Python ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ํŒจํ„ด - ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ์›์น™
  • ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ํŒจํ„ด - ์„œ๋น„์Šค ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • PostgreSQL ๊ณ ๊ธ‰ ์ฃผ์ œ

  • PostgreSQL ์„ฑ๋Šฅ ํŠœ๋‹ - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™” ๊ฐ€์ด๋“œ
  • ์—ฐ๊ฒฐ ํ’€๋ง ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • ์ฟผ๋ฆฌ ๊ณ„ํš ๋ฐ ์ตœ์ ํ™” - ์ฟผ๋ฆฌ ์„ฑ๋Šฅ
  • Python ๋น„๋™๊ธฐ ํŒจํ„ด

  • AsyncIO ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจํ„ด
  • FastAPI ์•„ํ‚คํ…์ฒ˜ - ํ˜„๋Œ€์ ์ธ Python ์›น ์•„ํ‚คํ…์ฒ˜
  • Pydantic ๋ชจ๋ธ - ๋ฐ์ดํ„ฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ์ง๋ ฌํ™”
  • ---

    ๋‹ค์Œ: ๋ณด์•ˆ ํŒจํ„ด์„ ํƒ๊ตฌํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? Lab 02: ๋ณด์•ˆ ๋ฐ ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ๋กœ ๊ณ„์† ์ง„ํ–‰ํ•˜์„ธ์š”.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•˜์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    02 ๋ณด์•ˆ ๋ฐ ๋‹ค์ค‘ ํ…Œ๋„Œ์‹œ

    ๋ณด์•ˆ ๋ฐ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ MCP ์„œ๋ฒ„์— ๋Œ€ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ ๋ฐ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ๊ตฌํ˜„์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ฏผ๊ฐํ•œ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ๋ณดํ˜ธํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ ํ…Œ๋„ŒํŠธ ๊ฐ„์— ์œ ์—ฐํ•œ ์ ‘๊ทผ ํŒจํ„ด์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ์•ˆ์ „ํ•˜๊ณ  ์ค€์ˆ˜ํ•œ ์‹œ์Šคํ…œ์„ ์„ค๊ณ„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ๊ณ ๊ฐ ๋ฐ์ดํ„ฐ, ๊ฒฐ์ œ ์ •๋ณด, ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์†Œ๋งค ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ณด์•ˆ์€ ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ๋ฐ ์ค€์ˆ˜ ๋ชจ๋‹ˆํ„ฐ๋ง๊นŒ์ง€ ์™„์ „ํ•œ ๋ณด์•ˆ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    Azure ID ์„œ๋น„์Šค, PostgreSQL ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ˆ˜์ค€ ์ œ์–ด, ํฌ๊ด„์ ์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ๋ฅผ ๊ฒฐํ•ฉํ•œ ์‹ฌ์ธต ๋ฐฉ์–ด ์ „๋žต์„ ๊ตฌํ˜„ํ•˜์—ฌ ๊ฐ•๋ ฅํ•˜๊ณ  ์ค€์ˆ˜ํ•œ ํ”Œ๋žซํผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๊ตฌํ˜„: ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ
  • ์„ค๊ณ„: Azure๋ฅผ ํ™œ์šฉํ•œ ์•ˆ์ „ํ•œ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ ํŒจํ„ด
  • ๊ตฌ์„ฑ: ์ค€์ˆ˜ ์š”๊ตฌ ์‚ฌํ•ญ์„ ์œ„ํ•œ ํฌ๊ด„์ ์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ
  • ์ ์šฉ: ๋ชจ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต์—์„œ ์‹ฌ์ธต ๋ฐฉ์–ด ๋ณด์•ˆ ์ „๋žต
  • ๊ฒ€์ฆ: ์ฒด๊ณ„์ ์ธ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋ณด์•ˆ ๊ตฌํ˜„ ํ™•์ธ
  • ๋ชจ๋‹ˆํ„ฐ๋ง: ๋ณด์•ˆ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์‹œํ•˜๊ณ  ์ž ์žฌ์  ์œ„ํ˜‘์— ๋Œ€์‘
  • ๐Ÿ” ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ณด์•ˆ ์•„ํ‚คํ…์ฒ˜

    ๋ณด์•ˆ ๊ณ„์ธต ๊ฐœ์š”

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚               Azure Front Door                  โ”‚ โ† WAF, DDoS Protection
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚              Application Gateway                โ”‚ โ† SSL Termination, Rate Limiting
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚                MCP Server                       โ”‚ โ† Authentication, Authorization
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚           Connection Layer                  โ”‚ โ† Connection Pooling, Circuit Breakers
    
    โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚         Business Logic Layer               โ”‚ โ† Input Validation, Business Rules
    
    โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚           Data Access Layer                โ”‚ โ† Query Sanitization, RLS Context
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚              PostgreSQL RLS                    โ”‚ โ† Row Level Security, Audit Triggers
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ๋ชจ๋ธ

    ์šฐ๋ฆฌ์˜ ๊ตฌํ˜„์€ ๊ณต์œ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๊ณต์œ  ์Šคํ‚ค๋งˆ ๋ชจ๋ธ๊ณผ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

    ์žฅ์ :

  • ๋น„์šฉ ํšจ์œจ์ ์ธ ์ž์› ํ™œ์šฉ
  • ๊ฐ„์†Œํ™”๋œ ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์—…๋ฐ์ดํŠธ
  • RLS๋ฅผ ํ†ตํ•œ ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ
  • ์ค€์ˆ˜ ์นœํ™”์ ์ธ ๊ฐ์‚ฌ ๊ธฐ๋ก
  • ๋‹จ์ :

  • ์‹ ์ค‘ํ•œ RLS ์ •์ฑ… ์„ค๊ณ„ ํ•„์š”
  • ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์ด ๋ชจ๋“  ํ…Œ๋„ŒํŠธ์— ์˜ํ–ฅ์„ ๋ฏธ์นจ
  • ๊ฒฌ๊ณ ํ•œ ๋ฐฑ์—…/๋ณต์› ์ ˆ์ฐจ ํ•„์š”
  • ๐Ÿ›ก๏ธ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„

    RLS ๊ธฐ์ดˆ

    
    -- Enable RLS on all multi-tenant tables
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.products ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.sales_transactions ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.sales_transaction_items ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.product_embeddings ENABLE ROW LEVEL SECURITY;
    
    
    
    -- Create application role for MCP server
    
    CREATE ROLE mcp_user LOGIN;
    
    GRANT USAGE ON SCHEMA retail TO mcp_user;
    
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA retail TO mcp_user;
    
    

    ์Šคํ† ์–ด ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ

    
    -- Function to securely set store context
    
    CREATE OR REPLACE FUNCTION retail.set_store_context(store_id_param VARCHAR(50))
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    SET search_path = retail, pg_temp
    
    AS $$
    
    DECLARE
    
        user_info RECORD;
    
    BEGIN
    
        -- Validate store exists and is active
    
        SELECT store_id, store_name, is_active 
    
        INTO user_info
    
        FROM retail.stores 
    
        WHERE store_id = store_id_param;
    
        
    
        IF NOT FOUND THEN
    
            RAISE EXCEPTION 'Store not found: %', store_id_param
    
                USING ERRCODE = 'invalid_parameter_value',
    
                      HINT = 'Verify store ID and ensure it exists in the system';
    
        END IF;
    
        
    
        IF NOT user_info.is_active THEN
    
            RAISE EXCEPTION 'Store is inactive: %', store_id_param
    
                USING ERRCODE = 'insufficient_privilege',
    
                      HINT = 'Contact administrator to activate store';
    
        END IF;
    
        
    
        -- Set the secure context
    
        PERFORM set_config('app.current_store_id', store_id_param, false);
    
        PERFORM set_config('app.store_name', user_info.store_name, false);
    
        PERFORM set_config('app.context_set_at', extract(epoch from current_timestamp)::text, false);
    
        
    
        -- Log context change for audit
    
        INSERT INTO retail.security_audit_log (
    
            event_type,
    
            user_name,
    
            store_id,
    
            ip_address,
    
            user_agent,
    
            details,
    
            severity
    
        ) VALUES (
    
            'store_context_set',
    
            current_user,
    
            store_id_param,
    
            inet_client_addr()::text,
    
            current_setting('application_name', true),
    
            jsonb_build_object(
    
                'store_name', user_info.store_name,
    
                'timestamp', current_timestamp,
    
                'session_id', pg_backend_pid()
    
            ),
    
            'INFO'
    
        );
    
    END;
    
    $$;
    
    
    
    -- Grant execute to MCP user
    
    GRANT EXECUTE ON FUNCTION retail.set_store_context TO mcp_user;
    
    

    RLS ์ •์ฑ…

    
    -- Customers RLS Policy
    
    CREATE POLICY customers_store_isolation ON retail.customers
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Products RLS Policy with additional business rules
    
    CREATE POLICY products_store_isolation ON retail.products
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
            AND is_active = TRUE  -- Additional business rule
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Sales Transactions RLS Policy
    
    CREATE POLICY sales_transactions_store_isolation ON retail.sales_transactions
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Transaction Items RLS Policy (via join)
    
    CREATE POLICY sales_transaction_items_store_isolation ON retail.sales_transaction_items
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        )
    
        WITH CHECK (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        );
    
    
    
    -- Product Embeddings RLS Policy
    
    CREATE POLICY product_embeddings_store_isolation ON retail.product_embeddings
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    

    RLS ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ

    
    -- Test RLS policies with different store contexts
    
    DO $$
    
    DECLARE
    
        test_result RECORD;
    
        customer_count INTEGER;
    
        product_count INTEGER;
    
    BEGIN
    
        -- Test Seattle store context
    
        PERFORM retail.set_store_context('seattle');
    
        
    
        SELECT COUNT(*) INTO customer_count FROM retail.customers;
    
        SELECT COUNT(*) INTO product_count FROM retail.products;
    
        
    
        RAISE NOTICE 'Seattle store - Customers: %, Products: %', customer_count, product_count;
    
        
    
        -- Test Redmond store context
    
        PERFORM retail.set_store_context('redmond');
    
        
    
        SELECT COUNT(*) INTO customer_count FROM retail.customers;
    
        SELECT COUNT(*) INTO product_count FROM retail.products;
    
        
    
        RAISE NOTICE 'Redmond store - Customers: %, Products: %', customer_count, product_count;
    
        
    
        -- Verify data isolation
    
        IF customer_count > 0 AND product_count > 0 THEN
    
            RAISE NOTICE 'RLS policies are working correctly';
    
        ELSE
    
            RAISE WARNING 'RLS policies may not be configured correctly';
    
        END IF;
    
    END;
    
    $$;
    
    

    ๐Ÿ”‘ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ

    Azure Entra ID ํ†ตํ•ฉ

    
    # mcp_server/security/authentication.py
    
    """
    
    Azure Entra ID authentication for MCP server.
    
    """
    
    import os
    
    import jwt
    
    import aiohttp
    
    import asyncio
    
    from typing import Dict, Optional, List
    
    from datetime import datetime, timezone
    
    from azure.identity.aio import DefaultAzureCredential
    
    from azure.keyvault.secrets.aio import SecretClient
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class AzureAuthenticator:
    
        """Handle Azure Entra ID authentication and token validation."""
    
        
    
        def __init__(self):
    
            self.tenant_id = os.getenv('AZURE_TENANT_ID')
    
            self.client_id = os.getenv('AZURE_CLIENT_ID')
    
            self.audience = os.getenv('AZURE_AUDIENCE', self.client_id)
    
            self.issuer = f"https://login.microsoftonline.com/{self.tenant_id}/v2.0"
    
            
    
            # Cache for JWKS (JSON Web Key Set)
    
            self._jwks_cache = None
    
            self._jwks_cache_expiry = None
    
            
    
            # Key Vault for secrets
    
            self.key_vault_url = os.getenv('AZURE_KEY_VAULT_URL')
    
            self.credential = DefaultAzureCredential()
    
            
    
            if self.key_vault_url:
    
                self.secret_client = SecretClient(
    
                    vault_url=self.key_vault_url,
    
                    credential=self.credential
    
                )
    
        
    
        async def validate_token(self, token: str) -> Dict:
    
            """Validate JWT token from Azure Entra ID."""
    
            
    
            try:
    
                # Get signing keys
    
                signing_keys = await self._get_signing_keys()
    
                
    
                # Decode token header to get key ID
    
                unverified_header = jwt.get_unverified_header(token)
    
                key_id = unverified_header.get('kid')
    
                
    
                if not key_id:
    
                    raise ValueError("Token missing key ID")
    
                
    
                # Find the corresponding key
    
                signing_key = None
    
                for key in signing_keys:
    
                    if key['kid'] == key_id:
    
                        signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
    
                        break
    
                
    
                if not signing_key:
    
                    raise ValueError(f"Unable to find signing key for kid: {key_id}")
    
                
    
                # Validate and decode token
    
                payload = jwt.decode(
    
                    token,
    
                    signing_key,
    
                    algorithms=['RS256'],
    
                    audience=self.audience,
    
                    issuer=self.issuer,
    
                    options={
    
                        'verify_exp': True,
    
                        'verify_aud': True,
    
                        'verify_iss': True
    
                    }
    
                )
    
                
    
                # Extract user information
    
                user_info = self._extract_user_info(payload)
    
                
    
                # Log successful authentication
    
                logger.info(
    
                    "User authenticated successfully",
    
                    extra={
    
                        'user_id': user_info['user_id'],
    
                        'email': user_info.get('email'),
    
                        'tenant_id': payload.get('tid')
    
                    }
    
                )
    
                
    
                return user_info
    
                
    
            except jwt.ExpiredSignatureError:
    
                logger.warning("Token has expired")
    
                raise ValueError("Token has expired")
    
            except jwt.InvalidAudienceError:
    
                logger.warning(f"Invalid audience in token. Expected: {self.audience}")
    
                raise ValueError("Invalid token audience")
    
            except jwt.InvalidIssuerError:
    
                logger.warning(f"Invalid issuer in token. Expected: {self.issuer}")
    
                raise ValueError("Invalid token issuer")
    
            except Exception as e:
    
                logger.error(f"Token validation failed: {str(e)}")
    
                raise ValueError(f"Token validation failed: {str(e)}")
    
        
    
        async def _get_signing_keys(self) -> List[Dict]:
    
            """Get JWKS from Azure Entra ID with caching."""
    
            
    
            current_time = datetime.now(timezone.utc)
    
            
    
            # Check if cache is valid
    
            if (self._jwks_cache and self._jwks_cache_expiry and 
    
                current_time < self._jwks_cache_expiry):
    
                return self._jwks_cache
    
            
    
            # Fetch new JWKS
    
            jwks_url = f"{self.issuer}/keys"
    
            
    
            async with aiohttp.ClientSession() as session:
    
                async with session.get(jwks_url) as response:
    
                    if response.status != 200:
    
                        raise Exception(f"Failed to fetch JWKS: {response.status}")
    
                    
    
                    jwks_data = await response.json()
    
                    
    
            # Cache for 1 hour
    
            self._jwks_cache = jwks_data['keys']
    
            self._jwks_cache_expiry = current_time.replace(
    
                hour=current_time.hour + 1
    
            )
    
            
    
            return self._jwks_cache
    
        
    
        def _extract_user_info(self, payload: Dict) -> Dict:
    
            """Extract user information from JWT payload."""
    
            
    
            return {
    
                'user_id': payload.get('oid') or payload.get('sub'),
    
                'email': payload.get('email') or payload.get('preferred_username'),
    
                'name': payload.get('name'),
    
                'tenant_id': payload.get('tid'),
    
                'roles': payload.get('roles', []),
    
                'groups': payload.get('groups', []),
    
                'app_roles': payload.get('app_roles', []),
    
                'scope': payload.get('scp', '').split() if payload.get('scp') else [],
    
                'expires_at': datetime.fromtimestamp(payload['exp'], timezone.utc),
    
                'issued_at': datetime.fromtimestamp(payload['iat'], timezone.utc)
    
            }
    
        
    
        async def get_user_store_access(self, user_id: str) -> List[str]:
    
            """Get list of stores the user has access to."""
    
            
    
            try:
    
                # This would typically query your user/store mapping
    
                # For demo, we'll use a simple Key Vault secret
    
                secret_name = f"user-{user_id}-stores"
    
                
    
                if self.secret_client:
    
                    secret = await self.secret_client.get_secret(secret_name)
    
                    store_list = secret.value.split(',')
    
                    return [store.strip() for store in store_list if store.strip()]
    
                
    
                # Fallback: return default store access
    
                logger.warning(f"No store mapping found for user {user_id}, using default")
    
                return ['seattle']  # Default store access
    
                
    
            except Exception as e:
    
                logger.error(f"Failed to get store access for user {user_id}: {e}")
    
                return []  # No access if we can't determine stores
    
    
    
    # Global authenticator instance
    
    azure_authenticator = AzureAuthenticator()
    
    

    ๊ถŒํ•œ ๋ถ€์—ฌ ๋ฏธ๋“ค์›จ์–ด

    
    # mcp_server/security/authorization.py
    
    """
    
    Authorization middleware and decorators for MCP server.
    
    """
    
    import functools
    
    from typing import Dict, List, Optional, Callable, Any
    
    from fastapi import HTTPException, status, Request
    
    from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    security = HTTPBearer()
    
    
    
    class AuthorizationError(Exception):
    
        """Custom authorization error."""
    
        pass
    
    
    
    class RoleBasedAuth:
    
        """Role-based access control implementation."""
    
        
    
        # Define role hierarchy
    
        ROLE_HIERARCHY = {
    
            'store_admin': ['store_manager', 'store_user', 'store_readonly'],
    
            'store_manager': ['store_user', 'store_readonly'],
    
            'store_user': ['store_readonly'],
    
            'store_readonly': []
    
        }
    
        
    
        # Define permissions for each role
    
        ROLE_PERMISSIONS = {
    
            'store_admin': [
    
                'read_all', 'write_all', 'delete_all', 'manage_users'
    
            ],
    
            'store_manager': [
    
                'read_all', 'write_transactions', 'write_inventory', 'read_reports'
    
            ],
    
            'store_user': [
    
                'read_products', 'read_customers', 'write_transactions'
    
            ],
    
            'store_readonly': [
    
                'read_products', 'read_basic_reports'
    
            ]
    
        }
    
        
    
        @classmethod
    
        def has_permission(cls, user_roles: List[str], required_permission: str) -> bool:
    
            """Check if user has required permission."""
    
            
    
            user_permissions = set()
    
            
    
            for role in user_roles:
    
                # Add direct permissions
    
                user_permissions.update(cls.ROLE_PERMISSIONS.get(role, []))
    
                
    
                # Add inherited permissions
    
                inherited_roles = cls.ROLE_HIERARCHY.get(role, [])
    
                for inherited_role in inherited_roles:
    
                    user_permissions.update(cls.ROLE_PERMISSIONS.get(inherited_role, []))
    
            
    
            return required_permission in user_permissions
    
        
    
        @classmethod
    
        def get_user_stores(cls, user_info: Dict) -> List[str]:
    
            """Extract stores user has access to from user info."""
    
            
    
            # This would typically come from your user management system
    
            # For demo, we'll extract from custom claims or groups
    
            
    
            stores = []
    
            
    
            # Check for direct store assignments in groups
    
            for group in user_info.get('groups', []):
    
                if group.startswith('store_'):
    
                    store_id = group.replace('store_', '')
    
                    stores.append(store_id)
    
            
    
            # Check for app-specific roles
    
            for role in user_info.get('app_roles', []):
    
                if 'store:' in role:
    
                    _, store_id = role.split('store:', 1)
    
                    stores.append(store_id)
    
            
    
            return list(set(stores))  # Remove duplicates
    
    
    
    def require_auth(required_permission: str = None, require_store_access: bool = True):
    
        """Decorator to require authentication and authorization."""
    
        
    
        def decorator(func: Callable) -> Callable:
    
            @functools.wraps(func)
    
            async def wrapper(*args, **kwargs):
    
                # Extract request from args (FastAPI dependency injection)
    
                request = None
    
                for arg in args:
    
                    if isinstance(arg, Request):
    
                        request = arg
    
                        break
    
                
    
                if not request:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    
                        detail="Request object not found"
    
                    )
    
                
    
                # Get authorization header
    
                auth_header = request.headers.get('Authorization')
    
                if not auth_header or not auth_header.startswith('Bearer '):
    
                    raise HTTPException(
    
                        status_code=status.HTTP_401_UNAUTHORIZED,
    
                        detail="Missing or invalid authorization header",
    
                        headers={"WWW-Authenticate": "Bearer"}
    
                    )
    
                
    
                token = auth_header.split(' ')[1]
    
                
    
                try:
    
                    # Validate token
    
                    user_info = await azure_authenticator.validate_token(token)
    
                    
    
                    # Check required permission
    
                    if required_permission:
    
                        user_roles = user_info.get('roles', [])
    
                        if not RoleBasedAuth.has_permission(user_roles, required_permission):
    
                            raise HTTPException(
    
                                status_code=status.HTTP_403_FORBIDDEN,
    
                                detail=f"Insufficient permissions. Required: {required_permission}"
    
                            )
    
                    
    
                    # Check store access
    
                    if require_store_access:
    
                        user_stores = RoleBasedAuth.get_user_stores(user_info)
    
                        if not user_stores:
    
                            raise HTTPException(
    
                                status_code=status.HTTP_403_FORBIDDEN,
    
                                detail="No store access configured for user"
    
                            )
    
                        
    
                        # Set default store context (first accessible store)
    
                        request.state.current_store = user_stores[0]
    
                        request.state.accessible_stores = user_stores
    
                    
    
                    # Add user info to request state
    
                    request.state.user_info = user_info
    
                    request.state.user_id = user_info['user_id']
    
                    
    
                    # Call the original function
    
                    return await func(*args, **kwargs)
    
                    
    
                except ValueError as e:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_401_UNAUTHORIZED,
    
                        detail=str(e),
    
                        headers={"WWW-Authenticate": "Bearer"}
    
                    )
    
                except AuthorizationError as e:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_403_FORBIDDEN,
    
                        detail=str(e)
    
                    )
    
            
    
            return wrapper
    
        return decorator
    
    
    
    def require_store_context(store_param: str = 'store_id'):
    
        """Decorator to validate and set store context."""
    
        
    
        def decorator(func: Callable) -> Callable:
    
            @functools.wraps(func)
    
            async def wrapper(*args, **kwargs):
    
                # Get store_id from kwargs
    
                store_id = kwargs.get(store_param)
    
                
    
                if not store_id:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_400_BAD_REQUEST,
    
                        detail=f"Missing required parameter: {store_param}"
    
                    )
    
                
    
                # Extract request from args
    
                request = None
    
                for arg in args:
    
                    if isinstance(arg, Request):
    
                        request = arg
    
                        break
    
                
    
                if not request or not hasattr(request.state, 'accessible_stores'):
    
                    raise HTTPException(
    
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    
                        detail="Authentication required before store context validation"
    
                    )
    
                
    
                # Validate user has access to requested store
    
                if store_id not in request.state.accessible_stores:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_403_FORBIDDEN,
    
                        detail=f"Access denied to store: {store_id}"
    
                    )
    
                
    
                # Set store context in request state
    
                request.state.current_store = store_id
    
                
    
                return await func(*args, **kwargs)
    
            
    
            return wrapper
    
        return decorator
    
    

    ๐Ÿ” ๋ณด์•ˆ ๊ฐ์‚ฌ ๋ฐ ์ค€์ˆ˜

    ํฌ๊ด„์ ์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ

    
    -- Security audit log table
    
    CREATE TABLE retail.security_audit_log (
    
        log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        event_type VARCHAR(100) NOT NULL,
    
        user_name VARCHAR(100) NOT NULL,
    
        user_id VARCHAR(100),
    
        store_id VARCHAR(50),
    
        ip_address INET,
    
        user_agent TEXT,
    
        request_id VARCHAR(100),
    
        session_id VARCHAR(100),
    
        resource_type VARCHAR(100),
    
        resource_id VARCHAR(100),
    
        action VARCHAR(50) NOT NULL,
    
        success BOOLEAN NOT NULL DEFAULT TRUE,
    
        failure_reason TEXT,
    
        details JSONB,
    
        severity VARCHAR(20) DEFAULT 'INFO',
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure proper indexing for security queries
    
        CONSTRAINT valid_severity CHECK (severity IN ('DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'))
    
    );
    
    
    
    -- Indexes for security audit queries
    
    CREATE INDEX idx_security_audit_event_type ON retail.security_audit_log(event_type);
    
    CREATE INDEX idx_security_audit_user_name ON retail.security_audit_log(user_name);
    
    CREATE INDEX idx_security_audit_store_id ON retail.security_audit_log(store_id);
    
    CREATE INDEX idx_security_audit_created_at ON retail.security_audit_log(created_at);
    
    CREATE INDEX idx_security_audit_success ON retail.security_audit_log(success);
    
    CREATE INDEX idx_security_audit_severity ON retail.security_audit_log(severity);
    
    CREATE INDEX idx_security_audit_details ON retail.security_audit_log USING GIN(details);
    
    
    
    -- Function to log security events
    
    CREATE OR REPLACE FUNCTION retail.log_security_event(
    
        p_event_type VARCHAR(100),
    
        p_user_name VARCHAR(100),
    
        p_user_id VARCHAR(100) DEFAULT NULL,
    
        p_store_id VARCHAR(50) DEFAULT NULL,
    
        p_ip_address TEXT DEFAULT NULL,
    
        p_action VARCHAR(50) DEFAULT 'unknown',
    
        p_success BOOLEAN DEFAULT TRUE,
    
        p_failure_reason TEXT DEFAULT NULL,
    
        p_details JSONB DEFAULT NULL,
    
        p_severity VARCHAR(20) DEFAULT 'INFO'
    
    )
    
    RETURNS UUID
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    DECLARE
    
        log_id UUID;
    
    BEGIN
    
        INSERT INTO retail.security_audit_log (
    
            event_type,
    
            user_name,
    
            user_id,
    
            store_id,
    
            ip_address,
    
            action,
    
            success,
    
            failure_reason,
    
            details,
    
            severity
    
        ) VALUES (
    
            p_event_type,
    
            p_user_name,
    
            p_user_id,
    
            p_store_id,
    
            p_ip_address::INET,
    
            p_action,
    
            p_success,
    
            p_failure_reason,
    
            p_details,
    
            p_severity
    
        ) RETURNING log_id INTO log_id;
    
        
    
        RETURN log_id;
    
    END;
    
    $$;
    
    
    
    -- Grant execute to MCP user
    
    GRANT EXECUTE ON FUNCTION retail.log_security_event TO mcp_user;
    
    

    ๋ณด์•ˆ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ทฐ

    
    -- Failed authentication attempts
    
    CREATE VIEW retail.security_failed_auth AS
    
    SELECT 
    
        event_type,
    
        user_name,
    
        ip_address,
    
        COUNT(*) as attempt_count,
    
        MIN(created_at) as first_attempt,
    
        MAX(created_at) as last_attempt,
    
        ARRAY_AGG(DISTINCT failure_reason) as failure_reasons
    
    FROM retail.security_audit_log
    
    WHERE success = FALSE 
    
      AND event_type IN ('authentication_failed', 'token_validation_failed')
    
      AND created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
    
    GROUP BY event_type, user_name, ip_address
    
    HAVING COUNT(*) >= 3  -- 3 or more failures
    
    ORDER BY attempt_count DESC, last_attempt DESC;
    
    
    
    -- Suspicious access patterns
    
    CREATE VIEW retail.security_suspicious_access AS
    
    SELECT 
    
        user_name,
    
        user_id,
    
        COUNT(DISTINCT ip_address) as ip_count,
    
        COUNT(DISTINCT store_id) as store_count,
    
        ARRAY_AGG(DISTINCT ip_address::TEXT) as ip_addresses,
    
        ARRAY_AGG(DISTINCT store_id) as stores_accessed,
    
        MIN(created_at) as first_access,
    
        MAX(created_at) as last_access
    
    FROM retail.security_audit_log
    
    WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
      AND success = TRUE
    
    GROUP BY user_name, user_id
    
    HAVING COUNT(DISTINCT ip_address) > 3  -- Access from multiple IPs
    
       OR COUNT(DISTINCT store_id) > 2     -- Access to multiple stores
    
    ORDER BY ip_count DESC, store_count DESC;
    
    
    
    -- Data access patterns
    
    CREATE VIEW retail.security_data_access_summary AS
    
    SELECT 
    
        DATE_TRUNC('hour', created_at) as access_hour,
    
        store_id,
    
        resource_type,
    
        action,
    
        COUNT(*) as access_count,
    
        COUNT(DISTINCT user_id) as unique_users
    
    FROM retail.security_audit_log
    
    WHERE resource_type IS NOT NULL
    
      AND created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
    
    GROUP BY DATE_TRUNC('hour', created_at), store_id, resource_type, action
    
    ORDER BY access_hour DESC, access_count DESC;
    
    

    ๋ณด์•ˆ ์ด๋ฒคํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง

    
    # mcp_server/security/monitoring.py
    
    """
    
    Security monitoring and alerting for MCP server.
    
    """
    
    import asyncio
    
    import asyncpg
    
    from typing import Dict, List, Any
    
    from datetime import datetime, timedelta
    
    from dataclasses import dataclass
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    @dataclass
    
    class SecurityAlert:
    
        """Security alert data structure."""
    
        alert_type: str
    
        severity: str
    
        message: str
    
        details: Dict[str, Any]
    
        timestamp: datetime
    
    
    
    class SecurityMonitor:
    
        """Monitor security events and generate alerts."""
    
        
    
        def __init__(self, db_connection_string: str):
    
            self.db_connection_string = db_connection_string
    
            self.alert_handlers = []
    
            
    
            # Alert thresholds
    
            self.thresholds = {
    
                'failed_auth_attempts': 5,      # per user per hour
    
                'multiple_ip_access': 3,        # different IPs per user per hour
    
                'excessive_data_access': 1000,  # queries per user per hour
    
                'privilege_escalation': 1,      # any attempt
    
                'unauthorized_store_access': 1  # any attempt
    
            }
    
        
    
        async def start_monitoring(self):
    
            """Start security monitoring loop."""
    
            logger.info("Starting security monitoring")
    
            
    
            while True:
    
                try:
    
                    await self._check_security_events()
    
                    await asyncio.sleep(300)  # Check every 5 minutes
    
                except Exception as e:
    
                    logger.error(f"Security monitoring error: {e}")
    
                    await asyncio.sleep(60)  # Short retry on error
    
        
    
        async def _check_security_events(self):
    
            """Check for security events and generate alerts."""
    
            
    
            conn = await asyncpg.connect(self.db_connection_string)
    
            
    
            try:
    
                # Check failed authentication attempts
    
                await self._check_failed_auth(conn)
    
                
    
                # Check suspicious access patterns
    
                await self._check_suspicious_access(conn)
    
                
    
                # Check data access anomalies
    
                await self._check_data_access_anomalies(conn)
    
                
    
                # Check unauthorized access attempts
    
                await self._check_unauthorized_access(conn)
    
                
    
            finally:
    
                await conn.close()
    
        
    
        async def _check_failed_auth(self, conn):
    
            """Check for excessive failed authentication attempts."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                ip_address,
    
                COUNT(*) as attempt_count,
    
                MAX(created_at) as last_attempt
    
            FROM retail.security_audit_log
    
            WHERE success = FALSE 
    
              AND event_type IN ('authentication_failed', 'token_validation_failed')
    
              AND created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
            GROUP BY user_name, ip_address
    
            HAVING COUNT(*) >= $1
    
            """
    
            
    
            results = await conn.fetch(query, self.thresholds['failed_auth_attempts'])
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='failed_authentication',
    
                    severity='HIGH',
    
                    message=f"Excessive failed login attempts for user {record['user_name']}",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'ip_address': str(record['ip_address']),
    
                        'attempt_count': record['attempt_count'],
    
                        'last_attempt': record['last_attempt'].isoformat()
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _check_suspicious_access(self, conn):
    
            """Check for suspicious access patterns."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                user_id,
    
                COUNT(DISTINCT ip_address) as ip_count,
    
                ARRAY_AGG(DISTINCT ip_address::TEXT) as ip_addresses
    
            FROM retail.security_audit_log
    
            WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
              AND success = TRUE
    
            GROUP BY user_name, user_id
    
            HAVING COUNT(DISTINCT ip_address) >= $1
    
            """
    
            
    
            results = await conn.fetch(query, self.thresholds['multiple_ip_access'])
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='suspicious_access',
    
                    severity='MEDIUM',
    
                    message=f"User {record['user_name']} accessed from multiple IP addresses",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'user_id': record['user_id'],
    
                        'ip_count': record['ip_count'],
    
                        'ip_addresses': record['ip_addresses']
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _check_unauthorized_access(self, conn):
    
            """Check for unauthorized store access attempts."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                user_id,
    
                store_id,
    
                failure_reason,
    
                created_at
    
            FROM retail.security_audit_log
    
            WHERE success = FALSE 
    
              AND event_type = 'unauthorized_store_access'
    
              AND created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
            """
    
            
    
            results = await conn.fetch(query)
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='unauthorized_access',
    
                    severity='HIGH',
    
                    message=f"Unauthorized store access attempt by {record['user_name']}",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'user_id': record['user_id'],
    
                        'store_id': record['store_id'],
    
                        'failure_reason': record['failure_reason'],
    
                        'timestamp': record['created_at'].isoformat()
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _send_alert(self, alert: SecurityAlert):
    
            """Send security alert to all configured handlers."""
    
            
    
            logger.warning(
    
                f"Security Alert: {alert.alert_type} - {alert.message}",
    
                extra={'alert_details': alert.details}
    
            )
    
            
    
            # Send to configured alert handlers
    
            for handler in self.alert_handlers:
    
                try:
    
                    await handler.send_alert(alert)
    
                except Exception as e:
    
                    logger.error(f"Failed to send alert via {handler.__class__.__name__}: {e}")
    
        
    
        def add_alert_handler(self, handler):
    
            """Add alert handler."""
    
            self.alert_handlers.append(handler)
    
    

    ๐Ÿงช ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ

    ์ž๋™ํ™”๋œ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ

    
    # tests/security/test_security.py
    
    """
    
    Comprehensive security tests for MCP server.
    
    """
    
    import pytest
    
    import asyncio
    
    import asyncpg
    
    from datetime import datetime, timezone
    
    import jwt
    
    from unittest.mock import Mock, patch
    
    
    
    class TestRowLevelSecurity:
    
        """Test Row Level Security implementation."""
    
        
    
        @pytest.fixture
    
        async def db_connection(self):
    
            """Database connection for testing."""
    
            conn = await asyncpg.connect(
    
                "postgresql://mcp_user:password@localhost:5432/retail_test"
    
            )
    
            yield conn
    
            await conn.close()
    
        
    
        async def test_store_context_isolation(self, db_connection):
    
            """Test that RLS properly isolates data by store."""
    
            
    
            # Set Seattle store context
    
            await db_connection.execute("SELECT retail.set_store_context('seattle')")
    
            
    
            # Get customer count
    
            seattle_customers = await db_connection.fetchval(
    
                "SELECT COUNT(*) FROM retail.customers"
    
            )
    
            
    
            # Set Redmond store context
    
            await db_connection.execute("SELECT retail.set_store_context('redmond')")
    
            
    
            # Get customer count
    
            redmond_customers = await db_connection.fetchval(
    
                "SELECT COUNT(*) FROM retail.customers"
    
            )
    
            
    
            # Verify isolation (counts should be different)
    
            assert seattle_customers != redmond_customers or (
    
                seattle_customers == 0 and redmond_customers == 0
    
            )
    
        
    
        async def test_unauthorized_store_access(self, db_connection):
    
            """Test that invalid store access is blocked."""
    
            
    
            with pytest.raises(Exception) as exc_info:
    
                await db_connection.execute("SELECT retail.set_store_context('invalid_store')")
    
            
    
            assert "Store not found" in str(exc_info.value)
    
        
    
        async def test_cross_store_data_leakage(self, db_connection):
    
            """Test that users cannot access data from other stores."""
    
            
    
            # Set context to one store
    
            await db_connection.execute("SELECT retail.set_store_context('seattle')")
    
            
    
            # Try to insert data with different store_id
    
            with pytest.raises(Exception):
    
                await db_connection.execute("""
    
                    INSERT INTO retail.customers (store_id, first_name, last_name, email)
    
                    VALUES ('redmond', 'Test', 'User', 'test@example.com')
    
                """)
    
    
    
    class TestAuthentication:
    
        """Test authentication and authorization."""
    
        
    
        def test_valid_jwt_token(self):
    
            """Test valid JWT token validation."""
    
            
    
            # Mock valid token
    
            token_payload = {
    
                'oid': 'user-123',
    
                'email': 'test@example.com',
    
                'name': 'Test User',
    
                'tid': 'tenant-123',
    
                'aud': 'app-client-id',
    
                'iss': 'https://login.microsoftonline.com/tenant-123/v2.0',
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) + 3600,
    
                'iat': int((datetime.now(timezone.utc)).timestamp()),
    
                'roles': ['store_user']
    
            }
    
            
    
            # This would require mocking the JWKS endpoint
    
            # In real implementation, use proper test JWT tokens
    
            
    
        def test_expired_token_rejection(self):
    
            """Test that expired tokens are rejected."""
    
            
    
            token_payload = {
    
                'oid': 'user-123',
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) - 3600,  # Expired
    
                'iat': int((datetime.now(timezone.utc)).timestamp()) - 7200
    
            }
    
            
    
            # Test would verify that expired tokens are rejected
    
            
    
        def test_invalid_audience_rejection(self):
    
            """Test that tokens with wrong audience are rejected."""
    
            
    
            token_payload = {
    
                'oid': 'user-123',
    
                'aud': 'wrong-audience',  # Invalid audience
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) + 3600,
    
                'iat': int((datetime.now(timezone.utc)).timestamp())
    
            }
    
            
    
            # Test would verify that wrong audience tokens are rejected
    
    
    
    class TestAuthorization:
    
        """Test role-based authorization."""
    
        
    
        def test_role_hierarchy(self):
    
            """Test that role hierarchy works correctly."""
    
            
    
            from mcp_server.security.authorization import RoleBasedAuth
    
            
    
            # Store admin should have all permissions
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'read_all')
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'write_all')
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'delete_all')
    
            
    
            # Store user should have limited permissions
    
            assert RoleBasedAuth.has_permission(['store_user'], 'read_products')
    
            assert not RoleBasedAuth.has_permission(['store_user'], 'delete_all')
    
            
    
            # Store readonly should have minimal permissions
    
            assert RoleBasedAuth.has_permission(['store_readonly'], 'read_products')
    
            assert not RoleBasedAuth.has_permission(['store_readonly'], 'write_transactions')
    
        
    
        def test_permission_inheritance(self):
    
            """Test that permissions are properly inherited."""
    
            
    
            from mcp_server.security.authorization import RoleBasedAuth
    
            
    
            # Manager should inherit user permissions
    
            assert RoleBasedAuth.has_permission(['store_manager'], 'read_products')
    
            assert RoleBasedAuth.has_permission(['store_manager'], 'write_transactions')
    
    
    
    # Security test runner
    
    if __name__ == "__main__":
    
        pytest.main([__file__, "-v"])
    
    

    ์นจํˆฌ ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

    
    # security-test-checklist.yml
    
    penetration_testing:
    
      
    
      authentication_bypass:
    
        - name: "Test authentication bypass attempts"
    
          tests:
    
            - "Missing Authorization header"
    
            - "Malformed JWT tokens"
    
            - "Replay attack with expired tokens"
    
            - "Token signature manipulation"
    
            - "Audience/issuer manipulation"
    
        
    
      authorization_escalation:
    
        - name: "Test privilege escalation attempts"
    
          tests:
    
            - "Role manipulation in token"
    
            - "Store access boundary testing"
    
            - "Cross-tenant data access attempts"
    
            - "Administrative function access"
    
        
    
      sql_injection:
    
        - name: "Test SQL injection vulnerabilities"
    
          tests:
    
            - "Parameter injection in search queries"
    
            - "Store ID manipulation"
    
            - "JSON parameter injection"
    
            - "Union-based injection attempts"
    
        
    
      data_exposure:
    
        - name: "Test for data exposure vulnerabilities"
    
          tests:
    
            - "Error message information disclosure"
    
            - "Timing attack possibilities"
    
            - "Cross-store data leakage"
    
            - "Audit log exposure"
    
        
    
      rate_limiting:
    
        - name: "Test rate limiting and DoS protection"
    
          tests:
    
            - "Authentication endpoint flooding"
    
            - "API endpoint rate limits"
    
            - "Resource exhaustion attempts"
    
            - "Connection pool exhaustion"
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ณด์•ˆ: ์™„์ „ํ•œ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„

    โœ… Azure ์ธ์ฆ: Azure Entra ID์™€ JWT ๊ฒ€์ฆ ํ†ตํ•ฉ

    โœ… ์—ญํ•  ๊ธฐ๋ฐ˜ ๊ถŒํ•œ ๋ถ€์—ฌ: ๊ณ„์ธต์  ์—ญํ•  ๋ฐ ๊ถŒํ•œ ์‹œ์Šคํ…œ ๊ตฌ์„ฑ

    โœ… ํฌ๊ด„์ ์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ: ๋ณด์•ˆ ์ด๋ฒคํŠธ ์ถ”์  ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •

    โœ… ๋ณด์•ˆ ํ…Œ์ŠคํŠธ: ์ž๋™ํ™”๋œ ๋ณด์•ˆ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„

    โœ… ์œ„ํ˜‘ ๋ชจ๋‹ˆํ„ฐ๋ง: ์‹ค์‹œ๊ฐ„ ๋ณด์•ˆ ์ด๋ฒคํŠธ ๊ฐ์ง€ ๋ฐ ๊ฒฝ๊ณ  ์ƒ์„ฑ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 03: ํ™˜๊ฒฝ ์„ค์ •์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ํ™œ์šฉํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์„ฑ
  • ์ธ์ฆ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ Azure ์„œ๋น„์Šค ์„ค์ •
  • ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ฐ ๋น„๋ฐ€ ๊ด€๋ฆฌ ๊ตฌํ˜„
  • ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ๋ณด์•ˆ ๊ตฌ์„ฑ ๊ฒ€์ฆ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    Azure ๋ณด์•ˆ

  • Azure Entra ID ๋ฌธ์„œ - ์™„์ „ํ•œ ID ํ”Œ๋žซํผ ๊ฐ€์ด๋“œ
  • Azure Key Vault - ๋น„๋ฐ€ ๊ด€๋ฆฌ ์„œ๋น„์Šค
  • Azure ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๋ณด์•ˆ ์ง€์นจ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ

  • PostgreSQL ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ - ๊ณต์‹ RLS ๋ฌธ์„œ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - PostgreSQL ๋ณด์•ˆ ๊ฐ€์ด๋“œ
  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒจํ„ด - ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ๋ณด์•ˆ ํ…Œ์ŠคํŠธ

  • OWASP ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ - ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ
  • JWT ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - JWT ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ
  • API ๋ณด์•ˆ ํ…Œ์ŠคํŠธ - API ์ „์šฉ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ
  • ---

    ์ด์ „: Lab 01: ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ๋…

    ๋‹ค์Œ: Lab 03: ํ™˜๊ฒฝ ์„ค์ •

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ๋ฅผ ํ•ด๋‹น ์–ธ์–ด๋กœ ์ž‘์„ฑ๋œ ์ƒํƒœ์—์„œ ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ, ์ธ์ฆ, ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ํ•™์Šตํ•˜๊ธฐ

    ๋ณด์•ˆ ๋ฐ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ MCP ์„œ๋ฒ„์— ๋Œ€ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ ๋ฐ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ๊ตฌํ˜„์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋ฏผ๊ฐํ•œ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ๋ณดํ˜ธํ•˜๋ฉด์„œ ์—ฌ๋Ÿฌ ํ…Œ๋„ŒํŠธ ๊ฐ„์— ์œ ์—ฐํ•œ ์ ‘๊ทผ ํŒจํ„ด์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜๋Š” ์•ˆ์ „ํ•˜๊ณ  ์ค€์ˆ˜ํ•œ ์‹œ์Šคํ…œ์„ ์„ค๊ณ„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ๊ณ ๊ฐ ๋ฐ์ดํ„ฐ, ๊ฒฐ์ œ ์ •๋ณด, ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ์†Œ๋งค ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ๋ณด์•ˆ์€ ๋งค์šฐ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ ๋ฐ ์ค€์ˆ˜ ๋ชจ๋‹ˆํ„ฐ๋ง๊นŒ์ง€ ์™„์ „ํ•œ ๋ณด์•ˆ ์•„ํ‚คํ…์ฒ˜๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    Azure ID ์„œ๋น„์Šค, PostgreSQL ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ˆ˜์ค€ ์ œ์–ด, ํฌ๊ด„์ ์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ๋ฅผ ๊ฒฐํ•ฉํ•œ ์‹ฌ์ธต ๋ฐฉ์–ด ์ „๋žต์„ ๊ตฌํ˜„ํ•˜์—ฌ ๊ฐ•๋ ฅํ•˜๊ณ  ์ค€์ˆ˜ํ•œ ํ”Œ๋žซํผ์„ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๊ตฌํ˜„: ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ
  • ์„ค๊ณ„: Azure๋ฅผ ํ™œ์šฉํ•œ ์•ˆ์ „ํ•œ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ ํŒจํ„ด
  • ๊ตฌ์„ฑ: ์ค€์ˆ˜ ์š”๊ตฌ ์‚ฌํ•ญ์„ ์œ„ํ•œ ํฌ๊ด„์ ์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ
  • ์ ์šฉ: ๋ชจ๋“  ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ณ„์ธต์—์„œ ์‹ฌ์ธต ๋ฐฉ์–ด ๋ณด์•ˆ ์ „๋žต
  • ๊ฒ€์ฆ: ์ฒด๊ณ„์ ์ธ ํ…Œ์ŠคํŠธ๋ฅผ ํ†ตํ•ด ๋ณด์•ˆ ๊ตฌํ˜„ ํ™•์ธ
  • ๋ชจ๋‹ˆํ„ฐ๋ง: ๋ณด์•ˆ ์ด๋ฒคํŠธ๋ฅผ ๊ฐ์‹œํ•˜๊ณ  ์ž ์žฌ์  ์œ„ํ˜‘์— ๋Œ€์‘
  • ๐Ÿ” ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ณด์•ˆ ์•„ํ‚คํ…์ฒ˜

    ๋ณด์•ˆ ๊ณ„์ธต ๊ฐœ์š”

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚               Azure Front Door                  โ”‚ โ† WAF, DDoS Protection
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚              Application Gateway                โ”‚ โ† SSL Termination, Rate Limiting
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚                MCP Server                       โ”‚ โ† Authentication, Authorization
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚           Connection Layer                  โ”‚ โ† Connection Pooling, Circuit Breakers
    
    โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚         Business Logic Layer               โ”‚ โ† Input Validation, Business Rules
    
    โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  โ”‚           Data Access Layer                โ”‚ โ† Query Sanitization, RLS Context
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚              PostgreSQL RLS                    โ”‚ โ† Row Level Security, Audit Triggers
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ๋ชจ๋ธ

    ์šฐ๋ฆฌ์˜ ๊ตฌํ˜„์€ ๊ณต์œ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๊ณต์œ  ์Šคํ‚ค๋งˆ ๋ชจ๋ธ๊ณผ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

    ์žฅ์ :

  • ๋น„์šฉ ํšจ์œจ์ ์ธ ์ž์› ํ™œ์šฉ
  • ๊ฐ„์†Œํ™”๋œ ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์—…๋ฐ์ดํŠธ
  • RLS๋ฅผ ํ†ตํ•œ ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ
  • ์ค€์ˆ˜ ์นœํ™”์ ์ธ ๊ฐ์‚ฌ ๊ธฐ๋ก
  • ๋‹จ์ :

  • ์‹ ์ค‘ํ•œ RLS ์ •์ฑ… ์„ค๊ณ„ ํ•„์š”
  • ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์ด ๋ชจ๋“  ํ…Œ๋„ŒํŠธ์— ์˜ํ–ฅ์„ ๋ฏธ์นจ
  • ๊ฒฌ๊ณ ํ•œ ๋ฐฑ์—…/๋ณต์› ์ ˆ์ฐจ ํ•„์š”
  • ๐Ÿ›ก๏ธ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„

    RLS ๊ธฐ์ดˆ

    
    -- Enable RLS on all multi-tenant tables
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.products ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.sales_transactions ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.sales_transaction_items ENABLE ROW LEVEL SECURITY;
    
    ALTER TABLE retail.product_embeddings ENABLE ROW LEVEL SECURITY;
    
    
    
    -- Create application role for MCP server
    
    CREATE ROLE mcp_user LOGIN;
    
    GRANT USAGE ON SCHEMA retail TO mcp_user;
    
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA retail TO mcp_user;
    
    

    ์Šคํ† ์–ด ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ

    
    -- Function to securely set store context
    
    CREATE OR REPLACE FUNCTION retail.set_store_context(store_id_param VARCHAR(50))
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    SET search_path = retail, pg_temp
    
    AS $$
    
    DECLARE
    
        user_info RECORD;
    
    BEGIN
    
        -- Validate store exists and is active
    
        SELECT store_id, store_name, is_active 
    
        INTO user_info
    
        FROM retail.stores 
    
        WHERE store_id = store_id_param;
    
        
    
        IF NOT FOUND THEN
    
            RAISE EXCEPTION 'Store not found: %', store_id_param
    
                USING ERRCODE = 'invalid_parameter_value',
    
                      HINT = 'Verify store ID and ensure it exists in the system';
    
        END IF;
    
        
    
        IF NOT user_info.is_active THEN
    
            RAISE EXCEPTION 'Store is inactive: %', store_id_param
    
                USING ERRCODE = 'insufficient_privilege',
    
                      HINT = 'Contact administrator to activate store';
    
        END IF;
    
        
    
        -- Set the secure context
    
        PERFORM set_config('app.current_store_id', store_id_param, false);
    
        PERFORM set_config('app.store_name', user_info.store_name, false);
    
        PERFORM set_config('app.context_set_at', extract(epoch from current_timestamp)::text, false);
    
        
    
        -- Log context change for audit
    
        INSERT INTO retail.security_audit_log (
    
            event_type,
    
            user_name,
    
            store_id,
    
            ip_address,
    
            user_agent,
    
            details,
    
            severity
    
        ) VALUES (
    
            'store_context_set',
    
            current_user,
    
            store_id_param,
    
            inet_client_addr()::text,
    
            current_setting('application_name', true),
    
            jsonb_build_object(
    
                'store_name', user_info.store_name,
    
                'timestamp', current_timestamp,
    
                'session_id', pg_backend_pid()
    
            ),
    
            'INFO'
    
        );
    
    END;
    
    $$;
    
    
    
    -- Grant execute to MCP user
    
    GRANT EXECUTE ON FUNCTION retail.set_store_context TO mcp_user;
    
    

    RLS ์ •์ฑ…

    
    -- Customers RLS Policy
    
    CREATE POLICY customers_store_isolation ON retail.customers
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Products RLS Policy with additional business rules
    
    CREATE POLICY products_store_isolation ON retail.products
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
            AND is_active = TRUE  -- Additional business rule
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Sales Transactions RLS Policy
    
    CREATE POLICY sales_transactions_store_isolation ON retail.sales_transactions
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    
    
    -- Transaction Items RLS Policy (via join)
    
    CREATE POLICY sales_transaction_items_store_isolation ON retail.sales_transaction_items
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        )
    
        WITH CHECK (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        );
    
    
    
    -- Product Embeddings RLS Policy
    
    CREATE POLICY product_embeddings_store_isolation ON retail.product_embeddings
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        )
    
        WITH CHECK (
    
            store_id = current_setting('app.current_store_id', true)
    
            AND current_setting('app.current_store_id', true) IS NOT NULL
    
            AND current_setting('app.current_store_id', true) != ''
    
        );
    
    

    RLS ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ

    
    -- Test RLS policies with different store contexts
    
    DO $$
    
    DECLARE
    
        test_result RECORD;
    
        customer_count INTEGER;
    
        product_count INTEGER;
    
    BEGIN
    
        -- Test Seattle store context
    
        PERFORM retail.set_store_context('seattle');
    
        
    
        SELECT COUNT(*) INTO customer_count FROM retail.customers;
    
        SELECT COUNT(*) INTO product_count FROM retail.products;
    
        
    
        RAISE NOTICE 'Seattle store - Customers: %, Products: %', customer_count, product_count;
    
        
    
        -- Test Redmond store context
    
        PERFORM retail.set_store_context('redmond');
    
        
    
        SELECT COUNT(*) INTO customer_count FROM retail.customers;
    
        SELECT COUNT(*) INTO product_count FROM retail.products;
    
        
    
        RAISE NOTICE 'Redmond store - Customers: %, Products: %', customer_count, product_count;
    
        
    
        -- Verify data isolation
    
        IF customer_count > 0 AND product_count > 0 THEN
    
            RAISE NOTICE 'RLS policies are working correctly';
    
        ELSE
    
            RAISE WARNING 'RLS policies may not be configured correctly';
    
        END IF;
    
    END;
    
    $$;
    
    

    ๐Ÿ”‘ ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ

    Azure Entra ID ํ†ตํ•ฉ

    
    # mcp_server/security/authentication.py
    
    """
    
    Azure Entra ID authentication for MCP server.
    
    """
    
    import os
    
    import jwt
    
    import aiohttp
    
    import asyncio
    
    from typing import Dict, Optional, List
    
    from datetime import datetime, timezone
    
    from azure.identity.aio import DefaultAzureCredential
    
    from azure.keyvault.secrets.aio import SecretClient
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class AzureAuthenticator:
    
        """Handle Azure Entra ID authentication and token validation."""
    
        
    
        def __init__(self):
    
            self.tenant_id = os.getenv('AZURE_TENANT_ID')
    
            self.client_id = os.getenv('AZURE_CLIENT_ID')
    
            self.audience = os.getenv('AZURE_AUDIENCE', self.client_id)
    
            self.issuer = f"https://login.microsoftonline.com/{self.tenant_id}/v2.0"
    
            
    
            # Cache for JWKS (JSON Web Key Set)
    
            self._jwks_cache = None
    
            self._jwks_cache_expiry = None
    
            
    
            # Key Vault for secrets
    
            self.key_vault_url = os.getenv('AZURE_KEY_VAULT_URL')
    
            self.credential = DefaultAzureCredential()
    
            
    
            if self.key_vault_url:
    
                self.secret_client = SecretClient(
    
                    vault_url=self.key_vault_url,
    
                    credential=self.credential
    
                )
    
        
    
        async def validate_token(self, token: str) -> Dict:
    
            """Validate JWT token from Azure Entra ID."""
    
            
    
            try:
    
                # Get signing keys
    
                signing_keys = await self._get_signing_keys()
    
                
    
                # Decode token header to get key ID
    
                unverified_header = jwt.get_unverified_header(token)
    
                key_id = unverified_header.get('kid')
    
                
    
                if not key_id:
    
                    raise ValueError("Token missing key ID")
    
                
    
                # Find the corresponding key
    
                signing_key = None
    
                for key in signing_keys:
    
                    if key['kid'] == key_id:
    
                        signing_key = jwt.algorithms.RSAAlgorithm.from_jwk(key)
    
                        break
    
                
    
                if not signing_key:
    
                    raise ValueError(f"Unable to find signing key for kid: {key_id}")
    
                
    
                # Validate and decode token
    
                payload = jwt.decode(
    
                    token,
    
                    signing_key,
    
                    algorithms=['RS256'],
    
                    audience=self.audience,
    
                    issuer=self.issuer,
    
                    options={
    
                        'verify_exp': True,
    
                        'verify_aud': True,
    
                        'verify_iss': True
    
                    }
    
                )
    
                
    
                # Extract user information
    
                user_info = self._extract_user_info(payload)
    
                
    
                # Log successful authentication
    
                logger.info(
    
                    "User authenticated successfully",
    
                    extra={
    
                        'user_id': user_info['user_id'],
    
                        'email': user_info.get('email'),
    
                        'tenant_id': payload.get('tid')
    
                    }
    
                )
    
                
    
                return user_info
    
                
    
            except jwt.ExpiredSignatureError:
    
                logger.warning("Token has expired")
    
                raise ValueError("Token has expired")
    
            except jwt.InvalidAudienceError:
    
                logger.warning(f"Invalid audience in token. Expected: {self.audience}")
    
                raise ValueError("Invalid token audience")
    
            except jwt.InvalidIssuerError:
    
                logger.warning(f"Invalid issuer in token. Expected: {self.issuer}")
    
                raise ValueError("Invalid token issuer")
    
            except Exception as e:
    
                logger.error(f"Token validation failed: {str(e)}")
    
                raise ValueError(f"Token validation failed: {str(e)}")
    
        
    
        async def _get_signing_keys(self) -> List[Dict]:
    
            """Get JWKS from Azure Entra ID with caching."""
    
            
    
            current_time = datetime.now(timezone.utc)
    
            
    
            # Check if cache is valid
    
            if (self._jwks_cache and self._jwks_cache_expiry and 
    
                current_time < self._jwks_cache_expiry):
    
                return self._jwks_cache
    
            
    
            # Fetch new JWKS
    
            jwks_url = f"{self.issuer}/keys"
    
            
    
            async with aiohttp.ClientSession() as session:
    
                async with session.get(jwks_url) as response:
    
                    if response.status != 200:
    
                        raise Exception(f"Failed to fetch JWKS: {response.status}")
    
                    
    
                    jwks_data = await response.json()
    
                    
    
            # Cache for 1 hour
    
            self._jwks_cache = jwks_data['keys']
    
            self._jwks_cache_expiry = current_time.replace(
    
                hour=current_time.hour + 1
    
            )
    
            
    
            return self._jwks_cache
    
        
    
        def _extract_user_info(self, payload: Dict) -> Dict:
    
            """Extract user information from JWT payload."""
    
            
    
            return {
    
                'user_id': payload.get('oid') or payload.get('sub'),
    
                'email': payload.get('email') or payload.get('preferred_username'),
    
                'name': payload.get('name'),
    
                'tenant_id': payload.get('tid'),
    
                'roles': payload.get('roles', []),
    
                'groups': payload.get('groups', []),
    
                'app_roles': payload.get('app_roles', []),
    
                'scope': payload.get('scp', '').split() if payload.get('scp') else [],
    
                'expires_at': datetime.fromtimestamp(payload['exp'], timezone.utc),
    
                'issued_at': datetime.fromtimestamp(payload['iat'], timezone.utc)
    
            }
    
        
    
        async def get_user_store_access(self, user_id: str) -> List[str]:
    
            """Get list of stores the user has access to."""
    
            
    
            try:
    
                # This would typically query your user/store mapping
    
                # For demo, we'll use a simple Key Vault secret
    
                secret_name = f"user-{user_id}-stores"
    
                
    
                if self.secret_client:
    
                    secret = await self.secret_client.get_secret(secret_name)
    
                    store_list = secret.value.split(',')
    
                    return [store.strip() for store in store_list if store.strip()]
    
                
    
                # Fallback: return default store access
    
                logger.warning(f"No store mapping found for user {user_id}, using default")
    
                return ['seattle']  # Default store access
    
                
    
            except Exception as e:
    
                logger.error(f"Failed to get store access for user {user_id}: {e}")
    
                return []  # No access if we can't determine stores
    
    
    
    # Global authenticator instance
    
    azure_authenticator = AzureAuthenticator()
    
    

    ๊ถŒํ•œ ๋ถ€์—ฌ ๋ฏธ๋“ค์›จ์–ด

    
    # mcp_server/security/authorization.py
    
    """
    
    Authorization middleware and decorators for MCP server.
    
    """
    
    import functools
    
    from typing import Dict, List, Optional, Callable, Any
    
    from fastapi import HTTPException, status, Request
    
    from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    security = HTTPBearer()
    
    
    
    class AuthorizationError(Exception):
    
        """Custom authorization error."""
    
        pass
    
    
    
    class RoleBasedAuth:
    
        """Role-based access control implementation."""
    
        
    
        # Define role hierarchy
    
        ROLE_HIERARCHY = {
    
            'store_admin': ['store_manager', 'store_user', 'store_readonly'],
    
            'store_manager': ['store_user', 'store_readonly'],
    
            'store_user': ['store_readonly'],
    
            'store_readonly': []
    
        }
    
        
    
        # Define permissions for each role
    
        ROLE_PERMISSIONS = {
    
            'store_admin': [
    
                'read_all', 'write_all', 'delete_all', 'manage_users'
    
            ],
    
            'store_manager': [
    
                'read_all', 'write_transactions', 'write_inventory', 'read_reports'
    
            ],
    
            'store_user': [
    
                'read_products', 'read_customers', 'write_transactions'
    
            ],
    
            'store_readonly': [
    
                'read_products', 'read_basic_reports'
    
            ]
    
        }
    
        
    
        @classmethod
    
        def has_permission(cls, user_roles: List[str], required_permission: str) -> bool:
    
            """Check if user has required permission."""
    
            
    
            user_permissions = set()
    
            
    
            for role in user_roles:
    
                # Add direct permissions
    
                user_permissions.update(cls.ROLE_PERMISSIONS.get(role, []))
    
                
    
                # Add inherited permissions
    
                inherited_roles = cls.ROLE_HIERARCHY.get(role, [])
    
                for inherited_role in inherited_roles:
    
                    user_permissions.update(cls.ROLE_PERMISSIONS.get(inherited_role, []))
    
            
    
            return required_permission in user_permissions
    
        
    
        @classmethod
    
        def get_user_stores(cls, user_info: Dict) -> List[str]:
    
            """Extract stores user has access to from user info."""
    
            
    
            # This would typically come from your user management system
    
            # For demo, we'll extract from custom claims or groups
    
            
    
            stores = []
    
            
    
            # Check for direct store assignments in groups
    
            for group in user_info.get('groups', []):
    
                if group.startswith('store_'):
    
                    store_id = group.replace('store_', '')
    
                    stores.append(store_id)
    
            
    
            # Check for app-specific roles
    
            for role in user_info.get('app_roles', []):
    
                if 'store:' in role:
    
                    _, store_id = role.split('store:', 1)
    
                    stores.append(store_id)
    
            
    
            return list(set(stores))  # Remove duplicates
    
    
    
    def require_auth(required_permission: str = None, require_store_access: bool = True):
    
        """Decorator to require authentication and authorization."""
    
        
    
        def decorator(func: Callable) -> Callable:
    
            @functools.wraps(func)
    
            async def wrapper(*args, **kwargs):
    
                # Extract request from args (FastAPI dependency injection)
    
                request = None
    
                for arg in args:
    
                    if isinstance(arg, Request):
    
                        request = arg
    
                        break
    
                
    
                if not request:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    
                        detail="Request object not found"
    
                    )
    
                
    
                # Get authorization header
    
                auth_header = request.headers.get('Authorization')
    
                if not auth_header or not auth_header.startswith('Bearer '):
    
                    raise HTTPException(
    
                        status_code=status.HTTP_401_UNAUTHORIZED,
    
                        detail="Missing or invalid authorization header",
    
                        headers={"WWW-Authenticate": "Bearer"}
    
                    )
    
                
    
                token = auth_header.split(' ')[1]
    
                
    
                try:
    
                    # Validate token
    
                    user_info = await azure_authenticator.validate_token(token)
    
                    
    
                    # Check required permission
    
                    if required_permission:
    
                        user_roles = user_info.get('roles', [])
    
                        if not RoleBasedAuth.has_permission(user_roles, required_permission):
    
                            raise HTTPException(
    
                                status_code=status.HTTP_403_FORBIDDEN,
    
                                detail=f"Insufficient permissions. Required: {required_permission}"
    
                            )
    
                    
    
                    # Check store access
    
                    if require_store_access:
    
                        user_stores = RoleBasedAuth.get_user_stores(user_info)
    
                        if not user_stores:
    
                            raise HTTPException(
    
                                status_code=status.HTTP_403_FORBIDDEN,
    
                                detail="No store access configured for user"
    
                            )
    
                        
    
                        # Set default store context (first accessible store)
    
                        request.state.current_store = user_stores[0]
    
                        request.state.accessible_stores = user_stores
    
                    
    
                    # Add user info to request state
    
                    request.state.user_info = user_info
    
                    request.state.user_id = user_info['user_id']
    
                    
    
                    # Call the original function
    
                    return await func(*args, **kwargs)
    
                    
    
                except ValueError as e:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_401_UNAUTHORIZED,
    
                        detail=str(e),
    
                        headers={"WWW-Authenticate": "Bearer"}
    
                    )
    
                except AuthorizationError as e:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_403_FORBIDDEN,
    
                        detail=str(e)
    
                    )
    
            
    
            return wrapper
    
        return decorator
    
    
    
    def require_store_context(store_param: str = 'store_id'):
    
        """Decorator to validate and set store context."""
    
        
    
        def decorator(func: Callable) -> Callable:
    
            @functools.wraps(func)
    
            async def wrapper(*args, **kwargs):
    
                # Get store_id from kwargs
    
                store_id = kwargs.get(store_param)
    
                
    
                if not store_id:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_400_BAD_REQUEST,
    
                        detail=f"Missing required parameter: {store_param}"
    
                    )
    
                
    
                # Extract request from args
    
                request = None
    
                for arg in args:
    
                    if isinstance(arg, Request):
    
                        request = arg
    
                        break
    
                
    
                if not request or not hasattr(request.state, 'accessible_stores'):
    
                    raise HTTPException(
    
                        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
    
                        detail="Authentication required before store context validation"
    
                    )
    
                
    
                # Validate user has access to requested store
    
                if store_id not in request.state.accessible_stores:
    
                    raise HTTPException(
    
                        status_code=status.HTTP_403_FORBIDDEN,
    
                        detail=f"Access denied to store: {store_id}"
    
                    )
    
                
    
                # Set store context in request state
    
                request.state.current_store = store_id
    
                
    
                return await func(*args, **kwargs)
    
            
    
            return wrapper
    
        return decorator
    
    

    ๐Ÿ” ๋ณด์•ˆ ๊ฐ์‚ฌ ๋ฐ ์ค€์ˆ˜

    ํฌ๊ด„์ ์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ

    
    -- Security audit log table
    
    CREATE TABLE retail.security_audit_log (
    
        log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        event_type VARCHAR(100) NOT NULL,
    
        user_name VARCHAR(100) NOT NULL,
    
        user_id VARCHAR(100),
    
        store_id VARCHAR(50),
    
        ip_address INET,
    
        user_agent TEXT,
    
        request_id VARCHAR(100),
    
        session_id VARCHAR(100),
    
        resource_type VARCHAR(100),
    
        resource_id VARCHAR(100),
    
        action VARCHAR(50) NOT NULL,
    
        success BOOLEAN NOT NULL DEFAULT TRUE,
    
        failure_reason TEXT,
    
        details JSONB,
    
        severity VARCHAR(20) DEFAULT 'INFO',
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure proper indexing for security queries
    
        CONSTRAINT valid_severity CHECK (severity IN ('DEBUG', 'INFO', 'WARN', 'ERROR', 'CRITICAL'))
    
    );
    
    
    
    -- Indexes for security audit queries
    
    CREATE INDEX idx_security_audit_event_type ON retail.security_audit_log(event_type);
    
    CREATE INDEX idx_security_audit_user_name ON retail.security_audit_log(user_name);
    
    CREATE INDEX idx_security_audit_store_id ON retail.security_audit_log(store_id);
    
    CREATE INDEX idx_security_audit_created_at ON retail.security_audit_log(created_at);
    
    CREATE INDEX idx_security_audit_success ON retail.security_audit_log(success);
    
    CREATE INDEX idx_security_audit_severity ON retail.security_audit_log(severity);
    
    CREATE INDEX idx_security_audit_details ON retail.security_audit_log USING GIN(details);
    
    
    
    -- Function to log security events
    
    CREATE OR REPLACE FUNCTION retail.log_security_event(
    
        p_event_type VARCHAR(100),
    
        p_user_name VARCHAR(100),
    
        p_user_id VARCHAR(100) DEFAULT NULL,
    
        p_store_id VARCHAR(50) DEFAULT NULL,
    
        p_ip_address TEXT DEFAULT NULL,
    
        p_action VARCHAR(50) DEFAULT 'unknown',
    
        p_success BOOLEAN DEFAULT TRUE,
    
        p_failure_reason TEXT DEFAULT NULL,
    
        p_details JSONB DEFAULT NULL,
    
        p_severity VARCHAR(20) DEFAULT 'INFO'
    
    )
    
    RETURNS UUID
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    DECLARE
    
        log_id UUID;
    
    BEGIN
    
        INSERT INTO retail.security_audit_log (
    
            event_type,
    
            user_name,
    
            user_id,
    
            store_id,
    
            ip_address,
    
            action,
    
            success,
    
            failure_reason,
    
            details,
    
            severity
    
        ) VALUES (
    
            p_event_type,
    
            p_user_name,
    
            p_user_id,
    
            p_store_id,
    
            p_ip_address::INET,
    
            p_action,
    
            p_success,
    
            p_failure_reason,
    
            p_details,
    
            p_severity
    
        ) RETURNING log_id INTO log_id;
    
        
    
        RETURN log_id;
    
    END;
    
    $$;
    
    
    
    -- Grant execute to MCP user
    
    GRANT EXECUTE ON FUNCTION retail.log_security_event TO mcp_user;
    
    

    ๋ณด์•ˆ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ทฐ

    
    -- Failed authentication attempts
    
    CREATE VIEW retail.security_failed_auth AS
    
    SELECT 
    
        event_type,
    
        user_name,
    
        ip_address,
    
        COUNT(*) as attempt_count,
    
        MIN(created_at) as first_attempt,
    
        MAX(created_at) as last_attempt,
    
        ARRAY_AGG(DISTINCT failure_reason) as failure_reasons
    
    FROM retail.security_audit_log
    
    WHERE success = FALSE 
    
      AND event_type IN ('authentication_failed', 'token_validation_failed')
    
      AND created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
    
    GROUP BY event_type, user_name, ip_address
    
    HAVING COUNT(*) >= 3  -- 3 or more failures
    
    ORDER BY attempt_count DESC, last_attempt DESC;
    
    
    
    -- Suspicious access patterns
    
    CREATE VIEW retail.security_suspicious_access AS
    
    SELECT 
    
        user_name,
    
        user_id,
    
        COUNT(DISTINCT ip_address) as ip_count,
    
        COUNT(DISTINCT store_id) as store_count,
    
        ARRAY_AGG(DISTINCT ip_address::TEXT) as ip_addresses,
    
        ARRAY_AGG(DISTINCT store_id) as stores_accessed,
    
        MIN(created_at) as first_access,
    
        MAX(created_at) as last_access
    
    FROM retail.security_audit_log
    
    WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
      AND success = TRUE
    
    GROUP BY user_name, user_id
    
    HAVING COUNT(DISTINCT ip_address) > 3  -- Access from multiple IPs
    
       OR COUNT(DISTINCT store_id) > 2     -- Access to multiple stores
    
    ORDER BY ip_count DESC, store_count DESC;
    
    
    
    -- Data access patterns
    
    CREATE VIEW retail.security_data_access_summary AS
    
    SELECT 
    
        DATE_TRUNC('hour', created_at) as access_hour,
    
        store_id,
    
        resource_type,
    
        action,
    
        COUNT(*) as access_count,
    
        COUNT(DISTINCT user_id) as unique_users
    
    FROM retail.security_audit_log
    
    WHERE resource_type IS NOT NULL
    
      AND created_at >= CURRENT_TIMESTAMP - INTERVAL '24 hours'
    
    GROUP BY DATE_TRUNC('hour', created_at), store_id, resource_type, action
    
    ORDER BY access_hour DESC, access_count DESC;
    
    

    ๋ณด์•ˆ ์ด๋ฒคํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง

    
    # mcp_server/security/monitoring.py
    
    """
    
    Security monitoring and alerting for MCP server.
    
    """
    
    import asyncio
    
    import asyncpg
    
    from typing import Dict, List, Any
    
    from datetime import datetime, timedelta
    
    from dataclasses import dataclass
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    @dataclass
    
    class SecurityAlert:
    
        """Security alert data structure."""
    
        alert_type: str
    
        severity: str
    
        message: str
    
        details: Dict[str, Any]
    
        timestamp: datetime
    
    
    
    class SecurityMonitor:
    
        """Monitor security events and generate alerts."""
    
        
    
        def __init__(self, db_connection_string: str):
    
            self.db_connection_string = db_connection_string
    
            self.alert_handlers = []
    
            
    
            # Alert thresholds
    
            self.thresholds = {
    
                'failed_auth_attempts': 5,      # per user per hour
    
                'multiple_ip_access': 3,        # different IPs per user per hour
    
                'excessive_data_access': 1000,  # queries per user per hour
    
                'privilege_escalation': 1,      # any attempt
    
                'unauthorized_store_access': 1  # any attempt
    
            }
    
        
    
        async def start_monitoring(self):
    
            """Start security monitoring loop."""
    
            logger.info("Starting security monitoring")
    
            
    
            while True:
    
                try:
    
                    await self._check_security_events()
    
                    await asyncio.sleep(300)  # Check every 5 minutes
    
                except Exception as e:
    
                    logger.error(f"Security monitoring error: {e}")
    
                    await asyncio.sleep(60)  # Short retry on error
    
        
    
        async def _check_security_events(self):
    
            """Check for security events and generate alerts."""
    
            
    
            conn = await asyncpg.connect(self.db_connection_string)
    
            
    
            try:
    
                # Check failed authentication attempts
    
                await self._check_failed_auth(conn)
    
                
    
                # Check suspicious access patterns
    
                await self._check_suspicious_access(conn)
    
                
    
                # Check data access anomalies
    
                await self._check_data_access_anomalies(conn)
    
                
    
                # Check unauthorized access attempts
    
                await self._check_unauthorized_access(conn)
    
                
    
            finally:
    
                await conn.close()
    
        
    
        async def _check_failed_auth(self, conn):
    
            """Check for excessive failed authentication attempts."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                ip_address,
    
                COUNT(*) as attempt_count,
    
                MAX(created_at) as last_attempt
    
            FROM retail.security_audit_log
    
            WHERE success = FALSE 
    
              AND event_type IN ('authentication_failed', 'token_validation_failed')
    
              AND created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
            GROUP BY user_name, ip_address
    
            HAVING COUNT(*) >= $1
    
            """
    
            
    
            results = await conn.fetch(query, self.thresholds['failed_auth_attempts'])
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='failed_authentication',
    
                    severity='HIGH',
    
                    message=f"Excessive failed login attempts for user {record['user_name']}",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'ip_address': str(record['ip_address']),
    
                        'attempt_count': record['attempt_count'],
    
                        'last_attempt': record['last_attempt'].isoformat()
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _check_suspicious_access(self, conn):
    
            """Check for suspicious access patterns."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                user_id,
    
                COUNT(DISTINCT ip_address) as ip_count,
    
                ARRAY_AGG(DISTINCT ip_address::TEXT) as ip_addresses
    
            FROM retail.security_audit_log
    
            WHERE created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
              AND success = TRUE
    
            GROUP BY user_name, user_id
    
            HAVING COUNT(DISTINCT ip_address) >= $1
    
            """
    
            
    
            results = await conn.fetch(query, self.thresholds['multiple_ip_access'])
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='suspicious_access',
    
                    severity='MEDIUM',
    
                    message=f"User {record['user_name']} accessed from multiple IP addresses",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'user_id': record['user_id'],
    
                        'ip_count': record['ip_count'],
    
                        'ip_addresses': record['ip_addresses']
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _check_unauthorized_access(self, conn):
    
            """Check for unauthorized store access attempts."""
    
            
    
            query = """
    
            SELECT 
    
                user_name,
    
                user_id,
    
                store_id,
    
                failure_reason,
    
                created_at
    
            FROM retail.security_audit_log
    
            WHERE success = FALSE 
    
              AND event_type = 'unauthorized_store_access'
    
              AND created_at >= CURRENT_TIMESTAMP - INTERVAL '1 hour'
    
            """
    
            
    
            results = await conn.fetch(query)
    
            
    
            for record in results:
    
                alert = SecurityAlert(
    
                    alert_type='unauthorized_access',
    
                    severity='HIGH',
    
                    message=f"Unauthorized store access attempt by {record['user_name']}",
    
                    details={
    
                        'user_name': record['user_name'],
    
                        'user_id': record['user_id'],
    
                        'store_id': record['store_id'],
    
                        'failure_reason': record['failure_reason'],
    
                        'timestamp': record['created_at'].isoformat()
    
                    },
    
                    timestamp=datetime.now()
    
                )
    
                
    
                await self._send_alert(alert)
    
        
    
        async def _send_alert(self, alert: SecurityAlert):
    
            """Send security alert to all configured handlers."""
    
            
    
            logger.warning(
    
                f"Security Alert: {alert.alert_type} - {alert.message}",
    
                extra={'alert_details': alert.details}
    
            )
    
            
    
            # Send to configured alert handlers
    
            for handler in self.alert_handlers:
    
                try:
    
                    await handler.send_alert(alert)
    
                except Exception as e:
    
                    logger.error(f"Failed to send alert via {handler.__class__.__name__}: {e}")
    
        
    
        def add_alert_handler(self, handler):
    
            """Add alert handler."""
    
            self.alert_handlers.append(handler)
    
    

    ๐Ÿงช ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋ฐ ๊ฒ€์ฆ

    ์ž๋™ํ™”๋œ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ

    
    # tests/security/test_security.py
    
    """
    
    Comprehensive security tests for MCP server.
    
    """
    
    import pytest
    
    import asyncio
    
    import asyncpg
    
    from datetime import datetime, timezone
    
    import jwt
    
    from unittest.mock import Mock, patch
    
    
    
    class TestRowLevelSecurity:
    
        """Test Row Level Security implementation."""
    
        
    
        @pytest.fixture
    
        async def db_connection(self):
    
            """Database connection for testing."""
    
            conn = await asyncpg.connect(
    
                "postgresql://mcp_user:password@localhost:5432/retail_test"
    
            )
    
            yield conn
    
            await conn.close()
    
        
    
        async def test_store_context_isolation(self, db_connection):
    
            """Test that RLS properly isolates data by store."""
    
            
    
            # Set Seattle store context
    
            await db_connection.execute("SELECT retail.set_store_context('seattle')")
    
            
    
            # Get customer count
    
            seattle_customers = await db_connection.fetchval(
    
                "SELECT COUNT(*) FROM retail.customers"
    
            )
    
            
    
            # Set Redmond store context
    
            await db_connection.execute("SELECT retail.set_store_context('redmond')")
    
            
    
            # Get customer count
    
            redmond_customers = await db_connection.fetchval(
    
                "SELECT COUNT(*) FROM retail.customers"
    
            )
    
            
    
            # Verify isolation (counts should be different)
    
            assert seattle_customers != redmond_customers or (
    
                seattle_customers == 0 and redmond_customers == 0
    
            )
    
        
    
        async def test_unauthorized_store_access(self, db_connection):
    
            """Test that invalid store access is blocked."""
    
            
    
            with pytest.raises(Exception) as exc_info:
    
                await db_connection.execute("SELECT retail.set_store_context('invalid_store')")
    
            
    
            assert "Store not found" in str(exc_info.value)
    
        
    
        async def test_cross_store_data_leakage(self, db_connection):
    
            """Test that users cannot access data from other stores."""
    
            
    
            # Set context to one store
    
            await db_connection.execute("SELECT retail.set_store_context('seattle')")
    
            
    
            # Try to insert data with different store_id
    
            with pytest.raises(Exception):
    
                await db_connection.execute("""
    
                    INSERT INTO retail.customers (store_id, first_name, last_name, email)
    
                    VALUES ('redmond', 'Test', 'User', 'test@example.com')
    
                """)
    
    
    
    class TestAuthentication:
    
        """Test authentication and authorization."""
    
        
    
        def test_valid_jwt_token(self):
    
            """Test valid JWT token validation."""
    
            
    
            # Mock valid token
    
            token_payload = {
    
                'oid': 'user-123',
    
                'email': 'test@example.com',
    
                'name': 'Test User',
    
                'tid': 'tenant-123',
    
                'aud': 'app-client-id',
    
                'iss': 'https://login.microsoftonline.com/tenant-123/v2.0',
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) + 3600,
    
                'iat': int((datetime.now(timezone.utc)).timestamp()),
    
                'roles': ['store_user']
    
            }
    
            
    
            # This would require mocking the JWKS endpoint
    
            # In real implementation, use proper test JWT tokens
    
            
    
        def test_expired_token_rejection(self):
    
            """Test that expired tokens are rejected."""
    
            
    
            token_payload = {
    
                'oid': 'user-123',
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) - 3600,  # Expired
    
                'iat': int((datetime.now(timezone.utc)).timestamp()) - 7200
    
            }
    
            
    
            # Test would verify that expired tokens are rejected
    
            
    
        def test_invalid_audience_rejection(self):
    
            """Test that tokens with wrong audience are rejected."""
    
            
    
            token_payload = {
    
                'oid': 'user-123',
    
                'aud': 'wrong-audience',  # Invalid audience
    
                'exp': int((datetime.now(timezone.utc)).timestamp()) + 3600,
    
                'iat': int((datetime.now(timezone.utc)).timestamp())
    
            }
    
            
    
            # Test would verify that wrong audience tokens are rejected
    
    
    
    class TestAuthorization:
    
        """Test role-based authorization."""
    
        
    
        def test_role_hierarchy(self):
    
            """Test that role hierarchy works correctly."""
    
            
    
            from mcp_server.security.authorization import RoleBasedAuth
    
            
    
            # Store admin should have all permissions
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'read_all')
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'write_all')
    
            assert RoleBasedAuth.has_permission(['store_admin'], 'delete_all')
    
            
    
            # Store user should have limited permissions
    
            assert RoleBasedAuth.has_permission(['store_user'], 'read_products')
    
            assert not RoleBasedAuth.has_permission(['store_user'], 'delete_all')
    
            
    
            # Store readonly should have minimal permissions
    
            assert RoleBasedAuth.has_permission(['store_readonly'], 'read_products')
    
            assert not RoleBasedAuth.has_permission(['store_readonly'], 'write_transactions')
    
        
    
        def test_permission_inheritance(self):
    
            """Test that permissions are properly inherited."""
    
            
    
            from mcp_server.security.authorization import RoleBasedAuth
    
            
    
            # Manager should inherit user permissions
    
            assert RoleBasedAuth.has_permission(['store_manager'], 'read_products')
    
            assert RoleBasedAuth.has_permission(['store_manager'], 'write_transactions')
    
    
    
    # Security test runner
    
    if __name__ == "__main__":
    
        pytest.main([__file__, "-v"])
    
    

    ์นจํˆฌ ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

    
    # security-test-checklist.yml
    
    penetration_testing:
    
      
    
      authentication_bypass:
    
        - name: "Test authentication bypass attempts"
    
          tests:
    
            - "Missing Authorization header"
    
            - "Malformed JWT tokens"
    
            - "Replay attack with expired tokens"
    
            - "Token signature manipulation"
    
            - "Audience/issuer manipulation"
    
        
    
      authorization_escalation:
    
        - name: "Test privilege escalation attempts"
    
          tests:
    
            - "Role manipulation in token"
    
            - "Store access boundary testing"
    
            - "Cross-tenant data access attempts"
    
            - "Administrative function access"
    
        
    
      sql_injection:
    
        - name: "Test SQL injection vulnerabilities"
    
          tests:
    
            - "Parameter injection in search queries"
    
            - "Store ID manipulation"
    
            - "JSON parameter injection"
    
            - "Union-based injection attempts"
    
        
    
      data_exposure:
    
        - name: "Test for data exposure vulnerabilities"
    
          tests:
    
            - "Error message information disclosure"
    
            - "Timing attack possibilities"
    
            - "Cross-store data leakage"
    
            - "Audit log exposure"
    
        
    
      rate_limiting:
    
        - name: "Test rate limiting and DoS protection"
    
          tests:
    
            - "Authentication endpoint flooding"
    
            - "API endpoint rate limits"
    
            - "Resource exhaustion attempts"
    
            - "Connection pool exhaustion"
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ณด์•ˆ: ์™„์ „ํ•œ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„

    โœ… Azure ์ธ์ฆ: Azure Entra ID์™€ JWT ๊ฒ€์ฆ ํ†ตํ•ฉ

    โœ… ์—ญํ•  ๊ธฐ๋ฐ˜ ๊ถŒํ•œ ๋ถ€์—ฌ: ๊ณ„์ธต์  ์—ญํ•  ๋ฐ ๊ถŒํ•œ ์‹œ์Šคํ…œ ๊ตฌ์„ฑ

    โœ… ํฌ๊ด„์ ์ธ ๊ฐ์‚ฌ ๋กœ๊ทธ: ๋ณด์•ˆ ์ด๋ฒคํŠธ ์ถ”์  ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •

    โœ… ๋ณด์•ˆ ํ…Œ์ŠคํŠธ: ์ž๋™ํ™”๋œ ๋ณด์•ˆ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ ๊ตฌํ˜„

    โœ… ์œ„ํ˜‘ ๋ชจ๋‹ˆํ„ฐ๋ง: ์‹ค์‹œ๊ฐ„ ๋ณด์•ˆ ์ด๋ฒคํŠธ ๊ฐ์ง€ ๋ฐ ๊ฒฝ๊ณ  ์ƒ์„ฑ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 03: ํ™˜๊ฒฝ ์„ค์ •์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ํ™œ์šฉํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ์„ฑ
  • ์ธ์ฆ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ Azure ์„œ๋น„์Šค ์„ค์ •
  • ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ฐ ๋น„๋ฐ€ ๊ด€๋ฆฌ ๊ตฌํ˜„
  • ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ ๋ณด์•ˆ ๊ตฌ์„ฑ ๊ฒ€์ฆ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    Azure ๋ณด์•ˆ

  • Azure Entra ID ๋ฌธ์„œ - ์™„์ „ํ•œ ID ํ”Œ๋žซํผ ๊ฐ€์ด๋“œ
  • Azure Key Vault - ๋น„๋ฐ€ ๊ด€๋ฆฌ ์„œ๋น„์Šค
  • Azure ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๋ณด์•ˆ ์ง€์นจ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ

  • PostgreSQL ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ - ๊ณต์‹ RLS ๋ฌธ์„œ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ - PostgreSQL ๋ณด์•ˆ ๊ฐ€์ด๋“œ
  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŒจํ„ด - ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ๋ณด์•ˆ ํ…Œ์ŠคํŠธ

  • OWASP ํ…Œ์ŠคํŠธ ๊ฐ€์ด๋“œ - ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ
  • JWT ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - JWT ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ
  • API ๋ณด์•ˆ ํ…Œ์ŠคํŠธ - API ์ „์šฉ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ
  • ---

    ์ด์ „: Lab 01: ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ๋…

    ๋‹ค์Œ: Lab 03: ํ™˜๊ฒฝ ์„ค์ •

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ๋ฅผ ํ•ด๋‹น ์–ธ์–ด๋กœ ์ž‘์„ฑ๋œ ์ƒํƒœ์—์„œ ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    03 ํ™˜๊ฒฝ ์„ค์ •

    ํ™˜๊ฒฝ ์„ค์ •

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ PostgreSQL ํ†ตํ•ฉ์„ ํ†ตํ•ด MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ ์™„์ „ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์„ค์ •ํ•˜๋Š” ๊ณผ์ •์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ๋ชจ๋“  ๋„๊ตฌ๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ , Azure ๋ฆฌ์†Œ์Šค๋ฅผ ๋ฐฐํฌํ•˜๋ฉฐ, ๊ตฌํ˜„์„ ์ง„ํ–‰ํ•˜๊ธฐ ์ „์— ์„ค์ •์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ์ ์ ˆํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์€ MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์˜ ์„ฑ๊ณต์— ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์€ Docker, Azure ์„œ๋น„์Šค, ๊ฐœ๋ฐœ ๋„๊ตฌ๋ฅผ ์„ค์ •ํ•˜๊ณ  ๋ชจ๋“  ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋‹จ๊ณ„๋ณ„ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด Zava Retail MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•  ์ค€๋น„๊ฐ€ ๋œ ์™„์ „ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ํ•„์š”ํ•œ ๊ฐœ๋ฐœ ๋„๊ตฌ ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ
  • MCP ์„œ๋ฒ„์— ํ•„์š”ํ•œ Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ
  • PostgreSQL ๋ฐ MCP ์„œ๋ฒ„๋ฅผ ์œ„ํ•œ Docker ์ปจํ…Œ์ด๋„ˆ ์„ค์ •
  • ํ…Œ์ŠคํŠธ ์—ฐ๊ฒฐ์„ ํ†ตํ•ด ํ™˜๊ฒฝ ์„ค์ • ๊ฒ€์ฆ
  • ์ผ๋ฐ˜์ ์ธ ์„ค์ • ๋ฌธ์ œ ๋ฐ ๊ตฌ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ
  • ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ ๋ฐ ํŒŒ์ผ ๊ตฌ์กฐ ์ดํ•ด
  • ๐Ÿ“‹ ์‚ฌ์ „ ์š”๊ตฌ ์‚ฌํ•ญ ํ™•์ธ

    ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”:

    ํ•„์š”ํ•œ ์ง€์‹

  • ๊ธฐ๋ณธ ๋ช…๋ น์ค„ ์‚ฌ์šฉ๋ฒ• (Windows Command Prompt/PowerShell)
  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ๋Œ€ํ•œ ์ดํ•ด
  • Git ๋ฒ„์ „ ๊ด€๋ฆฌ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ์ง€์‹
  • Docker์˜ ๊ธฐ๋ณธ ๊ฐœ๋… (์ปจํ…Œ์ด๋„ˆ, ์ด๋ฏธ์ง€, ๋ณผ๋ฅจ)
  • ์‹œ์Šคํ…œ ์š”๊ตฌ ์‚ฌํ•ญ

  • ์šด์˜ ์ฒด์ œ: Windows 10/11, macOS ๋˜๋Š” Linux
  • RAM: ์ตœ์†Œ 8GB (๊ถŒ์žฅ 16GB)
  • ์ €์žฅ ๊ณต๊ฐ„: ์ตœ์†Œ 10GB์˜ ์—ฌ์œ  ๊ณต๊ฐ„
  • ๋„คํŠธ์›Œํฌ: ๋‹ค์šด๋กœ๋“œ ๋ฐ Azure ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ
  • ๊ณ„์ • ์š”๊ตฌ ์‚ฌํ•ญ

  • Azure ๊ตฌ๋…: ๋ฌด๋ฃŒ ๊ณ„์ธต์œผ๋กœ ์ถฉ๋ถ„
  • GitHub ๊ณ„์ •: ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์•ก์„ธ์Šค๋ฅผ ์œ„ํ•ด
  • Docker Hub ๊ณ„์ •: (์„ ํƒ ์‚ฌํ•ญ) ์‚ฌ์šฉ์ž ์ •์˜ ์ด๋ฏธ์ง€ ๊ฒŒ์‹œ๋ฅผ ์œ„ํ•ด
  • ๐Ÿ› ๏ธ ๋„๊ตฌ ์„ค์น˜

    1. Docker Desktop ์„ค์น˜

    Docker๋Š” ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์ปจํ…Œ์ด๋„ˆํ™”๋œ ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    Windows ์„ค์น˜

    1. Docker Desktop ๋‹ค์šด๋กœ๋“œ:

    ```cmd

    # Visit https://desktop.docker.com/win/stable/Docker%20Desktop%20Installer.exe

    # Or use Windows Package Manager

    winget install Docker.DockerDesktop

    ```

    2. ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ:

    - ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์œผ๋กœ ์„ค์น˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰

    - WSL 2 ํ†ตํ•ฉ ํ™œ์„ฑํ™”

    - ์„ค์น˜ ์™„๋ฃŒ ํ›„ ์ปดํ“จํ„ฐ ์žฌ์‹œ์ž‘

    3. ์„ค์น˜ ํ™•์ธ:

    ```cmd

    docker --version

    docker-compose --version

    ```

    macOS ์„ค์น˜

    1. ๋‹ค์šด๋กœ๋“œ ๋ฐ ์„ค์น˜:

    ```bash

    # Download from https://desktop.docker.com/mac/stable/Docker.dmg

    # Or use Homebrew

    brew install --cask docker

    ```

    2. Docker Desktop ์‹œ์ž‘:

    - ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์—์„œ Docker Desktop ์‹คํ–‰

    - ์ดˆ๊ธฐ ์„ค์ • ๋งˆ๋ฒ•์‚ฌ ์™„๋ฃŒ

    3. ์„ค์น˜ ํ™•์ธ:

    ```bash

    docker --version

    docker-compose --version

    ```

    Linux ์„ค์น˜

    1. Docker Engine ์„ค์น˜:

    ```bash

    # Ubuntu/Debian

    curl -fsSL https://get.docker.com -o get-docker.sh

    sudo sh get-docker.sh

    sudo usermod -aG docker $USER

    # Log out and back in for group changes to take effect

    ```

    2. Docker Compose ์„ค์น˜:

    ```bash

    sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

    sudo chmod +x /usr/local/bin/docker-compose

    ```

    2. Azure CLI ์„ค์น˜

    Azure CLI๋Š” Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ ๋ฐ ๊ด€๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

    Windows ์„ค์น˜
    
    # Using Windows Package Manager
    
    winget install Microsoft.AzureCLI
    
    
    
    # Or download MSI from: https://aka.ms/installazurecliwindows
    
    
    macOS ์„ค์น˜
    
    # Using Homebrew
    
    brew install azure-cli
    
    
    
    # Or using installer
    
    curl -L https://aka.ms/InstallAzureCli | bash
    
    
    Linux ์„ค์น˜
    
    # Ubuntu/Debian
    
    curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
    
    
    
    # RHEL/CentOS
    
    sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
    
    sudo dnf install azure-cli
    
    
    ์„ค์น˜ ํ™•์ธ ๋ฐ ์ธ์ฆ
    
    # Check installation
    
    az version
    
    
    
    # Login to Azure
    
    az login
    
    
    
    # Set default subscription (if you have multiple)
    
    az account list --output table
    
    az account set --subscription "Your-Subscription-Name"
    
    

    3. Git ์„ค์น˜

    Git์€ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํด๋ก  ๋ฐ ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

    Windows
    
    # Using Windows Package Manager
    
    winget install Git.Git
    
    
    
    # Or download from: https://git-scm.com/download/win
    
    
    macOS
    
    # Git is usually pre-installed, but you can update via Homebrew
    
    brew install git
    
    
    Linux
    
    # Ubuntu/Debian
    
    sudo apt update && sudo apt install git
    
    
    
    # RHEL/CentOS
    
    sudo dnf install git
    
    

    4. VS Code ์„ค์น˜

    Visual Studio Code๋Š” MCP ์ง€์›์„ ์œ„ํ•œ ํ†ตํ•ฉ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์„ค์น˜
    
    # Windows
    
    winget install Microsoft.VisualStudioCode
    
    
    
    # macOS
    
    brew install --cask visual-studio-code
    
    
    
    # Linux (Ubuntu/Debian)
    
    sudo snap install code --classic
    
    
    ํ•„์ˆ˜ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ

    ๋‹ค์Œ VS Code ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์„ ์„ค์น˜ํ•˜์„ธ์š”:

    
    # Install via command line
    
    code --install-extension ms-python.python
    
    code --install-extension ms-vscode.vscode-json
    
    code --install-extension ms-azuretools.vscode-docker
    
    code --install-extension ms-vscode.azure-account
    
    

    ๋˜๋Š” VS Code๋ฅผ ํ†ตํ•ด ์„ค์น˜:

    1. VS Code ์—ด๊ธฐ

    2. ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ์ด๋™ (Ctrl+Shift+X)

    3. ์„ค์น˜:

    - Python (Microsoft)

    - Docker (Microsoft)

    - Azure Account (Microsoft)

    - JSON (Microsoft)

    5. Python ์„ค์น˜

    Python 3.8+๋Š” MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

    Windows
    
    # Using Windows Package Manager
    
    winget install Python.Python.3.11
    
    
    
    # Or download from: https://www.python.org/downloads/
    
    
    macOS
    
    # Using Homebrew
    
    brew install python@3.11
    
    
    Linux
    
    # Ubuntu/Debian
    
    sudo apt update && sudo apt install python3.11 python3.11-pip python3.11-venv
    
    
    
    # RHEL/CentOS
    
    sudo dnf install python3.11 python3.11-pip
    
    
    ์„ค์น˜ ํ™•์ธ
    
    python --version  # Should show Python 3.11.x
    
    pip --version      # Should show pip version
    
    

    ๐Ÿš€ ํ”„๋กœ์ ํŠธ ์„ค์ •

    1. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํด๋ก 

    
    # Clone the main repository
    
    git clone https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail.git
    
    
    
    # Navigate to the project directory
    
    cd MCP-Server-and-PostgreSQL-Sample-Retail
    
    
    
    # Verify repository structure
    
    ls -la
    
    

    2. Python ๊ฐ€์ƒ ํ™˜๊ฒฝ ์ƒ์„ฑ

    
    # Create virtual environment
    
    python -m venv mcp-env
    
    
    
    # Activate virtual environment
    
    # Windows
    
    mcp-env\Scripts\activate
    
    
    
    # macOS/Linux
    
    source mcp-env/bin/activate
    
    
    
    # Upgrade pip
    
    python -m pip install --upgrade pip
    
    

    3. Python ์ข…์†์„ฑ ์„ค์น˜

    
    # Install development dependencies
    
    pip install -r requirements.lock.txt
    
    
    
    # Verify key packages
    
    pip list | grep fastmcp
    
    pip list | grep asyncpg
    
    pip list | grep azure
    
    

    โ˜๏ธ Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ

    1. ๋ฆฌ์†Œ์Šค ์š”๊ตฌ ์‚ฌํ•ญ ์ดํ•ด

    MCP ์„œ๋ฒ„์—๋Š” ๋‹ค์Œ Azure ๋ฆฌ์†Œ์Šค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

    ๋ฆฌ์†Œ์Šค ๋ชฉ์  ์˜ˆ์ƒ ๋น„์šฉ ------------ ---------- --------------- Azure AI Foundry AI ๋ชจ๋ธ ํ˜ธ์ŠคํŒ… ๋ฐ ๊ด€๋ฆฌ ์›” $10-50 OpenAI ๋ฐฐํฌ ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ (text-embedding-3-small) ์›” $5-20 Application Insights ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์›๊ฒฉ ๋ถ„์„ ์›” $5-15 Resource Group ๋ฆฌ์†Œ์Šค ์กฐ์ง ๋ฌด๋ฃŒ

    2. Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ

    ์˜ต์…˜ A: ์ž๋™ ๋ฐฐํฌ (๊ถŒ์žฅ)
    
    # Navigate to infrastructure directory
    
    cd infra
    
    
    
    # Windows - PowerShell
    
    ./deploy.ps1
    
    
    
    # macOS/Linux - Bash
    
    ./deploy.sh
    
    

    ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ๋Š” ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค:

    1. ๊ณ ์œ ํ•œ ๋ฆฌ์†Œ์Šค ๊ทธ๋ฃน ์ƒ์„ฑ

    2. Azure AI Foundry ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ

    3. text-embedding-3-small ๋ชจ๋ธ ๋ฐฐํฌ

    4. Application Insights ๊ตฌ์„ฑ

    5. ์ธ์ฆ์„ ์œ„ํ•œ ์„œ๋น„์Šค ์ฃผ์ฒด ์ƒ์„ฑ

    6. ๊ตฌ์„ฑ๋œ .env ํŒŒ์ผ ์ƒ์„ฑ

    ์˜ต์…˜ B: ์ˆ˜๋™ ๋ฐฐํฌ

    ์ž๋™ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์ˆ˜๋™ ์ œ์–ด๋ฅผ ์„ ํ˜ธํ•˜๋Š” ๊ฒฝ์šฐ:

    
    # Set variables
    
    RESOURCE_GROUP="rg-zava-mcp-$(date +%s)"
    
    LOCATION="westus2"
    
    AI_PROJECT_NAME="zava-ai-project"
    
    
    
    # Create resource group
    
    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    
    
    # Deploy main template
    
    az deployment group create \
    
      --resource-group $RESOURCE_GROUP \
    
      --template-file main.bicep \
    
      --parameters location=$LOCATION \
    
      --parameters resourcePrefix="zava-mcp"
    
    

    3. Azure ๋ฐฐํฌ ํ™•์ธ

    
    # Check resource group
    
    az group show --name $RESOURCE_GROUP --output table
    
    
    
    # List deployed resources
    
    az resource list --resource-group $RESOURCE_GROUP --output table
    
    
    
    # Test AI service
    
    az cognitiveservices account show \
    
      --name "your-ai-service-name" \
    
      --resource-group $RESOURCE_GROUP
    
    

    4. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ตฌ์„ฑ

    ๋ฐฐํฌ ํ›„ .env ํŒŒ์ผ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์„ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”:

    
    # .env file contents
    
    PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com/
    
    AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/
    
    EMBEDDING_MODEL_DEPLOYMENT_NAME=text-embedding-3-small
    
    AZURE_CLIENT_ID=your-client-id
    
    AZURE_CLIENT_SECRET=your-client-secret
    
    AZURE_TENANT_ID=your-tenant-id
    
    APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=your-key;...
    
    
    
    # Database configuration (for development)
    
    POSTGRES_HOST=localhost
    
    POSTGRES_PORT=5432
    
    POSTGRES_DB=zava
    
    POSTGRES_USER=postgres
    
    POSTGRES_PASSWORD=your-secure-password
    
    

    ๐Ÿณ Docker ํ™˜๊ฒฝ ์„ค์ •

    1. Docker ๊ตฌ์„ฑ ์ดํ•ด

    ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์€ Docker Compose๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

    
    # docker-compose.yml overview
    
    version: '3.8'
    
    services:
    
      postgres:
    
        image: pgvector/pgvector:pg17
    
        environment:
    
          POSTGRES_DB: zava
    
          POSTGRES_USER: postgres
    
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - ./data:/backup_data:ro
    
          - ./docker-init:/docker-entrypoint-initdb.d:ro
    
        
    
      mcp_server:
    
        build: .
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        ports:
    
          - "8000:8000"
    
        env_file:
    
          - .env
    
    

    2. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์‹œ์ž‘

    
    # Ensure you're in the project root directory
    
    cd /path/to/MCP-Server-and-PostgreSQL-Sample-Retail
    
    
    
    # Start the services
    
    docker-compose up -d
    
    
    
    # Check service status
    
    docker-compose ps
    
    
    
    # View logs
    
    docker-compose logs -f
    
    

    3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ํ™•์ธ

    
    # Connect to PostgreSQL container
    
    docker-compose exec postgres psql -U postgres -d zava
    
    
    
    # Check database structure
    
    \dt retail.*
    
    
    
    # Verify sample data
    
    SELECT COUNT(*) FROM retail.stores;
    
    SELECT COUNT(*) FROM retail.products;
    
    SELECT COUNT(*) FROM retail.orders;
    
    
    
    # Exit PostgreSQL
    
    \q
    
    

    4. MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ

    
    # Check MCP server health
    
    curl http://localhost:8000/health
    
    
    
    # Test basic MCP endpoint
    
    curl -X POST http://localhost:8000/mcp \
    
      -H "Content-Type: application/json" \
    
      -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \
    
      -d '{"method": "tools/list", "params": {}}'
    
    

    ๐Ÿ”ง VS Code ๊ตฌ์„ฑ

    1. MCP ํ†ตํ•ฉ ๊ตฌ์„ฑ

    VS Code MCP ๊ตฌ์„ฑ์„ ์ƒ์„ฑํ•˜์„ธ์š”:

    
    // .vscode/mcp.json
    
    {
    
        "servers": {
    
            "zava-sales-analysis-headoffice": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "00000000-0000-0000-0000-000000000000"}
    
            },
    
            "zava-sales-analysis-seattle": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"}
    
            },
    
            "zava-sales-analysis-redmond": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "e7f8a9b0-c1d2-3e4f-5678-90abcdef1234"}
    
            }
    
        },
    
        "inputs": []
    
    }
    
    

    2. Python ํ™˜๊ฒฝ ๊ตฌ์„ฑ

    
    // .vscode/settings.json
    
    {
    
        "python.defaultInterpreterPath": "./mcp-env/bin/python",
    
        "python.linting.enabled": true,
    
        "python.linting.pylintEnabled": true,
    
        "python.formatting.provider": "black",
    
        "python.testing.pytestEnabled": true,
    
        "python.testing.pytestArgs": ["tests"],
    
        "files.exclude": {
    
            "**/__pycache__": true,
    
            "**/.pytest_cache": true,
    
            "**/mcp-env": true
    
        }
    
    }
    
    

    3. VS Code ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    1. ํ”„๋กœ์ ํŠธ๋ฅผ VS Code์—์„œ ์—ด๊ธฐ:

    ```bash

    code .

    ```

    2. AI Chat ์—ด๊ธฐ:

    - Ctrl+Shift+P (Windows/Linux) ๋˜๋Š” Cmd+Shift+P (macOS) ๋ˆ„๋ฅด๊ธฐ

    - "AI Chat" ์ž…๋ ฅ ํ›„ "AI Chat: Open Chat" ์„ ํƒ

    3. MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ:

    - AI Chat์—์„œ #zava ์ž…๋ ฅ ํ›„ ๊ตฌ์„ฑ๋œ ์„œ๋ฒ„ ์ค‘ ํ•˜๋‚˜ ์„ ํƒ

    - ์งˆ๋ฌธ: "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์–ด๋–ค ํ…Œ์ด๋ธ”์ด ์žˆ๋‚˜์š”?"

    - ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” ๋ชฉ๋ก์„ ํฌํ•จํ•œ ์‘๋‹ต์„ ๋ฐ›์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค

    โœ… ํ™˜๊ฒฝ ๊ฒ€์ฆ

    1. ์ข…ํ•ฉ ์‹œ์Šคํ…œ ์ ๊ฒ€

    ์„ค์ •์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ด ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”:

    
    # Create validation script
    
    cat > validate_setup.py << 'EOF'
    
    #!/usr/bin/env python3
    
    """
    
    Environment validation script for MCP Server setup.
    
    """
    
    import asyncio
    
    import os
    
    import sys
    
    import subprocess
    
    import requests
    
    import asyncpg
    
    from azure.identity import DefaultAzureCredential
    
    from azure.ai.projects import AIProjectClient
    
    
    
    async def validate_environment():
    
        """Comprehensive environment validation."""
    
        results = {}
    
        
    
        # Check Python version
    
        python_version = sys.version_info
    
        results['python'] = {
    
            'status': 'pass' if python_version >= (3, 8) else 'fail',
    
            'version': f"{python_version.major}.{python_version.minor}.{python_version.micro}",
    
            'required': '3.8+'
    
        }
    
        
    
        # Check required packages
    
        required_packages = ['fastmcp', 'asyncpg', 'azure-ai-projects']
    
        for package in required_packages:
    
            try:
    
                __import__(package)
    
                results[f'package_{package}'] = {'status': 'pass'}
    
            except ImportError:
    
                results[f'package_{package}'] = {'status': 'fail', 'error': 'Not installed'}
    
        
    
        # Check Docker
    
        try:
    
            result = subprocess.run(['docker', '--version'], capture_output=True, text=True)
    
            results['docker'] = {
    
                'status': 'pass' if result.returncode == 0 else 'fail',
    
                'version': result.stdout.strip() if result.returncode == 0 else 'Not available'
    
            }
    
        except FileNotFoundError:
    
            results['docker'] = {'status': 'fail', 'error': 'Docker not found'}
    
        
    
        # Check Azure CLI
    
        try:
    
            result = subprocess.run(['az', '--version'], capture_output=True, text=True)
    
            results['azure_cli'] = {
    
                'status': 'pass' if result.returncode == 0 else 'fail',
    
                'version': result.stdout.split('\n')[0] if result.returncode == 0 else 'Not available'
    
            }
    
        except FileNotFoundError:
    
            results['azure_cli'] = {'status': 'fail', 'error': 'Azure CLI not found'}
    
        
    
        # Check environment variables
    
        required_env_vars = [
    
            'PROJECT_ENDPOINT',
    
            'AZURE_OPENAI_ENDPOINT',
    
            'EMBEDDING_MODEL_DEPLOYMENT_NAME',
    
            'AZURE_CLIENT_ID',
    
            'AZURE_CLIENT_SECRET',
    
            'AZURE_TENANT_ID'
    
        ]
    
        
    
        for var in required_env_vars:
    
            value = os.getenv(var)
    
            results[f'env_{var}'] = {
    
                'status': 'pass' if value else 'fail',
    
                'value': '***' if value and 'SECRET' in var else value
    
            }
    
        
    
        # Check database connection
    
        try:
    
            conn = await asyncpg.connect(
    
                host=os.getenv('POSTGRES_HOST', 'localhost'),
    
                port=int(os.getenv('POSTGRES_PORT', 5432)),
    
                database=os.getenv('POSTGRES_DB', 'zava'),
    
                user=os.getenv('POSTGRES_USER', 'postgres'),
    
                password=os.getenv('POSTGRES_PASSWORD', 'secure_password')
    
            )
    
            
    
            # Test query
    
            result = await conn.fetchval('SELECT COUNT(*) FROM retail.stores')
    
            await conn.close()
    
            
    
            results['database'] = {
    
                'status': 'pass',
    
                'store_count': result
    
            }
    
        except Exception as e:
    
            results['database'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        # Check MCP server
    
        try:
    
            response = requests.get('http://localhost:8000/health', timeout=5)
    
            results['mcp_server'] = {
    
                'status': 'pass' if response.status_code == 200 else 'fail',
    
                'response': response.json() if response.status_code == 200 else response.text
    
            }
    
        except Exception as e:
    
            results['mcp_server'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        # Check Azure AI service
    
        try:
    
            credential = DefaultAzureCredential()
    
            project_client = AIProjectClient(
    
                endpoint=os.getenv('PROJECT_ENDPOINT'),
    
                credential=credential
    
            )
    
            
    
            # This will fail if credentials are invalid
    
            results['azure_ai'] = {'status': 'pass'}
    
            
    
        except Exception as e:
    
            results['azure_ai'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        return results
    
    
    
    def print_results(results):
    
        """Print formatted validation results."""
    
        print("๐Ÿ” Environment Validation Results\n")
    
        print("=" * 50)
    
        
    
        passed = 0
    
        failed = 0
    
        
    
        for component, result in results.items():
    
            status = result.get('status', 'unknown')
    
            if status == 'pass':
    
                print(f"โœ… {component}: PASS")
    
                passed += 1
    
            else:
    
                print(f"โŒ {component}: FAIL")
    
                if 'error' in result:
    
                    print(f"   Error: {result['error']}")
    
                failed += 1
    
        
    
        print("\n" + "=" * 50)
    
        print(f"Summary: {passed} passed, {failed} failed")
    
        
    
        if failed > 0:
    
            print("\nโ— Please fix the failed components before proceeding.")
    
            return False
    
        else:
    
            print("\n๐ŸŽ‰ All validations passed! Your environment is ready.")
    
            return True
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
    
    async def main():
    
        results = await validate_environment()
    
        success = print_results(results)
    
        sys.exit(0 if success else 1)
    
    
    
    EOF
    
    
    
    # Run validation
    
    python validate_setup.py
    
    

    2. ์ˆ˜๋™ ๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

    โœ… ๊ธฐ๋ณธ ๋„๊ตฌ

  • [ ] Docker ๋ฒ„์ „ 20.10+ ์„ค์น˜ ๋ฐ ์‹คํ–‰ ์ค‘
  • [ ] Azure CLI 2.40+ ์„ค์น˜ ๋ฐ ์ธ์ฆ ์™„๋ฃŒ
  • [ ] Python 3.8+ ๋ฐ pip ์„ค์น˜ ์™„๋ฃŒ
  • [ ] Git 2.30+ ์„ค์น˜ ์™„๋ฃŒ
  • [ ] VS Code ๋ฐ ํ•„์ˆ˜ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์„ค์น˜ ์™„๋ฃŒ
  • โœ… Azure ๋ฆฌ์†Œ์Šค

  • [ ] ๋ฆฌ์†Œ์Šค ๊ทธ๋ฃน ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋จ
  • [ ] AI Foundry ํ”„๋กœ์ ํŠธ ๋ฐฐํฌ ์™„๋ฃŒ
  • [ ] OpenAI text-embedding-3-small ๋ชจ๋ธ ๋ฐฐํฌ ์™„๋ฃŒ
  • [ ] Application Insights ๊ตฌ์„ฑ ์™„๋ฃŒ
  • [ ] ์ ์ ˆํ•œ ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์„œ๋น„์Šค ์ฃผ์ฒด ์ƒ์„ฑ๋จ
  • โœ… ํ™˜๊ฒฝ ๊ตฌ์„ฑ

  • [ ] .env ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ๋ชจ๋“  ํ•„์ˆ˜ ๋ณ€์ˆ˜ ํฌํ•จ
  • [ ] Azure ์ž๊ฒฉ ์ฆ๋ช… ์ž‘๋™ (az account show๋กœ ํ…Œ์ŠคํŠธ)
  • [ ] PostgreSQL ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ๋ฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ
  • โœ… VS Code ํ†ตํ•ฉ

  • [ ] .vscode/mcp.json ๊ตฌ์„ฑ ์™„๋ฃŒ
  • [ ] Python ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋ฅผ ๊ฐ€์ƒ ํ™˜๊ฒฝ์œผ๋กœ ์„ค์ •
  • [ ] MCP ์„œ๋ฒ„๊ฐ€ AI Chat์— ํ‘œ์‹œ๋จ
  • [ ] AI Chat์„ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ฐ€๋Šฅ
  • ๐Ÿ› ๏ธ ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ

    Docker ๋ฌธ์ œ

    ๋ฌธ์ œ: Docker ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š์Œ

    
    # Check Docker service status
    
    docker info
    
    
    
    # Check available resources
    
    docker system df
    
    
    
    # Clean up if needed
    
    docker system prune -f
    
    
    
    # Restart Docker Desktop (Windows/macOS)
    
    # Or restart Docker service (Linux)
    
    sudo systemctl restart docker
    
    

    ๋ฌธ์ œ: PostgreSQL ์—ฐ๊ฒฐ ์‹คํŒจ

    
    # Check container logs
    
    docker-compose logs postgres
    
    
    
    # Verify container is healthy
    
    docker-compose ps
    
    
    
    # Test direct connection
    
    docker-compose exec postgres psql -U postgres -d zava -c "SELECT 1;"
    
    

    Azure ๋ฐฐํฌ ๋ฌธ์ œ

    ๋ฌธ์ œ: Azure ๋ฐฐํฌ ์‹คํŒจ

    
    # Check Azure CLI authentication
    
    az account show
    
    
    
    # Verify subscription permissions
    
    az role assignment list --assignee $(az account show --query user.name -o tsv)
    
    
    
    # Check resource provider registration
    
    az provider register --namespace Microsoft.CognitiveServices
    
    az provider register --namespace Microsoft.Insights
    
    

    ๋ฌธ์ œ: AI ์„œ๋น„์Šค ์ธ์ฆ ์‹คํŒจ

    
    # Test service principal
    
    az login --service-principal \
    
      --username $AZURE_CLIENT_ID \
    
      --password $AZURE_CLIENT_SECRET \
    
      --tenant $AZURE_TENANT_ID
    
    
    
    # Verify AI service deployment
    
    az cognitiveservices account list --query "[].{Name:name,Kind:kind,Location:location}"
    
    

    Python ํ™˜๊ฒฝ ๋ฌธ์ œ

    ๋ฌธ์ œ: ํŒจํ‚ค์ง€ ์„ค์น˜ ์‹คํŒจ

    
    # Upgrade pip and setuptools
    
    python -m pip install --upgrade pip setuptools wheel
    
    
    
    # Clear pip cache
    
    pip cache purge
    
    
    
    # Install packages one by one to identify issues
    
    pip install fastmcp
    
    pip install asyncpg
    
    pip install azure-ai-projects
    
    

    ๋ฌธ์ œ: VS Code์—์„œ Python ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ

    
    # Show Python interpreter paths
    
    which python  # macOS/Linux
    
    where python  # Windows
    
    
    
    # Activate virtual environment first
    
    source mcp-env/bin/activate  # macOS/Linux
    
    mcp-env\Scripts\activate     # Windows
    
    
    
    # Then open VS Code
    
    code .
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ์™„์ „ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ: ๋ชจ๋“  ๋„๊ตฌ ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ ์™„๋ฃŒ

    โœ… Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ: AI ์„œ๋น„์Šค ๋ฐ ์ง€์› ์ธํ”„๋ผ

    โœ… Docker ํ™˜๊ฒฝ ์‹คํ–‰: PostgreSQL ๋ฐ MCP ์„œ๋ฒ„ ์ปจํ…Œ์ด๋„ˆ

    โœ… VS Code ํ†ตํ•ฉ: MCP ์„œ๋ฒ„ ๊ตฌ์„ฑ ๋ฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ

    โœ… ์„ค์ • ๊ฒ€์ฆ ์™„๋ฃŒ: ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ ํ…Œ์ŠคํŠธ ๋ฐ ์ž‘๋™ ํ™•์ธ

    โœ… ๋ฌธ์ œ ํ•ด๊ฒฐ ์ง€์‹: ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ ๋ฐ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ํ™˜๊ฒฝ์ด ์ค€๋น„๋˜์—ˆ์œผ๋ฉด Lab 04: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์Šคํ‚ค๋งˆ๋กœ ๊ณ„์† ์ง„ํ–‰ํ•˜์„ธ์š”:

  • ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ๋ฅผ ์ž์„ธํžˆ ํƒ์ƒ‰
  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋ง ์ดํ•ด
  • ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„ ํ•™์Šต
  • ์ƒ˜ํ”Œ ์†Œ๋งค ๋ฐ์ดํ„ฐ ์ž‘์—…
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ๊ฐœ๋ฐœ ๋„๊ตฌ

  • Docker ๋ฌธ์„œ - Docker ์ฐธ์กฐ ์ž๋ฃŒ
  • Azure CLI ์ฐธ์กฐ - Azure CLI ๋ช…๋ น์–ด
  • VS Code ๋ฌธ์„œ - ํŽธ์ง‘๊ธฐ ๊ตฌ์„ฑ ๋ฐ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ
  • Azure ์„œ๋น„์Šค

  • Azure AI Foundry ๋ฌธ์„œ - AI ์„œ๋น„์Šค ๊ตฌ์„ฑ
  • Azure OpenAI ์„œ๋น„์Šค - AI ๋ชจ๋ธ ๋ฐฐํฌ
  • Application Insights - ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •
  • Python ๊ฐœ๋ฐœ

  • Python ๊ฐ€์ƒ ํ™˜๊ฒฝ - ํ™˜๊ฒฝ ๊ด€๋ฆฌ
  • AsyncIO ๋ฌธ์„œ - ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจํ„ด
  • FastAPI ๋ฌธ์„œ - ์›น ํ”„๋ ˆ์ž„์›Œํฌ ํŒจํ„ด
  • ---

    ๋‹ค์Œ: ํ™˜๊ฒฝ์ด ์ค€๋น„๋˜์—ˆ๋‚˜์š”? Lab 04: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์Šคํ‚ค๋งˆ๋กœ ๊ณ„์† ์ง„ํ–‰ํ•˜์„ธ์š”.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ๊ฐœ๋ฐœ ํ™˜๊ฒฝ, Docker, Azure ๋ฆฌ์†Œ์Šค ์„ค์ • ์„ค์ •ํ•˜๊ธฐ

    ํ™˜๊ฒฝ ์„ค์ •

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ PostgreSQL ํ†ตํ•ฉ์„ ํ†ตํ•ด MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ ์™„์ „ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์„ค์ •ํ•˜๋Š” ๊ณผ์ •์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. ํ•„์š”ํ•œ ๋ชจ๋“  ๋„๊ตฌ๋ฅผ ๊ตฌ์„ฑํ•˜๊ณ , Azure ๋ฆฌ์†Œ์Šค๋ฅผ ๋ฐฐํฌํ•˜๋ฉฐ, ๊ตฌํ˜„์„ ์ง„ํ–‰ํ•˜๊ธฐ ์ „์— ์„ค์ •์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ์ ์ ˆํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์€ MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์˜ ์„ฑ๊ณต์— ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์€ Docker, Azure ์„œ๋น„์Šค, ๊ฐœ๋ฐœ ๋„๊ตฌ๋ฅผ ์„ค์ •ํ•˜๊ณ  ๋ชจ๋“  ๊ฒƒ์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋‹จ๊ณ„๋ณ„ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด Zava Retail MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•  ์ค€๋น„๊ฐ€ ๋œ ์™„์ „ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ํ•„์š”ํ•œ ๊ฐœ๋ฐœ ๋„๊ตฌ ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ
  • MCP ์„œ๋ฒ„์— ํ•„์š”ํ•œ Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ
  • PostgreSQL ๋ฐ MCP ์„œ๋ฒ„๋ฅผ ์œ„ํ•œ Docker ์ปจํ…Œ์ด๋„ˆ ์„ค์ •
  • ํ…Œ์ŠคํŠธ ์—ฐ๊ฒฐ์„ ํ†ตํ•ด ํ™˜๊ฒฝ ์„ค์ • ๊ฒ€์ฆ
  • ์ผ๋ฐ˜์ ์ธ ์„ค์ • ๋ฌธ์ œ ๋ฐ ๊ตฌ์„ฑ ๋ฌธ์ œ ํ•ด๊ฒฐ
  • ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ ๋ฐ ํŒŒ์ผ ๊ตฌ์กฐ ์ดํ•ด
  • ๐Ÿ“‹ ์‚ฌ์ „ ์š”๊ตฌ ์‚ฌํ•ญ ํ™•์ธ

    ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ๋‹ค์Œ์„ ํ™•์ธํ•˜์„ธ์š”:

    ํ•„์š”ํ•œ ์ง€์‹

  • ๊ธฐ๋ณธ ๋ช…๋ น์ค„ ์‚ฌ์šฉ๋ฒ• (Windows Command Prompt/PowerShell)
  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ๋Œ€ํ•œ ์ดํ•ด
  • Git ๋ฒ„์ „ ๊ด€๋ฆฌ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ์ง€์‹
  • Docker์˜ ๊ธฐ๋ณธ ๊ฐœ๋… (์ปจํ…Œ์ด๋„ˆ, ์ด๋ฏธ์ง€, ๋ณผ๋ฅจ)
  • ์‹œ์Šคํ…œ ์š”๊ตฌ ์‚ฌํ•ญ

  • ์šด์˜ ์ฒด์ œ: Windows 10/11, macOS ๋˜๋Š” Linux
  • RAM: ์ตœ์†Œ 8GB (๊ถŒ์žฅ 16GB)
  • ์ €์žฅ ๊ณต๊ฐ„: ์ตœ์†Œ 10GB์˜ ์—ฌ์œ  ๊ณต๊ฐ„
  • ๋„คํŠธ์›Œํฌ: ๋‹ค์šด๋กœ๋“œ ๋ฐ Azure ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ ์ธํ„ฐ๋„ท ์—ฐ๊ฒฐ
  • ๊ณ„์ • ์š”๊ตฌ ์‚ฌํ•ญ

  • Azure ๊ตฌ๋…: ๋ฌด๋ฃŒ ๊ณ„์ธต์œผ๋กœ ์ถฉ๋ถ„
  • GitHub ๊ณ„์ •: ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์•ก์„ธ์Šค๋ฅผ ์œ„ํ•ด
  • Docker Hub ๊ณ„์ •: (์„ ํƒ ์‚ฌํ•ญ) ์‚ฌ์šฉ์ž ์ •์˜ ์ด๋ฏธ์ง€ ๊ฒŒ์‹œ๋ฅผ ์œ„ํ•ด
  • ๐Ÿ› ๏ธ ๋„๊ตฌ ์„ค์น˜

    1. Docker Desktop ์„ค์น˜

    Docker๋Š” ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์ปจํ…Œ์ด๋„ˆํ™”๋œ ํ˜•ํƒœ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    Windows ์„ค์น˜

    1. Docker Desktop ๋‹ค์šด๋กœ๋“œ:

    ```cmd

    # Visit https://desktop.docker.com/win/stable/Docker%20Desktop%20Installer.exe

    # Or use Windows Package Manager

    winget install Docker.DockerDesktop

    ```

    2. ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ:

    - ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์œผ๋กœ ์„ค์น˜ ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰

    - WSL 2 ํ†ตํ•ฉ ํ™œ์„ฑํ™”

    - ์„ค์น˜ ์™„๋ฃŒ ํ›„ ์ปดํ“จํ„ฐ ์žฌ์‹œ์ž‘

    3. ์„ค์น˜ ํ™•์ธ:

    ```cmd

    docker --version

    docker-compose --version

    ```

    macOS ์„ค์น˜

    1. ๋‹ค์šด๋กœ๋“œ ๋ฐ ์„ค์น˜:

    ```bash

    # Download from https://desktop.docker.com/mac/stable/Docker.dmg

    # Or use Homebrew

    brew install --cask docker

    ```

    2. Docker Desktop ์‹œ์ž‘:

    - ์‘์šฉ ํ”„๋กœ๊ทธ๋žจ์—์„œ Docker Desktop ์‹คํ–‰

    - ์ดˆ๊ธฐ ์„ค์ • ๋งˆ๋ฒ•์‚ฌ ์™„๋ฃŒ

    3. ์„ค์น˜ ํ™•์ธ:

    ```bash

    docker --version

    docker-compose --version

    ```

    Linux ์„ค์น˜

    1. Docker Engine ์„ค์น˜:

    ```bash

    # Ubuntu/Debian

    curl -fsSL https://get.docker.com -o get-docker.sh

    sudo sh get-docker.sh

    sudo usermod -aG docker $USER

    # Log out and back in for group changes to take effect

    ```

    2. Docker Compose ์„ค์น˜:

    ```bash

    sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

    sudo chmod +x /usr/local/bin/docker-compose

    ```

    2. Azure CLI ์„ค์น˜

    Azure CLI๋Š” Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ ๋ฐ ๊ด€๋ฆฌ๋ฅผ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.

    Windows ์„ค์น˜
    
    # Using Windows Package Manager
    
    winget install Microsoft.AzureCLI
    
    
    
    # Or download MSI from: https://aka.ms/installazurecliwindows
    
    
    macOS ์„ค์น˜
    
    # Using Homebrew
    
    brew install azure-cli
    
    
    
    # Or using installer
    
    curl -L https://aka.ms/InstallAzureCli | bash
    
    
    Linux ์„ค์น˜
    
    # Ubuntu/Debian
    
    curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
    
    
    
    # RHEL/CentOS
    
    sudo rpm --import https://packages.microsoft.com/keys/microsoft.asc
    
    sudo dnf install azure-cli
    
    
    ์„ค์น˜ ํ™•์ธ ๋ฐ ์ธ์ฆ
    
    # Check installation
    
    az version
    
    
    
    # Login to Azure
    
    az login
    
    
    
    # Set default subscription (if you have multiple)
    
    az account list --output table
    
    az account set --subscription "Your-Subscription-Name"
    
    

    3. Git ์„ค์น˜

    Git์€ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํด๋ก  ๋ฐ ๋ฒ„์ „ ๊ด€๋ฆฌ๋ฅผ ์œ„ํ•ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

    Windows
    
    # Using Windows Package Manager
    
    winget install Git.Git
    
    
    
    # Or download from: https://git-scm.com/download/win
    
    
    macOS
    
    # Git is usually pre-installed, but you can update via Homebrew
    
    brew install git
    
    
    Linux
    
    # Ubuntu/Debian
    
    sudo apt update && sudo apt install git
    
    
    
    # RHEL/CentOS
    
    sudo dnf install git
    
    

    4. VS Code ์„ค์น˜

    Visual Studio Code๋Š” MCP ์ง€์›์„ ์œ„ํ•œ ํ†ตํ•ฉ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์„ค์น˜
    
    # Windows
    
    winget install Microsoft.VisualStudioCode
    
    
    
    # macOS
    
    brew install --cask visual-studio-code
    
    
    
    # Linux (Ubuntu/Debian)
    
    sudo snap install code --classic
    
    
    ํ•„์ˆ˜ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ

    ๋‹ค์Œ VS Code ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์„ ์„ค์น˜ํ•˜์„ธ์š”:

    
    # Install via command line
    
    code --install-extension ms-python.python
    
    code --install-extension ms-vscode.vscode-json
    
    code --install-extension ms-azuretools.vscode-docker
    
    code --install-extension ms-vscode.azure-account
    
    

    ๋˜๋Š” VS Code๋ฅผ ํ†ตํ•ด ์„ค์น˜:

    1. VS Code ์—ด๊ธฐ

    2. ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ์ด๋™ (Ctrl+Shift+X)

    3. ์„ค์น˜:

    - Python (Microsoft)

    - Docker (Microsoft)

    - Azure Account (Microsoft)

    - JSON (Microsoft)

    5. Python ์„ค์น˜

    Python 3.8+๋Š” MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์— ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

    Windows
    
    # Using Windows Package Manager
    
    winget install Python.Python.3.11
    
    
    
    # Or download from: https://www.python.org/downloads/
    
    
    macOS
    
    # Using Homebrew
    
    brew install python@3.11
    
    
    Linux
    
    # Ubuntu/Debian
    
    sudo apt update && sudo apt install python3.11 python3.11-pip python3.11-venv
    
    
    
    # RHEL/CentOS
    
    sudo dnf install python3.11 python3.11-pip
    
    
    ์„ค์น˜ ํ™•์ธ
    
    python --version  # Should show Python 3.11.x
    
    pip --version      # Should show pip version
    
    

    ๐Ÿš€ ํ”„๋กœ์ ํŠธ ์„ค์ •

    1. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํด๋ก 

    
    # Clone the main repository
    
    git clone https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail.git
    
    
    
    # Navigate to the project directory
    
    cd MCP-Server-and-PostgreSQL-Sample-Retail
    
    
    
    # Verify repository structure
    
    ls -la
    
    

    2. Python ๊ฐ€์ƒ ํ™˜๊ฒฝ ์ƒ์„ฑ

    
    # Create virtual environment
    
    python -m venv mcp-env
    
    
    
    # Activate virtual environment
    
    # Windows
    
    mcp-env\Scripts\activate
    
    
    
    # macOS/Linux
    
    source mcp-env/bin/activate
    
    
    
    # Upgrade pip
    
    python -m pip install --upgrade pip
    
    

    3. Python ์ข…์†์„ฑ ์„ค์น˜

    
    # Install development dependencies
    
    pip install -r requirements.lock.txt
    
    
    
    # Verify key packages
    
    pip list | grep fastmcp
    
    pip list | grep asyncpg
    
    pip list | grep azure
    
    

    โ˜๏ธ Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ

    1. ๋ฆฌ์†Œ์Šค ์š”๊ตฌ ์‚ฌํ•ญ ์ดํ•ด

    MCP ์„œ๋ฒ„์—๋Š” ๋‹ค์Œ Azure ๋ฆฌ์†Œ์Šค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค:

    ๋ฆฌ์†Œ์Šค ๋ชฉ์  ์˜ˆ์ƒ ๋น„์šฉ ------------ ---------- --------------- Azure AI Foundry AI ๋ชจ๋ธ ํ˜ธ์ŠคํŒ… ๋ฐ ๊ด€๋ฆฌ ์›” $10-50 OpenAI ๋ฐฐํฌ ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ (text-embedding-3-small) ์›” $5-20 Application Insights ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์›๊ฒฉ ๋ถ„์„ ์›” $5-15 Resource Group ๋ฆฌ์†Œ์Šค ์กฐ์ง ๋ฌด๋ฃŒ

    2. Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ

    ์˜ต์…˜ A: ์ž๋™ ๋ฐฐํฌ (๊ถŒ์žฅ)
    
    # Navigate to infrastructure directory
    
    cd infra
    
    
    
    # Windows - PowerShell
    
    ./deploy.ps1
    
    
    
    # macOS/Linux - Bash
    
    ./deploy.sh
    
    

    ๋ฐฐํฌ ์Šคํฌ๋ฆฝํŠธ๋Š” ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค:

    1. ๊ณ ์œ ํ•œ ๋ฆฌ์†Œ์Šค ๊ทธ๋ฃน ์ƒ์„ฑ

    2. Azure AI Foundry ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ

    3. text-embedding-3-small ๋ชจ๋ธ ๋ฐฐํฌ

    4. Application Insights ๊ตฌ์„ฑ

    5. ์ธ์ฆ์„ ์œ„ํ•œ ์„œ๋น„์Šค ์ฃผ์ฒด ์ƒ์„ฑ

    6. ๊ตฌ์„ฑ๋œ .env ํŒŒ์ผ ์ƒ์„ฑ

    ์˜ต์…˜ B: ์ˆ˜๋™ ๋ฐฐํฌ

    ์ž๋™ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํŒจํ•˜๊ฑฐ๋‚˜ ์ˆ˜๋™ ์ œ์–ด๋ฅผ ์„ ํ˜ธํ•˜๋Š” ๊ฒฝ์šฐ:

    
    # Set variables
    
    RESOURCE_GROUP="rg-zava-mcp-$(date +%s)"
    
    LOCATION="westus2"
    
    AI_PROJECT_NAME="zava-ai-project"
    
    
    
    # Create resource group
    
    az group create --name $RESOURCE_GROUP --location $LOCATION
    
    
    
    # Deploy main template
    
    az deployment group create \
    
      --resource-group $RESOURCE_GROUP \
    
      --template-file main.bicep \
    
      --parameters location=$LOCATION \
    
      --parameters resourcePrefix="zava-mcp"
    
    

    3. Azure ๋ฐฐํฌ ํ™•์ธ

    
    # Check resource group
    
    az group show --name $RESOURCE_GROUP --output table
    
    
    
    # List deployed resources
    
    az resource list --resource-group $RESOURCE_GROUP --output table
    
    
    
    # Test AI service
    
    az cognitiveservices account show \
    
      --name "your-ai-service-name" \
    
      --resource-group $RESOURCE_GROUP
    
    

    4. ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๊ตฌ์„ฑ

    ๋ฐฐํฌ ํ›„ .env ํŒŒ์ผ์ด ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์Œ์„ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”:

    
    # .env file contents
    
    PROJECT_ENDPOINT=https://your-project.cognitiveservices.azure.com/
    
    AZURE_OPENAI_ENDPOINT=https://your-openai.openai.azure.com/
    
    EMBEDDING_MODEL_DEPLOYMENT_NAME=text-embedding-3-small
    
    AZURE_CLIENT_ID=your-client-id
    
    AZURE_CLIENT_SECRET=your-client-secret
    
    AZURE_TENANT_ID=your-tenant-id
    
    APPLICATIONINSIGHTS_CONNECTION_STRING=InstrumentationKey=your-key;...
    
    
    
    # Database configuration (for development)
    
    POSTGRES_HOST=localhost
    
    POSTGRES_PORT=5432
    
    POSTGRES_DB=zava
    
    POSTGRES_USER=postgres
    
    POSTGRES_PASSWORD=your-secure-password
    
    

    ๐Ÿณ Docker ํ™˜๊ฒฝ ์„ค์ •

    1. Docker ๊ตฌ์„ฑ ์ดํ•ด

    ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์€ Docker Compose๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

    
    # docker-compose.yml overview
    
    version: '3.8'
    
    services:
    
      postgres:
    
        image: pgvector/pgvector:pg17
    
        environment:
    
          POSTGRES_DB: zava
    
          POSTGRES_USER: postgres
    
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-secure_password}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - ./data:/backup_data:ro
    
          - ./docker-init:/docker-entrypoint-initdb.d:ro
    
        
    
      mcp_server:
    
        build: .
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        ports:
    
          - "8000:8000"
    
        env_file:
    
          - .env
    
    

    2. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ์‹œ์ž‘

    
    # Ensure you're in the project root directory
    
    cd /path/to/MCP-Server-and-PostgreSQL-Sample-Retail
    
    
    
    # Start the services
    
    docker-compose up -d
    
    
    
    # Check service status
    
    docker-compose ps
    
    
    
    # View logs
    
    docker-compose logs -f
    
    

    3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค์ • ํ™•์ธ

    
    # Connect to PostgreSQL container
    
    docker-compose exec postgres psql -U postgres -d zava
    
    
    
    # Check database structure
    
    \dt retail.*
    
    
    
    # Verify sample data
    
    SELECT COUNT(*) FROM retail.stores;
    
    SELECT COUNT(*) FROM retail.products;
    
    SELECT COUNT(*) FROM retail.orders;
    
    
    
    # Exit PostgreSQL
    
    \q
    
    

    4. MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ

    
    # Check MCP server health
    
    curl http://localhost:8000/health
    
    
    
    # Test basic MCP endpoint
    
    curl -X POST http://localhost:8000/mcp \
    
      -H "Content-Type: application/json" \
    
      -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \
    
      -d '{"method": "tools/list", "params": {}}'
    
    

    ๐Ÿ”ง VS Code ๊ตฌ์„ฑ

    1. MCP ํ†ตํ•ฉ ๊ตฌ์„ฑ

    VS Code MCP ๊ตฌ์„ฑ์„ ์ƒ์„ฑํ•˜์„ธ์š”:

    
    // .vscode/mcp.json
    
    {
    
        "servers": {
    
            "zava-sales-analysis-headoffice": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "00000000-0000-0000-0000-000000000000"}
    
            },
    
            "zava-sales-analysis-seattle": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "f47ac10b-58cc-4372-a567-0e02b2c3d479"}
    
            },
    
            "zava-sales-analysis-redmond": {
    
                "url": "http://127.0.0.1:8000/mcp",
    
                "type": "http",
    
                "headers": {"x-rls-user-id": "e7f8a9b0-c1d2-3e4f-5678-90abcdef1234"}
    
            }
    
        },
    
        "inputs": []
    
    }
    
    

    2. Python ํ™˜๊ฒฝ ๊ตฌ์„ฑ

    
    // .vscode/settings.json
    
    {
    
        "python.defaultInterpreterPath": "./mcp-env/bin/python",
    
        "python.linting.enabled": true,
    
        "python.linting.pylintEnabled": true,
    
        "python.formatting.provider": "black",
    
        "python.testing.pytestEnabled": true,
    
        "python.testing.pytestArgs": ["tests"],
    
        "files.exclude": {
    
            "**/__pycache__": true,
    
            "**/.pytest_cache": true,
    
            "**/mcp-env": true
    
        }
    
    }
    
    

    3. VS Code ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    1. ํ”„๋กœ์ ํŠธ๋ฅผ VS Code์—์„œ ์—ด๊ธฐ:

    ```bash

    code .

    ```

    2. AI Chat ์—ด๊ธฐ:

    - Ctrl+Shift+P (Windows/Linux) ๋˜๋Š” Cmd+Shift+P (macOS) ๋ˆ„๋ฅด๊ธฐ

    - "AI Chat" ์ž…๋ ฅ ํ›„ "AI Chat: Open Chat" ์„ ํƒ

    3. MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ ํ…Œ์ŠคํŠธ:

    - AI Chat์—์„œ #zava ์ž…๋ ฅ ํ›„ ๊ตฌ์„ฑ๋œ ์„œ๋ฒ„ ์ค‘ ํ•˜๋‚˜ ์„ ํƒ

    - ์งˆ๋ฌธ: "๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์–ด๋–ค ํ…Œ์ด๋ธ”์ด ์žˆ๋‚˜์š”?"

    - ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ด๋ธ” ๋ชฉ๋ก์„ ํฌํ•จํ•œ ์‘๋‹ต์„ ๋ฐ›์•„์•ผ ํ•ฉ๋‹ˆ๋‹ค

    โœ… ํ™˜๊ฒฝ ๊ฒ€์ฆ

    1. ์ข…ํ•ฉ ์‹œ์Šคํ…œ ์ ๊ฒ€

    ์„ค์ •์„ ํ™•์ธํ•˜๊ธฐ ์œ„ํ•ด ์ด ๊ฒ€์ฆ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‹คํ–‰ํ•˜์„ธ์š”:

    
    # Create validation script
    
    cat > validate_setup.py << 'EOF'
    
    #!/usr/bin/env python3
    
    """
    
    Environment validation script for MCP Server setup.
    
    """
    
    import asyncio
    
    import os
    
    import sys
    
    import subprocess
    
    import requests
    
    import asyncpg
    
    from azure.identity import DefaultAzureCredential
    
    from azure.ai.projects import AIProjectClient
    
    
    
    async def validate_environment():
    
        """Comprehensive environment validation."""
    
        results = {}
    
        
    
        # Check Python version
    
        python_version = sys.version_info
    
        results['python'] = {
    
            'status': 'pass' if python_version >= (3, 8) else 'fail',
    
            'version': f"{python_version.major}.{python_version.minor}.{python_version.micro}",
    
            'required': '3.8+'
    
        }
    
        
    
        # Check required packages
    
        required_packages = ['fastmcp', 'asyncpg', 'azure-ai-projects']
    
        for package in required_packages:
    
            try:
    
                __import__(package)
    
                results[f'package_{package}'] = {'status': 'pass'}
    
            except ImportError:
    
                results[f'package_{package}'] = {'status': 'fail', 'error': 'Not installed'}
    
        
    
        # Check Docker
    
        try:
    
            result = subprocess.run(['docker', '--version'], capture_output=True, text=True)
    
            results['docker'] = {
    
                'status': 'pass' if result.returncode == 0 else 'fail',
    
                'version': result.stdout.strip() if result.returncode == 0 else 'Not available'
    
            }
    
        except FileNotFoundError:
    
            results['docker'] = {'status': 'fail', 'error': 'Docker not found'}
    
        
    
        # Check Azure CLI
    
        try:
    
            result = subprocess.run(['az', '--version'], capture_output=True, text=True)
    
            results['azure_cli'] = {
    
                'status': 'pass' if result.returncode == 0 else 'fail',
    
                'version': result.stdout.split('\n')[0] if result.returncode == 0 else 'Not available'
    
            }
    
        except FileNotFoundError:
    
            results['azure_cli'] = {'status': 'fail', 'error': 'Azure CLI not found'}
    
        
    
        # Check environment variables
    
        required_env_vars = [
    
            'PROJECT_ENDPOINT',
    
            'AZURE_OPENAI_ENDPOINT',
    
            'EMBEDDING_MODEL_DEPLOYMENT_NAME',
    
            'AZURE_CLIENT_ID',
    
            'AZURE_CLIENT_SECRET',
    
            'AZURE_TENANT_ID'
    
        ]
    
        
    
        for var in required_env_vars:
    
            value = os.getenv(var)
    
            results[f'env_{var}'] = {
    
                'status': 'pass' if value else 'fail',
    
                'value': '***' if value and 'SECRET' in var else value
    
            }
    
        
    
        # Check database connection
    
        try:
    
            conn = await asyncpg.connect(
    
                host=os.getenv('POSTGRES_HOST', 'localhost'),
    
                port=int(os.getenv('POSTGRES_PORT', 5432)),
    
                database=os.getenv('POSTGRES_DB', 'zava'),
    
                user=os.getenv('POSTGRES_USER', 'postgres'),
    
                password=os.getenv('POSTGRES_PASSWORD', 'secure_password')
    
            )
    
            
    
            # Test query
    
            result = await conn.fetchval('SELECT COUNT(*) FROM retail.stores')
    
            await conn.close()
    
            
    
            results['database'] = {
    
                'status': 'pass',
    
                'store_count': result
    
            }
    
        except Exception as e:
    
            results['database'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        # Check MCP server
    
        try:
    
            response = requests.get('http://localhost:8000/health', timeout=5)
    
            results['mcp_server'] = {
    
                'status': 'pass' if response.status_code == 200 else 'fail',
    
                'response': response.json() if response.status_code == 200 else response.text
    
            }
    
        except Exception as e:
    
            results['mcp_server'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        # Check Azure AI service
    
        try:
    
            credential = DefaultAzureCredential()
    
            project_client = AIProjectClient(
    
                endpoint=os.getenv('PROJECT_ENDPOINT'),
    
                credential=credential
    
            )
    
            
    
            # This will fail if credentials are invalid
    
            results['azure_ai'] = {'status': 'pass'}
    
            
    
        except Exception as e:
    
            results['azure_ai'] = {
    
                'status': 'fail',
    
                'error': str(e)
    
            }
    
        
    
        return results
    
    
    
    def print_results(results):
    
        """Print formatted validation results."""
    
        print("๐Ÿ” Environment Validation Results\n")
    
        print("=" * 50)
    
        
    
        passed = 0
    
        failed = 0
    
        
    
        for component, result in results.items():
    
            status = result.get('status', 'unknown')
    
            if status == 'pass':
    
                print(f"โœ… {component}: PASS")
    
                passed += 1
    
            else:
    
                print(f"โŒ {component}: FAIL")
    
                if 'error' in result:
    
                    print(f"   Error: {result['error']}")
    
                failed += 1
    
        
    
        print("\n" + "=" * 50)
    
        print(f"Summary: {passed} passed, {failed} failed")
    
        
    
        if failed > 0:
    
            print("\nโ— Please fix the failed components before proceeding.")
    
            return False
    
        else:
    
            print("\n๐ŸŽ‰ All validations passed! Your environment is ready.")
    
            return True
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    
    
    async def main():
    
        results = await validate_environment()
    
        success = print_results(results)
    
        sys.exit(0 if success else 1)
    
    
    
    EOF
    
    
    
    # Run validation
    
    python validate_setup.py
    
    

    2. ์ˆ˜๋™ ๊ฒ€์ฆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

    โœ… ๊ธฐ๋ณธ ๋„๊ตฌ

  • [ ] Docker ๋ฒ„์ „ 20.10+ ์„ค์น˜ ๋ฐ ์‹คํ–‰ ์ค‘
  • [ ] Azure CLI 2.40+ ์„ค์น˜ ๋ฐ ์ธ์ฆ ์™„๋ฃŒ
  • [ ] Python 3.8+ ๋ฐ pip ์„ค์น˜ ์™„๋ฃŒ
  • [ ] Git 2.30+ ์„ค์น˜ ์™„๋ฃŒ
  • [ ] VS Code ๋ฐ ํ•„์ˆ˜ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์„ค์น˜ ์™„๋ฃŒ
  • โœ… Azure ๋ฆฌ์†Œ์Šค

  • [ ] ๋ฆฌ์†Œ์Šค ๊ทธ๋ฃน ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋จ
  • [ ] AI Foundry ํ”„๋กœ์ ํŠธ ๋ฐฐํฌ ์™„๋ฃŒ
  • [ ] OpenAI text-embedding-3-small ๋ชจ๋ธ ๋ฐฐํฌ ์™„๋ฃŒ
  • [ ] Application Insights ๊ตฌ์„ฑ ์™„๋ฃŒ
  • [ ] ์ ์ ˆํ•œ ๊ถŒํ•œ์„ ๊ฐ€์ง„ ์„œ๋น„์Šค ์ฃผ์ฒด ์ƒ์„ฑ๋จ
  • โœ… ํ™˜๊ฒฝ ๊ตฌ์„ฑ

  • [ ] .env ํŒŒ์ผ ์ƒ์„ฑ ๋ฐ ๋ชจ๋“  ํ•„์ˆ˜ ๋ณ€์ˆ˜ ํฌํ•จ
  • [ ] Azure ์ž๊ฒฉ ์ฆ๋ช… ์ž‘๋™ (az account show๋กœ ํ…Œ์ŠคํŠธ)
  • [ ] PostgreSQL ์ปจํ…Œ์ด๋„ˆ ์‹คํ–‰ ๋ฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • [ ] ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์™„๋ฃŒ
  • โœ… VS Code ํ†ตํ•ฉ

  • [ ] .vscode/mcp.json ๊ตฌ์„ฑ ์™„๋ฃŒ
  • [ ] Python ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋ฅผ ๊ฐ€์ƒ ํ™˜๊ฒฝ์œผ๋กœ ์„ค์ •
  • [ ] MCP ์„œ๋ฒ„๊ฐ€ AI Chat์— ํ‘œ์‹œ๋จ
  • [ ] AI Chat์„ ํ†ตํ•ด ํ…Œ์ŠคํŠธ ์ฟผ๋ฆฌ ์‹คํ–‰ ๊ฐ€๋Šฅ
  • ๐Ÿ› ๏ธ ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ

    Docker ๋ฌธ์ œ

    ๋ฌธ์ œ: Docker ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ์‹œ์ž‘๋˜์ง€ ์•Š์Œ

    
    # Check Docker service status
    
    docker info
    
    
    
    # Check available resources
    
    docker system df
    
    
    
    # Clean up if needed
    
    docker system prune -f
    
    
    
    # Restart Docker Desktop (Windows/macOS)
    
    # Or restart Docker service (Linux)
    
    sudo systemctl restart docker
    
    

    ๋ฌธ์ œ: PostgreSQL ์—ฐ๊ฒฐ ์‹คํŒจ

    
    # Check container logs
    
    docker-compose logs postgres
    
    
    
    # Verify container is healthy
    
    docker-compose ps
    
    
    
    # Test direct connection
    
    docker-compose exec postgres psql -U postgres -d zava -c "SELECT 1;"
    
    

    Azure ๋ฐฐํฌ ๋ฌธ์ œ

    ๋ฌธ์ œ: Azure ๋ฐฐํฌ ์‹คํŒจ

    
    # Check Azure CLI authentication
    
    az account show
    
    
    
    # Verify subscription permissions
    
    az role assignment list --assignee $(az account show --query user.name -o tsv)
    
    
    
    # Check resource provider registration
    
    az provider register --namespace Microsoft.CognitiveServices
    
    az provider register --namespace Microsoft.Insights
    
    

    ๋ฌธ์ œ: AI ์„œ๋น„์Šค ์ธ์ฆ ์‹คํŒจ

    
    # Test service principal
    
    az login --service-principal \
    
      --username $AZURE_CLIENT_ID \
    
      --password $AZURE_CLIENT_SECRET \
    
      --tenant $AZURE_TENANT_ID
    
    
    
    # Verify AI service deployment
    
    az cognitiveservices account list --query "[].{Name:name,Kind:kind,Location:location}"
    
    

    Python ํ™˜๊ฒฝ ๋ฌธ์ œ

    ๋ฌธ์ œ: ํŒจํ‚ค์ง€ ์„ค์น˜ ์‹คํŒจ

    
    # Upgrade pip and setuptools
    
    python -m pip install --upgrade pip setuptools wheel
    
    
    
    # Clear pip cache
    
    pip cache purge
    
    
    
    # Install packages one by one to identify issues
    
    pip install fastmcp
    
    pip install asyncpg
    
    pip install azure-ai-projects
    
    

    ๋ฌธ์ œ: VS Code์—์„œ Python ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ

    
    # Show Python interpreter paths
    
    which python  # macOS/Linux
    
    where python  # Windows
    
    
    
    # Activate virtual environment first
    
    source mcp-env/bin/activate  # macOS/Linux
    
    mcp-env\Scripts\activate     # Windows
    
    
    
    # Then open VS Code
    
    code .
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ์™„์ „ํ•œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ: ๋ชจ๋“  ๋„๊ตฌ ์„ค์น˜ ๋ฐ ๊ตฌ์„ฑ ์™„๋ฃŒ

    โœ… Azure ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ: AI ์„œ๋น„์Šค ๋ฐ ์ง€์› ์ธํ”„๋ผ

    โœ… Docker ํ™˜๊ฒฝ ์‹คํ–‰: PostgreSQL ๋ฐ MCP ์„œ๋ฒ„ ์ปจํ…Œ์ด๋„ˆ

    โœ… VS Code ํ†ตํ•ฉ: MCP ์„œ๋ฒ„ ๊ตฌ์„ฑ ๋ฐ ์ ‘๊ทผ ๊ฐ€๋Šฅ

    โœ… ์„ค์ • ๊ฒ€์ฆ ์™„๋ฃŒ: ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ ํ…Œ์ŠคํŠธ ๋ฐ ์ž‘๋™ ํ™•์ธ

    โœ… ๋ฌธ์ œ ํ•ด๊ฒฐ ์ง€์‹: ์ผ๋ฐ˜์ ์ธ ๋ฌธ์ œ ๋ฐ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ํ™˜๊ฒฝ์ด ์ค€๋น„๋˜์—ˆ์œผ๋ฉด Lab 04: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์Šคํ‚ค๋งˆ๋กœ ๊ณ„์† ์ง„ํ–‰ํ•˜์„ธ์š”:

  • ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ๋ฅผ ์ž์„ธํžˆ ํƒ์ƒ‰
  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋ง ์ดํ•ด
  • ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ตฌํ˜„ ํ•™์Šต
  • ์ƒ˜ํ”Œ ์†Œ๋งค ๋ฐ์ดํ„ฐ ์ž‘์—…
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ๊ฐœ๋ฐœ ๋„๊ตฌ

  • Docker ๋ฌธ์„œ - Docker ์ฐธ์กฐ ์ž๋ฃŒ
  • Azure CLI ์ฐธ์กฐ - Azure CLI ๋ช…๋ น์–ด
  • VS Code ๋ฌธ์„œ - ํŽธ์ง‘๊ธฐ ๊ตฌ์„ฑ ๋ฐ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ
  • Azure ์„œ๋น„์Šค

  • Azure AI Foundry ๋ฌธ์„œ - AI ์„œ๋น„์Šค ๊ตฌ์„ฑ
  • Azure OpenAI ์„œ๋น„์Šค - AI ๋ชจ๋ธ ๋ฐฐํฌ
  • Application Insights - ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •
  • Python ๊ฐœ๋ฐœ

  • Python ๊ฐ€์ƒ ํ™˜๊ฒฝ - ํ™˜๊ฒฝ ๊ด€๋ฆฌ
  • AsyncIO ๋ฌธ์„œ - ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํŒจํ„ด
  • FastAPI ๋ฌธ์„œ - ์›น ํ”„๋ ˆ์ž„์›Œํฌ ํŒจํ„ด
  • ---

    ๋‹ค์Œ: ํ™˜๊ฒฝ์ด ์ค€๋น„๋˜์—ˆ๋‚˜์š”? Lab 04: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์Šคํ‚ค๋งˆ๋กœ ๊ณ„์† ์ง„ํ–‰ํ•˜์„ธ์š”.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์‹ค์Šต 4-6: MCP ์„œ๋ฒ„ ๊ตฌ์ถ• 04 ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์Šคํ‚ค๋งˆ

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์Šคํ‚ค๋งˆ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์—์„œ๋Š” Zava Retail ์‹œ์Šคํ…œ์„ ์œ„ํ•œ PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„์— ๋Œ€ํ•ด ๊นŠ์ด ํƒ๊ตฌํ•ฉ๋‹ˆ๋‹ค. ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ, ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋ง, ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ Row Level Security(RLS)๋ฅผ ํฌํ•จํ•œ ํฌ๊ด„์ ์ธ ์†Œ๋งค ์Šคํ‚ค๋งˆ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” MCP ์„œ๋ฒ„์˜ ๊ธฐ๋ฐ˜์œผ๋กœ, ์—ฌ๋Ÿฌ ๋งค์žฅ์˜ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋ฉด์„œ ์—„๊ฒฉํ•œ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” PostgreSQL๊ณผ pgvector ํ™•์žฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ณ ๊ฐ์ด ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์ œํ’ˆ์„ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” ์˜๋ฏธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์šฐ๋ฆฌ์˜ ์Šคํ‚ค๋งˆ๋Š” ํ˜„๋Œ€์ ์ธ ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ํŒจํ„ด์„ ๋”ฐ๋ฅด๋ฉฐ, Row Level Security๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์Šน์ธ๋œ ๋งค์žฅ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ์„ ์ œ๊ณตํ•˜๋ฉด์„œ๋„ ์ตœ์ ์˜ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์„ค๊ณ„: ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„
  • ๊ตฌํ˜„: ๋ฒกํ„ฐ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ PostgreSQL๊ณผ pgvector ์„ค์ •
  • ๊ตฌ์„ฑ: ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ Row Level Security ์„ค์ •
  • ์ƒ์„ฑ: ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ํ˜„์‹ค์ ์ธ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  • ์ตœ์ ํ™”: ์†Œ๋งค ์›Œํฌ๋กœ๋“œ๋ฅผ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๊ตฌํ˜„: ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ ์ „๋žต
  • ๐Ÿ—ƒ๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•„ํ‚คํ…์ฒ˜

    PostgreSQL๊ณผ pgvector

    ์šฐ๋ฆฌ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” PostgreSQL์˜ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๊ธฐ๋Šฅ๊ณผ AI ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ pgvector ํ™•์žฅ์„ ๊ฒฐํ•ฉํ•˜์—ฌ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค:

    
    -- Enable required extensions
    
    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
    
    CREATE EXTENSION IF NOT EXISTS "pgcrypto";
    
    CREATE EXTENSION IF NOT EXISTS "vector";
    
    
    
    -- Verify vector extension installation
    
    SELECT * FROM pg_extension WHERE extname = 'vector';
    
    

    ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” Row Level Security๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณต์œ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๊ณต์œ  ์Šคํ‚ค๋งˆ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                 PostgreSQL                      โ”‚
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  retail Schema (Shared)                        โ”‚
    
    โ”‚  โ”œโ”€โ”€ stores (Master tenant data)               โ”‚
    
    โ”‚  โ”œโ”€โ”€ customers (RLS by store_id)               โ”‚
    
    โ”‚  โ”œโ”€โ”€ products (RLS by store_id)                โ”‚
    
    โ”‚  โ”œโ”€โ”€ sales_transactions (RLS by store_id)      โ”‚
    
    โ”‚  โ”œโ”€โ”€ sales_transaction_items (RLS via join)    โ”‚
    
    โ”‚  โ””โ”€โ”€ product_embeddings (RLS by store_id)      โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ๐Ÿ“Š ํ•ต์‹ฌ ์Šคํ‚ค๋งˆ ์„ค๊ณ„

    Stores ํ…Œ์ด๋ธ” (ํ…Œ๋„ŒํŠธ ๋งˆ์Šคํ„ฐ)

    
    -- Stores table: Master tenant registry
    
    CREATE TABLE retail.stores (
    
        store_id VARCHAR(50) PRIMARY KEY,
    
        store_name VARCHAR(100) NOT NULL,
    
        store_location VARCHAR(100),
    
        store_type VARCHAR(50),
    
        region VARCHAR(50),
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        is_active BOOLEAN DEFAULT TRUE
    
    );
    
    
    
    -- Sample stores data
    
    INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region) VALUES
    
    ('seattle', 'Zava Retail Seattle', 'Seattle, WA', 'flagship', 'west'),
    
    ('redmond', 'Zava Retail Redmond', 'Redmond, WA', 'standard', 'west'),
    
    ('bellevue', 'Zava Retail Bellevue', 'Bellevue, WA', 'standard', 'west'),
    
    ('online', 'Zava Retail Online', 'Digital', 'ecommerce', 'global');
    
    
    
    -- Create index for performance
    
    CREATE INDEX idx_stores_region ON retail.stores(region);
    
    CREATE INDEX idx_stores_active ON retail.stores(is_active) WHERE is_active = TRUE;
    
    

    Customers ํ…Œ์ด๋ธ”

    
    -- Customers table with RLS
    
    CREATE TABLE retail.customers (
    
        customer_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        first_name VARCHAR(50) NOT NULL,
    
        last_name VARCHAR(50) NOT NULL,
    
        email VARCHAR(100) UNIQUE NOT NULL,
    
        phone VARCHAR(20),
    
        date_of_birth DATE,
    
        gender VARCHAR(20),
    
        customer_since DATE DEFAULT CURRENT_DATE,
    
        loyalty_tier VARCHAR(20) DEFAULT 'bronze',
    
        total_lifetime_value DECIMAL(10,2) DEFAULT 0.00,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Enable RLS
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy: Users can only see customers from their store
    
    CREATE POLICY customers_store_isolation ON retail.customers
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Indexes for performance
    
    CREATE INDEX idx_customers_store_id ON retail.customers(store_id);
    
    CREATE INDEX idx_customers_email ON retail.customers(email);
    
    CREATE INDEX idx_customers_loyalty_tier ON retail.customers(loyalty_tier);
    
    CREATE INDEX idx_customers_created_at ON retail.customers(created_at);
    
    

    Products ํ…Œ์ด๋ธ” ๋ฐ ์นดํ…Œ๊ณ ๋ฆฌ

    
    -- Product categories
    
    CREATE TABLE retail.product_categories (
    
        category_id SERIAL PRIMARY KEY,
    
        category_name VARCHAR(100) NOT NULL UNIQUE,
    
        parent_category_id INTEGER REFERENCES retail.product_categories(category_id),
    
        description TEXT,
    
        is_active BOOLEAN DEFAULT TRUE
    
    );
    
    
    
    -- Insert sample categories
    
    INSERT INTO retail.product_categories (category_name, description) VALUES
    
    ('Electronics', 'Electronic devices and accessories'),
    
    ('Clothing', 'Apparel and fashion items'),
    
    ('Home & Garden', 'Home improvement and garden supplies'),
    
    ('Sports & Outdoors', 'Sports equipment and outdoor gear'),
    
    ('Books & Media', 'Books, movies, and digital media'),
    
    ('Health & Beauty', 'Health and beauty products'),
    
    ('Automotive', 'Car parts and automotive accessories');
    
    
    
    -- Products table with rich metadata
    
    CREATE TABLE retail.products (
    
        product_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        sku VARCHAR(50) NOT NULL,
    
        product_name VARCHAR(200) NOT NULL,
    
        product_description TEXT,
    
        category_id INTEGER REFERENCES retail.product_categories(category_id),
    
        brand VARCHAR(100),
    
        model VARCHAR(100),
    
        color VARCHAR(50),
    
        size VARCHAR(50),
    
        weight_kg DECIMAL(8,3),
    
        dimensions_cm VARCHAR(50), -- e.g., "30x20x15"
    
        price DECIMAL(10,2) NOT NULL,
    
        cost DECIMAL(10,2),
    
        current_stock INTEGER DEFAULT 0,
    
        minimum_stock INTEGER DEFAULT 0,
    
        maximum_stock INTEGER DEFAULT 1000,
    
        reorder_point INTEGER DEFAULT 10,
    
        supplier_name VARCHAR(100),
    
        supplier_sku VARCHAR(50),
    
        is_active BOOLEAN DEFAULT TRUE,
    
        is_featured BOOLEAN DEFAULT FALSE,
    
        rating_average DECIMAL(3,2) DEFAULT 0.00,
    
        rating_count INTEGER DEFAULT 0,
    
        tags TEXT[], -- Array of tags for flexible categorization
    
        metadata JSONB, -- Flexible metadata storage
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure SKU uniqueness within store
    
        CONSTRAINT unique_sku_per_store UNIQUE (store_id, sku)
    
    );
    
    
    
    -- Enable RLS for products
    
    ALTER TABLE retail.products ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for products
    
    CREATE POLICY products_store_isolation ON retail.products
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Comprehensive indexes
    
    CREATE INDEX idx_products_store_id ON retail.products(store_id);
    
    CREATE INDEX idx_products_sku ON retail.products(sku);
    
    CREATE INDEX idx_products_category ON retail.products(category_id);
    
    CREATE INDEX idx_products_brand ON retail.products(brand);
    
    CREATE INDEX idx_products_price ON retail.products(price);
    
    CREATE INDEX idx_products_stock ON retail.products(current_stock);
    
    CREATE INDEX idx_products_active ON retail.products(is_active) WHERE is_active = TRUE;
    
    CREATE INDEX idx_products_featured ON retail.products(is_featured) WHERE is_featured = TRUE;
    
    CREATE INDEX idx_products_tags ON retail.products USING GIN(tags);
    
    CREATE INDEX idx_products_metadata ON retail.products USING GIN(metadata);
    
    CREATE INDEX idx_products_text_search ON retail.products USING GIN(
    
        to_tsvector('english', product_name || ' ' || COALESCE(product_description, '') || ' ' || COALESCE(brand, ''))
    
    );
    
    

    ํŒ๋งค ๊ฑฐ๋ž˜

    
    -- Sales transactions table
    
    CREATE TABLE retail.sales_transactions (
    
        transaction_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        customer_id UUID REFERENCES retail.customers(customer_id),
    
        transaction_date TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        transaction_type VARCHAR(20) DEFAULT 'sale', -- 'sale', 'return', 'exchange'
    
        payment_method VARCHAR(50), -- 'cash', 'credit_card', 'debit_card', 'digital_wallet'
    
        subtotal DECIMAL(10,2) NOT NULL,
    
        tax_amount DECIMAL(10,2) DEFAULT 0.00,
    
        discount_amount DECIMAL(10,2) DEFAULT 0.00,
    
        total_amount DECIMAL(10,2) NOT NULL,
    
        cashier_id VARCHAR(50),
    
        register_id VARCHAR(50),
    
        receipt_number VARCHAR(50),
    
        notes TEXT,
    
        metadata JSONB,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Sales transaction items (line items)
    
    CREATE TABLE retail.sales_transaction_items (
    
        item_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        transaction_id UUID NOT NULL REFERENCES retail.sales_transactions(transaction_id) ON DELETE CASCADE,
    
        product_id UUID NOT NULL REFERENCES retail.products(product_id),
    
        quantity INTEGER NOT NULL DEFAULT 1,
    
        unit_price DECIMAL(10,2) NOT NULL,
    
        total_price DECIMAL(10,2) NOT NULL,
    
        discount_amount DECIMAL(10,2) DEFAULT 0.00,
    
        tax_amount DECIMAL(10,2) DEFAULT 0.00,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure positive quantities and prices
    
        CONSTRAINT positive_quantity CHECK (quantity > 0),
    
        CONSTRAINT positive_unit_price CHECK (unit_price >= 0),
    
        CONSTRAINT positive_total_price CHECK (total_price >= 0)
    
    );
    
    
    
    -- Enable RLS for transactions
    
    ALTER TABLE retail.sales_transactions ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for sales transactions
    
    CREATE POLICY sales_transactions_store_isolation ON retail.sales_transactions
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- RLS for transaction items (via join with transactions)
    
    ALTER TABLE retail.sales_transaction_items ENABLE ROW LEVEL SECURITY;
    
    
    
    CREATE POLICY sales_transaction_items_store_isolation ON retail.sales_transaction_items
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        );
    
    
    
    -- Performance indexes
    
    CREATE INDEX idx_sales_transactions_store_id ON retail.sales_transactions(store_id);
    
    CREATE INDEX idx_sales_transactions_customer_id ON retail.sales_transactions(customer_id);
    
    CREATE INDEX idx_sales_transactions_date ON retail.sales_transactions(transaction_date);
    
    CREATE INDEX idx_sales_transactions_type ON retail.sales_transactions(transaction_type);
    
    CREATE INDEX idx_sales_transactions_payment ON retail.sales_transactions(payment_method);
    
    
    
    CREATE INDEX idx_sales_transaction_items_transaction_id ON retail.sales_transaction_items(transaction_id);
    
    CREATE INDEX idx_sales_transaction_items_product_id ON retail.sales_transaction_items(product_id);
    
    

    ๐Ÿ” ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ตฌํ˜„

    Product Embeddings ํ…Œ์ด๋ธ”

    
    -- Product embeddings for semantic search
    
    CREATE TABLE retail.product_embeddings (
    
        embedding_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        product_id UUID NOT NULL REFERENCES retail.products(product_id) ON DELETE CASCADE,
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        embedding_text TEXT NOT NULL, -- The text that was embedded
    
        embedding vector(1536), -- OpenAI text-embedding-3-small dimension
    
        embedding_model VARCHAR(100) NOT NULL DEFAULT 'text-embedding-3-small',
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure one embedding per product per model
    
        CONSTRAINT unique_product_embedding UNIQUE (product_id, embedding_model)
    
    );
    
    
    
    -- Enable RLS for embeddings
    
    ALTER TABLE retail.product_embeddings ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for embeddings
    
    CREATE POLICY product_embeddings_store_isolation ON retail.product_embeddings
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Vector similarity index (HNSW for fast approximate search)
    
    CREATE INDEX idx_product_embeddings_vector ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops);
    
    
    
    -- Additional indexes
    
    CREATE INDEX idx_product_embeddings_product_id ON retail.product_embeddings(product_id);
    
    CREATE INDEX idx_product_embeddings_store_id ON retail.product_embeddings(store_id);
    
    CREATE INDEX idx_product_embeddings_model ON retail.product_embeddings(embedding_model);
    
    

    ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ํ•จ์ˆ˜

    
    -- Function to search products by similarity
    
    CREATE OR REPLACE FUNCTION retail.search_products_by_similarity(
    
        search_embedding vector(1536),
    
        similarity_threshold float DEFAULT 0.7,
    
        max_results integer DEFAULT 20
    
    )
    
    RETURNS TABLE (
    
        product_id UUID,
    
        product_name VARCHAR(200),
    
        product_description TEXT,
    
        brand VARCHAR(100),
    
        price DECIMAL(10,2),
    
        similarity_score float
    
    ) 
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            p.product_id,
    
            p.product_name,
    
            p.product_description,
    
            p.brand,
    
            p.price,
    
            1 - (pe.embedding <=> search_embedding) as similarity_score
    
        FROM retail.product_embeddings pe
    
        JOIN retail.products p ON pe.product_id = p.product_id
    
        WHERE 
    
            pe.store_id = current_setting('app.current_store_id', true)
    
            AND p.is_active = TRUE
    
            AND 1 - (pe.embedding <=> search_embedding) >= similarity_threshold
    
        ORDER BY pe.embedding <=> search_embedding
    
        LIMIT max_results;
    
    END;
    
    $$;
    
    
    
    -- Grant execute permission
    
    GRANT EXECUTE ON FUNCTION retail.search_products_by_similarity TO mcp_user;
    
    

    ๐Ÿ” Row Level Security ์„ค์ •

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ญํ•  ๋ฐ ๊ถŒํ•œ

    
    -- Create MCP application role
    
    CREATE ROLE mcp_user LOGIN;
    
    
    
    -- Grant schema usage
    
    GRANT USAGE ON SCHEMA retail TO mcp_user;
    
    
    
    -- Grant table permissions
    
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA retail TO mcp_user;
    
    GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA retail TO mcp_user;
    
    
    
    -- Grant permissions on future tables
    
    ALTER DEFAULT PRIVILEGES IN SCHEMA retail GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO mcp_user;
    
    ALTER DEFAULT PRIVILEGES IN SCHEMA retail GRANT USAGE, SELECT ON SEQUENCES TO mcp_user;
    
    
    
    -- Function to set store context
    
    CREATE OR REPLACE FUNCTION retail.set_store_context(store_id_param VARCHAR(50))
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    BEGIN
    
        -- Verify store exists and user has access
    
        IF NOT EXISTS (SELECT 1 FROM retail.stores WHERE store_id = store_id_param AND is_active = TRUE) THEN
    
            RAISE EXCEPTION 'Invalid or inactive store: %', store_id_param;
    
        END IF;
    
        
    
        -- Set the store context
    
        PERFORM set_config('app.current_store_id', store_id_param, false);
    
        
    
        -- Log the context change
    
        INSERT INTO retail.audit_log (
    
            table_name,
    
            action,
    
            user_name,
    
            store_id,
    
            metadata
    
        ) VALUES (
    
            'security_context',
    
            'store_context_set',
    
            current_user,
    
            store_id_param,
    
            jsonb_build_object('timestamp', current_timestamp)
    
        );
    
    END;
    
    $$;
    
    
    
    -- Grant execute permission
    
    GRANT EXECUTE ON FUNCTION retail.set_store_context TO mcp_user;
    
    

    ๊ฐ์‚ฌ ๋กœ๊ทธ

    
    -- Audit log table for security and compliance
    
    CREATE TABLE retail.audit_log (
    
        log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        table_name VARCHAR(100) NOT NULL,
    
        action VARCHAR(50) NOT NULL, -- INSERT, UPDATE, DELETE, SELECT
    
        user_name VARCHAR(100) NOT NULL DEFAULT current_user,
    
        store_id VARCHAR(50),
    
        record_id UUID,
    
        old_values JSONB,
    
        new_values JSONB,
    
        metadata JSONB,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Index for audit queries
    
    CREATE INDEX idx_audit_log_table_name ON retail.audit_log(table_name);
    
    CREATE INDEX idx_audit_log_action ON retail.audit_log(action);
    
    CREATE INDEX idx_audit_log_user_name ON retail.audit_log(user_name);
    
    CREATE INDEX idx_audit_log_store_id ON retail.audit_log(store_id);
    
    CREATE INDEX idx_audit_log_created_at ON retail.audit_log(created_at);
    
    
    
    -- Audit trigger function
    
    CREATE OR REPLACE FUNCTION retail.audit_trigger()
    
    RETURNS trigger AS $$
    
    BEGIN
    
        IF TG_OP = 'DELETE' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                old_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(OLD.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(OLD.customer_id, OLD.product_id, OLD.transaction_id),
    
                row_to_json(OLD)
    
            );
    
            RETURN OLD;
    
        ELSIF TG_OP = 'UPDATE' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                old_values,
    
                new_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(NEW.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(NEW.customer_id, NEW.product_id, NEW.transaction_id),
    
                row_to_json(OLD),
    
                row_to_json(NEW)
    
            );
    
            RETURN NEW;
    
        ELSIF TG_OP = 'INSERT' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                new_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(NEW.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(NEW.customer_id, NEW.product_id, NEW.transaction_id),
    
                row_to_json(NEW)
    
            );
    
            RETURN NEW;
    
        END IF;
    
        RETURN NULL;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    
    
    -- Create audit triggers
    
    CREATE TRIGGER customers_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.customers
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    
    
    CREATE TRIGGER products_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.products
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    
    
    CREATE TRIGGER sales_transactions_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.sales_transactions
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    

    ๐Ÿ“Š ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ

    ํ˜„์‹ค์ ์ธ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์Šคํฌ๋ฆฝํŠธ

    
    # scripts/generate_sample_data.py
    
    """
    
    Generate realistic sample data for the Zava Retail database.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import random
    
    import json
    
    from datetime import datetime, timedelta
    
    from faker import Faker
    
    from typing import List, Dict, Any
    
    import numpy as np
    
    
    
    fake = Faker()
    
    
    
    class SampleDataGenerator:
    
        """Generate realistic retail sample data."""
    
        
    
        def __init__(self, connection_string: str):
    
            self.connection_string = connection_string
    
            self.stores = ['seattle', 'redmond', 'bellevue', 'online']
    
            
    
            # Product categories with realistic items
    
            self.product_data = {
    
                'Electronics': {
    
                    'brands': ['Apple', 'Samsung', 'Sony', 'LG', 'HP', 'Dell'],
    
                    'items': [
    
                        'Smartphone', 'Laptop', 'Tablet', 'Headphones', 'Smart TV',
    
                        'Gaming Console', 'Smartwatch', 'Bluetooth Speaker'
    
                    ]
    
                },
    
                'Clothing': {
    
                    'brands': ['Nike', 'Adidas', 'Zara', 'H&M', 'Levi\'s', 'Gap'],
    
                    'items': [
    
                        'T-Shirt', 'Jeans', 'Dress', 'Jacket', 'Sneakers',
    
                        'Sweater', 'Shorts', 'Blouse'
    
                    ]
    
                },
    
                'Home & Garden': {
    
                    'brands': ['IKEA', 'Home Depot', 'Wayfair', 'Target', 'Walmart'],
    
                    'items': [
    
                        'Sofa', 'Dining Table', 'Lamp', 'Garden Tool', 'Plant Pot',
    
                        'Curtains', 'Rug', 'Kitchen Appliance'
    
                    ]
    
                }
    
            }
    
        
    
        async def generate_all_data(self):
    
            """Generate complete sample dataset."""
    
            
    
            conn = await asyncpg.connect(self.connection_string)
    
            
    
            try:
    
                print("๐Ÿช Generating stores data...")
    
                await self._ensure_stores_exist(conn)
    
                
    
                print("๐Ÿ‘ฅ Generating customers...")
    
                customers = await self._generate_customers(conn, 2000)
    
                
    
                print("๐Ÿ“ฆ Generating products...")
    
                products = await self._generate_products(conn, 500)
    
                
    
                print("๐Ÿ›’ Generating sales transactions...")
    
                await self._generate_sales_transactions(conn, customers, products, 5000)
    
                
    
                print("โœ… Sample data generation complete!")
    
                
    
            finally:
    
                await conn.close()
    
        
    
        async def _ensure_stores_exist(self, conn):
    
            """Ensure all stores exist in the database."""
    
            
    
            stores_data = [
    
                ('seattle', 'Zava Retail Seattle', 'Seattle, WA', 'flagship', 'west'),
    
                ('redmond', 'Zava Retail Redmond', 'Redmond, WA', 'standard', 'west'),
    
                ('bellevue', 'Zava Retail Bellevue', 'Bellevue, WA', 'standard', 'west'),
    
                ('online', 'Zava Retail Online', 'Digital', 'ecommerce', 'global')
    
            ]
    
            
    
            for store_data in stores_data:
    
                await conn.execute("""
    
                    INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region)
    
                    VALUES ($1, $2, $3, $4, $5)
    
                    ON CONFLICT (store_id) DO NOTHING
    
                """, *store_data)
    
        
    
        async def _generate_customers(self, conn, count: int) -> List[Dict]:
    
            """Generate realistic customer data."""
    
            
    
            customers = []
    
            
    
            for _ in range(count):
    
                store_id = random.choice(self.stores)
    
                customer_data = {
    
                    'store_id': store_id,
    
                    'first_name': fake.first_name(),
    
                    'last_name': fake.last_name(),
    
                    'email': fake.unique.email(),
    
                    'phone': fake.phone_number()[:20],
    
                    'date_of_birth': fake.date_of_birth(minimum_age=18, maximum_age=80),
    
                    'gender': random.choice(['Male', 'Female', 'Other', 'Prefer not to say']),
    
                    'customer_since': fake.date_between(start_date='-5y', end_date='today'),
    
                    'loyalty_tier': random.choices(
    
                        ['bronze', 'silver', 'gold', 'platinum'],
    
                        weights=[50, 30, 15, 5]
    
                    )[0]
    
                }
    
                
    
                customer_id = await conn.fetchval("""
    
                    INSERT INTO retail.customers (
    
                        store_id, first_name, last_name, email, phone,
    
                        date_of_birth, gender, customer_since, loyalty_tier
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
    
                    RETURNING customer_id
    
                """, *customer_data.values())
    
                
    
                customer_data['customer_id'] = customer_id
    
                customers.append(customer_data)
    
            
    
            return customers
    
        
    
        async def _generate_products(self, conn, count: int) -> List[Dict]:
    
            """Generate realistic product data."""
    
            
    
            # Get category IDs
    
            categories = await conn.fetch("SELECT category_id, category_name FROM retail.product_categories")
    
            category_map = {cat['category_name']: cat['category_id'] for cat in categories}
    
            
    
            products = []
    
            
    
            for _ in range(count):
    
                store_id = random.choice(self.stores)
    
                category_name = random.choice(list(self.product_data.keys()))
    
                category_id = category_map.get(category_name)
    
                
    
                if not category_id:
    
                    continue
    
                
    
                brand = random.choice(self.product_data[category_name]['brands'])
    
                item_type = random.choice(self.product_data[category_name]['items'])
    
                
    
                # Generate realistic pricing
    
                base_price = random.uniform(10, 1000)
    
                cost = base_price * random.uniform(0.4, 0.7)  # 40-70% cost margin
    
                
    
                product_data = {
    
                    'store_id': store_id,
    
                    'sku': f"{brand[:3].upper()}-{fake.unique.random_number(digits=6)}",
    
                    'product_name': f"{brand} {item_type}",
    
                    'product_description': fake.text(max_nb_chars=500),
    
                    'category_id': category_id,
    
                    'brand': brand,
    
                    'model': f"Model {fake.random_number(digits=4)}",
    
                    'color': fake.color_name(),
    
                    'size': random.choice(['XS', 'S', 'M', 'L', 'XL', 'XXL', 'One Size']),
    
                    'weight_kg': round(random.uniform(0.1, 10.0), 2),
    
                    'price': round(base_price, 2),
    
                    'cost': round(cost, 2),
    
                    'current_stock': random.randint(0, 100),
    
                    'minimum_stock': random.randint(5, 20),
    
                    'reorder_point': random.randint(10, 30),
    
                    'supplier_name': fake.company(),
    
                    'is_featured': random.choice([True, False]),
    
                    'rating_average': round(random.uniform(3.0, 5.0), 2),
    
                    'rating_count': random.randint(0, 500),
    
                    'tags': random.sample([
    
                        'popular', 'new', 'sale', 'limited', 'bestseller', 
    
                        'eco-friendly', 'premium', 'budget'
    
                    ], k=random.randint(1, 3))
    
                }
    
                
    
                product_id = await conn.fetchval("""
    
                    INSERT INTO retail.products (
    
                        store_id, sku, product_name, product_description, category_id,
    
                        brand, model, color, size, weight_kg, price, cost,
    
                        current_stock, minimum_stock, reorder_point, supplier_name,
    
                        is_featured, rating_average, rating_count, tags
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
    
                    RETURNING product_id
    
                """, *product_data.values())
    
                
    
                product_data['product_id'] = product_id
    
                products.append(product_data)
    
            
    
            return products
    
        
    
        async def _generate_sales_transactions(self, conn, customers: List[Dict], products: List[Dict], count: int):
    
            """Generate realistic sales transaction data."""
    
            
    
            for _ in range(count):
    
                # Select customer and matching store products
    
                customer = random.choice(customers)
    
                store_products = [p for p in products if p['store_id'] == customer['store_id']]
    
                
    
                if not store_products:
    
                    continue
    
                
    
                # Generate transaction basics
    
                transaction_date = fake.date_time_between(start_date='-1y', end_date='now')
    
                transaction_type = random.choices(
    
                    ['sale', 'return', 'exchange'],
    
                    weights=[90, 7, 3]
    
                )[0]
    
                
    
                payment_method = random.choices(
    
                    ['credit_card', 'debit_card', 'cash', 'digital_wallet'],
    
                    weights=[45, 25, 20, 10]
    
                )[0]
    
                
    
                # Generate transaction items (1-5 items per transaction)
    
                num_items = random.choices([1, 2, 3, 4, 5], weights=[40, 30, 20, 7, 3])[0]
    
                selected_products = random.sample(store_products, min(num_items, len(store_products)))
    
                
    
                subtotal = 0
    
                transaction_items = []
    
                
    
                for product in selected_products:
    
                    quantity = random.randint(1, 3)
    
                    unit_price = product['price']
    
                    
    
                    # Apply random discounts occasionally
    
                    discount_amount = 0
    
                    if random.random() < 0.2:  # 20% chance of discount
    
                        discount_amount = unit_price * quantity * random.uniform(0.05, 0.25)
    
                    
    
                    total_price = (unit_price * quantity) - discount_amount
    
                    subtotal += total_price
    
                    
    
                    transaction_items.append({
    
                        'product_id': product['product_id'],
    
                        'quantity': quantity,
    
                        'unit_price': unit_price,
    
                        'total_price': total_price,
    
                        'discount_amount': discount_amount
    
                    })
    
                
    
                # Calculate totals
    
                discount_amount = sum(item['discount_amount'] for item in transaction_items)
    
                tax_amount = subtotal * 0.08  # 8% tax rate
    
                total_amount = subtotal + tax_amount
    
                
    
                # Insert transaction
    
                transaction_id = await conn.fetchval("""
    
                    INSERT INTO retail.sales_transactions (
    
                        store_id, customer_id, transaction_date, transaction_type,
    
                        payment_method, subtotal, tax_amount, discount_amount, total_amount,
    
                        cashier_id, register_id, receipt_number
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
    
                    RETURNING transaction_id
    
                """, 
    
                    customer['store_id'], customer['customer_id'], transaction_date,
    
                    transaction_type, payment_method, subtotal, tax_amount,
    
                    discount_amount, total_amount, f"CASHIER{random.randint(1, 10)}",
    
                    f"REG{random.randint(1, 5)}", f"RCP{fake.random_number(digits=8)}"
    
                )
    
                
    
                # Insert transaction items
    
                for item in transaction_items:
    
                    await conn.execute("""
    
                        INSERT INTO retail.sales_transaction_items (
    
                            transaction_id, product_id, quantity, unit_price,
    
                            total_price, discount_amount
    
                        ) VALUES ($1, $2, $3, $4, $5, $6)
    
                    """, 
    
                        transaction_id, item['product_id'], item['quantity'],
    
                        item['unit_price'], item['total_price'], item['discount_amount']
    
                    )
    
    
    
    # Usage example
    
    if __name__ == "__main__":
    
        import os
    
        from config import Config
    
        
    
        config = Config()
    
        generator = SampleDataGenerator(config.database.connection_string)
    
        
    
        asyncio.run(generator.generate_all_data())
    
    

    ๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™”

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์„ฑ

    
    -- Performance-oriented PostgreSQL settings
    
    -- Add to postgresql.conf
    
    
    
    # Memory settings
    
    shared_buffers = '256MB'                # 25% of RAM for dedicated DB server
    
    effective_cache_size = '1GB'           # Estimate of OS cache size
    
    work_mem = '4MB'                       # Memory for sorts and hash joins
    
    maintenance_work_mem = '64MB'          # Memory for VACUUM, CREATE INDEX
    
    
    
    # Connection settings
    
    max_connections = 100                  # Adjust based on application needs
    
    
    
    # Write-ahead logging
    
    wal_buffers = '16MB'
    
    checkpoint_segments = 32               # PostgreSQL < 9.5
    
    max_wal_size = '1GB'                   # PostgreSQL >= 9.5
    
    
    
    # Query planner
    
    random_page_cost = 1.1                 # SSD-optimized
    
    effective_io_concurrency = 200         # SSD concurrent I/O capability
    
    
    
    # Logging for performance monitoring
    
    log_min_duration_statement = 1000      # Log queries > 1 second
    
    log_checkpoints = on
    
    log_connections = on
    
    log_disconnections = on
    
    log_line_prefix = '%t [%p-%l] %q%u@%d '
    
    

    ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๋ทฐ

    
    -- Create monitoring views for query performance
    
    CREATE VIEW retail.slow_queries AS
    
    SELECT 
    
        query,
    
        calls,
    
        total_exec_time,
    
        mean_exec_time,
    
        max_exec_time,
    
        stddev_exec_time,
    
        rows,
    
        100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
    
    FROM pg_stat_statements
    
    WHERE mean_exec_time > 100  -- Queries taking more than 100ms on average
    
    ORDER BY mean_exec_time DESC;
    
    
    
    -- Table sizes and index usage
    
    CREATE VIEW retail.table_stats AS
    
    SELECT
    
        schemaname,
    
        tablename,
    
        pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
    
        pg_stat_get_tuples_inserted(c.oid) as inserts,
    
        pg_stat_get_tuples_updated(c.oid) as updates,
    
        pg_stat_get_tuples_deleted(c.oid) as deletes,
    
        pg_stat_get_live_tuples(c.oid) as live_tuples,
    
        pg_stat_get_dead_tuples(c.oid) as dead_tuples
    
    FROM pg_tables pt
    
    JOIN pg_class c ON c.relname = pt.tablename
    
    WHERE schemaname = 'retail'
    
    ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
    
    
    
    -- Index usage statistics
    
    CREATE VIEW retail.index_usage AS
    
    SELECT
    
        schemaname,
    
        tablename,
    
        indexname,
    
        idx_tup_read,
    
        idx_tup_fetch,
    
        pg_size_pretty(pg_relation_size(indexrelname)) as size
    
    FROM pg_stat_user_indexes
    
    WHERE schemaname = 'retail'
    
    ORDER BY idx_tup_read DESC;
    
    

    ์ž๋™ ์œ ์ง€ ๊ด€๋ฆฌ

    
    -- Create function for automated maintenance
    
    CREATE OR REPLACE FUNCTION retail.perform_maintenance()
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    AS $$
    
    BEGIN
    
        -- Update table statistics
    
        ANALYZE retail.customers;
    
        ANALYZE retail.products;
    
        ANALYZE retail.sales_transactions;
    
        ANALYZE retail.sales_transaction_items;
    
        ANALYZE retail.product_embeddings;
    
        
    
        -- Vacuum tables with high update/delete activity
    
        VACUUM (ANALYZE, VERBOSE) retail.customers;
    
        VACUUM (ANALYZE, VERBOSE) retail.products;
    
        
    
        -- Reindex if needed (check for index bloat)
    
        REINDEX INDEX CONCURRENTLY idx_products_text_search;
    
        REINDEX INDEX CONCURRENTLY idx_product_embeddings_vector;
    
        
    
        -- Log maintenance completion
    
        INSERT INTO retail.audit_log (
    
            table_name,
    
            action,
    
            metadata
    
        ) VALUES (
    
            'maintenance',
    
            'automated_maintenance_completed',
    
            jsonb_build_object(
    
                'timestamp', current_timestamp,
    
                'database_size', pg_database_size(current_database())
    
            )
    
        );
    
    END;
    
    $$;
    
    
    
    -- Schedule maintenance (would typically be done via cron or scheduled job)
    
    -- Example cron entry: 0 2 * * 0 psql -d retail_db -c "SELECT retail.perform_maintenance();"
    
    

    ๐Ÿ’พ ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ

    ๋ฐฑ์—… ์ „๋žต

    
    #!/bin/bash
    
    # scripts/backup_database.sh
    
    
    
    # Comprehensive backup script for production environments
    
    
    
    set -e
    
    
    
    # Configuration
    
    DB_HOST="${POSTGRES_HOST:-localhost}"
    
    DB_PORT="${POSTGRES_PORT:-5432}"
    
    DB_NAME="${POSTGRES_DB:-retail_db}"
    
    DB_USER="${POSTGRES_USER:-postgres}"
    
    BACKUP_DIR="/backups/postgresql"
    
    RETENTION_DAYS=30
    
    
    
    # Create backup directory
    
    mkdir -p "$BACKUP_DIR"
    
    
    
    # Generate backup filename with timestamp
    
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    
    BACKUP_FILE="$BACKUP_DIR/retail_backup_$TIMESTAMP.sql"
    
    COMPRESSED_BACKUP="$BACKUP_FILE.gz"
    
    
    
    echo "Starting database backup: $TIMESTAMP"
    
    
    
    # Create comprehensive backup
    
    pg_dump \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --dbname="$DB_NAME" \
    
        --verbose \
    
        --clean \
    
        --create \
    
        --if-exists \
    
        --format=custom \
    
        --file="$BACKUP_FILE"
    
    
    
    # Compress backup
    
    gzip "$BACKUP_FILE"
    
    
    
    # Verify backup integrity
    
    echo "Verifying backup integrity..."
    
    pg_restore --list "$COMPRESSED_BACKUP" > /dev/null
    
    
    
    # Clean up old backups
    
    find "$BACKUP_DIR" -name "retail_backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
    
    
    
    # Calculate backup size
    
    BACKUP_SIZE=$(du -h "$COMPRESSED_BACKUP" | cut -f1)
    
    
    
    echo "Backup completed successfully:"
    
    echo "  File: $COMPRESSED_BACKUP"
    
    echo "  Size: $BACKUP_SIZE"
    
    echo "  Timestamp: $TIMESTAMP"
    
    
    
    # Optional: Upload to cloud storage
    
    if [ -n "$AZURE_STORAGE_ACCOUNT" ] && [ -n "$AZURE_STORAGE_KEY" ]; then
    
        echo "Uploading backup to Azure Storage..."
    
        az storage blob upload \
    
            --account-name "$AZURE_STORAGE_ACCOUNT" \
    
            --account-key "$AZURE_STORAGE_KEY" \
    
            --container-name "database-backups" \
    
            --name "retail_backup_$TIMESTAMP.sql.gz" \
    
            --file "$COMPRESSED_BACKUP"
    
    fi
    
    

    ๋ณต๊ตฌ ์ ˆ์ฐจ

    
    #!/bin/bash
    
    # scripts/restore_database.sh
    
    
    
    # Database restoration script
    
    
    
    set -e
    
    
    
    if [ $# -lt 1 ]; then
    
        echo "Usage: $0 <backup_file> [target_database]"
    
        echo "Example: $0 /backups/retail_backup_20241001_120000.sql.gz retail_db_restored"
    
        exit 1
    
    fi
    
    
    
    BACKUP_FILE="$1"
    
    TARGET_DB="${2:-retail_db_restored}"
    
    
    
    # Configuration
    
    DB_HOST="${POSTGRES_HOST:-localhost}"
    
    DB_PORT="${POSTGRES_PORT:-5432}"
    
    DB_USER="${POSTGRES_USER:-postgres}"
    
    
    
    echo "Starting database restoration..."
    
    echo "  Source: $BACKUP_FILE"
    
    echo "  Target: $TARGET_DB"
    
    
    
    # Verify backup file exists
    
    if [ ! -f "$BACKUP_FILE" ]; then
    
        echo "Error: Backup file not found: $BACKUP_FILE"
    
        exit 1
    
    fi
    
    
    
    # Create target database
    
    createdb \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --owner="$DB_USER" \
    
        "$TARGET_DB"
    
    
    
    # Restore from backup
    
    if [[ "$BACKUP_FILE" == *.gz ]]; then
    
        # Compressed backup
    
        gunzip -c "$BACKUP_FILE" | pg_restore \
    
            --host="$DB_HOST" \
    
            --port="$DB_PORT" \
    
            --username="$DB_USER" \
    
            --dbname="$TARGET_DB" \
    
            --verbose \
    
            --clean \
    
            --if-exists
    
    else
    
        # Uncompressed backup
    
        pg_restore \
    
            --host="$DB_HOST" \
    
            --port="$DB_PORT" \
    
            --username="$DB_USER" \
    
            --dbname="$TARGET_DB" \
    
            --verbose \
    
            --clean \
    
            --if-exists \
    
            "$BACKUP_FILE"
    
    fi
    
    
    
    echo "Database restoration completed successfully!"
    
    echo "Restored database: $TARGET_DB"
    
    
    
    # Verify restoration
    
    echo "Verifying restoration..."
    
    TABLES_COUNT=$(psql \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --dbname="$TARGET_DB" \
    
        --tuples-only \
    
        --command="SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'retail';"
    
    )
    
    
    
    echo "Verified $TABLES_COUNT tables in retail schema"
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„: ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ Row Level Security ๊ตฌํ˜„

    โœ… ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ: ์˜๋ฏธ ์žˆ๋Š” ์ œํ’ˆ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ pgvector ์„ค์ •

    โœ… ํฌ๊ด„์ ์ธ ์Šคํ‚ค๋งˆ: ํ”„๋กœ๋•์…˜ ์ค€๋น„๊ฐ€ ๋œ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์ƒ์„ฑ

    โœ… ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ: ๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ํ˜„์‹ค์ ์ธ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์ถ•

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์ธ๋ฑ์Šค ๋ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๊ตฌ์„ฑ

    โœ… ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ: ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ ์ „๋žต ์ˆ˜๋ฆฝ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 05: MCP ์„œ๋ฒ„ ๊ตฌํ˜„์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ฐ๊ฒฐํ•˜๋Š” FastMCP ์„œ๋ฒ„ ๊ตฌ์ถ•
  • MCP ํ”„๋กœํ† ์ฝœ์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๋„๊ตฌ ๊ตฌํ˜„
  • Embeddings๋ฅผ ์‚ฌ์šฉํ•œ ์˜๋ฏธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
  • ์—ฐ๊ฒฐ ํ’€๋ง ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ตฌ์„ฑ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    PostgreSQL & pgvector

  • PostgreSQL ๋ฌธ์„œ - PostgreSQL์˜ ์™„์ „ํ•œ ์ฐธ์กฐ ์ž๋ฃŒ
  • pgvector ํ™•์žฅ - PostgreSQL์„ ์œ„ํ•œ ๋ฒกํ„ฐ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰
  • PostgreSQL ์„ฑ๋Šฅ ํŠœ๋‹ - ์ตœ์ ํ™” ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜

  • Row Level Security - PostgreSQL RLS ๋ฌธ์„œ
  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•„ํ‚คํ…์ฒ˜ - Azure ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - PostgreSQL ๋ณด์•ˆ ๊ฐ€์ด๋“œ
  • ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋ณธ - ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดํ•ดํ•˜๊ธฐ
  • Embedding ๋ชจ๋ธ - OpenAI Embeddings ๋ฌธ์„œ
  • HNSW ์•Œ๊ณ ๋ฆฌ์ฆ˜ - ๊ณ„์ธต์  ํƒ์ƒ‰ ๊ฐ€๋Šฅํ•œ ์†Œํ˜• ์„ธ๊ณ„ ๊ทธ๋ž˜ํ”„
  • ---

    ์ด์ „: Lab 03: ํ™˜๊ฒฝ ์„ค์ •

    ๋‹ค์Œ: Lab 05: MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    PostgreSQL ์„ค์ •, ์†Œ๋งค ์Šคํ‚ค๋งˆ ์„ค๊ณ„, ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ๊ตฌ์ถ•ํ•˜๊ธฐ

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ๋ฐ ์Šคํ‚ค๋งˆ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์—์„œ๋Š” Zava Retail ์‹œ์Šคํ…œ์„ ์œ„ํ•œ PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„์— ๋Œ€ํ•ด ๊นŠ์ด ํƒ๊ตฌํ•ฉ๋‹ˆ๋‹ค. ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ, ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๋ชจ๋ธ๋ง, ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ Row Level Security(RLS)๋ฅผ ํฌํ•จํ•œ ํฌ๊ด„์ ์ธ ์†Œ๋งค ์Šคํ‚ค๋งˆ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” MCP ์„œ๋ฒ„์˜ ๊ธฐ๋ฐ˜์œผ๋กœ, ์—ฌ๋Ÿฌ ๋งค์žฅ์˜ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋ฉด์„œ ์—„๊ฒฉํ•œ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์šฐ๋ฆฌ๋Š” PostgreSQL๊ณผ pgvector ํ™•์žฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ณ ๊ฐ์ด ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์ œํ’ˆ์„ ๊ฒ€์ƒ‰ํ•  ์ˆ˜ ์žˆ๋Š” ์˜๋ฏธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์šฐ๋ฆฌ์˜ ์Šคํ‚ค๋งˆ๋Š” ํ˜„๋Œ€์ ์ธ ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ํŒจํ„ด์„ ๋”ฐ๋ฅด๋ฉฐ, Row Level Security๋ฅผ ํ†ตํ•ด ์‚ฌ์šฉ์ž๊ฐ€ ์Šน์ธ๋œ ๋งค์žฅ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ์ด ์ ‘๊ทผ ๋ฐฉ์‹์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ์„ ์ œ๊ณตํ•˜๋ฉด์„œ๋„ ์ตœ์ ์˜ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์„ค๊ณ„: ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์„ค๊ณ„
  • ๊ตฌํ˜„: ๋ฒกํ„ฐ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ PostgreSQL๊ณผ pgvector ์„ค์ •
  • ๊ตฌ์„ฑ: ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ Row Level Security ์„ค์ •
  • ์ƒ์„ฑ: ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ํ˜„์‹ค์ ์ธ ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  • ์ตœ์ ํ™”: ์†Œ๋งค ์›Œํฌ๋กœ๋“œ๋ฅผ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๊ตฌํ˜„: ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ ์ „๋žต
  • ๐Ÿ—ƒ๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•„ํ‚คํ…์ฒ˜

    PostgreSQL๊ณผ pgvector

    ์šฐ๋ฆฌ์˜ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” PostgreSQL์˜ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ๊ธฐ๋Šฅ๊ณผ AI ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ pgvector ํ™•์žฅ์„ ๊ฒฐํ•ฉํ•˜์—ฌ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค:

    
    -- Enable required extensions
    
    CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
    
    CREATE EXTENSION IF NOT EXISTS "pgcrypto";
    
    CREATE EXTENSION IF NOT EXISTS "vector";
    
    
    
    -- Verify vector extension installation
    
    SELECT * FROM pg_extension WHERE extname = 'vector';
    
    

    ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” Row Level Security๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ณต์œ  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๊ณต์œ  ์Šคํ‚ค๋งˆ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ๋ชจ๋ธ์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                 PostgreSQL                      โ”‚
    
    โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
    
    โ”‚  retail Schema (Shared)                        โ”‚
    
    โ”‚  โ”œโ”€โ”€ stores (Master tenant data)               โ”‚
    
    โ”‚  โ”œโ”€โ”€ customers (RLS by store_id)               โ”‚
    
    โ”‚  โ”œโ”€โ”€ products (RLS by store_id)                โ”‚
    
    โ”‚  โ”œโ”€โ”€ sales_transactions (RLS by store_id)      โ”‚
    
    โ”‚  โ”œโ”€โ”€ sales_transaction_items (RLS via join)    โ”‚
    
    โ”‚  โ””โ”€โ”€ product_embeddings (RLS by store_id)      โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ๐Ÿ“Š ํ•ต์‹ฌ ์Šคํ‚ค๋งˆ ์„ค๊ณ„

    Stores ํ…Œ์ด๋ธ” (ํ…Œ๋„ŒํŠธ ๋งˆ์Šคํ„ฐ)

    
    -- Stores table: Master tenant registry
    
    CREATE TABLE retail.stores (
    
        store_id VARCHAR(50) PRIMARY KEY,
    
        store_name VARCHAR(100) NOT NULL,
    
        store_location VARCHAR(100),
    
        store_type VARCHAR(50),
    
        region VARCHAR(50),
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        is_active BOOLEAN DEFAULT TRUE
    
    );
    
    
    
    -- Sample stores data
    
    INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region) VALUES
    
    ('seattle', 'Zava Retail Seattle', 'Seattle, WA', 'flagship', 'west'),
    
    ('redmond', 'Zava Retail Redmond', 'Redmond, WA', 'standard', 'west'),
    
    ('bellevue', 'Zava Retail Bellevue', 'Bellevue, WA', 'standard', 'west'),
    
    ('online', 'Zava Retail Online', 'Digital', 'ecommerce', 'global');
    
    
    
    -- Create index for performance
    
    CREATE INDEX idx_stores_region ON retail.stores(region);
    
    CREATE INDEX idx_stores_active ON retail.stores(is_active) WHERE is_active = TRUE;
    
    

    Customers ํ…Œ์ด๋ธ”

    
    -- Customers table with RLS
    
    CREATE TABLE retail.customers (
    
        customer_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        first_name VARCHAR(50) NOT NULL,
    
        last_name VARCHAR(50) NOT NULL,
    
        email VARCHAR(100) UNIQUE NOT NULL,
    
        phone VARCHAR(20),
    
        date_of_birth DATE,
    
        gender VARCHAR(20),
    
        customer_since DATE DEFAULT CURRENT_DATE,
    
        loyalty_tier VARCHAR(20) DEFAULT 'bronze',
    
        total_lifetime_value DECIMAL(10,2) DEFAULT 0.00,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Enable RLS
    
    ALTER TABLE retail.customers ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy: Users can only see customers from their store
    
    CREATE POLICY customers_store_isolation ON retail.customers
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Indexes for performance
    
    CREATE INDEX idx_customers_store_id ON retail.customers(store_id);
    
    CREATE INDEX idx_customers_email ON retail.customers(email);
    
    CREATE INDEX idx_customers_loyalty_tier ON retail.customers(loyalty_tier);
    
    CREATE INDEX idx_customers_created_at ON retail.customers(created_at);
    
    

    Products ํ…Œ์ด๋ธ” ๋ฐ ์นดํ…Œ๊ณ ๋ฆฌ

    
    -- Product categories
    
    CREATE TABLE retail.product_categories (
    
        category_id SERIAL PRIMARY KEY,
    
        category_name VARCHAR(100) NOT NULL UNIQUE,
    
        parent_category_id INTEGER REFERENCES retail.product_categories(category_id),
    
        description TEXT,
    
        is_active BOOLEAN DEFAULT TRUE
    
    );
    
    
    
    -- Insert sample categories
    
    INSERT INTO retail.product_categories (category_name, description) VALUES
    
    ('Electronics', 'Electronic devices and accessories'),
    
    ('Clothing', 'Apparel and fashion items'),
    
    ('Home & Garden', 'Home improvement and garden supplies'),
    
    ('Sports & Outdoors', 'Sports equipment and outdoor gear'),
    
    ('Books & Media', 'Books, movies, and digital media'),
    
    ('Health & Beauty', 'Health and beauty products'),
    
    ('Automotive', 'Car parts and automotive accessories');
    
    
    
    -- Products table with rich metadata
    
    CREATE TABLE retail.products (
    
        product_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        sku VARCHAR(50) NOT NULL,
    
        product_name VARCHAR(200) NOT NULL,
    
        product_description TEXT,
    
        category_id INTEGER REFERENCES retail.product_categories(category_id),
    
        brand VARCHAR(100),
    
        model VARCHAR(100),
    
        color VARCHAR(50),
    
        size VARCHAR(50),
    
        weight_kg DECIMAL(8,3),
    
        dimensions_cm VARCHAR(50), -- e.g., "30x20x15"
    
        price DECIMAL(10,2) NOT NULL,
    
        cost DECIMAL(10,2),
    
        current_stock INTEGER DEFAULT 0,
    
        minimum_stock INTEGER DEFAULT 0,
    
        maximum_stock INTEGER DEFAULT 1000,
    
        reorder_point INTEGER DEFAULT 10,
    
        supplier_name VARCHAR(100),
    
        supplier_sku VARCHAR(50),
    
        is_active BOOLEAN DEFAULT TRUE,
    
        is_featured BOOLEAN DEFAULT FALSE,
    
        rating_average DECIMAL(3,2) DEFAULT 0.00,
    
        rating_count INTEGER DEFAULT 0,
    
        tags TEXT[], -- Array of tags for flexible categorization
    
        metadata JSONB, -- Flexible metadata storage
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure SKU uniqueness within store
    
        CONSTRAINT unique_sku_per_store UNIQUE (store_id, sku)
    
    );
    
    
    
    -- Enable RLS for products
    
    ALTER TABLE retail.products ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for products
    
    CREATE POLICY products_store_isolation ON retail.products
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Comprehensive indexes
    
    CREATE INDEX idx_products_store_id ON retail.products(store_id);
    
    CREATE INDEX idx_products_sku ON retail.products(sku);
    
    CREATE INDEX idx_products_category ON retail.products(category_id);
    
    CREATE INDEX idx_products_brand ON retail.products(brand);
    
    CREATE INDEX idx_products_price ON retail.products(price);
    
    CREATE INDEX idx_products_stock ON retail.products(current_stock);
    
    CREATE INDEX idx_products_active ON retail.products(is_active) WHERE is_active = TRUE;
    
    CREATE INDEX idx_products_featured ON retail.products(is_featured) WHERE is_featured = TRUE;
    
    CREATE INDEX idx_products_tags ON retail.products USING GIN(tags);
    
    CREATE INDEX idx_products_metadata ON retail.products USING GIN(metadata);
    
    CREATE INDEX idx_products_text_search ON retail.products USING GIN(
    
        to_tsvector('english', product_name || ' ' || COALESCE(product_description, '') || ' ' || COALESCE(brand, ''))
    
    );
    
    

    ํŒ๋งค ๊ฑฐ๋ž˜

    
    -- Sales transactions table
    
    CREATE TABLE retail.sales_transactions (
    
        transaction_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        customer_id UUID REFERENCES retail.customers(customer_id),
    
        transaction_date TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        transaction_type VARCHAR(20) DEFAULT 'sale', -- 'sale', 'return', 'exchange'
    
        payment_method VARCHAR(50), -- 'cash', 'credit_card', 'debit_card', 'digital_wallet'
    
        subtotal DECIMAL(10,2) NOT NULL,
    
        tax_amount DECIMAL(10,2) DEFAULT 0.00,
    
        discount_amount DECIMAL(10,2) DEFAULT 0.00,
    
        total_amount DECIMAL(10,2) NOT NULL,
    
        cashier_id VARCHAR(50),
    
        register_id VARCHAR(50),
    
        receipt_number VARCHAR(50),
    
        notes TEXT,
    
        metadata JSONB,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Sales transaction items (line items)
    
    CREATE TABLE retail.sales_transaction_items (
    
        item_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        transaction_id UUID NOT NULL REFERENCES retail.sales_transactions(transaction_id) ON DELETE CASCADE,
    
        product_id UUID NOT NULL REFERENCES retail.products(product_id),
    
        quantity INTEGER NOT NULL DEFAULT 1,
    
        unit_price DECIMAL(10,2) NOT NULL,
    
        total_price DECIMAL(10,2) NOT NULL,
    
        discount_amount DECIMAL(10,2) DEFAULT 0.00,
    
        tax_amount DECIMAL(10,2) DEFAULT 0.00,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure positive quantities and prices
    
        CONSTRAINT positive_quantity CHECK (quantity > 0),
    
        CONSTRAINT positive_unit_price CHECK (unit_price >= 0),
    
        CONSTRAINT positive_total_price CHECK (total_price >= 0)
    
    );
    
    
    
    -- Enable RLS for transactions
    
    ALTER TABLE retail.sales_transactions ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for sales transactions
    
    CREATE POLICY sales_transactions_store_isolation ON retail.sales_transactions
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- RLS for transaction items (via join with transactions)
    
    ALTER TABLE retail.sales_transaction_items ENABLE ROW LEVEL SECURITY;
    
    
    
    CREATE POLICY sales_transaction_items_store_isolation ON retail.sales_transaction_items
    
        FOR ALL
    
        TO mcp_user
    
        USING (
    
            transaction_id IN (
    
                SELECT transaction_id 
    
                FROM retail.sales_transactions 
    
                WHERE store_id = current_setting('app.current_store_id', true)
    
            )
    
        );
    
    
    
    -- Performance indexes
    
    CREATE INDEX idx_sales_transactions_store_id ON retail.sales_transactions(store_id);
    
    CREATE INDEX idx_sales_transactions_customer_id ON retail.sales_transactions(customer_id);
    
    CREATE INDEX idx_sales_transactions_date ON retail.sales_transactions(transaction_date);
    
    CREATE INDEX idx_sales_transactions_type ON retail.sales_transactions(transaction_type);
    
    CREATE INDEX idx_sales_transactions_payment ON retail.sales_transactions(payment_method);
    
    
    
    CREATE INDEX idx_sales_transaction_items_transaction_id ON retail.sales_transaction_items(transaction_id);
    
    CREATE INDEX idx_sales_transaction_items_product_id ON retail.sales_transaction_items(product_id);
    
    

    ๐Ÿ” ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ตฌํ˜„

    Product Embeddings ํ…Œ์ด๋ธ”

    
    -- Product embeddings for semantic search
    
    CREATE TABLE retail.product_embeddings (
    
        embedding_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        product_id UUID NOT NULL REFERENCES retail.products(product_id) ON DELETE CASCADE,
    
        store_id VARCHAR(50) NOT NULL REFERENCES retail.stores(store_id),
    
        embedding_text TEXT NOT NULL, -- The text that was embedded
    
        embedding vector(1536), -- OpenAI text-embedding-3-small dimension
    
        embedding_model VARCHAR(100) NOT NULL DEFAULT 'text-embedding-3-small',
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    
        
    
        -- Ensure one embedding per product per model
    
        CONSTRAINT unique_product_embedding UNIQUE (product_id, embedding_model)
    
    );
    
    
    
    -- Enable RLS for embeddings
    
    ALTER TABLE retail.product_embeddings ENABLE ROW LEVEL SECURITY;
    
    
    
    -- RLS Policy for embeddings
    
    CREATE POLICY product_embeddings_store_isolation ON retail.product_embeddings
    
        FOR ALL
    
        TO mcp_user
    
        USING (store_id = current_setting('app.current_store_id', true));
    
    
    
    -- Vector similarity index (HNSW for fast approximate search)
    
    CREATE INDEX idx_product_embeddings_vector ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops);
    
    
    
    -- Additional indexes
    
    CREATE INDEX idx_product_embeddings_product_id ON retail.product_embeddings(product_id);
    
    CREATE INDEX idx_product_embeddings_store_id ON retail.product_embeddings(store_id);
    
    CREATE INDEX idx_product_embeddings_model ON retail.product_embeddings(embedding_model);
    
    

    ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ํ•จ์ˆ˜

    
    -- Function to search products by similarity
    
    CREATE OR REPLACE FUNCTION retail.search_products_by_similarity(
    
        search_embedding vector(1536),
    
        similarity_threshold float DEFAULT 0.7,
    
        max_results integer DEFAULT 20
    
    )
    
    RETURNS TABLE (
    
        product_id UUID,
    
        product_name VARCHAR(200),
    
        product_description TEXT,
    
        brand VARCHAR(100),
    
        price DECIMAL(10,2),
    
        similarity_score float
    
    ) 
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            p.product_id,
    
            p.product_name,
    
            p.product_description,
    
            p.brand,
    
            p.price,
    
            1 - (pe.embedding <=> search_embedding) as similarity_score
    
        FROM retail.product_embeddings pe
    
        JOIN retail.products p ON pe.product_id = p.product_id
    
        WHERE 
    
            pe.store_id = current_setting('app.current_store_id', true)
    
            AND p.is_active = TRUE
    
            AND 1 - (pe.embedding <=> search_embedding) >= similarity_threshold
    
        ORDER BY pe.embedding <=> search_embedding
    
        LIMIT max_results;
    
    END;
    
    $$;
    
    
    
    -- Grant execute permission
    
    GRANT EXECUTE ON FUNCTION retail.search_products_by_similarity TO mcp_user;
    
    

    ๐Ÿ” Row Level Security ์„ค์ •

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ญํ•  ๋ฐ ๊ถŒํ•œ

    
    -- Create MCP application role
    
    CREATE ROLE mcp_user LOGIN;
    
    
    
    -- Grant schema usage
    
    GRANT USAGE ON SCHEMA retail TO mcp_user;
    
    
    
    -- Grant table permissions
    
    GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA retail TO mcp_user;
    
    GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA retail TO mcp_user;
    
    
    
    -- Grant permissions on future tables
    
    ALTER DEFAULT PRIVILEGES IN SCHEMA retail GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO mcp_user;
    
    ALTER DEFAULT PRIVILEGES IN SCHEMA retail GRANT USAGE, SELECT ON SEQUENCES TO mcp_user;
    
    
    
    -- Function to set store context
    
    CREATE OR REPLACE FUNCTION retail.set_store_context(store_id_param VARCHAR(50))
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    SECURITY DEFINER
    
    AS $$
    
    BEGIN
    
        -- Verify store exists and user has access
    
        IF NOT EXISTS (SELECT 1 FROM retail.stores WHERE store_id = store_id_param AND is_active = TRUE) THEN
    
            RAISE EXCEPTION 'Invalid or inactive store: %', store_id_param;
    
        END IF;
    
        
    
        -- Set the store context
    
        PERFORM set_config('app.current_store_id', store_id_param, false);
    
        
    
        -- Log the context change
    
        INSERT INTO retail.audit_log (
    
            table_name,
    
            action,
    
            user_name,
    
            store_id,
    
            metadata
    
        ) VALUES (
    
            'security_context',
    
            'store_context_set',
    
            current_user,
    
            store_id_param,
    
            jsonb_build_object('timestamp', current_timestamp)
    
        );
    
    END;
    
    $$;
    
    
    
    -- Grant execute permission
    
    GRANT EXECUTE ON FUNCTION retail.set_store_context TO mcp_user;
    
    

    ๊ฐ์‚ฌ ๋กœ๊ทธ

    
    -- Audit log table for security and compliance
    
    CREATE TABLE retail.audit_log (
    
        log_id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    
        table_name VARCHAR(100) NOT NULL,
    
        action VARCHAR(50) NOT NULL, -- INSERT, UPDATE, DELETE, SELECT
    
        user_name VARCHAR(100) NOT NULL DEFAULT current_user,
    
        store_id VARCHAR(50),
    
        record_id UUID,
    
        old_values JSONB,
    
        new_values JSONB,
    
        metadata JSONB,
    
        created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
    
    );
    
    
    
    -- Index for audit queries
    
    CREATE INDEX idx_audit_log_table_name ON retail.audit_log(table_name);
    
    CREATE INDEX idx_audit_log_action ON retail.audit_log(action);
    
    CREATE INDEX idx_audit_log_user_name ON retail.audit_log(user_name);
    
    CREATE INDEX idx_audit_log_store_id ON retail.audit_log(store_id);
    
    CREATE INDEX idx_audit_log_created_at ON retail.audit_log(created_at);
    
    
    
    -- Audit trigger function
    
    CREATE OR REPLACE FUNCTION retail.audit_trigger()
    
    RETURNS trigger AS $$
    
    BEGIN
    
        IF TG_OP = 'DELETE' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                old_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(OLD.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(OLD.customer_id, OLD.product_id, OLD.transaction_id),
    
                row_to_json(OLD)
    
            );
    
            RETURN OLD;
    
        ELSIF TG_OP = 'UPDATE' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                old_values,
    
                new_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(NEW.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(NEW.customer_id, NEW.product_id, NEW.transaction_id),
    
                row_to_json(OLD),
    
                row_to_json(NEW)
    
            );
    
            RETURN NEW;
    
        ELSIF TG_OP = 'INSERT' THEN
    
            INSERT INTO retail.audit_log (
    
                table_name,
    
                action,
    
                store_id,
    
                record_id,
    
                new_values
    
            ) VALUES (
    
                TG_TABLE_NAME,
    
                TG_OP,
    
                COALESCE(NEW.store_id, current_setting('app.current_store_id', true)),
    
                COALESCE(NEW.customer_id, NEW.product_id, NEW.transaction_id),
    
                row_to_json(NEW)
    
            );
    
            RETURN NEW;
    
        END IF;
    
        RETURN NULL;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    
    
    -- Create audit triggers
    
    CREATE TRIGGER customers_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.customers
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    
    
    CREATE TRIGGER products_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.products
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    
    
    CREATE TRIGGER sales_transactions_audit_trigger
    
        AFTER INSERT OR UPDATE OR DELETE ON retail.sales_transactions
    
        FOR EACH ROW EXECUTE FUNCTION retail.audit_trigger();
    
    

    ๐Ÿ“Š ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ

    ํ˜„์‹ค์ ์ธ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์Šคํฌ๋ฆฝํŠธ

    
    # scripts/generate_sample_data.py
    
    """
    
    Generate realistic sample data for the Zava Retail database.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import random
    
    import json
    
    from datetime import datetime, timedelta
    
    from faker import Faker
    
    from typing import List, Dict, Any
    
    import numpy as np
    
    
    
    fake = Faker()
    
    
    
    class SampleDataGenerator:
    
        """Generate realistic retail sample data."""
    
        
    
        def __init__(self, connection_string: str):
    
            self.connection_string = connection_string
    
            self.stores = ['seattle', 'redmond', 'bellevue', 'online']
    
            
    
            # Product categories with realistic items
    
            self.product_data = {
    
                'Electronics': {
    
                    'brands': ['Apple', 'Samsung', 'Sony', 'LG', 'HP', 'Dell'],
    
                    'items': [
    
                        'Smartphone', 'Laptop', 'Tablet', 'Headphones', 'Smart TV',
    
                        'Gaming Console', 'Smartwatch', 'Bluetooth Speaker'
    
                    ]
    
                },
    
                'Clothing': {
    
                    'brands': ['Nike', 'Adidas', 'Zara', 'H&M', 'Levi\'s', 'Gap'],
    
                    'items': [
    
                        'T-Shirt', 'Jeans', 'Dress', 'Jacket', 'Sneakers',
    
                        'Sweater', 'Shorts', 'Blouse'
    
                    ]
    
                },
    
                'Home & Garden': {
    
                    'brands': ['IKEA', 'Home Depot', 'Wayfair', 'Target', 'Walmart'],
    
                    'items': [
    
                        'Sofa', 'Dining Table', 'Lamp', 'Garden Tool', 'Plant Pot',
    
                        'Curtains', 'Rug', 'Kitchen Appliance'
    
                    ]
    
                }
    
            }
    
        
    
        async def generate_all_data(self):
    
            """Generate complete sample dataset."""
    
            
    
            conn = await asyncpg.connect(self.connection_string)
    
            
    
            try:
    
                print("๐Ÿช Generating stores data...")
    
                await self._ensure_stores_exist(conn)
    
                
    
                print("๐Ÿ‘ฅ Generating customers...")
    
                customers = await self._generate_customers(conn, 2000)
    
                
    
                print("๐Ÿ“ฆ Generating products...")
    
                products = await self._generate_products(conn, 500)
    
                
    
                print("๐Ÿ›’ Generating sales transactions...")
    
                await self._generate_sales_transactions(conn, customers, products, 5000)
    
                
    
                print("โœ… Sample data generation complete!")
    
                
    
            finally:
    
                await conn.close()
    
        
    
        async def _ensure_stores_exist(self, conn):
    
            """Ensure all stores exist in the database."""
    
            
    
            stores_data = [
    
                ('seattle', 'Zava Retail Seattle', 'Seattle, WA', 'flagship', 'west'),
    
                ('redmond', 'Zava Retail Redmond', 'Redmond, WA', 'standard', 'west'),
    
                ('bellevue', 'Zava Retail Bellevue', 'Bellevue, WA', 'standard', 'west'),
    
                ('online', 'Zava Retail Online', 'Digital', 'ecommerce', 'global')
    
            ]
    
            
    
            for store_data in stores_data:
    
                await conn.execute("""
    
                    INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region)
    
                    VALUES ($1, $2, $3, $4, $5)
    
                    ON CONFLICT (store_id) DO NOTHING
    
                """, *store_data)
    
        
    
        async def _generate_customers(self, conn, count: int) -> List[Dict]:
    
            """Generate realistic customer data."""
    
            
    
            customers = []
    
            
    
            for _ in range(count):
    
                store_id = random.choice(self.stores)
    
                customer_data = {
    
                    'store_id': store_id,
    
                    'first_name': fake.first_name(),
    
                    'last_name': fake.last_name(),
    
                    'email': fake.unique.email(),
    
                    'phone': fake.phone_number()[:20],
    
                    'date_of_birth': fake.date_of_birth(minimum_age=18, maximum_age=80),
    
                    'gender': random.choice(['Male', 'Female', 'Other', 'Prefer not to say']),
    
                    'customer_since': fake.date_between(start_date='-5y', end_date='today'),
    
                    'loyalty_tier': random.choices(
    
                        ['bronze', 'silver', 'gold', 'platinum'],
    
                        weights=[50, 30, 15, 5]
    
                    )[0]
    
                }
    
                
    
                customer_id = await conn.fetchval("""
    
                    INSERT INTO retail.customers (
    
                        store_id, first_name, last_name, email, phone,
    
                        date_of_birth, gender, customer_since, loyalty_tier
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
    
                    RETURNING customer_id
    
                """, *customer_data.values())
    
                
    
                customer_data['customer_id'] = customer_id
    
                customers.append(customer_data)
    
            
    
            return customers
    
        
    
        async def _generate_products(self, conn, count: int) -> List[Dict]:
    
            """Generate realistic product data."""
    
            
    
            # Get category IDs
    
            categories = await conn.fetch("SELECT category_id, category_name FROM retail.product_categories")
    
            category_map = {cat['category_name']: cat['category_id'] for cat in categories}
    
            
    
            products = []
    
            
    
            for _ in range(count):
    
                store_id = random.choice(self.stores)
    
                category_name = random.choice(list(self.product_data.keys()))
    
                category_id = category_map.get(category_name)
    
                
    
                if not category_id:
    
                    continue
    
                
    
                brand = random.choice(self.product_data[category_name]['brands'])
    
                item_type = random.choice(self.product_data[category_name]['items'])
    
                
    
                # Generate realistic pricing
    
                base_price = random.uniform(10, 1000)
    
                cost = base_price * random.uniform(0.4, 0.7)  # 40-70% cost margin
    
                
    
                product_data = {
    
                    'store_id': store_id,
    
                    'sku': f"{brand[:3].upper()}-{fake.unique.random_number(digits=6)}",
    
                    'product_name': f"{brand} {item_type}",
    
                    'product_description': fake.text(max_nb_chars=500),
    
                    'category_id': category_id,
    
                    'brand': brand,
    
                    'model': f"Model {fake.random_number(digits=4)}",
    
                    'color': fake.color_name(),
    
                    'size': random.choice(['XS', 'S', 'M', 'L', 'XL', 'XXL', 'One Size']),
    
                    'weight_kg': round(random.uniform(0.1, 10.0), 2),
    
                    'price': round(base_price, 2),
    
                    'cost': round(cost, 2),
    
                    'current_stock': random.randint(0, 100),
    
                    'minimum_stock': random.randint(5, 20),
    
                    'reorder_point': random.randint(10, 30),
    
                    'supplier_name': fake.company(),
    
                    'is_featured': random.choice([True, False]),
    
                    'rating_average': round(random.uniform(3.0, 5.0), 2),
    
                    'rating_count': random.randint(0, 500),
    
                    'tags': random.sample([
    
                        'popular', 'new', 'sale', 'limited', 'bestseller', 
    
                        'eco-friendly', 'premium', 'budget'
    
                    ], k=random.randint(1, 3))
    
                }
    
                
    
                product_id = await conn.fetchval("""
    
                    INSERT INTO retail.products (
    
                        store_id, sku, product_name, product_description, category_id,
    
                        brand, model, color, size, weight_kg, price, cost,
    
                        current_stock, minimum_stock, reorder_point, supplier_name,
    
                        is_featured, rating_average, rating_count, tags
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20)
    
                    RETURNING product_id
    
                """, *product_data.values())
    
                
    
                product_data['product_id'] = product_id
    
                products.append(product_data)
    
            
    
            return products
    
        
    
        async def _generate_sales_transactions(self, conn, customers: List[Dict], products: List[Dict], count: int):
    
            """Generate realistic sales transaction data."""
    
            
    
            for _ in range(count):
    
                # Select customer and matching store products
    
                customer = random.choice(customers)
    
                store_products = [p for p in products if p['store_id'] == customer['store_id']]
    
                
    
                if not store_products:
    
                    continue
    
                
    
                # Generate transaction basics
    
                transaction_date = fake.date_time_between(start_date='-1y', end_date='now')
    
                transaction_type = random.choices(
    
                    ['sale', 'return', 'exchange'],
    
                    weights=[90, 7, 3]
    
                )[0]
    
                
    
                payment_method = random.choices(
    
                    ['credit_card', 'debit_card', 'cash', 'digital_wallet'],
    
                    weights=[45, 25, 20, 10]
    
                )[0]
    
                
    
                # Generate transaction items (1-5 items per transaction)
    
                num_items = random.choices([1, 2, 3, 4, 5], weights=[40, 30, 20, 7, 3])[0]
    
                selected_products = random.sample(store_products, min(num_items, len(store_products)))
    
                
    
                subtotal = 0
    
                transaction_items = []
    
                
    
                for product in selected_products:
    
                    quantity = random.randint(1, 3)
    
                    unit_price = product['price']
    
                    
    
                    # Apply random discounts occasionally
    
                    discount_amount = 0
    
                    if random.random() < 0.2:  # 20% chance of discount
    
                        discount_amount = unit_price * quantity * random.uniform(0.05, 0.25)
    
                    
    
                    total_price = (unit_price * quantity) - discount_amount
    
                    subtotal += total_price
    
                    
    
                    transaction_items.append({
    
                        'product_id': product['product_id'],
    
                        'quantity': quantity,
    
                        'unit_price': unit_price,
    
                        'total_price': total_price,
    
                        'discount_amount': discount_amount
    
                    })
    
                
    
                # Calculate totals
    
                discount_amount = sum(item['discount_amount'] for item in transaction_items)
    
                tax_amount = subtotal * 0.08  # 8% tax rate
    
                total_amount = subtotal + tax_amount
    
                
    
                # Insert transaction
    
                transaction_id = await conn.fetchval("""
    
                    INSERT INTO retail.sales_transactions (
    
                        store_id, customer_id, transaction_date, transaction_type,
    
                        payment_method, subtotal, tax_amount, discount_amount, total_amount,
    
                        cashier_id, register_id, receipt_number
    
                    ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
    
                    RETURNING transaction_id
    
                """, 
    
                    customer['store_id'], customer['customer_id'], transaction_date,
    
                    transaction_type, payment_method, subtotal, tax_amount,
    
                    discount_amount, total_amount, f"CASHIER{random.randint(1, 10)}",
    
                    f"REG{random.randint(1, 5)}", f"RCP{fake.random_number(digits=8)}"
    
                )
    
                
    
                # Insert transaction items
    
                for item in transaction_items:
    
                    await conn.execute("""
    
                        INSERT INTO retail.sales_transaction_items (
    
                            transaction_id, product_id, quantity, unit_price,
    
                            total_price, discount_amount
    
                        ) VALUES ($1, $2, $3, $4, $5, $6)
    
                    """, 
    
                        transaction_id, item['product_id'], item['quantity'],
    
                        item['unit_price'], item['total_price'], item['discount_amount']
    
                    )
    
    
    
    # Usage example
    
    if __name__ == "__main__":
    
        import os
    
        from config import Config
    
        
    
        config = Config()
    
        generator = SampleDataGenerator(config.database.connection_string)
    
        
    
        asyncio.run(generator.generate_all_data())
    
    

    ๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™”

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์„ฑ

    
    -- Performance-oriented PostgreSQL settings
    
    -- Add to postgresql.conf
    
    
    
    # Memory settings
    
    shared_buffers = '256MB'                # 25% of RAM for dedicated DB server
    
    effective_cache_size = '1GB'           # Estimate of OS cache size
    
    work_mem = '4MB'                       # Memory for sorts and hash joins
    
    maintenance_work_mem = '64MB'          # Memory for VACUUM, CREATE INDEX
    
    
    
    # Connection settings
    
    max_connections = 100                  # Adjust based on application needs
    
    
    
    # Write-ahead logging
    
    wal_buffers = '16MB'
    
    checkpoint_segments = 32               # PostgreSQL < 9.5
    
    max_wal_size = '1GB'                   # PostgreSQL >= 9.5
    
    
    
    # Query planner
    
    random_page_cost = 1.1                 # SSD-optimized
    
    effective_io_concurrency = 200         # SSD concurrent I/O capability
    
    
    
    # Logging for performance monitoring
    
    log_min_duration_statement = 1000      # Log queries > 1 second
    
    log_checkpoints = on
    
    log_connections = on
    
    log_disconnections = on
    
    log_line_prefix = '%t [%p-%l] %q%u@%d '
    
    

    ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๋ทฐ

    
    -- Create monitoring views for query performance
    
    CREATE VIEW retail.slow_queries AS
    
    SELECT 
    
        query,
    
        calls,
    
        total_exec_time,
    
        mean_exec_time,
    
        max_exec_time,
    
        stddev_exec_time,
    
        rows,
    
        100.0 * shared_blks_hit / nullif(shared_blks_hit + shared_blks_read, 0) AS hit_percent
    
    FROM pg_stat_statements
    
    WHERE mean_exec_time > 100  -- Queries taking more than 100ms on average
    
    ORDER BY mean_exec_time DESC;
    
    
    
    -- Table sizes and index usage
    
    CREATE VIEW retail.table_stats AS
    
    SELECT
    
        schemaname,
    
        tablename,
    
        pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
    
        pg_stat_get_tuples_inserted(c.oid) as inserts,
    
        pg_stat_get_tuples_updated(c.oid) as updates,
    
        pg_stat_get_tuples_deleted(c.oid) as deletes,
    
        pg_stat_get_live_tuples(c.oid) as live_tuples,
    
        pg_stat_get_dead_tuples(c.oid) as dead_tuples
    
    FROM pg_tables pt
    
    JOIN pg_class c ON c.relname = pt.tablename
    
    WHERE schemaname = 'retail'
    
    ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
    
    
    
    -- Index usage statistics
    
    CREATE VIEW retail.index_usage AS
    
    SELECT
    
        schemaname,
    
        tablename,
    
        indexname,
    
        idx_tup_read,
    
        idx_tup_fetch,
    
        pg_size_pretty(pg_relation_size(indexrelname)) as size
    
    FROM pg_stat_user_indexes
    
    WHERE schemaname = 'retail'
    
    ORDER BY idx_tup_read DESC;
    
    

    ์ž๋™ ์œ ์ง€ ๊ด€๋ฆฌ

    
    -- Create function for automated maintenance
    
    CREATE OR REPLACE FUNCTION retail.perform_maintenance()
    
    RETURNS void
    
    LANGUAGE plpgsql
    
    AS $$
    
    BEGIN
    
        -- Update table statistics
    
        ANALYZE retail.customers;
    
        ANALYZE retail.products;
    
        ANALYZE retail.sales_transactions;
    
        ANALYZE retail.sales_transaction_items;
    
        ANALYZE retail.product_embeddings;
    
        
    
        -- Vacuum tables with high update/delete activity
    
        VACUUM (ANALYZE, VERBOSE) retail.customers;
    
        VACUUM (ANALYZE, VERBOSE) retail.products;
    
        
    
        -- Reindex if needed (check for index bloat)
    
        REINDEX INDEX CONCURRENTLY idx_products_text_search;
    
        REINDEX INDEX CONCURRENTLY idx_product_embeddings_vector;
    
        
    
        -- Log maintenance completion
    
        INSERT INTO retail.audit_log (
    
            table_name,
    
            action,
    
            metadata
    
        ) VALUES (
    
            'maintenance',
    
            'automated_maintenance_completed',
    
            jsonb_build_object(
    
                'timestamp', current_timestamp,
    
                'database_size', pg_database_size(current_database())
    
            )
    
        );
    
    END;
    
    $$;
    
    
    
    -- Schedule maintenance (would typically be done via cron or scheduled job)
    
    -- Example cron entry: 0 2 * * 0 psql -d retail_db -c "SELECT retail.perform_maintenance();"
    
    

    ๐Ÿ’พ ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ

    ๋ฐฑ์—… ์ „๋žต

    
    #!/bin/bash
    
    # scripts/backup_database.sh
    
    
    
    # Comprehensive backup script for production environments
    
    
    
    set -e
    
    
    
    # Configuration
    
    DB_HOST="${POSTGRES_HOST:-localhost}"
    
    DB_PORT="${POSTGRES_PORT:-5432}"
    
    DB_NAME="${POSTGRES_DB:-retail_db}"
    
    DB_USER="${POSTGRES_USER:-postgres}"
    
    BACKUP_DIR="/backups/postgresql"
    
    RETENTION_DAYS=30
    
    
    
    # Create backup directory
    
    mkdir -p "$BACKUP_DIR"
    
    
    
    # Generate backup filename with timestamp
    
    TIMESTAMP=$(date +%Y%m%d_%H%M%S)
    
    BACKUP_FILE="$BACKUP_DIR/retail_backup_$TIMESTAMP.sql"
    
    COMPRESSED_BACKUP="$BACKUP_FILE.gz"
    
    
    
    echo "Starting database backup: $TIMESTAMP"
    
    
    
    # Create comprehensive backup
    
    pg_dump \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --dbname="$DB_NAME" \
    
        --verbose \
    
        --clean \
    
        --create \
    
        --if-exists \
    
        --format=custom \
    
        --file="$BACKUP_FILE"
    
    
    
    # Compress backup
    
    gzip "$BACKUP_FILE"
    
    
    
    # Verify backup integrity
    
    echo "Verifying backup integrity..."
    
    pg_restore --list "$COMPRESSED_BACKUP" > /dev/null
    
    
    
    # Clean up old backups
    
    find "$BACKUP_DIR" -name "retail_backup_*.sql.gz" -mtime +$RETENTION_DAYS -delete
    
    
    
    # Calculate backup size
    
    BACKUP_SIZE=$(du -h "$COMPRESSED_BACKUP" | cut -f1)
    
    
    
    echo "Backup completed successfully:"
    
    echo "  File: $COMPRESSED_BACKUP"
    
    echo "  Size: $BACKUP_SIZE"
    
    echo "  Timestamp: $TIMESTAMP"
    
    
    
    # Optional: Upload to cloud storage
    
    if [ -n "$AZURE_STORAGE_ACCOUNT" ] && [ -n "$AZURE_STORAGE_KEY" ]; then
    
        echo "Uploading backup to Azure Storage..."
    
        az storage blob upload \
    
            --account-name "$AZURE_STORAGE_ACCOUNT" \
    
            --account-key "$AZURE_STORAGE_KEY" \
    
            --container-name "database-backups" \
    
            --name "retail_backup_$TIMESTAMP.sql.gz" \
    
            --file "$COMPRESSED_BACKUP"
    
    fi
    
    

    ๋ณต๊ตฌ ์ ˆ์ฐจ

    
    #!/bin/bash
    
    # scripts/restore_database.sh
    
    
    
    # Database restoration script
    
    
    
    set -e
    
    
    
    if [ $# -lt 1 ]; then
    
        echo "Usage: $0 <backup_file> [target_database]"
    
        echo "Example: $0 /backups/retail_backup_20241001_120000.sql.gz retail_db_restored"
    
        exit 1
    
    fi
    
    
    
    BACKUP_FILE="$1"
    
    TARGET_DB="${2:-retail_db_restored}"
    
    
    
    # Configuration
    
    DB_HOST="${POSTGRES_HOST:-localhost}"
    
    DB_PORT="${POSTGRES_PORT:-5432}"
    
    DB_USER="${POSTGRES_USER:-postgres}"
    
    
    
    echo "Starting database restoration..."
    
    echo "  Source: $BACKUP_FILE"
    
    echo "  Target: $TARGET_DB"
    
    
    
    # Verify backup file exists
    
    if [ ! -f "$BACKUP_FILE" ]; then
    
        echo "Error: Backup file not found: $BACKUP_FILE"
    
        exit 1
    
    fi
    
    
    
    # Create target database
    
    createdb \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --owner="$DB_USER" \
    
        "$TARGET_DB"
    
    
    
    # Restore from backup
    
    if [[ "$BACKUP_FILE" == *.gz ]]; then
    
        # Compressed backup
    
        gunzip -c "$BACKUP_FILE" | pg_restore \
    
            --host="$DB_HOST" \
    
            --port="$DB_PORT" \
    
            --username="$DB_USER" \
    
            --dbname="$TARGET_DB" \
    
            --verbose \
    
            --clean \
    
            --if-exists
    
    else
    
        # Uncompressed backup
    
        pg_restore \
    
            --host="$DB_HOST" \
    
            --port="$DB_PORT" \
    
            --username="$DB_USER" \
    
            --dbname="$TARGET_DB" \
    
            --verbose \
    
            --clean \
    
            --if-exists \
    
            "$BACKUP_FILE"
    
    fi
    
    
    
    echo "Database restoration completed successfully!"
    
    echo "Restored database: $TARGET_DB"
    
    
    
    # Verify restoration
    
    echo "Verifying restoration..."
    
    TABLES_COUNT=$(psql \
    
        --host="$DB_HOST" \
    
        --port="$DB_PORT" \
    
        --username="$DB_USER" \
    
        --dbname="$TARGET_DB" \
    
        --tuples-only \
    
        --command="SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = 'retail';"
    
    )
    
    
    
    echo "Verified $TABLES_COUNT tables in retail schema"
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„: ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ Row Level Security ๊ตฌํ˜„

    โœ… ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ: ์˜๋ฏธ ์žˆ๋Š” ์ œํ’ˆ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ pgvector ์„ค์ •

    โœ… ํฌ๊ด„์ ์ธ ์Šคํ‚ค๋งˆ: ํ”„๋กœ๋•์…˜ ์ค€๋น„๊ฐ€ ๋œ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ์ƒ์„ฑ

    โœ… ์ƒ˜ํ”Œ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ: ๊ฐœ๋ฐœ ๋ฐ ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•œ ํ˜„์‹ค์ ์ธ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ๊ตฌ์ถ•

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์ธ๋ฑ์Šค ๋ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™” ๊ตฌ์„ฑ

    โœ… ๋ฐฑ์—… ๋ฐ ๋ณต๊ตฌ: ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ ์ „๋žต ์ˆ˜๋ฆฝ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 05: MCP ์„œ๋ฒ„ ๊ตฌํ˜„์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • ์ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์—ฐ๊ฒฐํ•˜๋Š” FastMCP ์„œ๋ฒ„ ๊ตฌ์ถ•
  • MCP ํ”„๋กœํ† ์ฝœ์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๋„๊ตฌ ๊ตฌํ˜„
  • Embeddings๋ฅผ ์‚ฌ์šฉํ•œ ์˜๋ฏธ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ์ถ”๊ฐ€
  • ์—ฐ๊ฒฐ ํ’€๋ง ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๊ตฌ์„ฑ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    PostgreSQL & pgvector

  • PostgreSQL ๋ฌธ์„œ - PostgreSQL์˜ ์™„์ „ํ•œ ์ฐธ์กฐ ์ž๋ฃŒ
  • pgvector ํ™•์žฅ - PostgreSQL์„ ์œ„ํ•œ ๋ฒกํ„ฐ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰
  • PostgreSQL ์„ฑ๋Šฅ ํŠœ๋‹ - ์ตœ์ ํ™” ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์•„ํ‚คํ…์ฒ˜

  • Row Level Security - PostgreSQL RLS ๋ฌธ์„œ
  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•„ํ‚คํ…์ฒ˜ - Azure ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - PostgreSQL ๋ณด์•ˆ ๊ฐ€์ด๋“œ
  • ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋ณธ - ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ดํ•ดํ•˜๊ธฐ
  • Embedding ๋ชจ๋ธ - OpenAI Embeddings ๋ฌธ์„œ
  • HNSW ์•Œ๊ณ ๋ฆฌ์ฆ˜ - ๊ณ„์ธต์  ํƒ์ƒ‰ ๊ฐ€๋Šฅํ•œ ์†Œํ˜• ์„ธ๊ณ„ ๊ทธ๋ž˜ํ”„
  • ---

    ์ด์ „: Lab 03: ํ™˜๊ฒฝ ์„ค์ •

    ๋‹ค์Œ: Lab 05: MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    05 MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ FastMCP ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€์˜ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ •์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. ํ•ต์‹ฌ ์„œ๋ฒ„ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถ•ํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ๊ตฌํ˜„ํ•˜๋ฉฐ, ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค๋ฅผ ์œ„ํ•œ ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค๊ณ , AI ๊ธฐ๋ฐ˜ ์†Œ๋งค ๋ถ„์„์„ ์œ„ํ•œ ๊ธฐ์ดˆ๋ฅผ ์„ค์ •ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ์„œ๋ฒ„๋Š” ์†Œ๋งค ๋ถ„์„ ์†”๋ฃจ์…˜์˜ ์ค‘์‹ฌ์ž…๋‹ˆ๋‹ค. ์ด ์„œ๋ฒ„๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ„์˜ ๋‹ค๋ฆฌ ์—ญํ• ์„ ํ•˜๋ฉฐ, ํ‘œ์ค€ํ™”๋œ ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ณ  ์ง€๋Šฅ์ ์œผ๋กœ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

    ์ด ์‹ค์Šต์—์„œ๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด๊ณผ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ๋”ฐ๋ฅด๋Š” ๊ฒฌ๊ณ ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๊ตฌ์ถ•: ์ ์ ˆํ•œ ์•„ํ‚คํ…์ฒ˜์™€ ์กฐ์ง์„ ๊ฐ–์ถ˜ FastMCP ์„œ๋ฒ„
  • ๊ตฌํ˜„: ์—ฐ๊ฒฐ ํ’€๋ง ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ํฌํ•จํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ
  • ์ƒ์„ฑ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๋ฐ ์ฟผ๋ฆฌ ์‹คํ–‰์„ ์œ„ํ•œ MCP ๋„๊ตฌ
  • ๊ตฌ์„ฑ: ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ(RLS) ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ
  • ์ถ”๊ฐ€: ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€์ฐฐ ๊ธฐ๋Šฅ
  • ํ…Œ์ŠคํŠธ: MCP ์„œ๋ฒ„ ๊ตฌํ˜„์„ ๋กœ์ปฌ ๋ฐ VS Code์—์„œ ํ…Œ์ŠคํŠธ
  • ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

    MCP ์„œ๋ฒ„์˜ ์กฐ์ง์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

    
    mcp_server/
    
    โ”œโ”€โ”€ __init__.py                 # Package initialization
    
    โ”œโ”€โ”€ config.py                   # Configuration management
    
    โ”œโ”€โ”€ health_check.py             # Health monitoring endpoints
    
    โ”œโ”€โ”€ sales_analysis.py           # Main MCP server implementation
    
    โ”œโ”€โ”€ sales_analysis_postgres.py  # Database integration layer
    
    โ””โ”€โ”€ sales_analysis_text_embeddings.py  # AI/semantic search integration
    
    

    ๐Ÿ”ง ๊ตฌ์„ฑ ๊ด€๋ฆฌ

    ํ™˜๊ฒฝ ๊ตฌ์„ฑ (config.py)

    ๋จผ์ € ๊ฒฌ๊ณ ํ•œ ๊ตฌ์„ฑ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด ๋ด…์‹œ๋‹ค:

    
    # mcp_server/config.py
    
    """
    
    Configuration management for the MCP server.
    
    Handles environment variables, validation, and defaults.
    
    """
    
    import os
    
    import logging
    
    from typing import Optional, Dict, Any
    
    from dataclasses import dataclass
    
    from dotenv import load_dotenv
    
    
    
    # Load environment variables from .env file
    
    load_dotenv()
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    @dataclass
    
    class DatabaseConfig:
    
        """Database connection configuration."""
    
        host: str
    
        port: int
    
        database: str
    
        user: str
    
        password: str
    
        min_connections: int = 2
    
        max_connections: int = 10
    
        command_timeout: int = 30
    
        
    
        @classmethod
    
        def from_env(cls) -> 'DatabaseConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                host=os.getenv('POSTGRES_HOST', 'localhost'),
    
                port=int(os.getenv('POSTGRES_PORT', '5432')),
    
                database=os.getenv('POSTGRES_DB', 'zava'),
    
                user=os.getenv('POSTGRES_USER', 'postgres'),
    
                password=os.getenv('POSTGRES_PASSWORD', ''),
    
                min_connections=int(os.getenv('POSTGRES_MIN_CONNECTIONS', '2')),
    
                max_connections=int(os.getenv('POSTGRES_MAX_CONNECTIONS', '10')),
    
                command_timeout=int(os.getenv('POSTGRES_COMMAND_TIMEOUT', '30'))
    
            )
    
        
    
        def to_asyncpg_params(self) -> Dict[str, Any]:
    
            """Convert to asyncpg connection parameters."""
    
            return {
    
                'host': self.host,
    
                'port': self.port,
    
                'database': self.database,
    
                'user': self.user,
    
                'password': self.password,
    
                'command_timeout': self.command_timeout,
    
                'server_settings': {
    
                    'application_name': 'zava-mcp-server',
    
                    'jit': 'off',  # Disable JIT for stability
    
                    'work_mem': '4MB',
    
                    'statement_timeout': f'{self.command_timeout}s'
    
                }
    
            }
    
    
    
    @dataclass
    
    class AzureConfig:
    
        """Azure AI services configuration."""
    
        project_endpoint: str
    
        openai_endpoint: str
    
        embedding_model_deployment: str
    
        client_id: str
    
        client_secret: str
    
        tenant_id: str
    
        
    
        @classmethod
    
        def from_env(cls) -> 'AzureConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                project_endpoint=os.getenv('PROJECT_ENDPOINT', ''),
    
                openai_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
    
                embedding_model_deployment=os.getenv('EMBEDDING_MODEL_DEPLOYMENT_NAME', 'text-embedding-3-small'),
    
                client_id=os.getenv('AZURE_CLIENT_ID', ''),
    
                client_secret=os.getenv('AZURE_CLIENT_SECRET', ''),
    
                tenant_id=os.getenv('AZURE_TENANT_ID', '')
    
            )
    
        
    
        def is_configured(self) -> bool:
    
            """Check if all required Azure configuration is present."""
    
            return all([
    
                self.project_endpoint,
    
                self.openai_endpoint,
    
                self.client_id,
    
                self.client_secret,
    
                self.tenant_id
    
            ])
    
    
    
    @dataclass
    
    class ServerConfig:
    
        """MCP server configuration."""
    
        host: str = '0.0.0.0'
    
        port: int = 8000
    
        log_level: str = 'INFO'
    
        enable_cors: bool = True
    
        enable_health_check: bool = True
    
        applicationinsights_connection_string: Optional[str] = None
    
        
    
        @classmethod
    
        def from_env(cls) -> 'ServerConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                host=os.getenv('MCP_SERVER_HOST', '0.0.0.0'),
    
                port=int(os.getenv('MCP_SERVER_PORT', '8000')),
    
                log_level=os.getenv('LOG_LEVEL', 'INFO').upper(),
    
                enable_cors=os.getenv('ENABLE_CORS', 'true').lower() == 'true',
    
                enable_health_check=os.getenv('ENABLE_HEALTH_CHECK', 'true').lower() == 'true',
    
                applicationinsights_connection_string=os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING')
    
            )
    
    
    
    class MCPServerConfig:
    
        """Main configuration class for the MCP server."""
    
        
    
        def __init__(self):
    
            self.database = DatabaseConfig.from_env()
    
            self.azure = AzureConfig.from_env()
    
            self.server = ServerConfig.from_env()
    
            
    
            # Validate configuration
    
            self._validate_config()
    
        
    
        def _validate_config(self):
    
            """Validate configuration and log warnings for missing values."""
    
            if not self.database.password:
    
                logger.warning("Database password is empty. This may cause connection issues.")
    
            
    
            if not self.azure.is_configured():
    
                logger.warning("Azure configuration is incomplete. AI features may not work.")
    
            
    
            logger.info(f"Configuration loaded - Database: {self.database.host}:{self.database.port}")
    
            logger.info(f"Server will run on {self.server.host}:{self.server.port}")
    
    
    
    # Global configuration instance
    
    config = MCPServerConfig()
    
    

    ์ฃผ์š” ๊ตฌ์„ฑ ๊ธฐ๋Šฅ

  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ: ์ž๋™ .env ํŒŒ์ผ ์ง€์›
  • ํƒ€์ž… ์•ˆ์ „์„ฑ: ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค ๊ฒ€์ฆ ๋ฐ ํƒ€์ž… ํžŒํŠธ
  • ์œ ์—ฐํ•œ ๊ธฐ๋ณธ๊ฐ’: ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ํ•ฉ๋ฆฌ์ ์ธ ๊ธฐ๋ณธ๊ฐ’
  • ๊ฒ€์ฆ: ์œ ์šฉํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จํ•œ ๊ตฌ์„ฑ ๊ฒ€์ฆ
  • ๋ณด์•ˆ: ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ๋งŒ ๋ฏผ๊ฐํ•œ ๊ฐ’ ๋กœ๋“œ
  • ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ๊ณ„์ธต

    PostgreSQL ์ œ๊ณต์ž (sales_analysis_postgres.py)

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ๊ณ„์ธต์„ ๊ตฌํ˜„ํ•ด ๋ด…์‹œ๋‹ค:

    
    # mcp_server/sales_analysis_postgres.py
    
    """
    
    PostgreSQL database integration for MCP server.
    
    Handles connections, queries, and schema introspection.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import logging
    
    from typing import Dict, Any, List, Optional, Tuple
    
    from contextlib import asynccontextmanager
    
    from datetime import datetime
    
    import json
    
    
    
    from .config import config
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class PostgreSQLSchemaProvider:
    
        """Provides PostgreSQL database access and schema information."""
    
        
    
        def __init__(self):
    
            self.connection_pool: Optional[asyncpg.Pool] = None
    
            self.postgres_config = config.database.to_asyncpg_params()
    
            
    
        async def create_pool(self) -> None:
    
            """Create connection pool for database operations."""
    
            if self.connection_pool is None:
    
                try:
    
                    self.connection_pool = await asyncpg.create_pool(
    
                        **self.postgres_config,
    
                        min_size=config.database.min_connections,
    
                        max_size=config.database.max_connections,
    
                        max_inactive_connection_lifetime=300  # 5 minutes
    
                    )
    
                    logger.info("Database connection pool created successfully")
    
                except Exception as e:
    
                    logger.error(f"Failed to create database connection pool: {e}")
    
                    raise
    
        
    
        async def close_pool(self) -> None:
    
            """Close the connection pool."""
    
            if self.connection_pool:
    
                await self.connection_pool.close()
    
                self.connection_pool = None
    
                logger.info("Database connection pool closed")
    
        
    
        @asynccontextmanager
    
        async def get_connection(self):
    
            """Get a database connection from the pool."""
    
            if not self.connection_pool:
    
                await self.create_pool()
    
            
    
            async with self.connection_pool.acquire() as connection:
    
                yield connection
    
        
    
        async def set_rls_context(self, connection: asyncpg.Connection, rls_user_id: str) -> None:
    
            """Set Row Level Security context for the connection."""
    
            try:
    
                await connection.execute(
    
                    "SELECT set_config('app.current_rls_user_id', $1, false)",
    
                    rls_user_id
    
                )
    
                logger.debug(f"RLS context set for user: {rls_user_id}")
    
            except Exception as e:
    
                logger.error(f"Failed to set RLS context: {e}")
    
                raise
    
        
    
        async def get_table_schema(self, table_name: str, rls_user_id: str) -> Dict[str, Any]:
    
            """Get detailed schema information for a specific table."""
    
            async with self.get_connection() as conn:
    
                await self.set_rls_context(conn, rls_user_id)
    
                
    
                # Parse schema and table name
    
                if '.' in table_name:
    
                    schema_name, table_name = table_name.split('.', 1)
    
                else:
    
                    schema_name = 'retail'  # Default schema
    
                
    
                # Get column information
    
                columns_query = """
    
                    SELECT 
    
                        column_name,
    
                        data_type,
    
                        is_nullable,
    
                        column_default,
    
                        character_maximum_length,
    
                        numeric_precision,
    
                        numeric_scale,
    
                        ordinal_position
    
                    FROM information_schema.columns 
    
                    WHERE table_schema = $1 AND table_name = $2
    
                    ORDER BY ordinal_position
    
                """
    
                
    
                columns = await conn.fetch(columns_query, schema_name, table_name)
    
                
    
                if not columns:
    
                    raise ValueError(f"Table {schema_name}.{table_name} not found or not accessible")
    
                
    
                # Get foreign key relationships
    
                fk_query = """
    
                    SELECT 
    
                        kcu.column_name,
    
                        ccu.table_schema AS foreign_table_schema,
    
                        ccu.table_name AS foreign_table_name,
    
                        ccu.column_name AS foreign_column_name
    
                    FROM information_schema.table_constraints tc
    
                    JOIN information_schema.key_column_usage kcu 
    
                        ON tc.constraint_name = kcu.constraint_name
    
                    JOIN information_schema.constraint_column_usage ccu 
    
                        ON ccu.constraint_name = tc.constraint_name
    
                    WHERE tc.constraint_type = 'FOREIGN KEY' 
    
                        AND tc.table_schema = $1 
    
                        AND tc.table_name = $2
    
                """
    
                
    
                foreign_keys = await conn.fetch(fk_query, schema_name, table_name)
    
                
    
                # Get indexes
    
                index_query = """
    
                    SELECT 
    
                        indexname,
    
                        indexdef
    
                    FROM pg_indexes 
    
                    WHERE schemaname = $1 AND tablename = $2
    
                """
    
                
    
                indexes = await conn.fetch(index_query, schema_name, table_name)
    
                
    
                # Format schema information
    
                schema_info = {
    
                    "table_name": f"{schema_name}.{table_name}",
    
                    "columns": [
    
                        {
    
                            "name": col["column_name"],
    
                            "type": col["data_type"],
    
                            "nullable": col["is_nullable"] == "YES",
    
                            "default": col["column_default"],
    
                            "max_length": col["character_maximum_length"],
    
                            "precision": col["numeric_precision"],
    
                            "scale": col["numeric_scale"],
    
                            "position": col["ordinal_position"]
    
                        }
    
                        for col in columns
    
                    ],
    
                    "foreign_keys": [
    
                        {
    
                            "column": fk["column_name"],
    
                            "references": f"{fk['foreign_table_schema']}.{fk['foreign_table_name']}.{fk['foreign_column_name']}"
    
                        }
    
                        for fk in foreign_keys
    
                    ],
    
                    "indexes": [
    
                        {
    
                            "name": idx["indexname"],
    
                            "definition": idx["indexdef"]
    
                        }
    
                        for idx in indexes
    
                    ]
    
                }
    
                
    
                return schema_info
    
        
    
        async def get_multiple_table_schemas(
    
            self, 
    
            table_names: List[str], 
    
            rls_user_id: str
    
        ) -> str:
    
            """Get schema information for multiple tables."""
    
            schemas = []
    
            
    
            for table_name in table_names:
    
                try:
    
                    schema = await self.get_table_schema(table_name, rls_user_id)
    
                    schemas.append(self._format_schema_for_ai(schema))
    
                except Exception as e:
    
                    logger.warning(f"Failed to get schema for {table_name}: {e}")
    
                    schemas.append(f"Error retrieving schema for {table_name}: {str(e)}")
    
            
    
            return "\n\n".join(schemas)
    
        
    
        def _format_schema_for_ai(self, schema: Dict[str, Any]) -> str:
    
            """Format schema information for AI consumption."""
    
            table_name = schema["table_name"]
    
            columns = schema["columns"]
    
            foreign_keys = schema["foreign_keys"]
    
            
    
            # Create column definitions
    
            column_lines = []
    
            for col in columns:
    
                nullable = "NULL" if col["nullable"] else "NOT NULL"
    
                type_info = col["type"]
    
                
    
                if col["max_length"]:
    
                    type_info += f"({col['max_length']})"
    
                elif col["precision"] and col["scale"]:
    
                    type_info += f"({col['precision']},{col['scale']})"
    
                
    
                default_info = f" DEFAULT {col['default']}" if col["default"] else ""
    
                
    
                column_lines.append(f"  {col['name']} {type_info} {nullable}{default_info}")
    
            
    
            # Create foreign key information
    
            fk_lines = []
    
            for fk in foreign_keys:
    
                fk_lines.append(f"  {fk['column']} -> {fk['references']}")
    
            
    
            # Combine into readable format
    
            schema_text = f"Table: {table_name}\n"
    
            schema_text += "Columns:\n" + "\n".join(column_lines)
    
            
    
            if fk_lines:
    
                schema_text += "\n\nForeign Keys:\n" + "\n".join(fk_lines)
    
            
    
            return schema_text
    
        
    
        async def execute_query(
    
            self, 
    
            sql_query: str, 
    
            rls_user_id: str,
    
            max_rows: int = 20
    
        ) -> str:
    
            """Execute a SQL query with Row Level Security context."""
    
            async with self.get_connection() as conn:
    
                await self.set_rls_context(conn, rls_user_id)
    
                
    
                try:
    
                    # Set a query timeout
    
                    rows = await asyncio.wait_for(
    
                        conn.fetch(sql_query),
    
                        timeout=config.database.command_timeout
    
                    )
    
                    
    
                    if not rows:
    
                        return "Query executed successfully. No rows returned."
    
                    
    
                    # Limit result set size
    
                    limited_rows = rows[:max_rows]
    
                    
    
                    # Format results
    
                    result = self._format_query_results(limited_rows, len(rows), max_rows)
    
                    
    
                    logger.info(f"Query executed successfully. Returned {len(limited_rows)} rows.")
    
                    return result
    
                    
    
                except asyncio.TimeoutError:
    
                    error_msg = f"Query timeout after {config.database.command_timeout} seconds"
    
                    logger.error(error_msg)
    
                    raise Exception(error_msg)
    
                except Exception as e:
    
                    logger.error(f"Query execution failed: {e}")
    
                    raise
    
        
    
        def _format_query_results(
    
            self, 
    
            rows: List[asyncpg.Record], 
    
            total_rows: int,
    
            max_rows: int
    
        ) -> str:
    
            """Format query results for AI consumption."""
    
            if not rows:
    
                return "No results found."
    
            
    
            # Get column names
    
            columns = list(rows[0].keys())
    
            
    
            # Create header
    
            result_lines = [f"Results ({len(rows)} of {total_rows} rows):"]
    
            result_lines.append("=" * 50)
    
            
    
            # Add column headers
    
            header = " | ".join(columns)
    
            result_lines.append(header)
    
            result_lines.append("-" * len(header))
    
            
    
            # Add data rows
    
            for row in rows:
    
                formatted_values = []
    
                for col in columns:
    
                    value = row[col]
    
                    if value is None:
    
                        formatted_values.append("NULL")
    
                    elif isinstance(value, datetime):
    
                        formatted_values.append(value.strftime("%Y-%m-%d %H:%M:%S"))
    
                    elif isinstance(value, (dict, list)):
    
                        formatted_values.append(json.dumps(value))
    
                    else:
    
                        formatted_values.append(str(value))
    
                
    
                result_lines.append(" | ".join(formatted_values))
    
            
    
            # Add truncation notice if needed
    
            if total_rows > max_rows:
    
                result_lines.append(f"\n... and {total_rows - max_rows} more rows (truncated for display)")
    
            
    
            return "\n".join(result_lines)
    
        
    
        async def get_current_utc_date(self) -> str:
    
            """Get current UTC date/time."""
    
            async with self.get_connection() as conn:
    
                result = await conn.fetchval("SELECT NOW() AT TIME ZONE 'UTC'")
    
                return result.isoformat() + "Z"
    
        
    
        async def health_check(self) -> Dict[str, Any]:
    
            """Perform database health check."""
    
            try:
    
                async with self.get_connection() as conn:
    
                    # Simple connectivity test
    
                    result = await conn.fetchval("SELECT 1")
    
                    
    
                    # Check pool status
    
                    pool_info = {
    
                        "min_size": self.connection_pool._minsize if self.connection_pool else 0,
    
                        "max_size": self.connection_pool._maxsize if self.connection_pool else 0,
    
                        "current_size": self.connection_pool.get_size() if self.connection_pool else 0,
    
                        "idle_size": self.connection_pool.get_idle_size() if self.connection_pool else 0
    
                    }
    
                    
    
                    return {
    
                        "status": "healthy",
    
                        "database_responsive": result == 1,
    
                        "pool_info": pool_info
    
                    }
    
                    
    
            except Exception as e:
    
                return {
    
                    "status": "unhealthy",
    
                    "error": str(e)
    
                }
    
    
    
    # Global database provider instance
    
    db_provider = PostgreSQLSchemaProvider()
    
    

    ์ฃผ์š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณ„์ธต ๊ธฐ๋Šฅ

  • ์—ฐ๊ฒฐ ํ’€๋ง: asyncpg๋ฅผ ์‚ฌ์šฉํ•œ ํšจ์œจ์ ์ธ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ
  • RLS ํ†ตํ•ฉ: ์ž๋™ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ ์„ค์ •
  • ์Šคํ‚ค๋งˆ ํƒ์ƒ‰: ๋™์  ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ๊ฒ€์ƒ‰
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ๊ด€๋ฆฌ ๋ฐ ๋กœ๊น…
  • ์ฟผ๋ฆฌ ํฌ๋งทํŒ…: AI ์นœํ™”์ ์ธ ๊ฒฐ๊ณผ ํฌ๋งทํŒ…
  • ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ฐ ํ’€ ์ƒํƒœ ํ™•์ธ
  • ๐Ÿ”ง ์ฃผ์š” MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    FastMCP ์„œ๋ฒ„ (sales_analysis.py)

    ์ด์ œ ์ฃผ์š” MCP ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•ด ๋ด…์‹œ๋‹ค:

    
    # mcp_server/sales_analysis.py
    
    """
    
    Main MCP server implementation for Zava Retail Sales Analysis.
    
    Provides AI assistants with secure access to retail database.
    
    """
    
    import logging
    
    import asyncio
    
    from typing import Dict, Any, List, Annotated
    
    from contextlib import asynccontextmanager
    
    
    
    from fastmcp import FastMCP, Context
    
    from pydantic import Field
    
    
    
    from .config import config
    
    from .sales_analysis_postgres import db_provider
    
    from .health_check import setup_health_endpoints
    
    
    
    # Configure logging
    
    logging.basicConfig(
    
        level=getattr(logging, config.server.log_level),
    
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    
    )
    
    logger = logging.getLogger(__name__)
    
    
    
    # Create FastMCP server instance
    
    mcp = FastMCP("Zava Retail Sales Analysis")
    
    
    
    # List of valid tables for schema access
    
    VALID_TABLES = [
    
        "retail.stores",
    
        "retail.customers", 
    
        "retail.categories",
    
        "retail.product_types",
    
        "retail.products",
    
        "retail.orders",
    
        "retail.order_items",
    
        "retail.inventory"
    
    ]
    
    
    
    def get_rls_user_id(ctx: Context) -> str:
    
        """Extract Row Level Security User ID from request context."""
    
        # In HTTP mode, get from headers
    
        if hasattr(ctx, 'headers') and ctx.headers:
    
            rls_user_id = ctx.headers.get("x-rls-user-id")
    
            if rls_user_id:
    
                logger.debug(f"RLS User ID from headers: {rls_user_id}")
    
                return rls_user_id
    
        
    
        # Default fallback for development/testing
    
        default_id = "00000000-0000-0000-0000-000000000000"
    
        logger.warning(f"No RLS User ID found, using default: {default_id}")
    
        return default_id
    
    
    
    @mcp.tool()
    
    async def get_multiple_table_schemas(
    
        ctx: Context,
    
        table_names: Annotated[List[str], Field(description="List of table names to retrieve schemas for. Valid tables: " + ", ".join(VALID_TABLES))]
    
    ) -> str:
    
        """
    
        Retrieve database schemas for multiple tables in a single request.
    
        
    
        This tool provides comprehensive schema information including:
    
        - Column names, types, and constraints
    
        - Foreign key relationships
    
        - Index information
    
        - Table structure for AI query planning
    
        
    
        Args:
    
            table_names: List of valid table names from the retail schema
    
            
    
        Returns:
    
            Formatted schema information for all requested tables
    
        """
    
        rls_user_id = get_rls_user_id(ctx)
    
        
    
        # Validate table names
    
        invalid_tables = [table for table in table_names if table not in VALID_TABLES]
    
        if invalid_tables:
    
            logger.warning(f"Invalid table names requested: {invalid_tables}")
    
            return f"Error: Invalid table names: {', '.join(invalid_tables)}. Valid tables are: {', '.join(VALID_TABLES)}"
    
        
    
        try:
    
            logger.info(f"Retrieving schemas for tables: {table_names} (User: {rls_user_id})")
    
            result = await db_provider.get_multiple_table_schemas(table_names, rls_user_id)
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error retrieving table schemas: {e}")
    
            return f"Error retrieving table schemas: {e!s}"
    
    
    
    @mcp.tool()
    
    async def execute_sales_query(
    
        ctx: Context,
    
        postgresql_query: Annotated[str, Field(description="A well-formed PostgreSQL query to execute against the retail database. Always get table schemas first before writing queries.")]
    
    ) -> str:
    
        """
    
        Execute PostgreSQL queries against the retail sales database with Row Level Security.
    
        
    
        This tool allows AI assistants to run analytical queries on retail data including:
    
        - Sales performance analysis
    
        - Customer behavior insights  
    
        - Inventory management queries
    
        - Product performance metrics
    
        - Store-specific reporting
    
        
    
        Important: Row Level Security ensures users only see data they're authorized to access.
    
        
    
        Args:
    
            postgresql_query: SQL query to execute (automatically filtered by RLS)
    
            
    
        Returns:
    
            Query results formatted for AI analysis (limited to 20 rows for readability)
    
        """
    
        rls_user_id = get_rls_user_id(ctx)
    
        
    
        try:
    
            logger.info(f"Executing query for user: {rls_user_id}")
    
            logger.debug(f"Query: {postgresql_query[:100]}...")
    
            
    
            result = await db_provider.execute_query(postgresql_query, rls_user_id)
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error executing database query: {e}")
    
            return f"Error executing database query: {e!s}"
    
    
    
    @mcp.tool()
    
    async def get_current_utc_date(ctx: Context) -> str:
    
        """
    
        Get the current UTC date and time in ISO format.
    
        
    
        Useful for time-sensitive queries and date-based analysis.
    
        
    
        Returns:
    
            Current UTC date/time in ISO format (YYYY-MM-DDTHH:MM:SS.fffffZ)
    
        """
    
        try:
    
            result = await db_provider.get_current_utc_date()
    
            logger.debug(f"Current UTC date retrieved: {result}")
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error getting current UTC date: {e}")
    
            return f"Error getting current UTC date: {e!s}"
    
    
    
    # Application lifecycle management
    
    @asynccontextmanager
    
    async def lifespan(app):
    
        """Manage application startup and shutdown."""
    
        logger.info("Starting Zava Retail MCP Server...")
    
        
    
        try:
    
            # Initialize database connection pool
    
            await db_provider.create_pool()
    
            logger.info("Database connection pool initialized")
    
            
    
            # Test database connectivity
    
            health_status = await db_provider.health_check()
    
            if health_status["status"] != "healthy":
    
                logger.error(f"Database health check failed: {health_status}")
    
                raise Exception("Database not healthy")
    
            
    
            logger.info("MCP Server startup complete")
    
            yield
    
            
    
        except Exception as e:
    
            logger.error(f"Startup failed: {e}")
    
            raise
    
        finally:
    
            # Cleanup
    
            logger.info("Shutting down MCP Server...")
    
            await db_provider.close_pool()
    
            logger.info("MCP Server shutdown complete")
    
    
    
    # Configure server application
    
    def create_app():
    
        """Create and configure the MCP server application."""
    
        
    
        # Get the FastMCP app instance
    
        app = mcp.sse_app()
    
        
    
        # Set up lifecycle management
    
        app.router.lifespan_context = lifespan
    
        
    
        # Add health check endpoints if enabled
    
        if config.server.enable_health_check:
    
            setup_health_endpoints(app, db_provider)
    
        
    
        # Configure CORS if enabled
    
        if config.server.enable_cors:
    
            from fastapi.middleware.cors import CORSMiddleware
    
            app.add_middleware(
    
                CORSMiddleware,
    
                allow_origins=["*"],  # Configure appropriately for production
    
                allow_credentials=True,
    
                allow_methods=["*"],
    
                allow_headers=["*"],
    
            )
    
        
    
        logger.info(f"MCP Server configured - CORS: {config.server.enable_cors}, Health: {config.server.enable_health_check}")
    
        
    
        return app
    
    
    
    # Create the application instance
    
    app = create_app()
    
    
    
    # Main entry point for development
    
    if __name__ == "__main__":
    
        import uvicorn
    
        
    
        logger.info(f"Starting development server on {config.server.host}:{config.server.port}")
    
        
    
        uvicorn.run(
    
            "sales_analysis:app",
    
            host=config.server.host,
    
            port=config.server.port,
    
            reload=True,
    
            log_level=config.server.log_level.lower()
    
        )
    
    

    ์ฃผ์š” MCP ์„œ๋ฒ„ ๊ธฐ๋Šฅ

  • ๋„๊ตฌ ๋“ฑ๋ก: ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ฐ–์ถ˜ ์„ ์–ธ์  ๋„๊ตฌ ์ •์˜
  • RLS ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ: ์‚ฌ์šฉ์ž ์‹ ์› ์ถ”์ถœ ๋ฐ ์ปจํ…์ŠคํŠธ ์„ค์ • ์ž๋™ํ™”
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จํ•œ ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ๊ด€๋ฆฌ
  • ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ: ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ๋ฅผ ํฌํ•จํ•œ ์ ์ ˆํ•œ ์‹œ์ž‘/์ข…๋ฃŒ
  • ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง: ๋‚ด์žฅ ์ƒํƒœ ํ™•์ธ ์—”๋“œํฌ์ธํŠธ
  • ๊ฐœ๋ฐœ ์ง€์›: ํ•ซ ๋ฆฌ๋กœ๋“œ ๋ฐ ๋””๋ฒ„๊น… ๊ธฐ๋Šฅ
  • ๐Ÿฅ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง

    ์ƒํƒœ ํ™•์ธ ๊ตฌํ˜„ (health_check.py)

    
    # mcp_server/health_check.py
    
    """
    
    Health check endpoints for monitoring MCP server status.
    
    """
    
    import logging
    
    from typing import Dict, Any
    
    from fastapi import FastAPI, HTTPException
    
    from fastapi.responses import JSONResponse
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    def setup_health_endpoints(app: FastAPI, db_provider) -> None:
    
        """Add health check endpoints to the FastAPI application."""
    
        
    
        @app.get("/health")
    
        async def health_check() -> JSONResponse:
    
            """Basic health check endpoint."""
    
            return JSONResponse(
    
                status_code=200,
    
                content={
    
                    "status": "healthy",
    
                    "service": "zava-retail-mcp-server",
    
                    "timestamp": await db_provider.get_current_utc_date()
    
                }
    
            )
    
        
    
        @app.get("/health/detailed")
    
        async def detailed_health_check() -> JSONResponse:
    
            """Detailed health check including database connectivity."""
    
            health_status = {
    
                "service": "zava-retail-mcp-server",
    
                "status": "healthy",
    
                "components": {}
    
            }
    
            
    
            overall_healthy = True
    
            
    
            # Check database
    
            try:
    
                db_health = await db_provider.health_check()
    
                health_status["components"]["database"] = db_health
    
                
    
                if db_health["status"] != "healthy":
    
                    overall_healthy = False
    
                    
    
            except Exception as e:
    
                health_status["components"]["database"] = {
    
                    "status": "unhealthy",
    
                    "error": str(e)
    
                }
    
                overall_healthy = False
    
            
    
            # Update overall status
    
            if not overall_healthy:
    
                health_status["status"] = "unhealthy"
    
            
    
            status_code = 200 if overall_healthy else 503
    
            
    
            return JSONResponse(
    
                status_code=status_code,
    
                content=health_status
    
            )
    
        
    
        @app.get("/health/ready")
    
        async def readiness_check() -> JSONResponse:
    
            """Kubernetes readiness probe endpoint."""
    
            try:
    
                # Test critical functionality
    
                db_health = await db_provider.health_check()
    
                
    
                if db_health["status"] != "healthy":
    
                    raise HTTPException(status_code=503, detail="Database not ready")
    
                
    
                return JSONResponse(
    
                    status_code=200,
    
                    content={"status": "ready"}
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Readiness check failed: {e}")
    
                raise HTTPException(status_code=503, detail="Service not ready")
    
        
    
        @app.get("/health/live")
    
        async def liveness_check() -> JSONResponse:
    
            """Kubernetes liveness probe endpoint."""
    
            return JSONResponse(
    
                status_code=200,
    
                content={"status": "alive"}
    
            )
    
        
    
        logger.info("Health check endpoints configured")
    
    

    ๐Ÿงช MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ

    ๋กœ์ปฌ ํ…Œ์ŠคํŠธ

    1. MCP ์„œ๋ฒ„ ์‹œ์ž‘:

    ```bash

    # Activate virtual environment

    source mcp-env/bin/activate # macOS/Linux

    # mcp-env\Scripts\activate # Windows

    # Start server

    cd mcp_server

    python sales_analysis.py

    ```

    2. ์ƒํƒœ ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ:

    ```bash

    # Basic health check

    curl http://localhost:8000/health

    # Detailed health check

    curl http://localhost:8000/health/detailed

    ```

    3. MCP ๋„๊ตฌ ํ…Œ์ŠคํŠธ:

    ```bash

    # List available tools

    curl -X POST http://localhost:8000/mcp \

    -H "Content-Type: application/json" \

    -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \

    -d '{"method": "tools/list", "params": {}}'

    # Get table schemas

    curl -X POST http://localhost:8000/mcp \

    -H "Content-Type: application/json" \

    -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \

    -d '{

    "method": "tools/call",

    "params": {

    "name": "get_multiple_table_schemas",

    "arguments": {

    "table_names": ["retail.stores", "retail.products"]

    }

    }

    }'

    ```

    VS Code ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    1. VS Code MCP ๊ตฌ์„ฑ:

    ```json

    // .vscode/mcp.json

    {

    "servers": {

    "zava-retail-test": {

    "url": "http://127.0.0.1:8000/mcp",

    "type": "http",

    "headers": {"x-rls-user-id": "00000000-0000-0000-0000-000000000000"}

    }

    }

    }

    ```

    2. AI ์ฑ„ํŒ…์—์„œ ํ…Œ์ŠคํŠธ:

    - VS Code AI ์ฑ„ํŒ… ์—ด๊ธฐ

    - #zava๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์„œ๋ฒ„ ์„ ํƒ

    - ์งˆ๋ฌธ: "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ…Œ์ด๋ธ”์€ ๋ฌด์—‡์ธ๊ฐ€์š”?"

    - ์งˆ๋ฌธ: "์ฃผ๋ฌธ ์ˆ˜ ๊ธฐ์ค€ ์ƒ์œ„ 5๊ฐœ ๋งค์žฅ์„ ๋ณด์—ฌ์ฃผ์„ธ์š”."

    ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

    ํฌ๊ด„์ ์ธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”:

    
    # tests/test_mcp_server.py
    
    import pytest
    
    import asyncio
    
    from mcp_server.sales_analysis_postgres import PostgreSQLSchemaProvider
    
    from mcp_server.config import config
    
    
    
    @pytest.mark.asyncio
    
    async def test_database_connection():
    
        """Test database connectivity."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            health = await db.health_check()
    
            assert health["status"] == "healthy"
    
        finally:
    
            await db.close_pool()
    
    
    
    @pytest.mark.asyncio
    
    async def test_table_schema_retrieval():
    
        """Test table schema retrieval."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            schema = await db.get_table_schema("retail.stores", "00000000-0000-0000-0000-000000000000")
    
            
    
            assert schema["table_name"] == "retail.stores"
    
            assert len(schema["columns"]) > 0
    
            
    
        finally:
    
            await db.close_pool()
    
    
    
    @pytest.mark.asyncio
    
    async def test_query_execution():
    
        """Test query execution with RLS."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            result = await db.execute_query(
    
                "SELECT COUNT(*) as store_count FROM retail.stores",
    
                "00000000-0000-0000-0000-000000000000"
    
            )
    
            
    
            assert "store_count" in result
    
            
    
        finally:
    
            await db.close_pool()
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ์ž‘๋™ํ•˜๋Š” MCP ์„œ๋ฒ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ๊ฐ–์ถ˜ FastMCP ์„œ๋ฒ„

    โœ… ๊ตฌ์„ฑ ๊ด€๋ฆฌ: ํ™˜๊ฒฝ ๊ธฐ๋ฐ˜์˜ ๊ฒฌ๊ณ ํ•œ ๊ตฌ์„ฑ

    โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณ„์ธต: ์—ฐ๊ฒฐ ํ’€๋ง์„ ํฌํ•จํ•œ PostgreSQL ํ†ตํ•ฉ

    โœ… MCP ๋„๊ตฌ: ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๋ฐ ์ฟผ๋ฆฌ ์‹คํ–‰ ๋„๊ตฌ

    โœ… RLS ํ†ตํ•ฉ: ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ

    โœ… ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง: ํฌ๊ด„์ ์ธ ์ƒํƒœ ํ™•์ธ ์—”๋“œํฌ์ธํŠธ

    โœ… ํ…Œ์ŠคํŠธ ์ „๋žต: ๋กœ์ปฌ ํ…Œ์ŠคํŠธ ๋ฐ VS Code ํ†ตํ•ฉ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 06: ๋„๊ตฌ ๊ฐœ๋ฐœ์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • MCP ๋„๊ตฌ ์ปฌ๋ ‰์…˜ ํ™•์žฅ
  • ๊ณ ๊ธ‰ ์ฟผ๋ฆฌ ํŒจํ„ด ๊ตฌํ˜„
  • ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ๋ณ€ํ™˜ ์ถ”๊ฐ€
  • ์ „๋ฌธํ™”๋œ ๋ถ„์„ ๋„๊ตฌ ์ƒ์„ฑ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    FastMCP ํ”„๋ ˆ์ž„์›Œํฌ

  • FastMCP ๋ฌธ์„œ - ๊ณต์‹ FastMCP ๊ฐ€์ด๋“œ
  • MCP ์‚ฌ์–‘ - ํ”„๋กœํ† ์ฝœ ์‚ฌ์–‘
  • ๋„๊ตฌ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ - MCP ๋„๊ตฌ ์ƒ์„ฑ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ

  • asyncpg ๋ฌธ์„œ - PostgreSQL ๋น„๋™๊ธฐ ๋“œ๋ผ์ด๋ฒ„
  • ์—ฐ๊ฒฐ ํ’€๋ง ๋ชจ๋ฒ” ์‚ฌ๋ก€ - PostgreSQL ํŠœ๋‹
  • ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ฐ€์ด๋“œ - RLS ๊ตฌํ˜„
  • FastAPI ํŒจํ„ด

  • FastAPI ๋ฌธ์„œ - ์›น ํ”„๋ ˆ์ž„์›Œํฌ ์ฐธ์กฐ
  • ์˜์กด์„ฑ ์ฃผ์ž… - FastAPI ํŒจํ„ด
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… - ๋น„๋™๊ธฐ ์ž‘์—… ๊ด€๋ฆฌ
  • ---

    ๋‹ค์Œ: ๋„๊ตฌ๋ฅผ ํ™•์žฅํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? Lab 06: ๋„๊ตฌ ๊ฐœ๋ฐœ์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์„ธ์š”.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ FastMCP ์„œ๋ฒ„ ๊ตฌ์ถ• ๊ตฌ์ถ•ํ•˜๊ธฐ

    MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ FastMCP ํ”„๋ ˆ์ž„์›Œํฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํ”„๋กœ๋•์…˜ ์ˆ˜์ค€์˜ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๊ณผ์ •์„ ์•ˆ๋‚ดํ•ฉ๋‹ˆ๋‹ค. ํ•ต์‹ฌ ์„œ๋ฒ„ ๊ตฌ์กฐ๋ฅผ ๊ตฌ์ถ•ํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ๊ตฌํ˜„ํ•˜๋ฉฐ, ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค๋ฅผ ์œ„ํ•œ ๋„๊ตฌ๋ฅผ ๋งŒ๋“ค๊ณ , AI ๊ธฐ๋ฐ˜ ์†Œ๋งค ๋ถ„์„์„ ์œ„ํ•œ ๊ธฐ์ดˆ๋ฅผ ์„ค์ •ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ์„œ๋ฒ„๋Š” ์†Œ๋งค ๋ถ„์„ ์†”๋ฃจ์…˜์˜ ์ค‘์‹ฌ์ž…๋‹ˆ๋‹ค. ์ด ์„œ๋ฒ„๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐ„์˜ ๋‹ค๋ฆฌ ์—ญํ• ์„ ํ•˜๋ฉฐ, ํ‘œ์ค€ํ™”๋œ ํ”„๋กœํ† ์ฝœ์„ ํ†ตํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์•ˆ์ „ํ•˜๊ณ  ์ง€๋Šฅ์ ์œผ๋กœ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

    ์ด ์‹ค์Šต์—์„œ๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด๊ณผ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋ฅผ ๋”ฐ๋ฅด๋Š” ๊ฒฌ๊ณ ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ๊ตฌ์ถ•: ์ ์ ˆํ•œ ์•„ํ‚คํ…์ฒ˜์™€ ์กฐ์ง์„ ๊ฐ–์ถ˜ FastMCP ์„œ๋ฒ„
  • ๊ตฌํ˜„: ์—ฐ๊ฒฐ ํ’€๋ง ๋ฐ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ํฌํ•จํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ
  • ์ƒ์„ฑ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๋ฐ ์ฟผ๋ฆฌ ์‹คํ–‰์„ ์œ„ํ•œ MCP ๋„๊ตฌ
  • ๊ตฌ์„ฑ: ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ(RLS) ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ
  • ์ถ”๊ฐ€: ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€์ฐฐ ๊ธฐ๋Šฅ
  • ํ…Œ์ŠคํŠธ: MCP ์„œ๋ฒ„ ๊ตฌํ˜„์„ ๋กœ์ปฌ ๋ฐ VS Code์—์„œ ํ…Œ์ŠคํŠธ
  • ๐Ÿ“ ํ”„๋กœ์ ํŠธ ๊ตฌ์กฐ

    MCP ์„œ๋ฒ„์˜ ์กฐ์ง์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

    
    mcp_server/
    
    โ”œโ”€โ”€ __init__.py                 # Package initialization
    
    โ”œโ”€โ”€ config.py                   # Configuration management
    
    โ”œโ”€โ”€ health_check.py             # Health monitoring endpoints
    
    โ”œโ”€โ”€ sales_analysis.py           # Main MCP server implementation
    
    โ”œโ”€โ”€ sales_analysis_postgres.py  # Database integration layer
    
    โ””โ”€โ”€ sales_analysis_text_embeddings.py  # AI/semantic search integration
    
    

    ๐Ÿ”ง ๊ตฌ์„ฑ ๊ด€๋ฆฌ

    ํ™˜๊ฒฝ ๊ตฌ์„ฑ (config.py)

    ๋จผ์ € ๊ฒฌ๊ณ ํ•œ ๊ตฌ์„ฑ ์‹œ์Šคํ…œ์„ ๋งŒ๋“ค์–ด ๋ด…์‹œ๋‹ค:

    
    # mcp_server/config.py
    
    """
    
    Configuration management for the MCP server.
    
    Handles environment variables, validation, and defaults.
    
    """
    
    import os
    
    import logging
    
    from typing import Optional, Dict, Any
    
    from dataclasses import dataclass
    
    from dotenv import load_dotenv
    
    
    
    # Load environment variables from .env file
    
    load_dotenv()
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    @dataclass
    
    class DatabaseConfig:
    
        """Database connection configuration."""
    
        host: str
    
        port: int
    
        database: str
    
        user: str
    
        password: str
    
        min_connections: int = 2
    
        max_connections: int = 10
    
        command_timeout: int = 30
    
        
    
        @classmethod
    
        def from_env(cls) -> 'DatabaseConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                host=os.getenv('POSTGRES_HOST', 'localhost'),
    
                port=int(os.getenv('POSTGRES_PORT', '5432')),
    
                database=os.getenv('POSTGRES_DB', 'zava'),
    
                user=os.getenv('POSTGRES_USER', 'postgres'),
    
                password=os.getenv('POSTGRES_PASSWORD', ''),
    
                min_connections=int(os.getenv('POSTGRES_MIN_CONNECTIONS', '2')),
    
                max_connections=int(os.getenv('POSTGRES_MAX_CONNECTIONS', '10')),
    
                command_timeout=int(os.getenv('POSTGRES_COMMAND_TIMEOUT', '30'))
    
            )
    
        
    
        def to_asyncpg_params(self) -> Dict[str, Any]:
    
            """Convert to asyncpg connection parameters."""
    
            return {
    
                'host': self.host,
    
                'port': self.port,
    
                'database': self.database,
    
                'user': self.user,
    
                'password': self.password,
    
                'command_timeout': self.command_timeout,
    
                'server_settings': {
    
                    'application_name': 'zava-mcp-server',
    
                    'jit': 'off',  # Disable JIT for stability
    
                    'work_mem': '4MB',
    
                    'statement_timeout': f'{self.command_timeout}s'
    
                }
    
            }
    
    
    
    @dataclass
    
    class AzureConfig:
    
        """Azure AI services configuration."""
    
        project_endpoint: str
    
        openai_endpoint: str
    
        embedding_model_deployment: str
    
        client_id: str
    
        client_secret: str
    
        tenant_id: str
    
        
    
        @classmethod
    
        def from_env(cls) -> 'AzureConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                project_endpoint=os.getenv('PROJECT_ENDPOINT', ''),
    
                openai_endpoint=os.getenv('AZURE_OPENAI_ENDPOINT', ''),
    
                embedding_model_deployment=os.getenv('EMBEDDING_MODEL_DEPLOYMENT_NAME', 'text-embedding-3-small'),
    
                client_id=os.getenv('AZURE_CLIENT_ID', ''),
    
                client_secret=os.getenv('AZURE_CLIENT_SECRET', ''),
    
                tenant_id=os.getenv('AZURE_TENANT_ID', '')
    
            )
    
        
    
        def is_configured(self) -> bool:
    
            """Check if all required Azure configuration is present."""
    
            return all([
    
                self.project_endpoint,
    
                self.openai_endpoint,
    
                self.client_id,
    
                self.client_secret,
    
                self.tenant_id
    
            ])
    
    
    
    @dataclass
    
    class ServerConfig:
    
        """MCP server configuration."""
    
        host: str = '0.0.0.0'
    
        port: int = 8000
    
        log_level: str = 'INFO'
    
        enable_cors: bool = True
    
        enable_health_check: bool = True
    
        applicationinsights_connection_string: Optional[str] = None
    
        
    
        @classmethod
    
        def from_env(cls) -> 'ServerConfig':
    
            """Create configuration from environment variables."""
    
            return cls(
    
                host=os.getenv('MCP_SERVER_HOST', '0.0.0.0'),
    
                port=int(os.getenv('MCP_SERVER_PORT', '8000')),
    
                log_level=os.getenv('LOG_LEVEL', 'INFO').upper(),
    
                enable_cors=os.getenv('ENABLE_CORS', 'true').lower() == 'true',
    
                enable_health_check=os.getenv('ENABLE_HEALTH_CHECK', 'true').lower() == 'true',
    
                applicationinsights_connection_string=os.getenv('APPLICATIONINSIGHTS_CONNECTION_STRING')
    
            )
    
    
    
    class MCPServerConfig:
    
        """Main configuration class for the MCP server."""
    
        
    
        def __init__(self):
    
            self.database = DatabaseConfig.from_env()
    
            self.azure = AzureConfig.from_env()
    
            self.server = ServerConfig.from_env()
    
            
    
            # Validate configuration
    
            self._validate_config()
    
        
    
        def _validate_config(self):
    
            """Validate configuration and log warnings for missing values."""
    
            if not self.database.password:
    
                logger.warning("Database password is empty. This may cause connection issues.")
    
            
    
            if not self.azure.is_configured():
    
                logger.warning("Azure configuration is incomplete. AI features may not work.")
    
            
    
            logger.info(f"Configuration loaded - Database: {self.database.host}:{self.database.port}")
    
            logger.info(f"Server will run on {self.server.host}:{self.server.port}")
    
    
    
    # Global configuration instance
    
    config = MCPServerConfig()
    
    

    ์ฃผ์š” ๊ตฌ์„ฑ ๊ธฐ๋Šฅ

  • ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ: ์ž๋™ .env ํŒŒ์ผ ์ง€์›
  • ํƒ€์ž… ์•ˆ์ „์„ฑ: ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค ๊ฒ€์ฆ ๋ฐ ํƒ€์ž… ํžŒํŠธ
  • ์œ ์—ฐํ•œ ๊ธฐ๋ณธ๊ฐ’: ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ํ•ฉ๋ฆฌ์ ์ธ ๊ธฐ๋ณธ๊ฐ’
  • ๊ฒ€์ฆ: ์œ ์šฉํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จํ•œ ๊ตฌ์„ฑ ๊ฒ€์ฆ
  • ๋ณด์•ˆ: ํ™˜๊ฒฝ ๋ณ€์ˆ˜์—์„œ๋งŒ ๋ฏผ๊ฐํ•œ ๊ฐ’ ๋กœ๋“œ
  • ๐Ÿ—„๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ๊ณ„์ธต

    PostgreSQL ์ œ๊ณต์ž (sales_analysis_postgres.py)

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ๊ณ„์ธต์„ ๊ตฌํ˜„ํ•ด ๋ด…์‹œ๋‹ค:

    
    # mcp_server/sales_analysis_postgres.py
    
    """
    
    PostgreSQL database integration for MCP server.
    
    Handles connections, queries, and schema introspection.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import logging
    
    from typing import Dict, Any, List, Optional, Tuple
    
    from contextlib import asynccontextmanager
    
    from datetime import datetime
    
    import json
    
    
    
    from .config import config
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class PostgreSQLSchemaProvider:
    
        """Provides PostgreSQL database access and schema information."""
    
        
    
        def __init__(self):
    
            self.connection_pool: Optional[asyncpg.Pool] = None
    
            self.postgres_config = config.database.to_asyncpg_params()
    
            
    
        async def create_pool(self) -> None:
    
            """Create connection pool for database operations."""
    
            if self.connection_pool is None:
    
                try:
    
                    self.connection_pool = await asyncpg.create_pool(
    
                        **self.postgres_config,
    
                        min_size=config.database.min_connections,
    
                        max_size=config.database.max_connections,
    
                        max_inactive_connection_lifetime=300  # 5 minutes
    
                    )
    
                    logger.info("Database connection pool created successfully")
    
                except Exception as e:
    
                    logger.error(f"Failed to create database connection pool: {e}")
    
                    raise
    
        
    
        async def close_pool(self) -> None:
    
            """Close the connection pool."""
    
            if self.connection_pool:
    
                await self.connection_pool.close()
    
                self.connection_pool = None
    
                logger.info("Database connection pool closed")
    
        
    
        @asynccontextmanager
    
        async def get_connection(self):
    
            """Get a database connection from the pool."""
    
            if not self.connection_pool:
    
                await self.create_pool()
    
            
    
            async with self.connection_pool.acquire() as connection:
    
                yield connection
    
        
    
        async def set_rls_context(self, connection: asyncpg.Connection, rls_user_id: str) -> None:
    
            """Set Row Level Security context for the connection."""
    
            try:
    
                await connection.execute(
    
                    "SELECT set_config('app.current_rls_user_id', $1, false)",
    
                    rls_user_id
    
                )
    
                logger.debug(f"RLS context set for user: {rls_user_id}")
    
            except Exception as e:
    
                logger.error(f"Failed to set RLS context: {e}")
    
                raise
    
        
    
        async def get_table_schema(self, table_name: str, rls_user_id: str) -> Dict[str, Any]:
    
            """Get detailed schema information for a specific table."""
    
            async with self.get_connection() as conn:
    
                await self.set_rls_context(conn, rls_user_id)
    
                
    
                # Parse schema and table name
    
                if '.' in table_name:
    
                    schema_name, table_name = table_name.split('.', 1)
    
                else:
    
                    schema_name = 'retail'  # Default schema
    
                
    
                # Get column information
    
                columns_query = """
    
                    SELECT 
    
                        column_name,
    
                        data_type,
    
                        is_nullable,
    
                        column_default,
    
                        character_maximum_length,
    
                        numeric_precision,
    
                        numeric_scale,
    
                        ordinal_position
    
                    FROM information_schema.columns 
    
                    WHERE table_schema = $1 AND table_name = $2
    
                    ORDER BY ordinal_position
    
                """
    
                
    
                columns = await conn.fetch(columns_query, schema_name, table_name)
    
                
    
                if not columns:
    
                    raise ValueError(f"Table {schema_name}.{table_name} not found or not accessible")
    
                
    
                # Get foreign key relationships
    
                fk_query = """
    
                    SELECT 
    
                        kcu.column_name,
    
                        ccu.table_schema AS foreign_table_schema,
    
                        ccu.table_name AS foreign_table_name,
    
                        ccu.column_name AS foreign_column_name
    
                    FROM information_schema.table_constraints tc
    
                    JOIN information_schema.key_column_usage kcu 
    
                        ON tc.constraint_name = kcu.constraint_name
    
                    JOIN information_schema.constraint_column_usage ccu 
    
                        ON ccu.constraint_name = tc.constraint_name
    
                    WHERE tc.constraint_type = 'FOREIGN KEY' 
    
                        AND tc.table_schema = $1 
    
                        AND tc.table_name = $2
    
                """
    
                
    
                foreign_keys = await conn.fetch(fk_query, schema_name, table_name)
    
                
    
                # Get indexes
    
                index_query = """
    
                    SELECT 
    
                        indexname,
    
                        indexdef
    
                    FROM pg_indexes 
    
                    WHERE schemaname = $1 AND tablename = $2
    
                """
    
                
    
                indexes = await conn.fetch(index_query, schema_name, table_name)
    
                
    
                # Format schema information
    
                schema_info = {
    
                    "table_name": f"{schema_name}.{table_name}",
    
                    "columns": [
    
                        {
    
                            "name": col["column_name"],
    
                            "type": col["data_type"],
    
                            "nullable": col["is_nullable"] == "YES",
    
                            "default": col["column_default"],
    
                            "max_length": col["character_maximum_length"],
    
                            "precision": col["numeric_precision"],
    
                            "scale": col["numeric_scale"],
    
                            "position": col["ordinal_position"]
    
                        }
    
                        for col in columns
    
                    ],
    
                    "foreign_keys": [
    
                        {
    
                            "column": fk["column_name"],
    
                            "references": f"{fk['foreign_table_schema']}.{fk['foreign_table_name']}.{fk['foreign_column_name']}"
    
                        }
    
                        for fk in foreign_keys
    
                    ],
    
                    "indexes": [
    
                        {
    
                            "name": idx["indexname"],
    
                            "definition": idx["indexdef"]
    
                        }
    
                        for idx in indexes
    
                    ]
    
                }
    
                
    
                return schema_info
    
        
    
        async def get_multiple_table_schemas(
    
            self, 
    
            table_names: List[str], 
    
            rls_user_id: str
    
        ) -> str:
    
            """Get schema information for multiple tables."""
    
            schemas = []
    
            
    
            for table_name in table_names:
    
                try:
    
                    schema = await self.get_table_schema(table_name, rls_user_id)
    
                    schemas.append(self._format_schema_for_ai(schema))
    
                except Exception as e:
    
                    logger.warning(f"Failed to get schema for {table_name}: {e}")
    
                    schemas.append(f"Error retrieving schema for {table_name}: {str(e)}")
    
            
    
            return "\n\n".join(schemas)
    
        
    
        def _format_schema_for_ai(self, schema: Dict[str, Any]) -> str:
    
            """Format schema information for AI consumption."""
    
            table_name = schema["table_name"]
    
            columns = schema["columns"]
    
            foreign_keys = schema["foreign_keys"]
    
            
    
            # Create column definitions
    
            column_lines = []
    
            for col in columns:
    
                nullable = "NULL" if col["nullable"] else "NOT NULL"
    
                type_info = col["type"]
    
                
    
                if col["max_length"]:
    
                    type_info += f"({col['max_length']})"
    
                elif col["precision"] and col["scale"]:
    
                    type_info += f"({col['precision']},{col['scale']})"
    
                
    
                default_info = f" DEFAULT {col['default']}" if col["default"] else ""
    
                
    
                column_lines.append(f"  {col['name']} {type_info} {nullable}{default_info}")
    
            
    
            # Create foreign key information
    
            fk_lines = []
    
            for fk in foreign_keys:
    
                fk_lines.append(f"  {fk['column']} -> {fk['references']}")
    
            
    
            # Combine into readable format
    
            schema_text = f"Table: {table_name}\n"
    
            schema_text += "Columns:\n" + "\n".join(column_lines)
    
            
    
            if fk_lines:
    
                schema_text += "\n\nForeign Keys:\n" + "\n".join(fk_lines)
    
            
    
            return schema_text
    
        
    
        async def execute_query(
    
            self, 
    
            sql_query: str, 
    
            rls_user_id: str,
    
            max_rows: int = 20
    
        ) -> str:
    
            """Execute a SQL query with Row Level Security context."""
    
            async with self.get_connection() as conn:
    
                await self.set_rls_context(conn, rls_user_id)
    
                
    
                try:
    
                    # Set a query timeout
    
                    rows = await asyncio.wait_for(
    
                        conn.fetch(sql_query),
    
                        timeout=config.database.command_timeout
    
                    )
    
                    
    
                    if not rows:
    
                        return "Query executed successfully. No rows returned."
    
                    
    
                    # Limit result set size
    
                    limited_rows = rows[:max_rows]
    
                    
    
                    # Format results
    
                    result = self._format_query_results(limited_rows, len(rows), max_rows)
    
                    
    
                    logger.info(f"Query executed successfully. Returned {len(limited_rows)} rows.")
    
                    return result
    
                    
    
                except asyncio.TimeoutError:
    
                    error_msg = f"Query timeout after {config.database.command_timeout} seconds"
    
                    logger.error(error_msg)
    
                    raise Exception(error_msg)
    
                except Exception as e:
    
                    logger.error(f"Query execution failed: {e}")
    
                    raise
    
        
    
        def _format_query_results(
    
            self, 
    
            rows: List[asyncpg.Record], 
    
            total_rows: int,
    
            max_rows: int
    
        ) -> str:
    
            """Format query results for AI consumption."""
    
            if not rows:
    
                return "No results found."
    
            
    
            # Get column names
    
            columns = list(rows[0].keys())
    
            
    
            # Create header
    
            result_lines = [f"Results ({len(rows)} of {total_rows} rows):"]
    
            result_lines.append("=" * 50)
    
            
    
            # Add column headers
    
            header = " | ".join(columns)
    
            result_lines.append(header)
    
            result_lines.append("-" * len(header))
    
            
    
            # Add data rows
    
            for row in rows:
    
                formatted_values = []
    
                for col in columns:
    
                    value = row[col]
    
                    if value is None:
    
                        formatted_values.append("NULL")
    
                    elif isinstance(value, datetime):
    
                        formatted_values.append(value.strftime("%Y-%m-%d %H:%M:%S"))
    
                    elif isinstance(value, (dict, list)):
    
                        formatted_values.append(json.dumps(value))
    
                    else:
    
                        formatted_values.append(str(value))
    
                
    
                result_lines.append(" | ".join(formatted_values))
    
            
    
            # Add truncation notice if needed
    
            if total_rows > max_rows:
    
                result_lines.append(f"\n... and {total_rows - max_rows} more rows (truncated for display)")
    
            
    
            return "\n".join(result_lines)
    
        
    
        async def get_current_utc_date(self) -> str:
    
            """Get current UTC date/time."""
    
            async with self.get_connection() as conn:
    
                result = await conn.fetchval("SELECT NOW() AT TIME ZONE 'UTC'")
    
                return result.isoformat() + "Z"
    
        
    
        async def health_check(self) -> Dict[str, Any]:
    
            """Perform database health check."""
    
            try:
    
                async with self.get_connection() as conn:
    
                    # Simple connectivity test
    
                    result = await conn.fetchval("SELECT 1")
    
                    
    
                    # Check pool status
    
                    pool_info = {
    
                        "min_size": self.connection_pool._minsize if self.connection_pool else 0,
    
                        "max_size": self.connection_pool._maxsize if self.connection_pool else 0,
    
                        "current_size": self.connection_pool.get_size() if self.connection_pool else 0,
    
                        "idle_size": self.connection_pool.get_idle_size() if self.connection_pool else 0
    
                    }
    
                    
    
                    return {
    
                        "status": "healthy",
    
                        "database_responsive": result == 1,
    
                        "pool_info": pool_info
    
                    }
    
                    
    
            except Exception as e:
    
                return {
    
                    "status": "unhealthy",
    
                    "error": str(e)
    
                }
    
    
    
    # Global database provider instance
    
    db_provider = PostgreSQLSchemaProvider()
    
    

    ์ฃผ์š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณ„์ธต ๊ธฐ๋Šฅ

  • ์—ฐ๊ฒฐ ํ’€๋ง: asyncpg๋ฅผ ์‚ฌ์šฉํ•œ ํšจ์œจ์ ์ธ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ
  • RLS ํ†ตํ•ฉ: ์ž๋™ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ ์„ค์ •
  • ์Šคํ‚ค๋งˆ ํƒ์ƒ‰: ๋™์  ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ๊ฒ€์ƒ‰
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ๊ด€๋ฆฌ ๋ฐ ๋กœ๊น…
  • ์ฟผ๋ฆฌ ํฌ๋งทํŒ…: AI ์นœํ™”์ ์ธ ๊ฒฐ๊ณผ ํฌ๋งทํŒ…
  • ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ๋ฐ ํ’€ ์ƒํƒœ ํ™•์ธ
  • ๐Ÿ”ง ์ฃผ์š” MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    FastMCP ์„œ๋ฒ„ (sales_analysis.py)

    ์ด์ œ ์ฃผ์š” MCP ์„œ๋ฒ„๋ฅผ ๊ตฌํ˜„ํ•ด ๋ด…์‹œ๋‹ค:

    
    # mcp_server/sales_analysis.py
    
    """
    
    Main MCP server implementation for Zava Retail Sales Analysis.
    
    Provides AI assistants with secure access to retail database.
    
    """
    
    import logging
    
    import asyncio
    
    from typing import Dict, Any, List, Annotated
    
    from contextlib import asynccontextmanager
    
    
    
    from fastmcp import FastMCP, Context
    
    from pydantic import Field
    
    
    
    from .config import config
    
    from .sales_analysis_postgres import db_provider
    
    from .health_check import setup_health_endpoints
    
    
    
    # Configure logging
    
    logging.basicConfig(
    
        level=getattr(logging, config.server.log_level),
    
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    
    )
    
    logger = logging.getLogger(__name__)
    
    
    
    # Create FastMCP server instance
    
    mcp = FastMCP("Zava Retail Sales Analysis")
    
    
    
    # List of valid tables for schema access
    
    VALID_TABLES = [
    
        "retail.stores",
    
        "retail.customers", 
    
        "retail.categories",
    
        "retail.product_types",
    
        "retail.products",
    
        "retail.orders",
    
        "retail.order_items",
    
        "retail.inventory"
    
    ]
    
    
    
    def get_rls_user_id(ctx: Context) -> str:
    
        """Extract Row Level Security User ID from request context."""
    
        # In HTTP mode, get from headers
    
        if hasattr(ctx, 'headers') and ctx.headers:
    
            rls_user_id = ctx.headers.get("x-rls-user-id")
    
            if rls_user_id:
    
                logger.debug(f"RLS User ID from headers: {rls_user_id}")
    
                return rls_user_id
    
        
    
        # Default fallback for development/testing
    
        default_id = "00000000-0000-0000-0000-000000000000"
    
        logger.warning(f"No RLS User ID found, using default: {default_id}")
    
        return default_id
    
    
    
    @mcp.tool()
    
    async def get_multiple_table_schemas(
    
        ctx: Context,
    
        table_names: Annotated[List[str], Field(description="List of table names to retrieve schemas for. Valid tables: " + ", ".join(VALID_TABLES))]
    
    ) -> str:
    
        """
    
        Retrieve database schemas for multiple tables in a single request.
    
        
    
        This tool provides comprehensive schema information including:
    
        - Column names, types, and constraints
    
        - Foreign key relationships
    
        - Index information
    
        - Table structure for AI query planning
    
        
    
        Args:
    
            table_names: List of valid table names from the retail schema
    
            
    
        Returns:
    
            Formatted schema information for all requested tables
    
        """
    
        rls_user_id = get_rls_user_id(ctx)
    
        
    
        # Validate table names
    
        invalid_tables = [table for table in table_names if table not in VALID_TABLES]
    
        if invalid_tables:
    
            logger.warning(f"Invalid table names requested: {invalid_tables}")
    
            return f"Error: Invalid table names: {', '.join(invalid_tables)}. Valid tables are: {', '.join(VALID_TABLES)}"
    
        
    
        try:
    
            logger.info(f"Retrieving schemas for tables: {table_names} (User: {rls_user_id})")
    
            result = await db_provider.get_multiple_table_schemas(table_names, rls_user_id)
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error retrieving table schemas: {e}")
    
            return f"Error retrieving table schemas: {e!s}"
    
    
    
    @mcp.tool()
    
    async def execute_sales_query(
    
        ctx: Context,
    
        postgresql_query: Annotated[str, Field(description="A well-formed PostgreSQL query to execute against the retail database. Always get table schemas first before writing queries.")]
    
    ) -> str:
    
        """
    
        Execute PostgreSQL queries against the retail sales database with Row Level Security.
    
        
    
        This tool allows AI assistants to run analytical queries on retail data including:
    
        - Sales performance analysis
    
        - Customer behavior insights  
    
        - Inventory management queries
    
        - Product performance metrics
    
        - Store-specific reporting
    
        
    
        Important: Row Level Security ensures users only see data they're authorized to access.
    
        
    
        Args:
    
            postgresql_query: SQL query to execute (automatically filtered by RLS)
    
            
    
        Returns:
    
            Query results formatted for AI analysis (limited to 20 rows for readability)
    
        """
    
        rls_user_id = get_rls_user_id(ctx)
    
        
    
        try:
    
            logger.info(f"Executing query for user: {rls_user_id}")
    
            logger.debug(f"Query: {postgresql_query[:100]}...")
    
            
    
            result = await db_provider.execute_query(postgresql_query, rls_user_id)
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error executing database query: {e}")
    
            return f"Error executing database query: {e!s}"
    
    
    
    @mcp.tool()
    
    async def get_current_utc_date(ctx: Context) -> str:
    
        """
    
        Get the current UTC date and time in ISO format.
    
        
    
        Useful for time-sensitive queries and date-based analysis.
    
        
    
        Returns:
    
            Current UTC date/time in ISO format (YYYY-MM-DDTHH:MM:SS.fffffZ)
    
        """
    
        try:
    
            result = await db_provider.get_current_utc_date()
    
            logger.debug(f"Current UTC date retrieved: {result}")
    
            return result
    
        except Exception as e:
    
            logger.error(f"Error getting current UTC date: {e}")
    
            return f"Error getting current UTC date: {e!s}"
    
    
    
    # Application lifecycle management
    
    @asynccontextmanager
    
    async def lifespan(app):
    
        """Manage application startup and shutdown."""
    
        logger.info("Starting Zava Retail MCP Server...")
    
        
    
        try:
    
            # Initialize database connection pool
    
            await db_provider.create_pool()
    
            logger.info("Database connection pool initialized")
    
            
    
            # Test database connectivity
    
            health_status = await db_provider.health_check()
    
            if health_status["status"] != "healthy":
    
                logger.error(f"Database health check failed: {health_status}")
    
                raise Exception("Database not healthy")
    
            
    
            logger.info("MCP Server startup complete")
    
            yield
    
            
    
        except Exception as e:
    
            logger.error(f"Startup failed: {e}")
    
            raise
    
        finally:
    
            # Cleanup
    
            logger.info("Shutting down MCP Server...")
    
            await db_provider.close_pool()
    
            logger.info("MCP Server shutdown complete")
    
    
    
    # Configure server application
    
    def create_app():
    
        """Create and configure the MCP server application."""
    
        
    
        # Get the FastMCP app instance
    
        app = mcp.sse_app()
    
        
    
        # Set up lifecycle management
    
        app.router.lifespan_context = lifespan
    
        
    
        # Add health check endpoints if enabled
    
        if config.server.enable_health_check:
    
            setup_health_endpoints(app, db_provider)
    
        
    
        # Configure CORS if enabled
    
        if config.server.enable_cors:
    
            from fastapi.middleware.cors import CORSMiddleware
    
            app.add_middleware(
    
                CORSMiddleware,
    
                allow_origins=["*"],  # Configure appropriately for production
    
                allow_credentials=True,
    
                allow_methods=["*"],
    
                allow_headers=["*"],
    
            )
    
        
    
        logger.info(f"MCP Server configured - CORS: {config.server.enable_cors}, Health: {config.server.enable_health_check}")
    
        
    
        return app
    
    
    
    # Create the application instance
    
    app = create_app()
    
    
    
    # Main entry point for development
    
    if __name__ == "__main__":
    
        import uvicorn
    
        
    
        logger.info(f"Starting development server on {config.server.host}:{config.server.port}")
    
        
    
        uvicorn.run(
    
            "sales_analysis:app",
    
            host=config.server.host,
    
            port=config.server.port,
    
            reload=True,
    
            log_level=config.server.log_level.lower()
    
        )
    
    

    ์ฃผ์š” MCP ์„œ๋ฒ„ ๊ธฐ๋Šฅ

  • ๋„๊ตฌ ๋“ฑ๋ก: ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ฐ–์ถ˜ ์„ ์–ธ์  ๋„๊ตฌ ์ •์˜
  • RLS ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ: ์‚ฌ์šฉ์ž ์‹ ์› ์ถ”์ถœ ๋ฐ ์ปจํ…์ŠคํŠธ ์„ค์ • ์ž๋™ํ™”
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ๋ฉ”์‹œ์ง€๋ฅผ ํฌํ•จํ•œ ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ๊ด€๋ฆฌ
  • ๋ผ์ดํ”„์‚ฌ์ดํด ๊ด€๋ฆฌ: ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ๋ฅผ ํฌํ•จํ•œ ์ ์ ˆํ•œ ์‹œ์ž‘/์ข…๋ฃŒ
  • ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง: ๋‚ด์žฅ ์ƒํƒœ ํ™•์ธ ์—”๋“œํฌ์ธํŠธ
  • ๊ฐœ๋ฐœ ์ง€์›: ํ•ซ ๋ฆฌ๋กœ๋“œ ๋ฐ ๋””๋ฒ„๊น… ๊ธฐ๋Šฅ
  • ๐Ÿฅ ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง

    ์ƒํƒœ ํ™•์ธ ๊ตฌํ˜„ (health_check.py)

    
    # mcp_server/health_check.py
    
    """
    
    Health check endpoints for monitoring MCP server status.
    
    """
    
    import logging
    
    from typing import Dict, Any
    
    from fastapi import FastAPI, HTTPException
    
    from fastapi.responses import JSONResponse
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    def setup_health_endpoints(app: FastAPI, db_provider) -> None:
    
        """Add health check endpoints to the FastAPI application."""
    
        
    
        @app.get("/health")
    
        async def health_check() -> JSONResponse:
    
            """Basic health check endpoint."""
    
            return JSONResponse(
    
                status_code=200,
    
                content={
    
                    "status": "healthy",
    
                    "service": "zava-retail-mcp-server",
    
                    "timestamp": await db_provider.get_current_utc_date()
    
                }
    
            )
    
        
    
        @app.get("/health/detailed")
    
        async def detailed_health_check() -> JSONResponse:
    
            """Detailed health check including database connectivity."""
    
            health_status = {
    
                "service": "zava-retail-mcp-server",
    
                "status": "healthy",
    
                "components": {}
    
            }
    
            
    
            overall_healthy = True
    
            
    
            # Check database
    
            try:
    
                db_health = await db_provider.health_check()
    
                health_status["components"]["database"] = db_health
    
                
    
                if db_health["status"] != "healthy":
    
                    overall_healthy = False
    
                    
    
            except Exception as e:
    
                health_status["components"]["database"] = {
    
                    "status": "unhealthy",
    
                    "error": str(e)
    
                }
    
                overall_healthy = False
    
            
    
            # Update overall status
    
            if not overall_healthy:
    
                health_status["status"] = "unhealthy"
    
            
    
            status_code = 200 if overall_healthy else 503
    
            
    
            return JSONResponse(
    
                status_code=status_code,
    
                content=health_status
    
            )
    
        
    
        @app.get("/health/ready")
    
        async def readiness_check() -> JSONResponse:
    
            """Kubernetes readiness probe endpoint."""
    
            try:
    
                # Test critical functionality
    
                db_health = await db_provider.health_check()
    
                
    
                if db_health["status"] != "healthy":
    
                    raise HTTPException(status_code=503, detail="Database not ready")
    
                
    
                return JSONResponse(
    
                    status_code=200,
    
                    content={"status": "ready"}
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Readiness check failed: {e}")
    
                raise HTTPException(status_code=503, detail="Service not ready")
    
        
    
        @app.get("/health/live")
    
        async def liveness_check() -> JSONResponse:
    
            """Kubernetes liveness probe endpoint."""
    
            return JSONResponse(
    
                status_code=200,
    
                content={"status": "alive"}
    
            )
    
        
    
        logger.info("Health check endpoints configured")
    
    

    ๐Ÿงช MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ

    ๋กœ์ปฌ ํ…Œ์ŠคํŠธ

    1. MCP ์„œ๋ฒ„ ์‹œ์ž‘:

    ```bash

    # Activate virtual environment

    source mcp-env/bin/activate # macOS/Linux

    # mcp-env\Scripts\activate # Windows

    # Start server

    cd mcp_server

    python sales_analysis.py

    ```

    2. ์ƒํƒœ ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ:

    ```bash

    # Basic health check

    curl http://localhost:8000/health

    # Detailed health check

    curl http://localhost:8000/health/detailed

    ```

    3. MCP ๋„๊ตฌ ํ…Œ์ŠคํŠธ:

    ```bash

    # List available tools

    curl -X POST http://localhost:8000/mcp \

    -H "Content-Type: application/json" \

    -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \

    -d '{"method": "tools/list", "params": {}}'

    # Get table schemas

    curl -X POST http://localhost:8000/mcp \

    -H "Content-Type: application/json" \

    -H "x-rls-user-id: 00000000-0000-0000-0000-000000000000" \

    -d '{

    "method": "tools/call",

    "params": {

    "name": "get_multiple_table_schemas",

    "arguments": {

    "table_names": ["retail.stores", "retail.products"]

    }

    }

    }'

    ```

    VS Code ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    1. VS Code MCP ๊ตฌ์„ฑ:

    ```json

    // .vscode/mcp.json

    {

    "servers": {

    "zava-retail-test": {

    "url": "http://127.0.0.1:8000/mcp",

    "type": "http",

    "headers": {"x-rls-user-id": "00000000-0000-0000-0000-000000000000"}

    }

    }

    }

    ```

    2. AI ์ฑ„ํŒ…์—์„œ ํ…Œ์ŠคํŠธ:

    - VS Code AI ์ฑ„ํŒ… ์—ด๊ธฐ

    - #zava๋ฅผ ์ž…๋ ฅํ•˜๊ณ  ์„œ๋ฒ„ ์„ ํƒ

    - ์งˆ๋ฌธ: "์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ…Œ์ด๋ธ”์€ ๋ฌด์—‡์ธ๊ฐ€์š”?"

    - ์งˆ๋ฌธ: "์ฃผ๋ฌธ ์ˆ˜ ๊ธฐ์ค€ ์ƒ์œ„ 5๊ฐœ ๋งค์žฅ์„ ๋ณด์—ฌ์ฃผ์„ธ์š”."

    ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

    ํฌ๊ด„์ ์ธ ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๋ฅผ ์ž‘์„ฑํ•˜์„ธ์š”:

    
    # tests/test_mcp_server.py
    
    import pytest
    
    import asyncio
    
    from mcp_server.sales_analysis_postgres import PostgreSQLSchemaProvider
    
    from mcp_server.config import config
    
    
    
    @pytest.mark.asyncio
    
    async def test_database_connection():
    
        """Test database connectivity."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            health = await db.health_check()
    
            assert health["status"] == "healthy"
    
        finally:
    
            await db.close_pool()
    
    
    
    @pytest.mark.asyncio
    
    async def test_table_schema_retrieval():
    
        """Test table schema retrieval."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            schema = await db.get_table_schema("retail.stores", "00000000-0000-0000-0000-000000000000")
    
            
    
            assert schema["table_name"] == "retail.stores"
    
            assert len(schema["columns"]) > 0
    
            
    
        finally:
    
            await db.close_pool()
    
    
    
    @pytest.mark.asyncio
    
    async def test_query_execution():
    
        """Test query execution with RLS."""
    
        db = PostgreSQLSchemaProvider()
    
        
    
        try:
    
            await db.create_pool()
    
            result = await db.execute_query(
    
                "SELECT COUNT(*) as store_count FROM retail.stores",
    
                "00000000-0000-0000-0000-000000000000"
    
            )
    
            
    
            assert "store_count" in result
    
            
    
        finally:
    
            await db.close_pool()
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ์ž‘๋™ํ•˜๋Š” MCP ์„œ๋ฒ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ๊ฐ–์ถ˜ FastMCP ์„œ๋ฒ„

    โœ… ๊ตฌ์„ฑ ๊ด€๋ฆฌ: ํ™˜๊ฒฝ ๊ธฐ๋ฐ˜์˜ ๊ฒฌ๊ณ ํ•œ ๊ตฌ์„ฑ

    โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณ„์ธต: ์—ฐ๊ฒฐ ํ’€๋ง์„ ํฌํ•จํ•œ PostgreSQL ํ†ตํ•ฉ

    โœ… MCP ๋„๊ตฌ: ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๋ฐ ์ฟผ๋ฆฌ ์‹คํ–‰ ๋„๊ตฌ

    โœ… RLS ํ†ตํ•ฉ: ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ์ปจํ…์ŠคํŠธ ๊ด€๋ฆฌ

    โœ… ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง: ํฌ๊ด„์ ์ธ ์ƒํƒœ ํ™•์ธ ์—”๋“œํฌ์ธํŠธ

    โœ… ํ…Œ์ŠคํŠธ ์ „๋žต: ๋กœ์ปฌ ํ…Œ์ŠคํŠธ ๋ฐ VS Code ํ†ตํ•ฉ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 06: ๋„๊ตฌ ๊ฐœ๋ฐœ์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • MCP ๋„๊ตฌ ์ปฌ๋ ‰์…˜ ํ™•์žฅ
  • ๊ณ ๊ธ‰ ์ฟผ๋ฆฌ ํŒจํ„ด ๊ตฌํ˜„
  • ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ๋ณ€ํ™˜ ์ถ”๊ฐ€
  • ์ „๋ฌธํ™”๋œ ๋ถ„์„ ๋„๊ตฌ ์ƒ์„ฑ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    FastMCP ํ”„๋ ˆ์ž„์›Œํฌ

  • FastMCP ๋ฌธ์„œ - ๊ณต์‹ FastMCP ๊ฐ€์ด๋“œ
  • MCP ์‚ฌ์–‘ - ํ”„๋กœํ† ์ฝœ ์‚ฌ์–‘
  • ๋„๊ตฌ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ - MCP ๋„๊ตฌ ์ƒ์„ฑ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ

  • asyncpg ๋ฌธ์„œ - PostgreSQL ๋น„๋™๊ธฐ ๋“œ๋ผ์ด๋ฒ„
  • ์—ฐ๊ฒฐ ํ’€๋ง ๋ชจ๋ฒ” ์‚ฌ๋ก€ - PostgreSQL ํŠœ๋‹
  • ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ ๊ฐ€์ด๋“œ - RLS ๊ตฌํ˜„
  • FastAPI ํŒจํ„ด

  • FastAPI ๋ฌธ์„œ - ์›น ํ”„๋ ˆ์ž„์›Œํฌ ์ฐธ์กฐ
  • ์˜์กด์„ฑ ์ฃผ์ž… - FastAPI ํŒจํ„ด
  • ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—… - ๋น„๋™๊ธฐ ์ž‘์—… ๊ด€๋ฆฌ
  • ---

    ๋‹ค์Œ: ๋„๊ตฌ๋ฅผ ํ™•์žฅํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? Lab 06: ๋„๊ตฌ ๊ฐœ๋ฐœ์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์„ธ์š”.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    06 ๋„๊ตฌ ๊ฐœ๋ฐœ

    ๋„๊ตฌ ๊ฐœ๋ฐœ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์—์„œ๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์—๊ฒŒ ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ, ์Šคํ‚ค๋งˆ ํƒ์ƒ‰, ๋ถ„์„ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๊ณ ๊ธ‰ MCP ๋„๊ตฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ๊นŠ์ด ํƒ๊ตฌํ•ฉ๋‹ˆ๋‹ค. ๊ฐ•๋ ฅํ•˜๋ฉด์„œ๋„ ์•ˆ์ „ํ•œ ๋„๊ตฌ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ , ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์™€ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ๋„๊ตฌ๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ ๋ฐ์ดํ„ฐ ์‹œ์Šคํ…œ ๊ฐ„์˜ ์ธํ„ฐํŽ˜์ด์Šค ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ์ž˜ ์„ค๊ณ„๋œ ๋„๊ตฌ๋Š” ๋ณต์žกํ•œ ์ž‘์—…์— ๋Œ€ํ•ด ๊ตฌ์กฐํ™”๋˜๊ณ  ๊ฒ€์ฆ๋œ ์ ‘๊ทผ์„ ์ œ๊ณตํ•˜๋ฉฐ, ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” ์„ค๊ณ„๋ถ€ํ„ฐ ๋ฐฐํฌ๊นŒ์ง€ ๋„๊ตฌ ๊ฐœ๋ฐœ์˜ ์ „์ฒด ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    ์šฐ๋ฆฌ์˜ ์†Œ๋งค MCP ์„œ๋ฒ„๋Š” ํŒ๋งค ๋ฐ์ดํ„ฐ, ์ œํ’ˆ ์นดํƒˆ๋กœ๊ทธ, ๋น„์ฆˆ๋‹ˆ์Šค ๋ถ„์„์„ ์ž์—ฐ์–ด๋กœ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํฌ๊ด„์ ์ธ ๋„๊ตฌ ์„ธํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉฐ, ์—„๊ฒฉํ•œ ๋ณด์•ˆ ๊ฒฝ๊ณ„์™€ ์ตœ์ ์˜ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์„ค๊ณ„: ๋ณต์žกํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ์„ ํฌํ•จํ•œ ๊ณ ๊ธ‰ MCP ๋„๊ตฌ ์„ค๊ณ„
  • ๊ตฌํ˜„: SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๋„๊ตฌ ๊ตฌํ˜„
  • ์ƒ์„ฑ: ๋™์  ์ฟผ๋ฆฌ๋ฅผ ์œ„ํ•œ ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ
  • ๊ตฌ์ถ•: ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค๋ฅผ ์œ„ํ•œ ๋งž์ถคํ˜• ๋ถ„์„ ๋„๊ตฌ ์ œ์ž‘
  • ์ ์šฉ: ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ์šฐ์•„ํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ ๊ตฌํ˜„
  • ์ตœ์ ํ™”: ํ”„๋กœ๋•์…˜ ์›Œํฌ๋กœ๋“œ๋ฅผ ์œ„ํ•œ ๋„๊ตฌ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๐Ÿ› ๏ธ ํ•ต์‹ฌ ๋„๊ตฌ ์•„ํ‚คํ…์ฒ˜

    ๋„๊ตฌ ์„ค๊ณ„ ์›์น™

    
    # mcp_server/tools/base.py
    
    """
    
    Base classes and patterns for MCP tool development.
    
    """
    
    from abc import ABC, abstractmethod
    
    from typing import Any, Dict, List, Optional, Union
    
    from dataclasses import dataclass
    
    from enum import Enum
    
    import asyncio
    
    import time
    
    import logging
    
    from contextlib import asynccontextmanager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ToolCategory(Enum):
    
        """Tool categorization for organization and discovery."""
    
        DATABASE_QUERY = "database_query"
    
        SCHEMA_INTROSPECTION = "schema_introspection"
    
        ANALYTICS = "analytics"
    
        UTILITY = "utility"
    
        ADMINISTRATIVE = "administrative"
    
    
    
    @dataclass
    
    class ToolResult:
    
        """Standardized tool result structure."""
    
        success: bool
    
        data: Any = None
    
        error: Optional[str] = None
    
        metadata: Optional[Dict[str, Any]] = None
    
        execution_time_ms: Optional[float] = None
    
        row_count: Optional[int] = None
    
    
    
    class BaseTool(ABC):
    
        """Abstract base class for all MCP tools."""
    
        
    
        def __init__(self, name: str, description: str, category: ToolCategory):
    
            self.name = name
    
            self.description = description
    
            self.category = category
    
            self.call_count = 0
    
            self.total_execution_time = 0.0
    
            
    
        @abstractmethod
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute the tool with given parameters."""
    
            pass
    
        
    
        @abstractmethod
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get JSON schema for tool input validation."""
    
            pass
    
        
    
        async def call(self, **kwargs) -> ToolResult:
    
            """Wrapper for tool execution with metrics and error handling."""
    
            
    
            start_time = time.time()
    
            self.call_count += 1
    
            
    
            try:
    
                # Validate input parameters
    
                self._validate_input(kwargs)
    
                
    
                # Log tool execution
    
                logger.info(
    
                    f"Executing tool: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'tool_category': self.category.value,
    
                        'parameters': self._sanitize_parameters(kwargs)
    
                    }
    
                )
    
                
    
                # Execute the tool
    
                result = await self.execute(**kwargs)
    
                
    
                # Record execution time
    
                execution_time = (time.time() - start_time) * 1000
    
                result.execution_time_ms = execution_time
    
                self.total_execution_time += execution_time
    
                
    
                # Log success
    
                logger.info(
    
                    f"Tool execution completed: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'execution_time_ms': execution_time,
    
                        'success': result.success,
    
                        'row_count': result.row_count
    
                    }
    
                )
    
                
    
                return result
    
                
    
            except Exception as e:
    
                execution_time = (time.time() - start_time) * 1000
    
                
    
                logger.error(
    
                    f"Tool execution failed: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'execution_time_ms': execution_time,
    
                        'error': str(e)
    
                    },
    
                    exc_info=True
    
                )
    
                
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Tool execution failed: {str(e)}",
    
                    execution_time_ms=execution_time
    
                )
    
        
    
        def _validate_input(self, kwargs: Dict[str, Any]):
    
            """Validate input parameters against schema."""
    
            
    
            schema = self.get_input_schema()
    
            required_props = schema.get('required', [])
    
            properties = schema.get('properties', {})
    
            
    
            # Check required parameters
    
            missing_required = [prop for prop in required_props if prop not in kwargs]
    
            if missing_required:
    
                raise ValueError(f"Missing required parameters: {missing_required}")
    
            
    
            # Type validation would go here
    
            # For production, use jsonschema library for comprehensive validation
    
        
    
        def _sanitize_parameters(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
    
            """Sanitize parameters for logging (remove sensitive data)."""
    
            
    
            # Remove or mask sensitive parameters
    
            sanitized = kwargs.copy()
    
            sensitive_keys = ['password', 'token', 'secret', 'key']
    
            
    
            for key in sanitized:
    
                if any(sensitive in key.lower() for sensitive in sensitive_keys):
    
                    sanitized[key] = "***MASKED***"
    
            
    
            return sanitized
    
        
    
        def get_statistics(self) -> Dict[str, Any]:
    
            """Get tool usage statistics."""
    
            
    
            return {
    
                'name': self.name,
    
                'category': self.category.value,
    
                'call_count': self.call_count,
    
                'total_execution_time_ms': self.total_execution_time,
    
                'average_execution_time_ms': (
    
                    self.total_execution_time / self.call_count 
    
                    if self.call_count > 0 else 0
    
                )
    
            }
    
    
    
    class DatabaseTool(BaseTool):
    
        """Base class for database-related tools."""
    
        
    
        def __init__(self, name: str, description: str, db_provider):
    
            super().__init__(name, description, ToolCategory.DATABASE_QUERY)
    
            self.db_provider = db_provider
    
        
    
        @asynccontextmanager
    
        async def get_connection(self):
    
            """Get database connection with proper context management."""
    
            
    
            conn = None
    
            try:
    
                conn = await self.db_provider.get_connection()
    
                yield conn
    
            finally:
    
                if conn:
    
                    await self.db_provider.release_connection(conn)
    
        
    
        async def execute_query(
    
            self, 
    
            query: str, 
    
            params: tuple = None,
    
            store_id: str = None
    
        ) -> ToolResult:
    
            """Execute database query with security and performance monitoring."""
    
            
    
            async with self.get_connection() as conn:
    
                try:
    
                    # Set store context if provided
    
                    if store_id:
    
                        await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Execute query
    
                    start_time = time.time()
    
                    
    
                    if params:
    
                        rows = await conn.fetch(query, *params)
    
                    else:
    
                        rows = await conn.fetch(query)
    
                    
    
                    execution_time = (time.time() - start_time) * 1000
    
                    
    
                    # Convert rows to dictionaries
    
                    data = [dict(row) for row in rows]
    
                    
    
                    return ToolResult(
    
                        success=True,
    
                        data=data,
    
                        row_count=len(data),
    
                        execution_time_ms=execution_time
    
                    )
    
                    
    
                except Exception as e:
    
                    logger.error(f"Database query failed: {str(e)}")
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Query execution failed: {str(e)}"
    
                    )
    
    

    ์ฟผ๋ฆฌ ๊ฒ€์ฆ ๋ฐ ๋ณด์•ˆ

    
    # mcp_server/tools/query_validator.py
    
    """
    
    SQL query validation and security for MCP tools.
    
    """
    
    import re
    
    import sqlparse
    
    from typing import List, Dict, Any, Set
    
    from enum import Enum
    
    
    
    class QueryRisk(Enum):
    
        """Query risk levels."""
    
        LOW = "low"
    
        MEDIUM = "medium"
    
        HIGH = "high"
    
        CRITICAL = "critical"
    
    
    
    class QueryValidator:
    
        """Validate and analyze SQL queries for security risks."""
    
        
    
        # Dangerous SQL keywords and patterns
    
        DANGEROUS_KEYWORDS = {
    
            'DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE', 'INSERT',
    
            'UPDATE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE', 'sp_',
    
            'xp_', 'BULK', 'OPENROWSET', 'OPENDATASOURCE'
    
        }
    
        
    
        # Allowed read-only operations
    
        SAFE_KEYWORDS = {
    
            'SELECT', 'WITH', 'UNION', 'ORDER', 'GROUP', 'HAVING',
    
            'WHERE', 'FROM', 'JOIN', 'AS', 'ON', 'IN', 'EXISTS',
    
            'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'AND', 'OR', 'NOT'
    
        }
    
        
    
        # Allowed schemas and tables
    
        ALLOWED_SCHEMAS = {'retail', 'information_schema', 'pg_catalog'}
    
        ALLOWED_TABLES = {
    
            'customers', 'products', 'sales_transactions', 
    
            'sales_transaction_items', 'product_categories',
    
            'product_embeddings', 'stores'
    
        }
    
        
    
        def __init__(self):
    
            self.injection_patterns = [
    
                # SQL injection patterns
    
                r"(\b(UNION|union)\s+(ALL\s+)?(SELECT|select))",
    
                r"(\b(DROP|drop)\s+(TABLE|table|DATABASE|database))",
    
                r"(\b(DELETE|delete)\s+(FROM|from))",
    
                r"(\b(INSERT|insert)\s+(INTO|into))",
    
                r"(\b(UPDATE|update)\s+\w+\s+(SET|set))",
    
                r"(\b(EXEC|exec|EXECUTE|execute)\s*\()",
    
                r"(\b(sp_|xp_)\w+)",
    
                r"(--\s*$)",  # SQL comments
    
                r"(/\*.*?\*/)",  # Block comments
    
                r"(;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER))",
    
                r"(\bOR\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)",  # OR injection
    
                r"(\bAND\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)",  # AND injection
    
            ]
    
            
    
            self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.injection_patterns]
    
        
    
        def validate_query(self, query: str) -> Dict[str, Any]:
    
            """Comprehensive query validation."""
    
            
    
            validation_result = {
    
                'is_safe': True,
    
                'risk_level': QueryRisk.LOW,
    
                'issues': [],
    
                'warnings': [],
    
                'allowed_operations': [],
    
                'metadata': {}
    
            }
    
            
    
            try:
    
                # Parse the query
    
                parsed = sqlparse.parse(query)
    
                
    
                if not parsed:
    
                    validation_result['is_safe'] = False
    
                    validation_result['issues'].append("Unable to parse query")
    
                    validation_result['risk_level'] = QueryRisk.HIGH
    
                    return validation_result
    
                
    
                # Analyze each statement
    
                for statement in parsed:
    
                    self._analyze_statement(statement, validation_result)
    
                
    
                # Check for injection patterns
    
                self._check_injection_patterns(query, validation_result)
    
                
    
                # Validate table/schema access
    
                self._validate_table_access(query, validation_result)
    
                
    
                # Determine final risk level
    
                self._determine_risk_level(validation_result)
    
                
    
            except Exception as e:
    
                validation_result['is_safe'] = False
    
                validation_result['issues'].append(f"Query analysis failed: {str(e)}")
    
                validation_result['risk_level'] = QueryRisk.CRITICAL
    
            
    
            return validation_result
    
        
    
        def _analyze_statement(self, statement, validation_result):
    
            """Analyze individual SQL statement."""
    
            
    
            # Get statement type
    
            stmt_type = statement.get_type()
    
            
    
            # Check if statement type is allowed
    
            if stmt_type and stmt_type.upper() not in ['SELECT', 'WITH']:
    
                validation_result['issues'].append(f"Disallowed statement type: {stmt_type}")
    
                validation_result['is_safe'] = False
    
                return
    
            
    
            # Extract tokens and analyze
    
            for token in statement.flatten():
    
                if token.ttype is sqlparse.tokens.Keyword:
    
                    keyword = token.value.upper()
    
                    
    
                    if keyword in self.DANGEROUS_KEYWORDS:
    
                        validation_result['issues'].append(f"Dangerous keyword detected: {keyword}")
    
                        validation_result['is_safe'] = False
    
                    elif keyword in self.SAFE_KEYWORDS:
    
                        if keyword not in validation_result['allowed_operations']:
    
                            validation_result['allowed_operations'].append(keyword)
    
        
    
        def _check_injection_patterns(self, query: str, validation_result):
    
            """Check for SQL injection patterns."""
    
            
    
            for pattern in self.compiled_patterns:
    
                matches = pattern.findall(query)
    
                if matches:
    
                    validation_result['issues'].append(f"Potential injection pattern detected")
    
                    validation_result['is_safe'] = False
    
        
    
        def _validate_table_access(self, query: str, validation_result):
    
            """Validate that only allowed tables/schemas are accessed."""
    
            
    
            # Extract table names (simplified approach)
    
            # In production, use proper SQL parsing
    
            from_match = re.findall(r'FROM\s+(\w+\.?\w*)', query, re.IGNORECASE)
    
            join_match = re.findall(r'JOIN\s+(\w+\.?\w*)', query, re.IGNORECASE)
    
            
    
            all_tables = from_match + join_match
    
            
    
            for table_ref in all_tables:
    
                if '.' in table_ref:
    
                    schema, table = table_ref.split('.', 1)
    
                    if schema.lower() not in self.ALLOWED_SCHEMAS:
    
                        validation_result['issues'].append(f"Access to unauthorized schema: {schema}")
    
                        validation_result['is_safe'] = False
    
                    if table.lower() not in self.ALLOWED_TABLES:
    
                        validation_result['warnings'].append(f"Access to table: {table}")
    
                else:
    
                    # Assume retail schema if not specified
    
                    if table_ref.lower() not in self.ALLOWED_TABLES:
    
                        validation_result['warnings'].append(f"Access to table: {table_ref}")
    
        
    
        def _determine_risk_level(self, validation_result):
    
            """Determine overall risk level."""
    
            
    
            if not validation_result['is_safe']:
    
                if any('injection' in issue.lower() for issue in validation_result['issues']):
    
                    validation_result['risk_level'] = QueryRisk.CRITICAL
    
                elif any('DROP' in issue or 'DELETE' in issue for issue in validation_result['issues']):
    
                    validation_result['risk_level'] = QueryRisk.HIGH
    
                else:
    
                    validation_result['risk_level'] = QueryRisk.MEDIUM
    
            elif validation_result['warnings']:
    
                validation_result['risk_level'] = QueryRisk.LOW
    
            else:
    
                validation_result['risk_level'] = QueryRisk.LOW
    
    
    
    # Global validator instance
    
    query_validator = QueryValidator()
    
    

    ๐Ÿ—ƒ๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๋„๊ตฌ

    ํŒ๋งค ๋ถ„์„ ๋„๊ตฌ

    
    # mcp_server/tools/sales_analysis.py
    
    """
    
    Comprehensive sales analysis tool for retail data querying.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from datetime import datetime, timedelta
    
    from .base import DatabaseTool, ToolResult
    
    from .query_validator import query_validator
    
    
    
    class SalesAnalysisTool(DatabaseTool):
    
        """Advanced sales analysis and reporting tool."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="execute_sales_query",
    
                description="Execute sophisticated sales analysis queries with natural language support",
    
                db_provider=db_provider
    
            )
    
            
    
            # Pre-built query templates for common analysis
    
            self.query_templates = {
    
                'daily_sales': """
    
                    SELECT 
    
                        DATE(transaction_date) as sales_date,
    
                        COUNT(*) as transaction_count,
    
                        SUM(total_amount) as total_revenue,
    
                        AVG(total_amount) as avg_transaction_value,
    
                        COUNT(DISTINCT customer_id) as unique_customers
    
                    FROM retail.sales_transactions 
    
                    WHERE transaction_date >= $1 AND transaction_date <= $2
    
                      AND transaction_type = 'sale'
    
                    GROUP BY DATE(transaction_date)
    
                    ORDER BY sales_date DESC
    
                """,
    
                
    
                'top_products': """
    
                    SELECT 
    
                        p.product_name,
    
                        p.brand,
    
                        SUM(sti.quantity) as total_quantity_sold,
    
                        SUM(sti.total_price) as total_revenue,
    
                        COUNT(DISTINCT st.transaction_id) as transaction_count,
    
                        AVG(sti.unit_price) as avg_price
    
                    FROM retail.sales_transaction_items sti
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY p.product_id, p.product_name, p.brand
    
                    ORDER BY total_revenue DESC
    
                    LIMIT $3
    
                """,
    
                
    
                'customer_analysis': """
    
                    SELECT 
    
                        c.customer_id,
    
                        c.first_name || ' ' || c.last_name as customer_name,
    
                        c.loyalty_tier,
    
                        COUNT(st.transaction_id) as transaction_count,
    
                        SUM(st.total_amount) as total_spent,
    
                        AVG(st.total_amount) as avg_transaction_value,
    
                        MAX(st.transaction_date) as last_purchase_date,
    
                        DATE_PART('day', CURRENT_DATE - MAX(st.transaction_date)) as days_since_last_purchase
    
                    FROM retail.customers c
    
                    LEFT JOIN retail.sales_transactions st ON c.customer_id = st.customer_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY c.customer_id, c.first_name, c.last_name, c.loyalty_tier
    
                    HAVING COUNT(st.transaction_id) > 0
    
                    ORDER BY total_spent DESC
    
                    LIMIT $3
    
                """,
    
                
    
                'category_performance': """
    
                    SELECT 
    
                        pc.category_name,
    
                        COUNT(DISTINCT p.product_id) as unique_products,
    
                        SUM(sti.quantity) as total_quantity_sold,
    
                        SUM(sti.total_price) as total_revenue,
    
                        AVG(sti.unit_price) as avg_price,
    
                        COUNT(DISTINCT st.transaction_id) as transaction_count
    
                    FROM retail.product_categories pc
    
                    JOIN retail.products p ON pc.category_id = p.category_id
    
                    JOIN retail.sales_transaction_items sti ON p.product_id = sti.product_id
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY pc.category_id, pc.category_name
    
                    ORDER BY total_revenue DESC
    
                """,
    
                
    
                'sales_trends': """
    
                    WITH daily_sales AS (
    
                        SELECT 
    
                            DATE(transaction_date) as sales_date,
    
                            SUM(total_amount) as daily_revenue,
    
                            COUNT(*) as daily_transactions
    
                        FROM retail.sales_transactions 
    
                        WHERE transaction_date >= $1 AND transaction_date <= $2
    
                          AND transaction_type = 'sale'
    
                        GROUP BY DATE(transaction_date)
    
                    ),
    
                    trend_analysis AS (
    
                        SELECT 
    
                            sales_date,
    
                            daily_revenue,
    
                            daily_transactions,
    
                            LAG(daily_revenue, 1) OVER (ORDER BY sales_date) as prev_day_revenue,
    
                            LAG(daily_revenue, 7) OVER (ORDER BY sales_date) as prev_week_revenue,
    
                            AVG(daily_revenue) OVER (
    
                                ORDER BY sales_date 
    
                                ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    
                            ) as rolling_7day_avg
    
                        FROM daily_sales
    
                    )
    
                    SELECT 
    
                        sales_date,
    
                        daily_revenue,
    
                        daily_transactions,
    
                        rolling_7day_avg,
    
                        CASE 
    
                            WHEN prev_day_revenue IS NOT NULL THEN
    
                                ROUND(((daily_revenue - prev_day_revenue) / prev_day_revenue * 100)::numeric, 2)
    
                            ELSE NULL
    
                        END as day_over_day_growth_pct,
    
                        CASE 
    
                            WHEN prev_week_revenue IS NOT NULL THEN
    
                                ROUND(((daily_revenue - prev_week_revenue) / prev_week_revenue * 100)::numeric, 2)
    
                            ELSE NULL
    
                        END as week_over_week_growth_pct
    
                    FROM trend_analysis
    
                    ORDER BY sales_date DESC
    
                """
    
            }
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute sales analysis query."""
    
            
    
            query_type = kwargs.get('query_type', 'custom')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for sales analysis"
    
                )
    
            
    
            try:
    
                if query_type in self.query_templates:
    
                    return await self._execute_template_query(query_type, kwargs)
    
                elif query_type == 'custom':
    
                    return await self._execute_custom_query(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown query type: {query_type}"
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Sales analysis failed: {str(e)}"
    
                )
    
        
    
        async def _execute_template_query(self, query_type: str, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Execute pre-built template query."""
    
            
    
            query = self.query_templates[query_type]
    
            store_id = kwargs['store_id']
    
            
    
            # Default parameters for template queries
    
            start_date = kwargs.get('start_date', (datetime.now() - timedelta(days=30)).date())
    
            end_date = kwargs.get('end_date', datetime.now().date())
    
            limit = kwargs.get('limit', 20)
    
            
    
            # Convert string dates if needed
    
            if isinstance(start_date, str):
    
                start_date = datetime.fromisoformat(start_date).date()
    
            if isinstance(end_date, str):
    
                end_date = datetime.fromisoformat(end_date).date()
    
            
    
            # Execute query with parameters
    
            params = (start_date, end_date, limit) if '$3' in query else (start_date, end_date)
    
            
    
            result = await self.execute_query(query, params, store_id)
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'query_type': query_type,
    
                    'date_range': f"{start_date} to {end_date}",
    
                    'store_id': store_id,
    
                    'analysis_type': 'template'
    
                }
    
            
    
            return result
    
        
    
        async def _execute_custom_query(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Execute custom SQL query with validation."""
    
            
    
            custom_query = kwargs.get('query')
    
            store_id = kwargs['store_id']
    
            
    
            if not custom_query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Custom query is required when query_type is 'custom'"
    
                )
    
            
    
            # Validate the query for security
    
            validation = query_validator.validate_query(custom_query)
    
            
    
            if not validation['is_safe']:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Query validation failed: {', '.join(validation['issues'])}",
    
                    metadata={
    
                        'validation_result': validation,
    
                        'risk_level': validation['risk_level'].value
    
                    }
    
                )
    
            
    
            # Execute validated query
    
            result = await self.execute_query(custom_query, None, store_id)
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'validation_warnings': validation.get('warnings', []),
    
                    'analysis_type': 'custom'
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for the sales analysis tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query_type": {
    
                        "type": "string",
    
                        "enum": list(self.query_templates.keys()) + ["custom"],
    
                        "description": "Type of sales analysis to perform",
    
                        "default": "daily_sales"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for data isolation",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "start_date": {
    
                        "type": "string",
    
                        "format": "date",
    
                        "description": "Start date for analysis (YYYY-MM-DD)"
    
                    },
    
                    "end_date": {
    
                        "type": "string",
    
                        "format": "date",
    
                        "description": "End date for analysis (YYYY-MM-DD)"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "minimum": 1,
    
                        "maximum": 1000,
    
                        "description": "Maximum number of results to return",
    
                        "default": 20
    
                    },
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Custom SQL query (required when query_type is 'custom')"
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    

    ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๋„๊ตฌ

    
    # mcp_server/tools/schema_introspection.py
    
    """
    
    Database schema introspection and metadata tools.
    
    """
    
    from typing import Dict, Any, List
    
    from .base import DatabaseTool, ToolResult, ToolCategory
    
    
    
    class SchemaIntrospectionTool(DatabaseTool):
    
        """Tool for exploring database schema and metadata."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_table_schema",
    
                description="Get detailed schema information for database tables",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.SCHEMA_INTROSPECTION
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute schema introspection."""
    
            
    
            table_name = kwargs.get('table_name')
    
            include_constraints = kwargs.get('include_constraints', True)
    
            include_indexes = kwargs.get('include_indexes', True)
    
            include_statistics = kwargs.get('include_statistics', False)
    
            
    
            try:
    
                if table_name:
    
                    return await self._get_single_table_schema(
    
                        table_name, include_constraints, include_indexes, include_statistics
    
                    )
    
                else:
    
                    return await self._get_all_tables_schema(include_constraints, include_indexes)
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Schema introspection failed: {str(e)}"
    
                )
    
        
    
        async def _get_single_table_schema(
    
            self, 
    
            table_name: str, 
    
            include_constraints: bool,
    
            include_indexes: bool,
    
            include_statistics: bool
    
        ) -> ToolResult:
    
            """Get detailed schema for a single table."""
    
            
    
            schema_info = {
    
                'table_name': table_name,
    
                'columns': [],
    
                'constraints': [],
    
                'indexes': [],
    
                'statistics': {}
    
            }
    
            
    
            async with self.get_connection() as conn:
    
                # Get column information
    
                columns_query = """
    
                    SELECT 
    
                        column_name,
    
                        data_type,
    
                        is_nullable,
    
                        column_default,
    
                        character_maximum_length,
    
                        numeric_precision,
    
                        numeric_scale,
    
                        ordinal_position,
    
                        udt_name
    
                    FROM information_schema.columns
    
                    WHERE table_schema = 'retail' AND table_name = $1
    
                    ORDER BY ordinal_position
    
                """
    
                
    
                columns = await conn.fetch(columns_query, table_name)
    
                schema_info['columns'] = [dict(col) for col in columns]
    
                
    
                # Get constraints if requested
    
                if include_constraints:
    
                    constraints_query = """
    
                        SELECT 
    
                            constraint_name,
    
                            constraint_type,
    
                            column_name,
    
                            foreign_table_name,
    
                            foreign_column_name
    
                        FROM information_schema.table_constraints tc
    
                        LEFT JOIN information_schema.key_column_usage kcu 
    
                            ON tc.constraint_name = kcu.constraint_name
    
                        LEFT JOIN information_schema.referential_constraints rc
    
                            ON tc.constraint_name = rc.constraint_name
    
                        LEFT JOIN information_schema.key_column_usage fkcu
    
                            ON rc.unique_constraint_name = fkcu.constraint_name
    
                        WHERE tc.table_schema = 'retail' AND tc.table_name = $1
    
                    """
    
                    
    
                    constraints = await conn.fetch(constraints_query, table_name)
    
                    schema_info['constraints'] = [dict(const) for const in constraints]
    
                
    
                # Get indexes if requested
    
                if include_indexes:
    
                    indexes_query = """
    
                        SELECT 
    
                            indexname as index_name,
    
                            indexdef as index_definition,
    
                            tablespace
    
                        FROM pg_indexes
    
                        WHERE schemaname = 'retail' AND tablename = $1
    
                    """
    
                    
    
                    indexes = await conn.fetch(indexes_query, table_name)
    
                    schema_info['indexes'] = [dict(idx) for idx in indexes]
    
                
    
                # Get table statistics if requested
    
                if include_statistics:
    
                    stats_query = """
    
                        SELECT 
    
                            n_tup_ins as inserts,
    
                            n_tup_upd as updates,
    
                            n_tup_del as deletes,
    
                            n_live_tup as live_tuples,
    
                            n_dead_tup as dead_tuples,
    
                            last_vacuum,
    
                            last_autovacuum,
    
                            last_analyze,
    
                            last_autoanalyze
    
                        FROM pg_stat_user_tables
    
                        WHERE schemaname = 'retail' AND relname = $1
    
                    """
    
                    
    
                    stats = await conn.fetchrow(stats_query, table_name)
    
                    if stats:
    
                        schema_info['statistics'] = dict(stats)
    
            
    
            return ToolResult(
    
                success=True,
    
                data=schema_info,
    
                metadata={
    
                    'table_name': table_name,
    
                    'schema': 'retail',
    
                    'introspection_type': 'single_table'
    
                }
    
            )
    
        
    
        async def _get_all_tables_schema(
    
            self, 
    
            include_constraints: bool,
    
            include_indexes: bool
    
        ) -> ToolResult:
    
            """Get schema information for all tables."""
    
            
    
            async with self.get_connection() as conn:
    
                # Get all tables in retail schema
    
                tables_query = """
    
                    SELECT 
    
                        table_name,
    
                        table_type
    
                    FROM information_schema.tables
    
                    WHERE table_schema = 'retail'
    
                    ORDER BY table_name
    
                """
    
                
    
                tables = await conn.fetch(tables_query)
    
                schema_info = {
    
                    'schema_name': 'retail',
    
                    'tables': []
    
                }
    
                
    
                for table in tables:
    
                    table_info = {
    
                        'table_name': table['table_name'],
    
                        'table_type': table['table_type'],
    
                        'columns': []
    
                    }
    
                    
    
                    # Get columns for each table
    
                    columns_query = """
    
                        SELECT 
    
                            column_name,
    
                            data_type,
    
                            is_nullable,
    
                            column_default
    
                        FROM information_schema.columns
    
                        WHERE table_schema = 'retail' AND table_name = $1
    
                        ORDER BY ordinal_position
    
                    """
    
                    
    
                    columns = await conn.fetch(columns_query, table['table_name'])
    
                    table_info['columns'] = [dict(col) for col in columns]
    
                    
    
                    schema_info['tables'].append(table_info)
    
            
    
            return ToolResult(
    
                success=True,
    
                data=schema_info,
    
                metadata={
    
                    'schema': 'retail',
    
                    'table_count': len(schema_info['tables']),
    
                    'introspection_type': 'all_tables'
    
                }
    
            )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for schema introspection tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "table_name": {
    
                        "type": "string",
    
                        "description": "Specific table name to introspect (optional - if not provided, all tables are returned)",
    
                        "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
    
                    },
    
                    "include_constraints": {
    
                        "type": "boolean",
    
                        "description": "Include constraint information",
    
                        "default": True
    
                    },
    
                    "include_indexes": {
    
                        "type": "boolean",
    
                        "description": "Include index information",
    
                        "default": True
    
                    },
    
                    "include_statistics": {
    
                        "type": "boolean",
    
                        "description": "Include table statistics",
    
                        "default": False
    
                    }
    
                },
    
                "additionalProperties": False
    
            }
    
    
    
    class MultiTableSchemaTool(DatabaseTool):
    
        """Tool for getting schema information for multiple tables at once."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_multiple_table_schemas",
    
                description="Get schema information for multiple tables efficiently",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.SCHEMA_INTROSPECTION
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute multi-table schema introspection."""
    
            
    
            table_names = kwargs.get('table_names', [])
    
            
    
            if not table_names:
    
                return ToolResult(
    
                    success=False,
    
                    error="At least one table name is required"
    
                )
    
            
    
            try:
    
                schemas = {}
    
                
    
                async with self.get_connection() as conn:
    
                    for table_name in table_names:
    
                        # Get table schema
    
                        schema_query = """
    
                            SELECT 
    
                                c.column_name,
    
                                c.data_type,
    
                                c.is_nullable,
    
                                c.column_default,
    
                                c.character_maximum_length,
    
                                tc.constraint_type,
    
                                kcu.constraint_name
    
                            FROM information_schema.columns c
    
                            LEFT JOIN information_schema.key_column_usage kcu
    
                                ON c.table_name = kcu.table_name 
    
                                AND c.column_name = kcu.column_name
    
                                AND c.table_schema = kcu.table_schema
    
                            LEFT JOIN information_schema.table_constraints tc
    
                                ON kcu.constraint_name = tc.constraint_name
    
                                AND kcu.table_schema = tc.table_schema
    
                            WHERE c.table_schema = 'retail' AND c.table_name = $1
    
                            ORDER BY c.ordinal_position
    
                        """
    
                        
    
                        columns = await conn.fetch(schema_query, table_name)
    
                        
    
                        if columns:
    
                            schemas[table_name] = {
    
                                'table_name': table_name,
    
                                'columns': [dict(col) for col in columns]
    
                            }
    
                        else:
    
                            schemas[table_name] = {
    
                                'table_name': table_name,
    
                                'error': 'Table not found or not accessible'
    
                            }
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=schemas,
    
                    metadata={
    
                        'requested_tables': table_names,
    
                        'found_tables': [name for name, info in schemas.items() if 'error' not in info],
    
                        'missing_tables': [name for name, info in schemas.items() if 'error' in info]
    
                    }
    
                )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Multi-table schema introspection failed: {str(e)}"
    
                )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for multi-table schema tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "table_names": {
    
                        "type": "array",
    
                        "items": {
    
                            "type": "string",
    
                            "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
    
                        },
    
                        "description": "List of table names to get schema information for",
    
                        "minItems": 1,
    
                        "maxItems": 20
    
                    }
    
                },
    
                "required": ["table_names"],
    
                "additionalProperties": False
    
            }
    
    

    ๐Ÿ“Š ๋ถ„์„ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋„๊ตฌ

    ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค ๋„๊ตฌ

    
    # mcp_server/tools/business_intelligence.py
    
    """
    
    Advanced business intelligence and analytics tools.
    
    """
    
    from typing import Dict, Any, List
    
    from datetime import datetime, timedelta
    
    from .base import DatabaseTool, ToolResult, ToolCategory
    
    
    
    class BusinessIntelligenceTool(DatabaseTool):
    
        """Advanced analytics tool for business intelligence queries."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="generate_business_insights",
    
                description="Generate comprehensive business intelligence reports and insights",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.ANALYTICS
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute business intelligence analysis."""
    
            
    
            analysis_type = kwargs.get('analysis_type', 'summary')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for business intelligence analysis"
    
                )
    
            
    
            try:
    
                if analysis_type == 'summary':
    
                    return await self._generate_business_summary(kwargs)
    
                elif analysis_type == 'customer_segmentation':
    
                    return await self._analyze_customer_segmentation(kwargs)
    
                elif analysis_type == 'product_performance':
    
                    return await self._analyze_product_performance(kwargs)
    
                elif analysis_type == 'seasonal_trends':
    
                    return await self._analyze_seasonal_trends(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown analysis type: {analysis_type}"
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Business intelligence analysis failed: {str(e)}"
    
                )
    
        
    
        async def _generate_business_summary(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Generate comprehensive business summary."""
    
            
    
            store_id = kwargs['store_id']
    
            days = kwargs.get('days', 30)
    
            
    
            summary_query = """
    
                WITH date_range AS (
    
                    SELECT CURRENT_DATE - INTERVAL '%s days' as start_date,
    
                           CURRENT_DATE as end_date
    
                ),
    
                sales_summary AS (
    
                    SELECT 
    
                        COUNT(*) as total_transactions,
    
                        COUNT(DISTINCT customer_id) as unique_customers,
    
                        SUM(total_amount) as total_revenue,
    
                        AVG(total_amount) as avg_transaction_value,
    
                        COUNT(DISTINCT DATE(transaction_date)) as active_days
    
                    FROM retail.sales_transactions st, date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                product_summary AS (
    
                    SELECT 
    
                        COUNT(DISTINCT p.product_id) as products_sold,
    
                        SUM(sti.quantity) as total_items_sold
    
                    FROM retail.sales_transaction_items sti
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    CROSS JOIN date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                top_category AS (
    
                    SELECT 
    
                        pc.category_name,
    
                        SUM(sti.total_price) as category_revenue
    
                    FROM retail.product_categories pc
    
                    JOIN retail.products p ON pc.category_id = p.category_id
    
                    JOIN retail.sales_transaction_items sti ON p.product_id = sti.product_id
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    CROSS JOIN date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY pc.category_name
    
                    ORDER BY category_revenue DESC
    
                    LIMIT 1
    
                )
    
                SELECT 
    
                    ss.*,
    
                    ps.products_sold,
    
                    ps.total_items_sold,
    
                    tc.category_name as top_category,
    
                    tc.category_revenue as top_category_revenue,
    
                    CASE 
    
                        WHEN ss.active_days > 0 THEN ss.total_revenue / ss.active_days
    
                        ELSE 0
    
                    END as avg_daily_revenue
    
                FROM sales_summary ss
    
                CROSS JOIN product_summary ps
    
                CROSS JOIN top_category tc
    
            """ % days
    
            
    
            result = await self.execute_query(summary_query, None, store_id)
    
            
    
            if result.success and result.data:
    
                summary = result.data[0]
    
                
    
                # Add derived insights
    
                insights = {
    
                    'revenue_trend': 'stable',  # Would calculate based on historical data
    
                    'customer_retention': f"{summary.get('unique_customers', 0)} active customers",
    
                    'performance_indicators': {
    
                        'transactions_per_day': round(summary.get('total_transactions', 0) / max(summary.get('active_days', 1), 1), 2),
    
                        'revenue_per_customer': round(summary.get('total_revenue', 0) / max(summary.get('unique_customers', 1), 1), 2),
    
                        'items_per_transaction': round(summary.get('total_items_sold', 0) / max(summary.get('total_transactions', 1), 1), 2)
    
                    }
    
                }
    
                
    
                summary['insights'] = insights
    
                
    
                result.data = [summary]
    
                result.metadata = {
    
                    'analysis_type': 'business_summary',
    
                    'period_days': days,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for business intelligence tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "analysis_type": {
    
                        "type": "string",
    
                        "enum": ["summary", "customer_segmentation", "product_performance", "seasonal_trends"],
    
                        "description": "Type of business intelligence analysis to perform",
    
                        "default": "summary"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for analysis",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "days": {
    
                        "type": "integer",
    
                        "minimum": 1,
    
                        "maximum": 365,
    
                        "description": "Number of days to analyze",
    
                        "default": 30
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    
    
    class UtilityTool(DatabaseTool):
    
        """Utility tool for common operations."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_current_utc_date",
    
                description="Get current UTC date and time for reference",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.UTILITY
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute utility operation."""
    
            
    
            format_type = kwargs.get('format', 'iso')
    
            
    
            try:
    
                async with self.get_connection() as conn:
    
                    if format_type == 'iso':
    
                        query = "SELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC' as current_utc_datetime"
    
                    elif format_type == 'epoch':
    
                        query = "SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC') as current_utc_epoch"
    
                    elif format_type == 'date_only':
    
                        query = "SELECT CURRENT_DATE as current_date"
    
                    else:
    
                        return ToolResult(
    
                            success=False,
    
                            error=f"Unknown format type: {format_type}"
    
                        )
    
                    
    
                    result = await conn.fetchrow(query)
    
                    
    
                    return ToolResult(
    
                        success=True,
    
                        data=dict(result),
    
                        metadata={
    
                            'format_type': format_type,
    
                            'timezone': 'UTC'
    
                        }
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Utility operation failed: {str(e)}"
    
                )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for utility tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "format": {
    
                        "type": "string",
    
                        "enum": ["iso", "epoch", "date_only"],
    
                        "description": "Format for the returned date/time",
    
                        "default": "iso"
    
                    }
    
                },
    
                "additionalProperties": False
    
            }
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… ๊ณ ๊ธ‰ ๋„๊ตฌ ์•„ํ‚คํ…์ฒ˜: ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ–์ถ˜ ์ •๊ตํ•œ MCP ๋„๊ตฌ ๊ตฌํ˜„

    โœ… ์ฟผ๋ฆฌ ๊ฒ€์ฆ: SQL ์ธ์ ์…˜ ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๋Š” ์•ˆ์ „ํ•œ SQL ๊ฒ€์ฆ ๊ตฌ์ถ•

    โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋„๊ตฌ: ๊ฐ•๋ ฅํ•œ ํŒ๋งค ๋ถ„์„ ๋ฐ ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๊ธฐ๋Šฅ ์ƒ์„ฑ

    โœ… ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค: ํฌ๊ด„์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ํ†ต์ฐฐ๋ ฅ์„ ์œ„ํ•œ ๋ถ„์„ ๋„๊ตฌ ๊ฐœ๋ฐœ

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์บ์‹ฑ, ์—ฐ๊ฒฐ ํ’€๋ง, ์ฟผ๋ฆฌ ์ตœ์ ํ™” ์ ์šฉ

    โœ… ๋ณด์•ˆ ํ†ตํ•ฉ: ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด ๋ฐ ๊ฐ์‚ฌ ๋กœ๊น… ๊ตฌํ˜„

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต 07: ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ํ†ตํ•ฉ์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • MCP ๋„๊ตฌ์™€ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ†ตํ•ฉ
  • ์‹œ๋งจํ‹ฑ ์ œํ’ˆ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ตฌ์ถ•
  • AI ๊ธฐ๋ฐ˜ ์ฟผ๋ฆฌ ์ดํ•ด ๊ตฌํ˜„
  • ์ „ํ†ต์  ์ฟผ๋ฆฌ์™€ ๋ฒกํ„ฐ ์ฟผ๋ฆฌ๋ฅผ ๊ฒฐํ•ฉํ•œ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ ์ƒ์„ฑ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    MCP ๋„๊ตฌ ๊ฐœ๋ฐœ

  • Model Context Protocol Documentation - ๊ณต์‹ MCP ์‚ฌ์–‘
  • FastMCP Framework - Python MCP ๊ตฌํ˜„
  • MCP Tool Patterns - ๋„๊ตฌ ๊ตฌํ˜„ ์˜ˆ์ œ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ

  • SQL Injection Prevention - OWASP ๋ณด์•ˆ ๊ฐ€์ด๋“œ
  • PostgreSQL Security - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • Query Validation Techniques - ์•ˆ์ „ํ•œ ์ฟผ๋ฆฌ ํŒจํ„ด
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”

  • Database Query Optimization - PostgreSQL ์„ฑ๋Šฅ ๊ฐ€์ด๋“œ
  • Connection Pooling Best Practices - ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • Async Python Patterns - ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ฐ€์ด๋“œ
  • ---

    ์ด์ „: ์‹ค์Šต 05: MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    ๋‹ค์Œ: ์‹ค์Šต 07: ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ํ†ตํ•ฉ

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๋„๊ตฌ ๋ฐ ์Šคํ‚ค๋งˆ ์ธํŠธ๋กœ์ŠคํŽ™์…˜ ๊ตฌ์ถ•ํ•˜๊ธฐ

    ๋„๊ตฌ ๊ฐœ๋ฐœ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์—์„œ๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์—๊ฒŒ ๊ฐ•๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ, ์Šคํ‚ค๋งˆ ํƒ์ƒ‰, ๋ถ„์„ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋Š” ๊ณ ๊ธ‰ MCP ๋„๊ตฌ๋ฅผ ๋งŒ๋“œ๋Š” ๋ฐฉ๋ฒ•์„ ๊นŠ์ด ํƒ๊ตฌํ•ฉ๋‹ˆ๋‹ค. ๊ฐ•๋ ฅํ•˜๋ฉด์„œ๋„ ์•ˆ์ „ํ•œ ๋„๊ตฌ๋ฅผ ์„ค๊ณ„ํ•˜๊ณ , ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ์™€ ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ๋„๊ตฌ๋Š” AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ ๋ฐ์ดํ„ฐ ์‹œ์Šคํ…œ ๊ฐ„์˜ ์ธํ„ฐํŽ˜์ด์Šค ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค. ์ž˜ ์„ค๊ณ„๋œ ๋„๊ตฌ๋Š” ๋ณต์žกํ•œ ์ž‘์—…์— ๋Œ€ํ•ด ๊ตฌ์กฐํ™”๋˜๊ณ  ๊ฒ€์ฆ๋œ ์ ‘๊ทผ์„ ์ œ๊ณตํ•˜๋ฉฐ, ๋ณด์•ˆ๊ณผ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” ์„ค๊ณ„๋ถ€ํ„ฐ ๋ฐฐํฌ๊นŒ์ง€ ๋„๊ตฌ ๊ฐœ๋ฐœ์˜ ์ „์ฒด ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    ์šฐ๋ฆฌ์˜ ์†Œ๋งค MCP ์„œ๋ฒ„๋Š” ํŒ๋งค ๋ฐ์ดํ„ฐ, ์ œํ’ˆ ์นดํƒˆ๋กœ๊ทธ, ๋น„์ฆˆ๋‹ˆ์Šค ๋ถ„์„์„ ์ž์—ฐ์–ด๋กœ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํฌ๊ด„์ ์ธ ๋„๊ตฌ ์„ธํŠธ๋ฅผ ๊ตฌํ˜„ํ•˜๋ฉฐ, ์—„๊ฒฉํ•œ ๋ณด์•ˆ ๊ฒฝ๊ณ„์™€ ์ตœ์ ์˜ ์„ฑ๋Šฅ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์„ค๊ณ„: ๋ณต์žกํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜ ๊ฒ€์ฆ์„ ํฌํ•จํ•œ ๊ณ ๊ธ‰ MCP ๋„๊ตฌ ์„ค๊ณ„
  • ๊ตฌํ˜„: SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ์•ˆ์ „ํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๋„๊ตฌ ๊ตฌํ˜„
  • ์ƒ์„ฑ: ๋™์  ์ฟผ๋ฆฌ๋ฅผ ์œ„ํ•œ ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๊ธฐ๋Šฅ ๊ฐœ๋ฐœ
  • ๊ตฌ์ถ•: ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค๋ฅผ ์œ„ํ•œ ๋งž์ถคํ˜• ๋ถ„์„ ๋„๊ตฌ ์ œ์ž‘
  • ์ ์šฉ: ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋ฐ ์šฐ์•„ํ•œ ์„ฑ๋Šฅ ์ €ํ•˜ ๊ตฌํ˜„
  • ์ตœ์ ํ™”: ํ”„๋กœ๋•์…˜ ์›Œํฌ๋กœ๋“œ๋ฅผ ์œ„ํ•œ ๋„๊ตฌ ์„ฑ๋Šฅ ์ตœ์ ํ™”
  • ๐Ÿ› ๏ธ ํ•ต์‹ฌ ๋„๊ตฌ ์•„ํ‚คํ…์ฒ˜

    ๋„๊ตฌ ์„ค๊ณ„ ์›์น™

    
    # mcp_server/tools/base.py
    
    """
    
    Base classes and patterns for MCP tool development.
    
    """
    
    from abc import ABC, abstractmethod
    
    from typing import Any, Dict, List, Optional, Union
    
    from dataclasses import dataclass
    
    from enum import Enum
    
    import asyncio
    
    import time
    
    import logging
    
    from contextlib import asynccontextmanager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ToolCategory(Enum):
    
        """Tool categorization for organization and discovery."""
    
        DATABASE_QUERY = "database_query"
    
        SCHEMA_INTROSPECTION = "schema_introspection"
    
        ANALYTICS = "analytics"
    
        UTILITY = "utility"
    
        ADMINISTRATIVE = "administrative"
    
    
    
    @dataclass
    
    class ToolResult:
    
        """Standardized tool result structure."""
    
        success: bool
    
        data: Any = None
    
        error: Optional[str] = None
    
        metadata: Optional[Dict[str, Any]] = None
    
        execution_time_ms: Optional[float] = None
    
        row_count: Optional[int] = None
    
    
    
    class BaseTool(ABC):
    
        """Abstract base class for all MCP tools."""
    
        
    
        def __init__(self, name: str, description: str, category: ToolCategory):
    
            self.name = name
    
            self.description = description
    
            self.category = category
    
            self.call_count = 0
    
            self.total_execution_time = 0.0
    
            
    
        @abstractmethod
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute the tool with given parameters."""
    
            pass
    
        
    
        @abstractmethod
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get JSON schema for tool input validation."""
    
            pass
    
        
    
        async def call(self, **kwargs) -> ToolResult:
    
            """Wrapper for tool execution with metrics and error handling."""
    
            
    
            start_time = time.time()
    
            self.call_count += 1
    
            
    
            try:
    
                # Validate input parameters
    
                self._validate_input(kwargs)
    
                
    
                # Log tool execution
    
                logger.info(
    
                    f"Executing tool: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'tool_category': self.category.value,
    
                        'parameters': self._sanitize_parameters(kwargs)
    
                    }
    
                )
    
                
    
                # Execute the tool
    
                result = await self.execute(**kwargs)
    
                
    
                # Record execution time
    
                execution_time = (time.time() - start_time) * 1000
    
                result.execution_time_ms = execution_time
    
                self.total_execution_time += execution_time
    
                
    
                # Log success
    
                logger.info(
    
                    f"Tool execution completed: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'execution_time_ms': execution_time,
    
                        'success': result.success,
    
                        'row_count': result.row_count
    
                    }
    
                )
    
                
    
                return result
    
                
    
            except Exception as e:
    
                execution_time = (time.time() - start_time) * 1000
    
                
    
                logger.error(
    
                    f"Tool execution failed: {self.name}",
    
                    extra={
    
                        'tool_name': self.name,
    
                        'execution_time_ms': execution_time,
    
                        'error': str(e)
    
                    },
    
                    exc_info=True
    
                )
    
                
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Tool execution failed: {str(e)}",
    
                    execution_time_ms=execution_time
    
                )
    
        
    
        def _validate_input(self, kwargs: Dict[str, Any]):
    
            """Validate input parameters against schema."""
    
            
    
            schema = self.get_input_schema()
    
            required_props = schema.get('required', [])
    
            properties = schema.get('properties', {})
    
            
    
            # Check required parameters
    
            missing_required = [prop for prop in required_props if prop not in kwargs]
    
            if missing_required:
    
                raise ValueError(f"Missing required parameters: {missing_required}")
    
            
    
            # Type validation would go here
    
            # For production, use jsonschema library for comprehensive validation
    
        
    
        def _sanitize_parameters(self, kwargs: Dict[str, Any]) -> Dict[str, Any]:
    
            """Sanitize parameters for logging (remove sensitive data)."""
    
            
    
            # Remove or mask sensitive parameters
    
            sanitized = kwargs.copy()
    
            sensitive_keys = ['password', 'token', 'secret', 'key']
    
            
    
            for key in sanitized:
    
                if any(sensitive in key.lower() for sensitive in sensitive_keys):
    
                    sanitized[key] = "***MASKED***"
    
            
    
            return sanitized
    
        
    
        def get_statistics(self) -> Dict[str, Any]:
    
            """Get tool usage statistics."""
    
            
    
            return {
    
                'name': self.name,
    
                'category': self.category.value,
    
                'call_count': self.call_count,
    
                'total_execution_time_ms': self.total_execution_time,
    
                'average_execution_time_ms': (
    
                    self.total_execution_time / self.call_count 
    
                    if self.call_count > 0 else 0
    
                )
    
            }
    
    
    
    class DatabaseTool(BaseTool):
    
        """Base class for database-related tools."""
    
        
    
        def __init__(self, name: str, description: str, db_provider):
    
            super().__init__(name, description, ToolCategory.DATABASE_QUERY)
    
            self.db_provider = db_provider
    
        
    
        @asynccontextmanager
    
        async def get_connection(self):
    
            """Get database connection with proper context management."""
    
            
    
            conn = None
    
            try:
    
                conn = await self.db_provider.get_connection()
    
                yield conn
    
            finally:
    
                if conn:
    
                    await self.db_provider.release_connection(conn)
    
        
    
        async def execute_query(
    
            self, 
    
            query: str, 
    
            params: tuple = None,
    
            store_id: str = None
    
        ) -> ToolResult:
    
            """Execute database query with security and performance monitoring."""
    
            
    
            async with self.get_connection() as conn:
    
                try:
    
                    # Set store context if provided
    
                    if store_id:
    
                        await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Execute query
    
                    start_time = time.time()
    
                    
    
                    if params:
    
                        rows = await conn.fetch(query, *params)
    
                    else:
    
                        rows = await conn.fetch(query)
    
                    
    
                    execution_time = (time.time() - start_time) * 1000
    
                    
    
                    # Convert rows to dictionaries
    
                    data = [dict(row) for row in rows]
    
                    
    
                    return ToolResult(
    
                        success=True,
    
                        data=data,
    
                        row_count=len(data),
    
                        execution_time_ms=execution_time
    
                    )
    
                    
    
                except Exception as e:
    
                    logger.error(f"Database query failed: {str(e)}")
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Query execution failed: {str(e)}"
    
                    )
    
    

    ์ฟผ๋ฆฌ ๊ฒ€์ฆ ๋ฐ ๋ณด์•ˆ

    
    # mcp_server/tools/query_validator.py
    
    """
    
    SQL query validation and security for MCP tools.
    
    """
    
    import re
    
    import sqlparse
    
    from typing import List, Dict, Any, Set
    
    from enum import Enum
    
    
    
    class QueryRisk(Enum):
    
        """Query risk levels."""
    
        LOW = "low"
    
        MEDIUM = "medium"
    
        HIGH = "high"
    
        CRITICAL = "critical"
    
    
    
    class QueryValidator:
    
        """Validate and analyze SQL queries for security risks."""
    
        
    
        # Dangerous SQL keywords and patterns
    
        DANGEROUS_KEYWORDS = {
    
            'DROP', 'DELETE', 'TRUNCATE', 'ALTER', 'CREATE', 'INSERT',
    
            'UPDATE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE', 'sp_',
    
            'xp_', 'BULK', 'OPENROWSET', 'OPENDATASOURCE'
    
        }
    
        
    
        # Allowed read-only operations
    
        SAFE_KEYWORDS = {
    
            'SELECT', 'WITH', 'UNION', 'ORDER', 'GROUP', 'HAVING',
    
            'WHERE', 'FROM', 'JOIN', 'AS', 'ON', 'IN', 'EXISTS',
    
            'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'AND', 'OR', 'NOT'
    
        }
    
        
    
        # Allowed schemas and tables
    
        ALLOWED_SCHEMAS = {'retail', 'information_schema', 'pg_catalog'}
    
        ALLOWED_TABLES = {
    
            'customers', 'products', 'sales_transactions', 
    
            'sales_transaction_items', 'product_categories',
    
            'product_embeddings', 'stores'
    
        }
    
        
    
        def __init__(self):
    
            self.injection_patterns = [
    
                # SQL injection patterns
    
                r"(\b(UNION|union)\s+(ALL\s+)?(SELECT|select))",
    
                r"(\b(DROP|drop)\s+(TABLE|table|DATABASE|database))",
    
                r"(\b(DELETE|delete)\s+(FROM|from))",
    
                r"(\b(INSERT|insert)\s+(INTO|into))",
    
                r"(\b(UPDATE|update)\s+\w+\s+(SET|set))",
    
                r"(\b(EXEC|exec|EXECUTE|execute)\s*\()",
    
                r"(\b(sp_|xp_)\w+)",
    
                r"(--\s*$)",  # SQL comments
    
                r"(/\*.*?\*/)",  # Block comments
    
                r"(;\s*(DROP|DELETE|INSERT|UPDATE|CREATE|ALTER))",
    
                r"(\bOR\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)",  # OR injection
    
                r"(\bAND\b\s+['\"]?\w+['\"]?\s*=\s*['\"]?\w+['\"]?)",  # AND injection
    
            ]
    
            
    
            self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.injection_patterns]
    
        
    
        def validate_query(self, query: str) -> Dict[str, Any]:
    
            """Comprehensive query validation."""
    
            
    
            validation_result = {
    
                'is_safe': True,
    
                'risk_level': QueryRisk.LOW,
    
                'issues': [],
    
                'warnings': [],
    
                'allowed_operations': [],
    
                'metadata': {}
    
            }
    
            
    
            try:
    
                # Parse the query
    
                parsed = sqlparse.parse(query)
    
                
    
                if not parsed:
    
                    validation_result['is_safe'] = False
    
                    validation_result['issues'].append("Unable to parse query")
    
                    validation_result['risk_level'] = QueryRisk.HIGH
    
                    return validation_result
    
                
    
                # Analyze each statement
    
                for statement in parsed:
    
                    self._analyze_statement(statement, validation_result)
    
                
    
                # Check for injection patterns
    
                self._check_injection_patterns(query, validation_result)
    
                
    
                # Validate table/schema access
    
                self._validate_table_access(query, validation_result)
    
                
    
                # Determine final risk level
    
                self._determine_risk_level(validation_result)
    
                
    
            except Exception as e:
    
                validation_result['is_safe'] = False
    
                validation_result['issues'].append(f"Query analysis failed: {str(e)}")
    
                validation_result['risk_level'] = QueryRisk.CRITICAL
    
            
    
            return validation_result
    
        
    
        def _analyze_statement(self, statement, validation_result):
    
            """Analyze individual SQL statement."""
    
            
    
            # Get statement type
    
            stmt_type = statement.get_type()
    
            
    
            # Check if statement type is allowed
    
            if stmt_type and stmt_type.upper() not in ['SELECT', 'WITH']:
    
                validation_result['issues'].append(f"Disallowed statement type: {stmt_type}")
    
                validation_result['is_safe'] = False
    
                return
    
            
    
            # Extract tokens and analyze
    
            for token in statement.flatten():
    
                if token.ttype is sqlparse.tokens.Keyword:
    
                    keyword = token.value.upper()
    
                    
    
                    if keyword in self.DANGEROUS_KEYWORDS:
    
                        validation_result['issues'].append(f"Dangerous keyword detected: {keyword}")
    
                        validation_result['is_safe'] = False
    
                    elif keyword in self.SAFE_KEYWORDS:
    
                        if keyword not in validation_result['allowed_operations']:
    
                            validation_result['allowed_operations'].append(keyword)
    
        
    
        def _check_injection_patterns(self, query: str, validation_result):
    
            """Check for SQL injection patterns."""
    
            
    
            for pattern in self.compiled_patterns:
    
                matches = pattern.findall(query)
    
                if matches:
    
                    validation_result['issues'].append(f"Potential injection pattern detected")
    
                    validation_result['is_safe'] = False
    
        
    
        def _validate_table_access(self, query: str, validation_result):
    
            """Validate that only allowed tables/schemas are accessed."""
    
            
    
            # Extract table names (simplified approach)
    
            # In production, use proper SQL parsing
    
            from_match = re.findall(r'FROM\s+(\w+\.?\w*)', query, re.IGNORECASE)
    
            join_match = re.findall(r'JOIN\s+(\w+\.?\w*)', query, re.IGNORECASE)
    
            
    
            all_tables = from_match + join_match
    
            
    
            for table_ref in all_tables:
    
                if '.' in table_ref:
    
                    schema, table = table_ref.split('.', 1)
    
                    if schema.lower() not in self.ALLOWED_SCHEMAS:
    
                        validation_result['issues'].append(f"Access to unauthorized schema: {schema}")
    
                        validation_result['is_safe'] = False
    
                    if table.lower() not in self.ALLOWED_TABLES:
    
                        validation_result['warnings'].append(f"Access to table: {table}")
    
                else:
    
                    # Assume retail schema if not specified
    
                    if table_ref.lower() not in self.ALLOWED_TABLES:
    
                        validation_result['warnings'].append(f"Access to table: {table_ref}")
    
        
    
        def _determine_risk_level(self, validation_result):
    
            """Determine overall risk level."""
    
            
    
            if not validation_result['is_safe']:
    
                if any('injection' in issue.lower() for issue in validation_result['issues']):
    
                    validation_result['risk_level'] = QueryRisk.CRITICAL
    
                elif any('DROP' in issue or 'DELETE' in issue for issue in validation_result['issues']):
    
                    validation_result['risk_level'] = QueryRisk.HIGH
    
                else:
    
                    validation_result['risk_level'] = QueryRisk.MEDIUM
    
            elif validation_result['warnings']:
    
                validation_result['risk_level'] = QueryRisk.LOW
    
            else:
    
                validation_result['risk_level'] = QueryRisk.LOW
    
    
    
    # Global validator instance
    
    query_validator = QueryValidator()
    
    

    ๐Ÿ—ƒ๏ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ ๋„๊ตฌ

    ํŒ๋งค ๋ถ„์„ ๋„๊ตฌ

    
    # mcp_server/tools/sales_analysis.py
    
    """
    
    Comprehensive sales analysis tool for retail data querying.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from datetime import datetime, timedelta
    
    from .base import DatabaseTool, ToolResult
    
    from .query_validator import query_validator
    
    
    
    class SalesAnalysisTool(DatabaseTool):
    
        """Advanced sales analysis and reporting tool."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="execute_sales_query",
    
                description="Execute sophisticated sales analysis queries with natural language support",
    
                db_provider=db_provider
    
            )
    
            
    
            # Pre-built query templates for common analysis
    
            self.query_templates = {
    
                'daily_sales': """
    
                    SELECT 
    
                        DATE(transaction_date) as sales_date,
    
                        COUNT(*) as transaction_count,
    
                        SUM(total_amount) as total_revenue,
    
                        AVG(total_amount) as avg_transaction_value,
    
                        COUNT(DISTINCT customer_id) as unique_customers
    
                    FROM retail.sales_transactions 
    
                    WHERE transaction_date >= $1 AND transaction_date <= $2
    
                      AND transaction_type = 'sale'
    
                    GROUP BY DATE(transaction_date)
    
                    ORDER BY sales_date DESC
    
                """,
    
                
    
                'top_products': """
    
                    SELECT 
    
                        p.product_name,
    
                        p.brand,
    
                        SUM(sti.quantity) as total_quantity_sold,
    
                        SUM(sti.total_price) as total_revenue,
    
                        COUNT(DISTINCT st.transaction_id) as transaction_count,
    
                        AVG(sti.unit_price) as avg_price
    
                    FROM retail.sales_transaction_items sti
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY p.product_id, p.product_name, p.brand
    
                    ORDER BY total_revenue DESC
    
                    LIMIT $3
    
                """,
    
                
    
                'customer_analysis': """
    
                    SELECT 
    
                        c.customer_id,
    
                        c.first_name || ' ' || c.last_name as customer_name,
    
                        c.loyalty_tier,
    
                        COUNT(st.transaction_id) as transaction_count,
    
                        SUM(st.total_amount) as total_spent,
    
                        AVG(st.total_amount) as avg_transaction_value,
    
                        MAX(st.transaction_date) as last_purchase_date,
    
                        DATE_PART('day', CURRENT_DATE - MAX(st.transaction_date)) as days_since_last_purchase
    
                    FROM retail.customers c
    
                    LEFT JOIN retail.sales_transactions st ON c.customer_id = st.customer_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY c.customer_id, c.first_name, c.last_name, c.loyalty_tier
    
                    HAVING COUNT(st.transaction_id) > 0
    
                    ORDER BY total_spent DESC
    
                    LIMIT $3
    
                """,
    
                
    
                'category_performance': """
    
                    SELECT 
    
                        pc.category_name,
    
                        COUNT(DISTINCT p.product_id) as unique_products,
    
                        SUM(sti.quantity) as total_quantity_sold,
    
                        SUM(sti.total_price) as total_revenue,
    
                        AVG(sti.unit_price) as avg_price,
    
                        COUNT(DISTINCT st.transaction_id) as transaction_count
    
                    FROM retail.product_categories pc
    
                    JOIN retail.products p ON pc.category_id = p.category_id
    
                    JOIN retail.sales_transaction_items sti ON p.product_id = sti.product_id
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    WHERE st.transaction_date >= $1 AND st.transaction_date <= $2
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY pc.category_id, pc.category_name
    
                    ORDER BY total_revenue DESC
    
                """,
    
                
    
                'sales_trends': """
    
                    WITH daily_sales AS (
    
                        SELECT 
    
                            DATE(transaction_date) as sales_date,
    
                            SUM(total_amount) as daily_revenue,
    
                            COUNT(*) as daily_transactions
    
                        FROM retail.sales_transactions 
    
                        WHERE transaction_date >= $1 AND transaction_date <= $2
    
                          AND transaction_type = 'sale'
    
                        GROUP BY DATE(transaction_date)
    
                    ),
    
                    trend_analysis AS (
    
                        SELECT 
    
                            sales_date,
    
                            daily_revenue,
    
                            daily_transactions,
    
                            LAG(daily_revenue, 1) OVER (ORDER BY sales_date) as prev_day_revenue,
    
                            LAG(daily_revenue, 7) OVER (ORDER BY sales_date) as prev_week_revenue,
    
                            AVG(daily_revenue) OVER (
    
                                ORDER BY sales_date 
    
                                ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    
                            ) as rolling_7day_avg
    
                        FROM daily_sales
    
                    )
    
                    SELECT 
    
                        sales_date,
    
                        daily_revenue,
    
                        daily_transactions,
    
                        rolling_7day_avg,
    
                        CASE 
    
                            WHEN prev_day_revenue IS NOT NULL THEN
    
                                ROUND(((daily_revenue - prev_day_revenue) / prev_day_revenue * 100)::numeric, 2)
    
                            ELSE NULL
    
                        END as day_over_day_growth_pct,
    
                        CASE 
    
                            WHEN prev_week_revenue IS NOT NULL THEN
    
                                ROUND(((daily_revenue - prev_week_revenue) / prev_week_revenue * 100)::numeric, 2)
    
                            ELSE NULL
    
                        END as week_over_week_growth_pct
    
                    FROM trend_analysis
    
                    ORDER BY sales_date DESC
    
                """
    
            }
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute sales analysis query."""
    
            
    
            query_type = kwargs.get('query_type', 'custom')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for sales analysis"
    
                )
    
            
    
            try:
    
                if query_type in self.query_templates:
    
                    return await self._execute_template_query(query_type, kwargs)
    
                elif query_type == 'custom':
    
                    return await self._execute_custom_query(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown query type: {query_type}"
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Sales analysis failed: {str(e)}"
    
                )
    
        
    
        async def _execute_template_query(self, query_type: str, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Execute pre-built template query."""
    
            
    
            query = self.query_templates[query_type]
    
            store_id = kwargs['store_id']
    
            
    
            # Default parameters for template queries
    
            start_date = kwargs.get('start_date', (datetime.now() - timedelta(days=30)).date())
    
            end_date = kwargs.get('end_date', datetime.now().date())
    
            limit = kwargs.get('limit', 20)
    
            
    
            # Convert string dates if needed
    
            if isinstance(start_date, str):
    
                start_date = datetime.fromisoformat(start_date).date()
    
            if isinstance(end_date, str):
    
                end_date = datetime.fromisoformat(end_date).date()
    
            
    
            # Execute query with parameters
    
            params = (start_date, end_date, limit) if '$3' in query else (start_date, end_date)
    
            
    
            result = await self.execute_query(query, params, store_id)
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'query_type': query_type,
    
                    'date_range': f"{start_date} to {end_date}",
    
                    'store_id': store_id,
    
                    'analysis_type': 'template'
    
                }
    
            
    
            return result
    
        
    
        async def _execute_custom_query(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Execute custom SQL query with validation."""
    
            
    
            custom_query = kwargs.get('query')
    
            store_id = kwargs['store_id']
    
            
    
            if not custom_query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Custom query is required when query_type is 'custom'"
    
                )
    
            
    
            # Validate the query for security
    
            validation = query_validator.validate_query(custom_query)
    
            
    
            if not validation['is_safe']:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Query validation failed: {', '.join(validation['issues'])}",
    
                    metadata={
    
                        'validation_result': validation,
    
                        'risk_level': validation['risk_level'].value
    
                    }
    
                )
    
            
    
            # Execute validated query
    
            result = await self.execute_query(custom_query, None, store_id)
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'validation_warnings': validation.get('warnings', []),
    
                    'analysis_type': 'custom'
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for the sales analysis tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query_type": {
    
                        "type": "string",
    
                        "enum": list(self.query_templates.keys()) + ["custom"],
    
                        "description": "Type of sales analysis to perform",
    
                        "default": "daily_sales"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for data isolation",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "start_date": {
    
                        "type": "string",
    
                        "format": "date",
    
                        "description": "Start date for analysis (YYYY-MM-DD)"
    
                    },
    
                    "end_date": {
    
                        "type": "string",
    
                        "format": "date",
    
                        "description": "End date for analysis (YYYY-MM-DD)"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "minimum": 1,
    
                        "maximum": 1000,
    
                        "description": "Maximum number of results to return",
    
                        "default": 20
    
                    },
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Custom SQL query (required when query_type is 'custom')"
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    

    ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๋„๊ตฌ

    
    # mcp_server/tools/schema_introspection.py
    
    """
    
    Database schema introspection and metadata tools.
    
    """
    
    from typing import Dict, Any, List
    
    from .base import DatabaseTool, ToolResult, ToolCategory
    
    
    
    class SchemaIntrospectionTool(DatabaseTool):
    
        """Tool for exploring database schema and metadata."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_table_schema",
    
                description="Get detailed schema information for database tables",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.SCHEMA_INTROSPECTION
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute schema introspection."""
    
            
    
            table_name = kwargs.get('table_name')
    
            include_constraints = kwargs.get('include_constraints', True)
    
            include_indexes = kwargs.get('include_indexes', True)
    
            include_statistics = kwargs.get('include_statistics', False)
    
            
    
            try:
    
                if table_name:
    
                    return await self._get_single_table_schema(
    
                        table_name, include_constraints, include_indexes, include_statistics
    
                    )
    
                else:
    
                    return await self._get_all_tables_schema(include_constraints, include_indexes)
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Schema introspection failed: {str(e)}"
    
                )
    
        
    
        async def _get_single_table_schema(
    
            self, 
    
            table_name: str, 
    
            include_constraints: bool,
    
            include_indexes: bool,
    
            include_statistics: bool
    
        ) -> ToolResult:
    
            """Get detailed schema for a single table."""
    
            
    
            schema_info = {
    
                'table_name': table_name,
    
                'columns': [],
    
                'constraints': [],
    
                'indexes': [],
    
                'statistics': {}
    
            }
    
            
    
            async with self.get_connection() as conn:
    
                # Get column information
    
                columns_query = """
    
                    SELECT 
    
                        column_name,
    
                        data_type,
    
                        is_nullable,
    
                        column_default,
    
                        character_maximum_length,
    
                        numeric_precision,
    
                        numeric_scale,
    
                        ordinal_position,
    
                        udt_name
    
                    FROM information_schema.columns
    
                    WHERE table_schema = 'retail' AND table_name = $1
    
                    ORDER BY ordinal_position
    
                """
    
                
    
                columns = await conn.fetch(columns_query, table_name)
    
                schema_info['columns'] = [dict(col) for col in columns]
    
                
    
                # Get constraints if requested
    
                if include_constraints:
    
                    constraints_query = """
    
                        SELECT 
    
                            constraint_name,
    
                            constraint_type,
    
                            column_name,
    
                            foreign_table_name,
    
                            foreign_column_name
    
                        FROM information_schema.table_constraints tc
    
                        LEFT JOIN information_schema.key_column_usage kcu 
    
                            ON tc.constraint_name = kcu.constraint_name
    
                        LEFT JOIN information_schema.referential_constraints rc
    
                            ON tc.constraint_name = rc.constraint_name
    
                        LEFT JOIN information_schema.key_column_usage fkcu
    
                            ON rc.unique_constraint_name = fkcu.constraint_name
    
                        WHERE tc.table_schema = 'retail' AND tc.table_name = $1
    
                    """
    
                    
    
                    constraints = await conn.fetch(constraints_query, table_name)
    
                    schema_info['constraints'] = [dict(const) for const in constraints]
    
                
    
                # Get indexes if requested
    
                if include_indexes:
    
                    indexes_query = """
    
                        SELECT 
    
                            indexname as index_name,
    
                            indexdef as index_definition,
    
                            tablespace
    
                        FROM pg_indexes
    
                        WHERE schemaname = 'retail' AND tablename = $1
    
                    """
    
                    
    
                    indexes = await conn.fetch(indexes_query, table_name)
    
                    schema_info['indexes'] = [dict(idx) for idx in indexes]
    
                
    
                # Get table statistics if requested
    
                if include_statistics:
    
                    stats_query = """
    
                        SELECT 
    
                            n_tup_ins as inserts,
    
                            n_tup_upd as updates,
    
                            n_tup_del as deletes,
    
                            n_live_tup as live_tuples,
    
                            n_dead_tup as dead_tuples,
    
                            last_vacuum,
    
                            last_autovacuum,
    
                            last_analyze,
    
                            last_autoanalyze
    
                        FROM pg_stat_user_tables
    
                        WHERE schemaname = 'retail' AND relname = $1
    
                    """
    
                    
    
                    stats = await conn.fetchrow(stats_query, table_name)
    
                    if stats:
    
                        schema_info['statistics'] = dict(stats)
    
            
    
            return ToolResult(
    
                success=True,
    
                data=schema_info,
    
                metadata={
    
                    'table_name': table_name,
    
                    'schema': 'retail',
    
                    'introspection_type': 'single_table'
    
                }
    
            )
    
        
    
        async def _get_all_tables_schema(
    
            self, 
    
            include_constraints: bool,
    
            include_indexes: bool
    
        ) -> ToolResult:
    
            """Get schema information for all tables."""
    
            
    
            async with self.get_connection() as conn:
    
                # Get all tables in retail schema
    
                tables_query = """
    
                    SELECT 
    
                        table_name,
    
                        table_type
    
                    FROM information_schema.tables
    
                    WHERE table_schema = 'retail'
    
                    ORDER BY table_name
    
                """
    
                
    
                tables = await conn.fetch(tables_query)
    
                schema_info = {
    
                    'schema_name': 'retail',
    
                    'tables': []
    
                }
    
                
    
                for table in tables:
    
                    table_info = {
    
                        'table_name': table['table_name'],
    
                        'table_type': table['table_type'],
    
                        'columns': []
    
                    }
    
                    
    
                    # Get columns for each table
    
                    columns_query = """
    
                        SELECT 
    
                            column_name,
    
                            data_type,
    
                            is_nullable,
    
                            column_default
    
                        FROM information_schema.columns
    
                        WHERE table_schema = 'retail' AND table_name = $1
    
                        ORDER BY ordinal_position
    
                    """
    
                    
    
                    columns = await conn.fetch(columns_query, table['table_name'])
    
                    table_info['columns'] = [dict(col) for col in columns]
    
                    
    
                    schema_info['tables'].append(table_info)
    
            
    
            return ToolResult(
    
                success=True,
    
                data=schema_info,
    
                metadata={
    
                    'schema': 'retail',
    
                    'table_count': len(schema_info['tables']),
    
                    'introspection_type': 'all_tables'
    
                }
    
            )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for schema introspection tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "table_name": {
    
                        "type": "string",
    
                        "description": "Specific table name to introspect (optional - if not provided, all tables are returned)",
    
                        "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
    
                    },
    
                    "include_constraints": {
    
                        "type": "boolean",
    
                        "description": "Include constraint information",
    
                        "default": True
    
                    },
    
                    "include_indexes": {
    
                        "type": "boolean",
    
                        "description": "Include index information",
    
                        "default": True
    
                    },
    
                    "include_statistics": {
    
                        "type": "boolean",
    
                        "description": "Include table statistics",
    
                        "default": False
    
                    }
    
                },
    
                "additionalProperties": False
    
            }
    
    
    
    class MultiTableSchemaTool(DatabaseTool):
    
        """Tool for getting schema information for multiple tables at once."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_multiple_table_schemas",
    
                description="Get schema information for multiple tables efficiently",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.SCHEMA_INTROSPECTION
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute multi-table schema introspection."""
    
            
    
            table_names = kwargs.get('table_names', [])
    
            
    
            if not table_names:
    
                return ToolResult(
    
                    success=False,
    
                    error="At least one table name is required"
    
                )
    
            
    
            try:
    
                schemas = {}
    
                
    
                async with self.get_connection() as conn:
    
                    for table_name in table_names:
    
                        # Get table schema
    
                        schema_query = """
    
                            SELECT 
    
                                c.column_name,
    
                                c.data_type,
    
                                c.is_nullable,
    
                                c.column_default,
    
                                c.character_maximum_length,
    
                                tc.constraint_type,
    
                                kcu.constraint_name
    
                            FROM information_schema.columns c
    
                            LEFT JOIN information_schema.key_column_usage kcu
    
                                ON c.table_name = kcu.table_name 
    
                                AND c.column_name = kcu.column_name
    
                                AND c.table_schema = kcu.table_schema
    
                            LEFT JOIN information_schema.table_constraints tc
    
                                ON kcu.constraint_name = tc.constraint_name
    
                                AND kcu.table_schema = tc.table_schema
    
                            WHERE c.table_schema = 'retail' AND c.table_name = $1
    
                            ORDER BY c.ordinal_position
    
                        """
    
                        
    
                        columns = await conn.fetch(schema_query, table_name)
    
                        
    
                        if columns:
    
                            schemas[table_name] = {
    
                                'table_name': table_name,
    
                                'columns': [dict(col) for col in columns]
    
                            }
    
                        else:
    
                            schemas[table_name] = {
    
                                'table_name': table_name,
    
                                'error': 'Table not found or not accessible'
    
                            }
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=schemas,
    
                    metadata={
    
                        'requested_tables': table_names,
    
                        'found_tables': [name for name, info in schemas.items() if 'error' not in info],
    
                        'missing_tables': [name for name, info in schemas.items() if 'error' in info]
    
                    }
    
                )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Multi-table schema introspection failed: {str(e)}"
    
                )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for multi-table schema tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "table_names": {
    
                        "type": "array",
    
                        "items": {
    
                            "type": "string",
    
                            "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
    
                        },
    
                        "description": "List of table names to get schema information for",
    
                        "minItems": 1,
    
                        "maxItems": 20
    
                    }
    
                },
    
                "required": ["table_names"],
    
                "additionalProperties": False
    
            }
    
    

    ๐Ÿ“Š ๋ถ„์„ ๋ฐ ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋„๊ตฌ

    ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค ๋„๊ตฌ

    
    # mcp_server/tools/business_intelligence.py
    
    """
    
    Advanced business intelligence and analytics tools.
    
    """
    
    from typing import Dict, Any, List
    
    from datetime import datetime, timedelta
    
    from .base import DatabaseTool, ToolResult, ToolCategory
    
    
    
    class BusinessIntelligenceTool(DatabaseTool):
    
        """Advanced analytics tool for business intelligence queries."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="generate_business_insights",
    
                description="Generate comprehensive business intelligence reports and insights",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.ANALYTICS
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute business intelligence analysis."""
    
            
    
            analysis_type = kwargs.get('analysis_type', 'summary')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for business intelligence analysis"
    
                )
    
            
    
            try:
    
                if analysis_type == 'summary':
    
                    return await self._generate_business_summary(kwargs)
    
                elif analysis_type == 'customer_segmentation':
    
                    return await self._analyze_customer_segmentation(kwargs)
    
                elif analysis_type == 'product_performance':
    
                    return await self._analyze_product_performance(kwargs)
    
                elif analysis_type == 'seasonal_trends':
    
                    return await self._analyze_seasonal_trends(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown analysis type: {analysis_type}"
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Business intelligence analysis failed: {str(e)}"
    
                )
    
        
    
        async def _generate_business_summary(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Generate comprehensive business summary."""
    
            
    
            store_id = kwargs['store_id']
    
            days = kwargs.get('days', 30)
    
            
    
            summary_query = """
    
                WITH date_range AS (
    
                    SELECT CURRENT_DATE - INTERVAL '%s days' as start_date,
    
                           CURRENT_DATE as end_date
    
                ),
    
                sales_summary AS (
    
                    SELECT 
    
                        COUNT(*) as total_transactions,
    
                        COUNT(DISTINCT customer_id) as unique_customers,
    
                        SUM(total_amount) as total_revenue,
    
                        AVG(total_amount) as avg_transaction_value,
    
                        COUNT(DISTINCT DATE(transaction_date)) as active_days
    
                    FROM retail.sales_transactions st, date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                product_summary AS (
    
                    SELECT 
    
                        COUNT(DISTINCT p.product_id) as products_sold,
    
                        SUM(sti.quantity) as total_items_sold
    
                    FROM retail.sales_transaction_items sti
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    CROSS JOIN date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                top_category AS (
    
                    SELECT 
    
                        pc.category_name,
    
                        SUM(sti.total_price) as category_revenue
    
                    FROM retail.product_categories pc
    
                    JOIN retail.products p ON pc.category_id = p.category_id
    
                    JOIN retail.sales_transaction_items sti ON p.product_id = sti.product_id
    
                    JOIN retail.sales_transactions st ON sti.transaction_id = st.transaction_id
    
                    CROSS JOIN date_range dr
    
                    WHERE st.transaction_date >= dr.start_date 
    
                      AND st.transaction_date <= dr.end_date
    
                      AND st.transaction_type = 'sale'
    
                    GROUP BY pc.category_name
    
                    ORDER BY category_revenue DESC
    
                    LIMIT 1
    
                )
    
                SELECT 
    
                    ss.*,
    
                    ps.products_sold,
    
                    ps.total_items_sold,
    
                    tc.category_name as top_category,
    
                    tc.category_revenue as top_category_revenue,
    
                    CASE 
    
                        WHEN ss.active_days > 0 THEN ss.total_revenue / ss.active_days
    
                        ELSE 0
    
                    END as avg_daily_revenue
    
                FROM sales_summary ss
    
                CROSS JOIN product_summary ps
    
                CROSS JOIN top_category tc
    
            """ % days
    
            
    
            result = await self.execute_query(summary_query, None, store_id)
    
            
    
            if result.success and result.data:
    
                summary = result.data[0]
    
                
    
                # Add derived insights
    
                insights = {
    
                    'revenue_trend': 'stable',  # Would calculate based on historical data
    
                    'customer_retention': f"{summary.get('unique_customers', 0)} active customers",
    
                    'performance_indicators': {
    
                        'transactions_per_day': round(summary.get('total_transactions', 0) / max(summary.get('active_days', 1), 1), 2),
    
                        'revenue_per_customer': round(summary.get('total_revenue', 0) / max(summary.get('unique_customers', 1), 1), 2),
    
                        'items_per_transaction': round(summary.get('total_items_sold', 0) / max(summary.get('total_transactions', 1), 1), 2)
    
                    }
    
                }
    
                
    
                summary['insights'] = insights
    
                
    
                result.data = [summary]
    
                result.metadata = {
    
                    'analysis_type': 'business_summary',
    
                    'period_days': days,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for business intelligence tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "analysis_type": {
    
                        "type": "string",
    
                        "enum": ["summary", "customer_segmentation", "product_performance", "seasonal_trends"],
    
                        "description": "Type of business intelligence analysis to perform",
    
                        "default": "summary"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for analysis",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "days": {
    
                        "type": "integer",
    
                        "minimum": 1,
    
                        "maximum": 365,
    
                        "description": "Number of days to analyze",
    
                        "default": 30
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    
    
    class UtilityTool(DatabaseTool):
    
        """Utility tool for common operations."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_current_utc_date",
    
                description="Get current UTC date and time for reference",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.UTILITY
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute utility operation."""
    
            
    
            format_type = kwargs.get('format', 'iso')
    
            
    
            try:
    
                async with self.get_connection() as conn:
    
                    if format_type == 'iso':
    
                        query = "SELECT CURRENT_TIMESTAMP AT TIME ZONE 'UTC' as current_utc_datetime"
    
                    elif format_type == 'epoch':
    
                        query = "SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP AT TIME ZONE 'UTC') as current_utc_epoch"
    
                    elif format_type == 'date_only':
    
                        query = "SELECT CURRENT_DATE as current_date"
    
                    else:
    
                        return ToolResult(
    
                            success=False,
    
                            error=f"Unknown format type: {format_type}"
    
                        )
    
                    
    
                    result = await conn.fetchrow(query)
    
                    
    
                    return ToolResult(
    
                        success=True,
    
                        data=dict(result),
    
                        metadata={
    
                            'format_type': format_type,
    
                            'timezone': 'UTC'
    
                        }
    
                    )
    
            
    
            except Exception as e:
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Utility operation failed: {str(e)}"
    
                )
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for utility tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "format": {
    
                        "type": "string",
    
                        "enum": ["iso", "epoch", "date_only"],
    
                        "description": "Format for the returned date/time",
    
                        "default": "iso"
    
                    }
    
                },
    
                "additionalProperties": False
    
            }
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… ๊ณ ๊ธ‰ ๋„๊ตฌ ์•„ํ‚คํ…์ฒ˜: ํฌ๊ด„์ ์ธ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ๊ฐ–์ถ˜ ์ •๊ตํ•œ MCP ๋„๊ตฌ ๊ตฌํ˜„

    โœ… ์ฟผ๋ฆฌ ๊ฒ€์ฆ: SQL ์ธ์ ์…˜ ๊ณต๊ฒฉ์„ ๋ฐฉ์ง€ํ•˜๋Š” ์•ˆ์ „ํ•œ SQL ๊ฒ€์ฆ ๊ตฌ์ถ•

    โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋„๊ตฌ: ๊ฐ•๋ ฅํ•œ ํŒ๋งค ๋ถ„์„ ๋ฐ ์Šคํ‚ค๋งˆ ํƒ์ƒ‰ ๊ธฐ๋Šฅ ์ƒ์„ฑ

    โœ… ๋น„์ฆˆ๋‹ˆ์Šค ์ธํ…”๋ฆฌ์ „์Šค: ํฌ๊ด„์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ํ†ต์ฐฐ๋ ฅ์„ ์œ„ํ•œ ๋ถ„์„ ๋„๊ตฌ ๊ฐœ๋ฐœ

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ์บ์‹ฑ, ์—ฐ๊ฒฐ ํ’€๋ง, ์ฟผ๋ฆฌ ์ตœ์ ํ™” ์ ์šฉ

    โœ… ๋ณด์•ˆ ํ†ตํ•ฉ: ์—ญํ•  ๊ธฐ๋ฐ˜ ์ ‘๊ทผ ์ œ์–ด ๋ฐ ๊ฐ์‚ฌ ๋กœ๊น… ๊ตฌํ˜„

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต 07: ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ํ†ตํ•ฉ์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • MCP ๋„๊ตฌ์™€ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ํ†ตํ•ฉ
  • ์‹œ๋งจํ‹ฑ ์ œํ’ˆ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ ๊ตฌ์ถ•
  • AI ๊ธฐ๋ฐ˜ ์ฟผ๋ฆฌ ์ดํ•ด ๊ตฌํ˜„
  • ์ „ํ†ต์  ์ฟผ๋ฆฌ์™€ ๋ฒกํ„ฐ ์ฟผ๋ฆฌ๋ฅผ ๊ฒฐํ•ฉํ•œ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ ์ƒ์„ฑ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    MCP ๋„๊ตฌ ๊ฐœ๋ฐœ

  • Model Context Protocol Documentation - ๊ณต์‹ MCP ์‚ฌ์–‘
  • FastMCP Framework - Python MCP ๊ตฌํ˜„
  • MCP Tool Patterns - ๋„๊ตฌ ๊ตฌํ˜„ ์˜ˆ์ œ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ

  • SQL Injection Prevention - OWASP ๋ณด์•ˆ ๊ฐ€์ด๋“œ
  • PostgreSQL Security - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • Query Validation Techniques - ์•ˆ์ „ํ•œ ์ฟผ๋ฆฌ ํŒจํ„ด
  • ์„ฑ๋Šฅ ์ตœ์ ํ™”

  • Database Query Optimization - PostgreSQL ์„ฑ๋Šฅ ๊ฐ€์ด๋“œ
  • Connection Pooling Best Practices - ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • Async Python Patterns - ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ฐ€์ด๋“œ
  • ---

    ์ด์ „: ์‹ค์Šต 05: MCP ์„œ๋ฒ„ ๊ตฌํ˜„

    ๋‹ค์Œ: ์‹ค์Šต 07: ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ํ†ตํ•ฉ

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์‹ค์Šต 7-9: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ 07 ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ํ†ตํ•ฉ

    ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ํ†ตํ•ฉ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ Azure OpenAI ์ž„๋ฒ ๋”ฉ๊ณผ PostgreSQL์˜ pgvector ํ™•์žฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ณ  ์‹œ๋งจํ‹ฑ ์œ ์‚ฌ์„ฑ์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ๊ด€๋ จ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•˜๋Š” AI ๊ธฐ๋ฐ˜ ์ œํ’ˆ ๊ฒ€์ƒ‰์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ์ „ํ†ต์ ์ธ ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰์€ ์‚ฌ์šฉ์ž ์˜๋„์™€ ์‹œ๋งจํ‹ฑ ์˜๋ฏธ๋ฅผ ์ œ๋Œ€๋กœ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ์„ ํ™œ์šฉํ•œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰์€ "๋น„ ์˜ค๋Š” ๋‚ ์— ์ ํ•ฉํ•œ ํŽธ์•ˆํ•œ ๋Ÿฌ๋‹ํ™”"์™€ ๊ฐ™์€ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์ œํ’ˆ ์„ค๋ช…์— ์ •ํ™•ํžˆ ๋™์ผํ•œ ๋‹จ์–ด๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์•„๋„ ๊ด€๋ จ ์ œํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

    ์šฐ๋ฆฌ์˜ ๊ตฌํ˜„์€ Azure OpenAI์˜ ๊ฐ•๋ ฅํ•œ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ๊ณผ PostgreSQL์˜ pgvector ํ™•์žฅ์„ ๊ฒฐํ•ฉํ•˜์—ฌ ๊ณ ์„ฑ๋Šฅ, ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ์ง€๋Šฅ์ ์ธ ์ œํ’ˆ ๊ฒ€์ƒ‰ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ํ†ตํ•ฉ: ํ…์ŠคํŠธ ๋ฒกํ„ฐํ™”๋ฅผ ์œ„ํ•œ Azure OpenAI ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ํ†ตํ•ฉ
  • ๊ตฌํ˜„: ํšจ์œจ์ ์ธ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰ ์ž‘์—…์„ ์œ„ํ•œ pgvector ๊ตฌํ˜„
  • ๊ตฌ์ถ•: ์ž์—ฐ์–ด ์ œํ’ˆ ์ฟผ๋ฆฌ๋ฅผ ์œ„ํ•œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ๋„๊ตฌ ๊ฐœ๋ฐœ
  • ์ƒ์„ฑ: ์ „ํ†ต์  ๊ฒ€์ƒ‰๊ณผ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰์„ ๊ฒฐํ•ฉํ•œ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰
  • ์ตœ์ ํ™”: ํ”„๋กœ๋•์…˜ ์„ฑ๋Šฅ์„ ์œ„ํ•œ ๋ฒกํ„ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™”
  • ์„ค๊ณ„: ์ž„๋ฒ ๋”ฉ ์œ ์‚ฌ์„ฑ์„ ํ™œ์šฉํ•œ ์ถ”์ฒœ ์‹œ์Šคํ…œ ์„ค๊ณ„
  • ๐Ÿง  ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์•„ํ‚คํ…์ฒ˜

    ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ํŒŒ์ดํ”„๋ผ์ธ

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                User Query                       โ”‚
    
    โ”‚         "comfortable running shoes"            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚           Azure OpenAI API                     โ”‚
    
    โ”‚        text-embedding-3-small                  โ”‚
    
    โ”‚        Input: Query Text                       โ”‚
    
    โ”‚        Output: 1536-dimensional vector         โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚              pgvector Search                   โ”‚
    
    โ”‚      Cosine Similarity: embedding <=> vector   โ”‚
    
    โ”‚      WHERE similarity > threshold              โ”‚
    
    โ”‚      ORDER BY similarity DESC                  โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚            Ranked Results                      โ”‚
    
    โ”‚    1. Nike Air Zoom (0.89 similarity)         โ”‚
    
    โ”‚    2. Adidas Ultraboost (0.85 similarity)     โ”‚
    
    โ”‚    3. New Balance Fresh Foam (0.82 similarity) โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์ „๋žต

    
    # mcp_server/embeddings/embedding_manager.py
    
    """
    
    Comprehensive embedding management for semantic search.
    
    """
    
    import asyncio
    
    import hashlib
    
    import json
    
    from typing import List, Dict, Any, Optional, Tuple
    
    from datetime import datetime, timedelta
    
    import numpy as np
    
    from azure.ai.projects.aio import AIProjectClient
    
    from azure.identity.aio import DefaultAzureCredential
    
    from azure.core.exceptions import HttpResponseError
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class EmbeddingManager:
    
        """Manage text embeddings for semantic search."""
    
        
    
        def __init__(self, project_endpoint: str, deployment_name: str = "text-embedding-3-small"):
    
            self.project_endpoint = project_endpoint
    
            self.deployment_name = deployment_name
    
            self.credential = DefaultAzureCredential()
    
            self.client = None
    
            
    
            # Embedding configuration
    
            self.embedding_dimension = 1536  # text-embedding-3-small dimension
    
            self.max_tokens = 8000  # Maximum tokens per request
    
            self.batch_size = 100  # Batch processing size
    
            
    
            # Caching configuration
    
            self.embedding_cache = {}
    
            self.cache_ttl = timedelta(hours=24)
    
            
    
            # Rate limiting
    
            self.rate_limit_requests = 1000  # Per minute
    
            self.rate_limit_tokens = 150000  # Per minute
    
            
    
        async def initialize(self):
    
            """Initialize the Azure AI client."""
    
            
    
            try:
    
                self.client = AIProjectClient(
    
                    endpoint=self.project_endpoint,
    
                    credential=self.credential
    
                )
    
                
    
                # Test connection
    
                await self._test_connection()
    
                
    
                logger.info("Embedding manager initialized successfully")
    
                
    
            except Exception as e:
    
                logger.error(f"Failed to initialize embedding manager: {e}")
    
                raise
    
        
    
        async def _test_connection(self):
    
            """Test Azure OpenAI connection."""
    
            
    
            try:
    
                test_embedding = await self.generate_embedding("test connection")
    
                if len(test_embedding) != self.embedding_dimension:
    
                    raise ValueError(f"Unexpected embedding dimension: {len(test_embedding)}")
    
                
    
                logger.info("Azure OpenAI connection test successful")
    
                
    
            except Exception as e:
    
                logger.error(f"Azure OpenAI connection test failed: {e}")
    
                raise
    
        
    
        async def generate_embedding(self, text: str, use_cache: bool = True) -> List[float]:
    
            """Generate embedding for a single text."""
    
            
    
            if not text or not text.strip():
    
                raise ValueError("Text cannot be empty")
    
            
    
            # Check cache first
    
            if use_cache:
    
                cache_key = self._get_cache_key(text)
    
                cached_embedding = self._get_cached_embedding(cache_key)
    
                if cached_embedding:
    
                    return cached_embedding
    
            
    
            try:
    
                # Ensure client is initialized
    
                if not self.client:
    
                    await self.initialize()
    
                
    
                # Generate embedding
    
                response = await self.client.embeddings.create(
    
                    model=self.deployment_name,
    
                    input=text.strip()
    
                )
    
                
    
                embedding = response.data[0].embedding
    
                
    
                # Cache the result
    
                if use_cache:
    
                    self._cache_embedding(cache_key, embedding)
    
                
    
                logger.debug(f"Generated embedding for text (length: {len(text)})")
    
                
    
                return embedding
    
                
    
            except HttpResponseError as e:
    
                logger.error(f"Azure OpenAI API error: {e}")
    
                raise Exception(f"Embedding generation failed: {e}")
    
            except Exception as e:
    
                logger.error(f"Embedding generation error: {e}")
    
                raise
    
        
    
        async def generate_embeddings_batch(
    
            self, 
    
            texts: List[str], 
    
            use_cache: bool = True
    
        ) -> List[List[float]]:
    
            """Generate embeddings for multiple texts efficiently."""
    
            
    
            if not texts:
    
                return []
    
            
    
            embeddings = []
    
            cache_misses = []
    
            cache_miss_indices = []
    
            
    
            # Check cache for each text
    
            for i, text in enumerate(texts):
    
                if not text or not text.strip():
    
                    embeddings.append([])
    
                    continue
    
                    
    
                if use_cache:
    
                    cache_key = self._get_cache_key(text)
    
                    cached_embedding = self._get_cached_embedding(cache_key)
    
                    if cached_embedding:
    
                        embeddings.append(cached_embedding)
    
                        continue
    
                
    
                # Track cache misses
    
                embeddings.append(None)  # Placeholder
    
                cache_misses.append(text.strip())
    
                cache_miss_indices.append(i)
    
            
    
            # Generate embeddings for cache misses
    
            if cache_misses:
    
                try:
    
                    # Process in batches to respect API limits
    
                    for batch_start in range(0, len(cache_misses), self.batch_size):
    
                        batch_end = min(batch_start + self.batch_size, len(cache_misses))
    
                        batch_texts = cache_misses[batch_start:batch_end]
    
                        
    
                        # Generate batch embeddings
    
                        response = await self.client.embeddings.create(
    
                            model=self.deployment_name,
    
                            input=batch_texts
    
                        )
    
                        
    
                        # Process batch results
    
                        for j, embedding_data in enumerate(response.data):
    
                            actual_index = cache_miss_indices[batch_start + j]
    
                            embedding = embedding_data.embedding
    
                            embeddings[actual_index] = embedding
    
                            
    
                            # Cache the result
    
                            if use_cache:
    
                                text = batch_texts[j]
    
                                cache_key = self._get_cache_key(text)
    
                                self._cache_embedding(cache_key, embedding)
    
                        
    
                        # Rate limiting - small delay between batches
    
                        if batch_end < len(cache_misses):
    
                            await asyncio.sleep(0.1)
    
                    
    
                    logger.info(f"Generated {len(cache_misses)} embeddings in batch")
    
                    
    
                except Exception as e:
    
                    logger.error(f"Batch embedding generation failed: {e}")
    
                    raise
    
            
    
            return embeddings
    
        
    
        def _get_cache_key(self, text: str) -> str:
    
            """Generate cache key for text."""
    
            
    
            # Use SHA-256 hash of text + model for cache key
    
            content = f"{self.deployment_name}:{text.strip()}"
    
            return hashlib.sha256(content.encode()).hexdigest()
    
        
    
        def _get_cached_embedding(self, cache_key: str) -> Optional[List[float]]:
    
            """Get embedding from cache if not expired."""
    
            
    
            if cache_key in self.embedding_cache:
    
                embedding_data = self.embedding_cache[cache_key]
    
                
    
                # Check if cache entry is still valid
    
                if datetime.now() - embedding_data['timestamp'] < self.cache_ttl:
    
                    return embedding_data['embedding']
    
                else:
    
                    # Remove expired entry
    
                    del self.embedding_cache[cache_key]
    
            
    
            return None
    
        
    
        def _cache_embedding(self, cache_key: str, embedding: List[float]):
    
            """Cache embedding with timestamp."""
    
            
    
            self.embedding_cache[cache_key] = {
    
                'embedding': embedding,
    
                'timestamp': datetime.now()
    
            }
    
            
    
            # Limit cache size
    
            if len(self.embedding_cache) > 10000:
    
                # Remove oldest entries
    
                oldest_keys = sorted(
    
                    self.embedding_cache.keys(),
    
                    key=lambda k: self.embedding_cache[k]['timestamp']
    
                )[:1000]
    
                
    
                for key in oldest_keys:
    
                    del self.embedding_cache[key]
    
        
    
        async def cleanup(self):
    
            """Cleanup resources."""
    
            
    
            if self.client:
    
                await self.client.close()
    
            
    
            logger.info("Embedding manager cleanup completed")
    
    
    
    # Global embedding manager instance
    
    embedding_manager = EmbeddingManager(
    
        project_endpoint=os.getenv('PROJECT_ENDPOINT'),
    
        deployment_name=os.getenv('EMBEDDING_DEPLOYMENT_NAME', 'text-embedding-3-small')
    
    )
    
    

    ๐Ÿ” ์ œํ’ˆ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ

    ์ž๋™ํ™”๋œ ์ž„๋ฒ ๋”ฉ ํŒŒ์ดํ”„๋ผ์ธ

    
    # mcp_server/embeddings/product_embedder.py
    
    """
    
    Product embedding generation and management.
    
    """
    
    import asyncio
    
    import asyncpg
    
    from typing import List, Dict, Any, Optional
    
    from datetime import datetime
    
    import logging
    
    from .embedding_manager import embedding_manager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ProductEmbedder:
    
        """Generate and manage product embeddings for semantic search."""
    
        
    
        def __init__(self, db_provider):
    
            self.db_provider = db_provider
    
            self.embedding_manager = embedding_manager
    
            
    
            # Text combination strategy for products
    
            self.text_template = "{product_name} {brand} {description} {category} {tags}"
    
            
    
        async def generate_product_embeddings(
    
            self, 
    
            store_id: str,
    
            batch_size: int = 50,
    
            force_regenerate: bool = False
    
        ) -> Dict[str, Any]:
    
            """Generate embeddings for all products in a store."""
    
            
    
            async with self.db_provider.get_connection() as conn:
    
                try:
    
                    # Set store context
    
                    await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Get products that need embeddings
    
                    if force_regenerate:
    
                        products_query = """
    
                            SELECT 
    
                                p.product_id,
    
                                p.product_name,
    
                                p.product_description,
    
                                p.brand,
    
                                pc.category_name,
    
                                array_to_string(p.tags, ' ') as tags_text
    
                            FROM retail.products p
    
                            LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                            WHERE p.is_active = TRUE
    
                            ORDER BY p.created_at DESC
    
                        """
    
                    else:
    
                        products_query = """
    
                            SELECT 
    
                                p.product_id,
    
                                p.product_name,
    
                                p.product_description,
    
                                p.brand,
    
                                pc.category_name,
    
                                array_to_string(p.tags, ' ') as tags_text
    
                            FROM retail.products p
    
                            LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                            LEFT JOIN retail.product_embeddings pe ON p.product_id = pe.product_id
    
                            WHERE p.is_active = TRUE
    
                              AND (pe.product_id IS NULL OR pe.updated_at < p.updated_at)
    
                            ORDER BY p.created_at DESC
    
                        """
    
                    
    
                    products = await conn.fetch(products_query)
    
                    
    
                    if not products:
    
                        return {
    
                            'success': True,
    
                            'message': 'No products need embedding generation',
    
                            'processed_count': 0,
    
                            'store_id': store_id
    
                        }
    
                    
    
                    logger.info(f"Generating embeddings for {len(products)} products in store {store_id}")
    
                    
    
                    # Process products in batches
    
                    processed_count = 0
    
                    
    
                    for i in range(0, len(products), batch_size):
    
                        batch = products[i:i + batch_size]
    
                        await self._process_product_batch(conn, batch, store_id)
    
                        processed_count += len(batch)
    
                        
    
                        logger.info(f"Processed {processed_count}/{len(products)} products")
    
                    
    
                    return {
    
                        'success': True,
    
                        'message': f'Successfully generated embeddings for {processed_count} products',
    
                        'processed_count': processed_count,
    
                        'store_id': store_id,
    
                        'total_products': len(products)
    
                    }
    
                    
    
                except Exception as e:
    
                    logger.error(f"Product embedding generation failed: {e}")
    
                    return {
    
                        'success': False,
    
                        'error': str(e),
    
                        'store_id': store_id
    
                    }
    
        
    
        async def _process_product_batch(
    
            self, 
    
            conn: asyncpg.Connection, 
    
            products: List[Dict], 
    
            store_id: str
    
        ):
    
            """Process a batch of products for embedding generation."""
    
            
    
            # Prepare texts for embedding
    
            texts = []
    
            product_ids = []
    
            
    
            for product in products:
    
                # Combine product information into searchable text
    
                combined_text = self._create_product_text(product)
    
                texts.append(combined_text)
    
                product_ids.append(product['product_id'])
    
            
    
            # Generate embeddings
    
            embeddings = await self.embedding_manager.generate_embeddings_batch(texts)
    
            
    
            # Store embeddings in database
    
            for i, (product_id, embedding) in enumerate(zip(product_ids, embeddings)):
    
                if embedding:  # Skip failed embeddings
    
                    await self._store_product_embedding(
    
                        conn, 
    
                        product_id, 
    
                        store_id, 
    
                        texts[i], 
    
                        embedding
    
                    )
    
        
    
        def _create_product_text(self, product: Dict[str, Any]) -> str:
    
            """Create combined text for product embedding."""
    
            
    
            # Handle None values
    
            product_name = product.get('product_name') or ''
    
            brand = product.get('brand') or ''
    
            description = product.get('product_description') or ''
    
            category = product.get('category_name') or ''
    
            tags = product.get('tags_text') or ''
    
            
    
            # Combine into searchable text
    
            combined_text = self.text_template.format(
    
                product_name=product_name,
    
                brand=brand,
    
                description=description,
    
                category=category,
    
                tags=tags
    
            )
    
            
    
            # Clean up extra whitespace
    
            return ' '.join(combined_text.split())
    
        
    
        async def _store_product_embedding(
    
            self,
    
            conn: asyncpg.Connection,
    
            product_id: str,
    
            store_id: str,
    
            embedding_text: str,
    
            embedding: List[float]
    
        ):
    
            """Store product embedding in database."""
    
            
    
            # Convert embedding to pgvector format
    
            embedding_vector = f"[{','.join(map(str, embedding))}]"
    
            
    
            # Upsert embedding
    
            upsert_query = """
    
                INSERT INTO retail.product_embeddings (
    
                    product_id, store_id, embedding_text, embedding, embedding_model
    
                ) VALUES ($1, $2, $3, $4, $5)
    
                ON CONFLICT (product_id, embedding_model) 
    
                DO UPDATE SET
    
                    store_id = EXCLUDED.store_id,
    
                    embedding_text = EXCLUDED.embedding_text,
    
                    embedding = EXCLUDED.embedding,
    
                    updated_at = CURRENT_TIMESTAMP
    
            """
    
            
    
            await conn.execute(
    
                upsert_query,
    
                product_id,
    
                store_id,
    
                embedding_text,
    
                embedding_vector,
    
                self.embedding_manager.deployment_name
    
            )
    
        
    
        async def update_product_embedding(
    
            self, 
    
            product_id: str, 
    
            store_id: str
    
        ) -> Dict[str, Any]:
    
            """Update embedding for a single product."""
    
            
    
            async with self.db_provider.get_connection() as conn:
    
                try:
    
                    # Set store context
    
                    await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Get product information
    
                    product_query = """
    
                        SELECT 
    
                            p.product_id,
    
                            p.product_name,
    
                            p.product_description,
    
                            p.brand,
    
                            pc.category_name,
    
                            array_to_string(p.tags, ' ') as tags_text
    
                        FROM retail.products p
    
                        LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                        WHERE p.product_id = $1 AND p.is_active = TRUE
    
                    """
    
                    
    
                    product = await conn.fetchrow(product_query, product_id)
    
                    
    
                    if not product:
    
                        return {
    
                            'success': False,
    
                            'error': f'Product {product_id} not found or inactive'
    
                        }
    
                    
    
                    # Generate embedding
    
                    combined_text = self._create_product_text(dict(product))
    
                    embedding = await self.embedding_manager.generate_embedding(combined_text)
    
                    
    
                    # Store embedding
    
                    await self._store_product_embedding(
    
                        conn, product_id, store_id, combined_text, embedding
    
                    )
    
                    
    
                    return {
    
                        'success': True,
    
                        'message': f'Successfully updated embedding for product {product_id}',
    
                        'product_id': product_id,
    
                        'store_id': store_id
    
                    }
    
                    
    
                except Exception as e:
    
                    logger.error(f"Single product embedding update failed: {e}")
    
                    return {
    
                        'success': False,
    
                        'error': str(e),
    
                        'product_id': product_id
    
                    }
    
    
    
    # Global product embedder instance
    
    product_embedder = ProductEmbedder(db_provider)
    
    

    ๐Ÿ”Ž ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ๋„๊ตฌ

    ์‹œ๋งจํ‹ฑ ์ œํ’ˆ ๊ฒ€์ƒ‰ ๋„๊ตฌ

    
    # mcp_server/tools/semantic_search.py
    
    """
    
    Semantic search tools for natural language product queries.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from ..tools.base import DatabaseTool, ToolResult, ToolCategory
    
    from ..embeddings.embedding_manager import embedding_manager
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class SemanticProductSearchTool(DatabaseTool):
    
        """Advanced semantic search tool for products using vector similarity."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="semantic_search_products",
    
                description="Search products using natural language queries with semantic understanding",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.DATABASE_QUERY
    
            self.embedding_manager = embedding_manager
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute semantic product search."""
    
            
    
            query = kwargs.get('query')
    
            store_id = kwargs.get('store_id')
    
            limit = kwargs.get('limit', 20)
    
            similarity_threshold = kwargs.get('similarity_threshold', 0.7)
    
            include_metadata = kwargs.get('include_metadata', True)
    
            
    
            if not query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Search query is required"
    
                )
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for semantic search"
    
                )
    
            
    
            try:
    
                # Generate query embedding
    
                query_embedding = await self.embedding_manager.generate_embedding(query)
    
                
    
                # Perform semantic search
    
                search_results = await self._perform_semantic_search(
    
                    query_embedding,
    
                    store_id,
    
                    limit,
    
                    similarity_threshold,
    
                    include_metadata
    
                )
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=search_results,
    
                    row_count=len(search_results),
    
                    metadata={
    
                        'query': query,
    
                        'store_id': store_id,
    
                        'similarity_threshold': similarity_threshold,
    
                        'search_type': 'semantic'
    
                    }
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Semantic search failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Semantic search failed: {str(e)}"
    
                )
    
        
    
        async def _perform_semantic_search(
    
            self,
    
            query_embedding: List[float],
    
            store_id: str,
    
            limit: int,
    
            similarity_threshold: float,
    
            include_metadata: bool
    
        ) -> List[Dict[str, Any]]:
    
            """Perform vector similarity search."""
    
            
    
            # Convert embedding to PostgreSQL vector format
    
            embedding_vector = f"[{','.join(map(str, query_embedding))}]"
    
            
    
            # Build search query
    
            if include_metadata:
    
                search_query = """
    
                    SELECT 
    
                        p.product_id,
    
                        p.product_name,
    
                        p.brand,
    
                        p.price,
    
                        p.product_description,
    
                        p.current_stock,
    
                        p.rating_average,
    
                        p.rating_count,
    
                        p.tags,
    
                        pc.category_name,
    
                        pe.embedding_text,
    
                        1 - (pe.embedding <=> $1::vector) as similarity_score
    
                    FROM retail.product_embeddings pe
    
                    JOIN retail.products p ON pe.product_id = p.product_id
    
                    LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                    WHERE pe.store_id = $2
    
                      AND p.is_active = TRUE
    
                      AND 1 - (pe.embedding <=> $1::vector) >= $3
    
                    ORDER BY pe.embedding <=> $1::vector
    
                    LIMIT $4
    
                """
    
            else:
    
                search_query = """
    
                    SELECT 
    
                        p.product_id,
    
                        p.product_name,
    
                        p.brand,
    
                        p.price,
    
                        1 - (pe.embedding <=> $1::vector) as similarity_score
    
                    FROM retail.product_embeddings pe
    
                    JOIN retail.products p ON pe.product_id = p.product_id
    
                    WHERE pe.store_id = $2
    
                      AND p.is_active = TRUE
    
                      AND 1 - (pe.embedding <=> $1::vector) >= $3
    
                    ORDER BY pe.embedding <=> $1::vector
    
                    LIMIT $4
    
                """
    
            
    
            async with self.get_connection() as conn:
    
                # Set store context
    
                await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                
    
                # Execute search
    
                results = await conn.fetch(
    
                    search_query,
    
                    embedding_vector,
    
                    store_id,
    
                    similarity_threshold,
    
                    limit
    
                )
    
                
    
                return [dict(result) for result in results]
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for semantic search tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Natural language search query",
    
                        "minLength": 1,
    
                        "maxLength": 500
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for search scope",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of results to return",
    
                        "minimum": 1,
    
                        "maximum": 100,
    
                        "default": 20
    
                    },
    
                    "similarity_threshold": {
    
                        "type": "number",
    
                        "description": "Minimum similarity score (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "include_metadata": {
    
                        "type": "boolean",
    
                        "description": "Include detailed product metadata in results",
    
                        "default": True
    
                    }
    
                },
    
                "required": ["query", "store_id"],
    
                "additionalProperties": False
    
            }
    
    
    
    class HybridSearchTool(DatabaseTool):
    
        """Hybrid search combining traditional keyword and semantic search."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="hybrid_product_search",
    
                description="Hybrid search combining keyword matching and semantic similarity for optimal results",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.DATABASE_QUERY
    
            self.embedding_manager = embedding_manager
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute hybrid product search."""
    
            
    
            query = kwargs.get('query')
    
            store_id = kwargs.get('store_id')
    
            limit = kwargs.get('limit', 20)
    
            semantic_weight = kwargs.get('semantic_weight', 0.7)
    
            keyword_weight = kwargs.get('keyword_weight', 0.3)
    
            
    
            if not query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Search query is required"
    
                )
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for hybrid search"
    
                )
    
            
    
            try:
    
                # Generate query embedding for semantic search
    
                query_embedding = await self.embedding_manager.generate_embedding(query)
    
                
    
                # Perform hybrid search
    
                search_results = await self._perform_hybrid_search(
    
                    query,
    
                    query_embedding,
    
                    store_id,
    
                    limit,
    
                    semantic_weight,
    
                    keyword_weight
    
                )
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=search_results,
    
                    row_count=len(search_results),
    
                    metadata={
    
                        'query': query,
    
                        'store_id': store_id,
    
                        'semantic_weight': semantic_weight,
    
                        'keyword_weight': keyword_weight,
    
                        'search_type': 'hybrid'
    
                    }
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Hybrid search failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Hybrid search failed: {str(e)}"
    
                )
    
        
    
        async def _perform_hybrid_search(
    
            self,
    
            query: str,
    
            query_embedding: List[float],
    
            store_id: str,
    
            limit: int,
    
            semantic_weight: float,
    
            keyword_weight: float
    
        ) -> List[Dict[str, Any]]:
    
            """Perform hybrid search combining keyword and semantic similarity."""
    
            
    
            # Convert embedding to PostgreSQL vector format
    
            embedding_vector = f"[{','.join(map(str, query_embedding))}]"
    
            
    
            # Create search terms for keyword matching
    
            search_terms = ' & '.join(query.lower().split())
    
            
    
            hybrid_query = """
    
                WITH keyword_scores AS (
    
                    SELECT 
    
                        p.product_id,
    
                        ts_rank(
    
                            to_tsvector('english', 
    
                                p.product_name || ' ' || 
    
                                COALESCE(p.product_description, '') || ' ' || 
    
                                COALESCE(p.brand, '') || ' ' ||
    
                                COALESCE(array_to_string(p.tags, ' '), '')
    
                            ),
    
                            plainto_tsquery('english', $2)
    
                        ) as keyword_score
    
                    FROM retail.products p
    
                    WHERE p.is_active = TRUE
    
                      AND p.store_id = $3
    
                      AND (
    
                        to_tsvector('english', 
    
                            p.product_name || ' ' || 
    
                            COALESCE(p.product_description, '') || ' ' || 
    
                            COALESCE(p.brand, '') || ' ' ||
    
                            COALESCE(array_to_string(p.tags, ' '), '')
    
                        ) @@ plainto_tsquery('english', $2)
    
                        OR p.product_name ILIKE '%' || $2 || '%'
    
                        OR p.brand ILIKE '%' || $2 || '%'
    
                      )
    
                ),
    
                semantic_scores AS (
    
                    SELECT 
    
                        pe.product_id,
    
                        1 - (pe.embedding <=> $1::vector) as semantic_score
    
                    FROM retail.product_embeddings pe
    
                    WHERE pe.store_id = $3
    
                      AND 1 - (pe.embedding <=> $1::vector) >= 0.5
    
                ),
    
                combined_scores AS (
    
                    SELECT 
    
                        COALESCE(ks.product_id, ss.product_id) as product_id,
    
                        COALESCE(ks.keyword_score, 0) * $4 as weighted_keyword_score,
    
                        COALESCE(ss.semantic_score, 0) * $5 as weighted_semantic_score,
    
                        COALESCE(ks.keyword_score, 0) * $4 + COALESCE(ss.semantic_score, 0) * $5 as combined_score
    
                    FROM keyword_scores ks
    
                    FULL OUTER JOIN semantic_scores ss ON ks.product_id = ss.product_id
    
                    WHERE COALESCE(ks.keyword_score, 0) * $4 + COALESCE(ss.semantic_score, 0) * $5 > 0
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.current_stock,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    p.tags,
    
                    pc.category_name,
    
                    cs.weighted_keyword_score,
    
                    cs.weighted_semantic_score,
    
                    cs.combined_score
    
                FROM combined_scores cs
    
                JOIN retail.products p ON cs.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE p.is_active = TRUE
    
                ORDER BY cs.combined_score DESC
    
                LIMIT $6
    
            """
    
            
    
            async with self.get_connection() as conn:
    
                # Set store context
    
                await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                
    
                # Execute hybrid search
    
                results = await conn.fetch(
    
                    hybrid_query,
    
                    embedding_vector,  # $1
    
                    query,            # $2
    
                    store_id,         # $3
    
                    keyword_weight,   # $4
    
                    semantic_weight,  # $5
    
                    limit            # $6
    
                )
    
                
    
                return [dict(result) for result in results]
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for hybrid search tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Search query (supports both keywords and natural language)",
    
                        "minLength": 1,
    
                        "maxLength": 500
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for search scope",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of results to return",
    
                        "minimum": 1,
    
                        "maximum": 100,
    
                        "default": 20
    
                    },
    
                    "semantic_weight": {
    
                        "type": "number",
    
                        "description": "Weight for semantic similarity (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "keyword_weight": {
    
                        "type": "number",
    
                        "description": "Weight for keyword matching (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.3
    
                    }
    
                },
    
                "required": ["query", "store_id"],
    
                "additionalProperties": False
    
            }
    
    

    ๐ŸŽฏ ์ถ”์ฒœ ์‹œ์Šคํ…œ

    ์ œํ’ˆ ์ถ”์ฒœ ์—”์ง„

    
    # mcp_server/tools/recommendations.py
    
    """
    
    Product recommendation system using embedding similarity.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from ..tools.base import DatabaseTool, ToolResult, ToolCategory
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ProductRecommendationTool(DatabaseTool):
    
        """Generate product recommendations based on similarity and user behavior."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_product_recommendations",
    
                description="Generate personalized product recommendations using similarity analysis",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.ANALYTICS
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute product recommendation generation."""
    
            
    
            recommendation_type = kwargs.get('type', 'similar_products')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for recommendations"
    
                )
    
            
    
            try:
    
                if recommendation_type == 'similar_products':
    
                    return await self._get_similar_products(kwargs)
    
                elif recommendation_type == 'customer_based':
    
                    return await self._get_customer_recommendations(kwargs)
    
                elif recommendation_type == 'trending':
    
                    return await self._get_trending_products(kwargs)
    
                elif recommendation_type == 'cross_sell':
    
                    return await self._get_cross_sell_recommendations(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown recommendation type: {recommendation_type}"
    
                    )
    
            
    
            except Exception as e:
    
                logger.error(f"Product recommendation failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Recommendation generation failed: {str(e)}"
    
                )
    
        
    
        async def _get_similar_products(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Get products similar to a given product using embedding similarity."""
    
            
    
            product_id = kwargs.get('product_id')
    
            store_id = kwargs['store_id']
    
            limit = kwargs.get('limit', 10)
    
            similarity_threshold = kwargs.get('similarity_threshold', 0.7)
    
            
    
            if not product_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="product_id is required for similar product recommendations"
    
                )
    
            
    
            similar_products_query = """
    
                WITH target_product AS (
    
                    SELECT embedding
    
                    FROM retail.product_embeddings
    
                    WHERE product_id = $1 AND store_id = $2
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    pc.category_name,
    
                    1 - (pe.embedding <=> tp.embedding) as similarity_score
    
                FROM retail.product_embeddings pe
    
                CROSS JOIN target_product tp
    
                JOIN retail.products p ON pe.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE pe.store_id = $2
    
                  AND pe.product_id != $1  -- Exclude the target product itself
    
                  AND p.is_active = TRUE
    
                  AND 1 - (pe.embedding <=> tp.embedding) >= $3
    
                ORDER BY pe.embedding <=> tp.embedding
    
                LIMIT $4
    
            """
    
            
    
            result = await self.execute_query(
    
                similar_products_query,
    
                (product_id, store_id, similarity_threshold, limit),
    
                store_id
    
            )
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'recommendation_type': 'similar_products',
    
                    'target_product_id': product_id,
    
                    'similarity_threshold': similarity_threshold,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        async def _get_customer_recommendations(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Get personalized recommendations based on customer purchase history."""
    
            
    
            customer_id = kwargs.get('customer_id')
    
            store_id = kwargs['store_id']
    
            limit = kwargs.get('limit', 10)
    
            days_back = kwargs.get('days_back', 90)
    
            
    
            if not customer_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="customer_id is required for customer-based recommendations"
    
                )
    
            
    
            customer_recommendations_query = """
    
                WITH customer_purchases AS (
    
                    -- Get products purchased by the customer
    
                    SELECT DISTINCT p.product_id, pe.embedding
    
                    FROM retail.sales_transactions st
    
                    JOIN retail.sales_transaction_items sti ON st.transaction_id = sti.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    JOIN retail.product_embeddings pe ON p.product_id = pe.product_id
    
                    WHERE st.customer_id = $1
    
                      AND st.transaction_date >= CURRENT_DATE - INTERVAL '%s days'
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                avg_customer_embedding AS (
    
                    -- Calculate average embedding vector for customer preferences
    
                    SELECT 
    
                        (
    
                            SELECT ARRAY(
    
                                SELECT AVG(embedding_element)
    
                                FROM customer_purchases cp,
    
                                     LATERAL unnest(cp.embedding) WITH ORDINALITY AS t(embedding_element, ordinality)
    
                                GROUP BY ordinality
    
                                ORDER BY ordinality
    
                            )
    
                        )::vector as avg_embedding
    
                    FROM (SELECT 1) dummy
    
                    WHERE EXISTS (SELECT 1 FROM customer_purchases)
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    pc.category_name,
    
                    1 - (pe.embedding <=> ace.avg_embedding) as preference_score
    
                FROM retail.product_embeddings pe
    
                CROSS JOIN avg_customer_embedding ace
    
                JOIN retail.products p ON pe.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE pe.store_id = $2
    
                  AND p.is_active = TRUE
    
                  AND pe.product_id NOT IN (SELECT product_id FROM customer_purchases)
    
                  AND 1 - (pe.embedding <=> ace.avg_embedding) >= 0.6
    
                ORDER BY pe.embedding <=> ace.avg_embedding
    
                LIMIT $3
    
            """ % days_back
    
            
    
            result = await self.execute_query(
    
                customer_recommendations_query,
    
                (customer_id, store_id, limit),
    
                store_id
    
            )
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'recommendation_type': 'customer_based',
    
                    'customer_id': customer_id,
    
                    'days_back': days_back,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for recommendation tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "type": {
    
                        "type": "string",
    
                        "enum": ["similar_products", "customer_based", "trending", "cross_sell"],
    
                        "description": "Type of recommendation to generate",
    
                        "default": "similar_products"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for recommendations",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "product_id": {
    
                        "type": "string",
    
                        "description": "Product ID for similar product recommendations"
    
                    },
    
                    "customer_id": {
    
                        "type": "string",
    
                        "description": "Customer ID for personalized recommendations"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of recommendations",
    
                        "minimum": 1,
    
                        "maximum": 50,
    
                        "default": 10
    
                    },
    
                    "similarity_threshold": {
    
                        "type": "number",
    
                        "description": "Minimum similarity score",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "days_back": {
    
                        "type": "integer",
    
                        "description": "Days of purchase history to consider",
    
                        "minimum": 1,
    
                        "maximum": 365,
    
                        "default": 90
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    

    โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”

    ๋ฒกํ„ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™”

    
    -- Optimize pgvector performance
    
    -- Add to postgresql.conf
    
    
    
    # Increase work_mem for vector operations
    
    work_mem = '256MB'
    
    
    
    # Optimize shared_buffers for vector data
    
    shared_buffers = '512MB'
    
    
    
    # Enable parallel query execution
    
    max_parallel_workers_per_gather = 4
    
    max_parallel_workers = 8
    
    
    
    # Vector-specific optimizations
    
    SET maintenance_work_mem = '1GB';
    
    SET max_parallel_maintenance_workers = 4;
    
    
    
    -- Optimize HNSW index parameters
    
    CREATE INDEX CONCURRENTLY idx_product_embeddings_vector_optimized 
    
    ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops)
    
    WITH (m = 16, ef_construction = 200);
    
    
    
    -- Create partial indexes for active products only
    
    CREATE INDEX CONCURRENTLY idx_product_embeddings_active
    
    ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops)
    
    WHERE store_id IN (SELECT store_id FROM retail.stores WHERE is_active = TRUE);
    
    
    
    -- Analyze vector distribution for optimization
    
    ANALYZE retail.product_embeddings;
    
    
    
    -- Vector search performance monitoring
    
    CREATE OR REPLACE FUNCTION retail.analyze_vector_performance()
    
    RETURNS TABLE (
    
        avg_search_time_ms NUMERIC,
    
        index_size TEXT,
    
        total_vectors BIGINT,
    
        cache_hit_ratio NUMERIC
    
    ) AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            (SELECT AVG(EXTRACT(MILLISECONDS FROM clock_timestamp() - query_start))
    
             FROM pg_stat_activity 
    
             WHERE query LIKE '%embedding <=> %'
    
             AND state = 'active') as avg_search_time_ms,
    
            pg_size_pretty(pg_relation_size('idx_product_embeddings_vector')) as index_size,
    
            COUNT(*)::BIGINT as total_vectors,
    
            (SELECT 100.0 * blks_hit / (blks_hit + blks_read) 
    
             FROM pg_stat_user_indexes 
    
             WHERE indexrelname = 'idx_product_embeddings_vector') as cache_hit_ratio
    
        FROM retail.product_embeddings;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    

    ์ž„๋ฒ ๋”ฉ ์บ์‹œ ์ „๋žต

    
    # mcp_server/embeddings/cache_manager.py
    
    """
    
    Advanced caching strategy for embeddings and search results.
    
    """
    
    import redis.asyncio as redis
    
    import json
    
    import hashlib
    
    from typing import Dict, Any, List, Optional
    
    from datetime import timedelta
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class EmbeddingCacheManager:
    
        """Advanced caching for embeddings and search results."""
    
        
    
        def __init__(self, redis_url: str = "redis://localhost:6379"):
    
            self.redis_client = None
    
            self.redis_url = redis_url
    
            
    
            # Cache TTL settings
    
            self.embedding_ttl = timedelta(days=7)  # Embeddings cached for 1 week
    
            self.search_ttl = timedelta(hours=1)    # Search results cached for 1 hour
    
            self.recommendation_ttl = timedelta(hours=4)  # Recommendations cached for 4 hours
    
            
    
            # Cache key prefixes
    
            self.EMBEDDING_PREFIX = "emb:"
    
            self.SEARCH_PREFIX = "search:"
    
            self.RECOMMENDATION_PREFIX = "rec:"
    
        
    
        async def initialize(self):
    
            """Initialize Redis connection."""
    
            
    
            try:
    
                self.redis_client = redis.from_url(self.redis_url)
    
                # Test connection
    
                await self.redis_client.ping()
    
                logger.info("Embedding cache manager initialized")
    
            
    
            except Exception as e:
    
                logger.warning(f"Redis cache not available: {e}")
    
                self.redis_client = None
    
        
    
        async def cache_embedding(self, text: str, embedding: List[float], model: str):
    
            """Cache text embedding."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                cache_key = self._get_embedding_key(text, model)
    
                cache_data = {
    
                    'embedding': embedding,
    
                    'model': model,
    
                    'cached_at': str(datetime.utcnow())
    
                }
    
                
    
                await self.redis_client.setex(
    
                    cache_key,
    
                    self.embedding_ttl,
    
                    json.dumps(cache_data)
    
                )
    
                
    
            except Exception as e:
    
                logger.warning(f"Failed to cache embedding: {e}")
    
        
    
        async def get_cached_embedding(self, text: str, model: str) -> Optional[List[float]]:
    
            """Get cached embedding."""
    
            
    
            if not self.redis_client:
    
                return None
    
            
    
            try:
    
                cache_key = self._get_embedding_key(text, model)
    
                cached_data = await self.redis_client.get(cache_key)
    
                
    
                if cached_data:
    
                    data = json.loads(cached_data)
    
                    return data['embedding']
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to retrieve cached embedding: {e}")
    
            
    
            return None
    
        
    
        async def cache_search_results(
    
            self, 
    
            query: str, 
    
            store_id: str, 
    
            results: List[Dict],
    
            search_params: Dict[str, Any]
    
        ):
    
            """Cache search results."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                cache_key = self._get_search_key(query, store_id, search_params)
    
                cache_data = {
    
                    'results': results,
    
                    'query': query,
    
                    'store_id': store_id,
    
                    'params': search_params,
    
                    'cached_at': str(datetime.utcnow())
    
                }
    
                
    
                await self.redis_client.setex(
    
                    cache_key,
    
                    self.search_ttl,
    
                    json.dumps(cache_data, default=str)
    
                )
    
                
    
            except Exception as e:
    
                logger.warning(f"Failed to cache search results: {e}")
    
        
    
        async def get_cached_search_results(
    
            self, 
    
            query: str, 
    
            store_id: str, 
    
            search_params: Dict[str, Any]
    
        ) -> Optional[List[Dict]]:
    
            """Get cached search results."""
    
            
    
            if not self.redis_client:
    
                return None
    
            
    
            try:
    
                cache_key = self._get_search_key(query, store_id, search_params)
    
                cached_data = await self.redis_client.get(cache_key)
    
                
    
                if cached_data:
    
                    data = json.loads(cached_data)
    
                    return data['results']
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to retrieve cached search results: {e}")
    
            
    
            return None
    
        
    
        def _get_embedding_key(self, text: str, model: str) -> str:
    
            """Generate cache key for embedding."""
    
            
    
            content = f"{model}:{text.strip()}"
    
            hash_key = hashlib.sha256(content.encode()).hexdigest()
    
            return f"{self.EMBEDDING_PREFIX}{hash_key}"
    
        
    
        def _get_search_key(self, query: str, store_id: str, params: Dict[str, Any]) -> str:
    
            """Generate cache key for search results."""
    
            
    
            # Create stable hash from query and parameters
    
            content = f"{query}:{store_id}:{json.dumps(params, sort_keys=True)}"
    
            hash_key = hashlib.sha256(content.encode()).hexdigest()
    
            return f"{self.SEARCH_PREFIX}{hash_key}"
    
        
    
        async def invalidate_store_cache(self, store_id: str):
    
            """Invalidate all cached data for a store."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                # Find all keys related to the store
    
                pattern = f"*:{store_id}:*"
    
                keys = await self.redis_client.keys(pattern)
    
                
    
                if keys:
    
                    await self.redis_client.delete(*keys)
    
                    logger.info(f"Invalidated {len(keys)} cache entries for store {store_id}")
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to invalidate store cache: {e}")
    
        
    
        async def cleanup(self):
    
            """Cleanup cache resources."""
    
            
    
            if self.redis_client:
    
                await self.redis_client.close()
    
    
    
    # Global cache manager
    
    cache_manager = EmbeddingCacheManager()
    
    

    ๐ŸŽฏ ์ฃผ์š” ํ•™์Šต ๋‚ด์šฉ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… Azure OpenAI ํ†ตํ•ฉ: ์บ์‹ฑ ๋ฐ ์ตœ์ ํ™”๋ฅผ ํฌํ•จํ•œ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์™„๋ฃŒ

    โœ… ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ตฌํ˜„: pgvector๋ฅผ ํ™œ์šฉํ•œ ํ”„๋กœ๋•์…˜ ์ค€๋น„๋œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰

    โœ… ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ: ํ‚ค์›Œ๋“œ์™€ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰์„ ๊ฒฐํ•ฉํ•˜์—ฌ ์ตœ์ ์˜ ๊ฒฐ๊ณผ ์ œ๊ณต

    โœ… ์ถ”์ฒœ ์‹œ์Šคํ…œ: ์œ ์‚ฌ์„ฑ์„ ํ™œ์šฉํ•œ AI ๊ธฐ๋ฐ˜ ์ œํ’ˆ ์ถ”์ฒœ

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ฒกํ„ฐ ์ธ๋ฑ์Šค ์ตœ์ ํ™” ๋ฐ ์ง€๋Šฅํ˜• ์บ์‹ฑ

    โœ… ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜: ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์ค€๋น„๋œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์ธํ”„๋ผ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต 08: ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์ „๋žต ๊ตฌํ˜„
  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ์„ฑ๋Šฅ ๋ฌธ์ œ ๋””๋ฒ„๊น…
  • ์ž„๋ฒ ๋”ฉ ํ’ˆ์งˆ ๋ฐ ๊ด€๋ จ์„ฑ ๊ฒ€์ฆ
  • ์ถ”์ฒœ ์‹œ์Šคํ…œ ์ •ํ™•์„ฑ ํ…Œ์ŠคํŠธ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    Azure OpenAI

  • Azure OpenAI ์„œ๋น„์Šค ๋ฌธ์„œ - ์„œ๋น„์Šค ๊ฐ€์ด๋“œ ์ „์ฒด
  • ์ž„๋ฒ ๋”ฉ API ์ฐธ์กฐ - API ๋ฌธ์„œ
  • ์ž„๋ฒ ๋”ฉ์„ ์œ„ํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๊ตฌํ˜„ ์ง€์นจ
  • ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

  • pgvector ๋ฌธ์„œ - PostgreSQL ๋ฒกํ„ฐ ํ™•์žฅ
  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ์ตœ์ ํ™” - ์„ฑ๋Šฅ ํŠœ๋‹
  • HNSW ์•Œ๊ณ ๋ฆฌ์ฆ˜ - ๊ณ„์ธต์  ๋„ค๋น„๊ฒŒ์ด๋ธ” ์Šค๋ชฐ ์›”๋“œ ๊ทธ๋ž˜ํ”„
  • ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰

  • ์ •๋ณด ๊ฒ€์ƒ‰ ๊ธฐ๋ณธ - ์Šคํƒ ํฌ๋“œ IR ๊ต์žฌ
  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๊ตฌํ˜„ ํŒจํ„ด
  • ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ ์ „๋žต - ๋‹ค์–‘ํ•œ ๊ฒ€์ƒ‰ ์ ‘๊ทผ๋ฒ• ๊ฒฐํ•ฉ
  • ---

    ์ด์ „: ์‹ค์Šต 06: ๋„๊ตฌ ๊ฐœ๋ฐœ

    ๋‹ค์Œ: ์‹ค์Šต 08: ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    Azure OpenAI ๋ฐ pgvector๋ฅผ ์ด์šฉํ•œ ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ ๊ตฌํ˜„ ์‹ฌํ™”ํ•˜๊ธฐ

    ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ํ†ตํ•ฉ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ Azure OpenAI ์ž„๋ฒ ๋”ฉ๊ณผ PostgreSQL์˜ pgvector ํ™•์žฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ์ดํ•ดํ•˜๊ณ  ์‹œ๋งจํ‹ฑ ์œ ์‚ฌ์„ฑ์— ๊ธฐ๋ฐ˜ํ•˜์—ฌ ๊ด€๋ จ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•˜๋Š” AI ๊ธฐ๋ฐ˜ ์ œํ’ˆ ๊ฒ€์ƒ‰์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ์ „ํ†ต์ ์ธ ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰์€ ์‚ฌ์šฉ์ž ์˜๋„์™€ ์‹œ๋งจํ‹ฑ ์˜๋ฏธ๋ฅผ ์ œ๋Œ€๋กœ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์Šต๋‹ˆ๋‹ค. ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ์„ ํ™œ์šฉํ•œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰์€ "๋น„ ์˜ค๋Š” ๋‚ ์— ์ ํ•ฉํ•œ ํŽธ์•ˆํ•œ ๋Ÿฌ๋‹ํ™”"์™€ ๊ฐ™์€ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ํ†ตํ•ด ์ œํ’ˆ ์„ค๋ช…์— ์ •ํ™•ํžˆ ๋™์ผํ•œ ๋‹จ์–ด๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์•„๋„ ๊ด€๋ จ ์ œํ’ˆ์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค.

    ์šฐ๋ฆฌ์˜ ๊ตฌํ˜„์€ Azure OpenAI์˜ ๊ฐ•๋ ฅํ•œ ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ๊ณผ PostgreSQL์˜ pgvector ํ™•์žฅ์„ ๊ฒฐํ•ฉํ•˜์—ฌ ๊ณ ์„ฑ๋Šฅ, ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์„ ๊ตฌ์ถ•ํ•˜๋ฉฐ, ์ด๋ฅผ ํ†ตํ•ด ์ง€๋Šฅ์ ์ธ ์ œํ’ˆ ๊ฒ€์ƒ‰ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ํ†ตํ•ฉ: ํ…์ŠคํŠธ ๋ฒกํ„ฐํ™”๋ฅผ ์œ„ํ•œ Azure OpenAI ์ž„๋ฒ ๋”ฉ ๋ชจ๋ธ ํ†ตํ•ฉ
  • ๊ตฌํ˜„: ํšจ์œจ์ ์ธ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰ ์ž‘์—…์„ ์œ„ํ•œ pgvector ๊ตฌํ˜„
  • ๊ตฌ์ถ•: ์ž์—ฐ์–ด ์ œํ’ˆ ์ฟผ๋ฆฌ๋ฅผ ์œ„ํ•œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ๋„๊ตฌ ๊ฐœ๋ฐœ
  • ์ƒ์„ฑ: ์ „ํ†ต์  ๊ฒ€์ƒ‰๊ณผ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰์„ ๊ฒฐํ•ฉํ•œ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰
  • ์ตœ์ ํ™”: ํ”„๋กœ๋•์…˜ ์„ฑ๋Šฅ์„ ์œ„ํ•œ ๋ฒกํ„ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™”
  • ์„ค๊ณ„: ์ž„๋ฒ ๋”ฉ ์œ ์‚ฌ์„ฑ์„ ํ™œ์šฉํ•œ ์ถ”์ฒœ ์‹œ์Šคํ…œ ์„ค๊ณ„
  • ๐Ÿง  ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์•„ํ‚คํ…์ฒ˜

    ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ํŒŒ์ดํ”„๋ผ์ธ

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                User Query                       โ”‚
    
    โ”‚         "comfortable running shoes"            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚           Azure OpenAI API                     โ”‚
    
    โ”‚        text-embedding-3-small                  โ”‚
    
    โ”‚        Input: Query Text                       โ”‚
    
    โ”‚        Output: 1536-dimensional vector         โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚              pgvector Search                   โ”‚
    
    โ”‚      Cosine Similarity: embedding <=> vector   โ”‚
    
    โ”‚      WHERE similarity > threshold              โ”‚
    
    โ”‚      ORDER BY similarity DESC                  โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚            Ranked Results                      โ”‚
    
    โ”‚    1. Nike Air Zoom (0.89 similarity)         โ”‚
    
    โ”‚    2. Adidas Ultraboost (0.85 similarity)     โ”‚
    
    โ”‚    3. New Balance Fresh Foam (0.82 similarity) โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์ „๋žต

    
    # mcp_server/embeddings/embedding_manager.py
    
    """
    
    Comprehensive embedding management for semantic search.
    
    """
    
    import asyncio
    
    import hashlib
    
    import json
    
    from typing import List, Dict, Any, Optional, Tuple
    
    from datetime import datetime, timedelta
    
    import numpy as np
    
    from azure.ai.projects.aio import AIProjectClient
    
    from azure.identity.aio import DefaultAzureCredential
    
    from azure.core.exceptions import HttpResponseError
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class EmbeddingManager:
    
        """Manage text embeddings for semantic search."""
    
        
    
        def __init__(self, project_endpoint: str, deployment_name: str = "text-embedding-3-small"):
    
            self.project_endpoint = project_endpoint
    
            self.deployment_name = deployment_name
    
            self.credential = DefaultAzureCredential()
    
            self.client = None
    
            
    
            # Embedding configuration
    
            self.embedding_dimension = 1536  # text-embedding-3-small dimension
    
            self.max_tokens = 8000  # Maximum tokens per request
    
            self.batch_size = 100  # Batch processing size
    
            
    
            # Caching configuration
    
            self.embedding_cache = {}
    
            self.cache_ttl = timedelta(hours=24)
    
            
    
            # Rate limiting
    
            self.rate_limit_requests = 1000  # Per minute
    
            self.rate_limit_tokens = 150000  # Per minute
    
            
    
        async def initialize(self):
    
            """Initialize the Azure AI client."""
    
            
    
            try:
    
                self.client = AIProjectClient(
    
                    endpoint=self.project_endpoint,
    
                    credential=self.credential
    
                )
    
                
    
                # Test connection
    
                await self._test_connection()
    
                
    
                logger.info("Embedding manager initialized successfully")
    
                
    
            except Exception as e:
    
                logger.error(f"Failed to initialize embedding manager: {e}")
    
                raise
    
        
    
        async def _test_connection(self):
    
            """Test Azure OpenAI connection."""
    
            
    
            try:
    
                test_embedding = await self.generate_embedding("test connection")
    
                if len(test_embedding) != self.embedding_dimension:
    
                    raise ValueError(f"Unexpected embedding dimension: {len(test_embedding)}")
    
                
    
                logger.info("Azure OpenAI connection test successful")
    
                
    
            except Exception as e:
    
                logger.error(f"Azure OpenAI connection test failed: {e}")
    
                raise
    
        
    
        async def generate_embedding(self, text: str, use_cache: bool = True) -> List[float]:
    
            """Generate embedding for a single text."""
    
            
    
            if not text or not text.strip():
    
                raise ValueError("Text cannot be empty")
    
            
    
            # Check cache first
    
            if use_cache:
    
                cache_key = self._get_cache_key(text)
    
                cached_embedding = self._get_cached_embedding(cache_key)
    
                if cached_embedding:
    
                    return cached_embedding
    
            
    
            try:
    
                # Ensure client is initialized
    
                if not self.client:
    
                    await self.initialize()
    
                
    
                # Generate embedding
    
                response = await self.client.embeddings.create(
    
                    model=self.deployment_name,
    
                    input=text.strip()
    
                )
    
                
    
                embedding = response.data[0].embedding
    
                
    
                # Cache the result
    
                if use_cache:
    
                    self._cache_embedding(cache_key, embedding)
    
                
    
                logger.debug(f"Generated embedding for text (length: {len(text)})")
    
                
    
                return embedding
    
                
    
            except HttpResponseError as e:
    
                logger.error(f"Azure OpenAI API error: {e}")
    
                raise Exception(f"Embedding generation failed: {e}")
    
            except Exception as e:
    
                logger.error(f"Embedding generation error: {e}")
    
                raise
    
        
    
        async def generate_embeddings_batch(
    
            self, 
    
            texts: List[str], 
    
            use_cache: bool = True
    
        ) -> List[List[float]]:
    
            """Generate embeddings for multiple texts efficiently."""
    
            
    
            if not texts:
    
                return []
    
            
    
            embeddings = []
    
            cache_misses = []
    
            cache_miss_indices = []
    
            
    
            # Check cache for each text
    
            for i, text in enumerate(texts):
    
                if not text or not text.strip():
    
                    embeddings.append([])
    
                    continue
    
                    
    
                if use_cache:
    
                    cache_key = self._get_cache_key(text)
    
                    cached_embedding = self._get_cached_embedding(cache_key)
    
                    if cached_embedding:
    
                        embeddings.append(cached_embedding)
    
                        continue
    
                
    
                # Track cache misses
    
                embeddings.append(None)  # Placeholder
    
                cache_misses.append(text.strip())
    
                cache_miss_indices.append(i)
    
            
    
            # Generate embeddings for cache misses
    
            if cache_misses:
    
                try:
    
                    # Process in batches to respect API limits
    
                    for batch_start in range(0, len(cache_misses), self.batch_size):
    
                        batch_end = min(batch_start + self.batch_size, len(cache_misses))
    
                        batch_texts = cache_misses[batch_start:batch_end]
    
                        
    
                        # Generate batch embeddings
    
                        response = await self.client.embeddings.create(
    
                            model=self.deployment_name,
    
                            input=batch_texts
    
                        )
    
                        
    
                        # Process batch results
    
                        for j, embedding_data in enumerate(response.data):
    
                            actual_index = cache_miss_indices[batch_start + j]
    
                            embedding = embedding_data.embedding
    
                            embeddings[actual_index] = embedding
    
                            
    
                            # Cache the result
    
                            if use_cache:
    
                                text = batch_texts[j]
    
                                cache_key = self._get_cache_key(text)
    
                                self._cache_embedding(cache_key, embedding)
    
                        
    
                        # Rate limiting - small delay between batches
    
                        if batch_end < len(cache_misses):
    
                            await asyncio.sleep(0.1)
    
                    
    
                    logger.info(f"Generated {len(cache_misses)} embeddings in batch")
    
                    
    
                except Exception as e:
    
                    logger.error(f"Batch embedding generation failed: {e}")
    
                    raise
    
            
    
            return embeddings
    
        
    
        def _get_cache_key(self, text: str) -> str:
    
            """Generate cache key for text."""
    
            
    
            # Use SHA-256 hash of text + model for cache key
    
            content = f"{self.deployment_name}:{text.strip()}"
    
            return hashlib.sha256(content.encode()).hexdigest()
    
        
    
        def _get_cached_embedding(self, cache_key: str) -> Optional[List[float]]:
    
            """Get embedding from cache if not expired."""
    
            
    
            if cache_key in self.embedding_cache:
    
                embedding_data = self.embedding_cache[cache_key]
    
                
    
                # Check if cache entry is still valid
    
                if datetime.now() - embedding_data['timestamp'] < self.cache_ttl:
    
                    return embedding_data['embedding']
    
                else:
    
                    # Remove expired entry
    
                    del self.embedding_cache[cache_key]
    
            
    
            return None
    
        
    
        def _cache_embedding(self, cache_key: str, embedding: List[float]):
    
            """Cache embedding with timestamp."""
    
            
    
            self.embedding_cache[cache_key] = {
    
                'embedding': embedding,
    
                'timestamp': datetime.now()
    
            }
    
            
    
            # Limit cache size
    
            if len(self.embedding_cache) > 10000:
    
                # Remove oldest entries
    
                oldest_keys = sorted(
    
                    self.embedding_cache.keys(),
    
                    key=lambda k: self.embedding_cache[k]['timestamp']
    
                )[:1000]
    
                
    
                for key in oldest_keys:
    
                    del self.embedding_cache[key]
    
        
    
        async def cleanup(self):
    
            """Cleanup resources."""
    
            
    
            if self.client:
    
                await self.client.close()
    
            
    
            logger.info("Embedding manager cleanup completed")
    
    
    
    # Global embedding manager instance
    
    embedding_manager = EmbeddingManager(
    
        project_endpoint=os.getenv('PROJECT_ENDPOINT'),
    
        deployment_name=os.getenv('EMBEDDING_DEPLOYMENT_NAME', 'text-embedding-3-small')
    
    )
    
    

    ๐Ÿ” ์ œํ’ˆ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ

    ์ž๋™ํ™”๋œ ์ž„๋ฒ ๋”ฉ ํŒŒ์ดํ”„๋ผ์ธ

    
    # mcp_server/embeddings/product_embedder.py
    
    """
    
    Product embedding generation and management.
    
    """
    
    import asyncio
    
    import asyncpg
    
    from typing import List, Dict, Any, Optional
    
    from datetime import datetime
    
    import logging
    
    from .embedding_manager import embedding_manager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ProductEmbedder:
    
        """Generate and manage product embeddings for semantic search."""
    
        
    
        def __init__(self, db_provider):
    
            self.db_provider = db_provider
    
            self.embedding_manager = embedding_manager
    
            
    
            # Text combination strategy for products
    
            self.text_template = "{product_name} {brand} {description} {category} {tags}"
    
            
    
        async def generate_product_embeddings(
    
            self, 
    
            store_id: str,
    
            batch_size: int = 50,
    
            force_regenerate: bool = False
    
        ) -> Dict[str, Any]:
    
            """Generate embeddings for all products in a store."""
    
            
    
            async with self.db_provider.get_connection() as conn:
    
                try:
    
                    # Set store context
    
                    await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Get products that need embeddings
    
                    if force_regenerate:
    
                        products_query = """
    
                            SELECT 
    
                                p.product_id,
    
                                p.product_name,
    
                                p.product_description,
    
                                p.brand,
    
                                pc.category_name,
    
                                array_to_string(p.tags, ' ') as tags_text
    
                            FROM retail.products p
    
                            LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                            WHERE p.is_active = TRUE
    
                            ORDER BY p.created_at DESC
    
                        """
    
                    else:
    
                        products_query = """
    
                            SELECT 
    
                                p.product_id,
    
                                p.product_name,
    
                                p.product_description,
    
                                p.brand,
    
                                pc.category_name,
    
                                array_to_string(p.tags, ' ') as tags_text
    
                            FROM retail.products p
    
                            LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                            LEFT JOIN retail.product_embeddings pe ON p.product_id = pe.product_id
    
                            WHERE p.is_active = TRUE
    
                              AND (pe.product_id IS NULL OR pe.updated_at < p.updated_at)
    
                            ORDER BY p.created_at DESC
    
                        """
    
                    
    
                    products = await conn.fetch(products_query)
    
                    
    
                    if not products:
    
                        return {
    
                            'success': True,
    
                            'message': 'No products need embedding generation',
    
                            'processed_count': 0,
    
                            'store_id': store_id
    
                        }
    
                    
    
                    logger.info(f"Generating embeddings for {len(products)} products in store {store_id}")
    
                    
    
                    # Process products in batches
    
                    processed_count = 0
    
                    
    
                    for i in range(0, len(products), batch_size):
    
                        batch = products[i:i + batch_size]
    
                        await self._process_product_batch(conn, batch, store_id)
    
                        processed_count += len(batch)
    
                        
    
                        logger.info(f"Processed {processed_count}/{len(products)} products")
    
                    
    
                    return {
    
                        'success': True,
    
                        'message': f'Successfully generated embeddings for {processed_count} products',
    
                        'processed_count': processed_count,
    
                        'store_id': store_id,
    
                        'total_products': len(products)
    
                    }
    
                    
    
                except Exception as e:
    
                    logger.error(f"Product embedding generation failed: {e}")
    
                    return {
    
                        'success': False,
    
                        'error': str(e),
    
                        'store_id': store_id
    
                    }
    
        
    
        async def _process_product_batch(
    
            self, 
    
            conn: asyncpg.Connection, 
    
            products: List[Dict], 
    
            store_id: str
    
        ):
    
            """Process a batch of products for embedding generation."""
    
            
    
            # Prepare texts for embedding
    
            texts = []
    
            product_ids = []
    
            
    
            for product in products:
    
                # Combine product information into searchable text
    
                combined_text = self._create_product_text(product)
    
                texts.append(combined_text)
    
                product_ids.append(product['product_id'])
    
            
    
            # Generate embeddings
    
            embeddings = await self.embedding_manager.generate_embeddings_batch(texts)
    
            
    
            # Store embeddings in database
    
            for i, (product_id, embedding) in enumerate(zip(product_ids, embeddings)):
    
                if embedding:  # Skip failed embeddings
    
                    await self._store_product_embedding(
    
                        conn, 
    
                        product_id, 
    
                        store_id, 
    
                        texts[i], 
    
                        embedding
    
                    )
    
        
    
        def _create_product_text(self, product: Dict[str, Any]) -> str:
    
            """Create combined text for product embedding."""
    
            
    
            # Handle None values
    
            product_name = product.get('product_name') or ''
    
            brand = product.get('brand') or ''
    
            description = product.get('product_description') or ''
    
            category = product.get('category_name') or ''
    
            tags = product.get('tags_text') or ''
    
            
    
            # Combine into searchable text
    
            combined_text = self.text_template.format(
    
                product_name=product_name,
    
                brand=brand,
    
                description=description,
    
                category=category,
    
                tags=tags
    
            )
    
            
    
            # Clean up extra whitespace
    
            return ' '.join(combined_text.split())
    
        
    
        async def _store_product_embedding(
    
            self,
    
            conn: asyncpg.Connection,
    
            product_id: str,
    
            store_id: str,
    
            embedding_text: str,
    
            embedding: List[float]
    
        ):
    
            """Store product embedding in database."""
    
            
    
            # Convert embedding to pgvector format
    
            embedding_vector = f"[{','.join(map(str, embedding))}]"
    
            
    
            # Upsert embedding
    
            upsert_query = """
    
                INSERT INTO retail.product_embeddings (
    
                    product_id, store_id, embedding_text, embedding, embedding_model
    
                ) VALUES ($1, $2, $3, $4, $5)
    
                ON CONFLICT (product_id, embedding_model) 
    
                DO UPDATE SET
    
                    store_id = EXCLUDED.store_id,
    
                    embedding_text = EXCLUDED.embedding_text,
    
                    embedding = EXCLUDED.embedding,
    
                    updated_at = CURRENT_TIMESTAMP
    
            """
    
            
    
            await conn.execute(
    
                upsert_query,
    
                product_id,
    
                store_id,
    
                embedding_text,
    
                embedding_vector,
    
                self.embedding_manager.deployment_name
    
            )
    
        
    
        async def update_product_embedding(
    
            self, 
    
            product_id: str, 
    
            store_id: str
    
        ) -> Dict[str, Any]:
    
            """Update embedding for a single product."""
    
            
    
            async with self.db_provider.get_connection() as conn:
    
                try:
    
                    # Set store context
    
                    await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                    
    
                    # Get product information
    
                    product_query = """
    
                        SELECT 
    
                            p.product_id,
    
                            p.product_name,
    
                            p.product_description,
    
                            p.brand,
    
                            pc.category_name,
    
                            array_to_string(p.tags, ' ') as tags_text
    
                        FROM retail.products p
    
                        LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                        WHERE p.product_id = $1 AND p.is_active = TRUE
    
                    """
    
                    
    
                    product = await conn.fetchrow(product_query, product_id)
    
                    
    
                    if not product:
    
                        return {
    
                            'success': False,
    
                            'error': f'Product {product_id} not found or inactive'
    
                        }
    
                    
    
                    # Generate embedding
    
                    combined_text = self._create_product_text(dict(product))
    
                    embedding = await self.embedding_manager.generate_embedding(combined_text)
    
                    
    
                    # Store embedding
    
                    await self._store_product_embedding(
    
                        conn, product_id, store_id, combined_text, embedding
    
                    )
    
                    
    
                    return {
    
                        'success': True,
    
                        'message': f'Successfully updated embedding for product {product_id}',
    
                        'product_id': product_id,
    
                        'store_id': store_id
    
                    }
    
                    
    
                except Exception as e:
    
                    logger.error(f"Single product embedding update failed: {e}")
    
                    return {
    
                        'success': False,
    
                        'error': str(e),
    
                        'product_id': product_id
    
                    }
    
    
    
    # Global product embedder instance
    
    product_embedder = ProductEmbedder(db_provider)
    
    

    ๐Ÿ”Ž ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ๋„๊ตฌ

    ์‹œ๋งจํ‹ฑ ์ œํ’ˆ ๊ฒ€์ƒ‰ ๋„๊ตฌ

    
    # mcp_server/tools/semantic_search.py
    
    """
    
    Semantic search tools for natural language product queries.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from ..tools.base import DatabaseTool, ToolResult, ToolCategory
    
    from ..embeddings.embedding_manager import embedding_manager
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class SemanticProductSearchTool(DatabaseTool):
    
        """Advanced semantic search tool for products using vector similarity."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="semantic_search_products",
    
                description="Search products using natural language queries with semantic understanding",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.DATABASE_QUERY
    
            self.embedding_manager = embedding_manager
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute semantic product search."""
    
            
    
            query = kwargs.get('query')
    
            store_id = kwargs.get('store_id')
    
            limit = kwargs.get('limit', 20)
    
            similarity_threshold = kwargs.get('similarity_threshold', 0.7)
    
            include_metadata = kwargs.get('include_metadata', True)
    
            
    
            if not query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Search query is required"
    
                )
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for semantic search"
    
                )
    
            
    
            try:
    
                # Generate query embedding
    
                query_embedding = await self.embedding_manager.generate_embedding(query)
    
                
    
                # Perform semantic search
    
                search_results = await self._perform_semantic_search(
    
                    query_embedding,
    
                    store_id,
    
                    limit,
    
                    similarity_threshold,
    
                    include_metadata
    
                )
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=search_results,
    
                    row_count=len(search_results),
    
                    metadata={
    
                        'query': query,
    
                        'store_id': store_id,
    
                        'similarity_threshold': similarity_threshold,
    
                        'search_type': 'semantic'
    
                    }
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Semantic search failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Semantic search failed: {str(e)}"
    
                )
    
        
    
        async def _perform_semantic_search(
    
            self,
    
            query_embedding: List[float],
    
            store_id: str,
    
            limit: int,
    
            similarity_threshold: float,
    
            include_metadata: bool
    
        ) -> List[Dict[str, Any]]:
    
            """Perform vector similarity search."""
    
            
    
            # Convert embedding to PostgreSQL vector format
    
            embedding_vector = f"[{','.join(map(str, query_embedding))}]"
    
            
    
            # Build search query
    
            if include_metadata:
    
                search_query = """
    
                    SELECT 
    
                        p.product_id,
    
                        p.product_name,
    
                        p.brand,
    
                        p.price,
    
                        p.product_description,
    
                        p.current_stock,
    
                        p.rating_average,
    
                        p.rating_count,
    
                        p.tags,
    
                        pc.category_name,
    
                        pe.embedding_text,
    
                        1 - (pe.embedding <=> $1::vector) as similarity_score
    
                    FROM retail.product_embeddings pe
    
                    JOIN retail.products p ON pe.product_id = p.product_id
    
                    LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                    WHERE pe.store_id = $2
    
                      AND p.is_active = TRUE
    
                      AND 1 - (pe.embedding <=> $1::vector) >= $3
    
                    ORDER BY pe.embedding <=> $1::vector
    
                    LIMIT $4
    
                """
    
            else:
    
                search_query = """
    
                    SELECT 
    
                        p.product_id,
    
                        p.product_name,
    
                        p.brand,
    
                        p.price,
    
                        1 - (pe.embedding <=> $1::vector) as similarity_score
    
                    FROM retail.product_embeddings pe
    
                    JOIN retail.products p ON pe.product_id = p.product_id
    
                    WHERE pe.store_id = $2
    
                      AND p.is_active = TRUE
    
                      AND 1 - (pe.embedding <=> $1::vector) >= $3
    
                    ORDER BY pe.embedding <=> $1::vector
    
                    LIMIT $4
    
                """
    
            
    
            async with self.get_connection() as conn:
    
                # Set store context
    
                await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                
    
                # Execute search
    
                results = await conn.fetch(
    
                    search_query,
    
                    embedding_vector,
    
                    store_id,
    
                    similarity_threshold,
    
                    limit
    
                )
    
                
    
                return [dict(result) for result in results]
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for semantic search tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Natural language search query",
    
                        "minLength": 1,
    
                        "maxLength": 500
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for search scope",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of results to return",
    
                        "minimum": 1,
    
                        "maximum": 100,
    
                        "default": 20
    
                    },
    
                    "similarity_threshold": {
    
                        "type": "number",
    
                        "description": "Minimum similarity score (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "include_metadata": {
    
                        "type": "boolean",
    
                        "description": "Include detailed product metadata in results",
    
                        "default": True
    
                    }
    
                },
    
                "required": ["query", "store_id"],
    
                "additionalProperties": False
    
            }
    
    
    
    class HybridSearchTool(DatabaseTool):
    
        """Hybrid search combining traditional keyword and semantic search."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="hybrid_product_search",
    
                description="Hybrid search combining keyword matching and semantic similarity for optimal results",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.DATABASE_QUERY
    
            self.embedding_manager = embedding_manager
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute hybrid product search."""
    
            
    
            query = kwargs.get('query')
    
            store_id = kwargs.get('store_id')
    
            limit = kwargs.get('limit', 20)
    
            semantic_weight = kwargs.get('semantic_weight', 0.7)
    
            keyword_weight = kwargs.get('keyword_weight', 0.3)
    
            
    
            if not query:
    
                return ToolResult(
    
                    success=False,
    
                    error="Search query is required"
    
                )
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for hybrid search"
    
                )
    
            
    
            try:
    
                # Generate query embedding for semantic search
    
                query_embedding = await self.embedding_manager.generate_embedding(query)
    
                
    
                # Perform hybrid search
    
                search_results = await self._perform_hybrid_search(
    
                    query,
    
                    query_embedding,
    
                    store_id,
    
                    limit,
    
                    semantic_weight,
    
                    keyword_weight
    
                )
    
                
    
                return ToolResult(
    
                    success=True,
    
                    data=search_results,
    
                    row_count=len(search_results),
    
                    metadata={
    
                        'query': query,
    
                        'store_id': store_id,
    
                        'semantic_weight': semantic_weight,
    
                        'keyword_weight': keyword_weight,
    
                        'search_type': 'hybrid'
    
                    }
    
                )
    
                
    
            except Exception as e:
    
                logger.error(f"Hybrid search failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Hybrid search failed: {str(e)}"
    
                )
    
        
    
        async def _perform_hybrid_search(
    
            self,
    
            query: str,
    
            query_embedding: List[float],
    
            store_id: str,
    
            limit: int,
    
            semantic_weight: float,
    
            keyword_weight: float
    
        ) -> List[Dict[str, Any]]:
    
            """Perform hybrid search combining keyword and semantic similarity."""
    
            
    
            # Convert embedding to PostgreSQL vector format
    
            embedding_vector = f"[{','.join(map(str, query_embedding))}]"
    
            
    
            # Create search terms for keyword matching
    
            search_terms = ' & '.join(query.lower().split())
    
            
    
            hybrid_query = """
    
                WITH keyword_scores AS (
    
                    SELECT 
    
                        p.product_id,
    
                        ts_rank(
    
                            to_tsvector('english', 
    
                                p.product_name || ' ' || 
    
                                COALESCE(p.product_description, '') || ' ' || 
    
                                COALESCE(p.brand, '') || ' ' ||
    
                                COALESCE(array_to_string(p.tags, ' '), '')
    
                            ),
    
                            plainto_tsquery('english', $2)
    
                        ) as keyword_score
    
                    FROM retail.products p
    
                    WHERE p.is_active = TRUE
    
                      AND p.store_id = $3
    
                      AND (
    
                        to_tsvector('english', 
    
                            p.product_name || ' ' || 
    
                            COALESCE(p.product_description, '') || ' ' || 
    
                            COALESCE(p.brand, '') || ' ' ||
    
                            COALESCE(array_to_string(p.tags, ' '), '')
    
                        ) @@ plainto_tsquery('english', $2)
    
                        OR p.product_name ILIKE '%' || $2 || '%'
    
                        OR p.brand ILIKE '%' || $2 || '%'
    
                      )
    
                ),
    
                semantic_scores AS (
    
                    SELECT 
    
                        pe.product_id,
    
                        1 - (pe.embedding <=> $1::vector) as semantic_score
    
                    FROM retail.product_embeddings pe
    
                    WHERE pe.store_id = $3
    
                      AND 1 - (pe.embedding <=> $1::vector) >= 0.5
    
                ),
    
                combined_scores AS (
    
                    SELECT 
    
                        COALESCE(ks.product_id, ss.product_id) as product_id,
    
                        COALESCE(ks.keyword_score, 0) * $4 as weighted_keyword_score,
    
                        COALESCE(ss.semantic_score, 0) * $5 as weighted_semantic_score,
    
                        COALESCE(ks.keyword_score, 0) * $4 + COALESCE(ss.semantic_score, 0) * $5 as combined_score
    
                    FROM keyword_scores ks
    
                    FULL OUTER JOIN semantic_scores ss ON ks.product_id = ss.product_id
    
                    WHERE COALESCE(ks.keyword_score, 0) * $4 + COALESCE(ss.semantic_score, 0) * $5 > 0
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.current_stock,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    p.tags,
    
                    pc.category_name,
    
                    cs.weighted_keyword_score,
    
                    cs.weighted_semantic_score,
    
                    cs.combined_score
    
                FROM combined_scores cs
    
                JOIN retail.products p ON cs.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE p.is_active = TRUE
    
                ORDER BY cs.combined_score DESC
    
                LIMIT $6
    
            """
    
            
    
            async with self.get_connection() as conn:
    
                # Set store context
    
                await conn.execute("SELECT retail.set_store_context($1)", store_id)
    
                
    
                # Execute hybrid search
    
                results = await conn.fetch(
    
                    hybrid_query,
    
                    embedding_vector,  # $1
    
                    query,            # $2
    
                    store_id,         # $3
    
                    keyword_weight,   # $4
    
                    semantic_weight,  # $5
    
                    limit            # $6
    
                )
    
                
    
                return [dict(result) for result in results]
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for hybrid search tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "query": {
    
                        "type": "string",
    
                        "description": "Search query (supports both keywords and natural language)",
    
                        "minLength": 1,
    
                        "maxLength": 500
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for search scope",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of results to return",
    
                        "minimum": 1,
    
                        "maximum": 100,
    
                        "default": 20
    
                    },
    
                    "semantic_weight": {
    
                        "type": "number",
    
                        "description": "Weight for semantic similarity (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "keyword_weight": {
    
                        "type": "number",
    
                        "description": "Weight for keyword matching (0.0 to 1.0)",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.3
    
                    }
    
                },
    
                "required": ["query", "store_id"],
    
                "additionalProperties": False
    
            }
    
    

    ๐ŸŽฏ ์ถ”์ฒœ ์‹œ์Šคํ…œ

    ์ œํ’ˆ ์ถ”์ฒœ ์—”์ง„

    
    # mcp_server/tools/recommendations.py
    
    """
    
    Product recommendation system using embedding similarity.
    
    """
    
    from typing import Dict, Any, List, Optional
    
    from ..tools.base import DatabaseTool, ToolResult, ToolCategory
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class ProductRecommendationTool(DatabaseTool):
    
        """Generate product recommendations based on similarity and user behavior."""
    
        
    
        def __init__(self, db_provider):
    
            super().__init__(
    
                name="get_product_recommendations",
    
                description="Generate personalized product recommendations using similarity analysis",
    
                db_provider=db_provider
    
            )
    
            self.category = ToolCategory.ANALYTICS
    
        
    
        async def execute(self, **kwargs) -> ToolResult:
    
            """Execute product recommendation generation."""
    
            
    
            recommendation_type = kwargs.get('type', 'similar_products')
    
            store_id = kwargs.get('store_id')
    
            
    
            if not store_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="store_id is required for recommendations"
    
                )
    
            
    
            try:
    
                if recommendation_type == 'similar_products':
    
                    return await self._get_similar_products(kwargs)
    
                elif recommendation_type == 'customer_based':
    
                    return await self._get_customer_recommendations(kwargs)
    
                elif recommendation_type == 'trending':
    
                    return await self._get_trending_products(kwargs)
    
                elif recommendation_type == 'cross_sell':
    
                    return await self._get_cross_sell_recommendations(kwargs)
    
                else:
    
                    return ToolResult(
    
                        success=False,
    
                        error=f"Unknown recommendation type: {recommendation_type}"
    
                    )
    
            
    
            except Exception as e:
    
                logger.error(f"Product recommendation failed: {e}")
    
                return ToolResult(
    
                    success=False,
    
                    error=f"Recommendation generation failed: {str(e)}"
    
                )
    
        
    
        async def _get_similar_products(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Get products similar to a given product using embedding similarity."""
    
            
    
            product_id = kwargs.get('product_id')
    
            store_id = kwargs['store_id']
    
            limit = kwargs.get('limit', 10)
    
            similarity_threshold = kwargs.get('similarity_threshold', 0.7)
    
            
    
            if not product_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="product_id is required for similar product recommendations"
    
                )
    
            
    
            similar_products_query = """
    
                WITH target_product AS (
    
                    SELECT embedding
    
                    FROM retail.product_embeddings
    
                    WHERE product_id = $1 AND store_id = $2
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    pc.category_name,
    
                    1 - (pe.embedding <=> tp.embedding) as similarity_score
    
                FROM retail.product_embeddings pe
    
                CROSS JOIN target_product tp
    
                JOIN retail.products p ON pe.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE pe.store_id = $2
    
                  AND pe.product_id != $1  -- Exclude the target product itself
    
                  AND p.is_active = TRUE
    
                  AND 1 - (pe.embedding <=> tp.embedding) >= $3
    
                ORDER BY pe.embedding <=> tp.embedding
    
                LIMIT $4
    
            """
    
            
    
            result = await self.execute_query(
    
                similar_products_query,
    
                (product_id, store_id, similarity_threshold, limit),
    
                store_id
    
            )
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'recommendation_type': 'similar_products',
    
                    'target_product_id': product_id,
    
                    'similarity_threshold': similarity_threshold,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        async def _get_customer_recommendations(self, kwargs: Dict[str, Any]) -> ToolResult:
    
            """Get personalized recommendations based on customer purchase history."""
    
            
    
            customer_id = kwargs.get('customer_id')
    
            store_id = kwargs['store_id']
    
            limit = kwargs.get('limit', 10)
    
            days_back = kwargs.get('days_back', 90)
    
            
    
            if not customer_id:
    
                return ToolResult(
    
                    success=False,
    
                    error="customer_id is required for customer-based recommendations"
    
                )
    
            
    
            customer_recommendations_query = """
    
                WITH customer_purchases AS (
    
                    -- Get products purchased by the customer
    
                    SELECT DISTINCT p.product_id, pe.embedding
    
                    FROM retail.sales_transactions st
    
                    JOIN retail.sales_transaction_items sti ON st.transaction_id = sti.transaction_id
    
                    JOIN retail.products p ON sti.product_id = p.product_id
    
                    JOIN retail.product_embeddings pe ON p.product_id = pe.product_id
    
                    WHERE st.customer_id = $1
    
                      AND st.transaction_date >= CURRENT_DATE - INTERVAL '%s days'
    
                      AND st.transaction_type = 'sale'
    
                ),
    
                avg_customer_embedding AS (
    
                    -- Calculate average embedding vector for customer preferences
    
                    SELECT 
    
                        (
    
                            SELECT ARRAY(
    
                                SELECT AVG(embedding_element)
    
                                FROM customer_purchases cp,
    
                                     LATERAL unnest(cp.embedding) WITH ORDINALITY AS t(embedding_element, ordinality)
    
                                GROUP BY ordinality
    
                                ORDER BY ordinality
    
                            )
    
                        )::vector as avg_embedding
    
                    FROM (SELECT 1) dummy
    
                    WHERE EXISTS (SELECT 1 FROM customer_purchases)
    
                )
    
                SELECT 
    
                    p.product_id,
    
                    p.product_name,
    
                    p.brand,
    
                    p.price,
    
                    p.product_description,
    
                    p.rating_average,
    
                    p.rating_count,
    
                    pc.category_name,
    
                    1 - (pe.embedding <=> ace.avg_embedding) as preference_score
    
                FROM retail.product_embeddings pe
    
                CROSS JOIN avg_customer_embedding ace
    
                JOIN retail.products p ON pe.product_id = p.product_id
    
                LEFT JOIN retail.product_categories pc ON p.category_id = pc.category_id
    
                WHERE pe.store_id = $2
    
                  AND p.is_active = TRUE
    
                  AND pe.product_id NOT IN (SELECT product_id FROM customer_purchases)
    
                  AND 1 - (pe.embedding <=> ace.avg_embedding) >= 0.6
    
                ORDER BY pe.embedding <=> ace.avg_embedding
    
                LIMIT $3
    
            """ % days_back
    
            
    
            result = await self.execute_query(
    
                customer_recommendations_query,
    
                (customer_id, store_id, limit),
    
                store_id
    
            )
    
            
    
            if result.success:
    
                result.metadata = {
    
                    'recommendation_type': 'customer_based',
    
                    'customer_id': customer_id,
    
                    'days_back': days_back,
    
                    'store_id': store_id
    
                }
    
            
    
            return result
    
        
    
        def get_input_schema(self) -> Dict[str, Any]:
    
            """Get input schema for recommendation tool."""
    
            
    
            return {
    
                "type": "object",
    
                "properties": {
    
                    "type": {
    
                        "type": "string",
    
                        "enum": ["similar_products", "customer_based", "trending", "cross_sell"],
    
                        "description": "Type of recommendation to generate",
    
                        "default": "similar_products"
    
                    },
    
                    "store_id": {
    
                        "type": "string",
    
                        "description": "Store ID for recommendations",
    
                        "pattern": "^[a-zA-Z0-9_-]+$"
    
                    },
    
                    "product_id": {
    
                        "type": "string",
    
                        "description": "Product ID for similar product recommendations"
    
                    },
    
                    "customer_id": {
    
                        "type": "string",
    
                        "description": "Customer ID for personalized recommendations"
    
                    },
    
                    "limit": {
    
                        "type": "integer",
    
                        "description": "Maximum number of recommendations",
    
                        "minimum": 1,
    
                        "maximum": 50,
    
                        "default": 10
    
                    },
    
                    "similarity_threshold": {
    
                        "type": "number",
    
                        "description": "Minimum similarity score",
    
                        "minimum": 0.0,
    
                        "maximum": 1.0,
    
                        "default": 0.7
    
                    },
    
                    "days_back": {
    
                        "type": "integer",
    
                        "description": "Days of purchase history to consider",
    
                        "minimum": 1,
    
                        "maximum": 365,
    
                        "default": 90
    
                    }
    
                },
    
                "required": ["store_id"],
    
                "additionalProperties": False
    
            }
    
    

    โšก ์„ฑ๋Šฅ ์ตœ์ ํ™”

    ๋ฒกํ„ฐ ์ฟผ๋ฆฌ ์ตœ์ ํ™”

    
    -- Optimize pgvector performance
    
    -- Add to postgresql.conf
    
    
    
    # Increase work_mem for vector operations
    
    work_mem = '256MB'
    
    
    
    # Optimize shared_buffers for vector data
    
    shared_buffers = '512MB'
    
    
    
    # Enable parallel query execution
    
    max_parallel_workers_per_gather = 4
    
    max_parallel_workers = 8
    
    
    
    # Vector-specific optimizations
    
    SET maintenance_work_mem = '1GB';
    
    SET max_parallel_maintenance_workers = 4;
    
    
    
    -- Optimize HNSW index parameters
    
    CREATE INDEX CONCURRENTLY idx_product_embeddings_vector_optimized 
    
    ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops)
    
    WITH (m = 16, ef_construction = 200);
    
    
    
    -- Create partial indexes for active products only
    
    CREATE INDEX CONCURRENTLY idx_product_embeddings_active
    
    ON retail.product_embeddings 
    
    USING hnsw (embedding vector_cosine_ops)
    
    WHERE store_id IN (SELECT store_id FROM retail.stores WHERE is_active = TRUE);
    
    
    
    -- Analyze vector distribution for optimization
    
    ANALYZE retail.product_embeddings;
    
    
    
    -- Vector search performance monitoring
    
    CREATE OR REPLACE FUNCTION retail.analyze_vector_performance()
    
    RETURNS TABLE (
    
        avg_search_time_ms NUMERIC,
    
        index_size TEXT,
    
        total_vectors BIGINT,
    
        cache_hit_ratio NUMERIC
    
    ) AS $$
    
    BEGIN
    
        RETURN QUERY
    
        SELECT 
    
            (SELECT AVG(EXTRACT(MILLISECONDS FROM clock_timestamp() - query_start))
    
             FROM pg_stat_activity 
    
             WHERE query LIKE '%embedding <=> %'
    
             AND state = 'active') as avg_search_time_ms,
    
            pg_size_pretty(pg_relation_size('idx_product_embeddings_vector')) as index_size,
    
            COUNT(*)::BIGINT as total_vectors,
    
            (SELECT 100.0 * blks_hit / (blks_hit + blks_read) 
    
             FROM pg_stat_user_indexes 
    
             WHERE indexrelname = 'idx_product_embeddings_vector') as cache_hit_ratio
    
        FROM retail.product_embeddings;
    
    END;
    
    $$ LANGUAGE plpgsql;
    
    

    ์ž„๋ฒ ๋”ฉ ์บ์‹œ ์ „๋žต

    
    # mcp_server/embeddings/cache_manager.py
    
    """
    
    Advanced caching strategy for embeddings and search results.
    
    """
    
    import redis.asyncio as redis
    
    import json
    
    import hashlib
    
    from typing import Dict, Any, List, Optional
    
    from datetime import timedelta
    
    import logging
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class EmbeddingCacheManager:
    
        """Advanced caching for embeddings and search results."""
    
        
    
        def __init__(self, redis_url: str = "redis://localhost:6379"):
    
            self.redis_client = None
    
            self.redis_url = redis_url
    
            
    
            # Cache TTL settings
    
            self.embedding_ttl = timedelta(days=7)  # Embeddings cached for 1 week
    
            self.search_ttl = timedelta(hours=1)    # Search results cached for 1 hour
    
            self.recommendation_ttl = timedelta(hours=4)  # Recommendations cached for 4 hours
    
            
    
            # Cache key prefixes
    
            self.EMBEDDING_PREFIX = "emb:"
    
            self.SEARCH_PREFIX = "search:"
    
            self.RECOMMENDATION_PREFIX = "rec:"
    
        
    
        async def initialize(self):
    
            """Initialize Redis connection."""
    
            
    
            try:
    
                self.redis_client = redis.from_url(self.redis_url)
    
                # Test connection
    
                await self.redis_client.ping()
    
                logger.info("Embedding cache manager initialized")
    
            
    
            except Exception as e:
    
                logger.warning(f"Redis cache not available: {e}")
    
                self.redis_client = None
    
        
    
        async def cache_embedding(self, text: str, embedding: List[float], model: str):
    
            """Cache text embedding."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                cache_key = self._get_embedding_key(text, model)
    
                cache_data = {
    
                    'embedding': embedding,
    
                    'model': model,
    
                    'cached_at': str(datetime.utcnow())
    
                }
    
                
    
                await self.redis_client.setex(
    
                    cache_key,
    
                    self.embedding_ttl,
    
                    json.dumps(cache_data)
    
                )
    
                
    
            except Exception as e:
    
                logger.warning(f"Failed to cache embedding: {e}")
    
        
    
        async def get_cached_embedding(self, text: str, model: str) -> Optional[List[float]]:
    
            """Get cached embedding."""
    
            
    
            if not self.redis_client:
    
                return None
    
            
    
            try:
    
                cache_key = self._get_embedding_key(text, model)
    
                cached_data = await self.redis_client.get(cache_key)
    
                
    
                if cached_data:
    
                    data = json.loads(cached_data)
    
                    return data['embedding']
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to retrieve cached embedding: {e}")
    
            
    
            return None
    
        
    
        async def cache_search_results(
    
            self, 
    
            query: str, 
    
            store_id: str, 
    
            results: List[Dict],
    
            search_params: Dict[str, Any]
    
        ):
    
            """Cache search results."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                cache_key = self._get_search_key(query, store_id, search_params)
    
                cache_data = {
    
                    'results': results,
    
                    'query': query,
    
                    'store_id': store_id,
    
                    'params': search_params,
    
                    'cached_at': str(datetime.utcnow())
    
                }
    
                
    
                await self.redis_client.setex(
    
                    cache_key,
    
                    self.search_ttl,
    
                    json.dumps(cache_data, default=str)
    
                )
    
                
    
            except Exception as e:
    
                logger.warning(f"Failed to cache search results: {e}")
    
        
    
        async def get_cached_search_results(
    
            self, 
    
            query: str, 
    
            store_id: str, 
    
            search_params: Dict[str, Any]
    
        ) -> Optional[List[Dict]]:
    
            """Get cached search results."""
    
            
    
            if not self.redis_client:
    
                return None
    
            
    
            try:
    
                cache_key = self._get_search_key(query, store_id, search_params)
    
                cached_data = await self.redis_client.get(cache_key)
    
                
    
                if cached_data:
    
                    data = json.loads(cached_data)
    
                    return data['results']
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to retrieve cached search results: {e}")
    
            
    
            return None
    
        
    
        def _get_embedding_key(self, text: str, model: str) -> str:
    
            """Generate cache key for embedding."""
    
            
    
            content = f"{model}:{text.strip()}"
    
            hash_key = hashlib.sha256(content.encode()).hexdigest()
    
            return f"{self.EMBEDDING_PREFIX}{hash_key}"
    
        
    
        def _get_search_key(self, query: str, store_id: str, params: Dict[str, Any]) -> str:
    
            """Generate cache key for search results."""
    
            
    
            # Create stable hash from query and parameters
    
            content = f"{query}:{store_id}:{json.dumps(params, sort_keys=True)}"
    
            hash_key = hashlib.sha256(content.encode()).hexdigest()
    
            return f"{self.SEARCH_PREFIX}{hash_key}"
    
        
    
        async def invalidate_store_cache(self, store_id: str):
    
            """Invalidate all cached data for a store."""
    
            
    
            if not self.redis_client:
    
                return
    
            
    
            try:
    
                # Find all keys related to the store
    
                pattern = f"*:{store_id}:*"
    
                keys = await self.redis_client.keys(pattern)
    
                
    
                if keys:
    
                    await self.redis_client.delete(*keys)
    
                    logger.info(f"Invalidated {len(keys)} cache entries for store {store_id}")
    
            
    
            except Exception as e:
    
                logger.warning(f"Failed to invalidate store cache: {e}")
    
        
    
        async def cleanup(self):
    
            """Cleanup cache resources."""
    
            
    
            if self.redis_client:
    
                await self.redis_client.close()
    
    
    
    # Global cache manager
    
    cache_manager = EmbeddingCacheManager()
    
    

    ๐ŸŽฏ ์ฃผ์š” ํ•™์Šต ๋‚ด์šฉ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… Azure OpenAI ํ†ตํ•ฉ: ์บ์‹ฑ ๋ฐ ์ตœ์ ํ™”๋ฅผ ํฌํ•จํ•œ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ ์™„๋ฃŒ

    โœ… ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๊ตฌํ˜„: pgvector๋ฅผ ํ™œ์šฉํ•œ ํ”„๋กœ๋•์…˜ ์ค€๋น„๋œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰

    โœ… ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ: ํ‚ค์›Œ๋“œ์™€ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰์„ ๊ฒฐํ•ฉํ•˜์—ฌ ์ตœ์ ์˜ ๊ฒฐ๊ณผ ์ œ๊ณต

    โœ… ์ถ”์ฒœ ์‹œ์Šคํ…œ: ์œ ์‚ฌ์„ฑ์„ ํ™œ์šฉํ•œ AI ๊ธฐ๋ฐ˜ ์ œํ’ˆ ์ถ”์ฒœ

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ฒกํ„ฐ ์ธ๋ฑ์Šค ์ตœ์ ํ™” ๋ฐ ์ง€๋Šฅํ˜• ์บ์‹ฑ

    โœ… ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜: ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์ค€๋น„๋œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰ ์ธํ”„๋ผ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต 08: ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ์ „๋žต ๊ตฌํ˜„
  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ์„ฑ๋Šฅ ๋ฌธ์ œ ๋””๋ฒ„๊น…
  • ์ž„๋ฒ ๋”ฉ ํ’ˆ์งˆ ๋ฐ ๊ด€๋ จ์„ฑ ๊ฒ€์ฆ
  • ์ถ”์ฒœ ์‹œ์Šคํ…œ ์ •ํ™•์„ฑ ํ…Œ์ŠคํŠธ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    Azure OpenAI

  • Azure OpenAI ์„œ๋น„์Šค ๋ฌธ์„œ - ์„œ๋น„์Šค ๊ฐ€์ด๋“œ ์ „์ฒด
  • ์ž„๋ฒ ๋”ฉ API ์ฐธ์กฐ - API ๋ฌธ์„œ
  • ์ž„๋ฒ ๋”ฉ์„ ์œ„ํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๊ตฌํ˜„ ์ง€์นจ
  • ๋ฒกํ„ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

  • pgvector ๋ฌธ์„œ - PostgreSQL ๋ฒกํ„ฐ ํ™•์žฅ
  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ์ตœ์ ํ™” - ์„ฑ๋Šฅ ํŠœ๋‹
  • HNSW ์•Œ๊ณ ๋ฆฌ์ฆ˜ - ๊ณ„์ธต์  ๋„ค๋น„๊ฒŒ์ด๋ธ” ์Šค๋ชฐ ์›”๋“œ ๊ทธ๋ž˜ํ”„
  • ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰

  • ์ •๋ณด ๊ฒ€์ƒ‰ ๊ธฐ๋ณธ - ์Šคํƒ ํฌ๋“œ IR ๊ต์žฌ
  • ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๊ตฌํ˜„ ํŒจํ„ด
  • ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ ์ „๋žต - ๋‹ค์–‘ํ•œ ๊ฒ€์ƒ‰ ์ ‘๊ทผ๋ฒ• ๊ฒฐํ•ฉ
  • ---

    ์ด์ „: ์‹ค์Šต 06: ๋„๊ตฌ ๊ฐœ๋ฐœ

    ๋‹ค์Œ: ์‹ค์Šต 08: ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    08 ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…

    ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ MCP ์„œ๋ฒ„๋ฅผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ•๋ ฅํ•œ ํ…Œ์ŠคํŠธ ์ „๋žต์„ ๊ตฌํ˜„ํ•˜๊ณ  ๋ณต์žกํ•œ ๋ฌธ์ œ๋ฅผ ๋””๋ฒ„๊น…ํ•˜๋ฉฐ ๋‹ค์–‘ํ•œ ์กฐ๊ฑด์—์„œ MCP ์„œ๋ฒ„๊ฐ€ ์•ˆ์ •์ ์œผ๋กœ ์ž‘๋™ํ•˜๋„๋ก ๋ณด์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, ์„ฑ๋Šฅ ๊ฒ€์ฆ, ์‹ค์ œ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ๋ฅผ ํฌํ•จํ•˜๋Š” ๋‹ค์ธต์ ์ธ ์ ‘๊ทผ ๋ฐฉ์‹์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์€ ๊ฐœ๋ฐœ๋ถ€ํ„ฐ ํ”„๋กœ๋•์…˜ ๋ชจ๋‹ˆํ„ฐ๋ง๊นŒ์ง€์˜ ์ „์ฒด ํ…Œ์ŠคํŠธ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    ์šฐ๋ฆฌ์˜ ํ…Œ์ŠคํŠธ ์ „๋žต์€ ์‹ ๋ขฐ์„ฑ, ๋ณด์•ˆ, ์„ฑ๋Šฅ์„ ๊ฐ•์กฐํ•˜๋ฉฐ, MCP ์„œ๋ฒ„๊ฐ€ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ’ˆ์งˆ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ํ”„๋กœ๋•์…˜ ์›Œํฌ๋กœ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ํฌ๊ด„์ ์ธ ๋‹จ์œ„ ๋ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ ๊ตฌํ˜„
  • MCP ๋„๊ตฌ ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์„ ์œ„ํ•œ ํšจ๊ณผ์ ์ธ ํ…Œ์ŠคํŠธ ์ „๋žต ์„ค๊ณ„
  • ๊ณ ๊ธ‰ ๋””๋ฒ„๊น… ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ๋ฌธ์ œ ๋””๋ฒ„๊น…
  • ํ˜„์‹ค์ ์ธ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ†ตํ•ด ๋ถ€ํ•˜ ์ƒํƒœ์—์„œ ์„ฑ๋Šฅ ๊ฒ€์ฆ
  • ํšจ๊ณผ์ ์ธ ๊ฒฝ๊ณ  ๋ฐ ๊ด€์ฐฐ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ํ”„๋กœ๋•์…˜ ์‹œ์Šคํ…œ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ์ง€์†์  ํ†ตํ•ฉ์„ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ ์ž๋™ํ™”
  • ๐Ÿงช ํ…Œ์ŠคํŠธ ์•„ํ‚คํ…์ฒ˜

    ํ…Œ์ŠคํŠธ ์ „๋žต ๊ฐœ์š”

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                Unit Tests                       โ”‚
    
    โ”‚   โ€ข Tool execution logic                       โ”‚
    
    โ”‚   โ€ข Database query validation                  โ”‚
    
    โ”‚   โ€ข Authentication/authorization               โ”‚
    
    โ”‚   โ€ข Embedding generation                       โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚             Integration Tests                   โ”‚
    
    โ”‚   โ€ข End-to-end MCP workflows                  โ”‚
    
    โ”‚   โ€ข Database schema validation                 โ”‚
    
    โ”‚   โ€ข API endpoint testing                       โ”‚
    
    โ”‚   โ€ข Multi-tool interactions                    โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚            Performance Tests                    โ”‚
    
    โ”‚   โ€ข Load testing under realistic conditions    โ”‚
    
    โ”‚   โ€ข Database performance validation            โ”‚
    
    โ”‚   โ€ข Memory and resource usage                  โ”‚
    
    โ”‚   โ€ข Embedding generation performance           โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚              E2E Tests                         โ”‚
    
    โ”‚   โ€ข Complete user workflows                    โ”‚
    
    โ”‚   โ€ข VS Code integration testing               โ”‚
    
    โ”‚   โ€ข Real-world scenario validation            โ”‚
    
    โ”‚   โ€ข Cross-browser compatibility               โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ •

    
    # tests/conftest.py
    
    """
    
    Pytest configuration and shared fixtures for MCP server testing.
    
    """
    
    import pytest
    
    import asyncio
    
    import asyncpg
    
    import os
    
    from typing import AsyncGenerator, Dict, Any
    
    from unittest.mock import AsyncMock, Mock
    
    import tempfile
    
    import shutil
    
    from datetime import datetime
    
    
    
    # Test configuration
    
    TEST_DATABASE_URL = "postgresql://test_user:test_pass@localhost:5432/test_retail_db"
    
    TEST_STORE_IDS = ['test_seattle', 'test_redmond', 'test_bellevue']
    
    
    
    @pytest.fixture(scope="session")
    
    def event_loop():
    
        """Create an instance of the default event loop for the test session."""
    
        loop = asyncio.get_event_loop_policy().new_event_loop()
    
        yield loop
    
        loop.close()
    
    
    
    @pytest.fixture(scope="session")
    
    async def test_database():
    
        """Set up test database with schema and sample data."""
    
        
    
        # Create test database connection
    
        sys_conn = await asyncpg.connect(
    
            "postgresql://postgres:password@localhost:5432/postgres"
    
        )
    
        
    
        try:
    
            # Create test database
    
            await sys_conn.execute("DROP DATABASE IF EXISTS test_retail_db")
    
            await sys_conn.execute("CREATE DATABASE test_retail_db")
    
        finally:
    
            await sys_conn.close()
    
        
    
        # Connect to test database and set up schema
    
        test_conn = await asyncpg.connect(TEST_DATABASE_URL)
    
        
    
        try:
    
            # Load schema
    
            schema_sql = await load_sql_file("../scripts/create_schema.sql")
    
            await test_conn.execute(schema_sql)
    
            
    
            # Load sample data
    
            sample_data_sql = await load_sql_file("../scripts/sample_data.sql")
    
            await test_conn.execute(sample_data_sql)
    
            
    
            yield test_conn
    
            
    
        finally:
    
            await test_conn.close()
    
            
    
            # Cleanup test database
    
            sys_conn = await asyncpg.connect(
    
                "postgresql://postgres:password@localhost:5432/postgres"
    
            )
    
            try:
    
                await sys_conn.execute("DROP DATABASE IF EXISTS test_retail_db")
    
            finally:
    
                await sys_conn.close()
    
    
    
    @pytest.fixture
    
    async def db_connection(test_database):
    
        """Provide a clean database connection for each test."""
    
        
    
        conn = await asyncpg.connect(TEST_DATABASE_URL)
    
        
    
        # Start transaction for test isolation
    
        tx = conn.transaction()
    
        await tx.start()
    
        
    
        try:
    
            yield conn
    
        finally:
    
            # Rollback transaction to maintain test isolation
    
            await tx.rollback()
    
            await conn.close()
    
    
    
    @pytest.fixture
    
    async def mock_embedding_manager():
    
        """Mock embedding manager for testing without Azure OpenAI calls."""
    
        
    
        mock_manager = AsyncMock()
    
        
    
        # Mock embedding generation
    
        mock_manager.generate_embedding.return_value = [0.1] * 1536  # Mock embedding
    
        mock_manager.generate_embeddings_batch.return_value = [[0.1] * 1536] * 10
    
        
    
        # Mock initialization
    
        mock_manager.initialize.return_value = None
    
        mock_manager.cleanup.return_value = None
    
        
    
        return mock_manager
    
    
    
    @pytest.fixture
    
    async def test_mcp_server(db_connection, mock_embedding_manager):
    
        """Set up test MCP server instance."""
    
        
    
        from mcp_server.server import MCPServer
    
        from mcp_server.database import DatabaseProvider
    
        from mcp_server.config import Config
    
        
    
        # Create test configuration
    
        config = Config()
    
        config.database.connection_string = TEST_DATABASE_URL
    
        config.server.enable_debug = True
    
        
    
        # Create database provider
    
        db_provider = DatabaseProvider(config.database.connection_string)
    
        await db_provider.initialize()
    
        
    
        # Create MCP server
    
        server = MCPServer(config, db_provider)
    
        server.embedding_manager = mock_embedding_manager
    
        
    
        await server.initialize()
    
        
    
        yield server
    
        
    
        await server.cleanup()
    
    
    
    @pytest.fixture
    
    def sample_products():
    
        """Sample product data for testing."""
    
        
    
        return [
    
            {
    
                'product_id': 'test-product-1',
    
                'product_name': 'Test Running Shoes',
    
                'brand': 'TestBrand',
    
                'price': 99.99,
    
                'product_description': 'Comfortable running shoes for daily training',
    
                'category_name': 'Electronics',
    
                'current_stock': 50
    
            },
    
            {
    
                'product_id': 'test-product-2',
    
                'product_name': 'Test Laptop',
    
                'brand': 'TestTech',
    
                'price': 1299.99,
    
                'product_description': 'High-performance laptop for professional use',
    
                'category_name': 'Electronics',
    
                'current_stock': 25
    
            }
    
        ]
    
    
    
    async def load_sql_file(file_path: str) -> str:
    
        """Load SQL file content."""
    
        
    
        with open(file_path, 'r') as file:
    
            return file.read()
    
    
    
    # Test data helpers
    
    class TestDataHelper:
    
        """Helper class for managing test data."""
    
        
    
        @staticmethod
    
        async def create_test_store(conn: asyncpg.Connection, store_id: str) -> Dict[str, Any]:
    
            """Create a test store."""
    
            
    
            store_data = {
    
                'store_id': store_id,
    
                'store_name': f'Test Store {store_id}',
    
                'store_location': 'Test Location',
    
                'store_type': 'test',
    
                'region': 'test'
    
            }
    
            
    
            await conn.execute("""
    
                INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region)
    
                VALUES ($1, $2, $3, $4, $5)
    
                ON CONFLICT (store_id) DO NOTHING
    
            """, *store_data.values())
    
            
    
            return store_data
    
        
    
        @staticmethod
    
        async def create_test_customer(conn: asyncpg.Connection, store_id: str) -> str:
    
            """Create a test customer."""
    
            
    
            customer_id = await conn.fetchval("""
    
                INSERT INTO retail.customers (
    
                    store_id, first_name, last_name, email, loyalty_tier
    
                ) VALUES ($1, $2, $3, $4, $5)
    
                RETURNING customer_id
    
            """, store_id, 'Test', 'Customer', 'test@example.com', 'bronze')
    
            
    
            return customer_id
    
        
    
        @staticmethod
    
        async def create_test_product(
    
            conn: asyncpg.Connection, 
    
            store_id: str, 
    
            product_data: Dict[str, Any]
    
        ) -> str:
    
            """Create a test product."""
    
            
    
            product_id = await conn.fetchval("""
    
                INSERT INTO retail.products (
    
                    store_id, sku, product_name, brand, price, product_description, current_stock
    
                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    
                RETURNING product_id
    
            """, 
    
                store_id, 
    
                f"TEST-{product_data['product_name'][:10]}",
    
                product_data['product_name'],
    
                product_data['brand'],
    
                product_data['price'],
    
                product_data['product_description'],
    
                product_data['current_stock']
    
            )
    
            
    
            return product_id
    
    

    ๐Ÿ”ง ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

    ๋„๊ตฌ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ

    
    # tests/test_tools.py
    
    """
    
    Comprehensive unit tests for MCP tools.
    
    """
    
    import pytest
    
    import asyncio
    
    from unittest.mock import AsyncMock, patch
    
    from datetime import datetime, timedelta
    
    
    
    from mcp_server.tools.sales_analysis import SalesAnalysisTool
    
    from mcp_server.tools.semantic_search import SemanticProductSearchTool
    
    from mcp_server.tools.schema_introspection import SchemaIntrospectionTool
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestSalesAnalysisTool:
    
        """Test sales analysis tool functionality."""
    
        
    
        @pytest.fixture
    
        async def sales_tool(self, test_mcp_server):
    
            """Create sales analysis tool for testing."""
    
            return SalesAnalysisTool(test_mcp_server.db_provider)
    
        
    
        async def test_daily_sales_query(self, sales_tool, db_connection):
    
            """Test daily sales analysis query."""
    
            
    
            # Set up test data
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            customer_id = await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Create test transaction
    
            await db_connection.execute("""
    
                INSERT INTO retail.sales_transactions (
    
                    store_id, customer_id, transaction_date, total_amount, transaction_type
    
                ) VALUES ($1, $2, $3, $4, $5)
    
            """, store_id, customer_id, datetime.now(), 150.00, 'sale')
    
            
    
            # Execute tool
    
            result = await sales_tool.execute(
    
                query_type='daily_sales',
    
                store_id=store_id,
    
                start_date=(datetime.now() - timedelta(days=7)).date(),
    
                end_date=datetime.now().date()
    
            )
    
            
    
            # Validate results
    
            assert result.success is True
    
            assert len(result.data) > 0
    
            assert 'total_revenue' in result.data[0]
    
            assert result.metadata['query_type'] == 'daily_sales'
    
        
    
        async def test_custom_query_validation(self, sales_tool, db_connection):
    
            """Test custom query validation."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Test valid query
    
            valid_query = "SELECT COUNT(*) as customer_count FROM retail.customers"
    
            result = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store_id,
    
                query=valid_query
    
            )
    
            
    
            assert result.success is True
    
            
    
            # Test invalid query (should be blocked)
    
            invalid_query = "DROP TABLE retail.customers"
    
            result = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store_id,
    
                query=invalid_query
    
            )
    
            
    
            assert result.success is False
    
            assert 'validation failed' in result.error.lower()
    
        
    
        async def test_store_isolation(self, sales_tool, db_connection):
    
            """Test that store isolation works correctly."""
    
            
    
            # Create two different stores
    
            store1 = 'test_store1'
    
            store2 = 'test_store2'
    
            
    
            await TestDataHelper.create_test_store(db_connection, store1)
    
            await TestDataHelper.create_test_store(db_connection, store2)
    
            
    
            # Create customers in different stores
    
            customer1 = await TestDataHelper.create_test_customer(db_connection, store1)
    
            customer2 = await TestDataHelper.create_test_customer(db_connection, store2)
    
            
    
            # Query from store1 should only see store1 data
    
            result1 = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store1,
    
                query="SELECT COUNT(*) as count FROM retail.customers"
    
            )
    
            
    
            # Query from store2 should only see store2 data
    
            result2 = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store2,
    
                query="SELECT COUNT(*) as count FROM retail.customers"
    
            )
    
            
    
            assert result1.success is True
    
            assert result2.success is True
    
            assert result1.data[0]['count'] == 1
    
            assert result2.data[0]['count'] == 1
    
    
    
    class TestSemanticSearchTool:
    
        """Test semantic search tool functionality."""
    
        
    
        @pytest.fixture
    
        async def search_tool(self, test_mcp_server):
    
            """Create semantic search tool for testing."""
    
            return SemanticProductSearchTool(test_mcp_server.db_provider)
    
        
    
        async def test_semantic_search_execution(self, search_tool, db_connection, sample_products):
    
            """Test semantic search with mock embeddings."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test products
    
            for product_data in sample_products:
    
                product_id = await TestDataHelper.create_test_product(
    
                    db_connection, store_id, product_data
    
                )
    
                
    
                # Create mock embedding
    
                await db_connection.execute("""
    
                    INSERT INTO retail.product_embeddings (
    
                        product_id, store_id, embedding_text, embedding
    
                    ) VALUES ($1, $2, $3, $4)
    
                """, 
    
                    product_id, store_id, 
    
                    f"{product_data['product_name']} {product_data['brand']}", 
    
                    '[0.1,0.2,0.3]'  # Mock embedding
    
                )
    
            
    
            # Execute search
    
            result = await search_tool.execute(
    
                query='running shoes',
    
                store_id=store_id,
    
                limit=10,
    
                similarity_threshold=0.0
    
            )
    
            
    
            # Validate results
    
            assert result.success is True
    
            assert len(result.data) > 0
    
            assert 'similarity_score' in result.data[0]
    
            assert result.metadata['search_type'] == 'semantic'
    
        
    
        async def test_search_parameter_validation(self, search_tool):
    
            """Test search parameter validation."""
    
            
    
            # Test missing query
    
            result = await search_tool.execute(store_id='test_store')
    
            assert result.success is False
    
            assert 'query is required' in result.error.lower()
    
            
    
            # Test missing store_id
    
            result = await search_tool.execute(query='test query')
    
            assert result.success is False
    
            assert 'store_id is required' in result.error.lower()
    
    
    
    class TestSchemaIntrospectionTool:
    
        """Test schema introspection tool."""
    
        
    
        @pytest.fixture
    
        async def schema_tool(self, test_mcp_server):
    
            """Create schema introspection tool for testing."""
    
            return SchemaIntrospectionTool(test_mcp_server.db_provider)
    
        
    
        async def test_single_table_schema(self, schema_tool, db_connection):
    
            """Test getting schema for a single table."""
    
            
    
            result = await schema_tool.execute(
    
                table_name='customers',
    
                include_constraints=True,
    
                include_indexes=True
    
            )
    
            
    
            assert result.success is True
    
            assert result.data['table_name'] == 'customers'
    
            assert len(result.data['columns']) > 0
    
            assert 'customer_id' in [col['column_name'] for col in result.data['columns']]
    
        
    
        async def test_all_tables_schema(self, schema_tool, db_connection):
    
            """Test getting schema for all tables."""
    
            
    
            result = await schema_tool.execute()
    
            
    
            assert result.success is True
    
            assert result.data['schema_name'] == 'retail'
    
            assert len(result.data['tables']) > 0
    
            
    
            table_names = [table['table_name'] for table in result.data['tables']]
    
            expected_tables = ['customers', 'products', 'sales_transactions']
    
            
    
            for expected_table in expected_tables:
    
                assert expected_table in table_names
    
    

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ŠคํŠธ

    
    # tests/test_database.py
    
    """
    
    Database layer testing including RLS and security.
    
    """
    
    import pytest
    
    import asyncpg
    
    from datetime import datetime
    
    
    
    from mcp_server.database import DatabaseProvider
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestRowLevelSecurity:
    
        """Test Row Level Security implementation."""
    
        
    
        async def test_store_context_setting(self, db_connection):
    
            """Test that store context is set correctly."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Set store context
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store_id)
    
            
    
            # Verify context is set
    
            current_store = await db_connection.fetchval(
    
                "SELECT current_setting('app.current_store_id', true)"
    
            )
    
            
    
            assert current_store == store_id
    
        
    
        async def test_customer_isolation(self, db_connection):
    
            """Test that customers are properly isolated by store."""
    
            
    
            # Create two stores
    
            store1 = 'test_store1'
    
            store2 = 'test_store2'
    
            
    
            await TestDataHelper.create_test_store(db_connection, store1)
    
            await TestDataHelper.create_test_store(db_connection, store2)
    
            
    
            # Create customers in different stores
    
            await TestDataHelper.create_test_customer(db_connection, store1)
    
            await TestDataHelper.create_test_customer(db_connection, store2)
    
            
    
            # Set context to store1 and count customers
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store1)
    
            store1_count = await db_connection.fetchval("SELECT COUNT(*) FROM retail.customers")
    
            
    
            # Set context to store2 and count customers
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store2)
    
            store2_count = await db_connection.fetchval("SELECT COUNT(*) FROM retail.customers")
    
            
    
            # Each store should only see its own customers
    
            assert store1_count == 1
    
            assert store2_count == 1
    
        
    
        async def test_invalid_store_context(self, db_connection):
    
            """Test that invalid store context raises error."""
    
            
    
            with pytest.raises(Exception) as exc_info:
    
                await db_connection.execute("SELECT retail.set_store_context($1)", 'invalid_store')
    
            
    
            assert "Store not found" in str(exc_info.value)
    
        
    
        async def test_cross_store_data_insertion_blocked(self, db_connection):
    
            """Test that users cannot insert data for other stores."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Set store context
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store_id)
    
            
    
            # Try to insert customer for different store (should fail)
    
            with pytest.raises(Exception):
    
                await db_connection.execute("""
    
                    INSERT INTO retail.customers (store_id, first_name, last_name, email)
    
                    VALUES ($1, $2, $3, $4)
    
                """, 'different_store', 'Test', 'Customer', 'test@example.com')
    
    
    
    class TestDatabaseProvider:
    
        """Test database provider functionality."""
    
        
    
        @pytest.fixture
    
        async def db_provider(self):
    
            """Create database provider for testing."""
    
            
    
            provider = DatabaseProvider(TEST_DATABASE_URL)
    
            await provider.initialize()
    
            yield provider
    
            await provider.cleanup()
    
        
    
        async def test_connection_pooling(self, db_provider):
    
            """Test connection pool functionality."""
    
            
    
            # Get multiple connections
    
            conn1 = await db_provider.get_connection()
    
            conn2 = await db_provider.get_connection()
    
            
    
            assert conn1 is not None
    
            assert conn2 is not None
    
            assert conn1 != conn2  # Should be different connection objects
    
            
    
            # Release connections
    
            await db_provider.release_connection(conn1)
    
            await db_provider.release_connection(conn2)
    
        
    
        async def test_health_check(self, db_provider):
    
            """Test database health check."""
    
            
    
            health_status = await db_provider.health_check()
    
            
    
            assert health_status['status'] == 'healthy'
    
            assert 'connection_pool_size' in health_status
    
            assert 'database_version' in health_status
    
        
    
        async def test_connection_recovery(self, db_provider):
    
            """Test connection recovery after database issues."""
    
            
    
            # This would test connection recovery scenarios
    
            # In a real test, you might temporarily break the connection
    
            # and verify that the pool recovers
    
            
    
            # For now, just verify health check works
    
            health_status = await db_provider.health_check()
    
            assert health_status['status'] == 'healthy'
    
    

    ๐Ÿš€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    ์—”๋“œ ํˆฌ ์—”๋“œ ์›Œํฌํ”Œ๋กœ ํ…Œ์ŠคํŠธ

    
    # tests/test_integration.py
    
    """
    
    Integration tests for complete MCP workflows.
    
    """
    
    import pytest
    
    import json
    
    from datetime import datetime, timedelta
    
    
    
    from mcp_server.server import MCPServer
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestMCPWorkflows:
    
        """Test complete MCP server workflows."""
    
        
    
        async def test_product_search_workflow(self, test_mcp_server, db_connection, sample_products):
    
            """Test complete product search workflow."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test products with embeddings
    
            for product_data in sample_products:
    
                product_id = await TestDataHelper.create_test_product(
    
                    db_connection, store_id, product_data
    
                )
    
                
    
                # Create embedding for product
    
                await db_connection.execute("""
    
                    INSERT INTO retail.product_embeddings (
    
                        product_id, store_id, embedding_text, embedding
    
                    ) VALUES ($1, $2, $3, $4)
    
                """, 
    
                    product_id, store_id, 
    
                    f"{product_data['product_name']} {product_data['brand']}", 
    
                    '[' + ','.join(['0.1'] * 1536) + ']'  # Mock embedding
    
                )
    
            
    
            # Test semantic search
    
            search_result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {
    
                    'query': 'running shoes',
    
                    'store_id': store_id,
    
                    'limit': 10
    
                }
    
            )
    
            
    
            assert search_result['success'] is True
    
            assert len(search_result['data']) > 0
    
            
    
            # Test schema introspection
    
            schema_result = await test_mcp_server.execute_tool(
    
                'get_table_schema',
    
                {'table_name': 'products'}
    
            )
    
            
    
            assert schema_result['success'] is True
    
            assert schema_result['data']['table_name'] == 'products'
    
        
    
        async def test_sales_analysis_workflow(self, test_mcp_server, db_connection):
    
            """Test sales analysis workflow."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test customer and product
    
            customer_id = await TestDataHelper.create_test_customer(db_connection, store_id)
    
            product_id = await TestDataHelper.create_test_product(
    
                db_connection, store_id, {
    
                    'product_name': 'Test Product',
    
                    'brand': 'TestBrand',
    
                    'price': 99.99,
    
                    'product_description': 'Test product description',
    
                    'current_stock': 50
    
                }
    
            )
    
            
    
            # Create test transaction
    
            transaction_id = await db_connection.fetchval("""
    
                INSERT INTO retail.sales_transactions (
    
                    store_id, customer_id, transaction_date, total_amount, 
    
                    subtotal, tax_amount, transaction_type
    
                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    
                RETURNING transaction_id
    
            """, store_id, customer_id, datetime.now(), 107.99, 99.99, 8.00, 'sale')
    
            
    
            # Create transaction item
    
            await db_connection.execute("""
    
                INSERT INTO retail.sales_transaction_items (
    
                    transaction_id, product_id, quantity, unit_price, total_price
    
                ) VALUES ($1, $2, $3, $4, $5)
    
            """, transaction_id, product_id, 1, 99.99, 99.99)
    
            
    
            # Test daily sales analysis
    
            sales_result = await test_mcp_server.execute_tool(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'daily_sales',
    
                    'store_id': store_id,
    
                    'start_date': (datetime.now() - timedelta(days=1)).date().isoformat(),
    
                    'end_date': datetime.now().date().isoformat()
    
                }
    
            )
    
            
    
            assert sales_result['success'] is True
    
            assert len(sales_result['data']) > 0
    
            assert sales_result['data'][0]['total_revenue'] == 107.99
    
        
    
        async def test_multi_store_workflow(self, test_mcp_server, db_connection):
    
            """Test workflows across multiple stores."""
    
            
    
            # Create multiple stores
    
            stores = ['test_seattle', 'test_redmond', 'test_bellevue']
    
            
    
            for store_id in stores:
    
                await TestDataHelper.create_test_store(db_connection, store_id)
    
                
    
                # Create customer in each store
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Test that each store sees only its own data
    
            for store_id in stores:
    
                schema_result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) as customer_count FROM retail.customers'
    
                    }
    
                )
    
                
    
                assert schema_result['success'] is True
    
                assert schema_result['data'][0]['customer_count'] == 1
    
    
    
    class TestErrorHandling:
    
        """Test error handling and edge cases."""
    
        
    
        async def test_database_connection_failure(self, test_mcp_server):
    
            """Test behavior when database connection fails."""
    
            
    
            # Simulate database failure by using invalid connection
    
            with patch.object(test_mcp_server.db_provider, 'get_connection') as mock_conn:
    
                mock_conn.side_effect = Exception("Database connection failed")
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'get_table_schema',
    
                    {'table_name': 'customers'}
    
                )
    
                
    
                assert result['success'] is False
    
                assert 'connection failed' in result['error'].lower()
    
        
    
        async def test_invalid_tool_parameters(self, test_mcp_server):
    
            """Test handling of invalid tool parameters."""
    
            
    
            # Missing required parameter
    
            result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {'query': 'test query'}  # Missing store_id
    
            )
    
            
    
            assert result['success'] is False
    
            assert 'store_id is required' in result['error'].lower()
    
            
    
            # Invalid parameter type
    
            result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {
    
                    'query': 'test query',
    
                    'store_id': 'test_store',
    
                    'limit': 'invalid'  # Should be integer
    
                }
    
            )
    
            
    
            assert result['success'] is False
    
        
    
        async def test_sql_injection_prevention(self, test_mcp_server, db_connection):
    
            """Test that SQL injection attempts are blocked."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Attempt SQL injection
    
            malicious_query = "SELECT * FROM retail.customers; DROP TABLE retail.customers; --"
    
            
    
            result = await test_mcp_server.execute_tool(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'query': malicious_query
    
                }
    
            )
    
            
    
            assert result['success'] is False
    
            assert 'validation failed' in result['error'].lower()
    
    

    ๐Ÿ“Š ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

    ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ

    
    # tests/test_performance.py
    
    """
    
    Performance and load testing for MCP server.
    
    """
    
    import pytest
    
    import asyncio
    
    import time
    
    import statistics
    
    from concurrent.futures import ThreadPoolExecutor
    
    from typing import List, Dict, Any
    
    
    
    class TestPerformance:
    
        """Performance testing for MCP server operations."""
    
        
    
        async def test_concurrent_tool_execution(self, test_mcp_server, db_connection):
    
            """Test performance under concurrent tool execution."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test data
    
            for i in range(100):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Define test scenarios
    
            async def execute_tool_scenario():
    
                """Execute a tool and measure performance."""
    
                start_time = time.time()
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) as count FROM retail.customers'
    
                    }
    
                )
    
                
    
                execution_time = time.time() - start_time
    
                return {
    
                    'success': result['success'],
    
                    'execution_time': execution_time
    
                }
    
            
    
            # Run concurrent executions
    
            concurrent_tasks = 20
    
            tasks = [execute_tool_scenario() for _ in range(concurrent_tasks)]
    
            
    
            start_time = time.time()
    
            results = await asyncio.gather(*tasks)
    
            total_time = time.time() - start_time
    
            
    
            # Analyze results
    
            successful_executions = [r for r in results if r['success']]
    
            execution_times = [r['execution_time'] for r in successful_executions]
    
            
    
            assert len(successful_executions) == concurrent_tasks
    
            assert statistics.mean(execution_times) < 1.0  # Average under 1 second
    
            assert max(execution_times) < 5.0  # No execution over 5 seconds
    
            assert total_time < 10.0  # All executions under 10 seconds
    
            
    
            print(f"Concurrent execution stats:")
    
            print(f"  Total time: {total_time:.2f}s")
    
            print(f"  Average execution time: {statistics.mean(execution_times):.3f}s")
    
            print(f"  Max execution time: {max(execution_times):.3f}s")
    
            print(f"  Min execution time: {min(execution_times):.3f}s")
    
        
    
        async def test_database_query_performance(self, test_mcp_server, db_connection):
    
            """Test database query performance with large datasets."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create large dataset
    
            print("Creating test dataset...")
    
            for i in range(1000):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Test various query patterns
    
            query_tests = [
    
                {
    
                    'name': 'Simple COUNT',
    
                    'query': 'SELECT COUNT(*) FROM retail.customers',
    
                    'expected_max_time': 0.1
    
                },
    
                {
    
                    'name': 'Filtered SELECT',
    
                    'query': "SELECT * FROM retail.customers WHERE loyalty_tier = 'bronze' LIMIT 100",
    
                    'expected_max_time': 0.5
    
                },
    
                {
    
                    'name': 'Aggregation',
    
                    'query': 'SELECT loyalty_tier, COUNT(*) FROM retail.customers GROUP BY loyalty_tier',
    
                    'expected_max_time': 0.5
    
                }
    
            ]
    
            
    
            for test_case in query_tests:
    
                start_time = time.time()
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': test_case['query']
    
                    }
    
                )
    
                
    
                execution_time = time.time() - start_time
    
                
    
                assert result['success'] is True
    
                assert execution_time < test_case['expected_max_time']
    
                
    
                print(f"Query '{test_case['name']}': {execution_time:.3f}s")
    
        
    
        async def test_embedding_generation_performance(self, test_mcp_server):
    
            """Test embedding generation performance."""
    
            
    
            from mcp_server.embeddings.product_embedder import ProductEmbedder
    
            
    
            # Test with mock embedding manager (no actual API calls)
    
            embedder = ProductEmbedder(test_mcp_server.db_provider)
    
            embedder.embedding_manager = test_mcp_server.embedding_manager
    
            
    
            # Test batch embedding generation
    
            test_texts = [f"Test product {i} description" for i in range(100)]
    
            
    
            start_time = time.time()
    
            embeddings = await embedder.embedding_manager.generate_embeddings_batch(test_texts)
    
            batch_time = time.time() - start_time
    
            
    
            assert len(embeddings) == 100
    
            assert batch_time < 5.0  # Should complete in under 5 seconds with mocks
    
            
    
            print(f"Batch embedding generation (100 items): {batch_time:.3f}s")
    
            print(f"Average per embedding: {batch_time/100:.4f}s")
    
        
    
        @pytest.mark.slow
    
        async def test_memory_usage(self, test_mcp_server, db_connection):
    
            """Test memory usage under load."""
    
            
    
            import psutil
    
            import os
    
            
    
            process = psutil.Process(os.getpid())
    
            initial_memory = process.memory_info().rss / 1024 / 1024  # MB
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create substantial dataset
    
            for i in range(500):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Execute multiple operations
    
            for i in range(50):
    
                await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT * FROM retail.customers LIMIT 100'
    
                    }
    
                )
    
            
    
            final_memory = process.memory_info().rss / 1024 / 1024  # MB
    
            memory_increase = final_memory - initial_memory
    
            
    
            # Memory increase should be reasonable (under 100MB for this test)
    
            assert memory_increase < 100
    
            
    
            print(f"Memory usage:")
    
            print(f"  Initial: {initial_memory:.1f} MB")
    
            print(f"  Final: {final_memory:.1f} MB")
    
            print(f"  Increase: {memory_increase:.1f} MB")
    
    
    
    class TestScalability:
    
        """Test scalability characteristics."""
    
        
    
        async def test_response_time_scaling(self, test_mcp_server, db_connection):
    
            """Test how response time scales with data size."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Test with different data sizes
    
            data_sizes = [100, 500, 1000, 2000]
    
            response_times = []
    
            
    
            for size in data_sizes:
    
                # Clear existing data
    
                await db_connection.execute("DELETE FROM retail.customers WHERE store_id = $1", store_id)
    
                
    
                # Create dataset of specified size
    
                for i in range(size):
    
                    await TestDataHelper.create_test_customer(db_connection, store_id)
    
                
    
                # Measure query time
    
                start_time = time.time()
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) FROM retail.customers'
    
                    }
    
                )
    
                execution_time = time.time() - start_time
    
                
    
                assert result['success'] is True
    
                response_times.append(execution_time)
    
                
    
                print(f"Data size {size}: {execution_time:.3f}s")
    
            
    
            # Response time should scale reasonably (not exponentially)
    
            # Simple count queries should remain fast even with larger datasets
    
            for time_val in response_times:
    
                assert time_val < 1.0  # All queries under 1 second
    
    

    ๐Ÿ” ๋””๋ฒ„๊น… ๋„๊ตฌ

    ๊ณ ๊ธ‰ ๋””๋ฒ„๊น… ํ”„๋ ˆ์ž„์›Œํฌ

    
    # mcp_server/debugging/debug_tools.py
    
    """
    
    Advanced debugging tools for MCP server troubleshooting.
    
    """
    
    import asyncio
    
    import json
    
    import time
    
    import traceback
    
    from typing import Dict, Any, List, Optional
    
    from datetime import datetime
    
    import logging
    
    from contextlib import asynccontextmanager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class MCPDebugger:
    
        """Comprehensive debugging utilities for MCP server."""
    
        
    
        def __init__(self, server_instance):
    
            self.server = server_instance
    
            self.debug_logs = []
    
            self.performance_metrics = {}
    
            self.active_traces = {}
    
            
    
        @asynccontextmanager
    
        async def trace_execution(self, operation_name: str, context: Dict[str, Any] = None):
    
            """Trace operation execution with detailed logging."""
    
            
    
            trace_id = f"{operation_name}_{int(time.time() * 1000)}"
    
            start_time = time.time()
    
            
    
            trace_info = {
    
                'trace_id': trace_id,
    
                'operation': operation_name,
    
                'start_time': start_time,
    
                'context': context or {},
    
                'status': 'running'
    
            }
    
            
    
            self.active_traces[trace_id] = trace_info
    
            
    
            logger.debug(f"Starting trace: {trace_id} - {operation_name}")
    
            
    
            try:
    
                yield trace_info
    
                
    
                # Success
    
                execution_time = time.time() - start_time
    
                trace_info.update({
    
                    'status': 'completed',
    
                    'execution_time': execution_time
    
                })
    
                
    
                logger.debug(f"Completed trace: {trace_id} in {execution_time:.3f}s")
    
                
    
            except Exception as e:
    
                # Error
    
                execution_time = time.time() - start_time
    
                trace_info.update({
    
                    'status': 'error',
    
                    'execution_time': execution_time,
    
                    'error': str(e),
    
                    'traceback': traceback.format_exc()
    
                })
    
                
    
                logger.error(f"Error in trace: {trace_id} - {str(e)}")
    
                raise
    
                
    
            finally:
    
                # Store completed trace
    
                self.debug_logs.append(trace_info.copy())
    
                del self.active_traces[trace_id]
    
                
    
                # Limit debug log size
    
                if len(self.debug_logs) > 1000:
    
                    self.debug_logs = self.debug_logs[-500:]
    
        
    
        async def debug_tool_execution(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
    
            """Debug tool execution with comprehensive logging."""
    
            
    
            async with self.trace_execution(f"tool_execution_{tool_name}", {'parameters': parameters}) as trace:
    
                
    
                # Pre-execution validation
    
                validation_result = await self._validate_tool_parameters(tool_name, parameters)
    
                trace['validation'] = validation_result
    
                
    
                if not validation_result['valid']:
    
                    return {
    
                        'success': False,
    
                        'error': f"Parameter validation failed: {validation_result['errors']}",
    
                        'debug_info': trace
    
                    }
    
                
    
                # Database connection check
    
                db_health = await self._check_database_health()
    
                trace['database_health'] = db_health
    
                
    
                # Execute tool with monitoring
    
                try:
    
                    tool_instance = self.server.get_tool(tool_name)
    
                    if not tool_instance:
    
                        return {
    
                            'success': False,
    
                            'error': f"Tool '{tool_name}' not found",
    
                            'debug_info': trace
    
                        }
    
                    
    
                    # Monitor resource usage during execution
    
                    start_memory = await self._get_memory_usage()
    
                    
    
                    result = await tool_instance.call(**parameters)
    
                    
    
                    end_memory = await self._get_memory_usage()
    
                    
    
                    trace.update({
    
                        'memory_start_mb': start_memory,
    
                        'memory_end_mb': end_memory,
    
                        'memory_used_mb': end_memory - start_memory,
    
                        'result_success': result.success,
    
                        'result_row_count': result.row_count
    
                    })
    
                    
    
                    return {
    
                        'success': result.success,
    
                        'data': result.data,
    
                        'error': result.error,
    
                        'metadata': result.metadata,
    
                        'debug_info': trace
    
                    }
    
                    
    
                except Exception as e:
    
                    trace['exception'] = {
    
                        'type': type(e).__name__,
    
                        'message': str(e),
    
                        'traceback': traceback.format_exc()
    
                    }
    
                    
    
                    return {
    
                        'success': False,
    
                        'error': f"Tool execution failed: {str(e)}",
    
                        'debug_info': trace
    
                    }
    
        
    
        async def analyze_performance_bottlenecks(self) -> Dict[str, Any]:
    
            """Analyze performance bottlenecks from debug logs."""
    
            
    
            if not self.debug_logs:
    
                return {'message': 'No debug data available'}
    
            
    
            # Analyze execution times
    
            execution_times = {}
    
            error_rates = {}
    
            memory_usage = {}
    
            
    
            for log_entry in self.debug_logs[-100:]:  # Last 100 entries
    
                operation = log_entry['operation']
    
                
    
                # Execution time analysis
    
                if 'execution_time' in log_entry:
    
                    if operation not in execution_times:
    
                        execution_times[operation] = []
    
                    execution_times[operation].append(log_entry['execution_time'])
    
                
    
                # Error rate analysis
    
                if operation not in error_rates:
    
                    error_rates[operation] = {'total': 0, 'errors': 0}
    
                
    
                error_rates[operation]['total'] += 1
    
                if log_entry['status'] == 'error':
    
                    error_rates[operation]['errors'] += 1
    
                
    
                # Memory usage analysis
    
                if 'memory_used_mb' in log_entry:
    
                    if operation not in memory_usage:
    
                        memory_usage[operation] = []
    
                    memory_usage[operation].append(log_entry['memory_used_mb'])
    
            
    
            # Calculate statistics
    
            performance_stats = {}
    
            
    
            for operation, times in execution_times.items():
    
                if times:
    
                    performance_stats[operation] = {
    
                        'avg_execution_time': sum(times) / len(times),
    
                        'max_execution_time': max(times),
    
                        'min_execution_time': min(times),
    
                        'execution_count': len(times),
    
                        'error_rate': (error_rates[operation]['errors'] / 
    
                                     error_rates[operation]['total'] * 100),
    
                        'avg_memory_usage': (sum(memory_usage.get(operation, [0])) / 
    
                                           len(memory_usage.get(operation, [1])))
    
                    }
    
            
    
            # Identify bottlenecks
    
            bottlenecks = []
    
            
    
            for operation, stats in performance_stats.items():
    
                if stats['avg_execution_time'] > 2.0:  # Slow operations
    
                    bottlenecks.append({
    
                        'type': 'slow_execution',
    
                        'operation': operation,
    
                        'avg_time': stats['avg_execution_time']
    
                    })
    
                
    
                if stats['error_rate'] > 5.0:  # High error rate
    
                    bottlenecks.append({
    
                        'type': 'high_error_rate',
    
                        'operation': operation,
    
                        'error_rate': stats['error_rate']
    
                    })
    
                
    
                if stats['avg_memory_usage'] > 100:  # High memory usage
    
                    bottlenecks.append({
    
                        'type': 'high_memory_usage',
    
                        'operation': operation,
    
                        'memory_mb': stats['avg_memory_usage']
    
                    })
    
            
    
            return {
    
                'performance_stats': performance_stats,
    
                'bottlenecks': bottlenecks,
    
                'total_operations': len(self.debug_logs),
    
                'analysis_timestamp': datetime.now().isoformat()
    
            }
    
        
    
        async def _validate_tool_parameters(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
    
            """Validate tool parameters against schema."""
    
            
    
            try:
    
                tool_instance = self.server.get_tool(tool_name)
    
                if not tool_instance:
    
                    return {
    
                        'valid': False,
    
                        'errors': [f"Tool '{tool_name}' not found"]
    
                    }
    
                
    
                schema = tool_instance.get_input_schema()
    
                
    
                # Basic validation (in production, use jsonschema library)
    
                errors = []
    
                required_props = schema.get('required', [])
    
                
    
                for prop in required_props:
    
                    if prop not in parameters:
    
                        errors.append(f"Missing required parameter: {prop}")
    
                
    
                return {
    
                    'valid': len(errors) == 0,
    
                    'errors': errors,
    
                    'schema': schema
    
                }
    
                
    
            except Exception as e:
    
                return {
    
                    'valid': False,
    
                    'errors': [f"Validation error: {str(e)}"]
    
                }
    
        
    
        async def _check_database_health(self) -> Dict[str, Any]:
    
            """Check database health and connectivity."""
    
            
    
            try:
    
                health_status = await self.server.db_provider.health_check()
    
                return {
    
                    'healthy': health_status.get('status') == 'healthy',
    
                    'details': health_status
    
                }
    
            except Exception as e:
    
                return {
    
                    'healthy': False,
    
                    'error': str(e)
    
                }
    
        
    
        async def _get_memory_usage(self) -> float:
    
            """Get current memory usage in MB."""
    
            
    
            try:
    
                import psutil
    
                import os
    
                process = psutil.Process(os.getpid())
    
                return process.memory_info().rss / 1024 / 1024
    
            except:
    
                return 0.0
    
        
    
        def get_debug_summary(self) -> Dict[str, Any]:
    
            """Get summary of debug information."""
    
            
    
            recent_logs = self.debug_logs[-50:] if self.debug_logs else []
    
            
    
            return {
    
                'total_operations': len(self.debug_logs),
    
                'active_traces': len(self.active_traces),
    
                'recent_operations': [
    
                    {
    
                        'operation': log['operation'],
    
                        'status': log['status'],
    
                        'execution_time': log.get('execution_time', 0),
    
                        'timestamp': log.get('start_time', 0)
    
                    }
    
                    for log in recent_logs
    
                ],
    
                'current_traces': list(self.active_traces.keys())
    
            }
    
    
    
    # Debug tool for direct use
    
    class DebugTool:
    
        """Interactive debugging tool for MCP server."""
    
        
    
        def __init__(self, server_instance):
    
            self.debugger = MCPDebugger(server_instance)
    
        
    
        async def debug_query(self, query: str, store_id: str) -> Dict[str, Any]:
    
            """Debug a specific database query."""
    
            
    
            return await self.debugger.debug_tool_execution(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'query': query
    
                }
    
            )
    
        
    
        async def debug_search(self, query: str, store_id: str) -> Dict[str, Any]:
    
            """Debug a semantic search query."""
    
            
    
            return await self.debugger.debug_tool_execution(
    
                'semantic_search_products',
    
                {
    
                    'query': query,
    
                    'store_id': store_id,
    
                    'limit': 10
    
                }
    
            )
    
        
    
        async def get_performance_report(self) -> Dict[str, Any]:
    
            """Get comprehensive performance report."""
    
            
    
            return await self.debugger.analyze_performance_bottlenecks()
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ: ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ์— ๋Œ€ํ•œ ๋‹จ์œ„, ํ†ตํ•ฉ, ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

    โœ… ๊ณ ๊ธ‰ ๋””๋ฒ„๊น… ๋„๊ตฌ: ์‹คํ–‰ ์ถ”์  ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ์ •๊ตํ•œ ๋””๋ฒ„๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ

    โœ… ์„ฑ๋Šฅ ๊ฒ€์ฆ: ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๋ฐ ํ™•์žฅ์„ฑ ๋ถ„์„ ๊ธฐ๋Šฅ

    โœ… ๋ณด์•ˆ ํ…Œ์ŠคํŠธ: SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ๋ฐ RLS ๊ฒ€์ฆ

    โœ… ๋ชจ๋‹ˆํ„ฐ๋ง ํ†ตํ•ฉ: ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ๋ฐ ๋ณ‘๋ชฉ ํ˜„์ƒ ๋ถ„์„

    โœ… CI/CD ์ค€๋น„ ์™„๋ฃŒ: ์ง€์†์  ํ†ตํ•ฉ์„ ์œ„ํ•œ ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 09: VS Code Integration์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์„ ์œ„ํ•œ VS Code ๊ตฌ์„ฑ
  • VS Code์—์„œ ๋””๋ฒ„๊น… ํ™˜๊ฒฝ ์„ค์ •
  • MCP ์„œ๋ฒ„๋ฅผ VS Code Chat๊ณผ ํ†ตํ•ฉ
  • VS Code ์›Œํฌํ”Œ๋กœ ์ „์ฒด ํ…Œ์ŠคํŠธ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ

  • pytest Documentation - Python ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ
  • AsyncPG Testing - Async PostgreSQL ํ…Œ์ŠคํŠธ
  • FastAPI Testing - API ํ…Œ์ŠคํŠธ ํŒจํ„ด
  • ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

  • Load Testing Best Practices - Async ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ
  • Database Performance Testing - PostgreSQL ์ตœ์ ํ™”
  • Memory Profiling - Python ๋ฉ”๋ชจ๋ฆฌ ๋ถ„์„
  • ๋””๋ฒ„๊น… ๋„๊ตฌ

  • Python Debugging - Python ๋””๋ฒ„๊ฑฐ
  • Async Debugging - Asyncio ๋””๋ฒ„๊น…
  • SQL Debugging - PostgreSQL ๋กœ๊น…
  • ---

    ์ด์ „: Lab 07: Semantic Search Integration

    ๋‹ค์Œ: Lab 09: VS Code Integration

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ํ…Œ์ŠคํŠธ ์ „๋žต, ๋””๋ฒ„๊น… ๋„๊ตฌ, ๊ฒ€์ฆ ๋ฐฉ๋ฒ• ํ…Œ์ŠคํŠธํ•˜๊ธฐ

    ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ MCP ์„œ๋ฒ„๋ฅผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ํ…Œ์ŠคํŠธํ•˜๊ณ  ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ•๋ ฅํ•œ ํ…Œ์ŠคํŠธ ์ „๋žต์„ ๊ตฌํ˜„ํ•˜๊ณ  ๋ณต์žกํ•œ ๋ฌธ์ œ๋ฅผ ๋””๋ฒ„๊น…ํ•˜๋ฉฐ ๋‹ค์–‘ํ•œ ์กฐ๊ฑด์—์„œ MCP ์„œ๋ฒ„๊ฐ€ ์•ˆ์ •์ ์œผ๋กœ ์ž‘๋™ํ•˜๋„๋ก ๋ณด์žฅํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ์„œ๋ฒ„ ํ…Œ์ŠคํŠธ๋Š” ๋‹จ์œ„ ํ…Œ์ŠคํŠธ, ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, ์„ฑ๋Šฅ ๊ฒ€์ฆ, ์‹ค์ œ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ๋ฅผ ํฌํ•จํ•˜๋Š” ๋‹ค์ธต์ ์ธ ์ ‘๊ทผ ๋ฐฉ์‹์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์€ ๊ฐœ๋ฐœ๋ถ€ํ„ฐ ํ”„๋กœ๋•์…˜ ๋ชจ๋‹ˆํ„ฐ๋ง๊นŒ์ง€์˜ ์ „์ฒด ํ…Œ์ŠคํŠธ ๋ผ์ดํ”„์‚ฌ์ดํด์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    ์šฐ๋ฆฌ์˜ ํ…Œ์ŠคํŠธ ์ „๋žต์€ ์‹ ๋ขฐ์„ฑ, ๋ณด์•ˆ, ์„ฑ๋Šฅ์„ ๊ฐ•์กฐํ•˜๋ฉฐ, MCP ์„œ๋ฒ„๊ฐ€ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜ ํ’ˆ์งˆ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ํ”„๋กœ๋•์…˜ ์›Œํฌ๋กœ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ํฌ๊ด„์ ์ธ ๋‹จ์œ„ ๋ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ ์Šค์œ„ํŠธ ๊ตฌํ˜„
  • MCP ๋„๊ตฌ ๋ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž‘์—…์„ ์œ„ํ•œ ํšจ๊ณผ์ ์ธ ํ…Œ์ŠคํŠธ ์ „๋žต ์„ค๊ณ„
  • ๊ณ ๊ธ‰ ๋””๋ฒ„๊น… ๊ธฐ์ˆ ์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ๋ฌธ์ œ ๋””๋ฒ„๊น…
  • ํ˜„์‹ค์ ์ธ ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ†ตํ•ด ๋ถ€ํ•˜ ์ƒํƒœ์—์„œ ์„ฑ๋Šฅ ๊ฒ€์ฆ
  • ํšจ๊ณผ์ ์ธ ๊ฒฝ๊ณ  ๋ฐ ๊ด€์ฐฐ ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ํ”„๋กœ๋•์…˜ ์‹œ์Šคํ…œ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ์ง€์†์  ํ†ตํ•ฉ์„ ์œ„ํ•œ ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ ์ž๋™ํ™”
  • ๐Ÿงช ํ…Œ์ŠคํŠธ ์•„ํ‚คํ…์ฒ˜

    ํ…Œ์ŠคํŠธ ์ „๋žต ๊ฐœ์š”

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                Unit Tests                       โ”‚
    
    โ”‚   โ€ข Tool execution logic                       โ”‚
    
    โ”‚   โ€ข Database query validation                  โ”‚
    
    โ”‚   โ€ข Authentication/authorization               โ”‚
    
    โ”‚   โ€ข Embedding generation                       โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚             Integration Tests                   โ”‚
    
    โ”‚   โ€ข End-to-end MCP workflows                  โ”‚
    
    โ”‚   โ€ข Database schema validation                 โ”‚
    
    โ”‚   โ€ข API endpoint testing                       โ”‚
    
    โ”‚   โ€ข Multi-tool interactions                    โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚            Performance Tests                    โ”‚
    
    โ”‚   โ€ข Load testing under realistic conditions    โ”‚
    
    โ”‚   โ€ข Database performance validation            โ”‚
    
    โ”‚   โ€ข Memory and resource usage                  โ”‚
    
    โ”‚   โ€ข Embedding generation performance           โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                      โ”‚
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚              E2E Tests                         โ”‚
    
    โ”‚   โ€ข Complete user workflows                    โ”‚
    
    โ”‚   โ€ข VS Code integration testing               โ”‚
    
    โ”‚   โ€ข Real-world scenario validation            โ”‚
    
    โ”‚   โ€ข Cross-browser compatibility               โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ •

    
    # tests/conftest.py
    
    """
    
    Pytest configuration and shared fixtures for MCP server testing.
    
    """
    
    import pytest
    
    import asyncio
    
    import asyncpg
    
    import os
    
    from typing import AsyncGenerator, Dict, Any
    
    from unittest.mock import AsyncMock, Mock
    
    import tempfile
    
    import shutil
    
    from datetime import datetime
    
    
    
    # Test configuration
    
    TEST_DATABASE_URL = "postgresql://test_user:test_pass@localhost:5432/test_retail_db"
    
    TEST_STORE_IDS = ['test_seattle', 'test_redmond', 'test_bellevue']
    
    
    
    @pytest.fixture(scope="session")
    
    def event_loop():
    
        """Create an instance of the default event loop for the test session."""
    
        loop = asyncio.get_event_loop_policy().new_event_loop()
    
        yield loop
    
        loop.close()
    
    
    
    @pytest.fixture(scope="session")
    
    async def test_database():
    
        """Set up test database with schema and sample data."""
    
        
    
        # Create test database connection
    
        sys_conn = await asyncpg.connect(
    
            "postgresql://postgres:password@localhost:5432/postgres"
    
        )
    
        
    
        try:
    
            # Create test database
    
            await sys_conn.execute("DROP DATABASE IF EXISTS test_retail_db")
    
            await sys_conn.execute("CREATE DATABASE test_retail_db")
    
        finally:
    
            await sys_conn.close()
    
        
    
        # Connect to test database and set up schema
    
        test_conn = await asyncpg.connect(TEST_DATABASE_URL)
    
        
    
        try:
    
            # Load schema
    
            schema_sql = await load_sql_file("../scripts/create_schema.sql")
    
            await test_conn.execute(schema_sql)
    
            
    
            # Load sample data
    
            sample_data_sql = await load_sql_file("../scripts/sample_data.sql")
    
            await test_conn.execute(sample_data_sql)
    
            
    
            yield test_conn
    
            
    
        finally:
    
            await test_conn.close()
    
            
    
            # Cleanup test database
    
            sys_conn = await asyncpg.connect(
    
                "postgresql://postgres:password@localhost:5432/postgres"
    
            )
    
            try:
    
                await sys_conn.execute("DROP DATABASE IF EXISTS test_retail_db")
    
            finally:
    
                await sys_conn.close()
    
    
    
    @pytest.fixture
    
    async def db_connection(test_database):
    
        """Provide a clean database connection for each test."""
    
        
    
        conn = await asyncpg.connect(TEST_DATABASE_URL)
    
        
    
        # Start transaction for test isolation
    
        tx = conn.transaction()
    
        await tx.start()
    
        
    
        try:
    
            yield conn
    
        finally:
    
            # Rollback transaction to maintain test isolation
    
            await tx.rollback()
    
            await conn.close()
    
    
    
    @pytest.fixture
    
    async def mock_embedding_manager():
    
        """Mock embedding manager for testing without Azure OpenAI calls."""
    
        
    
        mock_manager = AsyncMock()
    
        
    
        # Mock embedding generation
    
        mock_manager.generate_embedding.return_value = [0.1] * 1536  # Mock embedding
    
        mock_manager.generate_embeddings_batch.return_value = [[0.1] * 1536] * 10
    
        
    
        # Mock initialization
    
        mock_manager.initialize.return_value = None
    
        mock_manager.cleanup.return_value = None
    
        
    
        return mock_manager
    
    
    
    @pytest.fixture
    
    async def test_mcp_server(db_connection, mock_embedding_manager):
    
        """Set up test MCP server instance."""
    
        
    
        from mcp_server.server import MCPServer
    
        from mcp_server.database import DatabaseProvider
    
        from mcp_server.config import Config
    
        
    
        # Create test configuration
    
        config = Config()
    
        config.database.connection_string = TEST_DATABASE_URL
    
        config.server.enable_debug = True
    
        
    
        # Create database provider
    
        db_provider = DatabaseProvider(config.database.connection_string)
    
        await db_provider.initialize()
    
        
    
        # Create MCP server
    
        server = MCPServer(config, db_provider)
    
        server.embedding_manager = mock_embedding_manager
    
        
    
        await server.initialize()
    
        
    
        yield server
    
        
    
        await server.cleanup()
    
    
    
    @pytest.fixture
    
    def sample_products():
    
        """Sample product data for testing."""
    
        
    
        return [
    
            {
    
                'product_id': 'test-product-1',
    
                'product_name': 'Test Running Shoes',
    
                'brand': 'TestBrand',
    
                'price': 99.99,
    
                'product_description': 'Comfortable running shoes for daily training',
    
                'category_name': 'Electronics',
    
                'current_stock': 50
    
            },
    
            {
    
                'product_id': 'test-product-2',
    
                'product_name': 'Test Laptop',
    
                'brand': 'TestTech',
    
                'price': 1299.99,
    
                'product_description': 'High-performance laptop for professional use',
    
                'category_name': 'Electronics',
    
                'current_stock': 25
    
            }
    
        ]
    
    
    
    async def load_sql_file(file_path: str) -> str:
    
        """Load SQL file content."""
    
        
    
        with open(file_path, 'r') as file:
    
            return file.read()
    
    
    
    # Test data helpers
    
    class TestDataHelper:
    
        """Helper class for managing test data."""
    
        
    
        @staticmethod
    
        async def create_test_store(conn: asyncpg.Connection, store_id: str) -> Dict[str, Any]:
    
            """Create a test store."""
    
            
    
            store_data = {
    
                'store_id': store_id,
    
                'store_name': f'Test Store {store_id}',
    
                'store_location': 'Test Location',
    
                'store_type': 'test',
    
                'region': 'test'
    
            }
    
            
    
            await conn.execute("""
    
                INSERT INTO retail.stores (store_id, store_name, store_location, store_type, region)
    
                VALUES ($1, $2, $3, $4, $5)
    
                ON CONFLICT (store_id) DO NOTHING
    
            """, *store_data.values())
    
            
    
            return store_data
    
        
    
        @staticmethod
    
        async def create_test_customer(conn: asyncpg.Connection, store_id: str) -> str:
    
            """Create a test customer."""
    
            
    
            customer_id = await conn.fetchval("""
    
                INSERT INTO retail.customers (
    
                    store_id, first_name, last_name, email, loyalty_tier
    
                ) VALUES ($1, $2, $3, $4, $5)
    
                RETURNING customer_id
    
            """, store_id, 'Test', 'Customer', 'test@example.com', 'bronze')
    
            
    
            return customer_id
    
        
    
        @staticmethod
    
        async def create_test_product(
    
            conn: asyncpg.Connection, 
    
            store_id: str, 
    
            product_data: Dict[str, Any]
    
        ) -> str:
    
            """Create a test product."""
    
            
    
            product_id = await conn.fetchval("""
    
                INSERT INTO retail.products (
    
                    store_id, sku, product_name, brand, price, product_description, current_stock
    
                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    
                RETURNING product_id
    
            """, 
    
                store_id, 
    
                f"TEST-{product_data['product_name'][:10]}",
    
                product_data['product_name'],
    
                product_data['brand'],
    
                product_data['price'],
    
                product_data['product_description'],
    
                product_data['current_stock']
    
            )
    
            
    
            return product_id
    
    

    ๐Ÿ”ง ๋‹จ์œ„ ํ…Œ์ŠคํŠธ

    ๋„๊ตฌ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ

    
    # tests/test_tools.py
    
    """
    
    Comprehensive unit tests for MCP tools.
    
    """
    
    import pytest
    
    import asyncio
    
    from unittest.mock import AsyncMock, patch
    
    from datetime import datetime, timedelta
    
    
    
    from mcp_server.tools.sales_analysis import SalesAnalysisTool
    
    from mcp_server.tools.semantic_search import SemanticProductSearchTool
    
    from mcp_server.tools.schema_introspection import SchemaIntrospectionTool
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestSalesAnalysisTool:
    
        """Test sales analysis tool functionality."""
    
        
    
        @pytest.fixture
    
        async def sales_tool(self, test_mcp_server):
    
            """Create sales analysis tool for testing."""
    
            return SalesAnalysisTool(test_mcp_server.db_provider)
    
        
    
        async def test_daily_sales_query(self, sales_tool, db_connection):
    
            """Test daily sales analysis query."""
    
            
    
            # Set up test data
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            customer_id = await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Create test transaction
    
            await db_connection.execute("""
    
                INSERT INTO retail.sales_transactions (
    
                    store_id, customer_id, transaction_date, total_amount, transaction_type
    
                ) VALUES ($1, $2, $3, $4, $5)
    
            """, store_id, customer_id, datetime.now(), 150.00, 'sale')
    
            
    
            # Execute tool
    
            result = await sales_tool.execute(
    
                query_type='daily_sales',
    
                store_id=store_id,
    
                start_date=(datetime.now() - timedelta(days=7)).date(),
    
                end_date=datetime.now().date()
    
            )
    
            
    
            # Validate results
    
            assert result.success is True
    
            assert len(result.data) > 0
    
            assert 'total_revenue' in result.data[0]
    
            assert result.metadata['query_type'] == 'daily_sales'
    
        
    
        async def test_custom_query_validation(self, sales_tool, db_connection):
    
            """Test custom query validation."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Test valid query
    
            valid_query = "SELECT COUNT(*) as customer_count FROM retail.customers"
    
            result = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store_id,
    
                query=valid_query
    
            )
    
            
    
            assert result.success is True
    
            
    
            # Test invalid query (should be blocked)
    
            invalid_query = "DROP TABLE retail.customers"
    
            result = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store_id,
    
                query=invalid_query
    
            )
    
            
    
            assert result.success is False
    
            assert 'validation failed' in result.error.lower()
    
        
    
        async def test_store_isolation(self, sales_tool, db_connection):
    
            """Test that store isolation works correctly."""
    
            
    
            # Create two different stores
    
            store1 = 'test_store1'
    
            store2 = 'test_store2'
    
            
    
            await TestDataHelper.create_test_store(db_connection, store1)
    
            await TestDataHelper.create_test_store(db_connection, store2)
    
            
    
            # Create customers in different stores
    
            customer1 = await TestDataHelper.create_test_customer(db_connection, store1)
    
            customer2 = await TestDataHelper.create_test_customer(db_connection, store2)
    
            
    
            # Query from store1 should only see store1 data
    
            result1 = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store1,
    
                query="SELECT COUNT(*) as count FROM retail.customers"
    
            )
    
            
    
            # Query from store2 should only see store2 data
    
            result2 = await sales_tool.execute(
    
                query_type='custom',
    
                store_id=store2,
    
                query="SELECT COUNT(*) as count FROM retail.customers"
    
            )
    
            
    
            assert result1.success is True
    
            assert result2.success is True
    
            assert result1.data[0]['count'] == 1
    
            assert result2.data[0]['count'] == 1
    
    
    
    class TestSemanticSearchTool:
    
        """Test semantic search tool functionality."""
    
        
    
        @pytest.fixture
    
        async def search_tool(self, test_mcp_server):
    
            """Create semantic search tool for testing."""
    
            return SemanticProductSearchTool(test_mcp_server.db_provider)
    
        
    
        async def test_semantic_search_execution(self, search_tool, db_connection, sample_products):
    
            """Test semantic search with mock embeddings."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test products
    
            for product_data in sample_products:
    
                product_id = await TestDataHelper.create_test_product(
    
                    db_connection, store_id, product_data
    
                )
    
                
    
                # Create mock embedding
    
                await db_connection.execute("""
    
                    INSERT INTO retail.product_embeddings (
    
                        product_id, store_id, embedding_text, embedding
    
                    ) VALUES ($1, $2, $3, $4)
    
                """, 
    
                    product_id, store_id, 
    
                    f"{product_data['product_name']} {product_data['brand']}", 
    
                    '[0.1,0.2,0.3]'  # Mock embedding
    
                )
    
            
    
            # Execute search
    
            result = await search_tool.execute(
    
                query='running shoes',
    
                store_id=store_id,
    
                limit=10,
    
                similarity_threshold=0.0
    
            )
    
            
    
            # Validate results
    
            assert result.success is True
    
            assert len(result.data) > 0
    
            assert 'similarity_score' in result.data[0]
    
            assert result.metadata['search_type'] == 'semantic'
    
        
    
        async def test_search_parameter_validation(self, search_tool):
    
            """Test search parameter validation."""
    
            
    
            # Test missing query
    
            result = await search_tool.execute(store_id='test_store')
    
            assert result.success is False
    
            assert 'query is required' in result.error.lower()
    
            
    
            # Test missing store_id
    
            result = await search_tool.execute(query='test query')
    
            assert result.success is False
    
            assert 'store_id is required' in result.error.lower()
    
    
    
    class TestSchemaIntrospectionTool:
    
        """Test schema introspection tool."""
    
        
    
        @pytest.fixture
    
        async def schema_tool(self, test_mcp_server):
    
            """Create schema introspection tool for testing."""
    
            return SchemaIntrospectionTool(test_mcp_server.db_provider)
    
        
    
        async def test_single_table_schema(self, schema_tool, db_connection):
    
            """Test getting schema for a single table."""
    
            
    
            result = await schema_tool.execute(
    
                table_name='customers',
    
                include_constraints=True,
    
                include_indexes=True
    
            )
    
            
    
            assert result.success is True
    
            assert result.data['table_name'] == 'customers'
    
            assert len(result.data['columns']) > 0
    
            assert 'customer_id' in [col['column_name'] for col in result.data['columns']]
    
        
    
        async def test_all_tables_schema(self, schema_tool, db_connection):
    
            """Test getting schema for all tables."""
    
            
    
            result = await schema_tool.execute()
    
            
    
            assert result.success is True
    
            assert result.data['schema_name'] == 'retail'
    
            assert len(result.data['tables']) > 0
    
            
    
            table_names = [table['table_name'] for table in result.data['tables']]
    
            expected_tables = ['customers', 'products', 'sales_transactions']
    
            
    
            for expected_table in expected_tables:
    
                assert expected_table in table_names
    
    

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ…Œ์ŠคํŠธ

    
    # tests/test_database.py
    
    """
    
    Database layer testing including RLS and security.
    
    """
    
    import pytest
    
    import asyncpg
    
    from datetime import datetime
    
    
    
    from mcp_server.database import DatabaseProvider
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestRowLevelSecurity:
    
        """Test Row Level Security implementation."""
    
        
    
        async def test_store_context_setting(self, db_connection):
    
            """Test that store context is set correctly."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Set store context
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store_id)
    
            
    
            # Verify context is set
    
            current_store = await db_connection.fetchval(
    
                "SELECT current_setting('app.current_store_id', true)"
    
            )
    
            
    
            assert current_store == store_id
    
        
    
        async def test_customer_isolation(self, db_connection):
    
            """Test that customers are properly isolated by store."""
    
            
    
            # Create two stores
    
            store1 = 'test_store1'
    
            store2 = 'test_store2'
    
            
    
            await TestDataHelper.create_test_store(db_connection, store1)
    
            await TestDataHelper.create_test_store(db_connection, store2)
    
            
    
            # Create customers in different stores
    
            await TestDataHelper.create_test_customer(db_connection, store1)
    
            await TestDataHelper.create_test_customer(db_connection, store2)
    
            
    
            # Set context to store1 and count customers
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store1)
    
            store1_count = await db_connection.fetchval("SELECT COUNT(*) FROM retail.customers")
    
            
    
            # Set context to store2 and count customers
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store2)
    
            store2_count = await db_connection.fetchval("SELECT COUNT(*) FROM retail.customers")
    
            
    
            # Each store should only see its own customers
    
            assert store1_count == 1
    
            assert store2_count == 1
    
        
    
        async def test_invalid_store_context(self, db_connection):
    
            """Test that invalid store context raises error."""
    
            
    
            with pytest.raises(Exception) as exc_info:
    
                await db_connection.execute("SELECT retail.set_store_context($1)", 'invalid_store')
    
            
    
            assert "Store not found" in str(exc_info.value)
    
        
    
        async def test_cross_store_data_insertion_blocked(self, db_connection):
    
            """Test that users cannot insert data for other stores."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Set store context
    
            await db_connection.execute("SELECT retail.set_store_context($1)", store_id)
    
            
    
            # Try to insert customer for different store (should fail)
    
            with pytest.raises(Exception):
    
                await db_connection.execute("""
    
                    INSERT INTO retail.customers (store_id, first_name, last_name, email)
    
                    VALUES ($1, $2, $3, $4)
    
                """, 'different_store', 'Test', 'Customer', 'test@example.com')
    
    
    
    class TestDatabaseProvider:
    
        """Test database provider functionality."""
    
        
    
        @pytest.fixture
    
        async def db_provider(self):
    
            """Create database provider for testing."""
    
            
    
            provider = DatabaseProvider(TEST_DATABASE_URL)
    
            await provider.initialize()
    
            yield provider
    
            await provider.cleanup()
    
        
    
        async def test_connection_pooling(self, db_provider):
    
            """Test connection pool functionality."""
    
            
    
            # Get multiple connections
    
            conn1 = await db_provider.get_connection()
    
            conn2 = await db_provider.get_connection()
    
            
    
            assert conn1 is not None
    
            assert conn2 is not None
    
            assert conn1 != conn2  # Should be different connection objects
    
            
    
            # Release connections
    
            await db_provider.release_connection(conn1)
    
            await db_provider.release_connection(conn2)
    
        
    
        async def test_health_check(self, db_provider):
    
            """Test database health check."""
    
            
    
            health_status = await db_provider.health_check()
    
            
    
            assert health_status['status'] == 'healthy'
    
            assert 'connection_pool_size' in health_status
    
            assert 'database_version' in health_status
    
        
    
        async def test_connection_recovery(self, db_provider):
    
            """Test connection recovery after database issues."""
    
            
    
            # This would test connection recovery scenarios
    
            # In a real test, you might temporarily break the connection
    
            # and verify that the pool recovers
    
            
    
            # For now, just verify health check works
    
            health_status = await db_provider.health_check()
    
            assert health_status['status'] == 'healthy'
    
    

    ๐Ÿš€ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ

    ์—”๋“œ ํˆฌ ์—”๋“œ ์›Œํฌํ”Œ๋กœ ํ…Œ์ŠคํŠธ

    
    # tests/test_integration.py
    
    """
    
    Integration tests for complete MCP workflows.
    
    """
    
    import pytest
    
    import json
    
    from datetime import datetime, timedelta
    
    
    
    from mcp_server.server import MCPServer
    
    from tests.conftest import TestDataHelper
    
    
    
    class TestMCPWorkflows:
    
        """Test complete MCP server workflows."""
    
        
    
        async def test_product_search_workflow(self, test_mcp_server, db_connection, sample_products):
    
            """Test complete product search workflow."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test products with embeddings
    
            for product_data in sample_products:
    
                product_id = await TestDataHelper.create_test_product(
    
                    db_connection, store_id, product_data
    
                )
    
                
    
                # Create embedding for product
    
                await db_connection.execute("""
    
                    INSERT INTO retail.product_embeddings (
    
                        product_id, store_id, embedding_text, embedding
    
                    ) VALUES ($1, $2, $3, $4)
    
                """, 
    
                    product_id, store_id, 
    
                    f"{product_data['product_name']} {product_data['brand']}", 
    
                    '[' + ','.join(['0.1'] * 1536) + ']'  # Mock embedding
    
                )
    
            
    
            # Test semantic search
    
            search_result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {
    
                    'query': 'running shoes',
    
                    'store_id': store_id,
    
                    'limit': 10
    
                }
    
            )
    
            
    
            assert search_result['success'] is True
    
            assert len(search_result['data']) > 0
    
            
    
            # Test schema introspection
    
            schema_result = await test_mcp_server.execute_tool(
    
                'get_table_schema',
    
                {'table_name': 'products'}
    
            )
    
            
    
            assert schema_result['success'] is True
    
            assert schema_result['data']['table_name'] == 'products'
    
        
    
        async def test_sales_analysis_workflow(self, test_mcp_server, db_connection):
    
            """Test sales analysis workflow."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test customer and product
    
            customer_id = await TestDataHelper.create_test_customer(db_connection, store_id)
    
            product_id = await TestDataHelper.create_test_product(
    
                db_connection, store_id, {
    
                    'product_name': 'Test Product',
    
                    'brand': 'TestBrand',
    
                    'price': 99.99,
    
                    'product_description': 'Test product description',
    
                    'current_stock': 50
    
                }
    
            )
    
            
    
            # Create test transaction
    
            transaction_id = await db_connection.fetchval("""
    
                INSERT INTO retail.sales_transactions (
    
                    store_id, customer_id, transaction_date, total_amount, 
    
                    subtotal, tax_amount, transaction_type
    
                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
    
                RETURNING transaction_id
    
            """, store_id, customer_id, datetime.now(), 107.99, 99.99, 8.00, 'sale')
    
            
    
            # Create transaction item
    
            await db_connection.execute("""
    
                INSERT INTO retail.sales_transaction_items (
    
                    transaction_id, product_id, quantity, unit_price, total_price
    
                ) VALUES ($1, $2, $3, $4, $5)
    
            """, transaction_id, product_id, 1, 99.99, 99.99)
    
            
    
            # Test daily sales analysis
    
            sales_result = await test_mcp_server.execute_tool(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'daily_sales',
    
                    'store_id': store_id,
    
                    'start_date': (datetime.now() - timedelta(days=1)).date().isoformat(),
    
                    'end_date': datetime.now().date().isoformat()
    
                }
    
            )
    
            
    
            assert sales_result['success'] is True
    
            assert len(sales_result['data']) > 0
    
            assert sales_result['data'][0]['total_revenue'] == 107.99
    
        
    
        async def test_multi_store_workflow(self, test_mcp_server, db_connection):
    
            """Test workflows across multiple stores."""
    
            
    
            # Create multiple stores
    
            stores = ['test_seattle', 'test_redmond', 'test_bellevue']
    
            
    
            for store_id in stores:
    
                await TestDataHelper.create_test_store(db_connection, store_id)
    
                
    
                # Create customer in each store
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Test that each store sees only its own data
    
            for store_id in stores:
    
                schema_result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) as customer_count FROM retail.customers'
    
                    }
    
                )
    
                
    
                assert schema_result['success'] is True
    
                assert schema_result['data'][0]['customer_count'] == 1
    
    
    
    class TestErrorHandling:
    
        """Test error handling and edge cases."""
    
        
    
        async def test_database_connection_failure(self, test_mcp_server):
    
            """Test behavior when database connection fails."""
    
            
    
            # Simulate database failure by using invalid connection
    
            with patch.object(test_mcp_server.db_provider, 'get_connection') as mock_conn:
    
                mock_conn.side_effect = Exception("Database connection failed")
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'get_table_schema',
    
                    {'table_name': 'customers'}
    
                )
    
                
    
                assert result['success'] is False
    
                assert 'connection failed' in result['error'].lower()
    
        
    
        async def test_invalid_tool_parameters(self, test_mcp_server):
    
            """Test handling of invalid tool parameters."""
    
            
    
            # Missing required parameter
    
            result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {'query': 'test query'}  # Missing store_id
    
            )
    
            
    
            assert result['success'] is False
    
            assert 'store_id is required' in result['error'].lower()
    
            
    
            # Invalid parameter type
    
            result = await test_mcp_server.execute_tool(
    
                'semantic_search_products',
    
                {
    
                    'query': 'test query',
    
                    'store_id': 'test_store',
    
                    'limit': 'invalid'  # Should be integer
    
                }
    
            )
    
            
    
            assert result['success'] is False
    
        
    
        async def test_sql_injection_prevention(self, test_mcp_server, db_connection):
    
            """Test that SQL injection attempts are blocked."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Attempt SQL injection
    
            malicious_query = "SELECT * FROM retail.customers; DROP TABLE retail.customers; --"
    
            
    
            result = await test_mcp_server.execute_tool(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'query': malicious_query
    
                }
    
            )
    
            
    
            assert result['success'] is False
    
            assert 'validation failed' in result['error'].lower()
    
    

    ๐Ÿ“Š ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

    ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ

    
    # tests/test_performance.py
    
    """
    
    Performance and load testing for MCP server.
    
    """
    
    import pytest
    
    import asyncio
    
    import time
    
    import statistics
    
    from concurrent.futures import ThreadPoolExecutor
    
    from typing import List, Dict, Any
    
    
    
    class TestPerformance:
    
        """Performance testing for MCP server operations."""
    
        
    
        async def test_concurrent_tool_execution(self, test_mcp_server, db_connection):
    
            """Test performance under concurrent tool execution."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create test data
    
            for i in range(100):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Define test scenarios
    
            async def execute_tool_scenario():
    
                """Execute a tool and measure performance."""
    
                start_time = time.time()
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) as count FROM retail.customers'
    
                    }
    
                )
    
                
    
                execution_time = time.time() - start_time
    
                return {
    
                    'success': result['success'],
    
                    'execution_time': execution_time
    
                }
    
            
    
            # Run concurrent executions
    
            concurrent_tasks = 20
    
            tasks = [execute_tool_scenario() for _ in range(concurrent_tasks)]
    
            
    
            start_time = time.time()
    
            results = await asyncio.gather(*tasks)
    
            total_time = time.time() - start_time
    
            
    
            # Analyze results
    
            successful_executions = [r for r in results if r['success']]
    
            execution_times = [r['execution_time'] for r in successful_executions]
    
            
    
            assert len(successful_executions) == concurrent_tasks
    
            assert statistics.mean(execution_times) < 1.0  # Average under 1 second
    
            assert max(execution_times) < 5.0  # No execution over 5 seconds
    
            assert total_time < 10.0  # All executions under 10 seconds
    
            
    
            print(f"Concurrent execution stats:")
    
            print(f"  Total time: {total_time:.2f}s")
    
            print(f"  Average execution time: {statistics.mean(execution_times):.3f}s")
    
            print(f"  Max execution time: {max(execution_times):.3f}s")
    
            print(f"  Min execution time: {min(execution_times):.3f}s")
    
        
    
        async def test_database_query_performance(self, test_mcp_server, db_connection):
    
            """Test database query performance with large datasets."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create large dataset
    
            print("Creating test dataset...")
    
            for i in range(1000):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Test various query patterns
    
            query_tests = [
    
                {
    
                    'name': 'Simple COUNT',
    
                    'query': 'SELECT COUNT(*) FROM retail.customers',
    
                    'expected_max_time': 0.1
    
                },
    
                {
    
                    'name': 'Filtered SELECT',
    
                    'query': "SELECT * FROM retail.customers WHERE loyalty_tier = 'bronze' LIMIT 100",
    
                    'expected_max_time': 0.5
    
                },
    
                {
    
                    'name': 'Aggregation',
    
                    'query': 'SELECT loyalty_tier, COUNT(*) FROM retail.customers GROUP BY loyalty_tier',
    
                    'expected_max_time': 0.5
    
                }
    
            ]
    
            
    
            for test_case in query_tests:
    
                start_time = time.time()
    
                
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': test_case['query']
    
                    }
    
                )
    
                
    
                execution_time = time.time() - start_time
    
                
    
                assert result['success'] is True
    
                assert execution_time < test_case['expected_max_time']
    
                
    
                print(f"Query '{test_case['name']}': {execution_time:.3f}s")
    
        
    
        async def test_embedding_generation_performance(self, test_mcp_server):
    
            """Test embedding generation performance."""
    
            
    
            from mcp_server.embeddings.product_embedder import ProductEmbedder
    
            
    
            # Test with mock embedding manager (no actual API calls)
    
            embedder = ProductEmbedder(test_mcp_server.db_provider)
    
            embedder.embedding_manager = test_mcp_server.embedding_manager
    
            
    
            # Test batch embedding generation
    
            test_texts = [f"Test product {i} description" for i in range(100)]
    
            
    
            start_time = time.time()
    
            embeddings = await embedder.embedding_manager.generate_embeddings_batch(test_texts)
    
            batch_time = time.time() - start_time
    
            
    
            assert len(embeddings) == 100
    
            assert batch_time < 5.0  # Should complete in under 5 seconds with mocks
    
            
    
            print(f"Batch embedding generation (100 items): {batch_time:.3f}s")
    
            print(f"Average per embedding: {batch_time/100:.4f}s")
    
        
    
        @pytest.mark.slow
    
        async def test_memory_usage(self, test_mcp_server, db_connection):
    
            """Test memory usage under load."""
    
            
    
            import psutil
    
            import os
    
            
    
            process = psutil.Process(os.getpid())
    
            initial_memory = process.memory_info().rss / 1024 / 1024  # MB
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Create substantial dataset
    
            for i in range(500):
    
                await TestDataHelper.create_test_customer(db_connection, store_id)
    
            
    
            # Execute multiple operations
    
            for i in range(50):
    
                await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT * FROM retail.customers LIMIT 100'
    
                    }
    
                )
    
            
    
            final_memory = process.memory_info().rss / 1024 / 1024  # MB
    
            memory_increase = final_memory - initial_memory
    
            
    
            # Memory increase should be reasonable (under 100MB for this test)
    
            assert memory_increase < 100
    
            
    
            print(f"Memory usage:")
    
            print(f"  Initial: {initial_memory:.1f} MB")
    
            print(f"  Final: {final_memory:.1f} MB")
    
            print(f"  Increase: {memory_increase:.1f} MB")
    
    
    
    class TestScalability:
    
        """Test scalability characteristics."""
    
        
    
        async def test_response_time_scaling(self, test_mcp_server, db_connection):
    
            """Test how response time scales with data size."""
    
            
    
            store_id = 'test_seattle'
    
            await TestDataHelper.create_test_store(db_connection, store_id)
    
            
    
            # Test with different data sizes
    
            data_sizes = [100, 500, 1000, 2000]
    
            response_times = []
    
            
    
            for size in data_sizes:
    
                # Clear existing data
    
                await db_connection.execute("DELETE FROM retail.customers WHERE store_id = $1", store_id)
    
                
    
                # Create dataset of specified size
    
                for i in range(size):
    
                    await TestDataHelper.create_test_customer(db_connection, store_id)
    
                
    
                # Measure query time
    
                start_time = time.time()
    
                result = await test_mcp_server.execute_tool(
    
                    'execute_sales_query',
    
                    {
    
                        'query_type': 'custom',
    
                        'store_id': store_id,
    
                        'query': 'SELECT COUNT(*) FROM retail.customers'
    
                    }
    
                )
    
                execution_time = time.time() - start_time
    
                
    
                assert result['success'] is True
    
                response_times.append(execution_time)
    
                
    
                print(f"Data size {size}: {execution_time:.3f}s")
    
            
    
            # Response time should scale reasonably (not exponentially)
    
            # Simple count queries should remain fast even with larger datasets
    
            for time_val in response_times:
    
                assert time_val < 1.0  # All queries under 1 second
    
    

    ๐Ÿ” ๋””๋ฒ„๊น… ๋„๊ตฌ

    ๊ณ ๊ธ‰ ๋””๋ฒ„๊น… ํ”„๋ ˆ์ž„์›Œํฌ

    
    # mcp_server/debugging/debug_tools.py
    
    """
    
    Advanced debugging tools for MCP server troubleshooting.
    
    """
    
    import asyncio
    
    import json
    
    import time
    
    import traceback
    
    from typing import Dict, Any, List, Optional
    
    from datetime import datetime
    
    import logging
    
    from contextlib import asynccontextmanager
    
    
    
    logger = logging.getLogger(__name__)
    
    
    
    class MCPDebugger:
    
        """Comprehensive debugging utilities for MCP server."""
    
        
    
        def __init__(self, server_instance):
    
            self.server = server_instance
    
            self.debug_logs = []
    
            self.performance_metrics = {}
    
            self.active_traces = {}
    
            
    
        @asynccontextmanager
    
        async def trace_execution(self, operation_name: str, context: Dict[str, Any] = None):
    
            """Trace operation execution with detailed logging."""
    
            
    
            trace_id = f"{operation_name}_{int(time.time() * 1000)}"
    
            start_time = time.time()
    
            
    
            trace_info = {
    
                'trace_id': trace_id,
    
                'operation': operation_name,
    
                'start_time': start_time,
    
                'context': context or {},
    
                'status': 'running'
    
            }
    
            
    
            self.active_traces[trace_id] = trace_info
    
            
    
            logger.debug(f"Starting trace: {trace_id} - {operation_name}")
    
            
    
            try:
    
                yield trace_info
    
                
    
                # Success
    
                execution_time = time.time() - start_time
    
                trace_info.update({
    
                    'status': 'completed',
    
                    'execution_time': execution_time
    
                })
    
                
    
                logger.debug(f"Completed trace: {trace_id} in {execution_time:.3f}s")
    
                
    
            except Exception as e:
    
                # Error
    
                execution_time = time.time() - start_time
    
                trace_info.update({
    
                    'status': 'error',
    
                    'execution_time': execution_time,
    
                    'error': str(e),
    
                    'traceback': traceback.format_exc()
    
                })
    
                
    
                logger.error(f"Error in trace: {trace_id} - {str(e)}")
    
                raise
    
                
    
            finally:
    
                # Store completed trace
    
                self.debug_logs.append(trace_info.copy())
    
                del self.active_traces[trace_id]
    
                
    
                # Limit debug log size
    
                if len(self.debug_logs) > 1000:
    
                    self.debug_logs = self.debug_logs[-500:]
    
        
    
        async def debug_tool_execution(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
    
            """Debug tool execution with comprehensive logging."""
    
            
    
            async with self.trace_execution(f"tool_execution_{tool_name}", {'parameters': parameters}) as trace:
    
                
    
                # Pre-execution validation
    
                validation_result = await self._validate_tool_parameters(tool_name, parameters)
    
                trace['validation'] = validation_result
    
                
    
                if not validation_result['valid']:
    
                    return {
    
                        'success': False,
    
                        'error': f"Parameter validation failed: {validation_result['errors']}",
    
                        'debug_info': trace
    
                    }
    
                
    
                # Database connection check
    
                db_health = await self._check_database_health()
    
                trace['database_health'] = db_health
    
                
    
                # Execute tool with monitoring
    
                try:
    
                    tool_instance = self.server.get_tool(tool_name)
    
                    if not tool_instance:
    
                        return {
    
                            'success': False,
    
                            'error': f"Tool '{tool_name}' not found",
    
                            'debug_info': trace
    
                        }
    
                    
    
                    # Monitor resource usage during execution
    
                    start_memory = await self._get_memory_usage()
    
                    
    
                    result = await tool_instance.call(**parameters)
    
                    
    
                    end_memory = await self._get_memory_usage()
    
                    
    
                    trace.update({
    
                        'memory_start_mb': start_memory,
    
                        'memory_end_mb': end_memory,
    
                        'memory_used_mb': end_memory - start_memory,
    
                        'result_success': result.success,
    
                        'result_row_count': result.row_count
    
                    })
    
                    
    
                    return {
    
                        'success': result.success,
    
                        'data': result.data,
    
                        'error': result.error,
    
                        'metadata': result.metadata,
    
                        'debug_info': trace
    
                    }
    
                    
    
                except Exception as e:
    
                    trace['exception'] = {
    
                        'type': type(e).__name__,
    
                        'message': str(e),
    
                        'traceback': traceback.format_exc()
    
                    }
    
                    
    
                    return {
    
                        'success': False,
    
                        'error': f"Tool execution failed: {str(e)}",
    
                        'debug_info': trace
    
                    }
    
        
    
        async def analyze_performance_bottlenecks(self) -> Dict[str, Any]:
    
            """Analyze performance bottlenecks from debug logs."""
    
            
    
            if not self.debug_logs:
    
                return {'message': 'No debug data available'}
    
            
    
            # Analyze execution times
    
            execution_times = {}
    
            error_rates = {}
    
            memory_usage = {}
    
            
    
            for log_entry in self.debug_logs[-100:]:  # Last 100 entries
    
                operation = log_entry['operation']
    
                
    
                # Execution time analysis
    
                if 'execution_time' in log_entry:
    
                    if operation not in execution_times:
    
                        execution_times[operation] = []
    
                    execution_times[operation].append(log_entry['execution_time'])
    
                
    
                # Error rate analysis
    
                if operation not in error_rates:
    
                    error_rates[operation] = {'total': 0, 'errors': 0}
    
                
    
                error_rates[operation]['total'] += 1
    
                if log_entry['status'] == 'error':
    
                    error_rates[operation]['errors'] += 1
    
                
    
                # Memory usage analysis
    
                if 'memory_used_mb' in log_entry:
    
                    if operation not in memory_usage:
    
                        memory_usage[operation] = []
    
                    memory_usage[operation].append(log_entry['memory_used_mb'])
    
            
    
            # Calculate statistics
    
            performance_stats = {}
    
            
    
            for operation, times in execution_times.items():
    
                if times:
    
                    performance_stats[operation] = {
    
                        'avg_execution_time': sum(times) / len(times),
    
                        'max_execution_time': max(times),
    
                        'min_execution_time': min(times),
    
                        'execution_count': len(times),
    
                        'error_rate': (error_rates[operation]['errors'] / 
    
                                     error_rates[operation]['total'] * 100),
    
                        'avg_memory_usage': (sum(memory_usage.get(operation, [0])) / 
    
                                           len(memory_usage.get(operation, [1])))
    
                    }
    
            
    
            # Identify bottlenecks
    
            bottlenecks = []
    
            
    
            for operation, stats in performance_stats.items():
    
                if stats['avg_execution_time'] > 2.0:  # Slow operations
    
                    bottlenecks.append({
    
                        'type': 'slow_execution',
    
                        'operation': operation,
    
                        'avg_time': stats['avg_execution_time']
    
                    })
    
                
    
                if stats['error_rate'] > 5.0:  # High error rate
    
                    bottlenecks.append({
    
                        'type': 'high_error_rate',
    
                        'operation': operation,
    
                        'error_rate': stats['error_rate']
    
                    })
    
                
    
                if stats['avg_memory_usage'] > 100:  # High memory usage
    
                    bottlenecks.append({
    
                        'type': 'high_memory_usage',
    
                        'operation': operation,
    
                        'memory_mb': stats['avg_memory_usage']
    
                    })
    
            
    
            return {
    
                'performance_stats': performance_stats,
    
                'bottlenecks': bottlenecks,
    
                'total_operations': len(self.debug_logs),
    
                'analysis_timestamp': datetime.now().isoformat()
    
            }
    
        
    
        async def _validate_tool_parameters(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]:
    
            """Validate tool parameters against schema."""
    
            
    
            try:
    
                tool_instance = self.server.get_tool(tool_name)
    
                if not tool_instance:
    
                    return {
    
                        'valid': False,
    
                        'errors': [f"Tool '{tool_name}' not found"]
    
                    }
    
                
    
                schema = tool_instance.get_input_schema()
    
                
    
                # Basic validation (in production, use jsonschema library)
    
                errors = []
    
                required_props = schema.get('required', [])
    
                
    
                for prop in required_props:
    
                    if prop not in parameters:
    
                        errors.append(f"Missing required parameter: {prop}")
    
                
    
                return {
    
                    'valid': len(errors) == 0,
    
                    'errors': errors,
    
                    'schema': schema
    
                }
    
                
    
            except Exception as e:
    
                return {
    
                    'valid': False,
    
                    'errors': [f"Validation error: {str(e)}"]
    
                }
    
        
    
        async def _check_database_health(self) -> Dict[str, Any]:
    
            """Check database health and connectivity."""
    
            
    
            try:
    
                health_status = await self.server.db_provider.health_check()
    
                return {
    
                    'healthy': health_status.get('status') == 'healthy',
    
                    'details': health_status
    
                }
    
            except Exception as e:
    
                return {
    
                    'healthy': False,
    
                    'error': str(e)
    
                }
    
        
    
        async def _get_memory_usage(self) -> float:
    
            """Get current memory usage in MB."""
    
            
    
            try:
    
                import psutil
    
                import os
    
                process = psutil.Process(os.getpid())
    
                return process.memory_info().rss / 1024 / 1024
    
            except:
    
                return 0.0
    
        
    
        def get_debug_summary(self) -> Dict[str, Any]:
    
            """Get summary of debug information."""
    
            
    
            recent_logs = self.debug_logs[-50:] if self.debug_logs else []
    
            
    
            return {
    
                'total_operations': len(self.debug_logs),
    
                'active_traces': len(self.active_traces),
    
                'recent_operations': [
    
                    {
    
                        'operation': log['operation'],
    
                        'status': log['status'],
    
                        'execution_time': log.get('execution_time', 0),
    
                        'timestamp': log.get('start_time', 0)
    
                    }
    
                    for log in recent_logs
    
                ],
    
                'current_traces': list(self.active_traces.keys())
    
            }
    
    
    
    # Debug tool for direct use
    
    class DebugTool:
    
        """Interactive debugging tool for MCP server."""
    
        
    
        def __init__(self, server_instance):
    
            self.debugger = MCPDebugger(server_instance)
    
        
    
        async def debug_query(self, query: str, store_id: str) -> Dict[str, Any]:
    
            """Debug a specific database query."""
    
            
    
            return await self.debugger.debug_tool_execution(
    
                'execute_sales_query',
    
                {
    
                    'query_type': 'custom',
    
                    'store_id': store_id,
    
                    'query': query
    
                }
    
            )
    
        
    
        async def debug_search(self, query: str, store_id: str) -> Dict[str, Any]:
    
            """Debug a semantic search query."""
    
            
    
            return await self.debugger.debug_tool_execution(
    
                'semantic_search_products',
    
                {
    
                    'query': query,
    
                    'store_id': store_id,
    
                    'limit': 10
    
                }
    
            )
    
        
    
        async def get_performance_report(self) -> Dict[str, Any]:
    
            """Get comprehensive performance report."""
    
            
    
            return await self.debugger.analyze_performance_bottlenecks()
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ํฌ๊ด„์ ์ธ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ: ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ์— ๋Œ€ํ•œ ๋‹จ์œ„, ํ†ตํ•ฉ, ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

    โœ… ๊ณ ๊ธ‰ ๋””๋ฒ„๊น… ๋„๊ตฌ: ์‹คํ–‰ ์ถ”์  ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ์ •๊ตํ•œ ๋””๋ฒ„๊น… ์œ ํ‹ธ๋ฆฌํ‹ฐ

    โœ… ์„ฑ๋Šฅ ๊ฒ€์ฆ: ๋ถ€ํ•˜ ํ…Œ์ŠคํŠธ ๋ฐ ํ™•์žฅ์„ฑ ๋ถ„์„ ๊ธฐ๋Šฅ

    โœ… ๋ณด์•ˆ ํ…Œ์ŠคํŠธ: SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ๋ฐ RLS ๊ฒ€์ฆ

    โœ… ๋ชจ๋‹ˆํ„ฐ๋ง ํ†ตํ•ฉ: ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ๋ฐ ๋ณ‘๋ชฉ ํ˜„์ƒ ๋ถ„์„

    โœ… CI/CD ์ค€๋น„ ์™„๋ฃŒ: ์ง€์†์  ํ†ตํ•ฉ์„ ์œ„ํ•œ ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ ์›Œํฌํ”Œ๋กœ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 09: VS Code Integration์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์„ ์œ„ํ•œ VS Code ๊ตฌ์„ฑ
  • VS Code์—์„œ ๋””๋ฒ„๊น… ํ™˜๊ฒฝ ์„ค์ •
  • MCP ์„œ๋ฒ„๋ฅผ VS Code Chat๊ณผ ํ†ตํ•ฉ
  • VS Code ์›Œํฌํ”Œ๋กœ ์ „์ฒด ํ…Œ์ŠคํŠธ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ

  • pytest Documentation - Python ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ
  • AsyncPG Testing - Async PostgreSQL ํ…Œ์ŠคํŠธ
  • FastAPI Testing - API ํ…Œ์ŠคํŠธ ํŒจํ„ด
  • ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ

  • Load Testing Best Practices - Async ์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ
  • Database Performance Testing - PostgreSQL ์ตœ์ ํ™”
  • Memory Profiling - Python ๋ฉ”๋ชจ๋ฆฌ ๋ถ„์„
  • ๋””๋ฒ„๊น… ๋„๊ตฌ

  • Python Debugging - Python ๋””๋ฒ„๊ฑฐ
  • Async Debugging - Asyncio ๋””๋ฒ„๊น…
  • SQL Debugging - PostgreSQL ๋กœ๊น…
  • ---

    ์ด์ „: Lab 07: Semantic Search Integration

    ๋‹ค์Œ: Lab 09: VS Code Integration

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ๋Š” ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    09 VS Code ํ†ตํ•ฉ

    VS Code ํ†ตํ•ฉ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ MCP ์„œ๋ฒ„๋ฅผ VS Code์™€ ํ†ตํ•ฉํ•˜์—ฌ AI ์ฑ„ํŒ…์„ ํ†ตํ•œ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ข…ํ•ฉ์ ์ธ ๊ฐ€์ด๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. VS Code๋ฅผ MCP ์‚ฌ์šฉ์— ์ตœ์ ํ™”ํ•˜๋„๋ก ์„ค์ •ํ•˜๊ณ , ์„œ๋ฒ„ ์—ฐ๊ฒฐ์„ ๋””๋ฒ„๊น…ํ•˜๋ฉฐ, AI ์ง€์› ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํ˜ธ์ž‘์šฉ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    VS Code์˜ MCP ํ†ตํ•ฉ์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž์—ฐ์–ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ API๋ฅผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ํ˜์‹ ์ ์œผ๋กœ ๋ณ€ํ™”์‹œํ‚ต๋‹ˆ๋‹ค. ์†Œ๋งค MCP ์„œ๋ฒ„๋ฅผ VS Code Chat์— ์—ฐ๊ฒฐํ•˜๋ฉด, ๋Œ€ํ™”ํ˜• AI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŒ๋งค ๋ฐ์ดํ„ฐ, ์ œํ’ˆ ์นดํƒˆ๋กœ๊ทธ, ๋น„์ฆˆ๋‹ˆ์Šค ๋ถ„์„์„ ์ง€๋Šฅ์ ์œผ๋กœ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ด ํ†ตํ•ฉ์„ ํ†ตํ•ด ๊ฐœ๋ฐœ์ž๋Š” "์ด๋ฒˆ ๋‹ฌ์— ๊ฐ€์žฅ ๋งŽ์ด ํŒ”๋ฆฐ ์ œํ’ˆ์„ ๋ณด์—ฌ์ค˜" ๋˜๋Š” "90์ผ ๋™์•ˆ ๊ตฌ๋งคํ•˜์ง€ ์•Š์€ ๊ณ ๊ฐ์„ ์ฐพ์•„์ค˜"์™€ ๊ฐ™์€ ์งˆ๋ฌธ์„ ํ•˜๊ณ , SQL ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š๊ณ ๋„ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • VS Code MCP ์„ค์ • ๊ตฌ์„ฑ: ์†Œ๋งค ์„œ๋ฒ„์— ๋งž๊ฒŒ VS Code ์„ค์ •
  • MCP ์„œ๋ฒ„ ํ†ตํ•ฉ: VS Code AI Chat ๊ธฐ๋Šฅ๊ณผ MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ
  • ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๋””๋ฒ„๊น…: ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ๋””๋ฒ„๊น…
  • ์ž์—ฐ์–ด ์ฟผ๋ฆฌ ์ตœ์ ํ™”: ๋” ๋‚˜์€ ๊ฒฐ๊ณผ๋ฅผ ์œ„ํ•œ ์ฟผ๋ฆฌ ํŒจํ„ด ์ตœ์ ํ™”
  • VS Code ์ž‘์—… ๊ณต๊ฐ„ ๋งž์ถคํ™”: MCP ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ์ž‘์—… ๊ณต๊ฐ„ ์„ค์ •
  • ๋‹ค์ค‘ ์„œ๋ฒ„ ๊ตฌ์„ฑ ๋ฐฐํฌ: ๋ณต์žกํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์œ„ํ•œ ๋‹ค์ค‘ ์„œ๋ฒ„ ์„ค์ •
  • ๐Ÿ”ง VS Code MCP ์„ค์ •

    ์ดˆ๊ธฐ ์„ค์ • ๋ฐ ์„ค์น˜

    
    // .vscode/settings.json
    
    {
    
        "mcp.servers": {
    
            "retail-mcp-server": {
    
                "command": "python",
    
                "args": [
    
                    "-m", "mcp_server.main"
    
                ],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_PORT": "5432",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "AZURE_CLIENT_ID": "${env:AZURE_CLIENT_ID}",
    
                    "AZURE_CLIENT_SECRET": "${env:AZURE_CLIENT_SECRET}",
    
                    "AZURE_TENANT_ID": "${env:AZURE_TENANT_ID}",
    
                    "LOG_LEVEL": "INFO",
    
                    "MCP_SERVER_DEBUG": "false"
    
                },
    
                "cwd": "${workspaceFolder}",
    
                "initializationOptions": {
    
                    "store_id": "seattle",
    
                    "enable_semantic_search": true,
    
                    "enable_analytics": true,
    
                    "cache_embeddings": true
    
                }
    
            }
    
        },
    
        "mcp.serverTimeout": 30000,
    
        "mcp.enableLogging": true,
    
        "mcp.logLevel": "info"
    
    }
    
    

    ํ™˜๊ฒฝ ๊ตฌ์„ฑ

    
    # .env file for development
    
    POSTGRES_HOST=localhost
    
    POSTGRES_PORT=5432
    
    POSTGRES_DB=retail_db
    
    POSTGRES_USER=mcp_user
    
    POSTGRES_PASSWORD=your_secure_password
    
    
    
    # Azure Configuration
    
    PROJECT_ENDPOINT=https://your-project.openai.azure.com
    
    AZURE_CLIENT_ID=your-client-id
    
    AZURE_CLIENT_SECRET=your-client-secret
    
    AZURE_TENANT_ID=your-tenant-id
    
    
    
    # Optional: Azure Key Vault
    
    AZURE_KEY_VAULT_URL=https://your-keyvault.vault.azure.net/
    
    
    
    # Server Configuration
    
    MCP_SERVER_PORT=8000
    
    MCP_SERVER_HOST=127.0.0.1
    
    LOG_LEVEL=INFO
    
    

    ์ž‘์—… ๊ณต๊ฐ„ ๊ตฌ์„ฑ

    
    // .vscode/launch.json
    
    {
    
        "version": "0.2.0",
    
        "configurations": [
    
            {
    
                "name": "Debug MCP Server",
    
                "type": "python",
    
                "request": "launch",
    
                "module": "mcp_server.main",
    
                "console": "integratedTerminal",
    
                "envFile": "${workspaceFolder}/.env",
    
                "env": {
    
                    "MCP_SERVER_DEBUG": "true",
    
                    "LOG_LEVEL": "DEBUG"
    
                },
    
                "args": [],
    
                "justMyCode": false,
    
                "stopOnEntry": false
    
            },
    
            {
    
                "name": "Test MCP Server",
    
                "type": "python",
    
                "request": "launch",
    
                "module": "pytest",
    
                "console": "integratedTerminal",
    
                "envFile": "${workspaceFolder}/.env.test",
    
                "args": [
    
                    "tests/",
    
                    "-v",
    
                    "--tb=short"
    
                ]
    
            }
    
        ]
    
    }
    
    

    ์ž‘์—…(Task) ๊ตฌ์„ฑ

    
    // .vscode/tasks.json
    
    {
    
        "version": "2.0.0",
    
        "tasks": [
    
            {
    
                "label": "Start MCP Server",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "-m", "mcp_server.main"
    
                ],
    
                "group": "build",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "new"
    
                },
    
                "options": {
    
                    "env": {
    
                        "PYTHONPATH": "${workspaceFolder}"
    
                    }
    
                },
    
                "isBackground": true,
    
                "problemMatcher": {
    
                    "pattern": {
    
                        "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
    
                        "file": 1,
    
                        "line": 2,
    
                        "column": 3,
    
                        "severity": 4,
    
                        "message": 5
    
                    },
    
                    "background": {
    
                        "activeOnStart": true,
    
                        "beginsPattern": "^.*Starting MCP server.*$",
    
                        "endsPattern": "^.*MCP server ready.*$"
    
                    }
    
                }
    
            },
    
            {
    
                "label": "Run Tests",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "-m", "pytest",
    
                    "tests/",
    
                    "-v"
    
                ],
    
                "group": "test",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "shared"
    
                }
    
            },
    
            {
    
                "label": "Generate Sample Data",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "scripts/generate_sample_data.py"
    
                ],
    
                "group": "build",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "shared"
    
                }
    
            },
    
            {
    
                "label": "Create Database Schema",
    
                "type": "shell",
    
                "command": "psql",
    
                "args": [
    
                    "-h", "${env:POSTGRES_HOST}",
    
                    "-p", "${env:POSTGRES_PORT}",
    
                    "-U", "${env:POSTGRES_USER}",
    
                    "-d", "${env:POSTGRES_DB}",
    
                    "-f", "scripts/create_schema.sql"
    
                ],
    
                "group": "build"
    
            }
    
        ]
    
    }
    
    

    ๐Ÿ’ฌ AI ์ฑ„ํŒ… ํ†ตํ•ฉ

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ ํŒจํ„ด

    
    // Example query patterns for VS Code Chat
    
    interface QueryPattern {
    
        intent: string;
    
        examples: string[];
    
        expectedTools: string[];
    
    }
    
    
    
    const retailQueryPatterns: QueryPattern[] = [
    
        {
    
            intent: "sales_analysis",
    
            examples: [
    
                "Show me daily sales for the last 30 days",
    
                "What are our top selling products this month?",
    
                "Which customers have spent the most this quarter?",
    
                "Compare sales performance between stores"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "product_search",
    
            examples: [
    
                "Find running shoes for women",
    
                "Show me electronics under $500",
    
                "What laptops do we have in stock?",
    
                "Search for wireless headphones"
    
            ],
    
            expectedTools: ["semantic_search_products", "hybrid_product_search"]
    
        },
    
        {
    
            intent: "inventory_management",
    
            examples: [
    
                "Which products are low on stock?",
    
                "Show me products that need reordering",
    
                "What's our current inventory value?",
    
                "Find products with zero stock"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "customer_analysis",
    
            examples: [
    
                "Show me customers who haven't purchased in 90 days",
    
                "What's the average customer lifetime value?",
    
                "Which customers are in the gold tier?",
    
                "Find customers with returns"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "business_intelligence",
    
            examples: [
    
                "Generate a business summary for this month",
    
                "Show me seasonal trends",
    
                "What are our best performing categories?",
    
                "Create a sales forecast"
    
            ],
    
            expectedTools: ["generate_business_insights"]
    
        },
    
        {
    
            intent: "recommendations",
    
            examples: [
    
                "Recommend products similar to product X",
    
                "What should we recommend to customer Y?",
    
                "Show me trending products",
    
                "Find cross-sell opportunities"
    
            ],
    
            expectedTools: ["get_product_recommendations"]
    
        }
    
    ];
    
    

    ์ฑ„ํŒ… ํ†ตํ•ฉ ์˜ˆ์ œ

    
    <!-- Examples of VS Code Chat interactions -->
    
    
    
    ## Sales Analysis Queries
    
    
    
    **User**: Show me the top 10 selling products in the Seattle store for the last month
    
    
    
    **Expected Response**: 
    
    - Tool: execute_sales_query
    
    - Parameters: query_type="top_products", store_id="seattle", start_date="2025-08-29", end_date="2025-09-29", limit=10
    
    - Result: Formatted table with product names, quantities sold, revenue, and performance metrics
    
    
    
    **User**: What was our daily revenue trend last week?
    
    
    
    **Expected Response**:
    
    - Tool: execute_sales_query  
    
    - Parameters: query_type="daily_sales", store_id="seattle", start_date="2025-09-22", end_date="2025-09-29"
    
    - Result: Chart-ready data with daily revenue figures and growth percentages
    
    
    
    ## Product Search Queries
    
    
    
    **User**: Find comfortable running shoes for outdoor activities
    
    
    
    **Expected Response**:
    
    - Tool: semantic_search_products
    
    - Parameters: query="comfortable running shoes outdoor activities", store_id="seattle", similarity_threshold=0.7
    
    - Result: Ranked list of relevant products with similarity scores and detailed information
    
    
    
    **User**: Search for laptops under $1500 with good reviews
    
    
    
    **Expected Response**:
    
    - Tool: hybrid_product_search
    
    - Parameters: query="laptops under $1500 good reviews", store_id="seattle", semantic_weight=0.6, keyword_weight=0.4
    
    - Result: Combined keyword and semantic search results with price and rating filters
    
    
    
    ## Business Intelligence Queries
    
    
    
    **User**: Generate a comprehensive business summary for September
    
    
    
    **Expected Response**:
    
    - Tool: generate_business_insights
    
    - Parameters: analysis_type="summary", store_id="seattle", days=30
    
    - Result: KPI dashboard with revenue, customer metrics, top categories, and growth trends
    
    

    ์ฑ„ํŒ… ์‘๋‹ต ํ˜•์‹ํ™”

    
    # mcp_server/chat/response_formatter.py
    
    """
    
    Format MCP tool responses for optimal VS Code Chat display.
    
    """
    
    from typing import Dict, Any, List
    
    import json
    
    from datetime import datetime
    
    
    
    class ChatResponseFormatter:
    
        """Format tool responses for VS Code Chat consumption."""
    
        
    
        @staticmethod
    
        def format_sales_data(data: List[Dict[str, Any]], query_type: str) -> str:
    
            """Format sales data for chat display."""
    
            
    
            if not data:
    
                return "No sales data found for the specified criteria."
    
            
    
            if query_type == "daily_sales":
    
                return ChatResponseFormatter._format_daily_sales(data)
    
            elif query_type == "top_products":
    
                return ChatResponseFormatter._format_top_products(data)
    
            elif query_type == "customer_analysis":
    
                return ChatResponseFormatter._format_customer_analysis(data)
    
            else:
    
                return ChatResponseFormatter._format_generic_table(data)
    
        
    
        @staticmethod
    
        def _format_daily_sales(data: List[Dict[str, Any]]) -> str:
    
            """Format daily sales data."""
    
            
    
            response = "## Daily Sales Summary\n\n"
    
            response += "| Date | Revenue | Transactions | Avg Order Value | Customers |\n"
    
            response += "|------|---------|-------------|----------------|----------|\n"
    
            
    
            total_revenue = 0
    
            total_transactions = 0
    
            
    
            for day in data:
    
                revenue = float(day.get('total_revenue', 0))
    
                transactions = int(day.get('transaction_count', 0))
    
                avg_value = float(day.get('avg_transaction_value', 0))
    
                customers = int(day.get('unique_customers', 0))
    
                
    
                total_revenue += revenue
    
                total_transactions += transactions
    
                
    
                response += f"| {day.get('sales_date', 'N/A')} | "
    
                response += f"${revenue:,.2f} | "
    
                response += f"{transactions:,} | "
    
                response += f"${avg_value:.2f} | "
    
                response += f"{customers:,} |\n"
    
            
    
            response += f"\n**Totals**: ${total_revenue:,.2f} revenue, {total_transactions:,} transactions"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def _format_top_products(data: List[Dict[str, Any]]) -> str:
    
            """Format top products data."""
    
            
    
            response = "## Top Selling Products\n\n"
    
            response += "| Rank | Product | Brand | Revenue | Qty Sold | Avg Price |\n"
    
            response += "|------|---------|-------|---------|----------|----------|\n"
    
            
    
            for i, product in enumerate(data, 1):
    
                response += f"| {i} | "
    
                response += f"{product.get('product_name', 'N/A')} | "
    
                response += f"{product.get('brand', 'N/A')} | "
    
                response += f"${float(product.get('total_revenue', 0)):,.2f} | "
    
                response += f"{int(product.get('total_quantity_sold', 0)):,} | "
    
                response += f"${float(product.get('avg_price', 0)):.2f} |\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_search_results(data: List[Dict[str, Any]], search_type: str) -> str:
    
            """Format product search results."""
    
            
    
            if not data:
    
                return "No products found matching your search criteria."
    
            
    
            response = f"## Product Search Results ({search_type})\n\n"
    
            
    
            for i, product in enumerate(data, 1):
    
                response += f"### {i}. {product.get('product_name', 'Unknown Product')}\n"
    
                response += f"**Brand**: {product.get('brand', 'N/A')}\n"
    
                response += f"**Price**: ${float(product.get('price', 0)):.2f}\n"
    
                response += f"**Stock**: {int(product.get('current_stock', 0))} units\n"
    
                
    
                if 'similarity_score' in product:
    
                    score = float(product['similarity_score'])
    
                    response += f"**Relevance**: {score:.1%}\n"
    
                
    
                if 'rating_average' in product and product['rating_average']:
    
                    rating = float(product['rating_average'])
    
                    count = int(product.get('rating_count', 0))
    
                    response += f"**Rating**: {rating:.1f}/5.0 ({count:,} reviews)\n"
    
                
    
                if product.get('product_description'):
    
                    desc = product['product_description']
    
                    if len(desc) > 150:
    
                        desc = desc[:150] + "..."
    
                    response += f"**Description**: {desc}\n"
    
                
    
                response += "\n---\n\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_business_insights(data: Dict[str, Any]) -> str:
    
            """Format business intelligence data."""
    
            
    
            response = "## Business Intelligence Summary\n\n"
    
            
    
            # Key metrics
    
            response += "### Key Performance Indicators\n\n"
    
            response += f"- **Total Revenue**: ${float(data.get('total_revenue', 0)):,.2f}\n"
    
            response += f"- **Total Transactions**: {int(data.get('total_transactions', 0)):,}\n"
    
            response += f"- **Unique Customers**: {int(data.get('unique_customers', 0)):,}\n"
    
            response += f"- **Average Order Value**: ${float(data.get('avg_transaction_value', 0)):.2f}\n"
    
            response += f"- **Products Sold**: {int(data.get('products_sold', 0)):,} items\n\n"
    
            
    
            # Performance indicators
    
            if 'insights' in data and 'performance_indicators' in data['insights']:
    
                pi = data['insights']['performance_indicators']
    
                response += "### Performance Indicators\n\n"
    
                response += f"- **Transactions per Day**: {float(pi.get('transactions_per_day', 0)):.1f}\n"
    
                response += f"- **Revenue per Customer**: ${float(pi.get('revenue_per_customer', 0)):,.2f}\n"
    
                response += f"- **Items per Transaction**: {float(pi.get('items_per_transaction', 0)):.1f}\n\n"
    
            
    
            # Top category
    
            if data.get('top_category'):
    
                response += f"### Top Performing Category\n\n"
    
                response += f"**{data['top_category']}** - ${float(data.get('top_category_revenue', 0)):,.2f} revenue\n\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_error_response(error: str, tool_name: str) -> str:
    
            """Format error responses for chat."""
    
            
    
            response = f"## โŒ Error in {tool_name}\n\n"
    
            response += f"I encountered an issue while processing your request:\n\n"
    
            response += f"**Error**: {error}\n\n"
    
            response += "Please try:\n"
    
            response += "- Checking your query parameters\n"
    
            response += "- Verifying store access permissions\n"
    
            response += "- Simplifying your request\n"
    
            response += "- Contacting support if the issue persists\n"
    
            
    
            return response
    
    

    ๐Ÿ” ๋””๋ฒ„๊น… ๋ฐ ๋ฌธ์ œ ํ•ด๊ฒฐ

    VS Code ๋””๋ฒ„๊ทธ ์„ค์ •

    
    # mcp_server/debug/vscode_debug.py
    
    """
    
    VS Code specific debugging utilities for MCP server.
    
    """
    
    import logging
    
    import json
    
    from typing import Dict, Any
    
    from datetime import datetime
    
    
    
    class VSCodeDebugLogger:
    
        """Enhanced logging for VS Code debugging."""
    
        
    
        def __init__(self):
    
            self.logger = logging.getLogger("mcp_vscode_debug")
    
            self.setup_vscode_logging()
    
        
    
        def setup_vscode_logging(self):
    
            """Configure logging for VS Code debugging."""
    
            
    
            # Create VS Code specific formatter
    
            formatter = logging.Formatter(
    
                '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s'
    
            )
    
            
    
            # Console handler for VS Code terminal
    
            console_handler = logging.StreamHandler()
    
            console_handler.setFormatter(formatter)
    
            console_handler.setLevel(logging.DEBUG)
    
            
    
            self.logger.addHandler(console_handler)
    
            self.logger.setLevel(logging.DEBUG)
    
        
    
        def log_mcp_request(self, method: str, params: Dict[str, Any]):
    
            """Log MCP requests for debugging."""
    
            
    
            self.logger.info(f"MCP Request: {method}")
    
            self.logger.debug(f"Parameters: {json.dumps(params, indent=2)}")
    
        
    
        def log_tool_execution(self, tool_name: str, result: Dict[str, Any]):
    
            """Log tool execution results."""
    
            
    
            success = result.get('success', False)
    
            level = logging.INFO if success else logging.ERROR
    
            
    
            self.logger.log(level, f"Tool '{tool_name}' - {'Success' if success else 'Failed'}")
    
            
    
            if not success and result.get('error'):
    
                self.logger.error(f"Error: {result['error']}")
    
            
    
            if result.get('data'):
    
                data_summary = self._summarize_data(result['data'])
    
                self.logger.debug(f"Result summary: {data_summary}")
    
        
    
        def _summarize_data(self, data: Any) -> str:
    
            """Create a summary of result data."""
    
            
    
            if isinstance(data, list):
    
                return f"List with {len(data)} items"
    
            elif isinstance(data, dict):
    
                return f"Dict with keys: {list(data.keys())}"
    
            else:
    
                return f"Data type: {type(data).__name__}"
    
    
    
    # Global debug logger
    
    vscode_debug_logger = VSCodeDebugLogger()
    
    

    ์—ฐ๊ฒฐ ๋ฌธ์ œ ํ•ด๊ฒฐ

    
    # scripts/debug_mcp_connection.py
    
    """
    
    Debug script for troubleshooting MCP server connections in VS Code.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import os
    
    import sys
    
    from typing import Dict, Any
    
    
    
    async def test_database_connection() -> Dict[str, Any]:
    
        """Test database connectivity."""
    
        
    
        try:
    
            # Get connection parameters from environment
    
            connection_params = {
    
                'host': os.getenv('POSTGRES_HOST', 'localhost'),
    
                'port': int(os.getenv('POSTGRES_PORT', '5432')),
    
                'database': os.getenv('POSTGRES_DB', 'retail_db'),
    
                'user': os.getenv('POSTGRES_USER', 'mcp_user'),
    
                'password': os.getenv('POSTGRES_PASSWORD', '')
    
            }
    
            
    
            print(f"Testing connection to {connection_params['host']}:{connection_params['port']}")
    
            
    
            # Test connection
    
            conn = await asyncpg.connect(**connection_params)
    
            
    
            # Test basic query
    
            result = await conn.fetchval("SELECT version()")
    
            
    
            # Test schema access
    
            tables = await conn.fetch("""
    
                SELECT table_name FROM information_schema.tables 
    
                WHERE table_schema = 'retail'
    
            """)
    
            
    
            await conn.close()
    
            
    
            return {
    
                'success': True,
    
                'database_version': result,
    
                'retail_tables': len(tables),
    
                'table_names': [table['table_name'] for table in tables]
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e),
    
                'connection_params': {k: v for k, v in connection_params.items() if k != 'password'}
    
            }
    
    
    
    async def test_azure_openai_connection() -> Dict[str, Any]:
    
        """Test Azure OpenAI connectivity."""
    
        
    
        try:
    
            from azure.identity import DefaultAzureCredential
    
            from azure.ai.projects import AIProjectClient
    
            
    
            project_endpoint = os.getenv('PROJECT_ENDPOINT')
    
            if not project_endpoint:
    
                return {
    
                    'success': False,
    
                    'error': 'PROJECT_ENDPOINT not configured'
    
                }
    
            
    
            print(f"Testing Azure OpenAI connection to {project_endpoint}")
    
            
    
            credential = DefaultAzureCredential()
    
            client = AIProjectClient(
    
                endpoint=project_endpoint,
    
                credential=credential
    
            )
    
            
    
            # Test embedding generation
    
            response = await client.embeddings.create(
    
                model="text-embedding-3-small",
    
                input="test connection"
    
            )
    
            
    
            embedding = response.data[0].embedding
    
            
    
            return {
    
                'success': True,
    
                'project_endpoint': project_endpoint,
    
                'embedding_dimension': len(embedding),
    
                'model': 'text-embedding-3-small'
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e),
    
                'project_endpoint': os.getenv('PROJECT_ENDPOINT', 'Not configured')
    
            }
    
    
    
    async def test_mcp_tools() -> Dict[str, Any]:
    
        """Test MCP tool availability."""
    
        
    
        try:
    
            # Import MCP server components
    
            sys.path.append(os.path.dirname(os.path.dirname(__file__)))
    
            
    
            from mcp_server.server import MCPServer
    
            from mcp_server.database import DatabaseProvider
    
            from mcp_server.config import Config
    
            
    
            # Create test configuration
    
            config = Config()
    
            db_provider = DatabaseProvider(config.database.connection_string)
    
            
    
            # Initialize server
    
            server = MCPServer(config, db_provider)
    
            await server.initialize()
    
            
    
            # Get available tools
    
            tools = server.get_available_tools()
    
            
    
            # Test a simple tool
    
            test_result = await server.execute_tool(
    
                'get_current_utc_date',
    
                {'format': 'iso'}
    
            )
    
            
    
            await server.cleanup()
    
            
    
            return {
    
                'success': True,
    
                'available_tools': [tool.name for tool in tools],
    
                'tool_count': len(tools),
    
                'test_tool_result': test_result.get('success', False)
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e)
    
            }
    
    
    
    async def main():
    
        """Run comprehensive connection tests."""
    
        
    
        print("๐Ÿ” MCP Server Connection Diagnostics")
    
        print("=" * 50)
    
        
    
        # Test database connection
    
        print("\n๐Ÿ“Š Testing Database Connection...")
    
        db_result = await test_database_connection()
    
        
    
        if db_result['success']:
    
            print("โœ… Database connection successful")
    
            print(f"   Database version: {db_result['database_version']}")
    
            print(f"   Retail tables found: {db_result['retail_tables']}")
    
            print(f"   Table names: {', '.join(db_result['table_names'])}")
    
        else:
    
            print("โŒ Database connection failed")
    
            print(f"   Error: {db_result['error']}")
    
        
    
        # Test Azure OpenAI connection
    
        print("\n๐Ÿค– Testing Azure OpenAI Connection...")
    
        azure_result = await test_azure_openai_connection()
    
        
    
        if azure_result['success']:
    
            print("โœ… Azure OpenAI connection successful")
    
            print(f"   Endpoint: {azure_result['project_endpoint']}")
    
            print(f"   Embedding dimension: {azure_result['embedding_dimension']}")
    
        else:
    
            print("โŒ Azure OpenAI connection failed")
    
            print(f"   Error: {azure_result['error']}")
    
        
    
        # Test MCP tools
    
        print("\n๐Ÿ› ๏ธ  Testing MCP Tools...")
    
        tools_result = await test_mcp_tools()
    
        
    
        if tools_result['success']:
    
            print("โœ… MCP tools loaded successfully")
    
            print(f"   Available tools: {tools_result['tool_count']}")
    
            print(f"   Tool names: {', '.join(tools_result['available_tools'])}")
    
            print(f"   Test execution: {'โœ…' if tools_result['test_tool_result'] else 'โŒ'}")
    
        else:
    
            print("โŒ MCP tools loading failed")
    
            print(f"   Error: {tools_result['error']}")
    
        
    
        # Overall status
    
        print("\n๐Ÿ“‹ Overall Status")
    
        print("=" * 50)
    
        
    
        all_success = all([
    
            db_result['success'],
    
            azure_result['success'],
    
            tools_result['success']
    
        ])
    
        
    
        if all_success:
    
            print("๐ŸŽ‰ All systems ready! MCP server should work correctly in VS Code.")
    
        else:
    
            print("โš ๏ธ  Some issues detected. Please resolve the errors above.")
    
            print("\n๐Ÿ’ก Troubleshooting tips:")
    
            print("   - Check environment variables in .env file")
    
            print("   - Verify database is running and accessible")
    
            print("   - Confirm Azure credentials are configured")
    
            print("   - Review VS Code MCP server configuration")
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    

    ๐Ÿš€ ๊ณ ๊ธ‰ ์„ค์ •

    ๋‹ค์ค‘ ์„œ๋ฒ„ ์„ค์ •

    
    // .vscode/settings.json - Multiple MCP servers
    
    {
    
        "mcp.servers": {
    
            "retail-seattle": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "DEFAULT_STORE_ID": "seattle"
    
                },
    
                "initializationOptions": {
    
                    "store_id": "seattle",
    
                    "server_name": "Seattle Store"
    
                }
    
            },
    
            "retail-redmond": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "DEFAULT_STORE_ID": "redmond"
    
                },
    
                "initializationOptions": {
    
                    "store_id": "redmond",
    
                    "server_name": "Redmond Store"
    
                }
    
            },
    
            "retail-analytics": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.analytics_main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "analytics_user",
    
                    "POSTGRES_PASSWORD": "${env:ANALYTICS_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}"
    
                },
    
                "initializationOptions": {
    
                    "mode": "analytics",
    
                    "cross_store_access": true
    
                }
    
            }
    
        }
    
    }
    
    

    ์‚ฌ์šฉ์ž ์ •์˜ VS Code ํ™•์žฅ

    
    // src/extension.ts - Custom MCP retail extension
    
    import * as vscode from 'vscode';
    
    
    
    export function activate(context: vscode.ExtensionContext) {
    
        
    
        // Register MCP retail commands
    
        const disposable = vscode.commands.registerCommand(
    
            'mcp-retail.quickQuery', 
    
            async () => {
    
                const quickPick = vscode.window.createQuickPick();
    
                quickPick.items = [
    
                    {
    
                        label: '๐Ÿ“Š Daily Sales',
    
                        description: 'Show daily sales for the last 30 days'
    
                    },
    
                    {
    
                        label: '๐Ÿ† Top Products',
    
                        description: 'Show top selling products this month'
    
                    },
    
                    {
    
                        label: '๐Ÿ‘ฅ Customer Analysis',
    
                        description: 'Analyze customer behavior and trends'
    
                    },
    
                    {
    
                        label: '๐Ÿ” Product Search',
    
                        description: 'Search for products using natural language'
    
                    },
    
                    {
    
                        label: '๐Ÿ“ˆ Business Insights',
    
                        description: 'Generate comprehensive business summary'
    
                    }
    
                ];
    
                
    
                quickPick.onDidChangeSelection(selection => {
    
                    if (selection[0]) {
    
                        executeQuickQuery(selection[0].label);
    
                    }
    
                });
    
                
    
                quickPick.onDidHide(() => quickPick.dispose());
    
                quickPick.show();
    
            }
    
        );
    
        
    
        context.subscriptions.push(disposable);
    
        
    
        // Register store switcher
    
        const storeSwitcher = vscode.commands.registerCommand(
    
            'mcp-retail.switchStore',
    
            async () => {
    
                const stores = ['seattle', 'redmond', 'bellevue', 'online'];
    
                const selected = await vscode.window.showQuickPick(stores, {
    
                    placeHolder: 'Select store for queries'
    
                });
    
                
    
                if (selected) {
    
                    // Update configuration
    
                    const config = vscode.workspace.getConfiguration('mcp');
    
                    await config.update('defaultStore', selected, true);
    
                    
    
                    vscode.window.showInformationMessage(
    
                        `Switched to ${selected.charAt(0).toUpperCase() + selected.slice(1)} store`
    
                    );
    
                }
    
            }
    
        );
    
        
    
        context.subscriptions.push(storeSwitcher);
    
    }
    
    
    
    async function executeQuickQuery(queryType: string) {
    
        // Execute predefined queries in VS Code Chat
    
        const chatCommands = {
    
            '๐Ÿ“Š Daily Sales': '@retail Show me daily sales for the last 30 days',
    
            '๐Ÿ† Top Products': '@retail What are the top 10 selling products this month?',
    
            '๐Ÿ‘ฅ Customer Analysis': '@retail Show me customer analysis for active customers',
    
            '๐Ÿ” Product Search': '@retail Find products matching "laptop computer"',
    
            '๐Ÿ“ˆ Business Insights': '@retail Generate a business summary for this month'
    
        };
    
        
    
        const command = chatCommands[queryType];
    
        if (command) {
    
            await vscode.commands.executeCommand('workbench.action.chat.open');
    
            await vscode.commands.executeCommand('workbench.action.chat.insert', command);
    
        }
    
    }
    
    
    
    export function deactivate() {}
    
    

    ํ™•์žฅ ํŒจํ‚ค์ง€ ๊ตฌ์„ฑ

    
    // package.json for VS Code extension
    
    {
    
        "name": "mcp-retail-assistant",
    
        "displayName": "MCP Retail Assistant",
    
        "description": "AI-powered retail data analysis through MCP",
    
        "version": "1.0.0",
    
        "engines": {
    
            "vscode": "^1.74.0"
    
        },
    
        "categories": [
    
            "Other",
    
            "Data Science",
    
            "Machine Learning"
    
        ],
    
        "activationEvents": [
    
            "onCommand:mcp-retail.quickQuery",
    
            "onCommand:mcp-retail.switchStore"
    
        ],
    
        "main": "./out/extension.js",
    
        "contributes": {
    
            "commands": [
    
                {
    
                    "command": "mcp-retail.quickQuery",
    
                    "title": "Quick Retail Query",
    
                    "category": "MCP Retail"
    
                },
    
                {
    
                    "command": "mcp-retail.switchStore",
    
                    "title": "Switch Store",
    
                    "category": "MCP Retail"
    
                }
    
            ],
    
            "keybindings": [
    
                {
    
                    "command": "mcp-retail.quickQuery",
    
                    "key": "ctrl+shift+r",
    
                    "mac": "cmd+shift+r"
    
                }
    
            ],
    
            "configuration": {
    
                "title": "MCP Retail",
    
                "properties": {
    
                    "mcp-retail.defaultStore": {
    
                        "type": "string",
    
                        "default": "seattle",
    
                        "enum": ["seattle", "redmond", "bellevue", "online"],
    
                        "description": "Default store for retail queries"
    
                    },
    
                    "mcp-retail.enableAnalytics": {
    
                        "type": "boolean",
    
                        "default": true,
    
                        "description": "Enable advanced analytics features"
    
                    }
    
                }
    
            }
    
        },
    
        "scripts": {
    
            "vscode:prepublish": "npm run compile",
    
            "compile": "tsc -p ./",
    
            "watch": "tsc -watch -p ./"
    
        },
    
        "devDependencies": {
    
            "@types/vscode": "^1.74.0",
    
            "@types/node": "16.x",
    
            "typescript": "^4.9.4"
    
        }
    
    }
    
    

    ๐ŸŽฏ ์ฃผ์š” ๋‚ด์šฉ ์š”์•ฝ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… VS Code MCP ์„ค์ •: MCP ํ†ตํ•ฉ์„ ์œ„ํ•œ ์ตœ์ ์˜ ์„ค์ • ์™„๋ฃŒ

    โœ… AI ์ฑ„ํŒ… ํ†ตํ•ฉ: VS Code์—์„œ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”

    โœ… ๋””๋ฒ„๊น… ๋„๊ตฌ: ํฌ๊ด„์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ์—ฐ๊ฒฐ ์ง„๋‹จ

    โœ… ๋‹ค์ค‘ ์„œ๋ฒ„ ์„ค์ •: ์—ฌ๋Ÿฌ MCP ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค ๊ตฌ์„ฑ

    โœ… ์‚ฌ์šฉ์ž ์ •์˜ ํ™•์žฅ: ์†Œ๋งค์—…์— ํŠนํ™”๋œ VS Code ๊ฒฝํ—˜ ๊ฐ•ํ™”

    โœ… ํ”„๋กœ๋•์…˜ ์ค€๋น„: ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์ˆ˜์ค€์˜ VS Code ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต 10: ๋ฐฐํฌ ์ „๋žต์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • MCP ์„œ๋ฒ„๋ฅผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐฐํฌ
  • ํ™•์žฅ์„ฑ์„ ์œ„ํ•œ ํด๋ผ์šฐ๋“œ ์ธํ”„๋ผ ๊ตฌ์„ฑ
  • ์ž๋™ ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„
  • ํ”„๋กœ๋•์…˜ MCP ์„œ๋ฒ„ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    VS Code ๊ฐœ๋ฐœ

  • VS Code Extension API - ๊ณต์‹ ํ™•์žฅ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ
  • VS Code MCP Documentation - MCP ํ†ตํ•ฉ ๋ฌธ์„œ
  • TypeScript for VS Code - VS Code์—์„œ TypeScript ๊ฐœ๋ฐœ
  • MCP ํ”„๋กœํ† ์ฝœ

  • Model Context Protocol Specification - ๊ณต์‹ MCP ์‚ฌ์–‘
  • MCP Best Practices - ๊ตฌํ˜„ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • FastMCP Framework - Python MCP ๊ตฌํ˜„
  • ๊ฐœ๋ฐœ ๋„๊ตฌ

  • Python in VS Code - VS Code์—์„œ Python ๊ฐœ๋ฐœ ์„ค์ •
  • Debugging in VS Code - ๊ณ ๊ธ‰ ๋””๋ฒ„๊น… ๊ธฐ์ˆ 
  • VS Code Tasks - ์ž‘์—… ์ž๋™ํ™” ๋ฐ ๊ตฌ์„ฑ
  • ---

    ์ด์ „: ์‹ค์Šต 08: ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…

    ๋‹ค์Œ: ์‹ค์Šต 10: ๋ฐฐํฌ ์ „๋žต

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    VS Code MCP ํ†ตํ•ฉ๊ณผ AI ์ฑ„ํŒ… ์‚ฌ์šฉ๋ฒ• ์„ค์ • ํ†ตํ•ฉํ•˜๊ธฐ

    VS Code ํ†ตํ•ฉ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ MCP ์„œ๋ฒ„๋ฅผ VS Code์™€ ํ†ตํ•ฉํ•˜์—ฌ AI ์ฑ„ํŒ…์„ ํ†ตํ•œ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ํ™œ์„ฑํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ข…ํ•ฉ์ ์ธ ๊ฐ€์ด๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. VS Code๋ฅผ MCP ์‚ฌ์šฉ์— ์ตœ์ ํ™”ํ•˜๋„๋ก ์„ค์ •ํ•˜๊ณ , ์„œ๋ฒ„ ์—ฐ๊ฒฐ์„ ๋””๋ฒ„๊น…ํ•˜๋ฉฐ, AI ์ง€์› ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํ˜ธ์ž‘์šฉ์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    VS Code์˜ MCP ํ†ตํ•ฉ์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ž์—ฐ์–ด๋ฅผ ํ†ตํ•ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ API๋ฅผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ฐฉ์‹์„ ํ˜์‹ ์ ์œผ๋กœ ๋ณ€ํ™”์‹œํ‚ต๋‹ˆ๋‹ค. ์†Œ๋งค MCP ์„œ๋ฒ„๋ฅผ VS Code Chat์— ์—ฐ๊ฒฐํ•˜๋ฉด, ๋Œ€ํ™”ํ˜• AI๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ํŒ๋งค ๋ฐ์ดํ„ฐ, ์ œํ’ˆ ์นดํƒˆ๋กœ๊ทธ, ๋น„์ฆˆ๋‹ˆ์Šค ๋ถ„์„์„ ์ง€๋Šฅ์ ์œผ๋กœ ์ฟผ๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ด ํ†ตํ•ฉ์„ ํ†ตํ•ด ๊ฐœ๋ฐœ์ž๋Š” "์ด๋ฒˆ ๋‹ฌ์— ๊ฐ€์žฅ ๋งŽ์ด ํŒ”๋ฆฐ ์ œํ’ˆ์„ ๋ณด์—ฌ์ค˜" ๋˜๋Š” "90์ผ ๋™์•ˆ ๊ตฌ๋งคํ•˜์ง€ ์•Š์€ ๊ณ ๊ฐ์„ ์ฐพ์•„์ค˜"์™€ ๊ฐ™์€ ์งˆ๋ฌธ์„ ํ•˜๊ณ , SQL ์ฟผ๋ฆฌ๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š๊ณ ๋„ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ ์‘๋‹ต์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • VS Code MCP ์„ค์ • ๊ตฌ์„ฑ: ์†Œ๋งค ์„œ๋ฒ„์— ๋งž๊ฒŒ VS Code ์„ค์ •
  • MCP ์„œ๋ฒ„ ํ†ตํ•ฉ: VS Code AI Chat ๊ธฐ๋Šฅ๊ณผ MCP ์„œ๋ฒ„ ์—ฐ๊ฒฐ
  • ์„œ๋ฒ„ ์—ฐ๊ฒฐ ๋””๋ฒ„๊น…: ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ๋””๋ฒ„๊น…
  • ์ž์—ฐ์–ด ์ฟผ๋ฆฌ ์ตœ์ ํ™”: ๋” ๋‚˜์€ ๊ฒฐ๊ณผ๋ฅผ ์œ„ํ•œ ์ฟผ๋ฆฌ ํŒจํ„ด ์ตœ์ ํ™”
  • VS Code ์ž‘์—… ๊ณต๊ฐ„ ๋งž์ถคํ™”: MCP ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ์ž‘์—… ๊ณต๊ฐ„ ์„ค์ •
  • ๋‹ค์ค‘ ์„œ๋ฒ„ ๊ตฌ์„ฑ ๋ฐฐํฌ: ๋ณต์žกํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ์œ„ํ•œ ๋‹ค์ค‘ ์„œ๋ฒ„ ์„ค์ •
  • ๐Ÿ”ง VS Code MCP ์„ค์ •

    ์ดˆ๊ธฐ ์„ค์ • ๋ฐ ์„ค์น˜

    
    // .vscode/settings.json
    
    {
    
        "mcp.servers": {
    
            "retail-mcp-server": {
    
                "command": "python",
    
                "args": [
    
                    "-m", "mcp_server.main"
    
                ],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_PORT": "5432",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "AZURE_CLIENT_ID": "${env:AZURE_CLIENT_ID}",
    
                    "AZURE_CLIENT_SECRET": "${env:AZURE_CLIENT_SECRET}",
    
                    "AZURE_TENANT_ID": "${env:AZURE_TENANT_ID}",
    
                    "LOG_LEVEL": "INFO",
    
                    "MCP_SERVER_DEBUG": "false"
    
                },
    
                "cwd": "${workspaceFolder}",
    
                "initializationOptions": {
    
                    "store_id": "seattle",
    
                    "enable_semantic_search": true,
    
                    "enable_analytics": true,
    
                    "cache_embeddings": true
    
                }
    
            }
    
        },
    
        "mcp.serverTimeout": 30000,
    
        "mcp.enableLogging": true,
    
        "mcp.logLevel": "info"
    
    }
    
    

    ํ™˜๊ฒฝ ๊ตฌ์„ฑ

    
    # .env file for development
    
    POSTGRES_HOST=localhost
    
    POSTGRES_PORT=5432
    
    POSTGRES_DB=retail_db
    
    POSTGRES_USER=mcp_user
    
    POSTGRES_PASSWORD=your_secure_password
    
    
    
    # Azure Configuration
    
    PROJECT_ENDPOINT=https://your-project.openai.azure.com
    
    AZURE_CLIENT_ID=your-client-id
    
    AZURE_CLIENT_SECRET=your-client-secret
    
    AZURE_TENANT_ID=your-tenant-id
    
    
    
    # Optional: Azure Key Vault
    
    AZURE_KEY_VAULT_URL=https://your-keyvault.vault.azure.net/
    
    
    
    # Server Configuration
    
    MCP_SERVER_PORT=8000
    
    MCP_SERVER_HOST=127.0.0.1
    
    LOG_LEVEL=INFO
    
    

    ์ž‘์—… ๊ณต๊ฐ„ ๊ตฌ์„ฑ

    
    // .vscode/launch.json
    
    {
    
        "version": "0.2.0",
    
        "configurations": [
    
            {
    
                "name": "Debug MCP Server",
    
                "type": "python",
    
                "request": "launch",
    
                "module": "mcp_server.main",
    
                "console": "integratedTerminal",
    
                "envFile": "${workspaceFolder}/.env",
    
                "env": {
    
                    "MCP_SERVER_DEBUG": "true",
    
                    "LOG_LEVEL": "DEBUG"
    
                },
    
                "args": [],
    
                "justMyCode": false,
    
                "stopOnEntry": false
    
            },
    
            {
    
                "name": "Test MCP Server",
    
                "type": "python",
    
                "request": "launch",
    
                "module": "pytest",
    
                "console": "integratedTerminal",
    
                "envFile": "${workspaceFolder}/.env.test",
    
                "args": [
    
                    "tests/",
    
                    "-v",
    
                    "--tb=short"
    
                ]
    
            }
    
        ]
    
    }
    
    

    ์ž‘์—…(Task) ๊ตฌ์„ฑ

    
    // .vscode/tasks.json
    
    {
    
        "version": "2.0.0",
    
        "tasks": [
    
            {
    
                "label": "Start MCP Server",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "-m", "mcp_server.main"
    
                ],
    
                "group": "build",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "new"
    
                },
    
                "options": {
    
                    "env": {
    
                        "PYTHONPATH": "${workspaceFolder}"
    
                    }
    
                },
    
                "isBackground": true,
    
                "problemMatcher": {
    
                    "pattern": {
    
                        "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
    
                        "file": 1,
    
                        "line": 2,
    
                        "column": 3,
    
                        "severity": 4,
    
                        "message": 5
    
                    },
    
                    "background": {
    
                        "activeOnStart": true,
    
                        "beginsPattern": "^.*Starting MCP server.*$",
    
                        "endsPattern": "^.*MCP server ready.*$"
    
                    }
    
                }
    
            },
    
            {
    
                "label": "Run Tests",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "-m", "pytest",
    
                    "tests/",
    
                    "-v"
    
                ],
    
                "group": "test",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "shared"
    
                }
    
            },
    
            {
    
                "label": "Generate Sample Data",
    
                "type": "shell",
    
                "command": "python",
    
                "args": [
    
                    "scripts/generate_sample_data.py"
    
                ],
    
                "group": "build",
    
                "presentation": {
    
                    "echo": true,
    
                    "reveal": "always",
    
                    "focus": false,
    
                    "panel": "shared"
    
                }
    
            },
    
            {
    
                "label": "Create Database Schema",
    
                "type": "shell",
    
                "command": "psql",
    
                "args": [
    
                    "-h", "${env:POSTGRES_HOST}",
    
                    "-p", "${env:POSTGRES_PORT}",
    
                    "-U", "${env:POSTGRES_USER}",
    
                    "-d", "${env:POSTGRES_DB}",
    
                    "-f", "scripts/create_schema.sql"
    
                ],
    
                "group": "build"
    
            }
    
        ]
    
    }
    
    

    ๐Ÿ’ฌ AI ์ฑ„ํŒ… ํ†ตํ•ฉ

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ ํŒจํ„ด

    
    // Example query patterns for VS Code Chat
    
    interface QueryPattern {
    
        intent: string;
    
        examples: string[];
    
        expectedTools: string[];
    
    }
    
    
    
    const retailQueryPatterns: QueryPattern[] = [
    
        {
    
            intent: "sales_analysis",
    
            examples: [
    
                "Show me daily sales for the last 30 days",
    
                "What are our top selling products this month?",
    
                "Which customers have spent the most this quarter?",
    
                "Compare sales performance between stores"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "product_search",
    
            examples: [
    
                "Find running shoes for women",
    
                "Show me electronics under $500",
    
                "What laptops do we have in stock?",
    
                "Search for wireless headphones"
    
            ],
    
            expectedTools: ["semantic_search_products", "hybrid_product_search"]
    
        },
    
        {
    
            intent: "inventory_management",
    
            examples: [
    
                "Which products are low on stock?",
    
                "Show me products that need reordering",
    
                "What's our current inventory value?",
    
                "Find products with zero stock"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "customer_analysis",
    
            examples: [
    
                "Show me customers who haven't purchased in 90 days",
    
                "What's the average customer lifetime value?",
    
                "Which customers are in the gold tier?",
    
                "Find customers with returns"
    
            ],
    
            expectedTools: ["execute_sales_query"]
    
        },
    
        {
    
            intent: "business_intelligence",
    
            examples: [
    
                "Generate a business summary for this month",
    
                "Show me seasonal trends",
    
                "What are our best performing categories?",
    
                "Create a sales forecast"
    
            ],
    
            expectedTools: ["generate_business_insights"]
    
        },
    
        {
    
            intent: "recommendations",
    
            examples: [
    
                "Recommend products similar to product X",
    
                "What should we recommend to customer Y?",
    
                "Show me trending products",
    
                "Find cross-sell opportunities"
    
            ],
    
            expectedTools: ["get_product_recommendations"]
    
        }
    
    ];
    
    

    ์ฑ„ํŒ… ํ†ตํ•ฉ ์˜ˆ์ œ

    
    <!-- Examples of VS Code Chat interactions -->
    
    
    
    ## Sales Analysis Queries
    
    
    
    **User**: Show me the top 10 selling products in the Seattle store for the last month
    
    
    
    **Expected Response**: 
    
    - Tool: execute_sales_query
    
    - Parameters: query_type="top_products", store_id="seattle", start_date="2025-08-29", end_date="2025-09-29", limit=10
    
    - Result: Formatted table with product names, quantities sold, revenue, and performance metrics
    
    
    
    **User**: What was our daily revenue trend last week?
    
    
    
    **Expected Response**:
    
    - Tool: execute_sales_query  
    
    - Parameters: query_type="daily_sales", store_id="seattle", start_date="2025-09-22", end_date="2025-09-29"
    
    - Result: Chart-ready data with daily revenue figures and growth percentages
    
    
    
    ## Product Search Queries
    
    
    
    **User**: Find comfortable running shoes for outdoor activities
    
    
    
    **Expected Response**:
    
    - Tool: semantic_search_products
    
    - Parameters: query="comfortable running shoes outdoor activities", store_id="seattle", similarity_threshold=0.7
    
    - Result: Ranked list of relevant products with similarity scores and detailed information
    
    
    
    **User**: Search for laptops under $1500 with good reviews
    
    
    
    **Expected Response**:
    
    - Tool: hybrid_product_search
    
    - Parameters: query="laptops under $1500 good reviews", store_id="seattle", semantic_weight=0.6, keyword_weight=0.4
    
    - Result: Combined keyword and semantic search results with price and rating filters
    
    
    
    ## Business Intelligence Queries
    
    
    
    **User**: Generate a comprehensive business summary for September
    
    
    
    **Expected Response**:
    
    - Tool: generate_business_insights
    
    - Parameters: analysis_type="summary", store_id="seattle", days=30
    
    - Result: KPI dashboard with revenue, customer metrics, top categories, and growth trends
    
    

    ์ฑ„ํŒ… ์‘๋‹ต ํ˜•์‹ํ™”

    
    # mcp_server/chat/response_formatter.py
    
    """
    
    Format MCP tool responses for optimal VS Code Chat display.
    
    """
    
    from typing import Dict, Any, List
    
    import json
    
    from datetime import datetime
    
    
    
    class ChatResponseFormatter:
    
        """Format tool responses for VS Code Chat consumption."""
    
        
    
        @staticmethod
    
        def format_sales_data(data: List[Dict[str, Any]], query_type: str) -> str:
    
            """Format sales data for chat display."""
    
            
    
            if not data:
    
                return "No sales data found for the specified criteria."
    
            
    
            if query_type == "daily_sales":
    
                return ChatResponseFormatter._format_daily_sales(data)
    
            elif query_type == "top_products":
    
                return ChatResponseFormatter._format_top_products(data)
    
            elif query_type == "customer_analysis":
    
                return ChatResponseFormatter._format_customer_analysis(data)
    
            else:
    
                return ChatResponseFormatter._format_generic_table(data)
    
        
    
        @staticmethod
    
        def _format_daily_sales(data: List[Dict[str, Any]]) -> str:
    
            """Format daily sales data."""
    
            
    
            response = "## Daily Sales Summary\n\n"
    
            response += "| Date | Revenue | Transactions | Avg Order Value | Customers |\n"
    
            response += "|------|---------|-------------|----------------|----------|\n"
    
            
    
            total_revenue = 0
    
            total_transactions = 0
    
            
    
            for day in data:
    
                revenue = float(day.get('total_revenue', 0))
    
                transactions = int(day.get('transaction_count', 0))
    
                avg_value = float(day.get('avg_transaction_value', 0))
    
                customers = int(day.get('unique_customers', 0))
    
                
    
                total_revenue += revenue
    
                total_transactions += transactions
    
                
    
                response += f"| {day.get('sales_date', 'N/A')} | "
    
                response += f"${revenue:,.2f} | "
    
                response += f"{transactions:,} | "
    
                response += f"${avg_value:.2f} | "
    
                response += f"{customers:,} |\n"
    
            
    
            response += f"\n**Totals**: ${total_revenue:,.2f} revenue, {total_transactions:,} transactions"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def _format_top_products(data: List[Dict[str, Any]]) -> str:
    
            """Format top products data."""
    
            
    
            response = "## Top Selling Products\n\n"
    
            response += "| Rank | Product | Brand | Revenue | Qty Sold | Avg Price |\n"
    
            response += "|------|---------|-------|---------|----------|----------|\n"
    
            
    
            for i, product in enumerate(data, 1):
    
                response += f"| {i} | "
    
                response += f"{product.get('product_name', 'N/A')} | "
    
                response += f"{product.get('brand', 'N/A')} | "
    
                response += f"${float(product.get('total_revenue', 0)):,.2f} | "
    
                response += f"{int(product.get('total_quantity_sold', 0)):,} | "
    
                response += f"${float(product.get('avg_price', 0)):.2f} |\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_search_results(data: List[Dict[str, Any]], search_type: str) -> str:
    
            """Format product search results."""
    
            
    
            if not data:
    
                return "No products found matching your search criteria."
    
            
    
            response = f"## Product Search Results ({search_type})\n\n"
    
            
    
            for i, product in enumerate(data, 1):
    
                response += f"### {i}. {product.get('product_name', 'Unknown Product')}\n"
    
                response += f"**Brand**: {product.get('brand', 'N/A')}\n"
    
                response += f"**Price**: ${float(product.get('price', 0)):.2f}\n"
    
                response += f"**Stock**: {int(product.get('current_stock', 0))} units\n"
    
                
    
                if 'similarity_score' in product:
    
                    score = float(product['similarity_score'])
    
                    response += f"**Relevance**: {score:.1%}\n"
    
                
    
                if 'rating_average' in product and product['rating_average']:
    
                    rating = float(product['rating_average'])
    
                    count = int(product.get('rating_count', 0))
    
                    response += f"**Rating**: {rating:.1f}/5.0 ({count:,} reviews)\n"
    
                
    
                if product.get('product_description'):
    
                    desc = product['product_description']
    
                    if len(desc) > 150:
    
                        desc = desc[:150] + "..."
    
                    response += f"**Description**: {desc}\n"
    
                
    
                response += "\n---\n\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_business_insights(data: Dict[str, Any]) -> str:
    
            """Format business intelligence data."""
    
            
    
            response = "## Business Intelligence Summary\n\n"
    
            
    
            # Key metrics
    
            response += "### Key Performance Indicators\n\n"
    
            response += f"- **Total Revenue**: ${float(data.get('total_revenue', 0)):,.2f}\n"
    
            response += f"- **Total Transactions**: {int(data.get('total_transactions', 0)):,}\n"
    
            response += f"- **Unique Customers**: {int(data.get('unique_customers', 0)):,}\n"
    
            response += f"- **Average Order Value**: ${float(data.get('avg_transaction_value', 0)):.2f}\n"
    
            response += f"- **Products Sold**: {int(data.get('products_sold', 0)):,} items\n\n"
    
            
    
            # Performance indicators
    
            if 'insights' in data and 'performance_indicators' in data['insights']:
    
                pi = data['insights']['performance_indicators']
    
                response += "### Performance Indicators\n\n"
    
                response += f"- **Transactions per Day**: {float(pi.get('transactions_per_day', 0)):.1f}\n"
    
                response += f"- **Revenue per Customer**: ${float(pi.get('revenue_per_customer', 0)):,.2f}\n"
    
                response += f"- **Items per Transaction**: {float(pi.get('items_per_transaction', 0)):.1f}\n\n"
    
            
    
            # Top category
    
            if data.get('top_category'):
    
                response += f"### Top Performing Category\n\n"
    
                response += f"**{data['top_category']}** - ${float(data.get('top_category_revenue', 0)):,.2f} revenue\n\n"
    
            
    
            return response
    
        
    
        @staticmethod
    
        def format_error_response(error: str, tool_name: str) -> str:
    
            """Format error responses for chat."""
    
            
    
            response = f"## โŒ Error in {tool_name}\n\n"
    
            response += f"I encountered an issue while processing your request:\n\n"
    
            response += f"**Error**: {error}\n\n"
    
            response += "Please try:\n"
    
            response += "- Checking your query parameters\n"
    
            response += "- Verifying store access permissions\n"
    
            response += "- Simplifying your request\n"
    
            response += "- Contacting support if the issue persists\n"
    
            
    
            return response
    
    

    ๐Ÿ” ๋””๋ฒ„๊น… ๋ฐ ๋ฌธ์ œ ํ•ด๊ฒฐ

    VS Code ๋””๋ฒ„๊ทธ ์„ค์ •

    
    # mcp_server/debug/vscode_debug.py
    
    """
    
    VS Code specific debugging utilities for MCP server.
    
    """
    
    import logging
    
    import json
    
    from typing import Dict, Any
    
    from datetime import datetime
    
    
    
    class VSCodeDebugLogger:
    
        """Enhanced logging for VS Code debugging."""
    
        
    
        def __init__(self):
    
            self.logger = logging.getLogger("mcp_vscode_debug")
    
            self.setup_vscode_logging()
    
        
    
        def setup_vscode_logging(self):
    
            """Configure logging for VS Code debugging."""
    
            
    
            # Create VS Code specific formatter
    
            formatter = logging.Formatter(
    
                '[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s'
    
            )
    
            
    
            # Console handler for VS Code terminal
    
            console_handler = logging.StreamHandler()
    
            console_handler.setFormatter(formatter)
    
            console_handler.setLevel(logging.DEBUG)
    
            
    
            self.logger.addHandler(console_handler)
    
            self.logger.setLevel(logging.DEBUG)
    
        
    
        def log_mcp_request(self, method: str, params: Dict[str, Any]):
    
            """Log MCP requests for debugging."""
    
            
    
            self.logger.info(f"MCP Request: {method}")
    
            self.logger.debug(f"Parameters: {json.dumps(params, indent=2)}")
    
        
    
        def log_tool_execution(self, tool_name: str, result: Dict[str, Any]):
    
            """Log tool execution results."""
    
            
    
            success = result.get('success', False)
    
            level = logging.INFO if success else logging.ERROR
    
            
    
            self.logger.log(level, f"Tool '{tool_name}' - {'Success' if success else 'Failed'}")
    
            
    
            if not success and result.get('error'):
    
                self.logger.error(f"Error: {result['error']}")
    
            
    
            if result.get('data'):
    
                data_summary = self._summarize_data(result['data'])
    
                self.logger.debug(f"Result summary: {data_summary}")
    
        
    
        def _summarize_data(self, data: Any) -> str:
    
            """Create a summary of result data."""
    
            
    
            if isinstance(data, list):
    
                return f"List with {len(data)} items"
    
            elif isinstance(data, dict):
    
                return f"Dict with keys: {list(data.keys())}"
    
            else:
    
                return f"Data type: {type(data).__name__}"
    
    
    
    # Global debug logger
    
    vscode_debug_logger = VSCodeDebugLogger()
    
    

    ์—ฐ๊ฒฐ ๋ฌธ์ œ ํ•ด๊ฒฐ

    
    # scripts/debug_mcp_connection.py
    
    """
    
    Debug script for troubleshooting MCP server connections in VS Code.
    
    """
    
    import asyncio
    
    import asyncpg
    
    import os
    
    import sys
    
    from typing import Dict, Any
    
    
    
    async def test_database_connection() -> Dict[str, Any]:
    
        """Test database connectivity."""
    
        
    
        try:
    
            # Get connection parameters from environment
    
            connection_params = {
    
                'host': os.getenv('POSTGRES_HOST', 'localhost'),
    
                'port': int(os.getenv('POSTGRES_PORT', '5432')),
    
                'database': os.getenv('POSTGRES_DB', 'retail_db'),
    
                'user': os.getenv('POSTGRES_USER', 'mcp_user'),
    
                'password': os.getenv('POSTGRES_PASSWORD', '')
    
            }
    
            
    
            print(f"Testing connection to {connection_params['host']}:{connection_params['port']}")
    
            
    
            # Test connection
    
            conn = await asyncpg.connect(**connection_params)
    
            
    
            # Test basic query
    
            result = await conn.fetchval("SELECT version()")
    
            
    
            # Test schema access
    
            tables = await conn.fetch("""
    
                SELECT table_name FROM information_schema.tables 
    
                WHERE table_schema = 'retail'
    
            """)
    
            
    
            await conn.close()
    
            
    
            return {
    
                'success': True,
    
                'database_version': result,
    
                'retail_tables': len(tables),
    
                'table_names': [table['table_name'] for table in tables]
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e),
    
                'connection_params': {k: v for k, v in connection_params.items() if k != 'password'}
    
            }
    
    
    
    async def test_azure_openai_connection() -> Dict[str, Any]:
    
        """Test Azure OpenAI connectivity."""
    
        
    
        try:
    
            from azure.identity import DefaultAzureCredential
    
            from azure.ai.projects import AIProjectClient
    
            
    
            project_endpoint = os.getenv('PROJECT_ENDPOINT')
    
            if not project_endpoint:
    
                return {
    
                    'success': False,
    
                    'error': 'PROJECT_ENDPOINT not configured'
    
                }
    
            
    
            print(f"Testing Azure OpenAI connection to {project_endpoint}")
    
            
    
            credential = DefaultAzureCredential()
    
            client = AIProjectClient(
    
                endpoint=project_endpoint,
    
                credential=credential
    
            )
    
            
    
            # Test embedding generation
    
            response = await client.embeddings.create(
    
                model="text-embedding-3-small",
    
                input="test connection"
    
            )
    
            
    
            embedding = response.data[0].embedding
    
            
    
            return {
    
                'success': True,
    
                'project_endpoint': project_endpoint,
    
                'embedding_dimension': len(embedding),
    
                'model': 'text-embedding-3-small'
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e),
    
                'project_endpoint': os.getenv('PROJECT_ENDPOINT', 'Not configured')
    
            }
    
    
    
    async def test_mcp_tools() -> Dict[str, Any]:
    
        """Test MCP tool availability."""
    
        
    
        try:
    
            # Import MCP server components
    
            sys.path.append(os.path.dirname(os.path.dirname(__file__)))
    
            
    
            from mcp_server.server import MCPServer
    
            from mcp_server.database import DatabaseProvider
    
            from mcp_server.config import Config
    
            
    
            # Create test configuration
    
            config = Config()
    
            db_provider = DatabaseProvider(config.database.connection_string)
    
            
    
            # Initialize server
    
            server = MCPServer(config, db_provider)
    
            await server.initialize()
    
            
    
            # Get available tools
    
            tools = server.get_available_tools()
    
            
    
            # Test a simple tool
    
            test_result = await server.execute_tool(
    
                'get_current_utc_date',
    
                {'format': 'iso'}
    
            )
    
            
    
            await server.cleanup()
    
            
    
            return {
    
                'success': True,
    
                'available_tools': [tool.name for tool in tools],
    
                'tool_count': len(tools),
    
                'test_tool_result': test_result.get('success', False)
    
            }
    
            
    
        except Exception as e:
    
            return {
    
                'success': False,
    
                'error': str(e)
    
            }
    
    
    
    async def main():
    
        """Run comprehensive connection tests."""
    
        
    
        print("๐Ÿ” MCP Server Connection Diagnostics")
    
        print("=" * 50)
    
        
    
        # Test database connection
    
        print("\n๐Ÿ“Š Testing Database Connection...")
    
        db_result = await test_database_connection()
    
        
    
        if db_result['success']:
    
            print("โœ… Database connection successful")
    
            print(f"   Database version: {db_result['database_version']}")
    
            print(f"   Retail tables found: {db_result['retail_tables']}")
    
            print(f"   Table names: {', '.join(db_result['table_names'])}")
    
        else:
    
            print("โŒ Database connection failed")
    
            print(f"   Error: {db_result['error']}")
    
        
    
        # Test Azure OpenAI connection
    
        print("\n๐Ÿค– Testing Azure OpenAI Connection...")
    
        azure_result = await test_azure_openai_connection()
    
        
    
        if azure_result['success']:
    
            print("โœ… Azure OpenAI connection successful")
    
            print(f"   Endpoint: {azure_result['project_endpoint']}")
    
            print(f"   Embedding dimension: {azure_result['embedding_dimension']}")
    
        else:
    
            print("โŒ Azure OpenAI connection failed")
    
            print(f"   Error: {azure_result['error']}")
    
        
    
        # Test MCP tools
    
        print("\n๐Ÿ› ๏ธ  Testing MCP Tools...")
    
        tools_result = await test_mcp_tools()
    
        
    
        if tools_result['success']:
    
            print("โœ… MCP tools loaded successfully")
    
            print(f"   Available tools: {tools_result['tool_count']}")
    
            print(f"   Tool names: {', '.join(tools_result['available_tools'])}")
    
            print(f"   Test execution: {'โœ…' if tools_result['test_tool_result'] else 'โŒ'}")
    
        else:
    
            print("โŒ MCP tools loading failed")
    
            print(f"   Error: {tools_result['error']}")
    
        
    
        # Overall status
    
        print("\n๐Ÿ“‹ Overall Status")
    
        print("=" * 50)
    
        
    
        all_success = all([
    
            db_result['success'],
    
            azure_result['success'],
    
            tools_result['success']
    
        ])
    
        
    
        if all_success:
    
            print("๐ŸŽ‰ All systems ready! MCP server should work correctly in VS Code.")
    
        else:
    
            print("โš ๏ธ  Some issues detected. Please resolve the errors above.")
    
            print("\n๐Ÿ’ก Troubleshooting tips:")
    
            print("   - Check environment variables in .env file")
    
            print("   - Verify database is running and accessible")
    
            print("   - Confirm Azure credentials are configured")
    
            print("   - Review VS Code MCP server configuration")
    
    
    
    if __name__ == "__main__":
    
        asyncio.run(main())
    
    

    ๐Ÿš€ ๊ณ ๊ธ‰ ์„ค์ •

    ๋‹ค์ค‘ ์„œ๋ฒ„ ์„ค์ •

    
    // .vscode/settings.json - Multiple MCP servers
    
    {
    
        "mcp.servers": {
    
            "retail-seattle": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "DEFAULT_STORE_ID": "seattle"
    
                },
    
                "initializationOptions": {
    
                    "store_id": "seattle",
    
                    "server_name": "Seattle Store"
    
                }
    
            },
    
            "retail-redmond": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "mcp_user",
    
                    "POSTGRES_PASSWORD": "${env:POSTGRES_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}",
    
                    "DEFAULT_STORE_ID": "redmond"
    
                },
    
                "initializationOptions": {
    
                    "store_id": "redmond",
    
                    "server_name": "Redmond Store"
    
                }
    
            },
    
            "retail-analytics": {
    
                "command": "python",
    
                "args": ["-m", "mcp_server.analytics_main"],
    
                "env": {
    
                    "POSTGRES_HOST": "localhost",
    
                    "POSTGRES_DB": "retail_db",
    
                    "POSTGRES_USER": "analytics_user",
    
                    "POSTGRES_PASSWORD": "${env:ANALYTICS_PASSWORD}",
    
                    "PROJECT_ENDPOINT": "${env:PROJECT_ENDPOINT}"
    
                },
    
                "initializationOptions": {
    
                    "mode": "analytics",
    
                    "cross_store_access": true
    
                }
    
            }
    
        }
    
    }
    
    

    ์‚ฌ์šฉ์ž ์ •์˜ VS Code ํ™•์žฅ

    
    // src/extension.ts - Custom MCP retail extension
    
    import * as vscode from 'vscode';
    
    
    
    export function activate(context: vscode.ExtensionContext) {
    
        
    
        // Register MCP retail commands
    
        const disposable = vscode.commands.registerCommand(
    
            'mcp-retail.quickQuery', 
    
            async () => {
    
                const quickPick = vscode.window.createQuickPick();
    
                quickPick.items = [
    
                    {
    
                        label: '๐Ÿ“Š Daily Sales',
    
                        description: 'Show daily sales for the last 30 days'
    
                    },
    
                    {
    
                        label: '๐Ÿ† Top Products',
    
                        description: 'Show top selling products this month'
    
                    },
    
                    {
    
                        label: '๐Ÿ‘ฅ Customer Analysis',
    
                        description: 'Analyze customer behavior and trends'
    
                    },
    
                    {
    
                        label: '๐Ÿ” Product Search',
    
                        description: 'Search for products using natural language'
    
                    },
    
                    {
    
                        label: '๐Ÿ“ˆ Business Insights',
    
                        description: 'Generate comprehensive business summary'
    
                    }
    
                ];
    
                
    
                quickPick.onDidChangeSelection(selection => {
    
                    if (selection[0]) {
    
                        executeQuickQuery(selection[0].label);
    
                    }
    
                });
    
                
    
                quickPick.onDidHide(() => quickPick.dispose());
    
                quickPick.show();
    
            }
    
        );
    
        
    
        context.subscriptions.push(disposable);
    
        
    
        // Register store switcher
    
        const storeSwitcher = vscode.commands.registerCommand(
    
            'mcp-retail.switchStore',
    
            async () => {
    
                const stores = ['seattle', 'redmond', 'bellevue', 'online'];
    
                const selected = await vscode.window.showQuickPick(stores, {
    
                    placeHolder: 'Select store for queries'
    
                });
    
                
    
                if (selected) {
    
                    // Update configuration
    
                    const config = vscode.workspace.getConfiguration('mcp');
    
                    await config.update('defaultStore', selected, true);
    
                    
    
                    vscode.window.showInformationMessage(
    
                        `Switched to ${selected.charAt(0).toUpperCase() + selected.slice(1)} store`
    
                    );
    
                }
    
            }
    
        );
    
        
    
        context.subscriptions.push(storeSwitcher);
    
    }
    
    
    
    async function executeQuickQuery(queryType: string) {
    
        // Execute predefined queries in VS Code Chat
    
        const chatCommands = {
    
            '๐Ÿ“Š Daily Sales': '@retail Show me daily sales for the last 30 days',
    
            '๐Ÿ† Top Products': '@retail What are the top 10 selling products this month?',
    
            '๐Ÿ‘ฅ Customer Analysis': '@retail Show me customer analysis for active customers',
    
            '๐Ÿ” Product Search': '@retail Find products matching "laptop computer"',
    
            '๐Ÿ“ˆ Business Insights': '@retail Generate a business summary for this month'
    
        };
    
        
    
        const command = chatCommands[queryType];
    
        if (command) {
    
            await vscode.commands.executeCommand('workbench.action.chat.open');
    
            await vscode.commands.executeCommand('workbench.action.chat.insert', command);
    
        }
    
    }
    
    
    
    export function deactivate() {}
    
    

    ํ™•์žฅ ํŒจํ‚ค์ง€ ๊ตฌ์„ฑ

    
    // package.json for VS Code extension
    
    {
    
        "name": "mcp-retail-assistant",
    
        "displayName": "MCP Retail Assistant",
    
        "description": "AI-powered retail data analysis through MCP",
    
        "version": "1.0.0",
    
        "engines": {
    
            "vscode": "^1.74.0"
    
        },
    
        "categories": [
    
            "Other",
    
            "Data Science",
    
            "Machine Learning"
    
        ],
    
        "activationEvents": [
    
            "onCommand:mcp-retail.quickQuery",
    
            "onCommand:mcp-retail.switchStore"
    
        ],
    
        "main": "./out/extension.js",
    
        "contributes": {
    
            "commands": [
    
                {
    
                    "command": "mcp-retail.quickQuery",
    
                    "title": "Quick Retail Query",
    
                    "category": "MCP Retail"
    
                },
    
                {
    
                    "command": "mcp-retail.switchStore",
    
                    "title": "Switch Store",
    
                    "category": "MCP Retail"
    
                }
    
            ],
    
            "keybindings": [
    
                {
    
                    "command": "mcp-retail.quickQuery",
    
                    "key": "ctrl+shift+r",
    
                    "mac": "cmd+shift+r"
    
                }
    
            ],
    
            "configuration": {
    
                "title": "MCP Retail",
    
                "properties": {
    
                    "mcp-retail.defaultStore": {
    
                        "type": "string",
    
                        "default": "seattle",
    
                        "enum": ["seattle", "redmond", "bellevue", "online"],
    
                        "description": "Default store for retail queries"
    
                    },
    
                    "mcp-retail.enableAnalytics": {
    
                        "type": "boolean",
    
                        "default": true,
    
                        "description": "Enable advanced analytics features"
    
                    }
    
                }
    
            }
    
        },
    
        "scripts": {
    
            "vscode:prepublish": "npm run compile",
    
            "compile": "tsc -p ./",
    
            "watch": "tsc -watch -p ./"
    
        },
    
        "devDependencies": {
    
            "@types/vscode": "^1.74.0",
    
            "@types/node": "16.x",
    
            "typescript": "^4.9.4"
    
        }
    
    }
    
    

    ๐ŸŽฏ ์ฃผ์š” ๋‚ด์šฉ ์š”์•ฝ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๋‹ฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

    โœ… VS Code MCP ์„ค์ •: MCP ํ†ตํ•ฉ์„ ์œ„ํ•œ ์ตœ์ ์˜ ์„ค์ • ์™„๋ฃŒ

    โœ… AI ์ฑ„ํŒ… ํ†ตํ•ฉ: VS Code์—์„œ ์ž์—ฐ์–ด ์ฟผ๋ฆฌ ๊ธฐ๋Šฅ ํ™œ์„ฑํ™”

    โœ… ๋””๋ฒ„๊น… ๋„๊ตฌ: ํฌ๊ด„์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ ๋ฐ ์—ฐ๊ฒฐ ์ง„๋‹จ

    โœ… ๋‹ค์ค‘ ์„œ๋ฒ„ ์„ค์ •: ์—ฌ๋Ÿฌ MCP ์„œ๋ฒ„ ์ธ์Šคํ„ด์Šค ๊ตฌ์„ฑ

    โœ… ์‚ฌ์šฉ์ž ์ •์˜ ํ™•์žฅ: ์†Œ๋งค์—…์— ํŠนํ™”๋œ VS Code ๊ฒฝํ—˜ ๊ฐ•ํ™”

    โœ… ํ”„๋กœ๋•์…˜ ์ค€๋น„: ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์ˆ˜์ค€์˜ VS Code ๊ฐœ๋ฐœ ํ™˜๊ฒฝ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต 10: ๋ฐฐํฌ ์ „๋žต์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • MCP ์„œ๋ฒ„๋ฅผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐฐํฌ
  • ํ™•์žฅ์„ฑ์„ ์œ„ํ•œ ํด๋ผ์šฐ๋“œ ์ธํ”„๋ผ ๊ตฌ์„ฑ
  • ์ž๋™ ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„
  • ํ”„๋กœ๋•์…˜ MCP ์„œ๋ฒ„ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    VS Code ๊ฐœ๋ฐœ

  • VS Code Extension API - ๊ณต์‹ ํ™•์žฅ ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ
  • VS Code MCP Documentation - MCP ํ†ตํ•ฉ ๋ฌธ์„œ
  • TypeScript for VS Code - VS Code์—์„œ TypeScript ๊ฐœ๋ฐœ
  • MCP ํ”„๋กœํ† ์ฝœ

  • Model Context Protocol Specification - ๊ณต์‹ MCP ์‚ฌ์–‘
  • MCP Best Practices - ๊ตฌํ˜„ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • FastMCP Framework - Python MCP ๊ตฌํ˜„
  • ๊ฐœ๋ฐœ ๋„๊ตฌ

  • Python in VS Code - VS Code์—์„œ Python ๊ฐœ๋ฐœ ์„ค์ •
  • Debugging in VS Code - ๊ณ ๊ธ‰ ๋””๋ฒ„๊น… ๊ธฐ์ˆ 
  • VS Code Tasks - ์ž‘์—… ์ž๋™ํ™” ๋ฐ ๊ตฌ์„ฑ
  • ---

    ์ด์ „: ์‹ค์Šต 08: ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…

    ๋‹ค์Œ: ์‹ค์Šต 10: ๋ฐฐํฌ ์ „๋žต

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์‹ค์Šต 10-12: ํ”„๋กœ๋•์…˜ ๋ฐ ๋ชจ๋ฒ” ์‚ฌ๋ก€ 10 ๋ฐฐํฌ ์ „๋žต

    ๋ฐฐํฌ ์ „๋žต

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ ํ˜„๋Œ€์ ์ธ ์ปจํ…Œ์ด๋„ˆํ™” ๋ฐ ํด๋ผ์šฐ๋“œ ๋„ค์ดํ‹ฐ๋ธŒ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ MCP ๋ฆฌํ…Œ์ผ ์„œ๋ฒ„๋ฅผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐฐํฌํ•˜๋Š” ํฌ๊ด„์ ์ธ ๊ฐ€์ด๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์›Œํฌ๋กœ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ณ  ์•ˆ์ „ํ•˜๋ฉฐ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ€๋Šฅํ•œ MCP ์„œ๋ฒ„๋ฅผ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ์„œ๋ฒ„์˜ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ๋Š” ์ปจํ…Œ์ด๋„ˆํ™”, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜, ๋ณด์•ˆ, ํ™•์žฅ์„ฑ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์‹ ์ค‘ํžˆ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” Azure Container Apps์™€ PostgreSQL Flexible Server๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐฐํฌ, CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„, ๊ฐ€๋ณ€ ์›Œํฌ๋กœ๋“œ๋ฅผ ์œ„ํ•œ ์ž๋™ ํ™•์žฅ ๊ตฌ์„ฑ์— ๋Œ€ํ•ด ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    ๋ฐฐํฌ ์ „๋žต์€ ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ๊ฐ„๋‹จํ•œ ๋‹จ์ผ ์ปจํ…Œ์ด๋„ˆ ๋ฐฐํฌ๋ถ€ํ„ฐ ๋‹ค์ค‘ ์ง€์—ญ, ์ž๋™ ํ™•์žฅ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ์ด๋ฅด๊ธฐ๊นŒ์ง€ ํฌ๊ด„์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ๋ณต์žกํ•œ ๋ฐฐํฌ๊นŒ์ง€ ๋‹ค์–‘ํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ปจํ…Œ์ด๋„ˆํ™”: Docker๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ MCP ์„œ๋ฒ„๋ฅผ ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ ๋นŒ๋“œ๋กœ ์ปจํ…Œ์ด๋„ˆํ™”
  • ๋ฐฐํฌ: ์•ˆ์ „ํ•œ ๋„คํŠธ์›Œํ‚น์„ ํ†ตํ•ด Azure Container Apps์— ๋ฐฐํฌ
  • ๊ตฌ์„ฑ: ๊ณ ๊ฐ€์šฉ์„ฑ์„ ๊ฐ–์ถ˜ ํ”„๋กœ๋•์…˜๊ธ‰ PostgreSQL ๊ตฌ์„ฑ
  • ๊ตฌํ˜„: ์ž๋™ ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ CI/CD ํŒŒ์ดํ”„๋ผ์ธ
  • ํ™•์žฅ: ์ˆ˜์š”์— ๋”ฐ๋ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ž๋™ ํ™•์žฅ
  • ๋ชจ๋‹ˆํ„ฐ๋ง: ํฌ๊ด„์ ์ธ ๊ด€์ธก ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๐Ÿณ Docker ์ปจํ…Œ์ด๋„ˆํ™”

    ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ Dockerfile

    
    # Dockerfile - Production-ready multi-stage build
    
    FROM python:3.11-slim AS builder
    
    
    
    # Set build environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PIP_NO_CACHE_DIR=1 \
    
        PIP_DISABLE_PIP_VERSION_CHECK=1
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        build-essential \
    
        libpq-dev \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install dependencies
    
    COPY requirements.lock.txt /tmp/
    
    RUN pip install --no-cache-dir -r /tmp/requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim AS production
    
    
    
    # Set production environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PATH="/opt/venv/bin:$PATH" \
    
        PYTHONPATH="/app"
    
    
    
    # Install runtime dependencies
    
    RUN apt-get update && apt-get install -y \
    
        libpq5 \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/* \
    
        && groupadd -r mcp \
    
        && useradd -r -g mcp -d /app -s /bin/bash mcp
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    
    
    # Set working directory and copy application
    
    WORKDIR /app
    
    COPY --chown=mcp:mcp . .
    
    
    
    # Create necessary directories with proper permissions
    
    RUN mkdir -p /app/logs /app/data /tmp/mcp \
    
        && chown -R mcp:mcp /app /tmp/mcp \
    
        && chmod -R 755 /app
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    
        CMD python -m mcp_server.health_check || exit 1
    
    
    
    # Switch to non-root user
    
    USER mcp
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Default command
    
    CMD ["python", "-m", "mcp_server.main"]
    
    

    ๊ฐœ๋ฐœ์šฉ Docker Compose

    
    # docker-compose.yml - Development environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        build:
    
          context: .
    
          dockerfile: Dockerfile
    
          target: production
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=postgres
    
          - POSTGRES_PORT=5432
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=mcp_user
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=development
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        volumes:
    
          - ./logs:/app/logs
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      postgres:
    
        image: pgvector/pgvector:pg16
    
        environment:
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=postgres
    
          - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - postgres_data:/var/lib/postgresql/data
    
          - ./docker-init:/docker-entrypoint-initdb.d
    
          - ./data:/backup
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD-SHELL", "pg_isready -U postgres -d retail_db"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      redis:
    
        image: redis:7-alpine
    
        ports:
    
          - "6379:6379"
    
        command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    
        volumes:
    
          - redis_data:/data
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
    
    
    volumes:
    
      postgres_data:
    
        driver: local
    
      redis_data:
    
        driver: local
    
    
    
    networks:
    
      mcp-network:
    
        driver: bridge
    
    

    ํ”„๋กœ๋•์…˜ Docker Compose

    
    # docker-compose.prod.yml - Production environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        image: ${CONTAINER_REGISTRY}/mcp-retail-server:${IMAGE_TAG}
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=${POSTGRES_HOST}
    
          - POSTGRES_PORT=${POSTGRES_PORT}
    
          - POSTGRES_DB=${POSTGRES_DB}
    
          - POSTGRES_USER=${POSTGRES_USER}
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=production
    
          - REDIS_URL=${REDIS_URL}
    
        deploy:
    
          replicas: 3
    
          resources:
    
            limits:
    
              cpus: '2.0'
    
              memory: 2G
    
            reservations:
    
              cpus: '0.5'
    
              memory: 512M
    
          restart_policy:
    
            condition: on-failure
    
            delay: 5s
    
            max_attempts: 3
    
          update_config:
    
            parallelism: 1
    
            delay: 10s
    
            failure_action: rollback
    
        networks:
    
          - mcp-network
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
    networks:
    
      mcp-network:
    
        external: true
    
    

    โ˜๏ธ Azure Container Apps ๋ฐฐํฌ

    Bicep์„ ์‚ฌ์šฉํ•œ ์ฝ”๋“œํ˜• ์ธํ”„๋ผ

    
    // infra/container-apps.bicep - Azure Container Apps deployment
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('Environment name')
    
    param environmentName string
    
    
    
    @description('Container App name')
    
    param containerAppName string
    
    
    
    @description('Container registry details')
    
    param containerRegistry object
    
    
    
    @description('Database connection details')
    
    @secure()
    
    param databaseConnectionString string
    
    
    
    @description('Azure OpenAI configuration')
    
    param azureOpenAI object
    
    
    
    @description('Application Insights workspace ID')
    
    param workspaceId string
    
    
    
    // Container Apps Environment
    
    resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
    
      name: '${environmentName}-env'
    
      location: location
    
      properties: {
    
        appLogsConfiguration: {
    
          destination: 'log-analytics'
    
          logAnalyticsConfiguration: {
    
            customerId: workspaceId
    
          }
    
        }
    
        infrastructureResourceGroup: '${environmentName}-infra-rg'
    
      }
    
    }
    
    
    
    // Container App
    
    resource mcp_retail_server 'Microsoft.App/containerApps@2023-05-01' = {
    
      name: containerAppName
    
      location: location
    
      properties: {
    
        managedEnvironmentId: containerAppsEnvironment.id
    
        configuration: {
    
          activeRevisionsMode: 'Single'
    
          ingress: {
    
            external: false
    
            targetPort: 8000
    
            allowInsecure: false
    
            traffic: [
    
              {
    
                weight: 100
    
                latestRevision: true
    
              }
    
            ]
    
          }
    
          registries: [
    
            {
    
              server: containerRegistry.server
    
              identity: containerRegistry.identity
    
            }
    
          ]
    
          secrets: [
    
            {
    
              name: 'database-connection-string'
    
              value: databaseConnectionString
    
            }
    
            {
    
              name: 'azure-openai-key'
    
              value: azureOpenAI.apiKey
    
            }
    
          ]
    
        }
    
        template: {
    
          containers: [
    
            {
    
              name: 'mcp-retail-server'
    
              image: '${containerRegistry.server}/mcp-retail-server:latest'
    
              resources: {
    
                cpu: json('1.0')
    
                memory: '2Gi'
    
              }
    
              env: [
    
                {
    
                  name: 'POSTGRES_CONNECTION_STRING'
    
                  secretRef: 'database-connection-string'
    
                }
    
                {
    
                  name: 'PROJECT_ENDPOINT'
    
                  value: azureOpenAI.endpoint
    
                }
    
                {
    
                  name: 'AZURE_OPENAI_API_KEY'
    
                  secretRef: 'azure-openai-key'
    
                }
    
                {
    
                  name: 'LOG_LEVEL'
    
                  value: 'INFO'
    
                }
    
                {
    
                  name: 'ENVIRONMENT'
    
                  value: 'production'
    
                }
    
              ]
    
              probes: [
    
                {
    
                  type: 'Liveness'
    
                  httpGet: {
    
                    path: '/health'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 60
    
                  periodSeconds: 30
    
                  timeoutSeconds: 10
    
                  failureThreshold: 3
    
                }
    
                {
    
                  type: 'Readiness'
    
                  httpGet: {
    
                    path: '/ready'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 30
    
                  periodSeconds: 10
    
                  timeoutSeconds: 5
    
                  failureThreshold: 3
    
                }
    
              ]
    
            }
    
          ]
    
          scale: {
    
            minReplicas: 2
    
            maxReplicas: 20
    
            rules: [
    
              {
    
                name: 'http-scaling'
    
                http: {
    
                  metadata: {
    
                    concurrentRequests: '10'
    
                  }
    
                }
    
              }
    
              {
    
                name: 'cpu-scaling'
    
                custom: {
    
                  type: 'cpu'
    
                  metadata: {
    
                    type: 'Utilization'
    
                    value: '70'
    
                  }
    
                }
    
              }
    
            ]
    
          }
    
        }
    
      }
    
    }
    
    
    
    // Output the FQDN
    
    output containerAppFQDN string = mcp_retail_server.properties.configuration.ingress.fqdn
    
    output containerAppId string = mcp_retail_server.id
    
    

    PostgreSQL Flexible Server

    
    // infra/database.bicep - PostgreSQL Flexible Server
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('PostgreSQL server name')
    
    param serverName string
    
    
    
    @description('Database administrator login')
    
    param administratorLogin string
    
    
    
    @description('Database administrator password')
    
    @secure()
    
    param administratorPassword string
    
    
    
    @description('Virtual network subnet ID')
    
    param subnetId string
    
    
    
    @description('Private DNS zone ID')
    
    param privateDnsZoneId string
    
    
    
    // PostgreSQL Flexible Server
    
    resource postgresqlServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
    
      name: serverName
    
      location: location
    
      sku: {
    
        name: 'Standard_D4s_v3'
    
        tier: 'GeneralPurpose'
    
      }
    
      properties: {
    
        administratorLogin: administratorLogin
    
        administratorLoginPassword: administratorPassword
    
        version: '16'
    
        storage: {
    
          storageSizeGB: 128
    
          autoGrow: 'Enabled'
    
          type: 'PremiumSSD'
    
        }
    
        backup: {
    
          backupRetentionDays: 35
    
          geoRedundantBackup: 'Enabled'
    
        }
    
        highAvailability: {
    
          mode: 'ZoneRedundant'
    
        }
    
        network: {
    
          delegatedSubnetResourceId: subnetId
    
          privateDnsZoneArmResourceId: privateDnsZoneId
    
        }
    
        maintenanceWindow: {
    
          dayOfWeek: 0
    
          startHour: 2
    
          startMinute: 0
    
        }
    
      }
    
    }
    
    
    
    // Database
    
    resource retailDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'retail_db'
    
      properties: {
    
        charset: 'UTF8'
    
        collation: 'en_US.utf8'
    
      }
    
    }
    
    
    
    // PostgreSQL extensions
    
    resource pgvectorExtension 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'shared_preload_libraries'
    
      properties: {
    
        value: 'pg_stat_statements,pgaudit,vector'
    
        source: 'user-override'
    
      }
    
    }
    
    
    
    // Output connection details
    
    output serverFQDN string = postgresqlServer.properties.fullyQualifiedDomainName
    
    output serverId string = postgresqlServer.id
    
    output databaseName string = retailDatabase.name
    
    

    ๐Ÿš€ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ

    GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ

    
    # .github/workflows/deploy.yml - CI/CD pipeline
    
    name: Deploy MCP Retail Server
    
    
    
    on:
    
      push:
    
        branches: [main]
    
      pull_request:
    
        branches: [main]
    
      workflow_dispatch:
    
        inputs:
    
          environment:
    
            description: 'Deployment environment'
    
            required: true
    
            default: 'development'
    
            type: choice
    
            options:
    
              - development
    
              - staging
    
              - production
    
    
    
    env:
    
      CONTAINER_REGISTRY: mcpretailregistry.azurecr.io
    
      IMAGE_NAME: mcp-retail-server
    
      AZURE_RESOURCE_GROUP: mcp-retail-rg
    
    
    
    jobs:
    
      test:
    
        runs-on: ubuntu-latest
    
        services:
    
          postgres:
    
            image: pgvector/pgvector:pg16
    
            env:
    
              POSTGRES_PASSWORD: postgres
    
              POSTGRES_DB: retail_test
    
            options: >-
    
              --health-cmd pg_isready
    
              --health-interval 10s
    
              --health-timeout 5s
    
              --health-retries 5
    
            ports:
    
              - 5432:5432
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Set up Python
    
            uses: actions/setup-python@v4
    
            with:
    
              python-version: '3.11'
    
              cache: 'pip'
    
    
    
          - name: Install dependencies
    
            run: |
    
              python -m pip install --upgrade pip
    
              pip install -r requirements.lock.txt
    
              pip install pytest pytest-cov pytest-asyncio
    
    
    
          - name: Set up test database
    
            run: |
    
              PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
              python scripts/generate_sample_data.py --test
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
    
    
          - name: Run tests
    
            run: |
    
              pytest tests/ -v --cov=mcp_server --cov-report=xml --cov-report=html
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
              PROJECT_ENDPOINT: ${{ secrets.TEST_PROJECT_ENDPOINT }}
    
              AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }}
    
              AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }}
    
              AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    
    
    
          - name: Upload coverage reports
    
            uses: codecov/codecov-action@v3
    
            with:
    
              file: ./coverage.xml
    
              flags: unittests
    
    
    
      security-scan:
    
        runs-on: ubuntu-latest
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Run Trivy vulnerability scanner
    
            uses: aquasecurity/trivy-action@master
    
            with:
    
              scan-type: 'fs'
    
              scan-ref: '.'
    
              format: 'sarif'
    
              output: 'trivy-results.sarif'
    
    
    
          - name: Upload Trivy scan results
    
            uses: github/codeql-action/upload-sarif@v2
    
            with:
    
              sarif_file: 'trivy-results.sarif'
    
    
    
          - name: Run Bandit security linter
    
            run: |
    
              pip install bandit[toml]
    
              bandit -r mcp_server/ -f json -o bandit-report.json
    
    
    
      build:
    
        runs-on: ubuntu-latest
    
        needs: [test, security-scan]
    
        if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
    
        
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Build and push Docker image
    
            uses: azure/docker-login@v1
    
            with:
    
              login-server: ${{ env.CONTAINER_REGISTRY }}
    
              username: ${{ secrets.REGISTRY_USERNAME }}
    
              password: ${{ secrets.REGISTRY_PASSWORD }}
    
    
    
          - name: Build, tag, and push image
    
            run: |
    
              # Generate unique tag
    
              IMAGE_TAG="${GITHUB_SHA::8}-$(date +%s)"
    
              
    
              # Build image
    
              docker build \
    
                --target production \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:latest \
    
                .
    
              
    
              # Push images
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:latest
    
              
    
              # Save tag for deployment
    
              echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
    
    
    
          - name: Output image details
    
            run: |
    
              echo "Built and pushed image: $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
    
    
    
      deploy-staging:
    
        runs-on: ubuntu-latest
    
        needs: build
    
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
        environment: staging
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to staging
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy infrastructure
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.staging.json \
    
                  --parameters containerImageTag=$IMAGE_TAG
    
    
    
                # Update container app
    
                az containerapp update \
    
                  --name mcp-retail-server-staging \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --image $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
    
    
          - name: Run integration tests
    
            run: |
    
              # Wait for deployment to be ready
    
              sleep 60
    
              
    
              # Run integration tests against staging
    
              pytest tests/integration/ \
    
                --endpoint https://mcp-retail-server-staging.azurecontainerapps.io \
    
                --timeout 300
    
    
    
      deploy-production:
    
        runs-on: ubuntu-latest
    
        needs: [build, deploy-staging]
    
        if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
    
        environment: production
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to production
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy with blue-green strategy
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.prod.json \
    
                  --parameters containerImageTag=$IMAGE_TAG \
    
                  --parameters deploymentSlot=green
    
    
    
                # Health check
    
                az containerapp show \
    
                  --name mcp-retail-server-prod-green \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod
    
    
    
                # Switch traffic (blue-green deployment)
    
                az containerapp ingress traffic set \
    
                  --name mcp-retail-server-prod \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --revision-weight latest=100
    
    

    Azure DevOps ํŒŒ์ดํ”„๋ผ์ธ

    
    # azure-pipelines.yml - Azure DevOps pipeline
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - develop
    
      paths:
    
        exclude:
    
          - docs/*
    
          - README.md
    
    
    
    variables:
    
      containerRegistry: 'mcpretailregistry.azurecr.io'
    
      imageName: 'mcp-retail-server'
    
      imageTag: '$(Build.BuildId)'
    
      azureServiceConnection: 'azure-service-connection'
    
    
    
    stages:
    
      - stage: Build
    
        displayName: 'Build and Test'
    
        jobs:
    
          - job: Test
    
            displayName: 'Run Tests'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            
    
            services:
    
              postgres:
    
                image: pgvector/pgvector:pg16
    
                env:
    
                  POSTGRES_PASSWORD: postgres
    
                  POSTGRES_DB: retail_test
    
                ports:
    
                  5432:5432
    
    
    
            steps:
    
              - task: UsePythonVersion@0
    
                inputs:
    
                  versionSpec: '3.11'
    
                  displayName: 'Use Python 3.11'
    
    
    
              - script: |
    
                  python -m pip install --upgrade pip
    
                  pip install -r requirements.lock.txt
    
                  pip install pytest pytest-cov pytest-asyncio
    
                displayName: 'Install dependencies'
    
    
    
              - script: |
    
                  PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
                  python scripts/generate_sample_data.py --test
    
                displayName: 'Set up test database'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - script: |
    
                  pytest tests/ -v --cov=mcp_server --cov-report=xml --junitxml=test-results.xml
    
                displayName: 'Run tests'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - task: PublishTestResults@2
    
                condition: succeededOrFailed()
    
                inputs:
    
                  testResultsFiles: 'test-results.xml'
    
                  testRunTitle: 'Python Tests'
    
    
    
              - task: PublishCodeCoverageResults@1
    
                inputs:
    
                  codeCoverageTool: 'Cobertura'
    
                  summaryFileLocation: 'coverage.xml'
    
    
    
          - job: Build
    
            displayName: 'Build Docker Image'
    
            dependsOn: Test
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
    
    
            steps:
    
              - task: AzureCLI@2
    
                displayName: 'Build and push Docker image'
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  scriptType: 'bash'
    
                  scriptLocation: 'inlineScript'
    
                  inlineScript: |
    
                    # Login to container registry
    
                    az acr login --name $(containerRegistry)
    
                    
    
                    # Build and push image
    
                    docker build \
    
                      --target production \
    
                      --tag $(containerRegistry)/$(imageName):$(imageTag) \
    
                      --tag $(containerRegistry)/$(imageName):latest \
    
                      .
    
                    
    
                    docker push $(containerRegistry)/$(imageName):$(imageTag)
    
                    docker push $(containerRegistry)/$(imageName):latest
    
    
    
      - stage: Deploy_Staging
    
        displayName: 'Deploy to Staging'
    
        dependsOn: Build
    
        condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
        
    
        jobs:
    
          - deployment: DeployStaging
    
            displayName: 'Deploy to Staging Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'staging'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy infrastructure'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-staging-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.staging.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    
    
      - stage: Deploy_Production
    
        displayName: 'Deploy to Production'
    
        dependsOn: Deploy_Staging
    
        condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
    
        
    
        jobs:
    
          - deployment: DeployProduction
    
            displayName: 'Deploy to Production Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'production'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy to production'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-prod-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.prod.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    

    ๐Ÿ“Š ํ™•์žฅ ๋ฐ ์„ฑ๋Šฅ

    ์ž๋™ ํ™•์žฅ ๊ตฌ์„ฑ

    
    # k8s/hpa.yaml - Horizontal Pod Autoscaler for Kubernetes
    
    apiVersion: autoscaling/v2
    
    kind: HorizontalPodAutoscaler
    
    metadata:
    
      name: mcp-retail-server-hpa
    
      namespace: mcp-retail
    
    spec:
    
      scaleTargetRef:
    
        apiVersion: apps/v1
    
        kind: Deployment
    
        name: mcp-retail-server
    
      minReplicas: 3
    
      maxReplicas: 50
    
      metrics:
    
        - type: Resource
    
          resource:
    
            name: cpu
    
            target:
    
              type: Utilization
    
              averageUtilization: 70
    
        - type: Resource
    
          resource:
    
            name: memory
    
            target:
    
              type: Utilization
    
              averageUtilization: 80
    
        - type: Pods
    
          pods:
    
            metric:
    
              name: http_requests_per_second
    
            target:
    
              type: AverageValue
    
              averageValue: 100
    
      behavior:
    
        scaleDown:
    
          stabilizationWindowSeconds: 300
    
          policies:
    
            - type: Percent
    
              value: 50
    
              periodSeconds: 60
    
        scaleUp:
    
          stabilizationWindowSeconds: 60
    
          policies:
    
            - type: Percent
    
              value: 100
    
              periodSeconds: 30
    
            - type: Pods
    
              value: 5
    
              periodSeconds: 30
    
          selectPolicy: Max
    
    

    ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

    
    # mcp_server/monitoring/performance.py
    
    """
    
    Performance monitoring and metrics collection for production deployment.
    
    """
    
    import asyncio
    
    import time
    
    import psutil
    
    from typing import Dict, Any
    
    from dataclasses import dataclass
    
    from datetime import datetime, timedelta
    
    import logging
    
    
    
    @dataclass
    
    class PerformanceMetrics:
    
        """Performance metrics data structure."""
    
        timestamp: datetime
    
        cpu_percent: float
    
        memory_percent: float
    
        memory_used_mb: float
    
        active_connections: int
    
        request_rate: float
    
        avg_response_time: float
    
        error_rate: float
    
        database_connections: int
    
    
    
    class PerformanceMonitor:
    
        """Monitor and collect performance metrics."""
    
        
    
        def __init__(self, config):
    
            self.config = config
    
            self.logger = logging.getLogger(__name__)
    
            
    
            # Metrics collection
    
            self.metrics_history = []
    
            self.request_times = []
    
            self.error_count = 0
    
            self.request_count = 0
    
            
    
            # Database monitoring
    
            self.db_pool = None
    
            
    
        async def start_monitoring(self):
    
            """Start continuous performance monitoring."""
    
            
    
            self.logger.info("Starting performance monitoring")
    
            
    
            # Start metrics collection task
    
            asyncio.create_task(self._collect_metrics_loop())
    
            asyncio.create_task(self._cleanup_old_metrics())
    
            
    
        async def _collect_metrics_loop(self):
    
            """Continuously collect performance metrics."""
    
            
    
            while True:
    
                try:
    
                    metrics = await self._collect_current_metrics()
    
                    self.metrics_history.append(metrics)
    
                    
    
                    # Log critical metrics
    
                    if metrics.cpu_percent > 90:
    
                        self.logger.warning(f"High CPU usage: {metrics.cpu_percent:.1f}%")
    
                    
    
                    if metrics.memory_percent > 90:
    
                        self.logger.warning(f"High memory usage: {metrics.memory_percent:.1f}%")
    
                    
    
                    if metrics.error_rate > 0.05:  # 5% error rate
    
                        self.logger.warning(f"High error rate: {metrics.error_rate:.2%}")
    
                    
    
                    await asyncio.sleep(30)  # Collect every 30 seconds
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error collecting metrics: {e}")
    
                    await asyncio.sleep(60)
    
        
    
        async def _collect_current_metrics(self) -> PerformanceMetrics:
    
            """Collect current system metrics."""
    
            
    
            # System metrics
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            memory = psutil.virtual_memory()
    
            
    
            # Application metrics
    
            current_time = datetime.utcnow()
    
            recent_requests = [
    
                req_time for req_time in self.request_times
    
                if current_time - req_time < timedelta(minutes=1)
    
            ]
    
            
    
            request_rate = len(recent_requests) / 60.0  # requests per second
    
            
    
            # Calculate average response time
    
            avg_response_time = 0.0
    
            if hasattr(self, '_recent_response_times'):
    
                recent_response_times = [
    
                    rt for rt in self._recent_response_times
    
                    if current_time - rt['timestamp'] < timedelta(minutes=5)
    
                ]
    
                if recent_response_times:
    
                    avg_response_time = sum(rt['time'] for rt in recent_response_times) / len(recent_response_times)
    
            
    
            # Error rate calculation
    
            error_rate = 0.0
    
            if self.request_count > 0:
    
                error_rate = self.error_count / self.request_count
    
            
    
            # Database connections
    
            db_connections = 0
    
            if self.db_pool:
    
                db_connections = len(self.db_pool._holders)
    
            
    
            return PerformanceMetrics(
    
                timestamp=current_time,
    
                cpu_percent=cpu_percent,
    
                memory_percent=memory.percent,
    
                memory_used_mb=memory.used / (1024 * 1024),
    
                active_connections=0,  # To be implemented with connection tracking
    
                request_rate=request_rate,
    
                avg_response_time=avg_response_time,
    
                error_rate=error_rate,
    
                database_connections=db_connections
    
            )
    
        
    
        async def _cleanup_old_metrics(self):
    
            """Clean up old metrics to prevent memory leaks."""
    
            
    
            while True:
    
                try:
    
                    cutoff_time = datetime.utcnow() - timedelta(hours=24)
    
                    
    
                    # Clean up metrics history
    
                    self.metrics_history = [
    
                        m for m in self.metrics_history
    
                        if m.timestamp > cutoff_time
    
                    ]
    
                    
    
                    # Clean up request times
    
                    self.request_times = [
    
                        rt for rt in self.request_times
    
                        if rt > cutoff_time
    
                    ]
    
                    
    
                    # Reset counters periodically
    
                    if datetime.utcnow().minute == 0:  # Every hour
    
                        self.error_count = 0
    
                        self.request_count = 0
    
                    
    
                    await asyncio.sleep(3600)  # Run every hour
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error cleaning up metrics: {e}")
    
                    await asyncio.sleep(3600)
    
        
    
        def record_request(self, response_time: float, success: bool = True):
    
            """Record a request for metrics."""
    
            
    
            current_time = datetime.utcnow()
    
            self.request_times.append(current_time)
    
            self.request_count += 1
    
            
    
            if not success:
    
                self.error_count += 1
    
            
    
            # Record response time
    
            if not hasattr(self, '_recent_response_times'):
    
                self._recent_response_times = []
    
            
    
            self._recent_response_times.append({
    
                'timestamp': current_time,
    
                'time': response_time
    
            })
    
        
    
        def get_current_metrics(self) -> Dict[str, Any]:
    
            """Get current performance metrics."""
    
            
    
            if not self.metrics_history:
    
                return {}
    
            
    
            latest_metrics = self.metrics_history[-1]
    
            
    
            return {
    
                'timestamp': latest_metrics.timestamp.isoformat(),
    
                'system': {
    
                    'cpu_percent': latest_metrics.cpu_percent,
    
                    'memory_percent': latest_metrics.memory_percent,
    
                    'memory_used_mb': latest_metrics.memory_used_mb
    
                },
    
                'application': {
    
                    'active_connections': latest_metrics.active_connections,
    
                    'request_rate': latest_metrics.request_rate,
    
                    'avg_response_time': latest_metrics.avg_response_time,
    
                    'error_rate': latest_metrics.error_rate
    
                },
    
                'database': {
    
                    'connections': latest_metrics.database_connections
    
                }
    
            }
    
        
    
        def get_metrics_summary(self, hours: int = 24) -> Dict[str, Any]:
    
            """Get performance metrics summary for the specified hours."""
    
            
    
            cutoff_time = datetime.utcnow() - timedelta(hours=hours)
    
            recent_metrics = [
    
                m for m in self.metrics_history
    
                if m.timestamp > cutoff_time
    
            ]
    
            
    
            if not recent_metrics:
    
                return {}
    
            
    
            # Calculate averages
    
            avg_cpu = sum(m.cpu_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_memory = sum(m.memory_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_response_time = sum(m.avg_response_time for m in recent_metrics) / len(recent_metrics)
    
            
    
            # Calculate peaks
    
            max_cpu = max(m.cpu_percent for m in recent_metrics)
    
            max_memory = max(m.memory_percent for m in recent_metrics)
    
            max_response_time = max(m.avg_response_time for m in recent_metrics)
    
            
    
            return {
    
                'period_hours': hours,
    
                'averages': {
    
                    'cpu_percent': round(avg_cpu, 2),
    
                    'memory_percent': round(avg_memory, 2),
    
                    'response_time': round(avg_response_time, 3)
    
                },
    
                'peaks': {
    
                    'cpu_percent': round(max_cpu, 2),
    
                    'memory_percent': round(max_memory, 2),
    
                    'response_time': round(max_response_time, 3)
    
                },
    
                'data_points': len(recent_metrics)
    
            }
    
    

    ๐Ÿ” ํ”„๋กœ๋•์…˜ ๋ณด์•ˆ ๊ตฌ์„ฑ

    ๋ณด์•ˆ ๊ฐ•ํ™”

    
    # k8s/security-policy.yaml - Kubernetes security policies
    
    apiVersion: v1
    
    kind: SecurityContext
    
    metadata:
    
      name: mcp-retail-security-context
    
    spec:
    
      runAsNonRoot: true
    
      runAsUser: 1000
    
      runAsGroup: 1000
    
      fsGroup: 1000
    
      seccompProfile:
    
        type: RuntimeDefault
    
      capabilities:
    
        drop:
    
          - ALL
    
      readOnlyRootFilesystem: true
    
      allowPrivilegeEscalation: false
    
    
    
    ---
    
    apiVersion: networking.k8s.io/v1
    
    kind: NetworkPolicy
    
    metadata:
    
      name: mcp-retail-network-policy
    
      namespace: mcp-retail
    
    spec:
    
      podSelector:
    
        matchLabels:
    
          app: mcp-retail-server
    
      policyTypes:
    
        - Ingress
    
        - Egress
    
      ingress:
    
        - from:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: ingress-nginx
    
          ports:
    
            - protocol: TCP
    
              port: 8000
    
      egress:
    
        - to:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: database
    
          ports:
    
            - protocol: TCP
    
              port: 5432
    
        - to: []
    
          ports:
    
            - protocol: TCP
    
              port: 443  # HTTPS for Azure OpenAI
    
            - protocol: TCP
    
              port: 53   # DNS
    
            - protocol: UDP
    
              port: 53   # DNS
    
    

    ํ™˜๊ฒฝ ๊ตฌ์„ฑ

    
    # scripts/setup-production-env.sh
    
    #!/bin/bash
    
    
    
    # Production environment setup script
    
    set -euo pipefail
    
    
    
    echo "๐Ÿ”ง Setting up production environment..."
    
    
    
    # Create resource groups
    
    az group create --name "mcp-retail-prod-rg" --location "East US"
    
    az group create --name "mcp-retail-shared-rg" --location "East US"
    
    
    
    # Create Key Vault
    
    echo "๐Ÿ” Creating Azure Key Vault..."
    
    az keyvault create \
    
      --name "mcp-retail-kv-prod" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --location "East US" \
    
      --enable-rbac-authorization true
    
    
    
    # Set secrets
    
    echo "๐Ÿ”‘ Setting up secrets..."
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "postgres-password" \
    
      --value "${POSTGRES_PASSWORD}"
    
    
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "azure-openai-key" \
    
      --value "${AZURE_OPENAI_KEY}"
    
    
    
    # Create container registry
    
    echo "๐Ÿ“ฆ Creating container registry..."
    
    az acr create \
    
      --name "mcpretailregistry" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --sku Premium \
    
      --admin-enabled false
    
    
    
    # Create virtual network
    
    echo "๐ŸŒ Creating virtual network..."
    
    az network vnet create \
    
      --name "mcp-retail-vnet" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --address-prefix "10.0.0.0/16" \
    
      --subnet-name "container-apps" \
    
      --subnet-prefix "10.0.1.0/24"
    
    
    
    az network vnet subnet create \
    
      --name "database" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --vnet-name "mcp-retail-vnet" \
    
      --address-prefix "10.0.2.0/24" \
    
      --delegations Microsoft.DBforPostgreSQL/flexibleServers
    
    
    
    # Deploy infrastructure
    
    echo "๐Ÿ—๏ธ  Deploying infrastructure..."
    
    az deployment group create \
    
      --resource-group "mcp-retail-prod-rg" \
    
      --template-file "infra/main.bicep" \
    
      --parameters "infra/main.parameters.prod.json"
    
    
    
    echo "โœ… Production environment setup complete!"
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ์ปจํ…Œ์ด๋„ˆ ์ „๋žต: ๋ณด์•ˆ์ด ๊ฐ•ํ™”๋œ ํ”„๋กœ๋•์…˜ ์ค€๋น„ Docker ์ปจํ…Œ์ด๋„ˆ

    โœ… ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ: ์ž๋™ ํ™•์žฅ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ํฌํ•จ๋œ Azure Container Apps

    โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฐํฌ: ๊ณ ๊ฐ€์šฉ์„ฑ์„ ๊ฐ–์ถ˜ PostgreSQL Flexible Server

    โœ… CI/CD ํŒŒ์ดํ”„๋ผ์ธ: ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ, ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ ์›Œํฌํ”Œ๋กœ์šฐ

    โœ… ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง: ํฌ๊ด„์ ์ธ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋ฐ ๊ฒฝ๊ณ 

    โœ… ๋ณด์•ˆ ๊ตฌ์„ฑ: ํ”„๋กœ๋•์…˜๊ธ‰ ๋ณด์•ˆ ์ •์ฑ… ๋ฐ ๋„คํŠธ์›Œํฌ ๊ฒฉ๋ฆฌ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 11: ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€์ธก์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • Application Insights๋ฅผ ์‚ฌ์šฉํ•œ ํฌ๊ด„์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •
  • ๊ตฌ์กฐํ™”๋œ ๋กœ๊น… ๋ฐ ๋ถ„์‚ฐ ์ถ”์  ๊ตฌ์„ฑ
  • ๊ฒฝ๊ณ  ๋ฐ ์ž๋™ ์‘๋‹ต ์‹œ์Šคํ…œ ๊ตฌํ˜„
  • ๋น„์ฆˆ๋‹ˆ์Šค ๋ฉ”ํŠธ๋ฆญ ๋ฐ ์„ฑ๋Šฅ KPI ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ์ปจํ…Œ์ด๋„ˆ ๊ธฐ์ˆ 

  • Docker Best Practices - Docker ๊ณต์‹ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • Azure Container Apps - Azure Container Apps ๋ฌธ์„œ
  • Kubernetes Documentation - Kubernetes ๊ณต์‹ ๋ฌธ์„œ
  • CI/CD ๋ฐ DevOps

  • GitHub Actions - GitHub Actions ๋ฌธ์„œ
  • Azure DevOps - Azure DevOps ์„œ๋น„์Šค
  • Infrastructure as Code - Azure Bicep ๋ฌธ์„œ
  • ๋ณด์•ˆ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

  • Azure Security Center - Azure ๋ณด์•ˆ ๊ถŒ์žฅ ์‚ฌํ•ญ
  • Container Security - Kubernetes ๋ณด์•ˆ ๊ฐœ๋…
  • Application Insights - Azure Application Insights
  • ---

    ์ด์ „: Lab 09: VS Code ํ†ตํ•ฉ

    ๋‹ค์Œ: Lab 11: ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€์ธก

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    Docker ๋ฐฐํฌ, Azure Container Apps, ์Šค์ผ€์ผ๋ง ๊ณ ๋ ค์‚ฌํ•ญ ๋ฐฐํฌํ•˜๊ธฐ

    ๋ฐฐํฌ ์ „๋žต

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ ํ˜„๋Œ€์ ์ธ ์ปจํ…Œ์ด๋„ˆํ™” ๋ฐ ํด๋ผ์šฐ๋“œ ๋„ค์ดํ‹ฐ๋ธŒ ์ ‘๊ทผ ๋ฐฉ์‹์„ ์‚ฌ์šฉํ•˜์—ฌ MCP ๋ฆฌํ…Œ์ผ ์„œ๋ฒ„๋ฅผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ๋ฐฐํฌํ•˜๋Š” ํฌ๊ด„์ ์ธ ๊ฐ€์ด๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ์›Œํฌ๋กœ๋“œ๋ฅผ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ณ  ์•ˆ์ „ํ•˜๋ฉฐ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ€๋Šฅํ•œ MCP ์„œ๋ฒ„๋ฅผ ๋ฐฐํฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    MCP ์„œ๋ฒ„์˜ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ๋Š” ์ปจํ…Œ์ด๋„ˆํ™”, ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜, ๋ณด์•ˆ, ํ™•์žฅ์„ฑ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์‹ ์ค‘ํžˆ ๊ณ ๋ คํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” Azure Container Apps์™€ PostgreSQL Flexible Server๋ฅผ ์‚ฌ์šฉํ•œ ๋ฐฐํฌ, CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌํ˜„, ๊ฐ€๋ณ€ ์›Œํฌ๋กœ๋“œ๋ฅผ ์œ„ํ•œ ์ž๋™ ํ™•์žฅ ๊ตฌ์„ฑ์— ๋Œ€ํ•ด ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    ๋ฐฐํฌ ์ „๋žต์€ ๊ฐœ๋ฐœ์„ ์œ„ํ•œ ๊ฐ„๋‹จํ•œ ๋‹จ์ผ ์ปจํ…Œ์ด๋„ˆ ๋ฐฐํฌ๋ถ€ํ„ฐ ๋‹ค์ค‘ ์ง€์—ญ, ์ž๋™ ํ™•์žฅ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ์ด๋ฅด๊ธฐ๊นŒ์ง€ ํฌ๊ด„์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋ณด์•ˆ ๊ธฐ๋Šฅ์„ ๊ฐ–์ถ˜ ๋ณต์žกํ•œ ๋ฐฐํฌ๊นŒ์ง€ ๋‹ค์–‘ํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ปจํ…Œ์ด๋„ˆํ™”: Docker๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ MCP ์„œ๋ฒ„๋ฅผ ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ ๋นŒ๋“œ๋กœ ์ปจํ…Œ์ด๋„ˆํ™”
  • ๋ฐฐํฌ: ์•ˆ์ „ํ•œ ๋„คํŠธ์›Œํ‚น์„ ํ†ตํ•ด Azure Container Apps์— ๋ฐฐํฌ
  • ๊ตฌ์„ฑ: ๊ณ ๊ฐ€์šฉ์„ฑ์„ ๊ฐ–์ถ˜ ํ”„๋กœ๋•์…˜๊ธ‰ PostgreSQL ๊ตฌ์„ฑ
  • ๊ตฌํ˜„: ์ž๋™ ๋ฐฐํฌ๋ฅผ ์œ„ํ•œ CI/CD ํŒŒ์ดํ”„๋ผ์ธ
  • ํ™•์žฅ: ์ˆ˜์š”์— ๋”ฐ๋ผ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ž๋™ ํ™•์žฅ
  • ๋ชจ๋‹ˆํ„ฐ๋ง: ํฌ๊ด„์ ์ธ ๊ด€์ธก ๊ธฐ๋Šฅ์„ ํ†ตํ•ด ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๐Ÿณ Docker ์ปจํ…Œ์ด๋„ˆํ™”

    ๋ฉ€ํ‹ฐ ์Šคํ…Œ์ด์ง€ Dockerfile

    
    # Dockerfile - Production-ready multi-stage build
    
    FROM python:3.11-slim AS builder
    
    
    
    # Set build environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PIP_NO_CACHE_DIR=1 \
    
        PIP_DISABLE_PIP_VERSION_CHECK=1
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        build-essential \
    
        libpq-dev \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install dependencies
    
    COPY requirements.lock.txt /tmp/
    
    RUN pip install --no-cache-dir -r /tmp/requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim AS production
    
    
    
    # Set production environment
    
    ENV PYTHONDONTWRITEBYTECODE=1 \
    
        PYTHONUNBUFFERED=1 \
    
        PATH="/opt/venv/bin:$PATH" \
    
        PYTHONPATH="/app"
    
    
    
    # Install runtime dependencies
    
    RUN apt-get update && apt-get install -y \
    
        libpq5 \
    
        curl \
    
        && rm -rf /var/lib/apt/lists/* \
    
        && groupadd -r mcp \
    
        && useradd -r -g mcp -d /app -s /bin/bash mcp
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    
    
    # Set working directory and copy application
    
    WORKDIR /app
    
    COPY --chown=mcp:mcp . .
    
    
    
    # Create necessary directories with proper permissions
    
    RUN mkdir -p /app/logs /app/data /tmp/mcp \
    
        && chown -R mcp:mcp /app /tmp/mcp \
    
        && chmod -R 755 /app
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
    
        CMD python -m mcp_server.health_check || exit 1
    
    
    
    # Switch to non-root user
    
    USER mcp
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Default command
    
    CMD ["python", "-m", "mcp_server.main"]
    
    

    ๊ฐœ๋ฐœ์šฉ Docker Compose

    
    # docker-compose.yml - Development environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        build:
    
          context: .
    
          dockerfile: Dockerfile
    
          target: production
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=postgres
    
          - POSTGRES_PORT=5432
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=mcp_user
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=development
    
        depends_on:
    
          postgres:
    
            condition: service_healthy
    
        volumes:
    
          - ./logs:/app/logs
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      postgres:
    
        image: pgvector/pgvector:pg16
    
        environment:
    
          - POSTGRES_DB=retail_db
    
          - POSTGRES_USER=postgres
    
          - POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD}
    
        ports:
    
          - "5432:5432"
    
        volumes:
    
          - postgres_data:/var/lib/postgresql/data
    
          - ./docker-init:/docker-entrypoint-initdb.d
    
          - ./data:/backup
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD-SHELL", "pg_isready -U postgres -d retail_db"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
      redis:
    
        image: redis:7-alpine
    
        ports:
    
          - "6379:6379"
    
        command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
    
        volumes:
    
          - redis_data:/data
    
        networks:
    
          - mcp-network
    
        restart: unless-stopped
    
        healthcheck:
    
          test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
    
    
    volumes:
    
      postgres_data:
    
        driver: local
    
      redis_data:
    
        driver: local
    
    
    
    networks:
    
      mcp-network:
    
        driver: bridge
    
    

    ํ”„๋กœ๋•์…˜ Docker Compose

    
    # docker-compose.prod.yml - Production environment
    
    version: '3.8'
    
    
    
    services:
    
      mcp-server:
    
        image: ${CONTAINER_REGISTRY}/mcp-retail-server:${IMAGE_TAG}
    
        ports:
    
          - "8000:8000"
    
        environment:
    
          - POSTGRES_HOST=${POSTGRES_HOST}
    
          - POSTGRES_PORT=${POSTGRES_PORT}
    
          - POSTGRES_DB=${POSTGRES_DB}
    
          - POSTGRES_USER=${POSTGRES_USER}
    
          - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
    
          - PROJECT_ENDPOINT=${PROJECT_ENDPOINT}
    
          - AZURE_CLIENT_ID=${AZURE_CLIENT_ID}
    
          - AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET}
    
          - AZURE_TENANT_ID=${AZURE_TENANT_ID}
    
          - APPLICATIONINSIGHTS_CONNECTION_STRING=${APPLICATIONINSIGHTS_CONNECTION_STRING}
    
          - LOG_LEVEL=INFO
    
          - ENVIRONMENT=production
    
          - REDIS_URL=${REDIS_URL}
    
        deploy:
    
          replicas: 3
    
          resources:
    
            limits:
    
              cpus: '2.0'
    
              memory: 2G
    
            reservations:
    
              cpus: '0.5'
    
              memory: 512M
    
          restart_policy:
    
            condition: on-failure
    
            delay: 5s
    
            max_attempts: 3
    
          update_config:
    
            parallelism: 1
    
            delay: 10s
    
            failure_action: rollback
    
        networks:
    
          - mcp-network
    
        healthcheck:
    
          test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
    
          interval: 30s
    
          timeout: 10s
    
          retries: 3
    
          start_period: 60s
    
    
    
    networks:
    
      mcp-network:
    
        external: true
    
    

    โ˜๏ธ Azure Container Apps ๋ฐฐํฌ

    Bicep์„ ์‚ฌ์šฉํ•œ ์ฝ”๋“œํ˜• ์ธํ”„๋ผ

    
    // infra/container-apps.bicep - Azure Container Apps deployment
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('Environment name')
    
    param environmentName string
    
    
    
    @description('Container App name')
    
    param containerAppName string
    
    
    
    @description('Container registry details')
    
    param containerRegistry object
    
    
    
    @description('Database connection details')
    
    @secure()
    
    param databaseConnectionString string
    
    
    
    @description('Azure OpenAI configuration')
    
    param azureOpenAI object
    
    
    
    @description('Application Insights workspace ID')
    
    param workspaceId string
    
    
    
    // Container Apps Environment
    
    resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2023-05-01' = {
    
      name: '${environmentName}-env'
    
      location: location
    
      properties: {
    
        appLogsConfiguration: {
    
          destination: 'log-analytics'
    
          logAnalyticsConfiguration: {
    
            customerId: workspaceId
    
          }
    
        }
    
        infrastructureResourceGroup: '${environmentName}-infra-rg'
    
      }
    
    }
    
    
    
    // Container App
    
    resource mcp_retail_server 'Microsoft.App/containerApps@2023-05-01' = {
    
      name: containerAppName
    
      location: location
    
      properties: {
    
        managedEnvironmentId: containerAppsEnvironment.id
    
        configuration: {
    
          activeRevisionsMode: 'Single'
    
          ingress: {
    
            external: false
    
            targetPort: 8000
    
            allowInsecure: false
    
            traffic: [
    
              {
    
                weight: 100
    
                latestRevision: true
    
              }
    
            ]
    
          }
    
          registries: [
    
            {
    
              server: containerRegistry.server
    
              identity: containerRegistry.identity
    
            }
    
          ]
    
          secrets: [
    
            {
    
              name: 'database-connection-string'
    
              value: databaseConnectionString
    
            }
    
            {
    
              name: 'azure-openai-key'
    
              value: azureOpenAI.apiKey
    
            }
    
          ]
    
        }
    
        template: {
    
          containers: [
    
            {
    
              name: 'mcp-retail-server'
    
              image: '${containerRegistry.server}/mcp-retail-server:latest'
    
              resources: {
    
                cpu: json('1.0')
    
                memory: '2Gi'
    
              }
    
              env: [
    
                {
    
                  name: 'POSTGRES_CONNECTION_STRING'
    
                  secretRef: 'database-connection-string'
    
                }
    
                {
    
                  name: 'PROJECT_ENDPOINT'
    
                  value: azureOpenAI.endpoint
    
                }
    
                {
    
                  name: 'AZURE_OPENAI_API_KEY'
    
                  secretRef: 'azure-openai-key'
    
                }
    
                {
    
                  name: 'LOG_LEVEL'
    
                  value: 'INFO'
    
                }
    
                {
    
                  name: 'ENVIRONMENT'
    
                  value: 'production'
    
                }
    
              ]
    
              probes: [
    
                {
    
                  type: 'Liveness'
    
                  httpGet: {
    
                    path: '/health'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 60
    
                  periodSeconds: 30
    
                  timeoutSeconds: 10
    
                  failureThreshold: 3
    
                }
    
                {
    
                  type: 'Readiness'
    
                  httpGet: {
    
                    path: '/ready'
    
                    port: 8000
    
                    scheme: 'HTTP'
    
                  }
    
                  initialDelaySeconds: 30
    
                  periodSeconds: 10
    
                  timeoutSeconds: 5
    
                  failureThreshold: 3
    
                }
    
              ]
    
            }
    
          ]
    
          scale: {
    
            minReplicas: 2
    
            maxReplicas: 20
    
            rules: [
    
              {
    
                name: 'http-scaling'
    
                http: {
    
                  metadata: {
    
                    concurrentRequests: '10'
    
                  }
    
                }
    
              }
    
              {
    
                name: 'cpu-scaling'
    
                custom: {
    
                  type: 'cpu'
    
                  metadata: {
    
                    type: 'Utilization'
    
                    value: '70'
    
                  }
    
                }
    
              }
    
            ]
    
          }
    
        }
    
      }
    
    }
    
    
    
    // Output the FQDN
    
    output containerAppFQDN string = mcp_retail_server.properties.configuration.ingress.fqdn
    
    output containerAppId string = mcp_retail_server.id
    
    

    PostgreSQL Flexible Server

    
    // infra/database.bicep - PostgreSQL Flexible Server
    
    @description('Location for all resources')
    
    param location string = resourceGroup().location
    
    
    
    @description('PostgreSQL server name')
    
    param serverName string
    
    
    
    @description('Database administrator login')
    
    param administratorLogin string
    
    
    
    @description('Database administrator password')
    
    @secure()
    
    param administratorPassword string
    
    
    
    @description('Virtual network subnet ID')
    
    param subnetId string
    
    
    
    @description('Private DNS zone ID')
    
    param privateDnsZoneId string
    
    
    
    // PostgreSQL Flexible Server
    
    resource postgresqlServer 'Microsoft.DBforPostgreSQL/flexibleServers@2023-03-01-preview' = {
    
      name: serverName
    
      location: location
    
      sku: {
    
        name: 'Standard_D4s_v3'
    
        tier: 'GeneralPurpose'
    
      }
    
      properties: {
    
        administratorLogin: administratorLogin
    
        administratorLoginPassword: administratorPassword
    
        version: '16'
    
        storage: {
    
          storageSizeGB: 128
    
          autoGrow: 'Enabled'
    
          type: 'PremiumSSD'
    
        }
    
        backup: {
    
          backupRetentionDays: 35
    
          geoRedundantBackup: 'Enabled'
    
        }
    
        highAvailability: {
    
          mode: 'ZoneRedundant'
    
        }
    
        network: {
    
          delegatedSubnetResourceId: subnetId
    
          privateDnsZoneArmResourceId: privateDnsZoneId
    
        }
    
        maintenanceWindow: {
    
          dayOfWeek: 0
    
          startHour: 2
    
          startMinute: 0
    
        }
    
      }
    
    }
    
    
    
    // Database
    
    resource retailDatabase 'Microsoft.DBforPostgreSQL/flexibleServers/databases@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'retail_db'
    
      properties: {
    
        charset: 'UTF8'
    
        collation: 'en_US.utf8'
    
      }
    
    }
    
    
    
    // PostgreSQL extensions
    
    resource pgvectorExtension 'Microsoft.DBforPostgreSQL/flexibleServers/configurations@2023-03-01-preview' = {
    
      parent: postgresqlServer
    
      name: 'shared_preload_libraries'
    
      properties: {
    
        value: 'pg_stat_statements,pgaudit,vector'
    
        source: 'user-override'
    
      }
    
    }
    
    
    
    // Output connection details
    
    output serverFQDN string = postgresqlServer.properties.fullyQualifiedDomainName
    
    output serverId string = postgresqlServer.id
    
    output databaseName string = retailDatabase.name
    
    

    ๐Ÿš€ CI/CD ํŒŒ์ดํ”„๋ผ์ธ ๊ตฌ์„ฑ

    GitHub Actions ์›Œํฌํ”Œ๋กœ์šฐ

    
    # .github/workflows/deploy.yml - CI/CD pipeline
    
    name: Deploy MCP Retail Server
    
    
    
    on:
    
      push:
    
        branches: [main]
    
      pull_request:
    
        branches: [main]
    
      workflow_dispatch:
    
        inputs:
    
          environment:
    
            description: 'Deployment environment'
    
            required: true
    
            default: 'development'
    
            type: choice
    
            options:
    
              - development
    
              - staging
    
              - production
    
    
    
    env:
    
      CONTAINER_REGISTRY: mcpretailregistry.azurecr.io
    
      IMAGE_NAME: mcp-retail-server
    
      AZURE_RESOURCE_GROUP: mcp-retail-rg
    
    
    
    jobs:
    
      test:
    
        runs-on: ubuntu-latest
    
        services:
    
          postgres:
    
            image: pgvector/pgvector:pg16
    
            env:
    
              POSTGRES_PASSWORD: postgres
    
              POSTGRES_DB: retail_test
    
            options: >-
    
              --health-cmd pg_isready
    
              --health-interval 10s
    
              --health-timeout 5s
    
              --health-retries 5
    
            ports:
    
              - 5432:5432
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Set up Python
    
            uses: actions/setup-python@v4
    
            with:
    
              python-version: '3.11'
    
              cache: 'pip'
    
    
    
          - name: Install dependencies
    
            run: |
    
              python -m pip install --upgrade pip
    
              pip install -r requirements.lock.txt
    
              pip install pytest pytest-cov pytest-asyncio
    
    
    
          - name: Set up test database
    
            run: |
    
              PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
              python scripts/generate_sample_data.py --test
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
    
    
          - name: Run tests
    
            run: |
    
              pytest tests/ -v --cov=mcp_server --cov-report=xml --cov-report=html
    
            env:
    
              POSTGRES_HOST: localhost
    
              POSTGRES_PORT: 5432
    
              POSTGRES_DB: retail_test
    
              POSTGRES_USER: postgres
    
              POSTGRES_PASSWORD: postgres
    
              PROJECT_ENDPOINT: ${{ secrets.TEST_PROJECT_ENDPOINT }}
    
              AZURE_CLIENT_ID: ${{ secrets.TEST_AZURE_CLIENT_ID }}
    
              AZURE_CLIENT_SECRET: ${{ secrets.TEST_AZURE_CLIENT_SECRET }}
    
              AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
    
    
    
          - name: Upload coverage reports
    
            uses: codecov/codecov-action@v3
    
            with:
    
              file: ./coverage.xml
    
              flags: unittests
    
    
    
      security-scan:
    
        runs-on: ubuntu-latest
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Run Trivy vulnerability scanner
    
            uses: aquasecurity/trivy-action@master
    
            with:
    
              scan-type: 'fs'
    
              scan-ref: '.'
    
              format: 'sarif'
    
              output: 'trivy-results.sarif'
    
    
    
          - name: Upload Trivy scan results
    
            uses: github/codeql-action/upload-sarif@v2
    
            with:
    
              sarif_file: 'trivy-results.sarif'
    
    
    
          - name: Run Bandit security linter
    
            run: |
    
              pip install bandit[toml]
    
              bandit -r mcp_server/ -f json -o bandit-report.json
    
    
    
      build:
    
        runs-on: ubuntu-latest
    
        needs: [test, security-scan]
    
        if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
    
        
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Build and push Docker image
    
            uses: azure/docker-login@v1
    
            with:
    
              login-server: ${{ env.CONTAINER_REGISTRY }}
    
              username: ${{ secrets.REGISTRY_USERNAME }}
    
              password: ${{ secrets.REGISTRY_PASSWORD }}
    
    
    
          - name: Build, tag, and push image
    
            run: |
    
              # Generate unique tag
    
              IMAGE_TAG="${GITHUB_SHA::8}-$(date +%s)"
    
              
    
              # Build image
    
              docker build \
    
                --target production \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG \
    
                --tag $CONTAINER_REGISTRY/$IMAGE_NAME:latest \
    
                .
    
              
    
              # Push images
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
              docker push $CONTAINER_REGISTRY/$IMAGE_NAME:latest
    
              
    
              # Save tag for deployment
    
              echo "IMAGE_TAG=$IMAGE_TAG" >> $GITHUB_ENV
    
    
    
          - name: Output image details
    
            run: |
    
              echo "Built and pushed image: $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG"
    
    
    
      deploy-staging:
    
        runs-on: ubuntu-latest
    
        needs: build
    
        if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    
        environment: staging
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to staging
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy infrastructure
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.staging.json \
    
                  --parameters containerImageTag=$IMAGE_TAG
    
    
    
                # Update container app
    
                az containerapp update \
    
                  --name mcp-retail-server-staging \
    
                  --resource-group $AZURE_RESOURCE_GROUP-staging \
    
                  --image $CONTAINER_REGISTRY/$IMAGE_NAME:$IMAGE_TAG
    
    
    
          - name: Run integration tests
    
            run: |
    
              # Wait for deployment to be ready
    
              sleep 60
    
              
    
              # Run integration tests against staging
    
              pytest tests/integration/ \
    
                --endpoint https://mcp-retail-server-staging.azurecontainerapps.io \
    
                --timeout 300
    
    
    
      deploy-production:
    
        runs-on: ubuntu-latest
    
        needs: [build, deploy-staging]
    
        if: github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production'
    
        environment: production
    
    
    
        steps:
    
          - name: Checkout code
    
            uses: actions/checkout@v4
    
    
    
          - name: Azure Login
    
            uses: azure/login@v1
    
            with:
    
              creds: ${{ secrets.AZURE_CREDENTIALS }}
    
    
    
          - name: Deploy to production
    
            uses: azure/CLI@v1
    
            with:
    
              azcliversion: latest
    
              inlineScript: |
    
                # Deploy with blue-green strategy
    
                az deployment group create \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --template-file infra/main.bicep \
    
                  --parameters infra/main.parameters.prod.json \
    
                  --parameters containerImageTag=$IMAGE_TAG \
    
                  --parameters deploymentSlot=green
    
    
    
                # Health check
    
                az containerapp show \
    
                  --name mcp-retail-server-prod-green \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod
    
    
    
                # Switch traffic (blue-green deployment)
    
                az containerapp ingress traffic set \
    
                  --name mcp-retail-server-prod \
    
                  --resource-group $AZURE_RESOURCE_GROUP-prod \
    
                  --revision-weight latest=100
    
    

    Azure DevOps ํŒŒ์ดํ”„๋ผ์ธ

    
    # azure-pipelines.yml - Azure DevOps pipeline
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - develop
    
      paths:
    
        exclude:
    
          - docs/*
    
          - README.md
    
    
    
    variables:
    
      containerRegistry: 'mcpretailregistry.azurecr.io'
    
      imageName: 'mcp-retail-server'
    
      imageTag: '$(Build.BuildId)'
    
      azureServiceConnection: 'azure-service-connection'
    
    
    
    stages:
    
      - stage: Build
    
        displayName: 'Build and Test'
    
        jobs:
    
          - job: Test
    
            displayName: 'Run Tests'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            
    
            services:
    
              postgres:
    
                image: pgvector/pgvector:pg16
    
                env:
    
                  POSTGRES_PASSWORD: postgres
    
                  POSTGRES_DB: retail_test
    
                ports:
    
                  5432:5432
    
    
    
            steps:
    
              - task: UsePythonVersion@0
    
                inputs:
    
                  versionSpec: '3.11'
    
                  displayName: 'Use Python 3.11'
    
    
    
              - script: |
    
                  python -m pip install --upgrade pip
    
                  pip install -r requirements.lock.txt
    
                  pip install pytest pytest-cov pytest-asyncio
    
                displayName: 'Install dependencies'
    
    
    
              - script: |
    
                  PGPASSWORD=postgres psql -h localhost -U postgres -d retail_test -f scripts/create_schema.sql
    
                  python scripts/generate_sample_data.py --test
    
                displayName: 'Set up test database'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - script: |
    
                  pytest tests/ -v --cov=mcp_server --cov-report=xml --junitxml=test-results.xml
    
                displayName: 'Run tests'
    
                env:
    
                  POSTGRES_HOST: localhost
    
                  POSTGRES_PORT: 5432
    
                  POSTGRES_DB: retail_test
    
                  POSTGRES_USER: postgres
    
                  POSTGRES_PASSWORD: postgres
    
    
    
              - task: PublishTestResults@2
    
                condition: succeededOrFailed()
    
                inputs:
    
                  testResultsFiles: 'test-results.xml'
    
                  testRunTitle: 'Python Tests'
    
    
    
              - task: PublishCodeCoverageResults@1
    
                inputs:
    
                  codeCoverageTool: 'Cobertura'
    
                  summaryFileLocation: 'coverage.xml'
    
    
    
          - job: Build
    
            displayName: 'Build Docker Image'
    
            dependsOn: Test
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
    
    
            steps:
    
              - task: AzureCLI@2
    
                displayName: 'Build and push Docker image'
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  scriptType: 'bash'
    
                  scriptLocation: 'inlineScript'
    
                  inlineScript: |
    
                    # Login to container registry
    
                    az acr login --name $(containerRegistry)
    
                    
    
                    # Build and push image
    
                    docker build \
    
                      --target production \
    
                      --tag $(containerRegistry)/$(imageName):$(imageTag) \
    
                      --tag $(containerRegistry)/$(imageName):latest \
    
                      .
    
                    
    
                    docker push $(containerRegistry)/$(imageName):$(imageTag)
    
                    docker push $(containerRegistry)/$(imageName):latest
    
    
    
      - stage: Deploy_Staging
    
        displayName: 'Deploy to Staging'
    
        dependsOn: Build
    
        condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
        
    
        jobs:
    
          - deployment: DeployStaging
    
            displayName: 'Deploy to Staging Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'staging'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy infrastructure'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-staging-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.staging.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    
    
      - stage: Deploy_Production
    
        displayName: 'Deploy to Production'
    
        dependsOn: Deploy_Staging
    
        condition: and(succeeded(), eq(variables['Build.Reason'], 'Manual'))
    
        
    
        jobs:
    
          - deployment: DeployProduction
    
            displayName: 'Deploy to Production Environment'
    
            pool:
    
              vmImage: 'ubuntu-latest'
    
            environment: 'production'
    
            
    
            strategy:
    
              runOnce:
    
                deploy:
    
                  steps:
    
                    - task: AzureCLI@2
    
                      displayName: 'Deploy to production'
    
                      inputs:
    
                        azureSubscription: $(azureServiceConnection)
    
                        scriptType: 'bash'
    
                        scriptLocation: 'inlineScript'
    
                        inlineScript: |
    
                          az deployment group create \
    
                            --resource-group mcp-retail-prod-rg \
    
                            --template-file infra/main.bicep \
    
                            --parameters infra/main.parameters.prod.json \
    
                            --parameters containerImageTag=$(imageTag)
    
    

    ๐Ÿ“Š ํ™•์žฅ ๋ฐ ์„ฑ๋Šฅ

    ์ž๋™ ํ™•์žฅ ๊ตฌ์„ฑ

    
    # k8s/hpa.yaml - Horizontal Pod Autoscaler for Kubernetes
    
    apiVersion: autoscaling/v2
    
    kind: HorizontalPodAutoscaler
    
    metadata:
    
      name: mcp-retail-server-hpa
    
      namespace: mcp-retail
    
    spec:
    
      scaleTargetRef:
    
        apiVersion: apps/v1
    
        kind: Deployment
    
        name: mcp-retail-server
    
      minReplicas: 3
    
      maxReplicas: 50
    
      metrics:
    
        - type: Resource
    
          resource:
    
            name: cpu
    
            target:
    
              type: Utilization
    
              averageUtilization: 70
    
        - type: Resource
    
          resource:
    
            name: memory
    
            target:
    
              type: Utilization
    
              averageUtilization: 80
    
        - type: Pods
    
          pods:
    
            metric:
    
              name: http_requests_per_second
    
            target:
    
              type: AverageValue
    
              averageValue: 100
    
      behavior:
    
        scaleDown:
    
          stabilizationWindowSeconds: 300
    
          policies:
    
            - type: Percent
    
              value: 50
    
              periodSeconds: 60
    
        scaleUp:
    
          stabilizationWindowSeconds: 60
    
          policies:
    
            - type: Percent
    
              value: 100
    
              periodSeconds: 30
    
            - type: Pods
    
              value: 5
    
              periodSeconds: 30
    
          selectPolicy: Max
    
    

    ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง

    
    # mcp_server/monitoring/performance.py
    
    """
    
    Performance monitoring and metrics collection for production deployment.
    
    """
    
    import asyncio
    
    import time
    
    import psutil
    
    from typing import Dict, Any
    
    from dataclasses import dataclass
    
    from datetime import datetime, timedelta
    
    import logging
    
    
    
    @dataclass
    
    class PerformanceMetrics:
    
        """Performance metrics data structure."""
    
        timestamp: datetime
    
        cpu_percent: float
    
        memory_percent: float
    
        memory_used_mb: float
    
        active_connections: int
    
        request_rate: float
    
        avg_response_time: float
    
        error_rate: float
    
        database_connections: int
    
    
    
    class PerformanceMonitor:
    
        """Monitor and collect performance metrics."""
    
        
    
        def __init__(self, config):
    
            self.config = config
    
            self.logger = logging.getLogger(__name__)
    
            
    
            # Metrics collection
    
            self.metrics_history = []
    
            self.request_times = []
    
            self.error_count = 0
    
            self.request_count = 0
    
            
    
            # Database monitoring
    
            self.db_pool = None
    
            
    
        async def start_monitoring(self):
    
            """Start continuous performance monitoring."""
    
            
    
            self.logger.info("Starting performance monitoring")
    
            
    
            # Start metrics collection task
    
            asyncio.create_task(self._collect_metrics_loop())
    
            asyncio.create_task(self._cleanup_old_metrics())
    
            
    
        async def _collect_metrics_loop(self):
    
            """Continuously collect performance metrics."""
    
            
    
            while True:
    
                try:
    
                    metrics = await self._collect_current_metrics()
    
                    self.metrics_history.append(metrics)
    
                    
    
                    # Log critical metrics
    
                    if metrics.cpu_percent > 90:
    
                        self.logger.warning(f"High CPU usage: {metrics.cpu_percent:.1f}%")
    
                    
    
                    if metrics.memory_percent > 90:
    
                        self.logger.warning(f"High memory usage: {metrics.memory_percent:.1f}%")
    
                    
    
                    if metrics.error_rate > 0.05:  # 5% error rate
    
                        self.logger.warning(f"High error rate: {metrics.error_rate:.2%}")
    
                    
    
                    await asyncio.sleep(30)  # Collect every 30 seconds
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error collecting metrics: {e}")
    
                    await asyncio.sleep(60)
    
        
    
        async def _collect_current_metrics(self) -> PerformanceMetrics:
    
            """Collect current system metrics."""
    
            
    
            # System metrics
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            memory = psutil.virtual_memory()
    
            
    
            # Application metrics
    
            current_time = datetime.utcnow()
    
            recent_requests = [
    
                req_time for req_time in self.request_times
    
                if current_time - req_time < timedelta(minutes=1)
    
            ]
    
            
    
            request_rate = len(recent_requests) / 60.0  # requests per second
    
            
    
            # Calculate average response time
    
            avg_response_time = 0.0
    
            if hasattr(self, '_recent_response_times'):
    
                recent_response_times = [
    
                    rt for rt in self._recent_response_times
    
                    if current_time - rt['timestamp'] < timedelta(minutes=5)
    
                ]
    
                if recent_response_times:
    
                    avg_response_time = sum(rt['time'] for rt in recent_response_times) / len(recent_response_times)
    
            
    
            # Error rate calculation
    
            error_rate = 0.0
    
            if self.request_count > 0:
    
                error_rate = self.error_count / self.request_count
    
            
    
            # Database connections
    
            db_connections = 0
    
            if self.db_pool:
    
                db_connections = len(self.db_pool._holders)
    
            
    
            return PerformanceMetrics(
    
                timestamp=current_time,
    
                cpu_percent=cpu_percent,
    
                memory_percent=memory.percent,
    
                memory_used_mb=memory.used / (1024 * 1024),
    
                active_connections=0,  # To be implemented with connection tracking
    
                request_rate=request_rate,
    
                avg_response_time=avg_response_time,
    
                error_rate=error_rate,
    
                database_connections=db_connections
    
            )
    
        
    
        async def _cleanup_old_metrics(self):
    
            """Clean up old metrics to prevent memory leaks."""
    
            
    
            while True:
    
                try:
    
                    cutoff_time = datetime.utcnow() - timedelta(hours=24)
    
                    
    
                    # Clean up metrics history
    
                    self.metrics_history = [
    
                        m for m in self.metrics_history
    
                        if m.timestamp > cutoff_time
    
                    ]
    
                    
    
                    # Clean up request times
    
                    self.request_times = [
    
                        rt for rt in self.request_times
    
                        if rt > cutoff_time
    
                    ]
    
                    
    
                    # Reset counters periodically
    
                    if datetime.utcnow().minute == 0:  # Every hour
    
                        self.error_count = 0
    
                        self.request_count = 0
    
                    
    
                    await asyncio.sleep(3600)  # Run every hour
    
                    
    
                except Exception as e:
    
                    self.logger.error(f"Error cleaning up metrics: {e}")
    
                    await asyncio.sleep(3600)
    
        
    
        def record_request(self, response_time: float, success: bool = True):
    
            """Record a request for metrics."""
    
            
    
            current_time = datetime.utcnow()
    
            self.request_times.append(current_time)
    
            self.request_count += 1
    
            
    
            if not success:
    
                self.error_count += 1
    
            
    
            # Record response time
    
            if not hasattr(self, '_recent_response_times'):
    
                self._recent_response_times = []
    
            
    
            self._recent_response_times.append({
    
                'timestamp': current_time,
    
                'time': response_time
    
            })
    
        
    
        def get_current_metrics(self) -> Dict[str, Any]:
    
            """Get current performance metrics."""
    
            
    
            if not self.metrics_history:
    
                return {}
    
            
    
            latest_metrics = self.metrics_history[-1]
    
            
    
            return {
    
                'timestamp': latest_metrics.timestamp.isoformat(),
    
                'system': {
    
                    'cpu_percent': latest_metrics.cpu_percent,
    
                    'memory_percent': latest_metrics.memory_percent,
    
                    'memory_used_mb': latest_metrics.memory_used_mb
    
                },
    
                'application': {
    
                    'active_connections': latest_metrics.active_connections,
    
                    'request_rate': latest_metrics.request_rate,
    
                    'avg_response_time': latest_metrics.avg_response_time,
    
                    'error_rate': latest_metrics.error_rate
    
                },
    
                'database': {
    
                    'connections': latest_metrics.database_connections
    
                }
    
            }
    
        
    
        def get_metrics_summary(self, hours: int = 24) -> Dict[str, Any]:
    
            """Get performance metrics summary for the specified hours."""
    
            
    
            cutoff_time = datetime.utcnow() - timedelta(hours=hours)
    
            recent_metrics = [
    
                m for m in self.metrics_history
    
                if m.timestamp > cutoff_time
    
            ]
    
            
    
            if not recent_metrics:
    
                return {}
    
            
    
            # Calculate averages
    
            avg_cpu = sum(m.cpu_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_memory = sum(m.memory_percent for m in recent_metrics) / len(recent_metrics)
    
            avg_response_time = sum(m.avg_response_time for m in recent_metrics) / len(recent_metrics)
    
            
    
            # Calculate peaks
    
            max_cpu = max(m.cpu_percent for m in recent_metrics)
    
            max_memory = max(m.memory_percent for m in recent_metrics)
    
            max_response_time = max(m.avg_response_time for m in recent_metrics)
    
            
    
            return {
    
                'period_hours': hours,
    
                'averages': {
    
                    'cpu_percent': round(avg_cpu, 2),
    
                    'memory_percent': round(avg_memory, 2),
    
                    'response_time': round(avg_response_time, 3)
    
                },
    
                'peaks': {
    
                    'cpu_percent': round(max_cpu, 2),
    
                    'memory_percent': round(max_memory, 2),
    
                    'response_time': round(max_response_time, 3)
    
                },
    
                'data_points': len(recent_metrics)
    
            }
    
    

    ๐Ÿ” ํ”„๋กœ๋•์…˜ ๋ณด์•ˆ ๊ตฌ์„ฑ

    ๋ณด์•ˆ ๊ฐ•ํ™”

    
    # k8s/security-policy.yaml - Kubernetes security policies
    
    apiVersion: v1
    
    kind: SecurityContext
    
    metadata:
    
      name: mcp-retail-security-context
    
    spec:
    
      runAsNonRoot: true
    
      runAsUser: 1000
    
      runAsGroup: 1000
    
      fsGroup: 1000
    
      seccompProfile:
    
        type: RuntimeDefault
    
      capabilities:
    
        drop:
    
          - ALL
    
      readOnlyRootFilesystem: true
    
      allowPrivilegeEscalation: false
    
    
    
    ---
    
    apiVersion: networking.k8s.io/v1
    
    kind: NetworkPolicy
    
    metadata:
    
      name: mcp-retail-network-policy
    
      namespace: mcp-retail
    
    spec:
    
      podSelector:
    
        matchLabels:
    
          app: mcp-retail-server
    
      policyTypes:
    
        - Ingress
    
        - Egress
    
      ingress:
    
        - from:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: ingress-nginx
    
          ports:
    
            - protocol: TCP
    
              port: 8000
    
      egress:
    
        - to:
    
            - namespaceSelector:
    
                matchLabels:
    
                  name: database
    
          ports:
    
            - protocol: TCP
    
              port: 5432
    
        - to: []
    
          ports:
    
            - protocol: TCP
    
              port: 443  # HTTPS for Azure OpenAI
    
            - protocol: TCP
    
              port: 53   # DNS
    
            - protocol: UDP
    
              port: 53   # DNS
    
    

    ํ™˜๊ฒฝ ๊ตฌ์„ฑ

    
    # scripts/setup-production-env.sh
    
    #!/bin/bash
    
    
    
    # Production environment setup script
    
    set -euo pipefail
    
    
    
    echo "๐Ÿ”ง Setting up production environment..."
    
    
    
    # Create resource groups
    
    az group create --name "mcp-retail-prod-rg" --location "East US"
    
    az group create --name "mcp-retail-shared-rg" --location "East US"
    
    
    
    # Create Key Vault
    
    echo "๐Ÿ” Creating Azure Key Vault..."
    
    az keyvault create \
    
      --name "mcp-retail-kv-prod" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --location "East US" \
    
      --enable-rbac-authorization true
    
    
    
    # Set secrets
    
    echo "๐Ÿ”‘ Setting up secrets..."
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "postgres-password" \
    
      --value "${POSTGRES_PASSWORD}"
    
    
    
    az keyvault secret set \
    
      --vault-name "mcp-retail-kv-prod" \
    
      --name "azure-openai-key" \
    
      --value "${AZURE_OPENAI_KEY}"
    
    
    
    # Create container registry
    
    echo "๐Ÿ“ฆ Creating container registry..."
    
    az acr create \
    
      --name "mcpretailregistry" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --sku Premium \
    
      --admin-enabled false
    
    
    
    # Create virtual network
    
    echo "๐ŸŒ Creating virtual network..."
    
    az network vnet create \
    
      --name "mcp-retail-vnet" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --address-prefix "10.0.0.0/16" \
    
      --subnet-name "container-apps" \
    
      --subnet-prefix "10.0.1.0/24"
    
    
    
    az network vnet subnet create \
    
      --name "database" \
    
      --resource-group "mcp-retail-shared-rg" \
    
      --vnet-name "mcp-retail-vnet" \
    
      --address-prefix "10.0.2.0/24" \
    
      --delegations Microsoft.DBforPostgreSQL/flexibleServers
    
    
    
    # Deploy infrastructure
    
    echo "๐Ÿ—๏ธ  Deploying infrastructure..."
    
    az deployment group create \
    
      --resource-group "mcp-retail-prod-rg" \
    
      --template-file "infra/main.bicep" \
    
      --parameters "infra/main.parameters.prod.json"
    
    
    
    echo "โœ… Production environment setup complete!"
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ์ปจํ…Œ์ด๋„ˆ ์ „๋žต: ๋ณด์•ˆ์ด ๊ฐ•ํ™”๋œ ํ”„๋กœ๋•์…˜ ์ค€๋น„ Docker ์ปจํ…Œ์ด๋„ˆ

    โœ… ํด๋ผ์šฐ๋“œ ๋ฐฐํฌ: ์ž๋™ ํ™•์žฅ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ํฌํ•จ๋œ Azure Container Apps

    โœ… ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ฐฐํฌ: ๊ณ ๊ฐ€์šฉ์„ฑ์„ ๊ฐ–์ถ˜ PostgreSQL Flexible Server

    โœ… CI/CD ํŒŒ์ดํ”„๋ผ์ธ: ์ž๋™ํ™”๋œ ํ…Œ์ŠคํŠธ, ๋นŒ๋“œ ๋ฐ ๋ฐฐํฌ ์›Œํฌํ”Œ๋กœ์šฐ

    โœ… ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง: ํฌ๊ด„์ ์ธ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋ฐ ๊ฒฝ๊ณ 

    โœ… ๋ณด์•ˆ ๊ตฌ์„ฑ: ํ”„๋กœ๋•์…˜๊ธ‰ ๋ณด์•ˆ ์ •์ฑ… ๋ฐ ๋„คํŠธ์›Œํฌ ๊ฒฉ๋ฆฌ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    Lab 11: ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€์ธก์„ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • Application Insights๋ฅผ ์‚ฌ์šฉํ•œ ํฌ๊ด„์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •
  • ๊ตฌ์กฐํ™”๋œ ๋กœ๊น… ๋ฐ ๋ถ„์‚ฐ ์ถ”์  ๊ตฌ์„ฑ
  • ๊ฒฝ๊ณ  ๋ฐ ์ž๋™ ์‘๋‹ต ์‹œ์Šคํ…œ ๊ตฌํ˜„
  • ๋น„์ฆˆ๋‹ˆ์Šค ๋ฉ”ํŠธ๋ฆญ ๋ฐ ์„ฑ๋Šฅ KPI ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ์ปจํ…Œ์ด๋„ˆ ๊ธฐ์ˆ 

  • Docker Best Practices - Docker ๊ณต์‹ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • Azure Container Apps - Azure Container Apps ๋ฌธ์„œ
  • Kubernetes Documentation - Kubernetes ๊ณต์‹ ๋ฌธ์„œ
  • CI/CD ๋ฐ DevOps

  • GitHub Actions - GitHub Actions ๋ฌธ์„œ
  • Azure DevOps - Azure DevOps ์„œ๋น„์Šค
  • Infrastructure as Code - Azure Bicep ๋ฌธ์„œ
  • ๋ณด์•ˆ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง

  • Azure Security Center - Azure ๋ณด์•ˆ ๊ถŒ์žฅ ์‚ฌํ•ญ
  • Container Security - Kubernetes ๋ณด์•ˆ ๊ฐœ๋…
  • Application Insights - Azure Application Insights
  • ---

    ์ด์ „: Lab 09: VS Code ํ†ตํ•ฉ

    ๋‹ค์Œ: Lab 11: ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€์ธก

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์„ ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    11 ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฐ€์‹œ์„ฑ

    ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ MCP ์„œ๋ฒ„๋ฅผ ์œ„ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง, ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ, ์•Œ๋ฆผ ๊ตฌํ˜„์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Application Insights ์„ค์ •, ์œ ์˜๋ฏธํ•œ ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ, ํšจ๊ณผ์ ์ธ ์•Œ๋ฆผ ๊ตฌํ˜„, ์šด์˜ ์šฐ์ˆ˜์„ฑ์„ ์œ„ํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ํšจ๊ณผ์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ์€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ MCP ์„œ๋ฒ„์˜ ์•ˆ์ •์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๋ฐ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ์˜ ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ์š”์†Œโ€”๋ฉ”ํŠธ๋ฆญ, ๋กœ๊ทธ, ํŠธ๋ ˆ์ด์Šคโ€”๋ฅผ ๋‹ค๋ฃจ๋ฉฐ, ๋ฌธ์ œ๋ฅผ ์‚ฌ์ „์— ๊ฐ์ง€ํ•˜๊ณ  ์‹ ์†ํ•˜๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ํฌ๊ด„์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ์›์‹œ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ์Šคํ…œ ๋™์ž‘์„ ์ดํ•ดํ•˜๊ณ  ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋ฉฐ ๋†’์€ ๊ฐ€์šฉ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋Š” ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ธ์‚ฌ์ดํŠธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • MCP ์„œ๋ฒ„๋ฅผ ์œ„ํ•œ ํฌ๊ด„์ ์ธ Application Insights ํ†ตํ•ฉ ๊ตฌํ˜„
  • ํšจ๊ณผ์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ์„ ์œ„ํ•œ ๊ตฌ์กฐํ™”๋œ ๋กœ๊น… ํŒจํ„ด ์„ค๊ณ„
  • ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋ฐ ๋ถ„์„ ์‹œ์Šคํ…œ ์ƒ์„ฑ
  • ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์•Œ๋ฆผ์„ ์ œ๊ณตํ•˜๋Š” ์ง€๋Šฅํ˜• ์•Œ๋ฆผ ๊ตฌ์„ฑ
  • ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ ์šด์˜ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์ถ•
  • ํšจ๊ณผ์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ ์›Œํฌํ”Œ๋กœ์šฐ ๋ฐ ๋Ÿฐ๋ถ ์ˆ˜๋ฆฝ
  • ๐Ÿ“Š Application Insights ํ†ตํ•ฉ

    Application Insights ์„ค์ •

    
    # mcp_server/monitoring.py
    
    """
    
    Comprehensive monitoring and telemetry for MCP server.
    
    """
    
    import logging
    
    import time
    
    import psutil
    
    from typing import Dict, Any, Optional
    
    from contextlib import contextmanager
    
    from azure.monitor.opentelemetry import configure_azure_monitor
    
    from opentelemetry import trace, metrics
    
    from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
    
    from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
    
    from opentelemetry.instrumentation.requests import RequestsInstrumentor
    
    
    
    class MCPTelemetryManager:
    
        """Comprehensive telemetry management for MCP server."""
    
        
    
        def __init__(self, connection_string: str):
    
            self.connection_string = connection_string
    
            self.tracer = None
    
            self.meter = None
    
            self.custom_metrics = {}
    
            
    
        def initialize_telemetry(self, app):
    
            """Initialize Application Insights and OpenTelemetry."""
    
            
    
            # Configure Azure Monitor
    
            configure_azure_monitor(
    
                connection_string=self.connection_string,
    
                logger_name="mcp_server",
    
                disable_offline_storage=False
    
            )
    
            
    
            # Get tracer and meter
    
            self.tracer = trace.get_tracer(__name__)
    
            self.meter = metrics.get_meter(__name__)
    
            
    
            # Initialize custom metrics
    
            self._setup_custom_metrics()
    
            
    
            # Instrument FastAPI
    
            FastAPIInstrumentor.instrument_app(app)
    
            
    
            # Instrument database
    
            AsyncPGInstrumentor().instrument()
    
            
    
            # Instrument HTTP requests
    
            RequestsInstrumentor().instrument()
    
            
    
            logging.info("Telemetry initialization complete")
    
        
    
        def _setup_custom_metrics(self):
    
            """Set up custom metrics for MCP server operations."""
    
            
    
            self.custom_metrics = {
    
                # Request metrics
    
                "mcp_requests_total": self.meter.create_counter(
    
                    name="mcp_requests_total",
    
                    description="Total number of MCP requests",
    
                    unit="1"
    
                ),
    
                
    
                "mcp_request_duration": self.meter.create_histogram(
    
                    name="mcp_request_duration_seconds",
    
                    description="MCP request duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                # Database metrics
    
                "database_queries_total": self.meter.create_counter(
    
                    name="database_queries_total",
    
                    description="Total database queries executed",
    
                    unit="1"
    
                ),
    
                
    
                "database_query_duration": self.meter.create_histogram(
    
                    name="database_query_duration_seconds",
    
                    description="Database query duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                "database_connections_active": self.meter.create_up_down_counter(
    
                    name="database_connections_active",
    
                    description="Number of active database connections",
    
                    unit="1"
    
                ),
    
                
    
                # Tool metrics
    
                "tool_executions_total": self.meter.create_counter(
    
                    name="tool_executions_total",
    
                    description="Total tool executions",
    
                    unit="1"
    
                ),
    
                
    
                "tool_execution_duration": self.meter.create_histogram(
    
                    name="tool_execution_duration_seconds",
    
                    description="Tool execution duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                # System metrics
    
                "system_cpu_usage": self.meter.create_gauge(
    
                    name="system_cpu_usage_percent",
    
                    description="System CPU usage percentage",
    
                    unit="%"
    
                ),
    
                
    
                "system_memory_usage": self.meter.create_gauge(
    
                    name="system_memory_usage_bytes",
    
                    description="System memory usage in bytes",
    
                    unit="byte"
    
                ),
    
                
    
                # Error metrics
    
                "errors_total": self.meter.create_counter(
    
                    name="errors_total",
    
                    description="Total number of errors",
    
                    unit="1"
    
                )
    
            }
    
        
    
        @contextmanager
    
        def trace_operation(self, operation_name: str, attributes: Dict[str, Any] = None):
    
            """Create a traced operation with automatic metrics collection."""
    
            
    
            with self.tracer.start_as_current_span(operation_name) as span:
    
                start_time = time.time()
    
                
    
                # Add attributes to span
    
                if attributes:
    
                    for key, value in attributes.items():
    
                        span.set_attribute(key, value)
    
                
    
                try:
    
                    yield span
    
                    
    
                    # Record success metrics
    
                    duration = time.time() - start_time
    
                    
    
                    if "request" in operation_name.lower():
    
                        self.custom_metrics["mcp_requests_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["mcp_request_duration"].record(duration)
    
                    
    
                    elif "query" in operation_name.lower():
    
                        self.custom_metrics["database_queries_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["database_query_duration"].record(duration)
    
                    
    
                    elif "tool" in operation_name.lower():
    
                        self.custom_metrics["tool_executions_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["tool_execution_duration"].record(duration)
    
                    
    
                except Exception as e:
    
                    # Record error
    
                    span.record_exception(e)
    
                    span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
    
                    
    
                    # Record error metrics
    
                    self.custom_metrics["errors_total"].add(1, {
    
                        "operation": operation_name,
    
                        "error_type": type(e).__name__
    
                    })
    
                    
    
                    raise
    
        
    
        def record_system_metrics(self):
    
            """Record system-level metrics."""
    
            
    
            # CPU usage
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            self.custom_metrics["system_cpu_usage"].set(cpu_percent)
    
            
    
            # Memory usage
    
            memory = psutil.virtual_memory()
    
            self.custom_metrics["system_memory_usage"].set(memory.used)
    
            
    
            # Database connections (if available)
    
            if hasattr(db_provider, 'connection_pool') and db_provider.connection_pool:
    
                active_connections = db_provider.connection_pool.get_size()
    
                self.custom_metrics["database_connections_active"].add(active_connections)
    
    
    
    # Global telemetry manager
    
    telemetry_manager = MCPTelemetryManager(
    
        connection_string=config.server.applicationinsights_connection_string
    
    )
    
    

    ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋กœ ๋กœ๊น… ๊ฐ•ํ™”

    
    # mcp_server/logging_config.py
    
    """
    
    Structured logging configuration for MCP server.
    
    """
    
    import logging
    
    import json
    
    import sys
    
    from datetime import datetime
    
    from typing import Dict, Any
    
    import traceback
    
    
    
    class StructuredFormatter(logging.Formatter):
    
        """Custom formatter for structured JSON logging."""
    
        
    
        def format(self, record: logging.LogRecord) -> str:
    
            """Format log record as structured JSON."""
    
            
    
            # Base log structure
    
            log_entry = {
    
                "timestamp": datetime.utcnow().isoformat() + "Z",
    
                "level": record.levelname,
    
                "logger": record.name,
    
                "message": record.getMessage(),
    
                "module": record.module,
    
                "function": record.funcName,
    
                "line": record.lineno
    
            }
    
            
    
            # Add exception information if present
    
            if record.exc_info:
    
                log_entry["exception"] = {
    
                    "type": record.exc_info[0].__name__,
    
                    "message": str(record.exc_info[1]),
    
                    "traceback": traceback.format_exception(*record.exc_info)
    
                }
    
            
    
            # Add custom attributes from extra
    
            if hasattr(record, 'extra_data'):
    
                log_entry.update(record.extra_data)
    
            
    
            # Add correlation ID if available
    
            if hasattr(record, 'correlation_id'):
    
                log_entry["correlation_id"] = record.correlation_id
    
            
    
            # Add user context if available
    
            if hasattr(record, 'user_id'):
    
                log_entry["user_id"] = record.user_id
    
            
    
            if hasattr(record, 'rls_user_id'):
    
                log_entry["rls_user_id"] = record.rls_user_id
    
            
    
            return json.dumps(log_entry, ensure_ascii=False)
    
    
    
    class MCPLogger:
    
        """Enhanced logging utilities for MCP server."""
    
        
    
        def __init__(self, name: str):
    
            self.logger = logging.getLogger(name)
    
            self._setup_structured_logging()
    
        
    
        def _setup_structured_logging(self):
    
            """Configure structured logging."""
    
            
    
            # Remove existing handlers
    
            for handler in self.logger.handlers[:]:
    
                self.logger.removeHandler(handler)
    
            
    
            # Create structured handler
    
            handler = logging.StreamHandler(sys.stdout)
    
            handler.setFormatter(StructuredFormatter())
    
            
    
            self.logger.addHandler(handler)
    
            self.logger.setLevel(logging.INFO)
    
        
    
        def log_mcp_request(
    
            self, 
    
            method: str, 
    
            user_id: str, 
    
            rls_user_id: str,
    
            duration: float = None,
    
            status: str = "success",
    
            **kwargs
    
        ):
    
            """Log MCP request with structured data."""
    
            
    
            extra_data = {
    
                "event_type": "mcp_request",
    
                "method": method,
    
                "user_id": user_id,
    
                "rls_user_id": rls_user_id,
    
                "status": status
    
            }
    
            
    
            if duration is not None:
    
                extra_data["duration_ms"] = duration * 1000
    
            
    
            extra_data.update(kwargs)
    
            
    
            self.logger.info(
    
                f"MCP request: {method} - {status}",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_database_query(
    
            self,
    
            query: str,
    
            duration: float,
    
            row_count: int = None,
    
            user_id: str = None,
    
            **kwargs
    
        ):
    
            """Log database query with performance data."""
    
            
    
            extra_data = {
    
                "event_type": "database_query",
    
                "query_hash": hash(query.strip()),
    
                "duration_ms": duration * 1000,
    
                "query_preview": query[:100] + "..." if len(query) > 100 else query
    
            }
    
            
    
            if row_count is not None:
    
                extra_data["row_count"] = row_count
    
            
    
            if user_id:
    
                extra_data["user_id"] = user_id
    
            
    
            extra_data.update(kwargs)
    
            
    
            level = logging.WARNING if duration > 1.0 else logging.INFO
    
            
    
            self.logger.log(
    
                level,
    
                f"Database query executed ({duration*1000:.2f}ms)",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_security_event(
    
            self,
    
            event_type: str,
    
            user_id: str = None,
    
            ip_address: str = None,
    
            success: bool = True,
    
            details: Dict[str, Any] = None
    
        ):
    
            """Log security-related events."""
    
            
    
            extra_data = {
    
                "event_type": "security_event",
    
                "security_event_type": event_type,
    
                "success": success
    
            }
    
            
    
            if user_id:
    
                extra_data["user_id"] = user_id
    
            
    
            if ip_address:
    
                extra_data["ip_address"] = ip_address
    
            
    
            if details:
    
                extra_data["details"] = details
    
            
    
            level = logging.INFO if success else logging.WARNING
    
            
    
            self.logger.log(
    
                level,
    
                f"Security event: {event_type} - {'success' if success else 'failure'}",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_performance_metric(
    
            self,
    
            metric_name: str,
    
            value: float,
    
            unit: str = "count",
    
            dimensions: Dict[str, str] = None
    
        ):
    
            """Log custom performance metrics."""
    
            
    
            extra_data = {
    
                "event_type": "performance_metric",
    
                "metric_name": metric_name,
    
                "value": value,
    
                "unit": unit
    
            }
    
            
    
            if dimensions:
    
                extra_data["dimensions"] = dimensions
    
            
    
            self.logger.info(
    
                f"Performance metric: {metric_name} = {value} {unit}",
    
                extra={"extra_data": extra_data}
    
            )
    
    
    
    # Global logger instance
    
    mcp_logger = MCPLogger("mcp_server")
    
    

    ์‚ฌ์šฉ์ž ์ •์˜ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘

    
    # mcp_server/metrics_collector.py
    
    """
    
    Custom metrics collection for business and operational insights.
    
    """
    
    import asyncio
    
    import time
    
    from typing import Dict, Any, List
    
    from dataclasses import dataclass
    
    from collections import defaultdict, deque
    
    import statistics
    
    
    
    @dataclass
    
    class MetricPoint:
    
        """Individual metric data point."""
    
        timestamp: float
    
        value: float
    
        dimensions: Dict[str, str]
    
    
    
    class MetricsCollector:
    
        """Advanced metrics collection and analysis."""
    
        
    
        def __init__(self, retention_minutes: int = 60):
    
            self.retention_seconds = retention_minutes * 60
    
            self.metrics_buffer = defaultdict(lambda: deque(maxlen=1000))
    
            self.aggregated_metrics = {}
    
            
    
        def record_metric(
    
            self,
    
            name: str,
    
            value: float,
    
            dimensions: Dict[str, str] = None
    
        ):
    
            """Record a metric point."""
    
            
    
            metric_point = MetricPoint(
    
                timestamp=time.time(),
    
                value=value,
    
                dimensions=dimensions or {}
    
            )
    
            
    
            self.metrics_buffer[name].append(metric_point)
    
            self._cleanup_old_metrics(name)
    
        
    
        def _cleanup_old_metrics(self, metric_name: str):
    
            """Remove metrics older than retention period."""
    
            
    
            cutoff_time = time.time() - self.retention_seconds
    
            buffer = self.metrics_buffer[metric_name]
    
            
    
            while buffer and buffer[0].timestamp < cutoff_time:
    
                buffer.popleft()
    
        
    
        def get_metric_summary(
    
            self,
    
            name: str,
    
            time_window_minutes: int = 5
    
        ) -> Dict[str, Any]:
    
            """Get statistical summary of a metric."""
    
            
    
            time_window_seconds = time_window_minutes * 60
    
            cutoff_time = time.time() - time_window_seconds
    
            
    
            relevant_points = [
    
                point for point in self.metrics_buffer[name]
    
                if point.timestamp >= cutoff_time
    
            ]
    
            
    
            if not relevant_points:
    
                return {"error": "No data available"}
    
            
    
            values = [point.value for point in relevant_points]
    
            
    
            return {
    
                "count": len(values),
    
                "min": min(values),
    
                "max": max(values),
    
                "mean": statistics.mean(values),
    
                "median": statistics.median(values),
    
                "p95": self._percentile(values, 95),
    
                "p99": self._percentile(values, 99),
    
                "time_window_minutes": time_window_minutes
    
            }
    
        
    
        def _percentile(self, values: List[float], percentile: float) -> float:
    
            """Calculate percentile value."""
    
            if not values:
    
                return 0
    
            
    
            sorted_values = sorted(values)
    
            index = int((percentile / 100) * len(sorted_values))
    
            index = min(index, len(sorted_values) - 1)
    
            
    
            return sorted_values[index]
    
        
    
        async def collect_business_metrics(self):
    
            """Collect business-specific metrics."""
    
            
    
            try:
    
                # Query execution patterns
    
                query_types = await self._analyze_query_patterns()
    
                for query_type, count in query_types.items():
    
                    self.record_metric(
    
                        "business_queries_by_type",
    
                        count,
    
                        {"query_type": query_type}
    
                    )
    
                
    
                # User activity patterns
    
                user_activity = await self._analyze_user_activity()
    
                for store_id, activity_count in user_activity.items():
    
                    self.record_metric(
    
                        "user_activity_by_store",
    
                        activity_count,
    
                        {"store_id": store_id}
    
                    )
    
                
    
                # Tool usage patterns
    
                tool_usage = await self._analyze_tool_usage()
    
                for tool_name, usage_count in tool_usage.items():
    
                    self.record_metric(
    
                        "tool_usage",
    
                        usage_count,
    
                        {"tool_name": tool_name}
    
                    )
    
                    
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Business metrics collection failed: {e}")
    
        
    
        async def _analyze_query_patterns(self) -> Dict[str, int]:
    
            """Analyze database query patterns."""
    
            
    
            # This would analyze actual query logs
    
            # For demo purposes, returning sample data
    
            return {
    
                "sales_analysis": 45,
    
                "inventory_check": 23,
    
                "customer_lookup": 18,
    
                "product_search": 31
    
            }
    
        
    
        async def _analyze_user_activity(self) -> Dict[str, int]:
    
            """Analyze user activity by store."""
    
            
    
            # This would analyze actual user activity logs
    
            return {
    
                "seattle": 67,
    
                "redmond": 34,
    
                "bellevue": 23,
    
                "online": 89
    
            }
    
        
    
        async def _analyze_tool_usage(self) -> Dict[str, int]:
    
            """Analyze MCP tool usage patterns."""
    
            
    
            return {
    
                "execute_sales_query": 156,
    
                "get_multiple_table_schemas": 45,
    
                "semantic_search_products": 78,
    
                "get_current_utc_date": 23
    
            }
    
    
    
    # Global metrics collector
    
    metrics_collector = MetricsCollector()
    
    

    ๐Ÿ”” ์•Œ๋ฆผ ๊ตฌ์„ฑ

    ์ง€๋Šฅํ˜• ์•Œ๋ฆผ ์‹œ์Šคํ…œ

    
    # mcp_server/alerting.py
    
    """
    
    Intelligent alerting system for MCP server operations.
    
    """
    
    import asyncio
    
    import json
    
    from typing import Dict, List, Any, Callable
    
    from enum import Enum
    
    from dataclasses import dataclass
    
    from azure.communication.email import EmailClient
    
    import smtplib
    
    from email.mime.text import MIMEText
    
    from email.mime.multipart import MIMEMultipart
    
    
    
    class AlertSeverity(Enum):
    
        LOW = "low"
    
        MEDIUM = "medium"
    
        HIGH = "high"
    
        CRITICAL = "critical"
    
    
    
    @dataclass
    
    class AlertRule:
    
        """Alert rule configuration."""
    
        name: str
    
        condition: Callable[[Dict[str, Any]], bool]
    
        severity: AlertSeverity
    
        cooldown_minutes: int
    
        message_template: str
    
        enabled: bool = True
    
    
    
    @dataclass
    
    class Alert:
    
        """Alert instance."""
    
        rule_name: str
    
        severity: AlertSeverity
    
        message: str
    
        timestamp: float
    
        details: Dict[str, Any]
    
        acknowledged: bool = False
    
    
    
    class AlertManager:
    
        """Comprehensive alerting management."""
    
        
    
        def __init__(self):
    
            self.alert_rules = {}
    
            self.active_alerts = {}
    
            self.alert_history = deque(maxlen=1000)
    
            self.notification_channels = {}
    
            self._setup_default_rules()
    
            self._setup_notification_channels()
    
        
    
        def _setup_default_rules(self):
    
            """Set up default alert rules."""
    
            
    
            # Database connection issues
    
            self.add_alert_rule(AlertRule(
    
                name="database_connection_failure",
    
                condition=lambda metrics: metrics.get("database_status") != "healthy",
    
                severity=AlertSeverity.CRITICAL,
    
                cooldown_minutes=5,
    
                message_template="Database connection failure detected. Service may be unavailable."
    
            ))
    
            
    
            # High error rate
    
            self.add_alert_rule(AlertRule(
    
                name="high_error_rate",
    
                condition=lambda metrics: metrics.get("error_rate", 0) > 0.05,  # 5% error rate
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=10,
    
                message_template="High error rate detected: {error_rate:.2%}. Investigate immediately."
    
            ))
    
            
    
            # Slow query performance
    
            self.add_alert_rule(AlertRule(
    
                name="slow_query_performance",
    
                condition=lambda metrics: metrics.get("avg_query_duration", 0) > 2.0,  # 2 seconds
    
                severity=AlertSeverity.MEDIUM,
    
                cooldown_minutes=15,
    
                message_template="Slow query performance detected. Average duration: {avg_query_duration:.2f}s"
    
            ))
    
            
    
            # High CPU usage
    
            self.add_alert_rule(AlertRule(
    
                name="high_cpu_usage",
    
                condition=lambda metrics: metrics.get("cpu_usage", 0) > 85,  # 85% CPU
    
                severity=AlertSeverity.MEDIUM,
    
                cooldown_minutes=10,
    
                message_template="High CPU usage detected: {cpu_usage:.1f}%"
    
            ))
    
            
    
            # Memory usage
    
            self.add_alert_rule(AlertRule(
    
                name="high_memory_usage",
    
                condition=lambda metrics: metrics.get("memory_usage_percent", 0) > 90,  # 90% memory
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=5,
    
                message_template="High memory usage detected: {memory_usage_percent:.1f}%"
    
            ))
    
            
    
            # Authentication failures
    
            self.add_alert_rule(AlertRule(
    
                name="authentication_failures",
    
                condition=lambda metrics: metrics.get("auth_failure_rate", 0) > 0.1,  # 10% failure rate
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=5,
    
                message_template="High authentication failure rate: {auth_failure_rate:.2%}. Possible security incident."
    
            ))
    
        
    
        def _setup_notification_channels(self):
    
            """Set up notification channels."""
    
            
    
            # Email notifications
    
            email_config = {
    
                "smtp_server": os.getenv("SMTP_SERVER", "smtp.office365.com"),
    
                "smtp_port": int(os.getenv("SMTP_PORT", "587")),
    
                "username": os.getenv("SMTP_USERNAME"),
    
                "password": os.getenv("SMTP_PASSWORD"),
    
                "from_address": os.getenv("ALERT_FROM_EMAIL"),
    
                "to_addresses": os.getenv("ALERT_TO_EMAILS", "").split(",")
    
            }
    
            
    
            if email_config["username"] and email_config["password"]:
    
                self.notification_channels["email"] = EmailNotifier(email_config)
    
            
    
            # Microsoft Teams webhook
    
            teams_webhook = os.getenv("TEAMS_WEBHOOK_URL")
    
            if teams_webhook:
    
                self.notification_channels["teams"] = TeamsNotifier(teams_webhook)
    
            
    
            # Slack webhook
    
            slack_webhook = os.getenv("SLACK_WEBHOOK_URL")
    
            if slack_webhook:
    
                self.notification_channels["slack"] = SlackNotifier(slack_webhook)
    
        
    
        def add_alert_rule(self, rule: AlertRule):
    
            """Add or update an alert rule."""
    
            self.alert_rules[rule.name] = rule
    
        
    
        async def evaluate_metrics(self, metrics: Dict[str, Any]):
    
            """Evaluate metrics against alert rules."""
    
            
    
            for rule_name, rule in self.alert_rules.items():
    
                if not rule.enabled:
    
                    continue
    
                
    
                try:
    
                    # Check if rule condition is met
    
                    if rule.condition(metrics):
    
                        await self._trigger_alert(rule, metrics)
    
                    else:
    
                        # Clear alert if condition no longer met
    
                        await self._clear_alert(rule_name)
    
                        
    
                except Exception as e:
    
                    mcp_logger.logger.error(f"Error evaluating alert rule {rule_name}: {e}")
    
        
    
        async def _trigger_alert(self, rule: AlertRule, metrics: Dict[str, Any]):
    
            """Trigger an alert."""
    
            
    
            current_time = time.time()
    
            
    
            # Check cooldown period
    
            if rule.name in self.active_alerts:
    
                last_alert_time = self.active_alerts[rule.name].timestamp
    
                if current_time - last_alert_time < rule.cooldown_minutes * 60:
    
                    return  # Still in cooldown
    
            
    
            # Format alert message
    
            message = rule.message_template.format(**metrics)
    
            
    
            # Create alert
    
            alert = Alert(
    
                rule_name=rule.name,
    
                severity=rule.severity,
    
                message=message,
    
                timestamp=current_time,
    
                details=metrics.copy()
    
            )
    
            
    
            # Store alert
    
            self.active_alerts[rule.name] = alert
    
            self.alert_history.append(alert)
    
            
    
            # Send notifications
    
            await self._send_notifications(alert)
    
            
    
            mcp_logger.log_security_event(
    
                "alert_triggered",
    
                details={
    
                    "rule_name": rule.name,
    
                    "severity": rule.severity.value,
    
                    "message": message
    
                }
    
            )
    
        
    
        async def _clear_alert(self, rule_name: str):
    
            """Clear an active alert."""
    
            
    
            if rule_name in self.active_alerts:
    
                alert = self.active_alerts[rule_name]
    
                del self.active_alerts[rule_name]
    
                
    
                # Send resolution notification for high/critical alerts
    
                if alert.severity in [AlertSeverity.HIGH, AlertSeverity.CRITICAL]:
    
                    resolution_alert = Alert(
    
                        rule_name=rule_name,
    
                        severity=AlertSeverity.LOW,
    
                        message=f"RESOLVED: {alert.message}",
    
                        timestamp=time.time(),
    
                        details={"resolution": True}
    
                    )
    
                    
    
                    await self._send_notifications(resolution_alert)
    
        
    
        async def _send_notifications(self, alert: Alert):
    
            """Send alert notifications through all configured channels."""
    
            
    
            tasks = []
    
            
    
            for channel_name, notifier in self.notification_channels.items():
    
                task = asyncio.create_task(
    
                    notifier.send_notification(alert),
    
                    name=f"notify_{channel_name}"
    
                )
    
                tasks.append(task)
    
            
    
            if tasks:
    
                # Wait for all notifications with timeout
    
                try:
    
                    await asyncio.wait_for(
    
                        asyncio.gather(*tasks, return_exceptions=True),
    
                        timeout=30.0
    
                    )
    
                except asyncio.TimeoutError:
    
                    mcp_logger.logger.warning("Some alert notifications timed out")
    
    
    
    # Notification implementations
    
    class EmailNotifier:
    
        """Email notification handler."""
    
        
    
        def __init__(self, config: Dict[str, Any]):
    
            self.config = config
    
        
    
        async def send_notification(self, alert: Alert):
    
            """Send email notification."""
    
            
    
            try:
    
                msg = MIMEMultipart()
    
                msg['From'] = self.config['from_address']
    
                msg['To'] = ', '.join(self.config['to_addresses'])
    
                msg['Subject'] = f"[{alert.severity.value.upper()}] MCP Server Alert: {alert.rule_name}"
    
                
    
                body = f"""
    
    Alert Details:
    
    - Rule: {alert.rule_name}
    
    - Severity: {alert.severity.value.upper()}
    
    - Time: {datetime.fromtimestamp(alert.timestamp).isoformat()}
    
    - Message: {alert.message}
    
    
    
    Additional Details:
    
    {json.dumps(alert.details, indent=2)}
    
    
    
    This is an automated alert from the MCP Server monitoring system.
    
                """
    
                
    
                msg.attach(MIMEText(body, 'plain'))
    
                
    
                # Send email
    
                with smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port']) as server:
    
                    server.starttls()
    
                    server.login(self.config['username'], self.config['password'])
    
                    server.send_message(msg)
    
                    
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Failed to send email notification: {e}")
    
    
    
    class TeamsNotifier:
    
        """Microsoft Teams notification handler."""
    
        
    
        def __init__(self, webhook_url: str):
    
            self.webhook_url = webhook_url
    
        
    
        async def send_notification(self, alert: Alert):
    
            """Send Teams notification."""
    
            
    
            color_map = {
    
                AlertSeverity.LOW: "28a745",     # Green
    
                AlertSeverity.MEDIUM: "ffc107",   # Yellow
    
                AlertSeverity.HIGH: "fd7e14",     # Orange
    
                AlertSeverity.CRITICAL: "dc3545"  # Red
    
            }
    
            
    
            payload = {
    
                "@type": "MessageCard",
    
                "@context": "http://schema.org/extensions",
    
                "themeColor": color_map.get(alert.severity, "0076D7"),
    
                "summary": f"MCP Server Alert: {alert.rule_name}",
    
                "sections": [{
    
                    "activityTitle": f"๐Ÿšจ {alert.severity.value.upper()} Alert",
    
                    "activitySubtitle": alert.rule_name,
    
                    "text": alert.message,
    
                    "facts": [
    
                        {"name": "Timestamp", "value": datetime.fromtimestamp(alert.timestamp).isoformat()},
    
                        {"name": "Severity", "value": alert.severity.value.upper()}
    
                    ]
    
                }]
    
            }
    
            
    
            try:
    
                async with aiohttp.ClientSession() as session:
    
                    async with session.post(self.webhook_url, json=payload) as response:
    
                        if response.status != 200:
    
                            raise Exception(f"Teams webhook returned {response.status}")
    
                            
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Failed to send Teams notification: {e}")
    
    
    
    # Global alert manager
    
    alert_manager = AlertManager()
    
    

    ๐Ÿ“ˆ ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ

    Azure Monitor Workbooks

    
    {
    
      "version": "Notebook/1.0",
    
      "items": [
    
        {
    
          "type": 1,
    
          "content": {
    
            "json": "# MCP Server Operations Dashboard\n\nComprehensive monitoring dashboard for Zava Retail MCP Server operations, performance, and health metrics."
    
          },
    
          "name": "title"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart",
    
            "version": "KqlItem/1.0",
    
            "query": "requests\n| where timestamp >= ago(1h)\n| where name contains \"mcp\"\n| summarize RequestCount = count(), AvgDuration = avg(duration) by bin(timestamp, 5m)\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "MCP Request Volume and Performance",
    
            "timeContext": {
    
              "durationMs": 3600000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "timechart"
    
          },
    
          "name": "request-metrics"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-2",
    
            "version": "KqlItem/1.0",
    
            "query": "customMetrics\n| where name == \"database_query_duration_seconds\"\n| where timestamp >= ago(1h)\n| summarize \n    AvgDuration = avg(value),\n    P95Duration = percentile(value, 95),\n    P99Duration = percentile(value, 99)\n    by bin(timestamp, 5m)\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "Database Query Performance",
    
            "timeContext": {
    
              "durationMs": 3600000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "timechart"
    
          },
    
          "name": "database-performance"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-3",
    
            "version": "KqlItem/1.0",
    
            "query": "exceptions\n| where timestamp >= ago(24h)\n| where method contains \"mcp\"\n| summarize ErrorCount = count() by bin(timestamp, 1h), type\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "Error Rate Analysis",
    
            "timeContext": {
    
              "durationMs": 86400000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "barchart"
    
          },
    
          "name": "error-analysis"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-4",
    
            "version": "KqlItem/1.0",
    
            "query": "customMetrics\n| where name in (\"system_cpu_usage_percent\", \"system_memory_usage_bytes\")\n| where timestamp >= ago(2h)\n| extend MetricType = case(\n    name == \"system_cpu_usage_percent\", \"CPU %\",\n    name == \"system_memory_usage_bytes\", \"Memory GB\",\n    \"Unknown\"\n)\n| extend NormalizedValue = case(\n    name == \"system_memory_usage_bytes\", value / (1024*1024*1024),\n    value\n)\n| summarize AvgValue = avg(NormalizedValue) by bin(timestamp, 5m), MetricType\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "System Resource Usage",
    
            "timeContext": {
    
              "durationMs": 7200000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "linechart"
    
          },
    
          "name": "system-resources"
    
        }
    
      ],
    
      "isLocked": false,
    
      "fallbackResourceIds": [
    
        "/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/microsoft.insights/components/{app-insights-name}"
    
      ]
    
    }
    
    

    ์‚ฌ์šฉ์ž ์ •์˜ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„

    
    # mcp_server/dashboard.py
    
    """
    
    Custom dashboard data provider for MCP server metrics.
    
    """
    
    from typing import Dict, List, Any
    
    from fastapi import APIRouter, Depends
    
    from datetime import datetime, timedelta
    
    
    
    dashboard_router = APIRouter(prefix="/dashboard", tags=["dashboard"])
    
    
    
    class DashboardDataProvider:
    
        """Provide dashboard data from various sources."""
    
        
    
        def __init__(self):
    
            self.metrics_collector = metrics_collector
    
            self.alert_manager = alert_manager
    
        
    
        async def get_overview_metrics(self) -> Dict[str, Any]:
    
            """Get high-level overview metrics."""
    
            
    
            current_time = time.time()
    
            one_hour_ago = current_time - 3600
    
            
    
            return {
    
                "timestamp": current_time,
    
                "active_alerts": len(self.alert_manager.active_alerts),
    
                "critical_alerts": len([
    
                    alert for alert in self.alert_manager.active_alerts.values()
    
                    if alert.severity == AlertSeverity.CRITICAL
    
                ]),
    
                "requests_last_hour": await self._get_request_count(one_hour_ago),
    
                "avg_response_time": await self._get_avg_response_time(one_hour_ago),
    
                "error_rate": await self._get_error_rate(one_hour_ago),
    
                "database_status": await self._get_database_status(),
    
                "system_health": await self._get_system_health()
    
            }
    
        
    
        async def get_performance_trends(self, hours: int = 24) -> Dict[str, List[Dict]]:
    
            """Get performance trends over time."""
    
            
    
            end_time = time.time()
    
            start_time = end_time - (hours * 3600)
    
            
    
            # Generate hourly data points
    
            data_points = []
    
            current = start_time
    
            
    
            while current < end_time:
    
                hour_start = current
    
                hour_end = current + 3600
    
                
    
                data_points.append({
    
                    "timestamp": current,
    
                    "requests": await self._get_request_count_range(hour_start, hour_end),
    
                    "avg_duration": await self._get_avg_duration_range(hour_start, hour_end),
    
                    "error_count": await self._get_error_count_range(hour_start, hour_end),
    
                    "cpu_usage": await self._get_cpu_usage_range(hour_start, hour_end),
    
                    "memory_usage": await self._get_memory_usage_range(hour_start, hour_end)
    
                })
    
                
    
                current = hour_end
    
            
    
            return {
    
                "time_series": data_points,
    
                "period_hours": hours,
    
                "data_points": len(data_points)
    
            }
    
        
    
        async def get_business_insights(self) -> Dict[str, Any]:
    
            """Get business-specific insights."""
    
            
    
            return {
    
                "top_queries": await self._get_top_queries(),
    
                "store_activity": await self._get_store_activity(),
    
                "tool_usage": await self._get_tool_usage_stats(),
    
                "user_patterns": await self._get_user_patterns(),
    
                "peak_hours": await self._get_peak_hours()
    
            }
    
        
    
        async def _get_request_count(self, since_time: float) -> int:
    
            """Get request count since specified time."""
    
            summary = self.metrics_collector.get_metric_summary(
    
                "mcp_requests_total",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            return summary.get("count", 0)
    
        
    
        async def _get_avg_response_time(self, since_time: float) -> float:
    
            """Get average response time since specified time."""
    
            summary = self.metrics_collector.get_metric_summary(
    
                "mcp_request_duration_seconds",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            return summary.get("mean", 0.0) * 1000  # Convert to milliseconds
    
        
    
        async def _get_error_rate(self, since_time: float) -> float:
    
            """Calculate error rate since specified time."""
    
            
    
            total_requests = await self._get_request_count(since_time)
    
            error_summary = self.metrics_collector.get_metric_summary(
    
                "errors_total",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            error_count = error_summary.get("count", 0)
    
            
    
            if total_requests == 0:
    
                return 0.0
    
            
    
            return error_count / total_requests
    
        
    
        async def _get_database_status(self) -> str:
    
            """Get current database status."""
    
            try:
    
                health = await db_provider.health_check()
    
                return health.get("status", "unknown")
    
            except Exception:
    
                return "unhealthy"
    
        
    
        async def _get_system_health(self) -> Dict[str, Any]:
    
            """Get current system health metrics."""
    
            
    
            cpu_summary = self.metrics_collector.get_metric_summary("system_cpu_usage_percent", 5)
    
            memory_summary = self.metrics_collector.get_metric_summary("system_memory_usage_bytes", 5)
    
            
    
            return {
    
                "cpu_usage": cpu_summary.get("mean", 0),
    
                "memory_usage_gb": memory_summary.get("mean", 0) / (1024**3),
    
                "status": "healthy"  # Would implement actual health logic
    
            }
    
    
    
    # Dashboard API endpoints
    
    dashboard_provider = DashboardDataProvider()
    
    
    
    @dashboard_router.get("/overview")
    
    async def get_dashboard_overview():
    
        """Get dashboard overview data."""
    
        return await dashboard_provider.get_overview_metrics()
    
    
    
    @dashboard_router.get("/performance")
    
    async def get_performance_data(hours: int = 24):
    
        """Get performance trend data."""
    
        return await dashboard_provider.get_performance_trends(hours)
    
    
    
    @dashboard_router.get("/business")
    
    async def get_business_insights():
    
        """Get business insights data."""
    
        return await dashboard_provider.get_business_insights()
    
    
    
    @dashboard_router.get("/alerts")
    
    async def get_active_alerts():
    
        """Get active alerts."""
    
        return {
    
            "active_alerts": [
    
                {
    
                    "rule_name": alert.rule_name,
    
                    "severity": alert.severity.value,
    
                    "message": alert.message,
    
                    "timestamp": alert.timestamp,
    
                    "acknowledged": alert.acknowledged
    
                }
    
                for alert in alert_manager.active_alerts.values()
    
            ],
    
            "alert_count": len(alert_manager.active_alerts)
    
        }
    
    

    ๐Ÿ” ๋ฌธ์ œ ํ•ด๊ฒฐ ์›Œํฌํ”Œ๋กœ์šฐ

    ์ž๋™ํ™”๋œ ์ง„๋‹จ

    
    # mcp_server/diagnostics.py
    
    """
    
    Automated diagnostics and troubleshooting for MCP server.
    
    """
    
    import asyncio
    
    import subprocess
    
    from typing import Dict, List, Any, Optional
    
    from dataclasses import dataclass
    
    
    
    @dataclass
    
    class DiagnosticResult:
    
        """Result of a diagnostic check."""
    
        check_name: str
    
        status: str  # "pass", "fail", "warning"
    
        message: str
    
        details: Dict[str, Any]
    
        remediation: Optional[str] = None
    
    
    
    class DiagnosticsEngine:
    
        """Comprehensive diagnostics engine."""
    
        
    
        def __init__(self):
    
            self.diagnostic_checks = []
    
            self._register_default_checks()
    
        
    
        def _register_default_checks(self):
    
            """Register default diagnostic checks."""
    
            
    
            self.diagnostic_checks = [
    
                self._check_database_connectivity,
    
                self._check_azure_services,
    
                self._check_system_resources,
    
                self._check_configuration,
    
                self._check_network_connectivity,
    
                self._check_disk_space,
    
                self._check_log_files,
    
                self._check_security_status
    
            ]
    
        
    
        async def run_full_diagnostics(self) -> List[DiagnosticResult]:
    
            """Run all diagnostic checks."""
    
            
    
            results = []
    
            
    
            for check_func in self.diagnostic_checks:
    
                try:
    
                    result = await check_func()
    
                    results.append(result)
    
                except Exception as e:
    
                    results.append(DiagnosticResult(
    
                        check_name=check_func.__name__,
    
                        status="fail",
    
                        message=f"Diagnostic check failed: {str(e)}",
    
                        details={"exception": str(e)}
    
                    ))
    
            
    
            return results
    
        
    
        async def _check_database_connectivity(self) -> DiagnosticResult:
    
            """Check database connectivity and performance."""
    
            
    
            try:
    
                start_time = time.time()
    
                health = await db_provider.health_check()
    
                duration = time.time() - start_time
    
                
    
                if health["status"] == "healthy":
    
                    if duration > 1.0:
    
                        return DiagnosticResult(
    
                            check_name="database_connectivity",
    
                            status="warning",
    
                            message=f"Database responsive but slow ({duration:.2f}s)",
    
                            details=health,
    
                            remediation="Check database server load and network latency"
    
                        )
    
                    else:
    
                        return DiagnosticResult(
    
                            check_name="database_connectivity",
    
                            status="pass",
    
                            message=f"Database healthy ({duration:.2f}s response time)",
    
                            details=health
    
                        )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="database_connectivity",
    
                        status="fail",
    
                        message="Database not healthy",
    
                        details=health,
    
                        remediation="Check database server status and connection parameters"
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="database_connectivity",
    
                    status="fail",
    
                    message=f"Database connectivity failed: {str(e)}",
    
                    details={"error": str(e)},
    
                    remediation="Verify database server is running and connection parameters are correct"
    
                )
    
        
    
        async def _check_azure_services(self) -> DiagnosticResult:
    
            """Check Azure AI services connectivity."""
    
            
    
            try:
    
                # Test Azure OpenAI connectivity
    
                from azure.identity import DefaultAzureCredential
    
                from azure.ai.projects import AIProjectClient
    
                
    
                credential = DefaultAzureCredential()
    
                project_client = AIProjectClient(
    
                    endpoint=config.azure.project_endpoint,
    
                    credential=credential
    
                )
    
                
    
                # This would perform actual connectivity test
    
                # For now, just check configuration
    
                
    
                if config.azure.is_configured():
    
                    return DiagnosticResult(
    
                        check_name="azure_services",
    
                        status="pass",
    
                        message="Azure services configuration valid",
    
                        details={
    
                            "project_endpoint": config.azure.project_endpoint,
    
                            "openai_endpoint": config.azure.openai_endpoint
    
                        }
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="azure_services",
    
                        status="fail",
    
                        message="Azure services not properly configured",
    
                        details={"missing_config": "Check environment variables"},
    
                        remediation="Ensure all Azure configuration environment variables are set"
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="azure_services",
    
                    status="fail",
    
                    message=f"Azure services check failed: {str(e)}",
    
                    details={"error": str(e)},
    
                    remediation="Check Azure credentials and network connectivity"
    
                )
    
        
    
        async def _check_system_resources(self) -> DiagnosticResult:
    
            """Check system resource usage."""
    
            
    
            try:
    
                import psutil
    
                
    
                cpu_percent = psutil.cpu_percent(interval=1)
    
                memory = psutil.virtual_memory()
    
                disk = psutil.disk_usage('/')
    
                
    
                warnings = []
    
                
    
                if cpu_percent > 85:
    
                    warnings.append(f"High CPU usage: {cpu_percent:.1f}%")
    
                
    
                if memory.percent > 85:
    
                    warnings.append(f"High memory usage: {memory.percent:.1f}%")
    
                
    
                if disk.percent > 85:
    
                    warnings.append(f"High disk usage: {disk.percent:.1f}%")
    
                
    
                details = {
    
                    "cpu_percent": cpu_percent,
    
                    "memory_percent": memory.percent,
    
                    "memory_available_gb": memory.available / (1024**3),
    
                    "disk_percent": disk.percent,
    
                    "disk_free_gb": disk.free / (1024**3)
    
                }
    
                
    
                if warnings:
    
                    return DiagnosticResult(
    
                        check_name="system_resources",
    
                        status="warning",
    
                        message=f"Resource warnings: {'; '.join(warnings)}",
    
                        details=details,
    
                        remediation="Monitor resource usage and consider scaling"
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="system_resources",
    
                        status="pass",
    
                        message="System resources normal",
    
                        details=details
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="system_resources",
    
                    status="fail",
    
                    message=f"Resource check failed: {str(e)}",
    
                    details={"error": str(e)}
    
                )
    
        
    
        async def _check_configuration(self) -> DiagnosticResult:
    
            """Check configuration validity."""
    
            
    
            try:
    
                issues = []
    
                
    
                # Check required environment variables
    
                required_vars = [
    
                    "POSTGRES_HOST", "POSTGRES_PASSWORD",
    
                    "PROJECT_ENDPOINT", "AZURE_CLIENT_ID"
    
                ]
    
                
    
                for var in required_vars:
    
                    if not os.getenv(var):
    
                        issues.append(f"Missing environment variable: {var}")
    
                
    
                # Check configuration consistency
    
                if config.server.enable_health_check and not config.server.applicationinsights_connection_string:
    
                    issues.append("Health check enabled but Application Insights not configured")
    
                
    
                if issues:
    
                    return DiagnosticResult(
    
                        check_name="configuration",
    
                        status="fail",
    
                        message=f"Configuration issues: {'; '.join(issues)}",
    
                        details={"issues": issues},
    
                        remediation="Fix configuration issues and restart service"
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="configuration",
    
                        status="pass",
    
                        message="Configuration valid",
    
                        details={"status": "all_checks_passed"}
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="configuration",
    
                    status="fail",
    
                    message=f"Configuration check failed: {str(e)}",
    
                    details={"error": str(e)}
    
                )
    
    
    
    # Diagnostic API endpoint
    
    @dashboard_router.get("/diagnostics")
    
    async def run_diagnostics():
    
        """Run comprehensive diagnostics."""
    
        
    
        diagnostics_engine = DiagnosticsEngine()
    
        results = await diagnostics_engine.run_full_diagnostics()
    
        
    
        # Summarize results
    
        summary = {
    
            "total_checks": len(results),
    
            "passed": len([r for r in results if r.status == "pass"]),
    
            "warnings": len([r for r in results if r.status == "warning"]),
    
            "failed": len([r for r in results if r.status == "fail"]),
    
            "overall_status": "healthy" if all(r.status in ["pass", "warning"] for r in results) else "unhealthy"
    
        }
    
        
    
        return {
    
            "summary": summary,
    
            "results": [
    
                {
    
                    "check_name": r.check_name,
    
                    "status": r.status,
    
                    "message": r.message,
    
                    "details": r.details,
    
                    "remediation": r.remediation
    
                }
    
                for r in results
    
            ],
    
            "timestamp": time.time()
    
        }
    
    

    ์šด์˜ ๋Ÿฐ๋ถ

    
    # operational-runbooks.yml
    
    runbooks:
    
      
    
      database_connection_failure:
    
        title: "Database Connection Failure"
    
        description: "Steps to resolve database connectivity issues"
    
        severity: "critical"
    
        steps:
    
          - name: "Check database server status"
    
            action: "Verify PostgreSQL service is running"
    
            commands:
    
              - "docker-compose ps postgres"
    
              - "docker-compose logs postgres"
    
          
    
          - name: "Test network connectivity"
    
            action: "Verify network connection to database"
    
            commands:
    
              - "telnet postgres-host 5432"
    
              - "nslookup postgres-host"
    
          
    
          - name: "Check connection pool"
    
            action: "Verify connection pool status"
    
            commands:
    
              - "curl http://localhost:8000/health/detailed"
    
          
    
          - name: "Restart services"
    
            action: "Restart MCP server and database if needed"
    
            commands:
    
              - "docker-compose restart"
    
        
    
        escalation:
    
          - "If issue persists, contact database administrator"
    
          - "Check for infrastructure issues in Azure portal"
    
    
    
      high_error_rate:
    
        title: "High Error Rate Detected"
    
        description: "Steps to investigate and resolve high error rates"
    
        severity: "high"
    
        steps:
    
          - name: "Check recent logs"
    
            action: "Review error logs for patterns"
    
            commands:
    
              - "docker-compose logs mcp_server | grep ERROR | tail -50"
    
          
    
          - name: "Analyze error types"
    
            action: "Categorize errors by type and frequency"
    
            api_endpoint: "/dashboard/diagnostics"
    
          
    
          - name: "Check system resources"
    
            action: "Verify system is not under resource pressure"
    
            commands:
    
              - "curl http://localhost:8000/health/detailed"
    
          
    
          - name: "Review recent deployments"
    
            action: "Check if errors started after recent deployment"
    
            
    
          - name: "Enable debug logging"
    
            action: "Temporarily increase log level for detailed diagnostics"
    
            environment_variable: "LOG_LEVEL=DEBUG"
    
    
    
      slow_performance:
    
        title: "Slow Query Performance"
    
        description: "Steps to diagnose and improve query performance"
    
        severity: "medium"
    
        steps:
    
          - name: "Identify slow queries"
    
            action: "Find queries taking longer than normal"
    
            sql_query: "SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10"
    
          
    
          - name: "Check database indexes"
    
            action: "Verify proper indexes exist"
    
            sql_query: "SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname = 'retail'"
    
          
    
          - name: "Analyze query plans"
    
            action: "Review execution plans for slow queries"
    
            sql_command: "EXPLAIN ANALYZE"
    
          
    
          - name: "Check connection pool"
    
            action: "Verify connection pool is not exhausted"
    
            api_endpoint: "/health/detailed"
    
          
    
          - name: "Monitor resource usage"
    
            action: "Check CPU and memory during queries"
    
            commands:
    
              - "top -p $(pgrep postgres)"
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… Application Insights ํ†ตํ•ฉ: ์™„์ „ํ•œ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •

    โœ… ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…: ์ƒ๊ด€๊ด€๊ณ„์™€ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ฐ–์ถ˜ ํ”„๋กœ๋•์…˜ ์ค€๋น„ ๋กœ๊น…

    โœ… ์‚ฌ์šฉ์ž ์ •์˜ ๋ฉ”ํŠธ๋ฆญ: ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ ๊ธฐ์ˆ  ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋ฐ ๋ถ„์„

    โœ… ์ง€๋Šฅํ˜• ์•Œ๋ฆผ: ์—ฌ๋Ÿฌ ์•Œ๋ฆผ ์ฑ„๋„์„ ํ†ตํ•œ ์‚ฌ์ „ ์•Œ๋ฆผ

    โœ… ์šด์˜ ๋Œ€์‹œ๋ณด๋“œ: ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ์ธ์‚ฌ์ดํŠธ

    โœ… ๋ฌธ์ œ ํ•ด๊ฒฐ ์›Œํฌํ”Œ๋กœ์šฐ: ์ž๋™ํ™”๋œ ์ง„๋‹จ ๋ฐ ์šด์˜ ๋Ÿฐ๋ถ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต 12: ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ตœ์ ํ™”๋ฅผ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ์ˆ  ์ ์šฉ
  • ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ๊ฐ•ํ™” ๊ตฌํ˜„
  • ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ํ•™์Šต
  • ๋น„์šฉ ์ตœ์ ํ™” ์ „๋žต ์ˆ˜๋ฆฝ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    Azure Monitor

  • Application Insights ๋ฌธ์„œ - ์™„์ „ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ€์ด๋“œ
  • KQL ์ฟผ๋ฆฌ ์ฐธ์กฐ - Application Insights์šฉ ์ฟผ๋ฆฌ ์–ธ์–ด
  • Azure Monitor Workbooks - ์‚ฌ์šฉ์ž ์ •์˜ ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ
  • OpenTelemetry

  • OpenTelemetry Python - ๊ณ„์ธก ๊ฐ€์ด๋“œ
  • ๋ถ„์‚ฐ ํŠธ๋ ˆ์ด์‹ฑ - ํŠธ๋ ˆ์ด์‹ฑ ๊ฐœ๋…
  • ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ - ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ์šด์˜ ์šฐ์ˆ˜์„ฑ

  • SRE ํ•ธ๋“œ๋ถ - ์‚ฌ์ดํŠธ ์•ˆ์ •์„ฑ ์—”์ง€๋‹ˆ์–ด๋ง ์›์น™
  • ๋ชจ๋‹ˆํ„ฐ๋ง ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ์—…๊ณ„ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ์‚ฌ๊ฑด ๋Œ€์‘ - ์‚ฌ๊ฑด ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ
  • ---

    ์ด์ „: ์‹ค์Šต 10: ๋ฐฐํฌ ์ „๋žต

    ๋‹ค์Œ: ์‹ค์Šต 12: ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ตœ์ ํ™”

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    Application Insights, ๋กœ๊น…, ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ชจ๋‹ˆํ„ฐ๋งํ•˜๊ธฐ

    ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์‹ค์Šต์€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ MCP ์„œ๋ฒ„๋ฅผ ์œ„ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง, ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ, ์•Œ๋ฆผ ๊ตฌํ˜„์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ์ง€์นจ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. Application Insights ์„ค์ •, ์œ ์˜๋ฏธํ•œ ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ, ํšจ๊ณผ์ ์ธ ์•Œ๋ฆผ ๊ตฌํ˜„, ์šด์˜ ์šฐ์ˆ˜์„ฑ์„ ์œ„ํ•œ ๋ฌธ์ œ ํ•ด๊ฒฐ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ํšจ๊ณผ์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง๊ณผ ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ์€ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ MCP ์„œ๋ฒ„์˜ ์•ˆ์ •์„ฑ์„ ์œ ์ง€ํ•˜๋Š” ๋ฐ ํ•„์ˆ˜์ ์ž…๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ์˜ ์„ธ ๊ฐ€์ง€ ํ•ต์‹ฌ ์š”์†Œโ€”๋ฉ”ํŠธ๋ฆญ, ๋กœ๊ทธ, ํŠธ๋ ˆ์ด์Šคโ€”๋ฅผ ๋‹ค๋ฃจ๋ฉฐ, ๋ฌธ์ œ๋ฅผ ์‚ฌ์ „์— ๊ฐ์ง€ํ•˜๊ณ  ์‹ ์†ํ•˜๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋Š” ํฌ๊ด„์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ์›์‹œ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ์Šคํ…œ ๋™์ž‘์„ ์ดํ•ดํ•˜๊ณ  ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•˜๋ฉฐ ๋†’์€ ๊ฐ€์šฉ์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๋Š” ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์ธ์‚ฌ์ดํŠธ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์šฐ๊ฒŒ ๋ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • MCP ์„œ๋ฒ„๋ฅผ ์œ„ํ•œ ํฌ๊ด„์ ์ธ Application Insights ํ†ตํ•ฉ ๊ตฌํ˜„
  • ํšจ๊ณผ์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ์„ ์œ„ํ•œ ๊ตฌ์กฐํ™”๋œ ๋กœ๊น… ํŒจํ„ด ์„ค๊ณ„
  • ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋ฐ ๋ถ„์„ ์‹œ์Šคํ…œ ์ƒ์„ฑ
  • ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ์•Œ๋ฆผ์„ ์ œ๊ณตํ•˜๋Š” ์ง€๋Šฅํ˜• ์•Œ๋ฆผ ๊ตฌ์„ฑ
  • ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ ์šด์˜ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌ์ถ•
  • ํšจ๊ณผ์ ์ธ ๋ฌธ์ œ ํ•ด๊ฒฐ ์›Œํฌํ”Œ๋กœ์šฐ ๋ฐ ๋Ÿฐ๋ถ ์ˆ˜๋ฆฝ
  • ๐Ÿ“Š Application Insights ํ†ตํ•ฉ

    Application Insights ์„ค์ •

    
    # mcp_server/monitoring.py
    
    """
    
    Comprehensive monitoring and telemetry for MCP server.
    
    """
    
    import logging
    
    import time
    
    import psutil
    
    from typing import Dict, Any, Optional
    
    from contextlib import contextmanager
    
    from azure.monitor.opentelemetry import configure_azure_monitor
    
    from opentelemetry import trace, metrics
    
    from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
    
    from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
    
    from opentelemetry.instrumentation.requests import RequestsInstrumentor
    
    
    
    class MCPTelemetryManager:
    
        """Comprehensive telemetry management for MCP server."""
    
        
    
        def __init__(self, connection_string: str):
    
            self.connection_string = connection_string
    
            self.tracer = None
    
            self.meter = None
    
            self.custom_metrics = {}
    
            
    
        def initialize_telemetry(self, app):
    
            """Initialize Application Insights and OpenTelemetry."""
    
            
    
            # Configure Azure Monitor
    
            configure_azure_monitor(
    
                connection_string=self.connection_string,
    
                logger_name="mcp_server",
    
                disable_offline_storage=False
    
            )
    
            
    
            # Get tracer and meter
    
            self.tracer = trace.get_tracer(__name__)
    
            self.meter = metrics.get_meter(__name__)
    
            
    
            # Initialize custom metrics
    
            self._setup_custom_metrics()
    
            
    
            # Instrument FastAPI
    
            FastAPIInstrumentor.instrument_app(app)
    
            
    
            # Instrument database
    
            AsyncPGInstrumentor().instrument()
    
            
    
            # Instrument HTTP requests
    
            RequestsInstrumentor().instrument()
    
            
    
            logging.info("Telemetry initialization complete")
    
        
    
        def _setup_custom_metrics(self):
    
            """Set up custom metrics for MCP server operations."""
    
            
    
            self.custom_metrics = {
    
                # Request metrics
    
                "mcp_requests_total": self.meter.create_counter(
    
                    name="mcp_requests_total",
    
                    description="Total number of MCP requests",
    
                    unit="1"
    
                ),
    
                
    
                "mcp_request_duration": self.meter.create_histogram(
    
                    name="mcp_request_duration_seconds",
    
                    description="MCP request duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                # Database metrics
    
                "database_queries_total": self.meter.create_counter(
    
                    name="database_queries_total",
    
                    description="Total database queries executed",
    
                    unit="1"
    
                ),
    
                
    
                "database_query_duration": self.meter.create_histogram(
    
                    name="database_query_duration_seconds",
    
                    description="Database query duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                "database_connections_active": self.meter.create_up_down_counter(
    
                    name="database_connections_active",
    
                    description="Number of active database connections",
    
                    unit="1"
    
                ),
    
                
    
                # Tool metrics
    
                "tool_executions_total": self.meter.create_counter(
    
                    name="tool_executions_total",
    
                    description="Total tool executions",
    
                    unit="1"
    
                ),
    
                
    
                "tool_execution_duration": self.meter.create_histogram(
    
                    name="tool_execution_duration_seconds",
    
                    description="Tool execution duration in seconds",
    
                    unit="s"
    
                ),
    
                
    
                # System metrics
    
                "system_cpu_usage": self.meter.create_gauge(
    
                    name="system_cpu_usage_percent",
    
                    description="System CPU usage percentage",
    
                    unit="%"
    
                ),
    
                
    
                "system_memory_usage": self.meter.create_gauge(
    
                    name="system_memory_usage_bytes",
    
                    description="System memory usage in bytes",
    
                    unit="byte"
    
                ),
    
                
    
                # Error metrics
    
                "errors_total": self.meter.create_counter(
    
                    name="errors_total",
    
                    description="Total number of errors",
    
                    unit="1"
    
                )
    
            }
    
        
    
        @contextmanager
    
        def trace_operation(self, operation_name: str, attributes: Dict[str, Any] = None):
    
            """Create a traced operation with automatic metrics collection."""
    
            
    
            with self.tracer.start_as_current_span(operation_name) as span:
    
                start_time = time.time()
    
                
    
                # Add attributes to span
    
                if attributes:
    
                    for key, value in attributes.items():
    
                        span.set_attribute(key, value)
    
                
    
                try:
    
                    yield span
    
                    
    
                    # Record success metrics
    
                    duration = time.time() - start_time
    
                    
    
                    if "request" in operation_name.lower():
    
                        self.custom_metrics["mcp_requests_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["mcp_request_duration"].record(duration)
    
                    
    
                    elif "query" in operation_name.lower():
    
                        self.custom_metrics["database_queries_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["database_query_duration"].record(duration)
    
                    
    
                    elif "tool" in operation_name.lower():
    
                        self.custom_metrics["tool_executions_total"].add(1, {"status": "success"})
    
                        self.custom_metrics["tool_execution_duration"].record(duration)
    
                    
    
                except Exception as e:
    
                    # Record error
    
                    span.record_exception(e)
    
                    span.set_status(trace.Status(trace.StatusCode.ERROR, str(e)))
    
                    
    
                    # Record error metrics
    
                    self.custom_metrics["errors_total"].add(1, {
    
                        "operation": operation_name,
    
                        "error_type": type(e).__name__
    
                    })
    
                    
    
                    raise
    
        
    
        def record_system_metrics(self):
    
            """Record system-level metrics."""
    
            
    
            # CPU usage
    
            cpu_percent = psutil.cpu_percent(interval=1)
    
            self.custom_metrics["system_cpu_usage"].set(cpu_percent)
    
            
    
            # Memory usage
    
            memory = psutil.virtual_memory()
    
            self.custom_metrics["system_memory_usage"].set(memory.used)
    
            
    
            # Database connections (if available)
    
            if hasattr(db_provider, 'connection_pool') and db_provider.connection_pool:
    
                active_connections = db_provider.connection_pool.get_size()
    
                self.custom_metrics["database_connections_active"].add(active_connections)
    
    
    
    # Global telemetry manager
    
    telemetry_manager = MCPTelemetryManager(
    
        connection_string=config.server.applicationinsights_connection_string
    
    )
    
    

    ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋กœ ๋กœ๊น… ๊ฐ•ํ™”

    
    # mcp_server/logging_config.py
    
    """
    
    Structured logging configuration for MCP server.
    
    """
    
    import logging
    
    import json
    
    import sys
    
    from datetime import datetime
    
    from typing import Dict, Any
    
    import traceback
    
    
    
    class StructuredFormatter(logging.Formatter):
    
        """Custom formatter for structured JSON logging."""
    
        
    
        def format(self, record: logging.LogRecord) -> str:
    
            """Format log record as structured JSON."""
    
            
    
            # Base log structure
    
            log_entry = {
    
                "timestamp": datetime.utcnow().isoformat() + "Z",
    
                "level": record.levelname,
    
                "logger": record.name,
    
                "message": record.getMessage(),
    
                "module": record.module,
    
                "function": record.funcName,
    
                "line": record.lineno
    
            }
    
            
    
            # Add exception information if present
    
            if record.exc_info:
    
                log_entry["exception"] = {
    
                    "type": record.exc_info[0].__name__,
    
                    "message": str(record.exc_info[1]),
    
                    "traceback": traceback.format_exception(*record.exc_info)
    
                }
    
            
    
            # Add custom attributes from extra
    
            if hasattr(record, 'extra_data'):
    
                log_entry.update(record.extra_data)
    
            
    
            # Add correlation ID if available
    
            if hasattr(record, 'correlation_id'):
    
                log_entry["correlation_id"] = record.correlation_id
    
            
    
            # Add user context if available
    
            if hasattr(record, 'user_id'):
    
                log_entry["user_id"] = record.user_id
    
            
    
            if hasattr(record, 'rls_user_id'):
    
                log_entry["rls_user_id"] = record.rls_user_id
    
            
    
            return json.dumps(log_entry, ensure_ascii=False)
    
    
    
    class MCPLogger:
    
        """Enhanced logging utilities for MCP server."""
    
        
    
        def __init__(self, name: str):
    
            self.logger = logging.getLogger(name)
    
            self._setup_structured_logging()
    
        
    
        def _setup_structured_logging(self):
    
            """Configure structured logging."""
    
            
    
            # Remove existing handlers
    
            for handler in self.logger.handlers[:]:
    
                self.logger.removeHandler(handler)
    
            
    
            # Create structured handler
    
            handler = logging.StreamHandler(sys.stdout)
    
            handler.setFormatter(StructuredFormatter())
    
            
    
            self.logger.addHandler(handler)
    
            self.logger.setLevel(logging.INFO)
    
        
    
        def log_mcp_request(
    
            self, 
    
            method: str, 
    
            user_id: str, 
    
            rls_user_id: str,
    
            duration: float = None,
    
            status: str = "success",
    
            **kwargs
    
        ):
    
            """Log MCP request with structured data."""
    
            
    
            extra_data = {
    
                "event_type": "mcp_request",
    
                "method": method,
    
                "user_id": user_id,
    
                "rls_user_id": rls_user_id,
    
                "status": status
    
            }
    
            
    
            if duration is not None:
    
                extra_data["duration_ms"] = duration * 1000
    
            
    
            extra_data.update(kwargs)
    
            
    
            self.logger.info(
    
                f"MCP request: {method} - {status}",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_database_query(
    
            self,
    
            query: str,
    
            duration: float,
    
            row_count: int = None,
    
            user_id: str = None,
    
            **kwargs
    
        ):
    
            """Log database query with performance data."""
    
            
    
            extra_data = {
    
                "event_type": "database_query",
    
                "query_hash": hash(query.strip()),
    
                "duration_ms": duration * 1000,
    
                "query_preview": query[:100] + "..." if len(query) > 100 else query
    
            }
    
            
    
            if row_count is not None:
    
                extra_data["row_count"] = row_count
    
            
    
            if user_id:
    
                extra_data["user_id"] = user_id
    
            
    
            extra_data.update(kwargs)
    
            
    
            level = logging.WARNING if duration > 1.0 else logging.INFO
    
            
    
            self.logger.log(
    
                level,
    
                f"Database query executed ({duration*1000:.2f}ms)",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_security_event(
    
            self,
    
            event_type: str,
    
            user_id: str = None,
    
            ip_address: str = None,
    
            success: bool = True,
    
            details: Dict[str, Any] = None
    
        ):
    
            """Log security-related events."""
    
            
    
            extra_data = {
    
                "event_type": "security_event",
    
                "security_event_type": event_type,
    
                "success": success
    
            }
    
            
    
            if user_id:
    
                extra_data["user_id"] = user_id
    
            
    
            if ip_address:
    
                extra_data["ip_address"] = ip_address
    
            
    
            if details:
    
                extra_data["details"] = details
    
            
    
            level = logging.INFO if success else logging.WARNING
    
            
    
            self.logger.log(
    
                level,
    
                f"Security event: {event_type} - {'success' if success else 'failure'}",
    
                extra={"extra_data": extra_data}
    
            )
    
        
    
        def log_performance_metric(
    
            self,
    
            metric_name: str,
    
            value: float,
    
            unit: str = "count",
    
            dimensions: Dict[str, str] = None
    
        ):
    
            """Log custom performance metrics."""
    
            
    
            extra_data = {
    
                "event_type": "performance_metric",
    
                "metric_name": metric_name,
    
                "value": value,
    
                "unit": unit
    
            }
    
            
    
            if dimensions:
    
                extra_data["dimensions"] = dimensions
    
            
    
            self.logger.info(
    
                f"Performance metric: {metric_name} = {value} {unit}",
    
                extra={"extra_data": extra_data}
    
            )
    
    
    
    # Global logger instance
    
    mcp_logger = MCPLogger("mcp_server")
    
    

    ์‚ฌ์šฉ์ž ์ •์˜ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘

    
    # mcp_server/metrics_collector.py
    
    """
    
    Custom metrics collection for business and operational insights.
    
    """
    
    import asyncio
    
    import time
    
    from typing import Dict, Any, List
    
    from dataclasses import dataclass
    
    from collections import defaultdict, deque
    
    import statistics
    
    
    
    @dataclass
    
    class MetricPoint:
    
        """Individual metric data point."""
    
        timestamp: float
    
        value: float
    
        dimensions: Dict[str, str]
    
    
    
    class MetricsCollector:
    
        """Advanced metrics collection and analysis."""
    
        
    
        def __init__(self, retention_minutes: int = 60):
    
            self.retention_seconds = retention_minutes * 60
    
            self.metrics_buffer = defaultdict(lambda: deque(maxlen=1000))
    
            self.aggregated_metrics = {}
    
            
    
        def record_metric(
    
            self,
    
            name: str,
    
            value: float,
    
            dimensions: Dict[str, str] = None
    
        ):
    
            """Record a metric point."""
    
            
    
            metric_point = MetricPoint(
    
                timestamp=time.time(),
    
                value=value,
    
                dimensions=dimensions or {}
    
            )
    
            
    
            self.metrics_buffer[name].append(metric_point)
    
            self._cleanup_old_metrics(name)
    
        
    
        def _cleanup_old_metrics(self, metric_name: str):
    
            """Remove metrics older than retention period."""
    
            
    
            cutoff_time = time.time() - self.retention_seconds
    
            buffer = self.metrics_buffer[metric_name]
    
            
    
            while buffer and buffer[0].timestamp < cutoff_time:
    
                buffer.popleft()
    
        
    
        def get_metric_summary(
    
            self,
    
            name: str,
    
            time_window_minutes: int = 5
    
        ) -> Dict[str, Any]:
    
            """Get statistical summary of a metric."""
    
            
    
            time_window_seconds = time_window_minutes * 60
    
            cutoff_time = time.time() - time_window_seconds
    
            
    
            relevant_points = [
    
                point for point in self.metrics_buffer[name]
    
                if point.timestamp >= cutoff_time
    
            ]
    
            
    
            if not relevant_points:
    
                return {"error": "No data available"}
    
            
    
            values = [point.value for point in relevant_points]
    
            
    
            return {
    
                "count": len(values),
    
                "min": min(values),
    
                "max": max(values),
    
                "mean": statistics.mean(values),
    
                "median": statistics.median(values),
    
                "p95": self._percentile(values, 95),
    
                "p99": self._percentile(values, 99),
    
                "time_window_minutes": time_window_minutes
    
            }
    
        
    
        def _percentile(self, values: List[float], percentile: float) -> float:
    
            """Calculate percentile value."""
    
            if not values:
    
                return 0
    
            
    
            sorted_values = sorted(values)
    
            index = int((percentile / 100) * len(sorted_values))
    
            index = min(index, len(sorted_values) - 1)
    
            
    
            return sorted_values[index]
    
        
    
        async def collect_business_metrics(self):
    
            """Collect business-specific metrics."""
    
            
    
            try:
    
                # Query execution patterns
    
                query_types = await self._analyze_query_patterns()
    
                for query_type, count in query_types.items():
    
                    self.record_metric(
    
                        "business_queries_by_type",
    
                        count,
    
                        {"query_type": query_type}
    
                    )
    
                
    
                # User activity patterns
    
                user_activity = await self._analyze_user_activity()
    
                for store_id, activity_count in user_activity.items():
    
                    self.record_metric(
    
                        "user_activity_by_store",
    
                        activity_count,
    
                        {"store_id": store_id}
    
                    )
    
                
    
                # Tool usage patterns
    
                tool_usage = await self._analyze_tool_usage()
    
                for tool_name, usage_count in tool_usage.items():
    
                    self.record_metric(
    
                        "tool_usage",
    
                        usage_count,
    
                        {"tool_name": tool_name}
    
                    )
    
                    
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Business metrics collection failed: {e}")
    
        
    
        async def _analyze_query_patterns(self) -> Dict[str, int]:
    
            """Analyze database query patterns."""
    
            
    
            # This would analyze actual query logs
    
            # For demo purposes, returning sample data
    
            return {
    
                "sales_analysis": 45,
    
                "inventory_check": 23,
    
                "customer_lookup": 18,
    
                "product_search": 31
    
            }
    
        
    
        async def _analyze_user_activity(self) -> Dict[str, int]:
    
            """Analyze user activity by store."""
    
            
    
            # This would analyze actual user activity logs
    
            return {
    
                "seattle": 67,
    
                "redmond": 34,
    
                "bellevue": 23,
    
                "online": 89
    
            }
    
        
    
        async def _analyze_tool_usage(self) -> Dict[str, int]:
    
            """Analyze MCP tool usage patterns."""
    
            
    
            return {
    
                "execute_sales_query": 156,
    
                "get_multiple_table_schemas": 45,
    
                "semantic_search_products": 78,
    
                "get_current_utc_date": 23
    
            }
    
    
    
    # Global metrics collector
    
    metrics_collector = MetricsCollector()
    
    

    ๐Ÿ”” ์•Œ๋ฆผ ๊ตฌ์„ฑ

    ์ง€๋Šฅํ˜• ์•Œ๋ฆผ ์‹œ์Šคํ…œ

    
    # mcp_server/alerting.py
    
    """
    
    Intelligent alerting system for MCP server operations.
    
    """
    
    import asyncio
    
    import json
    
    from typing import Dict, List, Any, Callable
    
    from enum import Enum
    
    from dataclasses import dataclass
    
    from azure.communication.email import EmailClient
    
    import smtplib
    
    from email.mime.text import MIMEText
    
    from email.mime.multipart import MIMEMultipart
    
    
    
    class AlertSeverity(Enum):
    
        LOW = "low"
    
        MEDIUM = "medium"
    
        HIGH = "high"
    
        CRITICAL = "critical"
    
    
    
    @dataclass
    
    class AlertRule:
    
        """Alert rule configuration."""
    
        name: str
    
        condition: Callable[[Dict[str, Any]], bool]
    
        severity: AlertSeverity
    
        cooldown_minutes: int
    
        message_template: str
    
        enabled: bool = True
    
    
    
    @dataclass
    
    class Alert:
    
        """Alert instance."""
    
        rule_name: str
    
        severity: AlertSeverity
    
        message: str
    
        timestamp: float
    
        details: Dict[str, Any]
    
        acknowledged: bool = False
    
    
    
    class AlertManager:
    
        """Comprehensive alerting management."""
    
        
    
        def __init__(self):
    
            self.alert_rules = {}
    
            self.active_alerts = {}
    
            self.alert_history = deque(maxlen=1000)
    
            self.notification_channels = {}
    
            self._setup_default_rules()
    
            self._setup_notification_channels()
    
        
    
        def _setup_default_rules(self):
    
            """Set up default alert rules."""
    
            
    
            # Database connection issues
    
            self.add_alert_rule(AlertRule(
    
                name="database_connection_failure",
    
                condition=lambda metrics: metrics.get("database_status") != "healthy",
    
                severity=AlertSeverity.CRITICAL,
    
                cooldown_minutes=5,
    
                message_template="Database connection failure detected. Service may be unavailable."
    
            ))
    
            
    
            # High error rate
    
            self.add_alert_rule(AlertRule(
    
                name="high_error_rate",
    
                condition=lambda metrics: metrics.get("error_rate", 0) > 0.05,  # 5% error rate
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=10,
    
                message_template="High error rate detected: {error_rate:.2%}. Investigate immediately."
    
            ))
    
            
    
            # Slow query performance
    
            self.add_alert_rule(AlertRule(
    
                name="slow_query_performance",
    
                condition=lambda metrics: metrics.get("avg_query_duration", 0) > 2.0,  # 2 seconds
    
                severity=AlertSeverity.MEDIUM,
    
                cooldown_minutes=15,
    
                message_template="Slow query performance detected. Average duration: {avg_query_duration:.2f}s"
    
            ))
    
            
    
            # High CPU usage
    
            self.add_alert_rule(AlertRule(
    
                name="high_cpu_usage",
    
                condition=lambda metrics: metrics.get("cpu_usage", 0) > 85,  # 85% CPU
    
                severity=AlertSeverity.MEDIUM,
    
                cooldown_minutes=10,
    
                message_template="High CPU usage detected: {cpu_usage:.1f}%"
    
            ))
    
            
    
            # Memory usage
    
            self.add_alert_rule(AlertRule(
    
                name="high_memory_usage",
    
                condition=lambda metrics: metrics.get("memory_usage_percent", 0) > 90,  # 90% memory
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=5,
    
                message_template="High memory usage detected: {memory_usage_percent:.1f}%"
    
            ))
    
            
    
            # Authentication failures
    
            self.add_alert_rule(AlertRule(
    
                name="authentication_failures",
    
                condition=lambda metrics: metrics.get("auth_failure_rate", 0) > 0.1,  # 10% failure rate
    
                severity=AlertSeverity.HIGH,
    
                cooldown_minutes=5,
    
                message_template="High authentication failure rate: {auth_failure_rate:.2%}. Possible security incident."
    
            ))
    
        
    
        def _setup_notification_channels(self):
    
            """Set up notification channels."""
    
            
    
            # Email notifications
    
            email_config = {
    
                "smtp_server": os.getenv("SMTP_SERVER", "smtp.office365.com"),
    
                "smtp_port": int(os.getenv("SMTP_PORT", "587")),
    
                "username": os.getenv("SMTP_USERNAME"),
    
                "password": os.getenv("SMTP_PASSWORD"),
    
                "from_address": os.getenv("ALERT_FROM_EMAIL"),
    
                "to_addresses": os.getenv("ALERT_TO_EMAILS", "").split(",")
    
            }
    
            
    
            if email_config["username"] and email_config["password"]:
    
                self.notification_channels["email"] = EmailNotifier(email_config)
    
            
    
            # Microsoft Teams webhook
    
            teams_webhook = os.getenv("TEAMS_WEBHOOK_URL")
    
            if teams_webhook:
    
                self.notification_channels["teams"] = TeamsNotifier(teams_webhook)
    
            
    
            # Slack webhook
    
            slack_webhook = os.getenv("SLACK_WEBHOOK_URL")
    
            if slack_webhook:
    
                self.notification_channels["slack"] = SlackNotifier(slack_webhook)
    
        
    
        def add_alert_rule(self, rule: AlertRule):
    
            """Add or update an alert rule."""
    
            self.alert_rules[rule.name] = rule
    
        
    
        async def evaluate_metrics(self, metrics: Dict[str, Any]):
    
            """Evaluate metrics against alert rules."""
    
            
    
            for rule_name, rule in self.alert_rules.items():
    
                if not rule.enabled:
    
                    continue
    
                
    
                try:
    
                    # Check if rule condition is met
    
                    if rule.condition(metrics):
    
                        await self._trigger_alert(rule, metrics)
    
                    else:
    
                        # Clear alert if condition no longer met
    
                        await self._clear_alert(rule_name)
    
                        
    
                except Exception as e:
    
                    mcp_logger.logger.error(f"Error evaluating alert rule {rule_name}: {e}")
    
        
    
        async def _trigger_alert(self, rule: AlertRule, metrics: Dict[str, Any]):
    
            """Trigger an alert."""
    
            
    
            current_time = time.time()
    
            
    
            # Check cooldown period
    
            if rule.name in self.active_alerts:
    
                last_alert_time = self.active_alerts[rule.name].timestamp
    
                if current_time - last_alert_time < rule.cooldown_minutes * 60:
    
                    return  # Still in cooldown
    
            
    
            # Format alert message
    
            message = rule.message_template.format(**metrics)
    
            
    
            # Create alert
    
            alert = Alert(
    
                rule_name=rule.name,
    
                severity=rule.severity,
    
                message=message,
    
                timestamp=current_time,
    
                details=metrics.copy()
    
            )
    
            
    
            # Store alert
    
            self.active_alerts[rule.name] = alert
    
            self.alert_history.append(alert)
    
            
    
            # Send notifications
    
            await self._send_notifications(alert)
    
            
    
            mcp_logger.log_security_event(
    
                "alert_triggered",
    
                details={
    
                    "rule_name": rule.name,
    
                    "severity": rule.severity.value,
    
                    "message": message
    
                }
    
            )
    
        
    
        async def _clear_alert(self, rule_name: str):
    
            """Clear an active alert."""
    
            
    
            if rule_name in self.active_alerts:
    
                alert = self.active_alerts[rule_name]
    
                del self.active_alerts[rule_name]
    
                
    
                # Send resolution notification for high/critical alerts
    
                if alert.severity in [AlertSeverity.HIGH, AlertSeverity.CRITICAL]:
    
                    resolution_alert = Alert(
    
                        rule_name=rule_name,
    
                        severity=AlertSeverity.LOW,
    
                        message=f"RESOLVED: {alert.message}",
    
                        timestamp=time.time(),
    
                        details={"resolution": True}
    
                    )
    
                    
    
                    await self._send_notifications(resolution_alert)
    
        
    
        async def _send_notifications(self, alert: Alert):
    
            """Send alert notifications through all configured channels."""
    
            
    
            tasks = []
    
            
    
            for channel_name, notifier in self.notification_channels.items():
    
                task = asyncio.create_task(
    
                    notifier.send_notification(alert),
    
                    name=f"notify_{channel_name}"
    
                )
    
                tasks.append(task)
    
            
    
            if tasks:
    
                # Wait for all notifications with timeout
    
                try:
    
                    await asyncio.wait_for(
    
                        asyncio.gather(*tasks, return_exceptions=True),
    
                        timeout=30.0
    
                    )
    
                except asyncio.TimeoutError:
    
                    mcp_logger.logger.warning("Some alert notifications timed out")
    
    
    
    # Notification implementations
    
    class EmailNotifier:
    
        """Email notification handler."""
    
        
    
        def __init__(self, config: Dict[str, Any]):
    
            self.config = config
    
        
    
        async def send_notification(self, alert: Alert):
    
            """Send email notification."""
    
            
    
            try:
    
                msg = MIMEMultipart()
    
                msg['From'] = self.config['from_address']
    
                msg['To'] = ', '.join(self.config['to_addresses'])
    
                msg['Subject'] = f"[{alert.severity.value.upper()}] MCP Server Alert: {alert.rule_name}"
    
                
    
                body = f"""
    
    Alert Details:
    
    - Rule: {alert.rule_name}
    
    - Severity: {alert.severity.value.upper()}
    
    - Time: {datetime.fromtimestamp(alert.timestamp).isoformat()}
    
    - Message: {alert.message}
    
    
    
    Additional Details:
    
    {json.dumps(alert.details, indent=2)}
    
    
    
    This is an automated alert from the MCP Server monitoring system.
    
                """
    
                
    
                msg.attach(MIMEText(body, 'plain'))
    
                
    
                # Send email
    
                with smtplib.SMTP(self.config['smtp_server'], self.config['smtp_port']) as server:
    
                    server.starttls()
    
                    server.login(self.config['username'], self.config['password'])
    
                    server.send_message(msg)
    
                    
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Failed to send email notification: {e}")
    
    
    
    class TeamsNotifier:
    
        """Microsoft Teams notification handler."""
    
        
    
        def __init__(self, webhook_url: str):
    
            self.webhook_url = webhook_url
    
        
    
        async def send_notification(self, alert: Alert):
    
            """Send Teams notification."""
    
            
    
            color_map = {
    
                AlertSeverity.LOW: "28a745",     # Green
    
                AlertSeverity.MEDIUM: "ffc107",   # Yellow
    
                AlertSeverity.HIGH: "fd7e14",     # Orange
    
                AlertSeverity.CRITICAL: "dc3545"  # Red
    
            }
    
            
    
            payload = {
    
                "@type": "MessageCard",
    
                "@context": "http://schema.org/extensions",
    
                "themeColor": color_map.get(alert.severity, "0076D7"),
    
                "summary": f"MCP Server Alert: {alert.rule_name}",
    
                "sections": [{
    
                    "activityTitle": f"๐Ÿšจ {alert.severity.value.upper()} Alert",
    
                    "activitySubtitle": alert.rule_name,
    
                    "text": alert.message,
    
                    "facts": [
    
                        {"name": "Timestamp", "value": datetime.fromtimestamp(alert.timestamp).isoformat()},
    
                        {"name": "Severity", "value": alert.severity.value.upper()}
    
                    ]
    
                }]
    
            }
    
            
    
            try:
    
                async with aiohttp.ClientSession() as session:
    
                    async with session.post(self.webhook_url, json=payload) as response:
    
                        if response.status != 200:
    
                            raise Exception(f"Teams webhook returned {response.status}")
    
                            
    
            except Exception as e:
    
                mcp_logger.logger.error(f"Failed to send Teams notification: {e}")
    
    
    
    # Global alert manager
    
    alert_manager = AlertManager()
    
    

    ๐Ÿ“ˆ ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ

    Azure Monitor Workbooks

    
    {
    
      "version": "Notebook/1.0",
    
      "items": [
    
        {
    
          "type": 1,
    
          "content": {
    
            "json": "# MCP Server Operations Dashboard\n\nComprehensive monitoring dashboard for Zava Retail MCP Server operations, performance, and health metrics."
    
          },
    
          "name": "title"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart",
    
            "version": "KqlItem/1.0",
    
            "query": "requests\n| where timestamp >= ago(1h)\n| where name contains \"mcp\"\n| summarize RequestCount = count(), AvgDuration = avg(duration) by bin(timestamp, 5m)\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "MCP Request Volume and Performance",
    
            "timeContext": {
    
              "durationMs": 3600000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "timechart"
    
          },
    
          "name": "request-metrics"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-2",
    
            "version": "KqlItem/1.0",
    
            "query": "customMetrics\n| where name == \"database_query_duration_seconds\"\n| where timestamp >= ago(1h)\n| summarize \n    AvgDuration = avg(value),\n    P95Duration = percentile(value, 95),\n    P99Duration = percentile(value, 99)\n    by bin(timestamp, 5m)\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "Database Query Performance",
    
            "timeContext": {
    
              "durationMs": 3600000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "timechart"
    
          },
    
          "name": "database-performance"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-3",
    
            "version": "KqlItem/1.0",
    
            "query": "exceptions\n| where timestamp >= ago(24h)\n| where method contains \"mcp\"\n| summarize ErrorCount = count() by bin(timestamp, 1h), type\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "Error Rate Analysis",
    
            "timeContext": {
    
              "durationMs": 86400000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "barchart"
    
          },
    
          "name": "error-analysis"
    
        },
    
        {
    
          "type": 10,
    
          "content": {
    
            "chartId": "workbook-interactive-chart-4",
    
            "version": "KqlItem/1.0",
    
            "query": "customMetrics\n| where name in (\"system_cpu_usage_percent\", \"system_memory_usage_bytes\")\n| where timestamp >= ago(2h)\n| extend MetricType = case(\n    name == \"system_cpu_usage_percent\", \"CPU %\",\n    name == \"system_memory_usage_bytes\", \"Memory GB\",\n    \"Unknown\"\n)\n| extend NormalizedValue = case(\n    name == \"system_memory_usage_bytes\", value / (1024*1024*1024),\n    value\n)\n| summarize AvgValue = avg(NormalizedValue) by bin(timestamp, 5m), MetricType\n| order by timestamp asc",
    
            "size": 0,
    
            "title": "System Resource Usage",
    
            "timeContext": {
    
              "durationMs": 7200000
    
            },
    
            "queryType": 0,
    
            "resourceType": "microsoft.insights/components",
    
            "visualization": "linechart"
    
          },
    
          "name": "system-resources"
    
        }
    
      ],
    
      "isLocked": false,
    
      "fallbackResourceIds": [
    
        "/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/microsoft.insights/components/{app-insights-name}"
    
      ]
    
    }
    
    

    ์‚ฌ์šฉ์ž ์ •์˜ ๋Œ€์‹œ๋ณด๋“œ ๊ตฌํ˜„

    
    # mcp_server/dashboard.py
    
    """
    
    Custom dashboard data provider for MCP server metrics.
    
    """
    
    from typing import Dict, List, Any
    
    from fastapi import APIRouter, Depends
    
    from datetime import datetime, timedelta
    
    
    
    dashboard_router = APIRouter(prefix="/dashboard", tags=["dashboard"])
    
    
    
    class DashboardDataProvider:
    
        """Provide dashboard data from various sources."""
    
        
    
        def __init__(self):
    
            self.metrics_collector = metrics_collector
    
            self.alert_manager = alert_manager
    
        
    
        async def get_overview_metrics(self) -> Dict[str, Any]:
    
            """Get high-level overview metrics."""
    
            
    
            current_time = time.time()
    
            one_hour_ago = current_time - 3600
    
            
    
            return {
    
                "timestamp": current_time,
    
                "active_alerts": len(self.alert_manager.active_alerts),
    
                "critical_alerts": len([
    
                    alert for alert in self.alert_manager.active_alerts.values()
    
                    if alert.severity == AlertSeverity.CRITICAL
    
                ]),
    
                "requests_last_hour": await self._get_request_count(one_hour_ago),
    
                "avg_response_time": await self._get_avg_response_time(one_hour_ago),
    
                "error_rate": await self._get_error_rate(one_hour_ago),
    
                "database_status": await self._get_database_status(),
    
                "system_health": await self._get_system_health()
    
            }
    
        
    
        async def get_performance_trends(self, hours: int = 24) -> Dict[str, List[Dict]]:
    
            """Get performance trends over time."""
    
            
    
            end_time = time.time()
    
            start_time = end_time - (hours * 3600)
    
            
    
            # Generate hourly data points
    
            data_points = []
    
            current = start_time
    
            
    
            while current < end_time:
    
                hour_start = current
    
                hour_end = current + 3600
    
                
    
                data_points.append({
    
                    "timestamp": current,
    
                    "requests": await self._get_request_count_range(hour_start, hour_end),
    
                    "avg_duration": await self._get_avg_duration_range(hour_start, hour_end),
    
                    "error_count": await self._get_error_count_range(hour_start, hour_end),
    
                    "cpu_usage": await self._get_cpu_usage_range(hour_start, hour_end),
    
                    "memory_usage": await self._get_memory_usage_range(hour_start, hour_end)
    
                })
    
                
    
                current = hour_end
    
            
    
            return {
    
                "time_series": data_points,
    
                "period_hours": hours,
    
                "data_points": len(data_points)
    
            }
    
        
    
        async def get_business_insights(self) -> Dict[str, Any]:
    
            """Get business-specific insights."""
    
            
    
            return {
    
                "top_queries": await self._get_top_queries(),
    
                "store_activity": await self._get_store_activity(),
    
                "tool_usage": await self._get_tool_usage_stats(),
    
                "user_patterns": await self._get_user_patterns(),
    
                "peak_hours": await self._get_peak_hours()
    
            }
    
        
    
        async def _get_request_count(self, since_time: float) -> int:
    
            """Get request count since specified time."""
    
            summary = self.metrics_collector.get_metric_summary(
    
                "mcp_requests_total",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            return summary.get("count", 0)
    
        
    
        async def _get_avg_response_time(self, since_time: float) -> float:
    
            """Get average response time since specified time."""
    
            summary = self.metrics_collector.get_metric_summary(
    
                "mcp_request_duration_seconds",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            return summary.get("mean", 0.0) * 1000  # Convert to milliseconds
    
        
    
        async def _get_error_rate(self, since_time: float) -> float:
    
            """Calculate error rate since specified time."""
    
            
    
            total_requests = await self._get_request_count(since_time)
    
            error_summary = self.metrics_collector.get_metric_summary(
    
                "errors_total",
    
                time_window_minutes=int((time.time() - since_time) / 60)
    
            )
    
            error_count = error_summary.get("count", 0)
    
            
    
            if total_requests == 0:
    
                return 0.0
    
            
    
            return error_count / total_requests
    
        
    
        async def _get_database_status(self) -> str:
    
            """Get current database status."""
    
            try:
    
                health = await db_provider.health_check()
    
                return health.get("status", "unknown")
    
            except Exception:
    
                return "unhealthy"
    
        
    
        async def _get_system_health(self) -> Dict[str, Any]:
    
            """Get current system health metrics."""
    
            
    
            cpu_summary = self.metrics_collector.get_metric_summary("system_cpu_usage_percent", 5)
    
            memory_summary = self.metrics_collector.get_metric_summary("system_memory_usage_bytes", 5)
    
            
    
            return {
    
                "cpu_usage": cpu_summary.get("mean", 0),
    
                "memory_usage_gb": memory_summary.get("mean", 0) / (1024**3),
    
                "status": "healthy"  # Would implement actual health logic
    
            }
    
    
    
    # Dashboard API endpoints
    
    dashboard_provider = DashboardDataProvider()
    
    
    
    @dashboard_router.get("/overview")
    
    async def get_dashboard_overview():
    
        """Get dashboard overview data."""
    
        return await dashboard_provider.get_overview_metrics()
    
    
    
    @dashboard_router.get("/performance")
    
    async def get_performance_data(hours: int = 24):
    
        """Get performance trend data."""
    
        return await dashboard_provider.get_performance_trends(hours)
    
    
    
    @dashboard_router.get("/business")
    
    async def get_business_insights():
    
        """Get business insights data."""
    
        return await dashboard_provider.get_business_insights()
    
    
    
    @dashboard_router.get("/alerts")
    
    async def get_active_alerts():
    
        """Get active alerts."""
    
        return {
    
            "active_alerts": [
    
                {
    
                    "rule_name": alert.rule_name,
    
                    "severity": alert.severity.value,
    
                    "message": alert.message,
    
                    "timestamp": alert.timestamp,
    
                    "acknowledged": alert.acknowledged
    
                }
    
                for alert in alert_manager.active_alerts.values()
    
            ],
    
            "alert_count": len(alert_manager.active_alerts)
    
        }
    
    

    ๐Ÿ” ๋ฌธ์ œ ํ•ด๊ฒฐ ์›Œํฌํ”Œ๋กœ์šฐ

    ์ž๋™ํ™”๋œ ์ง„๋‹จ

    
    # mcp_server/diagnostics.py
    
    """
    
    Automated diagnostics and troubleshooting for MCP server.
    
    """
    
    import asyncio
    
    import subprocess
    
    from typing import Dict, List, Any, Optional
    
    from dataclasses import dataclass
    
    
    
    @dataclass
    
    class DiagnosticResult:
    
        """Result of a diagnostic check."""
    
        check_name: str
    
        status: str  # "pass", "fail", "warning"
    
        message: str
    
        details: Dict[str, Any]
    
        remediation: Optional[str] = None
    
    
    
    class DiagnosticsEngine:
    
        """Comprehensive diagnostics engine."""
    
        
    
        def __init__(self):
    
            self.diagnostic_checks = []
    
            self._register_default_checks()
    
        
    
        def _register_default_checks(self):
    
            """Register default diagnostic checks."""
    
            
    
            self.diagnostic_checks = [
    
                self._check_database_connectivity,
    
                self._check_azure_services,
    
                self._check_system_resources,
    
                self._check_configuration,
    
                self._check_network_connectivity,
    
                self._check_disk_space,
    
                self._check_log_files,
    
                self._check_security_status
    
            ]
    
        
    
        async def run_full_diagnostics(self) -> List[DiagnosticResult]:
    
            """Run all diagnostic checks."""
    
            
    
            results = []
    
            
    
            for check_func in self.diagnostic_checks:
    
                try:
    
                    result = await check_func()
    
                    results.append(result)
    
                except Exception as e:
    
                    results.append(DiagnosticResult(
    
                        check_name=check_func.__name__,
    
                        status="fail",
    
                        message=f"Diagnostic check failed: {str(e)}",
    
                        details={"exception": str(e)}
    
                    ))
    
            
    
            return results
    
        
    
        async def _check_database_connectivity(self) -> DiagnosticResult:
    
            """Check database connectivity and performance."""
    
            
    
            try:
    
                start_time = time.time()
    
                health = await db_provider.health_check()
    
                duration = time.time() - start_time
    
                
    
                if health["status"] == "healthy":
    
                    if duration > 1.0:
    
                        return DiagnosticResult(
    
                            check_name="database_connectivity",
    
                            status="warning",
    
                            message=f"Database responsive but slow ({duration:.2f}s)",
    
                            details=health,
    
                            remediation="Check database server load and network latency"
    
                        )
    
                    else:
    
                        return DiagnosticResult(
    
                            check_name="database_connectivity",
    
                            status="pass",
    
                            message=f"Database healthy ({duration:.2f}s response time)",
    
                            details=health
    
                        )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="database_connectivity",
    
                        status="fail",
    
                        message="Database not healthy",
    
                        details=health,
    
                        remediation="Check database server status and connection parameters"
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="database_connectivity",
    
                    status="fail",
    
                    message=f"Database connectivity failed: {str(e)}",
    
                    details={"error": str(e)},
    
                    remediation="Verify database server is running and connection parameters are correct"
    
                )
    
        
    
        async def _check_azure_services(self) -> DiagnosticResult:
    
            """Check Azure AI services connectivity."""
    
            
    
            try:
    
                # Test Azure OpenAI connectivity
    
                from azure.identity import DefaultAzureCredential
    
                from azure.ai.projects import AIProjectClient
    
                
    
                credential = DefaultAzureCredential()
    
                project_client = AIProjectClient(
    
                    endpoint=config.azure.project_endpoint,
    
                    credential=credential
    
                )
    
                
    
                # This would perform actual connectivity test
    
                # For now, just check configuration
    
                
    
                if config.azure.is_configured():
    
                    return DiagnosticResult(
    
                        check_name="azure_services",
    
                        status="pass",
    
                        message="Azure services configuration valid",
    
                        details={
    
                            "project_endpoint": config.azure.project_endpoint,
    
                            "openai_endpoint": config.azure.openai_endpoint
    
                        }
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="azure_services",
    
                        status="fail",
    
                        message="Azure services not properly configured",
    
                        details={"missing_config": "Check environment variables"},
    
                        remediation="Ensure all Azure configuration environment variables are set"
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="azure_services",
    
                    status="fail",
    
                    message=f"Azure services check failed: {str(e)}",
    
                    details={"error": str(e)},
    
                    remediation="Check Azure credentials and network connectivity"
    
                )
    
        
    
        async def _check_system_resources(self) -> DiagnosticResult:
    
            """Check system resource usage."""
    
            
    
            try:
    
                import psutil
    
                
    
                cpu_percent = psutil.cpu_percent(interval=1)
    
                memory = psutil.virtual_memory()
    
                disk = psutil.disk_usage('/')
    
                
    
                warnings = []
    
                
    
                if cpu_percent > 85:
    
                    warnings.append(f"High CPU usage: {cpu_percent:.1f}%")
    
                
    
                if memory.percent > 85:
    
                    warnings.append(f"High memory usage: {memory.percent:.1f}%")
    
                
    
                if disk.percent > 85:
    
                    warnings.append(f"High disk usage: {disk.percent:.1f}%")
    
                
    
                details = {
    
                    "cpu_percent": cpu_percent,
    
                    "memory_percent": memory.percent,
    
                    "memory_available_gb": memory.available / (1024**3),
    
                    "disk_percent": disk.percent,
    
                    "disk_free_gb": disk.free / (1024**3)
    
                }
    
                
    
                if warnings:
    
                    return DiagnosticResult(
    
                        check_name="system_resources",
    
                        status="warning",
    
                        message=f"Resource warnings: {'; '.join(warnings)}",
    
                        details=details,
    
                        remediation="Monitor resource usage and consider scaling"
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="system_resources",
    
                        status="pass",
    
                        message="System resources normal",
    
                        details=details
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="system_resources",
    
                    status="fail",
    
                    message=f"Resource check failed: {str(e)}",
    
                    details={"error": str(e)}
    
                )
    
        
    
        async def _check_configuration(self) -> DiagnosticResult:
    
            """Check configuration validity."""
    
            
    
            try:
    
                issues = []
    
                
    
                # Check required environment variables
    
                required_vars = [
    
                    "POSTGRES_HOST", "POSTGRES_PASSWORD",
    
                    "PROJECT_ENDPOINT", "AZURE_CLIENT_ID"
    
                ]
    
                
    
                for var in required_vars:
    
                    if not os.getenv(var):
    
                        issues.append(f"Missing environment variable: {var}")
    
                
    
                # Check configuration consistency
    
                if config.server.enable_health_check and not config.server.applicationinsights_connection_string:
    
                    issues.append("Health check enabled but Application Insights not configured")
    
                
    
                if issues:
    
                    return DiagnosticResult(
    
                        check_name="configuration",
    
                        status="fail",
    
                        message=f"Configuration issues: {'; '.join(issues)}",
    
                        details={"issues": issues},
    
                        remediation="Fix configuration issues and restart service"
    
                    )
    
                else:
    
                    return DiagnosticResult(
    
                        check_name="configuration",
    
                        status="pass",
    
                        message="Configuration valid",
    
                        details={"status": "all_checks_passed"}
    
                    )
    
                    
    
            except Exception as e:
    
                return DiagnosticResult(
    
                    check_name="configuration",
    
                    status="fail",
    
                    message=f"Configuration check failed: {str(e)}",
    
                    details={"error": str(e)}
    
                )
    
    
    
    # Diagnostic API endpoint
    
    @dashboard_router.get("/diagnostics")
    
    async def run_diagnostics():
    
        """Run comprehensive diagnostics."""
    
        
    
        diagnostics_engine = DiagnosticsEngine()
    
        results = await diagnostics_engine.run_full_diagnostics()
    
        
    
        # Summarize results
    
        summary = {
    
            "total_checks": len(results),
    
            "passed": len([r for r in results if r.status == "pass"]),
    
            "warnings": len([r for r in results if r.status == "warning"]),
    
            "failed": len([r for r in results if r.status == "fail"]),
    
            "overall_status": "healthy" if all(r.status in ["pass", "warning"] for r in results) else "unhealthy"
    
        }
    
        
    
        return {
    
            "summary": summary,
    
            "results": [
    
                {
    
                    "check_name": r.check_name,
    
                    "status": r.status,
    
                    "message": r.message,
    
                    "details": r.details,
    
                    "remediation": r.remediation
    
                }
    
                for r in results
    
            ],
    
            "timestamp": time.time()
    
        }
    
    

    ์šด์˜ ๋Ÿฐ๋ถ

    
    # operational-runbooks.yml
    
    runbooks:
    
      
    
      database_connection_failure:
    
        title: "Database Connection Failure"
    
        description: "Steps to resolve database connectivity issues"
    
        severity: "critical"
    
        steps:
    
          - name: "Check database server status"
    
            action: "Verify PostgreSQL service is running"
    
            commands:
    
              - "docker-compose ps postgres"
    
              - "docker-compose logs postgres"
    
          
    
          - name: "Test network connectivity"
    
            action: "Verify network connection to database"
    
            commands:
    
              - "telnet postgres-host 5432"
    
              - "nslookup postgres-host"
    
          
    
          - name: "Check connection pool"
    
            action: "Verify connection pool status"
    
            commands:
    
              - "curl http://localhost:8000/health/detailed"
    
          
    
          - name: "Restart services"
    
            action: "Restart MCP server and database if needed"
    
            commands:
    
              - "docker-compose restart"
    
        
    
        escalation:
    
          - "If issue persists, contact database administrator"
    
          - "Check for infrastructure issues in Azure portal"
    
    
    
      high_error_rate:
    
        title: "High Error Rate Detected"
    
        description: "Steps to investigate and resolve high error rates"
    
        severity: "high"
    
        steps:
    
          - name: "Check recent logs"
    
            action: "Review error logs for patterns"
    
            commands:
    
              - "docker-compose logs mcp_server | grep ERROR | tail -50"
    
          
    
          - name: "Analyze error types"
    
            action: "Categorize errors by type and frequency"
    
            api_endpoint: "/dashboard/diagnostics"
    
          
    
          - name: "Check system resources"
    
            action: "Verify system is not under resource pressure"
    
            commands:
    
              - "curl http://localhost:8000/health/detailed"
    
          
    
          - name: "Review recent deployments"
    
            action: "Check if errors started after recent deployment"
    
            
    
          - name: "Enable debug logging"
    
            action: "Temporarily increase log level for detailed diagnostics"
    
            environment_variable: "LOG_LEVEL=DEBUG"
    
    
    
      slow_performance:
    
        title: "Slow Query Performance"
    
        description: "Steps to diagnose and improve query performance"
    
        severity: "medium"
    
        steps:
    
          - name: "Identify slow queries"
    
            action: "Find queries taking longer than normal"
    
            sql_query: "SELECT query, mean_exec_time FROM pg_stat_statements ORDER BY mean_exec_time DESC LIMIT 10"
    
          
    
          - name: "Check database indexes"
    
            action: "Verify proper indexes exist"
    
            sql_query: "SELECT schemaname, tablename, indexname FROM pg_indexes WHERE schemaname = 'retail'"
    
          
    
          - name: "Analyze query plans"
    
            action: "Review execution plans for slow queries"
    
            sql_command: "EXPLAIN ANALYZE"
    
          
    
          - name: "Check connection pool"
    
            action: "Verify connection pool is not exhausted"
    
            api_endpoint: "/health/detailed"
    
          
    
          - name: "Monitor resource usage"
    
            action: "Check CPU and memory during queries"
    
            commands:
    
              - "top -p $(pgrep postgres)"
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ๊ฐ–์ถ”๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… Application Insights ํ†ตํ•ฉ: ์™„์ „ํ•œ ํ…”๋ ˆ๋ฉ”ํŠธ๋ฆฌ ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง ์„ค์ •

    โœ… ๊ตฌ์กฐํ™”๋œ ๋กœ๊น…: ์ƒ๊ด€๊ด€๊ณ„์™€ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ฐ–์ถ˜ ํ”„๋กœ๋•์…˜ ์ค€๋น„ ๋กœ๊น…

    โœ… ์‚ฌ์šฉ์ž ์ •์˜ ๋ฉ”ํŠธ๋ฆญ: ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ ๊ธฐ์ˆ  ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋ฐ ๋ถ„์„

    โœ… ์ง€๋Šฅํ˜• ์•Œ๋ฆผ: ์—ฌ๋Ÿฌ ์•Œ๋ฆผ ์ฑ„๋„์„ ํ†ตํ•œ ์‚ฌ์ „ ์•Œ๋ฆผ

    โœ… ์šด์˜ ๋Œ€์‹œ๋ณด๋“œ: ์‹ค์‹œ๊ฐ„ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ์ธ์‚ฌ์ดํŠธ

    โœ… ๋ฌธ์ œ ํ•ด๊ฒฐ ์›Œํฌํ”Œ๋กœ์šฐ: ์ž๋™ํ™”๋œ ์ง„๋‹จ ๋ฐ ์šด์˜ ๋Ÿฐ๋ถ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต 12: ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ตœ์ ํ™”๋ฅผ ๊ณ„์† ์ง„ํ–‰ํ•˜์—ฌ:

  • ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ์ˆ  ์ ์šฉ
  • ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ๊ฐ•ํ™” ๊ตฌํ˜„
  • ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๋ชจ๋ฒ” ์‚ฌ๋ก€ ํ•™์Šต
  • ๋น„์šฉ ์ตœ์ ํ™” ์ „๋žต ์ˆ˜๋ฆฝ
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    Azure Monitor

  • Application Insights ๋ฌธ์„œ - ์™„์ „ํ•œ ๋ชจ๋‹ˆํ„ฐ๋ง ๊ฐ€์ด๋“œ
  • KQL ์ฟผ๋ฆฌ ์ฐธ์กฐ - Application Insights์šฉ ์ฟผ๋ฆฌ ์–ธ์–ด
  • Azure Monitor Workbooks - ์‚ฌ์šฉ์ž ์ •์˜ ๋Œ€์‹œ๋ณด๋“œ ์ƒ์„ฑ
  • OpenTelemetry

  • OpenTelemetry Python - ๊ณ„์ธก ๊ฐ€์ด๋“œ
  • ๋ถ„์‚ฐ ํŠธ๋ ˆ์ด์‹ฑ - ํŠธ๋ ˆ์ด์‹ฑ ๊ฐœ๋…
  • ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ - ๋ฉ”ํŠธ๋ฆญ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ์šด์˜ ์šฐ์ˆ˜์„ฑ

  • SRE ํ•ธ๋“œ๋ถ - ์‚ฌ์ดํŠธ ์•ˆ์ •์„ฑ ์—”์ง€๋‹ˆ์–ด๋ง ์›์น™
  • ๋ชจ๋‹ˆํ„ฐ๋ง ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ์—…๊ณ„ ๋ชจ๋ฒ” ์‚ฌ๋ก€
  • ์‚ฌ๊ฑด ๋Œ€์‘ - ์‚ฌ๊ฑด ๊ด€๋ฆฌ ๊ฐ€์ด๋“œ
  • ---

    ์ด์ „: ์‹ค์Šต 10: ๋ฐฐํฌ ์ „๋žต

    ๋‹ค์Œ: ์‹ค์Šต 12: ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ตœ์ ํ™”

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    12 ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ตœ์ ํ™”

    ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ตœ์ ํ™”

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์ข…ํ•ฉ ์‹ค์Šต์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ํ†ตํ•ด ๊ฐ•๋ ฅํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์•ˆ์ „ํ•œ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€, ์ตœ์ ํ™” ๊ธฐ์ˆ  ๋ฐ ํ”„๋กœ๋•์…˜ ๊ฐ€์ด๋“œ๋ผ์ธ์„ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์‹ค๋ฌด ๊ฒฝํ—˜๊ณผ ์—…๊ณ„ ํ‘œ์ค€์„ ํ†ตํ•ด ๊ตฌํ˜„์ด ํ”„๋กœ๋•์…˜ ์ค€๋น„ ์ƒํƒœ๊ฐ€ ๋˜๋„๋ก ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ์„ฑ๊ณต์ ์ธ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๊ฒƒ์€ ๋‹จ์ˆœํžˆ ์ฝ”๋“œ๊ฐ€ ์ž‘๋™ํ•˜๋„๋ก ๋งŒ๋“œ๋Š” ๊ฒƒ ์ด์ƒ์ž…๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” ๊ฐœ๋… ์ฆ๋ช… ๊ตฌํ˜„๊ณผ ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ณ  ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋ณด์•ˆ ํ‘œ์ค€์„ ์œ ์ง€ํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ค€๋น„ ์‹œ์Šคํ…œ์„ ๊ตฌ๋ถ„ํ•˜๋Š” ํ•„์ˆ˜์ ์ธ ๊ด€ํ–‰์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    ์ด๋Ÿฌํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋Š” ์‹ค๋ฌด ๋ฐฐํฌ, ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”ผ๋“œ๋ฐฑ, ๊ธฐ์—… ๊ตฌํ˜„์—์„œ ์–ป์€ ๊ตํ›ˆ์„ ๋ฐ”ํƒ•์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ ์šฉ: MCP ์„œ๋ฒ„์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ์ˆ 
  • ๊ตฌํ˜„: ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ๊ฐ•ํ™” ์กฐ์น˜
  • ์„ค๊ณ„: ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์„ ์œ„ํ•œ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ์ˆ˜๋ฆฝ: ๋ชจ๋‹ˆํ„ฐ๋ง, ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์šด์˜ ์ ˆ์ฐจ
  • ์ตœ์ ํ™”: ์„ฑ๋Šฅ๊ณผ ์‹ ๋ขฐ์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ๋น„์šฉ ์ ˆ๊ฐ
  • ๊ธฐ์—ฌ: MCP ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ์ƒํƒœ๊ณ„์— ๊ณตํ—Œ
  • ๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™”

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ

    ์—ฐ๊ฒฐ ํ’€ ์ตœ์ ํ™”
    
    # Optimized connection pool configuration
    
    POOL_CONFIG = {
    
        # Size configuration
    
        "min_size": max(2, cpu_count()),           # At least 2, scale with CPU
    
        "max_size": min(20, cpu_count() * 4),     # Cap at reasonable maximum
    
        
    
        # Timing configuration
    
        "max_inactive_connection_lifetime": 300,   # 5 minutes
    
        "command_timeout": 30,                     # 30 seconds
    
        "max_queries": 50000,                      # Rotate connections
    
        
    
        # PostgreSQL settings
    
        "server_settings": {
    
            "application_name": "mcp-server-prod",
    
            "jit": "off",                          # Disable for consistency
    
            "work_mem": "8MB",                     # Optimize for queries
    
            "shared_preload_libraries": "pg_stat_statements",
    
            "log_statement": "mod",                # Log modifications only
    
            "log_min_duration_statement": "1s",   # Log slow queries
    
        }
    
    }
    
    
    ์ฟผ๋ฆฌ ์ตœ์ ํ™” ํŒจํ„ด
    
    class QueryOptimizer:
    
        """Database query optimization utilities."""
    
        
    
        def __init__(self):
    
            self.query_cache = {}
    
            self.slow_query_threshold = 1.0  # seconds
    
            
    
        async def execute_optimized_query(
    
            self, 
    
            query: str, 
    
            params: tuple = None,
    
            cache_key: str = None,
    
            cache_ttl: int = 300
    
        ):
    
            """Execute query with optimization and caching."""
    
            
    
            # Check cache first
    
            if cache_key and cache_key in self.query_cache:
    
                cache_entry = self.query_cache[cache_key]
    
                if time.time() - cache_entry['timestamp'] < cache_ttl:
    
                    return cache_entry['result']
    
            
    
            # Execute with monitoring
    
            start_time = time.time()
    
            
    
            try:
    
                async with db_provider.get_connection() as conn:
    
                    # Optimize query execution
    
                    await conn.execute("SET enable_seqscan = off")  # Prefer indexes
    
                    await conn.execute("SET work_mem = '16MB'")     # More memory for this query
    
                    
    
                    result = await conn.fetch(query, *params if params else ())
    
                    
    
                    duration = time.time() - start_time
    
                    
    
                    # Log slow queries
    
                    if duration > self.slow_query_threshold:
    
                        logger.warning(f"Slow query detected: {duration:.2f}s", extra={
    
                            "query": query[:200],
    
                            "duration": duration,
    
                            "params_count": len(params) if params else 0
    
                        })
    
                    
    
                    # Cache successful results
    
                    if cache_key and len(result) < 1000:  # Don't cache large results
    
                        self.query_cache[cache_key] = {
    
                            'result': result,
    
                            'timestamp': time.time()
    
                        }
    
                    
    
                    return result
    
                    
    
            except Exception as e:
    
                logger.error(f"Query optimization failed: {e}")
    
                raise
    
    
    
    # Index recommendations
    
    RECOMMENDED_INDEXES = [
    
        # Core business indexes
    
        "CREATE INDEX CONCURRENTLY idx_orders_store_date ON retail.orders (store_id, order_date DESC);",
    
        "CREATE INDEX CONCURRENTLY idx_order_items_product ON retail.order_items (product_id);",
    
        "CREATE INDEX CONCURRENTLY idx_customers_store_email ON retail.customers (store_id, email);",
    
        
    
        # Analytics indexes
    
        "CREATE INDEX CONCURRENTLY idx_orders_date_amount ON retail.orders (order_date, total_amount);",
    
        "CREATE INDEX CONCURRENTLY idx_products_category_price ON retail.products (category_id, unit_price);",
    
        
    
        # Vector search optimization
    
        "CREATE INDEX CONCURRENTLY idx_embeddings_vector ON retail.product_description_embeddings USING ivfflat (description_embedding vector_cosine_ops) WITH (lists = 100);",
    
    ]
    
    

    ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ฑ๋Šฅ

    ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ฒ” ์‚ฌ๋ก€
    
    import asyncio
    
    from asyncio import Semaphore
    
    from typing import List, Any
    
    
    
    class AsyncOptimizer:
    
        """Async operation optimization patterns."""
    
        
    
        def __init__(self, max_concurrent: int = 10):
    
            self.semaphore = Semaphore(max_concurrent)
    
            self.circuit_breaker = CircuitBreaker()
    
        
    
        async def batch_process(
    
            self, 
    
            items: List[Any], 
    
            process_func: callable,
    
            batch_size: int = 100
    
        ):
    
            """Process items in optimized batches."""
    
            
    
            async def process_batch(batch):
    
                async with self.semaphore:
    
                    return await asyncio.gather(
    
                        *[process_func(item) for item in batch],
    
                        return_exceptions=True
    
                    )
    
            
    
            # Process in batches to avoid overwhelming the system
    
            results = []
    
            for i in range(0, len(items), batch_size):
    
                batch = items[i:i + batch_size]
    
                batch_results = await process_batch(batch)
    
                results.extend(batch_results)
    
                
    
                # Small delay between batches to prevent resource exhaustion
    
                if i + batch_size < len(items):
    
                    await asyncio.sleep(0.1)
    
            
    
            return results
    
        
    
        @circuit_breaker_decorator
    
        async def resilient_operation(self, operation: callable, *args, **kwargs):
    
            """Execute operation with circuit breaker protection."""
    
            return await operation(*args, **kwargs)
    
    
    
    # Circuit breaker implementation
    
    class CircuitBreaker:
    
        """Circuit breaker for external service calls."""
    
        
    
        def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
    
            self.failure_threshold = failure_threshold
    
            self.recovery_timeout = recovery_timeout
    
            self.failure_count = 0
    
            self.last_failure_time = None
    
            self.state = "CLOSED"  # CLOSED, OPEN, HALF_OPEN
    
        
    
        async def call(self, func, *args, **kwargs):
    
            """Execute function with circuit breaker protection."""
    
            
    
            if self.state == "OPEN":
    
                if time.time() - self.last_failure_time > self.recovery_timeout:
    
                    self.state = "HALF_OPEN"
    
                else:
    
                    raise Exception("Circuit breaker is OPEN")
    
            
    
            try:
    
                result = await func(*args, **kwargs)
    
                
    
                # Reset on success
    
                if self.state == "HALF_OPEN":
    
                    self.state = "CLOSED"
    
                    self.failure_count = 0
    
                
    
                return result
    
                
    
            except Exception as e:
    
                self.failure_count += 1
    
                self.last_failure_time = time.time()
    
                
    
                if self.failure_count >= self.failure_threshold:
    
                    self.state = "OPEN"
    
                
    
                raise
    
    

    ์บ์‹ฑ ์ „๋žต

    
    import redis
    
    import pickle
    
    from typing import Union, Optional
    
    
    
    class SmartCache:
    
        """Multi-level caching system."""
    
        
    
        def __init__(self, redis_url: Optional[str] = None):
    
            self.memory_cache = {}
    
            self.redis_client = redis.Redis.from_url(redis_url) if redis_url else None
    
            self.max_memory_items = 1000
    
        
    
        async def get(self, key: str) -> Optional[Any]:
    
            """Get from cache with fallback levels."""
    
            
    
            # Level 1: Memory cache
    
            if key in self.memory_cache:
    
                return self.memory_cache[key]['value']
    
            
    
            # Level 2: Redis cache
    
            if self.redis_client:
    
                try:
    
                    cached_data = self.redis_client.get(key)
    
                    if cached_data:
    
                        value = pickle.loads(cached_data)
    
                        
    
                        # Promote to memory cache
    
                        self._set_memory_cache(key, value)
    
                        return value
    
                except Exception as e:
    
                    logger.warning(f"Redis cache error: {e}")
    
            
    
            return None
    
        
    
        async def set(
    
            self, 
    
            key: str, 
    
            value: Any, 
    
            ttl: int = 300,
    
            cache_level: str = "both"
    
        ):
    
            """Set cache value at specified levels."""
    
            
    
            if cache_level in ["memory", "both"]:
    
                self._set_memory_cache(key, value, ttl)
    
            
    
            if cache_level in ["redis", "both"] and self.redis_client:
    
                try:
    
                    self.redis_client.setex(
    
                        key, 
    
                        ttl, 
    
                        pickle.dumps(value)
    
                    )
    
                except Exception as e:
    
                    logger.warning(f"Redis set error: {e}")
    
        
    
        def _set_memory_cache(self, key: str, value: Any, ttl: int = 300):
    
            """Set value in memory cache with LRU eviction."""
    
            
    
            # Implement LRU eviction
    
            if len(self.memory_cache) >= self.max_memory_items:
    
                oldest_key = min(
    
                    self.memory_cache.keys(),
    
                    key=lambda k: self.memory_cache[k]['timestamp']
    
                )
    
                del self.memory_cache[oldest_key]
    
            
    
            self.memory_cache[key] = {
    
                'value': value,
    
                'timestamp': time.time(),
    
                'ttl': ttl
    
            }
    
    
    
    # Cache key generation
    
    def generate_cache_key(query: str, user_context: str, params: dict = None) -> str:
    
        """Generate consistent cache keys."""
    
        key_components = [
    
            query.strip().lower(),
    
            user_context,
    
            json.dumps(params, sort_keys=True) if params else ""
    
        ]
    
        
    
        key_string = "|".join(key_components)
    
        return hashlib.sha256(key_string.encode()).hexdigest()
    
    

    ๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™”

    ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ

    
    from azure.identity import DefaultAzureCredential, ClientSecretCredential
    
    from azure.keyvault.secrets import SecretClient
    
    import jwt
    
    from typing import Dict, List
    
    
    
    class SecurityManager:
    
        """Comprehensive security management."""
    
        
    
        def __init__(self):
    
            self.key_vault_client = self._setup_key_vault()
    
            self.token_blacklist = set()
    
            
    
        def _setup_key_vault(self) -> SecretClient:
    
            """Initialize Azure Key Vault client."""
    
            credential = DefaultAzureCredential()
    
            vault_url = os.getenv("AZURE_KEY_VAULT_URL")
    
            
    
            if vault_url:
    
                return SecretClient(vault_url=vault_url, credential=credential)
    
            return None
    
        
    
        async def validate_request(self, request_headers: Dict[str, str]) -> Dict[str, Any]:
    
            """Comprehensive request validation."""
    
            
    
            # Extract and validate authentication
    
            auth_token = request_headers.get("authorization", "").replace("Bearer ", "")
    
            if not auth_token:
    
                raise AuthenticationError("Missing authentication token")
    
            
    
            # Validate token
    
            user_context = await self._validate_token(auth_token)
    
            
    
            # Check rate limiting
    
            await self._check_rate_limit(user_context["user_id"])
    
            
    
            # Validate RLS context
    
            rls_user_id = request_headers.get("x-rls-user-id")
    
            if not self._validate_rls_access(user_context, rls_user_id):
    
                raise AuthorizationError("Invalid RLS context for user")
    
            
    
            return {
    
                "user_id": user_context["user_id"],
    
                "roles": user_context["roles"],
    
                "rls_user_id": rls_user_id,
    
                "permissions": user_context["permissions"]
    
            }
    
        
    
        async def _validate_token(self, token: str) -> Dict[str, Any]:
    
            """Validate JWT token."""
    
            
    
            if token in self.token_blacklist:
    
                raise AuthenticationError("Token has been revoked")
    
            
    
            try:
    
                # Get public key from Key Vault or cache
    
                public_key = await self._get_public_key()
    
                
    
                # Decode and validate token
    
                payload = jwt.decode(
    
                    token, 
    
                    public_key, 
    
                    algorithms=["RS256"],
    
                    audience="mcp-server",
    
                    issuer="zava-auth"
    
                )
    
                
    
                return {
    
                    "user_id": payload["sub"],
    
                    "roles": payload.get("roles", []),
    
                    "permissions": payload.get("permissions", []),
    
                    "expires_at": payload["exp"]
    
                }
    
                
    
            except jwt.InvalidTokenError as e:
    
                raise AuthenticationError(f"Invalid token: {e}")
    
        
    
        def _validate_rls_access(self, user_context: Dict, rls_user_id: str) -> bool:
    
            """Validate RLS context access."""
    
            
    
            # Super admins can access any context
    
            if "super_admin" in user_context["roles"]:
    
                return True
    
            
    
            # Store managers can only access their own store
    
            if "store_manager" in user_context["roles"]:
    
                allowed_stores = user_context.get("allowed_stores", [])
    
                return rls_user_id in allowed_stores
    
            
    
            # Regional managers can access multiple stores
    
            if "regional_manager" in user_context["roles"]:
    
                allowed_regions = user_context.get("allowed_regions", [])
    
                return self._check_store_in_regions(rls_user_id, allowed_regions)
    
            
    
            return False
    
    
    
    # Input validation and sanitization
    
    class InputValidator:
    
        """SQL injection prevention and input validation."""
    
        
    
        @staticmethod
    
        def validate_sql_query(query: str) -> bool:
    
            """Validate SQL query for safety."""
    
            
    
            # Forbidden patterns
    
            forbidden_patterns = [
    
                r";\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+",
    
                r"--.*",
    
                r"/\*.*\*/",
    
                r"xp_cmdshell",
    
                r"sp_executesql",
    
                r"EXEC\s*\(",
    
            ]
    
            
    
            query_upper = query.upper()
    
            
    
            for pattern in forbidden_patterns:
    
                if re.search(pattern, query_upper, re.IGNORECASE):
    
                    logger.warning(f"Blocked potentially dangerous query: {pattern}")
    
                    return False
    
            
    
            # Only allow SELECT statements
    
            if not query_upper.strip().startswith("SELECT"):
    
                return False
    
            
    
            return True
    
        
    
        @staticmethod
    
        def sanitize_table_name(table_name: str) -> str:
    
            """Sanitize table name input."""
    
            
    
            # Only allow alphanumeric, underscore, and dot
    
            if not re.match(r"^[a-zA-Z0-9_.]+$", table_name):
    
                raise ValueError("Invalid table name format")
    
            
    
            # Validate against allowed tables
    
            if table_name not in VALID_TABLES:
    
                raise ValueError(f"Table {table_name} not allowed")
    
            
    
            return table_name
    
    

    ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ

    
    from cryptography.fernet import Fernet
    
    import hashlib
    
    
    
    class DataProtection:
    
        """Data encryption and protection utilities."""
    
        
    
        def __init__(self):
    
            self.encryption_key = self._get_encryption_key()
    
            self.cipher_suite = Fernet(self.encryption_key)
    
        
    
        def _get_encryption_key(self) -> bytes:
    
            """Get encryption key from secure storage."""
    
            
    
            # In production, get from Azure Key Vault
    
            key_vault_secret = os.getenv("ENCRYPTION_KEY_SECRET_NAME")
    
            if key_vault_secret and self.key_vault_client:
    
                secret = self.key_vault_client.get_secret(key_vault_secret)
    
                return secret.value.encode()
    
            
    
            # Fallback for development (not for production!)
    
            dev_key = os.getenv("DEV_ENCRYPTION_KEY")
    
            if dev_key:
    
                return dev_key.encode()
    
            
    
            raise ValueError("No encryption key available")
    
        
    
        def encrypt_sensitive_data(self, data: str) -> str:
    
            """Encrypt sensitive data."""
    
            return self.cipher_suite.encrypt(data.encode()).decode()
    
        
    
        def decrypt_sensitive_data(self, encrypted_data: str) -> str:
    
            """Decrypt sensitive data."""
    
            return self.cipher_suite.decrypt(encrypted_data.encode()).decode()
    
        
    
        @staticmethod
    
        def hash_password(password: str, salt: str = None) -> tuple:
    
            """Hash password with salt."""
    
            if not salt:
    
                salt = os.urandom(32).hex()
    
            
    
            password_hash = hashlib.pbkdf2_hmac(
    
                'sha256',
    
                password.encode(),
    
                salt.encode(),
    
                100000  # iterations
    
            ).hex()
    
            
    
            return password_hash, salt
    
        
    
        @staticmethod
    
        def mask_sensitive_logs(log_data: dict) -> dict:
    
            """Mask sensitive information in logs."""
    
            
    
            sensitive_fields = [
    
                'password', 'token', 'secret', 'key', 'authorization',
    
                'x-api-key', 'client_secret', 'connection_string'
    
            ]
    
            
    
            masked_data = log_data.copy()
    
            
    
            for field in sensitive_fields:
    
                if field in masked_data:
    
                    value = str(masked_data[field])
    
                    if len(value) > 4:
    
                        masked_data[field] = value[:2] + "*" * (len(value) - 4) + value[-2:]
    
                    else:
    
                        masked_data[field] = "***"
    
            
    
            return masked_data
    
    

    ๐Ÿ“Š ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๊ฐ€์ด๋“œ๋ผ์ธ

    ์ฝ”๋“œ๋กœ์„œ์˜ ์ธํ”„๋ผ

    
    # azure-pipelines.yml
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - release/*
    
    
    
    variables:
    
      - group: mcp-server-secrets
    
      - name: imageRepository
    
        value: 'zava-mcp-server'
    
      - name: containerRegistry
    
        value: 'zavamcpregistry.azurecr.io'
    
    
    
    stages:
    
    - stage: Build
    
      displayName: Build and Test
    
      jobs:
    
      - job: Build
    
        displayName: Build
    
        pool:
    
          vmImage: ubuntu-latest
    
        
    
        steps:
    
        - task: UsePythonVersion@0
    
          inputs:
    
            versionSpec: '3.11'
    
            displayName: 'Use Python 3.11'
    
        
    
        - script: |
    
            python -m pip install --upgrade pip
    
            pip install -r requirements.lock.txt
    
            pip install pytest pytest-cov
    
          displayName: 'Install dependencies'
    
        
    
        - script: |
    
            pytest tests/ --cov=mcp_server --cov-report=xml
    
          displayName: 'Run tests with coverage'
    
        
    
        - task: PublishCodeCoverageResults@1
    
          inputs:
    
            codeCoverageTool: Cobertura
    
            summaryFileLocation: 'coverage.xml'
    
        
    
        - task: Docker@2
    
          displayName: Build Docker image
    
          inputs:
    
            command: build
    
            repository: $(imageRepository)
    
            dockerfile: Dockerfile
    
            tags: |
    
              $(Build.BuildId)
    
              latest
    
    
    
    - stage: Deploy
    
      displayName: Deploy to Production
    
      dependsOn: Build
    
      condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
      
    
      jobs:
    
      - deployment: DeployProduction
    
        displayName: Deploy to Production
    
        environment: 'production'
    
        pool:
    
          vmImage: ubuntu-latest
    
        
    
        strategy:
    
          runOnce:
    
            deploy:
    
              steps:
    
              - task: AzureContainerApps@1
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  containerAppName: 'zava-mcp-server'
    
                  resourceGroup: '$(resourceGroupName)'
    
                  imageToDeploy: '$(containerRegistry)/$(imageRepository):$(Build.BuildId)'
    
    

    ์ปจํ…Œ์ด๋„ˆ ์ตœ์ ํ™”

    
    # Multi-stage Dockerfile for production
    
    FROM python:3.11-slim as builder
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        gcc \
    
        g++ \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install Python dependencies
    
    COPY requirements.lock.txt .
    
    RUN pip install --no-cache-dir --upgrade pip && \
    
        pip install --no-cache-dir -r requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim as production
    
    
    
    # Create non-root user
    
    RUN groupadd -r mcpserver && useradd -r -g mcpserver mcpserver
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Set working directory
    
    WORKDIR /app
    
    
    
    # Copy application code
    
    COPY mcp_server/ ./mcp_server/
    
    COPY --chown=mcpserver:mcpserver . .
    
    
    
    # Set security configurations
    
    RUN chmod -R 755 /app && \
    
        chown -R mcpserver:mcpserver /app
    
    
    
    # Switch to non-root user
    
    USER mcpserver
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    
        CMD curl -f http://localhost:8000/health || exit 1
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Start application
    
    CMD ["python", "-m", "mcp_server.sales_analysis"]
    
    

    ํ™˜๊ฒฝ ๊ตฌ์„ฑ

    
    # Production configuration management
    
    class ProductionConfig:
    
        """Production-specific configuration."""
    
        
    
        def __init__(self):
    
            self.validate_production_requirements()
    
            self.setup_logging()
    
            self.configure_security()
    
        
    
        def validate_production_requirements(self):
    
            """Validate all required production settings."""
    
            
    
            required_settings = [
    
                "AZURE_CLIENT_ID",
    
                "AZURE_CLIENT_SECRET", 
    
                "AZURE_TENANT_ID",
    
                "PROJECT_ENDPOINT",
    
                "AZURE_OPENAI_ENDPOINT",
    
                "POSTGRES_HOST",
    
                "POSTGRES_PASSWORD",
    
                "APPLICATIONINSIGHTS_CONNECTION_STRING"
    
            ]
    
            
    
            missing_settings = [
    
                setting for setting in required_settings 
    
                if not os.getenv(setting)
    
            ]
    
            
    
            if missing_settings:
    
                raise EnvironmentError(
    
                    f"Missing required production settings: {missing_settings}"
    
                )
    
        
    
        def setup_logging(self):
    
            """Configure production logging."""
    
            
    
            logging.basicConfig(
    
                level=logging.INFO,
    
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    
                handlers=[
    
                    logging.StreamHandler(sys.stdout),
    
                    logging.handlers.RotatingFileHandler(
    
                        '/var/log/mcp-server.log',
    
                        maxBytes=50*1024*1024,  # 50MB
    
                        backupCount=5
    
                    )
    
                ]
    
            )
    
            
    
            # Set third-party loggers to WARNING
    
            logging.getLogger('azure').setLevel(logging.WARNING)
    
            logging.getLogger('urllib3').setLevel(logging.WARNING)
    
        
    
        def configure_security(self):
    
            """Configure production security settings."""
    
            
    
            # Disable debug mode
    
            os.environ['DEBUG'] = 'False'
    
            
    
            # Set secure headers
    
            os.environ['SECURE_SSL_REDIRECT'] = 'True'
    
            os.environ['SECURE_HSTS_SECONDS'] = '31536000'
    
            os.environ['SECURE_CONTENT_TYPE_NOSNIFF'] = 'True'
    
            os.environ['SECURE_BROWSER_XSS_FILTER'] = 'True'
    
    

    ๐Ÿ’ฐ ๋น„์šฉ ์ตœ์ ํ™”

    ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ

    
    class CostOptimizer:
    
        """Cost optimization strategies."""
    
        
    
        def __init__(self):
    
            self.metrics_collector = MetricsCollector()
    
            self.auto_scaler = AutoScaler()
    
        
    
        async def optimize_database_connections(self):
    
            """Dynamically adjust connection pool based on load."""
    
            
    
            current_load = await self.metrics_collector.get_current_load()
    
            
    
            if current_load < 0.3:  # Low load
    
                target_pool_size = max(2, int(current_load * 10))
    
            elif current_load < 0.7:  # Medium load
    
                target_pool_size = max(5, int(current_load * 15))
    
            else:  # High load
    
                target_pool_size = min(20, int(current_load * 25))
    
            
    
            await db_provider.adjust_pool_size(target_pool_size)
    
            
    
            logger.info(f"Adjusted pool size to {target_pool_size} for load {current_load}")
    
        
    
        async def implement_smart_caching(self):
    
            """Implement intelligent caching to reduce compute costs."""
    
            
    
            # Cache expensive operations
    
            expensive_queries = await self.identify_expensive_queries()
    
            
    
            for query in expensive_queries:
    
                cache_key = self.generate_cache_key(query)
    
                ttl = self.calculate_optimal_ttl(query)
    
                
    
                await smart_cache.set(cache_key, None, ttl=ttl)
    
        
    
        def calculate_azure_costs(self) -> Dict[str, float]:
    
            """Calculate estimated Azure resource costs."""
    
            
    
            return {
    
                "container_apps": self.estimate_container_costs(),
    
                "postgresql": self.estimate_database_costs(),
    
                "openai": self.estimate_ai_costs(),
    
                "application_insights": self.estimate_monitoring_costs(),
    
                "storage": self.estimate_storage_costs()
    
            }
    
    
    
    # Auto-scaling configuration
    
    class AutoScaler:
    
        """Automatic scaling based on metrics."""
    
        
    
        async def scale_decision(self) -> str:
    
            """Determine scaling action based on metrics."""
    
            
    
            metrics = await self.collect_scaling_metrics()
    
            
    
            # CPU-based scaling
    
            if metrics['cpu_usage'] > 80:
    
                return "scale_up"
    
            elif metrics['cpu_usage'] < 20 and metrics['instance_count'] > 1:
    
                return "scale_down"
    
            
    
            # Memory-based scaling
    
            if metrics['memory_usage'] > 85:
    
                return "scale_up"
    
            
    
            # Request queue scaling
    
            if metrics['queue_length'] > 100:
    
                return "scale_up"
    
            elif metrics['queue_length'] < 10 and metrics['instance_count'] > 1:
    
                return "scale_down"
    
            
    
            return "no_action"
    
    

    ๐Ÿ”ง ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์šด์˜

    ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง

    
    class OperationalHealth:
    
        """Comprehensive operational health monitoring."""
    
        
    
        def __init__(self):
    
            self.alert_manager = AlertManager()
    
            self.health_checks = {}
    
            
    
        async def comprehensive_health_check(self) -> Dict[str, Any]:
    
            """Perform comprehensive system health check."""
    
            
    
            health_report = {
    
                "timestamp": datetime.utcnow().isoformat(),
    
                "overall_status": "healthy",
    
                "components": {}
    
            }
    
            
    
            # Database health
    
            db_health = await self.check_database_health()
    
            health_report["components"]["database"] = db_health
    
            
    
            # External services health
    
            ai_health = await self.check_ai_service_health()
    
            health_report["components"]["ai_service"] = ai_health
    
            
    
            # System resources
    
            system_health = await self.check_system_resources()
    
            health_report["components"]["system"] = system_health
    
            
    
            # Application metrics
    
            app_health = await self.check_application_health()
    
            health_report["components"]["application"] = app_health
    
            
    
            # Determine overall status
    
            failed_components = [
    
                name for name, status in health_report["components"].items()
    
                if status.get("status") != "healthy"
    
            ]
    
            
    
            if failed_components:
    
                health_report["overall_status"] = "unhealthy"
    
                health_report["failed_components"] = failed_components
    
                
    
                # Trigger alerts
    
                await self.alert_manager.send_alert(
    
                    severity="high",
    
                    message=f"Health check failed for: {failed_components}",
    
                    details=health_report
    
                )
    
            
    
            return health_report
    
        
    
        async def check_database_health(self) -> Dict[str, Any]:
    
            """Check database connectivity and performance."""
    
            
    
            try:
    
                start_time = time.time()
    
                
    
                async with db_provider.get_connection() as conn:
    
                    # Basic connectivity
    
                    await conn.fetchval("SELECT 1")
    
                    
    
                    # Check slow queries
    
                    slow_queries = await conn.fetch("""
    
                        SELECT query, mean_exec_time, calls 
    
                        FROM pg_stat_statements 
    
                        WHERE mean_exec_time > 1000 
    
                        ORDER BY mean_exec_time DESC 
    
                        LIMIT 5
    
                    """)
    
                    
    
                    # Check connection count
    
                    connection_count = await conn.fetchval("""
    
                        SELECT count(*) FROM pg_stat_activity 
    
                        WHERE state = 'active'
    
                    """)
    
                    
    
                    response_time = time.time() - start_time
    
                    
    
                    return {
    
                        "status": "healthy",
    
                        "response_time_ms": response_time * 1000,
    
                        "active_connections": connection_count,
    
                        "slow_queries_count": len(slow_queries),
    
                        "pool_size": db_provider.connection_pool.get_size()
    
                    }
    
                    
    
            except Exception as e:
    
                return {
    
                    "status": "unhealthy",
    
                    "error": str(e),
    
                    "last_check": datetime.utcnow().isoformat()
    
                }
    
    
    
    # Automated backup and recovery
    
    class BackupManager:
    
        """Database backup and recovery management."""
    
        
    
        async def create_backup(self, backup_type: str = "full") -> str:
    
            """Create database backup."""
    
            
    
            timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
    
            backup_name = f"zava_backup_{backup_type}_{timestamp}"
    
            
    
            if backup_type == "full":
    
                await self.create_full_backup(backup_name)
    
            elif backup_type == "incremental":
    
                await self.create_incremental_backup(backup_name)
    
            
    
            # Upload to Azure Blob Storage
    
            await self.upload_backup_to_azure(backup_name)
    
            
    
            return backup_name
    
        
    
        async def schedule_automated_backups(self):
    
            """Schedule regular automated backups."""
    
            
    
            # Daily full backup at 2 AM UTC
    
            schedule.every().day.at("02:00").do(
    
                lambda: asyncio.create_task(self.create_backup("full"))
    
            )
    
            
    
            # Hourly incremental backups
    
            schedule.every().hour.do(
    
                lambda: asyncio.create_task(self.create_backup("incremental"))
    
            )
    
    

    ๐ŸŒ ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ์—ฌ

    ์˜คํ”ˆ ์†Œ์Šค ๋ชจ๋ฒ” ์‚ฌ๋ก€

    
    # Contributing to MCP Database Integration
    
    
    
    ## Development Guidelines
    
    
    
    ### Code Quality Standards
    
    - Follow PEP 8 for Python code style
    
    - Maintain test coverage above 90%
    
    - Use type hints throughout the codebase
    
    - Write comprehensive docstrings
    
    
    
    ### Testing Requirements
    
    - Unit tests for all new functionality
    
    - Integration tests for database operations
    
    - Performance benchmarks for critical paths
    
    - Security tests for authentication/authorization
    
    
    
    ### Documentation Standards
    
    - Update README.md for any new features
    
    - Add inline code documentation
    
    - Create examples for new tools or patterns
    
    - Maintain API documentation
    
    
    
    ## Security Considerations
    
    
    
    ### Reporting Security Issues
    
    - Report security vulnerabilities privately
    
    - Use encrypted communication channels
    
    - Provide detailed reproduction steps
    
    - Include potential impact assessment
    
    
    
    ### Security Review Process
    
    - All PRs undergo security review
    
    - Static analysis tools required to pass
    
    - Dependency vulnerability scanning
    
    - Manual security testing for critical changes
    
    

    ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ

    
    class CommunityContributor:
    
        """Tools for community engagement and contribution."""
    
        
    
        @staticmethod
    
        def generate_contribution_guide():
    
            """Generate personalized contribution guide."""
    
            
    
            return {
    
                "getting_started": {
    
                    "setup": "Follow setup guide in Lab 03",
    
                    "first_contribution": "Start with documentation improvements",
    
                    "testing": "Run full test suite before submitting PR"
    
                },
    
                
    
                "contribution_areas": {
    
                    "documentation": "Improve learning labs and examples",
    
                    "testing": "Add test cases and improve coverage",
    
                    "features": "Implement new MCP tools and capabilities",
    
                    "performance": "Optimize queries and caching",
    
                    "security": "Enhance security measures and validation"
    
                },
    
                
    
                "community_resources": {
    
                    "discord": "https://discord.com/invite/ByRwuEEgH4",
    
                    "discussions": "GitHub Discussions for Q&A",
    
                    "issues": "GitHub Issues for bug reports",
    
                    "examples": "Share your implementation examples"
    
                }
    
            }
    
        
    
        @staticmethod
    
        def validate_contribution(pr_data: Dict) -> Dict[str, bool]:
    
            """Validate contribution meets standards."""
    
            
    
            return {
    
                "has_tests": "test" in pr_data.get("files_changed", []),
    
                "has_documentation": "README" in str(pr_data.get("files_changed", [])),
    
                "follows_conventions": True,  # Would implement actual checks
    
                "security_reviewed": pr_data.get("security_review", False),
    
                "performance_tested": pr_data.get("benchmark_results", False)
    
            }
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์ข…ํ•ฉ ํ•™์Šต ๊ฒฝ๋กœ๋ฅผ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ์ˆ™๋‹ฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠœ๋‹, ๋น„๋™๊ธฐ ํŒจํ„ด ๋ฐ ์บ์‹ฑ ์ „๋žต

    โœ… ๋ณด์•ˆ ๊ฐ•ํ™”: ์ธ์ฆ, ๊ถŒํ•œ ๋ถ€์—ฌ ๋ฐ ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ

    โœ… ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ: ์ฝ”๋“œ๋กœ์„œ์˜ ์ธํ”„๋ผ ๋ฐ ์ปจํ…Œ์ด๋„ˆ ์ตœ์ ํ™”

    โœ… ๋น„์šฉ ๊ด€๋ฆฌ: ๋ฆฌ์†Œ์Šค ์ตœ์ ํ™” ๋ฐ ์ง€๋Šฅํ˜• ํ™•์žฅ

    โœ… ์šด์˜ ์šฐ์ˆ˜์„ฑ: ๋ชจ๋‹ˆํ„ฐ๋ง, ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์ž๋™ํ™”

    โœ… ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ: MCP ์ƒํƒœ๊ณ„์— ๊ธฐ์—ฌ

    ๐Ÿ† ์ธ์ฆ ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต ํ‰๊ฐ€

    ๋‹ค์Œ ํ”„๋กœ์ ํŠธ๋ฅผ ์™„๋ฃŒํ•˜์—ฌ ์ˆ™๋ จ๋„๋ฅผ ์ž…์ฆํ•˜์„ธ์š”:

    ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„ ๊ตฌ์ถ• ํฌํ•จ:

  • [ ] RLS๋ฅผ ํ™œ์šฉํ•œ ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์†Œ๋งค ๋ถ„์„
  • [ ] Azure OpenAI๋ฅผ ํ™œ์šฉํ•œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰
  • [ ] ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ๊ตฌํ˜„
  • [ ] Azure์—์„œ์˜ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ
  • [ ] ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฒฝ๊ณ  ์„ค์ •
  • [ ] ๋ฌธ์„œํ™” ๋ฐ ํ…Œ์ŠคํŠธ
  • ๊ณ ๊ธ‰ ํ•™์Šต ๊ฒฝ๋กœ

    MCP ์—ฌ์ •์„ ๊ณ„์†ํ•˜์„ธ์š”:

  • MCP ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด: ๊ณ ๊ธ‰ ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜
  • ๋ฉ€ํ‹ฐ ๋ชจ๋ธ ํ†ตํ•ฉ: ๋‹ค์–‘ํ•œ AI ๋ชจ๋ธ ๊ฒฐํ•ฉ
  • ๊ธฐ์—… ๊ทœ๋ชจ: ๋Œ€๊ทœ๋ชจ MCP ๋ฐฐํฌ
  • ๋งž์ถคํ˜• ๋„๊ตฌ ๊ฐœ๋ฐœ: ์ „๋ฌธ MCP ๋„๊ตฌ ๊ตฌ์ถ•
  • MCP ์ƒํƒœ๊ณ„: ๊ด‘๋ฒ”์œ„ํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๊ธฐ์—ฌ
  • ์ปค๋ฎค๋‹ˆํ‹ฐ ์ธ์ •

    ์„ฑ๊ณผ๋ฅผ ๊ณต์œ ํ•˜์„ธ์š”:

  • GitHub ํฌํŠธํด๋ฆฌ์˜ค: ๊ตฌํ˜„ ์‚ฌ๋ก€๋ฅผ ๊ณต๊ฐœ
  • ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ์—ฌ: ๊ฐœ์„  ์‚ฌํ•ญ ๋˜๋Š” ์˜ˆ์ œ ์ œ์ถœ
  • ๋ฐœํ‘œ ๊ธฐํšŒ: ๋ฐ‹์—… ๋˜๋Š” ์ปจํผ๋Ÿฐ์Šค์—์„œ ๋ฐœํ‘œ
  • ๋ฉ˜ํ† ๋ง: ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ MCP๋ฅผ ๋ฐฐ์šฐ๋„๋ก ์ง€์›
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ๊ณ ๊ธ‰ ์ฃผ์ œ

  • PostgreSQL ์„ฑ๋Šฅ ํŠœ๋‹ - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™”
  • Azure ์ปจํ…Œ์ด๋„ˆ ์•ฑ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ
  • Python ๋น„๋™๊ธฐ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ
  • ๋ณด์•ˆ ์ž๋ฃŒ

  • OWASP Top 10 - ๋ณด์•ˆ ์ทจ์•ฝ์ 
  • Azure ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ํด๋ผ์šฐ๋“œ ๋ณด์•ˆ
  • Python ๋ณด์•ˆ ๊ฐ€์ด๋“œ๋ผ์ธ - ์•ˆ์ „ํ•œ ์ฝ”๋”ฉ
  • ์ปค๋ฎค๋‹ˆํ‹ฐ

  • MCP ์ปค๋ฎค๋‹ˆํ‹ฐ Discord - ์‹ค์‹œ๊ฐ„ ํ† ๋ก 
  • GitHub Discussions - Q&A ๋ฐ ๊ณต์œ 
  • Stack Overflow - ๊ธฐ์ˆ  ์งˆ๋ฌธ
  • ---

    ๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ํ•™์Šต ๊ฒฝ๋กœ๋ฅผ ์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์‹œ์Šคํ…œ์„ ์—ฐ๊ฒฐํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ์ง€์‹๊ณผ ๊ธฐ์ˆ ์„ ๊ฐ–์ถ”๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ๊ธฐ์—ฌํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ฐธ์—ฌํ•˜์—ฌ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ์ฝ”๋“œ ๊ฐœ์„ ์— ๊ธฐ์—ฌํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ ํ•™์Šต ์ž๋ฃŒ๋ฅผ ๋งŒ๋“ค์–ด ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด MCP๋ฅผ ๋ฐฐ์šฐ๋„๋ก ๋„์™€์ฃผ์„ธ์š”.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ์„ฑ๋Šฅ ์ตœ์ ํ™”, ๋ณด์•ˆ ๊ฐ•ํ™”, ํ”„๋กœ๋•์…˜ ํŒ ์ตœ์ ํ™”ํ•˜๊ธฐ

    ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋ฐ ์ตœ์ ํ™”

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์ข…ํ•ฉ ์‹ค์Šต์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ํ†ตํ•ด ๊ฐ•๋ ฅํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๋ฉฐ ์•ˆ์ „ํ•œ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๊ธฐ ์œ„ํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€, ์ตœ์ ํ™” ๊ธฐ์ˆ  ๋ฐ ํ”„๋กœ๋•์…˜ ๊ฐ€์ด๋“œ๋ผ์ธ์„ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค. ์‹ค๋ฌด ๊ฒฝํ—˜๊ณผ ์—…๊ณ„ ํ‘œ์ค€์„ ํ†ตํ•ด ๊ตฌํ˜„์ด ํ”„๋กœ๋•์…˜ ์ค€๋น„ ์ƒํƒœ๊ฐ€ ๋˜๋„๋ก ํ•™์Šตํ•ฉ๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    ์„ฑ๊ณต์ ์ธ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๊ฒƒ์€ ๋‹จ์ˆœํžˆ ์ฝ”๋“œ๊ฐ€ ์ž‘๋™ํ•˜๋„๋ก ๋งŒ๋“œ๋Š” ๊ฒƒ ์ด์ƒ์ž…๋‹ˆ๋‹ค. ์ด ์‹ค์Šต์—์„œ๋Š” ๊ฐœ๋… ์ฆ๋ช… ๊ตฌํ˜„๊ณผ ํ™•์žฅ ๊ฐ€๋Šฅํ•˜๊ณ  ์‹ ๋ขฐํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ ๋ณด์•ˆ ํ‘œ์ค€์„ ์œ ์ง€ํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ค€๋น„ ์‹œ์Šคํ…œ์„ ๊ตฌ๋ถ„ํ•˜๋Š” ํ•„์ˆ˜์ ์ธ ๊ด€ํ–‰์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

    ์ด๋Ÿฌํ•œ ๋ชจ๋ฒ” ์‚ฌ๋ก€๋Š” ์‹ค๋ฌด ๋ฐฐํฌ, ์ปค๋ฎค๋‹ˆํ‹ฐ ํ”ผ๋“œ๋ฐฑ, ๊ธฐ์—… ๊ตฌํ˜„์—์„œ ์–ป์€ ๊ตํ›ˆ์„ ๋ฐ”ํƒ•์œผ๋กœ ํ•ฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ ์šฉ: MCP ์„œ๋ฒ„์™€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ธฐ์ˆ 
  • ๊ตฌํ˜„: ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ๊ฐ•ํ™” ์กฐ์น˜
  • ์„ค๊ณ„: ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์„ ์œ„ํ•œ ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด
  • ์ˆ˜๋ฆฝ: ๋ชจ๋‹ˆํ„ฐ๋ง, ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์šด์˜ ์ ˆ์ฐจ
  • ์ตœ์ ํ™”: ์„ฑ๋Šฅ๊ณผ ์‹ ๋ขฐ์„ฑ์„ ์œ ์ง€ํ•˜๋ฉด์„œ ๋น„์šฉ ์ ˆ๊ฐ
  • ๊ธฐ์—ฌ: MCP ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ์ƒํƒœ๊ณ„์— ๊ณตํ—Œ
  • ๐Ÿš€ ์„ฑ๋Šฅ ์ตœ์ ํ™”

    ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ฑ๋Šฅ

    ์—ฐ๊ฒฐ ํ’€ ์ตœ์ ํ™”
    
    # Optimized connection pool configuration
    
    POOL_CONFIG = {
    
        # Size configuration
    
        "min_size": max(2, cpu_count()),           # At least 2, scale with CPU
    
        "max_size": min(20, cpu_count() * 4),     # Cap at reasonable maximum
    
        
    
        # Timing configuration
    
        "max_inactive_connection_lifetime": 300,   # 5 minutes
    
        "command_timeout": 30,                     # 30 seconds
    
        "max_queries": 50000,                      # Rotate connections
    
        
    
        # PostgreSQL settings
    
        "server_settings": {
    
            "application_name": "mcp-server-prod",
    
            "jit": "off",                          # Disable for consistency
    
            "work_mem": "8MB",                     # Optimize for queries
    
            "shared_preload_libraries": "pg_stat_statements",
    
            "log_statement": "mod",                # Log modifications only
    
            "log_min_duration_statement": "1s",   # Log slow queries
    
        }
    
    }
    
    
    ์ฟผ๋ฆฌ ์ตœ์ ํ™” ํŒจํ„ด
    
    class QueryOptimizer:
    
        """Database query optimization utilities."""
    
        
    
        def __init__(self):
    
            self.query_cache = {}
    
            self.slow_query_threshold = 1.0  # seconds
    
            
    
        async def execute_optimized_query(
    
            self, 
    
            query: str, 
    
            params: tuple = None,
    
            cache_key: str = None,
    
            cache_ttl: int = 300
    
        ):
    
            """Execute query with optimization and caching."""
    
            
    
            # Check cache first
    
            if cache_key and cache_key in self.query_cache:
    
                cache_entry = self.query_cache[cache_key]
    
                if time.time() - cache_entry['timestamp'] < cache_ttl:
    
                    return cache_entry['result']
    
            
    
            # Execute with monitoring
    
            start_time = time.time()
    
            
    
            try:
    
                async with db_provider.get_connection() as conn:
    
                    # Optimize query execution
    
                    await conn.execute("SET enable_seqscan = off")  # Prefer indexes
    
                    await conn.execute("SET work_mem = '16MB'")     # More memory for this query
    
                    
    
                    result = await conn.fetch(query, *params if params else ())
    
                    
    
                    duration = time.time() - start_time
    
                    
    
                    # Log slow queries
    
                    if duration > self.slow_query_threshold:
    
                        logger.warning(f"Slow query detected: {duration:.2f}s", extra={
    
                            "query": query[:200],
    
                            "duration": duration,
    
                            "params_count": len(params) if params else 0
    
                        })
    
                    
    
                    # Cache successful results
    
                    if cache_key and len(result) < 1000:  # Don't cache large results
    
                        self.query_cache[cache_key] = {
    
                            'result': result,
    
                            'timestamp': time.time()
    
                        }
    
                    
    
                    return result
    
                    
    
            except Exception as e:
    
                logger.error(f"Query optimization failed: {e}")
    
                raise
    
    
    
    # Index recommendations
    
    RECOMMENDED_INDEXES = [
    
        # Core business indexes
    
        "CREATE INDEX CONCURRENTLY idx_orders_store_date ON retail.orders (store_id, order_date DESC);",
    
        "CREATE INDEX CONCURRENTLY idx_order_items_product ON retail.order_items (product_id);",
    
        "CREATE INDEX CONCURRENTLY idx_customers_store_email ON retail.customers (store_id, email);",
    
        
    
        # Analytics indexes
    
        "CREATE INDEX CONCURRENTLY idx_orders_date_amount ON retail.orders (order_date, total_amount);",
    
        "CREATE INDEX CONCURRENTLY idx_products_category_price ON retail.products (category_id, unit_price);",
    
        
    
        # Vector search optimization
    
        "CREATE INDEX CONCURRENTLY idx_embeddings_vector ON retail.product_description_embeddings USING ivfflat (description_embedding vector_cosine_ops) WITH (lists = 100);",
    
    ]
    
    

    ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„ฑ๋Šฅ

    ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๋ชจ๋ฒ” ์‚ฌ๋ก€
    
    import asyncio
    
    from asyncio import Semaphore
    
    from typing import List, Any
    
    
    
    class AsyncOptimizer:
    
        """Async operation optimization patterns."""
    
        
    
        def __init__(self, max_concurrent: int = 10):
    
            self.semaphore = Semaphore(max_concurrent)
    
            self.circuit_breaker = CircuitBreaker()
    
        
    
        async def batch_process(
    
            self, 
    
            items: List[Any], 
    
            process_func: callable,
    
            batch_size: int = 100
    
        ):
    
            """Process items in optimized batches."""
    
            
    
            async def process_batch(batch):
    
                async with self.semaphore:
    
                    return await asyncio.gather(
    
                        *[process_func(item) for item in batch],
    
                        return_exceptions=True
    
                    )
    
            
    
            # Process in batches to avoid overwhelming the system
    
            results = []
    
            for i in range(0, len(items), batch_size):
    
                batch = items[i:i + batch_size]
    
                batch_results = await process_batch(batch)
    
                results.extend(batch_results)
    
                
    
                # Small delay between batches to prevent resource exhaustion
    
                if i + batch_size < len(items):
    
                    await asyncio.sleep(0.1)
    
            
    
            return results
    
        
    
        @circuit_breaker_decorator
    
        async def resilient_operation(self, operation: callable, *args, **kwargs):
    
            """Execute operation with circuit breaker protection."""
    
            return await operation(*args, **kwargs)
    
    
    
    # Circuit breaker implementation
    
    class CircuitBreaker:
    
        """Circuit breaker for external service calls."""
    
        
    
        def __init__(self, failure_threshold: int = 5, recovery_timeout: int = 60):
    
            self.failure_threshold = failure_threshold
    
            self.recovery_timeout = recovery_timeout
    
            self.failure_count = 0
    
            self.last_failure_time = None
    
            self.state = "CLOSED"  # CLOSED, OPEN, HALF_OPEN
    
        
    
        async def call(self, func, *args, **kwargs):
    
            """Execute function with circuit breaker protection."""
    
            
    
            if self.state == "OPEN":
    
                if time.time() - self.last_failure_time > self.recovery_timeout:
    
                    self.state = "HALF_OPEN"
    
                else:
    
                    raise Exception("Circuit breaker is OPEN")
    
            
    
            try:
    
                result = await func(*args, **kwargs)
    
                
    
                # Reset on success
    
                if self.state == "HALF_OPEN":
    
                    self.state = "CLOSED"
    
                    self.failure_count = 0
    
                
    
                return result
    
                
    
            except Exception as e:
    
                self.failure_count += 1
    
                self.last_failure_time = time.time()
    
                
    
                if self.failure_count >= self.failure_threshold:
    
                    self.state = "OPEN"
    
                
    
                raise
    
    

    ์บ์‹ฑ ์ „๋žต

    
    import redis
    
    import pickle
    
    from typing import Union, Optional
    
    
    
    class SmartCache:
    
        """Multi-level caching system."""
    
        
    
        def __init__(self, redis_url: Optional[str] = None):
    
            self.memory_cache = {}
    
            self.redis_client = redis.Redis.from_url(redis_url) if redis_url else None
    
            self.max_memory_items = 1000
    
        
    
        async def get(self, key: str) -> Optional[Any]:
    
            """Get from cache with fallback levels."""
    
            
    
            # Level 1: Memory cache
    
            if key in self.memory_cache:
    
                return self.memory_cache[key]['value']
    
            
    
            # Level 2: Redis cache
    
            if self.redis_client:
    
                try:
    
                    cached_data = self.redis_client.get(key)
    
                    if cached_data:
    
                        value = pickle.loads(cached_data)
    
                        
    
                        # Promote to memory cache
    
                        self._set_memory_cache(key, value)
    
                        return value
    
                except Exception as e:
    
                    logger.warning(f"Redis cache error: {e}")
    
            
    
            return None
    
        
    
        async def set(
    
            self, 
    
            key: str, 
    
            value: Any, 
    
            ttl: int = 300,
    
            cache_level: str = "both"
    
        ):
    
            """Set cache value at specified levels."""
    
            
    
            if cache_level in ["memory", "both"]:
    
                self._set_memory_cache(key, value, ttl)
    
            
    
            if cache_level in ["redis", "both"] and self.redis_client:
    
                try:
    
                    self.redis_client.setex(
    
                        key, 
    
                        ttl, 
    
                        pickle.dumps(value)
    
                    )
    
                except Exception as e:
    
                    logger.warning(f"Redis set error: {e}")
    
        
    
        def _set_memory_cache(self, key: str, value: Any, ttl: int = 300):
    
            """Set value in memory cache with LRU eviction."""
    
            
    
            # Implement LRU eviction
    
            if len(self.memory_cache) >= self.max_memory_items:
    
                oldest_key = min(
    
                    self.memory_cache.keys(),
    
                    key=lambda k: self.memory_cache[k]['timestamp']
    
                )
    
                del self.memory_cache[oldest_key]
    
            
    
            self.memory_cache[key] = {
    
                'value': value,
    
                'timestamp': time.time(),
    
                'ttl': ttl
    
            }
    
    
    
    # Cache key generation
    
    def generate_cache_key(query: str, user_context: str, params: dict = None) -> str:
    
        """Generate consistent cache keys."""
    
        key_components = [
    
            query.strip().lower(),
    
            user_context,
    
            json.dumps(params, sort_keys=True) if params else ""
    
        ]
    
        
    
        key_string = "|".join(key_components)
    
        return hashlib.sha256(key_string.encode()).hexdigest()
    
    

    ๐Ÿ”’ ๋ณด์•ˆ ๊ฐ•ํ™”

    ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ

    
    from azure.identity import DefaultAzureCredential, ClientSecretCredential
    
    from azure.keyvault.secrets import SecretClient
    
    import jwt
    
    from typing import Dict, List
    
    
    
    class SecurityManager:
    
        """Comprehensive security management."""
    
        
    
        def __init__(self):
    
            self.key_vault_client = self._setup_key_vault()
    
            self.token_blacklist = set()
    
            
    
        def _setup_key_vault(self) -> SecretClient:
    
            """Initialize Azure Key Vault client."""
    
            credential = DefaultAzureCredential()
    
            vault_url = os.getenv("AZURE_KEY_VAULT_URL")
    
            
    
            if vault_url:
    
                return SecretClient(vault_url=vault_url, credential=credential)
    
            return None
    
        
    
        async def validate_request(self, request_headers: Dict[str, str]) -> Dict[str, Any]:
    
            """Comprehensive request validation."""
    
            
    
            # Extract and validate authentication
    
            auth_token = request_headers.get("authorization", "").replace("Bearer ", "")
    
            if not auth_token:
    
                raise AuthenticationError("Missing authentication token")
    
            
    
            # Validate token
    
            user_context = await self._validate_token(auth_token)
    
            
    
            # Check rate limiting
    
            await self._check_rate_limit(user_context["user_id"])
    
            
    
            # Validate RLS context
    
            rls_user_id = request_headers.get("x-rls-user-id")
    
            if not self._validate_rls_access(user_context, rls_user_id):
    
                raise AuthorizationError("Invalid RLS context for user")
    
            
    
            return {
    
                "user_id": user_context["user_id"],
    
                "roles": user_context["roles"],
    
                "rls_user_id": rls_user_id,
    
                "permissions": user_context["permissions"]
    
            }
    
        
    
        async def _validate_token(self, token: str) -> Dict[str, Any]:
    
            """Validate JWT token."""
    
            
    
            if token in self.token_blacklist:
    
                raise AuthenticationError("Token has been revoked")
    
            
    
            try:
    
                # Get public key from Key Vault or cache
    
                public_key = await self._get_public_key()
    
                
    
                # Decode and validate token
    
                payload = jwt.decode(
    
                    token, 
    
                    public_key, 
    
                    algorithms=["RS256"],
    
                    audience="mcp-server",
    
                    issuer="zava-auth"
    
                )
    
                
    
                return {
    
                    "user_id": payload["sub"],
    
                    "roles": payload.get("roles", []),
    
                    "permissions": payload.get("permissions", []),
    
                    "expires_at": payload["exp"]
    
                }
    
                
    
            except jwt.InvalidTokenError as e:
    
                raise AuthenticationError(f"Invalid token: {e}")
    
        
    
        def _validate_rls_access(self, user_context: Dict, rls_user_id: str) -> bool:
    
            """Validate RLS context access."""
    
            
    
            # Super admins can access any context
    
            if "super_admin" in user_context["roles"]:
    
                return True
    
            
    
            # Store managers can only access their own store
    
            if "store_manager" in user_context["roles"]:
    
                allowed_stores = user_context.get("allowed_stores", [])
    
                return rls_user_id in allowed_stores
    
            
    
            # Regional managers can access multiple stores
    
            if "regional_manager" in user_context["roles"]:
    
                allowed_regions = user_context.get("allowed_regions", [])
    
                return self._check_store_in_regions(rls_user_id, allowed_regions)
    
            
    
            return False
    
    
    
    # Input validation and sanitization
    
    class InputValidator:
    
        """SQL injection prevention and input validation."""
    
        
    
        @staticmethod
    
        def validate_sql_query(query: str) -> bool:
    
            """Validate SQL query for safety."""
    
            
    
            # Forbidden patterns
    
            forbidden_patterns = [
    
                r";\s*(DROP|DELETE|UPDATE|INSERT|ALTER|CREATE)\s+",
    
                r"--.*",
    
                r"/\*.*\*/",
    
                r"xp_cmdshell",
    
                r"sp_executesql",
    
                r"EXEC\s*\(",
    
            ]
    
            
    
            query_upper = query.upper()
    
            
    
            for pattern in forbidden_patterns:
    
                if re.search(pattern, query_upper, re.IGNORECASE):
    
                    logger.warning(f"Blocked potentially dangerous query: {pattern}")
    
                    return False
    
            
    
            # Only allow SELECT statements
    
            if not query_upper.strip().startswith("SELECT"):
    
                return False
    
            
    
            return True
    
        
    
        @staticmethod
    
        def sanitize_table_name(table_name: str) -> str:
    
            """Sanitize table name input."""
    
            
    
            # Only allow alphanumeric, underscore, and dot
    
            if not re.match(r"^[a-zA-Z0-9_.]+$", table_name):
    
                raise ValueError("Invalid table name format")
    
            
    
            # Validate against allowed tables
    
            if table_name not in VALID_TABLES:
    
                raise ValueError(f"Table {table_name} not allowed")
    
            
    
            return table_name
    
    

    ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ

    
    from cryptography.fernet import Fernet
    
    import hashlib
    
    
    
    class DataProtection:
    
        """Data encryption and protection utilities."""
    
        
    
        def __init__(self):
    
            self.encryption_key = self._get_encryption_key()
    
            self.cipher_suite = Fernet(self.encryption_key)
    
        
    
        def _get_encryption_key(self) -> bytes:
    
            """Get encryption key from secure storage."""
    
            
    
            # In production, get from Azure Key Vault
    
            key_vault_secret = os.getenv("ENCRYPTION_KEY_SECRET_NAME")
    
            if key_vault_secret and self.key_vault_client:
    
                secret = self.key_vault_client.get_secret(key_vault_secret)
    
                return secret.value.encode()
    
            
    
            # Fallback for development (not for production!)
    
            dev_key = os.getenv("DEV_ENCRYPTION_KEY")
    
            if dev_key:
    
                return dev_key.encode()
    
            
    
            raise ValueError("No encryption key available")
    
        
    
        def encrypt_sensitive_data(self, data: str) -> str:
    
            """Encrypt sensitive data."""
    
            return self.cipher_suite.encrypt(data.encode()).decode()
    
        
    
        def decrypt_sensitive_data(self, encrypted_data: str) -> str:
    
            """Decrypt sensitive data."""
    
            return self.cipher_suite.decrypt(encrypted_data.encode()).decode()
    
        
    
        @staticmethod
    
        def hash_password(password: str, salt: str = None) -> tuple:
    
            """Hash password with salt."""
    
            if not salt:
    
                salt = os.urandom(32).hex()
    
            
    
            password_hash = hashlib.pbkdf2_hmac(
    
                'sha256',
    
                password.encode(),
    
                salt.encode(),
    
                100000  # iterations
    
            ).hex()
    
            
    
            return password_hash, salt
    
        
    
        @staticmethod
    
        def mask_sensitive_logs(log_data: dict) -> dict:
    
            """Mask sensitive information in logs."""
    
            
    
            sensitive_fields = [
    
                'password', 'token', 'secret', 'key', 'authorization',
    
                'x-api-key', 'client_secret', 'connection_string'
    
            ]
    
            
    
            masked_data = log_data.copy()
    
            
    
            for field in sensitive_fields:
    
                if field in masked_data:
    
                    value = str(masked_data[field])
    
                    if len(value) > 4:
    
                        masked_data[field] = value[:2] + "*" * (len(value) - 4) + value[-2:]
    
                    else:
    
                        masked_data[field] = "***"
    
            
    
            return masked_data
    
    

    ๐Ÿ“Š ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ๊ฐ€์ด๋“œ๋ผ์ธ

    ์ฝ”๋“œ๋กœ์„œ์˜ ์ธํ”„๋ผ

    
    # azure-pipelines.yml
    
    trigger:
    
      branches:
    
        include:
    
          - main
    
          - release/*
    
    
    
    variables:
    
      - group: mcp-server-secrets
    
      - name: imageRepository
    
        value: 'zava-mcp-server'
    
      - name: containerRegistry
    
        value: 'zavamcpregistry.azurecr.io'
    
    
    
    stages:
    
    - stage: Build
    
      displayName: Build and Test
    
      jobs:
    
      - job: Build
    
        displayName: Build
    
        pool:
    
          vmImage: ubuntu-latest
    
        
    
        steps:
    
        - task: UsePythonVersion@0
    
          inputs:
    
            versionSpec: '3.11'
    
            displayName: 'Use Python 3.11'
    
        
    
        - script: |
    
            python -m pip install --upgrade pip
    
            pip install -r requirements.lock.txt
    
            pip install pytest pytest-cov
    
          displayName: 'Install dependencies'
    
        
    
        - script: |
    
            pytest tests/ --cov=mcp_server --cov-report=xml
    
          displayName: 'Run tests with coverage'
    
        
    
        - task: PublishCodeCoverageResults@1
    
          inputs:
    
            codeCoverageTool: Cobertura
    
            summaryFileLocation: 'coverage.xml'
    
        
    
        - task: Docker@2
    
          displayName: Build Docker image
    
          inputs:
    
            command: build
    
            repository: $(imageRepository)
    
            dockerfile: Dockerfile
    
            tags: |
    
              $(Build.BuildId)
    
              latest
    
    
    
    - stage: Deploy
    
      displayName: Deploy to Production
    
      dependsOn: Build
    
      condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    
      
    
      jobs:
    
      - deployment: DeployProduction
    
        displayName: Deploy to Production
    
        environment: 'production'
    
        pool:
    
          vmImage: ubuntu-latest
    
        
    
        strategy:
    
          runOnce:
    
            deploy:
    
              steps:
    
              - task: AzureContainerApps@1
    
                inputs:
    
                  azureSubscription: $(azureServiceConnection)
    
                  containerAppName: 'zava-mcp-server'
    
                  resourceGroup: '$(resourceGroupName)'
    
                  imageToDeploy: '$(containerRegistry)/$(imageRepository):$(Build.BuildId)'
    
    

    ์ปจํ…Œ์ด๋„ˆ ์ตœ์ ํ™”

    
    # Multi-stage Dockerfile for production
    
    FROM python:3.11-slim as builder
    
    
    
    # Install build dependencies
    
    RUN apt-get update && apt-get install -y \
    
        gcc \
    
        g++ \
    
        && rm -rf /var/lib/apt/lists/*
    
    
    
    # Create virtual environment
    
    RUN python -m venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Copy requirements and install Python dependencies
    
    COPY requirements.lock.txt .
    
    RUN pip install --no-cache-dir --upgrade pip && \
    
        pip install --no-cache-dir -r requirements.lock.txt
    
    
    
    # Production stage
    
    FROM python:3.11-slim as production
    
    
    
    # Create non-root user
    
    RUN groupadd -r mcpserver && useradd -r -g mcpserver mcpserver
    
    
    
    # Copy virtual environment from builder
    
    COPY --from=builder /opt/venv /opt/venv
    
    ENV PATH="/opt/venv/bin:$PATH"
    
    
    
    # Set working directory
    
    WORKDIR /app
    
    
    
    # Copy application code
    
    COPY mcp_server/ ./mcp_server/
    
    COPY --chown=mcpserver:mcpserver . .
    
    
    
    # Set security configurations
    
    RUN chmod -R 755 /app && \
    
        chown -R mcpserver:mcpserver /app
    
    
    
    # Switch to non-root user
    
    USER mcpserver
    
    
    
    # Health check
    
    HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    
        CMD curl -f http://localhost:8000/health || exit 1
    
    
    
    # Expose port
    
    EXPOSE 8000
    
    
    
    # Start application
    
    CMD ["python", "-m", "mcp_server.sales_analysis"]
    
    

    ํ™˜๊ฒฝ ๊ตฌ์„ฑ

    
    # Production configuration management
    
    class ProductionConfig:
    
        """Production-specific configuration."""
    
        
    
        def __init__(self):
    
            self.validate_production_requirements()
    
            self.setup_logging()
    
            self.configure_security()
    
        
    
        def validate_production_requirements(self):
    
            """Validate all required production settings."""
    
            
    
            required_settings = [
    
                "AZURE_CLIENT_ID",
    
                "AZURE_CLIENT_SECRET", 
    
                "AZURE_TENANT_ID",
    
                "PROJECT_ENDPOINT",
    
                "AZURE_OPENAI_ENDPOINT",
    
                "POSTGRES_HOST",
    
                "POSTGRES_PASSWORD",
    
                "APPLICATIONINSIGHTS_CONNECTION_STRING"
    
            ]
    
            
    
            missing_settings = [
    
                setting for setting in required_settings 
    
                if not os.getenv(setting)
    
            ]
    
            
    
            if missing_settings:
    
                raise EnvironmentError(
    
                    f"Missing required production settings: {missing_settings}"
    
                )
    
        
    
        def setup_logging(self):
    
            """Configure production logging."""
    
            
    
            logging.basicConfig(
    
                level=logging.INFO,
    
                format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    
                handlers=[
    
                    logging.StreamHandler(sys.stdout),
    
                    logging.handlers.RotatingFileHandler(
    
                        '/var/log/mcp-server.log',
    
                        maxBytes=50*1024*1024,  # 50MB
    
                        backupCount=5
    
                    )
    
                ]
    
            )
    
            
    
            # Set third-party loggers to WARNING
    
            logging.getLogger('azure').setLevel(logging.WARNING)
    
            logging.getLogger('urllib3').setLevel(logging.WARNING)
    
        
    
        def configure_security(self):
    
            """Configure production security settings."""
    
            
    
            # Disable debug mode
    
            os.environ['DEBUG'] = 'False'
    
            
    
            # Set secure headers
    
            os.environ['SECURE_SSL_REDIRECT'] = 'True'
    
            os.environ['SECURE_HSTS_SECONDS'] = '31536000'
    
            os.environ['SECURE_CONTENT_TYPE_NOSNIFF'] = 'True'
    
            os.environ['SECURE_BROWSER_XSS_FILTER'] = 'True'
    
    

    ๐Ÿ’ฐ ๋น„์šฉ ์ตœ์ ํ™”

    ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ

    
    class CostOptimizer:
    
        """Cost optimization strategies."""
    
        
    
        def __init__(self):
    
            self.metrics_collector = MetricsCollector()
    
            self.auto_scaler = AutoScaler()
    
        
    
        async def optimize_database_connections(self):
    
            """Dynamically adjust connection pool based on load."""
    
            
    
            current_load = await self.metrics_collector.get_current_load()
    
            
    
            if current_load < 0.3:  # Low load
    
                target_pool_size = max(2, int(current_load * 10))
    
            elif current_load < 0.7:  # Medium load
    
                target_pool_size = max(5, int(current_load * 15))
    
            else:  # High load
    
                target_pool_size = min(20, int(current_load * 25))
    
            
    
            await db_provider.adjust_pool_size(target_pool_size)
    
            
    
            logger.info(f"Adjusted pool size to {target_pool_size} for load {current_load}")
    
        
    
        async def implement_smart_caching(self):
    
            """Implement intelligent caching to reduce compute costs."""
    
            
    
            # Cache expensive operations
    
            expensive_queries = await self.identify_expensive_queries()
    
            
    
            for query in expensive_queries:
    
                cache_key = self.generate_cache_key(query)
    
                ttl = self.calculate_optimal_ttl(query)
    
                
    
                await smart_cache.set(cache_key, None, ttl=ttl)
    
        
    
        def calculate_azure_costs(self) -> Dict[str, float]:
    
            """Calculate estimated Azure resource costs."""
    
            
    
            return {
    
                "container_apps": self.estimate_container_costs(),
    
                "postgresql": self.estimate_database_costs(),
    
                "openai": self.estimate_ai_costs(),
    
                "application_insights": self.estimate_monitoring_costs(),
    
                "storage": self.estimate_storage_costs()
    
            }
    
    
    
    # Auto-scaling configuration
    
    class AutoScaler:
    
        """Automatic scaling based on metrics."""
    
        
    
        async def scale_decision(self) -> str:
    
            """Determine scaling action based on metrics."""
    
            
    
            metrics = await self.collect_scaling_metrics()
    
            
    
            # CPU-based scaling
    
            if metrics['cpu_usage'] > 80:
    
                return "scale_up"
    
            elif metrics['cpu_usage'] < 20 and metrics['instance_count'] > 1:
    
                return "scale_down"
    
            
    
            # Memory-based scaling
    
            if metrics['memory_usage'] > 85:
    
                return "scale_up"
    
            
    
            # Request queue scaling
    
            if metrics['queue_length'] > 100:
    
                return "scale_up"
    
            elif metrics['queue_length'] < 10 and metrics['instance_count'] > 1:
    
                return "scale_down"
    
            
    
            return "no_action"
    
    

    ๐Ÿ”ง ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์šด์˜

    ์ƒํƒœ ๋ชจ๋‹ˆํ„ฐ๋ง

    
    class OperationalHealth:
    
        """Comprehensive operational health monitoring."""
    
        
    
        def __init__(self):
    
            self.alert_manager = AlertManager()
    
            self.health_checks = {}
    
            
    
        async def comprehensive_health_check(self) -> Dict[str, Any]:
    
            """Perform comprehensive system health check."""
    
            
    
            health_report = {
    
                "timestamp": datetime.utcnow().isoformat(),
    
                "overall_status": "healthy",
    
                "components": {}
    
            }
    
            
    
            # Database health
    
            db_health = await self.check_database_health()
    
            health_report["components"]["database"] = db_health
    
            
    
            # External services health
    
            ai_health = await self.check_ai_service_health()
    
            health_report["components"]["ai_service"] = ai_health
    
            
    
            # System resources
    
            system_health = await self.check_system_resources()
    
            health_report["components"]["system"] = system_health
    
            
    
            # Application metrics
    
            app_health = await self.check_application_health()
    
            health_report["components"]["application"] = app_health
    
            
    
            # Determine overall status
    
            failed_components = [
    
                name for name, status in health_report["components"].items()
    
                if status.get("status") != "healthy"
    
            ]
    
            
    
            if failed_components:
    
                health_report["overall_status"] = "unhealthy"
    
                health_report["failed_components"] = failed_components
    
                
    
                # Trigger alerts
    
                await self.alert_manager.send_alert(
    
                    severity="high",
    
                    message=f"Health check failed for: {failed_components}",
    
                    details=health_report
    
                )
    
            
    
            return health_report
    
        
    
        async def check_database_health(self) -> Dict[str, Any]:
    
            """Check database connectivity and performance."""
    
            
    
            try:
    
                start_time = time.time()
    
                
    
                async with db_provider.get_connection() as conn:
    
                    # Basic connectivity
    
                    await conn.fetchval("SELECT 1")
    
                    
    
                    # Check slow queries
    
                    slow_queries = await conn.fetch("""
    
                        SELECT query, mean_exec_time, calls 
    
                        FROM pg_stat_statements 
    
                        WHERE mean_exec_time > 1000 
    
                        ORDER BY mean_exec_time DESC 
    
                        LIMIT 5
    
                    """)
    
                    
    
                    # Check connection count
    
                    connection_count = await conn.fetchval("""
    
                        SELECT count(*) FROM pg_stat_activity 
    
                        WHERE state = 'active'
    
                    """)
    
                    
    
                    response_time = time.time() - start_time
    
                    
    
                    return {
    
                        "status": "healthy",
    
                        "response_time_ms": response_time * 1000,
    
                        "active_connections": connection_count,
    
                        "slow_queries_count": len(slow_queries),
    
                        "pool_size": db_provider.connection_pool.get_size()
    
                    }
    
                    
    
            except Exception as e:
    
                return {
    
                    "status": "unhealthy",
    
                    "error": str(e),
    
                    "last_check": datetime.utcnow().isoformat()
    
                }
    
    
    
    # Automated backup and recovery
    
    class BackupManager:
    
        """Database backup and recovery management."""
    
        
    
        async def create_backup(self, backup_type: str = "full") -> str:
    
            """Create database backup."""
    
            
    
            timestamp = datetime.utcnow().strftime("%Y%m%d_%H%M%S")
    
            backup_name = f"zava_backup_{backup_type}_{timestamp}"
    
            
    
            if backup_type == "full":
    
                await self.create_full_backup(backup_name)
    
            elif backup_type == "incremental":
    
                await self.create_incremental_backup(backup_name)
    
            
    
            # Upload to Azure Blob Storage
    
            await self.upload_backup_to_azure(backup_name)
    
            
    
            return backup_name
    
        
    
        async def schedule_automated_backups(self):
    
            """Schedule regular automated backups."""
    
            
    
            # Daily full backup at 2 AM UTC
    
            schedule.every().day.at("02:00").do(
    
                lambda: asyncio.create_task(self.create_backup("full"))
    
            )
    
            
    
            # Hourly incremental backups
    
            schedule.every().hour.do(
    
                lambda: asyncio.create_task(self.create_backup("incremental"))
    
            )
    
    

    ๐ŸŒ ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ์—ฌ

    ์˜คํ”ˆ ์†Œ์Šค ๋ชจ๋ฒ” ์‚ฌ๋ก€

    
    # Contributing to MCP Database Integration
    
    
    
    ## Development Guidelines
    
    
    
    ### Code Quality Standards
    
    - Follow PEP 8 for Python code style
    
    - Maintain test coverage above 90%
    
    - Use type hints throughout the codebase
    
    - Write comprehensive docstrings
    
    
    
    ### Testing Requirements
    
    - Unit tests for all new functionality
    
    - Integration tests for database operations
    
    - Performance benchmarks for critical paths
    
    - Security tests for authentication/authorization
    
    
    
    ### Documentation Standards
    
    - Update README.md for any new features
    
    - Add inline code documentation
    
    - Create examples for new tools or patterns
    
    - Maintain API documentation
    
    
    
    ## Security Considerations
    
    
    
    ### Reporting Security Issues
    
    - Report security vulnerabilities privately
    
    - Use encrypted communication channels
    
    - Provide detailed reproduction steps
    
    - Include potential impact assessment
    
    
    
    ### Security Review Process
    
    - All PRs undergo security review
    
    - Static analysis tools required to pass
    
    - Dependency vulnerability scanning
    
    - Manual security testing for critical changes
    
    

    ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ

    
    class CommunityContributor:
    
        """Tools for community engagement and contribution."""
    
        
    
        @staticmethod
    
        def generate_contribution_guide():
    
            """Generate personalized contribution guide."""
    
            
    
            return {
    
                "getting_started": {
    
                    "setup": "Follow setup guide in Lab 03",
    
                    "first_contribution": "Start with documentation improvements",
    
                    "testing": "Run full test suite before submitting PR"
    
                },
    
                
    
                "contribution_areas": {
    
                    "documentation": "Improve learning labs and examples",
    
                    "testing": "Add test cases and improve coverage",
    
                    "features": "Implement new MCP tools and capabilities",
    
                    "performance": "Optimize queries and caching",
    
                    "security": "Enhance security measures and validation"
    
                },
    
                
    
                "community_resources": {
    
                    "discord": "https://discord.com/invite/ByRwuEEgH4",
    
                    "discussions": "GitHub Discussions for Q&A",
    
                    "issues": "GitHub Issues for bug reports",
    
                    "examples": "Share your implementation examples"
    
                }
    
            }
    
        
    
        @staticmethod
    
        def validate_contribution(pr_data: Dict) -> Dict[str, bool]:
    
            """Validate contribution meets standards."""
    
            
    
            return {
    
                "has_tests": "test" in pr_data.get("files_changed", []),
    
                "has_documentation": "README" in str(pr_data.get("files_changed", [])),
    
                "follows_conventions": True,  # Would implement actual checks
    
                "security_reviewed": pr_data.get("security_review", False),
    
                "performance_tested": pr_data.get("benchmark_results", False)
    
            }
    
    

    ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์ข…ํ•ฉ ํ•™์Šต ๊ฒฝ๋กœ๋ฅผ ์™„๋ฃŒํ•œ ํ›„, ๋‹ค์Œ์„ ์ˆ™๋‹ฌํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

    โœ… ์„ฑ๋Šฅ ์ตœ์ ํ™”: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํŠœ๋‹, ๋น„๋™๊ธฐ ํŒจํ„ด ๋ฐ ์บ์‹ฑ ์ „๋žต

    โœ… ๋ณด์•ˆ ๊ฐ•ํ™”: ์ธ์ฆ, ๊ถŒํ•œ ๋ถ€์—ฌ ๋ฐ ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ

    โœ… ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ: ์ฝ”๋“œ๋กœ์„œ์˜ ์ธํ”„๋ผ ๋ฐ ์ปจํ…Œ์ด๋„ˆ ์ตœ์ ํ™”

    โœ… ๋น„์šฉ ๊ด€๋ฆฌ: ๋ฆฌ์†Œ์Šค ์ตœ์ ํ™” ๋ฐ ์ง€๋Šฅํ˜• ํ™•์žฅ

    โœ… ์šด์˜ ์šฐ์ˆ˜์„ฑ: ๋ชจ๋‹ˆํ„ฐ๋ง, ์œ ์ง€๋ณด์ˆ˜ ๋ฐ ์ž๋™ํ™”

    โœ… ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ: MCP ์ƒํƒœ๊ณ„์— ๊ธฐ์—ฌ

    ๐Ÿ† ์ธ์ฆ ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„

    ์‹ค์Šต ํ‰๊ฐ€

    ๋‹ค์Œ ํ”„๋กœ์ ํŠธ๋ฅผ ์™„๋ฃŒํ•˜์—ฌ ์ˆ™๋ จ๋„๋ฅผ ์ž…์ฆํ•˜์„ธ์š”:

    ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„ ๊ตฌ์ถ• ํฌํ•จ:

  • [ ] RLS๋ฅผ ํ™œ์šฉํ•œ ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ์†Œ๋งค ๋ถ„์„
  • [ ] Azure OpenAI๋ฅผ ํ™œ์šฉํ•œ ์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰
  • [ ] ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ๊ตฌํ˜„
  • [ ] Azure์—์„œ์˜ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ
  • [ ] ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฒฝ๊ณ  ์„ค์ •
  • [ ] ๋ฌธ์„œํ™” ๋ฐ ํ…Œ์ŠคํŠธ
  • ๊ณ ๊ธ‰ ํ•™์Šต ๊ฒฝ๋กœ

    MCP ์—ฌ์ •์„ ๊ณ„์†ํ•˜์„ธ์š”:

  • MCP ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด: ๊ณ ๊ธ‰ ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜
  • ๋ฉ€ํ‹ฐ ๋ชจ๋ธ ํ†ตํ•ฉ: ๋‹ค์–‘ํ•œ AI ๋ชจ๋ธ ๊ฒฐํ•ฉ
  • ๊ธฐ์—… ๊ทœ๋ชจ: ๋Œ€๊ทœ๋ชจ MCP ๋ฐฐํฌ
  • ๋งž์ถคํ˜• ๋„๊ตฌ ๊ฐœ๋ฐœ: ์ „๋ฌธ MCP ๋„๊ตฌ ๊ตฌ์ถ•
  • MCP ์ƒํƒœ๊ณ„: ๊ด‘๋ฒ”์œ„ํ•œ ์ปค๋ฎค๋‹ˆํ‹ฐ์— ๊ธฐ์—ฌ
  • ์ปค๋ฎค๋‹ˆํ‹ฐ ์ธ์ •

    ์„ฑ๊ณผ๋ฅผ ๊ณต์œ ํ•˜์„ธ์š”:

  • GitHub ํฌํŠธํด๋ฆฌ์˜ค: ๊ตฌํ˜„ ์‚ฌ๋ก€๋ฅผ ๊ณต๊ฐœ
  • ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ์—ฌ: ๊ฐœ์„  ์‚ฌํ•ญ ๋˜๋Š” ์˜ˆ์ œ ์ œ์ถœ
  • ๋ฐœํ‘œ ๊ธฐํšŒ: ๋ฐ‹์—… ๋˜๋Š” ์ปจํผ๋Ÿฐ์Šค์—์„œ ๋ฐœํ‘œ
  • ๋ฉ˜ํ† ๋ง: ๋‹ค๋ฅธ ๊ฐœ๋ฐœ์ž๊ฐ€ MCP๋ฅผ ๋ฐฐ์šฐ๋„๋ก ์ง€์›
  • ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    ๊ณ ๊ธ‰ ์ฃผ์ œ

  • PostgreSQL ์„ฑ๋Šฅ ํŠœ๋‹ - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ตœ์ ํ™”
  • Azure ์ปจํ…Œ์ด๋„ˆ ์•ฑ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ
  • Python ๋น„๋™๊ธฐ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๋น„๋™๊ธฐ ํ”„๋กœ๊ทธ๋ž˜๋ฐ
  • ๋ณด์•ˆ ์ž๋ฃŒ

  • OWASP Top 10 - ๋ณด์•ˆ ์ทจ์•ฝ์ 
  • Azure ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ํด๋ผ์šฐ๋“œ ๋ณด์•ˆ
  • Python ๋ณด์•ˆ ๊ฐ€์ด๋“œ๋ผ์ธ - ์•ˆ์ „ํ•œ ์ฝ”๋”ฉ
  • ์ปค๋ฎค๋‹ˆํ‹ฐ

  • MCP ์ปค๋ฎค๋‹ˆํ‹ฐ Discord - ์‹ค์‹œ๊ฐ„ ํ† ๋ก 
  • GitHub Discussions - Q&A ๋ฐ ๊ณต์œ 
  • Stack Overflow - ๊ธฐ์ˆ  ์งˆ๋ฌธ
  • ---

    ๐ŸŽ‰ ์ถ•ํ•˜ํ•ฉ๋‹ˆ๋‹ค! MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ํ•™์Šต ๊ฒฝ๋กœ๋ฅผ ์™„์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. ์ด์ œ AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์‹œ์Šคํ…œ์„ ์—ฐ๊ฒฐํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ๋Š” ์ง€์‹๊ณผ ๊ธฐ์ˆ ์„ ๊ฐ–์ถ”๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ๊ธฐ์—ฌํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ฐธ์—ฌํ•˜์—ฌ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๊ฑฐ๋‚˜ ์ฝ”๋“œ ๊ฐœ์„ ์— ๊ธฐ์—ฌํ•˜๊ฑฐ๋‚˜ ์ถ”๊ฐ€ ํ•™์Šต ์ž๋ฃŒ๋ฅผ ๋งŒ๋“ค์–ด ๋‹ค๋ฅธ ์‚ฌ๋žŒ๋“ค์ด MCP๋ฅผ ๋ฐฐ์šฐ๋„๋ก ๋„์™€์ฃผ์„ธ์š”.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ๐Ÿ’ป ๊ตฌ์ถ•ํ•  ๋‚ด์šฉ

    ํ•™์Šต ๊ฒฝ๋กœ๋ฅผ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์™„์ „ํ•œ Zava ์†Œ๋งค ๋ถ„์„ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๊ฒŒ ๋ฉ๋‹ˆ๋‹ค:

  • ๊ณ ๊ฐ ์ฃผ๋ฌธ, ์ƒํ’ˆ, ์žฌ๊ณ ๊ฐ€ ํฌํ•จ๋œ ๋‹ค์ค‘ ํ…Œ์ด๋ธ” ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
  • ๋งค์žฅ ๊ธฐ๋ฐ˜ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ
  • Azure OpenAI ์ž„๋ฒ ๋”ฉ์„ ํ™œ์šฉํ•œ ์‹œ๋งจํ‹ฑ ์ƒํ’ˆ ๊ฒ€์ƒ‰
  • ์ž์—ฐ์–ด ์งˆ์˜๋ฅผ ์œ„ํ•œ VS Code AI ์ฑ„ํŒ… ํ†ตํ•ฉ
  • Docker์™€ Azure๋ฅผ ํ†ตํ•œ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ ์ค€๋น„
  • Application Insights๋ฅผ ํ™œ์šฉํ•œ ํฌ๊ด„์  ๋ชจ๋‹ˆํ„ฐ๋ง
  • ๐ŸŽฏ ํ•™์Šต ์ „์ œ ์กฐ๊ฑด

    ์ด ํ•™์Šต ๊ฒฝ๋กœ๋ฅผ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜๋ ค๋ฉด ๋‹ค์Œ ์‚ฌํ•ญ์„ ๊ฐ–์ถ”์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • ํ”„๋กœ๊ทธ๋ž˜๋ฐ ๊ฒฝํ—˜: Python(๊ถŒ์žฅ) ๋˜๋Š” ์œ ์‚ฌ ์–ธ์–ด์— ์นœ์ˆ™
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ง€์‹: SQL๊ณผ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ธฐ๋ณธ ์ดํ•ด
  • API ๊ฐœ๋…: REST API ๋ฐ HTTP ๊ธฐ๋ณธ ๊ฐœ๋… ์ดํ•ด
  • ๊ฐœ๋ฐœ ๋„๊ตฌ ์‚ฌ์šฉ ๊ฒฝํ—˜: ์ปค๋งจ๋“œ๋ผ์ธ, Git, ์ฝ”๋“œ ํŽธ์ง‘๊ธฐ
  • ํด๋ผ์šฐ๋“œ ๊ธฐ์ดˆ: (์„ ํƒ) Azure ๋˜๋Š” ์œ ์‚ฌ ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ ๊ธฐ๋ณธ ์ง€์‹
  • Docker ์ดํ•ด: (์„ ํƒ) ์ปจํ…Œ์ด๋„ˆํ™” ๊ฐœ๋… ์ดํ•ด
  • ํ•„์ˆ˜ ๋„๊ตฌ

  • Docker Desktop - PostgreSQL ๋ฐ MCP ์„œ๋ฒ„ ์‹คํ–‰์šฉ
  • Azure CLI - ํด๋ผ์šฐ๋“œ ๋ฆฌ์†Œ์Šค ๋ฐฐํฌ์šฉ
  • VS Code - ๊ฐœ๋ฐœ ๋ฐ MCP ํ†ตํ•ฉ์šฉ
  • Git - ๋ฒ„์ „ ๊ด€๋ฆฌ์šฉ
  • Python 3.8+ - MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ์šฉ
  • ๐Ÿ“š ํ•™์Šต ๊ฐ€์ด๋“œ ๋ฐ ์ž๋ฃŒ

    ์ด ํ•™์Šต ๊ฒฝ๋กœ์—๋Š” ํšจ๊ณผ์ ์ธ ํ•™์Šต์„ ๋„์™€์ค„ ๋‹ค์–‘ํ•œ ์ž๋ฃŒ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

    ํ•™์Šต ๊ฐ€์ด๋“œ

    ๊ฐ ์‹ค์Šต์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:

  • ๋ช…ํ™•ํ•œ ํ•™์Šต ๋ชฉํ‘œ - ๋‹ฌ์„ฑํ•  ๋‚ด์šฉ
  • ๋‹จ๊ณ„๋ณ„ ์ง€์นจ - ์ƒ์„ธ ๊ตฌํ˜„ ์•ˆ๋‚ด
  • ์ฝ”๋“œ ์˜ˆ์ œ - ์ž‘๋™ ์ƒ˜ํ”Œ๊ณผ ์„ค๋ช…
  • ์‹ค์Šต ๋ฌธ์ œ - ์ง์ ‘ ์‹ค์Šตํ•  ๊ธฐํšŒ
  • ๋ฌธ์ œ ํ•ด๊ฒฐ ๊ฐ€์ด๋“œ - ์ผ๋ฐ˜ ๋ฌธ์ œ์™€ ํ•ด๊ฒฐ์ฑ…
  • ์ถ”๊ฐ€ ์ž๋ฃŒ - ๋” ๊นŠ์ด ์žˆ๋Š” ์ฝ์„๊ฑฐ๋ฆฌ
  • ์ „์ œ ์กฐ๊ฑด ํ™•์ธ

    ์‹ค์Šต์„ ์‹œ์ž‘ํ•˜๊ธฐ ์ „์—:

  • ํ•„์š” ์ง€์‹ - ์‚ฌ์ „ ํ•™์Šต ๋‚ด์šฉ
  • ํ™˜๊ฒฝ ํ™•์ธ - ํ™˜๊ฒฝ ์„ค์ • ๊ฒ€์ฆ ๋ฐฉ๋ฒ•
  • ์˜ˆ์ƒ ์†Œ์š” ์‹œ๊ฐ„ - ์™„๋ฃŒ ์˜ˆ์ƒ ์‹œ๊ฐ„
  • ํ•™์Šต ๊ฒฐ๊ณผ - ์™„๋ฃŒ ํ›„ ์–ป๊ฒŒ ๋  ์ง€์‹
  • ์ถ”์ฒœ ํ•™์Šต ๊ฒฝ๋กœ

    ๊ฒฝํ—˜ ์ˆ˜์ค€์— ๋”ฐ๋ผ ๊ฒฝ๋กœ๋ฅผ ์„ ํƒํ•˜์„ธ์š”:

    ๐ŸŸข ์ดˆ๊ธ‰ ๊ฒฝ๋กœ (MCP ์ฒ˜์Œ ์ ‘ํ•˜๋Š” ๋ถ„)

    1. ๋จผ์ € MCP ์ดˆ๋ณด์ž์šฉ 0-10์„ ์™„๋ฃŒํ•˜์„ธ์š”

    2. 00-03 ์‹ค์Šต์œผ๋กœ ๊ธฐ์ดˆ๋ฅผ ํ™•์‹คํžˆ ํ•˜์„ธ์š”

    3. 04-06 ์‹ค์Šต์œผ๋กœ ์ง์ ‘ ๊ตฌ์ถ•ํ•ด ๋ณด์„ธ์š”

    4. 07-09 ์‹ค์Šต์œผ๋กœ ์‹ค์ œ ํ™œ์šฉ ๊ฒฝํ—˜์„ ์Œ“์œผ์„ธ์š”

    ๐ŸŸก ์ค‘๊ธ‰ ๊ฒฝ๋กœ (MCP ๊ฒฝํ—˜ ์ผ๋ถ€ ๋ณด์œ )

    1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ฐœ๋… ์ •๋ฆฌ๋ฅผ ์œ„ํ•ด 00-01 ์‹ค์Šต ์ ๊ฒ€

    2. 02-06 ์‹ค์Šต์— ์ง‘์ค‘ํ•ด ๊ตฌํ˜„ ๋Šฅ๋ ฅ ๊ฐ•ํ™”

    3. 07-12 ์‹ค์Šต์—์„œ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ ์‹ฌํ™” ํ•™์Šต

    ๐Ÿ”ด ๊ณ ๊ธ‰ ๊ฒฝ๋กœ (MCP ์ˆ™๋ จ์ž)

    1. ์ „๋ฐ˜์ ์ธ ๋งฅ๋ฝ ํŒŒ์•… ์œ„ํ•ด 00-03 ์‹ค์Šต ๋น ๋ฅด๊ฒŒ ์กฐํšŒ

    2. 04-09 ์‹ค์Šต์— ์ง‘์ค‘ํ•˜์—ฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ์‹ฌํ™”

    3. 10-12 ์‹ค์Šต์œผ๋กœ ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ์— ์ง‘์ค‘

    ๐Ÿ› ๏ธ ํšจ๊ณผ์ ์ธ ํ•™์Šต๋ฒ•

    ์ˆœ์ฐจ์  ํ•™์Šต (๊ถŒ์žฅ)

    ์ „์ฒด ๊ฐœ๋… ์ดํ•ด๋ฅผ ์œ„ํ•ด ์ฐจ๋ก€๋Œ€๋กœ ์‹ค์Šต ์ง„ํ–‰:

    1. ๊ฐœ์š” ์ฝ๊ธฐ - ๋ฐฐ์šธ ๋‚ด์šฉ์„ ์ดํ•ด

    2. ์ „์ œ ์กฐ๊ฑด ์ ๊ฒ€ - ์ง€์‹ ์ค€๋น„ ์—ฌ๋ถ€ ํ™•์ธ

    3. ๋‹จ๊ณ„๋ณ„ ๊ฐ€์ด๋“œ ๋”ฐ๋ฅด๊ธฐ - ํ•™์Šตํ•˜๋ฉด์„œ ๊ตฌํ˜„

    4. ์‹ค์Šต ์™„์ˆ˜ - ์ดํ•ด๋„ ๊ฐ•ํ™”

    5. ํ•ต์‹ฌ ๋‚ด์šฉ ๋ณต์Šต - ๋ฐฐ์šด ๋‚ด์šฉ ํ™•์‹คํžˆ ์ •๋ฆฌ

    ๋ชฉํ‘œ๋ณ„ ํ•™์Šต

    ํŠน์ • ๊ธฐ์ˆ ์ด ํ•„์š”ํ•˜๋ฉด:

  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ: 04-06 ์‹ค์Šต ์ง‘์ค‘
  • ๋ณด์•ˆ ๊ตฌํ˜„: 02, 08, 12 ์‹ค์Šต ์ง‘์ค‘
  • AI/์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰: 07 ์‹ค์Šต ์ง‘์ค‘
  • ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ: 10-12 ์‹ค์Šต ์ง‘์ค‘
  • ์‹ค์Šต ์ค‘์‹ฌ ํ•™์Šต

    ๊ฐ ์‹ค์Šต์—๋Š”:

  • ์ž‘๋™ํ•˜๋Š” ์ฝ”๋“œ ์˜ˆ์ œ - ๋ณต์‚ฌ, ์ˆ˜์ •, ์‹คํ—˜ ๊ฐ€๋Šฅ
  • ์‹ค์ œ ์‹œ๋‚˜๋ฆฌ์˜ค - ์†Œ๋งค ๋ถ„์„ ์‹ค์šฉ ์‚ฌ๋ก€
  • ์ ์ง„์  ๋‚œ์ด๋„ - ๊ฐ„๋‹จํ•œ ๊ฒƒ๋ถ€ํ„ฐ ๊ณ ๊ธ‰๊นŒ์ง€
  • ๊ฒ€์ฆ ๋‹จ๊ณ„ - ๊ตฌํ˜„ ๊ฒฐ๊ณผ ํ™•์ธ
  • ๐ŸŒŸ ์ปค๋ฎค๋‹ˆํ‹ฐ์™€ ์ง€์›

    ๋„์›€๋ฐ›๊ธฐ

  • Azure AI Discord: ์ „๋ฌธ๊ฐ€ ์ง€์› ์ฐธ์—ฌ
  • GitHub ์ €์žฅ์†Œ ๋ฐ ๊ตฌํ˜„ ์ƒ˜ํ”Œ: ๋ฐฐํฌ ์ƒ˜ํ”Œ ๋ฐ ์ž๋ฃŒ
  • MCP ์ปค๋ฎค๋‹ˆํ‹ฐ: ๋„“์€ MCP ํ† ๋ก  ์ฐธ์—ฌ
  • ๐Ÿš€ ์‹œ์ž‘ํ•  ์ค€๋น„๊ฐ€ ๋˜์—ˆ๋‚˜์š”?

    ์ง€๊ธˆ ๋ฐ”๋กœ ์‹ค์Šต 00: MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ์†Œ๊ฐœ

    MCP ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ์†Œ๊ฐœ

    ๐ŸŽฏ ์ด ์‹ค์Šต์—์„œ ๋‹ค๋ฃจ๋Š” ๋‚ด์šฉ

    ์ด ์ž…๋ฌธ ์‹ค์Šต์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ํ†ตํ•ด Model Context Protocol (MCP) ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ํฌ๊ด„์ ์ธ ๊ฐœ์š”๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail์˜ Zava Retail ๋ถ„์„ ์‚ฌ๋ก€๋ฅผ ํ†ตํ•ด ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ๋ก€, ๊ธฐ์ˆ  ์•„ํ‚คํ…์ฒ˜, ์‹ค์ œ ์‘์šฉ ์‚ฌ๋ก€๋ฅผ ์ดํ•ดํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ๊ฐœ์š”

    Model Context Protocol (MCP)์€ AI ์–ด์‹œ์Šคํ„ดํŠธ๊ฐ€ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์†Œ์Šค์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ์•ˆ์ „ํ•˜๊ฒŒ ์•ก์„ธ์Šคํ•˜๊ณ  ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ๊ณผ ๊ฒฐํ•ฉํ•˜๋ฉด MCP๋Š” ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ AI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์œ„ํ•œ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์ด ํ•™์Šต ๊ฒฝ๋กœ๋Š” PostgreSQL์„ ํ†ตํ•ด AI ์–ด์‹œ์Šคํ„ดํŠธ๋ฅผ ์†Œ๋งค ํŒ๋งค ๋ฐ์ดํ„ฐ์— ์—ฐ๊ฒฐํ•˜๊ณ , Row Level Security, ์˜๋ฏธ ๊ฒ€์ƒ‰, ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค์™€ ๊ฐ™์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ํ”„๋กœ๋•์…˜ ์ค€๋น„ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๊ฐ€๋ฅด์นฉ๋‹ˆ๋‹ค.

    ํ•™์Šต ๋ชฉํ‘œ

    ์ด ์‹ค์Šต์„ ์™„๋ฃŒํ•˜๋ฉด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ •์˜: Model Context Protocol๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์˜ ํ•ต์‹ฌ ์ด์ 
  • ์‹๋ณ„: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ํฌํ•จํ•œ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜์˜ ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ
  • ์ดํ•ด: Zava Retail ์‚ฌ๋ก€์™€ ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ ์‚ฌํ•ญ
  • ์ธ์‹: ์•ˆ์ „ํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ก์„ธ์Šค๋ฅผ ์œ„ํ•œ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด
  • ๋ชฉ๋ก ์ž‘์„ฑ: ์ด ํ•™์Šต ๊ฒฝ๋กœ์—์„œ ์‚ฌ์šฉ๋œ ๋„๊ตฌ์™€ ๊ธฐ์ˆ 
  • ๐Ÿงญ ๋„์ „ ๊ณผ์ œ: AI์™€ ์‹ค์ œ ๋ฐ์ดํ„ฐ์˜ ๋งŒ๋‚จ

    ๊ธฐ์กด AI์˜ ํ•œ๊ณ„

    ํ˜„๋Œ€์˜ AI ์–ด์‹œ์Šคํ„ดํŠธ๋Š” ๋งค์šฐ ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ์‹ค์ œ ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ์™€ ์ž‘์—…ํ•  ๋•Œ ์ค‘์š”ํ•œ ํ•œ๊ณ„๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค:

    ๋„์ „ ๊ณผ์ œ ์„ค๋ช… ๋น„์ฆˆ๋‹ˆ์Šค ์˜ํ–ฅ --------------- ----------------- ------------------- ์ •์  ์ง€์‹ ๊ณ ์ •๋œ ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ํ›ˆ๋ จ๋œ AI ๋ชจ๋ธ์€ ํ˜„์žฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐ์ดํ„ฐ๋ฅผ ์•ก์„ธ์Šคํ•  ์ˆ˜ ์—†์Œ ์˜ค๋ž˜๋œ ํ†ต์ฐฐ๋ ฅ, ๊ธฐํšŒ ์ƒ์‹ค ๋ฐ์ดํ„ฐ ์‚ฌ์ผ๋กœ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, API, ์‹œ์Šคํ…œ์— ์ž ๊ธด ์ •๋ณด๋กœ ์ธํ•ด AI๊ฐ€ ์ ‘๊ทผ ๋ถˆ๊ฐ€ ๋ถˆ์™„์ „ํ•œ ๋ถ„์„, ๋‹จํŽธํ™”๋œ ์›Œํฌํ”Œ๋กœ ๋ณด์•ˆ ์ œ์•ฝ ์ง์ ‘์ ์ธ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์•ก์„ธ์Šค๋Š” ๋ณด์•ˆ ๋ฐ ๊ทœ์ • ์ค€์ˆ˜ ๋ฌธ์ œ๋ฅผ ์•ผ๊ธฐ ์ œํ•œ๋œ ๋ฐฐํฌ, ์ˆ˜๋™ ๋ฐ์ดํ„ฐ ์ค€๋น„ ๋ณต์žกํ•œ ์ฟผ๋ฆฌ ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ์šฉ์ž๊ฐ€ ๋ฐ์ดํ„ฐ ํ†ต์ฐฐ๋ ฅ์„ ์ถ”์ถœํ•˜๋ ค๋ฉด ๊ธฐ์ˆ ์  ์ง€์‹์ด ํ•„์š” ๋‚ฎ์€ ์ฑ„ํƒ๋ฅ , ๋น„ํšจ์œจ์ ์ธ ํ”„๋กœ์„ธ์Šค

    MCP ์†”๋ฃจ์…˜

    Model Context Protocol์€ ๋‹ค์Œ์„ ํ†ตํ•ด ์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค:

  • ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค: AI ์–ด์‹œ์Šคํ„ดํŠธ๊ฐ€ ๋ผ์ด๋ธŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์™€ API๋ฅผ ์ฟผ๋ฆฌ
  • ์•ˆ์ „ํ•œ ํ†ตํ•ฉ: ์ธ์ฆ ๋ฐ ๊ถŒํ•œ์„ ํ†ตํ•œ ์ œ์–ด๋œ ์•ก์„ธ์Šค
  • ์ž์—ฐ์–ด ์ธํ„ฐํŽ˜์ด์Šค: ๋น„์ฆˆ๋‹ˆ์Šค ์‚ฌ์šฉ์ž๊ฐ€ ํ‰๋ฒ”ํ•œ ์˜์–ด๋กœ ์งˆ๋ฌธ
  • ํ‘œ์ค€ํ™”๋œ ํ”„๋กœํ† ์ฝœ: ๋‹ค์–‘ํ•œ AI ํ”Œ๋žซํผ ๋ฐ ๋„๊ตฌ์—์„œ ์ž‘๋™
  • ๐Ÿช Zava Retail ์†Œ๊ฐœ: ํ•™์Šต ์‚ฌ๋ก€ ์—ฐ๊ตฌ https://github.com/microsoft/MCP-Server-and-PostgreSQL-Sample-Retail

    ์ด ํ•™์Šต ๊ฒฝ๋กœ์—์„œ๋Š” Zava Retail์ด๋ผ๋Š” ๊ฐ€์ƒ์˜ DIY ์†Œ๋งค ์ฒด์ธ์„ ์œ„ํ•œ MCP ์„œ๋ฒ„๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. ์ด ํ˜„์‹ค์ ์ธ ์‹œ๋‚˜๋ฆฌ์˜ค๋Š” ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ MCP ๊ตฌํ˜„์„ ๋ณด์—ฌ์ค๋‹ˆ๋‹ค.

    ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐฐ๊ฒฝ

    Zava Retail์€ ๋‹ค์Œ์„ ์šด์˜ํ•ฉ๋‹ˆ๋‹ค:

  • ์›Œ์‹ฑํ„ด ์ฃผ ์ „์—ญ์— ๊ฑธ์นœ 8๊ฐœ์˜ ์˜คํ”„๋ผ์ธ ๋งค์žฅ (์‹œ์• ํ‹€, ๋ฒจ๋ทฐ, ํƒ€์ฝ”๋งˆ, ์Šคํฌ์บ”, ์—๋ฒ„๋ ›, ๋ ˆ๋“œ๋จผ๋“œ, ์ปคํด๋žœ๋“œ)
  • 1๊ฐœ์˜ ์˜จ๋ผ์ธ ๋งค์žฅ์„ ํ†ตํ•œ ์ „์ž์ƒ๊ฑฐ๋ž˜ ํŒ๋งค
  • ๋„๊ตฌ, ํ•˜๋“œ์›จ์–ด, ์ •์› ์šฉํ’ˆ, ๊ฑด์ถ• ์ž์žฌ๋ฅผ ํฌํ•จํ•œ ๋‹ค์–‘ํ•œ ์ œํ’ˆ ์นดํƒˆ๋กœ๊ทธ
  • ๋งค์žฅ ๊ด€๋ฆฌ์ž, ์ง€์—ญ ๊ด€๋ฆฌ์ž, ์ž„์›์„ ํฌํ•จํ•œ ๋‹ค๋‹จ๊ณ„ ๊ด€๋ฆฌ
  • ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ ์‚ฌํ•ญ

    ๋งค์žฅ ๊ด€๋ฆฌ์ž์™€ ์ž„์›์€ AI ๊ธฐ๋ฐ˜ ๋ถ„์„์„ ํ†ตํ•ด ๋‹ค์Œ์„ ์ˆ˜ํ–‰ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

    1. ๋งค์žฅ ๋ฐ ๊ธฐ๊ฐ„๋ณ„ ํŒ๋งค ์„ฑ๊ณผ ๋ถ„์„

    2. ์žฌ๊ณ  ์ˆ˜์ค€ ์ถ”์  ๋ฐ ์žฌ์ž…๊ณ  ํ•„์š”์„ฑ ์‹๋ณ„

    3. ๊ณ ๊ฐ ํ–‰๋™ ๋ฐ ๊ตฌ๋งค ํŒจํ„ด ์ดํ•ด

    4. ์˜๋ฏธ ๊ฒ€์ƒ‰์„ ํ†ตํ•œ ์ œํ’ˆ ํ†ต์ฐฐ๋ ฅ ๋ฐœ๊ฒฌ

    5. ์ž์—ฐ์–ด ์ฟผ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•œ ๋ณด๊ณ ์„œ ์ƒ์„ฑ

    6. ์—ญํ•  ๊ธฐ๋ฐ˜ ์•ก์„ธ์Šค ์ œ์–ด๋ฅผ ํ†ตํ•œ ๋ฐ์ดํ„ฐ ๋ณด์•ˆ ์œ ์ง€

    ๊ธฐ์ˆ  ์š”๊ตฌ ์‚ฌํ•ญ

    MCP ์„œ๋ฒ„๋Š” ๋‹ค์Œ์„ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

  • ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค: ๋งค์žฅ ๊ด€๋ฆฌ์ž๊ฐ€ ์ž์‹ ์˜ ๋งค์žฅ ๋ฐ์ดํ„ฐ๋งŒ ๋ณผ ์ˆ˜ ์žˆ๋„๋ก
  • ์œ ์—ฐํ•œ ์ฟผ๋ฆฌ: ๋ณต์žกํ•œ SQL ์ž‘์—… ์ง€์›
  • ์˜๋ฏธ ๊ฒ€์ƒ‰: ์ œํ’ˆ ๊ฒ€์ƒ‰ ๋ฐ ์ถ”์ฒœ
  • ์‹ค์‹œ๊ฐ„ ๋ฐ์ดํ„ฐ: ํ˜„์žฌ ๋น„์ฆˆ๋‹ˆ์Šค ์ƒํƒœ ๋ฐ˜์˜
  • ์•ˆ์ „ํ•œ ์ธ์ฆ: Row Level Security ํฌํ•จ
  • ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ์•„ํ‚คํ…์ฒ˜: ์—ฌ๋Ÿฌ ๋™์‹œ ์‚ฌ์šฉ์ž๋ฅผ ์ง€์›
  • ๐Ÿ—๏ธ MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”

    ์šฐ๋ฆฌ์˜ MCP ์„œ๋ฒ„๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์— ์ตœ์ ํ™”๋œ ๊ณ„์ธตํ˜• ์•„ํ‚คํ…์ฒ˜๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค:

    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                    VS Code AI Client                       โ”‚
    
    โ”‚                  (Natural Language Queries)                โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ HTTP/SSE
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                     MCP Server                             โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚   Tool Layer    โ”‚ โ”‚  Security Layer โ”‚ โ”‚  Config Layer โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Query Tools   โ”‚ โ”‚ โ€ข RLS Context   โ”‚ โ”‚ โ€ข Environment โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Schema Tools  โ”‚ โ”‚ โ€ข User Identity โ”‚ โ”‚ โ€ข Connections โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Search Tools  โ”‚ โ”‚ โ€ข Access Controlโ”‚ โ”‚ โ€ข Validation  โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ asyncpg
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                PostgreSQL Database                         โ”‚
    
    โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
    
    โ”‚  โ”‚  Retail Schema  โ”‚ โ”‚   RLS Policies  โ”‚ โ”‚   pgvector    โ”‚ โ”‚
    
    โ”‚  โ”‚                 โ”‚ โ”‚                 โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Stores        โ”‚ โ”‚ โ€ข Store-based   โ”‚ โ”‚ โ€ข Embeddings  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Customers     โ”‚ โ”‚   Isolation     โ”‚ โ”‚ โ€ข Similarity  โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Products      โ”‚ โ”‚ โ€ข Role Control  โ”‚ โ”‚   Search      โ”‚ โ”‚
    
    โ”‚  โ”‚ โ€ข Orders        โ”‚ โ”‚ โ€ข Audit Logs    โ”‚ โ”‚               โ”‚ โ”‚
    
    โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
                          โ”‚ REST API
    
                          โ–ผ
    
    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
    
    โ”‚                  Azure OpenAI                              โ”‚
    
    โ”‚               (Text Embeddings)                            โ”‚
    
    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
    
    

    ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ

    1. MCP ์„œ๋ฒ„ ๊ณ„์ธต
  • FastMCP Framework: ํ˜„๋Œ€์ ์ธ Python MCP ์„œ๋ฒ„ ๊ตฌํ˜„
  • ๋„๊ตฌ ๋“ฑ๋ก: ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๊ฐ–์ถ˜ ์„ ์–ธ์  ๋„๊ตฌ ์ •์˜
  • ์š”์ฒญ ์ปจํ…์ŠคํŠธ: ์‚ฌ์šฉ์ž ์‹ ์› ๋ฐ ์„ธ์…˜ ๊ด€๋ฆฌ
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ๊ฐ•๋ ฅํ•œ ์˜ค๋ฅ˜ ๊ด€๋ฆฌ ๋ฐ ๋กœ๊น…
  • 2. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ๊ณ„์ธต
  • ์—ฐ๊ฒฐ ํ’€๋ง: ํšจ์œจ์ ์ธ asyncpg ์—ฐ๊ฒฐ ๊ด€๋ฆฌ
  • ์Šคํ‚ค๋งˆ ์ œ๊ณต์ž: ๋™์  ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ ๊ฒ€์ƒ‰
  • ์ฟผ๋ฆฌ ์‹คํ–‰๊ธฐ: RLS ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•œ ์•ˆ์ „ํ•œ SQL ์‹คํ–‰
  • ํŠธ๋žœ์žญ์…˜ ๊ด€๋ฆฌ: ACID ์ค€์ˆ˜ ๋ฐ ๋กค๋ฐฑ ์ฒ˜๋ฆฌ
  • 3. ๋ณด์•ˆ ๊ณ„์ธต
  • Row Level Security: ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ์œ„ํ•œ PostgreSQL RLS
  • ์‚ฌ์šฉ์ž ์‹ ์›: ๋งค์žฅ ๊ด€๋ฆฌ์ž ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ
  • ์•ก์„ธ์Šค ์ œ์–ด: ์„ธ๋ถ„ํ™”๋œ ๊ถŒํ•œ ๋ฐ ๊ฐ์‚ฌ ๊ธฐ๋ก
  • ์ž…๋ ฅ ๊ฒ€์ฆ: SQL ์ธ์ ์…˜ ๋ฐฉ์ง€ ๋ฐ ์ฟผ๋ฆฌ ๊ฒ€์ฆ
  • 4. AI ๊ฐ•ํ™” ๊ณ„์ธต
  • ์˜๋ฏธ ๊ฒ€์ƒ‰: ์ œํ’ˆ ๊ฒ€์ƒ‰์„ ์œ„ํ•œ ๋ฒกํ„ฐ ์ž„๋ฒ ๋”ฉ
  • Azure OpenAI ํ†ตํ•ฉ: ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ์ƒ์„ฑ
  • ์œ ์‚ฌ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜: pgvector ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰
  • ๊ฒ€์ƒ‰ ์ตœ์ ํ™”: ์ธ๋ฑ์‹ฑ ๋ฐ ์„ฑ๋Šฅ ํŠœ๋‹
  • ๐Ÿ”ง ๊ธฐ์ˆ  ์Šคํƒ

    ํ•ต์‹ฌ ๊ธฐ์ˆ 

    ๊ตฌ์„ฑ ์š”์†Œ ๊ธฐ์ˆ  ๋ชฉ์  --------------- ---------------- ------------- MCP Framework FastMCP (Python) ํ˜„๋Œ€์ ์ธ MCP ์„œ๋ฒ„ ๊ตฌํ˜„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค PostgreSQL 17 + pgvector ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ์™€ ๋ฒกํ„ฐ ๊ฒ€์ƒ‰ AI ์„œ๋น„์Šค Azure OpenAI ํ…์ŠคํŠธ ์ž„๋ฒ ๋”ฉ ๋ฐ ์–ธ์–ด ๋ชจ๋ธ ์ปจํ…Œ์ด๋„ˆํ™” Docker + Docker Compose ๊ฐœ๋ฐœ ํ™˜๊ฒฝ ํด๋ผ์šฐ๋“œ ํ”Œ๋žซํผ Microsoft Azure ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ IDE ํ†ตํ•ฉ VS Code AI ์ฑ„ํŒ… ๋ฐ ๊ฐœ๋ฐœ ์›Œํฌํ”Œ๋กœ

    ๊ฐœ๋ฐœ ๋„๊ตฌ

    ๋„๊ตฌ ๋ชฉ์  ---------- ------------- asyncpg ๊ณ ์„ฑ๋Šฅ PostgreSQL ๋“œ๋ผ์ด๋ฒ„ Pydantic ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ ๋ฐ ์ง๋ ฌํ™” Azure SDK ํด๋ผ์šฐ๋“œ ์„œ๋น„์Šค ํ†ตํ•ฉ pytest ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ Docker ์ปจํ…Œ์ด๋„ˆํ™” ๋ฐ ๋ฐฐํฌ

    ํ”„๋กœ๋•์…˜ ์Šคํƒ

    ์„œ๋น„์Šค Azure ๋ฆฌ์†Œ์Šค ๋ชฉ์  ------------- ------------------- ------------- ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค Azure Database for PostgreSQL ๊ด€๋ฆฌํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋น„์Šค ์ปจํ…Œ์ด๋„ˆ Azure Container Apps ์„œ๋ฒ„๋ฆฌ์Šค ์ปจํ…Œ์ด๋„ˆ ํ˜ธ์ŠคํŒ… AI ์„œ๋น„์Šค Azure AI Foundry OpenAI ๋ชจ๋ธ ๋ฐ ์—”๋“œํฌ์ธํŠธ ๋ชจ๋‹ˆํ„ฐ๋ง Application Insights ๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ ๋ฐ ์ง„๋‹จ ๋ณด์•ˆ Azure Key Vault ๋น„๋ฐ€ ๋ฐ ๊ตฌ์„ฑ ๊ด€๋ฆฌ

    ๐ŸŽฌ ์‹ค์ œ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค

    ๋‹ค์–‘ํ•œ ์‚ฌ์šฉ์ž๊ฐ€ MCP ์„œ๋ฒ„์™€ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

    ์‹œ๋‚˜๋ฆฌ์˜ค 1: ๋งค์žฅ ๊ด€๋ฆฌ์ž ์„ฑ๊ณผ ๊ฒ€ํ† 

    ์‚ฌ์šฉ์ž: Sarah, ์‹œ์• ํ‹€ ๋งค์žฅ ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ์ง€๋‚œ ๋ถ„๊ธฐ์˜ ํŒ๋งค ์„ฑ๊ณผ ๋ถ„์„

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "2024๋…„ 4๋ถ„๊ธฐ ๋™์•ˆ ๋‚ด ๋งค์žฅ์—์„œ ๋งค์ถœ ๊ธฐ์ค€ ์ƒ์œ„ 10๊ฐœ ์ œํ’ˆ์„ ๋ณด์—ฌ์ค˜"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. VS Code AI ์ฑ„ํŒ…์ด ์ฟผ๋ฆฌ๋ฅผ MCP ์„œ๋ฒ„๋กœ ์ „์†ก

    2. MCP ์„œ๋ฒ„๊ฐ€ Sarah์˜ ๋งค์žฅ ์ปจํ…์ŠคํŠธ(์‹œ์• ํ‹€)๋ฅผ ์‹๋ณ„

    3. RLS ์ •์ฑ…์ด ๋ฐ์ดํ„ฐ๋ฅผ ์‹œ์• ํ‹€ ๋งค์žฅ์œผ๋กœ ํ•„ํ„ฐ๋ง

    4. SQL ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์‹คํ–‰๋จ

    5. ๊ฒฐ๊ณผ๊ฐ€ ํฌ๋งท๋˜์–ด AI ์ฑ„ํŒ…์œผ๋กœ ๋ฐ˜ํ™˜

    6. AI๊ฐ€ ๋ถ„์„ ๋ฐ ํ†ต์ฐฐ๋ ฅ์„ ์ œ๊ณต

    ์‹œ๋‚˜๋ฆฌ์˜ค 2: ์˜๋ฏธ ๊ฒ€์ƒ‰์„ ํ†ตํ•œ ์ œํ’ˆ ๋ฐœ๊ฒฌ

    ์‚ฌ์šฉ์ž: Mike, ์žฌ๊ณ  ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ๊ณ ๊ฐ ์š”์ฒญ๊ณผ ์œ ์‚ฌํ•œ ์ œํ’ˆ ์ฐพ๊ธฐ

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "์•ผ์™ธ์šฉ ๋ฐฉ์ˆ˜ ์ „๊ธฐ ์ปค๋„ฅํ„ฐ์™€ ์œ ์‚ฌํ•œ ์ œํ’ˆ์„ ์šฐ๋ฆฌ๊ฐ€ ํŒ๋งคํ•˜๋‚˜์š”?"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. ์ฟผ๋ฆฌ๊ฐ€ ์˜๋ฏธ ๊ฒ€์ƒ‰ ๋„๊ตฌ์— ์˜ํ•ด ์ฒ˜๋ฆฌ๋จ

    2. Azure OpenAI๊ฐ€ ์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ๋ฅผ ์ƒ์„ฑ

    3. pgvector๊ฐ€ ์œ ์‚ฌ์„ฑ ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰

    4. ๊ด€๋ จ ์ œํ’ˆ์ด ๊ด€๋ จ์„ฑ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋จ

    5. ๊ฒฐ๊ณผ์— ์ œํ’ˆ ์„ธ๋ถ€ ์ •๋ณด์™€ ๊ฐ€์šฉ์„ฑ์ด ํฌํ•จ๋จ

    6. AI๊ฐ€ ๋Œ€์•ˆ ๋ฐ ๋ฒˆ๋“ค๋ง ๊ธฐํšŒ๋ฅผ ์ œ์•ˆ

    ์‹œ๋‚˜๋ฆฌ์˜ค 3: ๋งค์žฅ ๊ฐ„ ๋ถ„์„

    ์‚ฌ์šฉ์ž: Jennifer, ์ง€์—ญ ๊ด€๋ฆฌ์ž

    ๋ชฉํ‘œ: ๋ชจ๋“  ๋งค์žฅ์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํŒ๋งค ๋น„๊ต

    ์ž์—ฐ์–ด ์ฟผ๋ฆฌ:

    > "์ง€๋‚œ 6๊ฐœ์›” ๋™์•ˆ ๋ชจ๋“  ๋งค์žฅ์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ํŒ๋งค๋ฅผ ๋น„๊ตํ•ด์ค˜"

    ์ง„ํ–‰ ๊ณผ์ •:

    1. RLS ์ปจํ…์ŠคํŠธ๊ฐ€ ์ง€์—ญ ๊ด€๋ฆฌ์ž ์•ก์„ธ์Šค๋กœ ์„ค์ •๋จ

    2. ๋ณต์žกํ•œ ๋‹ค์ค‘ ๋งค์žฅ ์ฟผ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋จ

    3. ๋ฐ์ดํ„ฐ๊ฐ€ ๋งค์žฅ ์œ„์น˜๋ณ„๋กœ ์ง‘๊ณ„๋จ

    4. ๊ฒฐ๊ณผ์— ํŠธ๋ Œ๋“œ์™€ ๋น„๊ต๊ฐ€ ํฌํ•จ๋จ

    5. AI๊ฐ€ ํ†ต์ฐฐ๋ ฅ๊ณผ ์ถ”์ฒœ์„ ์‹๋ณ„

    ๐Ÿ”’ ๋ณด์•ˆ ๋ฐ ๋ฉ€ํ‹ฐ ํ…Œ๋„Œ์‹œ ์‹ฌ์ธต ๋ถ„์„

    ์šฐ๋ฆฌ์˜ ๊ตฌํ˜„์€ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ๊ธ‰ ๋ณด์•ˆ์„ ์šฐ์„ ์‹œํ•ฉ๋‹ˆ๋‹ค:

    Row Level Security (RLS)

    PostgreSQL RLS๋Š” ๋ฐ์ดํ„ฐ ๊ฒฉ๋ฆฌ๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค:

    
    -- Store managers see only their store's data
    
    CREATE POLICY store_manager_policy ON retail.orders
    
      FOR ALL TO store_managers
    
      USING (store_id = get_current_user_store());
    
    
    
    -- Regional managers see multiple stores
    
    CREATE POLICY regional_manager_policy ON retail.orders
    
      FOR ALL TO regional_managers
    
      USING (store_id = ANY(get_user_store_list()));
    
    

    ์‚ฌ์šฉ์ž ์‹ ์› ๊ด€๋ฆฌ

    ๊ฐ MCP ์—ฐ๊ฒฐ์—๋Š” ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค:

  • ๋งค์žฅ ๊ด€๋ฆฌ์ž ID: RLS ์ปจํ…์ŠคํŠธ๋ฅผ ์œ„ํ•œ ๊ณ ์œ  ์‹๋ณ„์ž
  • ์—ญํ•  ํ• ๋‹น: ๊ถŒํ•œ ๋ฐ ์•ก์„ธ์Šค ์ˆ˜์ค€
  • ์„ธ์…˜ ๊ด€๋ฆฌ: ์•ˆ์ „ํ•œ ์ธ์ฆ ํ† ํฐ
  • ๊ฐ์‚ฌ ๋กœ๊น…: ์™„์ „ํ•œ ์•ก์„ธ์Šค ๊ธฐ๋ก
  • ๋ฐ์ดํ„ฐ ๋ณดํ˜ธ

    ๋‹ค์ค‘ ๋ณด์•ˆ ๊ณ„์ธต:

  • ์—ฐ๊ฒฐ ์•”ํ˜ธํ™”: ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ์— TLS ์‚ฌ์šฉ
  • SQL ์ธ์ ์…˜ ๋ฐฉ์ง€: ๋งค๊ฐœ๋ณ€์ˆ˜ํ™”๋œ ์ฟผ๋ฆฌ๋งŒ ํ—ˆ์šฉ
  • ์ž…๋ ฅ ๊ฒ€์ฆ: ํฌ๊ด„์ ์ธ ์š”์ฒญ ๊ฒ€์ฆ
  • ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ: ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์— ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ ํฌํ•จ ๊ธˆ์ง€
  • ๐ŸŽฏ ์ฃผ์š” ์š”์ 

    ์ด ์†Œ๊ฐœ๋ฅผ ์™„๋ฃŒํ•œ ํ›„ ๋‹ค์Œ์„ ์ดํ•ดํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค:

    โœ… MCP ๊ฐ€์น˜ ์ œ์•ˆ: MCP๊ฐ€ AI ์–ด์‹œ์Šคํ„ดํŠธ์™€ ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ๋ฐฉ๋ฒ•

    โœ… ๋น„์ฆˆ๋‹ˆ์Šค ๋ฐฐ๊ฒฝ: Zava Retail์˜ ์š”๊ตฌ ์‚ฌํ•ญ๊ณผ ๊ณผ์ œ

    โœ… ์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š”: ์ฃผ์š” ๊ตฌ์„ฑ ์š”์†Œ์™€ ์ƒํ˜ธ์ž‘์šฉ

    โœ… ๊ธฐ์ˆ  ์Šคํƒ: ์‚ฌ์šฉ๋œ ๋„๊ตฌ์™€ ํ”„๋ ˆ์ž„์›Œํฌ

    โœ… ๋ณด์•ˆ ๋ชจ๋ธ: ๋ฉ€ํ‹ฐ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์•ก์„ธ์Šค ๋ฐ ๋ณดํ˜ธ

    โœ… ์‚ฌ์šฉ ํŒจํ„ด: ์‹ค์ œ ์ฟผ๋ฆฌ ์‹œ๋‚˜๋ฆฌ์˜ค์™€ ์›Œํฌํ”Œ๋กœ

    ๐Ÿš€ ๋‹ค์Œ ๋‹จ๊ณ„

    ๋” ๊นŠ์ด ํƒ๊ตฌํ•  ์ค€๋น„๊ฐ€ ๋˜์…จ๋‚˜์š”? ๋‹ค์Œ์„ ์ง„ํ–‰ํ•˜์„ธ์š”:

    Lab 01: ํ•ต์‹ฌ ์•„ํ‚คํ…์ฒ˜ ๊ฐœ๋…

    MCP ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜ ํŒจํ„ด, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„ ์›์น™, ์†Œ๋งค ๋ถ„์„ ์†”๋ฃจ์…˜์„ ์ง€์›ํ•˜๋Š” ์ƒ์„ธ ๊ธฐ์ˆ  ๊ตฌํ˜„์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์„ธ์š”.

    ๐Ÿ“š ์ถ”๊ฐ€ ์ž๋ฃŒ

    MCP ๋ฌธ์„œ

  • MCP ์‚ฌ์–‘ - ๊ณต์‹ ํ”„๋กœํ† ์ฝœ ๋ฌธ์„œ
  • MCP ์ดˆ๋ณด์ž์šฉ - ํฌ๊ด„์ ์ธ MCP ํ•™์Šต ๊ฐ€์ด๋“œ
  • FastMCP ๋ฌธ์„œ - Python SDK ๋ฌธ์„œ
  • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ

  • PostgreSQL ๋ฌธ์„œ - PostgreSQL ์ฐธ์กฐ ์ž๋ฃŒ
  • pgvector ๊ฐ€์ด๋“œ - ๋ฒกํ„ฐ ํ™•์žฅ ๋ฌธ์„œ
  • Row Level Security - PostgreSQL RLS ๊ฐ€์ด๋“œ
  • Azure ์„œ๋น„์Šค

  • Azure OpenAI ๋ฌธ์„œ - AI ์„œ๋น„์Šค ํ†ตํ•ฉ
  • Azure Database for PostgreSQL - ๊ด€๋ฆฌํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„œ๋น„์Šค
  • Azure Container Apps - ์„œ๋ฒ„๋ฆฌ์Šค ์ปจํ…Œ์ด๋„ˆ
  • ---

    ๋ฉด์ฑ… ์กฐํ•ญ: ์ด๋Š” ๊ฐ€์ƒ์˜ ์†Œ๋งค ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ•™์Šต ์—ฐ์Šต์ž…๋‹ˆ๋‹ค. ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์—์„œ ์œ ์‚ฌํ•œ ์†”๋ฃจ์…˜์„ ๊ตฌํ˜„ํ•  ๋•Œ๋Š” ํ•ญ์ƒ ์กฐ์ง์˜ ๋ฐ์ดํ„ฐ ๊ฑฐ๋ฒ„๋„Œ์Šค ๋ฐ ๋ณด์•ˆ ์ •์ฑ…์„ ๋”ฐ๋ฅด์‹ญ์‹œ์˜ค.

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ์ตœ์„ ์„ ๋‹คํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์ด ํฌํ•จ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด ๋ฒ„์ „์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ, ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜์„ธ์š”

    ---

    *๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ์„ ํ†ตํ•œ ํ”„๋กœ๋•์…˜๊ธ‰ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•์„ ์ด ํฌ๊ด„์ ์ด๊ณ  ์‹ค์Šต ์ค‘์‹ฌ์˜ ํ•™์Šต ๊ฒฝํ—˜์—์„œ ๋งˆ์Šคํ„ฐํ•˜์„ธ์š”.*

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์œผ๋‚˜, ์ž๋™ ๋ฒˆ์—ญ์€ ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•์„ฑ์„ ํฌํ•จํ•  ์ˆ˜ ์žˆ์Œ์„ ์•Œ๋ ค๋“œ๋ฆฝ๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ์˜ ์›์–ด๊ฐ€ ๊ถŒ์œ„ ์žˆ๋Š” ์ถœ์ฒ˜๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ์ด ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์˜คํ•ด๋‚˜ ์˜ค์—ญ์— ๋Œ€ํ•ด ๋‹น์‚ฌ๋Š” ์ฑ…์ž„์„ ์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

    school ์Šคํ„ฐ๋”” ๊ฐ€์ด๋“œ

    ์Šคํ„ฐ๋”” ๊ฐ€์ด๋“œ

    ์ดˆ๋ณด์ž๋ฅผ ์œ„ํ•œ ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ํ”„๋กœํ† ์ฝœ(MCP) - ํ•™์Šต ๊ฐ€์ด๋“œ

    ์ด ํ•™์Šต ๊ฐ€์ด๋“œ๋Š” "์ดˆ๋ณด์ž๋ฅผ ์œ„ํ•œ ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ํ”„๋กœํ† ์ฝœ(MCP)" ์ปค๋ฆฌํ˜๋Ÿผ์˜ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌ์กฐ ๋ฐ ๋‚ด์šฉ์„ ๊ฐœ์š”ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ฐ€์ด๋“œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ํƒ์ƒ‰ํ•˜๊ณ  ์ด์šฉ ๊ฐ€๋Šฅํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ตœ๋Œ€ํ•œ ํ™œ์šฉํ•˜์„ธ์š”.

    ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ฐœ์š”

    ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ํ”„๋กœํ† ์ฝœ(MCP)์€ AI ๋ชจ๋ธ๊ณผ ํด๋ผ์ด์–ธํŠธ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ์„ ์œ„ํ•œ ํ‘œ์ค€ํ™”๋œ ํ”„๋ ˆ์ž„์›Œํฌ์ž…๋‹ˆ๋‹ค.

    ์ฒ˜์Œ์—๋Š” Anthropic์—์„œ ๋งŒ๋“ค์–ด์กŒ์œผ๋ฉฐ, ํ˜„์žฌ๋Š” ๊ณต์‹ GitHub ์กฐ์ง์„ ํ†ตํ•ด ๋„“์€ MCP ์ปค๋ฎค๋‹ˆํ‹ฐ๊ฐ€ ์œ ์ง€ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ด ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” C#, Java, JavaScript, Python, TypeScript์˜ ์‹ค์Šต ์ฝ”๋“œ ์˜ˆ์ œ์™€ ํ•จ๊ป˜ AI ๊ฐœ๋ฐœ์ž, ์‹œ์Šคํ…œ ์•„ํ‚คํ…ํŠธ, ์†Œํ”„ํŠธ์›จ์–ด ์—”์ง€๋‹ˆ์–ด๋ฅผ ์œ„ํ•ด ์„ค๊ณ„๋œ ํฌ๊ด„์ ์ธ ์ปค๋ฆฌํ˜๋Ÿผ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

    ์‹œ๊ฐ์  ์ปค๋ฆฌํ˜๋Ÿผ ์ง€๋„

    
    mindmap
    
      root((์ดˆ๋ณด์ž๋ฅผ ์œ„ํ•œ MCP))
    
        00. ์†Œ๊ฐœ
    
          ::icon(fa fa-book)
    
          (ํ”„๋กœํ† ์ฝœ ๊ฐœ์š”)
    
          (ํ‘œ์ค€ํ™”์˜ ์žฅ์ )
    
          (์‹ค์ œ ์‚ฌ์šฉ ์‚ฌ๋ก€)
    
          (AI ํ†ตํ•ฉ ๊ธฐ๋ณธ)
    
        01. ํ•ต์‹ฌ ๊ฐœ๋…
    
          ::icon(fa fa-puzzle-piece)
    
          (ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜)
    
          (ํ”„๋กœํ† ์ฝœ ๊ตฌ์„ฑ ์š”์†Œ)
    
          (๋ฉ”์‹œ์ง• ํŒจํ„ด)
    
          (์ „์†ก ๋ฉ”์ปค๋‹ˆ์ฆ˜)
    
          (์ž‘์—… - ์‹คํ—˜์ )
    
          (๋„๊ตฌ ์ฃผ์„)
    
        02. ๋ณด์•ˆ
    
          ::icon(fa fa-shield)
    
          (AI ํŠน์ • ์œ„ํ˜‘)
    
          (2025 ๋ชจ๋ฒ” ์‚ฌ๋ก€)
    
          (Azure ์ฝ˜ํ…์ธ  ์•ˆ์ „)
    
          (์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ)
    
          (Microsoft ํ”„๋กฌํ”„ํŠธ ๋ณดํ˜ธ)
    
          (OWASP MCP ์ƒ์œ„ 10)
    
          (Sherpa ๋ณด์•ˆ ์›Œํฌ์ˆ)
    
        03. ์‹œ์ž‘ํ•˜๊ธฐ
    
          ::icon(fa fa-rocket)
    
          (์ฒซ ๋ฒˆ์งธ ์„œ๋ฒ„ ๊ตฌํ˜„)
    
          (ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ)
    
          (LLM ํด๋ผ์ด์–ธํŠธ ํ†ตํ•ฉ)
    
          (VS ์ฝ”๋“œ ํ™•์žฅ)
    
          (SSE ์„œ๋ฒ„ ์„ค์ •)
    
          (HTTP ์ŠคํŠธ๋ฆฌ๋ฐ)
    
          (AI ๋„๊ตฌ ํ†ตํ•ฉ)
    
          (ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ)
    
          (๊ณ ๊ธ‰ ์„œ๋ฒ„ ์‚ฌ์šฉ๋ฒ•)
    
          (๊ฐ„๋‹จํ•œ ์ธ์ฆ)
    
          (๋ฐฐํฌ ์ „๋žต)
    
          (MCP ํ˜ธ์ŠคํŠธ ์„ค์ •)
    
          (MCP ๊ฒ€์‚ฌ๊ธฐ)
    
        04. ์‹ค์šฉ ๊ตฌํ˜„
    
          ::icon(fa fa-code)
    
          (๋‹ค์ค‘ ์–ธ์–ด SDK)
    
          (ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…)
    
          (ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ)
    
          (์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ)
    
          (ํ”„๋กœ๋•์…˜ ํŒจํ„ด)
    
          (ํŽ˜์ด์ง€๋„ค์ด์…˜ ์ „๋žต)
    
        05. ๊ณ ๊ธ‰ ์ฃผ์ œ
    
          ::icon(fa fa-graduation-cap)
    
          (์ปจํ…์ŠคํŠธ ์—”์ง€๋‹ˆ์–ด๋ง)
    
          (Foundry ์—์ด์ „ํŠธ ํ†ตํ•ฉ)
    
          (๋‹ค์ค‘ ๋ชจ๋“œ AI ์›Œํฌํ”Œ๋กœ์šฐ)
    
          (OAuth2 ์ธ์ฆ)
    
          (์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰)
    
          (์ŠคํŠธ๋ฆฌ๋ฐ ํ”„๋กœํ† ์ฝœ)
    
          (๋ฃจํŠธ ์ปจํ…์ŠคํŠธ)
    
          (๋ผ์šฐํŒ… ์ „๋žต)
    
          (์ƒ˜ํ”Œ๋ง ๊ธฐ๋ฒ•)
    
          (ํ™•์žฅ ์†”๋ฃจ์…˜)
    
          (๋ณด์•ˆ ๊ฐ•ํ™”)
    
          (Entra ID ํ†ตํ•ฉ)
    
          (์›น ๊ฒ€์ƒ‰ MCP)
    
          (ํ”„๋กœํ† ์ฝœ ๊ธฐ๋Šฅ ์‹ฌ์ธต ๋ถ„์„)
    
          (์ ๋Œ€์  ๋‹ค์ค‘ ์—์ด์ „ํŠธ ์ถ”๋ก )
    
          
    
        06. ์ปค๋ฎค๋‹ˆํ‹ฐ
    
          ::icon(fa fa-users)
    
          (์ฝ”๋“œ ๊ธฐ์—ฌ)
    
          (๋ฌธ์„œํ™”)
    
          (MCP ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ๊ณ„)
    
          (MCP ์„œ๋ฒ„ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ)
    
          (์ด๋ฏธ์ง€ ์ƒ์„ฑ ๋„๊ตฌ)
    
          (GitHub ํ˜‘์—…)
    
        07. ์ดˆ๊ธฐ ์ฑ„ํƒ
    
          ::icon(fa fa-lightbulb)
    
          (ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ)
    
          (Microsoft MCP ์„œ๋ฒ„)
    
          (Azure MCP ์„œ๋น„์Šค)
    
          (๊ธฐ์—… ์‚ฌ๋ก€ ์—ฐ๊ตฌ)
    
          (๋ฏธ๋ž˜ ๋กœ๋“œ๋งต)
    
        08. ๋ชจ๋ฒ” ์‚ฌ๋ก€
    
          ::icon(fa fa-check)
    
          (์„ฑ๋Šฅ ์ตœ์ ํ™”)
    
          (์žฅ์•  ํ—ˆ์šฉ)
    
          (์‹œ์Šคํ…œ ๋ณต์›๋ ฅ)
    
          (๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๊ฐ€์‹œ์„ฑ)
    
        09. ์‚ฌ๋ก€ ์—ฐ๊ตฌ
    
          ::icon(fa fa-file-text)
    
          (Azure API ๊ด€๋ฆฌ)
    
          (AI ์—ฌํ–‰์‚ฌ)
    
          (Azure DevOps ํ†ตํ•ฉ)
    
          (๋ฌธ์„œํ™” MCP)
    
          (GitHub MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ)
    
          (VS ์ฝ”๋“œ ํ†ตํ•ฉ)
    
          (์‹ค์ œ ๊ตฌํ˜„)
    
        10. ์‹ค์Šต ์›Œํฌ์ˆ
    
          ::icon(fa fa-laptop)
    
          (MCP ์„œ๋ฒ„ ๊ธฐ๋ณธ)
    
          (๊ณ ๊ธ‰ ๊ฐœ๋ฐœ)
    
          (AI ๋„๊ตฌ ํ†ตํ•ฉ)
    
          (ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ)
    
          (4๊ฐœ ์‹ค์Šต ๊ตฌ์กฐ)
    
        11. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ์‹ค์Šต
    
          ::icon(fa fa-database)
    
          (PostgreSQL ํ†ตํ•ฉ)
    
          (๋ฆฌํ…Œ์ผ ๋ถ„์„ ์‚ฌ์šฉ ์‚ฌ๋ก€)
    
          (ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ)
    
          (์‹œ๋งจํ‹ฑ ๊ฒ€์ƒ‰)
    
          (ํ”„๋กœ๋•์…˜ ๋ฐฐํฌ)
    
          (13๊ฐœ ์‹ค์Šต ๊ตฌ์กฐ)
    
          (์‹ค์Šต ํ•™์Šต)
    
    

    ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๊ตฌ์กฐ

    ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” MCP์˜ ๋‹ค์–‘ํ•œ ์ธก๋ฉด์— ์ง‘์ค‘ํ•˜๋Š” ์—ดํ•œ ๊ฐœ ์ฃผ์š” ์„น์…˜์œผ๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

    1. ์†Œ๊ฐœ (00-Introduction/)

    - ๋ชจ๋ธ ์ปจํ…์ŠคํŠธ ํ”„๋กœํ† ์ฝœ ๊ฐœ์š”

    - AI ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ํ‘œ์ค€ํ™”๊ฐ€ ์ค‘์š”ํ•œ ์ด์œ 

    - ์‹ค์šฉ์ ์ธ ์‚ฌ์šฉ ์‚ฌ๋ก€์™€ ์žฅ์ 

    2. ํ•ต์‹ฌ ๊ฐœ๋… (01-CoreConcepts/)

    - ํด๋ผ์ด์–ธํŠธ-์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜

    - ์ฃผ์š” ํ”„๋กœํ† ์ฝœ ๊ตฌ์„ฑ ์š”์†Œ

    - MCP์˜ ๋ฉ”์‹œ์ง• ํŒจํ„ด

    3. ๋ณด์•ˆ (02-Security/)

    - MCP ๊ธฐ๋ฐ˜ ์‹œ์Šคํ…œ์˜ ๋ณด์•ˆ ์œ„ํ˜‘

    - ๊ตฌํ˜„ ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€

    - ์ธ์ฆ ๋ฐ ๊ถŒํ•œ ๋ถ€์—ฌ ์ „๋žต

    - ํฌ๊ด„์ ์ธ ๋ณด์•ˆ ๋ฌธ์„œ:

    - MCP ๋ณด์•ˆ ๋ชจ๋ฒ” ์‚ฌ๋ก€ 2025

    - Azure ์ฝ˜ํ…์ธ  ์•ˆ์ „ ๊ตฌํ˜„ ๊ฐ€์ด๋“œ

    - MCP ๋ณด์•ˆ ์ œ์–ด ๋ฐ ๊ธฐ๋ฒ•

    - MCP ๋ชจ๋ฒ” ์‚ฌ๋ก€ ๋น ๋ฅธ ์ฐธ์กฐ

    - ์ฃผ์š” ๋ณด์•ˆ ์ฃผ์ œ:

    - ํ”„๋กฌํ”„ํŠธ ์ฃผ์ž… ๋ฐ ๋„๊ตฌ ์ค‘๋… ๊ณต๊ฒฉ

    - ์„ธ์…˜ ํƒˆ์ทจ์™€ ํ˜ผ๋ž€์Šค๋Ÿฌ์šด ๋Œ€๋ฆฌ์ธ ๋ฌธ์ œ

    - ํ† ํฐ ์ „๋‹ฌ ์ทจ์•ฝ์ 

    - ๊ณผ๋„ํ•œ ๊ถŒํ•œ ๋ฐ ์ ‘๊ทผ ์ œ์–ด

    - AI ๊ตฌ์„ฑ์š”์†Œ ๊ณต๊ธ‰๋ง ๋ณด์•ˆ

    - Microsoft ํ”„๋กฌํ”„ํŠธ ์‹ค๋“œ ํ†ตํ•ฉ

    4. ์‹œ์ž‘ํ•˜๊ธฐ (03-GettingStarted/)

    - ํ™˜๊ฒฝ ์„ค์ • ๋ฐ ๊ตฌ์„ฑ

    - ๊ธฐ๋ณธ MCP ์„œ๋ฒ„ ๋ฐ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ

    - ๊ธฐ์กด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ†ตํ•ฉ

    - ๋‹ค์Œ ์„น์…˜ ํฌํ•จ:

    - ์ฒซ ์„œ๋ฒ„ ๊ตฌํ˜„

    - ํด๋ผ์ด์–ธํŠธ ๊ฐœ๋ฐœ

    - LLM ํด๋ผ์ด์–ธํŠธ ํ†ตํ•ฉ

    - VS Code ์—ฐ๋™

    - ์„œ๋ฒ„-๋ฐœ์‹  ์ด๋ฒคํŠธ(SSE) ์„œ๋ฒ„

    - ๊ณ ๊ธ‰ ์„œ๋ฒ„ ์‚ฌ์šฉ๋ฒ•

    - HTTP ์ŠคํŠธ๋ฆฌ๋ฐ

    - AI ํˆดํ‚ท ํ†ตํ•ฉ

    - ํ…Œ์ŠคํŠธ ์ „๋žต

    - ๋ฐฐํฌ ๊ฐ€์ด๋“œ๋ผ์ธ

    5. ์‹ค์ „ ๊ตฌํ˜„ (04-PracticalImplementation/)

    - ๋‹ค์–‘ํ•œ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์šฉ SDK ์‚ฌ์šฉ๋ฒ•

    - ๋””๋ฒ„๊น…, ํ…Œ์ŠคํŠธ, ๊ฒ€์ฆ ๊ธฐ๋ฒ•

    - ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ”„๋กฌํ”„ํŠธ ํ…œํ”Œ๋ฆฟ ๋ฐ ์›Œํฌํ”Œ๋กœ์šฐ ์ œ์ž‘

    - ๊ตฌํ˜„ ์˜ˆ์ œ์™€ ์ƒ˜ํ”Œ ํ”„๋กœ์ ํŠธ

    6. ์‹ฌํ™” ์ฃผ์ œ (05-AdvancedTopics/)

    - ์ปจํ…์ŠคํŠธ ์—”์ง€๋‹ˆ์–ด๋ง ๊ธฐ๋ฒ•

    - Foundry ์—์ด์ „ํŠธ ํ†ตํ•ฉ

    - ๋ฉ€ํ‹ฐ๋ชจ๋‹ฌ AI ์›Œํฌํ”Œ๋กœ์šฐ

    - OAuth2 ์ธ์ฆ ๋ฐ๋ชจ

    - ์‹ค์‹œ๊ฐ„ ๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ

    - ์‹ค์‹œ๊ฐ„ ์ŠคํŠธ๋ฆฌ๋ฐ

    - ๋ฃจํŠธ ์ปจํ…์ŠคํŠธ ๊ตฌํ˜„

    - ๋ผ์šฐํŒ… ์ „๋žต

    - ์ƒ˜ํ”Œ๋ง ๊ธฐ๋ฒ•

    - ํ™•์žฅ ๋ฐฉ๋ฒ•๋ก 

    - ๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

    - Entra ID ๋ณด์•ˆ ํ†ตํ•ฉ

    - ์›น ๊ฒ€์ƒ‰ ํ†ตํ•ฉ

    - ๊ณต๊ฒฉ์  ๋ฉ€ํ‹ฐ ์—์ด์ „ํŠธ ์ถ”๋ก  (ํ† ๋ก  ํŒจํ„ด)

    7. ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ์—ฌ (06-CommunityContributions/)

    - ์ฝ”๋“œ ๋ฐ ๋ฌธ์„œ ๊ธฐ์—ฌ ๋ฐฉ๋ฒ•

    - GitHub๋ฅผ ํ†ตํ•œ ํ˜‘์—…

    - ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฃผ๋„ ๊ฐœ์„  ๋ฐ ํ”ผ๋“œ๋ฐฑ

    - ๋‹ค์–‘ํ•œ MCP ํด๋ผ์ด์–ธํŠธ ์‚ฌ์šฉ๋ฒ• (Claude Desktop, Cline, VSCode)

    - ์ด๋ฏธ์ง€ ์ƒ์„ฑ ํฌํ•จ ์ธ๊ธฐ MCP ์„œ๋ฒ„ ์ž‘์—…๋ฒ•

    8. ์ดˆ๊ธฐ ๋„์ž… ์‚ฌ๋ก€ (07-LessonsfromEarlyAdoption/)

    - ์‹ค์ œ ๊ตฌํ˜„๊ณผ ์„ฑ๊ณต ์‚ฌ๋ก€

    - MCP ๊ธฐ๋ฐ˜ ์†”๋ฃจ์…˜ ๊ตฌ์ถ• ๋ฐ ๋ฐฐํฌ

    - ํŠธ๋ Œ๋“œ ๋ฐ ํ–ฅํ›„ ๋กœ๋“œ๋งต

    - Microsoft MCP ์„œ๋ฒ„ ๊ฐ€์ด๋“œ: 10๊ฐœ ์ด์ƒ์˜ ์ƒ์‚ฐ ์ค€๋น„๋œ Microsoft MCP ์„œ๋ฒ„ ์ข…ํ•ฉ ์•ˆ๋‚ด:

    - Microsoft Learn Docs MCP ์„œ๋ฒ„

    - Azure MCP ์„œ๋ฒ„ (15๊ฐœ ์ด์ƒ ์ „๋ฌธ ์ปค๋„ฅํ„ฐ)

    - GitHub MCP ์„œ๋ฒ„

    - Azure DevOps MCP ์„œ๋ฒ„

    - MarkItDown MCP ์„œ๋ฒ„

    - SQL Server MCP ์„œ๋ฒ„

    - Playwright MCP ์„œ๋ฒ„

    - Dev Box MCP ์„œ๋ฒ„

    - Azure AI Foundry MCP ์„œ๋ฒ„

    - Microsoft 365 Agents Toolkit MCP ์„œ๋ฒ„

    9. ๋ชจ๋ฒ” ์‚ฌ๋ก€ (08-BestPractices/)

    - ์„ฑ๋Šฅ ํŠœ๋‹ ๋ฐ ์ตœ์ ํ™”

    - ๋‚ด๊ฒฐํ•จ์„ฑ MCP ์‹œ์Šคํ…œ ์„ค๊ณ„

    - ํ…Œ์ŠคํŠธ ๋ฐ ๋ณต์›๋ ฅ ์ „๋žต

    10. ์‚ฌ๋ก€ ์—ฐ๊ตฌ (09-CaseStudy/)

    - 7๊ฐ€์ง€ ํฌ๊ด„์ ์ธ ์‚ฌ๋ก€ ์—ฐ๊ตฌ๋ฅผ ํ†ตํ•ด MCP์˜ ๋‹ค์žฌ๋‹ค๋Šฅ์„ฑ ์‹œ์—ฐ:

    - Azure AI ์—ฌํ–‰ ์—์ด์ „ํŠธ: Azure OpenAI ๋ฐ AI ๊ฒ€์ƒ‰์„ ํ™œ์šฉํ•œ ๋‹ค์ค‘ ์—์ด์ „ํŠธ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜

    - Azure DevOps ํ†ตํ•ฉ: YouTube ๋ฐ์ดํ„ฐ ์ž๋™ ์—…๋ฐ์ดํŠธ ์›Œํฌํ”Œ๋กœ์šฐ ์ž๋™ํ™”

    - ์‹ค์‹œ๊ฐ„ ๋ฌธ์„œ ๊ฒ€์ƒ‰: Python ์ฝ˜์†” ํด๋ผ์ด์–ธํŠธ์™€ HTTP ์ŠคํŠธ๋ฆฌ๋ฐ

    - ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ ํ•™์Šต ๊ณ„ํš ์ƒ์„ฑ๊ธฐ: Chainlit ์›น ์•ฑ๊ณผ ๋Œ€ํ™”ํ˜• AI

    - ํŽธ์ง‘๊ธฐ ๋‚ด ๋ฌธ์„œํ™”: VS Code์™€ GitHub Copilot ์›Œํฌํ”Œ๋กœ์šฐ ํ†ตํ•ฉ

    - Azure API ๊ด€๋ฆฌ: ๊ธฐ์—… API ํ†ตํ•ฉ๊ณผ MCP ์„œ๋ฒ„ ์ƒ์„ฑ

    - GitHub MCP ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ: ์ƒํƒœ๊ณ„ ๊ฐœ๋ฐœ ๋ฐ ์—์ด์ „ํŠธ ํ†ตํ•ฉ ํ”Œ๋žซํผ

    - ๊ธฐ์—… ํ†ตํ•ฉ, ๊ฐœ๋ฐœ์ž ์ƒ์‚ฐ์„ฑ, ์ƒํƒœ๊ณ„ ๊ฐœ๋ฐœ์„ ์•„์šฐ๋ฅด๋Š” ๊ตฌํ˜„ ์‚ฌ๋ก€

    11. ์‹ค์Šต ์›Œํฌ์ˆ (10-StreamliningAIWorkflowsBuildingAnMCPServerWithAIToolkit/)

    - MCP์™€ AI ํˆดํ‚ท์„ ๊ฒฐํ•ฉํ•œ ํฌ๊ด„์  ์‹ค์Šต ์›Œํฌ์ˆ

    - AI ๋ชจ๋ธ๊ณผ ์‹ค์ œ ๋„๊ตฌ๋ฅผ ์—ฐ๊ฒฐํ•˜๋Š” ์ง€๋Šฅํ˜• ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๊ตฌ์ถ•

    - ๊ธฐ์ดˆ, ๋งž์ถค ์„œ๋ฒ„ ๊ฐœ๋ฐœ, ์ƒ์‚ฐ ๋ฐฐํฌ ์ „๋žต์„ ๋‹ค๋ฃจ๋Š” ์‹ค์šฉ ๋ชจ๋“ˆ

    - ๋žฉ ๊ตฌ์„ฑ:

    - ๋žฉ 1: MCP ์„œ๋ฒ„ ๊ธฐ๋ณธ

    - ๋žฉ 2: ๊ณ ๊ธ‰ MCP ์„œ๋ฒ„ ๊ฐœ๋ฐœ

    - ๋žฉ 3: AI ํˆดํ‚ท ํ†ตํ•ฉ

    - ๋žฉ 4: ์ƒ์‚ฐ ํ™˜๊ฒฝ ๋ฐฐํฌ ๋ฐ ํ™•์žฅ

    - ๋‹จ๊ณ„๋ณ„ ์ง€์นจ์ด ํฌํ•จ๋œ ๋žฉ ๊ธฐ๋ฐ˜ ํ•™์Šต

    12. MCP ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ๋žฉ (11-MCPServerHandsOnLabs/)

    - PostgreSQL ํ†ตํ•ฉ์œผ๋กœ ์ƒ์‚ฐ ํ™˜๊ฒฝ ์ค€๋น„ MCP ์„œ๋ฒ„ ๊ตฌ์ถ•์„ ์œ„ํ•œ ์ด 13๊ฐœ์˜ ๋žฉ ํ•™์Šต ๊ฒฝ๋กœ

    - Zava Retail ์‚ฌ๋ก€๋ฅผ ์‚ฌ์šฉํ•œ ์‹ค์ œ ์†Œ๋งค ๋ถ„์„ ๊ตฌํ˜„

    - ํ–‰ ์ˆ˜์ค€ ๋ณด์•ˆ(RLS), ์˜๋ฏธ ๊ธฐ๋ฐ˜ ๊ฒ€์ƒ‰, ๋‹ค์ค‘ ํ…Œ๋„ŒํŠธ ๋ฐ์ดํ„ฐ ์ ‘๊ทผ ๋“ฑ ์—”ํ„ฐํ”„๋ผ์ด์ฆˆ ํŒจํ„ด ํฌํ•จ

    - ์™„์ „ํ•œ ๋žฉ ๊ตฌ์„ฑ:

    - ๋žฉ 00-03: ๊ธฐ์ดˆ - ์†Œ๊ฐœ, ์•„ํ‚คํ…์ฒ˜, ๋ณด์•ˆ, ํ™˜๊ฒฝ ์„ค์ •

    - ๋žฉ 04-06: MCP ์„œ๋ฒ„ ๊ตฌ์ถ• - ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์„ค๊ณ„, MCP ์„œ๋ฒ„ ๊ตฌํ˜„, ๋„๊ตฌ ๊ฐœ๋ฐœ

    - ๋žฉ 07-09: ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ - ์˜๋ฏธ ๊ฒ€์ƒ‰, ํ…Œ์ŠคํŠธ ๋ฐ ๋””๋ฒ„๊น…, VS Code ํ†ตํ•ฉ

    - ๋žฉ 10-12: ์ƒ์‚ฐ ๋ฐ ๋ชจ๋ฒ” ์‚ฌ๋ก€ - ๋ฐฐํฌ, ๋ชจ๋‹ˆํ„ฐ๋ง, ์ตœ์ ํ™”

    - ์‚ฌ์šฉ ๊ธฐ์ˆ : FastMCP ํ”„๋ ˆ์ž„์›Œํฌ, PostgreSQL, Azure OpenAI, Azure ์ปจํ…Œ์ด๋„ˆ ์•ฑ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ธ์‚ฌ์ดํŠธ

    - ํ•™์Šต ์„ฑ๊ณผ: ์ƒ์‚ฐ ์ค€๋น„๋œ MCP ์„œ๋ฒ„, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ํ†ตํ•ฉ ํŒจํ„ด, AI ๊ธฐ๋ฐ˜ ๋ถ„์„, ๊ธฐ์—… ๋ณด์•ˆ

    ์ถ”๊ฐ€ ๋ฆฌ์†Œ์Šค

    ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์—๋Š” ๋‹ค์Œ์˜ ์ง€์› ๋ฆฌ์†Œ์Šค๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค:

  • ์ด๋ฏธ์ง€ ํด๋”: ์ปค๋ฆฌํ˜๋Ÿผ ์ „๋ฐ˜์— ์‚ฌ์šฉ๋œ ๋‹ค์ด์–ด๊ทธ๋žจ ๋ฐ ์ผ๋Ÿฌ์ŠคํŠธ
  • ๋ฒˆ์—ญ: ๋ฌธ์„œ์˜ ๋‹ค์ค‘ ์–ธ์–ด ์ž๋™ ๋ฒˆ์—ญ ์ง€์›
  • ๊ณต์‹ MCP ๋ฆฌ์†Œ์Šค:
  • - MCP ๋ฌธ์„œ

    - MCP ์‚ฌ์–‘

    - MCP GitHub ๋ฆฌํฌ์ง€ํ† ๋ฆฌ

    ์ด ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์‚ฌ์šฉ ๋ฐฉ๋ฒ•

    1. ์ˆœ์ฐจ์  ํ•™์Šต: ๊ตฌ์กฐํ™”๋œ ํ•™์Šต์„ ์œ„ํ•ด 00๋ถ€ํ„ฐ 11์žฅ๊นŒ์ง€ ์ˆœ์„œ๋Œ€๋กœ ์ง„ํ–‰ํ•˜์„ธ์š”.

    2. ์–ธ์–ด๋ณ„ ์ง‘์ค‘: ์›ํ•˜๋Š” ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์˜ ์ƒ˜ํ”Œ ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ํƒ์ƒ‰ํ•˜์„ธ์š”.

    3. ์‹ค์ „ ๊ตฌํ˜„: "์‹œ์ž‘ํ•˜๊ธฐ" ์„น์…˜์—์„œ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•˜๊ณ  ์ฒซ MCP ์„œ๋ฒ„ ๋ฐ ํด๋ผ์ด์–ธํŠธ๋ฅผ ๋งŒ๋“ค์–ด๋ณด์„ธ์š”.

    4. ์‹ฌํ™” ํƒ๊ตฌ: ๊ธฐ๋ณธ๊ธฐ๋ฅผ ์ตํžŒ ํ›„ ์‹ฌํ™” ์ฃผ์ œ๋กœ ๋„˜์–ด๊ฐ€ ์ง€์‹์„ ํ™•์žฅํ•˜์„ธ์š”.

    5. ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ: GitHub ํ† ๋ก ๊ณผ Discord ์ฑ„๋„์—์„œ MCP ์ปค๋ฎค๋‹ˆํ‹ฐ์— ์ฐธ์—ฌํ•ด ์ „๋ฌธ๊ฐ€ ๋ฐ ๋™๋ฃŒ ๊ฐœ๋ฐœ์ž์™€ ๊ต๋ฅ˜ํ•˜์„ธ์š”.

    MCP ํด๋ผ์ด์–ธํŠธ ๋ฐ ๋„๊ตฌ

    ์ปค๋ฆฌํ˜๋Ÿผ์€ ๋‹ค์–‘ํ•œ MCP ํด๋ผ์ด์–ธํŠธ ๋ฐ ๋„๊ตฌ๋ฅผ ๋‹ค๋ฃน๋‹ˆ๋‹ค:

    1. ๊ณต์‹ ํด๋ผ์ด์–ธํŠธ:

    - Visual Studio Code

    - Visual Studio Code ๋‚ด MCP

    - Claude Desktop

    - VSCode ๋‚ด Claude

    - Claude API

    2. ์ปค๋ฎค๋‹ˆํ‹ฐ ํด๋ผ์ด์–ธํŠธ:

    - Cline (ํ„ฐ๋ฏธ๋„ ๊ธฐ๋ฐ˜)

    - Cursor (์ฝ”๋“œ ์—๋””ํ„ฐ)

    - ChatMCP

    - Windsurf

    3. MCP ๊ด€๋ฆฌ ๋„๊ตฌ:

    - MCP CLI

    - MCP Manager

    - MCP Linker

    - MCP Router

    ์ธ๊ธฐ MCP ์„œ๋ฒ„

    ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” ๋‹ค์–‘ํ•œ MCP ์„œ๋ฒ„๋ฅผ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค:

    1. ๊ณต์‹ Microsoft MCP ์„œ๋ฒ„:

    - Microsoft Learn Docs MCP ์„œ๋ฒ„

    - Azure MCP ์„œ๋ฒ„ (15๊ฐœ ์ด์ƒ ์ „๋ฌธ ์ปค๋„ฅํ„ฐ)

    - GitHub MCP ์„œ๋ฒ„

    - Azure DevOps MCP ์„œ๋ฒ„

    - MarkItDown MCP ์„œ๋ฒ„

    - SQL Server MCP ์„œ๋ฒ„

    - Playwright MCP ์„œ๋ฒ„

    - Dev Box MCP ์„œ๋ฒ„

    - Azure AI Foundry MCP ์„œ๋ฒ„

    - Microsoft 365 Agents Toolkit MCP ์„œ๋ฒ„

    2. ๊ณต์‹ ์ฐธ์กฐ ์„œ๋ฒ„:

    - ํŒŒ์ผ ์‹œ์Šคํ…œ

    - Fetch

    - ๋ฉ”๋ชจ๋ฆฌ

    - ์ˆœ์ฐจ์  ์‚ฌ๊ณ 

    3. ์ด๋ฏธ์ง€ ์ƒ์„ฑ:

    - Azure OpenAI DALL-E 3

    - Stable Diffusion WebUI

    - Replicate

    4. ๊ฐœ๋ฐœ ๋„๊ตฌ:

    - Git MCP

    - ํ„ฐ๋ฏธ๋„ ์ œ์–ด

    - ์ฝ”๋“œ ์–ด์‹œ์Šคํ„ดํŠธ

    5. ํŠนํ™” ์„œ๋ฒ„:

    - Salesforce

    - Microsoft Teams

    - Jira & Confluence

    ๊ธฐ์—ฌํ•˜๊ธฐ

    ์ด ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋Š” ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ์—ฌ๋ฅผ ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค. MCP ์ƒํƒœ๊ณ„์— ํšจ๊ณผ์ ์œผ๋กœ ๊ธฐ์—ฌํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ์ปค๋ฎค๋‹ˆํ‹ฐ ๊ธฐ์—ฌ ์„น์…˜์„ ์ฐธ์กฐํ•˜์„ธ์š”.

    ----

    *์ด ํ•™์Šต ๊ฐ€์ด๋“œ๋Š” 2026๋…„ 2์›” 5์ผ ๋งˆ์ง€๋ง‰ ์—…๋ฐ์ดํŠธ๋˜์—ˆ์œผ๋ฉฐ ์ตœ์‹  MCP ์‚ฌ์–‘ 2025-11-25๋ฅผ ๋ฐ˜์˜ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ๋‚ด์šฉ์€ ์ด ๋‚ ์งœ ์ดํ›„์— ์—…๋ฐ์ดํŠธ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.*

    ---

    ๋ฉด์ฑ… ์กฐํ•ญ:

    ์ด ๋ฌธ์„œ๋Š” AI ๋ฒˆ์—ญ ์„œ๋น„์Šค Co-op Translator๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฒˆ์—ญ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

    ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์ง€๋งŒ, ์ž๋™ ๋ฒˆ์—ญ์—๋Š” ์˜ค๋ฅ˜๋‚˜ ๋ถ€์ •ํ™•ํ•œ ๋ถ€๋ถ„์ด ์žˆ์„ ์ˆ˜ ์žˆ์Œ์„ ์œ ์˜ํ•ด ์ฃผ์‹œ๊ธฐ ๋ฐ”๋ž๋‹ˆ๋‹ค.

    ์›๋ณธ ๋ฌธ์„œ๋Š” ํ•ด๋‹น ์–ธ์–ด๋กœ ๋œ ์›๋ฌธ์ด ๊ถŒ์œ„ ์žˆ๋Š” ์ž๋ฃŒ๋กœ ๊ฐ„์ฃผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ์ค‘์š”ํ•œ ์ •๋ณด์˜ ๊ฒฝ์šฐ ์ „๋ฌธ์ ์ธ ์ธ๊ฐ„ ๋ฒˆ์—ญ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

    ๋ณธ ๋ฒˆ์—ญ ์‚ฌ์šฉ์œผ๋กœ ์ธํ•ด ๋ฐœ์ƒํ•˜๋Š” ์˜คํ•ด๋‚˜ ์ž˜๋ชป๋œ ํ•ด์„์— ๋Œ€ํ•ด์„œ๋Š” ๋‹น์‚ฌ๊ฐ€ ์ฑ…์ž„์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค.